- 메뉴바 상단으로 변경 - 대시보드 datagroup 데이터 오류 수정 - Experiment run 상세페이지 추가 - dataset, trainingscript no 추가main
parent
07f63adc00
commit
5e5dffcde9
@ -0,0 +1,383 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
} from "vue";
|
||||
import Plotly from "plotly.js-dist-min";
|
||||
import { MlflowService } from "@/components/service/mlflow/MlflowService";
|
||||
|
||||
/** 부모에서 받는 값: 제목 라벨은 그대로 쓰고, 데이터는 runId 하나로 조회 */
|
||||
const props = defineProps<{
|
||||
expName: string; // 상단 라벨 유지용 (표시만)
|
||||
runId?: string; // ★ 이 값으로만 MLflow 단건 조회
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: "close"): void }>();
|
||||
|
||||
/* ---------- 상태 ---------- */
|
||||
const runs = ref<any[]>([]); // 드롭다운/카운트용: 단건이면 길이=1
|
||||
const loadingRuns = ref(false);
|
||||
const selectedRunId = ref<string>(props.runId ?? "");
|
||||
const loadingRunDetail = ref(false);
|
||||
const runDetail = ref<any | null>(null);
|
||||
|
||||
/* 표시용: runs가 비어도 runDetail이 있으면 1로 보이도록 */
|
||||
const runsLoadedCount = computed(() =>
|
||||
runs.value.length > 0 ? runs.value.length : runDetail.value ? 1 : 0,
|
||||
);
|
||||
|
||||
/* ---------- 렌더링용 ---------- */
|
||||
const runItems = computed(() => {
|
||||
// runs(단건) 우선, 없으면 runDetail로 fallback, 그것도 없으면 selectedRunId로 임시 항목
|
||||
const source = runs.value?.length
|
||||
? runs.value
|
||||
: runDetail.value
|
||||
? [runDetail.value]
|
||||
: [];
|
||||
|
||||
const items = source.map((r: any) => {
|
||||
const id = r?.info?.run_id || r?.info?.run_uuid || "";
|
||||
const name = r?.info?.run_name || id || "(no name)";
|
||||
return { value: id, label: name };
|
||||
});
|
||||
|
||||
if (items.length === 0 && selectedRunId.value) {
|
||||
items.push({ value: selectedRunId.value, label: selectedRunId.value });
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const selectedMetrics = computed(() => {
|
||||
const mm: Array<{ key: string; value: number }> =
|
||||
runDetail.value?.data?.metrics ?? [];
|
||||
return mm.map((m) => ({ key: m.key, value: m.value }));
|
||||
});
|
||||
|
||||
/* ---------- Plotly ---------- */
|
||||
const elMetrics = ref<HTMLDivElement | null>(null);
|
||||
|
||||
function drawCharts() {
|
||||
const mm = selectedMetrics.value.filter(
|
||||
(m) => typeof m.value === "number" && isFinite(m.value),
|
||||
);
|
||||
const x = mm.map((m) => m.key);
|
||||
const y = mm.map((m) => m.value);
|
||||
|
||||
if (!elMetrics.value) return;
|
||||
|
||||
if (x.length === 0) {
|
||||
Plotly.purge(elMetrics.value);
|
||||
elMetrics.value.innerHTML =
|
||||
'<div style="padding:16px;color:#999;text-align:center;">No Data</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const trace: Partial<Plotly.PlotData> = {
|
||||
type: "bar",
|
||||
x,
|
||||
y,
|
||||
text: y.map((v) => (typeof v === "number" ? v.toFixed(4) : "")),
|
||||
textposition: "auto",
|
||||
hovertemplate: "%{x}: %{y}<extra></extra>",
|
||||
};
|
||||
const layout: Partial<Plotly.Layout> = {
|
||||
title: { text: "Metrics" },
|
||||
margin: { t: 40, r: 20, b: 60, l: 50 },
|
||||
height: 400,
|
||||
showlegend: false,
|
||||
paper_bgcolor: "rgba(0,0,0,0)",
|
||||
plot_bgcolor: "rgba(0,0,0,0)",
|
||||
font: { color: "#E0E0E0" },
|
||||
xaxis: {
|
||||
automargin: true,
|
||||
gridcolor: "rgba(255,255,255,0.08)",
|
||||
zerolinecolor: "rgba(255,255,255,0.18)",
|
||||
linecolor: "rgba(255,255,255,0.18)",
|
||||
},
|
||||
yaxis: {
|
||||
rangemode: "tozero",
|
||||
gridcolor: "rgba(255,255,255,0.08)",
|
||||
zerolinecolor: "rgba(255,255,255,0.18)",
|
||||
linecolor: "rgba(255,255,255,0.18)",
|
||||
},
|
||||
};
|
||||
Plotly.react(elMetrics.value, [trace], layout, {
|
||||
displayModeBar: false,
|
||||
responsive: true,
|
||||
});
|
||||
}
|
||||
function resizeCharts() {
|
||||
if (elMetrics.value) Plotly.Plots.resize(elMetrics.value);
|
||||
}
|
||||
|
||||
/* ---------- MLflow helpers ---------- */
|
||||
function normalizeRun(res: any) {
|
||||
const v = res?.data?.run ?? res?.run ?? res?.data ?? res;
|
||||
return v?.info && v?.data ? v : null;
|
||||
}
|
||||
|
||||
/* ---------- API (runId 단건만 조회) ---------- */
|
||||
async function fetchRunDetailById(runId: string) {
|
||||
loadingRuns.value = true; // 상단 로딩 아이콘 유지
|
||||
loadingRunDetail.value = true;
|
||||
try {
|
||||
if (!runId) {
|
||||
runDetail.value = null;
|
||||
runs.value = [];
|
||||
await nextTick();
|
||||
drawCharts();
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await MlflowService.getExperimentRun(runId);
|
||||
const one = normalizeRun(res);
|
||||
runDetail.value = one;
|
||||
|
||||
// 드롭다운/카운트 유지용으로 단건을 배열로 보관
|
||||
runs.value = one ? [one] : [];
|
||||
|
||||
// 드롭다운 선택값 보정
|
||||
if (!selectedRunId.value) selectedRunId.value = runId;
|
||||
|
||||
await nextTick();
|
||||
drawCharts();
|
||||
} catch (e) {
|
||||
runDetail.value = null;
|
||||
runs.value = [];
|
||||
await nextTick();
|
||||
drawCharts();
|
||||
} finally {
|
||||
loadingRunDetail.value = false;
|
||||
loadingRuns.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- watch & lifecycle ---------- */
|
||||
// 부모가 준 runId가 바뀌면 재조회
|
||||
watch(
|
||||
() => props.runId,
|
||||
(nv) => {
|
||||
selectedRunId.value = nv || "";
|
||||
fetchRunDetailById(selectedRunId.value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 드롭다운 변경 시에도 동일하게 단건 조회(템플릿 유지 목적)
|
||||
watch(selectedRunId, (id, prev) => {
|
||||
if (id && id !== prev) fetchRunDetailById(id);
|
||||
});
|
||||
|
||||
// 최초 마운트 시(부모 runId가 이미 세팅된 케이스 포함) 한 번 더 안전하게
|
||||
onMounted(() => {
|
||||
if (selectedRunId.value) fetchRunDetailById(selectedRunId.value);
|
||||
window.addEventListener("resize", resizeCharts);
|
||||
});
|
||||
onBeforeUnmount(() => window.removeEventListener("resize", resizeCharts));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
|
||||
<v-card flat class="w-100 rounded-lg pa-8">
|
||||
<v-card-title class="grey lighten-4 py-2 px-4">
|
||||
<span class="font-weight-bold">Metrics</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="px-6 pb-2 pt-4">
|
||||
<v-row class="mb-4" align="center">
|
||||
<v-col
|
||||
cols="12"
|
||||
class="d-flex align-center justify-between flex-wrap ga-3"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
Runs loaded:
|
||||
<strong>{{ runsLoadedCount.toLocaleString() }}</strong>
|
||||
</div>
|
||||
|
||||
<v-select
|
||||
v-model="selectedRunId"
|
||||
:items="runItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
:clearable="false"
|
||||
:disabled="runItems.length <= 1"
|
||||
style="min-width: 280px; max-width: 440px"
|
||||
/>
|
||||
<v-progress-circular
|
||||
v-if="loadingRuns || loadingRunDetail"
|
||||
indeterminate
|
||||
size="16"
|
||||
class="ml-2"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 선택 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>
|
||||
|
||||
<!-- 요약 테이블 -->
|
||||
<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>
|
||||
|
||||
<!-- 차트 -->
|
||||
<v-card flat class="mb-6">
|
||||
<v-card-title class="py-2 px-0 text-button text-medium-emphasis">
|
||||
Metrics (bar chart)
|
||||
</v-card-title>
|
||||
<div ref="elMetrics" style="width: 100%; height: 400px"></div>
|
||||
</v-card>
|
||||
</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-container>
|
||||
</template>
|
||||
Loading…
Reference in new issue