多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

609 lines
20 KiB

<script setup lang="ts">
import Draggable from 'vuedraggable'
import type { BaseType } from 'nocodb-sdk'
import CreateBase from './data-sources/CreateBase.vue'
import EditBase from './data-sources/EditBase.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Erd from './Erd.vue'
import BaseAudit from './BaseAudit.vue'
import { ClientType, DataSourcesSubTab, storeToRefs, useCommandPalette, useNuxtApp, useProject } from '#imports'
interface Props {
state: string
reload?: boolean
}
const props = defineProps<Props>()
const emits = defineEmits(['update:state', 'update:reload', 'awaken'])
const vState = useVModel(props, 'state', emits)
const vReload = useVModel(props, 'reload', emits)
const { $api, $e } = useNuxtApp()
const { loadProject } = useProjects()
const projectStore = useProject()
const { project } = storeToRefs(projectStore)
const { refreshCommandPalette } = useCommandPalette()
const sources = ref<BaseType[]>([])
const activeBaseId = ref('')
const metadiffbases = ref<string[]>([])
const clientType = ref<ClientType>(ClientType.MYSQL)
const isReloading = ref(false)
const forceAwakened = ref(false)
const dataSourcesAwakened = ref(false)
const isDeleteBaseModalOpen = ref(false)
const toBeDeletedBase = ref<BaseType | undefined>()
async function loadBases(changed?: boolean) {
try {
if (changed) refreshCommandPalette()
isReloading.value = true
vReload.value = true
const baseList = await $api.base.list(project.value.id as string)
if (baseList.list && baseList.list.length) {
sources.value = baseList.list
}
await loadMetaDiff()
} catch (e) {
console.error(e)
} finally {
vReload.value = false
isReloading.value = false
}
}
async function loadMetaDiff() {
try {
metadiffbases.value = []
const metadiff = await $api.project.metaDiffGet(project.value.id as string)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
metadiffbases.value.push(model.base_id)
}
}
} catch (e) {
console.error(e)
}
}
const baseAction = (baseId?: string, action?: string) => {
if (!baseId) return
activeBaseId.value = baseId
vState.value = action || ''
}
const openDeleteBase = (base: BaseType) => {
$e('c:base:delete')
isDeleteBaseModalOpen.value = true
toBeDeletedBase.value = base
}
const deleteBase = async () => {
if (!toBeDeletedBase.value) return
try {
await $api.base.delete(toBeDeletedBase.value.project_id as string, toBeDeletedBase.value.id as string)
$e('a:base:delete')
sources.value.splice(sources.value.indexOf(toBeDeletedBase.value), 1)
await loadProject(project.value.id as string, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const toggleBase = async (base: BaseType, state: boolean) => {
try {
if (!state && sources.value.filter((src) => src.enabled).length < 2) {
message.info('There should be at least one enabled base!')
return
}
base.enabled = state
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
enabled: base.enabled,
})
await loadProject(project.value.id as string, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const moveBase = async (e: any) => {
try {
if (e.oldIndex === e.newIndex) return
// sources list is mutated so we have to get the new index and mirror it to backend
const base = sources.value[e.newIndex]
if (base) {
if (!base.order) {
// empty update call to reorder bases (migration)
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
})
message.info('Bases are migrated. Please try again.')
} else {
await $api.base.update(base.project_id as string, base.id as string, {
id: base.id,
project_id: base.project_id,
order: e.newIndex + 1,
})
}
}
await loadProject(project.value.id as string, true)
await loadBases()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const forceAwaken = () => {
forceAwakened.value = !forceAwakened.value
dataSourcesAwakened.value = forceAwakened.value
emits('awaken', forceAwakened.value)
}
onMounted(async () => {
if (sources.value.length === 0) {
await loadBases()
await loadMetaDiff()
}
})
watch(
() => props.reload,
async (reload) => {
if (reload && !isReloading.value) {
await loadBases()
await loadMetaDiff()
}
},
)
watch(
() => sources.value.length,
(l) => {
if (l > 1 && !forceAwakened.value) {
dataSourcesAwakened.value = false
emits('awaken', false)
} else {
dataSourcesAwakened.value = true
emits('awaken', true)
}
},
{ immediate: true },
)
watch(
vState,
async (newState) => {
if (!sources.value.length) {
await loadBases()
}
switch (newState) {
case ClientType.MYSQL:
clientType.value = ClientType.MYSQL
vState.value = DataSourcesSubTab.New
break
case ClientType.PG:
clientType.value = ClientType.PG
vState.value = DataSourcesSubTab.New
break
case ClientType.SQLITE:
clientType.value = ClientType.SQLITE
vState.value = DataSourcesSubTab.New
break
case ClientType.MSSQL:
clientType.value = ClientType.MSSQL
vState.value = DataSourcesSubTab.New
break
case ClientType.SNOWFLAKE:
clientType.value = ClientType.SNOWFLAKE
vState.value = DataSourcesSubTab.New
break
case DataSourcesSubTab.New:
if (sources.value.length > 1 && !forceAwakened.value) {
vState.value = ''
}
break
}
},
{ immediate: true },
)
const isNewBaseModalOpen = computed({
get: () => {
return [DataSourcesSubTab.New].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isErdModalOpen = computed({
get: () => {
return [DataSourcesSubTab.ERD].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isMetaDataModal = computed({
get: () => {
return [DataSourcesSubTab.Metadata].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isUIAclModalOpen = computed({
get: () => {
return [DataSourcesSubTab.UIAcl].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isBaseAuditModalOpen = computed({
get: () => {
return [DataSourcesSubTab.Audit].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isEditBaseModalOpen = computed({
get: () => {
return [DataSourcesSubTab.Edit].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
</script>
<template>
<div class="flex flex-row w-full h-full nc-data-sources-view">
<div class="flex flex-col w-full overflow-auto">
<div class="flex flex-row w-full justify-end mt-6 mb-5">
<NcButton
v-if="dataSourcesAwakened"
size="large"
class="z-10 !rounded-lg !px-2 mr-2.5"
type="primary"
@click="vState = DataSourcesSubTab.New"
>
<div class="flex flex-row items-center w-full gap-x-1">
<component :is="iconMap.plus" />
<div class="flex">New Source</div>
</div>
</NcButton>
</div>
<div
class="overflow-y-auto nc-scrollbar-md"
:style="{
maxHeight: 'calc(100vh - 200px)',
}"
>
<div class="ds-table-head">
<div class="ds-table-row">
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">Visibility</div>
<div class="ds-table-col ds-table-name">Name</div>
<div class="ds-table-col ds-table-type">Type</div>
<div class="ds-table-col ds-table-actions pl-2">Actions</div>
<div class="ds-table-col ds-table-crud"></div>
</div>
</div>
<div class="ds-table-body">
<Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase">
<template #header>
<div v-if="sources[0]" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer">
<a-tooltip>
<template #title>
<template v-if="sources[0].enabled">Hide in UI</template>
<template v-else>Show in UI</template>
</template>
<a-switch
:checked="sources[0].enabled ? true : false"
size="default"
@change="toggleBase(sources[0], $event)"
/>
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium">
<div class="flex items-center gap-1">
<!-- <GeneralBaseLogo :base-type="sources[0].type" /> -->
Default
</div>
</div>
<div class="ds-table-col ds-table-type">
<div class="flex items-center gap-1">-</div>
</div>
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<a-button
v-if="!sources[0].is_meta && !sources[0].is_local"
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<a-tooltip v-if="metadiffbases.includes(sources[0].id)">
<template #title>Out of sync</template>
<GeneralIcon icon="warning" class="group-hover:text-accent text-primary" />
</a-tooltip>
<GeneralIcon v-else icon="sync" class="group-hover:text-accent" />
Sync Metadata
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
Relations
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
UI ACL
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Audit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
Audit
</div>
</a-button>
</div>
</div>
<div class="ds-table-col ds-table-crud">
<a-button
v-if="!sources[0].is_meta && !sources[0].is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</a-button>
</div>
</div>
</template>
<template #item="{ element: base, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer">
<a-tooltip>
<template #title>
<template v-if="base.enabled">Hide in UI</template>
<template v-else>Show in UI</template>
</template>
<a-switch :checked="base.enabled ? true : false" @change="toggleBase(base, $event)" />
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div v-if="base.is_meta || base.is_local">-</div>
<div v-else class="flex items-center gap-1">
{{ base.is_meta || base.is_local ? 'BASE' : base.alias }}
</div>
</div>
<div class="ds-table-col ds-table-type">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div class="flex items-center gap-2">
<GeneralBaseLogo :base-type="base.type" />
<span class="text-gray-700 capitalize">{{ base.type }}</span>
</div>
</div>
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(base.id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
Relations
</div>
</a-button>
<a-button
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
UI ACL
</div>
</a-button>
<a-button
v-if="!base.is_meta && !base.is_local"
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(base.id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<a-tooltip v-if="metadiffbases.includes(base.id)">
<template #title>Out of sync</template>
<GeneralIcon icon="warning" class="group-hover:text-accent text-primary" />
</a-tooltip>
<GeneralIcon v-else icon="sync" class="group-hover:text-accent" />
Sync Metadata
</div>
</a-button>
</div>
</div>
<div class="ds-table-col ds-table-crud justify-end gap-x-1">
<a-button
v-if="!base.is_meta && !base.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="baseAction(base.id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600 -mt-0.5" />
</a-button>
<a-button
v-if="!base.is_meta && !base.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="openDeleteBase(base)"
>
<GeneralIcon icon="delete" class="text-red-500 -mt-0.5" />
</a-button>
</div>
</div>
</template>
</Draggable>
</div>
</div>
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium">
<div class="py-6 px-8">
<CreateBase :connection-type="clientType" @base-created="loadBases(true)" @close="isNewBaseModalOpen = false" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div
class="p-6"
:style="{
height: '80vh',
}"
>
<Erd :base-id="activeBaseId" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isMetaDataModal" size="medium">
<div class="p-6">
<Metadata :base-id="activeBaseId" @base-synced="loadBases(true)" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isUIAclModalOpen" class="!w-[60rem]">
<div class="p-6">
<UIAcl :base-id="activeBaseId" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isEditBaseModalOpen" size="medium">
<div class="p-6">
<EditBase :base-id="activeBaseId" @base-updated="loadBases(true)" @close="isEditBaseModalOpen = false" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isBaseAuditModalOpen" class="!w-[70rem]">
<div class="p-6">
<BaseAudit :base-id="activeBaseId" @close="isBaseAuditModalOpen = false" />
</div>
</GeneralModal>
<GeneralDeleteModal v-model:visible="isDeleteBaseModalOpen" entity-name="base" :on-delete="deleteBase">
<template #entity-preview>
<div v-if="toBeDeletedBase" class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralBaseLogo :base-type="toBeDeletedBase.type" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ toBeDeletedBase.alias }}
</div>
</div>
</template>
</GeneralDeleteModal>
</div>
</div>
</template>
<style>
.ds-table-head {
@apply flex items-center border-0 text-gray-400;
}
.ds-table-body {
@apply flex flex-col;
}
.ds-table-row {
@apply grid grid-cols-20 border-b border-gray-100 w-full h-full;
}
.ds-table-col {
@apply flex items-start py-3 mr-2;
}
.ds-table-enabled {
@apply col-span-2 flex justify-center items-center;
}
.ds-table-name {
@apply col-span-6 items-center capitalize;
}
.ds-table-type {
@apply col-span-3 items-center;
}
.ds-table-actions {
@apply col-span-7;
}
.ds-table-crud {
@apply col-span-2;
}
.ds-table-col:last-child {
@apply border-r-0;
}
.ds-table-handle {
@apply cursor-pointer justify-self-start mr-2;
}
</style>