parent
5e5dffcde9
commit
465b56fad8
@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["onClick"]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emit("onClick");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-tooltip location="bottom" text="Deploy">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
@click="onClick"
|
||||||
|
class="ma-1"
|
||||||
|
icon="mdi-rocket-launch"
|
||||||
|
color="info"
|
||||||
|
density="comfortable"
|
||||||
|
elevation="0"
|
||||||
|
size="small"
|
||||||
|
v-bind="props"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,387 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
} from "vue";
|
||||||
|
import Plotly from "plotly.js-dist-min";
|
||||||
|
|
||||||
|
/** ===== Types ===== */
|
||||||
|
type MetricKV = { key: string; value: number };
|
||||||
|
type RunDetailType = {
|
||||||
|
info: any;
|
||||||
|
data: { metrics: MetricKV[]; params?: any[]; tags?: any[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ===== Props / Emits ===== */
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
items: Array<{ value: string; label: string }>;
|
||||||
|
selectedRunIds: string[];
|
||||||
|
selectedMetricKeys: string[];
|
||||||
|
ensureRunDetail: (runId: string) => Promise<RunDetailType | null>;
|
||||||
|
runDetailCache: Map<string, RunDetailType>;
|
||||||
|
compareChartMode?: "byMetric" | "byRun";
|
||||||
|
normalizeValues?: boolean;
|
||||||
|
baselineRunId?: string | null;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: false,
|
||||||
|
items: () => [],
|
||||||
|
selectedRunIds: () => [],
|
||||||
|
selectedMetricKeys: () => [],
|
||||||
|
compareChartMode: "byMetric",
|
||||||
|
normalizeValues: false,
|
||||||
|
baselineRunId: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", v: boolean): void;
|
||||||
|
(e: "update:selectedRunIds", v: string[]): void;
|
||||||
|
(e: "update:selectedMetricKeys", v: string[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** ===== local refs ===== */
|
||||||
|
const dialogOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v: boolean) => emit("update:modelValue", v),
|
||||||
|
});
|
||||||
|
const selectedRunIdsProxy = computed({
|
||||||
|
get: () => props.selectedRunIds,
|
||||||
|
set: (v: string[]) => emit("update:selectedRunIds", v),
|
||||||
|
});
|
||||||
|
const selectedMetricKeysProxy = computed({
|
||||||
|
get: () => props.selectedMetricKeys,
|
||||||
|
set: (v: string[]) => emit("update:selectedMetricKeys", v),
|
||||||
|
});
|
||||||
|
|
||||||
|
const elCompare = ref<HTMLDivElement | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/** ===== helpers ===== */
|
||||||
|
const fmtNumber = (v: number | null, digits = 3) => {
|
||||||
|
if (v == null || !Number.isFinite(v)) return "";
|
||||||
|
const abs = Math.abs(v);
|
||||||
|
if (abs >= 1000) return v.toLocaleString();
|
||||||
|
if (abs >= 1) return v.toFixed(digits);
|
||||||
|
if (abs >= 0.01) return v.toFixed(digits + 1);
|
||||||
|
return v.toExponential(2);
|
||||||
|
};
|
||||||
|
const normalizeArray = (vals: (number | null)[]) => {
|
||||||
|
const xs = vals.filter((v): v is number => Number.isFinite(v as number));
|
||||||
|
if (xs.length === 0) return vals;
|
||||||
|
const min = Math.min(...xs),
|
||||||
|
max = Math.max(...xs);
|
||||||
|
if (max === min) return vals.map((v) => (v == null ? v : 1));
|
||||||
|
return vals.map((v) => (v == null ? v : (v - min) / (max - min)));
|
||||||
|
};
|
||||||
|
const labelOfRun = (r: RunDetailType) => r.info.run_name || r.info.run_id;
|
||||||
|
const valueOf = (run: RunDetailType, key: string): number | null => {
|
||||||
|
const m = run.data.metrics.find((x) => x.key === key)?.value;
|
||||||
|
return Number.isFinite(m as number) ? Number(m) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ===== derived ===== */
|
||||||
|
const compareRuns = computed<RunDetailType[]>(
|
||||||
|
() =>
|
||||||
|
selectedRunIdsProxy.value
|
||||||
|
.map((id) => props.runDetailCache.get(id))
|
||||||
|
.filter(Boolean) as RunDetailType[],
|
||||||
|
);
|
||||||
|
const metricsKeySetByRun = computed(() =>
|
||||||
|
compareRuns.value.map((r) => new Set(r.data.metrics.map((m) => m.key))),
|
||||||
|
);
|
||||||
|
const commonMetricKeys = computed<string[]>(() => {
|
||||||
|
if (metricsKeySetByRun.value.length === 0) return [];
|
||||||
|
const [first, ...rest] = metricsKeySetByRun.value;
|
||||||
|
let inter = new Set(first);
|
||||||
|
rest.forEach((s) => (inter = new Set([...inter].filter((k) => s.has(k)))));
|
||||||
|
return Array.from(inter).sort();
|
||||||
|
});
|
||||||
|
const activeMetricKeys = computed<string[]>(() =>
|
||||||
|
selectedMetricKeysProxy.value.length
|
||||||
|
? selectedMetricKeysProxy.value
|
||||||
|
: commonMetricKeys.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
/** ===== chart ===== */
|
||||||
|
const commonLayout: Partial<any> = {
|
||||||
|
autosize: true,
|
||||||
|
margin: { t: 28, r: 12, b: 56, l: 56 },
|
||||||
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
|
font: { color: "#E0E0E0" },
|
||||||
|
xaxis: { automargin: true, gridcolor: "rgba(255,255,255,0.08)" },
|
||||||
|
yaxis: {
|
||||||
|
rangemode: "tozero",
|
||||||
|
gridcolor: "rgba(255,255,255,0.08)",
|
||||||
|
automargin: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orientation: "h",
|
||||||
|
x: 0,
|
||||||
|
y: 1.12,
|
||||||
|
yanchor: "bottom",
|
||||||
|
xanchor: "left",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTracesByMetric(metricKeys: string[], runsData: RunDetailType[]) {
|
||||||
|
const runLabels = runsData.map(labelOfRun);
|
||||||
|
return runsData.map((run, i) => {
|
||||||
|
const yRaw = metricKeys.map((k) => valueOf(run, k));
|
||||||
|
const y = props.normalizeValues ? normalizeArray(yRaw) : yRaw;
|
||||||
|
const text = yRaw.map((v) => fmtNumber(v));
|
||||||
|
return {
|
||||||
|
type: "bar",
|
||||||
|
name: runLabels[i],
|
||||||
|
x: metricKeys,
|
||||||
|
y,
|
||||||
|
text: props.normalizeValues ? undefined : text,
|
||||||
|
textposition: "auto",
|
||||||
|
customdata: text,
|
||||||
|
hovertemplate: `${runLabels[i]}<br>%{x}: %{customdata}<extra></extra>`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function createTracesByRun(metricKeys: string[], runsData: RunDetailType[]) {
|
||||||
|
const rowLabels = runsData.map(labelOfRun);
|
||||||
|
return metricKeys.map((key) => {
|
||||||
|
const yRaw = rowLabels.map((_, i) => valueOf(runsData[i], key));
|
||||||
|
const y = props.normalizeValues ? normalizeArray(yRaw) : yRaw;
|
||||||
|
const text = yRaw.map((v) => fmtNumber(v));
|
||||||
|
return {
|
||||||
|
type: "bar",
|
||||||
|
name: key,
|
||||||
|
x: rowLabels,
|
||||||
|
y,
|
||||||
|
text: props.normalizeValues ? undefined : text,
|
||||||
|
textposition: "auto",
|
||||||
|
customdata: text,
|
||||||
|
hovertemplate: `${key}: %{customdata}<extra>%{x}</extra>`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function drawCompareChart() {
|
||||||
|
if (!elCompare.value) return;
|
||||||
|
const metricKeys = activeMetricKeys.value;
|
||||||
|
const runsData = compareRuns.value;
|
||||||
|
if (!metricKeys.length || !runsData.length) {
|
||||||
|
elCompare.value.innerHTML =
|
||||||
|
'<div style="padding:16px;color:#999;text-align:center;">No Data</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const traces =
|
||||||
|
props.compareChartMode === "byMetric"
|
||||||
|
? createTracesByMetric(metricKeys, runsData)
|
||||||
|
: createTracesByRun(metricKeys, runsData);
|
||||||
|
|
||||||
|
if (props.compareChartMode === "byMetric") {
|
||||||
|
const varianceOrder = metricKeys
|
||||||
|
.map((k, idx) => {
|
||||||
|
const vals = traces
|
||||||
|
.map((t: any) => t.y[idx])
|
||||||
|
.filter((v: any) => v != null) as number[];
|
||||||
|
const min = Math.min(...vals),
|
||||||
|
max = Math.max(...vals);
|
||||||
|
return { k, spread: max - min };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.spread - a.spread)
|
||||||
|
.map((v) => v.k);
|
||||||
|
|
||||||
|
traces.forEach((t: any) => {
|
||||||
|
t.x = varianceOrder;
|
||||||
|
t.y = varianceOrder.map((mk: string) => t.y[metricKeys.indexOf(mk)]);
|
||||||
|
if (t.text)
|
||||||
|
t.text = varianceOrder.map(
|
||||||
|
(mk: string) => t.text[metricKeys.indexOf(mk)],
|
||||||
|
);
|
||||||
|
if (t.customdata)
|
||||||
|
t.customdata = varianceOrder.map(
|
||||||
|
(mk: string) => t.customdata[metricKeys.indexOf(mk)],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
elCompare.value,
|
||||||
|
traces as any,
|
||||||
|
{ ...commonLayout, barmode: "group" },
|
||||||
|
{ displayModeBar: false, responsive: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ===== load & watch ===== */
|
||||||
|
async function loadCompareData() {
|
||||||
|
if (!dialogOpen.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await Promise.all(selectedRunIdsProxy.value.map(props.ensureRunDetail));
|
||||||
|
if (selectedMetricKeysProxy.value.length === 0) {
|
||||||
|
selectedMetricKeysProxy.value = commonMetricKeys.value.slice(0, 6);
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
drawCompareChart();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => dialogOpen.value,
|
||||||
|
(open) => {
|
||||||
|
if (open) loadCompareData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
watch([selectedRunIdsProxy, selectedMetricKeysProxy], async () => {
|
||||||
|
if (!dialogOpen.value) return;
|
||||||
|
await loadCompareData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
if (elCompare.value) Plotly.Plots.resize(elCompare.value);
|
||||||
|
};
|
||||||
|
onMounted(() => window.addEventListener("resize", onResize));
|
||||||
|
onBeforeUnmount(() => window.removeEventListener("resize", onResize));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialogOpen" max-width="1000">
|
||||||
|
<v-card class="rounded-lg overflow-hidden">
|
||||||
|
<!-- 헤더: Deploy Model 스타일 적용 -->
|
||||||
|
<v-card-title
|
||||||
|
class="text-white font-weight-bold text-h6"
|
||||||
|
style="background-color: #1976d2"
|
||||||
|
>
|
||||||
|
Compare Runs
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<!-- 상단: 선택 섹션 -->
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-row dense class="mb-2">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-subheader class="font-weight-medium mb-2"
|
||||||
|
>Select Runs</v-subheader
|
||||||
|
>
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="selectedRunIdsProxy"
|
||||||
|
:items="items"
|
||||||
|
item-title="label"
|
||||||
|
item-value="value"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
closable-chips
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
:disabled="loading"
|
||||||
|
placeholder="Select runs to compare"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-expand-transition>
|
||||||
|
<v-alert
|
||||||
|
v-if="!loading && selectedRunIdsProxy.length < 2"
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
2개 이상의 Run을 선택해 주세요.
|
||||||
|
</v-alert>
|
||||||
|
</v-expand-transition>
|
||||||
|
|
||||||
|
<v-row dense class="mt-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-subheader class="font-weight-medium mb-2">Metrics</v-subheader>
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="selectedMetricKeysProxy"
|
||||||
|
:items="activeMetricKeys.length ? activeMetricKeys : []"
|
||||||
|
:menu-props="{ maxHeight: 360 }"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
closable-chips
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
:disabled="loading || compareRuns.length === 0"
|
||||||
|
placeholder="Select metrics (empty = all common)"
|
||||||
|
/>
|
||||||
|
<div class="d-flex align-center mt-2 ga-2">
|
||||||
|
<v-chip size="small" color="primary" variant="tonal">
|
||||||
|
Mode:
|
||||||
|
{{
|
||||||
|
props.compareChartMode === "byMetric" ? "By Metric" : "By Run"
|
||||||
|
}}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip size="small" color="secondary" variant="tonal">
|
||||||
|
{{ props.normalizeValues ? "Normalized" : "Raw values" }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
v-if="props.baselineRunId"
|
||||||
|
size="small"
|
||||||
|
color="info"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
Baseline: {{ props.baselineRunId }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 차트 & 테이블 섹션 (톤 카드로 감싸기) -->
|
||||||
|
<v-card-text class="pt-0 px-6 pb-2">
|
||||||
|
<v-card variant="tonal" class="mb-4">
|
||||||
|
<v-card-title class="py-2 px-4">Metrics (grouped bar)</v-card-title>
|
||||||
|
<v-divider />
|
||||||
|
<v-card-text class="px-4">
|
||||||
|
<div v-if="loading" class="my-2">
|
||||||
|
<v-progress-linear indeterminate />
|
||||||
|
</div>
|
||||||
|
<div ref="elCompare" style="width: 100%; height: 420px"></div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card variant="tonal">
|
||||||
|
<v-card-title class="py-2 px-4">Comparison Table</v-card-title>
|
||||||
|
<v-divider />
|
||||||
|
<v-card-text class="px-0">
|
||||||
|
<v-table density="comfortable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 28%">Run</th>
|
||||||
|
<th v-for="k in activeMetricKeys" :key="k">{{ k }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in compareRuns" :key="r.info.run_id">
|
||||||
|
<td class="text-no-wrap">
|
||||||
|
{{ r.info.run_name || r.info.run_id }}
|
||||||
|
</td>
|
||||||
|
<td v-for="k in activeMetricKeys" :key="k">
|
||||||
|
{{ r.data.metrics.find((m) => m.key === k)?.value ?? "—" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 액션 -->
|
||||||
|
<v-card-actions class="justify-end">
|
||||||
|
<v-btn variant="text" class="text-white" @click="dialogOpen = false">
|
||||||
|
CLOSE
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -1,168 +1,301 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
|
|
||||||
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
|
||||||
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
|
||||||
import { ref, watch } from "vue";
|
|
||||||
|
|
||||||
const steps = ref([
|
|
||||||
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
|
|
||||||
{
|
|
||||||
order: 2,
|
|
||||||
stepName: "Preprocessing",
|
|
||||||
type: "Preprocess",
|
|
||||||
status: "Not Configured",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 3,
|
|
||||||
stepName: "Train Model",
|
|
||||||
type: "Train",
|
|
||||||
status: "Not Configured",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
type PackageOption = { label: string; value: string; raw: any };
|
||||||
editData: Object,
|
|
||||||
mode: String,
|
|
||||||
userOption: Array,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["handle-data", "close-modal"]);
|
const props = defineProps<{
|
||||||
|
packages: PackageOption[];
|
||||||
|
packagesLoading?: boolean;
|
||||||
|
packagesError?: string;
|
||||||
|
artifactPath?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
const visible = ref(true);
|
const emit = defineEmits<{
|
||||||
|
(e: "close-modal"): void;
|
||||||
|
(e: "handle-data", value: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
name: "",
|
package_id: "",
|
||||||
description: "",
|
sw_id: "",
|
||||||
|
sw_version: "",
|
||||||
|
software_name: "",
|
||||||
|
executed: true,
|
||||||
|
file_type: "bundle" as "bundle" | "single",
|
||||||
|
os: "" as "" | "Windows" | "Linux",
|
||||||
|
|
||||||
|
win_exe_name: "",
|
||||||
|
win_root_path: "",
|
||||||
|
linux_exe_name: "",
|
||||||
|
linux_root_path: "",
|
||||||
|
|
||||||
|
install_location: "",
|
||||||
|
private_only: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = () => {
|
const isWin = computed(() => form.value.os === "Windows");
|
||||||
emit("handle-data", form.value);
|
const isLinux = computed(() => form.value.os === "Linux");
|
||||||
};
|
|
||||||
|
const selectedOption = computed(() =>
|
||||||
|
props.packages?.find((p) => p.value === form.value.package_id),
|
||||||
|
);
|
||||||
|
const selectedRaw = computed(() => selectedOption.value?.raw ?? null);
|
||||||
|
|
||||||
|
const readonlyFields = computed(() => ({
|
||||||
|
swTypeName: selectedRaw.value?.sw_type_name ?? "",
|
||||||
|
swGroupName: selectedRaw.value?.sw_group_name ?? "",
|
||||||
|
manufacturer: selectedRaw.value?.sw_manufacturer ?? "",
|
||||||
|
packageId: selectedRaw.value?.package_id ?? "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
function onPickPackage() {
|
||||||
|
form.value.win_exe_name = selectedRaw.value?.window_exe_name ?? "";
|
||||||
|
form.value.win_root_path = selectedRaw.value?.window_root_location ?? "";
|
||||||
|
form.value.linux_exe_name = selectedRaw.value?.linux_exe_name ?? "";
|
||||||
|
form.value.linux_root_path = selectedRaw.value?.linux_root_location ?? "";
|
||||||
|
|
||||||
|
if (form.value.win_exe_name || form.value.win_root_path)
|
||||||
|
form.value.os = "Windows";
|
||||||
|
else if (form.value.linux_exe_name || form.value.linux_root_path)
|
||||||
|
form.value.os = "Linux";
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.packages,
|
||||||
|
() => {
|
||||||
|
if (!props.packages?.some((p) => p.value === form.value.package_id)) {
|
||||||
|
form.value.package_id = "";
|
||||||
|
form.value.win_exe_name = "";
|
||||||
|
form.value.win_root_path = "";
|
||||||
|
form.value.linux_exe_name = "";
|
||||||
|
form.value.linux_root_path = "";
|
||||||
|
form.value.os = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
if (!form.value.package_id) return;
|
||||||
|
emit("handle-data", {
|
||||||
|
...form.value,
|
||||||
|
resolved: readonlyFields.value,
|
||||||
|
artifact_path: props.artifactPath ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEsc(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") emit("close-modal");
|
||||||
|
}
|
||||||
|
onMounted(() => window.addEventListener("keydown", onEsc));
|
||||||
|
onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-card class="rounded-lg overflow-hidden">
|
<v-card rounded="lg" elevation="4" max-height="85vh" class="overflow-auto">
|
||||||
<!-- 타이틀 영역 -->
|
|
||||||
<v-card-title
|
<v-card-title
|
||||||
class="text-white font-weight-bold text-h6"
|
class="text-white text-h6 font-weight-bold"
|
||||||
style="background-color: #1976d2"
|
style="background: #1976d2"
|
||||||
>
|
>
|
||||||
Deploy Model
|
차량 지능 SW 등록
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-card-text class="pa-6">
|
<v-defaults-provider
|
||||||
<div class="text-subtitle-1 font-weight-medium mb-4">
|
:defaults="{
|
||||||
Select Model : ImageClassifier
|
VTextField: {
|
||||||
</div>
|
density: 'compact',
|
||||||
<VDivider class="my-2" />
|
variant: 'outlined',
|
||||||
<v-form @submit.prevent="submit">
|
hideDetails: true,
|
||||||
<div class="mb-5">
|
},
|
||||||
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
|
VSelect: { density: 'compact', variant: 'outlined', hideDetails: true },
|
||||||
>OTA
|
VRadioGroup: { density: 'compact' },
|
||||||
</label>
|
VCheckbox: { density: 'compact' },
|
||||||
<v-row dense class="mb-6">
|
VAlert: { density: 'compact' },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- 패키지 선택 & 자동 표기 -->
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row dense class="mb-2">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-subheader class="font-weight-medium white--text mb-2">
|
<div class="text-body-2 font-weight-medium mb-1">SW 패키지</div>
|
||||||
Select Package
|
|
||||||
</v-subheader>
|
|
||||||
<v-select
|
<v-select
|
||||||
dense
|
v-model="form.package_id"
|
||||||
hide-details
|
:items="packages"
|
||||||
outlined
|
item-title="label"
|
||||||
style="background: #1e1e1e; color: #fff"
|
item-value="value"
|
||||||
|
:loading="packagesLoading"
|
||||||
|
:disabled="packagesLoading"
|
||||||
|
placeholder="선택해주세요."
|
||||||
|
@update:model-value="onPickPackage"
|
||||||
/>
|
/>
|
||||||
|
<v-alert
|
||||||
|
v-if="packagesError"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
{{ packagesError }}
|
||||||
|
</v-alert>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-text class="pt-6 pb-4 px-6">
|
<!-- 자동 표기 (읽기 전용) -->
|
||||||
<v-subheader class="font-weight-medium mb-2">
|
<v-row dense class="mb-2">
|
||||||
Package Preview
|
|
||||||
</v-subheader>
|
|
||||||
<v-sheet class="pa-4 mb-6" elevation="1" rounded>
|
|
||||||
<v-row dense>
|
|
||||||
<!-- Linux -->
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<div class="font-weight-medium mb-2">Linux</div>
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="File Name"
|
label="SW 종류"
|
||||||
placeholder="4_EdgeInfra_Perception.sh"
|
:model-value="readonlyFields.swTypeName"
|
||||||
dense
|
disabled
|
||||||
outlined
|
|
||||||
hide-details
|
|
||||||
/>
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="File Path"
|
label="SW 상세 종류"
|
||||||
placeholder="/home/etri/TeslaSystem/EdgeInfraVision/RUN"
|
:model-value="readonlyFields.swGroupName"
|
||||||
dense
|
disabled
|
||||||
outlined
|
/>
|
||||||
hide-details
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
label="제조사"
|
||||||
|
:model-value="readonlyFields.manufacturer"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
label="패키지 번호"
|
||||||
|
:model-value="readonlyFields.packageId"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider class="mx-4" />
|
||||||
|
|
||||||
|
<!-- 사용자 입력 -->
|
||||||
|
<v-card-text class="pa-4 pt-3">
|
||||||
|
<v-row dense class="mb-2">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field label="SW ID" v-model="form.sw_id" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field label="SW 버전" v-model="form.sw_version" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
label="SW 명칭 (Software Name)"
|
||||||
|
v-model="form.software_name"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row dense class="mb-1">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-radio-group v-model="form.executed" inline>
|
||||||
|
<template #label
|
||||||
|
><span class="text-body-2">실행 여부</span></template
|
||||||
|
>
|
||||||
|
<v-radio class="mr-13" label="실행" :value="true" />
|
||||||
|
<v-radio label="미실행" :value="false" />
|
||||||
|
</v-radio-group>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-radio-group v-model="form.file_type" inline>
|
||||||
|
<template #label
|
||||||
|
><span class="text-body-2">파일 종류</span></template
|
||||||
|
>
|
||||||
|
<v-radio class="mr-13" label="묶음" value="bundle" />
|
||||||
|
<v-radio label="단일" value="single" />
|
||||||
|
</v-radio-group>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row dense class="mb-1">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-radio-group v-model="form.os" inline>
|
||||||
|
<template #label><span class="text-body-2">OS</span></template>
|
||||||
|
<v-radio class="mr-4" label="Windows" value="Windows" />
|
||||||
|
<v-radio label="Linux" value="Linux" />
|
||||||
|
</v-radio-group>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- Windows -->
|
<!-- Windows -->
|
||||||
|
<v-row dense v-if="isWin" class="mb-1">
|
||||||
|
<v-col cols="12"
|
||||||
|
><div class="text-body-2 font-weight-medium mb-1">
|
||||||
|
Windows
|
||||||
|
</div></v-col
|
||||||
|
>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<div class="font-weight-medium mb-2">Windows</div>
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="File Name"
|
label="윈도우 실행 파일명"
|
||||||
placeholder="4_EdgeInfra_Perception.exe"
|
v-model="form.win_exe_name"
|
||||||
dense
|
disabled
|
||||||
outlined
|
|
||||||
hide-details
|
|
||||||
/>
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="File Path"
|
label="윈도우 경로"
|
||||||
placeholder="C:/etri/TeslaSystem/EdgeInfraVision/RUN"
|
v-model="form.win_root_path"
|
||||||
dense
|
disabled
|
||||||
outlined
|
|
||||||
hide-details
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-sheet>
|
|
||||||
<!-- Package Preview 끝 -->
|
|
||||||
|
|
||||||
<!-- Software Name / Version -->
|
<!-- Linux -->
|
||||||
<v-row dense class="mb-4">
|
<v-row dense v-if="isLinux" class="mb-1">
|
||||||
<v-col cols="12">
|
<v-col cols="12"
|
||||||
|
><div class="text-body-2 font-weight-medium mb-1">Linux</div></v-col
|
||||||
|
>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="Software Name"
|
label="리눅스 실행 파일명"
|
||||||
placeholder="Enter software Name"
|
v-model="form.linux_exe_name"
|
||||||
dense
|
disabled
|
||||||
outlined
|
|
||||||
hide-details
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="Software Version"
|
label="리눅스 경로"
|
||||||
placeholder="Enter software Version"
|
v-model="form.linux_root_path"
|
||||||
dense
|
disabled
|
||||||
outlined
|
|
||||||
hide-details
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- Executed 여부 -->
|
<!-- Artifact Path -->
|
||||||
<v-row dense class="mb-6">
|
<v-row dense class="mb-1">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-radio-group row>
|
<v-text-field
|
||||||
<v-radio label="Executed" value="executed" />
|
label="차량 SW 파일 (Artifact Path)"
|
||||||
<v-radio label="Not Executed" value="not_executed" />
|
:model-value="props.artifactPath || '-'"
|
||||||
</v-radio-group>
|
disabled
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 설치 위치 & 등록인만 접근 -->
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-text-field label="설치 위치" v-model="form.install_location" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4" class="d-flex align-center">
|
||||||
|
<v-checkbox
|
||||||
|
v-model="form.private_only"
|
||||||
|
label="등록인만 접근 여부"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions class="justify-end" style="padding: 16px 24px">
|
<v-card-actions class="py-2 px-3">
|
||||||
<v-btn color="success" @click="submit">Save</v-btn>
|
<v-spacer />
|
||||||
<v-btn text class="white--text" @click="$emit('close-modal')"
|
<v-btn color="success" @click="submit">SAVE</v-btn>
|
||||||
>Close</v-btn
|
<v-btn variant="text" @click="$emit('close-modal')">CANCEL</v-btn>
|
||||||
>
|
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
</v-defaults-provider>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -0,0 +1,180 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted } from "vue";
|
||||||
|
import {
|
||||||
|
DatasetService,
|
||||||
|
type ExternalDatasetItem,
|
||||||
|
} from "@/components/service/management/DatasetService";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
projectId: number;
|
||||||
|
refId?: number | null;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", v: boolean): void;
|
||||||
|
(e: "close"): void;
|
||||||
|
(e: "saved"): void; // 저장 완료시 부모 새로고침
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue);
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(v) => (visible.value = v),
|
||||||
|
);
|
||||||
|
watch(visible, (v) => emit("update:modelValue", v));
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref("");
|
||||||
|
const rows = ref<ExternalDatasetItem[]>([]);
|
||||||
|
const searchKeyword = ref("");
|
||||||
|
const groupName = ref("");
|
||||||
|
|
||||||
|
function unwrap<T = any>(res: any): T {
|
||||||
|
return res && res.data ? res.data : res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
if (!props.projectId) return;
|
||||||
|
loading.value = true;
|
||||||
|
errorMsg.value = "";
|
||||||
|
try {
|
||||||
|
const res = await DatasetService.listExternal({
|
||||||
|
ds_prj_idx: props.projectId,
|
||||||
|
search_keyword: searchKeyword.value || undefined, // 서버 필터
|
||||||
|
grp_name: groupName.value || undefined,
|
||||||
|
});
|
||||||
|
const body = unwrap<any>(res);
|
||||||
|
const arr: ExternalDatasetItem[] = Array.isArray(body?.data)
|
||||||
|
? body.data
|
||||||
|
: Array.isArray(body)
|
||||||
|
? body
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// ✅ ds_dataset_name 기준 로컬 필터 한 번 더
|
||||||
|
const kw = (searchKeyword.value || "").trim().toLowerCase();
|
||||||
|
rows.value =
|
||||||
|
kw.length === 0
|
||||||
|
? arr
|
||||||
|
: arr.filter((r) =>
|
||||||
|
String(r.ds_dataset_name || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(kw),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = "외부 데이터셋 조회에 실패했습니다.";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible.value = false;
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pick(item: ExternalDatasetItem) {
|
||||||
|
try {
|
||||||
|
// 필요시 여기서 DatasetService.saveExternal(...) 호출하고 emit("saved")
|
||||||
|
// 예시:
|
||||||
|
// await DatasetService.saveExternal({
|
||||||
|
// datasetName: item.ds_dataset_name,
|
||||||
|
// path: item.ds_dataset_base_path,
|
||||||
|
// refId: props.refId ?? 0,
|
||||||
|
// refType: "DATASET",
|
||||||
|
// title: item.ds_dataset_name,
|
||||||
|
// version: 1,
|
||||||
|
// description: "",
|
||||||
|
// regUserId: "currentUser",
|
||||||
|
// projectId: props.projectId,
|
||||||
|
// });
|
||||||
|
emit("saved");
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.projectId, fetchList);
|
||||||
|
onMounted(fetchList);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card rounded="lg" elevation="4">
|
||||||
|
<v-card-title
|
||||||
|
class="text-white text-h6 font-weight-bold"
|
||||||
|
style="background: #1976d2"
|
||||||
|
>
|
||||||
|
External Datasets
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<!-- 🔧 검색바 한 줄 정렬 -->
|
||||||
|
<div class="d-flex align-center ga-2 mb-3" style="flex-wrap: wrap">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchKeyword"
|
||||||
|
label="데이터셋 이름 (ds_dataset_name)"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
style="min-width: 240px; flex: 1 1 240px"
|
||||||
|
@keyup.enter="fetchList"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="groupName"
|
||||||
|
label="그룹 이름"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
style="min-width: 240px; flex: 1 1 240px"
|
||||||
|
@keyup.enter="fetchList"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<v-btn color="primary" :loading="loading" @click="fetchList"
|
||||||
|
>검색</v-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="errorMsg"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-table density="compact" fixed-header height="360">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Dataset Name</th>
|
||||||
|
<th class="text-left">Images</th>
|
||||||
|
<th class="text-left">Label Type</th>
|
||||||
|
<th class="text-left">Base Path</th>
|
||||||
|
<th class="text-left">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(r, i) in rows" :key="i">
|
||||||
|
<td>{{ r.ds_dataset_name }}</td>
|
||||||
|
<td>{{ r.ds_dataset_image_count }}</td>
|
||||||
|
<td>{{ r.labelling_tool_kr }}</td>
|
||||||
|
<td>{{ r.ds_dataset_base_path }}</td>
|
||||||
|
<td>
|
||||||
|
<v-btn size="small" color="primary" @click="pick(r)"
|
||||||
|
>Select</v-btn
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!loading && rows.length === 0">
|
||||||
|
<td colspan="5" class="text-center text-medium-emphasis">
|
||||||
|
No data
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="justify-end px-4 py-2">
|
||||||
|
<v-btn variant="text" @click="close">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
// src/components/service/management/DatasetService.ts
|
||||||
|
import { request } from "@/components/service/index";
|
||||||
|
|
||||||
|
export interface ExternalListQuery {
|
||||||
|
ds_prj_idx: number;
|
||||||
|
search_keyword?: string;
|
||||||
|
grp_name?: string;
|
||||||
|
}
|
||||||
|
export interface ExternalDatasetItem {
|
||||||
|
ds_dataset_name: string;
|
||||||
|
ds_dataset_image_count: number;
|
||||||
|
labelling_tool_kr: string;
|
||||||
|
ds_dataset_base_path: string;
|
||||||
|
}
|
||||||
|
export interface SaveExternalDatasetPayload {
|
||||||
|
datasetName: string;
|
||||||
|
path: string;
|
||||||
|
refId: number;
|
||||||
|
refType: "DATASET";
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
version: number;
|
||||||
|
regUserId: string;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatasetService = {
|
||||||
|
listExternal(query: ExternalListQuery) {
|
||||||
|
return request.get("/api/datasets/list", query as any);
|
||||||
|
},
|
||||||
|
|
||||||
|
saveExternal(payload: SaveExternalDatasetPayload) {
|
||||||
|
return request.post("/api/datasets/dataset/save", payload);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { request } from "@/components/service/index";
|
||||||
|
|
||||||
|
export const ExternalAuthControllerService = {
|
||||||
|
signIn: (id: string, password: string) => {
|
||||||
|
return request.post("/api/external-auth/signin", { id, password });
|
||||||
|
},
|
||||||
|
|
||||||
|
add: (token: string, edgePkgInfoVO: string, file: any) => {
|
||||||
|
return request.post("/api/external-auth/add", {
|
||||||
|
token,
|
||||||
|
edgePkgInfoVO,
|
||||||
|
file,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
search: (id: string, token: string) => {
|
||||||
|
return request.get("/api/external-auth/edge-search", {
|
||||||
|
id,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,563 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
|
|
||||||
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
|
||||||
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
|
|
||||||
import CompareComponent from "@/components/templates/run/executions/CompareComponent.vue";
|
|
||||||
import ViewComponent from "@/components/templates/run/executions/ViewComponent.vue";
|
|
||||||
import ExecutionBaseDialog from "@/components/atoms/organisms/ExecutionBaseDialog.vue";
|
|
||||||
import { ExecutionsService } from "@/components/service/management/ExecutionsService";
|
|
||||||
// const store = commonStore();
|
|
||||||
|
|
||||||
const openCompare = ref(false);
|
|
||||||
const openView = ref(false);
|
|
||||||
const runsLoading = ref(false);
|
|
||||||
|
|
||||||
// ✅ 파라미터 없이 1회 호출만 해서 콘솔에 출력
|
|
||||||
// ✅ 상태 라벨 매핑 (아이콘 로직이 'Succeeded'/'Failed' 기준이라 맞춰줌)
|
|
||||||
function toUiStatus(state?: string) {
|
|
||||||
switch ((state || "").toUpperCase()) {
|
|
||||||
case "SUCCEEDED":
|
|
||||||
return "Succeeded";
|
|
||||||
case "FAILED":
|
|
||||||
return "Failed";
|
|
||||||
case "RUNNING":
|
|
||||||
return "Running";
|
|
||||||
case "PENDING":
|
|
||||||
return "Pending";
|
|
||||||
case "SKIPPED":
|
|
||||||
return "Skipped";
|
|
||||||
default:
|
|
||||||
return state || "-";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 시간 포맷/지속시간 유틸
|
|
||||||
function fmtStart(start?: string) {
|
|
||||||
if (!start) return "-";
|
|
||||||
const d = new Date(start);
|
|
||||||
if (isNaN(d.getTime())) return start;
|
|
||||||
const yyyy = d.getFullYear();
|
|
||||||
const MM = String(d.getMonth() + 1).padStart(2, "0");
|
|
||||||
const dd = String(d.getDate()).padStart(2, "0");
|
|
||||||
const hh = String(d.getHours()).padStart(2, "0");
|
|
||||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
||||||
return `${yyyy}-${MM}-${dd} ${hh}:${mi}`;
|
|
||||||
}
|
|
||||||
function fmtDuration(start?: string, end?: string) {
|
|
||||||
if (!start || !end) return "-";
|
|
||||||
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
||||||
if (!isFinite(ms) || ms < 0) return "-";
|
|
||||||
const s = Math.floor(ms / 1000);
|
|
||||||
const h = Math.floor(s / 3600);
|
|
||||||
const m = Math.floor((s % 3600) / 60);
|
|
||||||
const sec = s % 60;
|
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
|
||||||
return `${h}:${pad(m)}:${pad(sec)}`;
|
|
||||||
}
|
|
||||||
const displayNo = (i: number) => {
|
|
||||||
const start = (data.value.params.pageNum - 1) * data.value.params.pageSize;
|
|
||||||
return data.value.totalDataLength - (start + i);
|
|
||||||
};
|
|
||||||
async function loadRunsAll() {
|
|
||||||
runsLoading.value = true;
|
|
||||||
try {
|
|
||||||
const all: any[] = [];
|
|
||||||
|
|
||||||
// 1페이지
|
|
||||||
let resp = await ExecutionsService.search({
|
|
||||||
page_size: 500,
|
|
||||||
} as any);
|
|
||||||
all.push(...(resp?.data?.runs ?? []));
|
|
||||||
|
|
||||||
// 다음 토큰 추출(스네이크/카멜 모두 대비)
|
|
||||||
let token: string | undefined =
|
|
||||||
resp?.data?.next_page_token ?? resp?.data?.nextPageToken;
|
|
||||||
|
|
||||||
// 다음 페이지들
|
|
||||||
const seen = new Set<string>();
|
|
||||||
while (token && !seen.has(token)) {
|
|
||||||
seen.add(token);
|
|
||||||
|
|
||||||
// ✅ 무조건 token을 넣어서 호출 (snake/camel 둘 다 넣기)
|
|
||||||
resp = await ExecutionsService.search({
|
|
||||||
page_token: token,
|
|
||||||
pageToken: token,
|
|
||||||
page_size: 500,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
all.push(...(resp?.data?.runs ?? []));
|
|
||||||
token = resp?.data?.next_page_token ?? resp?.data?.nextPageToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 중복 방지
|
|
||||||
const dedup = Array.from(
|
|
||||||
new Map(all.map((r: any) => [r?.run_id ?? r?.id ?? r?.name, r])).values(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 테이블 행으로 매핑
|
|
||||||
data.value.results = dedup.map((r: any, idx: number) => ({
|
|
||||||
no: idx + 1,
|
|
||||||
name: r?.display_name ?? r?.name ?? r?.run_id ?? "(no name)",
|
|
||||||
status: toUiStatus(r?.state),
|
|
||||||
duration: fmtDuration(r?.created_at, r?.finished_at),
|
|
||||||
experiment: r?.experiment_id ?? "-",
|
|
||||||
workflow:
|
|
||||||
r?.pipeline_version_reference?.pipeline_id ??
|
|
||||||
r?.pipeline_version_reference?.pipeline_version_id ??
|
|
||||||
"-",
|
|
||||||
startTime: fmtStart(r?.created_at),
|
|
||||||
registryStatus: r?.storage_state ?? "-",
|
|
||||||
run_id: r?.run_id,
|
|
||||||
raw: r,
|
|
||||||
}));
|
|
||||||
|
|
||||||
experimentOptions.value = Array.from(
|
|
||||||
new Set(data.value.results.map((r: any) => String(r.experiment || "-"))),
|
|
||||||
).filter((v) => v && v !== "-");
|
|
||||||
|
|
||||||
workflowOptions.value = Array.from(
|
|
||||||
new Set(data.value.results.map((r: any) => String(r.workflow || "-"))),
|
|
||||||
).filter((v) => v && v !== "-");
|
|
||||||
data.value.totalDataLength = data.value.results.length;
|
|
||||||
setPaginationLength();
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("[Runs] 호출 실패:", e?.response?.data ?? e);
|
|
||||||
} finally {
|
|
||||||
runsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagedResults = computed(() => {
|
|
||||||
const { pageNum, pageSize } = data.value.params;
|
|
||||||
const start = (pageNum - 1) * pageSize;
|
|
||||||
return data.value.results.slice(start, start + pageSize);
|
|
||||||
});
|
|
||||||
const tableHeader = [
|
|
||||||
{ label: "No", width: "5%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Execution Name", width: "20%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Status", width: "10%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Duration", width: "10%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Experiment", width: "15%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Workflow", width: "15%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Start Time", width: "15%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Registry Status", width: "10%", style: "word-break: keep-all;" },
|
|
||||||
{ label: "Action", width: "5%", style: "word-break: keep-all;" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const searchOptions = [
|
|
||||||
{ searchType: "All", searchText: "" },
|
|
||||||
{ searchType: "Execution Name", searchText: "name" },
|
|
||||||
{ searchType: "Status", searchText: "status" },
|
|
||||||
{ searchType: "Duration", searchText: "duration" },
|
|
||||||
{ searchType: "Experiment", searchText: "experiment" },
|
|
||||||
{ searchType: "Workflow", searchText: "workflow" },
|
|
||||||
{ searchType: "Registry Status", searchText: "registryStatus" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const experimentOptions = ref<string[]>([]);
|
|
||||||
const workflowOptions = ref<string[]>([]);
|
|
||||||
const execDialogOpen = ref(false);
|
|
||||||
const execMode = ref<"create" | "edit" | "clone">("create");
|
|
||||||
const execSelected = ref<any>(null);
|
|
||||||
const searchExperimentOptions = [{ searchType: "Experiment", searchText: "" }];
|
|
||||||
const searchWorkflowOptions = [{ searchType: "Workflow", searchText: "" }];
|
|
||||||
|
|
||||||
const workflowList = ref(["pipeline-a", "pipeline-b", "pipeline-c"]);
|
|
||||||
const executionTypes = ref(["One-off", "Recurring"]);
|
|
||||||
|
|
||||||
const pageSizeOptions = [
|
|
||||||
{ text: "10 페이지", value: 10 },
|
|
||||||
{ text: "50 페이지", value: 50 },
|
|
||||||
{ text: "100 페이지", value: 100 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const data = ref({
|
|
||||||
params: {
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
searchType: "",
|
|
||||||
searchText: "",
|
|
||||||
experimentFilter: "",
|
|
||||||
workflowFilter: "",
|
|
||||||
},
|
|
||||||
results: [],
|
|
||||||
totalDataLength: 0,
|
|
||||||
pageLength: 0,
|
|
||||||
modalMode: "",
|
|
||||||
selectedData: null,
|
|
||||||
allSelected: false,
|
|
||||||
selected: [],
|
|
||||||
isModalVisible: false,
|
|
||||||
isConfirmDialogVisible: false,
|
|
||||||
userOption: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredResults = computed(() => {
|
|
||||||
const { searchType, searchText, experimentFilter, workflowFilter } =
|
|
||||||
data.value.params;
|
|
||||||
|
|
||||||
let list = data.value.results;
|
|
||||||
|
|
||||||
// 실드롭다운 필터
|
|
||||||
if (experimentFilter) {
|
|
||||||
list = list.filter((r) => String(r.experiment).includes(experimentFilter));
|
|
||||||
}
|
|
||||||
if (workflowFilter) {
|
|
||||||
list = list.filter((r) => String(r.workflow).includes(workflowFilter));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 텍스트 검색
|
|
||||||
const q = (searchText || "").trim().toLowerCase();
|
|
||||||
if (q) {
|
|
||||||
if (!searchType) {
|
|
||||||
// All: 여러 필드 OR 매칭
|
|
||||||
list = list.filter((r) => {
|
|
||||||
const pool = [
|
|
||||||
r.name,
|
|
||||||
r.status,
|
|
||||||
r.duration,
|
|
||||||
r.experiment,
|
|
||||||
r.workflow,
|
|
||||||
r.registryStatus,
|
|
||||||
r.startTime,
|
|
||||||
];
|
|
||||||
return pool.some((v) =>
|
|
||||||
String(v ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(q),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
list = list.filter((r) =>
|
|
||||||
String(r[searchType] ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(q),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
const setPaginationLength = () => {
|
|
||||||
if (data.value.totalDataLength % data.value.params.pageSize === 0) {
|
|
||||||
data.value.pageLength =
|
|
||||||
data.value.totalDataLength / data.value.params.pageSize;
|
|
||||||
} else {
|
|
||||||
data.value.pageLength = Math.ceil(
|
|
||||||
data.value.totalDataLength / data.value.params.pageSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTerminate = () => {
|
|
||||||
alert("Terminate 작업 진행중...");
|
|
||||||
};
|
|
||||||
const handleRetry = () => {
|
|
||||||
alert("Retry 작업 진행중...");
|
|
||||||
};
|
|
||||||
const handleClone = () => {
|
|
||||||
alert("Clone 작업 진행중...");
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePageNum = (page) => {
|
|
||||||
data.value.params.pageNum = page;
|
|
||||||
};
|
|
||||||
const openCreateExecution = () => {
|
|
||||||
execMode.value = "create";
|
|
||||||
execSelected.value = null;
|
|
||||||
execDialogOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openComparePage = () => {
|
|
||||||
openCompare.value = true;
|
|
||||||
openView.value = false;
|
|
||||||
};
|
|
||||||
const openInfoModal = (item: any) => {
|
|
||||||
execSelected.value = item;
|
|
||||||
openView.value = true;
|
|
||||||
openCompare.value = false;
|
|
||||||
};
|
|
||||||
const openModifyModal = (selectedItem) => {
|
|
||||||
execMode.value = "edit";
|
|
||||||
execDialogOpen.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDownloadModal = () => {
|
|
||||||
data.value.selectedData = null;
|
|
||||||
data.value.modalMode = "download";
|
|
||||||
};
|
|
||||||
function closeCompare() {
|
|
||||||
openCompare.value = false;
|
|
||||||
}
|
|
||||||
function closeView() {
|
|
||||||
openView.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSelectedAllData = () => {
|
|
||||||
data.value.selected = data.value.allSelected
|
|
||||||
? data.value.results.map((item) => {
|
|
||||||
return {
|
|
||||||
deviceKey: item.deviceKey,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadRunsAll();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-100" v-if="!openCompare && !openView">
|
|
||||||
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
|
||||||
<v-card
|
|
||||||
flat
|
|
||||||
class="bg-shades-transparent d-flex flex-column align-center justify-center w-100"
|
|
||||||
>
|
|
||||||
<v-card flat class="bg-shades-transparent w-100">
|
|
||||||
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
|
||||||
<div class="d-flex flex-row justify-start align-center">
|
|
||||||
<div class="text-primary">Executions</div>
|
|
||||||
</div>
|
|
||||||
</v-card-item>
|
|
||||||
</v-card>
|
|
||||||
<v-card flat class="bg-shades-transparent w-100">
|
|
||||||
<v-card flat class="bg-shades-transparent mb-4">
|
|
||||||
<div class="d-flex justify-center flex-wrap align-center">
|
|
||||||
<v-responsive
|
|
||||||
max-width="180"
|
|
||||||
min-width="180"
|
|
||||||
class="mr-3 mt-3 mb-3"
|
|
||||||
>
|
|
||||||
<v-select
|
|
||||||
v-model="data.params.searchType"
|
|
||||||
label="검색조건"
|
|
||||||
density="compact"
|
|
||||||
:items="searchOptions"
|
|
||||||
item-title="searchType"
|
|
||||||
item-value="searchText"
|
|
||||||
hide-details
|
|
||||||
></v-select>
|
|
||||||
</v-responsive>
|
|
||||||
<v-responsive
|
|
||||||
max-width="180"
|
|
||||||
min-width="180"
|
|
||||||
class="mr-3 mt-3 mb-3"
|
|
||||||
>
|
|
||||||
<v-select
|
|
||||||
v-model="data.params.searchType"
|
|
||||||
label="검색조건"
|
|
||||||
density="compact"
|
|
||||||
:items="searchExperimentOptions"
|
|
||||||
item-title="searchType"
|
|
||||||
item-value="searchText"
|
|
||||||
hide-details
|
|
||||||
></v-select>
|
|
||||||
</v-responsive>
|
|
||||||
<v-responsive
|
|
||||||
max-width="180"
|
|
||||||
min-width="180"
|
|
||||||
class="mr-3 mt-3 mb-3"
|
|
||||||
>
|
|
||||||
<v-select
|
|
||||||
v-model="data.params.searchType"
|
|
||||||
label="검색조건"
|
|
||||||
density="compact"
|
|
||||||
:items="searchWorkflowOptions"
|
|
||||||
item-title="searchType"
|
|
||||||
item-value="searchText"
|
|
||||||
hide-details
|
|
||||||
></v-select>
|
|
||||||
</v-responsive>
|
|
||||||
<v-responsive min-width="540" max-width="540">
|
|
||||||
<v-text-field
|
|
||||||
v-model="data.params.searchText"
|
|
||||||
label="검색어"
|
|
||||||
density="compact"
|
|
||||||
clearable
|
|
||||||
required
|
|
||||||
class="mt-3 mb-3"
|
|
||||||
hide-details
|
|
||||||
@keyup.enter="changePageNum(1)"
|
|
||||||
></v-text-field>
|
|
||||||
</v-responsive>
|
|
||||||
|
|
||||||
<div class="ml-3">
|
|
||||||
<v-btn
|
|
||||||
size="large"
|
|
||||||
color="primary"
|
|
||||||
:rounded="5"
|
|
||||||
@click="changePageNum(1)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-magnify</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-sheet
|
|
||||||
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
|
|
||||||
>
|
|
||||||
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
|
|
||||||
<v-sheet
|
|
||||||
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
|
|
||||||
>
|
|
||||||
<v-chip color="primary"
|
|
||||||
>총 {{ data.totalDataLength.toLocaleString() }}개
|
|
||||||
</v-chip>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="bg-shades-transparent">
|
|
||||||
<v-responsive max-width="140" min-width="140" class="mb-2">
|
|
||||||
<v-select
|
|
||||||
v-model="data.params.pageSize"
|
|
||||||
density="compact"
|
|
||||||
:items="pageSizeOptions"
|
|
||||||
item-title="text"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
hide-details
|
|
||||||
@update:model-value="changePageNum(1)"
|
|
||||||
></v-select>
|
|
||||||
</v-responsive>
|
|
||||||
</v-sheet>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="justify-end mb-2 mr-3" @click="handleTerminate">
|
|
||||||
<v-btn color="primary">Terminate </v-btn>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="justify-end mb-2 mr-3" @click="handleRetry">
|
|
||||||
<v-btn color="primary">Retry </v-btn>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="justify-end mb-2 mr-3" @click="handleClone">
|
|
||||||
<v-btn color="primary">Clone </v-btn>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="justify-end mb-2 mr-3" @click="openComparePage">
|
|
||||||
<v-btn color="primary">Compare </v-btn>
|
|
||||||
</v-sheet>
|
|
||||||
<v-sheet class="justify-end mb-2" @click="openCreateExecution">
|
|
||||||
<v-btn color="primary">Execution </v-btn>
|
|
||||||
</v-sheet>
|
|
||||||
</v-sheet>
|
|
||||||
|
|
||||||
<v-card class="rounded-lg pa-8">
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-sheet>
|
|
||||||
<v-table
|
|
||||||
density="comfortable"
|
|
||||||
fixed-header
|
|
||||||
height="625"
|
|
||||||
col-md-12
|
|
||||||
col-12
|
|
||||||
overflow-x-auto
|
|
||||||
>
|
|
||||||
<colgroup>
|
|
||||||
<col style="width: 5%" />
|
|
||||||
<col
|
|
||||||
v-for="(item, i) in tableHeader"
|
|
||||||
:key="i"
|
|
||||||
:style="`width:${item.width}`"
|
|
||||||
/>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<v-checkbox
|
|
||||||
v-model="data.allSelected"
|
|
||||||
style="min-width: 36px"
|
|
||||||
:indeterminate="data.allSelected === true"
|
|
||||||
hide-details
|
|
||||||
@change="getSelectedAllData"
|
|
||||||
></v-checkbox>
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
v-for="(item, i) in tableHeader"
|
|
||||||
:key="i"
|
|
||||||
class="text-center font-weight-bold"
|
|
||||||
:style="`${item.style}`"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-body-2">
|
|
||||||
<tr
|
|
||||||
v-for="(item, i) in pagedResults"
|
|
||||||
:key="item.run_id || item.no || i"
|
|
||||||
class="text-center"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<v-checkbox
|
|
||||||
v-model="data.selected"
|
|
||||||
:value="{ deviceKey: item.deviceKey }"
|
|
||||||
hide-details
|
|
||||||
style="min-width: 36px"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>{{ displayNo(i) }}</td>
|
|
||||||
<td>{{ item.name }}</td>
|
|
||||||
<td>
|
|
||||||
<v-icon v-if="item.status === 'Succeeded'" color="green"
|
|
||||||
>mdi-check-circle</v-icon
|
|
||||||
>
|
|
||||||
<v-icon v-else-if="item.status === 'Failed'" color="red"
|
|
||||||
>mdi-close-circle</v-icon
|
|
||||||
>
|
|
||||||
<v-icon v-else color="grey">mdi-loading</v-icon>
|
|
||||||
</td>
|
|
||||||
<td>{{ item.duration }}</td>
|
|
||||||
<td>{{ item.experiment }}</td>
|
|
||||||
<td>{{ item.workflow }}</td>
|
|
||||||
<td>{{ item.startTime }}</td>
|
|
||||||
<td>{{ item.registryStatus }}</td>
|
|
||||||
<td style="white-space: nowrap">
|
|
||||||
<IconInfoBtn @on-click="openInfoModal(item)" />
|
|
||||||
<IconModifyBtn @on-click="openModifyModal(item)" />
|
|
||||||
<IconDownloadBtn @on-click="openDownloadModal(item)" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</v-table>
|
|
||||||
</v-sheet>
|
|
||||||
<v-card-actions class="text-center mt-8 justify-center">
|
|
||||||
<v-pagination
|
|
||||||
v-model="data.params.pageNum"
|
|
||||||
:length="data.pageLength"
|
|
||||||
:total-visible="10"
|
|
||||||
color="primary"
|
|
||||||
rounded="circle"
|
|
||||||
@update:model-value="changePageNum"
|
|
||||||
></v-pagination>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-col>
|
|
||||||
</v-card>
|
|
||||||
</v-card>
|
|
||||||
</v-card>
|
|
||||||
<v-dialog v-model="execDialogOpen" max-width="800" persistent>
|
|
||||||
<ExecutionBaseDialog
|
|
||||||
:model-value="execDialogOpen"
|
|
||||||
:mode="execMode"
|
|
||||||
:selectedData="execSelected"
|
|
||||||
:workflowList="workflowList"
|
|
||||||
:executionTypes="executionTypes"
|
|
||||||
@update:modelValue="execDialogOpen = $event"
|
|
||||||
/>
|
|
||||||
</v-dialog>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
<div class="w-100" v-else-if="openCompare">
|
|
||||||
<CompareComponent @close="closeCompare" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-100" v-else-if="openView">
|
|
||||||
<ViewComponent
|
|
||||||
v-if="openView"
|
|
||||||
:experimentInfo="execSelected"
|
|
||||||
@close="closeView"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue