mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-01 23:58:31 +00:00
fix: version deployments modal refactor
This commit is contained in:
@@ -4,7 +4,7 @@ interface Props {
|
||||
modalSize: 'small' | 'medium' | 'large' | keyof typeof modalSizes
|
||||
title?: string
|
||||
subTitle?: string
|
||||
variant?: 'draftOrPublish'
|
||||
variant?: 'draftOrPublish' | 'versionHistory'
|
||||
contentClass?: string
|
||||
maskClosable?: boolean
|
||||
}
|
||||
@@ -33,10 +33,13 @@ const { modalSize, variant } = toRefs(props)
|
||||
<template v-if="variant === 'draftOrPublish'">
|
||||
<DlgManagedAppDraftOrPublish v-model:visible="vVisible" />
|
||||
</template>
|
||||
<template v-if="variant === 'versionHistory'">
|
||||
<DlgManagedAppVersionHistory v-model:visible="vVisible" />
|
||||
</template>
|
||||
<slot v-else-if="$slots.default"> </slot>
|
||||
<template v-else>
|
||||
<slot name="header">
|
||||
<NcDlgManagedAppHeader :visible="vVisible" :modalSize="modalSize" :title="title" :subTitle="subTitle" />
|
||||
<DlgManagedAppHeader v-model:visible="vVisible" :modalSize="modalSize" :title="title" :subTitle="subTitle" />
|
||||
</slot>
|
||||
|
||||
<div class="flex-1 nc-scrollbar-thin" :class="contentClass">
|
||||
@@ -48,8 +51,6 @@ const { modalSize, variant } = toRefs(props)
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-dlg-managed-app {
|
||||
@apply !p-0;
|
||||
|
||||
492
packages/nc-gui/components/dlg/ManagedApp/VersionDeployments.vue
Normal file
492
packages/nc-gui/components/dlg/ManagedApp/VersionDeployments.vue
Normal file
@@ -0,0 +1,492 @@
|
||||
<script lang="ts" setup>
|
||||
import { DeploymentStatus } from 'nocodb-sdk'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
version: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const emits = defineEmits(['update:visible'])
|
||||
|
||||
const vVisible = useVModel(props, 'visible', emits)
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const baseStore = useBase()
|
||||
|
||||
const { base, managedApp } = storeToRefs(baseStore)
|
||||
|
||||
const isLoading = ref(false)
|
||||
const deployments = ref<any[]>([])
|
||||
const pageInfo = ref<any>({})
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
// Expanded deployment logs state
|
||||
const expandedBaseId = ref<string | null>(null)
|
||||
const deploymentLogs = ref<any[]>([])
|
||||
const logsPageInfo = ref<any>({})
|
||||
const logsCurrentPage = ref(1)
|
||||
const logsPageSize = 10
|
||||
const isLoadingLogs = ref(false)
|
||||
|
||||
const loadDeployments = async (page = 1) => {
|
||||
if (!managedApp.value?.id || !props.version?.versionId || !base.value?.fk_workspace_id) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const offset = (page - 1) * pageSize
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppVersionDeployments',
|
||||
managedAppId: managedApp.value.id,
|
||||
versionId: props.version.versionId,
|
||||
limit: pageSize,
|
||||
offset,
|
||||
} as any)
|
||||
|
||||
if (response) {
|
||||
deployments.value = response.list || []
|
||||
pageInfo.value = response.pageInfo || {}
|
||||
currentPage.value = page
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadDeploymentLogs = async (baseId: string, page = 1) => {
|
||||
if (!base.value?.fk_workspace_id) return
|
||||
|
||||
isLoadingLogs.value = true
|
||||
try {
|
||||
const offset = (page - 1) * logsPageSize
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppDeploymentLogs',
|
||||
baseId,
|
||||
limit: logsPageSize,
|
||||
offset,
|
||||
} as any)
|
||||
|
||||
if (response) {
|
||||
deploymentLogs.value = response.logs || []
|
||||
logsPageInfo.value = response.pageInfo || {}
|
||||
logsCurrentPage.value = page
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoadingLogs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExpand = async (deployment: any) => {
|
||||
if (expandedBaseId.value === deployment.baseId) {
|
||||
// Collapse
|
||||
expandedBaseId.value = null
|
||||
deploymentLogs.value = []
|
||||
logsCurrentPage.value = 1
|
||||
} else {
|
||||
// Expand and load logs
|
||||
expandedBaseId.value = deployment.baseId
|
||||
await loadDeploymentLogs(deployment.baseId, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
|
||||
return parseStringDateTime(dateString, 'MMM DD, YYYY, hh:mm A')
|
||||
}
|
||||
|
||||
const getDeploymentTypeLabel = (type: string) => {
|
||||
const labels = {
|
||||
install: 'Initial Install',
|
||||
update: 'Update',
|
||||
}
|
||||
return labels[type as keyof typeof labels] || type
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
[DeploymentStatus.SUCCESS]: 'text-nc-content-green-dark bg-nc-bg-green-light',
|
||||
[DeploymentStatus.FAILED]: 'text-nc-content-red-dark bg-nc-bg-red-light',
|
||||
[DeploymentStatus.PENDING]: 'text-nc-content-orange-dark bg-nc-bg-orange-light',
|
||||
[DeploymentStatus.IN_PROGRESS]: 'text-nc-content-blue-dark bg-nc-bg-blue-light',
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'text-nc-content-gray bg-nc-bg-gray-light'
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
loadDeployments(page)
|
||||
}
|
||||
|
||||
const handleLogsPageChange = (page: number) => {
|
||||
if (expandedBaseId.value) {
|
||||
loadDeploymentLogs(expandedBaseId.value, page)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
vVisible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadDeployments(1)
|
||||
expandedBaseId.value = null
|
||||
deploymentLogs.value = []
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<DlgManagedAppHeader v-model:visible="vVisible" title="Version Deployments">
|
||||
<template #icon>
|
||||
<GeneralIcon icon="ncServer" class="w-5 h-5 text-white" />
|
||||
</template>
|
||||
<template #subTitle>
|
||||
Tracking installations for
|
||||
<span class="font-mono font-semibold text-nc-content-brand">v{{ version?.version }}</span>
|
||||
</template>
|
||||
</DlgManagedAppHeader>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="nc-deployments-content">
|
||||
<div v-if="isLoading" class="nc-deployments-loading">
|
||||
<a-spin size="large" />
|
||||
<div class="text-sm text-nc-content-gray-muted mt-3">Loading deployments...</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="deployments.length > 0">
|
||||
<div class="nc-deployments-list">
|
||||
<div v-for="deployment in deployments" :key="deployment.baseId" class="nc-deployment-item">
|
||||
<!-- Deployment Row -->
|
||||
<div class="nc-deployment-row" @click="toggleExpand(deployment)">
|
||||
<div class="nc-deployment-info">
|
||||
<!-- Expand Icon -->
|
||||
<div class="nc-expand-icon">
|
||||
<GeneralIcon
|
||||
:icon="expandedBaseId === deployment.baseId ? 'ncChevronDown' : 'ncChevronRight'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Details -->
|
||||
<div class="nc-deployment-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="nc-deployment-details">
|
||||
<div class="nc-deployment-title">{{ deployment.baseTitle }}</div>
|
||||
<div class="nc-deployment-date">
|
||||
<GeneralIcon icon="calendar" class="w-3.5 h-3.5 opacity-60" />
|
||||
<span>Installed {{ formatDate(deployment.installedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="nc-deployment-status">
|
||||
<div class="nc-status-badge" :class="getStatusColor(deployment.status)">
|
||||
<span>{{ deployment.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Logs Section -->
|
||||
<div v-if="expandedBaseId === deployment.baseId" class="nc-deployment-logs-wrapper">
|
||||
<div class="nc-deployment-logs">
|
||||
<div class="nc-logs-header">
|
||||
<GeneralIcon icon="ncFileText" class="w-4 h-4 text-nc-content-gray-subtle2" />
|
||||
<span>Deployment History</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingLogs" class="nc-logs-loading">
|
||||
<a-spin size="small" />
|
||||
<span class="text-xs text-nc-content-gray-muted ml-2">Loading history...</span>
|
||||
</div>
|
||||
|
||||
<template v-else-if="deploymentLogs.length > 0">
|
||||
<div class="nc-logs-list">
|
||||
<div v-for="log in deploymentLogs" :key="log.id" class="nc-log-item">
|
||||
<div class="nc-log-content">
|
||||
<div class="nc-log-header">
|
||||
<div class="nc-log-badges">
|
||||
<div class="nc-log-status" :class="getStatusColor(log.status)">
|
||||
{{ log.status }}
|
||||
</div>
|
||||
<div class="nc-log-type">
|
||||
<GeneralIcon
|
||||
:icon="log.deploymentType === 'install' ? 'download' : 'reload'"
|
||||
class="w-3 h-3 opacity-60"
|
||||
/>
|
||||
<span>{{ getDeploymentTypeLabel(log.deploymentType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-log-meta">
|
||||
<div class="nc-log-version">
|
||||
<div v-if="log.fromVersion" class="flex items-center gap-1.5">
|
||||
<span class="font-mono text-nc-content-gray-subtle2">v{{ log.fromVersion.version }}</span>
|
||||
<GeneralIcon icon="arrowRight" class="w-3 h-3 text-nc-content-gray-subtle2" />
|
||||
</div>
|
||||
<span class="font-mono font-semibold text-nc-content-brand">v{{ log.toVersion?.version }}</span>
|
||||
</div>
|
||||
<span class="nc-log-divider">•</span>
|
||||
<div class="nc-log-time">
|
||||
<GeneralIcon icon="ncClock" class="w-3.5 h-3.5 opacity-60" />
|
||||
<span>{{ formatDate(log.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="log.errorMessage" class="nc-log-error">
|
||||
<GeneralIcon icon="alertTriangle" class="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>{{ log.errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Pagination -->
|
||||
<div v-if="logsPageInfo.totalRows > logsPageSize" class="nc-logs-pagination">
|
||||
<a-pagination
|
||||
v-model:current="logsCurrentPage"
|
||||
:total="logsPageInfo.totalRows"
|
||||
:page-size="logsPageSize"
|
||||
:show-size-changer="false"
|
||||
size="small"
|
||||
@change="handleLogsPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="nc-logs-empty">
|
||||
<GeneralIcon icon="inbox" class="w-8 h-8 text-nc-content-gray-subtle2 mb-2" />
|
||||
<div class="text-sm text-nc-content-gray-subtle2">No deployment history available</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="nc-deployments-empty">
|
||||
<div class="nc-empty-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-10 h-10 text-nc-content-gray-muted" />
|
||||
</div>
|
||||
<div class="text-base font-semibold text-nc-content-gray mb-1">No installations found</div>
|
||||
<div class="text-sm text-nc-content-gray-subtle max-w-md text-center">
|
||||
Version <span class="font-mono font-semibold">v{{ version?.version }}</span> hasn't been installed by any users yet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Deployments Pagination -->
|
||||
<div v-if="deployments.length > 0 && pageInfo.totalRows > pageSize" class="nc-deployments-pagination">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="pageInfo.totalRows"
|
||||
:page-size="pageSize"
|
||||
:show-size-changer="false"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-deployments-content {
|
||||
@apply flex-1 nc-scrollbar-thin p-6;
|
||||
}
|
||||
|
||||
.nc-deployments-loading {
|
||||
@apply h-full flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-deployments-list {
|
||||
@apply space-y-3;
|
||||
}
|
||||
|
||||
.nc-deployment-item {
|
||||
@apply bg-nc-bg-default border-1 border-nc-border-gray-medium rounded-xl overflow-hidden relative;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
|
||||
&::before {
|
||||
@apply absolute left-0 top-0 bottom-0 w-1 bg-nc-content-brand opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply border-nc-border-brand transform translate-x-0.5;
|
||||
box-shadow: 0 4px 12px rgba(51, 102, 255, 0.08);
|
||||
|
||||
&::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.nc-deployment-icon {
|
||||
@apply transform scale-105;
|
||||
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nc-deployment-row {
|
||||
@apply flex items-center justify-between gap-4 p-4 cursor-pointer;
|
||||
}
|
||||
|
||||
.nc-deployment-info {
|
||||
@apply flex items-center gap-3 flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-expand-icon {
|
||||
@apply w-6 h-6 flex items-center justify-center text-nc-content-gray-subtle2 flex-shrink-0;
|
||||
@apply transition-transform duration-200;
|
||||
}
|
||||
|
||||
.nc-deployment-icon {
|
||||
@apply w-9 h-9 rounded-lg bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
|
||||
@apply flex items-center justify-center text-nc-content-gray flex-shrink-0;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.nc-deployment-details {
|
||||
@apply flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-deployment-title {
|
||||
@apply font-semibold text-sm text-nc-content-gray-emphasis truncate mb-1;
|
||||
}
|
||||
|
||||
.nc-deployment-date {
|
||||
@apply flex items-center gap-1.5 text-xs text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.nc-deployment-status {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
|
||||
.nc-status-badge {
|
||||
@apply inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-deployment-logs-wrapper {
|
||||
@apply border-t-1 border-nc-border-gray-medium bg-nc-bg-gray-extralight;
|
||||
}
|
||||
|
||||
.nc-deployment-logs {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
.nc-logs-header {
|
||||
@apply flex items-center gap-2 text-xs font-semibold text-nc-content-gray-emphasis mb-3;
|
||||
@apply uppercase tracking-wide;
|
||||
}
|
||||
|
||||
.nc-logs-loading {
|
||||
@apply flex items-center justify-center py-8;
|
||||
}
|
||||
|
||||
.nc-logs-list {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.nc-log-item {
|
||||
@apply bg-nc-bg-default border-1 border-nc-border-gray-light rounded-lg p-3;
|
||||
@apply transition-all duration-150;
|
||||
|
||||
&:hover {
|
||||
@apply border-nc-border-gray-medium shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-log-content {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.nc-log-header {
|
||||
@apply flex items-center justify-between gap-2;
|
||||
}
|
||||
|
||||
.nc-log-badges {
|
||||
@apply flex items-center gap-2 flex-wrap;
|
||||
}
|
||||
|
||||
.nc-log-status {
|
||||
@apply inline-flex px-2 py-0.5 rounded text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-log-type {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded;
|
||||
@apply bg-nc-bg-gray-light text-nc-content-gray text-xs font-medium;
|
||||
}
|
||||
|
||||
.nc-log-meta {
|
||||
@apply flex items-center gap-2 text-xs text-nc-content-gray-subtle2 flex-wrap;
|
||||
}
|
||||
|
||||
.nc-log-version {
|
||||
@apply flex items-center gap-1.5;
|
||||
}
|
||||
|
||||
.nc-log-divider {
|
||||
@apply text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.nc-log-time {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.nc-log-error {
|
||||
@apply flex items-start gap-2 p-2 rounded-lg;
|
||||
@apply bg-nc-bg-red-light text-nc-content-red-dark text-xs;
|
||||
}
|
||||
|
||||
.nc-logs-pagination {
|
||||
@apply flex justify-center mt-4 pt-4 border-t-1 border-nc-border-gray-light;
|
||||
}
|
||||
|
||||
.nc-logs-empty {
|
||||
@apply flex flex-col items-center justify-center py-12 text-center;
|
||||
}
|
||||
|
||||
.nc-deployments-pagination {
|
||||
@apply flex justify-center mt-6;
|
||||
}
|
||||
|
||||
.nc-deployments-empty {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-empty-icon {
|
||||
@apply w-16 h-16 rounded-full bg-nc-bg-gray-light;
|
||||
@apply flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 640px) {
|
||||
.nc-deployment-row {
|
||||
@apply flex-col items-start;
|
||||
}
|
||||
|
||||
.nc-deployment-info {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.nc-deployment-status {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.nc-log-meta {
|
||||
@apply flex-col items-start gap-1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
344
packages/nc-gui/components/dlg/ManagedApp/VersionHistory.vue
Normal file
344
packages/nc-gui/components/dlg/ManagedApp/VersionHistory.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const emits = defineEmits(['update:visible'])
|
||||
|
||||
const vVisible = useVModel(props, 'visible', emits)
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const baseStore = useBase()
|
||||
|
||||
const { base, managedApp } = storeToRefs(baseStore)
|
||||
|
||||
const isLoadingDeployments = ref(true)
|
||||
|
||||
const deploymentStats = ref<any>(null)
|
||||
|
||||
// Version deployments modal
|
||||
const showVersionDeploymentsModal = ref(false)
|
||||
const selectedVersion = ref<any>(null)
|
||||
|
||||
// Load real deployment statistics
|
||||
const loadDeployments = async () => {
|
||||
if (!managedApp.value?.id || !base.value?.fk_workspace_id) return
|
||||
|
||||
isLoadingDeployments.value = true
|
||||
try {
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppDeployments',
|
||||
managedAppId: managedApp.value.id,
|
||||
} as any)
|
||||
if (response) {
|
||||
deploymentStats.value = response
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load deployments:', e)
|
||||
} finally {
|
||||
isLoadingDeployments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
|
||||
return parseStringDateTime(dateString, 'MMM DD, YYYY, hh:mm A')
|
||||
}
|
||||
|
||||
const openVersionDeploymentsModal = (version: any) => {
|
||||
selectedVersion.value = version
|
||||
showVersionDeploymentsModal.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
vVisible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadDeployments()
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<DlgManagedAppHeader v-model:visible="vVisible" title="Version History" subTitle="Manage versions and track deployments" />
|
||||
|
||||
<div class="flex-1 nc-scrollbar-thin">
|
||||
<div
|
||||
class="nc-deployments-content"
|
||||
:class="{
|
||||
'h-full': isLoadingDeployments,
|
||||
}"
|
||||
>
|
||||
<div v-if="isLoadingDeployments" class="nc-deployments-loading">
|
||||
<a-spin size="large" />
|
||||
<div class="text-sm text-nc-content-gray-muted mt-3">Loading deployment statistics...</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="deploymentStats">
|
||||
<!-- Stats Cards -->
|
||||
<div class="nc-deployment-stats">
|
||||
<!-- Total Deployments -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Total Installs</div>
|
||||
</div>
|
||||
|
||||
<!-- Active -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value text-green-600">{{ deploymentStats.statistics?.activeDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Active</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value text-red-600">{{ deploymentStats.statistics?.failedDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Failed</div>
|
||||
</div>
|
||||
|
||||
<!-- Versions -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalVersions || 0 }}</div>
|
||||
<div class="nc-stat-label">Versions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version List -->
|
||||
<div v-if="deploymentStats.versionStats && deploymentStats.versionStats.length > 0" class="nc-version-list-wrapper">
|
||||
<div class="nc-version-list-header">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-nc-content-gray-emphasis">Version History</h3>
|
||||
<p class="text-xs text-nc-content-gray-subtle2 mt-0.5 mb-0">
|
||||
{{ deploymentStats.versionStats.length }}
|
||||
{{ deploymentStats.versionStats.length === 1 ? 'version' : 'versions' }}
|
||||
published
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-version-list">
|
||||
<div
|
||||
v-for="(versionStat, index) in deploymentStats.versionStats"
|
||||
:key="versionStat.versionId"
|
||||
class="nc-version-item"
|
||||
:class="{ 'nc-version-item-clickable': versionStat.deploymentCount > 0 }"
|
||||
@click="versionStat.deploymentCount > 0 && openVersionDeploymentsModal(versionStat)"
|
||||
>
|
||||
<div class="nc-version-info">
|
||||
<div class="nc-version-icon">
|
||||
<GeneralIcon icon="ncGitBranch" class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="nc-version-details">
|
||||
<div class="nc-version-title">
|
||||
<span class="nc-version-number">v{{ versionStat.version }}</span>
|
||||
<div v-if="index === 0 && versionStat.status === 'published'" class="nc-version-badge">
|
||||
<GeneralIcon icon="check" class="w-3 h-3" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nc-version-date">Published {{ formatDate(versionStat.publishedAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-version-installs">
|
||||
<div class="nc-installs-count">
|
||||
<GeneralIcon icon="download" class="w-4 h-4 text-nc-content-gray-subtle2" />
|
||||
<span class="font-bold">{{ versionStat.deploymentCount }}</span>
|
||||
<span class="text-nc-content-gray-muted">{{
|
||||
versionStat.deploymentCount === 1 ? 'install' : 'installs'
|
||||
}}</span>
|
||||
</div>
|
||||
<GeneralIcon
|
||||
v-if="versionStat.deploymentCount > 0"
|
||||
icon="chevronRight"
|
||||
class="w-4 h-4 text-nc-content-gray-subtle2 nc-chevron-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="nc-deployments-empty">
|
||||
<div class="nc-empty-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-10 h-10 text-nc-content-gray-muted" />
|
||||
</div>
|
||||
<div class="text-base font-semibold text-nc-content-gray mb-1">No installations yet</div>
|
||||
<div class="text-sm text-nc-content-gray-subtle max-w-md text-center">
|
||||
Once users install your application from the App Store, their deployments will appear here.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="nc-deployments-error">
|
||||
<div class="nc-error-icon">
|
||||
<GeneralIcon icon="alertTriangle" class="w-10 h-10 text-nc-content-red-dark" />
|
||||
</div>
|
||||
<div class="text-base font-semibold text-nc-content-gray mb-1">Failed to load statistics</div>
|
||||
<div class="text-sm text-nc-content-gray-subtle mb-4">There was an error loading deployment data</div>
|
||||
<NcButton size="small" type="secondary" @click="loadDeployments">
|
||||
<template #icon>
|
||||
<GeneralIcon icon="reload" />
|
||||
</template>
|
||||
Retry
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DlgManagedApp v-model:visible="showVersionDeploymentsModal" modal-size="md">
|
||||
<DlgManagedAppVersionDeployments v-model:visible="showVersionDeploymentsModal" :version="selectedVersion" />
|
||||
</DlgManagedApp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Deployments Tab Styles
|
||||
.nc-deployments-content {
|
||||
@apply px-6 pb-6;
|
||||
}
|
||||
|
||||
.nc-deployments-loading {
|
||||
@apply h-full flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-deployment-stats {
|
||||
@apply grid grid-cols-4 mb-4;
|
||||
}
|
||||
|
||||
.nc-stat-card {
|
||||
@apply flex flex-col gap-1 border-r-1 border-nc-border-gray-medium last:border-r-0 p-4 text-nc-content-gray-subtle text-center;
|
||||
}
|
||||
|
||||
.nc-stat-icon-wrapper {
|
||||
@apply w-10 h-10 rounded-lg flex items-center justify-center mb-3;
|
||||
}
|
||||
|
||||
.nc-stat-value {
|
||||
@apply text-subHeading1 font-normal;
|
||||
}
|
||||
|
||||
.nc-stat-label {
|
||||
@apply text-tiny text-nc-content-gray-muted uppercase;
|
||||
}
|
||||
|
||||
.nc-version-list-wrapper {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
.nc-version-list-header {
|
||||
@apply flex items-center justify-between mb-4;
|
||||
}
|
||||
|
||||
.nc-version-list {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.nc-version-item {
|
||||
@apply bg-nc-bg-default border-1 border-nc-border-gray-medium rounded-xl p-4 flex items-center justify-between gap-4 relative overflow-hidden;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
|
||||
&::before {
|
||||
@apply absolute left-0 top-0 bottom-0 w-1 bg-nc-content-brand opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&.nc-version-item-clickable {
|
||||
@apply cursor-pointer;
|
||||
|
||||
&:hover {
|
||||
@apply border-nc-border-brand transform translate-x-0.5;
|
||||
box-shadow: 0 4px 12px rgba(51, 102, 255, 0.08);
|
||||
|
||||
&::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.nc-version-icon {
|
||||
@apply transform scale-105;
|
||||
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.nc-chevron-icon {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-chevron-icon {
|
||||
@apply opacity-0 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.nc-version-info {
|
||||
@apply flex items-center gap-3 flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-version-icon {
|
||||
@apply w-9 h-9 rounded-lg bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
|
||||
@apply flex items-center justify-center text-nc-content-gray flex-shrink-0;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.nc-version-details {
|
||||
@apply flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-version-title {
|
||||
@apply flex items-center gap-2 mb-1;
|
||||
}
|
||||
|
||||
.nc-version-number {
|
||||
@apply font-mono font-bold text-base text-nc-content-gray-emphasis;
|
||||
}
|
||||
|
||||
.nc-version-badge {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full;
|
||||
@apply bg-nc-bg-green-light text-nc-content-green-dark;
|
||||
@apply text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-version-date {
|
||||
@apply text-xs text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.nc-version-installs {
|
||||
@apply flex items-center gap-3;
|
||||
}
|
||||
|
||||
.nc-installs-count {
|
||||
@apply flex items-center gap-2 text-sm;
|
||||
@apply px-3 py-1.5 rounded-lg bg-nc-bg-gray-light;
|
||||
}
|
||||
|
||||
.nc-deployments-empty {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-empty-icon {
|
||||
@apply w-16 h-16 rounded-full bg-nc-bg-gray-light;
|
||||
@apply flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
.nc-deployments-error {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-error-icon {
|
||||
@apply w-16 h-16 rounded-full bg-nc-bg-red-light;
|
||||
@apply flex items-center justify-center mb-4;
|
||||
}
|
||||
</style>
|
||||
@@ -1,654 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
managedApp: any
|
||||
currentVersion: any
|
||||
initialTab?: 'publish' | 'fork' | 'deployments'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'published', 'forked'])
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const baseStore = useBase()
|
||||
const { base } = storeToRefs(baseStore)
|
||||
|
||||
const activeTab = ref('publish')
|
||||
const isLoading = ref(false)
|
||||
const isLoadingDeployments = ref(false)
|
||||
const versions = ref<any[]>([])
|
||||
const deploymentStats = ref<any>(null)
|
||||
|
||||
// Version deployments modal
|
||||
const showVersionDeploymentsModal = ref(false)
|
||||
const selectedVersion = ref<any>(null)
|
||||
|
||||
// Publish form (for draft versions)
|
||||
const publishForm = reactive({
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
// Fork form (for creating new draft from published)
|
||||
const forkForm = reactive({
|
||||
version: '',
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
const isPublished = computed(() => props.currentVersion?.status === 'published')
|
||||
const isDraft = computed(() => props.currentVersion?.status === 'draft')
|
||||
|
||||
// Load versions
|
||||
const loadVersions = async () => {
|
||||
if (!props.managedApp?.id || !base.value?.fk_workspace_id) return
|
||||
|
||||
try {
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppVersionsList',
|
||||
managedAppId: props.managedApp.id,
|
||||
} as any)
|
||||
if (response?.list) {
|
||||
versions.value = response.list
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load versions:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Load real deployment statistics
|
||||
const loadDeployments = async () => {
|
||||
if (!props.managedApp?.id || !base.value?.fk_workspace_id) return
|
||||
|
||||
isLoadingDeployments.value = true
|
||||
try {
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppDeployments',
|
||||
managedAppId: props.managedApp.id,
|
||||
} as any)
|
||||
if (response) {
|
||||
deploymentStats.value = response
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load deployments:', e)
|
||||
} finally {
|
||||
isLoadingDeployments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const publishCurrentDraft = async () => {
|
||||
if (!base.value?.fk_workspace_id || !base.value?.id || !props.currentVersion?.id) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
await $api.internal.postOperation(
|
||||
base.value.fk_workspace_id,
|
||||
base.value.id,
|
||||
{
|
||||
operation: 'managedAppPublish',
|
||||
},
|
||||
{
|
||||
managedAppVersionId: props.currentVersion.id,
|
||||
},
|
||||
)
|
||||
|
||||
// Reload base to get updated managed app version info
|
||||
if (base.value?.id) {
|
||||
await baseStore.loadProject()
|
||||
}
|
||||
|
||||
message.success(`Version ${props.currentVersion.version} published successfully!`)
|
||||
emit('published')
|
||||
emit('update:visible', false)
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createNewDraft = async () => {
|
||||
if (!base.value?.fk_workspace_id || !base.value?.id || !props.managedApp?.id) return
|
||||
if (!forkForm.version) {
|
||||
message.error('Please provide a version')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
await $api.internal.postOperation(
|
||||
base.value.fk_workspace_id,
|
||||
base.value.id,
|
||||
{
|
||||
operation: 'managedAppCreateDraft',
|
||||
},
|
||||
{
|
||||
managedAppId: props.managedApp.id,
|
||||
version: forkForm.version,
|
||||
},
|
||||
)
|
||||
|
||||
// Reload base to get updated managed app version info
|
||||
if (base.value?.id) {
|
||||
await baseStore.loadProject()
|
||||
}
|
||||
|
||||
message.success(`New draft version ${forkForm.version} created successfully!`)
|
||||
emit('forked')
|
||||
emit('update:visible', false)
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
|
||||
return parseStringDateTime(dateString, 'MMM DD, YYYY, HH:mm A')
|
||||
}
|
||||
|
||||
const openVersionDeploymentsModal = (version: any) => {
|
||||
selectedVersion.value = version
|
||||
showVersionDeploymentsModal.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
// Use initialTab if provided, otherwise default based on version status
|
||||
if (props.initialTab) {
|
||||
activeTab.value = props.initialTab
|
||||
} else if (isDraft.value) {
|
||||
activeTab.value = 'publish'
|
||||
} else if (isPublished.value) {
|
||||
activeTab.value = 'fork'
|
||||
} else {
|
||||
// Fallback to deployments if version status is unclear
|
||||
activeTab.value = 'deployments'
|
||||
}
|
||||
|
||||
await loadVersions()
|
||||
await loadDeployments()
|
||||
if (!isDraft.value) {
|
||||
forkForm.version = suggestManagedAppNextVersion(props.currentVersion?.version)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const modalSize = computed(() => {
|
||||
if (props.initialTab === 'fork' || props.initialTab === 'publish') {
|
||||
return 'sm'
|
||||
}
|
||||
|
||||
return 'sm'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcModal
|
||||
:visible="visible"
|
||||
:size="modalSize"
|
||||
:height="modalSize === 'sm' ? 'auto' : undefined"
|
||||
nc-modal-class-name="nc-modal-managed-app-management"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header with Tabs -->
|
||||
<div class="nc-managed-app-header">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<div class="nc-managed-app-icon">
|
||||
<GeneralIcon icon="ncBox" class="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-lg font-semibold text-nc-content-gray-emphasis">Managed App Management</div>
|
||||
<div class="text-xs text-nc-content-gray-subtle2">Manage versions and track deployments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NcButton size="small" type="text" @click="emit('update:visible', false)">
|
||||
<GeneralIcon icon="close" class="text-nc-content-gray-muted h-4 w-4" />
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 nc-scrollbar-thin">
|
||||
<!-- Publish Tab -->
|
||||
<div v-if="activeTab === 'publish'" class="p-6">
|
||||
<NcAlert
|
||||
type="info"
|
||||
align="top"
|
||||
class="!p-3 !items-start bg-nc-bg-blue-light border-1 !border-nc-blue-200 rounded-lg p-3 mb-4"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="info" class="w-4 h-4 mt-0.5 text-nc-content-blue-dark flex-none" />
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Publishing version <strong>{{ currentVersion?.version }}</strong> will make it available in the App Store and
|
||||
automatically update all installations.
|
||||
</template>
|
||||
</NcAlert>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-nc-content-gray text-sm font-medium mb-2 block">Version</label>
|
||||
<a-input :value="currentVersion?.version" disabled size="large" class="rounded-lg nc-input-sm">
|
||||
<template #prefix>
|
||||
<span class="text-nc-content-gray-subtle2">v</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-nc-content-gray text-sm font-medium mb-2 block">Release Notes (Optional)</label>
|
||||
<a-textarea
|
||||
v-model:value="publishForm.releaseNotes"
|
||||
placeholder="Describe what's new in this version"
|
||||
:rows="6"
|
||||
size="large"
|
||||
class="rounded-lg nc-input-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fork Tab -->
|
||||
<div v-if="activeTab === 'fork'" class="p-6">
|
||||
<NcAlert
|
||||
type="info"
|
||||
align="top"
|
||||
class="!p-3 !items-start bg-nc-bg-blue-light border-1 !border-nc-blue-200 rounded-lg p-3 mb-4"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="info" class="w-4 h-4 mt-0.5 text-nc-content-blue-dark flex-none" />
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Create a new draft version to work on updates. Current published version
|
||||
<strong>{{ currentVersion?.version }}</strong> will remain unchanged.
|
||||
</template>
|
||||
</NcAlert>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-nc-content-gray text-sm font-medium mb-2 block">
|
||||
New Version <span class="text-nc-content-red-dark">*</span>
|
||||
</label>
|
||||
<a-input
|
||||
v-model:value="forkForm.version"
|
||||
placeholder="e.g., 2.0.0"
|
||||
size="large"
|
||||
class="rounded-lg nc-input-sm nc-input-shadow"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="text-nc-content-gray-subtle2">v</span>
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="text-xs text-nc-content-gray-subtle2 mt-1.5">Use semantic versioning (e.g., 2.0.0, 2.1.0)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployments Tab -->
|
||||
<div v-if="activeTab === 'deployments'" class="nc-deployments-content">
|
||||
<div v-if="isLoadingDeployments" class="nc-deployments-loading">
|
||||
<a-spin size="large" />
|
||||
<div class="text-sm text-nc-content-gray-muted mt-3">Loading deployment statistics...</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="deploymentStats">
|
||||
<!-- Stats Cards -->
|
||||
<div class="nc-deployment-stats">
|
||||
<!-- Total Deployments -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Total Installs</div>
|
||||
</div>
|
||||
|
||||
<!-- Active -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value text-green-600">{{ deploymentStats.statistics?.activeDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Active</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value text-red-600">{{ deploymentStats.statistics?.failedDeployments || 0 }}</div>
|
||||
<div class="nc-stat-label">Failed</div>
|
||||
</div>
|
||||
|
||||
<!-- Versions -->
|
||||
<div class="nc-stat-card">
|
||||
<div class="nc-stat-value">{{ deploymentStats.statistics?.totalVersions || 0 }}</div>
|
||||
<div class="nc-stat-label">Versions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version List -->
|
||||
<div v-if="deploymentStats.versionStats && deploymentStats.versionStats.length > 0" class="nc-version-list-wrapper">
|
||||
<div class="nc-version-list-header">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-nc-content-gray-emphasis">Version History</h3>
|
||||
<p class="text-xs text-nc-content-gray-subtle2 mt-0.5">
|
||||
{{ deploymentStats.versionStats.length }}
|
||||
{{ deploymentStats.versionStats.length === 1 ? 'version' : 'versions' }}
|
||||
published
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-version-list">
|
||||
<div
|
||||
v-for="(versionStat, index) in deploymentStats.versionStats"
|
||||
:key="versionStat.versionId"
|
||||
class="nc-version-item"
|
||||
:class="{ 'nc-version-item-clickable': versionStat.deploymentCount > 0 }"
|
||||
@click="versionStat.deploymentCount > 0 && openVersionDeploymentsModal(versionStat)"
|
||||
>
|
||||
<div class="nc-version-info">
|
||||
<div class="nc-version-icon">
|
||||
<GeneralIcon icon="ncGitBranch" class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="nc-version-details">
|
||||
<div class="nc-version-title">
|
||||
<span class="nc-version-number">v{{ versionStat.version }}</span>
|
||||
<div v-if="index === 0 && versionStat.status === 'published'" class="nc-version-badge">
|
||||
<GeneralIcon icon="check" class="w-3 h-3" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nc-version-date">Published {{ formatDate(versionStat.publishedAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-version-installs">
|
||||
<div class="nc-installs-count">
|
||||
<GeneralIcon icon="download" class="w-4 h-4 text-nc-content-gray-subtle2" />
|
||||
<span class="font-bold">{{ versionStat.deploymentCount }}</span>
|
||||
<span class="text-nc-content-gray-muted">{{
|
||||
versionStat.deploymentCount === 1 ? 'install' : 'installs'
|
||||
}}</span>
|
||||
</div>
|
||||
<GeneralIcon
|
||||
v-if="versionStat.deploymentCount > 0"
|
||||
icon="chevronRight"
|
||||
class="w-4 h-4 text-nc-content-gray-subtle2 nc-chevron-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="nc-deployments-empty">
|
||||
<div class="nc-empty-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-10 h-10 text-nc-content-gray-muted" />
|
||||
</div>
|
||||
<div class="text-base font-semibold text-nc-content-gray mb-1">No installations yet</div>
|
||||
<div class="text-sm text-nc-content-gray-subtle max-w-md text-center">
|
||||
Once users install your application from the App Store, their deployments will appear here.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="nc-deployments-error">
|
||||
<div class="nc-error-icon">
|
||||
<GeneralIcon icon="alertTriangle" class="w-10 h-10 text-nc-content-red-dark" />
|
||||
</div>
|
||||
<div class="text-base font-semibold text-nc-content-gray mb-1">Failed to load statistics</div>
|
||||
<div class="text-sm text-nc-content-gray-subtle mb-4">There was an error loading deployment data</div>
|
||||
<NcButton size="small" type="secondary" @click="loadDeployments">
|
||||
<template #icon>
|
||||
<GeneralIcon icon="reload" />
|
||||
</template>
|
||||
Retry
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="activeTab === 'publish' || activeTab === 'fork'" class="nc-managed-app-footer">
|
||||
<div class="flex justify-end gap-2">
|
||||
<NcButton type="secondary" size="small" @click="emit('update:visible', false)"> Cancel </NcButton>
|
||||
|
||||
<NcButton v-if="activeTab === 'publish'" type="primary" size="small" :loading="isLoading" @click="publishCurrentDraft">
|
||||
<template #icon>
|
||||
<GeneralIcon icon="upload" />
|
||||
</template>
|
||||
Publish
|
||||
</NcButton>
|
||||
|
||||
<NcButton
|
||||
v-if="activeTab === 'fork'"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="isLoading"
|
||||
:disabled="!forkForm.version"
|
||||
@click="createNewDraft"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="plus" />
|
||||
</template>
|
||||
Create Draft
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Deployments Modal -->
|
||||
<SmartsheetTopbarManagedAppVersionDeploymentsModal
|
||||
v-model:visible="showVersionDeploymentsModal"
|
||||
:managed-app="managedApp"
|
||||
:version="selectedVersion"
|
||||
/>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-managed-app-header {
|
||||
@apply flex items-center gap-4 px-4 py-3 border-b-1 border-nc-border-gray-medium;
|
||||
}
|
||||
|
||||
.nc-managed-app-icon {
|
||||
@apply w-10 h-10 rounded-xl flex items-center justify-center;
|
||||
background: linear-gradient(135deg, var(--nc-content-brand) 0%, var(--nc-content-blue-medium) 100%);
|
||||
box-shadow: 0 2px 4px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.nc-managed-app-tabs {
|
||||
@apply flex bg-nc-bg-gray-medium rounded-lg p-1;
|
||||
}
|
||||
|
||||
.nc-managed-app-tab {
|
||||
@apply px-3 py-1.5 flex items-center gap-2 text-xs rounded-md select-none cursor-pointer;
|
||||
@apply text-nc-content-gray-subtle2 transition-all duration-200;
|
||||
|
||||
&.selected {
|
||||
@apply bg-nc-bg-default text-nc-content-gray-emphasis;
|
||||
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
@apply text-nc-content-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-managed-app-footer {
|
||||
@apply px-6 py-3 border-t-1 border-nc-border-gray-medium;
|
||||
}
|
||||
|
||||
// Deployments Tab Styles
|
||||
.nc-deployments-content {
|
||||
@apply px-6 pb-6;
|
||||
}
|
||||
|
||||
.nc-deployments-loading {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-deployment-stats {
|
||||
@apply grid grid-cols-4 mb-6;
|
||||
}
|
||||
|
||||
.nc-stat-card {
|
||||
@apply flex flex-col gap-1 items-center justify-center border-r-1 border-nc-border-gray-medium last:border-r-0 p-4 text-nc-content-gray-subtle;
|
||||
}
|
||||
|
||||
.nc-stat-icon-wrapper {
|
||||
@apply w-10 h-10 rounded-lg flex items-center justify-center mb-3;
|
||||
}
|
||||
|
||||
.nc-stat-value {
|
||||
@apply text-subHeading1 font-normal;
|
||||
}
|
||||
|
||||
.nc-stat-label {
|
||||
@apply text-tiny text-nc-content-gray-muted uppercase;
|
||||
}
|
||||
|
||||
.nc-version-list-wrapper {
|
||||
@apply mt-6;
|
||||
}
|
||||
|
||||
.nc-version-list-header {
|
||||
@apply flex items-center justify-between mb-4;
|
||||
}
|
||||
|
||||
.nc-version-list {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.nc-version-item {
|
||||
@apply bg-nc-bg-default border-1 border-nc-border-gray-medium rounded-xl p-4 flex items-center justify-between gap-4 relative overflow-hidden;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
|
||||
&::before {
|
||||
@apply absolute left-0 top-0 bottom-0 w-1 bg-nc-content-brand opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&.nc-version-item-clickable {
|
||||
@apply cursor-pointer;
|
||||
|
||||
&:hover {
|
||||
@apply border-nc-border-brand transform translate-x-0.5;
|
||||
box-shadow: 0 4px 12px rgba(51, 102, 255, 0.08);
|
||||
|
||||
&::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.nc-version-icon {
|
||||
@apply transform scale-105;
|
||||
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.nc-chevron-icon {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-chevron-icon {
|
||||
@apply opacity-0 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.nc-version-info {
|
||||
@apply flex items-center gap-3 flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-version-icon {
|
||||
@apply w-9 h-9 rounded-lg bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
|
||||
@apply flex items-center justify-center text-nc-content-gray flex-shrink-0;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.nc-version-details {
|
||||
@apply flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-version-title {
|
||||
@apply flex items-center gap-2 mb-1;
|
||||
}
|
||||
|
||||
.nc-version-number {
|
||||
@apply font-mono font-bold text-base text-nc-content-gray-emphasis;
|
||||
}
|
||||
|
||||
.nc-version-badge {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full;
|
||||
@apply bg-nc-bg-green-light text-nc-content-green-dark;
|
||||
@apply text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-version-date {
|
||||
@apply text-xs text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.nc-version-installs {
|
||||
@apply flex items-center gap-3;
|
||||
}
|
||||
|
||||
.nc-installs-count {
|
||||
@apply flex items-center gap-2 text-sm;
|
||||
@apply px-3 py-1.5 rounded-lg bg-nc-bg-gray-light;
|
||||
}
|
||||
|
||||
.nc-deployments-empty {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-empty-icon {
|
||||
@apply w-16 h-16 rounded-full bg-nc-bg-gray-light;
|
||||
@apply flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
.nc-deployments-error {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-error-icon {
|
||||
@apply w-16 h-16 rounded-full bg-nc-bg-red-light;
|
||||
@apply flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 1024px) {
|
||||
.nc-deployment-stats {
|
||||
@apply grid-cols-2 gap-3;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nc-deployment-stats {
|
||||
@apply grid-cols-1;
|
||||
}
|
||||
|
||||
.nc-version-item {
|
||||
@apply flex-col items-start;
|
||||
}
|
||||
|
||||
.nc-version-installs {
|
||||
@apply w-full justify-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-managed-app-management {
|
||||
@apply !p-0;
|
||||
|
||||
&.nc-modal-size-sm {
|
||||
max-height: min(90vh, 540px) !important;
|
||||
height: min(90vh, 540px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,16 +10,16 @@ const { base, isManagedAppMaster, isManagedAppInstaller, managedApp, currentVers
|
||||
|
||||
const isModalVisible = ref(false)
|
||||
|
||||
const initialTab = ref<'publish' | 'fork' | 'deployments' | undefined>(undefined)
|
||||
const modalVariant = ref<'draftOrPublish' | 'versionHistory' | undefined>(undefined)
|
||||
|
||||
const isOpenDropdown = ref<boolean>(false)
|
||||
|
||||
const isDraft = computed(() => managedAppVersionsInfo.value.current?.status === 'draft')
|
||||
|
||||
const openModal = (tab?: 'publish' | 'fork' | 'deployments') => {
|
||||
const openModal = (variant?: 'draftOrPublish' | 'versionHistory') => {
|
||||
isOpenDropdown.value = false
|
||||
|
||||
initialTab.value = tab
|
||||
modalVariant.value = variant
|
||||
|
||||
nextTick(() => {
|
||||
isModalVisible.value = true
|
||||
@@ -31,14 +31,6 @@ const loadManagedAppAndCurrentVersion = async () => {
|
||||
await loadCurrentVersion()
|
||||
}
|
||||
|
||||
const handlePublished = async () => {
|
||||
await loadManagedAppAndCurrentVersion()
|
||||
}
|
||||
|
||||
const handleForked = async () => {
|
||||
await loadManagedAppAndCurrentVersion()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => (base.value as any)?.managed_app_id,
|
||||
async (managedAppId) => {
|
||||
@@ -158,7 +150,7 @@ const badgeConfig = computed(() => {
|
||||
managedAppVersionsInfo.published.version || '1.0.0',
|
||||
)} to make changes`"
|
||||
icon-wrapper-class="bg-nc-bg-gray-light dakr:bg-nc-bg-gray-light/75"
|
||||
@click="openModal('fork')"
|
||||
@click="openModal('draftOrPublish')"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="ncCopy" class="text-nc-content-gray-muted" />
|
||||
@@ -184,7 +176,7 @@ const badgeConfig = computed(() => {
|
||||
<SmartsheetTopbarManagedAppStatusMenuItem
|
||||
clickable
|
||||
icon-wrapper-class="bg-green-50 dark:bg-nc-green-20"
|
||||
@click="openModal('publish')"
|
||||
@click="openModal('draftOrPublish')"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="ncArrowUp" class="text-green-600" />
|
||||
@@ -219,7 +211,7 @@ const badgeConfig = computed(() => {
|
||||
<!-- Version history -->
|
||||
<div
|
||||
class="flex items-center gap-2 px-5 py-2 text-captionSm text-nc-content-gray-muted cursor-pointer select-none"
|
||||
@click="openModal('deployments')"
|
||||
@click="openModal('versionHistory')"
|
||||
>
|
||||
<GeneralIcon icon="ncClock" />
|
||||
View version history
|
||||
@@ -238,7 +230,7 @@ const badgeConfig = computed(() => {
|
||||
|
||||
<template v-if="managedAppVersionsInfo.current?.published_at" #subtext>
|
||||
<span class="text-green-600">
|
||||
Published {{ parseStringDateTime(managedAppVersionsInfo.current?.published_at, 'MMM DD, YYYY, HH:mm A') }}
|
||||
Published {{ parseStringDateTime(managedAppVersionsInfo.current?.published_at, 'MMM DD, YYYY, hh:mm A') }}
|
||||
</span>
|
||||
</template>
|
||||
</SmartsheetTopbarManagedAppStatusMenuItem>
|
||||
@@ -280,19 +272,7 @@ const badgeConfig = computed(() => {
|
||||
</template>
|
||||
</NcDropdown>
|
||||
|
||||
<DlgManagedApp v-if="initialTab !== 'deployments'" v-model:visible="isModalVisible" modal-size="sm" variant="draftOrPublish">
|
||||
</DlgManagedApp>
|
||||
|
||||
<!-- Managed App Modal -->
|
||||
<SmartsheetTopbarManagedAppModal
|
||||
v-if="initialTab === 'deployments'"
|
||||
v-model:visible="isModalVisible"
|
||||
:managed-app="managedApp"
|
||||
:current-version="managedAppVersionsInfo.current"
|
||||
:initial-tab="initialTab"
|
||||
@published="handlePublished"
|
||||
@forked="handleForked"
|
||||
/>
|
||||
<DlgManagedApp v-model:visible="isModalVisible" modal-size="sm" :variant="modalVariant"> </DlgManagedApp>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,513 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { DeploymentStatus } from 'nocodb-sdk'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
managedApp: any
|
||||
version: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const { base } = storeToRefs(useBase())
|
||||
|
||||
const isLoading = ref(false)
|
||||
const deployments = ref<any[]>([])
|
||||
const pageInfo = ref<any>({})
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
// Expanded deployment logs state
|
||||
const expandedBaseId = ref<string | null>(null)
|
||||
const deploymentLogs = ref<any[]>([])
|
||||
const logsPageInfo = ref<any>({})
|
||||
const logsCurrentPage = ref(1)
|
||||
const logsPageSize = 10
|
||||
const isLoadingLogs = ref(false)
|
||||
|
||||
const loadDeployments = async (page = 1) => {
|
||||
if (!props.managedApp?.id || !props.version?.versionId || !base.value?.fk_workspace_id) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const offset = (page - 1) * pageSize
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppVersionDeployments',
|
||||
managedAppId: props.managedApp.id,
|
||||
versionId: props.version.versionId,
|
||||
limit: pageSize,
|
||||
offset,
|
||||
} as any)
|
||||
|
||||
if (response) {
|
||||
deployments.value = response.list || []
|
||||
pageInfo.value = response.pageInfo || {}
|
||||
currentPage.value = page
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadDeploymentLogs = async (baseId: string, page = 1) => {
|
||||
if (!base.value?.fk_workspace_id) return
|
||||
|
||||
isLoadingLogs.value = true
|
||||
try {
|
||||
const offset = (page - 1) * logsPageSize
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
operation: 'managedAppDeploymentLogs',
|
||||
baseId,
|
||||
limit: logsPageSize,
|
||||
offset,
|
||||
} as any)
|
||||
|
||||
if (response) {
|
||||
deploymentLogs.value = response.logs || []
|
||||
logsPageInfo.value = response.pageInfo || {}
|
||||
logsCurrentPage.value = page
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
} finally {
|
||||
isLoadingLogs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExpand = async (deployment: any) => {
|
||||
if (expandedBaseId.value === deployment.baseId) {
|
||||
// Collapse
|
||||
expandedBaseId.value = null
|
||||
deploymentLogs.value = []
|
||||
logsCurrentPage.value = 1
|
||||
} else {
|
||||
// Expand and load logs
|
||||
expandedBaseId.value = deployment.baseId
|
||||
await loadDeploymentLogs(deployment.baseId, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
const date = new Date(dateString)
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
const getDeploymentTypeLabel = (type: string) => {
|
||||
const labels = {
|
||||
install: 'Initial Install',
|
||||
update: 'Update',
|
||||
}
|
||||
return labels[type as keyof typeof labels] || type
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
[DeploymentStatus.SUCCESS]: 'text-nc-content-green-dark bg-nc-bg-green-light',
|
||||
[DeploymentStatus.FAILED]: 'text-nc-content-red-dark bg-nc-bg-red-light',
|
||||
[DeploymentStatus.PENDING]: 'text-nc-content-orange-dark bg-nc-bg-orange-light',
|
||||
[DeploymentStatus.IN_PROGRESS]: 'text-nc-content-blue-dark bg-nc-bg-blue-light',
|
||||
}
|
||||
return colors[status as keyof typeof colors] || 'text-nc-content-gray bg-nc-bg-gray-light'
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
loadDeployments(page)
|
||||
}
|
||||
|
||||
const handleLogsPageChange = (page: number) => {
|
||||
if (expandedBaseId.value) {
|
||||
loadDeploymentLogs(expandedBaseId.value, page)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadDeployments(1)
|
||||
expandedBaseId.value = null
|
||||
deploymentLogs.value = []
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcModal :visible="visible" size="lg" nc-modal-class-name="!p-0" @update:visible="emit('update:visible', $event)">
|
||||
<div class="nc-deployments-modal">
|
||||
<!-- Header -->
|
||||
<div class="nc-deployments-header">
|
||||
<div class="nc-deployments-header-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-base font-semibold text-nc-content-gray-emphasis m-0">Version Deployments</h2>
|
||||
<p class="text-xs text-nc-content-gray-subtle2 m-0 mt-0.5">
|
||||
Tracking installations for
|
||||
<span class="font-mono font-semibold text-nc-content-brand">v{{ version?.version }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<NcButton type="text" size="small" class="!px-1" @click="emit('update:visible', false)">
|
||||
<GeneralIcon icon="close" class="w-4 h-4" />
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="nc-deployments-content">
|
||||
<div v-if="isLoading" class="nc-deployments-loading">
|
||||
<a-spin size="large" />
|
||||
<div class="text-sm text-nc-content-gray-muted mt-3">Loading deployments...</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="deployments.length > 0">
|
||||
<div class="nc-deployments-list">
|
||||
<div v-for="deployment in deployments" :key="deployment.baseId" class="nc-deployment-item">
|
||||
<!-- Deployment Row -->
|
||||
<div class="nc-deployment-row" @click="toggleExpand(deployment)">
|
||||
<div class="nc-deployment-info">
|
||||
<!-- Expand Icon -->
|
||||
<div class="nc-expand-icon">
|
||||
<GeneralIcon
|
||||
:icon="expandedBaseId === deployment.baseId ? 'ncChevronDown' : 'ncChevronRight'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Details -->
|
||||
<div class="nc-deployment-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="nc-deployment-details">
|
||||
<div class="nc-deployment-title">{{ deployment.baseTitle }}</div>
|
||||
<div class="nc-deployment-date">
|
||||
<GeneralIcon icon="calendar" class="w-3.5 h-3.5 opacity-60" />
|
||||
<span>Installed {{ formatDate(deployment.installedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="nc-deployment-status">
|
||||
<div class="nc-status-badge" :class="getStatusColor(deployment.status)">
|
||||
<span>{{ deployment.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Logs Section -->
|
||||
<div v-if="expandedBaseId === deployment.baseId" class="nc-deployment-logs-wrapper">
|
||||
<div class="nc-deployment-logs">
|
||||
<div class="nc-logs-header">
|
||||
<GeneralIcon icon="ncFileText" class="w-4 h-4 text-nc-content-gray-subtle2" />
|
||||
<span>Deployment History</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingLogs" class="nc-logs-loading">
|
||||
<a-spin size="small" />
|
||||
<span class="text-xs text-nc-content-gray-muted ml-2">Loading history...</span>
|
||||
</div>
|
||||
|
||||
<template v-else-if="deploymentLogs.length > 0">
|
||||
<div class="nc-logs-list">
|
||||
<div v-for="log in deploymentLogs" :key="log.id" class="nc-log-item">
|
||||
<div class="nc-log-content">
|
||||
<div class="nc-log-header">
|
||||
<div class="nc-log-badges">
|
||||
<div class="nc-log-status" :class="getStatusColor(log.status)">
|
||||
{{ log.status }}
|
||||
</div>
|
||||
<div class="nc-log-type">
|
||||
<GeneralIcon
|
||||
:icon="log.deploymentType === 'install' ? 'download' : 'reload'"
|
||||
class="w-3 h-3 opacity-60"
|
||||
/>
|
||||
<span>{{ getDeploymentTypeLabel(log.deploymentType) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-log-meta">
|
||||
<div class="nc-log-version">
|
||||
<div v-if="log.fromVersion" class="flex items-center gap-1.5">
|
||||
<span class="font-mono text-nc-content-gray-subtle2">v{{ log.fromVersion.version }}</span>
|
||||
<GeneralIcon icon="arrowRight" class="w-3 h-3 text-nc-content-gray-subtle2" />
|
||||
</div>
|
||||
<span class="font-mono font-semibold text-nc-content-brand">v{{ log.toVersion?.version }}</span>
|
||||
</div>
|
||||
<span class="nc-log-divider">•</span>
|
||||
<div class="nc-log-time">
|
||||
<GeneralIcon icon="ncClock" class="w-3.5 h-3.5 opacity-60" />
|
||||
<span>{{ formatDate(log.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="log.errorMessage" class="nc-log-error">
|
||||
<GeneralIcon icon="alertTriangle" class="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>{{ log.errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Pagination -->
|
||||
<div v-if="logsPageInfo.totalRows > logsPageSize" class="nc-logs-pagination">
|
||||
<a-pagination
|
||||
v-model:current="logsCurrentPage"
|
||||
:total="logsPageInfo.totalRows"
|
||||
:page-size="logsPageSize"
|
||||
:show-size-changer="false"
|
||||
size="small"
|
||||
@change="handleLogsPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="nc-logs-empty">
|
||||
<GeneralIcon icon="inbox" class="w-8 h-8 text-nc-content-gray-subtle2 mb-2" />
|
||||
<div class="text-sm text-nc-content-gray-subtle2">No deployment history available</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployments Pagination -->
|
||||
<div v-if="pageInfo.totalRows > pageSize" class="nc-deployments-pagination">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="pageInfo.totalRows"
|
||||
:page-size="pageSize"
|
||||
:show-size-changer="false"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="nc-deployments-empty">
|
||||
<div class="nc-empty-icon">
|
||||
<GeneralIcon icon="ncServer" class="w-10 h-10 text-nc-content-gray-muted" />
|
||||
</div>
|
||||
<div class="text-base font-semibold text-nc-content-gray mb-1">No installations found</div>
|
||||
<div class="text-sm text-nc-content-gray-subtle max-w-md text-center">
|
||||
Version <span class="font-mono font-semibold">v{{ version?.version }}</span> hasn't been installed by any users yet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-deployments-modal {
|
||||
@apply flex flex-col h-full max-h-[80vh];
|
||||
}
|
||||
|
||||
.nc-deployments-header {
|
||||
@apply flex items-center gap-3 px-4 py-3 border-b-1 border-nc-border-gray-medium;
|
||||
}
|
||||
|
||||
.nc-deployments-header-icon {
|
||||
@apply w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0;
|
||||
background: linear-gradient(135deg, var(--nc-content-brand) 0%, var(--nc-content-blue-medium) 100%);
|
||||
box-shadow: 0 2px 4px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.nc-deployments-content {
|
||||
@apply flex-1 overflow-y-auto p-6;
|
||||
}
|
||||
|
||||
.nc-deployments-loading {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-deployments-list {
|
||||
@apply space-y-3;
|
||||
}
|
||||
|
||||
.nc-deployment-item {
|
||||
@apply bg-nc-bg-default border-1 border-nc-border-gray-medium rounded-xl overflow-hidden relative;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
|
||||
&::before {
|
||||
@apply absolute left-0 top-0 bottom-0 w-1 bg-nc-content-brand opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply border-nc-border-brand transform translate-x-0.5;
|
||||
box-shadow: 0 4px 12px rgba(51, 102, 255, 0.08);
|
||||
|
||||
&::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.nc-deployment-icon {
|
||||
@apply transform scale-105;
|
||||
box-shadow: 0 4px 8px rgba(51, 102, 255, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nc-deployment-row {
|
||||
@apply flex items-center justify-between gap-4 p-4 cursor-pointer;
|
||||
}
|
||||
|
||||
.nc-deployment-info {
|
||||
@apply flex items-center gap-3 flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-expand-icon {
|
||||
@apply w-6 h-6 flex items-center justify-center text-nc-content-gray-subtle2 flex-shrink-0;
|
||||
@apply transition-transform duration-200;
|
||||
}
|
||||
|
||||
.nc-deployment-icon {
|
||||
@apply w-9 h-9 rounded-lg bg-nc-bg-gray-light border-1 border-nc-border-gray-light;
|
||||
@apply flex items-center justify-center text-nc-content-gray flex-shrink-0;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.nc-deployment-details {
|
||||
@apply flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.nc-deployment-title {
|
||||
@apply font-semibold text-sm text-nc-content-gray-emphasis truncate mb-1;
|
||||
}
|
||||
|
||||
.nc-deployment-date {
|
||||
@apply flex items-center gap-1.5 text-xs text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.nc-deployment-status {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
|
||||
.nc-status-badge {
|
||||
@apply inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-deployment-logs-wrapper {
|
||||
@apply border-t-1 border-nc-border-gray-medium bg-nc-bg-gray-extralight;
|
||||
}
|
||||
|
||||
.nc-deployment-logs {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
.nc-logs-header {
|
||||
@apply flex items-center gap-2 text-xs font-semibold text-nc-content-gray-emphasis mb-3;
|
||||
@apply uppercase tracking-wide;
|
||||
}
|
||||
|
||||
.nc-logs-loading {
|
||||
@apply flex items-center justify-center py-8;
|
||||
}
|
||||
|
||||
.nc-logs-list {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.nc-log-item {
|
||||
@apply bg-nc-bg-default border-1 border-nc-border-gray-light rounded-lg p-3;
|
||||
@apply transition-all duration-150;
|
||||
|
||||
&:hover {
|
||||
@apply border-nc-border-gray-medium shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-log-content {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.nc-log-header {
|
||||
@apply flex items-center justify-between gap-2;
|
||||
}
|
||||
|
||||
.nc-log-badges {
|
||||
@apply flex items-center gap-2 flex-wrap;
|
||||
}
|
||||
|
||||
.nc-log-status {
|
||||
@apply inline-flex px-2 py-0.5 rounded text-xs font-semibold;
|
||||
}
|
||||
|
||||
.nc-log-type {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded;
|
||||
@apply bg-nc-bg-gray-light text-nc-content-gray text-xs font-medium;
|
||||
}
|
||||
|
||||
.nc-log-meta {
|
||||
@apply flex items-center gap-2 text-xs text-nc-content-gray-subtle2 flex-wrap;
|
||||
}
|
||||
|
||||
.nc-log-version {
|
||||
@apply flex items-center gap-1.5;
|
||||
}
|
||||
|
||||
.nc-log-divider {
|
||||
@apply text-nc-content-gray-subtle2;
|
||||
}
|
||||
|
||||
.nc-log-time {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.nc-log-error {
|
||||
@apply flex items-start gap-2 p-2 rounded-lg;
|
||||
@apply bg-nc-bg-red-light text-nc-content-red-dark text-xs;
|
||||
}
|
||||
|
||||
.nc-logs-pagination {
|
||||
@apply flex justify-center mt-4 pt-4 border-t-1 border-nc-border-gray-light;
|
||||
}
|
||||
|
||||
.nc-logs-empty {
|
||||
@apply flex flex-col items-center justify-center py-12 text-center;
|
||||
}
|
||||
|
||||
.nc-deployments-pagination {
|
||||
@apply flex justify-center mt-6;
|
||||
}
|
||||
|
||||
.nc-deployments-empty {
|
||||
@apply flex flex-col items-center justify-center py-16;
|
||||
}
|
||||
|
||||
.nc-empty-icon {
|
||||
@apply w-16 h-16 rounded-full bg-nc-bg-gray-light;
|
||||
@apply flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 640px) {
|
||||
.nc-deployment-row {
|
||||
@apply flex-col items-start;
|
||||
}
|
||||
|
||||
.nc-deployment-info {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.nc-deployment-status {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.nc-log-meta {
|
||||
@apply flex-col items-start gap-1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user