|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { onMounted, ref, watch, onBeforeUnmount } from "vue";
|
|
|
|
|
import * as monaco from "monaco-editor";
|
|
|
|
|
import "monaco-editor/min/vs/editor/editor.main.css";
|
|
|
|
|
import { WorkflowService } from "@/components/service/management/WorkflowService";
|
|
|
|
|
import { AttachmentsService } from "@/components/service/management/AttachmentsService"; // ⬅️ 파일 읽기용
|
|
|
|
|
|
|
|
|
|
type TabKey = "details" | "yaml";
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ id: number | string }>();
|
|
|
|
|
const emit = defineEmits<{ (e: "close"): void }>();
|
|
|
|
|
|
|
|
|
|
const activeTab = ref<TabKey>("details");
|
|
|
|
|
|
|
|
|
|
// ----- Monaco Editor -----
|
|
|
|
|
const editorRef = ref<HTMLDivElement | null>(null);
|
|
|
|
|
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
|
|
|
|
|
|
|
|
|
|
function ensureEditor() {
|
|
|
|
|
if (editorInstance || !editorRef.value) return;
|
|
|
|
|
editorInstance = monaco.editor.create(editorRef.value, {
|
|
|
|
|
value: defaultYaml,
|
|
|
|
|
language: "yaml",
|
|
|
|
|
theme: "vs-dark",
|
|
|
|
|
readOnly: true,
|
|
|
|
|
automaticLayout: true,
|
|
|
|
|
minimap: { enabled: false },
|
|
|
|
|
lineNumbers: "on",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 화면에 뿌릴 상세 데이터
|
|
|
|
|
const detail = ref({
|
|
|
|
|
name: "",
|
|
|
|
|
version: "",
|
|
|
|
|
description: "",
|
|
|
|
|
kubeflowStatus: "",
|
|
|
|
|
namespace: "",
|
|
|
|
|
pipelineId: "",
|
|
|
|
|
regDt: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const defaultYaml = "";
|
|
|
|
|
|
|
|
|
|
// 날짜 포맷
|
|
|
|
|
function formatDateTime(raw?: string): string {
|
|
|
|
|
if (!raw) return "-";
|
|
|
|
|
const s = String(raw).replace("T", " ");
|
|
|
|
|
const m = s.match(/^(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})/);
|
|
|
|
|
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();
|
|
|
|
|
editorInstance?.setValue(text || defaultYaml);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("[Workflow Detail] readTextByPath failed:", e);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 상세 조회 → 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 ?? ""),
|
|
|
|
|
description: d.description ?? d.workflowDescription ?? "",
|
|
|
|
|
kubeflowStatus: d.kubeflow_status ?? d.kubeflowStatus ?? "",
|
|
|
|
|
namespace: d.namespace ?? "",
|
|
|
|
|
pipelineId: d.pipeline_id ?? d.pipelineId ?? "",
|
|
|
|
|
regDt: formatDateTime(d.reg_dt ?? d.regDt ?? d.regDate),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 서버가 YAML 문자열을 직접 줄 경우엔 덮어씌우고 끝
|
|
|
|
|
const yamlFromServer =
|
|
|
|
|
d.workflowYaml ||
|
|
|
|
|
d.yaml ||
|
|
|
|
|
d.pipelineYaml ||
|
|
|
|
|
d.specYaml ||
|
|
|
|
|
d.yamlStr ||
|
|
|
|
|
"";
|
|
|
|
|
if (yamlFromServer) {
|
|
|
|
|
ensureEditor();
|
|
|
|
|
editorInstance!.setValue(yamlFromServer);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("[Workflow Detail] view API failed:", e);
|
|
|
|
|
ensureEditor();
|
|
|
|
|
editorInstance!.setValue(defaultYaml);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 마운트 & 변경 감지 =====
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
ensureEditor();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.id,
|
|
|
|
|
(val) => {
|
|
|
|
|
if (val !== null && val !== undefined && val !== "") {
|
|
|
|
|
fetchDetail(val);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// YAML 탭으로 전환되었을 때도 에디터 보장
|
|
|
|
|
watch(
|
|
|
|
|
() => activeTab.value,
|
|
|
|
|
(tab) => {
|
|
|
|
|
if (tab === "yaml") ensureEditor();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
editorInstance?.dispose();
|
|
|
|
|
editorInstance = null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// (나중 사용) Step 테이블
|
|
|
|
|
const stepHeaders = [
|
|
|
|
|
{ title: "Order", key: "order", width: "10%", align: "center" },
|
|
|
|
|
{ title: "Step Name", key: "name", width: "40%", align: "center" },
|
|
|
|
|
{
|
|
|
|
|
title: "Component Type",
|
|
|
|
|
key: "componentType",
|
|
|
|
|
width: "30%",
|
|
|
|
|
align: "center",
|
|
|
|
|
},
|
|
|
|
|
{ title: "Status", key: "status", width: "20%", align: "center" },
|
|
|
|
|
];
|
|
|
|
|
const steps = ref<
|
|
|
|
|
Array<{ order: number; name: string; componentType: string; status: string }>
|
|
|
|
|
>([]);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<v-container class="h-100 w-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-tabs
|
|
|
|
|
v-model="activeTab"
|
|
|
|
|
background-color="grey lighten-4"
|
|
|
|
|
style="max-width: 360px"
|
|
|
|
|
grow
|
|
|
|
|
>
|
|
|
|
|
<v-tab value="details">Details</v-tab>
|
|
|
|
|
<v-tab value="yaml">YAML</v-tab>
|
|
|
|
|
</v-tabs>
|
|
|
|
|
|
|
|
|
|
<!-- Details 탭 -->
|
|
|
|
|
<template v-if="activeTab === 'details'">
|
|
|
|
|
<v-card class="bordered-box mb-6 w-100 rounded-lg pa-8 step-card">
|
|
|
|
|
<v-card-title class="grey lighten-4 py-2 px-4">
|
|
|
|
|
<span class="font-weight-bold">Workflow 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"
|
|
|
|
|
>Workflow Name</v-col
|
|
|
|
|
>
|
|
|
|
|
<v-col cols="3">{{ detail.name || "-" }}</v-col>
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold">Version</v-col>
|
|
|
|
|
<v-col cols="3">{{ detail.version || "-" }}</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
<v-divider class="my-2" />
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
>Workflow Description</v-col
|
|
|
|
|
>
|
|
|
|
|
<v-col cols="9">{{ detail.description || "-" }}</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
<v-divider class="my-2" />
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
>Kubeflow Status</v-col
|
|
|
|
|
>
|
|
|
|
|
<v-col cols="3">{{ detail.kubeflowStatus || "-" }}</v-col>
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold">Namespace</v-col>
|
|
|
|
|
<v-col cols="3">{{ detail.namespace || "-" }}</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
<v-divider class="my-2" />
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
>Pipeline ID</v-col
|
|
|
|
|
>
|
|
|
|
|
<v-col cols="9" class="text-truncate">{{
|
|
|
|
|
detail.pipelineId || "-"
|
|
|
|
|
}}</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
<v-divider class="my-2" />
|
|
|
|
|
|
|
|
|
|
<v-row align="center" class="py-2">
|
|
|
|
|
<v-col cols="3" class="text-h6 font-weight-bold"
|
|
|
|
|
>Created Date</v-col
|
|
|
|
|
>
|
|
|
|
|
<v-col cols="9">{{ detail.regDt || "-" }}</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
|
|
|
|
<v-sheet class="d-flex justify-end mt-4">
|
|
|
|
|
<v-btn class="back-to-list" color="primary" @click="emit('close')">
|
|
|
|
|
Back to List
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-sheet>
|
|
|
|
|
</v-card>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- YAML 탭 -->
|
|
|
|
|
<div
|
|
|
|
|
v-show="activeTab === 'yaml'"
|
|
|
|
|
ref="editorRef"
|
|
|
|
|
class="editor-container"
|
|
|
|
|
/>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-container>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.editor-container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 900px;
|
|
|
|
|
max-height: 900px;
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
.step-card {
|
|
|
|
|
position: relative;
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
padding-bottom: 84px;
|
|
|
|
|
}
|
|
|
|
|
.back-to-list {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 24px;
|
|
|
|
|
bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
.text-truncate {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
</style>
|