mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 13:46:56 +00:00
394 lines
11 KiB
Vue
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>
|