mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-02 15:16:40 +00:00
feat: redesign integrations page — replace tabs with single-page layout
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user