Files
nocodb/packages/nc-gui/components/dlg/NocoDbImport.vue
2026-01-17 10:29:17 +00:00

394 lines
11 KiB
Vue

<script setup lang="ts">
import { JobStatus } from '#imports'
const { modelValue, baseId, transition } = defineProps<{
modelValue: boolean
baseId: string
transition?: string
showBackBtn?: boolean
}>()
const emit = defineEmits(['update:modelValue', 'back'])
const { $api } = useNuxtApp()
const { t } = useI18n()
const { copy } = useCopy()
const { activeWorkspace } = storeToRefs(useWorkspace())
const { appInfo } = useGlobal()
const { ncSiteUrl } = appInfo.value
const { $poller } = useNuxtApp()
const baseStore = useBase()
const basesStore = useBases()
const { refreshCommandPalette } = useCommandPalette()
const showGoToDashboardButton = ref(false)
const step = ref(1)
const progressRef = ref()
const lastProgress = ref()
const listeningImport = ref(false)
const listeningJobId = ref<string | null>(null)
const goBack = ref(false)
const listeningForUpdates = ref(false)
const advancedOptionsCounter = ref(0)
const advancedOptionsEnabled = computed(() => advancedOptionsCounter.value >= 2)
const syncOptions = ref({
baseId,
workspaceMode: false,
newBase: false,
secretToken: null,
})
const migrationUrl = computed(() => {
return syncOptions.value.secretToken ? `${ncSiteUrl}/?secret=${syncOptions.value.secretToken}` : ''
})
const onLog = (data: { message: string }) => {
progressRef.value?.pushProgress(data.message, 'progress')
lastProgress.value = { msg: data.message, status: 'progress' }
}
const onStatus = async (status: JobStatus, data?: any) => {
lastProgress.value = { msg: data?.message, status }
try {
if (status === JobStatus.COMPLETED) {
showGoToDashboardButton.value = true
if (syncOptions.value.workspaceMode || syncOptions.value.newBase) {
await basesStore.loadProjects()
} else {
await baseStore.loadProject()
}
progressRef.value?.pushProgress('Done!', status)
refreshCommandPalette()
// TODO: add tab of the first table
} else if (status === JobStatus.FAILED) {
if (syncOptions.value.workspaceMode || syncOptions.value.newBase) {
await basesStore.loadProjects()
} else {
await baseStore.loadProject()
}
goBack.value = true
progressRef.value?.pushProgress(data?.error?.message, status)
lastProgress.value = { msg: data?.error?.message, status }
refreshCommandPalette()
}
} catch (e: any) {
console.log('Error while loading project(s)', e)
}
}
const dialogShow = computed({
get: () => modelValue,
set: (v) => emit('update:modelValue', v),
})
async function startListening() {
if (!activeWorkspace.value?.id) return
listeningImport.value = true
try {
const res = await $api.internal.postOperation(
activeWorkspace.value.id,
baseId,
{
operation: 'listenRemoteImport',
},
syncOptions.value,
)
syncOptions.value.secretToken = res.secret
listeningJobId.value = res.id
$poller.subscribe(
{ id: listeningJobId.value },
(data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status) {
onStatus(data.status as JobStatus, data.data)
} else {
step.value = 2
onLog(data.data as any)
}
} else {
listeningForUpdates.value = false
}
},
)
await copy(migrationUrl.value)
message.info(t('msg.info.copiedToClipboard'))
} catch (e: any) {
console.error(e)
message.error('Failed to start listening')
listeningImport.value = false
}
// await createOrUpdate()
// await sync()
}
async function abortListening() {
if (!activeWorkspace.value?.id) return
if (syncOptions.value.secretToken) {
await $api.internal.postOperation(
activeWorkspace.value.id,
baseId,
{
operation: 'abortRemoteImport',
},
{
secret: syncOptions.value.secretToken,
},
)
}
if (listeningJobId.value) {
$poller.unsubscribe({ id: listeningJobId.value })
}
listeningImport.value = false
listeningForUpdates.value = false
dialogShow.value = false
emit('back')
}
async function retryImport() {
step.value = 1
lastProgress.value = null
syncOptions.value.secretToken = null
listeningImport.value = false
}
const isInProgress = computed(() => {
return !lastProgress.value || ![JobStatus.COMPLETED, JobStatus.FAILED].includes(lastProgress.value?.status)
})
const detailsIsShown = ref(false)
const collapseKey = ref('')
onUnmounted(() => {
if (listeningJobId.value) {
$poller.unsubscribe({ id: listeningJobId.value })
}
})
</script>
<template>
<a-modal
v-model:visible="dialogShow"
class="!top-[25vh]"
:class="{ active: dialogShow }"
:closable="false"
:transition-name="transition"
:keyboard="step !== 2"
:mask-closable="step !== 2"
width="448px"
wrap-class-name="nc-modal-nocodb-import"
hide
@keydown.esc="dialogShow = false"
>
<div class="text-base font-weight-bold flex items-center gap-4 mb-6">
<GeneralIcon icon="nocodb1" class="w-6 h-6" @dblclick="advancedOptionsCounter++" />
<span v-if="step === 1">
{{ $t('title.quickImportNocoDB') }}
</span>
<span v-else-if="isInProgress"> {{ `${$t('labels.importingFromNocoDB')}...` }} </span>
<span v-else> {{ $t('labels.nocoDBBaseImported') }} </span>
<a
v-if="step === 1"
href="https://docs.nocodb.com/bases/import-base-from-nocodb#get-nocodb-credentials"
class="!text-nc-content-gray-muted prose-sm ml-auto"
target="_blank"
rel="noopener"
>
Docs
</a>
<NcButton v-else-if="step === 2" type="text" size="xs" class="ml-auto" @click="detailsIsShown = !detailsIsShown">
{{ detailsIsShown ? 'Hide' : 'Show' }} Details
<GeneralIcon icon="chevronDown" class="ml-2 transition-all transform" :class="{ 'rotate-180': detailsIsShown }" />
</NcButton>
</div>
<div v-if="step === 1">
<div class="text-nc-content-gray-subtle2 text-sm px-2">
<p class="mb-2">Easily migrate your base with the following steps:</p>
<ol class="list-decimal list-inside mt-2 pl-1">
<li>Open <strong>settings</strong> in your NocoDB base</li>
<li>Navigate to <strong>Migrate</strong> tab</li>
<li>Paste the <strong>URL</strong></li>
<li>Click <strong>Migrate</strong></li>
</ol>
</div>
<a-form ref="form" :model="syncOptions" name="quick-import-nocodb-form" layout="horizontal" class="!m-0 w-full">
<a-form-item v-if="listeningImport" class="!mt-0 !pb-2 !mb-0">
<LazyGeneralCopyInput :model-value="migrationUrl" class="!rounded-lg !mt-2 nc-input-shared-base" />
</a-form-item>
<NcButton
v-if="advancedOptionsEnabled && !listeningImport"
class="!mt-2"
type="text"
size="small"
@click="collapseKey = !collapseKey ? 'advanced-settings' : ''"
>
{{ $t('title.advancedSettings') }}
<GeneralIcon
icon="chevronDown"
class="ml-2 !transition-all !transform"
:class="{ '!rotate-180': collapseKey === 'advanced-settings' }"
/>
</NcButton>
<a-collapse v-if="!listeningImport" v-model:active-key="collapseKey" ghost class="nc-import-collapse">
<a-collapse-panel key="advanced-settings">
<div class="mb-2">
<a-checkbox v-model:checked="syncOptions.newBase"> New Base </a-checkbox>
</div>
<div class="mt-2">
<a-checkbox v-model:checked="syncOptions.workspaceMode"> Workspace Mode </a-checkbox>
</div>
<!--
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncViews">
Import Workspace Mode
</a-checkbox>
</div>
-->
</a-collapse-panel>
</a-collapse>
</a-form>
</div>
<div v-if="step === 2">
<GeneralProgressPanel v-show="detailsIsShown" ref="progressRef" class="w-full h-[200px]" />
<div v-show="!detailsIsShown" class="flex items-start gap-2">
<template v-if="isInProgress">
<component :is="iconMap.loading" class="text-primary animate-spin mt-1" />
<span>
{{ lastProgress?.msg ?? '---' }}
</span>
</template>
<template v-else-if="lastProgress?.status === JobStatus.FAILED">
<a-alert class="!rounded-lg !bg-transparent !border-nc-border-gray-medium !p-3 !w-full">
>
<template #message>
<div class="flex flex-row items-center gap-2 mb-2">
<GeneralIcon icon="ncAlertCircleFilled" class="text-nc-content-red-medium w-4 h-4" />
<span class="font-weight-700 text-[14px]">Import error</span>
</div>
</template>
<template #description>
<div class="text-nc-content-gray-muted text-[13px] leading-5 ml-6">
{{ lastProgress?.msg ?? '---' }}
</div>
</template>
</a-alert>
</template>
<div v-else class="flex items-start gap-3">
<GeneralIcon icon="checkFill" class="text-white w-4 h-4 mt-0.75" />
<span> {{ $t('msg.nocoDBImportSuccess') }} </span>
</div>
</div>
<div v-if="!isInProgress" class="text-right mt-4">
<NcButton v-if="lastProgress?.status === JobStatus.FAILED" size="small" @click="retryImport"> Retry import </NcButton>
<NcButton v-else size="small" @click="dialogShow = false">
{{ syncOptions.workspaceMode || syncOptions.newBase ? 'Go To Dashboard' : 'Go To Base' }}
</NcButton>
</div>
</div>
<template #footer>
<div v-if="step === 1" class="flex justify-between mt-2">
<NcButton
v-if="!listeningImport"
key="back"
type="text"
size="small"
@click="
() => {
dialogShow = false
emit('back')
}
"
>
<GeneralIcon v-if="showBackBtn" icon="chevronLeft" class="mr-1" />
{{ showBackBtn ? $t('general.back') : $t('general.cancel') }}
</NcButton>
<NcButton v-else key="abort" type="danger" size="small" @click="abortListening">
{{ $t('general.abort') }}
</NcButton>
<NcButton
v-if="listeningImport"
type="ghost"
class="nc-btn-nocodb-import"
size="small"
:loading="listeningImport"
@click="startListening"
>
Listening
</NcButton>
<NcButton v-else type="primary" class="nc-btn-nocodb-import" size="small" @click="startListening">
Generate & Copy URL
</NcButton>
</div>
</template>
</a-modal>
</template>
<style lang="scss" scoped>
.nc-import-collapse :deep(.ant-collapse-header) {
display: none !important;
}
</style>
<style>
.nc-modal-nocodb-import .ant-modal-footer {
@apply !border-none p-0;
}
.nc-modal-nocodb-import .ant-collapse-content-box {
padding-left: 6px;
}
</style>