feat: redesign integrations page — replace tabs with single-page layout

This commit is contained in:
Ramesh Mane
2026-04-10 09:37:38 +00:00
parent 4c16ab5326
commit f00dfd83a7
7 changed files with 807 additions and 270 deletions

View File

@@ -0,0 +1,297 @@
<script setup lang="ts">
import type { IntegrationType, UserType, WorkspaceUserType } from 'nocodb-sdk'
import { IntegrationsType } from 'nocodb-sdk'
interface Props {
connections: IntegrationType[]
totalCount: number
maxVisible?: number
}
const props = withDefaults(defineProps<Props>(), {
maxVisible: 6,
})
const emits = defineEmits<{
(e: 'view-all'): void
}>()
const { t } = useI18n()
const {
editIntegration,
deleteIntegration,
getIntegration,
loadIntegrations,
deleteConfirmText,
successConfirmModal,
} = useIntegrationStore()
const { allCollaborators } = storeToRefs(useWorkspace())
const { bases } = storeToRefs(useBases())
const collaboratorsMap = computed<Map<string, (WorkspaceUserType & { id: string }) | UserType>>(() => {
const map = new Map()
allCollaborators.value?.forEach((coll: any) => {
if (coll?.id) {
map.set(coll.id, coll)
}
})
return map
})
const filteredConnections = computed(() =>
(props.connections || []).filter((i) => IntegrationsType.Sync !== i.type),
)
const visibleConnections = computed(() => {
return filteredConnections.value.slice(0, props.maxVisible)
})
const filteredTotalCount = computed(() => filteredConnections.value.length)
const overflowCount = computed(() => {
return Math.max(0, filteredTotalCount.value - props.maxVisible)
})
// Delete integration handling
const isDeleteModalOpen = ref(false)
const isLoadingGetLinkedSources = ref(false)
const toBeDeletedIntegration = ref<
| (IntegrationType & {
sources?: {
id: string
alias: string
project_title: string
base_id: string
}[]
})
| null
>(null)
const openDeleteIntegration = async (integration: IntegrationType) => {
isLoadingGetLinkedSources.value = true
deleteConfirmText.value = null
isDeleteModalOpen.value = true
toBeDeletedIntegration.value = integration
const connectionDetails = await getIntegration(integration, {
includeSources: true,
})
toBeDeletedIntegration.value.sources = connectionDetails?.sources || []
isLoadingGetLinkedSources.value = false
}
const onDeleteConfirm = async () => {
const isDeleted = await deleteIntegration(toBeDeletedIntegration.value, true)
if (isDeleted) {
for (const source of toBeDeletedIntegration.value?.sources || []) {
if (!source.base_id || !source.id || (source.base_id && !bases.value.get(source.base_id))) {
continue
}
const base = bases.value.get(source.base_id)
if (!Array.isArray(base?.sources)) {
continue
}
bases.value.set(source.base_id, {
...(base || {}),
sources: [...base.sources.filter((s) => s.id !== source.id)],
})
}
}
}
// Base assignment dialog state
const isBaseAssignmentOpen = ref(false)
const baseAssignmentIntegration = ref<IntegrationType | null>(null)
const openBaseAssignment = (integration: IntegrationType) => {
baseAssignmentIntegration.value = integration
isBaseAssignmentOpen.value = true
}
const onBaseAssignmentUpdated = () => {
loadIntegrations()
}
const handleEdit = (integration: IntegrationType) => {
editIntegration(integration)
}
</script>
<template>
<div class="nc-active-connections-section">
<!-- Section header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-nc-content-gray mb-0">
{{ t('general.activeConnections') }}
</h3>
<NcBadge v-if="filteredTotalCount" :border="false" class="bg-nc-bg-gray-extralight text-nc-content-gray-subtle2 text-xs">
{{ filteredTotalCount }}
</NcBadge>
</div>
<NcButton v-if="filteredTotalCount > 0" type="text" size="small" class="!text-nc-content-brand" @click="emits('view-all')">
{{ t('general.viewAllConnections') }}
<GeneralIcon icon="arrowRight" class="ml-1" />
</NcButton>
</div>
<!-- Connection cards grid -->
<div class="nc-connection-cards-grid">
<WorkspaceIntegrationsConnectionCard
v-for="connection in visibleConnections"
:key="connection.id"
:integration="connection"
:collaborators-map="collaboratorsMap"
@edit="handleEdit"
@delete="openDeleteIntegration"
@base-assignment="openBaseAssignment"
/>
<!-- Overflow card -->
<div
v-if="overflowCount > 0"
class="nc-connection-overflow-card"
@click="emits('view-all')"
>
<div class="text-sm font-semibold text-nc-content-gray">
+{{ overflowCount }} {{ t('general.more') }}
</div>
<div class="text-xs text-nc-content-gray-subtle2">
{{ t('general.viewAllConnections') }}
</div>
</div>
</div>
<!-- Delete confirmation modal -->
<GeneralDeleteModal
v-model:visible="isDeleteModalOpen"
:entity-name="$t('general.connection')"
:on-delete="onDeleteConfirm"
:delete-label="$t('general.delete')"
:show-default-delete-msg="!isLoadingGetLinkedSources && !toBeDeletedIntegration?.sources?.length"
>
<template #entity-preview>
<template v-if="isLoadingGetLinkedSources">
<div class="rounded-lg overflow-hidden">
<a-skeleton-input active class="h-9 !rounded-md !w-full" />
</div>
<div class="rounded-lg overflow-hidden mt-2">
<a-skeleton-input active class="h-9 !rounded-md !w-full" />
</div>
</template>
<div v-else-if="toBeDeletedIntegration" class="w-full flex flex-col text-nc-content-gray">
<div
class="flex flex-row items-center py-2 px-3.25 bg-nc-bg-gray-extralight rounded-lg text-nc-content-inverted-secondary mb-4"
>
<GeneralIntegrationIcon :type="toBeDeletedIntegration.sub_type" />
<div
class="text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ toBeDeletedIntegration.title }}
</div>
</div>
<div
v-if="toBeDeletedIntegration?.sources?.length"
class="flex flex-col pb-2 text-small leading-[18px] text-nc-content-gray-muted"
>
<div class="mb-1">Following external data sources using this connection will also be removed</div>
<ul class="!list-disc ml-6 mb-0">
<li
v-for="(source, idx) of toBeDeletedIntegration.sources"
:key="idx"
class="marker:text-nc-content-gray-muted"
>
<div class="flex items-center gap-1">
<GeneralProjectIcon
type="database"
class="!grayscale min-w-5 flex-none"
:style="{ filter: 'grayscale(100%) brightness(115%)' }"
/>
<NcTooltip class="!truncate !max-w-[45%] flex-none" show-on-truncate-only>
<template #title>{{ source.project_title }}</template>
{{ source.project_title }}
</NcTooltip>
>
<GeneralBaseLogo
class="!grayscale min-w-4 flex-none"
:style="{ filter: 'grayscale(100%) brightness(115%)' }"
/>
<NcTooltip class="truncate !max-w-[45%] capitalize" show-on-truncate-only>
<template #title>{{ source.alias }}</template>
{{ source.alias }}
</NcTooltip>
</div>
</li>
</ul>
<div class="mt-2">Do you want to proceed anyway?</div>
</div>
</div>
</template>
</GeneralDeleteModal>
<!-- Success modal -->
<NcModal v-model:visible="successConfirmModal.isOpen" centered size="small" @keydown.esc="successConfirmModal.isOpen = false">
<div class="flex gap-4">
<div>
<GeneralIcon icon="circleCheckSolid" class="flex-none !text-green-700 mt-0.5 !h-6 !w-6" />
</div>
<div class="flex flex-col gap-3">
<div class="flex">
<h3 class="!m-0 text-base font-weight-700 flex-1">{{ successConfirmModal.title }}</h3>
<NcButton size="xsmall" type="text" @click="successConfirmModal.isOpen = false">
<GeneralIcon icon="close" class="text-nc-content-gray-subtle2" />
</NcButton>
</div>
<div class="text-sm text-nc-content-inverted-secondary">{{ successConfirmModal.description }}</div>
<a target="_blank" href="https://nocodb.com/docs/product-docs/data-sources/connect-to-data-source" rel="noopener noreferrer">
Learn more
</a>
</div>
</div>
</NcModal>
<!-- Base assignment dialog (EE) -->
<WorkspaceIntegrationsBaseAssignment
v-if="isEeUI && baseAssignmentIntegration"
v-model:visible="isBaseAssignmentOpen"
:integration="baseAssignmentIntegration"
@updated="onBaseAssignmentUpdated"
/>
</div>
</template>
<style lang="scss" scoped>
.nc-active-connections-section {
.nc-connection-cards-grid {
@apply grid gap-4;
grid-template-columns: repeat(3, 1fr);
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
.nc-connection-overflow-card {
@apply flex flex-col items-center justify-center gap-1 border-1 border-dashed border-nc-border-gray-medium rounded-xl p-3 cursor-pointer transition-all duration-200;
&:hover {
@apply bg-nc-bg-gray-extralight border-nc-border-gray-dark;
}
}
}
</style>