From 7244a01bfad5583ae65720eaad484bd5af20e62e Mon Sep 17 00:00:00 2001 From: jschoi Date: Wed, 24 Sep 2025 13:49:17 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 2 +- .../atoms/organisms/WorkflowsRunDialog.vue | 96 ++- src/components/service/index.ts | 3 + .../service/management/ExecutionsService.ts | 10 +- .../service/management/KubefliwRunService.ts | 24 + .../service/management/kubeflowService.ts | 8 +- .../templates/home/ListComponent.vue | 382 ++++++++- .../run/executions/ListComponent.vue | 800 ++++++++++-------- .../run/executions/ViewComponent.vue | 475 +++++------ .../templates/workflow/ViewComponent.vue | 103 ++- 10 files changed, 1196 insertions(+), 707 deletions(-) create mode 100644 src/components/service/management/KubefliwRunService.ts diff --git a/.env.dev b/.env.dev index 759edf0..b0c7266 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,3 @@ NODE_ENV = "dev" -VITE_APP_API_SERVER_URL = "http://localhost:80" +VITE_APP_API_SERVER_URL = "http://localhost:8080" VITE_ROOT_PATH = "" \ No newline at end of file diff --git a/src/components/atoms/organisms/WorkflowsRunDialog.vue b/src/components/atoms/organisms/WorkflowsRunDialog.vue index 4fe3ad7..c1fbdeb 100644 --- a/src/components/atoms/organisms/WorkflowsRunDialog.vue +++ b/src/components/atoms/organisms/WorkflowsRunDialog.vue @@ -6,14 +6,12 @@ type RunPayload = { display_name: string; description?: string; pipeline_version_reference: { pipeline_id: string }; - // 필요 시 아래 두 개만 추가하세요 - // runtime_config?: { parameters?: Record }; - // service_account?: string; + runtime_config?: { parameters?: Record }; + service_account?: string; + experiment_id?: string; }; -const props = defineProps<{ - pipelineId?: string | number | null; // 테이블에서 넘어온 pipelineId -}>(); +const props = defineProps<{ pipelineId?: string | number | null }>(); const emit = defineEmits<{ (e: "close-modal"): void; @@ -24,10 +22,17 @@ const form = ref({ display_name: "", description: "", pipeline_id: "", + experiment_id: "" as string, }); + const loading = ref(false); const errorMsg = ref(""); +// 드롭다운 옵션 +type ExperimentOption = { label: string; value: string; created?: string }; +const experimentOptions = ref([]); +const expLoading = ref(false); + const isValid = computed( () => !!form.value.display_name.trim() && !!form.value.pipeline_id.trim(), ); @@ -44,6 +49,44 @@ function onEsc(e: KeyboardEvent) { onMounted(() => window.addEventListener("keydown", onEsc)); onBeforeUnmount(() => window.removeEventListener("keydown", onEsc)); +/** 모든 experiments 수집해서 드롭다운 채우기 (콘솔 출력 제거) */ +async function loadExperimentsAll() { + expLoading.value = true; + try { + const first = await kubeflowService.experiments({ pageSize: 500 }); + const all: any[] = [...(first.data?.experiments ?? [])]; + let token: string | undefined = first.data?.next_page_token; + + if (token) { + const { data } = await kubeflowService.experiments({ + pageSize: 500, + pageToken: token, + }); + all.push(...(data?.experiments ?? [])); + token = data?.next_page_token; + } + + experimentOptions.value = all.map((x: any) => ({ + label: x?.display_name ?? x?.name ?? "(no name)", + value: x?.experiment_id ?? x?.id ?? x?.name, + created: x?.created_at ?? x?.create_time, + })); + + if (!form.value.experiment_id && experimentOptions.value.length > 0) { + form.value.experiment_id = experimentOptions.value[0].value; + } + } catch (e: any) { + errorMsg.value = + e?.response?.data?.message || + e?.response?.data?.error || + e?.message || + "Experiments 로드에 실패했습니다."; + } finally { + expLoading.value = false; + } +} +onMounted(loadExperimentsAll); + async function submitRun() { errorMsg.value = ""; if (!isValid.value) { @@ -53,11 +96,14 @@ async function submitRun() { const payload: RunPayload = { display_name: form.value.display_name.trim(), - // description은 값이 있으면만 넣음 ...(form.value.description.trim() && { description: form.value.description.trim(), }), pipeline_version_reference: { pipeline_id: form.value.pipeline_id.trim() }, + service_account: "pipeline-runner", + ...(form.value.experiment_id && { + experiment_id: form.value.experiment_id, + }), }; try { @@ -66,7 +112,6 @@ async function submitRun() { emit("submitted", data); emit("close-modal"); } catch (e: any) { - console.error("Run 생성 실패:", e); errorMsg.value = e?.response?.data?.message || e?.response?.data?.error || @@ -89,41 +134,70 @@ async function submitRun() { +
+
+
+ +
+ + +
+
{{ errorMsg }}
diff --git a/src/components/service/index.ts b/src/components/service/index.ts index 91ae7b7..394712d 100644 --- a/src/components/service/index.ts +++ b/src/components/service/index.ts @@ -14,6 +14,9 @@ export const request = { get: (uri: string, param: any): any => { return axios.get(`${API_URL}${uri}`, { params: param }); }, + getsize: (uri: string): any => { + return axios.get(`${API_URL}${uri}`); + }, delete: (uri: string, param: any): any => { return axios.delete(`${API_URL}${uri}`, param); }, diff --git a/src/components/service/management/ExecutionsService.ts b/src/components/service/management/ExecutionsService.ts index 755e828..3063bf8 100644 --- a/src/components/service/management/ExecutionsService.ts +++ b/src/components/service/management/ExecutionsService.ts @@ -1,7 +1,9 @@ -import { RunsSearchParams } from "@/components/models/management/Exeucutios"; import { request } from "@/components/service/index"; export const ExecutionsService = { - search: (payload: RunsSearchParams) => { - return request.get("/api/runs/runs", { params: payload }); - }, + search: (params?: { + pageToken?: string; + pageSize?: number; + sortBy?: string; + filter?: string; + }) => request.get("/api/runs/runs", params), }; diff --git a/src/components/service/management/KubefliwRunService.ts b/src/components/service/management/KubefliwRunService.ts new file mode 100644 index 0000000..4125fe4 --- /dev/null +++ b/src/components/service/management/KubefliwRunService.ts @@ -0,0 +1,24 @@ +import { request } from "@/components/service/index"; + +export type KubeflowRunSearchParams = { + experimentId?: string; // 실험 ID + page?: number; // 페이지 번호 (0부터 시작) + size?: number; // 한 페이지당 출력 건수 + keyword?: string; // 공통 키워드 검색 + searchType?: string; // 검색 유형 (전체, 제목, 작성자 등) + startDate?: string; // 등록일자 검색 시작 (yyyy-MM-dd) + endDate?: string; // 등록일자 검색 종료 (yyyy-MM-dd) + sortField?: string; // 정렬 기준 필드명 + sortDirection?: "ASC" | "DESC"; // 정렬 방향 +}; +export const kubeflowRunService = { + getAll: () => { + return request.get("/api/kubeflow/runs", {}); + }, + singleData: (runId: number) => { + return request.get(`/api/kubeflow/runs/${runId}`, {}); + }, + search: (params?: KubeflowRunSearchParams) => { + return request.get("/api/kubeflow/runs", params); + }, +}; diff --git a/src/components/service/management/kubeflowService.ts b/src/components/service/management/kubeflowService.ts index f7d7b36..c265548 100644 --- a/src/components/service/management/kubeflowService.ts +++ b/src/components/service/management/kubeflowService.ts @@ -7,7 +7,9 @@ export const kubeflowService = { run: (payload: kubeflow) => { return request.post("/pipelines/runs", payload); }, - kubeflowSize: (payload: kubeflow) => { - return request.post("/pipelines/experiments", payload); - }, + experiments: (params?: { + namespace?: string; + pageSize?: number; + pageToken?: string; + }) => request.get("/api/kubeflow/experiments", params), }; diff --git a/src/components/templates/home/ListComponent.vue b/src/components/templates/home/ListComponent.vue index a4ea1b3..9ad4e93 100644 --- a/src/components/templates/home/ListComponent.vue +++ b/src/components/templates/home/ListComponent.vue @@ -2,6 +2,8 @@ import { onMounted, ref, computed, watch } from "vue"; import Plotly from "plotly.js-dist-min"; import { WorkflowService } from "@/components/service/management/WorkflowService"; +import { ExecutionsService } from "@/components/service/management/ExecutionsService"; +import { AttachmentsService } from "@/components/service/management/AttachmentsService"; import { useAutoflowStore } from "@/stores/autoflowStore"; const store = useAutoflowStore(); @@ -10,29 +12,234 @@ const currentProjectId = computed(() => store.projectId); const pieChartRef = ref(null); const workflows = ref([]); const recentLimit = 10; +const runsLoading = ref(false); +const recentRuns = ref< + { + name: string; + status: "success" | "failed" | "running" | "pending"; + time: string; + }[] +>([]); +// dataset +const dsLoading = ref(false); +type DsItem = { + name: string; + version: number; + rows: number; + last?: string; + pct: number; +}; +const dsItems = ref([]); +const dsWindowDays = ref<7 | 30 | 90>(30); + +// 날짜 유틸 +const toTime = (x?: string) => new Date(x ?? 0).getTime(); +const fmtYmd = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "-"); + +async function loadDatasetActivity() { + dsLoading.value = true; + try { + const payload = { + projectId: currentProjectId.value, + page: 0, + size: 1000, + refType: "DATASET", + sortField: "id", + sortDirection: "DESC", + }; + + const res = await AttachmentsService.search(payload as any); + const list: any[] = res?.data?.content ?? res?.data ?? []; + + const map = new Map< + string, + { name: string; version: number; rows: number; last?: string } + >(); + + for (const a of list) { + const name = String(a?.title ?? a?.originalName ?? "(no name)"); + const v = Number(a?.version) || 1; // null 방어 + const reg = (a?.modDt ?? a?.regDt ?? a?.createdAt) as string | undefined; + + const cur = map.get(name) ?? { + name, + version: 0, + rows: 0, + last: undefined, + }; + cur.rows += 1; + cur.version = Math.max(cur.version, v); // 최신 버전 반영 + if (!cur.last || (reg && toTime(reg) > toTime(cur.last))) cur.last = reg; + map.set(name, cur); + } + + const arr = Array.from(map.values()); + const maxVer = Math.max(1, ...arr.map((i) => i.version)); + + dsItems.value = arr + .map((i) => ({ ...i, pct: Math.round((i.version / maxVer) * 100) })) + .sort((a, b) => toTime(b.last) - toTime(a.last) || b.version - a.version); + } catch (e) { + console.error("[Home] loadDatasetActivity error:", e); + dsItems.value = []; + } finally { + dsLoading.value = false; + } +} + +function toUiStatus(state?: string) { + switch ((state || "").toUpperCase()) { + case "SUCCEEDED": + return "success"; + case "FAILED": + return "failed"; + case "RUNNING": + return "running"; + case "PENDING": + return "pending"; + default: + return "pending"; + } +} + +function fmtYmdHm(iso?: string) { + if (!iso) return "-"; + const d = new Date(iso); + const p = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} + +async function loadRecentRuns() { + runsLoading.value = true; + try { + const all: any[] = []; + const seen = new Set(); + + // 첫 페이지 + let page = await ExecutionsService.search({ pageSize: 200 }); + all.push(...(page?.data?.runs ?? [])); + let token = page?.data?.next_page_token ?? page?.data?.nextPageToken; + + // 다음 페이지들(필요 시) + let guard = 0; + while (token && !seen.has(token) && guard < 20) { + seen.add(token); + + // snake_case 우선 + page = await ExecutionsService.search({ + pageToken: token, + pageSize: 200, + } as any); + all.push(...(page?.data?.runs ?? [])); + token = page?.data?.next_page_token ?? page?.data?.nextPageToken; + guard++; + } + + // 중복 제거 + const dedup = Array.from( + new Map(all.map((r) => [r?.run_id ?? r?.id ?? r?.name, r])).values(), + ); + + // (선택) 프로젝트 필터가 필요하면 여기에서 필터 + // const filtered = dedup.filter(r => String(r?.projectId ?? "") === String(currentProjectId.value)); + + const latest = dedup + .sort( + (a, b) => + new Date(b?.created_at ?? 0).getTime() - + new Date(a?.created_at ?? 0).getTime(), + ) + .slice(0, recentLimit) + .map((r) => ({ + name: r?.display_name ?? r?.name ?? r?.run_id ?? "(no name)", + status: toUiStatus(r?.state), + time: fmtYmdHm(r?.created_at), + })); + + recentRuns.value = latest; + } catch (e) { + console.error("[Home] loadRecentRuns error:", e); + recentRuns.value = []; + } finally { + runsLoading.value = false; + } +} -// ---- kubeflowStatus 파이차트 (색상 매핑 없음, 기본 팔레트 사용) ---- function renderStatusPie() { if (!pieChartRef.value) return; + // 1) 상태별 고정 팔레트 + const COLOR = { + SUCCEEDED: "#2ecc71", + FAILED: "#e74c3c", + RUNNING: "#3498db", + PENDING: "#f1c40f", + CREATED: "#3498db", + SKIPPED: "#95a5a6", + UNKNOWN: "#7f8c8d", + NODATA: "#4b4f55", + }; + + // 2) 집계 const counts = new Map(); for (const wf of workflows.value ?? []) { - const status = - String(wf?.kubeflowStatus ?? wf?.kubeflow_status ?? "Unknown").trim() || - "Unknown"; + const raw = (wf?.kubeflowStatus ?? wf?.kubeflow_status ?? "UNKNOWN") + ""; + const status = raw.toUpperCase().trim(); counts.set(status, (counts.get(status) || 0) + 1); } + // 3) No Data 전용 처리 (중립 색 + 중앙 텍스트 / 범례 숨김) + if (counts.size === 0) { + const trace: Partial = { + values: [1], + labels: ["No Data"], + type: "pie", + hole: 0.55, + textinfo: "none", + marker: { colors: [COLOR.NODATA] }, + hoverinfo: "skip", + }; + + const layout: Partial = { + paper_bgcolor: "#1e1e1e", + plot_bgcolor: "#1e1e1e", + showlegend: false, + margin: { t: 20, b: 40, l: 0, r: 0 }, + annotations: [ + { + text: "No Data", + showarrow: false, + font: { color: "#ffffff", size: 16 }, + }, + ], + }; + + Plotly.react(pieChartRef.value, [trace], layout, { displayModeBar: false }); + return; + } + + // 4) 데이터가 있을 때: 상태별 색 지정 const labels = Array.from(counts.keys()); const values = Array.from(counts.values()); + const colors = labels.map((s) => { + if (s.includes("SUCCEED")) return COLOR.SUCCEEDED; + if (s.includes("FAIL")) return COLOR.FAILED; + if (s.includes("RUN")) return COLOR.RUNNING; + if (s.includes("CREATE")) return COLOR.CREATED; + if (s.includes("PEND")) return COLOR.PENDING; + if (s.includes("SKIP")) return COLOR.SKIPPED; + return COLOR.UNKNOWN; + }); const trace: Partial = { - values: values.length ? values : [1], - labels: labels.length ? labels : ["No Data"], + values, + labels, type: "pie", - textinfo: "label+percent", - textfont: { color: "#fff", size: 14 }, hole: 0.4, + textinfo: "label+percent", + textfont: { color: "#fff", size: 13 }, + marker: { colors }, + hovertemplate: "%{label}: %{percent}", }; const layout: Partial = { @@ -52,13 +259,6 @@ function renderStatusPie() { Plotly.react(pieChartRef.value, [trace], layout, { displayModeBar: false }); } -// ---- 데모 나머지 (기존 그대로) ---- -const recentRuns = [ - { name: "Model A - v1", status: "success", time: "2025-05-12 09:12" }, - { name: "Model B - tuning", status: "success", time: "2025-05-14 08:59" }, - { name: "Model C - test run", status: "failed", time: "2025-05-13 18:13" }, -]; - const datasetUpdates = [ { name: "DrivingLog2025", count: 7 }, { name: "CameraFrames", count: 3 }, @@ -174,9 +374,10 @@ async function loadWorkflows() { } onMounted(async () => { - // 차트 컨테이너가 있을 때, 초기 빈 차트 렌더 renderStatusPie(); + await loadRecentRuns(); await loadWorkflows(); + await loadDatasetActivity(); }); // 프로젝트 변경 시 재조회 @@ -246,32 +447,77 @@ watch(currentProjectId, () => loadWorkflows());

Recent Run

+
- + + + + + -
- - - {{ run.status === "success" ? "mdi-check" : "mdi-close" }} - - + +
+ +
+ + + + {{ + run.status === "success" + ? "mdi-check" + : run.status === "failed" + ? "mdi-close" + : run.status === "running" + ? "mdi-progress-clock" + : "mdi-clock-outline" + }} + + + + {{ run.status }} + +
+
- + {{ run.name }} @@ -280,6 +526,14 @@ watch(currentProjectId, () => loadWorkflows());
+ + + +
@@ -287,23 +541,65 @@ watch(currentProjectId, () => loadWorkflows());
-

- Dataset Update Activity -

+
+

+ Dataset Update Activity +

+
+
- - - {{ data.name }} + + + + +
+ {{ it.name }} + + v{{ it.version }} + + + +
+ + Last: {{ fmtYmd(it.last) }} + +
+
+ - {{ data.count }} updates +
+ {{ it.version }} Update{{ it.version > 1 ? "s" : "" }} + + • {{ it.rows }} Rows +
+
+ + +
diff --git a/src/components/templates/run/executions/ListComponent.vue b/src/components/templates/run/executions/ListComponent.vue index f35d056..dcc8ef1 100644 --- a/src/components/templates/run/executions/ListComponent.vue +++ b/src/components/templates/run/executions/ListComponent.vue @@ -1,28 +1,27 @@ diff --git a/src/components/templates/run/executions/ViewComponent.vue b/src/components/templates/run/executions/ViewComponent.vue index 4cc68c9..a65bba9 100644 --- a/src/components/templates/run/executions/ViewComponent.vue +++ b/src/components/templates/run/executions/ViewComponent.vue @@ -2,228 +2,100 @@ import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue"; // import FormComponent from "@/components/device/FormComponent.vue"; -import { onMounted, ref, watch } from "vue"; +import { computed, onMounted, ref, watch } from "vue"; // const store = commonStore(); -const experimentInfo = ref({ - executionsName: "run-batch32-lr0.001", - status: "Succeeded", - duration: "0:00:21", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-20 10:12", - registryStatus: "Registered", -}); +const props = defineProps<{ + experimentInfo: any; +}>(); -const otaInfo = ref({ - packageName: "자율주행 타차량 예측", - os: "Linux", - packageFileName: "4_EdgeInfra_Perception.sh", - packageFilePath: "/home/etri/TeslaSystem/EdgeInfraVision/RUN", - softwareName: "4_EdgeInfra_Perception.sh", - softwareVersion: "v2.0", - execute: "Not Executed", -}); +const emit = defineEmits<{ (e: "close"): void }>(); + +const history = computed(() => + (props.experimentInfo.raw?.state_history ?? []) + .slice() + .sort( + (a: any, b: any) => + new Date(a.update_time).getTime() - new Date(b.update_time).getTime(), + ), +); -const data = ref({ - params: { - pageNum: 1, - pageSize: 10, - searchType: "", - searchText: "", - }, - results: [], - totalDataLength: 0, - pageLength: 0, - modalMode: "", - selectedData: null, - allSelected: false, - selected: [], - isModalVisible: false, - isConfirmDialogVisible: false, - userOption: [], +// 히스토리에서 각 단계의 기록 찾기 +const hPending = computed(() => + history.value.find((h) => (h.state || "").toUpperCase() === "PENDING"), +); +const hRunning = computed(() => + history.value.find((h) => (h.state || "").toUpperCase() === "RUNNING"), +); +const hTerminal = computed(() => { + const t = history.value + .slice() + .reverse() + .find((h) => + ["SUCCEEDED", "FAILED"].includes((h.state || "").toUpperCase()), + ); + return t ?? null; }); -const getCodeList = () => { - // UserService.search(data.value.params).then((d) => { - // if (d.status === 200) { - // data.value.userOption = d.data.userList; - // } - // }); -}; +// 3단계 고정 스텝 정의 +const steps = computed(() => { + const lastLabel = (hTerminal.value?.state || "COMPLETED").toUpperCase(); -const getData = () => { - const params = { ...data.value.params }; - if (params.searchType === "" || params.searchText === "") { - delete params.searchType; - delete params.searchText; - } - data.value.results = [ - { - name: "run-batch32-lr0.001", - status: "Succeeded", - Duration: "0:00:21", - configProgress: "0/2", - Pipeline: "baseline_train_pipeline", - registDt: "2025-06-10T00:00:00Z", - }, - { - name: "run-batch64-lr0.001", - status: "Failed", - Duration: "0:00:21", - configProgress: "1/3", - Pipeline: "baseline_train_pipeline", - registDt: "2025-06-09T00:00:00Z", - }, + return [ { - name: "run-batch32-lr0.0005", - status: "Succeeded", - Duration: "0:00:21", - configProgress: "0/3", - Pipeline: "baseline_train_pipeline", - registDt: "2025-06-01T00:00:00Z", + key: "PENDING", + label: "PENDING", + active: !!(hPending.value || hRunning.value || hTerminal.value), + color: "primary", + icon: "mdi-clock-outline", + ts: hPending.value?.update_time, }, { - name: "run-batch64-lr0.0005", - status: "Running", - Duration: "0:00:21", - configProgress: "1/3", - Pipeline: "baseline_train_pipeline", - registDt: "2025-05-29T00:00:00Z", + key: "RUNNING", + label: "RUNNING", + active: !!(hRunning.value || hTerminal.value), + color: "info", + icon: "mdi-progress-clock", + ts: hRunning.value?.update_time, }, { - name: "run-augmented-data", - status: "Succeeded", - Duration: "0:00:21", - configProgress: "0/3", - Pipeline: "baseline_train_pipeline", - registDt: "2025-05-31T00:00:00Z", + key: "TERMINAL", + label: ["SUCCEEDED", "FAILED"].includes(lastLabel) + ? lastLabel + : "COMPLETED", + active: !!hTerminal.value, + color: + lastLabel === "FAILED" + ? "error" + : lastLabel === "SUCCEEDED" + ? "success" + : "surface-variant", + icon: + lastLabel === "FAILED" + ? "mdi-close" + : lastLabel === "SUCCEEDED" + ? "mdi-check" + : "mdi-dots-horizontal", + ts: hTerminal.value?.update_time, }, ]; - data.value.totalDataLength = 5; - setPaginationLength(); - // DeviceService.search(params).then((d) => { - // if (d.status === 200) { - // data.value.results = d.data.deviceList; - // data.value.totalDataLength = d.data.totalCount; - // setTimeout(() => { - // setPaginationLength(); - // }, 200); - // } else { - // store.setSnackbarMsg({ - // text: "디바이스 조회 실패", - // color: "error", - // }); - // } - // }); - // DeviceService.search().then((d) => { - // data.value.totalDataLength = d.data.totalCount; - // setTimeout(() => { - // setPaginationLength(); - // }, 200); - // }); -}; - -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 saveData = (formData) => { - if (data.value.modalMode === "create") { - // DeviceService.add(formData).then((d) => { - // if (d.status === 200) { - // data.value.isModalVisible = false; - // store.setSnackbarMsg({ - // text: "등록 되었습니다.", - // result: 200, - // }); - // changePageNum(1); - // } else { - // store.setSnackbarMsg({ - // text: d, - // result: 500, - // }); - // } - // }); - } else { - // DeviceService.update(formData.deviceKey, formData).then((d) => { - // if (d.status === 200) { - // data.value.isModalVisible = false; - // store.setSnackbarMsg({ - // text: "수정 되었습니다.", - // result: 200, - // }); - // changePageNum(); - // } else { - // store.setSnackbarMsg({ - // text: d, - // result: 500, - // }); - // } - // }); - } -}; - -const removeData = (value) => { - let removeList = value ? value : data.value.selected; - const remove = (code) => { - // return DeviceService.delete(code).then((d) => { - // if (d.status !== 200) { - // store.setSnackbarMsg({ - // text: d, - // result: 500, - // }); - // } - // }); - }; +}); - if (removeList.length === 1) { - remove(removeList[0].deviceKey).then(() => { - // store.setSnackbarMsg({ - // text: "삭제되었습니다.", - // result: 200, - // }); - changePageNum(); - data.value.isConfirmDialogVisible = false; - data.value.selected = []; - data.value.allSelected = false; - }); - } else { - Promise.all(removeList.map((item) => remove(item.deviceKey))).finally( - () => { - // store.setSnackbarMsg({ - // text: "모두 삭제되었습니다.", - // result: 200, - // }); - changePageNum(); - data.value.isConfirmDialogVisible = false; - data.value.selected = []; - data.value.allSelected = false; - }, - ); - } -}; +// 고정 3스텝 기준 위치/세그먼트 +const nSteps = 3; +const activeIndex = computed( + () => steps.value.map((s) => s.active).lastIndexOf(true), // -1이면 아무것도 진행X +); +const leftPct = (i: number) => (i / (nSteps - 1)) * 100; +const segWidthPct = () => 100 / (nSteps - 1); -const changePageNum = (page) => { - data.value.params.pageNum = page; - getData(); -}; - -const emit = defineEmits<{ - (e: "close"): void; -}>(); +// 유틸 +const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—"); +// 콘솔로 확인 onMounted(() => { - getData(); - getCodeList(); + console.log("[Child] 받은 데이터:", props.experimentInfo); }); @@ -243,97 +115,204 @@ onMounted(() => { - Deploy Model Information + Execution Information - - Executions Name - - {{ - experimentInfo.executionsName - }} + Name + {{ props.experimentInfo.name }} - Status - mdi-check-circle - mdi-close-circle - mdi-loading + mdi-loading + {{ props.experimentInfo.status }} +
+ - Duration - {{ experimentInfo.duration }} + Duration + {{ + props.experimentInfo.duration + }} + + + + + Experiment ID + {{ + props.experimentInfo.experiment + }} - + - Experiment - {{ experimentInfo.experiment }} + Workflow + {{ + props.experimentInfo.workflow + }} + - Workflow - {{ experimentInfo.workflow }} Start Time - {{ experimentInfo.startTime }} + {{ + props.experimentInfo.startTime + }} - Registry Status {{ - experimentInfo.registryStatus + props.experimentInfo.registryStatus }} + + + + + State History + + +
+ +
+ + + + + + +
+ + + - Back to List + Back to List - diff --git a/src/components/templates/workflow/ViewComponent.vue b/src/components/templates/workflow/ViewComponent.vue index 44daca1..7465f1e 100644 --- a/src/components/templates/workflow/ViewComponent.vue +++ b/src/components/templates/workflow/ViewComponent.vue @@ -3,6 +3,7 @@ import { onMounted, ref, watch, onBeforeUnmount } from "vue"; import * as monaco from "monaco-editor"; import "monaco-editor/min/vs/editor/editor.main.css"; import { WorkflowService } from "@/components/service/management/WorkflowService"; +import { AttachmentsService } from "@/components/service/management/AttachmentsService"; // ⬅️ 파일 읽기용 type TabKey = "details" | "yaml"; @@ -15,7 +16,20 @@ const activeTab = ref("details"); const editorRef = ref(null); let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null; -// 화면에 뿌릴 상세 데이터(요청 항목만) +function ensureEditor() { + if (editorInstance || !editorRef.value) return; + editorInstance = monaco.editor.create(editorRef.value, { + value: defaultYaml, + language: "yaml", + theme: "vs-dark", + readOnly: true, + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: "on", + }); +} + +// 화면에 뿌릴 상세 데이터 const detail = ref({ name: "", version: "", @@ -41,7 +55,7 @@ spec: args: ["echo hello"] `; -// 간단한 날짜 포맷터 (ISO/T 포함 모두 대응) +// 날짜 포맷 function formatDateTime(raw?: string): string { if (!raw) return "-"; const s = String(raw).replace("T", " "); @@ -49,13 +63,30 @@ function formatDateTime(raw?: string): string { return m ? m[1] : s.slice(0, 19); } -// ===== 상세 조회 ===== +/** ⬅️ storagePath(object key)로부터 YAML 읽기 */ +async function loadYamlFromStoragePath(objectName?: string) { + const key = (objectName || "").trim(); + if (!key) return false; + try { + const res = await AttachmentsService.readTextByPath(key); + const text = + typeof res?.data === "string" ? res.data : String(res?.data ?? ""); + ensureEditor(); + editorInstance?.setValue(text || defaultYaml); + return true; + } catch (e) { + console.warn("[Workflow Detail] readTextByPath failed:", e); + return false; + } +} + +/** 상세 조회 → YAML 문자열 우선 → storagePath 후보들 시도 → 실패 시 기본 YAML */ async function fetchDetail(id: number | string) { try { const res = await WorkflowService.view(Number(id)); const d = res?.data ?? {}; - // 백엔드 필드명(스네이크/카멜) 혼재 대비 매핑 + // 정보 매핑(스네이크/카멜 혼용 방어) detail.value = { name: d.name ?? d.workflowName ?? "", version: String(d.version ?? ""), @@ -66,7 +97,9 @@ async function fetchDetail(id: number | string) { regDt: formatDateTime(d.reg_dt ?? d.regDt ?? d.regDate), }; - // YAML (있으면 보여주고, 없으면 기본 예시) + ensureEditor(); + + // 1) 서버가 YAML 문자열을 직접 줄 때 const yamlFromServer = d.workflowYaml || d.yaml || @@ -74,27 +107,45 @@ async function fetchDetail(id: number | string) { d.specYaml || d.yamlStr || ""; - if (editorInstance) { - editorInstance.setValue(yamlFromServer || defaultYaml); + if (yamlFromServer) { + editorInstance!.setValue(yamlFromServer); + return; + } + + // 2) 업로드 파일의 object key(=storagePath 등) 후보 + const objectKeyCandidates = [ + d.yamlStoragePath, + d.yaml_object_name, + d.yamlObjectName, + d.storagePath, + d.storedName, + d.objectName, + d.object_key, + d.yamlPath, + d.filePath, + d.yamlFile, + d.yamlFilePath, + ].filter(Boolean) as string[]; + + // 3) 후보 중 하나라도 성공하면 반환 + for (const key of objectKeyCandidates) { + const ok = await loadYamlFromStoragePath(key); + if (ok) return; } + + // 4) 전부 실패하면 기본 YAML 출력 + editorInstance!.setValue(defaultYaml); } catch (e) { console.error("[Workflow Detail] view API failed:", e); + ensureEditor(); + editorInstance!.setValue(defaultYaml); } } // ===== 마운트 & 변경 감지 ===== onMounted(() => { - if (editorRef.value) { - editorInstance = monaco.editor.create(editorRef.value, { - value: defaultYaml, - language: "yaml", - theme: "vs-dark", - readOnly: true, - automaticLayout: true, - minimap: { enabled: false }, - lineNumbers: "on", - }); - } + // 탭 전환 관계없이 미리 생성해도 ok (automaticLayout) + ensureEditor(); }); watch( @@ -107,14 +158,20 @@ watch( { immediate: true }, ); +// YAML 탭으로 전환되었을 때도 에디터 보장 +watch( + () => activeTab.value, + (tab) => { + if (tab === "yaml") ensureEditor(); + }, +); + onBeforeUnmount(() => { - if (editorInstance) { - editorInstance.dispose(); - editorInstance = null; - } + editorInstance?.dispose(); + editorInstance = null; }); -// ===== (나중 사용) Step 테이블 정의 ===== +// (나중 사용) Step 테이블 const stepHeaders = [ { title: "Order", key: "order", width: "10%", align: "center" }, { title: "Step Name", key: "name", width: "40%", align: "center" },