fix: 워크플로우 컴포넌트 수정

main
jschoi 9 months ago
parent efc6d0651e
commit c2fb4c6966

@ -0,0 +1,27 @@
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["onClick"]);
const onClick = () => {
emit("onClick");
};
</script>
<template>
<v-tooltip location="bottom" text="실행">
<template v-slot:activator="{ props }">
<v-btn
@click="onClick"
class="ma-1"
icon="mdi-cog-play-outline"
color="success"
density="comfortable"
elevation="0"
size="small"
v-bind="props"
>
</v-btn>
</template>
</v-tooltip>
</template>

@ -9,6 +9,11 @@ import { storage } from "@/utils/storage";
import type { Workflow } from "@/components/models/management/Workflow"; import type { Workflow } from "@/components/models/management/Workflow";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore"; import { useAutoflowStore } from "@/stores/autoflowStore";
import { kubeflowService } from "@/components/service/management/kubeflowService";
import {
toKubeflowForm,
type KubeflowUploadDto,
} from "@/components/models/management/Kubeflow";
const { projectId } = storeToRefs(useAutoflowStore()); const { projectId } = storeToRefs(useAutoflowStore());
@ -27,6 +32,84 @@ const isEdit = computed(() => props.mode === "edit");
const saving = ref(false); const saving = ref(false);
const errorMsg = ref(""); const errorMsg = ref("");
// ====== KFP & ======
const KFP_NAME_REGEX = /^[a-z0-9]([-a-z0-9\.]*[a-z0-9])?$/; // //.-, /
const KOREAN_RX = /[ㄱ-ㅎㅏ-ㅣ가-힣]/g;
const sanitizeKfpName = (s: string) => {
let x = (s ?? "").toLowerCase();
x = x.replace(/[\s_]+/g, "-"); // / ->
x = x.replace(/[^a-z0-9.-]/g, ""); // ( )
x = x.replace(/-+/g, "-"); //
x = x.replace(/^[^a-z0-9]+/, ""); //
x = x.replace(/[^a-z0-9]+$/, ""); //
return x;
};
const stripKorean = (s: string) => (s ?? "").replace(KOREAN_RX, "");
// /
const nameHint = ref(
"허용 문자: 소문자 az, 숫자 09, '-', '.' (시작/끝은 영숫자). 한글/공백/대문자/언더스코어 불가",
);
const descHint = ref("한글은 사용할 수 없습니다.");
const nameInvalid = computed(
() => !!form.value.name && !KFP_NAME_REGEX.test(form.value.name),
);
const nameErrorMsg = computed(() =>
nameInvalid.value
? "형식이 올바르지 않습니다. (소문자/숫자, '-', '.', 시작/끝은 영숫자)"
: "",
);
//
function onNameInput(v: string) {
const cleaned = sanitizeKfpName(v || "");
if (cleaned !== v) {
nameHint.value = "허용되지 않는 문자는 자동으로 제거됩니다.";
} else {
nameHint.value =
"허용 문자: 소문자 az, 숫자 09, '-', '.' (시작/끝은 영숫자)";
}
form.value.name = cleaned;
}
function onDescInput(v: string) {
const cleaned = stripKorean(v || "");
form.value.description = cleaned;
}
function extractApiErrorMessage(err: any): string {
const status = err?.response?.status;
const data = err?.response?.data;
const raw =
(typeof data === "string"
? data
: data?.message || data?.error || data?.detail) ||
err?.message ||
"";
const text = String(raw);
if (
status === 409 ||
/already\s*exists|duplicate|이미 존재|중복/i.test(text)
) {
return "같은 이름의 파이프라인이 이미 존재합니다. 다른 이름으로 등록해주세요.";
}
if (status === 400 && /name|display[_ ]?name|invalid/i.test(text)) {
return "이름(name)이 유효하지 않습니다. 공백/특수문자 여부를 확인해주세요.";
}
if (status === 401 || status === 403) {
return "권한이 없거나 로그인 정보가 만료되었습니다. 다시 로그인 후 시도하세요.";
}
if (status === 413 || /file too large|payload too large|size/i.test(text)) {
return "업로드 파일 용량이 너무 큽니다.";
}
if (status === 500 && /InvalidUrl|Bad authority|host/i.test(text)) {
return "서버 설정 오류로 업로드에 실패했습니다. (관리자에게 KFP URL 설정 점검을 요청하세요)";
}
return text || `요청에 실패했습니다. (HTTP ${status ?? "Error"})`;
}
const steps = ref([ const steps = ref([
{ order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" }, { order: 1, stepName: "Data Load", type: "DataPrep", status: "Configured" },
{ {
@ -47,15 +130,16 @@ const steps = ref([
const form = ref({ const form = ref({
name: "", name: "",
description: "", description: "",
file: null as File | null,
}); });
/** props.editData -> form 바인딩 */ /** props.editData -> form 바인딩 */
function hydrateFormFromEdit(data: any) { function hydrateFormFromEdit(data: any) {
if (!data) return; if (!data) return;
// , /
form.value.name = data.workflowName ?? data.name ?? ""; form.value.name = data.workflowName ?? data.name ?? "";
form.value.description = data.workflowDescription ?? data.description ?? ""; form.value.description = data.workflowDescription ?? data.description ?? "";
} }
onMounted(() => { onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData); if (isEdit.value) hydrateFormFromEdit(props.editData);
}); });
@ -75,9 +159,14 @@ const nowLocalIso = (): string => {
async function submit() { async function submit() {
errorMsg.value = ""; errorMsg.value = "";
// &
form.value.name = sanitizeKfpName(form.value.name);
form.value.description = stripKorean(form.value.description);
const name = form.value.name.trim(); const name = form.value.name.trim();
if (!name) { if (!name || !KFP_NAME_REGEX.test(name)) {
errorMsg.value = "Workflow Name은 필수입니다."; errorMsg.value =
"Workflow Name 형식이 올바르지 않습니다. (소문자/숫자, '-', '.', 시작/끝은 영숫자)";
return; return;
} }
@ -96,7 +185,6 @@ async function submit() {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다."; errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return; return;
} }
if (!projectId.value) { if (!projectId.value) {
errorMsg.value = "프로젝트가 선택되지 않았습니다."; errorMsg.value = "프로젝트가 선택되지 않았습니다.";
return; return;
@ -116,50 +204,67 @@ async function submit() {
return; return;
} }
// //
const viewRes = await WorkflowService.view(id); const viewRes = await WorkflowService.view(id);
const current = (viewRes?.data ?? viewRes) || {}; const current = (viewRes?.data ?? viewRes) || {};
const updatePayload: any = { // name/description , null
const updatePayload = cleanUndefined({
id, id,
workflowName: name, name, //
workflowDescription: form.value.description?.trim() || "", description: form.value.description?.trim() || "", //
uploadYn: current.uploadYn ?? "Y",
// (null ) // ===== =====
displayName: current.displayName,
namespace: current.namespace,
pipelineId: current.pipelineId,
kubeflowStatus: current.kubeflowStatus,
version: current.version,
regUserId: current.regUserId ?? regUserId, regUserId: current.regUserId ?? regUserId,
regDt: current.regDt ?? now,
projectId: current.projectId ?? projectId.value, projectId: current.projectId ?? projectId.value,
// regDt: current.regDt,
modDt: now, modDt: now,
}; });
const { data } = await WorkflowService.update(id, updatePayload); const { data } = await WorkflowService.update(id, updatePayload);
emit("saved", data); emit("saved", data);
emit("close-modal"); emit("close-modal");
} else { } else {
// ===== ===== // ===== =====
const createPayload: any = { if (!form.value.file) {
workflowName: name, errorMsg.value = "업로드할 파일을 선택하세요.";
workflowDescription: form.value.description?.trim() || "", return;
uploadYn: "Y", }
regUserId, // const dto: KubeflowUploadDto = {
regDt: now, // name,
projectId: projectId.value, display_name: name,
// modDt ( ) description: form.value.description?.trim() || "",
namespace: "default",
regUserId,
projectId: projectId.value!,
uploadfile: form.value.file,
}; };
const fd = toKubeflowForm(dto);
const { data } = await WorkflowService.add(createPayload); const { data } = await kubeflowService.upload(fd);
emit("saved", data); emit("saved", data);
emit("close-modal"); emit("close-modal");
} }
} catch (e) { } catch (e: any) {
console.error("워크플로우 저장 실패:", e); console.error("워크플로우 저장 실패:", e);
errorMsg.value = "저장에 실패했습니다. 잠시 후 다시 시도하세요."; errorMsg.value = extractApiErrorMessage(e);
} finally { } finally {
saving.value = false; saving.value = false;
} }
} }
/** undefined 필드는 제거해서 불필요한 키 전송 방지 */
function cleanUndefined<T extends Record<string, any>>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== undefined),
) as T;
}
/** ESC로 닫기 */ /** ESC로 닫기 */
function onEsc(e: KeyboardEvent) { function onEsc(e: KeyboardEvent) {
if (e.key === "Escape") emit("close-modal"); if (e.key === "Escape") emit("close-modal");
@ -184,31 +289,57 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</div> </div>
<v-form @submit.prevent="submit"> <v-form @submit.prevent="submit">
<!-- Name -->
<div class="mb-5"> <div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block" <label class="text-subtitle-2 font-weight-medium mb-1 d-block">
>Workflow Name</label Workflow Name
> </label>
<v-text-field <v-text-field
v-model="form.name" v-model="form.name"
variant="outlined" variant="outlined"
:disabled="saving" :disabled="saving"
dense dense
hide-details hide-details="auto"
persistent-hint
:hint="nameHint"
:error="nameInvalid"
:error-messages="nameErrorMsg"
@update:model-value="onNameInput"
required required
/> />
</div> </div>
<div> <!-- Description -->
<label class="text-subtitle-2 font-weight-medium mb-1 d-block" <div class="mb-5">
>Workflow Description</label <label class="text-subtitle-2 font-weight-medium mb-1 d-block">
> Workflow Description
</label>
<v-textarea <v-textarea
v-model="form.description" v-model="form.description"
variant="outlined" variant="outlined"
:disabled="saving" :disabled="saving"
rows="3" rows="3"
dense dense
hide-details="auto"
persistent-hint
:hint="descHint"
@update:model-value="onDescInput"
/>
</div>
<!-- Upload File -->
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block">
Upload File
</label>
<v-file-input
v-model="form.file"
accept=".zip,.tar,.gz,.yaml,.yml"
show-size
variant="outlined"
dense
hide-details hide-details
placeholder="파이프라인 파일 선택"
/> />
</div> </div>
@ -216,82 +347,18 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-text class="pt-6 pb-4 px-6">
<div class="text-subtitle-1 font-weight-medium mb-4">Workflow Steps</div>
<v-row class="align-center mb-4">
<v-col cols="auto"
><v-btn color="primary" small :disabled="saving"
><v-icon left>mdi-plus</v-icon> Add Step</v-btn
></v-col
>
<v-col cols="auto"
><v-btn color="success" small :loading="saving" @click="submit">{{
isEdit ? "Update" : "Save"
}}</v-btn></v-col
>
<v-col cols="auto"
><v-btn
color="grey"
small
:disabled="saving"
@click="$emit('close-modal')"
>Cancel</v-btn
></v-col
>
<v-spacer />
</v-row>
<v-simple-table dense>
<thead>
<tr class="grey lighten-2">
<th class="text-center" style="width: 5%">Order</th>
<th class="text-center">Step Name</th>
<th class="text-center">Component Type</th>
<th class="text-center">Status</th>
<th class="text-center" style="width: 20%">Action</th>
</tr>
</thead>
<tbody>
<tr
v-for="step in steps"
:key="step.order"
style="border: 1px solid #ccc"
>
<td class="text-center">{{ step.order }}</td>
<td class="text-center">{{ step.stepName }}</td>
<td class="d-flex justify-center align-center">
<v-select
v-model="step.type"
:items="['DataPrep', 'Preprocess', 'Train']"
dense
hide-details
style="max-width: 180px"
/>
</td>
<td class="text-center">{{ step.status }}</td>
<td class="text-center">
<IconArrowUp />
<IconArrowDown />
<IconModifyBtn />
<IconDeleteBtn />
</td>
</tr>
</tbody>
</v-simple-table>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px"> <v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" :loading="saving" @click="submit">{{ <v-btn color="success" :loading="saving" @click="submit">
isEdit ? "Update" : "Save" {{ isEdit ? "Update" : "Save" }}
}}</v-btn> </v-btn>
<v-btn <v-btn
text text
class="white--text" class="white--text"
:disabled="saving" :disabled="saving"
@click="$emit('close-modal')" @click="$emit('close-modal')"
>Close</v-btn
> >
Close
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>

@ -126,9 +126,8 @@ async function submit() {
if (!projectId.value) if (!projectId.value)
return (errorMsg.value = "프로젝트가 선택되지 않았습니다."); return (errorMsg.value = "프로젝트가 선택되지 않았습니다.");
if (!selectedWorkflowId.value) if (!selectedWorkflowId.value)
return (errorMsg.value = "Workflow를 선택해주세요."); // return (errorMsg.value = "Workflow를 선택해주세요.");
// (NOT NULL )
const payload: any = { const payload: any = {
stepName, stepName,
status: form.value.status, status: form.value.status,
@ -136,7 +135,7 @@ async function submit() {
regDt: nowLocalIso(), regDt: nowLocalIso(),
version: 1, version: 1,
projectId: projectId.value, projectId: projectId.value,
workflowStepId: selectedWorkflowId.value, // 500 workflowStepId: selectedWorkflowId.value,
}; };
try { try {
@ -197,6 +196,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
required required
/> />
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block" <label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Workflow</label >Workflow</label

@ -10,9 +10,10 @@ import { WorkflowService } from "@/components/service/management/workflowService
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue"; import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsBaseDialog from "@/components/atoms/organisms/WorkflowsBaseDialog.vue"; import WorkflowsBaseDialog from "@/components/atoms/organisms/WorkflowsBaseDialog.vue";
import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue"; import WorkflowsUploadDialog from "@/components/atoms/organisms/WorkflowsUploadDialog.vue";
import WorkflowsRunDialog from "@/components/atoms/organisms/WorkflowsRunDialog.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue"; import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue"; import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconRunBtn from "@/components/atoms/button/IconRunBtn.vue";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(tz); dayjs.extend(tz);
const KST = "Asia/Seoul"; const KST = "Asia/Seoul";
@ -20,16 +21,18 @@ const store = commonStore();
const openView = ref(false); const openView = ref(false);
type SearchType = "전체" | "제목" | "작성자"; type SearchType = "전체" | "제목" | "작성자";
const isRunVisible = ref(false);
const selectedRun = ref<any | null>(null);
const tableHeader = [ const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" }, { label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Workflow Name", width: "7%", style: "word-break: keep-all;" }, { label: "Workflow Name", width: "18%", style: "word-break: keep-all;" },
{ label: "Step Count", width: "7%", style: "word-break: keep-all;" }, { label: "Description", width: "28%", style: "word-break: keep-all;" },
{ label: "Config Progress", width: "7%", style: "word-break: keep-all;" }, { label: "Version", width: "10%", style: "word-break: keep-all;" },
{ label: "Kubeflow Status", width: "7%", style: "word-break: keep-all;" }, { label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "7%", style: "word-break: keep-all;" }, { label: "Created DateTime", width: "15%", style: "word-break: keep-all;" },
{ label: "Action", width: "7%", style: "word-break: keep-all;" }, { label: "Action", width: "12%", style: "word-break: keep-all;" },
]; ];
const searchOptions = [ const searchOptions = [
{ label: "전체", value: "전체" as SearchType }, { label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType }, { label: "제목", value: "제목" as SearchType },
@ -77,14 +80,13 @@ const formatDateTime = (
const toRow = (w: any, no: number) => ({ const toRow = (w: any, no: number) => ({
no, no,
name: w.workflowName, name: w.name,
description: w.workflowDescription, description: w.description,
version: w.version, version: w.version,
stepCount: w.stepCount,
configProgress: w.configProgress,
kubeflowStatus: w.kubeflowStatus, kubeflowStatus: w.kubeflowStatus,
registDt: w.regDt, registDt: w.regDt,
deviceKey: w.id, deviceKey: w.id,
pipelineId: w.pipelineId ?? w.pipeline_id ?? "",
}); });
const fetchList = () => { const fetchList = () => {
@ -130,7 +132,7 @@ const fetchList = () => {
if (mapped === "TITLE") { if (mapped === "TITLE") {
list = list.filter((w: any) => list = list.filter((w: any) =>
String(w?.workflowName ?? "") String(w?.name ?? "")
.toLowerCase() .toLowerCase()
.includes(kw), .includes(kw),
); );
@ -279,6 +281,12 @@ const openDetailModal = (selectedItem: any) => {
}; };
const closeDetail = () => (openView.value = false); const closeDetail = () => (openView.value = false);
const closeRunModal = () => (isRunVisible.value = false);
const openRunModal = (item: any) => {
selectedRun.value = item;
isRunVisible.value = true;
};
const openModifyModal = (item: any) => { const openModifyModal = (item: any) => {
data.value.selectedData = { data.value.selectedData = {
@ -484,14 +492,14 @@ onMounted(() => {
</td> </td>
<td>{{ item.no }}</td> <td>{{ item.no }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.stepCount }}</td> <td>{{ item.description }}</td>
<td>{{ item.configProgress }}</td> <td>{{ item.version }}</td>
<td>{{ item.kubeflowStatus }}</td> <td>{{ item.kubeflowStatus }}</td>
<td>{{ formatDateTime(item.registDt) }}</td> <td>{{ formatDateTime(item.registDt) }}</td>
<td style="white-space: nowrap"> <td style="white-space: nowrap">
<IconRunBtn @on-click="openRunModal(item)" />
<IconInfoBtn @on-click="openDetailModal(item)" /> <IconInfoBtn @on-click="openDetailModal(item)" />
<!-- <IconSettingBtn /> --> <!-- <IconModifyBtn @on-click="openModifyModal(item)" /> -->
<IconModifyBtn @on-click="openModifyModal(item)" />
<IconDeleteBtn <IconDeleteBtn
@on-click=" @on-click="
removeData([{ deviceKey: item.deviceKey }]) removeData([{ deviceKey: item.deviceKey }])
@ -532,6 +540,14 @@ onMounted(() => {
@close-modal="closeUploadModal" @close-modal="closeUploadModal"
/> />
</v-dialog> </v-dialog>
<v-dialog v-model="isRunVisible" max-width="600" persistent>
<WorkflowsRunDialog
:pipeline-id="selectedRun?.pipelineId"
:display-name="`Run of ${selectedRun?.name ?? 'Pipeline'} (${new Date().toLocaleString()})`"
:description="selectedRun?.description || ''"
@close-modal="closeRunModal"
/>
</v-dialog>
</div> </div>
<div class="w-100" v-else> <div class="w-100" v-else>

@ -10,32 +10,22 @@ const props = defineProps<{ id: number | string }>();
const emit = defineEmits<{ (e: "close"): void }>(); const emit = defineEmits<{ (e: "close"): void }>();
const activeTab = ref<TabKey>("details"); const activeTab = ref<TabKey>("details");
// ----- Monaco Editor -----
const editorRef = ref<HTMLDivElement | null>(null); const editorRef = ref<HTMLDivElement | null>(null);
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null; let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
// ( )
const detail = ref({ const detail = ref({
workflowName: "", name: "",
version: "", version: "",
workflowDescription: "", description: "",
createdDate: "", kubeflowStatus: "",
createdId: "", namespace: "",
pipelineId: "",
regDt: "",
}); });
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 }>
>([]);
const defaultYaml = `# YAML not provided by server const defaultYaml = `# YAML not provided by server
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Workflow kind: Workflow
@ -51,30 +41,32 @@ spec:
args: ["echo hello"] args: ["echo hello"]
`; `;
/** ===== 상세 조회 ===== */ // (ISO/T )
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 fetchDetail(id: number | string) { async function fetchDetail(id: number | string) {
try { try {
const res = await WorkflowService.view(Number(id)); const res = await WorkflowService.view(Number(id));
const d = res.data; const d = res?.data ?? {};
detail.value.workflowName = d.workflowName || ""; // (/)
detail.value.version = String(d.version || 1); detail.value = {
detail.value.workflowDescription = d.workflowDescription || ""; name: d.name ?? d.workflowName ?? "",
detail.value.createdDate = d.regDt || d.regDate || "-"; version: String(d.version ?? ""),
detail.value.createdId = d.regUserId || "-"; description: d.description ?? d.workflowDescription ?? "",
kubeflowStatus: d.kubeflow_status ?? d.kubeflowStatus ?? "",
if (Array.isArray(d.steps)) { namespace: d.namespace ?? "",
steps.value = d.steps.map((s: any, idx: number) => ({ pipelineId: d.pipeline_id ?? d.pipelineId ?? "",
order: idx + 1, regDt: formatDateTime(d.reg_dt ?? d.regDt ?? d.regDate),
name: s.stepName || s.name || `Step ${idx + 1}`, };
componentType: s.componentType || s.type || "-",
status: s.status || "Not Configured",
}));
} else {
steps.value = [];
}
// YAML ( ) // YAML ( , )
const yamlFromServer = const yamlFromServer =
d.workflowYaml || d.workflowYaml ||
d.yaml || d.yaml ||
@ -86,11 +78,11 @@ async function fetchDetail(id: number | string) {
editorInstance.setValue(yamlFromServer || defaultYaml); editorInstance.setValue(yamlFromServer || defaultYaml);
} }
} catch (e) { } catch (e) {
console.error("[Child] view API failed:", e); console.error("[Workflow Detail] view API failed:", e);
} }
} }
/** ===== 마운트 & 변경 감지 ===== */ // ===== & =====
onMounted(() => { onMounted(() => {
if (editorRef.value) { if (editorRef.value) {
editorInstance = monaco.editor.create(editorRef.value, { editorInstance = monaco.editor.create(editorRef.value, {
@ -105,7 +97,6 @@ onMounted(() => {
} }
}); });
// props.id
watch( watch(
() => props.id, () => props.id,
(val) => { (val) => {
@ -122,6 +113,22 @@ onBeforeUnmount(() => {
editorInstance = null; 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> </script>
<template> <template>
@ -156,37 +163,67 @@ onBeforeUnmount(() => {
<v-card-title class="grey lighten-4 py-2 px-4"> <v-card-title class="grey lighten-4 py-2 px-4">
<span class="font-weight-bold">Workflow Information</span> <span class="font-weight-bold">Workflow Information</span>
</v-card-title> </v-card-title>
<v-card-text class="px-6 pb-6 pt-4"> <v-card-text class="px-6 pb-6 pt-4">
<v-row align="center" class="py-2"> <v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold" <v-col cols="3" class="text-h6 font-weight-bold"
>Workflow Name</v-col >Workflow Name</v-col
> >
<v-col cols="3">{{ detail.workflowName }}</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" class="text-h6 font-weight-bold">Version</v-col>
<v-col cols="3">{{ detail.version }}</v-col> <v-col cols="3">{{ detail.version || "-" }}</v-col>
</v-row> </v-row>
<v-divider class="my-2" /> <v-divider class="my-2" />
<v-row align="center" class="py-2"> <v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold" <v-col cols="3" class="text-h6 font-weight-bold"
>Workflow Description</v-col >Workflow Description</v-col
> >
<v-col cols="9">{{ detail.workflowDescription }}</v-col> <v-col cols="9">{{ detail.description || "-" }}</v-col>
</v-row> </v-row>
<v-divider class="my-2" /> <v-divider class="my-2" />
<v-row align="center" class="py-2"> <v-row align="center" class="py-2">
<v-col cols="3" class="text-h6 font-weight-bold" <v-col cols="3" class="text-h6 font-weight-bold"
>Created Date</v-col >Kubeflow Status</v-col
> >
<v-col cols="3">{{ detail.createdDate }}</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" <v-col cols="3" class="text-h6 font-weight-bold"
>Created ID</v-col >Pipeline ID</v-col
> >
<v-col cols="3">{{ detail.createdId }}</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-row>
</v-card-text> </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> </v-card>
<!-- Steps --> <!--
==================== [나중에 사용할 Step Overview 섹션] ====================
<v-card <v-card
flat flat
class="bordered-box mb-6 w-100 rounded-lg pa-8" class="bordered-box mb-6 w-100 rounded-lg pa-8"
@ -209,11 +246,7 @@ onBeforeUnmount(() => {
<template #item.order="{ index }">{{ index + 1 }}</template> <template #item.order="{ index }">{{ index + 1 }}</template>
<template #item.status="{ item }"> <template #item.status="{ item }">
<v-chip <v-chip
:color=" :color="({ Configured: 'success', 'Not Configured': 'warning' } as any)[item.status] || 'default'"
{ Configured: 'success', 'Not Configured': 'warning' }[
item.status
] || 'default'
"
small small
dark dark
> >
@ -221,13 +254,9 @@ onBeforeUnmount(() => {
</v-chip> </v-chip>
</template> </template>
</v-data-table> </v-data-table>
<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> </v-card>
========================================================================
-->
</template> </template>
<!-- YAML --> <!-- YAML -->
@ -253,10 +282,14 @@ onBeforeUnmount(() => {
min-height: 500px; min-height: 500px;
padding-bottom: 84px; padding-bottom: 84px;
} }
.back-to-list { .back-to-list {
position: absolute; position: absolute;
right: 24px; right: 24px;
bottom: 24px; bottom: 24px;
} }
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style> </style>

Loading…
Cancel
Save