fix: 홈 화면 스타일 수정 및 Deployment 데이터 바인딩

main
jschoi 8 months ago
parent 465b56fad8
commit adac240170

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { ExternalAuthControllerService } from "@/components/service/management/ExternalAuthControllerService";
import { EdgePkgInfoVOModel } from "@/components/models/management/ExternalAuthController";
type PackageOption = { label: string; value: string; raw: any }; type PackageOption = { label: string; value: string; raw: any };
@ -8,6 +10,7 @@ const props = defineProps<{
packagesLoading?: boolean; packagesLoading?: boolean;
packagesError?: string; packagesError?: string;
artifactPath?: string; artifactPath?: string;
token: string; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -18,7 +21,7 @@ const emit = defineEmits<{
const form = ref({ const form = ref({
package_id: "", package_id: "",
sw_id: "", sw_id: "",
sw_version: "", sw_version: "1",
software_name: "", software_name: "",
executed: true, executed: true,
file_type: "bundle" as "bundle" | "single", file_type: "bundle" as "bundle" | "single",
@ -33,6 +36,25 @@ const form = ref({
private_only: false, private_only: false,
}); });
const saving = ref(false);
const errorMsg = ref("");
const successOpen = ref(false);
const successDialog = ref(false);
const pendingPayload = ref<any>(null);
// ---- ----
const file = ref<File | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
const onFileChange = (e: Event) => {
const input = e.target as HTMLInputElement;
const f = input.files?.[0] ?? null;
file.value = f;
};
const clearFile = () => {
if (fileInput.value) fileInput.value.value = "";
file.value = null;
};
// ------------------------
const isWin = computed(() => form.value.os === "Windows"); const isWin = computed(() => form.value.os === "Windows");
const isLinux = computed(() => form.value.os === "Linux"); const isLinux = computed(() => form.value.os === "Linux");
@ -48,6 +70,13 @@ const readonlyFields = computed(() => ({
packageId: selectedRaw.value?.package_id ?? "", packageId: selectedRaw.value?.package_id ?? "",
})); }));
function confirmSuccess() {
if (pendingPayload.value) {
emit("handle-data", pendingPayload.value); //
}
emit("close-modal"); //
successDialog.value = false;
}
function onPickPackage() { function onPickPackage() {
form.value.win_exe_name = selectedRaw.value?.window_exe_name ?? ""; form.value.win_exe_name = selectedRaw.value?.window_exe_name ?? "";
form.value.win_root_path = selectedRaw.value?.window_root_location ?? ""; form.value.win_root_path = selectedRaw.value?.window_root_location ?? "";
@ -74,13 +103,87 @@ watch(
}, },
); );
function submit() { const installOsCode = computed(() => {
if (!form.value.package_id) return; if (form.value.os === "Windows") return 0;
emit("handle-data", { if (form.value.os === "Linux") return 1;
return null;
});
const archiveTypeCode = computed(() =>
form.value.file_type === "bundle" ? 1 : 0,
);
function getCurrentUserId(): string {
try {
const raw = localStorage.getItem("autoflow-auth");
const obj = raw ? JSON.parse(raw) : null;
return obj?.userInfo?.username ? String(obj.userInfo.username) : "";
} catch {
return "";
}
}
const toInt = (v: unknown, fallback = 1) => {
const n = parseInt(String(v ?? "").trim(), 10);
return Number.isFinite(n) ? n : fallback;
};
async function submit() {
errorMsg.value = "";
if (!file.value) return (errorMsg.value = "업로드할 파일을 선택하세요.");
if (!form.value.sw_id?.trim())
return (errorMsg.value = "SW ID를 입력하세요.");
if (!form.value.software_name?.trim())
return (errorMsg.value = "SW 명칭을 입력하세요.");
if (!selectedRaw.value?.ed_pkg_serial)
return (errorMsg.value = "SW 패키지를 선택하세요.");
if (!form.value.install_location?.trim())
return (errorMsg.value = "설치 위치를 입력하세요.");
if (!props.token)
return (errorMsg.value = "토큰이 없습니다. 다시 로그인하세요.");
try {
saving.value = true;
const params: EdgePkgInfoVOModel = {
sw_id: (form.value.sw_id || "").trim(),
sw_version: toInt(form.value.sw_version, 1), //
sw_name: (form.value.software_name || "").trim(),
auth_id: props.token,
edPkgSerial: toInt(selectedRaw.value?.ed_pkg_serial, 0),
archiveType: form.value.file_type === "bundle" ? 1 : 0,
execYn: form.value.executed ? 1 : 0,
secretAt: !!form.value.private_only,
downloadLocation: (form.value.install_location || "").trim(),
user_id: getCurrentUserId() || "admin",
creation_datetime: new Date().toISOString(),
};
const res = await ExternalAuthControllerService.add(params, file.value!);
// success ( )
const ok =
res?.success === true || (res?.data && res?.data?.success === true);
if (!ok) {
const msg =
res?.errorMessage || res?.data?.errorMessage || "등록에 실패했습니다.";
throw new Error(msg);
}
successOpen.value = true; //
pendingPayload.value = {
...form.value, ...form.value,
resolved: readonlyFields.value,
artifact_path: props.artifactPath ?? "", artifact_path: props.artifactPath ?? "",
}); };
successDialog.value = true;
clearFile();
} catch (e: any) {
errorMsg.value =
e?.response?.data?.message ||
e?.message ||
"등록 중 오류가 발생했습니다.";
} finally {
saving.value = false;
}
} }
function onEsc(e: KeyboardEvent) { function onEsc(e: KeyboardEvent) {
@ -96,7 +199,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
class="text-white text-h6 font-weight-bold" class="text-white text-h6 font-weight-bold"
style="background: #1976d2" style="background: #1976d2"
> >
차량 지능 SW 등록 Vehicle Intelligence Software Registration
</v-card-title> </v-card-title>
<v-defaults-provider <v-defaults-provider
@ -180,7 +283,12 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
<v-text-field label="SW ID" v-model="form.sw_id" /> <v-text-field label="SW ID" v-model="form.sw_id" />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field label="SW 버전" v-model="form.sw_version" /> <v-text-field
label="SW 버전"
v-model="form.sw_version"
type="number"
min="0"
/>
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-text-field
@ -276,6 +384,36 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</v-col> </v-col>
</v-row> </v-row>
<!-- 파일 업로드 (추가) -->
<v-row dense class="mb-1">
<v-col cols="12">
<div class="text-body-2 font-weight-medium mb-1">업로드 파일</div>
<div class="d-flex align-center ga-3">
<v-btn size="small" color="primary" @click="fileInput?.click()"
>파일 선택</v-btn
>
<span v-if="file" class="text-body-2">
{{ file.name }} ({{ file.size.toLocaleString() }} bytes)
</span>
<v-btn
v-if="file"
size="x-small"
variant="text"
class="ml-1"
@click="clearFile"
>
지우기
</v-btn>
</div>
<input
ref="fileInput"
type="file"
class="d-none"
@change="onFileChange"
/>
</v-col>
</v-row>
<!-- 설치 위치 & 등록인만 접근 --> <!-- 설치 위치 & 등록인만 접근 -->
<v-row dense> <v-row dense>
<v-col cols="12" md="8"> <v-col cols="12" md="8">
@ -289,13 +427,40 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
/> />
</v-col> </v-col>
</v-row> </v-row>
<!-- 에러 메시지 -->
<v-alert
v-if="errorMsg"
type="error"
variant="tonal"
class="mt-3"
density="comfortable"
>
{{ errorMsg }}
</v-alert>
</v-card-text> </v-card-text>
<v-card-actions class="py-2 px-3"> <v-card-actions class="py-2 px-3">
<v-spacer /> <v-spacer />
<v-btn color="success" @click="submit">SAVE</v-btn> <v-btn color="success" :loading="saving" @click="submit">SAVE</v-btn>
<v-btn variant="text" @click="$emit('close-modal')">CANCEL</v-btn> <v-btn variant="text" :disabled="saving" @click="$emit('close-modal')"
>CANCEL</v-btn
>
</v-card-actions> </v-card-actions>
</v-defaults-provider> </v-defaults-provider>
<v-dialog v-model="successDialog" width="360" persistent>
<v-card rounded="lg">
<v-card-title class="text-h6" style="background: #1976d2"
>등록 완료</v-card-title
>
<v-card-text class="font-weight-bold text-center"
>등록이 완료되었습니다.</v-card-text
>
<v-card-actions>
<v-spacer />
<v-btn color="primary" @click="confirmSuccess"></v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card> </v-card>
</template> </template>

@ -9,11 +9,12 @@ const props = defineProps<{
modelValue: boolean; modelValue: boolean;
projectId: number; projectId: number;
refId?: number | null; refId?: number | null;
regUserId?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", v: boolean): void; (e: "update:modelValue", v: boolean): void;
(e: "close"): void; (e: "close"): void;
(e: "saved"): void; // (e: "saved"): void;
}>(); }>();
const visible = ref(props.modelValue); const visible = ref(props.modelValue);
@ -24,11 +25,19 @@ watch(
watch(visible, (v) => emit("update:modelValue", v)); watch(visible, (v) => emit("update:modelValue", v));
const loading = ref(false); const loading = ref(false);
const savingKey = ref<string | null>(null);
const errorMsg = ref(""); const errorMsg = ref("");
const rows = ref<ExternalDatasetItem[]>([]); const rows = ref<ExternalDatasetItem[]>([]);
const searchKeyword = ref(""); const searchKeyword = ref("");
const groupName = ref(""); const groupName = ref("");
//
const successVisible = ref(false);
const successMsg = ref("업로드되었습니다.");
// Description : { [datasetName]: string }
const descMap = ref<Record<string, string>>({});
function unwrap<T = any>(res: any): T { function unwrap<T = any>(res: any): T {
return res && res.data ? res.data : res; return res && res.data ? res.data : res;
} }
@ -40,7 +49,7 @@ async function fetchList() {
try { try {
const res = await DatasetService.listExternal({ const res = await DatasetService.listExternal({
ds_prj_idx: props.projectId, ds_prj_idx: props.projectId,
search_keyword: searchKeyword.value || undefined, // search_keyword: searchKeyword.value || undefined,
grp_name: groupName.value || undefined, grp_name: groupName.value || undefined,
}); });
const body = unwrap<any>(res); const body = unwrap<any>(res);
@ -50,7 +59,6 @@ async function fetchList() {
? body ? body
: []; : [];
// ds_dataset_name
const kw = (searchKeyword.value || "").trim().toLowerCase(); const kw = (searchKeyword.value || "").trim().toLowerCase();
rows.value = rows.value =
kw.length === 0 kw.length === 0
@ -60,7 +68,7 @@ async function fetchList() {
.toLowerCase() .toLowerCase()
.includes(kw), .includes(kw),
); );
} catch (e) { } catch {
errorMsg.value = "외부 데이터셋 조회에 실패했습니다."; errorMsg.value = "외부 데이터셋 조회에 실패했습니다.";
} finally { } finally {
loading.value = false; loading.value = false;
@ -73,26 +81,40 @@ function close() {
} }
async function pick(item: ExternalDatasetItem) { async function pick(item: ExternalDatasetItem) {
savingKey.value = item.ds_dataset_name;
errorMsg.value = "";
try { try {
// DatasetService.saveExternal(...) emit("saved") await DatasetService.saveExternal({
// : datasetName: item.ds_dataset_name,
// await DatasetService.saveExternal({ path: "", // UI Base Path ,
// datasetName: item.ds_dataset_name, refId: props.refId ?? 0,
// path: item.ds_dataset_base_path, refType: "DATASET",
// refId: props.refId ?? 0, title: item.ds_dataset_name,
// refType: "DATASET", description: (descMap.value[item.ds_dataset_name] || "").trim(), //
// title: item.ds_dataset_name, version: 1,
// version: 1, regUserId: props.regUserId || "system",
// description: "", projectId: props.projectId,
// regUserId: "currentUser", });
// projectId: props.projectId,
// }); // &
emit("saved"); successMsg.value = "업로드되었습니다.";
successVisible.value = true;
descMap.value[item.ds_dataset_name] = "";
} catch (e: any) {
errorMsg.value =
e?.response?.data?.message ||
"데이터셋 저장(다운로드→S3 업로드) 중 오류가 발생했습니다.";
} finally { } finally {
close(); savingKey.value = null;
} }
} }
function confirmSuccess() {
successVisible.value = false;
emit("saved"); //
close();
}
watch(() => props.projectId, fetchList); watch(() => props.projectId, fetchList);
onMounted(fetchList); onMounted(fetchList);
</script> </script>
@ -107,11 +129,10 @@ onMounted(fetchList);
</v-card-title> </v-card-title>
<v-card-text class="pa-4"> <v-card-text class="pa-4">
<!-- 🔧 검색바 정렬 -->
<div class="d-flex align-center ga-2 mb-3" style="flex-wrap: wrap"> <div class="d-flex align-center ga-2 mb-3" style="flex-wrap: wrap">
<v-text-field <v-text-field
v-model="searchKeyword" v-model="searchKeyword"
label="데이터셋 이름 (ds_dataset_name)" label="데이터셋 이름 (Dataset Name)"
density="compact" density="compact"
hide-details hide-details
style="min-width: 240px; flex: 1 1 240px" style="min-width: 240px; flex: 1 1 240px"
@ -148,7 +169,8 @@ onMounted(fetchList);
<th class="text-left">Dataset Name</th> <th class="text-left">Dataset Name</th>
<th class="text-left">Images</th> <th class="text-left">Images</th>
<th class="text-left">Label Type</th> <th class="text-left">Label Type</th>
<th class="text-left">Base Path</th> <!-- Base Path 대신 Description -->
<th class="text-left" style="min-width: 260px">Description</th>
<th class="text-left">Action</th> <th class="text-left">Action</th>
</tr> </tr>
</thead> </thead>
@ -157,11 +179,28 @@ onMounted(fetchList);
<td>{{ r.ds_dataset_name }}</td> <td>{{ r.ds_dataset_name }}</td>
<td>{{ r.ds_dataset_image_count }}</td> <td>{{ r.ds_dataset_image_count }}</td>
<td>{{ r.labelling_tool_kr }}</td> <td>{{ r.labelling_tool_kr }}</td>
<td>{{ r.ds_dataset_base_path }}</td>
<!-- 행별 Description 입력 -->
<td>
<v-text-field
v-model="descMap[r.ds_dataset_name]"
placeholder="설명을 입력하세요"
density="compact"
hide-details
clearable
/>
</td>
<td> <td>
<v-btn size="small" color="primary" @click="pick(r)" <v-btn
>Select</v-btn size="small"
color="primary"
:loading="savingKey === r.ds_dataset_name"
:disabled="savingKey !== null"
@click="pick(r)"
> >
Select
</v-btn>
</td> </td>
</tr> </tr>
<tr v-if="!loading && rows.length === 0"> <tr v-if="!loading && rows.length === 0">
@ -177,4 +216,19 @@ onMounted(fetchList);
<v-btn variant="text" @click="close">Close</v-btn> <v-btn variant="text" @click="close">Close</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
<!-- 성공 다이얼로그 -->
<v-dialog v-model="successVisible" max-width="420">
<v-card>
<v-card-title
class="text-h6 font-weight-bold text-center"
style="background: #1976d2"
>알림</v-card-title
>
<v-card-text class="pt-2 mt-4 text-center">{{ successMsg }}</v-card-text>
<v-card-actions class="justify-end">
<v-btn color="primary" @click="confirmSuccess"></v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template> </template>

@ -0,0 +1,13 @@
export type EdgePkgInfoVOModel = {
sw_id: string;
sw_version: number;
sw_name: string;
auth_id: string;
edPkgSerial: number;
archiveType: 0 | 1;
execYn: 0 | 1;
secretAt: boolean;
downloadLocation: string;
user_id: string;
creation_datetime: string;
};

@ -53,6 +53,16 @@ export const request = {
onUploadProgress: progress, onUploadProgress: progress,
}); });
}, },
postWithConfig: (uri: string, data: any, config?: any): any => {
return axios.post(`${API_URL}${uri}`, data, config);
},
postMultipartWithParams: (uri: string, form: FormData, params?: any): any => {
return axios.post(`${API_URL}${uri}`, form, {
params,
headers: { "Content-Type": "multipart/form-data" },
});
},
postResponseFile: ( postResponseFile: (
uri: string, uri: string,
param: any, param: any,

@ -28,8 +28,17 @@ export const DatasetService = {
listExternal(query: ExternalListQuery) { listExternal(query: ExternalListQuery) {
return request.get("/api/datasets/list", query as any); return request.get("/api/datasets/list", query as any);
}, },
saveExternal(payload: SaveExternalDatasetPayload) { saveExternal(payload: SaveExternalDatasetPayload) {
return request.post("/api/datasets/dataset/save", payload); const params = new URLSearchParams();
(
Object.entries(payload) as [keyof SaveExternalDatasetPayload, any][]
).forEach(([k, v]) => {
if (v !== undefined && v !== null) params.append(k, String(v));
});
return request.post(
`/api/datasets/dataset/save?${params.toString()}`,
null,
);
}, },
}; };

@ -1,3 +1,4 @@
import { EdgePkgInfoVOModel } from "@/components/models/management/ExternalAuthController";
import { request } from "@/components/service/index"; import { request } from "@/components/service/index";
export const ExternalAuthControllerService = { export const ExternalAuthControllerService = {
@ -5,11 +6,11 @@ export const ExternalAuthControllerService = {
return request.post("/api/external-auth/signin", { id, password }); return request.post("/api/external-auth/signin", { id, password });
}, },
add: (token: string, edgePkgInfoVO: string, file: any) => { add: (params: EdgePkgInfoVOModel, file: File | Blob) => {
return request.post("/api/external-auth/add", { const fd = new FormData();
token, fd.append("file", file);
edgePkgInfoVO, return request.postWithConfig("/api/external-auth/register-with-file", fd, {
file, params,
}); });
}, },

@ -69,7 +69,6 @@ const data = ref({
allSelected: false, allSelected: false,
selected: [] as Array<{ deviceKey: number }>, selected: [] as Array<{ deviceKey: number }>,
isCreateVisible: false, isCreateVisible: false,
isExternalVisible: false,
isUploadVisible: false, isUploadVisible: false,
isModalVisible: false, isModalVisible: false,
isConfirmDialogVisible: false, isConfirmDialogVisible: false,
@ -574,9 +573,10 @@ watch(
</v-dialog> </v-dialog>
<v-dialog v-model="externalVisible" max-width="900" persistent> <v-dialog v-model="externalVisible" max-width="900" persistent>
<ExternalDatasetDialog <ExternalDatasetDialog
v-model="externalVisible"
:project-id="getProjectId()" :project-id="getProjectId()"
:ref-id="activeRefId" :ref-id="activeRefId"
@close="externalVisible = false" :reg-user-id="username"
@saved="onPickExternalSaved" @saved="onPickExternalSaved"
/> />
</v-dialog> </v-dialog>

@ -23,15 +23,23 @@ const experimentInfo = ref({
fileSize: "-", fileSize: "-",
}); });
const downloadObjectName = computed(() => detailRaw.value?.storagePath || ""); // storagePath ( ) , storedName
const canDownload = computed(() => !!downloadObjectName.value); const downloadObjectName = computed(() => {
const sp = detailRaw.value?.storagePath as string | undefined;
const sn = detailRaw.value?.storedName as string | undefined;
const raw = sp && /[\\/]/.test(sp) ? sp : sn || sp || "";
return (raw || "").replace(/^\/+/, "").replace(/\/{2,}/g, "/");
});
async function handleDownload() { async function handleDownload() {
const key = downloadObjectName.value.trim(); const key = downloadObjectName.value;
if (!key) return; if (!key) return;
try {
// 1: (/ )
const res = await AttachmentsService.downloadFile(key); const res = await AttachmentsService.downloadFile(key);
// (JSON)
const ct = String(res.headers["content-type"] || "").toLowerCase(); const ct = String(res.headers["content-type"] || "").toLowerCase();
if (ct.includes("application/json")) { if (ct.includes("application/json")) {
const text = await (res.data as Blob).text(); const text = await (res.data as Blob).text();
@ -43,26 +51,15 @@ async function handleDownload() {
} }
} }
// ( basename)
const cd = res.headers["content-disposition"] || ""; const cd = res.headers["content-disposition"] || "";
let filename: string | undefined;
const mUtf8 = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); const mUtf8 = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
const mStd = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i); const mStd = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i);
if (mUtf8?.[1]) { let filename =
try { (mUtf8 && decodeURIComponent(mUtf8[1].trim())) ||
filename = decodeURIComponent(mUtf8[1].trim()); (mStd && (mStd[1] || mStd[2])?.trim()) ||
} catch { key.split(/[\\/]/).pop() ||
filename = mUtf8[1].trim(); "download.bin";
}
} else if (mStd) {
filename = (mStd[1] || mStd[2])?.trim();
}
// 2) : objectName basename (fallback)
if (!filename) {
// key : "11fc4121-...-mlflow_pipeline.yaml" "dir/subdir/11fc...yaml"
const parts = key.split(/[\\/]/);
filename = parts[parts.length - 1] || "download.bin";
}
const blob = new Blob([res.data]); const blob = new Blob([res.data]);
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@ -73,8 +70,28 @@ async function handleDownload() {
a.click(); a.click();
a.remove(); a.remove();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} catch (err) {
// 2 : basename
const base = key.split(/[\\/]/).pop() || key;
if (base && base !== key) {
const res2 = await AttachmentsService.downloadFile(base);
const blob = new Blob([res2.data]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.setAttribute("download", base);
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
return;
}
throw err;
}
} }
const canDownload = computed(() => !!downloadObjectName.value);
// -------- utils -------- // -------- utils --------
const formatIso = (s?: string) => const formatIso = (s?: string) =>
s ? String(s).replace("T", " ").slice(0, 19) : "-"; s ? String(s).replace("T", " ").slice(0, 19) : "-";

@ -369,25 +369,23 @@ async function loadDatasetActivity() {
})) }))
.filter((g) => Number.isFinite(g.id)); .filter((g) => Number.isFinite(g.id));
//
datasetsByGroup.value = {}; datasetsByGroup.value = {};
groupLoaded.value = {}; groupLoaded.value = {};
groupLoading.value = {}; groupLoading.value = {};
datasetCountByGroup.value = {}; datasetCountByGroup.value = {};
// () // ()
const tasks = groupSummaries.value.map((g) => const tasks = groupSummaries.value.map((g) =>
AttachmentsService.search({ AttachmentsService.search({
projectId: currentProjectId.value, projectId: currentProjectId.value,
page: 0, page: 0,
size: 1, // 1, size: 1,
refType: "DATASET", refType: "DATASET",
refId: g.id, refId: g.id,
sortField: "id", sortField: "id",
sortDirection: "DESC", sortDirection: "DESC",
} as any) } as any)
.then((res: any) => { .then((res: any) => {
//
const total = Number( const total = Number(
res?.data?.totalElements ?? res?.data?.totalElements ??
res?.data?.total ?? res?.data?.total ??
@ -406,8 +404,12 @@ async function loadDatasetActivity() {
}; };
}), }),
); );
await Promise.allSettled(tasks); await Promise.allSettled(tasks);
//
await Promise.all(
groupSummaries.value.map((g) => loadDatasetsForGroup(g.id, g.name)),
);
} catch (err) { } catch (err) {
console.error("[Dashboard] loadDatasetActivity error:", err); console.error("[Dashboard] loadDatasetActivity error:", err);
groupSummaries.value = []; groupSummaries.value = [];
@ -677,7 +679,7 @@ watch(showKubeflowDetails, async () => {
<v-row> <v-row>
<!-- Recent Run --> <!-- Recent Run -->
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px"> <v-card class="pa-0" style="min-height: 450px; max-height: 450px">
<div <div
class="d-flex align-center justify-space-between w-100" class="d-flex align-center justify-space-between w-100"
style="padding: 16px; border-bottom: 1px solid #ccc" style="padding: 16px; border-bottom: 1px solid #ccc"
@ -695,7 +697,7 @@ watch(showKubeflowDetails, async () => {
</v-btn> </v-btn>
</div> </div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px"> <div style="overflow-y: auto; max-height: 380px; padding: 8px 16px">
<v-skeleton-loader <v-skeleton-loader
v-if="runsLoading" v-if="runsLoading"
type="list-item-two-line" type="list-item-two-line"
@ -759,7 +761,7 @@ watch(showKubeflowDetails, async () => {
<!-- Workflows --> <!-- Workflows -->
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px"> <v-card class="pa-0" style="min-height: 450px; max-height: 450px">
<div <div
class="d-flex align-center justify-space-between w-100" class="d-flex align-center justify-space-between w-100"
style="padding: 16px; border-bottom: 1px solid #ccc" style="padding: 16px; border-bottom: 1px solid #ccc"
@ -778,7 +780,7 @@ watch(showKubeflowDetails, async () => {
</v-btn> </v-btn>
</div> </div>
<div style="overflow-y: auto; max-height: 300px; padding: 8px 16px"> <div style="overflow-y: auto; max-height: 380px; padding: 8px 16px">
<v-list density="comfortable" nav> <v-list density="comfortable" nav>
<v-list-item <v-list-item
v-for="wfRow in workflows v-for="wfRow in workflows
@ -816,7 +818,7 @@ watch(showKubeflowDetails, async () => {
<!-- Kubeflow --> <!-- Kubeflow -->
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-card class="pa-0" style="min-height: 360px; max-height: 360px"> <v-card class="pa-0" style="min-height: 450px; max-height: 450px">
<div style="padding: 16px; border-bottom: 1px solid #3a3a3a"> <div style="padding: 16px; border-bottom: 1px solid #3a3a3a">
<div class="d-flex align-center justify-space-between w-100"> <div class="d-flex align-center justify-space-between w-100">
<h3 class="text-subtitle-1 font-weight-bold mb-0"> <h3 class="text-subtitle-1 font-weight-bold mb-0">
@ -859,7 +861,7 @@ watch(showKubeflowDetails, async () => {
</div> </div>
</div> </div>
<div style="padding: 8px 12px; height: 260px"> <div style="height: 380px">
<!-- Only Chart --> <!-- Only Chart -->
<div <div
v-if="!showKubeflowDetails" v-if="!showKubeflowDetails"
@ -961,13 +963,13 @@ watch(showKubeflowDetails, async () => {
<!-- Dataset (fixed height, inner scroll, compact) --> <!-- Dataset (fixed height, inner scroll, compact) -->
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-card class="pa-0 d-flex flex-column" :height="360"> <v-card class="pa-0 d-flex flex-column" :height="450">
<!-- Header (고정 높이) --> <!-- Header (고정 높이) -->
<div <div
class="d-flex align-center justify-space-between w-100 pa-4 flex-shrink-0" class="d-flex align-center justify-space-between w-100 pa-4 flex-shrink-0"
> >
<h3 class="text-subtitle-1 font-weight-bold mb-0"> <h3 class="text-subtitle-1 font-weight-bold mb-0">
Dataset Update Activity DataGroup Update Activity
</h3> </h3>
<v-btn <v-btn
variant="text" variant="text"
@ -1088,7 +1090,7 @@ watch(showKubeflowDetails, async () => {
</v-row> </v-row>
<!-- Bottom: Model Deployment (demo) --> <!-- Bottom: Model Deployment (demo) -->
<v-card class="rounded-lg pa-4 mt-4"> <!-- <v-card class="rounded-lg pa-4 mt-4">
<div class="d-flex justify-space-between align-center mt-8 mb-2 px-2"> <div class="d-flex justify-space-between align-center mt-8 mb-2 px-2">
<div class="d-flex align-center"> <div class="d-flex align-center">
<span class="text-subtitle-1 font-weight-bold">Model Deployment</span> <span class="text-subtitle-1 font-weight-bold">Model Deployment</span>
@ -1163,7 +1165,7 @@ watch(showKubeflowDetails, async () => {
</v-table> </v-table>
</v-sheet> </v-sheet>
</v-col> </v-col>
</v-card> </v-card> -->
</v-container> </v-container>
</template> </template>

@ -12,9 +12,10 @@ import {
} from "vue"; } from "vue";
import Plotly from "plotly.js-dist-min"; import Plotly from "plotly.js-dist-min";
import CompareRunsDialog from "@/components/atoms/organisms/CompareRunDialog.vue"; import CompareRunsDialog from "@/components/atoms/organisms/CompareRunDialog.vue";
import DeploymentDialog from "@/components/atoms/organisms/DeploymentDialog.vue";
/* ========= Constants & Types ========= */ /* ========= Constants & Types ========= */
const AUTH_KEY = "external-auth"; const AUTH_KEY = "external-auth";
const externalToken = computed(() => externalAuth.value?.token ?? "");
type ExternalAuth = { id: string; name: string; token: string }; type ExternalAuth = { id: string; name: string; token: string };
type PackageOption = { label: string; value: string; raw: any }; type PackageOption = { label: string; value: string; raw: any };
type MetricKV = { key: string; value: number }; type MetricKV = { key: string; value: number };
@ -679,7 +680,12 @@ const openDeploymentModal = async (fullPath?: string) => {
const closeCreateModal = () => { const closeCreateModal = () => {
isEditVisible.value = false; isEditVisible.value = false;
}; };
const saveData = (payload: any) => {
// /
console.log("[DeploymentDialog payload]", payload);
//
isEditVisible.value = false;
};
/* ========= Timeline (fallback) ========= */ /* ========= Timeline (fallback) ========= */
const rawHistory = computed<any[]>(() => { const rawHistory = computed<any[]>(() => {
const h = const h =
@ -1433,12 +1439,12 @@ const artifactsLoading = ref(false);
:packages-loading="packagesLoading" :packages-loading="packagesLoading"
:packages-error="packagesError" :packages-error="packagesError"
:artifact-path="lastArtifactUri" :artifact-path="lastArtifactUri"
:token="externalToken"
@close-modal="closeCreateModal" @close-modal="closeCreateModal"
@handle-data="() => {}" @handle-data="saveData"
:user-option="[]" :user-option="[]"
/> />
</v-dialog> </v-dialog>
<!-- 로그인 모달 --> <!-- 로그인 모달 -->
<v-dialog v-model="loginDialog" max-width="450" persistent> <v-dialog v-model="loginDialog" max-width="450" persistent>
<v-card> <v-card>

Loading…
Cancel
Save