Merge pull request #12922 from nocodb/nc-feat/sandbox-to-managed

Nc feat/sandbox to managed
This commit is contained in:
Ramesh Mane
2026-01-23 14:45:25 +05:30
committed by GitHub
25 changed files with 542 additions and 264 deletions

View File

@@ -369,10 +369,10 @@ const duplicateProject = (base: BaseType) => {
isDuplicateDlgOpen.value = true
}
const isConvertToSandboxDlgOpen = ref(false)
const isConvertToManagedAppDlgOpen = ref(false)
const convertToSandbox = () => {
isConvertToSandboxDlgOpen.value = true
const convertToManagedApp = () => {
isConvertToManagedAppDlgOpen.value = true
}
const tableDelete = () => {
@@ -619,7 +619,7 @@ defineExpose({
@click-menu="onClickMenu"
@rename="enableEditMode()"
@duplicate-project="duplicateProject($event)"
@convert-to-sandbox="convertToSandbox"
@convert-to-managed-app="convertToManagedApp"
@copy-project-info="copyProjectInfo()"
@open-erd-view="openErdView($event)"
@open-base-settings="openBaseSettings($event)"
@@ -680,7 +680,7 @@ defineExpose({
@click-menu="onClickMenu"
@rename="enableEditMode(true)"
@duplicate-project="duplicateProject($event)"
@convert-to-sandbox="convertToSandbox"
@convert-to-managed-app="convertToManagedApp"
@copy-project-info="copyProjectInfo()"
@open-erd-view="openErdView($event)"
@open-base-settings="openBaseSettings($event)"
@@ -765,7 +765,7 @@ defineExpose({
/>
<DlgBaseDelete v-model:visible="isBaseDeleteDialogVisible" :base-id="base?.id" />
<DlgBaseDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<DlgConvertToSandbox v-if="base?.id" v-model:visible="isConvertToSandboxDlgOpen" :base-id="base.id" />
<DlgConvertToManagedApp v-if="base?.id" v-model:visible="isConvertToManagedAppDlgOpen" :base-id="base.id" />
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :base-id="base?.id" :source-id="activeBaseId" />

View File

@@ -11,7 +11,7 @@ const visible = defineModel<boolean>('visible', { required: true })
const { $api } = useNuxtApp()
const { t } = useI18n()
const initialSanboxFormState = ref<Record<string, any>>({
const initialManagedAppFormState = ref<Record<string, any>>({
title: '',
description: '',
category: '',
@@ -22,13 +22,13 @@ const { base } = storeToRefs(useBase())
const basesStore = useBases()
const convertToSandbox = async (formState: Record<string, any>) => {
const convertToManagedApp = async (formState: Record<string, any>) => {
try {
const response = await $api.internal.postOperation(
base.value!.fk_workspace_id as string,
props.baseId,
{
operation: 'sandboxCreate',
operation: 'managedAppCreate',
} as any,
{
title: formState.title,
@@ -38,18 +38,18 @@ const convertToSandbox = async (formState: Record<string, any>) => {
},
)
message.success(t('msg.success.sandboxCreated'))
message.success(t('msg.success.managedAppCreated'))
visible.value = false
// Update the base with the sandbox_id from response
if (response && response.sandbox_id) {
// Update the base with the managed_app_id from response
if (response && response.managed_app_id) {
const currentBase = basesStore.bases.get(props.baseId)
if (currentBase) {
;(currentBase as any).sandbox_id = response.sandbox_id
;(currentBase as any).managed_app_id = response.managed_app_id
}
}
// Reload base to ensure all sandbox data is loaded
// Reload base to ensure all managed app data is loaded
await basesStore.loadProject(props.baseId, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@@ -60,7 +60,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
formSchema: [
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxTitle'),
label: t('labels.managedAppTitle'),
span: 24,
model: 'title',
placeholder: 'Enter a descriptive title',
@@ -75,7 +75,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Textarea,
label: t('labels.sandboxDescription'),
label: t('labels.managedAppDescription'),
span: 24,
model: 'description',
placeholder: "Describe your application's capabilities",
@@ -83,7 +83,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxCategory'),
label: t('labels.managedAppCategory'),
span: 12,
model: 'category',
placeholder: 'e.g., CRM, HR',
@@ -91,7 +91,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Select,
label: t('labels.sandboxVisibility'),
label: t('labels.managedAppVisibility'),
span: 12,
model: 'visibility',
category: FORM_BUILDER_NON_CATEGORIZED,
@@ -104,9 +104,9 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
],
onSubmit: async () => {
return await convertToSandbox(formState.value)
return await convertToManagedApp(formState.value)
},
initialState: initialSanboxFormState,
initialState: initialManagedAppFormState,
})
watch(visible, (isVisible) => {
@@ -127,7 +127,7 @@ watch(visible, (isVisible) => {
size="sm"
height="auto"
centered
wrap-class-name="nc-modal-convert-to-sandbox "
wrap-class-name="nc-modal-convert-to-managed-app "
nc-modal-class-name="!p-0"
>
<div class="p-4 w-full flex items-center gap-3 border-b border-nc-border-gray-medium">
@@ -135,7 +135,7 @@ watch(visible, (isVisible) => {
<GeneralIcon icon="ncBox" class="w-5 h-5 text-white" />
</div>
<div class="flex-1">
<div class="font-semibold text-lg text-nc-content-gray-emphasis">Convert to Sandbox</div>
<div class="font-semibold text-lg text-nc-content-gray-emphasis">Convert to Managed App</div>
<div class="text-xs text-nc-content-gray-subtle2">{{ $t('labels.publishToAppStore') }}</div>
</div>
@@ -169,14 +169,14 @@ watch(visible, (isVisible) => {
<template #icon>
<GeneralIcon icon="ncBox" />
</template>
Convert to sandbox
Convert to Managed App
</NcButton>
</div>
</NcModal>
</template>
<style lang="scss">
.nc-modal-convert-to-sandbox {
.nc-modal-convert-to-managed-app {
.nc-modal {
max-height: min(90vh, 540px) !important;
height: min(90vh, 540px) !important;

View File

@@ -12,7 +12,7 @@ const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore())
const { base, isSharedBase, isSandboxMaster } = storeToRefs(useBase())
const { base, isSharedBase, isManagedAppMaster } = storeToRefs(useBase())
const { hideSharedBaseBtn } = storeToRefs(useConfigStore())
@@ -46,7 +46,7 @@ const copySharedBase = async () => {
<template>
<div
v-if="!isSharedBase && !isSandboxMaster && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
v-if="!isSharedBase && !isManagedAppMaster && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
class="nc-share-base-button flex flex-col justify-center"
data-testid="share-base-button"
:data-sharetype="visibility"

View File

@@ -232,7 +232,7 @@ onMounted(() => {
</div>
</div>
<div v-if="!showEmptySkeleton && !isMobileMode" class="flex items-center gap-2">
<SmartsheetTopbarSandboxStatus />
<SmartsheetTopbarManagedAppStatus />
<LazyGeneralShareProject />
</div>
</div>

View File

@@ -72,8 +72,8 @@ const topbarBreadcrumbItemWidth = computed(() => {
<div class="flex items-center justify-end gap-2 flex-1">
<GeneralApiLoader v-if="!isMobileMode && !activeScriptId && !activeDashboardId" />
<!-- Sandbox Status -->
<LazySmartsheetTopbarSandboxStatus v-if="!isSharedBase && !isMobileMode" />
<!-- Managed App Status -->
<LazySmartsheetTopbarManagedAppStatus v-if="!isSharedBase && !isMobileMode" />
<NcButton
v-if="

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
const props = defineProps<{
visible: boolean
sandbox: any
managedApp: any
currentVersion: any
initialTab?: 'publish' | 'fork' | 'deployments'
}>()
@@ -38,12 +38,12 @@ const isDraft = computed(() => props.currentVersion?.status === 'draft')
// Load versions
const loadVersions = async () => {
if (!props.sandbox?.id || !base.value?.fk_workspace_id) return
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: 'sandboxVersionsList',
sandboxId: props.sandbox.id,
operation: 'managedAppVersionsList',
managedAppId: props.managedApp.id,
} as any)
if (response?.list) {
versions.value = response.list
@@ -55,13 +55,13 @@ const loadVersions = async () => {
// Load real deployment statistics
const loadDeployments = async () => {
if (!props.sandbox?.id || !base.value?.fk_workspace_id) return
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: 'sandboxDeployments',
sandboxId: props.sandbox.id,
operation: 'managedAppDeployments',
managedAppId: props.managedApp.id,
} as any)
if (response) {
deploymentStats.value = response
@@ -82,14 +82,14 @@ const publishCurrentDraft = async () => {
base.value.fk_workspace_id,
base.value.id,
{
operation: 'sandboxPublish',
operation: 'managedAppPublish',
},
{
sandboxVersionId: props.currentVersion.id,
managedAppVersionId: props.currentVersion.id,
},
)
// Reload base to get updated sandbox version info
// Reload base to get updated managed app version info
if (base.value?.id) {
await baseStore.loadProject()
}
@@ -105,7 +105,7 @@ const publishCurrentDraft = async () => {
}
const createNewDraft = async () => {
if (!base.value?.fk_workspace_id || !base.value?.id || !props.sandbox?.id) return
if (!base.value?.fk_workspace_id || !base.value?.id || !props.managedApp?.id) return
if (!forkForm.version) {
message.error('Please provide a version')
return
@@ -117,15 +117,15 @@ const createNewDraft = async () => {
base.value.fk_workspace_id,
base.value.id,
{
operation: 'sandboxCreateDraft',
operation: 'managedAppCreateDraft',
},
{
sandboxId: props.sandbox.id,
managedAppId: props.managedApp.id,
version: forkForm.version,
},
)
// Reload base to get updated sandbox version info
// Reload base to get updated managed app version info
if (base.value?.id) {
await baseStore.loadProject()
}
@@ -205,29 +205,29 @@ watch(
<NcModal
:visible="visible"
size="lg"
nc-modal-class-name="nc-modal-sandbox-management"
nc-modal-class-name="nc-modal-managed-app-management"
centered
@update:visible="emit('update:visible', $event)"
>
<div class="flex flex-col h-full">
<!-- Header with Tabs -->
<div class="nc-sandbox-header">
<div class="nc-managed-app-header">
<div class="flex items-center gap-3 flex-1">
<div class="nc-sandbox-icon">
<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">Sandbox Management</div>
<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>
<!-- Tabs (Segmented Control) -->
<div class="nc-sandbox-tabs">
<div class="nc-managed-app-tabs">
<div class="flex items-center">
<div
v-if="isDraft"
class="nc-sandbox-tab"
class="nc-managed-app-tab"
:class="{ selected: activeTab === 'publish' }"
@click="activeTab = 'publish'"
>
@@ -236,14 +236,14 @@ watch(
</div>
<div
v-if="isPublished"
class="nc-sandbox-tab"
class="nc-managed-app-tab"
:class="{ selected: activeTab === 'fork' }"
@click="activeTab = 'fork'"
>
<GeneralIcon icon="ncGitBranch" class="h-4 w-4 flex-none opacity-75" />
<span>Fork</span>
</div>
<div class="nc-sandbox-tab" :class="{ selected: activeTab === 'deployments' }" @click="activeTab = 'deployments'">
<div class="nc-managed-app-tab" :class="{ selected: activeTab === 'deployments' }" @click="activeTab = 'deployments'">
<GeneralIcon icon="ncServer" class="h-4 w-4 flex-none opacity-75" />
<span>Deployments</span>
</div>
@@ -473,7 +473,7 @@ watch(
</div>
<!-- Footer -->
<div v-if="activeTab === 'publish' || activeTab === 'fork'" class="nc-sandbox-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>
@@ -502,30 +502,30 @@ watch(
</div>
<!-- Version Deployments Modal -->
<SmartsheetTopbarSandboxVersionDeploymentsModal
<SmartsheetTopbarManagedAppVersionDeploymentsModal
v-model:visible="showVersionDeploymentsModal"
:sandbox="sandbox"
:managed-app="managedApp"
:version="selectedVersion"
/>
</NcModal>
</template>
<style lang="scss" scoped>
.nc-sandbox-header {
.nc-managed-app-header {
@apply flex items-center gap-4 px-4 py-3 border-b-1 border-nc-border-gray-medium;
}
.nc-sandbox-icon {
.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-sandbox-tabs {
.nc-managed-app-tabs {
@apply flex bg-nc-bg-gray-medium rounded-lg p-1;
}
.nc-sandbox-tab {
.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;
@@ -539,7 +539,7 @@ watch(
}
}
.nc-sandbox-footer {
.nc-managed-app-footer {
@apply px-6 py-3 border-t-1 border-nc-border-gray-medium;
}
@@ -711,7 +711,7 @@ watch(
</style>
<style lang="scss">
.nc-modal-sandbox-management {
.nc-modal-managed-app-management {
@apply !p-0;
}
</style>

View File

@@ -1,44 +1,42 @@
<script setup lang="ts">
const { base } = storeToRefs(useBase())
const { base, isManagedAppMaster } = storeToRefs(useBase())
const { $api } = useNuxtApp()
const isModalVisible = ref(false)
const initialTab = ref<'publish' | 'fork' | 'deployments' | undefined>(undefined)
const sandbox = ref<any>(null)
const managedApp = ref<any>(null)
const currentVersion = ref<any>(null)
const isSandboxMaster = computed(() => !!(base.value as any)?.sandbox_master && !!(base.value as any)?.sandbox_id)
// Load sandbox info and current version
const loadSandbox = async () => {
if (!(base.value as any)?.sandbox_id || !base.value?.fk_workspace_id) return
// Load managed app info and current version
const loadManagedApp = async () => {
if (!(base.value as any)?.managed_app_id || !base.value?.fk_workspace_id) return
try {
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxGet',
operation: 'managedAppGet',
baseId: base.value.id,
} as any)
if (response) {
sandbox.value = response
managedApp.value = response
}
} catch (e) {
console.error('Failed to load sandbox:', e)
console.error('Failed to load managed app:', e)
}
}
// Load current version info
const loadCurrentVersion = async () => {
if (!base.value?.sandbox_version_id || !base.value?.fk_workspace_id) return
if (!base.value?.managed_app_version_id || !base.value?.fk_workspace_id) return
try {
// Get version details from versions list
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxVersionsList',
sandboxId: (base.value as any).sandbox_id,
operation: 'managedAppVersionsList',
managedAppId: (base.value as any).managed_app_id,
} as any)
if (response?.list) {
currentVersion.value = response.list.find((v: any) => v.id === base.value.sandbox_version_id)
currentVersion.value = response.list.find((v: any) => v.id === base.value.managed_app_version_id)
}
} catch (e) {
console.error('Failed to load current version:', e)
@@ -51,20 +49,20 @@ const openModal = (tab?: 'publish' | 'fork' | 'deployments') => {
}
const handlePublished = async () => {
await loadSandbox()
await loadManagedApp()
await loadCurrentVersion()
}
const handleForked = async () => {
await loadSandbox()
await loadManagedApp()
await loadCurrentVersion()
}
watch(
() => (base.value as any)?.sandbox_id,
async (sandboxId) => {
if (sandboxId) {
await loadSandbox()
() => (base.value as any)?.managed_app_id,
async (managedAppId) => {
if (managedAppId) {
await loadManagedApp()
await loadCurrentVersion()
}
},
@@ -73,13 +71,13 @@ watch(
</script>
<template>
<div v-if="isSandboxMaster" class="flex items-center gap-2">
<div v-if="isManagedAppMaster" class="flex items-center gap-2">
<!-- Version Badge (clickable to open modal) -->
<div
class="flex items-center gap-1.5 px-2.5 py-1 bg-nc-bg-gray-light rounded-md border-1 border-nc-border-gray-medium cursor-pointer hover:(bg-nc-bg-gray-medium border-nc-border-gray-dark) transition-colors"
@click="openModal()"
>
<GeneralIcon icon="ncInfoSolid" class="w-3.5 h-3.5 text-nc-content-gray nc-sanbox-status-info-icon" />
<GeneralIcon icon="ncInfoSolid" class="w-3.5 h-3.5 text-nc-content-gray nc-managed-app-status-info-icon" />
<span class="text-xs font-mono font-semibold text-nc-content-gray-emphasis">v{{ currentVersion?.version || '1.0.0' }}</span>
<div
v-if="currentVersion?.status === 'draft'"
@@ -96,10 +94,10 @@ watch(
</div>
</div>
<!-- Sandbox Modal -->
<SmartsheetTopbarSandboxModal
<!-- Managed App Modal -->
<SmartsheetTopbarManagedAppModal
v-model:visible="isModalVisible"
:sandbox="sandbox"
:managed-app="managedApp"
:current-version="currentVersion"
:initial-tab="initialTab"
@published="handlePublished"
@@ -108,7 +106,7 @@ watch(
</template>
<style lang="scss" scoped>
:deep(.nc-sanbox-status-info-icon path.nc-icon-inner) {
:deep(.nc-managed-app-status-info-icon path.nc-icon-inner) {
stroke: var(--nc-bg-gray-light) !important;
}
</style>

View File

@@ -3,7 +3,7 @@ import { DeploymentStatus } from 'nocodb-sdk'
const props = defineProps<{
visible: boolean
sandbox: any
managedApp: any
version: any
}>()
@@ -27,14 +27,14 @@ const logsPageSize = 10
const isLoadingLogs = ref(false)
const loadDeployments = async (page = 1) => {
if (!props.sandbox?.id || !props.version?.versionId || !base.value?.fk_workspace_id) return
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: 'sandboxVersionDeployments',
sandboxId: props.sandbox.id,
operation: 'managedAppVersionDeployments',
managedAppId: props.managedApp.id,
versionId: props.version.versionId,
limit: pageSize,
offset,
@@ -59,7 +59,7 @@ const loadDeploymentLogs = async (baseId: string, page = 1) => {
try {
const offset = (page - 1) * logsPageSize
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
operation: 'sandboxDeploymentLogs',
operation: 'managedAppDeploymentLogs',
baseId,
limit: logsPageSize,
offset,

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
interface SandboxType {
interface ManagedAppType {
id: string
title: string
description?: string
@@ -22,7 +22,7 @@ const visible = useVModel(props, 'visible', emit)
const { $api } = useNuxtApp()
const { t } = useI18n()
const sandboxes = ref<SandboxType[]>([])
const managedApps = ref<ManagedAppType[]>([])
const loading = ref(false)
const installing = ref<string | null>(null)
const searchQuery = ref('')
@@ -30,10 +30,10 @@ const selectedCategory = ref<string | undefined>(undefined)
const categories = computed(() => {
const cats = new Set<string>()
sandboxes.value.forEach((sb) => {
if (sb.category) {
managedApps.value.forEach((ma) => {
if (ma.category) {
// Split comma-separated categories
sb.category.split(',').forEach((cat) => {
ma.category.split(',').forEach((cat) => {
const trimmed = cat.trim()
if (trimmed) cats.add(trimmed)
})
@@ -42,28 +42,28 @@ const categories = computed(() => {
return Array.from(cats).sort()
})
const filteredSandboxes = computed(() => {
let filtered = sandboxes.value
const filteredManagedApps = computed(() => {
let filtered = managedApps.value
if (selectedCategory.value) {
const selected = selectedCategory.value
filtered = filtered.filter((sb) => {
if (!sb.category) return false
filtered = filtered.filter((ma) => {
if (!ma.category) return false
// Check if selected category exists in comma-separated list
const categories = sb.category.split(',').map((c) => c.trim())
const categories = ma.category.split(',').map((c) => c.trim())
return categories.includes(selected)
})
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter((sb) => searchCompare([sb.title, sb.description, sb.category], query))
filtered = filtered.filter((ma) => searchCompare([ma.title, ma.description, ma.category], query))
}
return filtered
})
const loadSandboxes = async () => {
const loadManagedApps = async () => {
if (!props.workspaceId) {
console.error('WorkspaceId is required')
return
@@ -77,10 +77,10 @@ const loadSandboxes = async () => {
loading.value = true
try {
const response = await $api.internal.getOperation(props.workspaceId, NO_SCOPE, {
operation: 'sandboxStoreList',
operation: 'managedAppStoreList',
})
sandboxes.value = response?.list || []
managedApps.value = response?.list || []
} catch (e: any) {
console.error('API error:', e)
message.error(await extractSdkResponseErrorMsg(e))
@@ -89,23 +89,23 @@ const loadSandboxes = async () => {
}
}
const installSandbox = async (sandbox: SandboxType) => {
installing.value = sandbox.id
const installManagedApp = async (managedApp: ManagedAppType) => {
installing.value = managedApp.id
try {
await $api.internal.postOperation(
props.workspaceId,
NO_SCOPE,
{
operation: 'sandboxInstall',
operation: 'managedAppInstall',
},
{
sandboxId: sandbox.id,
managedAppId: managedApp.id,
target_workspace_id: props.workspaceId,
},
)
message.success(t('msg.success.baseInstalled'))
emit('installed', sandbox)
emit('installed', managedApp)
visible.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@@ -129,7 +129,7 @@ watch(
() => props.workspaceId,
(newVal) => {
if (newVal) {
loadSandboxes()
loadManagedApps()
}
},
{ immediate: true },
@@ -177,8 +177,8 @@ watch(
</div>
<!-- Results count -->
<div v-if="!loading && filteredSandboxes.length > 0" class="mt-3 text-xs text-nc-content-gray-muted">
{{ filteredSandboxes.length }} {{ filteredSandboxes.length === 1 ? 'app' : 'apps' }} available
<div v-if="!loading && filteredManagedApps.length > 0" class="mt-3 text-xs text-nc-content-gray-muted">
{{ filteredManagedApps.length }} {{ filteredManagedApps.length === 1 ? 'app' : 'apps' }} available
</div>
</div>
@@ -193,7 +193,7 @@ watch(
</div>
<!-- Empty State -->
<div v-else-if="filteredSandboxes.length === 0" class="nc-app-market-empty">
<div v-else-if="filteredManagedApps.length === 0" class="nc-app-market-empty">
<div class="nc-empty-icon">
<GeneralIcon icon="ncBox" class="h-10 w-10 text-nc-content-gray-muted" />
</div>
@@ -209,7 +209,7 @@ watch(
<!-- App List -->
<div v-else class="nc-app-market-list">
<div v-for="sandbox in filteredSandboxes" :key="sandbox.id" class="nc-app-item">
<div v-for="managedApp in filteredManagedApps" :key="managedApp.id" class="nc-app-item">
<div class="nc-app-item-content">
<!-- App Icon & Info -->
<div class="nc-app-info">
@@ -218,10 +218,10 @@ watch(
</div>
<div class="nc-app-details">
<div class="nc-app-title-row">
<h3 class="nc-app-title">{{ sandbox.title }}</h3>
<div v-if="sandbox.category" class="nc-app-categories">
<h3 class="nc-app-title">{{ managedApp.title }}</h3>
<div v-if="managedApp.category" class="nc-app-categories">
<div
v-for="cat in sandbox.category
v-for="cat in managedApp.category
.split(',')
.map((c) => c.trim())
.filter(Boolean)"
@@ -236,20 +236,20 @@ watch(
<p
class="nc-app-description"
:class="{
'!text-nc-content-gray-muted': !sandbox.description,
'!text-nc-content-gray-muted': !managedApp.description,
}"
>
{{ sandbox.description || 'No description available' }}
{{ managedApp.description || 'No description available' }}
</p>
<div class="nc-app-meta">
<span class="nc-app-meta-item">
<GeneralIcon icon="download" class="h-3.5 w-3.5" />
<span class="font-medium">{{ formatInstallCount(sandbox.install_count || 0) }}</span>
<span class="font-medium">{{ formatInstallCount(managedApp.install_count || 0) }}</span>
<span class="text-nc-content-gray-muted">installs</span>
</span>
<span v-if="sandbox.version" class="nc-app-meta-item">
<span v-if="managedApp.version" class="nc-app-meta-item">
<GeneralIcon icon="gitCommit" class="h-3.5 w-3.5" />
<span>v{{ sandbox.version }}</span>
<span>v{{ managedApp.version }}</span>
</span>
</div>
</div>
@@ -258,16 +258,16 @@ watch(
<!-- Install Button -->
<div class="nc-app-action">
<NcButton
:loading="installing === sandbox.id"
:loading="installing === managedApp.id"
:disabled="!!installing"
size="small"
type="primary"
@click="installSandbox(sandbox)"
@click="installManagedApp(managedApp)"
>
<template #icon>
<GeneralIcon icon="download" class="h-4 w-4" />
</template>
{{ installing === sandbox.id ? 'Installing...' : t('general.install') }}
{{ installing === managedApp.id ? 'Installing...' : t('general.install') }}
</NcButton>
</div>
</div>

View File

@@ -11,7 +11,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {})
const emit = defineEmits(['update:aiMode', 'update:mode', 'sandboxInstalled', 'close'])
const emit = defineEmits(['update:aiMode', 'update:mode', 'managedAppInstalled', 'close'])
const aiMode = useVModel(props, 'aiMode', emit)
@@ -48,9 +48,9 @@ const selectMode = (mode: CreateMode) => {
emit('update:mode', mode)
}
const onSandboxInstalled = (sandbox: any) => {
const onManagedAppInstalled = (managedApp: any) => {
showAppMarket.value = false
emit('sandboxInstalled', sandbox)
emit('managedAppInstalled', managedApp)
}
const onAppMarketClose = () => {
@@ -93,7 +93,7 @@ onMounted(() => {
</div>
</div>
<div
v-if="isFeatureEnabled(FEATURE_FLAG.SANDBOXES)"
v-if="isFeatureEnabled(FEATURE_FLAG.MANAGED_APPS)"
v-e="['c:base:market:create']"
class="nc-create-base-market"
@click="selectMode('market')"
@@ -115,7 +115,7 @@ onMounted(() => {
v-if="showAppMarket && workspaceId"
:workspace-id="workspaceId"
@close="onAppMarketClose"
@installed="onSandboxInstalled"
@installed="onManagedAppInstalled"
/>
</div>
</template>

View File

@@ -38,7 +38,7 @@ const createManagedApp = async (formState: Record<string, any>) => {
activeWorkspaceId.value as string,
formState.baseId || NO_SCOPE,
{
operation: 'sandboxCreate',
operation: 'managedAppCreate',
} as any,
{
title: formState.title,
@@ -59,18 +59,18 @@ const createManagedApp = async (formState: Record<string, any>) => {
},
)
message.success(t('msg.success.sandboxCreated'))
message.success(t('msg.success.managedAppCreated'))
visible.value = false
// Update the base with the sandbox_id from response
if (response && response.sandbox_id && formState.baseId) {
// Update the base with the managed_app_id from response
if (response && response.managed_app_id && formState.baseId) {
const currentBase = basesStore.bases.get(formState.baseId as string)
if (currentBase) {
;(currentBase as any).sandbox_id = response.sandbox_id
;(currentBase as any).managed_app_id = response.managed_app_id
}
}
// Reload base to ensure all sandbox data is loaded
// Reload base to ensure all managed app data is loaded
if (formState.baseId) {
await basesStore.loadProject(formState.baseId, true)
} else {
@@ -92,7 +92,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
formSchema: [
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxTitle'),
label: t('labels.managedAppTitle'),
span: 24,
model: 'title',
placeholder: 'Enter a descriptive title',
@@ -111,7 +111,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Textarea,
label: t('labels.sandboxDescription'),
label: t('labels.managedAppDescription'),
span: 24,
model: 'description',
placeholder: "Describe your application's capabilities",
@@ -149,11 +149,11 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
equal: 'existing',
},
defaultValue: undefined,
filterOption: (base) => base && !base?.sandbox_id,
filterOption: (base) => base && !base?.managed_app_id,
},
{
type: FormBuilderInputType.Input,
label: t('labels.sandboxCategory'),
label: t('labels.managedAppCategory'),
span: 12,
model: 'category',
placeholder: 'e.g., CRM, HR',
@@ -161,7 +161,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
},
{
type: FormBuilderInputType.Select,
label: t('labels.sandboxVisibility'),
label: t('labels.managedAppVisibility'),
span: 12,
model: 'visibility',
category: FORM_BUILDER_NON_CATEGORIZED,
@@ -234,7 +234,7 @@ const { formState, isLoading, submit } = useProvideFormBuilderHelper({
</template>
<style lang="scss">
.nc-modal-convert-to-sandbox {
.nc-modal-convert-to-managed-app {
.nc-modal {
max-height: min(90vh, 540px) !important;
height: min(90vh, 540px) !important;

View File

@@ -3,9 +3,9 @@ import rfdc from 'rfdc'
const deepClone = rfdc()
const FEATURES = [
{
id: 'sandboxes',
title: 'Sandboxes',
description: 'Allow users to create replicable sandbox environments',
id: 'managed_apps',
title: 'Managed Apps',
description: 'Allow users to create replicable managed app environments',
enabled: false,
isEngineering: true,
isAdvanced: true,

View File

@@ -1173,11 +1173,11 @@
"visibilityAndDataHandling": "Visibility & Data Handling",
"visibilityConfigLabel": "Base specific additional configurations to customise data display & default behaviours.",
"migrateToV3": "Migrate to V3",
"sandbox": "Sandbox",
"sandboxAppStore": "Sandbox App Store",
"managedApp": "Managed App",
"managedAppStore": "Managed App Store",
"baseMustBeV3": "Base must be V3",
"createSandbox": "Create Sandbox",
"sandboxDetails": "Sandbox Details",
"createManagedApp": "Create Managed App",
"managedAppDetails": "Managed App Details",
"publishToAppStore": "Publish to App Store",
"publishChanges": "Publish Changes",
"publishingChanges": "Publishing changes",
@@ -1185,11 +1185,11 @@
"updateDetails": "Update Details",
"published": "Published",
"draft": "Draft",
"sandboxTitle": "Title",
"sandboxDescription": "Description",
"sandboxCategory": "Category",
"sandboxTags": "Tags",
"sandboxVisibility": "Visibility",
"managedAppTitle": "Title",
"managedAppDescription": "Description",
"managedAppCategory": "Category",
"managedAppTags": "Tags",
"managedAppVisibility": "Visibility",
"snapShotSubText": "Snapshots serve as comprehensive backups of your base, capturing its state at the time of creation. Restoring a snapshot creates a new instance of the base in the designated workspace.",
"newSnapshot": "New Snapshot",
"searchASnapshot": "Search a snapshot",
@@ -2480,7 +2480,7 @@
"deleteProject": "Do you want to delete the base?",
"shareBasePrivate": "Generate publicly shareable readonly base",
"shareBasePublic": "Anyone on the internet with this link can view",
"noSandboxesFound": "No apps found in the marketplace",
"noManagedAppsFound": "No apps found in the marketplace",
"userInviteNoSMTP": "Looks like you have not configured mailer yet! Please copy above invite link and send it to",
"dragDropHide": "Drag and drop fields here to hide",
"formInput": "Enter form input label",
@@ -2807,10 +2807,10 @@
},
"success": {
"passwordSet": "Password set successfully",
"sandboxCreated": "Sandbox created successfully",
"sandboxUpdated": "Sandbox updated successfully",
"sandboxPublished": "Sandbox published successfully",
"sandboxUnpublished": "Sandbox unpublished successfully",
"managedAppCreated": "Managed App created successfully",
"managedAppUpdated": "Managed App updated successfully",
"managedAppPublished": "Managed App published successfully",
"managedAppUnpublished": "Managed App unpublished successfully",
"changesPublished": "Changes published to all installations successfully",
"mcpTokenDeleted": "MCP Token Deleted",
"mcpTokenUpdated": "MCP Token Updated",

View File

@@ -149,6 +149,10 @@ export enum ImportWorkerResponse {
ERROR = 'error',
}
export enum FeatureFlag {
MANAGED_APP = 'MANAGED_APP',
}
export enum ImportType {
EXCEL = 'excel',
CSV = 'csv',
@@ -227,5 +231,4 @@ export enum NcBaseCreateMode {
BUILD_WITH_AI = 'buildWithAi',
FROM_APP_STORE = 'fromAppStore',
MANAGED_APP = 'managedApp',
SANDBOX_APP = 'sandboxApp',
}

View File

@@ -225,12 +225,12 @@ type NcProject = BaseType & {
users?: User[]
default_role?: ProjectRoles | string
version?: BaseVersion
// Sandbox fields
sandbox_master?: boolean
sandbox_id?: string
sandbox_version_id?: string
// Managed App fields
managed_app_master?: boolean
managed_app_id?: string
managed_app_version_id?: string
auto_update?: boolean
sandbox_schema_locked?: boolean
managed_app_schema_locked?: boolean
}
interface UndoRedoAction {

View File

@@ -18,7 +18,7 @@ export const useBase = defineStore('baseStore', () => {
const basesStore = useBases()
const isSandboxMaster = ref(false)
const isManagedAppMaster = ref(false)
const baseId = computed(() => {
// In shared base mode, use activeProjectId from basesStore which has the correct base ID
@@ -342,7 +342,7 @@ export const useBase = defineStore('baseStore', () => {
idUserMap,
isPrivateBase,
showBaseAccessRequestOverlay,
isSandboxMaster,
isManagedAppMaster,
}
})

View File

@@ -370,12 +370,12 @@ export enum BaseVersion {
V3 = 3,
}
export enum SandboxVersionStatus {
export enum ManagedAppVersionStatus {
DRAFT = 'draft',
PUBLISHED = 'published',
}
export enum SandboxVisibility {
export enum ManagedAppVisibility {
PUBLIC = 'public',
PRIVATE = 'private',
UNLISTED = 'unlisted',

View File

@@ -125,21 +125,21 @@ export const OPERATION_SCOPES = {
extensionUpdate: 'base',
extensionDelete: 'base',
// Sandbox Operations
sandboxStoreList: 'org',
sandboxList: 'workspace',
sandboxGet: 'org',
sandboxCreate: 'workspace',
sandboxUpdate: 'base',
sandboxDelete: 'base',
sandboxPublish: 'base',
sandboxCreateDraft: 'base',
sandboxUnpublish: 'base',
sandboxInstall: 'workspace',
sandboxGetUpdates: 'base',
sandboxVersionsList: 'base',
sandboxInstallationsList: 'base',
sandboxDeployments: 'base',
sandboxVersionDeployments: 'base',
sandboxDeploymentLogs: 'base',
// Managed App Operations
managedAppStoreList: 'org',
managedAppList: 'workspace',
managedAppGet: 'org',
managedAppCreate: 'workspace',
managedAppUpdate: 'base',
managedAppDelete: 'base',
managedAppPublish: 'base',
managedAppCreateDraft: 'base',
managedAppUnpublish: 'base',
managedAppInstall: 'workspace',
managedAppGetUpdates: 'base',
managedAppVersionsList: 'base',
managedAppInstallationsList: 'base',
managedAppDeployments: 'base',
managedAppVersionDeployments: 'base',
managedAppDeploymentLogs: 'base',
} as const;

View File

@@ -137,8 +137,7 @@ export class NcErrorV1 extends NcErrorBase {
schemaLocked(message?: string): never {
return this.forbidden(
message ||
'Schema modifications are not allowed on installed sandbox bases',
message || 'Schema modifications are not allowed on managed apps',
);
}
}

View File

@@ -131,9 +131,9 @@ export class MetaService {
[MetaTable.INSTALLATIONS]: 'inst',
[MetaTable.AUTOMATIONS]: 'aut',
[MetaTable.AUTOMATION_EXECUTIONS]: 'auex',
[MetaTable.SANDBOXES]: 'sb',
[MetaTable.SANDBOX_VERSIONS]: 'sbv',
[MetaTable.SANDBOX_DEPLOYMENT_LOGS]: 'sbdl',
[MetaTable.MANAGED_APPS]: 'ma',
[MetaTable.MANAGED_APP_VERSIONS]: 'mav',
[MetaTable.MANAGED_APP_DEPLOYMENT_LOGS]: 'madl',
};
const prefix = prefixMap[target] || 'nc';

View File

@@ -12,6 +12,7 @@ import * as nc_011_merge_workflows_scripts from './v0/nc_011_merge_workflows_scr
import * as nc_012_workflow_delay from './v0/nc_012_workflow_delay';
import * as nc_013_composite_pk_missing_tables from './v0/nc_013_composite_pk_missing_tables';
import * as nc_014_sandboxes from './v0/nc_014_sandboxes';
import * as nc_015_managed_apps from './v0/nc_015_managed_apps';
// Create a custom migration source class
export default class XcMigrationSourcev0 {
@@ -35,6 +36,7 @@ export default class XcMigrationSourcev0 {
'nc_012_workflow_delay',
'nc_013_composite_pk_missing_tables',
'nc_014_sandboxes',
'nc_015_managed_apps',
]);
}
@@ -72,6 +74,8 @@ export default class XcMigrationSourcev0 {
return nc_013_composite_pk_missing_tables;
case 'nc_014_sandboxes':
return nc_014_sandboxes;
case 'nc_015_managed_apps':
return nc_015_managed_apps;
}
}
}

View File

@@ -3,7 +3,7 @@ import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
// Create sandboxes table
await knex.schema.createTable(MetaTable.SANDBOXES, (table) => {
await knex.schema.createTable(MetaTable.SANDBOXES_OLD, (table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
table.string('base_id', 20).notNullable().unique();
@@ -44,7 +44,7 @@ const up = async (knex: Knex) => {
});
// Create sandbox_versions table to store serialized schemas for each published version
await knex.schema.createTable(MetaTable.SANDBOX_VERSIONS, (table) => {
await knex.schema.createTable(MetaTable.SANDBOX_VERSIONS_OLD, (table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
table.string('fk_sandbox_id', 20).notNullable();
@@ -92,11 +92,11 @@ const up = async (knex: Knex) => {
// Is this base a sandbox master?
table.boolean('sandbox_master').defaultTo(false);
// Points to SANDBOXES.id (for both master and installed instances)
// Points to SANDBOXES_OLD.id (for both master and installed instances)
table.string('sandbox_id', 20);
// Current version: for master=draft/published being worked on, for installed=installed version
// Points to SANDBOX_VERSIONS.id
// Points to SANDBOX_VERSIONS_OLD.id
table.string('sandbox_version_id', 20);
// For installed instances: auto-update to new published versions
@@ -115,60 +115,69 @@ const up = async (knex: Knex) => {
});
// Create sandbox_deployment_logs table
await knex.schema.createTable(MetaTable.SANDBOX_DEPLOYMENT_LOGS, (table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
await knex.schema.createTable(
MetaTable.SANDBOX_DEPLOYMENT_LOGS_OLD,
(table) => {
table.string('id', 20).primary();
table.string('fk_workspace_id', 20).notNullable();
// Installation reference
table.string('base_id', 20).notNullable(); // The installed base
table.string('fk_sandbox_id', 20).notNullable(); // The sandbox being deployed
// Installation reference
table.string('base_id', 20).notNullable(); // The installed base
table.string('fk_sandbox_id', 20).notNullable(); // The sandbox being deployed
// Version tracking
table.string('from_version_id', 20); // NULL for initial install
table.string('to_version_id', 20).notNullable(); // Target version
// Version tracking
table.string('from_version_id', 20); // NULL for initial install
table.string('to_version_id', 20).notNullable(); // Target version
// Deployment status: 'pending', 'in_progress', 'success', 'failed'
table.string('status', 20).notNullable().defaultTo('pending');
// Deployment status: 'pending', 'in_progress', 'success', 'failed'
table.string('status', 20).notNullable().defaultTo('pending');
// Deployment type: 'install', 'update'
table.string('deployment_type', 20).notNullable();
// Deployment type: 'install', 'update'
table.string('deployment_type', 20).notNullable();
// Deployment details
table.text('error_message'); // Error message if failed
table.text('deployment_log'); // Detailed logs (JSON or text)
table.text('meta'); // Additional metadata (JSON)
// Deployment details
table.text('error_message'); // Error message if failed
table.text('deployment_log'); // Detailed logs (JSON or text)
table.text('meta'); // Additional metadata (JSON)
// Timestamps
table.timestamps(true, true);
table.timestamp('started_at');
table.timestamp('completed_at');
// Timestamps
table.timestamps(true, true);
table.timestamp('started_at');
table.timestamp('completed_at');
// Indexes for performance
table.index(
['fk_workspace_id'],
'nc_sandbox_deployment_logs_workspace_id_idx',
);
table.index(['base_id'], 'nc_sandbox_deployment_logs_base_id_idx');
table.index(['fk_sandbox_id'], 'nc_sandbox_deployment_logs_sandbox_id_idx');
table.index(
['base_id', 'created_at'],
'nc_sandbox_deployment_logs_base_created_idx',
);
table.index(['status'], 'nc_sandbox_deployment_logs_status_idx');
table.index(
['from_version_id'],
'nc_sandbox_deployment_logs_from_version_idx',
);
table.index(['to_version_id'], 'nc_sandbox_deployment_logs_to_version_idx');
});
// Indexes for performance
table.index(
['fk_workspace_id'],
'nc_sandbox_deployment_logs_workspace_id_idx',
);
table.index(['base_id'], 'nc_sandbox_deployment_logs_base_id_idx');
table.index(
['fk_sandbox_id'],
'nc_sandbox_deployment_logs_sandbox_id_idx',
);
table.index(
['base_id', 'created_at'],
'nc_sandbox_deployment_logs_base_created_idx',
);
table.index(['status'], 'nc_sandbox_deployment_logs_status_idx');
table.index(
['from_version_id'],
'nc_sandbox_deployment_logs_from_version_idx',
);
table.index(
['to_version_id'],
'nc_sandbox_deployment_logs_to_version_idx',
);
},
);
};
const down = async (knex: Knex) => {
// Drop sandbox_deployment_logs table
await knex.schema.dropTable(MetaTable.SANDBOX_DEPLOYMENT_LOGS);
await knex.schema.dropTable(MetaTable.SANDBOX_DEPLOYMENT_LOGS_OLD);
// Drop sandbox_versions table
await knex.schema.dropTable(MetaTable.SANDBOX_VERSIONS);
await knex.schema.dropTable(MetaTable.SANDBOX_VERSIONS_OLD);
// Drop indexes from bases table
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
@@ -190,7 +199,7 @@ const down = async (knex: Knex) => {
});
// Drop sandboxes table
await knex.schema.dropTable(MetaTable.SANDBOXES);
await knex.schema.dropTable(MetaTable.SANDBOXES_OLD);
};
export { up, down };

View File

@@ -0,0 +1,262 @@
import type { Knex } from 'knex';
import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
// Rename sandboxes table to managed_apps
await knex.schema.renameTable(
MetaTable.SANDBOXES_OLD,
MetaTable.MANAGED_APPS,
);
// Rename sandbox_versions table to managed_app_versions
await knex.schema.renameTable(
MetaTable.SANDBOX_VERSIONS_OLD,
MetaTable.MANAGED_APP_VERSIONS,
);
// Rename sandbox_deployment_logs table to managed_app_deployment_logs
await knex.schema.renameTable(
MetaTable.SANDBOX_DEPLOYMENT_LOGS_OLD,
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
);
// Rename columns in managed_app_versions table (formerly sandbox_versions)
await knex.schema.alterTable(MetaTable.MANAGED_APP_VERSIONS, (table) => {
table.renameColumn('fk_sandbox_id', 'fk_managed_app_id');
});
// Rename indexes in managed_app_versions table
// Note: Index renaming varies by database. We'll drop and recreate them.
await knex.schema.alterTable(MetaTable.MANAGED_APP_VERSIONS, (table) => {
// Drop old indexes
table.dropUnique(
['fk_sandbox_id', 'version'],
'nc_sandbox_versions_unique_idx',
);
table.dropUnique(
['fk_sandbox_id', 'version_number'],
'nc_sandbox_versions_number_unique_idx',
);
table.dropIndex(['fk_sandbox_id'], 'nc_sandbox_versions_sandbox_id_idx');
table.dropIndex(
['fk_sandbox_id', 'status'],
'nc_sandbox_versions_status_idx',
);
table.dropIndex(
['fk_sandbox_id', 'version_number'],
'nc_sandbox_versions_ordering_idx',
);
});
await knex.schema.alterTable(MetaTable.MANAGED_APP_VERSIONS, (table) => {
// Create new indexes with updated names
table.unique(['fk_managed_app_id', 'version'], {
indexName: 'nc_managed_app_versions_unique_idx',
});
table.unique(['fk_managed_app_id', 'version_number'], {
indexName: 'nc_managed_app_versions_number_unique_idx',
});
table.index(
['fk_managed_app_id'],
'nc_managed_app_versions_managed_app_id_idx',
);
table.index(
['fk_managed_app_id', 'status'],
'nc_managed_app_versions_status_idx',
);
table.index(
['fk_managed_app_id', 'version_number'],
'nc_managed_app_versions_ordering_idx',
);
});
// Rename columns in managed_app_deployment_logs table (formerly sandbox_deployment_logs)
await knex.schema.alterTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
(table) => {
table.renameColumn('fk_sandbox_id', 'fk_managed_app_id');
},
);
// Rename indexes in managed_app_deployment_logs table
await knex.schema.alterTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
(table) => {
// Drop old indexes
table.dropIndex(
['fk_sandbox_id'],
'nc_sandbox_deployment_logs_sandbox_id_idx',
);
},
);
await knex.schema.alterTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
(table) => {
// Create new indexes
table.index(
['fk_managed_app_id'],
'nc_managed_app_deployment_logs_managed_app_id_idx',
);
},
);
// Rename columns in bases table (PROJECT)
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
// Drop old indexes first
table.dropIndex(
['sandbox_id', 'auto_update'],
'nc_bases_sandbox_auto_update_idx',
);
table.dropIndex(['sandbox_version_id'], 'nc_bases_sandbox_version_id_idx');
table.dropIndex(['sandbox_id'], 'nc_bases_sandbox_id_idx');
table.dropIndex(['sandbox_master'], 'nc_bases_sandbox_master_idx');
});
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
// Rename columns
table.renameColumn('sandbox_master', 'managed_app_master');
table.renameColumn('sandbox_id', 'managed_app_id');
table.renameColumn('sandbox_version_id', 'managed_app_version_id');
});
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
// Create new indexes
table.index(['managed_app_master'], 'nc_bases_managed_app_master_idx');
table.index(['managed_app_id'], 'nc_bases_managed_app_id_idx');
table.index(
['managed_app_version_id'],
'nc_bases_managed_app_version_id_idx',
);
table.index(
['managed_app_id', 'auto_update'],
'nc_bases_managed_app_auto_update_idx',
);
});
};
const down = async (knex: Knex) => {
// Revert columns in bases table (PROJECT)
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
// Drop new indexes
table.dropIndex(
['managed_app_id', 'auto_update'],
'nc_bases_managed_app_auto_update_idx',
);
table.dropIndex(
['managed_app_version_id'],
'nc_bases_managed_app_version_id_idx',
);
table.dropIndex(['managed_app_id'], 'nc_bases_managed_app_id_idx');
table.dropIndex(['managed_app_master'], 'nc_bases_managed_app_master_idx');
});
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
// Rename columns back
table.renameColumn('managed_app_version_id', 'sandbox_version_id');
table.renameColumn('managed_app_id', 'sandbox_id');
table.renameColumn('managed_app_master', 'sandbox_master');
});
await knex.schema.alterTable(MetaTable.PROJECT, (table) => {
// Recreate old indexes
table.index(['sandbox_master'], 'nc_bases_sandbox_master_idx');
table.index(['sandbox_id'], 'nc_bases_sandbox_id_idx');
table.index(['sandbox_version_id'], 'nc_bases_sandbox_version_id_idx');
table.index(
['sandbox_id', 'auto_update'],
'nc_bases_sandbox_auto_update_idx',
);
});
// Revert indexes in managed_app_deployment_logs table
await knex.schema.alterTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
(table) => {
table.dropIndex(
['fk_managed_app_id'],
'nc_managed_app_deployment_logs_managed_app_id_idx',
);
},
);
await knex.schema.alterTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
(table) => {
table.index(
['fk_sandbox_id'],
'nc_sandbox_deployment_logs_sandbox_id_idx',
);
},
);
// Revert columns in managed_app_deployment_logs table
await knex.schema.alterTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
(table) => {
table.renameColumn('fk_managed_app_id', 'fk_sandbox_id');
},
);
// Revert indexes in managed_app_versions table
await knex.schema.alterTable(MetaTable.MANAGED_APP_VERSIONS, (table) => {
table.dropIndex(
['fk_managed_app_id', 'version_number'],
'nc_managed_app_versions_ordering_idx',
);
table.dropIndex(
['fk_managed_app_id', 'status'],
'nc_managed_app_versions_status_idx',
);
table.dropIndex(
['fk_managed_app_id'],
'nc_managed_app_versions_managed_app_id_idx',
);
table.dropUnique(
['fk_managed_app_id', 'version_number'],
'nc_managed_app_versions_number_unique_idx',
);
table.dropUnique(
['fk_managed_app_id', 'version'],
'nc_managed_app_versions_unique_idx',
);
});
await knex.schema.alterTable(MetaTable.MANAGED_APP_VERSIONS, (table) => {
table.unique(['fk_sandbox_id', 'version'], {
indexName: 'nc_sandbox_versions_unique_idx',
});
table.unique(['fk_sandbox_id', 'version_number'], {
indexName: 'nc_sandbox_versions_number_unique_idx',
});
table.index(['fk_sandbox_id'], 'nc_sandbox_versions_sandbox_id_idx');
table.index(['fk_sandbox_id', 'status'], 'nc_sandbox_versions_status_idx');
table.index(
['fk_sandbox_id', 'version_number'],
'nc_sandbox_versions_ordering_idx',
);
});
// Revert columns in managed_app_versions table
await knex.schema.alterTable(MetaTable.MANAGED_APP_VERSIONS, (table) => {
table.renameColumn('fk_managed_app_id', 'fk_sandbox_id');
});
// Rename tables back
await knex.schema.renameTable(
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
MetaTable.SANDBOX_DEPLOYMENT_LOGS_OLD,
);
await knex.schema.renameTable(
MetaTable.MANAGED_APP_VERSIONS,
MetaTable.SANDBOX_VERSIONS_OLD,
);
await knex.schema.renameTable(
MetaTable.MANAGED_APPS,
MetaTable.SANDBOXES_OLD,
);
};
export { up, down };

View File

@@ -54,12 +54,12 @@ export default class Base implements BaseType {
roles?: string;
fk_custom_url_id?: string;
// sandbox props
sandbox_master?: boolean; // Is this base a sandbox master?
sandbox_id?: string; // Points to SANDBOXES (for both master and installed instances)
sandbox_version_id?: string; // Current version ID from SANDBOX_VERSIONS
// managed app props
managed_app_master?: boolean; // Is this base a managed app master?
managed_app_id?: string; // Points to MANAGED_APPS (for both master and installed instances)
managed_app_version_id?: string; // Current version ID from MANAGED_APP_VERSIONS
auto_update?: boolean; // For installed instances: auto-update to new published versions
sandbox_schema_locked?: boolean; // Computed: whether schema modifications are allowed
managed_app_schema_locked?: boolean; // Computed: whether schema modifications are allowed
constructor(base: Partial<Base>) {
Object.assign(this, base);
@@ -88,9 +88,9 @@ export default class Base implements BaseType {
'color',
'order',
'version',
'sandbox_master',
'sandbox_id',
'sandbox_version_id',
'managed_app_master',
'managed_app_id',
'managed_app_version_id',
'auto_update',
]);
@@ -289,9 +289,9 @@ export default class Base implements BaseType {
}
const base = this.castType(baseData);
// Compute sandbox_schema_locked
if (base && base.sandbox_id) {
base.sandbox_schema_locked = await this.computeSchemaLocked(base);
// Compute managed_app_schema_locked
if (base && base.managed_app_id) {
base.managed_app_schema_locked = await this.computeSchemaLocked(base);
}
return base;
@@ -370,9 +370,9 @@ export default class Base implements BaseType {
if (baseData) {
const base = this.castType(baseData);
// Compute sandbox_schema_locked
if (base.sandbox_id) {
base.sandbox_schema_locked = await this.computeSchemaLocked(base);
// Compute managed_app_schema_locked
if (base.managed_app_id) {
base.managed_app_schema_locked = await this.computeSchemaLocked(base);
}
await base.getSources(includeConfig, ncMeta);
@@ -471,9 +471,9 @@ export default class Base implements BaseType {
'password',
'roles',
'version',
'sandbox_master',
'sandbox_id',
'sandbox_version_id',
'managed_app_master',
'managed_app_id',
'managed_app_version_id',
'auto_update',
]);

View File

@@ -102,9 +102,12 @@ export enum MetaTable {
AUTOMATION_EXECUTIONS = 'nc_automation_executions',
DEPENDENCY_TRACKER = 'nc_dependency_tracker',
INSTALLATIONS = 'nc_installations',
SANDBOXES = 'nc_sandboxes',
SANDBOX_VERSIONS = 'nc_sandbox_versions',
SANDBOX_DEPLOYMENT_LOGS = 'nc_sandbox_deployment_logs',
SANDBOXES_OLD = 'nc_sandboxes',
SANDBOX_VERSIONS_OLD = 'nc_sandbox_versions',
SANDBOX_DEPLOYMENT_LOGS_OLD = 'nc_sandbox_deployment_logs',
MANAGED_APPS = 'nc_managed_apps',
MANAGED_APP_VERSIONS = 'nc_managed_app_versions',
MANAGED_APP_DEPLOYMENT_LOGS = 'nc_managed_app_deployment_logs',
}
export const BaseRelatedMetaTables = [
@@ -326,9 +329,9 @@ export enum CacheScope {
DEPENDENCY_TRACKER = 'nc_dependency_tracker',
INSTALLATION = 'installation',
INSTALLATION_ALIAS = 'installationAlias',
SANDBOX = 'sandbox',
SANDBOX_VERSION = 'sandboxVersion',
SANDBOX_DEPLOYMENT_LOG = 'sandboxDeploymentLog',
MANAGED_APP = 'managedApp',
MANAGED_APP_VERSION = 'managedAppVersion',
MANAGED_APP_DEPLOYMENT_LOG = 'managedAppDeploymentLog',
SUBSCRIPTIONS_ALIAS = 'subscriptionsAlias',
}
@@ -377,9 +380,9 @@ export const RootScopeTables = {
MetaTable.JOBS,
MetaTable.FILE_REFERENCES,
MetaTable.DATA_REFLECTION,
MetaTable.SANDBOXES,
MetaTable.SANDBOX_VERSIONS,
MetaTable.SANDBOX_DEPLOYMENT_LOGS,
MetaTable.MANAGED_APPS,
MetaTable.MANAGED_APP_VERSIONS,
MetaTable.MANAGED_APP_DEPLOYMENT_LOGS,
// Temporarily added need to be discussed within team
MetaTable.AUDIT,
MetaTable.CUSTOM_URLS,