executions 상세페이지 단건 조회, workflow yaml 파일 조회

main
jschoi 8 months ago
parent 96d6d13f61
commit 07f63adc00

@ -22,4 +22,7 @@ export const WorkflowService = {
search: (payload: WorkflowSearch) => {
return request.get("/api/workflows/search", payload);
},
pipelineIdName: (pipelineId: string | number) => {
return request.get(`/api/workflows/pipeline/${pipelineId}`, {});
},
};

@ -10,6 +10,7 @@ import { ExperimentService } from "@/components/service/management/ExperimentSer
import { commonStore } from "@/stores/commonStore";
import { KubeflowRunService } from "@/components/service/management/KubeflowRunService";
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
import { WorkflowService } from "@/components/service/management/WorkflowService";
const store = commonStore();
@ -17,6 +18,7 @@ const openView = ref(false);
const execSelected = ref<any>(null);
const username = ref<string>("");
const experimentNameMap = ref<Record<string, string>>({});
const pipelineNameMap = ref<Record<string, string>>({});
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
@ -65,6 +67,89 @@ const data = ref({
isCreateVisible: false,
});
// ---------- NEW: / ----------
function pick<T = any>(obj: any, keys: string[]): T | undefined {
for (const k of keys) {
const v = obj?.[k];
if (v !== undefined && v !== null && v !== "") return v as T;
}
return undefined;
}
function normTime(v: any): string | undefined {
if (!v) return undefined;
const d = new Date(v);
return isNaN(d.getTime()) ? undefined : d.toISOString();
}
function normalizeRun(raw: any) {
const stateRaw =
pick<string>(raw, [
"state",
"status",
"lifecycle_state",
"lifecycleState",
"lifecycleStage", // MLflow
"phase", // K8s
]) ?? "";
const createdAt =
normTime(
pick(raw, ["createdAt", "startTime", "startedAt", "start_time"]),
) ?? undefined;
const finishedAt =
normTime(
pick(raw, [
"finishedAt",
"finished_at",
"endTime",
"end_time",
"completedAt",
"completed_at",
"terminateTime",
]),
) ?? undefined;
return { stateRaw, createdAt, finishedAt };
}
function toUiStatusByRaw(
raw: any,
): "Succeeded" | "Failed" | "Running" | "Pending" {
const { stateRaw, finishedAt } = normalizeRun(raw);
const s = String(stateRaw || "").toUpperCase();
if (
s.includes("SUCCEED") ||
s.includes("SUCCESS") ||
s.includes("FINISH") || // FINISHED
s.includes("COMPLETE") || // COMPLETED
s === "DONE" ||
s === "OK"
)
return "Succeeded";
if (
s.includes("FAIL") ||
s.includes("ERROR") ||
s.includes("CANCEL") ||
s.includes("KILL") ||
s.includes("TERMINAT") ||
s.includes("ABORT")
)
return "Failed";
if (s.includes("RUN") || s.includes("ACTIVE") || s.includes("EXECUT"))
return "Running";
if (!s && finishedAt) return "Succeeded";
if (s.includes("PEND") || s.includes("QUEUE") || s.includes("SCHED"))
return "Pending";
return "Pending";
}
// -------------------------------------------------
async function resolveExperimentNamesWithApi(ids: Array<string | number>) {
const targets = Array.from(
new Set(
@ -78,20 +163,43 @@ async function resolveExperimentNamesWithApi(ids: Array<string | number>) {
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
} catch {
experimentNameMap.value[id] = String(id);
}
}),
);
}
async function resolvePipelineNamesWithApi(ids: Array<string | number>) {
const targets = Array.from(
new Set(
(ids || [])
.map((x) => String(x))
.filter((id) => id && pipelineNameMap.value[id] == null),
),
);
if (!targets.length) return;
await Promise.all(
targets.map(async (id) => {
try {
const res = await WorkflowService.pipelineIdName(id);
const body = res?.data ?? res ?? {};
const name =
body.displayName ?? body.display_name ?? body.name ?? String(id);
pipelineNameMap.value[id] = name;
} catch {
pipelineNameMap.value[id] = String(id);
}
}),
);
}
function readUsernameFromStorage(): string {
try {
const raw =
@ -121,10 +229,20 @@ const toRow = (r: any, no: number) => {
r.experiment?.displayName ??
r.experiment?.name ??
"-";
const fmtStart = (start?: string) => {
if (!start) return "-";
const d = new Date(start);
if (isNaN(d.getTime())) return start;
const pipelineId =
r.pipelineId ?? r.pipelineVersionId ?? r.pipeline?.id ?? r.pipeline_id;
const wfName =
(pipelineId && pipelineNameMap.value[String(pipelineId)]) ??
r.pipeline?.displayName ??
r.pipeline?.name ??
"-";
const { createdAt: createdIso, finishedAt: finishedIso } = normalizeRun(r);
const fmt = (iso?: string) => {
if (!iso) return "-";
const d = new Date(iso);
const yyyy = d.getFullYear();
const MM = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
@ -133,9 +251,9 @@ const toRow = (r: any, no: number) => {
return `${yyyy}-${MM}-${dd} ${hh}:${mi}`;
};
const fmtDuration = (start?: string, end?: string) => {
if (!start || !end) return "-";
const ms = new Date(end).getTime() - new Date(start).getTime();
const fmtDuration = (startIso?: string, endIso?: string) => {
if (!startIso || !endIso) return "-";
const ms = new Date(endIso).getTime() - new Date(startIso).getTime();
if (!isFinite(ms) || ms < 0) return "-";
const s = Math.floor(ms / 1000);
const h = Math.floor(s / 3600);
@ -145,58 +263,98 @@ const toRow = (r: any, no: number) => {
return `${h}:${pad(m)}:${pad(sec)}`;
};
const toUiStatus = (state?: string, finishedAt?: string) => {
const s = String(state || "").toUpperCase();
// : 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, //
no,
name: r.displayName ?? r.name ?? r.runId ?? "(no name)",
status: toUiStatus(r.state, r.finishedAt),
duration: fmtDuration(r.createdAt, r.finishedAt),
status: toUiStatusByRaw(r),
duration: fmtDuration(createdIso, finishedIso),
expName,
workflow: r.pipelineId ?? r.pipelineVersionId ?? "-",
startTime: fmtStart(r.createdAt),
workflow: wfName,
startTime: fmt(createdIso),
registryStatus: r.storageState ?? "-",
run_id: r.runId,
raw: r,
};
};
// --- [] : // ---
function includes(hay: any, needle: string) {
if (!hay) return false;
return String(hay).toLowerCase().includes(needle.toLowerCase());
}
function matchBySearchType(
rowRaw: any,
mapped: "ALL" | "TITLE" | "AUTHOR",
kw: string,
) {
if (!kw) return true;
//
const titleFields = [
rowRaw.displayName,
rowRaw.name,
rowRaw.runName,
rowRaw.title,
rowRaw.pipeline?.displayName,
rowRaw.pipeline?.name,
];
//
const authorFields = [
rowRaw.username,
rowRaw.userName,
rowRaw.owner,
rowRaw.createdBy,
rowRaw.user?.name,
rowRaw.user?.username,
];
if (mapped === "TITLE") {
return titleFields.some((v) => includes(v, kw));
}
if (mapped === "AUTHOR") {
return authorFields.some((v) => includes(v, kw));
}
// ALL: / + ,
const extraFields = [
rowRaw.description,
rowRaw.desc,
rowRaw.tags,
rowRaw.tagString,
];
return [...titleFields, ...authorFields, ...extraFields].some((v) =>
includes(v, kw),
);
}
async function fetchList() {
const { pageNum, pageSize, searchType, searchText } = data.value.params;
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
const keyword = (searchText || "").trim();
const payload = {
// alias
const payload: any = {
projectId: getProjectId(),
page: pageNum - 1, // 0-based
size: pageSize,
keyword,
searchType: mapped,
sortField: "createdAt",
sortDirection: "DESC",
// --- keyword aliases ---
keyword,
searchKeyword: keyword,
query: keyword,
q: keyword,
search_text: keyword,
searchText: keyword,
// --- type aliases ---
searchType: mapped,
type: mapped,
filterType: mapped,
};
try {
const res = await KubeflowRunService.search(payload as any);
const res = await KubeflowRunService.search(payload);
const result = res?.data ?? res;
let list: any[] = [];
@ -213,7 +371,16 @@ async function fetchList() {
isServerPaged = true;
} else if (Array.isArray(result?.runs)) list = result.runs;
// ( )
// 💡 :
if (keyword) {
list = list.filter((raw) => matchBySearchType(raw, mapped, keyword));
// totalElements ,
totalElements = list.length;
totalPages = Math.ceil(list.length / pageSize);
isServerPaged = false;
}
// ( )
list.sort((a, b) => {
const ta = new Date(
a.createdAt ||
@ -232,42 +399,51 @@ async function fetchList() {
0,
).getTime();
if (tb !== ta) return tb - ta; //
if (tb !== ta) return tb - ta;
const aid = a.id ?? a.runId ?? a.run_id ?? a.name ?? "";
const bid = b.id ?? b.runId ?? b.run_id ?? b.name ?? "";
return String(bid).localeCompare(String(aid)); //
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
.filter(Boolean);
await resolveExperimentNamesWithApi(expIds);
const pipeIds = list
.map(
(r) =>
r.pipelineId ??
r.pipelineVersionId ??
r.pipeline?.id ??
r.pipeline_id,
)
.filter(Boolean);
await resolvePipelineNamesWithApi(pipeIds);
//
if (!isServerPaged) {
const total = list.length;
const total =
typeof totalElements === "number" ? totalElements : list.length;
const pages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(Math.max(1, pageNum), pages);
const start = (safePage - 1) * pageSize;
const slice = list.slice(start, start + pageSize);
// ()
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 {
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"
@ -525,7 +701,6 @@ onMounted(() => {
width="2"
color="info"
/>
<v-icon
v-else-if="item.status === 'Pending'"
color="grey"
@ -533,13 +708,10 @@ onMounted(() => {
>
mdi-loading
</v-icon>
<!-- -->
<v-icon v-else color="grey">mdi-help-circle</v-icon>
</td>
<td>{{ item.duration }}</td>
<td>{{ item.expName }}</td>
<td>{{ item.workflow }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.registryStatus }}</td>

@ -31,12 +31,9 @@ const selectedRunId = ref<string>("");
const loadingRunDetail = ref(false);
const runDetail = ref<any | null>(null);
/* 목록 옵션 (run_name 표시, 값은 run_id / run_uuid) */
const runOptions = computed(() =>
runs.value.map((r) => ({
title: r?.info?.run_name || r?.info?.run_id || "—",
value: r?.info?.run_id || r?.info?.run_uuid || "",
})),
/* 표시용 라벨 */
const selectedLabel = computed(
() => runDetail.value?.info?.run_name ?? runDetail.value?.info?.run_id ?? "-",
);
/* ============ Plotly refs ============ */
@ -52,9 +49,6 @@ function metricValue(key: "accuracy" | "precision" | "recall" | "f1_score") {
);
return m?.value ?? null;
}
const selectedLabel = computed(
() => runDetail.value?.info?.run_name ?? runDetail.value?.info?.run_id ?? "-",
);
/* 선택한 run의 메트릭 표 데이터 */
const selectedMetrics = computed(() => {
@ -63,62 +57,72 @@ const selectedMetrics = computed(() => {
return mm.map((m) => ({ key: m.key, value: m.value }));
});
/* ===== 부모에서 내려온 runId 픽 ===== */
function pickParentRunId(v: any): string {
const cand = [
v?.run_id,
v?.runId,
v?.raw?.run_id,
v?.raw?.runId,
v?.raw?.kfp_run_id,
v?.raw?.kubeflow_run_id,
v?.raw?.id,
].filter(Boolean);
return String(cand[0] ?? "");
}
/* ===== MLflow 태그 읽기 (array or object 모두 대응) ===== */
function getTag(run: any, key: string): string | undefined {
const tags = run?.data?.tags;
if (Array.isArray(tags)) return tags.find((t: any) => t?.key === key)?.value;
if (tags && typeof tags === "object") return tags[key];
return undefined;
}
/* ===== 차트 렌더: 선택한 단건만 ===== */
function drawCharts() {
// Plotly Layout
const baseLayout = (titleText: string, xlabel: string): Partial<any> => ({
title: { text: titleText }, //
title: { text: titleText },
margin: { t: 40, r: 20, b: 40, l: 40 },
height: 290,
yaxis: { rangemode: "tozero" },
xaxis: {
tickmode: "array",
tickvals: [xlabel],
ticktext: [xlabel],
},
xaxis: { tickmode: "array", tickvals: [xlabel], ticktext: [xlabel] },
showlegend: false,
});
const config = { displayModeBar: false, responsive: true };
if (elAccuracy.value) {
const x = ["accuracy"];
if (elAccuracy.value)
Plotly.react(
elAccuracy.value,
[{ x, y: [metricValue("accuracy")], type: "bar" }],
baseLayout("accuracy", x[0]),
[{ x: ["accuracy"], y: [metricValue("accuracy")], type: "bar" }],
baseLayout("accuracy"),
config,
);
}
if (elF1.value) {
const x = ["f1_score"];
if (elF1.value)
Plotly.react(
elF1.value,
[{ x, y: [metricValue("f1_score")], type: "bar" }],
baseLayout("f1_score", x[0]),
[{ x: ["f1_score"], y: [metricValue("f1_score")], type: "bar" }],
baseLayout("f1_score"),
config,
);
}
if (elPrecision.value) {
const x = ["precision"];
if (elPrecision.value)
Plotly.react(
elPrecision.value,
[{ x, y: [metricValue("precision")], type: "bar" }],
baseLayout("precision", x[0]),
[{ x: ["precision"], y: [metricValue("precision")], type: "bar" }],
baseLayout("precision"),
config,
);
}
if (elRecall.value) {
const x = ["recall"];
if (elRecall.value)
Plotly.react(
elRecall.value,
[{ x, y: [metricValue("recall")], type: "bar" }],
baseLayout("recall", x[0]),
[{ x: ["recall"], y: [metricValue("recall")], type: "bar" }],
baseLayout("recall"),
config,
);
}
}
function resizeCharts() {
[elAccuracy.value, elF1.value, elPrecision.value, elRecall.value]
.filter(Boolean)
@ -142,30 +146,44 @@ function findFromList(runId: string) {
}
/* ============ API: runs 목록 & 단건 run ============ */
async function fetchRunsOnce(expName?: string) {
if (!expName || runs.value.length > 0) return;
async function fetchRuns(expName?: string) {
const parentRunId = pickParentRunId(props.experimentInfo);
if (!expName || !parentRunId) {
runs.value = [];
selectedRunId.value = "";
return;
}
loadingRuns.value = true;
try {
const expRes = await MlflowService.getExperimentByName(expName);
const exp = expRes?.data ?? exp;
const exp = expRes?.data ?? expRes;
const expId = String(
exp?.experiment_id ?? exp?.experimentId ?? exp?.id ?? "",
);
if (!expId) return;
if (!expId) {
runs.value = [];
selectedRunId.value = "";
return;
}
const runsRes = await MlflowService.getRuns(expId);
const body = runsRes?.data ?? runsRes;
const list =
body?.runs ?? body?.data?.runs ?? (Array.isArray(body) ? body : []);
runs.value = Array.isArray(list) ? list : [];
// : run
const first = [...runs.value].sort(
// runId MLflow tag(kubeflow_run_id)
const filtered = (Array.isArray(list) ? list : []).filter(
(r) => getTag(r, "kubeflow_run_id") === parentRunId,
);
runs.value = filtered;
// : 1
const first = [...filtered].sort(
(a, b) => (b?.info?.start_time ?? 0) - (a?.info?.start_time ?? 0),
)[0];
selectedRunId.value =
first?.info?.run_id || first?.info?.run_uuid || selectedRunId.value || "";
selectedRunId.value = first?.info?.run_id || first?.info?.run_uuid || "";
} finally {
loadingRuns.value = false;
}
@ -174,7 +192,6 @@ async function fetchRunsOnce(expName?: string) {
async function fetchRunDetail(runId: string) {
if (!runId) {
runDetail.value = null;
//
await nextTick();
drawCharts();
return;
@ -191,19 +208,30 @@ async function fetchRunDetail(runId: string) {
}
/* ============ 트리거 ============ */
// Visualizations +
// viz
watch(
() => mainTab.value,
async (t) => {
if (t === "viz") {
await fetchRunsOnce(pickExpName(props.experimentInfo));
if (selectedRunId.value) await fetchRunDetail(selectedRunId.value);
await fetchRuns(pickExpName(props.experimentInfo));
await fetchRunDetail(selectedRunId.value);
}
},
{ immediate: true },
);
// Run
// execution viz
watch(
() => props.experimentInfo,
async () => {
if (mainTab.value === "viz") {
await fetchRuns(pickExpName(props.experimentInfo));
await fetchRunDetail(selectedRunId.value);
}
},
);
// runId
watch(selectedRunId, (id) => fetchRunDetail(id));
// metrics
@ -234,7 +262,6 @@ const rawHistory = computed<any[]>(() => {
if (Array.isArray(h) && h.length > 0) return h;
// fallback
const startIso =
props.experimentInfo?.startTime &&
!isNaN(new Date(props.experimentInfo.startTime).getTime())
@ -512,25 +539,23 @@ const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
<v-window-item value="metrics">
<v-card-text>
<v-row class="mb-4" align="center">
<v-col cols="12" md="6">
<v-select
v-model="selectedRunId"
:items="runOptions"
item-title="title"
item-value="value"
label="Select run (run_uuid)"
<v-col cols="12" class="pa-0">
<v-alert
v-if="!loadingRuns && runs.length === 0"
type="info"
variant="tonal"
class="mx-3"
density="comfortable"
:loading="loadingRuns"
clearable
/>
>
부모 runId와 일치하는 MLflow run이 없습니다.
</v-alert>
</v-col>
<v-col
cols="12"
md="6"
class="d-flex align-center justify-end"
class="d-flex align-center justify-between"
>
<div class="text-body-2">
Runs:
Runs (matched):
<strong>{{ runs.length.toLocaleString() }}</strong>
<v-progress-circular
v-if="loadingRuns || loadingRunDetail"
@ -539,6 +564,9 @@ const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
class="ml-2"
/>
</div>
<div class="text-body-2">
Selected: <strong>{{ selectedLabel }}</strong>
</div>
</v-col>
</v-row>
@ -711,48 +739,51 @@ const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
<!-- (C) 2×2 단건 바차트 -->
<v-row>
<v-col cols="12" md="6">
<div
<v-col cols="12" md="6"
><div
ref="elAccuracy"
style="width: 100%; height: 290px"
></div>
</v-col>
<v-col cols="12" md="6">
<div ref="elF1" style="width: 100%; height: 290px"></div>
</v-col>
<v-col cols="12" md="6">
<div
></div
></v-col>
<v-col cols="12" md="6"
><div ref="elF1" style="width: 100%; height: 290px"></div
></v-col>
<v-col cols="12" md="6"
><div
ref="elPrecision"
style="width: 100%; height: 290px"
></div>
</v-col>
<v-col cols="12" md="6">
<div
></div
></v-col>
<v-col cols="12" md="6"
><div
ref="elRecall"
style="width: 100%; height: 290px"
></div>
</v-col>
></div
></v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- placeholders -->
<v-window-item value="scatter">
<v-card-text class="px-6 py-10 text-medium-emphasis">
(준비중) X/Y 선택 산점도 표시
</v-card-text>
<v-card-text class="px-6 py-10 text-medium-emphasis"
>(준비중) X/Y 선택 산점도 표시</v-card-text
>
</v-window-item>
<v-window-item value="box">
<v-card-text class="px-6 py-10 text-medium-emphasis">
(준비중) 메트릭 분포 Box Plot
</v-card-text>
<v-card-text class="px-6 py-10 text-medium-emphasis"
>(준비중) 메트릭 분포 Box Plot</v-card-text
>
</v-window-item>
<v-window-item value="contour">
<v-card-text class="px-6 py-10 text-medium-emphasis">
(준비중) 2D/3D Contour Plot
</v-card-text>
<v-card-text class="px-6 py-10 text-medium-emphasis"
>(준비중) 2D/3D Contour Plot</v-card-text
>
</v-window-item>
</v-window>
<v-sheet class="d-flex justify-end mb-2">
<v-btn color="primary" @click="emit('close')">Back to List</v-btn>
</v-sheet>
</v-card>
</v-window-item>
</v-window>

@ -40,20 +40,7 @@ const detail = ref({
regDt: "",
});
const defaultYaml = `# YAML not provided by server
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: dummy-
spec:
entrypoint: main
templates:
- name: main
container:
image: alpine:latest
command: [sh, -c]
args: ["echo hello"]
`;
const defaultYaml = "";
//
function formatDateTime(raw?: string): string {
@ -63,12 +50,54 @@ function formatDateTime(raw?: string): string {
return m ? m[1] : s.slice(0, 19);
}
async function loadYamlByAttachmentId(attachmentId?: number | string) {
if (attachmentId == null || attachmentId === "") return false;
try {
console.log("[YAML] call /api/attachments/{id} with id:", attachmentId);
const attRes = await AttachmentsService.view(Number(attachmentId)); // GET /api/attachments/{id}
const att = attRes?.data ?? attRes ?? {};
console.log("[YAML] attachments response:", att);
const storagePath =
att.storagePath ||
att.storedName ||
att.objectName ||
att.object_key ||
"";
if (!storagePath) {
console.warn("[YAML] storagePath not found on attachment:", att);
ensureEditor();
editorInstance?.setValue(defaultYaml);
return false;
}
console.log("[YAML] call readYamlText with objectName:", storagePath);
const textRes = await AttachmentsService.readTextByPath(storagePath);
const text =
typeof textRes?.data === "string"
? textRes.data
: String(textRes?.data ?? "");
ensureEditor();
editorInstance?.setValue(text || defaultYaml);
return Boolean(text);
} catch (e) {
console.error("[YAML] loadYamlByAttachmentId failed:", e);
ensureEditor();
editorInstance?.setValue(defaultYaml);
return false;
}
}
/** ⬅️ 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();
@ -83,10 +112,14 @@ async function loadYamlFromStoragePath(objectName?: string) {
/** 상세 조회 → YAML 문자열 우선 → storagePath 후보들 시도 → 실패 시 기본 YAML */
async function fetchDetail(id: number | string) {
try {
// 0) YAML attachments -> readYamlText
await loadYamlByAttachmentId(id);
// 1) () API
const res = await WorkflowService.view(Number(id));
const d = res?.data ?? {};
console.log("[Workflow Detail] view response:", d);
// (/ )
detail.value = {
name: d.name ?? d.workflowName ?? "",
version: String(d.version ?? ""),
@ -97,9 +130,7 @@ async function fetchDetail(id: number | string) {
regDt: formatDateTime(d.reg_dt ?? d.regDt ?? d.regDate),
};
ensureEditor();
// 1) YAML
// YAML
const yamlFromServer =
d.workflowYaml ||
d.yaml ||
@ -108,33 +139,9 @@ async function fetchDetail(id: number | string) {
d.yamlStr ||
"";
if (yamlFromServer) {
ensureEditor();
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();
@ -144,7 +151,6 @@ async function fetchDetail(id: number | string) {
// ===== & =====
onMounted(() => {
// ok (automaticLayout)
ensureEditor();
});
@ -278,42 +284,6 @@ const steps = ref<
</v-btn>
</v-sheet>
</v-card>
<!--
==================== [나중에 사용할 Step Overview 섹션] ====================
<v-card
flat
class="bordered-box mb-6 w-100 rounded-lg pa-8"
style="min-height: 500px"
>
<v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Step Overview</span>
</v-card-title>
<v-data-table
:headers="stepHeaders"
:items="steps"
dense
class="text-center"
hide-default-footer
:items-per-page="5"
header-color="primary"
disable-sort
>
<template #item.order="{ index }">{{ index + 1 }}</template>
<template #item.status="{ item }">
<v-chip
:color="({ Configured: 'success', 'Not Configured': 'warning' } as any)[item.status] || 'default'"
small
dark
>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
</v-card>
========================================================================
-->
</template>
<!-- YAML -->

Loading…
Cancel
Save