) {
+ const targets = Array.from(
+ new Set(
+ (ids || [])
+ .map((x) => String(x))
+ .filter((id) => id && experimentNameMap.value[id] == null),
+ ),
+ );
+ if (!targets.length) return;
+
+ await Promise.all(
+ targets.map(async (id) => {
+ try {
+ // ✅ 너가 만든 API 사용 (POST /api/kubeflow/experiments/{id})
+ const res = await KubeflowService.experimentData(id);
+ const body = res?.data ?? res ?? {};
+ const name =
+ body.display_name ?? body.name ?? body.experiment_name ?? String(id);
+ experimentNameMap.value[id] = name;
+ } catch (e) {
+ // 실패 시 ID fallback
+ experimentNameMap.value[id] = String(id);
+ }
+ }),
+ );
+}
+
function readUsernameFromStorage(): string {
try {
const raw =
@@ -84,7 +114,13 @@ const getProjectId = (): number => {
return Number.isFinite(v) ? v : 0;
};
-const toRow = (r: any, idx: number) => {
+const toRow = (r: any, no: number) => {
+ const expId = r.experimentId ?? r.experiment_id ?? r.experiment?.id;
+ const expName =
+ (expId && experimentNameMap.value[String(expId)]) ??
+ r.experiment?.displayName ??
+ r.experiment?.name ??
+ "-";
const fmtStart = (start?: string) => {
if (!start) return "-";
const d = new Date(start);
@@ -109,31 +145,32 @@ const toRow = (r: any, idx: number) => {
return `${h}:${pad(m)}:${pad(sec)}`;
};
- const 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 || "-";
- }
- };
+ const toUiStatus = (state?: string, finishedAt?: string) => {
+ const s = String(state || "").toUpperCase();
- const { pageNum, pageSize } = data.value.params;
+ // 완료 판단: SUCCEED* 이거나 상태가 비어있지만 finishedAt이 있으면 완료로 간주
+ if (s.includes("SUCCEED") || (!s && finishedAt)) return "Succeeded";
+
+ // 실패
+ if (s.includes("FAIL") || s.includes("ERROR")) return "Failed";
+
+ // 실행 중
+ if (s.includes("RUN")) return "Running";
+
+ // 대기
+ if (s.includes("PEND") || s.includes("QUEUE") || s.includes("SCHED"))
+ return "Pending";
+
+ // 모르겠으면 대기로
+ return "Pending";
+ };
return {
- no: (pageNum - 1) * pageSize + (idx + 1),
+ no, // ← 전달받은 번호 사용
name: r.displayName ?? r.name ?? r.runId ?? "(no name)",
- status: toUiStatus(r.state),
+ status: toUiStatus(r.state, r.finishedAt),
duration: fmtDuration(r.createdAt, r.finishedAt),
- experiment: r.experimentId ?? "-",
+ expName,
workflow: r.pipelineId ?? r.pipelineVersionId ?? "-",
startTime: fmtStart(r.createdAt),
registryStatus: r.storageState ?? "-",
@@ -149,6 +186,7 @@ async function fetchList() {
const payload = {
projectId: getProjectId(),
+
page: pageNum - 1, // 0-based
size: pageSize,
keyword,
@@ -199,7 +237,12 @@ async function fetchList() {
const bid = b.id ?? b.runId ?? b.run_id ?? b.name ?? "";
return String(bid).localeCompare(String(aid)); // 안정화
});
+ const expIds = list
+ .map((r) => r.experimentId ?? r.experiment_id ?? r.experiment?.id)
+ .filter((v) => v != null);
+ // ✅ 여기서 네가 만든 API로 display_name 미리 채움
+ await resolveExperimentNamesWithApi(expIds);
if (!isServerPaged) {
const total = list.length;
const pages = Math.max(1, Math.ceil(total / pageSize));
@@ -207,17 +250,29 @@ async function fetchList() {
const start = (safePage - 1) * pageSize;
const slice = list.slice(start, start + pageSize);
- data.value.results = slice.map((r, i) => toRow(r, i));
+ // ↓ 이번 페이지의 시작 번호(내림차순)
+ const startNo = total - (safePage - 1) * pageSize;
+ data.value.results = slice.map((r, i) =>
+ toRow(r, Math.max(startNo - i, 1)),
+ );
+
data.value.totalElements = total;
data.value.pageLength = pages;
} else {
- data.value.results = list.map((r, i) => toRow(r, i));
- data.value.totalElements =
+ const te =
typeof totalElements === "number" ? totalElements : list.length;
+
+ // ↓ 서버 페이징일 때도 동일하게 시작 번호 계산
+ const startNo = te - (pageNum - 1) * pageSize;
+ data.value.results = list.map((r, i) =>
+ toRow(r, Math.max(startNo - i, 1)),
+ );
+
+ data.value.totalElements = te;
data.value.pageLength =
typeof totalPages === "number"
? Math.max(1, totalPages)
- : Math.max(1, Math.ceil((data.value.totalElements || 0) / pageSize));
+ : Math.max(1, Math.ceil((te || 0) / pageSize));
}
} catch (err) {
console.error("[Executions] 조회 에러:", err);
@@ -305,9 +360,9 @@ const openInfoModal = (item: any) => {
execSelected.value = item;
openView.value = true;
};
-function closeView() {
+const closeView = () => {
openView.value = false;
-}
+};
const onSaved = () => fetchList();
const openCreateModal = () => {
data.value.modalMode = "create";
@@ -483,14 +538,13 @@ onMounted(() => {
mdi-help-circle
| {{ item.duration }} |
- {{ item.experiment }} |
+ {{ item.expName }} |
{{ item.workflow }} |
{{ item.startTime }} |
{{ item.registryStatus }} |
-
{
};
const openInfoModal = (item: any) => {
execSelected.value = item;
- console.log("[Parent] 선택된 실행:", item);
openView.value = true;
openCompare.value = false;
};
diff --git a/src/components/templates/run/executions/ViewComponent.vue b/src/components/templates/run/executions/ViewComponent.vue
index 258a75d..09ada4c 100644
--- a/src/components/templates/run/executions/ViewComponent.vue
+++ b/src/components/templates/run/executions/ViewComponent.vue
@@ -1,27 +1,277 @@
@@ -103,206 +356,445 @@ const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
- View Details
+ Compare Executions
-
-
- Execution Information
-
-
-
-
- Name
- {{ props.experimentInfo.name }}
-
-
-
-
- Status
-
- mdi-check-circle
- mdi-close-circle
- mdi-loading
- {{ props.experimentInfo.status }}
-
-
-
-
-
- Duration
- {{
- props.experimentInfo.duration
- }}
-
-
-
-
- Experiment ID
- {{
- props.experimentInfo.experiment
- }}
-
-
-
-
- Workflow
- {{
- props.experimentInfo.workflow
- }}
-
-
-
-
- Start Time
- {{
- props.experimentInfo.startTime
- }}
-
-
-
-
- Registry Status
- {{
- props.experimentInfo.registryStatus
- }}
-
-
-
-
-
-
- State History
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Details
+ Metrics
+
+
+
+
+
+
+
+ Execution Information
+
+
+
+
+ Name
+ {{ props.experimentInfo.name }}
+
+
+
+
+ Status
+
+ mdi-check-circle
-
- {{ s.icon }}
-
+ mdi-close-circle
+ {{ props.experimentInfo.status }}
+
+
+
+
+
+ Duration
+ {{ props.experimentInfo.duration }}
+
+
+
+
+ Experiment Name
+ {{ props.experimentInfo.expName }}
+
+
+
+
+ Workflow
+ {{ props.experimentInfo.workflow }}
+
+
+
+
+ Start Time
+ {{ props.experimentInfo.startTime }}
+
+
+
+
+ Registry Status
+ {{
+ props.experimentInfo.registryStatus
+ }}
+
+
+
+
+
+ State History
+
+
+
+
+
+
+
+
+
+
+ {{ s.icon }}
+
+
+
+
+ {{ s.label }}
+
+
+ {{ fmt(s.ts) }}
+
+
+
+
+
+
-
-
+ Back to List
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {{ s.label }}
-
-
- {{ fmt(s.ts) }}
-
-
-
-
-
-
-
-
-
- Back to List
-
-
+
+ Runs:
+ {{ runs.length.toLocaleString() }}
+
+
+
+
+
+
+
+ Selected Run
+
+
+
+
+
+
+ | Run ID |
+ {{ runDetail?.info?.run_id || "—" }} |
+
+
+ | Run Name |
+ {{ runDetail?.info?.run_name || "—" }} |
+
+
+ | Status |
+ {{ runDetail?.info?.status || "—" }} |
+
+
+ | Start |
+
+ {{
+ runDetail?.info?.start_time
+ ? new Date(
+ runDetail.info.start_time,
+ ).toLocaleString()
+ : "—"
+ }}
+ |
+
+
+ | End |
+
+ {{
+ runDetail?.info?.end_time
+ ? new Date(
+ runDetail.info.end_time,
+ ).toLocaleString()
+ : "—"
+ }}
+ |
+
+
+
+
+
+
+ Parameters
+
+
+
+ | Key |
+ Value |
+
+
+
+
+ |
+ No params
+ |
+
+
+ | {{ p.key }} |
+ {{ p.value }} |
+
+
+
+
+
+
+
+
+ Metrics
+
+
+
+ | Metric |
+ Value |
+
+
+
+
+ |
+ No metrics
+ |
+
+
+ | {{ m.key }} |
+ {{ m.value }} |
+
+
+
+
+
+
+ Tags
+
+
+
+ | Key |
+ Value |
+
+
+
+
+ |
+ No tags
+ |
+
+
+ | {{ t.key }} |
+
+ {{ t.value }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+ Model Metrics (selected run)
+
+
+
+
+ | Metric |
+ Value |
+
+
+
+
+ |
+ No Data
+ |
+
+
+ | {{ m.key }} |
+ {{ m.value }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (준비중) X/Y 축 선택 후 산점도 표시
+
+
+
+
+ (준비중) 메트릭 분포 Box Plot
+
+
+
+
+ (준비중) 2D/3D Contour Plot
+
+
+
+
+
+
+
+
diff --git a/src/components/templates/run/experiment/ViewComponent.vue b/src/components/templates/run/experiment/ViewComponent.vue
index 08080cc..9d77da3 100644
--- a/src/components/templates/run/experiment/ViewComponent.vue
+++ b/src/components/templates/run/experiment/ViewComponent.vue
@@ -1,138 +1,146 @@
@@ -150,6 +158,7 @@ watch(
+
-
Experiment Name
- {{
- experimentInfo.experimentName
- }}
+ {{ header.experimentName }}
-
Project Name
- {{
- experimentInfo.projectName
- }}
+ {{ header.projectName }}
-
Created ID
- {{ experimentInfo.createdId }}
+ {{ header.createdId }}
Created Date
- {{
- experimentInfo.createdDate
- }}
-
-
-
-
-
- Kubeflow ID
- {{ experimentInfo.kubeFlowId }}
- MLflow ID
- {{ experimentInfo.mlFlowId }}
+ {{ header.createdDate }}
-
Description
- {{
- experimentInfo.description
- }}
+ {{ header.description }}
@@ -220,6 +208,110 @@ watch(
>
+
+
+
+
+
+ Runs
+
+
+
+
+
+ 총 {{ runRows.length.toLocaleString() }}개 · Experiment ID:
+ {{ experimentId || "-" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Run Name |
+ Status |
+ Duration |
+ Pipeline |
+ Start Time |
+
+
+
+
+
+ |
+ No runs
+ |
+
+
+
+
+ |
+ {{ r.runName }}
+ |
+
+
+ mdi-check-circle
+ mdi-close-circle
+
+ mdi-help-circle
+ {{ r.status }}
+ |
+
+
+ {{ r.duration }}
+ |
+
+
+ {{ r.pipeline }}
+ |
+
+
+ {{ r.startTime }}
+ |
+
+
+
+
+
+
+
+
Back to List
diff --git a/src/components/templates/trainingscript/ListComponent.vue b/src/components/templates/trainingscript/ListComponent.vue
index f132052..e4f34b0 100644
--- a/src/components/templates/trainingscript/ListComponent.vue
+++ b/src/components/templates/trainingscript/ListComponent.vue
@@ -355,7 +355,7 @@ watch(
- Training Script
+ TrainingScript
diff --git a/src/components/templates/trainingscriptgroup/ListComponent.vue b/src/components/templates/trainingscriptgroup/ListComponent.vue
index bdaff10..524675f 100644
--- a/src/components/templates/trainingscriptgroup/ListComponent.vue
+++ b/src/components/templates/trainingscriptgroup/ListComponent.vue
@@ -11,8 +11,8 @@ import { DataGroupService } from "@/components/service/management/DataGroupServi
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
-import DatagroupBaseDoalog from "@/components/atoms/organisms/DatagroupBaseDoalog.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
+import TrainingGroupBaseDoalog from "@/components/atoms/organisms/TrainingGroupBaseDoalog.vue";
/* -------------------------
* Dayjs & Router
@@ -153,6 +153,7 @@ async function fetchList() {
size: reqSize,
keyword,
searchType: mapped,
+ refType: "TRAINING_SCRIPT",
};
const res: any = await DataGroupService.search(payload);
if (res?.status !== 200) return;
@@ -455,7 +456,7 @@ onMounted(fetchList);
Create DataGroupCreate TrainingGroup
@@ -508,7 +509,6 @@ onMounted(fetchList);
@click.stop
@mousedown.stop
>
-
- {
if (res.status !== 200) return;
const result = res.data;
- console.log("Workflows", result);
let list = result?.content ?? [];
if (needLocalFilter) {
@@ -220,8 +219,6 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
data.value.selected = [];
data.value.allSelected = false;
};
- console.log(ids.length);
-
if (ids.length === 1) {
remove(ids[0])
.then(() => {
diff --git a/src/layouts/TopNav.vue b/src/layouts/TopNav.vue
new file mode 100644
index 0000000..3ee2b94
--- /dev/null
+++ b/src/layouts/TopNav.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+ Autoflow Web Console
+
+
+
+
+
+
+
+
+
+
+ {{ m.title }}
+
+
+
+
+
+
+
+
+
+
+ {{ m.title }}
+
+
+
+
+
+
+
+
+ mdi-home
+ mdi-cog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vite.config.mjs b/vite.config.mjs
index f61162d..b3e6abf 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -14,7 +14,8 @@ import { fileURLToPath, URL } from "node:url";
// https://vitejs.dev/config/
export default defineConfig({
- base: process.env.NODE_ENV === "production" ? process.env.VITE_ROOT_PATH : "/",
+ base:
+ process.env.NODE_ENV === "production" ? process.env.VITE_ROOT_PATH : "/",
plugins: [
VueRouter(),
Layouts(),
|