feat:Deployment 버튼 분리

main
jschoi 8 months ago
parent 96b3718493
commit 482b35da8d

@ -21,6 +21,8 @@ const props = defineProps<{
packagesError?: string; packagesError?: string;
artifactPath?: string; artifactPath?: string;
token: string; token: string;
hideArtifactPath?: boolean;
hideUploadFile?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -74,7 +76,9 @@ const remoteError = ref("");
const pkgOptions = computed<PackageOption[]>(() => const pkgOptions = computed<PackageOption[]>(() =>
remotePackages.value.length ? remotePackages.value : (props.packages ?? []), remotePackages.value.length ? remotePackages.value : (props.packages ?? []),
); );
/* ========================= 표시/숨김 스위치 ========================= */
const showArtifactPath = computed(() => !!props.artifactPath);
const showUploadFile = computed(() => !showArtifactPath.value);
function getCurrentUserId(): string { function getCurrentUserId(): string {
// id , // id ,
try { try {
@ -258,7 +262,16 @@ async function submit() {
sw_type: sourceType.value === "edge" ? 1 : 0, sw_type: sourceType.value === "edge" ? 1 : 0,
creation_datetime: new Date().toISOString(), creation_datetime: new Date().toISOString(),
}; };
if (props.hideArtifactPath) {
// Upload
if (!file.value) return (errorMsg.value = "업로드 파일을 선택하세요.");
}
if (props.hideUploadFile) {
// Artifact artifactPath ,
if (file.value) clearFile();
if (!props.artifactPath)
return (errorMsg.value = "Artifact 경로가 없습니다.");
}
try { try {
saving.value = true; saving.value = true;
let res: any; let res: any;
@ -501,7 +514,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</v-row> </v-row>
<!-- Artifact Path --> <!-- Artifact Path -->
<v-row dense class="mb-1"> <v-row dense class="mb-1" v-if="!props.hideArtifactPath">
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-text-field
label="차량 SW 파일 (Artifact Path)" label="차량 SW 파일 (Artifact Path)"
@ -512,16 +525,16 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onEsc));
</v-row> </v-row>
<!-- 파일 업로드 --> <!-- 파일 업로드 -->
<v-row dense class="mb-1"> <v-row dense class="mb-1" v-if="!props.hideUploadFile">
<v-col cols="12"> <v-col cols="12">
<div class="text-body-2 font-weight-medium mb-1">업로드 파일</div> <div class="text-body-2 font-weight-medium mb-1">업로드 파일</div>
<div class="d-flex align-center ga-3"> <div class="d-flex align-center ga-3">
<v-btn size="small" color="primary" @click="fileInput?.click()" <v-btn size="small" color="primary" @click="fileInput?.click()"
>파일 선택</v-btn >파일 선택</v-btn
> >
<span v-if="file" class="text-body-2" <span v-if="file" class="text-body-2">
>{{ file.name }} ({{ file.size.toLocaleString() }} bytes)</span {{ file.name }} ({{ file.size.toLocaleString() }} bytes)
> </span>
<v-btn <v-btn
v-if="file" v-if="file"
size="x-small" size="x-small"

@ -5,7 +5,7 @@
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { storage } from "@/utils/storage.js"; import { storage } from "@/utils/storage.js";
import { UserManagerService } from "@/components/service/management/userManagerService"; import { UserManagerService } from "@/components/service/management/UserManagerService";
import { menuUtils } from "@/utils/menuUtils"; import { menuUtils } from "@/utils/menuUtils";
/* ================================ /* ================================
@ -20,7 +20,7 @@ type MenuItem = {
}; };
/* ================================ /* ================================
* Router / Base states * Router / Reactive base state
* ================================ */ * ================================ */
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -29,9 +29,12 @@ const username = ref<string>("");
const projectName = ref<string>(localStorage.getItem("projectName") || ""); const projectName = ref<string>(localStorage.getItem("projectName") || "");
const isAdmin = ref<boolean>(false); const isAdmin = ref<boolean>(false);
const adminMode = ref<boolean>(false); const adminMode = ref<boolean>(false); // Settings /
const lastNonAdminPath = ref<string>("/home"); const lastNonAdminPath = ref<string>("/home");
/* ================================
* Auth / Role helpers
* ================================ */
function readAuth() { function readAuth() {
try { try {
const raw = const raw =
@ -43,6 +46,7 @@ function readAuth() {
return null; return null;
} }
} }
function computeIsAdmin() { function computeIsAdmin() {
const auth = readAuth(); const auth = readAuth();
const roles = auth?.userInfo?.roles ?? auth?.roles ?? []; const roles = auth?.userInfo?.roles ?? auth?.roles ?? [];
@ -52,6 +56,7 @@ function computeIsAdmin() {
: roles === "ROLE_ADMIN"; : roles === "ROLE_ADMIN";
isAdmin.value = inRoles || authCd === "ADMIN"; isAdmin.value = inRoles || authCd === "ADMIN";
} }
function updateUsername() { function updateUsername() {
const auth = readAuth(); const auth = readAuth();
username.value = auth?.userInfo?.username ?? auth?.username ?? ""; username.value = auth?.userInfo?.username ?? auth?.username ?? "";
@ -59,6 +64,8 @@ function updateUsername() {
/* ================================ /* ================================
* Derived route state * Derived route state
* - /select
* - 관리자 표시 조건: adminMode ON || 관리자 라우트
* ================================ */ * ================================ */
const hideAllMenus = computed<boolean>(() => route.path.startsWith("/select")); const hideAllMenus = computed<boolean>(() => route.path.startsWith("/select"));
@ -71,16 +78,18 @@ const isAdminRoute = computed<boolean>(() => {
const hitMeta = route.matched.some((r) => r.meta?.requiresAdmin); const hitMeta = route.matched.some((r) => r.meta?.requiresAdmin);
return hitPath || hitMeta; return hitPath || hitMeta;
}); });
const showAdminTabs = computed<boolean>( const showAdminTabs = computed<boolean>(
() => adminMode.value || isAdminRoute.value, () => adminMode.value || isAdminRoute.value,
); );
/* ================================ /* ================================
* Menus * Menus (기본/관리자)
* ================================ */ * ================================ */
const baseMenus = computed<MenuItem[]>( const baseMenus = computed<MenuItem[]>(
() => (menuUtils?.menuItem ?? []) as MenuItem[], () => (menuUtils?.menuItem ?? []) as MenuItem[],
); );
const adminMenus = computed<MenuItem[]>(() => { const adminMenus = computed<MenuItem[]>(() => {
const fromUtil = (menuUtils?.adminMenuItem ?? []) as MenuItem[]; const fromUtil = (menuUtils?.adminMenuItem ?? []) as MenuItem[];
return fromUtil.length return fromUtil.length
@ -90,75 +99,50 @@ const adminMenus = computed<MenuItem[]>(() => {
{ title: "Users", icon: "mdi-account-multiple", path: "/users" }, { title: "Users", icon: "mdi-account-multiple", path: "/users" },
]; ];
}); });
const isLinkActive = (path?: string) => !!path && route.path.startsWith(path); const isLinkActive = (path?: string) => !!path && route.path.startsWith(path);
/* ================================ /* ================================
* 사용자 메뉴 (우측) * Header dropdown menu
* ================================ */ * ================================ */
const menu = ref<MenuItem[]>([]); const menu = ref<MenuItem[]>([]);
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ title: "Select Project", click: () => goSelect() }, { title: "Select Project", click: () => goSelect() },
{ title: "Change Password", click: () => {} }, {
title: "Change Password",
click: () => {
/* open modal */
},
},
{ title: "Logout", icon: "mdi-logout", click: () => logOut() }, { title: "Logout", icon: "mdi-logout", click: () => logOut() },
]; ];
/* ================================ /* ================================
* 상단 Hover 하위 메뉴 스트립 * Navigation actions
* ================================ */
type DepthItem = { title: string; path: string };
const hoverBar = ref<{
open: boolean;
items: DepthItem[];
}>({ open: false, items: [] });
let hideTimer: number | null = null;
function showHoverStrip(m: MenuItem) {
if (!m.depth?.length) return;
if (hideTimer) {
window.clearTimeout(hideTimer);
hideTimer = null;
}
hoverBar.value = {
open: true,
items: m.depth,
};
}
function scheduleHideStrip() {
if (hideTimer) window.clearTimeout(hideTimer);
hideTimer = window.setTimeout(() => {
hoverBar.value.open = false;
}, 140);
}
function keepStrip() {
if (hideTimer) {
window.clearTimeout(hideTimer);
hideTimer = null;
}
}
/* ================================
* Navigation
* ================================ */ * ================================ */
function goMain() { function goMain() {
adminMode.value = false; adminMode.value = false;
router.push("/home"); router.push("/home");
} }
function goSelect() { function goSelect() {
adminMode.value = false; adminMode.value = false;
router.push("/select"); router.push("/select");
} }
function toggleAdmin() { function toggleAdmin() {
if (!isAdmin.value) return; if (!isAdmin.value) return;
if (adminMode.value) { if (adminMode.value) {
//
adminMode.value = false; adminMode.value = false;
router.push("/home"); router.push("/home");
} else { } else {
adminMode.value = true; adminMode.value = true;
//
if (!isAdminRoute.value) router.push("/project"); if (!isAdminRoute.value) router.push("/project");
} }
} }
function logOut() { function logOut() {
UserManagerService.signOut() UserManagerService.signOut()
.catch(console.error) .catch(console.error)
@ -181,16 +165,18 @@ function logOut() {
function refreshProjectName() { function refreshProjectName() {
projectName.value = localStorage.getItem("projectName") || ""; projectName.value = localStorage.getItem("projectName") || "";
} }
//
watch( watch(
() => route.fullPath, () => route.fullPath,
() => { () => {
refreshProjectName(); refreshProjectName();
//
hoverBar.value.open = false;
if (!isAdminRoute.value) lastNonAdminPath.value = route.fullPath || "/home"; if (!isAdminRoute.value) lastNonAdminPath.value = route.fullPath || "/home";
}, },
{ immediate: true }, { immediate: true },
); );
// storage
function onStorage(e: StorageEvent) { function onStorage(e: StorageEvent) {
if (!e.key || e.key === "projectName") { if (!e.key || e.key === "projectName") {
refreshProjectName(); refreshProjectName();
@ -212,9 +198,9 @@ onMounted(() => {
menu.value = menuItems; menu.value = menuItems;
window.addEventListener("storage", onStorage); window.addEventListener("storage", onStorage);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("storage", onStorage); window.removeEventListener("storage", onStorage);
if (hideTimer) window.clearTimeout(hideTimer);
}); });
</script> </script>
@ -232,24 +218,54 @@ onBeforeUnmount(() => {
AUTOFLOW WEB CONSOLE AUTOFLOW WEB CONSOLE
</div> </div>
<!-- 여기 스페이서를 '브랜드 다음' 둬서 오른쪽으로 밀기 --> <!-- 중앙: 메뉴 그룹 (Settings / 분기) -->
<v-spacer /> <div class="center-nav d-none d-md-flex" v-if="!hideAllMenus">
<!-- 관리자 메뉴: showAdminTabs 조건으로 표시 -->
<!-- 메뉴는 우측 정렬 --> <!-- 관리자 메뉴바: 기본 메뉴바와 1:1 동일 구조 -->
<div class="right-nav d-none d-md-flex" v-if="!hideAllMenus">
<!-- 관리자 메뉴 -->
<template v-if="showAdminTabs"> <template v-if="showAdminTabs">
<template v-for="(m, i) in adminMenus" :key="'am_' + i"> <template v-for="(m, i) in adminMenus" :key="'am_' + i">
<!-- 드롭다운 있는 항목 -->
<v-menu
v-if="m.depth?.length"
open-on-hover
close-on-content-click
location="bottom"
>
<template #activator="{ props }">
<v-btn <v-btn
v-bind="props"
variant="text" variant="text"
class="nav-btn" class="nav-btn"
:class="{ :class="{
'nav-active': 'nav-active': m.depth?.some((d: any) =>
m.depth?.some((d: any) => isLinkActive(d.path)) || isLinkActive(d.path),
isLinkActive(m.path), ),
}" }"
@mouseenter="showHoverStrip(m)" append-icon="mdi-chevron-down"
@mouseleave="scheduleHideStrip" >
<v-icon start :icon="m.icon" class="mr-1" />
{{ m.title }}
</v-btn>
</template>
<v-list density="compact" class="min-w-48">
<v-list-item
v-for="(d, j) in m.depth"
:key="'amd_' + j"
:title="d.title"
:to="d.path"
:active="isLinkActive(d.path)"
active-class="nav-active"
/>
</v-list>
</v-menu>
<!-- 드롭다운 없는 단일 항목 -->
<v-btn
v-else
variant="text"
class="nav-btn"
:class="{ 'nav-active': isLinkActive(m.path) }"
@click="m.path && router.push(m.path)" @click="m.path && router.push(m.path)"
> >
<v-icon start :icon="m.icon" class="mr-1" /> <v-icon start :icon="m.icon" class="mr-1" />
@ -261,17 +277,87 @@ onBeforeUnmount(() => {
<!-- 기본 메뉴 --> <!-- 기본 메뉴 -->
<template v-else> <template v-else>
<template v-for="(m, i) in baseMenus" :key="'m_' + i"> <template v-for="(m, i) in baseMenus" :key="'m_' + i">
<v-menu
v-if="m.depth?.length"
open-on-hover
close-on-content-click
location="bottom"
transition="scale-transition"
>
<template #activator="{ props }">
<v-btn <v-btn
v-bind="props"
variant="text" variant="text"
class="nav-btn text-white" class="nav-btn text-white"
:class="{ :class="{
'nav-active': 'nav-active': m.depth?.some((d: any) =>
m.depth?.some((d: any) => isLinkActive(d.path)) || isLinkActive(d.path),
isLinkActive(m.path), ),
}" }"
@mouseenter="showHoverStrip(m)" append-icon="mdi-chevron-down"
@mouseleave="scheduleHideStrip" >
@click="!m.depth?.length && m.path && router.push(m.path)" <v-icon start :icon="m.icon" class="mr-1" />
{{ m.title }}
</v-btn>
</template>
<!-- 여기부터: Run 전용 디자인 -->
<template
v-if="
(m.title && m.title.toLowerCase() === 'run') ||
(m.path && m.path.startsWith('/run'))
"
>
<v-card
rounded="lg"
elevation="12"
color="surface"
class="px-2 py-2"
>
<v-list density="comfortable" lines="one" class="min-w-48">
<template v-for="(d, j) in m.depth" :key="'run_' + j">
<v-hover v-slot="{ isHovering, props: liProps }">
<v-list-item
v-bind="liProps"
:title="d.title"
:to="d.path"
:active="isLinkActive(d.path)"
color="primary"
:rounded="'lg'"
:variant="
isHovering || isLinkActive(d.path)
? 'tonal'
: 'text'
"
class="mx-2 my-1 text-white"
/>
</v-hover>
</template>
</v-list>
</v-card>
</template>
<!-- 기본 하위메뉴 (Run 이외는 기존 그대로) -->
<template v-else>
<v-list density="compact" class="min-w-48 subnav-list">
<v-list-item
v-for="(d, j) in m.depth"
:key="'d_' + j"
:title="d.title"
:to="d.path"
class="submenu-item text-white"
:class="{ 'submenu-active': isLinkActive(d.path) }"
/>
</v-list>
</template>
</v-menu>
<v-btn
v-else
variant="text"
class="nav-btn"
:class="{ 'nav-active': isLinkActive(m.path) }"
@click="m.path && router.push(m.path)"
> >
<v-icon start :icon="m.icon" class="mr-1" /> <v-icon start :icon="m.icon" class="mr-1" />
{{ m.title }} {{ m.title }}
@ -279,7 +365,10 @@ onBeforeUnmount(() => {
</template> </template>
</template> </template>
</div> </div>
<!-- 우측 아이콘들 -->
<v-spacer />
<!-- 우측: 기존 기능 유지 -->
<v-tooltip v-if="isAdmin" location="bottom" text="Settings"> <v-tooltip v-if="isAdmin" location="bottom" text="Settings">
<template #activator="{ props }" v-if="!hideAllMenus"> <template #activator="{ props }" v-if="!hideAllMenus">
<v-btn <v-btn
@ -337,37 +426,6 @@ onBeforeUnmount(() => {
</v-menu> </v-menu>
</v-app-bar> </v-app-bar>
<!-- ===== 상단 하위 메뉴 스트립 (호버 표시) ===== -->
<v-slide-y-transition>
<v-sheet
v-if="hoverBar.open"
class="hover-strip"
elevation="8"
color="surface"
@mouseenter="keepStrip"
@mouseleave="scheduleHideStrip"
>
<v-container class="py-2" :fluid="true">
<!-- 중앙 정렬 -->
<v-row class="g-2" no-gutters justify="center" align="center">
<v-col class="d-flex flex-wrap justify-center" cols="12">
<v-btn
v-for="d in hoverBar.items"
:key="d.path"
size="small"
class="mx-1 my-1 strip-chip"
:variant="isLinkActive(d.path) ? 'tonal' : 'text'"
:color="isLinkActive(d.path) ? 'primary' : undefined"
@click="router.push(d.path)"
>
{{ d.title }}
</v-btn>
</v-col>
</v-row>
</v-container>
</v-sheet>
</v-slide-y-transition>
<!-- 본문 --> <!-- 본문 -->
<v-main> <v-main>
<v-container <v-container
@ -388,70 +446,64 @@ onBeforeUnmount(() => {
border-bottom: 1px solid rgba(255, 255, 255, 0.06); border-bottom: 1px solid rgba(255, 255, 255, 0.06);
} }
/* 브랜드 */ /* 더 커진 홈(브랜드) 버튼 */
.brand-btn { .brand-btn {
font-weight: 800; font-weight: 800;
letter-spacing: 0.08em; letter-spacing: 0.08em;
padding: 0 14px; padding: 0 14px;
} }
.right-nav { /* 중앙 고정 네비게이션 */
display: flex; .center-nav {
position: absolute;
left: 50%;
transform: translateX(-50%);
gap: 8px;
align-items: center; align-items: center;
gap: 8px; /* 버튼 간격 */
justify-content: flex-end;
} }
.nav-btn { .nav-btn {
text-transform: none; text-transform: none;
border-radius: 10px; border-radius: 10px;
padding: 0 16px; padding: 0 16px;
font-size: 14px; font-size: 14px;
color: #fff !important; color: #fff !important; /* 흰색 텍스트 통일 */
} }
.nav-btn:hover { .nav-btn:hover {
background: rgba(59, 130, 246, 0.08); background: rgba(59, 130, 246, 0.08);
} }
.nav-active { .nav-active {
background: rgba(59, 130, 246, 0.22); background: rgba(59, 130, 246, 0.22);
height: 46px; height: 46px;
color: #fff !important; color: #fff !important;
} }
.userbox { /* 드롭다운(하위 메뉴)도 동일 룩으로 */
min-width: 180px; .subnav-list {
background: transparent; /* 탑바 느낌 유지 */
} }
/* ===== 호버 스트립 (상단 바로 아래, 이미지 스타일) ===== */ .submenu-item {
.hover-strip { color: #fff !important;
position: fixed; border-radius: 10px;
top: var(--v-layout-top, 64px); /* app-bar 바로 아래 */ margin: 2px 8px;
left: 0;
right: 0;
z-index: 2500;
/* 다크 배경 + 살짝 투명 + 경계 */
background: rgba(32, 32, 32, 0.96);
border-bottom: 1px solid rgb(145, 61, 61);
backdrop-filter: blur(6px);
} }
/* 버튼(알약) 다크에서도 비활성 글자/테두리 선명 */ .submenu-item:hover {
.strip-chip { background: rgba(59, 130, 246, 0.08);
border-radius: 9999px !important;
text-transform: none;
font-weight: 600;
letter-spacing: 0;
height: 30px;
padding: 0 14px;
color: #e5e7eb !important; /* 비활성도 흐려 보이지 않게 */
} }
.strip-chip.v-btn--variant-text { .submenu-active {
/* text 변형일 때도 흐릿하지 않게 약한 테두리 */ background: rgba(59, 130, 246, 0.22);
border: 1px solid rgba(255, 255, 255, 0.14) !important; color: #fff !important;
background: transparent !important; }
.min-w-48 {
min-width: 12rem;
} }
.strip-chip:hover { .userbox {
background: rgba(255, 255, 255, 0.06) !important; min-width: 180px;
} }
</style> </style>

@ -60,6 +60,8 @@ const packageOptions = ref<PackageOption[]>([]);
const packagesLoading = ref(false); const packagesLoading = ref(false);
const packagesError = ref(""); const packagesError = ref("");
const shouldOpenDeploymentAfterLogin = ref(false); const shouldOpenDeploymentAfterLogin = ref(false);
const dlgHideArtifactPath = ref(false);
const dlgHideUploadFile = ref(false);
/* ========= MLflow State ========= */ /* ========= MLflow State ========= */
const runs = ref<any[]>([]); const runs = ref<any[]>([]);
const loadingRuns = ref(false); const loadingRuns = ref(false);
@ -72,7 +74,7 @@ const runDetailCache = ref<Map<string, RunDetailType>>(new Map());
/* ========= UI Tabs / Compare ========= */ /* ========= UI Tabs / Compare ========= */
const mainTab = ref<"details" | "viz" | "artifacts">("details"); const mainTab = ref<"details" | "viz" | "artifacts">("details");
const vizTab = ref<"metrics" | "scatter" | "box" | "contour">("metrics"); const vizTab = ref<"metrics" | "scatter" | "box" | "contour">("metrics");
const deployOpenMode = ref<"artifact" | "upload">("artifact");
const compareDialog = ref(false); const compareDialog = ref(false);
const compareLoading = ref(false); const compareLoading = ref(false);
const compareChartMode = ref<"byMetric" | "byRun">("byMetric"); const compareChartMode = ref<"byMetric" | "byRun">("byMetric");
@ -649,19 +651,35 @@ const handleLogin = async () => {
}; };
/* ========= Deployment Modal ========= */ /* ========= Deployment Modal ========= */
const openDeploymentModal = async (fullPath?: string) => { const openDeploymentModal = async (
fullPath?: string,
mode: "upload" | "artifact" = fullPath ? "artifact" : "upload",
) => {
if (fullPath) { if (fullPath) {
const uri = buildArtifactUri(fullPath); const uri = buildArtifactUri(fullPath);
lastArtifactUri.value = uri; lastArtifactUri.value = uri;
pendingArtifactPath.value = uri; pendingArtifactPath.value = uri;
} else {
lastArtifactUri.value = "";
pendingArtifactPath.value = null;
} }
if (!isAuthenticated.value) { if (!isAuthenticated.value) {
shouldOpenDeploymentAfterLogin.value = true; shouldOpenDeploymentAfterLogin.value = true;
loginDialog.value = true; loginDialog.value = true;
return; return;
} }
if (mode === "artifact") {
dlgHideArtifactPath.value = false;
dlgHideUploadFile.value = true;
if (!fullPath && pendingArtifactPath.value) if (!fullPath && pendingArtifactPath.value)
lastArtifactUri.value = pendingArtifactPath.value; lastArtifactUri.value = pendingArtifactPath.value;
} else {
dlgHideArtifactPath.value = true;
dlgHideUploadFile.value = false;
lastArtifactUri.value = "";
}
isEditVisible.value = true; isEditVisible.value = true;
packagesError.value = ""; packagesError.value = "";
@ -699,6 +717,7 @@ const openDeploymentModal = async (fullPath?: string) => {
packagesLoading.value = false; packagesLoading.value = false;
} }
}; };
const closeCreateModal = () => { const closeCreateModal = () => {
isEditVisible.value = false; isEditVisible.value = false;
}; };
@ -1259,6 +1278,15 @@ const artifactsLoading = ref(false);
<v-icon start size="16">mdi-check-decagram</v-icon> <v-icon start size="16">mdi-check-decagram</v-icon>
{{ externalAuth?.name || externalAuth?.id }} {{ externalAuth?.name || externalAuth?.id }}
</v-chip> </v-chip>
<v-btn
size="small"
color="primary"
variant="text"
@click="openDeploymentModal(undefined, 'upload')"
title="업로드 파일로 등록(Artifact Path 숨김)"
>
<v-icon start size="16">mdi-rocket-launch</v-icon> Deploy
</v-btn>
<v-btn <v-btn
v-if="isAuthenticated" v-if="isAuthenticated"
size="small" size="small"
@ -1379,7 +1407,9 @@ const artifactsLoading = ref(false);
? 'Deploy' ? 'Deploy'
: 'Login required' : 'Login required'
" "
@onClick="openDeploymentModal(it.path)" @onClick="
openDeploymentModal(it.path, 'artifact')
"
/> />
</template> </template>
<template v-else> <template v-else>
@ -1428,6 +1458,8 @@ const artifactsLoading = ref(false);
:packages-error="packagesError" :packages-error="packagesError"
:artifact-path="lastArtifactUri" :artifact-path="lastArtifactUri"
:token="externalToken" :token="externalToken"
:hide-artifact-path="dlgHideArtifactPath"
:hide-upload-file="dlgHideUploadFile"
@close-modal="closeCreateModal" @close-modal="closeCreateModal"
@handle-data="saveData" @handle-data="saveData"
:user-option="[]" :user-option="[]"

Loading…
Cancel
Save