|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
|
|
|
|
|
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
|
|
|
|
|
// import FormComponent from "@/components/device/FormComponent.vue";
|
|
|
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
|
|
|
|
|
|
|
|
// const store = commonStore();
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
experimentInfo: any;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{ (e: "close"): void }>();
|
|
|
|
|
|
|
|
|
|
const history = computed(() =>
|
|
|
|
|
(props.experimentInfo.raw?.state_history ?? [])
|
|
|
|
|
.slice()
|
|
|
|
|
.sort(
|
|
|
|
|
(a: any, b: any) =>
|
|
|
|
|
new Date(a.update_time).getTime() - new Date(b.update_time).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"].includes((h.state || "").toUpperCase()),
|
|
|
|
|
);
|
|
|
|
|
return t ?? null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 3단계 고정 스텝 정의
|
|
|
|
|
const steps = computed(() => {
|
|
|
|
|
const lastLabel = (hTerminal.value?.state || "COMPLETED").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,
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 고정 3스텝 기준 위치/세그먼트
|
|
|
|
|
const nSteps = 3;
|
|
|
|
|
const activeIndex = computed(
|
|
|
|
|
() => steps.value.map((s) => s.active).lastIndexOf(true), // -1이면 아무것도 진행X
|
|
|
|
|
);
|
|
|
|
|
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">View Details</div>
|
|
|
|
|
</div>
|
|
|
|
|
</v-card-item>
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
|
|
<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" class="pa-2">{{ 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" class="pa-2">
|
|
|
|
|
<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
|
|
|
|
|
>
|
|
|
|
|
<v-icon v-else color="grey">mdi-loading</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" class="pa-2">{{
|
|
|
|
|
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 ID</v-col
|
|
|
|
|
>
|
|
|
|
|
<v-col cols="9" class="pa-2">{{
|
|
|
|
|
props.experimentInfo.experiment
|
|
|
|
|
}}</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" class="pa-2">{{
|
|
|
|
|
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" class="pa-2">{{
|
|
|
|
|
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" class="pa-2">{{
|
|
|
|
|
props.experimentInfo.registryStatus
|
|
|
|
|
}}</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
<VDivider class="my-2" />
|
|
|
|
|
|
|
|
|
|
<!-- 🔹 state_history 출력 -->
|
|
|
|
|
<!-- 🔹 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" class="pa-2">
|
|
|
|
|
<div class="history-rail">
|
|
|
|
|
<!-- 기본(회색) 레일 -->
|
|
|
|
|
<div class="history-rail__line" />
|
|
|
|
|
|
|
|
|
|
<!-- 진행 세그먼트 2개 고정 -->
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<!-- 점/아이콘 + 라벨/시간 : 3개 고정 -->
|
|
|
|
|
<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-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
|
|
|
|
|
); /* theme primary(보라) 느낌, 필요시 바꿔도 OK */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 가로 기준선 */
|
|
|
|
|
.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 240ms ease,
|
|
|
|
|
background 240ms 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>
|