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

main
jschoi 8 months ago
parent 465b56fad8
commit adac240170

@ -1,5 +1,7 @@
<script setup lang="ts">
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 };
@ -8,6 +10,7 @@ const props = defineProps<{
packagesLoading?: boolean;
packagesError?: string;
artifactPath?: string;
token: string; //
}>();
const emit = defineEmits<{
@ -18,7 +21,7 @@ const emit = defineEmits<{
const form = ref({
package_id: "",
sw_id: "",
sw_version: "",
sw_version: "1",
software_name: "",
executed: true,
file_type: "bundle" as "bundle" | "single",
@ -33,6 +36,25 @@ const form = ref({
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 isLinux = computed(() => form.value.os === "Linux");
@ -48,6 +70,13 @@ const readonlyFields = computed(() => ({
packageId: selectedRaw.value?.package_id ?? "",
}));
function confirmSuccess() {
if (pendingPayload.value) {
emit("handle-data", pendingPayload.value); //
}
emit("close-modal"); //
successDialog.value = false;
}
function onPickPackage() {
form.value.win_exe_name = selectedRaw.value?.window_exe_name ?? "";
form.value.win_root_path = selectedRaw.value?.window_root_location ?? "";
@ -74,13 +103,87 @@ watch(
},
);
function submit() {
if (!form.value.package_id) return;
emit("handle-data", {
...form.value,
resolved: readonlyFields.value,
artifact_path: props.artifactPath ?? "",
});
const installOsCode = computed(() => {
if (form.value.os === "Windows") return 0;
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,
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) {
@ -96,7 +199,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
class="text-white text-h6 font-weight-bold"
style="background: #1976d2"
>
차량 지능 SW 등록
Vehicle Intelligence Software Registration
</v-card-title>
<v-defaults-provider
@ -180,7 +283,12 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
<v-text-field label="SW ID" v-model="form.sw_id" />
</v-col>
<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 cols="12">
<v-text-field
@ -276,6 +384,36 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</v-col>
</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-col cols="12" md="8">
@ -289,13 +427,40 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
/>
</v-col>
</v-row>
<!-- 에러 메시지 -->
<v-alert
v-if="errorMsg"
type="error"
variant="tonal"
class="mt-3"
density="comfortable"
>
{{ errorMsg }}
</v-alert>
</v-card-text>
<v-card-actions class="py-2 px-3">
<v-spacer />
<v-btn color="success" @click="submit">SAVE</v-btn>
<v-btn variant="text" @click="$emit('close-modal')">CANCEL</v-btn>
<v-btn color="success" :loading="saving" @click="submit">SAVE</v-btn>
<v-btn variant="text" :disabled="saving" @click="$emit('close-modal')"
>CANCEL</v-btn
>
</v-card-actions>
</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>
</template>

@ -9,11 +9,12 @@ const props = defineProps<{
modelValue: boolean;
projectId: number;
refId?: number | null;
regUserId?: string;
}>();
const emit = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "close"): void;
(e: "saved"): void; //
(e: "saved"): void;
}>();
const visible = ref(props.modelValue);
@ -24,11 +25,19 @@ watch(
watch(visible, (v) => emit("update:modelValue", v));
const loading = ref(false);
const savingKey = ref<string | null>(null);
const errorMsg = ref("");
const rows = ref<ExternalDatasetItem[]>([]);
const searchKeyword = 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 {
return res && res.data ? res.data : res;
}
@ -40,7 +49,7 @@ async function fetchList() {
try {
const res = await DatasetService.listExternal({
ds_prj_idx: props.projectId,
search_keyword: searchKeyword.value || undefined, //
search_keyword: searchKeyword.value || undefined,
grp_name: groupName.value || undefined,
});
const body = unwrap<any>(res);
@ -50,7 +59,6 @@ async function fetchList() {
? body
: [];
// ds_dataset_name
const kw = (searchKeyword.value || "").trim().toLowerCase();
rows.value =
kw.length === 0
@ -60,7 +68,7 @@ async function fetchList() {
.toLowerCase()
.includes(kw),
);
} catch (e) {
} catch {
errorMsg.value = "외부 데이터셋 조회에 실패했습니다.";
} finally {
loading.value = false;
@ -73,26 +81,40 @@ function close() {
}
async function pick(item: ExternalDatasetItem) {
savingKey.value = item.ds_dataset_name;
errorMsg.value = "";
try {
// DatasetService.saveExternal(...) emit("saved")
// :
// await DatasetService.saveExternal({
// datasetName: item.ds_dataset_name,
// path: item.ds_dataset_base_path,
// refId: props.refId ?? 0,
// refType: "DATASET",
// title: item.ds_dataset_name,
// version: 1,
// description: "",
// regUserId: "currentUser",
// projectId: props.projectId,
// });
emit("saved");
await DatasetService.saveExternal({
datasetName: item.ds_dataset_name,
path: "", // UI Base Path ,
refId: props.refId ?? 0,
refType: "DATASET",
title: item.ds_dataset_name,
description: (descMap.value[item.ds_dataset_name] || "").trim(), //
version: 1,
regUserId: props.regUserId || "system",
projectId: props.projectId,
});
// &
successMsg.value = "업로드되었습니다.";
successVisible.value = true;
descMap.value[item.ds_dataset_name] = "";
} catch (e: any) {
errorMsg.value =
e?.response?.data?.message ||
"데이터셋 저장(다운로드→S3 업로드) 중 오류가 발생했습니다.";
} finally {
close();
savingKey.value = null;
}
}
function confirmSuccess() {
successVisible.value = false;
emit("saved"); //
close();
}
watch(() => props.projectId, fetchList);
onMounted(fetchList);
</script>
@ -107,11 +129,10 @@ onMounted(fetchList);
</v-card-title>
<v-card-text class="pa-4">
<!-- 🔧 검색바 정렬 -->
<div class="d-flex align-center ga-2 mb-3" style="flex-wrap: wrap">
<v-text-field
v-model="searchKeyword"
label="데이터셋 이름 (ds_dataset_name)"
label="데이터셋 이름 (Dataset Name)"
density="compact"
hide-details
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">Images</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>
</tr>
</thead>
@ -157,11 +179,28 @@ onMounted(fetchList);
<td>{{ r.ds_dataset_name }}</td>
<td>{{ r.ds_dataset_image_count }}</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>
<v-btn size="small" color="primary" @click="pick(r)"
>Select</v-btn
<v-btn
size="small"
color="primary"
:loading="savingKey === r.ds_dataset_name"
:disabled="savingKey !== null"
@click="pick(r)"
>
Select
</v-btn>
</td>
</tr>
<tr v-if="!loading && rows.length === 0">
@ -177,4 +216,19 @@ onMounted(fetchList);
<v-btn variant="text" @click="close">Close</v-btn>
</v-card-actions>
</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>

@ -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,
});
},
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: (
uri: string,
param: any,

@ -28,8 +28,17 @@ export const DatasetService = {
listExternal(query: ExternalListQuery) {
return request.get("/api/datasets/list", query as any);
},
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";
export const ExternalAuthControllerService = {
@ -5,11 +6,11 @@ export const ExternalAuthControllerService = {
return request.post("/api/external-auth/signin", { id, password });
},
add: (token: string, edgePkgInfoVO: string, file: any) => {
return request.post("/api/external-auth/add", {
token,
edgePkgInfoVO,
file,
add: (params: EdgePkgInfoVOModel, file: File | Blob) => {
const fd = new FormData();
fd.append("file", file);
return request.postWithConfig("/api/external-auth/register-with-file", fd, {
params,
});
},

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

@ -23,58 +23,75 @@ const experimentInfo = ref({
fileSize: "-",
});
const downloadObjectName = computed(() => detailRaw.value?.storagePath || "");
const canDownload = computed(() => !!downloadObjectName.value);
// storagePath ( ) , storedName
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() {
const key = downloadObjectName.value.trim();
const key = downloadObjectName.value;
if (!key) return;
const res = await AttachmentsService.downloadFile(key);
try {
// 1: (/ )
const res = await AttachmentsService.downloadFile(key);
const ct = String(res.headers["content-type"] || "").toLowerCase();
if (ct.includes("application/json")) {
const text = await (res.data as Blob).text();
try {
const json = JSON.parse(text);
throw new Error(json.message || text);
} catch {
throw new Error(text);
// (JSON)
const ct = String(res.headers["content-type"] || "").toLowerCase();
if (ct.includes("application/json")) {
const text = await (res.data as Blob).text();
try {
const json = JSON.parse(text);
throw new Error(json.message || text);
} catch {
throw new Error(text);
}
}
}
const cd = res.headers["content-disposition"] || "";
let filename: string | undefined;
const mUtf8 = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
const mStd = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i);
if (mUtf8?.[1]) {
try {
filename = decodeURIComponent(mUtf8[1].trim());
} catch {
filename = mUtf8[1].trim();
}
} else if (mStd) {
filename = (mStd[1] || mStd[2])?.trim();
}
// ( basename)
const cd = res.headers["content-disposition"] || "";
const mUtf8 = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
const mStd = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i);
let filename =
(mUtf8 && decodeURIComponent(mUtf8[1].trim())) ||
(mStd && (mStd[1] || mStd[2])?.trim()) ||
key.split(/[\\/]/).pop() ||
"download.bin";
// 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 url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.setAttribute("download", filename);
document.body.appendChild(a);
a.click();
a.remove();
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 blob = new Blob([res.data]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.setAttribute("download", filename);
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
const canDownload = computed(() => !!downloadObjectName.value);
// -------- utils --------
const formatIso = (s?: string) =>
s ? String(s).replace("T", " ").slice(0, 19) : "-";

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

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

Loading…
Cancel
Save