-
+
+
diff --git a/src/components/templates/run/executions/ListComponentback.vue b/src/components/templates/run/executions/ListComponentback.vue
new file mode 100644
index 0000000..5869825
--- /dev/null
+++ b/src/components/templates/run/executions/ListComponentback.vue
@@ -0,0 +1,564 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-magnify
+
+
+
+
+
+
+
+
+ 총 {{ data.totalDataLength.toLocaleString() }}개
+
+
+
+
+
+
+
+
+
+ Terminate
+
+
+ Retry
+
+
+ Clone
+
+
+ Compare
+
+
+ Execution
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+ {{ item.label }}
+ |
+
+
+
+
+ |
+
+ |
+
+ {{ displayNo(i) }} |
+ {{ item.name }} |
+
+ mdi-check-circle
+ mdi-close-circle
+ mdi-loading
+ |
+ {{ item.duration }} |
+ {{ item.experiment }} |
+ {{ item.workflow }} |
+ {{ item.startTime }} |
+ {{ item.registryStatus }} |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/templates/run/executions/ViewComponent.vue b/src/components/templates/run/executions/ViewComponent.vue
index 4cc68c9..a65bba9 100644
--- a/src/components/templates/run/executions/ViewComponent.vue
+++ b/src/components/templates/run/executions/ViewComponent.vue
@@ -2,228 +2,100 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
// import FormComponent from "@/components/device/FormComponent.vue";
-import { onMounted, ref, watch } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
// const store = commonStore();
-const experimentInfo = ref({
- executionsName: "run-batch32-lr0.001",
- status: "Succeeded",
- duration: "0:00:21",
- experiment: "Baseline Model Training",
- workflow: "baseline_train_pipeline",
- startTime: "2025-05-20 10:12",
- registryStatus: "Registered",
-});
+const props = defineProps<{
+ experimentInfo: any;
+}>();
-const otaInfo = ref({
- packageName: "자율주행 타차량 예측",
- os: "Linux",
- packageFileName: "4_EdgeInfra_Perception.sh",
- packageFilePath: "/home/etri/TeslaSystem/EdgeInfraVision/RUN",
- softwareName: "4_EdgeInfra_Perception.sh",
- softwareVersion: "v2.0",
- execute: "Not Executed",
-});
+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 data = ref({
- params: {
- pageNum: 1,
- pageSize: 10,
- searchType: "",
- searchText: "",
- },
- results: [],
- totalDataLength: 0,
- pageLength: 0,
- modalMode: "",
- selectedData: null,
- allSelected: false,
- selected: [],
- isModalVisible: false,
- isConfirmDialogVisible: false,
- userOption: [],
+// 히스토리에서 각 단계의 기록 찾기
+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;
});
-const getCodeList = () => {
- // UserService.search(data.value.params).then((d) => {
- // if (d.status === 200) {
- // data.value.userOption = d.data.userList;
- // }
- // });
-};
+// 3단계 고정 스텝 정의
+const steps = computed(() => {
+ const lastLabel = (hTerminal.value?.state || "COMPLETED").toUpperCase();
-const getData = () => {
- const params = { ...data.value.params };
- if (params.searchType === "" || params.searchText === "") {
- delete params.searchType;
- delete params.searchText;
- }
- data.value.results = [
- {
- name: "run-batch32-lr0.001",
- status: "Succeeded",
- Duration: "0:00:21",
- configProgress: "0/2",
- Pipeline: "baseline_train_pipeline",
- registDt: "2025-06-10T00:00:00Z",
- },
- {
- name: "run-batch64-lr0.001",
- status: "Failed",
- Duration: "0:00:21",
- configProgress: "1/3",
- Pipeline: "baseline_train_pipeline",
- registDt: "2025-06-09T00:00:00Z",
- },
+ return [
{
- name: "run-batch32-lr0.0005",
- status: "Succeeded",
- Duration: "0:00:21",
- configProgress: "0/3",
- Pipeline: "baseline_train_pipeline",
- registDt: "2025-06-01T00:00:00Z",
+ key: "PENDING",
+ label: "PENDING",
+ active: !!(hPending.value || hRunning.value || hTerminal.value),
+ color: "primary",
+ icon: "mdi-clock-outline",
+ ts: hPending.value?.update_time,
},
{
- name: "run-batch64-lr0.0005",
- status: "Running",
- Duration: "0:00:21",
- configProgress: "1/3",
- Pipeline: "baseline_train_pipeline",
- registDt: "2025-05-29T00:00:00Z",
+ key: "RUNNING",
+ label: "RUNNING",
+ active: !!(hRunning.value || hTerminal.value),
+ color: "info",
+ icon: "mdi-progress-clock",
+ ts: hRunning.value?.update_time,
},
{
- name: "run-augmented-data",
- status: "Succeeded",
- Duration: "0:00:21",
- configProgress: "0/3",
- Pipeline: "baseline_train_pipeline",
- registDt: "2025-05-31T00:00:00Z",
+ 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,
},
];
- data.value.totalDataLength = 5;
- setPaginationLength();
- // DeviceService.search(params).then((d) => {
- // if (d.status === 200) {
- // data.value.results = d.data.deviceList;
- // data.value.totalDataLength = d.data.totalCount;
- // setTimeout(() => {
- // setPaginationLength();
- // }, 200);
- // } else {
- // store.setSnackbarMsg({
- // text: "디바이스 조회 실패",
- // color: "error",
- // });
- // }
- // });
- // DeviceService.search().then((d) => {
- // data.value.totalDataLength = d.data.totalCount;
- // setTimeout(() => {
- // setPaginationLength();
- // }, 200);
- // });
-};
-
-const setPaginationLength = () => {
- if (data.value.totalDataLength % data.value.params.pageSize === 0) {
- data.value.pageLength =
- data.value.totalDataLength / data.value.params.pageSize;
- } else {
- data.value.pageLength = Math.ceil(
- data.value.totalDataLength / data.value.params.pageSize,
- );
- }
-};
-
-const saveData = (formData) => {
- if (data.value.modalMode === "create") {
- // DeviceService.add(formData).then((d) => {
- // if (d.status === 200) {
- // data.value.isModalVisible = false;
- // store.setSnackbarMsg({
- // text: "등록 되었습니다.",
- // result: 200,
- // });
- // changePageNum(1);
- // } else {
- // store.setSnackbarMsg({
- // text: d,
- // result: 500,
- // });
- // }
- // });
- } else {
- // DeviceService.update(formData.deviceKey, formData).then((d) => {
- // if (d.status === 200) {
- // data.value.isModalVisible = false;
- // store.setSnackbarMsg({
- // text: "수정 되었습니다.",
- // result: 200,
- // });
- // changePageNum();
- // } else {
- // store.setSnackbarMsg({
- // text: d,
- // result: 500,
- // });
- // }
- // });
- }
-};
-
-const removeData = (value) => {
- let removeList = value ? value : data.value.selected;
- const remove = (code) => {
- // return DeviceService.delete(code).then((d) => {
- // if (d.status !== 200) {
- // store.setSnackbarMsg({
- // text: d,
- // result: 500,
- // });
- // }
- // });
- };
+});
- if (removeList.length === 1) {
- remove(removeList[0].deviceKey).then(() => {
- // store.setSnackbarMsg({
- // text: "삭제되었습니다.",
- // result: 200,
- // });
- changePageNum();
- data.value.isConfirmDialogVisible = false;
- data.value.selected = [];
- data.value.allSelected = false;
- });
- } else {
- Promise.all(removeList.map((item) => remove(item.deviceKey))).finally(
- () => {
- // store.setSnackbarMsg({
- // text: "모두 삭제되었습니다.",
- // result: 200,
- // });
- changePageNum();
- data.value.isConfirmDialogVisible = false;
- data.value.selected = [];
- data.value.allSelected = false;
- },
- );
- }
-};
+// 고정 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 changePageNum = (page) => {
- data.value.params.pageNum = page;
- getData();
-};
-
-const emit = defineEmits<{
- (e: "close"): void;
-}>();
+// 유틸
+const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
+// 콘솔로 확인
onMounted(() => {
- getData();
- getCodeList();
+ console.log("[Child] 받은 데이터:", props.experimentInfo);
});
@@ -243,97 +115,204 @@ onMounted(() => {
- Deploy Model Information
+ Execution Information
-
- Executions Name
-
- {{
- experimentInfo.executionsName
- }}
+ Name
+ {{ props.experimentInfo.name }}
-
Status
- mdi-check-circle
- mdi-close-circle
- mdi-loading
+ mdi-loading
+ {{ props.experimentInfo.status }}
+
+
- Duration
- {{ experimentInfo.duration }}
+ Duration
+ {{
+ props.experimentInfo.duration
+ }}
+
+
+
+
+ Experiment ID
+ {{
+ props.experimentInfo.experiment
+ }}
-
+
- Experiment
- {{ experimentInfo.experiment }}
+ Workflow
+ {{
+ props.experimentInfo.workflow
+ }}
+
- Workflow
- {{ experimentInfo.workflow }}
Start Time
- {{ experimentInfo.startTime }}
+ {{
+ props.experimentInfo.startTime
+ }}
-
Registry Status
{{
- experimentInfo.registryStatus
+ props.experimentInfo.registryStatus
}}
+
+
+
+
+ State History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ s.icon }}
+
+
+
+
+
+ {{ s.label }}
+
+
+ {{ fmt(s.ts) }}
+
+
+
+
+
+
+
- Back to List
+ Back to List
-
diff --git a/src/components/templates/workflow/ViewComponent.vue b/src/components/templates/workflow/ViewComponent.vue
index 44daca1..7465f1e 100644
--- a/src/components/templates/workflow/ViewComponent.vue
+++ b/src/components/templates/workflow/ViewComponent.vue
@@ -3,6 +3,7 @@ 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";
@@ -15,7 +16,20 @@ const activeTab = ref
("details");
const editorRef = ref(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: "",
@@ -41,7 +55,7 @@ spec:
args: ["echo hello"]
`;
-// 간단한 날짜 포맷터 (ISO/T 포함 모두 대응)
+// 날짜 포맷
function formatDateTime(raw?: string): string {
if (!raw) return "-";
const s = String(raw).replace("T", " ");
@@ -49,13 +63,30 @@ function formatDateTime(raw?: string): string {
return m ? m[1] : s.slice(0, 19);
}
-// ===== 상세 조회 =====
+/** ⬅️ 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 {
const res = await WorkflowService.view(Number(id));
const d = res?.data ?? {};
- // 백엔드 필드명(스네이크/카멜) 혼재 대비 매핑
+ // 정보 매핑(스네이크/카멜 혼용 방어)
detail.value = {
name: d.name ?? d.workflowName ?? "",
version: String(d.version ?? ""),
@@ -66,7 +97,9 @@ async function fetchDetail(id: number | string) {
regDt: formatDateTime(d.reg_dt ?? d.regDt ?? d.regDate),
};
- // YAML (있으면 보여주고, 없으면 기본 예시)
+ ensureEditor();
+
+ // 1) 서버가 YAML 문자열을 직접 줄 때
const yamlFromServer =
d.workflowYaml ||
d.yaml ||
@@ -74,27 +107,45 @@ async function fetchDetail(id: number | string) {
d.specYaml ||
d.yamlStr ||
"";
- if (editorInstance) {
- editorInstance.setValue(yamlFromServer || defaultYaml);
+ if (yamlFromServer) {
+ editorInstance!.setValue(yamlFromServer);
+ return;
+ }
+
+ // 2) 업로드 파일의 object key(=storagePath 등) 후보
+ const objectKeyCandidates = [
+ d.yamlStoragePath,
+ d.yaml_object_name,
+ d.yamlObjectName,
+ d.storagePath,
+ d.storedName,
+ d.objectName,
+ d.object_key,
+ d.yamlPath,
+ d.filePath,
+ d.yamlFile,
+ d.yamlFilePath,
+ ].filter(Boolean) as string[];
+
+ // 3) 후보 중 하나라도 성공하면 반환
+ for (const key of objectKeyCandidates) {
+ const ok = await loadYamlFromStoragePath(key);
+ if (ok) return;
}
+
+ // 4) 전부 실패하면 기본 YAML 출력
+ editorInstance!.setValue(defaultYaml);
} catch (e) {
console.error("[Workflow Detail] view API failed:", e);
+ ensureEditor();
+ editorInstance!.setValue(defaultYaml);
}
}
// ===== 마운트 & 변경 감지 =====
onMounted(() => {
- if (editorRef.value) {
- editorInstance = monaco.editor.create(editorRef.value, {
- value: defaultYaml,
- language: "yaml",
- theme: "vs-dark",
- readOnly: true,
- automaticLayout: true,
- minimap: { enabled: false },
- lineNumbers: "on",
- });
- }
+ // 탭 전환 관계없이 미리 생성해도 ok (automaticLayout)
+ ensureEditor();
});
watch(
@@ -107,14 +158,20 @@ watch(
{ immediate: true },
);
+// YAML 탭으로 전환되었을 때도 에디터 보장
+watch(
+ () => activeTab.value,
+ (tab) => {
+ if (tab === "yaml") ensureEditor();
+ },
+);
+
onBeforeUnmount(() => {
- if (editorInstance) {
- editorInstance.dispose();
- editorInstance = null;
- }
+ editorInstance?.dispose();
+ editorInstance = null;
});
-// ===== (나중 사용) Step 테이블 정의 =====
+// (나중 사용) Step 테이블
const stepHeaders = [
{ title: "Order", key: "order", width: "10%", align: "center" },
{ title: "Step Name", key: "name", width: "40%", align: "center" },
diff --git a/src/router/index.js b/src/router/index.js
index 82f1c49..5da2208 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -122,16 +122,13 @@ const router = createRouter({
routes,
});
-router.beforeEach((to) => {
+router.beforeEach((to, from) => {
const authed = !!(
typeof storage.getToken === "function" && storage.getToken()
);
-
- const isLogin = to.name === "login" || to.path === "/login";
- const isSignup = to.name === "signup" || to.path === "/signup";
- const isSelect = to.name === "select" || to.path === "/select";
-
- const bootDone = sessionStorage.getItem("initialRedirectDone") === "1";
+ const isLogin = to.name === "login";
+ const isSignup = to.name === "signup";
+ const isSelect = to.name === "select";
if (!authed) {
if (!isLogin && !isSignup) {
@@ -140,32 +137,26 @@ router.beforeEach((to) => {
return true;
}
- if (!bootDone && !isSelect && !isLogin && !isSignup) {
- sessionStorage.setItem("initialRedirectDone", "1");
- return { name: "select", replace: true, query: { redirect: to.fullPath } };
- }
-
- if (to.matched.some((r) => r.meta?.requiresAdmin)) {
- try {
- const raw =
- typeof storage?.getAuth === "function"
- ? storage.getAuth()
- : JSON.parse(localStorage.getItem("autoflow-auth") || "null");
-
- const roles = raw?.userInfo?.roles ?? raw?.roles ?? [];
- const authCd = raw?.userInfo?.authCd ?? raw?.authCd ?? raw?.auth;
+ const hasProject = !!localStorage.getItem("projectId"); // ✅ 프로젝트 선택 여부
+ const bootDone = sessionStorage.getItem("initialRedirectDone") === "1";
- const isAdmin =
- (Array.isArray(roles)
- ? roles.includes("ROLE_ADMIN")
- : roles === "ROLE_ADMIN") || authCd === "ADMIN";
+ // 이미 프로젝트 선택됨 → 어떤 화면이든 통과
+ if (hasProject) return true;
- if (!isAdmin) {
- return { name: "home", replace: true };
- }
- } catch {
- return { name: "home", replace: true };
+ // 아직 프로젝트 미선택
+ if (!bootDone) {
+ // ✅ 선택 화면에 "들어온 순간"을 부트 완료로 간주 (여기서 한 번만 세팅)
+ if (isSelect) {
+ sessionStorage.setItem("initialRedirectDone", "1");
+ return true;
}
+ // ✅ select 로 1회만 보냄
+ return { name: "select", replace: true, query: { redirect: to.fullPath } };
+ }
+
+ // 부트 완료인데 여전히 프로젝트 미선택이면, select만 허용
+ if (!isSelect) {
+ return { name: "select", replace: true, query: { redirect: to.fullPath } };
}
return true;
diff --git a/src/views/Select.vue b/src/views/Select.vue
index a2503de..4b3f684 100644
--- a/src/views/Select.vue
+++ b/src/views/Select.vue
@@ -1,6 +1,6 @@