|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { MlflowService } from "@/components/service/mlflow/MlflowService";
|
|
|
|
|
|
import {
|
|
|
|
|
|
computed,
|
|
|
|
|
|
watch,
|
|
|
|
|
|
ref,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
onMounted,
|
|
|
|
|
|
onBeforeUnmount,
|
|
|
|
|
|
} from "vue";
|
|
|
|
|
|
import Plotly from "plotly.js-dist-min";
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ experimentInfo: any }>();
|
|
|
|
|
|
const emit = defineEmits<{ (e: "close"): void }>();
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ Tabs ============ */
|
|
|
|
|
|
const mainTab = ref<"details" | "viz">("details");
|
|
|
|
|
|
const vizTab = ref<"metrics" | "scatter" | "box" | "contour">("metrics");
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ expName pick ============ */
|
|
|
|
|
|
function pickExpName(v: any): string {
|
|
|
|
|
|
return v?.expName || v?.experiment || v?.name || v?.raw?.displayName || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ MLflow state ============ */
|
|
|
|
|
|
const runs = ref<any[]>([]);
|
|
|
|
|
|
const loadingRuns = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
/* --- 단건 조회 state --- */
|
|
|
|
|
|
const selectedRunId = ref<string>("");
|
|
|
|
|
|
const loadingRunDetail = ref(false);
|
|
|
|
|
|
const runDetail = ref<any | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
/* 표시용 라벨 */
|
|
|
|
|
|
const selectedLabel = computed(
|
|
|
|
|
|
() => runDetail.value?.info?.run_name ?? runDetail.value?.info?.run_id ?? "-",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ Plotly refs ============ */
|
|
|
|
|
|
const elAccuracy = ref<HTMLDivElement | null>(null);
|
|
|
|
|
|
const elF1 = ref<HTMLDivElement | null>(null);
|
|
|
|
|
|
const elPrecision = ref<HTMLDivElement | null>(null);
|
|
|
|
|
|
const elRecall = ref<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== 유틸: 단건 값 꺼내기 ===== */
|
|
|
|
|
|
function metricValue(key: "accuracy" | "precision" | "recall" | "f1_score") {
|
|
|
|
|
|
const m = (runDetail.value?.data?.metrics ?? []).find(
|
|
|
|
|
|
(x: any) => x.key === key,
|
|
|
|
|
|
);
|
|
|
|
|
|
return m?.value ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 선택한 run의 메트릭 표 데이터 */
|
|
|
|
|
|
const selectedMetrics = computed(() => {
|
|
|
|
|
|
const mm: Array<{ key: string; value: number }> =
|
|
|
|
|
|
runDetail.value?.data?.metrics ?? [];
|
|
|
|
|
|
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() {
|
|
|
|
|
|
const baseLayout = (titleText: string, xlabel: string): Partial<any> => ({
|
|
|
|
|
|
title: { text: titleText },
|
|
|
|
|
|
margin: { t: 40, r: 20, b: 40, l: 40 },
|
|
|
|
|
|
height: 290,
|
|
|
|
|
|
yaxis: { rangemode: "tozero" },
|
|
|
|
|
|
xaxis: { tickmode: "array", tickvals: [xlabel], ticktext: [xlabel] },
|
|
|
|
|
|
showlegend: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
const config = { displayModeBar: false, responsive: true };
|
|
|
|
|
|
|
|
|
|
|
|
if (elAccuracy.value)
|
|
|
|
|
|
Plotly.react(
|
|
|
|
|
|
elAccuracy.value,
|
|
|
|
|
|
[{ x: ["accuracy"], y: [metricValue("accuracy")], type: "bar" }],
|
|
|
|
|
|
baseLayout("accuracy"),
|
|
|
|
|
|
config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (elF1.value)
|
|
|
|
|
|
Plotly.react(
|
|
|
|
|
|
elF1.value,
|
|
|
|
|
|
[{ x: ["f1_score"], y: [metricValue("f1_score")], type: "bar" }],
|
|
|
|
|
|
baseLayout("f1_score"),
|
|
|
|
|
|
config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (elPrecision.value)
|
|
|
|
|
|
Plotly.react(
|
|
|
|
|
|
elPrecision.value,
|
|
|
|
|
|
[{ x: ["precision"], y: [metricValue("precision")], type: "bar" }],
|
|
|
|
|
|
baseLayout("precision"),
|
|
|
|
|
|
config,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (elRecall.value)
|
|
|
|
|
|
Plotly.react(
|
|
|
|
|
|
elRecall.value,
|
|
|
|
|
|
[{ x: ["recall"], y: [metricValue("recall")], type: "bar" }],
|
|
|
|
|
|
baseLayout("recall"),
|
|
|
|
|
|
config,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
function resizeCharts() {
|
|
|
|
|
|
[elAccuracy.value, elF1.value, elPrecision.value, elRecall.value]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.forEach((el: any) => Plotly.Plots.resize(el));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== 응답 정규화 + fallback ===== */
|
|
|
|
|
|
function normalizeRun(res: any) {
|
|
|
|
|
|
const v = res?.data?.run ?? res?.run ?? res?.data ?? res;
|
|
|
|
|
|
return v?.info && v?.data ? v : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function findFromList(runId: string) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
runs.value.find(
|
|
|
|
|
|
(r) =>
|
|
|
|
|
|
r?.info?.run_id === runId ||
|
|
|
|
|
|
r?.info?.run_uuid === runId ||
|
|
|
|
|
|
r?.info?.run_name === runId,
|
|
|
|
|
|
) ?? null
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ API: runs 목록 & 단건 run ============ */
|
|
|
|
|
|
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 ?? expRes;
|
|
|
|
|
|
const expId = String(
|
|
|
|
|
|
exp?.experiment_id ?? exp?.experimentId ?? exp?.id ?? "",
|
|
|
|
|
|
);
|
|
|
|
|
|
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 : []);
|
|
|
|
|
|
|
|
|
|
|
|
// 부모 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 || "";
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingRuns.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchRunDetail(runId: string) {
|
|
|
|
|
|
if (!runId) {
|
|
|
|
|
|
runDetail.value = null;
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
drawCharts();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
loadingRunDetail.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await MlflowService.getExperimentRun(runId);
|
|
|
|
|
|
runDetail.value = normalizeRun(res) || findFromList(runId);
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
drawCharts();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingRunDetail.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ 트리거 ============ */
|
|
|
|
|
|
// 탭 전환 시 viz 탭이면 로드
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => mainTab.value,
|
|
|
|
|
|
async (t) => {
|
|
|
|
|
|
if (t === "viz") {
|
|
|
|
|
|
await fetchRuns(pickExpName(props.experimentInfo));
|
|
|
|
|
|
await fetchRunDetail(selectedRunId.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 부모 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 서브탭일 때 리렌더
|
|
|
|
|
|
watch(vizTab, async (t) => {
|
|
|
|
|
|
if (mainTab.value === "viz" && t === "metrics") {
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
drawCharts();
|
|
|
|
|
|
resizeCharts();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function onResize() {
|
|
|
|
|
|
if (mainTab.value === "viz" && vizTab.value === "metrics") {
|
|
|
|
|
|
resizeCharts();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
onMounted(() => window.addEventListener("resize", onResize));
|
|
|
|
|
|
onBeforeUnmount(() => window.removeEventListener("resize", onResize));
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ State History (Details 탭) ============ */
|
|
|
|
|
|
const rawHistory = computed<any[]>(() => {
|
|
|
|
|
|
const h =
|
|
|
|
|
|
props.experimentInfo?.raw?.state_history ??
|
|
|
|
|
|
props.experimentInfo?.raw?.stateHistory ??
|
|
|
|
|
|
props.experimentInfo?.state_history ??
|
|
|
|
|
|
props.experimentInfo?.stateHistory ??
|
|
|
|
|
|
[];
|
|
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(h) && h.length > 0) return h;
|
|
|
|
|
|
|
|
|
|
|
|
const startIso =
|
|
|
|
|
|
props.experimentInfo?.startTime &&
|
|
|
|
|
|
!isNaN(new Date(props.experimentInfo.startTime).getTime())
|
|
|
|
|
|
? new Date(props.experimentInfo.startTime).toISOString()
|
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const latest = [...(runs.value ?? [])].sort(
|
|
|
|
|
|
(a, b) => (b?.info?.start_time ?? 0) - (a?.info?.start_time ?? 0),
|
|
|
|
|
|
)[0];
|
|
|
|
|
|
|
|
|
|
|
|
const endIso =
|
|
|
|
|
|
latest?.info?.end_time && isFinite(Number(latest.info.end_time))
|
|
|
|
|
|
? new Date(Number(latest.info.end_time)).toISOString()
|
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const status = String(props.experimentInfo?.status ?? "").toUpperCase();
|
|
|
|
|
|
|
|
|
|
|
|
const out: Array<{ state: string; update_time?: string }> = [];
|
|
|
|
|
|
if (startIso) out.push({ state: "PENDING", update_time: startIso });
|
|
|
|
|
|
if (startIso) out.push({ state: "RUNNING", update_time: startIso });
|
|
|
|
|
|
if (status === "SUCCEEDED" || status === "FAILED") {
|
|
|
|
|
|
out.push({ state: status, update_time: endIso ?? startIso });
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const history = computed(() =>
|
|
|
|
|
|
rawHistory.value
|
|
|
|
|
|
.slice()
|
|
|
|
|
|
.filter((h) => h && h.state)
|
|
|
|
|
|
.sort(
|
|
|
|
|
|
(a: any, b: any) =>
|
|
|
|
|
|
new Date(a.update_time || 0).getTime() -
|
|
|
|
|
|
new Date(b.update_time || 0).getTime(),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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", "COMPLETED"].includes(
|
|
|
|
|
|
(h.state || "").toUpperCase(),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
return t ?? null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const steps = computed(() => {
|
|
|
|
|
|
const lastLabel = (
|
|
|
|
|
|
hTerminal.value?.state ||
|
|
|
|
|
|
props.experimentInfo?.status ||
|
|
|
|
|
|
"COMPLETED"
|
|
|
|
|
|
)
|
|
|
|
|
|
.toString()
|
|
|
|
|
|
.toUpperCase();
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "PENDING",
|
|
|
|
|
|
label: "PENDING",
|
|
|
|
|
|
active: !!(hPending.value || hRunning.value || hTerminal.value),
|
|
|
|
|
|
color: "primary",
|
|
|
|
|
|
icon: "mdi-clock-outline",
|
|
|
|
|
|
ts: hPending.value?.update_time,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "RUNNING",
|
|
|
|
|
|
label: "RUNNING",
|
|
|
|
|
|
active: !!(hRunning.value || hTerminal.value),
|
|
|
|
|
|
color: "info",
|
|
|
|
|
|
icon: "mdi-progress-clock",
|
|
|
|
|
|
ts: hRunning.value?.update_time,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "TERMINAL",
|
|
|
|
|
|
label: ["SUCCEEDED", "FAILED"].includes(lastLabel)
|
|
|
|
|
|
? lastLabel
|
|
|
|
|
|
: "COMPLETED",
|
|
|
|
|
|
active: !!hTerminal.value || ["SUCCEEDED", "FAILED"].includes(lastLabel),
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
const nSteps = 3;
|
|
|
|
|
|
const activeIndex = computed(() =>
|
|
|
|
|
|
steps.value.map((s) => s.active).lastIndexOf(true),
|
|
|
|
|
|
);
|
|
|
|
|
|
const leftPct = (i: number) => (i / (nSteps - 1)) * 100;
|
|
|
|
|
|
const segWidthPct = () => 100 / (nSteps - 1);
|
|
|
|
|
|
const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
|
|
|
|
|
<v-card
|
|
|
|
|
|
flat
|
|
|
|
|
|
class="bg-shades-transparent d-flex flex-column justify-center w-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-card flat class="bg-shades-transparent w-100">
|
|
|
|
|
|
<v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
|
|
|
|
|
|
<div class="d-flex flex-row justify-start align-center">
|
|
|
|
|
|
<div class="text-primary">Compare Executions</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</v-card-item>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main Tabs -->
|
|
|
|
|
|
<v-tabs v-model="mainTab" density="comfortable" color="primary">
|
|
|
|
|
|
<v-tab value="details">Details</v-tab>
|
|
|
|
|
|
<v-tab value="viz">Metrics</v-tab>
|
|
|
|
|
|
</v-tabs>
|
|
|
|
|
|
|
|
|
|
|
|
<v-window v-model="mainTab">
|
|
|
|
|
|
<!-- ========= Details ========= -->
|
|
|
|
|
|
<v-window-item value="details">
|
|
|
|
|
|
<v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
|
|
|
|
|
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
|
|
|
|
|
<span class="font-weight-bold">Execution Information</span>
|
|
|
|
|
|
</v-card-title>
|
|
|
|
|
|
|
|
|
|
|
|
<v-card-text class="px-6 pb-6 pt-4">
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold">Name</v-col>
|
|
|
|
|
|
<v-col cols="9">{{ props.experimentInfo.name }}</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold">Status</v-col>
|
|
|
|
|
|
<v-col cols="9">
|
|
|
|
|
|
<v-icon
|
|
|
|
|
|
v-if="props.experimentInfo.status === 'Succeeded'"
|
|
|
|
|
|
color="green"
|
|
|
|
|
|
>mdi-check-circle</v-icon
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-icon
|
|
|
|
|
|
v-else-if="props.experimentInfo.status === 'Failed'"
|
|
|
|
|
|
color="red"
|
|
|
|
|
|
>mdi-close-circle</v-icon
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ props.experimentInfo.status }}
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
|
>Duration</v-col
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-col cols="9">{{ props.experimentInfo.duration }}</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
|
>Experiment Name</v-col
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-col cols="9">{{ props.experimentInfo.expName }}</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
|
>Workflow</v-col
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-col cols="9">{{ props.experimentInfo.workflow }}</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
|
>Start Time</v-col
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-col cols="9">{{ props.experimentInfo.startTime }}</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
|
>Registry Status</v-col
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-col cols="9">{{
|
|
|
|
|
|
props.experimentInfo.registryStatus
|
|
|
|
|
|
}}</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- State History -->
|
|
|
|
|
|
<v-row align="center" class="py-6">
|
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
|
>State History</v-col
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-col cols="9">
|
|
|
|
|
|
<div class="history-rail">
|
|
|
|
|
|
<div class="history-rail__line" />
|
|
|
|
|
|
<template v-for="i in 2" :key="'seg-' + i">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="history-rail__seg"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
left: `calc(${leftPct(i - 1)}% + var(--dot-size)/2)`,
|
|
|
|
|
|
width: `calc(${segWidthPct()}% - var(--dot-size))`,
|
|
|
|
|
|
background:
|
|
|
|
|
|
i - 1 < activeIndex
|
|
|
|
|
|
? 'var(--color-active)'
|
|
|
|
|
|
: 'var(--color-idle)',
|
|
|
|
|
|
}"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-for="(s, i) in steps" :key="'dot-' + s.key">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="history-rail__dot"
|
|
|
|
|
|
:style="{ left: leftPct(i) + '%' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-avatar
|
|
|
|
|
|
:color="s.active ? s.color : 'surface-variant'"
|
|
|
|
|
|
size="28"
|
|
|
|
|
|
class="elev-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-icon size="18">{{ s.icon }}</v-icon>
|
|
|
|
|
|
</v-avatar>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="history-rail__label"
|
|
|
|
|
|
:style="{ left: leftPct(i) + '%' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<v-chip
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:color="s.active ? s.color : undefined"
|
|
|
|
|
|
variant="tonal"
|
|
|
|
|
|
class="mb-1 text-uppercase font-weight-medium"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ s.label }}
|
|
|
|
|
|
</v-chip>
|
|
|
|
|
|
<div class="text-caption text-medium-emphasis">
|
|
|
|
|
|
{{ fmt(s.ts) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ========= Visualizations ========= -->
|
|
|
|
|
|
<v-window-item value="viz">
|
|
|
|
|
|
<v-card class="rounded-lg pa-8 w-100">
|
|
|
|
|
|
<v-window v-model="vizTab">
|
|
|
|
|
|
<v-window-item value="metrics">
|
|
|
|
|
|
<v-card-text>
|
|
|
|
|
|
<v-row class="mb-4" align="center">
|
|
|
|
|
|
<v-col cols="12" class="pa-0">
|
|
|
|
|
|
<v-alert
|
|
|
|
|
|
v-if="!loadingRuns && runs.length === 0"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
variant="tonal"
|
|
|
|
|
|
class="mx-3"
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
>
|
|
|
|
|
|
부모 runId와 일치하는 MLflow run이 없습니다.
|
|
|
|
|
|
</v-alert>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
<v-col
|
|
|
|
|
|
cols="12"
|
|
|
|
|
|
class="d-flex align-center justify-between"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="text-body-2">
|
|
|
|
|
|
Runs (matched):
|
|
|
|
|
|
<strong>{{ runs.length.toLocaleString() }}</strong>
|
|
|
|
|
|
<v-progress-circular
|
|
|
|
|
|
v-if="loadingRuns || loadingRunDetail"
|
|
|
|
|
|
indeterminate
|
|
|
|
|
|
size="16"
|
|
|
|
|
|
class="ml-2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-body-2">
|
|
|
|
|
|
Selected: <strong>{{ selectedLabel }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- (A) 선택한 단건 run 상세 -->
|
|
|
|
|
|
<v-card class="mb-6" variant="tonal">
|
|
|
|
|
|
<v-card-title class="py-2 px-4">Selected Run</v-card-title>
|
|
|
|
|
|
<v-card-text class="px-4 pb-4">
|
|
|
|
|
|
<v-row>
|
|
|
|
|
|
<v-col cols="12" md="6">
|
|
|
|
|
|
<v-table density="comfortable">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td style="width: 40%">Run ID</td>
|
|
|
|
|
|
<td>{{ runDetail?.info?.run_id || "—" }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>Run Name</td>
|
|
|
|
|
|
<td>{{ runDetail?.info?.run_name || "—" }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>Status</td>
|
|
|
|
|
|
<td>{{ runDetail?.info?.status || "—" }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>Start</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
{{
|
|
|
|
|
|
runDetail?.info?.start_time
|
|
|
|
|
|
? new Date(
|
|
|
|
|
|
runDetail.info.start_time,
|
|
|
|
|
|
).toLocaleString()
|
|
|
|
|
|
: "—"
|
|
|
|
|
|
}}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>End</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
{{
|
|
|
|
|
|
runDetail?.info?.end_time
|
|
|
|
|
|
? new Date(
|
|
|
|
|
|
runDetail.info.end_time,
|
|
|
|
|
|
).toLocaleString()
|
|
|
|
|
|
: "—"
|
|
|
|
|
|
}}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</v-table>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
|
|
<v-col cols="12" md="6">
|
|
|
|
|
|
<div class="text-subtitle-2 mb-2">Parameters</div>
|
|
|
|
|
|
<v-table density="compact">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Key</th>
|
|
|
|
|
|
<th>Value</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-if="!runDetail?.data?.params?.length">
|
|
|
|
|
|
<td colspan="2" class="text-medium-emphasis">
|
|
|
|
|
|
No params
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr
|
|
|
|
|
|
v-for="p in runDetail?.data?.params || []"
|
|
|
|
|
|
:key="p.key"
|
|
|
|
|
|
>
|
|
|
|
|
|
<td>{{ p.key }}</td>
|
|
|
|
|
|
<td>{{ p.value }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</v-table>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
|
|
<v-row class="mt-4">
|
|
|
|
|
|
<v-col cols="12" md="6">
|
|
|
|
|
|
<div class="text-subtitle-2 mb-2">Metrics</div>
|
|
|
|
|
|
<v-table density="compact">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Metric</th>
|
|
|
|
|
|
<th>Value</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-if="!runDetail?.data?.metrics?.length">
|
|
|
|
|
|
<td colspan="2" class="text-medium-emphasis">
|
|
|
|
|
|
No metrics
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr
|
|
|
|
|
|
v-for="m in runDetail?.data?.metrics || []"
|
|
|
|
|
|
:key="m.key"
|
|
|
|
|
|
>
|
|
|
|
|
|
<td>{{ m.key }}</td>
|
|
|
|
|
|
<td>{{ m.value }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</v-table>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
|
|
<v-col cols="12" md="6">
|
|
|
|
|
|
<div class="text-subtitle-2 mb-2">Tags</div>
|
|
|
|
|
|
<v-table density="compact">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Key</th>
|
|
|
|
|
|
<th>Value</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-if="!runDetail?.data?.tags?.length">
|
|
|
|
|
|
<td colspan="2" class="text-medium-emphasis">
|
|
|
|
|
|
No tags
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr
|
|
|
|
|
|
v-for="t in runDetail?.data?.tags || []"
|
|
|
|
|
|
:key="t.key"
|
|
|
|
|
|
>
|
|
|
|
|
|
<td>{{ t.key }}</td>
|
|
|
|
|
|
<td
|
|
|
|
|
|
class="text-truncate"
|
|
|
|
|
|
style="max-width: 420px"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ t.value }}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</v-table>
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- (B) 선택한 run 기준의 간단 메트릭 표 -->
|
|
|
|
|
|
<v-card flat class="mb-6">
|
|
|
|
|
|
<v-card-title
|
|
|
|
|
|
class="py-2 px-0 text-button text-medium-emphasis"
|
|
|
|
|
|
>
|
|
|
|
|
|
Model Metrics (selected run)
|
|
|
|
|
|
</v-card-title>
|
|
|
|
|
|
<v-table density="comfortable">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th class="text-left" style="width: 50%">Metric</th>
|
|
|
|
|
|
<th class="text-left">Value</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-if="!selectedMetrics.length">
|
|
|
|
|
|
<td
|
|
|
|
|
|
colspan="2"
|
|
|
|
|
|
class="text-center py-6 text-medium-emphasis"
|
|
|
|
|
|
>
|
|
|
|
|
|
No Data
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr v-for="m in selectedMetrics" :key="m.key">
|
|
|
|
|
|
<td>{{ m.key }}</td>
|
|
|
|
|
|
<td>{{ m.value }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</v-table>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- (C) 2×2 단건 바차트 -->
|
|
|
|
|
|
<v-row>
|
|
|
|
|
|
<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
|
|
|
|
|
|
ref="elPrecision"
|
|
|
|
|
|
style="width: 100%; height: 290px"
|
|
|
|
|
|
></div
|
|
|
|
|
|
></v-col>
|
|
|
|
|
|
<v-col cols="12" md="6"
|
|
|
|
|
|
><div
|
|
|
|
|
|
ref="elRecall"
|
|
|
|
|
|
style="width: 100%; height: 290px"
|
|
|
|
|
|
></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-window-item>
|
|
|
|
|
|
<v-window-item value="box">
|
|
|
|
|
|
<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-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>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
</v-container>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
:root {
|
|
|
|
|
|
--dot-size: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.history-rail {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 40%;
|
|
|
|
|
|
padding: 12px 0 34px;
|
|
|
|
|
|
--color-rail: rgba(255, 255, 255, 0.12);
|
|
|
|
|
|
--color-idle: rgba(255, 255, 255, 0.12);
|
|
|
|
|
|
--color-active: rgb(98, 0, 238);
|
|
|
|
|
|
}
|
|
|
|
|
|
.history-rail__line {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
top: 14px;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
background: var(--color-rail);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.history-rail__seg {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 14px;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
transition:
|
|
|
|
|
|
width 0.24s ease,
|
|
|
|
|
|
background 0.24s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.history-rail__dot {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
}
|
|
|
|
|
|
.history-rail__label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 28px;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|