From 7244a01bfad5583ae65720eaad484bd5af20e62e Mon Sep 17 00:00:00 2001 From: jschoi Date: Wed, 24 Sep 2025 13:49:17 +0900 Subject: [PATCH 1/4] =?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" }, From f9800c23d14f5e759f621830f12864cb323bab95 Mon Sep 17 00:00:00 2001 From: jschoi Date: Wed, 24 Sep 2025 15:03:11 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/executions/ListComponent.vue | 844 +++++++++--------- 1 file changed, 412 insertions(+), 432 deletions(-) diff --git a/src/components/templates/run/executions/ListComponent.vue b/src/components/templates/run/executions/ListComponent.vue index dcc8ef1..e997b37 100644 --- a/src/components/templates/run/executions/ListComponent.vue +++ b/src/components/templates/run/executions/ListComponent.vue @@ -1,27 +1,154 @@ From ac1f47a72a3c06ad999f070d615fb418d7cfe250 Mon Sep 17 00:00:00 2001 From: jschoi Date: Thu, 25 Sep 2025 09:42:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix=20:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A0=ED=83=9D=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20Execution=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/executions/ListComponent.vue | 227 +++++++----------- src/router/index.js | 51 ++-- src/views/Select.vue | 16 +- 3 files changed, 119 insertions(+), 175 deletions(-) diff --git a/src/components/templates/run/executions/ListComponent.vue b/src/components/templates/run/executions/ListComponent.vue index e997b37..5869825 100644 --- a/src/components/templates/run/executions/ListComponent.vue +++ b/src/components/templates/run/executions/ListComponent.vue @@ -55,52 +55,47 @@ function fmtDuration(start?: string, end?: string) { const pad = (n: number) => String(n).padStart(2, "0"); return `${h}:${pad(m)}:${pad(sec)}`; } - -// ✅ 모든 Runs 페이징 수집 후 테이블에 주입 +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[] = []; - const seen = new Set(); - // 첫 페이지부터 page_size 명시 (SDK 케이스 둘 다 지원) - let first = await ExecutionsService.search({ pageSize: 79 }); + // 1페이지 + let resp = await ExecutionsService.search({ + page_size: 500, + } as any); + all.push(...(resp?.data?.runs ?? [])); - all.push(...(first?.data?.runs ?? [])); + // 다음 토큰 추출(스네이크/카멜 모두 대비) let token: string | undefined = - first?.data?.next_page_token ?? first?.data?.nextPageToken; + resp?.data?.next_page_token ?? resp?.data?.nextPageToken; // 다음 페이지들 - let guard = 0; - while (token && !seen.has(token) && guard < 1000) { + const seen = new Set(); + while (token && !seen.has(token)) { seen.add(token); - // snake_case 우선 호출 - let page = await ExecutionsService.search({ pageSize: 79 }); - - // camelCase만 받는 SDK 대비 폴백 - if ( - (!Array.isArray(page?.data?.runs) || page?.data?.runs.length === 0) && - (page?.data?.next_page_token === token || - page?.data?.next_page_token === undefined) - ) { - page = await ExecutionsService.search({ - pageToken: token, - pageSize: 500, - } as any); - } - - all.push(...(page?.data?.runs ?? [])); - token = page?.data?.next_page_token ?? page?.data?.nextPageToken; - guard += 1; + // ✅ 무조건 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)", @@ -112,38 +107,27 @@ async function loadRunsAll() { r?.pipeline_version_reference?.pipeline_version_id ?? "-", startTime: fmtStart(r?.created_at), - registryStatus: r?.storage_state ?? "-", // AVAILABLE 등 - // 필요 시 원본 활용 + 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(); - - console.log( - "[Runs] total_size(from API):", - first?.data?.total_size ?? first?.data?.totalSize, - "fetched:", - data.value.results.length, - ); - console.table( - data.value.results.slice(0, 50).map((r: any) => ({ - no: r.no, - run_id: r.run_id, - name: r.name, - status: r.status, - startTime: r.startTime, - duration: r.duration, - })), - ); } 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; @@ -171,6 +155,8 @@ const searchOptions = [ { searchType: "Registry Status", searchText: "registryStatus" }, ]; +const experimentOptions = ref([]); +const workflowOptions = ref([]); const execDialogOpen = ref(false); const execMode = ref<"create" | "edit" | "clone">("create"); const execSelected = ref(null); @@ -192,6 +178,8 @@ const data = ref({ pageSize: 10, searchType: "", searchText: "", + experimentFilter: "", + workflowFilter: "", }, results: [], totalDataLength: 0, @@ -205,96 +193,52 @@ const data = ref({ userOption: [], }); -const getCodeList = () => { - // UserService.search(data.value.params).then((d) => { - // if (d.status === 200) { - // data.value.userOption = d.data.userList; - // } - // }); -}; +const filteredResults = computed(() => { + const { searchType, searchText, experimentFilter, workflowFilter } = + data.value.params; + + let list = data.value.results; -const getData = () => { - const params = { ...data.value.params }; - if (params.searchType === "" || params.searchText === "") { - delete params.searchType; - delete params.searchText; + // 실드롭다운 필터 + if (experimentFilter) { + list = list.filter((r) => String(r.experiment).includes(experimentFilter)); + } + if (workflowFilter) { + list = list.filter((r) => String(r.workflow).includes(workflowFilter)); } - data.value.results = [ - { - no: 11, - name: "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", - }, - { - no: 10, - name: "run-batch64-lr0.001", - status: "Failed", - duration: "0:00:20", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-20 09:10", - registryStatus: "Not Registered", - }, - { - no: 9, - name: "run-batch32-lr0.0005", - status: "Succeeded", - duration: "0:00:21", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-19 10:12", - registryStatus: "Registered", - }, - { - no: 8, - name: "run-batch64-lr0.0005", - status: "Running", - duration: "0:00:20", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-18 11:50", - registryStatus: "Not Registered", - }, - { - no: 7, - name: "run-augmented-data", - status: "Succeeded", - duration: "0:00:20", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-17 09:12", - registryStatus: "Registered", - }, - { - no: 6, - name: "run-augmented-data", - status: "Succeeded", - duration: "0:00:20", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-17 09:12", - registryStatus: "Registered", - }, - { - no: 5, - name: "run-augmented-data", - status: "Succeeded", - duration: "0:00:20", - experiment: "Baseline Model Training", - workflow: "baseline_train_pipeline", - startTime: "2025-05-17 09:12", - registryStatus: "Registered", - }, - ]; - data.value.totalDataLength = data.value.results.length; - setPaginationLength(); -}; + // 텍스트 검색 + 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 = @@ -318,7 +262,6 @@ const handleClone = () => { const changePageNum = (page) => { data.value.params.pageNum = page; - getData(); }; const openCreateExecution = () => { execMode.value = "create"; @@ -364,7 +307,6 @@ const getSelectedAllData = () => { onMounted(() => { loadRunsAll(); - getCodeList(); }); @@ -542,8 +484,8 @@ onMounted(() => { @@ -554,7 +496,8 @@ onMounted(() => { style="min-width: 36px" /> - {{ item.no }} + + {{ displayNo(i) }} {{ item.name }} { :total-visible="10" color="primary" rounded="circle" - @update:model-value="getData" + @update:model-value="changePageNum" > diff --git a/src/router/index.js b/src/router/index.js index 82f1c49..5da2208 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -122,16 +122,13 @@ const router = createRouter({ routes, }); -router.beforeEach((to) => { +router.beforeEach((to, from) => { const authed = !!( typeof storage.getToken === "function" && storage.getToken() ); - - const isLogin = to.name === "login" || to.path === "/login"; - const isSignup = to.name === "signup" || to.path === "/signup"; - const isSelect = to.name === "select" || to.path === "/select"; - - const bootDone = sessionStorage.getItem("initialRedirectDone") === "1"; + const isLogin = to.name === "login"; + const isSignup = to.name === "signup"; + const isSelect = to.name === "select"; if (!authed) { if (!isLogin && !isSignup) { @@ -140,32 +137,26 @@ router.beforeEach((to) => { return true; } - if (!bootDone && !isSelect && !isLogin && !isSignup) { - sessionStorage.setItem("initialRedirectDone", "1"); - return { name: "select", replace: true, query: { redirect: to.fullPath } }; - } - - if (to.matched.some((r) => r.meta?.requiresAdmin)) { - try { - const raw = - typeof storage?.getAuth === "function" - ? storage.getAuth() - : JSON.parse(localStorage.getItem("autoflow-auth") || "null"); - - const roles = raw?.userInfo?.roles ?? raw?.roles ?? []; - const authCd = raw?.userInfo?.authCd ?? raw?.authCd ?? raw?.auth; + const hasProject = !!localStorage.getItem("projectId"); // ✅ 프로젝트 선택 여부 + const bootDone = sessionStorage.getItem("initialRedirectDone") === "1"; - const isAdmin = - (Array.isArray(roles) - ? roles.includes("ROLE_ADMIN") - : roles === "ROLE_ADMIN") || authCd === "ADMIN"; + // 이미 프로젝트 선택됨 → 어떤 화면이든 통과 + if (hasProject) return true; - if (!isAdmin) { - return { name: "home", replace: true }; - } - } catch { - return { name: "home", replace: true }; + // 아직 프로젝트 미선택 + if (!bootDone) { + // ✅ 선택 화면에 "들어온 순간"을 부트 완료로 간주 (여기서 한 번만 세팅) + if (isSelect) { + sessionStorage.setItem("initialRedirectDone", "1"); + return true; } + // ✅ select 로 1회만 보냄 + return { name: "select", replace: true, query: { redirect: to.fullPath } }; + } + + // 부트 완료인데 여전히 프로젝트 미선택이면, select만 허용 + if (!isSelect) { + return { name: "select", replace: true, query: { redirect: to.fullPath } }; } return true; diff --git a/src/views/Select.vue b/src/views/Select.vue index a2503de..4b3f684 100644 --- a/src/views/Select.vue +++ b/src/views/Select.vue @@ -1,6 +1,6 @@