You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
autoflow-web-console/src/components/templates/home/ListComponent.vue

397 lines
12 KiB

<script setup lang="ts">
import { onMounted, ref, computed, watch } from "vue";
import Plotly from "plotly.js-dist-min";
import { WorkflowService } from "@/components/service/management/workflowService";
import { useAutoflowStore } from "@/stores/autoflowStore";
const store = useAutoflowStore();
const currentProjectId = computed(() => store.projectId);
const pieChartRef = ref<HTMLElement | null>(null);
const workflows = ref<any[]>([]);
const recentLimit = 10;
// ---- kubeflowStatus 파이차트 (색상 매핑 없음, 기본 팔레트 사용) ----
function renderStatusPie() {
if (!pieChartRef.value) return;
const counts = new Map<string, number>();
for (const wf of workflows.value ?? []) {
const status =
String(wf?.kubeflowStatus ?? wf?.kubeflow_status ?? "Unknown").trim() ||
"Unknown";
counts.set(status, (counts.get(status) || 0) + 1);
}
const labels = Array.from(counts.keys());
const values = Array.from(counts.values());
const trace: Partial<Plotly.PlotData> = {
values: values.length ? values : [1],
labels: labels.length ? labels : ["No Data"],
type: "pie",
textinfo: "label+percent",
textfont: { color: "#fff", size: 14 },
hole: 0.4,
};
const layout: Partial<Plotly.Layout> = {
paper_bgcolor: "#1e1e1e",
plot_bgcolor: "#1e1e1e",
showlegend: true,
legend: {
font: { color: "#ffffff", size: 12 },
orientation: "h",
x: 0.5,
xanchor: "center",
y: -0.2,
},
margin: { t: 20, b: 40, l: 0, r: 0 },
};
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 },
{ name: "LidarScans", count: 2 },
{ name: "Traffic_log", count: 2 },
{ name: "Traffic_log2", count: 2 },
];
const tableHeader = [
{ label: "Model Name", width: "10%", style: "word-break: keep-all;" },
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
{ label: "Deployed At", width: "10%", style: "word-break: keep-all;" },
{ label: "Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Download", width: "10%", style: "word-break: keep-all;" },
];
const data = ref({
results: [
{
deviceKey: "1",
name: "LaneDetectionModel",
version: "v1.2.0",
time: "2025-05-13 14:32",
status: "Active",
download: "Finished",
},
{
deviceKey: "2",
name: "TrafficSignClassifier",
version: "v0.9.3",
time: "2025-05-13 09:00",
status: "Pending",
download: "-",
},
{
deviceKey: "3",
name: "PathPlannerModel",
version: "v2.0.1",
time: "2025-05-12 17:44",
status: "Failed",
download: "Failed",
},
],
allSelected: false,
selected: [],
});
11 months ago
const handleRefresh = () => {
alert("Refresh 작업 진행중...");
};
const getSelectedAllData = () => {
data.value.selected = data.value.allSelected
? data.value.results.map(({ deviceKey }) => ({ deviceKey }))
: [];
};
const getLatestTimestamp = (wf: any): string => wf.modDt;
// "YYYY-MM-DD HH:mm" 포맷
const formatToYmdHm = (isoString: string): string => {
if (!isoString) return "-";
const d = new Date(isoString);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const recentWorkflowList = computed(() => {
return (workflows.value ?? [])
.map((wf: any) => ({
id: wf.id,
title: wf.name,
timestamp: getLatestTimestamp(wf),
}))
.filter((item) => item.title && item.timestamp)
.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
)
.slice(0, recentLimit);
});
// ---- 데이터 로드 & 차트 갱신 ----
async function loadWorkflows() {
try {
const payload = {
page: 0,
size: 1000,
projectId: currentProjectId.value,
sortField: "id",
sortDirection: "DESC",
};
const res = await WorkflowService.search(payload);
const raw = Array.isArray(res?.data?.content)
? res.data.content
: Array.isArray(res?.data)
? res.data
: [];
workflows.value = raw.filter(
(wf: any) =>
String(
wf?.projectId ?? wf?.prjId ?? wf?.project_id ?? wf?.project?.id ?? "",
) === String(currentProjectId.value),
);
renderStatusPie();
} catch (err) {
console.error("GET /api/workflows failed:", err);
workflows.value = [];
renderStatusPie();
}
}
onMounted(async () => {
// 차트 컨테이너가 있을 때, 초기 빈 차트 렌더
renderStatusPie();
await loadWorkflows();
});
// 프로젝트 변경 시 재조회
watch(currentProjectId, () => loadWorkflows());
</script>
<template>
<v-container fluid>
<div class="d-flex justify-space-between align-center mb-6">
<h2 class="text-h6 font-weight-bold">배터리 상태 예측 모델 프로젝트</h2>
11 months ago
<v-btn color="primary" prepend-icon="mdi-refresh" @click="handleRefresh"
>Refresh</v-btn
>
</div>
<v-row>
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">
Workflow Success Rate
</h3>
</div>
<div style="overflow-y: auto; padding: 8px 16px">
<div ref="pieChartRef" style="height: 280px"></div>
</div>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">
Recently Registered Workflow
</h3>
</div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
<v-list density="comfortable" nav>
<v-list-item v-for="item in recentWorkflowList" :key="item.id">
<template #title>
<div class="d-flex justify-space-between align-center w-100">
<span class="text-body-2 font-weight-medium">{{
item.title
}}</span>
<span class="text-caption text-grey-lighten-1">
{{ formatToYmdHm(item.timestamp) }}
</span>
</div>
</template>
</v-list-item>
<v-list-item v-if="recentWorkflowList.length === 0">
<template #title>
<div class="text-caption text-grey">
최근 등록/수정된 워크플로우가 없습니다.
</div>
</template>
</v-list-item>
</v-list>
</div>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">Recent Run</h3>
</div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
<v-list density="comfortable">
<v-list-item
v-for="(run, idx) in recentRuns"
:key="idx"
class="py-2"
>
<div class="d-flex align-center">
<v-avatar
size="28"
:color="
run.status === 'success'
? 'green lighten-1'
: 'red lighten-1'
"
>
<v-icon size="20" color="white">
{{ run.status === "success" ? "mdi-check" : "mdi-close" }}
</v-icon>
</v-avatar>
<div
class="d-flex flex-column text-right ml-4"
style="flex: 1"
>
<span class="font-weight-medium text-body-2">
{{ run.name }}
</span>
<span class="text-caption text-grey-darken-1">
{{ run.time }}
</span>
</div>
</div>
</v-list-item>
</v-list>
</div>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px">
<div style="padding: 16px; border-bottom: 1px solid #ccc">
<h3 class="text-subtitle-1 font-weight-bold mb-0">
Dataset Update Activity
</h3>
</div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px">
<v-list dense>
<v-list-item v-for="(data, idx) in datasetUpdates" :key="idx">
<v-list-item-title>{{ data.name }}</v-list-item-title>
<v-progress-linear
:model-value="data.count * 10"
height="8"
color="primary"
class="mt-1"
/>
<v-list-item-subtitle
>{{ data.count }} updates</v-list-item-subtitle
>
</v-list-item>
</v-list>
</div>
</v-card>
</v-col>
</v-row>
<v-card class="rounded-lg pa-4 mt-4">
<div class="d-flex justify-space-between align-center mt-8 mb-2 px-2">
<div class="d-flex align-center">
<span class="text-subtitle-1 font-weight-bold">Model Deployment</span>
</div>
<v-btn
variant="text"
class="text-caption font-weight-bold"
append-icon="mdi-arrow-right"
style="text-transform: none"
>
Go to Model Deploy
</v-btn>
</div>
<v-col cols="12">
<v-sheet>
<v-table density="comfortable" fixed-header height="625">
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
:style="`width:${item.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
class="text-center font-weight-bold"
:style="item.style"
>
{{ item.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
:key="i"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{ deviceKey: item.deviceKey }"
/>
</td>
<td>{{ item.name }}</td>
<td>{{ item.version }}</td>
<td>{{ item.time }}</td>
<td>{{ item.status }}</td>
<td>{{ item.download }}</td>
</tr>
</tbody>
</v-table>
</v-sheet>
</v-col>
</v-card>
</v-container>
</template>
<style scoped>
ul {
list-style: none;
padding-left: 0;
margin-top: 8px;
}
li {
margin-bottom: 8px;
}
</style>