|
|
|
@@ -0,0 +1,598 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
|
|
|
|
import type { SelectOption } from "naive-ui";
|
|
|
|
|
import { Popup } from "@vicons/carbon";
|
|
|
|
|
import { t } from "@/language";
|
|
|
|
|
import { App, Hooks, Utils, DataBindingManager } from "@astral3d/engine";
|
|
|
|
|
import { fetchDataSetPage, fetchExecuteDataSet } from "@/http/api/dataSet";
|
|
|
|
|
import CodeEditor from "@/components/code/CodeEditor.vue";
|
|
|
|
|
import EsInputNumber from "@/components/es/EsInputNumber.vue";
|
|
|
|
|
|
|
|
|
|
type DataSetId = IDataSet.Item["id"];
|
|
|
|
|
|
|
|
|
|
type DataComponentConfig = {
|
|
|
|
|
dataSetId?: DataSetId | null;
|
|
|
|
|
filterEnabled?: boolean;
|
|
|
|
|
filterBody?: string;
|
|
|
|
|
autoRefreshEnabled?: boolean;
|
|
|
|
|
autoRefreshInterval?: number;
|
|
|
|
|
applyToModel?: boolean;
|
|
|
|
|
transition?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const defaultFilterBody = [" // Editable area", " // Example: return data.filter(item => item.value > 100);", " return data;"].join("\n");
|
|
|
|
|
|
|
|
|
|
const defaultConfig = {
|
|
|
|
|
dataSetId: null as DataSetId | null,
|
|
|
|
|
filterEnabled: false,
|
|
|
|
|
filterBody: defaultFilterBody,
|
|
|
|
|
autoRefreshEnabled: false,
|
|
|
|
|
autoRefreshInterval: 5,
|
|
|
|
|
applyToModel: false,
|
|
|
|
|
transition: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isSelectObject3D = ref(false);
|
|
|
|
|
const dataSetOptions = ref<SelectOption[]>([]);
|
|
|
|
|
const dataSetLoading = ref(false);
|
|
|
|
|
const resultSource = ref("");
|
|
|
|
|
const resultLoading = ref(false);
|
|
|
|
|
const fetchError = ref("");
|
|
|
|
|
const filterError = ref("");
|
|
|
|
|
const applyError = ref("");
|
|
|
|
|
const showFilterModal = ref(false);
|
|
|
|
|
const showResultModal = ref(false);
|
|
|
|
|
const resultError = computed(() => [fetchError.value, filterError.value, applyError.value].filter(Boolean).join(" | "));
|
|
|
|
|
const rawResult = ref<unknown>(undefined);
|
|
|
|
|
const processedResult = ref<unknown>(undefined);
|
|
|
|
|
let selectedObject: any = null;
|
|
|
|
|
|
|
|
|
|
const config = reactive({ ...defaultConfig });
|
|
|
|
|
|
|
|
|
|
const filterEditorKey = computed(() => (config.filterEnabled ? "filter-enabled" : "filter-disabled"));
|
|
|
|
|
const filterEditorConfig = computed(() => ({
|
|
|
|
|
readOnly: !config.filterEnabled,
|
|
|
|
|
minimap: { enabled: false },
|
|
|
|
|
renderValidationDecorations: "off",
|
|
|
|
|
automaticLayout: true,
|
|
|
|
|
lineNumbers: "off",
|
|
|
|
|
}));
|
|
|
|
|
const filterModalEditorConfig = computed(() => ({
|
|
|
|
|
readOnly: !config.filterEnabled,
|
|
|
|
|
minimap: { enabled: false },
|
|
|
|
|
renderValidationDecorations: "off",
|
|
|
|
|
automaticLayout: true,
|
|
|
|
|
lineNumbers: "on",
|
|
|
|
|
}));
|
|
|
|
|
const resultEditorConfig = {
|
|
|
|
|
readOnly: true,
|
|
|
|
|
minimap: { enabled: false },
|
|
|
|
|
renderValidationDecorations: "off",
|
|
|
|
|
automaticLayout: true,
|
|
|
|
|
lineNumbers: "off",
|
|
|
|
|
};
|
|
|
|
|
const resultModalEditorConfig = {
|
|
|
|
|
readOnly: true,
|
|
|
|
|
minimap: { enabled: false },
|
|
|
|
|
renderValidationDecorations: "off",
|
|
|
|
|
automaticLayout: true,
|
|
|
|
|
lineNumbers: "on",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let isHydrating = false;
|
|
|
|
|
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
let executeToken = 0;
|
|
|
|
|
|
|
|
|
|
async function loadDataSets() {
|
|
|
|
|
dataSetLoading.value = true;
|
|
|
|
|
const res = await fetchDataSetPage({ page: 1, pageSize: 1000 });
|
|
|
|
|
dataSetLoading.value = false;
|
|
|
|
|
if (res.error) return;
|
|
|
|
|
dataSetOptions.value =
|
|
|
|
|
res.data?.items?.map(item => ({
|
|
|
|
|
label: item.name,
|
|
|
|
|
value: item.id,
|
|
|
|
|
})) || [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearResult() {
|
|
|
|
|
executeToken += 1;
|
|
|
|
|
rawResult.value = undefined;
|
|
|
|
|
processedResult.value = undefined;
|
|
|
|
|
resultSource.value = "";
|
|
|
|
|
resultLoading.value = false;
|
|
|
|
|
fetchError.value = "";
|
|
|
|
|
filterError.value = "";
|
|
|
|
|
applyError.value = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeInterval(value: number | null | undefined) {
|
|
|
|
|
const numeric = Number(value);
|
|
|
|
|
if (!Number.isFinite(numeric)) {
|
|
|
|
|
return defaultConfig.autoRefreshInterval;
|
|
|
|
|
}
|
|
|
|
|
return Math.min(600, Math.max(1, Math.round(numeric)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeResult(value: unknown) {
|
|
|
|
|
if (typeof value !== "string") {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
const trimmed = value.trim();
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(trimmed);
|
|
|
|
|
} catch {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatResult(value: unknown) {
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return JSON.stringify(value, null, 2);
|
|
|
|
|
} catch {
|
|
|
|
|
return String(value ?? "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyFilter(value: unknown) {
|
|
|
|
|
if (!config.filterEnabled) {
|
|
|
|
|
return { value, error: "" };
|
|
|
|
|
}
|
|
|
|
|
const body = (config.filterBody || "").trim();
|
|
|
|
|
if (!body) {
|
|
|
|
|
return { value, error: "" };
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const filterFn = new Function("data", body);
|
|
|
|
|
return { value: filterFn(value), error: "" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = (error as Error).message || t("prompt.Parse failed");
|
|
|
|
|
window.$message?.error(message);
|
|
|
|
|
return { value, error: message };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function writeComponentData(value: unknown, options: { applyToModel?: boolean } = {}) {
|
|
|
|
|
const object = selectedObject;
|
|
|
|
|
if (!object) return;
|
|
|
|
|
|
|
|
|
|
const result = DataBindingManager.setData(object, value, {
|
|
|
|
|
index: 0,
|
|
|
|
|
applyToModel: options.applyToModel,
|
|
|
|
|
transition: config.transition,
|
|
|
|
|
onUpdate: () => Hooks.useDispatchSignal("objectChanged", object),
|
|
|
|
|
});
|
|
|
|
|
if (options.applyToModel && result.errors.length > 0) {
|
|
|
|
|
applyError.value = result.errors.join(" | ");
|
|
|
|
|
} else {
|
|
|
|
|
applyError.value = "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncProcessedData(options: { applyToModel?: boolean } = {}) {
|
|
|
|
|
if (rawResult.value === undefined) {
|
|
|
|
|
processedResult.value = undefined;
|
|
|
|
|
resultSource.value = "";
|
|
|
|
|
filterError.value = "";
|
|
|
|
|
applyError.value = "";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const { value, error } = applyFilter(rawResult.value);
|
|
|
|
|
processedResult.value = value;
|
|
|
|
|
filterError.value = error;
|
|
|
|
|
resultSource.value = formatResult(value);
|
|
|
|
|
writeComponentData(value, { applyToModel: options.applyToModel });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function executeDataSet() {
|
|
|
|
|
if (!config.dataSetId) {
|
|
|
|
|
clearResult();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const dataSetId = config.dataSetId;
|
|
|
|
|
const token = ++executeToken;
|
|
|
|
|
resultLoading.value = true;
|
|
|
|
|
fetchError.value = "";
|
|
|
|
|
filterError.value = "";
|
|
|
|
|
applyError.value = "";
|
|
|
|
|
const res = await fetchExecuteDataSet(dataSetId);
|
|
|
|
|
if (token !== executeToken) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resultLoading.value = false;
|
|
|
|
|
if (res.error) {
|
|
|
|
|
fetchError.value = res.error.msg || t("other['Query failed']");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (dataSetId !== config.dataSetId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rawResult.value = normalizeResult(res.data);
|
|
|
|
|
syncProcessedData({ applyToModel: config.applyToModel });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopAutoRefresh() {
|
|
|
|
|
if (refreshTimer) {
|
|
|
|
|
clearInterval(refreshTimer);
|
|
|
|
|
refreshTimer = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncAutoRefresh() {
|
|
|
|
|
stopAutoRefresh();
|
|
|
|
|
if (!config.autoRefreshEnabled || !config.dataSetId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const interval = normalizeInterval(config.autoRefreshInterval);
|
|
|
|
|
refreshTimer = setInterval(() => {
|
|
|
|
|
executeDataSet();
|
|
|
|
|
}, interval * 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildConfig(): DataComponentConfig {
|
|
|
|
|
return {
|
|
|
|
|
dataSetId: config.dataSetId ?? null,
|
|
|
|
|
filterEnabled: Boolean(config.filterEnabled),
|
|
|
|
|
filterBody: config.filterBody ?? "",
|
|
|
|
|
autoRefreshEnabled: Boolean(config.autoRefreshEnabled),
|
|
|
|
|
autoRefreshInterval: normalizeInterval(config.autoRefreshInterval),
|
|
|
|
|
applyToModel: Boolean(config.applyToModel),
|
|
|
|
|
transition: Number.isFinite(Number(config.transition)) ? Number(config.transition) : defaultConfig.transition,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSameConfig(left: unknown, right: unknown) {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveConfigToObject() {
|
|
|
|
|
const object = selectedObject;
|
|
|
|
|
if (!object) return;
|
|
|
|
|
|
|
|
|
|
const nextConfig = buildConfig();
|
|
|
|
|
const currentList = Array.isArray(object.dataComponent) ? object.dataComponent : [];
|
|
|
|
|
const currentConfig = currentList[0]?.config;
|
|
|
|
|
if (isSameConfig(currentConfig, nextConfig)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
DataBindingManager.setConfig(object, nextConfig, { index: 0 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scheduleSave = Utils.debounce(() => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
saveConfigToObject();
|
|
|
|
|
}, 200);
|
|
|
|
|
|
|
|
|
|
const schedulePreview = Utils.debounce(() => {
|
|
|
|
|
syncProcessedData({ applyToModel: config.applyToModel });
|
|
|
|
|
}, 150);
|
|
|
|
|
|
|
|
|
|
function applyConfigFromObject(object: any) {
|
|
|
|
|
const saved = Array.isArray(object?.dataComponent) ? (object.dataComponent[0]?.config as DataComponentConfig | undefined) : undefined;
|
|
|
|
|
isHydrating = true;
|
|
|
|
|
config.dataSetId = saved?.dataSetId ?? defaultConfig.dataSetId;
|
|
|
|
|
config.filterEnabled = saved?.filterEnabled ?? defaultConfig.filterEnabled;
|
|
|
|
|
config.filterBody = typeof saved?.filterBody === "string" ? saved.filterBody : defaultConfig.filterBody;
|
|
|
|
|
config.autoRefreshEnabled = saved?.autoRefreshEnabled ?? defaultConfig.autoRefreshEnabled;
|
|
|
|
|
config.autoRefreshInterval = normalizeInterval(saved?.autoRefreshInterval ?? defaultConfig.autoRefreshInterval);
|
|
|
|
|
config.applyToModel = saved?.applyToModel ?? defaultConfig.applyToModel;
|
|
|
|
|
config.transition = Number.isFinite(Number(saved?.transition)) ? Number(saved?.transition) : defaultConfig.transition;
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
isHydrating = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSelectObject(object: any) {
|
|
|
|
|
stopAutoRefresh();
|
|
|
|
|
selectedObject = object || null;
|
|
|
|
|
if (object) {
|
|
|
|
|
isSelectObject3D.value = true;
|
|
|
|
|
applyConfigFromObject(object);
|
|
|
|
|
if (config.dataSetId) {
|
|
|
|
|
executeDataSet();
|
|
|
|
|
} else {
|
|
|
|
|
clearResult();
|
|
|
|
|
}
|
|
|
|
|
syncAutoRefresh();
|
|
|
|
|
} else {
|
|
|
|
|
isSelectObject3D.value = false;
|
|
|
|
|
clearResult();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.dataSetId,
|
|
|
|
|
value => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
if (value) {
|
|
|
|
|
executeDataSet();
|
|
|
|
|
} else {
|
|
|
|
|
clearResult();
|
|
|
|
|
stopAutoRefresh();
|
|
|
|
|
}
|
|
|
|
|
syncAutoRefresh();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.filterEnabled,
|
|
|
|
|
() => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
syncProcessedData({ applyToModel: config.applyToModel });
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.filterBody,
|
|
|
|
|
() => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
schedulePreview();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.autoRefreshEnabled,
|
|
|
|
|
value => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
syncAutoRefresh();
|
|
|
|
|
if (value) {
|
|
|
|
|
executeDataSet();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.autoRefreshInterval,
|
|
|
|
|
() => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
syncAutoRefresh();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.transition,
|
|
|
|
|
() => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => config.applyToModel,
|
|
|
|
|
value => {
|
|
|
|
|
if (isHydrating) return;
|
|
|
|
|
scheduleSave();
|
|
|
|
|
if (!value) {
|
|
|
|
|
applyError.value = "";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (processedResult.value !== undefined) {
|
|
|
|
|
writeComponentData(processedResult.value, { applyToModel: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
Hooks.useAddSignal("objectSelected", handleSelectObject);
|
|
|
|
|
loadDataSets();
|
|
|
|
|
handleSelectObject(App.selected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
Hooks.useRemoveSignal("objectSelected", handleSelectObject);
|
|
|
|
|
stopAutoRefresh();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div v-if="isSelectObject3D" class="data-component-panel">
|
|
|
|
|
<div class="sidebar-config-item">
|
|
|
|
|
<span>{{ t("layout.sider.dataComponent.Data set") }}</span>
|
|
|
|
|
<n-select
|
|
|
|
|
v-model:value="config.dataSetId"
|
|
|
|
|
size="small"
|
|
|
|
|
:options="dataSetOptions"
|
|
|
|
|
:loading="dataSetLoading"
|
|
|
|
|
:placeholder="t('layout.sider.dataComponent.Select data set')"
|
|
|
|
|
filterable
|
|
|
|
|
clearable
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<n-divider class="!mt-2 !mb-4" />
|
|
|
|
|
|
|
|
|
|
<div class="sidebar-config-item">
|
|
|
|
|
<span>{{ t("layout.sider.dataComponent.Data filter") }}</span>
|
|
|
|
|
|
|
|
|
|
<div class="flex justify-end gap-2">
|
|
|
|
|
<n-switch v-model:value="config.filterEnabled">
|
|
|
|
|
<template #checked>
|
|
|
|
|
{{ t("layout.sider.dataComponent.Enable filter") }}
|
|
|
|
|
</template>
|
|
|
|
|
<template #unchecked>
|
|
|
|
|
{{ t("layout.sider.dataComponent.Disabled filter") }}
|
|
|
|
|
</template>
|
|
|
|
|
</n-switch>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="filter-editor relative" :class="{ 'is-disabled': !config.filterEnabled }">
|
|
|
|
|
<div class="filter-editor-line">function filter(data) {</div>
|
|
|
|
|
<CodeEditor :key="filterEditorKey" v-model:source="config.filterBody" mode="javascript" :config="filterEditorConfig" class="filter-editor-code" />
|
|
|
|
|
<div class="filter-editor-line">}</div>
|
|
|
|
|
|
|
|
|
|
<n-tooltip trigger="hover">
|
|
|
|
|
<template #trigger>
|
|
|
|
|
<n-button size="small" tertiary @click="showFilterModal = true" class="absolute top-1 right-1">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon><Popup /></n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</template>
|
|
|
|
|
{{ t("layout.sider.dataComponent.Popup editor") }}
|
|
|
|
|
</n-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<n-divider class="!mt-2 !mb-4" />
|
|
|
|
|
|
|
|
|
|
<div class="sidebar-config-item">
|
|
|
|
|
<span>{{ t("layout.sider.dataComponent.Apply to model") }}</span>
|
|
|
|
|
<div class="apply-controls">
|
|
|
|
|
<n-checkbox v-model:checked="config.applyToModel" size="small" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sidebar-config-item">
|
|
|
|
|
<span>{{ t("layout.sider.dataComponent.Auto refresh") }}</span>
|
|
|
|
|
|
|
|
|
|
<div class="flex justify-end gap-2 items-center">
|
|
|
|
|
<n-checkbox v-model:checked="config.autoRefreshEnabled" size="small" />
|
|
|
|
|
<EsInputNumber
|
|
|
|
|
v-model:value="config.autoRefreshInterval"
|
|
|
|
|
size="small"
|
|
|
|
|
:min="1"
|
|
|
|
|
:max="86400"
|
|
|
|
|
:decimal="0"
|
|
|
|
|
:step="1"
|
|
|
|
|
:show-button="false"
|
|
|
|
|
:disabled="!config.autoRefreshEnabled"
|
|
|
|
|
:unit="t('layout.sider.dataComponent.Seconds once')"
|
|
|
|
|
class="!w-60%"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="sidebar-config-item">
|
|
|
|
|
<span>{{ t("layout.sider.dataComponent.Transition") }}</span>
|
|
|
|
|
<div class="flex justify-end items-center">
|
|
|
|
|
<EsInputNumber
|
|
|
|
|
v-model:value="config.transition"
|
|
|
|
|
size="small"
|
|
|
|
|
:min="0"
|
|
|
|
|
:max="600000"
|
|
|
|
|
:step="100"
|
|
|
|
|
:decimal="0"
|
|
|
|
|
:show-button="false"
|
|
|
|
|
:unit="t('layout.sider.dataComponent.Milliseconds')"
|
|
|
|
|
class="!w-60%"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<n-divider class="!mt-2 !mb-4" />
|
|
|
|
|
|
|
|
|
|
<div class="sidebar-config-item">
|
|
|
|
|
<span>{{ t("layout.sider.dataComponent.Data result") }}</span>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-end gap-2">
|
|
|
|
|
<n-text depth="3" v-if="resultLoading">{{ t("other.Loading") }}</n-text>
|
|
|
|
|
|
|
|
|
|
<n-button size="tiny" tertiary :disabled="!config.dataSetId" :loading="resultLoading" @click="executeDataSet">
|
|
|
|
|
{{ t("layout.sider.dataComponent.Manual refresh") }}
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="filter-editor relative">
|
|
|
|
|
<n-alert v-if="resultError" type="error" :title="t('layout.sider.dataComponent.Result error')" class="mb-2">
|
|
|
|
|
{{ resultError }}
|
|
|
|
|
</n-alert>
|
|
|
|
|
<CodeEditor v-model:source="resultSource" mode="json" :config="resultEditorConfig" class="flex-1 min-h-200px" />
|
|
|
|
|
|
|
|
|
|
<n-tooltip trigger="hover">
|
|
|
|
|
<template #trigger>
|
|
|
|
|
<n-button size="small" tertiary @click="showResultModal = true" class="absolute top-1 right-1">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon><Popup /></n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</template>
|
|
|
|
|
{{ t("layout.sider.dataComponent.Popup editor") }}
|
|
|
|
|
</n-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<n-result v-else status="418" title="Empty" :description="t('prompt[\'No object selected.\']')" />
|
|
|
|
|
|
|
|
|
|
<n-modal v-model:show="showFilterModal" preset="dialog" :showIcon="false" :title="t('layout.sider.dataComponent.Filter editor')" class="!w-90vw">
|
|
|
|
|
<div class="filter-editor filter-editor-modal" :class="{ 'is-disabled': !config.filterEnabled }">
|
|
|
|
|
<div class="filter-editor-line">function filter(data) {</div>
|
|
|
|
|
<CodeEditor
|
|
|
|
|
:key="`modal-${filterEditorKey}`"
|
|
|
|
|
v-model:source="config.filterBody"
|
|
|
|
|
mode="javascript"
|
|
|
|
|
:config="filterModalEditorConfig"
|
|
|
|
|
class="filter-editor-code filter-editor-code--modal"
|
|
|
|
|
/>
|
|
|
|
|
<div class="filter-editor-line">}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-end mt-3">
|
|
|
|
|
<n-button size="small" @click="showFilterModal = false">{{ t("other.Close") }}</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
</n-modal>
|
|
|
|
|
|
|
|
|
|
<n-modal v-model:show="showResultModal" preset="dialog" :showIcon="false" :title="t('layout.sider.dataComponent.Result editor')" class="!w-90vw">
|
|
|
|
|
<CodeEditor v-model:source="resultSource" mode="json" :config="resultModalEditorConfig" class="flex-1 min-h-200px !h-70vh" />
|
|
|
|
|
<div class="flex justify-end mt-3">
|
|
|
|
|
<n-button size="small" @click="showResultModal = false">{{ t("other.Close") }}</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
</n-modal>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
.data-component-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-editor {
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
width: 100%;
|
|
|
|
|
border: 1px solid var(--n-border-color);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-editor.is-disabled {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-editor-line {
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #c5c5c5;
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-editor-code {
|
|
|
|
|
height: 180px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-editor-code--modal {
|
|
|
|
|
height: 60vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apply-controls {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
</style>
|