Browse Source

chore: move to ee and acl

pull/9879/head
DarkPhoenix2704 1 month ago
parent
commit
b8ae038da7
  1. 47
      packages/nc-gui/components/dashboard/settings/BaseSettings.vue
  2. 206
      packages/nc-gui/components/dashboard/settings/BaseSettings/Snapshots.vue
  3. 51
      packages/nc-gui/components/dlg/Snapshot/Delete.vue
  4. 81
      packages/nc-gui/components/dlg/Snapshot/Restore.vue
  5. 203
      packages/nc-gui/composables/useBaseSettings.ts
  6. 1
      packages/nc-gui/lib/acl.ts

47
packages/nc-gui/components/dashboard/settings/BaseSettings.vue

@ -1,24 +1,17 @@
<script setup lang="ts">
const { t } = useI18n()
const { isUIAllowed } = useRoles()
const activeMenu = ref('snapshots')
const hasPermissionForSnapshots = computed(() => isUIAllowed('manageSnapshot'))
const activeMenu = ref(isEeUI && hasPermissionForSnapshots.value ? 'snapshots' : 'visibility')
const selectMenu = (option: string) => {
if (!hasPermissionForSnapshots.value && option === 'snapshots') {
return
}
activeMenu.value = option
}
const options: { key: string; label: string; icon: keyof typeof iconMap }[] = [
{
key: 'snapshots',
label: t('general.snapshots'),
icon: 'camera',
},
{
key: 'visibility',
label: t('labels.visibilityAndDataHandling'),
icon: 'ncEye',
},
]
</script>
<template>
@ -27,18 +20,30 @@ const options: { key: string; label: string; icon: keyof typeof iconMap }[] = [
<div class="flex flex-col">
<div class="h-full w-60">
<div
v-for="option in options"
:key="option.key"
v-if="isEeUI && hasPermissionForSnapshots"
:class="{
'active-menu': activeMenu === option.key,
'active-menu': activeMenu === 'snapshots',
}"
class="gap-3 !hover:bg-gray-50 transition-all text-nc-content-gray flex rounded-lg items-center cursor-pointer py-1.5 px-3"
@click="selectMenu(option.key)"
@click="selectMenu('snapshots')"
>
<GeneralIcon :icon="option.icon" />
<GeneralIcon icon="camera" />
<span>
{{ $t('general.snapshots') }}
</span>
</div>
<div
:class="{
'active-menu': activeMenu === 'visibility',
}"
class="gap-3 !hover:bg-gray-50 transition-all text-nc-content-gray flex rounded-lg items-center cursor-pointer py-1.5 px-3"
@click="selectMenu('visibility')"
>
<GeneralIcon icon="ncEye" />
<span>
{{ option.label }}
{{ $t('labels.visibilityAndDataHandling') }}
</span>
</div>
</div>

206
packages/nc-gui/components/dashboard/settings/BaseSettings/Snapshots.vue

@ -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>

51
packages/nc-gui/components/dlg/Snapshot/Delete.vue

@ -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>

81
packages/nc-gui/components/dlg/Snapshot/Restore.vue

@ -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>

203
packages/nc-gui/composables/useBaseSettings.ts

@ -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 {}
})

1
packages/nc-gui/lib/acl.ts

@ -53,6 +53,7 @@ const rolePermissions = {
[ProjectRoles.OWNER]: {
include: {
baseDelete: true,
manageSnapshot: true,
},
},
[ProjectRoles.CREATOR]: {

Loading…
Cancel
Save