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/workflow/ViewComponent.vue

367 lines
11 KiB

<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";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
type TabKey = "details" | "yaml";
const props = defineProps<{ id: number | string }>();
const emit = defineEmits<{ (e: "close"): void }>();
const { projectId } = storeToRefs(useAutoflowStore());
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;
}
}
/** workflow id에 연결된 attachment를 refType/refId로 검색해 YAML storagePath 반환 */
async function findWorkflowAttachmentStoragePath(workflowId: number, projId: number): Promise<string | null> {
try {
const searchRes = await AttachmentsService.search({
projectId: projId,
page: 1,
size: 1,
refType: "workflows",
refId: workflowId,
});
const page = searchRes?.data ?? searchRes ?? {};
const content = page.content ?? page.data ?? [];
const att = Array.isArray(content) ? content[0] : null;
if (!att) return null;
return (
att.storagePath ??
att.storedName ??
att.objectName ??
att.object_key ??
null
);
} catch (e) {
console.warn("[Workflow Detail] attachment search failed:", e);
return null;
}
}
/** 상세 조회 → workflow 상세 API 후, refType=workflows/refId=workflowId로 attachment 검색해 YAML 로드 */
async function fetchDetail(id: number | string) {
const wid = Number(id);
try {
// 1) 워크플로우 상세 API로 기본 정보 조회
const res = await WorkflowService.view(wid);
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),
};
// 2) 서버가 YAML 문자열을 직접 주면 그대로 사용
const yamlFromServer =
d.workflowYaml ||
d.yaml ||
d.pipelineYaml ||
d.specYaml ||
d.yamlStr ||
"";
if (yamlFromServer) {
ensureEditor();
editorInstance!.setValue(yamlFromServer);
} else {
// 3) workflow에 연결된 attachment 검색(refType=workflows, refId=workflowId) 후 storagePath로 YAML 로드
const projId = d.projectId ?? projectId.value ?? Number(localStorage.getItem("projectId"));
if (projId) {
const storagePath = await findWorkflowAttachmentStoragePath(wid, projId);
if (storagePath) {
const loaded = await loadYamlFromStoragePath(storagePath);
if (!loaded) {
ensureEditor();
editorInstance?.setValue(defaultYaml);
}
} else {
ensureEditor();
editorInstance?.setValue(defaultYaml);
}
} else {
ensureEditor();
editorInstance?.setValue(defaultYaml);
}
}
} 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-space-between align-center w-100">
<div>View Details</div>
<v-btn color="primary" variant="elevated" @click="emit('close')">
Back to List
</v-btn>
</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-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>