|
|
|
<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 { ClientType, DataSourcesSubTab } from '~/lib'
|
|
|
|
import { 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 { project, loadProject } = useProject()
|
|
|
|
|
|
|
|
let sources = $ref<BaseType[]>([])
|
|
|
|
let activeBaseId = $ref('')
|
|
|
|
let metadiffbases = $ref<string[]>([])
|
|
|
|
let clientType = $ref<ClientType>(ClientType.MYSQL)
|
|
|
|
let isReloading = $ref(false)
|
|
|
|
let forceAwakened = $ref(false)
|
|
|
|
|
|
|
|
async function loadBases() {
|
|
|
|
try {
|
|
|
|
if (!project.value?.id) return
|
|
|
|
|
|
|
|
isReloading = true
|
|
|
|
vReload.value = true
|
|
|
|
const baseList = await $api.base.list(project.value?.id)
|
|
|
|
if (baseList.bases.list && baseList.bases.list.length) {
|
|
|
|
sources = baseList.bases.list
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
} finally {
|
|
|
|
vReload.value = false
|
|
|
|
isReloading = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadMetaDiff() {
|
|
|
|
try {
|
|
|
|
if (!project.value?.id) return
|
|
|
|
|
|
|
|
metadiffbases = []
|
|
|
|
|
|
|
|
const metadiff = await $api.project.metaDiffGet(project.value?.id)
|
|
|
|
for (const model of metadiff) {
|
|
|
|
if (model.detectedChanges?.length > 0) {
|
|
|
|
metadiffbases.push(model.base_id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const baseAction = (baseId?: string, action?: string) => {
|
|
|
|
if (!baseId) return
|
|
|
|
activeBaseId = baseId
|
|
|
|
vState.value = action || ''
|
|
|
|
}
|
|
|
|
|
|
|
|
const deleteBase = (base: BaseType) => {
|
|
|
|
$e('c:base:delete')
|
|
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
title: `Do you want to delete '${base.alias}' project?`,
|
|
|
|
wrapClassName: 'nc-modal-base-delete',
|
|
|
|
okText: 'Yes',
|
|
|
|
okType: 'danger',
|
|
|
|
cancelText: 'No',
|
|
|
|
async onOk() {
|
|
|
|
try {
|
|
|
|
await $api.base.delete(base.project_id as string, base.id as string)
|
|
|
|
|
|
|
|
$e('a:base:delete')
|
|
|
|
|
|
|
|
sources.splice(sources.indexOf(base), 1)
|
|
|
|
await loadProject()
|
|
|
|
} catch (e: any) {
|
|
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
|
|
}
|
|
|
|
},
|
|
|
|
style: 'top: 30%!important',
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const toggleBase = async (base: BaseType, state: boolean) => {
|
|
|
|
try {
|
|
|
|
if (!state && sources.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()
|
|
|
|
} 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[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()
|
|
|
|
await loadBases()
|
|
|
|
} catch (e: any) {
|
|
|
|
message.error(await extractSdkResponseErrorMsg(e))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const forceAwaken = () => {
|
|
|
|
forceAwakened = !forceAwakened
|
|
|
|
emits('awaken', forceAwakened)
|
|
|
|
}
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
if (sources.length === 0) {
|
|
|
|
await loadBases()
|
|
|
|
await loadMetaDiff()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => props.reload,
|
|
|
|
async (reload) => {
|
|
|
|
if (reload && !isReloading) {
|
|
|
|
await loadBases()
|
|
|
|
await loadMetaDiff()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => sources.length,
|
|
|
|
(l) => {
|
|
|
|
if (l > 1 && !forceAwakened) {
|
|
|
|
emits('awaken', false)
|
|
|
|
} else {
|
|
|
|
emits('awaken', true)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ immediate: true },
|
|
|
|
)
|
|
|
|
|
|
|
|
watch(
|
|
|
|
vState,
|
|
|
|
async (newState) => {
|
|
|
|
if (!sources.length) {
|
|
|
|
await loadBases()
|
|
|
|
}
|
|
|
|
switch (newState) {
|
|
|
|
case ClientType.MYSQL:
|
|
|
|
clientType = ClientType.MYSQL
|
|
|
|
vState.value = DataSourcesSubTab.New
|
|
|
|
break
|
|
|
|
case ClientType.PG:
|
|
|
|
clientType = ClientType.PG
|
|
|
|
vState.value = DataSourcesSubTab.New
|
|
|
|
break
|
|
|
|
case ClientType.SQLITE:
|
|
|
|
clientType = ClientType.SQLITE
|
|
|
|
vState.value = DataSourcesSubTab.New
|
|
|
|
break
|
|
|
|
case ClientType.MSSQL:
|
|
|
|
clientType = ClientType.MSSQL
|
|
|
|
vState.value = DataSourcesSubTab.New
|
|
|
|
break
|
|
|
|
case ClientType.SNOWFLAKE:
|
|
|
|
clientType = ClientType.SNOWFLAKE
|
|
|
|
vState.value = DataSourcesSubTab.New
|
|
|
|
break
|
|
|
|
case DataSourcesSubTab.New:
|
|
|
|
if (sources.length > 1 && !forceAwakened) {
|
|
|
|
vState.value = ''
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ immediate: true },
|
|
|
|
)
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div class="flex flex-row w-full h-full">
|
|
|
|
<div class="flex flex-col w-full overflow-auto">
|
|
|
|
<div v-if="vState === ''" class="max-h-600px min-w-1200px overflow-y-auto">
|
|
|
|
<div class="ds-table-head">
|
|
|
|
<div class="ds-table-row">
|
|
|
|
<div class="ds-table-col ds-table-name">Name</div>
|
|
|
|
<div class="ds-table-col ds-table-actions">Actions</div>
|
|
|
|
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">Show / Hide</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-name">
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
<GeneralBaseLogo :base-type="sources[0].type" />
|
|
|
|
BASE
|
|
|
|
<span class="text-gray-400 text-xs">({{ sources[0].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"
|
|
|
|
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<a-tooltip v-if="metadiffbases.includes(sources[0].id as string)">
|
|
|
|
<template #title>Out of sync</template>
|
|
|
|
<MdiDatabaseAlert class="text-lg group-hover:text-accent text-primary" />
|
|
|
|
</a-tooltip>
|
|
|
|
<MdiDatabaseSync v-else class="text-lg group-hover:text-accent" />
|
|
|
|
Sync Metadata
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button
|
|
|
|
class="nc-action-btn cursor-pointer outline-0"
|
|
|
|
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<MdiDatabaseLockOutline class="text-lg group-hover:text-accent" />
|
|
|
|
UI ACL
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button
|
|
|
|
class="nc-action-btn cursor-pointer outline-0"
|
|
|
|
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<MdiGraphOutline class="text-lg group-hover:text-accent" />
|
|
|
|
ERD
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button
|
|
|
|
v-if="!sources[0].is_meta"
|
|
|
|
class="nc-action-btn cursor-pointer outline-0"
|
|
|
|
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<MdiEditOutline class="text-lg group-hover:text-accent" />
|
|
|
|
Edit
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<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" @change="toggleBase(sources[0], $event)" />
|
|
|
|
</a-tooltip>
|
|
|
|
</div>
|
|
|
|
</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-name">
|
|
|
|
<MdiDragVertical v-if="sources.length > 2" small class="ds-table-handle" />
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
<GeneralBaseLogo :base-type="base.type" />
|
|
|
|
{{ base.is_meta ? 'BASE' : base.alias }} <span class="text-gray-400 text-xs">({{ 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"
|
|
|
|
@click="baseAction(base.id, DataSourcesSubTab.Metadata)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<a-tooltip v-if="metadiffbases.includes(base.id as string)">
|
|
|
|
<template #title>Out of sync</template>
|
|
|
|
<MdiDatabaseAlert class="text-lg group-hover:text-accent text-primary" />
|
|
|
|
</a-tooltip>
|
|
|
|
<MdiDatabaseSync v-else class="text-lg group-hover:text-accent" />
|
|
|
|
Sync Metadata
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button
|
|
|
|
class="nc-action-btn cursor-pointer outline-0"
|
|
|
|
@click="baseAction(base.id, DataSourcesSubTab.UIAcl)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<MdiDatabaseLockOutline class="text-lg group-hover:text-accent" />
|
|
|
|
UI ACL
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button class="nc-action-btn cursor-pointer outline-0" @click="baseAction(base.id, DataSourcesSubTab.ERD)">
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<MdiGraphOutline class="text-lg group-hover:text-accent" />
|
|
|
|
ERD
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button
|
|
|
|
v-if="!base.is_meta"
|
|
|
|
class="nc-action-btn cursor-pointer outline-0"
|
|
|
|
@click="baseAction(base.id, DataSourcesSubTab.Edit)"
|
|
|
|
>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600 font-light">
|
|
|
|
<MdiEditOutline class="text-lg group-hover:text-accent" />
|
|
|
|
Edit
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
<a-button v-if="!base.is_meta" class="nc-action-btn cursor-pointer outline-0" @click="deleteBase(base)">
|
|
|
|
<div class="flex items-center gap-2 text-red-500 font-light">
|
|
|
|
<MdiDeleteOutline class="text-lg group-hover:text-accent" />
|
|
|
|
Delete
|
|
|
|
</div>
|
|
|
|
</a-button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<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>
|
|
|
|
</template>
|
|
|
|
</Draggable>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div v-else-if="vState === DataSourcesSubTab.New" class="max-h-600px overflow-y-auto">
|
|
|
|
<CreateBase :connection-type="clientType" @base-created="loadBases" />
|
|
|
|
</div>
|
|
|
|
<div v-else-if="vState === DataSourcesSubTab.Metadata" class="max-h-600px overflow-y-auto">
|
|
|
|
<Metadata :base-id="activeBaseId" @base-synced="loadBases" />
|
|
|
|
</div>
|
|
|
|
<div v-else-if="vState === DataSourcesSubTab.UIAcl" class="max-h-600px overflow-y-auto">
|
|
|
|
<UIAcl :base-id="activeBaseId" />
|
|
|
|
</div>
|
|
|
|
<div v-else-if="vState === DataSourcesSubTab.ERD" class="max-h-600px overflow-y-auto">
|
|
|
|
<Erd :base-id="activeBaseId" />
|
|
|
|
</div>
|
|
|
|
<div v-else-if="vState === DataSourcesSubTab.Edit" class="max-h-600px overflow-y-auto">
|
|
|
|
<EditBase :base-id="activeBaseId" @base-updated="loadBases" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
.ds-table-head {
|
|
|
|
@apply flex items-center border-t bg-gray-100 font-bold text-gray-500;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-body {
|
|
|
|
@apply flex flex-col;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-row {
|
|
|
|
@apply grid grid-cols-20 border-b w-full h-full border-l border-r;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-col {
|
|
|
|
@apply flex items-center p-3 border-r-1 mr-2 h-50px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-enabled {
|
|
|
|
@apply col-span-2 flex justify-center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-name {
|
|
|
|
@apply col-span-8;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-actions {
|
|
|
|
@apply col-span-10;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-col:last-child {
|
|
|
|
@apply border-r-0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ds-table-handle {
|
|
|
|
@apply cursor-pointer justify-self-start mr-2;
|
|
|
|
}
|
|
|
|
</style>
|