mirror of https://github.com/nocodb/nocodb
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.
696 lines
23 KiB
696 lines
23 KiB
<script setup lang="ts"> |
|
import Draggable from 'vuedraggable' |
|
import type { SourceType } from 'nocodb-sdk' |
|
import { ClientType } from '#imports' |
|
|
|
interface Props { |
|
state: string |
|
baseId: string |
|
reload?: boolean |
|
} |
|
|
|
const props = defineProps<Props>() |
|
|
|
const emits = defineEmits(['update:state', 'update:reload']) |
|
|
|
const vState = useVModel(props, 'state', emits) |
|
|
|
const vReload = useVModel(props, 'reload', emits) |
|
|
|
const { $api, $e } = useNuxtApp() |
|
|
|
const basesStore = useBases() |
|
const { loadProject } = basesStore |
|
const { isDataSourceLimitReached, bases } = storeToRefs(basesStore) |
|
|
|
const base = computed(() => bases.value.get(props.baseId) ?? {}) |
|
|
|
const { isUIAllowed } = useRoles() |
|
|
|
const { projectPageTab } = storeToRefs(useConfigStore()) |
|
|
|
const { refreshCommandPalette } = useCommandPalette() |
|
|
|
const sources = ref<SourceType[]>([]) |
|
|
|
const activeBaseId = ref('') |
|
|
|
const clientType = ref<ClientType>(ClientType.MYSQL) |
|
|
|
const isReloading = ref(false) |
|
|
|
const isDeleteBaseModalOpen = ref(false) |
|
const toBeDeletedBase = ref<SourceType | undefined>() |
|
|
|
const searchQuery = ref<string>('') |
|
|
|
async function updateIfSourceOrderIsNullOrDuplicate() { |
|
const sourceOrderSet = new Set() |
|
let hasNullOrDuplicates = false |
|
|
|
// Check if sources.value contains null or duplicate order |
|
for (const source of sources.value) { |
|
if (source.order === null || sourceOrderSet.has(source.order)) { |
|
hasNullOrDuplicates = true |
|
break |
|
} |
|
sourceOrderSet.add(source.order) |
|
} |
|
|
|
if (!hasNullOrDuplicates) return |
|
|
|
// make sure default source is always first |
|
sources.value = sources.value.sort((a, b) => { |
|
if (a.is_local || a.is_meta) return -1 |
|
if (b.is_local || b.is_meta) return 1 |
|
return (a.order ?? 0) - (b.order ?? 0) |
|
}) |
|
|
|
// update the local state |
|
sources.value = sources.value.map((source, i) => { |
|
return { |
|
...source, |
|
order: i + 1, |
|
} |
|
}) |
|
|
|
try { |
|
await Promise.all( |
|
sources.value.map(async (source) => { |
|
await $api.source.update(source.base_id as string, source.id as string, { |
|
id: source.id, |
|
base_id: source.base_id, |
|
order: source.order, |
|
}) |
|
}), |
|
) |
|
await loadProject(base.value.id as string, true) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
} |
|
|
|
async function loadBases(changed?: boolean) { |
|
try { |
|
if (changed) refreshCommandPalette() |
|
|
|
await until(() => !!base.value.id).toBeTruthy() |
|
isReloading.value = true |
|
vReload.value = true |
|
const baseList = await $api.source.list(base.value.id as string) |
|
if (baseList.list && baseList.list.length) { |
|
sources.value = baseList.list |
|
} |
|
await updateIfSourceOrderIsNullOrDuplicate() |
|
} catch (e) { |
|
console.error(e) |
|
} finally { |
|
vReload.value = false |
|
isReloading.value = false |
|
} |
|
} |
|
|
|
const baseAction = (sourceId?: string, action?: string) => { |
|
if (!sourceId) return |
|
activeBaseId.value = sourceId |
|
vState.value = action || '' |
|
} |
|
|
|
const openDeleteBase = (source: SourceType) => { |
|
$e('c:source:delete') |
|
isDeleteBaseModalOpen.value = true |
|
toBeDeletedBase.value = source |
|
} |
|
|
|
const deleteBase = async () => { |
|
if (!toBeDeletedBase.value) return |
|
|
|
try { |
|
await $api.source.delete(toBeDeletedBase.value.base_id as string, toBeDeletedBase.value.id as string) |
|
|
|
$e('a:source:delete') |
|
|
|
sources.value.splice(sources.value.indexOf(toBeDeletedBase.value), 1) |
|
await loadProject(base.value.id as string, true) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} finally { |
|
// TODO @mertmit |
|
refreshCommandPalette() |
|
} |
|
} |
|
const toggleBase = async (source: SourceType, state: boolean) => { |
|
try { |
|
if (!state && sources.value.filter((src) => src.enabled).length < 2) { |
|
message.info('There should be at least one enabled source!') |
|
return |
|
} |
|
source.enabled = state |
|
await $api.source.update(source.base_id as string, source.id as string, { |
|
id: source.id, |
|
base_id: source.base_id, |
|
enabled: source.enabled, |
|
}) |
|
await loadProject(base.value.id as string, true) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} finally { |
|
refreshCommandPalette() |
|
} |
|
} |
|
|
|
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 source = sources.value[e.newIndex] |
|
if (source) { |
|
let nextOrder: number |
|
|
|
// set new order value based on the new order of the items |
|
if (sources.value.length - 1 === e.newIndex) { |
|
// If moving to the end, set nextOrder greater than the maximum order in the list |
|
nextOrder = Math.max(...sources.value.map((item) => item?.order ?? 0)) + 1 |
|
} else { |
|
nextOrder = |
|
(parseFloat(String(sources.value[e.newIndex - 1]?.order ?? 0)) + |
|
parseFloat(String(sources.value[e.newIndex + 1]?.order ?? 0))) / |
|
2 |
|
} |
|
|
|
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : e.oldIndex |
|
|
|
await $api.source.update(source.base_id as string, source.id as string, { |
|
id: source.id, |
|
base_id: source.base_id, |
|
order: _nextOrder, |
|
}) |
|
} |
|
await loadProject(base.value.id as string, true) |
|
await loadBases() |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} finally { |
|
await refreshCommandPalette() |
|
} |
|
} |
|
|
|
watch( |
|
projectPageTab, |
|
() => { |
|
if (projectPageTab.value === 'data-source') { |
|
loadBases() |
|
} |
|
}, |
|
{ |
|
immediate: true, |
|
}, |
|
) |
|
|
|
watch( |
|
() => props.reload, |
|
async (reload) => { |
|
if (reload && !isReloading.value) { |
|
await loadBases() |
|
} |
|
}, |
|
) |
|
|
|
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 (isDataSourceLimitReached.value) { |
|
vState.value = '' |
|
} |
|
break |
|
} |
|
refreshCommandPalette() |
|
}, |
|
{ immediate: true }, |
|
) |
|
|
|
const isNewBaseModalOpen = computed({ |
|
get: () => { |
|
return [DataSourcesSubTab.New].includes(vState.value as any) |
|
}, |
|
set: (val) => { |
|
if (!val) { |
|
vState.value = '' |
|
} |
|
}, |
|
}) |
|
|
|
const activeSource = ref<SourceType | null>(null) |
|
const openedTab = ref('erd') |
|
|
|
const isSearchResultAvailable = () => { |
|
return ( |
|
sources.value.filter((s) => s?.alias?.toLowerCase()?.includes(searchQuery.value?.toLowerCase())).length || |
|
'default'.includes(searchQuery.value?.toLowerCase()) |
|
) |
|
} |
|
|
|
const isOpenModal = computed({ |
|
get: () => !!activeSource.value, |
|
set: (value) => { |
|
if (!value) { |
|
activeSource.value = null |
|
} |
|
}, |
|
}) |
|
|
|
const handleClickRow = (source: SourceType, tab?: string) => { |
|
if (tab && tab !== openedTab.value) { |
|
openedTab.value = tab |
|
} |
|
|
|
activeSource.value = source |
|
} |
|
</script> |
|
|
|
<template> |
|
<div class="flex flex-col h-full" data-testid="nc-settings-datasources-tab"> |
|
<div class="px-1 pt-3 mb-6 flex items-center justify-between gap-3"> |
|
<a-input |
|
v-model:value="searchQuery" |
|
type="text" |
|
class="nc-search-data-source-input !max-w-90 nc-input-sm" |
|
placeholder="Search data source" |
|
allow-clear |
|
> |
|
<template #prefix> |
|
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500" /> |
|
</template> |
|
</a-input> |
|
|
|
<NcButton |
|
v-if="!isDataSourceLimitReached && isUIAllowed('sourceCreate')" |
|
size="large" |
|
class="z-10 !px-2" |
|
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">{{ $t('activity.newSource') }}</div> |
|
</div> |
|
</NcButton> |
|
</div> |
|
<div |
|
data-testid="nc-settings-datasources" |
|
class="flex flex-row w-full nc-data-sources-view flex-grow min-h-0" |
|
:style="{ |
|
maxHeight: isNewBaseModalOpen ? '100%' : activeSource ? 'calc(100% - 46px)' : 'calc(100% - 66px)', |
|
}" |
|
> |
|
<NcModal |
|
v-model:visible="isOpenModal" |
|
centered |
|
size="large" |
|
wrap-class-name="nc-active-data-sources-view" |
|
@keydown.esc="activeSource = null" |
|
> |
|
<div v-if="activeSource" class="h-full"> |
|
<div class="px-4 pt-4 pb-2 flex items-center justify-between gap-3"> |
|
<a-breadcrumb separator=">" class="flex-1 cursor-pointer font-weight-bold"> |
|
<a-breadcrumb-item @click="activeSource = null"> |
|
<a class="!no-underline text-base">Data Sources</a> |
|
</a-breadcrumb-item> |
|
<a-breadcrumb-item v-if="activeSource"> |
|
<span class="capitalize text-base">{{ activeSource.alias || 'Default Source' }}</span> |
|
</a-breadcrumb-item> |
|
</a-breadcrumb> |
|
|
|
<NcButton size="small" type="text" class="nc-close-btn" @click="isOpenModal = false"> |
|
<GeneralIcon icon="close" class="text-gray-600" /> |
|
</NcButton> |
|
</div> |
|
|
|
<NcTabs v-model:activeKey="openedTab" class="nc-source-tab w-full h-[calc(100%_-_58px)] max-h-[calc(100%_-_58px)]"> |
|
<a-tab-pane v-if="!activeSource.is_meta && !activeSource.is_local" key="edit"> |
|
<template #tab> |
|
<div class="tab" data-testid="nc-connection-tab"> |
|
<div>{{ $t('labels.connectionDetails') }}</div> |
|
</div> |
|
</template> |
|
<div class="h-full"> |
|
<LazyDashboardSettingsDataSourcesEditBase |
|
:source-id="activeSource.id" |
|
@source-updated="loadBases(true)" |
|
@close="activeSource = null" |
|
/> |
|
</div> |
|
</a-tab-pane> |
|
<a-tab-pane key="erd"> |
|
<template #tab> |
|
<div class="tab" data-testid="nc-erd-tab"> |
|
<div>{{ $t('title.erdView') }}</div> |
|
</div> |
|
</template> |
|
<div class="h-full p-6"> |
|
<LazyDashboardSettingsErd |
|
class="h-full overflow-auto" |
|
:base-id="base.id" |
|
:source-id="activeSource.id" |
|
:show-all-columns="false" |
|
/> |
|
</div> |
|
</a-tab-pane> |
|
<a-tab-pane v-if="sources && activeSource === sources[0]" key="audit"> |
|
<template #tab> |
|
<div class="tab" data-testid="nc-audit-tab"> |
|
<div>{{ $t('title.auditLogs') }}</div> |
|
</div> |
|
</template> |
|
<div class="p-6 h-full"> |
|
<LazyDashboardSettingsBaseAudit :source-id="activeSource.id" /> |
|
</div> |
|
</a-tab-pane> |
|
<a-tab-pane key="acl"> |
|
<template #tab> |
|
<div class="tab" data-testid="nc-acl-tab"> |
|
<div>{{ $t('labels.uiAcl') }}</div> |
|
</div> |
|
</template> |
|
|
|
<div class="p-6 h-full"> |
|
<LazyDashboardSettingsUIAcl :source-id="activeSource.id" /> |
|
</div> |
|
</a-tab-pane> |
|
<a-tab-pane v-if="!activeSource.is_meta && !activeSource.is_local" key="meta-sync"> |
|
<template #tab> |
|
<div class="tab" data-testid="nc-meta-sync-tab"> |
|
<div>{{ $t('labels.metaSync') }}</div> |
|
</div> |
|
</template> |
|
<div class="p-6 h-full"> |
|
<LazyDashboardSettingsMetadata :source-id="activeSource.id" @source-synced="loadBases(true)" /> |
|
</div> |
|
</a-tab-pane> |
|
</NcTabs> |
|
</div> |
|
</NcModal> |
|
<div |
|
class="flex flex-col w-full" |
|
:class="{ |
|
'overflow-auto': !isNewBaseModalOpen, |
|
}" |
|
> |
|
<template v-if="isNewBaseModalOpen"> |
|
<DashboardSettingsDataSourcesCreateBase |
|
v-model:open="isNewBaseModalOpen" |
|
:connection-type="clientType" |
|
is-modal |
|
@source-created="loadBases(true)" |
|
/> |
|
</template> |
|
<div v-else class="ds-table overflow-y-auto nc-scrollbar-thin relative max-h-full mx-1 mb-4"> |
|
<div class="ds-table-head sticky top-0 bg-white z-10"> |
|
<div class="ds-table-row !border-0"> |
|
<div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div> |
|
<div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div> |
|
<div class="ds-table-col ds-table-integration-name">{{ $t('general.connection') }} {{ $t('general.name') }}</div> |
|
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div> |
|
<div class="ds-table-col ds-table-actions">{{ $t('labels.actions') }}</div> |
|
</div> |
|
</div> |
|
<div class="ds-table-body relative"> |
|
<Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase"> |
|
<template v-if="'default'.includes(searchQuery.toLowerCase())" #header> |
|
<div |
|
v-if="sources[0]" |
|
class="ds-table-row border-gray-200 cursor-pointer" |
|
@click="handleClickRow(sources[0], 'erd')" |
|
> |
|
<div class="ds-table-col ds-table-enabled"> |
|
<div class="flex items-center gap-1" @click.stop> |
|
<div v-if="sources.length > 2" class="ds-table-handle" /> |
|
<NcTooltip> |
|
<template #title> |
|
<template v-if="sources[0].enabled">{{ $t('activity.hideInUI') }}</template> |
|
<template v-else>{{ $t('activity.showInUI') }}</template> |
|
</template> |
|
<a-switch |
|
:checked="sources[0].enabled ? true : false" |
|
class="cursor-pointer" |
|
size="small" |
|
@change="toggleBase(sources[0], $event)" |
|
/> |
|
</NcTooltip> |
|
</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" /> --> |
|
{{ $t('general.default') }} |
|
</div> |
|
</div> |
|
|
|
<div class="ds-table-col ds-table-integration-name"> |
|
<div class="flex items-center gap-1">-</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"> |
|
<NcButton |
|
v-if="!sources[0].is_meta && !sources[0].is_local" |
|
size="small" |
|
class="nc-action-btn nc-edit-base cursor-pointer outline-0 !w-8 !px-1 !rounded-lg" |
|
type="text" |
|
@click.stop="baseAction(sources[0].id, DataSourcesSubTab.Edit)" |
|
> |
|
<GeneralIcon icon="edit" class="text-gray-600" /> |
|
</NcButton> |
|
</div> |
|
</div> |
|
</template> |
|
<template #item="{ element: source, index }"> |
|
<div |
|
v-if="index !== 0" |
|
class="ds-table-row border-gray-200 cursor-pointer" |
|
:class="{ |
|
'!hidden': !source?.alias?.toLowerCase()?.includes(searchQuery.toLowerCase()), |
|
}" |
|
@click="handleClickRow(source, 'edit')" |
|
> |
|
<div class="ds-table-col ds-table-enabled"> |
|
<div class="flex items-center gap-1" @click.stop> |
|
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" /> |
|
<NcTooltip> |
|
<template #title> |
|
<template v-if="source.enabled">{{ $t('activity.hideInUI') }}</template> |
|
<template v-else>{{ $t('activity.showInUI') }}</template> |
|
</template> |
|
<a-switch |
|
:checked="source.enabled ? true : false" |
|
class="cursor-pointer" |
|
size="small" |
|
@change="toggleBase(source, $event)" |
|
/> |
|
</NcTooltip> |
|
</div> |
|
</div> |
|
<div class="ds-table-col ds-table-name font-medium w-full"> |
|
<div v-if="source.is_meta || source.is_local" class="h-8 w-1">-</div> |
|
|
|
<NcTooltip v-else class="truncate" show-on-truncate-only> |
|
<template #title> |
|
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }} |
|
</template> |
|
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }} |
|
</NcTooltip> |
|
</div> |
|
<div class="ds-table-col ds-table-integration-name font-medium w-full"> |
|
<NcTooltip class="truncate" show-on-truncate-only> |
|
<template #title> |
|
{{ source?.integration_title || '-' }} |
|
</template> |
|
{{ source?.integration_title || '-' }} |
|
</NcTooltip> |
|
</div> |
|
|
|
<div class="ds-table-col ds-table-type"> |
|
<NcBadge rounded="lg" class="flex items-center gap-2 px-2 py-1 !h-7 truncate !border-transparent"> |
|
<GeneralBaseLogo :source-type="source.type" class="flex-none !w-4 !h-4" /> |
|
<NcTooltip placement="bottom" show-on-truncate-only class="text-sm truncate"> |
|
<template #title> {{ clientTypesMap[source.type]?.text || source.type }}</template> |
|
|
|
{{ source.type && clientTypesMap[source.type] ? clientTypesMap[source.type]?.text : source.type }} |
|
</NcTooltip> |
|
</NcBadge> |
|
</div> |
|
<div class="ds-table-col justify-end gap-x-1 ds-table-actions" @click.stop> |
|
<div class="flex justify-end"> |
|
<NcDropdown v-if="!source.is_meta && !source.is_local" placement="bottomRight"> |
|
<NcButton size="small" type="secondary"> |
|
<GeneralIcon icon="threeDotVertical" /> |
|
</NcButton> |
|
<template #overlay> |
|
<NcMenu> |
|
<NcMenuItem @click="handleClickRow(source, 'edit')"> |
|
<GeneralIcon class="text-gray-800" icon="edit" /> |
|
<span>{{ $t('general.edit') }}</span> |
|
</NcMenuItem> |
|
|
|
<NcDivider /> |
|
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click.stop="openDeleteBase(source)"> |
|
<GeneralIcon icon="delete" /> |
|
{{ $t('general.remove') }} |
|
</NcMenuItem> |
|
</NcMenu> |
|
</template> |
|
</NcDropdown> |
|
</div> |
|
</div> |
|
</div> |
|
</template> |
|
</Draggable> |
|
|
|
<div |
|
v-if="!isReloading && sources?.length && !isSearchResultAvailable()" |
|
class="flex-none integration-table-empty flex items-center justify-center py-8 px-6" |
|
> |
|
<div class="px-2 py-6 text-gray-500 flex flex-col items-center gap-6 text-center"> |
|
<img |
|
src="~assets/img/placeholder/no-search-result-found.png" |
|
class="!w-[164px] flex-none" |
|
alt="No search results found" |
|
/> |
|
|
|
{{ $t('title.noResultsMatchedYourSearch') }} |
|
</div> |
|
</div> |
|
</div> |
|
<div |
|
v-show="isReloading" |
|
class="flex items-center justify-center absolute left-0 top-0 w-full h-[calc(100%_-_45px)] z-10 pb-10 pointer-events-none" |
|
> |
|
<div class="flex flex-col justify-center items-center gap-2"> |
|
<GeneralLoader size="xlarge" /> |
|
<span class="text-center">{{ $t('general.loading') }}</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<GeneralDeleteModal |
|
v-model:visible="isDeleteBaseModalOpen" |
|
:entity-name="$t('general.datasource')" |
|
:on-delete="deleteBase" |
|
:delete-label="$t('general.remove')" |
|
> |
|
<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 :source-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> |
|
</div> |
|
</template> |
|
|
|
<style scoped lang="scss"> |
|
.ds-table { |
|
@apply border-1 border-gray-200 rounded-lg h-full; |
|
} |
|
.ds-table-head { |
|
@apply flex items-center border-b-1 text-gray-500 bg-gray-50 text-sm font-weight-500; |
|
} |
|
|
|
.ds-table-body { |
|
@apply flex flex-col; |
|
} |
|
|
|
.ds-table-row { |
|
@apply grid grid-cols-18 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-integration-name { |
|
@apply col-span-5 items-center capitalize; |
|
} |
|
|
|
.ds-table-type { |
|
@apply col-span-3 items-center; |
|
} |
|
|
|
.ds-table-actions { |
|
@apply col-span-2 flex w-full justify-center; |
|
} |
|
|
|
.ds-table-col:last-child { |
|
@apply border-r-0; |
|
} |
|
|
|
.ds-table-handle { |
|
@apply cursor-pointer justify-self-start mr-2 w-[16px]; |
|
} |
|
.ds-table-body .ds-table-row:hover { |
|
@apply bg-gray-50/60; |
|
} |
|
|
|
:deep(.ant-tabs-content), |
|
:deep(.ant-tabs) { |
|
@apply !h-full; |
|
} |
|
:deep(.ant-tabs-content-holder) { |
|
@apply !min-h-0 !flex-shrink; |
|
} |
|
</style> |
|
|
|
<style lang="scss"> |
|
.nc-active-data-sources-view { |
|
.ant-modal-content { |
|
@apply overflow-hidden; |
|
} |
|
.nc-modal { |
|
@apply !p-0; |
|
height: min(calc(100vh - 100px), 1024px); |
|
max-height: min(calc(100vh - 100px), 1024px) !important; |
|
} |
|
} |
|
</style>
|
|
|