mirror of https://github.com/nocodb/nocodb
DarkPhoenix2704
1 month ago
6 changed files with 31 additions and 558 deletions
@ -1,207 +1,7 @@
|
||||
<script setup lang="ts"> |
||||
import dayjs from 'dayjs' |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const { sorts, sortDirection, loadSorts, handleGetSortedData, saveOrUpdate: saveOrUpdateSort } = useUserSorts('Webhook') |
||||
|
||||
const orderBy = computed<Record<string, SordDirectionType>>({ |
||||
get: () => { |
||||
return sortDirection.value |
||||
}, |
||||
set: (value: Record<string, SordDirectionType>) => { |
||||
// Check if value is an empty object |
||||
if (Object.keys(value).length === 0) { |
||||
saveOrUpdateSort({}) |
||||
return |
||||
} |
||||
|
||||
const [field, direction] = Object.entries(value)[0] |
||||
|
||||
saveOrUpdateSort({ |
||||
field, |
||||
direction, |
||||
}) |
||||
}, |
||||
}) |
||||
|
||||
const { |
||||
snapshots, |
||||
createSnapshot, |
||||
listSnapshots, |
||||
updateSnapshot, |
||||
cancelNewSnapshot, |
||||
isUnsavedSnapshotsPending, |
||||
addNewSnapshot, |
||||
isCreatingSnapshot, |
||||
} = useBaseSettings() |
||||
|
||||
const columns = [ |
||||
{ |
||||
key: 'name', |
||||
title: t('general.snapshot'), |
||||
name: 'Snapshot', |
||||
minWidth: 397, |
||||
padding: '12px 24px', |
||||
showOrderBy: true, |
||||
dataIndex: 'title', |
||||
}, |
||||
{ |
||||
key: 'action', |
||||
title: t('general.action'), |
||||
width: 162, |
||||
minWidth: 162, |
||||
padding: '12px 24px', |
||||
}, |
||||
] as NcTableColumnProps[] |
||||
|
||||
onMounted(async () => { |
||||
await listSnapshots() |
||||
}) |
||||
const deleteSnapshot = (s: SnapshotExtendedType) => { |
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgSnapshotDelete'), { |
||||
'modelValue': isOpen, |
||||
'snapshot': s, |
||||
'onUpdate:modelValue': closeDialog, |
||||
'onDeleted': async () => { |
||||
closeDialog() |
||||
await listSnapshots() |
||||
}, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
close(1000) |
||||
} |
||||
} |
||||
|
||||
const restoreSnapshot = (s: SnapshotExtendedType) => { |
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgSnapshotRestore'), { |
||||
'modelValue': isOpen, |
||||
'snapshot': s, |
||||
'onUpdate:modelValue': closeDialog, |
||||
'onRestored': async () => { |
||||
closeDialog() |
||||
}, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
close(1000) |
||||
} |
||||
} |
||||
</script> |
||||
<script setup lang="ts"></script> |
||||
|
||||
<template> |
||||
<div v-if="isCreatingSnapshot" class="absolute w-full h-full inset-0 flex items-center justify-center z-90 bg-black/12"> |
||||
<div |
||||
v-if="isCreatingSnapshot" |
||||
style="box-shadow: 0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.1)" |
||||
class="bg-white p-6 flex flex-col w-[488px] rounded-2xl" |
||||
> |
||||
<div class="text-nc-content-gray-emphasis text-lg font-bold">Creating base snapshot</div> |
||||
<div class="text-nc-gray-subtle2 mt-2"> |
||||
Your database snapshot is being created. This process may take some time to complete. Please do not close this window |
||||
until the snapshot has finished. |
||||
</div> |
||||
|
||||
<div class="w-full flex justify-between items-center gap-3 mt-5"> |
||||
<GeneralLoader size="xlarge" /> |
||||
|
||||
<NcButton size="small" type="secondary"> |
||||
{{ $t('general.cancel') }} |
||||
</NcButton> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="item-card flex flex-col w-full"> |
||||
<div class="text-nc-content-gray-emphasis font-semibold text-lg"> |
||||
{{ $t('general.baseSnapshots') }} |
||||
</div> |
||||
|
||||
<div class="text-nc-content-gray-subtle2 mt-2 leading-5"> |
||||
{{ $t('labels.snapShotSubText') }} |
||||
</div> |
||||
|
||||
<div class="flex items-center mt-6 gap-5"> |
||||
<NcButton :disabled="isUnsavedSnapshotsPending" class="!w-36" size="small" type="secondary" @click="addNewSnapshot"> |
||||
{{ $t('labels.newSnapshot') }} |
||||
</NcButton> |
||||
</div> |
||||
|
||||
<NcTable |
||||
v-model:order-by="orderBy" |
||||
:columns="columns" |
||||
:data="snapshots" |
||||
class="h-full mt-5" |
||||
body-row-class-name="nc-base-settings-snapshot-item" |
||||
> |
||||
<template #bodyCell="{ column, record: snapshot }"> |
||||
<template v-if="column.key === 'name'"> |
||||
<NcTooltip v-if="!snapshot.isNew" class="truncate max-w-full text-gray-800 font-semibold text-sm"> |
||||
{{ snapshot.title }} |
||||
|
||||
<template #title> |
||||
<div class="text-xs font-semibold text-nc-gray-300">Created On</div> |
||||
<div class="mt-1 text-[13px]"> |
||||
{{ dayjs(snapshot.created_at).format('D MMMM YYYY, hh:mm A') }} |
||||
</div> |
||||
<div class="text-xs font-semibold mt-2 text-nc-gray-300">Created By</div> |
||||
<div class="mt-1 text-[13px]"> |
||||
{{ snapshot.created_display_name }} |
||||
</div> |
||||
</template> |
||||
</NcTooltip> |
||||
<a-input v-else v-model:value="snapshot.title" class="new-snapshot-title" /> |
||||
</template> |
||||
<template v-if="column.key === 'action'"> |
||||
<div v-if="!snapshot?.isNew" class="flex row-action items-center"> |
||||
<NcButton |
||||
size="small" |
||||
type="secondary" |
||||
class="!text-xs !rounded-r-none !border-r-0" |
||||
:shadow="false" |
||||
@click="restoreSnapshot(snapshot)" |
||||
> |
||||
<div class="text-nc-content-gray-subtle font-semibold"> |
||||
{{ $t('general.restore') }} |
||||
</div> |
||||
</NcButton> |
||||
<NcButton |
||||
size="small" |
||||
type="secondary" |
||||
class="!text-xs !rounded-l-none" |
||||
:shadow="false" |
||||
@click="deleteSnapshot(snapshot)" |
||||
> |
||||
<GeneralIcon icon="delete" /> |
||||
</NcButton> |
||||
</div> |
||||
|
||||
<div v-else> |
||||
<div class="flex gap-2"> |
||||
<NcButton type="secondary" size="small" @click="cancelNewSnapshot()"> |
||||
{{ $t('general.cancel') }} |
||||
</NcButton> |
||||
|
||||
<NcButton type="primary" size="small" @click="createSnapshot(snapshot)"> |
||||
{{ $t('general.save') }} |
||||
</NcButton> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</template> |
||||
</NcTable> |
||||
</div> |
||||
<div></div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.ant-input { |
||||
@apply rounded-lg py-1 px-3 w-398 h-8 border-1 focus:border-brand-500 border-gray-200; |
||||
} |
||||
</style> |
||||
<style scoped lang="scss"></style> |
||||
|
@ -1,51 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
interface Props { |
||||
modelValue: boolean |
||||
snapshot: SnapshotExtendedType |
||||
} |
||||
|
||||
interface Emits { |
||||
(event: 'update:modelValue', data: boolean): void |
||||
(event: 'deleted'): void |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const { snapshot } = props |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
|
||||
const { deleteSnapshot } = useBaseSettings() |
||||
|
||||
async function onDelete() { |
||||
if (!snapshot.id) return |
||||
|
||||
try { |
||||
await deleteSnapshot(snapshot) |
||||
|
||||
vModel.value = false |
||||
emits('deleted') |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<GeneralDeleteModal v-model:visible="vModel" :entity-name="$t('general.snapshot')" :on-delete="onDelete"> |
||||
<template #entity-preview> |
||||
<div class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700"> |
||||
<div |
||||
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3" |
||||
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" |
||||
> |
||||
<span> |
||||
{{ snapshot.title }} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</GeneralDeleteModal> |
||||
</template> |
@ -1,81 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
interface Props { |
||||
modelValue: boolean |
||||
snapshot: SnapshotExtendedType |
||||
} |
||||
|
||||
interface Emits { |
||||
(event: 'update:modelValue', data: boolean): void |
||||
(event: 'restored'): void |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const { snapshot } = props |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
|
||||
const { restoreSnapshot: _restoreSnapshot, isRestoringSnapshot } = useBaseSettings() |
||||
|
||||
const restoreSnapshot = async (snapshot: SnapshotExtendedType) => { |
||||
try { |
||||
await _restoreSnapshot(snapshot, () => { |
||||
vModel.value = false |
||||
emits('update:modelValue', false) |
||||
}) |
||||
} catch (error) { |
||||
console.error(error) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NcModal |
||||
v-model:visible="vModel" |
||||
:mask-closable="!isRestoringSnapshot" |
||||
size="xs" |
||||
height="auto" |
||||
:show-separator="false" |
||||
nc-modal-class-name="!p-6" |
||||
> |
||||
<div class="text-nc-content-gray-emphasis font-semibold text-lg">Confirm Snapshot Restore</div> |
||||
|
||||
<div class="text-nc-content-gray-subtle2 my-2 leading-5">Are you sure you want to restore this base snapshot.</div> |
||||
|
||||
<div class="leading-5 text-nc-content-gray-subtle">Note:</div> |
||||
|
||||
<ul class="list-disc leading-5 text-nc-content-gray-subtle pl-4 !mb-0"> |
||||
<li>Restoring this snapshot will not affect the existing base.</li> |
||||
<li> |
||||
On restore, a new base |
||||
|
||||
<span class="font-semibold"> |
||||
{{ snapshot.title }} |
||||
</span> |
||||
will be created in the same workspace. |
||||
</li> |
||||
</ul> |
||||
|
||||
<div class="my-5 px-4 py-2 bg-nc-bg-gray-light rounded-lg"> |
||||
{{ snapshot.title }} |
||||
</div> |
||||
|
||||
<div class="flex items-center gap-2 justify-end"> |
||||
<NcButton :disabled="isRestoringSnapshot" type="secondary" size="small" @click="vModel = false"> |
||||
{{ $t('general.cancel') }} |
||||
</NcButton> |
||||
|
||||
<NcButton |
||||
:disabled="isRestoringSnapshot" |
||||
type="primary" |
||||
size="small" |
||||
:loading="isRestoringSnapshot" |
||||
@click="restoreSnapshot(snapshot)" |
||||
> |
||||
{{ isRestoringSnapshot ? 'Restoring Snapshot' : $t('labels.confirmRestore') }} |
||||
</NcButton> |
||||
</div> |
||||
</NcModal> |
||||
</template> |
@ -1,206 +1,5 @@
|
||||
import { createSharedComposable } from '@vueuse/core' |
||||
import type { SnapshotType } from 'nocodb-sdk' |
||||
import { computed, ref } from 'vue' |
||||
import dayjs from 'dayjs' |
||||
|
||||
export type SnapshotExtendedType = SnapshotType & { |
||||
isNew?: boolean |
||||
loading?: boolean |
||||
error?: boolean |
||||
created_display_name?: string |
||||
} |
||||
|
||||
export const useBaseSettings = createSharedComposable(() => { |
||||
const { $api, $poller } = useNuxtApp() |
||||
|
||||
const baseStore = useBase() |
||||
const { base } = storeToRefs(baseStore) |
||||
|
||||
const basesStore = useBases() |
||||
|
||||
const { loadProjects } = basesStore |
||||
|
||||
const { navigateToProject } = useGlobal() |
||||
|
||||
const { refreshCommandPalette } = useCommandPalette() |
||||
|
||||
const _projectId = inject(ProjectIdInj, undefined) |
||||
|
||||
const isCreatingSnapshot = ref(false) |
||||
|
||||
const isRestoringSnapshot = ref(false) |
||||
|
||||
const baseId = computed(() => _projectId?.value ?? base.value?.id) |
||||
|
||||
const { basesUser, bases } = storeToRefs(basesStore) |
||||
|
||||
const baseUsers = computed(() => (baseId.value ? basesUser.value.get(baseId.value) || [] : [])) |
||||
|
||||
const snapshots = ref<SnapshotExtendedType[]>([] as SnapshotExtendedType[]) |
||||
|
||||
const updateSnapshot = async (snapshot: SnapshotExtendedType) => { |
||||
try { |
||||
snapshot.loading = true |
||||
await $api.snapshot.update(baseId.value, snapshot.id!, { |
||||
title: snapshot.title, |
||||
}) |
||||
snapshot.loading = false |
||||
} catch (error) { |
||||
message.error(await extractSdkResponseErrorMsg(error)) |
||||
snapshot.loading = false |
||||
console.error(error) |
||||
} |
||||
} |
||||
|
||||
const deleteSnapshot = async (snapshot: SnapshotExtendedType) => { |
||||
if (!baseId.value) return |
||||
try { |
||||
await $api.snapshot.delete(baseId.value, snapshot.id!) |
||||
snapshots.value = snapshots.value.filter((s) => s.id !== snapshot.id) |
||||
} catch (error) { |
||||
message.error(await extractSdkResponseErrorMsg(error)) |
||||
console.error(error) |
||||
} |
||||
} |
||||
|
||||
const listSnapshots = async () => { |
||||
try { |
||||
const response = await $api.snapshot.list(baseId.value) |
||||
snapshots.value = response.map((snapshot) => { |
||||
const user = baseUsers.value.find((u) => u.id === snapshot.created_by) |
||||
return { |
||||
...snapshot, |
||||
isNew: false, |
||||
created_display_name: user?.display_name ?? (user?.email ?? '').split('@')[0], |
||||
} |
||||
}) |
||||
} catch (error) { |
||||
message.error(await extractSdkResponseErrorMsg(error)) |
||||
console.error(error) |
||||
} |
||||
} |
||||
|
||||
const isUnsavedSnapshotsPending = computed(() => snapshots.value.some((s) => s.isNew)) |
||||
|
||||
const cancelNewSnapshot = () => { |
||||
snapshots.value = snapshots.value.filter((s) => !s.isNew) |
||||
} |
||||
|
||||
const createSnapshot = async (snapshot: Partial<SnapshotExtendedType>) => { |
||||
if (!baseId.value) return |
||||
try { |
||||
const response = await $api.snapshot.create(baseId.value, snapshot) |
||||
isCreatingSnapshot.value = true |
||||
|
||||
$poller.subscribe( |
||||
{ id: response.id }, |
||||
async (data: { |
||||
id: string |
||||
status?: string |
||||
data?: { |
||||
error?: { |
||||
message: string |
||||
} |
||||
message?: string |
||||
result?: any |
||||
} |
||||
}) => { |
||||
if (data.status !== 'close') { |
||||
if (data.status === JobStatus.COMPLETED) { |
||||
// Table metadata recreated successfully
|
||||
message.info('Snapshot created successfully') |
||||
await listSnapshots() |
||||
isCreatingSnapshot.value = false |
||||
} else if (status === JobStatus.FAILED) { |
||||
message.error('Failed to create snapshot') |
||||
isCreatingSnapshot.value = false |
||||
} |
||||
} |
||||
}, |
||||
) |
||||
} catch (error) { |
||||
message.error(await extractSdkResponseErrorMsg(error)) |
||||
|
||||
snapshot.error = true |
||||
console.error(error) |
||||
} |
||||
} |
||||
|
||||
const restoreSnapshot = async (snapshot: SnapshotExtendedType, onRestoreSuccess?: () => void | Promise<void>) => { |
||||
if (!baseId.value) return |
||||
try { |
||||
isRestoringSnapshot.value = true |
||||
const response = await $api.snapshot.restore(baseId.value, snapshot.id!) |
||||
|
||||
$poller.subscribe( |
||||
{ id: response.id! }, |
||||
async (data: { |
||||
id: string |
||||
status?: string |
||||
data?: { |
||||
error?: { |
||||
message: string |
||||
} |
||||
message?: string |
||||
result?: any |
||||
} |
||||
}) => { |
||||
if (data.status === JobStatus.COMPLETED) { |
||||
await loadProjects('workspace') |
||||
|
||||
const base = bases.value.get(data.data?.result.id) |
||||
|
||||
isRestoringSnapshot.value = false |
||||
|
||||
// open project after snapshot success
|
||||
if (base) { |
||||
await navigateToProject({ |
||||
workspaceId: isEeUI ? base.fk_workspace_id : undefined, |
||||
baseId: base.id, |
||||
type: base.type, |
||||
}) |
||||
} |
||||
message.info('Snapshot restored successfully') |
||||
|
||||
onRestoreSuccess?.() |
||||
|
||||
refreshCommandPalette() |
||||
} else if (data.status === JobStatus.FAILED) { |
||||
message.error('Failed to restore snapshot') |
||||
await loadProjects('workspace') |
||||
isRestoringSnapshot.value = false |
||||
|
||||
refreshCommandPalette() |
||||
} |
||||
}, |
||||
) |
||||
} catch (error) { |
||||
message.error(await extractSdkResponseErrorMsg(error)) |
||||
console.error(error) |
||||
} |
||||
} |
||||
|
||||
const addNewSnapshot = () => { |
||||
snapshots.value = [ |
||||
{ |
||||
title: dayjs().format('D MMMM YYYY, h:mm A'), |
||||
isNew: true, |
||||
}, |
||||
...snapshots.value, |
||||
] |
||||
} |
||||
|
||||
return { |
||||
snapshots, |
||||
createSnapshot, |
||||
listSnapshots, |
||||
updateSnapshot, |
||||
deleteSnapshot, |
||||
isUnsavedSnapshotsPending, |
||||
cancelNewSnapshot, |
||||
addNewSnapshot, |
||||
isCreatingSnapshot, |
||||
isRestoringSnapshot, |
||||
restoreSnapshot, |
||||
} |
||||
return {} |
||||
}) |
||||
|
Loading…
Reference in new issue