feat: DataGroup 컴포넌트 추가, Executions Status 수정, 대시보드 데이터 바인딩

main
jschoi 9 months ago
parent 4686379184
commit 8114ca58c5

@ -1,3 +1,3 @@
NODE_ENV = "development"
VITE_APP_API_SERVER_URL = "http://localhost:80"
VITE_APP_API_SERVER_URL = "http://localhost:8080"
VITE_ROOT_PATH = ""

4
components.d.ts vendored

@ -10,6 +10,7 @@ declare module 'vue' {
export interface GlobalComponents {
AppFooter: typeof import('./src/components/AppFooter.vue')['default']
CompareComponent: typeof import('./src/components/templates/run/executions/CompareComponent.vue')['default']
DatagroupBaseDoalog: typeof import('./src/components/atoms/organisms/DatagroupBaseDoalog.vue')['default']
DatasetBaseDoalog: typeof import('./src/components/atoms/organisms/DatasetBaseDoalog.vue')['default']
DeploymentDialog: typeof import('./src/components/atoms/organisms/DeploymentDialog.vue')['default']
DrawerComponent: typeof import('./src/components/common/DrawerComponent.vue')['default']
@ -26,7 +27,8 @@ declare module 'vue' {
IconRunBtn: typeof import('./src/components/atoms/button/IconRunBtn.vue')['default']
IconSettingBtn: typeof import('./src/components/atoms/button/IconSettingBtn.vue')['default']
LayoutComponent: typeof import('./src/components/common/LayoutComponent.vue')['default']
ListComponent: typeof import('./src/components/templates/Datasets/ListComponent.vue')['default']
ListComponent: typeof import('./src/components/templates/datagroup/ListComponent.vue')['default']
ListComponentback: typeof import('./src/components/templates/run/executions/ListComponentback.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SidebarHeader: typeof import('./src/components/common/SidebarHeader.vue')['default']

@ -0,0 +1,210 @@
<script setup lang="ts">
import IconArrowDown from "@/components/atoms/button/IconArrowDown.vue";
import IconArrowUp from "@/components/atoms/button/IconArrowUp.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import { computed, onBeforeUnmount, onMounted, watch, ref } from "vue";
import { DataGroupService } from "@/components/service/management/DataGroupService";
import { storage } from "@/utils/storage";
import { storeToRefs } from "pinia";
import { useAutoflowStore } from "@/stores/autoflowStore";
const { projectId } = storeToRefs(useAutoflowStore());
const props = defineProps<{ editData: any; mode: "create" | "edit" }>();
const emit = defineEmits<{
(e: "close-modal"): void;
(e: "saved", v: any): void;
}>();
const isEdit = computed(() => props.mode === "edit");
const saving = ref(false);
const errorMsg = ref("");
const form = ref({ name: "", description: "" });
//
function hydrateFormFromEdit(data: any) {
if (!data) return;
form.value.name = data.workflowName ?? data.dsNm ?? data.name ?? "";
form.value.description =
data.workflowDescription ?? data.dsDesc ?? data.description ?? "";
}
onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData);
});
watch(
() => props.editData,
(v) => {
if (isEdit.value) hydrateFormFromEdit(v);
},
);
// /
function getAuthUser() {
const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
const ui = authObj?.userInfo ?? authObj?.userinfo ?? authObj ?? {};
return { id: Number(ui.id), username: String(ui.username ?? "").trim() };
}
function cleanUndefined<T extends Record<string, any>>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== undefined),
) as T;
}
async function submit() {
errorMsg.value = "";
const name = (form.value.name ?? "").trim(); // (/ OK)
if (!name) {
errorMsg.value = "이름을 입력하세요.";
return;
}
const { id: userId, username } = getAuthUser();
if (!userId || !username) {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return;
}
if (!projectId.value) {
errorMsg.value = "프로젝트가 선택되지 않았습니다.";
return;
}
try {
saving.value = true;
if (isEdit.value) {
const rawId = props.editData?.id ?? props.editData?.deviceKey;
const id = Number(rawId);
if (!id) {
errorMsg.value = "수정할 ID가 없습니다.";
return;
}
const viewRes = await DataGroupService.view(id);
const current = (viewRes?.data ?? viewRes) || {};
const updatePayload = {
id,
dsNm: name,
dsDesc: form.value.description ?? "",
projectId: current.projectId,
regUserId: current.regUserId,
regUserNm: current.regUserNm,
modUserId: userId,
modUserNm: username,
};
console.log(id);
const { data } = await DataGroupService.update(id, updatePayload);
emit("saved", data);
emit("close-modal");
} else {
const createPayload = {
dsNm: name,
dsDesc: form.value.description ?? "",
regUserId: userId,
regUserNm: username,
projectId: projectId.value!,
};
const { data } = await DataGroupService.add(createPayload);
emit("saved", data);
emit("close-modal");
}
} catch (e: any) {
console.error("데이터그룹 저장 실패:", e);
const status = e?.response?.status;
const raw =
(typeof e?.response?.data === "string"
? e?.response?.data
: e?.response?.data?.message || e?.response?.data?.error) ||
e?.message ||
"";
if (status === 409)
errorMsg.value = "같은 이름의 데이터그룹이 이미 존재합니다.";
else if (status === 400) errorMsg.value = "요청 형식이 올바르지 않습니다.";
else if (status === 401 || status === 403)
errorMsg.value = "권한이 없거나 로그인 정보가 만료되었습니다.";
else errorMsg.value = raw || `요청 실패 (HTTP ${status ?? "Error"})`;
} finally {
saving.value = false;
}
}
function onEsc(e: KeyboardEvent) {
if (e.key === "Escape") emit("close-modal");
}
onMounted(() => window.addEventListener("keydown", onEsc));
onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</script>
<template>
<v-card>
<v-card-title
class="text-white font-weight-bold text-h6"
style="background-color: #1976d2"
>
{{ isEdit ? "Edit DataGroup" : "Create DataGroup" }}
</v-card-title>
<v-card-text class="pa-6">
<div class="text-subtitle-1 font-weight-medium mb-4">
DataGroup Information
</div>
<v-form @submit.prevent="submit">
<!-- Name: 제한 없음 -->
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>DataGroup Name</label
>
<v-text-field
v-model="form.name"
variant="outlined"
:disabled="saving"
dense
hide-details="auto"
required
/>
</div>
<!-- Description: 제한 없음 -->
<div class="mb-5">
<label class="text-subtitle-2 font-weight-medium mb-1 d-block"
>Description</label
>
<v-textarea
v-model="form.description"
variant="outlined"
:disabled="saving"
rows="3"
dense
hide-details="auto"
/>
</div>
<div v-if="errorMsg" class="mt-3 text-error">{{ errorMsg }}</div>
</v-form>
</v-card-text>
<v-card-actions class="justify-end" style="padding: 16px 24px">
<v-btn color="success" :loading="saving" @click="submit">
{{ isEdit ? "Update" : "Save" }}
</v-btn>
<v-btn
text
class="white--text"
:disabled="saving"
@click="$emit('close-modal')"
>Close</v-btn
>
</v-card-actions>
</v-card>
</template>

@ -28,6 +28,7 @@ function hydrateFormFromEdit(d: any) {
form.value.description = (d?.description ?? "") + "";
}
const getRefId = () => String(props.editData?.refId ?? "0");
onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData);
});
@ -39,7 +40,7 @@ watch(
);
const dialogTitle = computed(() =>
isEdit.value ? "Edit Training Script" : "Create Training Script",
isEdit.value ? "Edit DataSet Script" : "Create DataSet Script",
);
//
@ -94,7 +95,7 @@ async function submit() {
return (errorMsg.value = "프로젝트가 선택되지 않았습니다.");
const fd = new FormData();
fd.append("refId", "0");
fd.append("refId", getRefId());
fd.append("refType", "DATASET");
fd.append("title", title);
fd.append("description", desc);

@ -28,6 +28,7 @@ function hydrateFormFromEdit(d: any) {
form.value.description = (d?.description ?? "") + "";
}
const getRefId = () => String(props.editData?.refId ?? "0");
onMounted(() => {
if (isEdit.value) hydrateFormFromEdit(props.editData);
});
@ -59,7 +60,6 @@ const regUserId = (() => {
}
})();
//
async function submit() {
errorMsg.value = "";
@ -95,7 +95,7 @@ async function submit() {
return (errorMsg.value = "프로젝트가 선택되지 않았습니다.");
const fd = new FormData();
fd.append("refId", "0");
fd.append("refId", getRefId());
fd.append("refType", "TRAINING_SCRIPT");
fd.append("title", title);
fd.append("description", desc);

@ -169,14 +169,10 @@ async function submit() {
const authObj =
(typeof storage?.getAuth === "function" ? storage.getAuth() : null) ??
JSON.parse(localStorage.getItem("autoflow-auth") || "{}");
const regUserId =
authObj?.userInfo?.username ??
authObj?.userinfo?.username ??
authObj?.username ??
authObj?.userId ??
"";
if (!regUserId) {
const ui = authObj?.userInfo ?? authObj?.userinfo ?? authObj ?? {};
const userId = Number(ui.id); // ID
if (!userId) {
errorMsg.value = "로그인 사용자 정보를 찾을 수 없습니다.";
return;
}
@ -198,28 +194,21 @@ async function submit() {
errorMsg.value = "수정할 ID가 없습니다.";
return;
}
//
const viewRes = await WorkflowService.view(id);
const current = (viewRes?.data ?? viewRes) || {};
// name/description , null
const updatePayload = cleanUndefined({
id,
name, //
description: form.value.description?.trim() || "", //
// ===== =====
name,
description: form.value.description?.trim() || "",
displayName: current.displayName,
namespace: current.namespace,
pipelineId: current.pipelineId,
kubeflowStatus: current.kubeflowStatus,
version: current.version,
regUserId: current.regUserId ?? regUserId,
projectId: current.projectId ?? projectId.value,
regUserId: current.regUserId,
regDt: current.regDt,
modDt: now,
projectId: current.projectId ?? projectId.value,
});
const { data } = await WorkflowService.update(id, updatePayload);
@ -236,7 +225,7 @@ async function submit() {
display_name: name,
description: form.value.description?.trim() || "",
namespace: "default",
regUserId,
regUserId: userId,
projectId: projectId.value!,
uploadfile: form.value.file,
};

@ -12,12 +12,9 @@ const router = useRouter();
const username = ref("");
const projectName = ref(localStorage.getItem("projectName") || "");
// ----------------------
// Admin + Admin
// ----------------------
const isAdmin = ref(false); //
const adminMode = ref(false); //
const lastNonAdminPath = ref("/home"); //
const isAdmin = ref(false);
const adminMode = ref(false);
const lastNonAdminPath = ref("/home");
function computeIsAdmin() {
try {
@ -79,11 +76,19 @@ function updateUsername() {
const auth = storage.getAuth?.() ?? null;
username.value = auth?.userInfo?.username ?? auth?.username ?? "";
}
function syncAdminModeWithRoute() {
const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin);
if (!isAdminRoute && adminMode.value) {
adminMode.value = false;
}
}
function refreshProjectName() {
const v = localStorage.getItem("projectName");
projectName.value = v ? v : "";
}
function goSelect() {
adminMode.value = false;
router.push("/select");
}
function logOut() {
@ -103,13 +108,17 @@ function logOut() {
// storage
function onStorage(e) {
if (!e.key || e.key === "projectName") refreshProjectName();
if (!e.key || e.key === "projectName") {
refreshProjectName();
adminMode.value = false;
}
if (!e.key || e.key === "autoflow-auth" || e.key === "auth") {
updateUsername();
computeIsAdmin();
}
}
const goMain = () => {
adminMode.value = false;
router.push("/home");
};
@ -120,6 +129,7 @@ watch(
refreshProjectName();
const isAdminRoute = route.matched.some((r) => r.meta?.requiresAdmin);
if (!isAdminRoute) lastNonAdminPath.value = route.fullPath || "/home";
syncAdminModeWithRoute();
},
{ immediate: true },
);
@ -130,6 +140,7 @@ onMounted(() => {
refreshProjectName();
menu.value = menuItems;
window.addEventListener("storage", onStorage);
syncAdminModeWithRoute();
});
onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage);

@ -3,13 +3,11 @@ export type KubeflowUploadDto = {
display_name?: string;
description?: string;
namespace?: string;
regUserId: string;
regUserId: string | number; // number도 허용
projectId: number | string;
uploadfile: File | Blob;
};
export type kubeflow = FormData;
export function toKubeflowForm(dto: KubeflowUploadDto): FormData {
const fd = new FormData();
fd.append("name", dto.name);

@ -2,6 +2,7 @@ import {
DataGroup,
DataGroupSearch,
} from "@/components/models/management/DataGroup";
import { request } from "@/components/service/index";
export const DataGroupService = {
add: (payload: DataGroup) => {
@ -16,8 +17,8 @@ export const DataGroupService = {
view: (id: Number) => {
return request.get(`/api/datagroup/${id}`, {});
},
update: (id: number, payload: DataGroup) => {
return request.put(`/api/datagroup/${id}`, payload);
update: (id: number, updatePayload: DataGroup) => {
return request.put(`/api/datagroup/${id}`, updatePayload);
},
search: (payload: DataGroupSearch) => {
return request.get("/api/datagroup/search", payload);

@ -1,4 +1,4 @@
import { kubeflow } from "@/components/models/management/Kubeflow";
import { Kubeflow } from "@/components/models/management/Kubeflow";
import { request } from "@/components/service/index";
export const KubeflowService = {
upload: (payload: kubeflow) => {

@ -2,13 +2,18 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import { onMounted, ref } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { storage } from "@/utils/storage";
import ViewComponent from "@/components/templates/Datasets/ViewComponent.vue";
import DatasetBaseDoalog from "@/components/atoms/organisms/DatasetBaseDoalog.vue";
import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { commonStore } from "@/stores/commonStore";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const activeRefId = ref<number | null>(null);
const activeRefName = computed(() => String(route.query.refName));
const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
@ -69,6 +74,12 @@ const data = ref({
userOption: [] as any[],
});
function getRefIdFromRoute(q: any): number | null {
const raw = q?.refId;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? n : null;
}
//
function readUsernameFromStorage(): string {
try {
@ -140,6 +151,7 @@ const fetchList = async () => {
sortField: "id",
sortDirection: "DESC",
refType: "DATASET",
refId: activeRefId.value,
};
try {
@ -286,6 +298,7 @@ const openCreateModal = () => {
data.value.selectedData = {
username: username.value,
projectId: getProjectId(),
refId: activeRefId.value,
};
data.value.isCreateVisible = true;
};
@ -318,8 +331,18 @@ const getSelectedAllData = () => {
};
onMounted(() => {
username.value = readUsernameFromStorage();
activeRefId.value = getRefIdFromRoute(route.query);
fetchList();
});
watch(
() => route.query.refId,
() => {
activeRefId.value = getRefIdFromRoute(route.query);
data.value.params.pageNum = 1;
fetchList();
},
);
</script>
<template>
@ -413,6 +436,26 @@ onMounted(() => {
/>
</v-responsive>
</v-sheet>
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip
v-if="activeRefId"
class="ml-2"
color="secondary"
variant="tonal"
>
Filter: DataGroup {{ activeRefName }}
</v-chip>
<v-btn
v-if="activeRefId"
size="small"
variant="text"
@click="router.replace({ path: '/DataGroup' })"
>
필터 해제
</v-btn>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">

@ -3,55 +3,80 @@ import { onMounted, ref, watch } from "vue";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import tz from "dayjs/plugin/timezone";
import { useRouter } from "vue-router";
import { commonStore } from "@/stores/commonStore";
import { DataGroupService } from "@/components/service/management/DataGroupService";
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import WorkflowsBaseDialog from "@/components/atoms/organisms/WorkflowsBaseDialog.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 IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconRunBtn from "@/components/atoms/button/IconRunBtn.vue";
import DatagroupBaseDoalog from "@/components/atoms/organisms/DatagroupBaseDoalog.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
/* -------------------------
* Dayjs & Router
* ------------------------*/
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
const store = commonStore();
const openView = ref(false);
type SearchType = "전체" | "제목" | "작성자";
const isRunVisible = ref(false);
const selectedRun = ref<any | null>(null);
const router = useRouter();
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Workflow Name", width: "18%", style: "word-break: keep-all;" },
{ label: "Description", width: "28%", style: "word-break: keep-all;" },
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
{ label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "15%", style: "word-break: keep-all;" },
{ label: "Action", width: "12%", style: "word-break: keep-all;" },
];
const searchOptions = [
/* -------------------------
* Types
* ------------------------*/
type SearchType = "전체" | "제목" | "작성자";
type ApiSearchType = "ALL" | "TITLE" | "AUTHOR";
interface DataGroupRow {
no: number;
name: string;
description: string;
author: string;
registDt: string | number | Date;
deviceKey: number;
}
/* -------------------------
* Constants
* ------------------------*/
const TABLE_HEADERS = [
{ label: "No", width: "6%", style: "word-break: keep-all;" },
{ label: "DataGroup Name", width: "24%", style: "word-break: keep-all;" },
{ label: "Description", width: "32%", style: "word-break: keep-all;" },
{ label: "Author", width: "14%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "16%", style: "word-break: keep-all;" },
{ label: "Action", width: "8%", style: "word-break: keep-all;" },
] as const;
const SEARCH_OPTIONS = [
{ label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType },
{ label: "사용자", value: "작성자" as SearchType },
{ label: "작성자", value: "작성자" as SearchType },
];
const pageSizeOptions = [
const PAGE_SIZE_OPTIONS = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
const SEARCH_TYPE_MAP: Record<SearchType | "", ApiSearchType> = {
"": "ALL",
전체: "ALL",
제목: "TITLE",
작성자: "AUTHOR",
};
/* -------------------------
* Store & Local State
* ------------------------*/
const store = commonStore();
const openView = ref(false);
const isRunVisible = ref(false); // ( )
const selectedRun = ref<any | null>(null);
const data = ref({
params: {
pageNum: 1,
@ -59,7 +84,7 @@ const data = ref({
searchType: "전체" as SearchType,
searchText: "",
},
results: [] as any[],
results: [] as DataGroupRow[],
totalElements: 0,
pageLength: 0,
modalMode: "" as "create" | "edit" | "upload" | "",
@ -73,137 +98,144 @@ const data = ref({
userOption: [] as any[],
});
/* -------------------------
* Utils
* ------------------------*/
const formatDateTime = (
v?: string | number | Date,
fmt = "YYYY-MM-DD HH:mm:ss",
) => (v ? dayjs(v).tz(KST).format(fmt) : "");
const toRow = (w: any, no: number) => ({
const toRow = (g: any, no: number): DataGroupRow => ({
no,
name: w.name,
description: w.description,
version: w.version,
kubeflowStatus: w.kubeflowStatus,
registDt: w.regDt,
deviceKey: w.id,
pipelineId: w.pipelineId ?? w.pipeline_id ?? "",
name: g.dsNm,
description: g.dsDesc,
author: g.regUserNm,
registDt: g.regDate,
deviceKey: g.id,
});
const fetchList = () => {
function onRowClick(row: DataGroupRow) {
const id = Number(row?.deviceKey);
if (!Number.isFinite(id)) return;
router.push({
path: "/datasets",
query: { refId: String(id), refName: row.name },
});
}
/* -------------------------
* Data Loaders
* ------------------------*/
async function fetchList() {
const projectId = Number(localStorage.getItem("projectId"));
if (!projectId) {
console.warn("[Workflows] projectId 없음 — 프로젝트 먼저 선택");
console.warn("[DataGroup] projectId 없음 — 프로젝트 먼저 선택");
data.value.results = [];
data.value.totalElements = 0;
data.value.pageLength = 0;
return;
}
const { pageNum, searchText, searchType } = data.value.params;
//
const { pageNum, pageSize, searchType, searchText } = data.value.params;
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
const keyword = (searchText || "").trim();
const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
let reqPage = data.value.params.pageNum;
let reqSize = data.value.params.pageSize;
if (needLocalFilter) {
reqPage = 0;
reqSize = 1000;
}
const payload = {
projectId,
page: reqPage,
size: reqSize,
keyword,
searchType: mapped,
};
DataGroupService.search(payload)
.then((res: any) => {
if (res.status !== 200) return;
const result = res.data;
let list = result?.content ?? [];
if (needLocalFilter) {
const kw = keyword.toLowerCase();
if (mapped === "TITLE") {
list = list.filter((w: any) =>
String(w?.name ?? "")
.toLowerCase()
.includes(kw),
);
} else if (mapped === "AUTHOR") {
list = list.filter((w: any) =>
String(w?.regUserId ?? "")
.toLowerCase()
.includes(kw),
);
}
const uiSize = data.value.params.pageSize;
const totalElements = list.length;
const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
const safePage = Math.min(Math.max(1, pageNum), totalPages);
const start = (safePage - 1) * uiSize;
const pageSlice = list.slice(start, start + uiSize);
// ()
const firstNo = totalElements - start;
data.value.results = pageSlice.map((w: any, i: number) =>
toRow(w, firstNo - i),
// ( )
const reqPage = needLocalFilter ? 0 : pageNum - 1; // 0-based
const reqSize = needLocalFilter ? 1000 : pageSize;
try {
const payload = {
projectId,
page: reqPage,
size: reqSize,
keyword,
searchType: mapped,
};
const res: any = await DataGroupService.search(payload);
if (res?.status !== 200) return;
const result = res.data;
let list: any[] = result?.content ?? [];
//
if (needLocalFilter) {
const kw = keyword.toLowerCase();
if (mapped === "TITLE") {
list = list.filter((w: any) =>
String(w?.name ?? "")
.toLowerCase()
.includes(kw),
);
} else if (mapped === "AUTHOR") {
list = list.filter((w: any) =>
String(w?.regUserNm ?? "")
.toLowerCase()
.includes(kw),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
return;
}
const totalElements = result.totalElements;
const totalPages = result.totalPages;
const serverPage = result.pageable.pageNumber;
const serverSize = result.pageable.pageSize;
const offset =
typeof result.pageable.offset === "number"
? result.pageable.offset
: serverPage * serverSize;
const firstNo = totalElements - offset;
data.value.results = list.map((w: any, i: number) =>
const uiSize = pageSize;
const totalElements = list.length;
const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
const safePage = Math.min(Math.max(1, pageNum), totalPages);
const start = (safePage - 1) * uiSize;
const pageSlice = list.slice(start, start + uiSize);
const firstNo = totalElements - start;
data.value.results = pageSlice.map((w: any, i: number) =>
toRow(w, firstNo - i),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
})
.catch((err: any) => console.error("워크플로우 조회 에러:", err));
};
return;
}
const doSearch = () => {
//
const totalElements = result.totalElements ?? list.length;
const totalPages = result.totalPages ?? 1;
const serverPage = result.pageable?.pageNumber ?? 0;
const serverSize = result.pageable?.pageSize ?? pageSize;
const offset =
typeof result.pageable?.offset === "number"
? result.pageable.offset
: serverPage * serverSize;
const firstNo = totalElements - offset;
data.value.results = list.map((w: any, i: number) => toRow(w, firstNo - i));
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
} catch (err) {
console.error("[DataGroup] 조회 에러:", err);
}
}
/* -------------------------
* Actions
* ------------------------*/
function doSearch() {
data.value.params.pageNum = 1;
fetchList();
};
const changePageNum = (page: number) => {
}
function changePageNum(page: number) {
data.value.params.pageNum = page;
fetchList();
};
const changePageSize = (size: number) => {
}
function changePageSize(size: number) {
data.value.params.pageSize = size;
data.value.params.pageNum = 1;
fetchList();
};
}
const removeData = (value?: Array<{ deviceKey: number }>) => {
function removeData(value?: Array<{ deviceKey: number }>) {
const removeList = value ?? data.value.selected;
if (!removeList || removeList.length === 0) return;
if (!removeList?.length) return;
const ids = removeList.map((x) => x.deviceKey);
const remove = (id: number) =>
DataGroupService.delete(id).then((res) => {
const removeOnce = (id: number) =>
DataGroupService.delete(id).then((res: any) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
});
@ -219,76 +251,73 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
data.value.selected = [];
data.value.allSelected = false;
};
console.log(ids.length);
if (ids.length === 1) {
remove(ids[0])
.then(() => {
removeOnce(ids[0])
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "삭제되었습니다.",
result: 200,
});
after();
})
}),
)
.catch((err) => {
console.error(err);
store.setSnackbarMsg({
color: "warning",
text: "삭제 실패",
result: 500,
});
console.error(err);
});
})
.finally(after);
} else {
Promise.all(ids.map(remove))
.then(() => {
Promise.all(ids.map(removeOnce))
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
});
})
}),
)
.catch((err) => {
console.error(err);
store.setSnackbarMsg({
color: "warning",
text: "일부 삭제 실패",
result: 500,
});
console.error(err);
})
.finally(after);
}
};
}
const handleRemoveData = () => {
function handleRemoveData() {
if (data.value.selected.length === 0) return;
if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true;
return;
}
removeData();
};
}
const getSelectedAllData = () => {
function getSelectedAllData() {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => ({ deviceKey: item.deviceKey }))
: [];
};
}
const openDetailModal = (selectedItem: any) => {
function openDetailModal(selectedItem: DataGroupRow) {
data.value.selectedData = selectedItem;
openView.value = true;
};
}
const closeDetail = () => (openView.value = false);
const closeRunModal = () => (isRunVisible.value = false);
const openRunModal = (item: any) => {
function openRunModal(item: any) {
selectedRun.value = item;
isRunVisible.value = true;
};
}
const openModifyModal = (item: any) => {
function openModifyModal(item: DataGroupRow) {
data.value.selectedData = {
id: item.deviceKey,
workflowName: item.name,
@ -296,47 +325,42 @@ const openModifyModal = (item: any) => {
};
data.value.modalMode = "edit";
data.value.isCreateVisible = true;
};
const openCreateModal = () => {
}
function openCreateModal() {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isCreateVisible = true;
};
const openUploadModal = () => {
}
function openUploadModal() {
data.value.selectedData = null;
data.value.modalMode = "upload";
data.value.isUploadVisible = true;
};
const closeCreateModal = () => {
}
function closeCreateModal() {
data.value.isModalVisible = false;
data.value.isCreateVisible = false;
};
const closeUploadModal = () => {
}
function closeUploadModal() {
data.value.isModalVisible = false;
data.value.isUploadVisible = false;
};
}
/* -------------------------
* Watchers & Lifecycle
* ------------------------*/
watch(
() => data.value.isCreateVisible,
(now, prev) => {
if (prev && !now) fetchList();
},
);
watch(
() => data.value.isUploadVisible,
(now, prev) => {
if (prev && !now) fetchList();
},
);
onMounted(() => {
fetchList();
});
onMounted(fetchList);
</script>
<template>
@ -353,6 +377,8 @@ onMounted(() => {
</div>
</v-card-item>
</v-card>
<!-- Search -->
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
@ -365,13 +391,14 @@ onMounted(() => {
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="searchOptions"
:items="SEARCH_OPTIONS"
item-title="label"
item-value="value"
hide-details
@update:model-value="doSearch"
/>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
@ -382,7 +409,7 @@ onMounted(() => {
class="mt-3 mb-3"
hide-details
@keyup.enter="doSearch"
></v-text-field>
/>
</v-responsive>
<div class="ml-3">
@ -392,12 +419,13 @@ onMounted(() => {
:rounded="5"
@click="doSearch"
>
<v-icon> mdi-magnify</v-icon>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<!-- Toolbar -->
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
@ -414,27 +442,25 @@ onMounted(() => {
<v-select
v-model="data.params.pageSize"
density="compact"
:items="pageSizeOptions"
:items="PAGE_SIZE_OPTIONS"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageSize"
></v-select>
/>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<!-- <v-btn color="info" class="mr-4" @click="openUploadModal"
>Upload Workflow
</v-btn> -->
<v-btn color="info" @click="openCreateModal"
>Create Workflow
</v-btn>
>Create DataGroup</v-btn
>
</v-sheet>
</v-sheet>
<!-- Table -->
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
@ -447,69 +473,52 @@ onMounted(() => {
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
v-for="(h, i) in TABLE_HEADERS"
:key="i"
:style="`width:${item.width}`"
:style="`width:${h.width}`"
/>
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
v-for="(h, i) in TABLE_HEADERS"
:key="i"
class="text-center font-weight-bold"
:style="`${item.style}`"
:style="h.style"
>
{{ item.label }}
{{ h.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(item, i) in data.results"
v-for="(row, i) in data.results"
:key="i"
class="text-center"
class="text-center row-hover"
@click="onRowClick(row)"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{
deviceKey: item.deviceKey,
}"
></v-checkbox>
</td>
<td>{{ item.no }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.version }}</td>
<td>{{ item.kubeflowStatus }}</td>
<td>{{ formatDateTime(item.registDt) }}</td>
<td style="white-space: nowrap">
<IconRunBtn @on-click="openRunModal(item)" />
<IconInfoBtn @on-click="openDetailModal(item)" />
<!-- <IconModifyBtn @on-click="openModifyModal(item)" /> -->
<td>{{ row.no }}</td>
<td>{{ row.name }}</td>
<td>{{ row.description }}</td>
<td>{{ row.author || "-" }}</td>
<td>{{ formatDateTime(row.registDt) }}</td>
<td
style="white-space: nowrap"
@click.stop
@mousedown.stop
>
<IconInfoBtn @on-click="openDetailModal(row)" />
<IconModifyBtn @on-click="openModifyModal(row)" />
<IconDeleteBtn
@on-click="
removeData([{ deviceKey: item.deviceKey }])
"
@on-click="removeData([{ deviceKey: row.deviceKey }])"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
@ -518,36 +527,23 @@ onMounted(() => {
color="primary"
rounded="circle"
@update:model-value="changePageNum"
></v-pagination>
/>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<!-- Create/Edit Dialog -->
<v-dialog v-model="data.isCreateVisible" max-width="800" persistent>
<WorkflowsBaseDialog
<DatagroupBaseDoalog
:key="data.modalMode + String(data.selectedData?.deviceKey ?? '')"
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
/>
</v-dialog>
<v-dialog v-model="data.isUploadVisible" max-width="800" persistent>
<WorkflowsUploadDialog
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeUploadModal"
/>
</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 class="w-100" v-else>
@ -559,4 +555,16 @@ onMounted(() => {
</div>
</template>
<style scoped></style>
<style scoped>
/* 행 Hover 효과 */
.row-hover {
cursor: pointer;
transition: background-color 120ms ease;
}
tbody tr.row-hover:hover {
background-color: rgba(255, 255, 255, 0.06);
}
tbody tr.row-hover:active {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

File diff suppressed because it is too large Load Diff

@ -11,19 +11,11 @@ import { KubeflowRunService } from "@/components/service/management/KubeflowRunS
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
const store = commonStore();
const openView = ref(false);
const username = ref<string>("");
const openCompare = ref(false);
const execSelected = ref<any>(null);
const selectedExperiment = ref<{
name: string;
description: string;
createdDate: string;
createdID: string;
deviceKey: number;
} | null>(null);
// ===== =====
const username = ref<string>("");
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Execution Name", width: "20%", style: "word-break: keep-all;" },
@ -35,15 +27,13 @@ const tableHeader = [
{ label: "Registry Status", width: "10%", style: "word-break: keep-all;" },
{ label: "Action", width: "10%", style: "word-break: keep-all;" },
];
// ===== / (/ '') =====
type SearchType = "전체" | "제목" | "작성자";
type SearchType = "전체" | "제목" | "작성자";
const searchOptions = [
{ label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType },
{ label: "작성자", value: "작성자" as SearchType },
];
const SEARCH_TYPE_MAP: Record<SearchType | "", "ALL" | "TITLE" | "AUTHOR"> = {
"": "ALL",
전체: "ALL",
@ -57,7 +47,6 @@ const pageSizeOptions = [
{ text: "100 페이지", value: 100 },
];
// ===== =====
const data = ref({
params: {
pageNum: 1,
@ -70,15 +59,9 @@ const data = ref({
pageLength: 0,
modalMode: "" as "create" | "edit" | "",
selectedData: null as any,
allSelected: false,
selected: [] as any[],
isCreateVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [] as any[],
});
// ===== =====
function readUsernameFromStorage(): string {
try {
const raw =
@ -100,11 +83,7 @@ const getProjectId = (): number => {
const v = Number(localStorage.getItem("projectId"));
return Number.isFinite(v) ? v : 0;
};
const fmtDate = (v?: string) =>
v ? String(v).replace("T", " ").slice(0, 19) : "";
// Row
// Execution Row
const toRow = (r: any, idx: number) => {
const fmtStart = (start?: string) => {
if (!start) return "-";
@ -155,7 +134,6 @@ const toRow = (r: any, idx: number) => {
status: toUiStatus(r.state),
duration: fmtDuration(r.createdAt, r.finishedAt),
experiment: r.experimentId ?? "-",
workflow: r.pipelineId ?? r.pipelineVersionId ?? "-",
startTime: fmtStart(r.createdAt),
registryStatus: r.storageState ?? "-",
@ -164,95 +142,6 @@ const toRow = (r: any, idx: number) => {
};
};
// async function fetchList() {
// const { pageNum, pageSize, searchType, searchText } = data.value.params;
// const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
// const keyword = (searchText || "").trim();
// const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
// // (: 1000) +
// // 0-based
// const reqPage = needLocalFilter ? 0 : pageNum ;
// const reqSize = needLocalFilter ? 1000 : pageSize;
// const payload = {
// projectId: getProjectId(),
// page: reqPage, // : reqPage
// size: reqSize, // : reqSize
// keyword,
// searchType: mapped,
// sortField: "id",
// sortDirection: "DESC",
// };
// try {
// const res = await kubeflowRunService.search(payload as any);
// const result = res?.data ?? res;
// // : content | data | runs | []
// let list: any[] = Array.isArray(result)
// ? result
// : Array.isArray(result?.data)
// ? result.data
// : Array.isArray(result?.content)
// ? result.content
// : Array.isArray(result?.runs)
// ? result.runs
// : [];
// if (needLocalFilter) {
// const kw = keyword.toLowerCase();
// if (mapped === "TITLE") {
// list = list.filter((r: any) =>
// String(r?.displayName ?? r?.name ?? r?.runId ?? "")
// .toLowerCase()
// .includes(kw),
// );
// } else if (mapped === "AUTHOR") {
// list = list.filter((r: any) =>
// String(r?.regUserId ?? r?.createdBy ?? r?.serviceAccount ?? "")
// .toLowerCase()
// .includes(kw),
// );
// }
// // ( )
// const total = list.length;
// const pages = Math.max(1, Math.ceil(total / pageSize));
// const safePage = Math.min(Math.max(1, pageNum), pages);
// const start = (safePage - 1) * pageSize;
// const slice = list.slice(start, start + pageSize);
// data.value.params.pageNum = safePage;
// data.value.results = slice.map((r, i) => toRow(r, i));
// data.value.totalElements = total;
// data.value.pageLength = pages;
// return;
// }
// //
// const totalElements =
// typeof result?.totalElements === "number"
// ? result.totalElements
// : list.length;
// const totalPages =
// typeof result?.totalPages === "number"
// ? Math.max(1, result.totalPages)
// : Math.max(1, Math.ceil(totalElements / pageSize));
// data.value.results = list.map((r, i) => toRow(r, i));
// data.value.totalElements = totalElements;
// data.value.pageLength = totalPages;
// } catch (err) {
// console.error("[Executions] :", err);
// data.value.results = [];
// data.value.totalElements = 0;
// data.value.pageLength = 1;
// }
// }
async function fetchList() {
const { pageNum, pageSize, searchType, searchText } = data.value.params;
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
@ -264,7 +153,7 @@ async function fetchList() {
size: pageSize,
keyword,
searchType: mapped,
sortField: "id",
sortField: "createdAt",
sortDirection: "DESC",
};
@ -272,29 +161,44 @@ async function fetchList() {
const res = await KubeflowRunService.search(payload as any);
const result = res?.data ?? res;
// 1)
let list: any[] = [];
let totalElements: number | undefined;
let totalPages: number | undefined;
let isServerPaged = false;
if (Array.isArray(result)) {
//
list = result;
} else if (Array.isArray(result?.data)) {
// data
list = result.data;
} else if (Array.isArray(result?.content)) {
// (Page)
if (Array.isArray(result)) list = result;
else if (Array.isArray(result?.data)) list = result.data;
else if (Array.isArray(result?.content)) {
list = result.content;
totalElements = result.totalElements;
totalPages = result.totalPages;
isServerPaged = true;
} else if (Array.isArray(result?.runs)) {
list = result.runs;
} else {
list = [];
}
} else if (Array.isArray(result?.runs)) list = result.runs;
// ( )
list.sort((a, b) => {
const ta = new Date(
a.createdAt ||
a.lastUpdateTime ||
a.scheduledAt ||
a.finishedAt ||
a.startTime ||
0,
).getTime();
const tb = new Date(
b.createdAt ||
b.lastUpdateTime ||
b.scheduledAt ||
b.finishedAt ||
b.startTime ||
0,
).getTime();
if (tb !== ta) return tb - ta; //
const aid = a.id ?? a.runId ?? a.run_id ?? a.name ?? "";
const bid = b.id ?? b.runId ?? b.run_id ?? b.name ?? "";
return String(bid).localeCompare(String(aid)); //
});
if (!isServerPaged) {
const total = list.length;
@ -307,8 +211,7 @@ async function fetchList() {
data.value.totalElements = total;
data.value.pageLength = pages;
} else {
// 3)
data.value.results = (list as any[]).map((r, i) => toRow(r, i));
data.value.results = list.map((r, i) => toRow(r, i));
data.value.totalElements =
typeof totalElements === "number" ? totalElements : list.length;
data.value.pageLength =
@ -323,7 +226,8 @@ async function fetchList() {
data.value.pageLength = 1;
}
}
// ===== / =====
// /
const doSearch = () => {
data.value.params.pageNum = 1;
fetchList();
@ -338,12 +242,11 @@ const changePageSize = (size: number) => {
fetchList();
};
// / ( )
const removeData = (value?: Array<{ deviceKey: number }>) => {
const removeList = value ?? data.value.selected;
if (!removeList || removeList.length === 0) return;
//
const removeData = (value: Array<{ deviceKey: number }>) => {
const ids = (value || []).map((x) => x.deviceKey);
if (ids.length === 0) return;
const ids = removeList.map((x) => x.deviceKey);
const removeOne = (id: number) =>
ExperimentService.delete(id).then((res) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
@ -353,27 +256,20 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
if (
ids.length >= data.value.results.length &&
data.value.params.pageNum > 1
) {
)
data.value.params.pageNum -= 1;
}
fetchList();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
// /
if (ids.length === 1) {
removeOne(ids[0])
.then(() => {
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "삭제되었습니다.",
result: 200,
});
after();
})
}),
)
.catch((err) => {
console.error("삭제 실패:", err);
store.setSnackbarMsg({
@ -381,16 +277,17 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
text: "삭제 실패",
result: 500,
});
});
})
.finally(after);
} else {
Promise.all(ids.map(removeOne))
.then(() => {
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
});
})
}),
)
.catch((err) => {
console.error("일부 삭제 실패:", err);
store.setSnackbarMsg({
@ -403,29 +300,15 @@ const removeData = (value?: Array<{ deviceKey: number }>) => {
}
};
// ===== & ( ) =====
const closeDetail = () => {
openView.value = false;
selectedExperiment.value = null;
};
//
const openInfoModal = (item: any) => {
execSelected.value = item;
console.log("[Parent] 선택된 실행:", item);
openView.value = true;
openCompare.value = false;
};
function closeView() {
openView.value = false;
}
const onSaved = () => fetchList();
const openDetailModal = (selectedItem: any) => {
console.log("[Experiment/List] row clicked:", selectedItem);
if (!selectedItem?.deviceKey) {
console.warn("[Experiment/List] deviceKey 없음!", selectedItem);
}
data.value.selectedData = selectedItem;
openView.value = true;
};
const openCreateModal = () => {
data.value.modalMode = "create";
data.value.selectedData = {
@ -534,12 +417,6 @@ onMounted(() => {
</v-responsive>
</v-sheet>
</v-sheet>
<!-- <v-sheet class="justify-end mb-2">
<v-btn color="primary" @click="openCreateModal"
>Create Experiment</v-btn
>
</v-sheet> -->
</v-sheet>
<!-- 목록 -->
@ -586,10 +463,28 @@ onMounted(() => {
<v-icon v-else-if="item.status === 'Failed'" color="red"
>mdi-close-circle</v-icon
>
<v-icon v-else color="grey">mdi-loading</v-icon>
<v-progress-circular
v-else-if="item.status === 'Running'"
indeterminate
size="18"
width="2"
color="info"
/>
<v-icon
v-else-if="item.status === 'Pending'"
color="grey"
class="mdi-spin"
>
mdi-loading
</v-icon>
<!-- -->
<v-icon v-else color="grey">mdi-help-circle</v-icon>
</td>
<td>{{ item.duration }}</td>
<td>{{ item.experiment }}</td>
<td>{{ item.workflow }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.registryStatus }}</td>
@ -640,7 +535,7 @@ onMounted(() => {
<div class="w-100" v-else>
<ViewComponent
v-if="openView"
:id="execSelected.deviceKey"
:experiment-info="execSelected"
:onClose="closeView"
/>
</div>

@ -92,11 +92,6 @@ const segWidthPct = () => 100 / (nSteps - 1);
//
const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");
//
onMounted(() => {
console.log("[Child] 받은 데이터:", props.experimentInfo);
});
</script>
<template>

@ -3,7 +3,7 @@ import { ref, computed, onMounted, watch } from "vue";
import { ExperimentService } from "@/components/service/management/ExperimentService";
import { ProjectService } from "@/components/service/project/projectService";
const props = defineProps<{ id: number | string }>();
const props = defineProps<{ experimentInfo: any }>();
const emit = defineEmits<{ (e: "close"): void }>();
const loading = ref(false);
@ -19,27 +19,67 @@ const experimentInfo = ref({
mlFlowId: "-",
});
const formatIso = (s?: string) =>
s ? String(s).replace("T", " ").slice(0, 19) : "-";
function formatIso(s?: string) {
return s ? String(s).replace("T", " ").slice(0, 19) : "-";
}
const mapToViewModel = (raw: any) => ({
experimentName: raw.displayName ?? raw.name ?? "-",
projectName: "-",
createdDate: formatIso(raw.lastUpdateTime),
createdId: raw.regUserId ?? "-",
description: raw.description ?? "-",
kubeFlowId: raw.kubeFlowId ?? "-",
mlFlowId: raw.mlFlowId ?? "-",
});
function mapToViewModel(raw: any) {
const hasRaw = !!raw;
const created = hasRaw && (raw.createdAt || raw.lastUpdateTime);
const createdId =
(hasRaw && (raw.regUserId || raw.createdBy || raw.serviceAccount)) || "-";
return {
experimentName: (hasRaw && (raw.displayName || raw.name)) || "-",
projectName: "-",
createdDate: formatIso(created as string | undefined),
createdId: createdId,
description: (hasRaw && raw.description) || "-",
kubeFlowId: (hasRaw && (raw.runId || raw.run_id || raw.id)) || "-",
mlFlowId: (hasRaw && raw.mlFlowId) || "-",
};
}
const info = computed(() => mapToViewModel(detailRaw.value || {}));
function bindFromProp() {
const hasProp =
props.experimentInfo !== null && props.experimentInfo !== undefined;
if (!hasProp) return;
const raw =
hasProp && props.experimentInfo.raw
? props.experimentInfo.raw
: props.experimentInfo;
if (!raw) return;
detailRaw.value = raw;
const vm = mapToViewModel(detailRaw.value);
experimentInfo.value = { ...experimentInfo.value, ...vm };
const prjName = localStorage.getItem("projectName");
if (prjName) {
experimentInfo.value.projectName = prjName;
}
if (
detailRaw.value &&
detailRaw.value.projectId !== undefined &&
detailRaw.value.projectId !== null
) {
fetchProjectName(Number(detailRaw.value.projectId)).catch(function () {});
}
}
async function fetchProjectName(projectId?: number) {
if (!projectId && projectId !== 0) return;
if (projectId === undefined || projectId === null) return;
try {
const res = await ProjectService.fetchProjectById(projectId as number);
const prj = res?.data ?? res;
experimentInfo.value.projectName = prj?.prjNm ?? prj?.name ?? "-";
const prj = res && res.data ? res.data : res;
const name = prj && (prj.prjNm || prj.name) ? prj.prjNm || prj.name : "-";
experimentInfo.value.projectName = name;
} catch (e) {
console.warn("[Experiment/View] project fetch fail:", e);
}
@ -52,13 +92,20 @@ async function fetchDetail(id: number | string) {
loading.value = true;
try {
const res = await ExperimentService.view(idNum as number);
detailRaw.value = res?.data ?? res;
const payload = res && res.data ? res.data : res;
detailRaw.value = payload;
const vm = mapToViewModel(detailRaw.value);
experimentInfo.value = { ...experimentInfo.value, ...vm };
//
await fetchProjectName(detailRaw.value?.projectId);
const hasProjectId =
detailRaw.value &&
detailRaw.value.projectId !== undefined &&
detailRaw.value.projectId !== null;
if (hasProjectId) {
await fetchProjectName(Number(detailRaw.value.projectId));
}
} catch (e) {
console.error("[Experiment/View] fetch detail error:", e);
} finally {
@ -66,11 +113,25 @@ async function fetchDetail(id: number | string) {
}
}
onMounted(() => fetchDetail(props.id));
onMounted(function () {
const hasObj =
props.experimentInfo && typeof props.experimentInfo === "object";
if (hasObj) {
bindFromProp();
} else {
fetchDetail(props.experimentInfo as any);
}
});
watch(
() => props.id,
(nv) => {
if (nv !== undefined && nv !== null && nv !== "") fetchDetail(nv);
() => props.experimentInfo,
function (nv) {
if (nv === null || nv === undefined || nv === "") return;
if (typeof nv === "object") {
bindFromProp();
} else {
fetchDetail(nv as any);
}
},
);
</script>

@ -74,9 +74,6 @@ async function fetchDetail(id: number | string) {
//
await resolveWorkflowName(raw);
console.log("[ViewComponent] raw:", raw);
console.log("[ViewComponent] info:", info.value);
} catch (e) {
console.error("[ViewComponent] fetch detail error:", e);
} finally {

@ -2,13 +2,18 @@
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import { onMounted, ref } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { storage } from "@/utils/storage";
import ViewComponent from "@/components/templates/trainingscript/ViewComponent.vue";
import TrainingScriptBaseDoalog from "@/components/atoms/organisms/TrainingScriptBaseDoalog.vue";
import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { commonStore } from "@/stores/commonStore";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const activeRefId = ref<number | null>(null);
const activeRefName = computed(() => String(route.query.refName ?? ""));
const store = commonStore();
const openView = ref(false);
const openModify = ref(false);
@ -68,6 +73,12 @@ const data = ref({
userOption: [] as any[],
});
function getRefIdFromRoute(q: any): number | null {
const raw = q?.refId;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? n : null;
}
//
function readUsernameFromStorage(): string {
try {
@ -139,6 +150,7 @@ const fetchList = async () => {
sortField: "id",
sortDirection: "DESC",
refType: "TRAINING_SCRIPT",
refId: activeRefId.value,
};
try {
@ -286,6 +298,7 @@ const openCreateModal = () => {
data.value.selectedData = {
username: username.value,
projectId: getProjectId(),
refId: activeRefId.value,
};
data.value.isCreateVisible = true;
};
@ -318,8 +331,17 @@ const getSelectedAllData = () => {
};
onMounted(() => {
username.value = readUsernameFromStorage();
activeRefId.value = getRefIdFromRoute(route.query);
fetchList();
});
watch(
() => route.query.refId,
() => {
activeRefId.value = getRefIdFromRoute(route.query);
data.value.params.pageNum = 1;
fetchList();
},
);
</script>
<template>
@ -413,6 +435,26 @@ onMounted(() => {
/>
</v-responsive>
</v-sheet>
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip
v-if="activeRefId"
class="ml-2"
color="secondary"
variant="tonal"
>
Filter: DataGroup {{ activeRefName }}
</v-chip>
<v-btn
v-if="activeRefId"
size="small"
variant="text"
@click="router.replace({ path: '/TrainingScriptGroup' })"
>
필터 해제
</v-btn>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">

@ -0,0 +1,570 @@
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import tz from "dayjs/plugin/timezone";
import { useRouter } from "vue-router";
import { commonStore } from "@/stores/commonStore";
import { DataGroupService } from "@/components/service/management/DataGroupService";
import ViewComponent from "@/components/templates/workflow/ViewComponent.vue";
import IconInfoBtn from "@/components/atoms/button/IconInfoBtn.vue";
import IconDeleteBtn from "@/components/atoms/button/IconDeleteBtn.vue";
import DatagroupBaseDoalog from "@/components/atoms/organisms/DatagroupBaseDoalog.vue";
import IconModifyBtn from "@/components/atoms/button/IconModifyBtn.vue";
/* -------------------------
* Dayjs & Router
* ------------------------*/
dayjs.extend(utc);
dayjs.extend(tz);
const KST = "Asia/Seoul";
const router = useRouter();
/* -------------------------
* Types
* ------------------------*/
type SearchType = "전체" | "제목" | "작성자";
type ApiSearchType = "ALL" | "TITLE" | "AUTHOR";
interface DataGroupRow {
no: number;
name: string;
description: string;
author: string;
registDt: string | number | Date;
deviceKey: number;
}
/* -------------------------
* Constants
* ------------------------*/
const TABLE_HEADERS = [
{ label: "No", width: "6%", style: "word-break: keep-all;" },
{ label: "DataGroup Name", width: "24%", style: "word-break: keep-all;" },
{ label: "Description", width: "32%", style: "word-break: keep-all;" },
{ label: "Author", width: "14%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "16%", style: "word-break: keep-all;" },
{ label: "Action", width: "8%", style: "word-break: keep-all;" },
] as const;
const SEARCH_OPTIONS = [
{ label: "전체", value: "전체" as SearchType },
{ label: "제목", value: "제목" as SearchType },
{ label: "작성자", value: "작성자" as SearchType },
];
const PAGE_SIZE_OPTIONS = [
{ text: "10 페이지", value: 10 },
{ text: "50 페이지", value: 50 },
{ text: "100 페이지", value: 100 },
];
const SEARCH_TYPE_MAP: Record<SearchType | "", ApiSearchType> = {
"": "ALL",
전체: "ALL",
제목: "TITLE",
작성자: "AUTHOR",
};
/* -------------------------
* Store & Local State
* ------------------------*/
const store = commonStore();
const openView = ref(false);
const isRunVisible = ref(false); // ( )
const selectedRun = ref<any | null>(null);
const data = ref({
params: {
pageNum: 1,
pageSize: 10,
searchType: "전체" as SearchType,
searchText: "",
},
results: [] as DataGroupRow[],
totalElements: 0,
pageLength: 0,
modalMode: "" as "create" | "edit" | "upload" | "",
selectedData: null as any,
allSelected: false,
selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false,
isUploadVisible: false,
isModalVisible: false,
isConfirmDialogVisible: false,
userOption: [] as any[],
});
/* -------------------------
* Utils
* ------------------------*/
const formatDateTime = (
v?: string | number | Date,
fmt = "YYYY-MM-DD HH:mm:ss",
) => (v ? dayjs(v).tz(KST).format(fmt) : "");
const toRow = (g: any, no: number): DataGroupRow => ({
no,
name: g.dsNm,
description: g.dsDesc,
author: g.regUserNm,
registDt: g.regDate,
deviceKey: g.id,
});
function onRowClick(row: DataGroupRow) {
const id = Number(row?.deviceKey);
if (!Number.isFinite(id)) return;
router.push({
path: "/training-script",
query: { refId: String(id), refName: row.name },
});
}
/* -------------------------
* Data Loaders
* ------------------------*/
async function fetchList() {
const projectId = Number(localStorage.getItem("projectId"));
if (!projectId) {
console.warn("[TrainingscriptGroup] projectId 없음 — 프로젝트 먼저 선택");
data.value.results = [];
data.value.totalElements = 0;
data.value.pageLength = 0;
return;
}
const { pageNum, pageSize, searchType, searchText } = data.value.params;
const mapped = SEARCH_TYPE_MAP[searchType] || "ALL";
const keyword = (searchText || "").trim();
const needLocalFilter = mapped !== "ALL" && keyword.length > 0;
// ( )
const reqPage = needLocalFilter ? 0 : pageNum - 1; // 0-based
const reqSize = needLocalFilter ? 1000 : pageSize;
try {
const payload = {
projectId,
page: reqPage,
size: reqSize,
keyword,
searchType: mapped,
};
const res: any = await DataGroupService.search(payload);
if (res?.status !== 200) return;
const result = res.data;
let list: any[] = result?.content ?? [];
//
if (needLocalFilter) {
const kw = keyword.toLowerCase();
if (mapped === "TITLE") {
list = list.filter((w: any) =>
String(w?.name ?? "")
.toLowerCase()
.includes(kw),
);
} else if (mapped === "AUTHOR") {
list = list.filter((w: any) =>
String(w?.regUserNm ?? "")
.toLowerCase()
.includes(kw),
);
}
const uiSize = pageSize;
const totalElements = list.length;
const totalPages = Math.max(1, Math.ceil(totalElements / uiSize));
const safePage = Math.min(Math.max(1, pageNum), totalPages);
const start = (safePage - 1) * uiSize;
const pageSlice = list.slice(start, start + uiSize);
const firstNo = totalElements - start;
data.value.results = pageSlice.map((w: any, i: number) =>
toRow(w, firstNo - i),
);
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
return;
}
//
const totalElements = result.totalElements ?? list.length;
const totalPages = result.totalPages ?? 1;
const serverPage = result.pageable?.pageNumber ?? 0;
const serverSize = result.pageable?.pageSize ?? pageSize;
const offset =
typeof result.pageable?.offset === "number"
? result.pageable.offset
: serverPage * serverSize;
const firstNo = totalElements - offset;
data.value.results = list.map((w: any, i: number) => toRow(w, firstNo - i));
data.value.totalElements = totalElements;
data.value.pageLength = totalPages;
} catch (err) {
console.error("[TrainingscriptGroup] 조회 에러:", err);
}
}
/* -------------------------
* Actions
* ------------------------*/
function doSearch() {
data.value.params.pageNum = 1;
fetchList();
}
function changePageNum(page: number) {
data.value.params.pageNum = page;
fetchList();
}
function changePageSize(size: number) {
data.value.params.pageSize = size;
data.value.params.pageNum = 1;
fetchList();
}
function removeData(value?: Array<{ deviceKey: number }>) {
const removeList = value ?? data.value.selected;
if (!removeList?.length) return;
const ids = removeList.map((x) => x.deviceKey);
const removeOnce = (id: number) =>
DataGroupService.delete(id).then((res: any) => {
if (res.status < 200 || res.status >= 300) return Promise.reject(res);
});
const after = () => {
if (
ids.length >= data.value.results.length &&
data.value.params.pageNum > 1
) {
data.value.params.pageNum -= 1;
}
fetchList();
data.value.isConfirmDialogVisible = false;
data.value.selected = [];
data.value.allSelected = false;
};
if (ids.length === 1) {
removeOnce(ids[0])
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "삭제되었습니다.",
result: 200,
}),
)
.catch((err) => {
console.error(err);
store.setSnackbarMsg({
color: "warning",
text: "삭제 실패",
result: 500,
});
})
.finally(after);
} else {
Promise.all(ids.map(removeOnce))
.then(() =>
store.setSnackbarMsg({
color: "success",
text: "모두 삭제되었습니다.",
result: 200,
}),
)
.catch((err) => {
console.error(err);
store.setSnackbarMsg({
color: "warning",
text: "일부 삭제 실패",
result: 500,
});
})
.finally(after);
}
}
function handleRemoveData() {
if (data.value.selected.length === 0) return;
if (data.value.allSelected || data.value.selected.length !== 1) {
data.value.isConfirmDialogVisible = true;
return;
}
removeData();
}
function getSelectedAllData() {
data.value.selected = data.value.allSelected
? data.value.results.map((item) => ({ deviceKey: item.deviceKey }))
: [];
}
function openDetailModal(selectedItem: DataGroupRow) {
data.value.selectedData = selectedItem;
openView.value = true;
}
const closeDetail = () => (openView.value = false);
function openRunModal(item: any) {
selectedRun.value = item;
isRunVisible.value = true;
}
function openModifyModal(item: DataGroupRow) {
data.value.selectedData = {
id: item.deviceKey,
workflowName: item.name,
workflowDescription: item.description,
};
data.value.modalMode = "edit";
data.value.isCreateVisible = true;
}
function openCreateModal() {
data.value.selectedData = null;
data.value.modalMode = "create";
data.value.isCreateVisible = true;
}
function openUploadModal() {
data.value.selectedData = null;
data.value.modalMode = "upload";
data.value.isUploadVisible = true;
}
function closeCreateModal() {
data.value.isModalVisible = false;
data.value.isCreateVisible = false;
}
function closeUploadModal() {
data.value.isModalVisible = false;
data.value.isUploadVisible = false;
}
/* -------------------------
* Watchers & Lifecycle
* ------------------------*/
watch(
() => data.value.isCreateVisible,
(now, prev) => {
if (prev && !now) fetchList();
},
);
watch(
() => data.value.isUploadVisible,
(now, prev) => {
if (prev && !now) fetchList();
},
);
onMounted(fetchList);
</script>
<template>
<div class="w-100" v-if="!openView">
<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 align-center 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">TrainingScriptGroup</div>
</div>
</v-card-item>
</v-card>
<!-- Search -->
<v-card flat class="bg-shades-transparent w-100">
<v-card flat class="bg-shades-transparent mb-4">
<div class="d-flex justify-center flex-wrap align-center">
<v-responsive
max-width="180"
min-width="180"
class="mr-3 mt-3 mb-3"
>
<v-select
v-model="data.params.searchType"
label="검색조건"
density="compact"
:items="SEARCH_OPTIONS"
item-title="label"
item-value="value"
hide-details
@update:model-value="doSearch"
/>
</v-responsive>
<v-responsive min-width="540" max-width="540">
<v-text-field
v-model="data.params.searchText"
label="검색어"
density="compact"
clearable
required
class="mt-3 mb-3"
hide-details
@keyup.enter="doSearch"
/>
</v-responsive>
<div class="ml-3">
<v-btn
size="large"
color="primary"
:rounded="5"
@click="doSearch"
>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</div>
</div>
</v-card>
<!-- Toolbar -->
<v-sheet
class="bg-shades-transparent d-flex flex-wrap align-center mb-2"
>
<v-sheet class="d-flex flex-wrap me-auto bg-shades-transparent">
<v-sheet
class="d-flex align-center mr-3 mb-2 bg-shades-transparent"
>
<v-chip color="primary"
> {{ data.totalElements.toLocaleString() }}</v-chip
>
</v-sheet>
<v-sheet class="bg-shades-transparent">
<v-responsive max-width="140" min-width="140" class="mb-2">
<v-select
v-model="data.params.pageSize"
density="compact"
:items="PAGE_SIZE_OPTIONS"
item-title="text"
item-value="value"
variant="outlined"
color="primary"
hide-details
@update:model-value="changePageSize"
/>
</v-responsive>
</v-sheet>
</v-sheet>
<v-sheet class="justify-end mb-2">
<v-btn color="info" @click="openCreateModal"
>Create DataGroup</v-btn
>
</v-sheet>
</v-sheet>
<!-- Table -->
<v-card class="rounded-lg pa-8">
<v-col cols="12">
<v-sheet>
<v-table
density="comfortable"
fixed-header
height="625"
col-md-12
col-12
overflow-x-auto
>
<colgroup>
<col
v-for="(h, i) in TABLE_HEADERS"
:key="i"
:style="`width:${h.width}`"
/>
</colgroup>
<thead>
<tr>
<th
v-for="(h, i) in TABLE_HEADERS"
:key="i"
class="text-center font-weight-bold"
:style="h.style"
>
{{ h.label }}
</th>
</tr>
</thead>
<tbody class="text-body-2">
<tr
v-for="(row, i) in data.results"
:key="i"
class="text-center row-hover"
@click="onRowClick(row)"
>
<td>{{ row.no }}</td>
<td>{{ row.name }}</td>
<td>{{ row.description }}</td>
<td>{{ row.author || "-" }}</td>
<td>{{ formatDateTime(row.registDt) }}</td>
<td
style="white-space: nowrap"
@click.stop
@mousedown.stop
>
<IconInfoBtn @on-click="openDetailModal(row)" />
<IconModifyBtn @on-click="openModifyModal(row)" />
<IconDeleteBtn
@on-click="removeData([{ deviceKey: row.deviceKey }])"
/>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
<v-card-actions class="text-center mt-8 justify-center">
<v-pagination
v-model="data.params.pageNum"
:length="data.pageLength"
:total-visible="10"
color="primary"
rounded="circle"
@update:model-value="changePageNum"
/>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</v-card>
</v-container>
<!-- Create/Edit Dialog -->
<v-dialog v-model="data.isCreateVisible" max-width="800" persistent>
<DatagroupBaseDoalog
:key="data.modalMode + String(data.selectedData?.deviceKey ?? '')"
:edit-data="data.selectedData"
:mode="data.modalMode"
@close-modal="closeCreateModal"
/>
</v-dialog>
</div>
<div class="w-100" v-else>
<ViewComponent
v-if="data.selectedData"
:id="data.selectedData.deviceKey"
@close="closeDetail"
/>
</div>
</template>
<style scoped>
/* 행 Hover 효과 */
.row-hover {
cursor: pointer;
transition: background-color 120ms ease;
}
tbody tr.row-hover:hover {
background-color: rgba(255, 255, 255, 0.06);
}
tbody tr.row-hover:active {
background-color: rgba(255, 255, 255, 0.1);
}
</style>

@ -25,13 +25,13 @@ const isRunVisible = ref(false);
const selectedRun = ref<any | null>(null);
const tableHeader = [
{ label: "No", width: "5%", style: "word-break: keep-all;" },
{ label: "Workflow Name", width: "18%", style: "word-break: keep-all;" },
{ label: "Description", width: "28%", style: "word-break: keep-all;" },
{ label: "Version", width: "10%", style: "word-break: keep-all;" },
{ label: "Kubeflow Status", width: "12%", style: "word-break: keep-all;" },
{ label: "Created DateTime", width: "15%", style: "word-break: keep-all;" },
{ label: "Action", width: "12%", style: "word-break: keep-all;" },
{ label: "No", width: "6%", style: "white-space: nowrap;" },
{ label: "Workflow Name", width: "22%", style: "white-space: nowrap;" },
{ label: "Description", width: "30%", style: "white-space: nowrap;" },
{ label: "Version", width: "10%", style: "white-space: nowrap;" },
{ label: "Kubeflow Status", width: "12%", style: "white-space: nowrap;" },
{ label: "Created DateTime", width: "12%", style: "white-space: nowrap;" },
{ label: "Action", width: "8%", style: "white-space: nowrap;" },
];
const searchOptions = [
{ label: "전체", value: "전체" as SearchType },
@ -125,6 +125,7 @@ const fetchList = () => {
if (res.status !== 200) return;
const result = res.data;
console.log("Workflows", result);
let list = result?.content ?? [];
if (needLocalFilter) {
@ -447,7 +448,6 @@ onMounted(() => {
overflow-x-auto
>
<colgroup>
<col style="width: 5%" />
<col
v-for="(item, i) in tableHeader"
:key="i"
@ -456,15 +456,6 @@ onMounted(() => {
</colgroup>
<thead>
<tr>
<th>
<v-checkbox
v-model="data.allSelected"
style="min-width: 36px"
:indeterminate="data.allSelected === true"
hide-details
@change="getSelectedAllData"
></v-checkbox>
</th>
<th
v-for="(item, i) in tableHeader"
:key="i"
@ -481,15 +472,6 @@ onMounted(() => {
:key="i"
class="text-center"
>
<td>
<v-checkbox
v-model="data.selected"
hide-details
:value="{
deviceKey: item.deviceKey,
}"
></v-checkbox>
</td>
<td>{{ item.no }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>

@ -47,7 +47,6 @@ const signUp = () => {
role: data.value.role ? [data.value.role] : [],
password: data.value.password,
};
console.log("회원가입 호출 payload:", payload);
UserManagerService.signUp(payload)
.then((res) => {

@ -0,0 +1,9 @@
<script setup>
import ListComponent from "@/components/templates/trainingscriptgroup/ListComponent.vue";
</script>
<template>
<ListComponent />
</template>
<style scoped lang="sass"></style>

@ -44,7 +44,13 @@ const routes = [
name: "dataGroup",
path: "/datagroup",
meta: { title: "DataGroup", requiresAuth: false },
component: () => import("@/pages/Datagroup.vue"),
component: () => import("@/pages/DatagroupView.vue"),
},
{
name: "TrainingScriptGroup",
path: "/trainingscriptgroup",
meta: { title: "TrainingScriptGroup", requiresAuth: false },
component: () => import("@/pages/TrainingscriptgroupView.vue"),
},
{
name: "run",

@ -12,12 +12,7 @@ export const menuUtils = {
value: "workflows",
icon: "mdi-code-braces",
},
{
title: "DataGroup",
path: "/DataGroup",
value: "DataGroup",
icon: "mdi-hammer-wrench",
},
{
title: "Run",
path: "/run",
@ -34,19 +29,31 @@ export const menuUtils = {
value: "deployment",
icon: "mdi-folder-search",
},
],
adminMenuItem: [
{
title: "Training Script",
path: "/training-script",
value: "training-script",
title: "TrainingScriptGroup",
path: "/TrainingScriptGroup",
value: "TrainingScriptGroup",
icon: "mdi-file-code-outline",
},
{
title: "Datasets",
path: "/datasets",
value: "datasets",
title: "DataSetGroup",
path: "/DataGroup",
value: "DataGroup",
icon: "mdi-database-outline",
},
],
adminMenuItem: [
// {
// title: "Training Script",
// path: "/training-script",
// value: "training-script",
// icon: "mdi-file-code-outline",
// },
// {
// title: "Datasets",
// path: "/datasets",
// value: "datasets",
// icon: "mdi-database-outline",
// },
],
};

3
typed-router.d.ts vendored

@ -19,7 +19,7 @@ declare module 'vue-router/auto-routes' {
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/Datagroup': RouteRecordInfo<'/Datagroup', '/Datagroup', Record<never, never>, Record<never, never>>,
'/DatagroupView': RouteRecordInfo<'/DatagroupView', '/DatagroupView', Record<never, never>, Record<never, never>>,
'/DatasetView': RouteRecordInfo<'/DatasetView', '/DatasetView', Record<never, never>, Record<never, never>>,
'/DeploymentView': RouteRecordInfo<'/DeploymentView', '/DeploymentView', Record<never, never>, Record<never, never>>,
'/ExecutionsView': RouteRecordInfo<'/ExecutionsView', '/ExecutionsView', Record<never, never>, Record<never, never>>,
@ -29,6 +29,7 @@ declare module 'vue-router/auto-routes' {
'/MainView': RouteRecordInfo<'/MainView', '/MainView', Record<never, never>, Record<never, never>>,
'/ProjectView': RouteRecordInfo<'/ProjectView', '/ProjectView', Record<never, never>, Record<never, never>>,
'/SignupView': RouteRecordInfo<'/SignupView', '/SignupView', Record<never, never>, Record<never, never>>,
'/TrainingscriptgroupView': RouteRecordInfo<'/TrainingscriptgroupView', '/TrainingscriptgroupView', Record<never, never>, Record<never, never>>,
'/TrainingScriptView': RouteRecordInfo<'/TrainingScriptView', '/TrainingScriptView', Record<never, never>, Record<never, never>>,
'/UsersView': RouteRecordInfo<'/UsersView', '/UsersView', Record<never, never>, Record<never, never>>,
'/WorkflowStepConfigView': RouteRecordInfo<'/WorkflowStepConfigView', '/WorkflowStepConfigView', Record<never, never>, Record<never, never>>,

Loading…
Cancel
Save