mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-02 22:57:08 +00:00
Nc feat/integrations (#8903)
* feat: integrations backend (WIP) * feat: migration - source table * feat: updated migration * feat: integration APIs - WIP * feat: integration - crud, acl, api tests * feat: integration - crud, acl, api tests * feat: integration - GUI integration * feat: private integration config * feat: integration GUI * feat: delete api and source creation * feat: add hint for input fields * fix: source creation bugs * refactor: placeholder text correction * refactor: include context * feat: integration delete with transaction * refactor: permission scope correction and move ee logic * refactor: migration correction and improvements * feat: confirm dialog * refactor: review comments * refactor: meta service changes * feat: add oss support - WIP * feat: add oss support * refactor: coderabbt suggestions * refactor: exclude config from api response * refactor: coderabbit review comments * refactor: rename migration names * fix: method name correction * fix(nc-gui): integration ui changes * fix(nc-gui): add edit integration ui changes * fix(nc-gui): add shared badge in integrations list * feat(nc-gui): duplicate integration * fix(nocodb): add copy from id integration support in create integration api * fix(nc-gui): update useIntegration store * fix(nc-gui): test connection btn style update * fix(nc-gui): update new integration modal * feat(nc-gui): add sort integration list support * fix(nc-gui): integration table to be center aligned * fix(nc-gui): move form item required mark to right side * fix: remove divider * fix(nc-gui): add input shadow * fix(nc-gui): base name validator error message * fix(nc-gui): add border if search connection input has some value * fix(nc-gui): add close btn in integration modal * chore(nc-gui): lint * fix(nc-gui): pr review changes * chore(nc-gui): cleanup unused code * chore(nc-gui): lint * fix(nc-gui): integrationsType not found issue * fix(nc-gui): update data source table * fix(nc-gui): populate integration name only on input value change * fix(nc-gui): create data source form update * fix: type correction * fix: label correction * fix: font corrections * fix: remove help text * fix: grammar in help text * fix(nc-gui): edit source ui changes * fix(nc-gui): base settings modal changes & datasource search feat * fix(nc-gui): update data source table * fix(nc-gui): move integrations outside team & settings * fix(nc-gui): make connections table full width * fix(nc-gui): modal height issue in small screen * fix(nc-gui): disable editing selected connection in edit data source * fix(nc-gui): add data sources in base settings tab * fix(nc-gui): ant design multiple warnings issue * fix(nc-gui): create source page scrollbar issue * feat(nc-gui): create connection from create source page * chore(nc-gui): lint * fix(nc-gui): update project members tab content margin * chore: label text change * fix: font changes * chore: font corrections * chore: integration => connection * fix(nc-gui): disable auto editing database name on changing connection name * fix(nc-gui): table header overflow issue * fix(nc-gui): show connection crud operation messages in toast * feat(nc-gui): request new integration ui * fix(nc-gui): text area height adjust issue * fix(nc-gui): add connection from source create issue * fix(nc-gui): show data source details in modal * fix(nc-gui): hide private connection option * fix(nc-gui): user should able to edit & save connection without test connection if only title updated * fix(nc-gui): add integration page in oss * fix(nc-gui): typo currection * fix(nc-gui): oss create base ui changes * misc: minor formatting changes * misc: formatting corrections * fix(nc-gui): overlay close btn issue * fix(nc-gui): some review changes * fix(nc-gui): remove link beetween connection name & database name * fix(nc-gui): update edit base/source modal oss * fix(nc-gui): add db type icon in select connection * chore(nc-gui): lint * fix: integration list - allow access based on base level role * fix(nc-gui): load integrations on creating integration from source create issue * fix(nc-gui): add connection count in tab * fix: correction in soft delete logic * fix(nc-gui): reset use ssl on panel collapse * fix(nc-gui): reduce select input font weight * fix(nc-gui): update connection edit access control * fix: integration read api correction * fix(nc-gui): some review changes * fix(nc-gui): labels update * fix(nc-gui): udpate text in delete modal integration -> connection * fix: remove permission from wrong scope * refactor: swagger description correction * fix(nc-gui): remove connection between source name & database name * fix(nc-gui): test connection is not needed form source name. inflection field changes * refactor: include integration title with source * feat: integration pagination * fix: remove unused prop * fix(nc-gui): update all tables tab btns tooltip * feat: new integration request * refactor: replace delete statement and use assigning undefined for better performance * feat(nc-gui): sync data support in project page * fix(nc-gui): all sync data type list * fix(nc-gui): close sync data modal issue * fix(nc-gui): add bg gray color on db icon of tooltip * fix(nc-gui): make connection as required field * fix(nc-gui): show connection name if not found and reload page * fix(nc-gui): show connection name in ds list * fix(nc-gui): ssl related changes * fix: oss permission * fix(nc-gui): active tab issue on clicking source * feat: include source count and sources in api response * fix(nc-gui): add getIntegration fun in useIntegrationStore * fix(nc-gui): source list udpate issue on updating source details * fix(nc-gui): fix external source icon alignment * feat: include base name and source count * fix: query correction * fix(nc-gui): show liked sources list in delete connection modal * fix(nc-gui): display connection usage information in list * fix(nc-gui): add sync data types icons * fix(nc-gui): add pagination support in connection list * fix(nc-gui): connection pagination issue * fix(nc-gui): connection tab count update issue * test(nc-gui): some of test cases updated * fix(nc-gui): some minor review changes * fix(nc-gui): minor ui changes * fix(nc-gui): Cannot read properties of undefined (reading 'sub_type') * fix(nc-gui): udpate all tables btn text * fix(nc-gui): ui changes * fix(nc-gui): overflow issue * fix(nc-gui): add connection icon & back btn in modal * fix(nc-gui): some minor ui changes * test(nc-gui): update source restriction test cases * chore(test): remove only from test * fix(nc-gui): update style of delete connection modal * test(nc-gui): update acl pw test cases * fix(test): ws collaboration role accss test fail issue * fix(nc-gui): add connection successfully added modal * fix(nc-gui): update connection added modal * fix(nc-gui): trigger sync request event on upvote * chore(nc-gui): lint * fix(nc-gui): add learn more btn in connection successfull modal * fix(nc-gui): add integration docs link support * fix(nc-gui): integration table name field text truncate issue * fix: misc corrections * misc: button width change * fix(nc-gui): update icons * fix(nc-gui): update test connection btn icons * fix(nc-gui): all tables btn gap issue * feat(nc-gui): search option in sync data modal * feat(nc-gui): search connection through api * fix(nc-gui): add base and source icon in delete connection modal * fix: update sync request event * fix(nc-gui): rebase conflict issue * fix: connections text length * fix(nc-gui): enable integration/create source supported docs option * fix(nc-gui): update advanced option header style --------- Co-authored-by: mertmit <mertmit99@gmail.com> Co-authored-by: Ramesh Mane <101566080+rameshmane7218@users.noreply.github.com> Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
This commit is contained in:
@@ -263,7 +263,7 @@ onKeyStroke('ArrowDown', onDown)
|
||||
ref="tableWrapper"
|
||||
class="nc-audit-logs-table h-full max-h-[calc(100%_-_40px)] relative nc-scrollbar-thin !overflow-auto"
|
||||
>
|
||||
<table class="!sticky top-0 z-10">
|
||||
<table class="!sticky top-0 z-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
@@ -630,11 +630,9 @@ onKeyStroke('ArrowDown', onDown)
|
||||
tr {
|
||||
@apply cursor-pointer;
|
||||
|
||||
.td {
|
||||
@apply text-small leading-[18px] text-gray-600;
|
||||
}
|
||||
|
||||
td {
|
||||
@apply text-sm text-gray-600;
|
||||
|
||||
&.cell-user {
|
||||
@apply sticky left-0 z-4 bg-white;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const nameValidationRules = [
|
||||
required: true,
|
||||
message: t('msg.info.dbNameRequired'),
|
||||
},
|
||||
baseTitleValidator,
|
||||
baseTitleValidator(),
|
||||
] as RuleObject[]
|
||||
|
||||
const form = ref<typeof Form>()
|
||||
|
||||
@@ -71,7 +71,7 @@ onMounted(() => {
|
||||
<div v-if="currentWorkspace" class="flex w-full px-6 max-w-[97.5rem] flex-col nc-workspace-settings">
|
||||
<div v-if="!props.workspaceId" class="flex gap-2 items-center min-w-0 py-4">
|
||||
<h1 class="text-base capitalize font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize">
|
||||
{{ currentWorkspace?.title }} > {{ $t('title.teamAndSettings') }}
|
||||
<span class="text-gray-500">{{ currentWorkspace?.title }} ></span> {{ $t('title.teamAndSettings') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -99,7 +99,7 @@ onMounted(() => {
|
||||
<template v-if="isUIAllowed('workspaceSettings')">
|
||||
<a-tab-pane key="collaborators" class="w-full">
|
||||
<template #tab>
|
||||
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
|
||||
<div class="flex flex-row items-center pb-1 gap-x-1.5">
|
||||
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
|
||||
Members
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@ onMounted(() => {
|
||||
<template v-if="isUIAllowed('workspaceManage')">
|
||||
<a-tab-pane key="settings" class="w-full">
|
||||
<template #tab>
|
||||
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-settings">
|
||||
<div class="flex flex-row items-center pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-settings">
|
||||
<GeneralIcon icon="settings" />
|
||||
Settings
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@ onMounted(() => {
|
||||
<template v-if="isUIAllowed('workspaceAuditList') && !props.workspaceId">
|
||||
<a-tab-pane key="audit" class="w-full">
|
||||
<template #tab>
|
||||
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
|
||||
<div class="flex flex-row items-center pb-1 gap-x-1.5">
|
||||
<GeneralIcon icon="audit" class="!h-3.5 !w-3.5" />
|
||||
Audit Logs
|
||||
</div>
|
||||
|
||||
60
packages/nc-gui/components/workspace/integrations/Edit.vue
Normal file
60
packages/nc-gui/components/workspace/integrations/Edit.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
const { pageMode, IntegrationsPageMode, activeIntegration, categories, activeCategory } = useIntegrationStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col nc-workspace-settings-integrations-new">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center p-6">
|
||||
<div class="cursor-pointer text-primary mr-4" @click="pageMode = IntegrationsPageMode.LIST">
|
||||
<GeneralIcon icon="arrowLeft" />
|
||||
Back
|
||||
</div>
|
||||
<WorkspaceIntegrationsIcon :integration-type="activeIntegration.type" size="sm" />
|
||||
<div class="text-md font-bold">New {{ activeIntegration.title }}</div>
|
||||
</div>
|
||||
<div class="border-b-1 border-gray-200 mx-4"></div>
|
||||
</div>
|
||||
<div class="panel-view">
|
||||
<div class="panel-indices">
|
||||
<div v-for="ct of categories" :key="ct.label" class="panel-index">
|
||||
<div class="flex items-center gap-2" :class="{ 'text-primary': activeCategory?.label === ct.label }">
|
||||
<div class="logo-wrapper">
|
||||
<GeneralIcon :icon="ct.icon" />
|
||||
</div>
|
||||
<div class="text-sm">{{ ct.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeIntegration" class="panel">
|
||||
<WorkspaceIntegrationsFormsEditDatabase />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo-wrapper {
|
||||
@apply bg-gray-200 p-2 mr-2 rounded-lg flex items-center justify-center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.panel-view {
|
||||
@apply flex gap-2 mx-4 mt-6;
|
||||
max-width: 1024px;
|
||||
|
||||
.panel {
|
||||
@apply w-3/4;
|
||||
}
|
||||
|
||||
.panel-indices {
|
||||
@apply mr-4 flex flex-col cursor-default;
|
||||
|
||||
.panel-index {
|
||||
@apply p-2 rounded-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{ loadDatasourceInfo?: boolean; baseId?: string }>()
|
||||
|
||||
const { loadDatasourceInfo, baseId } = toRefs(props)
|
||||
|
||||
const { pageMode, IntegrationsPageMode, integrationType, activeIntegration } = useIntegrationStore()
|
||||
|
||||
const isEditOrAddIntegrationModalOpen = computed({
|
||||
get: () => {
|
||||
return pageMode.value === IntegrationsPageMode.ADD || pageMode.value === IntegrationsPageMode.EDIT
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (!value) {
|
||||
pageMode.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const connectionType = computed(() => {
|
||||
switch (
|
||||
pageMode.value === IntegrationsPageMode.EDIT
|
||||
? activeIntegration.value?.sub_type || activeIntegration.value?.config?.client
|
||||
: activeIntegration.value?.type
|
||||
) {
|
||||
case integrationType.PostgreSQL:
|
||||
return ClientType.PG
|
||||
case integrationType.MySQL:
|
||||
return ClientType.MYSQL
|
||||
default: {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcModal
|
||||
v-model:visible="isEditOrAddIntegrationModalOpen"
|
||||
size="large"
|
||||
wrap-class-name="nc-modal-edit-or-add-integration"
|
||||
@keydown.esc="isEditOrAddIntegrationModalOpen = false"
|
||||
>
|
||||
<div v-if="connectionType" class="h-full">
|
||||
<WorkspaceIntegrationsFormsEditOrAddDatabase
|
||||
v-model:open="isEditOrAddIntegrationModalOpen"
|
||||
:connection-type="connectionType"
|
||||
:load-datasource-info="loadDatasourceInfo"
|
||||
:base-id="baseId"
|
||||
/>
|
||||
</div>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-edit-or-add-integration {
|
||||
.nc-modal {
|
||||
@apply !p-0;
|
||||
height: min(calc(100vh - 100px), 1024px);
|
||||
max-height: min(calc(100vh - 100px), 1024px) !important;
|
||||
|
||||
.nc-edit-or-add-integration-left-panel {
|
||||
@apply w-full p-6 flex-1 flex justify-center;
|
||||
}
|
||||
.nc-edit-or-add-integration-right-panel {
|
||||
@apply p-5 w-[320px] border-l-1 border-gray-200 flex flex-col gap-4 bg-gray-50 rounded-br-2xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
packages/nc-gui/components/workspace/integrations/Icon.vue
Normal file
70
packages/nc-gui/components/workspace/integrations/Icon.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
integrationType: string
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||
}>(),
|
||||
{
|
||||
size: 'md',
|
||||
},
|
||||
)
|
||||
|
||||
const { integrationType: integrationTypeOrigin } = useIntegrationStore()
|
||||
|
||||
const { size, integrationType } = toRefs(props)
|
||||
|
||||
const pxSize = computed(() => {
|
||||
switch (size.value) {
|
||||
case 'xs':
|
||||
return '16px'
|
||||
case 'sm':
|
||||
return '24px'
|
||||
case 'md':
|
||||
return '32px'
|
||||
case 'lg':
|
||||
return '48px'
|
||||
}
|
||||
})
|
||||
const pxWrapperPadding = computed(() => {
|
||||
switch (size.value) {
|
||||
case 'xs':
|
||||
return '8px'
|
||||
case 'sm':
|
||||
return '8px'
|
||||
default:
|
||||
return '10px'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="logo-wrapper"
|
||||
:style="{
|
||||
padding: pxWrapperPadding,
|
||||
}"
|
||||
>
|
||||
<GeneralBaseLogo
|
||||
v-if="integrationType === integrationTypeOrigin.MySQL"
|
||||
source-type="mysql2"
|
||||
:style="{ width: pxSize, height: pxSize }"
|
||||
/>
|
||||
<GeneralBaseLogo
|
||||
v-else-if="integrationType === integrationTypeOrigin.PostgreSQL"
|
||||
source-type="pg"
|
||||
:style="{ width: pxSize, height: pxSize }"
|
||||
/>
|
||||
<GeneralIcon
|
||||
v-else-if="integrationType === 'request'"
|
||||
icon="plusSquare"
|
||||
class="text-gray-700"
|
||||
:style="{ width: pxSize, height: pxSize }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo-wrapper {
|
||||
@apply bg-gray-200 rounded-lg flex items-center justify-center;
|
||||
}
|
||||
</style>
|
||||
803
packages/nc-gui/components/workspace/integrations/List.vue
Normal file
803
packages/nc-gui/components/workspace/integrations/List.vue
Normal file
@@ -0,0 +1,803 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IntegrationType, UserType, WorkspaceUserType } from 'nocodb-sdk'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
type SortFields = 'title' | 'sub_type' | 'created_at' | 'created_by' | 'source_count'
|
||||
|
||||
const {
|
||||
integrations,
|
||||
isLoadingIntegrations,
|
||||
deleteConfirmText,
|
||||
integrationPaginationData,
|
||||
successConfirmModal,
|
||||
searchQuery,
|
||||
loadIntegrations,
|
||||
deleteIntegration,
|
||||
editIntegration,
|
||||
duplicateIntegration,
|
||||
getIntegration,
|
||||
} = useIntegrationStore()
|
||||
|
||||
const { $api, $e } = useNuxtApp()
|
||||
|
||||
const { collaborators } = storeToRefs(useWorkspace())
|
||||
|
||||
const isDeleteIntegrationModalOpen = ref(false)
|
||||
const toBeDeletedIntegration = ref<
|
||||
| (IntegrationType & {
|
||||
sources?: {
|
||||
id: string
|
||||
alias: string
|
||||
project_title: string
|
||||
base_id: string
|
||||
}[]
|
||||
})
|
||||
| null
|
||||
>(null)
|
||||
|
||||
const isLoadingGetLinkedSources = ref(false)
|
||||
|
||||
const tableWrapper = ref<HTMLDivElement>()
|
||||
|
||||
const titleHeaderCellRef = ref<HTMLDivElement>()
|
||||
|
||||
const orderBy = ref<Partial<Record<SortFields, 'asc' | 'desc' | undefined>>>({})
|
||||
|
||||
const localCollaborators = ref<User[] | UserType[]>([])
|
||||
|
||||
const { width } = useElementBounding(titleHeaderCellRef)
|
||||
|
||||
const collaboratorsMap = computed<Map<string, (WorkspaceUserType & { id: string }) | User | UserType>>(() => {
|
||||
const map = new Map()
|
||||
|
||||
;(isEeUI ? collaborators.value : localCollaborators.value)?.forEach((coll) => {
|
||||
if (coll?.id) {
|
||||
map.set(coll.id, coll)
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const filteredIntegrations = computed(() =>
|
||||
(integrations.value || []).sort((a, b) => {
|
||||
if (orderBy.value.title) {
|
||||
if (a.title && b.title) {
|
||||
return orderBy.value.title === 'asc' ? (a.title < b.title ? -1 : 1) : a.title > b.title ? -1 : 1
|
||||
}
|
||||
} else if (orderBy.value.sub_type) {
|
||||
if (a.sub_type && b.sub_type) {
|
||||
return orderBy.value.sub_type === 'asc' ? (a.sub_type < b.sub_type ? -1 : 1) : a.sub_type > b.sub_type ? -1 : 1
|
||||
}
|
||||
} else if (orderBy.value.created_at) {
|
||||
if (a?.created_at && b?.created_at) {
|
||||
return orderBy.value.created_at === 'asc'
|
||||
? dayjs(a.created_at).local().isBefore(dayjs(b.created_at).local())
|
||||
? -1
|
||||
: 1
|
||||
: dayjs(a.created_at).local().isAfter(dayjs(b.created_at).local())
|
||||
? -1
|
||||
: 1
|
||||
}
|
||||
} else if (orderBy.value.created_by) {
|
||||
if (a.created_by && b.created_by && collaboratorsMap.value.get(a.created_by) && collaboratorsMap.value.get(b.created_by)) {
|
||||
return orderBy.value.created_by === 'asc'
|
||||
? collaboratorsMap.value.get(a.created_by)?.email < collaboratorsMap.value.get(b.created_by)?.email
|
||||
? -1
|
||||
: 1
|
||||
: collaboratorsMap.value.get(a.created_by)?.email > collaboratorsMap.value.get(b.created_by)?.email
|
||||
? -1
|
||||
: 1
|
||||
}
|
||||
} else if (orderBy.value.source_count) {
|
||||
if (a.source_count !== undefined && b.source_count !== undefined) {
|
||||
return orderBy.value.source_count === 'asc' ? a.source_count - b.source_count : b.source_count - a.source_count
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}),
|
||||
)
|
||||
|
||||
async function loadConnections(
|
||||
page = integrationPaginationData.value.page,
|
||||
limit = integrationPaginationData.value.pageSize,
|
||||
updateCurrentPage = true,
|
||||
) {
|
||||
try {
|
||||
if (updateCurrentPage) {
|
||||
integrationPaginationData.value.page = 1
|
||||
}
|
||||
|
||||
await loadIntegrations(undefined, undefined, updateCurrentPage ? 1 : page, limit)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleChangePage = async (page: number) => {
|
||||
integrationPaginationData.value.page = page
|
||||
await loadConnections(undefined, undefined, false)
|
||||
}
|
||||
|
||||
const { onLeft, onRight, onUp, onDown } = usePaginationShortcuts({
|
||||
paginationDataRef: integrationPaginationData,
|
||||
changePage: handleChangePage,
|
||||
isViewDataLoading: isLoadingIntegrations,
|
||||
})
|
||||
|
||||
const openDeleteIntegration = async (source: IntegrationType) => {
|
||||
isLoadingGetLinkedSources.value = true
|
||||
|
||||
$e('c:integration:delete')
|
||||
deleteConfirmText.value = null
|
||||
isDeleteIntegrationModalOpen.value = true
|
||||
toBeDeletedIntegration.value = source
|
||||
|
||||
const connectionDetails = await getIntegration(source, {
|
||||
includeSources: true,
|
||||
})
|
||||
toBeDeletedIntegration.value.sources = connectionDetails?.sources || []
|
||||
|
||||
isLoadingGetLinkedSources.value = false
|
||||
}
|
||||
|
||||
const onDeleteConfirm = async () => {
|
||||
await deleteIntegration(toBeDeletedIntegration.value, true)
|
||||
}
|
||||
|
||||
const loadOrgUsers = async () => {
|
||||
try {
|
||||
const response: any = await $api.orgUsers.list()
|
||||
|
||||
if (!response?.list) return
|
||||
|
||||
localCollaborators.value = response.list as UserType[]
|
||||
} catch (e: any) {
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
}
|
||||
}
|
||||
|
||||
const updateOrderBy = (field: SortFields) => {
|
||||
if (!integrations.value?.length) return
|
||||
|
||||
// Only single field sort supported, other old field sort config will reset
|
||||
if (orderBy.value?.[field] === 'asc') {
|
||||
orderBy.value = {
|
||||
[field]: 'desc',
|
||||
}
|
||||
} else if (orderBy.value?.[field] === 'desc') {
|
||||
orderBy.value = {
|
||||
[field]: undefined,
|
||||
}
|
||||
} else {
|
||||
orderBy.value = {
|
||||
[field]: 'asc',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchConnection = useDebounceFn(() => {
|
||||
loadConnections()
|
||||
}, 250)
|
||||
|
||||
const renderAltOrOptlKey = () => {
|
||||
return isMac() ? '⌥' : 'ALT'
|
||||
}
|
||||
|
||||
useEventListener(tableWrapper, 'scroll', () => {
|
||||
const stickyHeaderCell = tableWrapper.value?.querySelector('th.cell-title')
|
||||
const nonStickyHeaderFirstCell = tableWrapper.value?.querySelector('th.cell-type')
|
||||
|
||||
if (!stickyHeaderCell?.getBoundingClientRect().right || !nonStickyHeaderFirstCell?.getBoundingClientRect().left) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nonStickyHeaderFirstCell?.getBoundingClientRect().left < stickyHeaderCell?.getBoundingClientRect().right) {
|
||||
tableWrapper.value?.classList.add('sticky-shadow')
|
||||
} else {
|
||||
tableWrapper.value?.classList.remove('sticky-shadow')
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isEeUI) {
|
||||
await Promise.allSettled([!integrations.value.length && loadIntegrations(), loadOrgUsers()])
|
||||
} else if (!integrations.value.length) {
|
||||
await loadIntegrations()
|
||||
}
|
||||
})
|
||||
// Keyboard shortcuts for pagination
|
||||
onKeyStroke('ArrowLeft', onLeft)
|
||||
onKeyStroke('ArrowRight', onRight)
|
||||
onKeyStroke('ArrowUp', onUp)
|
||||
onKeyStroke('ArrowDown', onDown)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col gap-6 nc-workspace-connections">
|
||||
<div class="flex justify-between gap-12">
|
||||
<div class="text-sm font-normal text-gray-600">
|
||||
<div>
|
||||
Connections simplify managing stored configurations for different integrations.
|
||||
<a target="_blank" rel="noopener noreferrer"> Learn more </a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 mx-1">
|
||||
<a-input
|
||||
v-model:value="searchQuery"
|
||||
type="text"
|
||||
class="nc-search-integration-input !min-w-[300px] nc-input-sm flex-none"
|
||||
:placeholder="`${$t('general.search')} ${$t('general.connections').toLowerCase()}`"
|
||||
allow-clear
|
||||
@input="handleSearchConnection"
|
||||
>
|
||||
<template #prefix>
|
||||
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container relative flex-1">
|
||||
<div
|
||||
ref="tableWrapper"
|
||||
class="nc-workspace-integration-table relative nc-scrollbar-thin !overflow-auto max-h-[calc(100%_-_40px)]"
|
||||
:class="{
|
||||
'h-full': filteredIntegrations?.length,
|
||||
}"
|
||||
>
|
||||
<table class="!sticky top-0 z-5 w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="cell-title !hover:bg-gray-100 select-none cursor-pointer"
|
||||
:class="{
|
||||
'cursor-not-allowed': !filteredIntegrations?.length,
|
||||
'!text-gray-700': orderBy.title,
|
||||
}"
|
||||
@click="updateOrderBy('title')"
|
||||
>
|
||||
<div ref="titleHeaderCellRef" class="flex items-center gap-3">
|
||||
<div>Name</div>
|
||||
<GeneralIcon
|
||||
v-if="orderBy.title"
|
||||
icon="chevronDown"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'transform rotate-180': orderBy?.title === 'asc',
|
||||
}"
|
||||
/>
|
||||
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="cell-type !hover:bg-gray-100 select-none cursor-pointer"
|
||||
:class="{
|
||||
'cursor-not-allowed': !filteredIntegrations?.length,
|
||||
'!text-gray-700': orderBy.sub_type,
|
||||
}"
|
||||
@click="updateOrderBy('sub_type')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>Type</div>
|
||||
<GeneralIcon
|
||||
v-if="orderBy.sub_type"
|
||||
icon="chevronDown"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'transform rotate-180': orderBy.sub_type === 'asc',
|
||||
}"
|
||||
/>
|
||||
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th
|
||||
class="cell-created-date !hover:bg-gray-100 select-none cursor-pointer"
|
||||
:class="{
|
||||
'cursor-not-allowed': !filteredIntegrations?.length,
|
||||
'!text-gray-700': orderBy.created_at,
|
||||
}"
|
||||
@click="updateOrderBy('created_at')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>Date added</div>
|
||||
<GeneralIcon
|
||||
v-if="orderBy.created_at"
|
||||
icon="chevronDown"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'transform rotate-180': orderBy.created_at === 'asc',
|
||||
}"
|
||||
/>
|
||||
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="cell-added-by !hover:bg-gray-100 select-none cursor-pointer"
|
||||
:class="{
|
||||
'cursor-not-allowed': !filteredIntegrations?.length,
|
||||
'!text-gray-700': orderBy.created_by,
|
||||
}"
|
||||
@click="updateOrderBy('created_by')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>Added by</div>
|
||||
<GeneralIcon
|
||||
v-if="orderBy.created_by"
|
||||
icon="chevronDown"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'transform rotate-180': orderBy.created_by === 'asc',
|
||||
}"
|
||||
/>
|
||||
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="cell-usage !hover:bg-gray-100 select-none cursor-pointer"
|
||||
:class="{
|
||||
'cursor-not-allowed': !filteredIntegrations?.length,
|
||||
}"
|
||||
@click="updateOrderBy('source_count')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div>Usage</div>
|
||||
<GeneralIcon
|
||||
v-if="orderBy?.source_count"
|
||||
icon="chevronDown"
|
||||
class="flex-none"
|
||||
:class="{
|
||||
'transform rotate-180': orderBy?.source_count === 'asc',
|
||||
}"
|
||||
/>
|
||||
<GeneralIcon v-else icon="chevronUpDown" class="flex-none" />
|
||||
</div>
|
||||
</th>
|
||||
<th class="cell-actions">
|
||||
<div>Actions</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<template v-if="filteredIntegrations?.length">
|
||||
<table class="h-full max-h-[calc(100%_-_55px)] w-full">
|
||||
<tbody>
|
||||
<tr v-for="integration of filteredIntegrations" :key="integration.id" @click="editIntegration(integration)">
|
||||
<td class="cell-title">
|
||||
<div
|
||||
class="gap-3"
|
||||
:style="{
|
||||
maxWidth: `${width}px`,
|
||||
}"
|
||||
>
|
||||
<NcTooltip placement="bottom" class="truncate" show-on-truncate-only>
|
||||
<template #title> {{ integration.title }}</template>
|
||||
{{ integration.title }}
|
||||
</NcTooltip>
|
||||
<span v-if="integration.is_private">
|
||||
<NcBadge :border="false" class="text-primary !h-4.5 bg-brand-50 text-xs">{{
|
||||
$t('general.private')
|
||||
}}</NcBadge>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-type">
|
||||
<div>
|
||||
<NcBadge rounded="lg" class="flex items-center gap-2 px-2 py-1 !h-7 truncate">
|
||||
<WorkspaceIntegrationsIcon
|
||||
v-if="integration.sub_type"
|
||||
:integration-type="integration.sub_type"
|
||||
size="xs"
|
||||
class="!p-0 !bg-transparent"
|
||||
/>
|
||||
<NcTooltip placement="bottom" show-on-truncate-only class="text-sm truncate">
|
||||
<template #title> {{ clientTypesMap[integration?.sub_type]?.text || integration?.sub_type }}</template>
|
||||
|
||||
{{
|
||||
integration.sub_type && clientTypesMap[integration.sub_type]
|
||||
? clientTypesMap[integration.sub_type]?.text
|
||||
: integration.sub_type
|
||||
}}
|
||||
</NcTooltip>
|
||||
</NcBadge>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-created-date">
|
||||
<div>
|
||||
<NcTooltip placement="bottom" show-on-truncate-only>
|
||||
<template #title> {{ dayjs(integration.created_at).local().format('DD MMM YYYY') }}</template>
|
||||
|
||||
{{ dayjs(integration.created_at).local().format('DD MMM YYYY') }}
|
||||
</NcTooltip>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-added-by">
|
||||
<div>
|
||||
<div
|
||||
v-if="integration.created_by && collaboratorsMap.get(integration.created_by)?.email"
|
||||
class="w-full flex gap-3 items-center"
|
||||
>
|
||||
<GeneralUserIcon
|
||||
:email="collaboratorsMap.get(integration.created_by)?.email"
|
||||
size="base"
|
||||
class="flex-none"
|
||||
/>
|
||||
<div class="flex-1 flex flex-col max-w-[calc(100%_-_44px)]">
|
||||
<div class="w-full flex gap-3">
|
||||
<NcTooltip
|
||||
class="text-sm !leading-5 text-gray-800 capitalize font-semibold truncate"
|
||||
show-on-truncate-only
|
||||
placement="bottom"
|
||||
>
|
||||
<template #title>
|
||||
{{
|
||||
collaboratorsMap.get(integration.created_by)?.display_name ||
|
||||
collaboratorsMap
|
||||
.get(integration.created_by)
|
||||
?.email?.slice(0, collaboratorsMap.get(integration.created_by)?.email.indexOf('@'))
|
||||
}}
|
||||
</template>
|
||||
{{
|
||||
collaboratorsMap.get(integration.created_by)?.display_name ||
|
||||
collaboratorsMap
|
||||
.get(integration.created_by)
|
||||
?.email?.slice(0, collaboratorsMap.get(integration.created_by)?.email.indexOf('@'))
|
||||
}}
|
||||
</NcTooltip>
|
||||
</div>
|
||||
<NcTooltip class="text-xs !leading-4 text-gray-600 truncate" show-on-truncate-only placement="bottom">
|
||||
<template #title>
|
||||
{{ collaboratorsMap.get(integration.created_by)?.email }}
|
||||
</template>
|
||||
{{ collaboratorsMap.get(integration.created_by)?.email }}
|
||||
</NcTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>{{ integration.created_by }} </template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-usage">
|
||||
<div>{{ integration?.source_count ?? 0 }}</div>
|
||||
</td>
|
||||
<td class="cell-actions" @click.stop>
|
||||
<div class="justify-end">
|
||||
<NcDropdown placement="bottomRight">
|
||||
<NcButton size="small" type="secondary">
|
||||
<GeneralIcon icon="threeDotVertical" />
|
||||
</NcButton>
|
||||
<template #overlay>
|
||||
<NcMenu>
|
||||
<NcMenuItem @click="editIntegration(integration)">
|
||||
<GeneralIcon class="text-gray-800" icon="edit" />
|
||||
<span>{{ $t('general.edit') }}</span>
|
||||
</NcMenuItem>
|
||||
<NcMenuItem @click="duplicateIntegration(integration)">
|
||||
<GeneralIcon class="text-gray-800" icon="duplicate" />
|
||||
<span>{{ $t('general.duplicate') }}</span>
|
||||
</NcMenuItem>
|
||||
<NcDivider />
|
||||
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click="openDeleteIntegration(integration)">
|
||||
<GeneralIcon icon="delete" />
|
||||
{{ $t('general.delete') }}
|
||||
</NcMenuItem>
|
||||
</NcMenu>
|
||||
</template>
|
||||
</NcDropdown>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="isLoadingIntegrations"
|
||||
class="flex items-center justify-center absolute left-0 top-0 w-full h-full 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
|
||||
v-if="!isLoadingIntegrations && (!integrations?.length || !filteredIntegrations.length)"
|
||||
class="flex-none integration-table-empty flex items-center justify-center py-8 px-6 h-full max-h-[calc(100%_-_94px)]"
|
||||
>
|
||||
<div
|
||||
v-if="integrations?.length && !filteredIntegrations.length"
|
||||
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 v-else class="flex-none text-center flex flex-col items-center gap-3">
|
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="integrationPaginationData.totalRows"
|
||||
class="flex flex-row justify-center items-center bg-gray-50 min-h-10"
|
||||
:class="{
|
||||
'pointer-events-none': isLoadingIntegrations,
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full px-6">
|
||||
<div> </div>
|
||||
<NcPagination
|
||||
v-model:current="integrationPaginationData.page"
|
||||
v-model:page-size="integrationPaginationData.pageSize"
|
||||
:total="+integrationPaginationData.totalRows"
|
||||
show-size-changer
|
||||
:use-stored-page-size="false"
|
||||
:prev-page-tooltip="`${renderAltOrOptlKey()}+←`"
|
||||
:next-page-tooltip="`${renderAltOrOptlKey()}+→`"
|
||||
:first-page-tooltip="`${renderAltOrOptlKey()}+↓`"
|
||||
:last-page-tooltip="`${renderAltOrOptlKey()}+↑`"
|
||||
@update:current="loadConnections(undefined, undefined, false)"
|
||||
@update:page-size="loadConnections(integrationPaginationData.page, $event, false)"
|
||||
/>
|
||||
<div class="text-gray-500 text-xs">
|
||||
{{ integrationPaginationData.totalRows }} {{ integrationPaginationData.totalRows === 1 ? 'record' : 'records' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GeneralDeleteModal
|
||||
v-model:visible="isDeleteIntegrationModalOpen"
|
||||
: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"></a-skeleton-input>
|
||||
</div>
|
||||
<div class="rounded-lg overflow-hidden mt-2">
|
||||
<a-skeleton-input active class="h-9 !rounded-md !w-full"></a-skeleton-input>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="toBeDeletedIntegration" class="w-full flex flex-col text-gray-800">
|
||||
<div class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
|
||||
<WorkspaceIntegrationsIcon
|
||||
:integration-type="toBeDeletedIntegration.sub_type"
|
||||
size="xs"
|
||||
class="!p-0 !bg-transparent"
|
||||
/>
|
||||
<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-gray-500">
|
||||
<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-gray-500 !marker:(flex items-center !-mt-1)"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center">
|
||||
|
||||
<GeneralProjectIcon
|
||||
type="database"
|
||||
class="!grayscale min-w-4 flex-none -ml-1"
|
||||
:style="{
|
||||
filter: 'grayscale(100%) brightness(115%)',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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-gray-600" />
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700">
|
||||
{{ successConfirmModal.description }}
|
||||
</div>
|
||||
|
||||
<!-- Todo: add link -->
|
||||
<a target="_blank" rel="noopener noreferrer"> Learn more </a>
|
||||
</div>
|
||||
</div>
|
||||
</NcModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.source-card-link {
|
||||
@apply !text-black !no-underline;
|
||||
|
||||
.nc-new-integration-type-title {
|
||||
@apply text-sm font-weight-600 text-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.source-card {
|
||||
@apply flex items-center border-1 rounded-lg p-3 cursor-pointer hover:bg-gray-50;
|
||||
width: 288px;
|
||||
|
||||
.name {
|
||||
@apply ml-4 text-md font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper.nc-search-integration-input) {
|
||||
&:not(:has(.ant-input-clear-icon-hidden)):has(.ant-input-clear-icon) {
|
||||
@apply border-[var(--ant-primary-5)];
|
||||
}
|
||||
}
|
||||
|
||||
.nc-new-integration-type-wrapper {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
@apply border-1 border-gray-200 rounded-lg overflow-hidden w-full;
|
||||
|
||||
.nc-workspace-integration-table {
|
||||
&.sticky-shadow {
|
||||
th,
|
||||
td {
|
||||
&.cell-title {
|
||||
@apply border-r-1 border-gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.sticky-shadow) {
|
||||
th,
|
||||
td {
|
||||
&.cell-title {
|
||||
@apply border-r-1 border-transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
@apply w-full;
|
||||
th {
|
||||
@apply bg-gray-50 text-sm text-gray-500 font-weight-500;
|
||||
|
||||
&.cell-title {
|
||||
@apply sticky left-0 z-4 bg-gray-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
@apply w-full;
|
||||
tr {
|
||||
@apply cursor-pointer;
|
||||
|
||||
td {
|
||||
@apply text-sm text-gray-600;
|
||||
|
||||
&.cell-title {
|
||||
@apply sticky left-0 z-4 bg-white !text-gray-800 font-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
@apply h-[54px] flex border-b-1 border-gray-200 w-full;
|
||||
|
||||
&:hover td {
|
||||
@apply !bg-gray-50;
|
||||
}
|
||||
|
||||
&.selected td {
|
||||
@apply !bg-gray-50;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
@apply h-full;
|
||||
|
||||
& > div {
|
||||
@apply px-6 h-full flex-1 flex items-center;
|
||||
}
|
||||
|
||||
&.cell-title {
|
||||
@apply flex-1 sticky left-0 z-5;
|
||||
& > div {
|
||||
@apply min-w-[250px];
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-added-by {
|
||||
@apply basis-[20%];
|
||||
& > div {
|
||||
@apply min-w-[250px];
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-type {
|
||||
@apply basis-[20%];
|
||||
& > div {
|
||||
@apply min-w-[178px];
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-created-date {
|
||||
@apply basis-[20%];
|
||||
& > div {
|
||||
@apply min-w-[158px];
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-usage {
|
||||
@apply w-[120px];
|
||||
& > div {
|
||||
@apply min-w-[118px];
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-actions {
|
||||
@apply w-[100px];
|
||||
& > div {
|
||||
@apply min-w-[98px];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell-header {
|
||||
@apply text-xs font-semibold text-gray-500;
|
||||
}
|
||||
</style>
|
||||
55
packages/nc-gui/components/workspace/integrations/Panel.vue
Normal file
55
packages/nc-gui/components/workspace/integrations/Panel.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { iconMap } from '#imports'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
icon?: keyof typeof iconMap
|
||||
collapsible?: boolean
|
||||
collapsed?: boolean
|
||||
}>(),
|
||||
{
|
||||
collapsible: false,
|
||||
collapsed: true,
|
||||
},
|
||||
)
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const collapsed = ref(props.collapsible ? props.collapsed : false)
|
||||
|
||||
const toggleCollapse = () => {
|
||||
if (!props.collapsible) return
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="panelRef" class="panel" :data-label="props.title" :data-icon="props.icon">
|
||||
<div class="flex items-center gap-2" @click="toggleCollapse">
|
||||
<template v-if="props.collapsible">
|
||||
<GeneralIcon :icon="collapsed ? 'arrowRight' : 'arrowDown'" />
|
||||
</template>
|
||||
<GeneralIcon v-else-if="props.icon" :icon="props.icon" />
|
||||
<div class="panel-label" :class="{ 'cursor-pointer': props.collapsible, 'cursor-default': !props.collapsible }">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<slot name="header-info"></slot>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="panel-body"><slot></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
@apply border-1 border-gray-200 px-6 py-4 rounded-lg mb-4;
|
||||
|
||||
.panel-label {
|
||||
@apply text-md font-weight-bold flex-1;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
@apply mt-4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
const supportedDocs = [
|
||||
{
|
||||
title: 'Configure a PostgreSQL Integration',
|
||||
href: '',
|
||||
},
|
||||
{
|
||||
title: 'Getting started with Integrations',
|
||||
href: '',
|
||||
},
|
||||
{
|
||||
title: 'Troubleshoot database connection',
|
||||
href: '',
|
||||
},
|
||||
] as {
|
||||
title: string
|
||||
href: string
|
||||
}[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="w-full flex flex-col gap-3">
|
||||
<div class="text-sm text-gray-800 font-semibold">Supported Docs</div>
|
||||
|
||||
<div>
|
||||
<div v-for="doc of supportedDocs" class="flex items-center gap-1">
|
||||
<div class="h-7 w-7 flex items-center justify-center">
|
||||
<GeneralIcon icon="bookOpen" class="flex-none w-4 h-4 text-gray-600"/>
|
||||
</div>
|
||||
<a :href="doc.href" target="_blank" rel="noopener noreferrer" class="!text-gray-700 text-sm !no-underline !hover:underline">
|
||||
{{ doc.title }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="panels">
|
||||
<div class="panel">DUMMY</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panels {
|
||||
@apply w-3/4 flex flex-col;
|
||||
|
||||
.panel {
|
||||
@apply border-1 border-gray-200 p-6 rounded-lg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
const { activeIntegration, categories, activeCategory } = useIntegrationStore()
|
||||
|
||||
const { copy } = useCopy()
|
||||
|
||||
const copyIp = async () => {
|
||||
await copy('52.15.226.51')
|
||||
message.success('Copied to clipboard')
|
||||
}
|
||||
|
||||
const panelsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (!panelsRef.value) return
|
||||
|
||||
const panels = panelsRef.value.querySelectorAll('.panel')
|
||||
panels.forEach((panel) => {
|
||||
categories.value.push({
|
||||
label: panel.getAttribute('data-label') as string,
|
||||
icon: panel.getAttribute('data-icon'),
|
||||
})
|
||||
})
|
||||
|
||||
// focus first input
|
||||
nextTick(() => {
|
||||
const firstInput = panels[0].querySelector('input')
|
||||
if (firstInput) firstInput.focus()
|
||||
})
|
||||
})
|
||||
|
||||
const onInputFocus = () => {
|
||||
const target = document.activeElement
|
||||
const panel = target?.closest('.panel')
|
||||
if (panel) {
|
||||
activeCategory.value = {
|
||||
label: panel.getAttribute('data-label') as string,
|
||||
icon: panel.getAttribute('data-icon'),
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="panelsRef" class="panels">
|
||||
<WorkspaceIntegrationsPanel title="Integration Details" icon="info">
|
||||
<template #header-info>
|
||||
<div
|
||||
class="text-gray-500 !text-xs font-weight-normal flex items-center gap-2 cursor-pointer flex items-center"
|
||||
@click="copyIp"
|
||||
>
|
||||
<GeneralIcon icon="info" class="text-primary" />
|
||||
Whitelist our ip: 52.15.226.51 to allow database access
|
||||
<GeneralIcon icon="duplicate" class="text-gray-800 w-5 h-5 p-1 border-1 rounded-md border-gray-200" />
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div class="flex flex-col w-1/2 pr-3">
|
||||
<label class="!text-xs font-weight-normal pb-1">Title</label>
|
||||
<a-input v-model:value="activeIntegration.payload.title" class="input-text" :maxlength="255" @focus="onInputFocus" />
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceIntegrationsPanel>
|
||||
<WorkspaceIntegrationsPanel title="Connection Details" icon="link">
|
||||
<div class="input-group">
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">Host</label>
|
||||
<a-input v-model:value="activeIntegration.payload.host" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">Port</label>
|
||||
<a-input v-model:value="activeIntegration.payload.port" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">User</label>
|
||||
<a-input v-model:value="activeIntegration.payload.user" class="input-text" autocomplete="off" @focus="onInputFocus" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">Password</label>
|
||||
<a-input
|
||||
v-model:value="activeIntegration.payload.password"
|
||||
class="input-text"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@focus="onInputFocus"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">Schema</label>
|
||||
<a-input v-model:value="activeIntegration.payload.schema" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">Database</label>
|
||||
<a-input v-model:value="activeIntegration.payload.database" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceIntegrationsPanel>
|
||||
<WorkspaceIntegrationsPanel title="SSL & Advanced Parameters" icon="lock" :collapsible="true">
|
||||
<div class="input-group">
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">SSL Mode</label>
|
||||
<a-select v-model:value="activeIntegration.payload.sslMode" class="input-text" @focus="onInputFocus">
|
||||
<a-select-option value="disable">Disable</a-select-option>
|
||||
<a-select-option value="require">Require</a-select-option>
|
||||
<a-select-option value="verify-ca">Verify CA</a-select-option>
|
||||
<a-select-option value="verify-full">Verify Full</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">SSL Root Certificate</label>
|
||||
<a-input v-model:value="activeIntegration.payload.sslRootCert" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">SSL Certificate</label>
|
||||
<a-input v-model:value="activeIntegration.payload.sslCert" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">SSL Key</label>
|
||||
<a-input v-model:value="activeIntegration.payload.sslKey" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full border-t-1 mt-2 mb-4"></div>
|
||||
<div class="input-group">
|
||||
<div class="input-item">
|
||||
<label class="!text-xs font-weight-normal pb-1">Extra Connection Parameters</label>
|
||||
<a-input v-model:value="activeIntegration.payload.sslCert" class="input-text" @focus="onInputFocus" />
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceIntegrationsPanel>
|
||||
<WorkspaceIntegrationsPanel title="Connection JSON" icon="code">DUMMY</WorkspaceIntegrationsPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panels {
|
||||
@apply w-3/4 flex flex-col;
|
||||
|
||||
.panel {
|
||||
@apply border-1 border-gray-200 px-4 py-4 rounded-lg mb-4;
|
||||
|
||||
.panel-label {
|
||||
@apply text-md font-weight-bold;
|
||||
}
|
||||
|
||||
.input-text {
|
||||
@apply w-full rounded-md;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@apply flex flex-wrap w-full;
|
||||
|
||||
.input-item {
|
||||
@apply flex flex-col w-1/2 pr-3 mb-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<script lang="ts" setup>
|
||||
import NcModal from '~/components/nc/Modal.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isModal?: boolean
|
||||
}>(),
|
||||
{
|
||||
isModal: false,
|
||||
},
|
||||
)
|
||||
|
||||
const { isModal } = props
|
||||
|
||||
const { pageMode, IntegrationsPageMode, integrationType, requestIntegration, addIntegration, saveIntegraitonRequest } =
|
||||
useIntegrationStore()
|
||||
|
||||
const isAddNewIntegrationModalOpen = computed({
|
||||
get: () => {
|
||||
return pageMode.value === IntegrationsPageMode.LIST
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
pageMode.value = IntegrationsPageMode.LIST
|
||||
} else {
|
||||
pageMode.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleAddIntegration = (type: typeof integrationType) => {
|
||||
if (requestIntegration.value.isOpen) {
|
||||
requestIntegration.value.isOpen = false
|
||||
}
|
||||
|
||||
addIntegration(type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="isModal ? NcModal : 'div'"
|
||||
v-model:visible="isAddNewIntegrationModalOpen"
|
||||
centered
|
||||
size="large"
|
||||
:class="{
|
||||
'h-full': !isModal,
|
||||
}"
|
||||
wrap-class-name="nc-modal-available-integrations-list"
|
||||
@keydown.esc="isAddNewIntegrationModalOpen = false"
|
||||
>
|
||||
<div class="h-full flex flex-col">
|
||||
<div v-if="isModal" class="p-4 w-full flex items-center justify-between gap-3 border-b-1 border-gray-200">
|
||||
<NcButton type="text" size="small" @click="isAddNewIntegrationModalOpen = false">
|
||||
<GeneralIcon icon="arrowLeft" />
|
||||
</NcButton>
|
||||
<GeneralIcon icon="gitCommit" class="flex-none h-5 w-5" />
|
||||
<div class="flex-1 text-base font-weight-700">New Connection</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<NcButton size="small" type="text" @click="isAddNewIntegrationModalOpen = false">
|
||||
<GeneralIcon icon="close" class="text-gray-600" />
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col nc-workspace-settings-integrations-new-available-list"
|
||||
:class="{
|
||||
'h-[calc(80vh_-_66px)] p-6': isModal,
|
||||
'h-full': !isModal,
|
||||
}"
|
||||
>
|
||||
<div class="w-full flex justify-center">
|
||||
<div
|
||||
class="flex flex-col gap-6 w-full"
|
||||
:class="{
|
||||
'max-w-[1088px]': isModal,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="text-sm font-normal text-gray-600"
|
||||
:class="{
|
||||
'max-w-[740px]': !isModal,
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
Centralise your operations by aggregating information from various external platforms into NocoDB. Select from the
|
||||
available integrations below to get started. <a target="_blank" rel="noopener noreferrer"> Learn more </a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="integration-type-wrapper">
|
||||
<div class="integration-type-title">Databases</div>
|
||||
<div class="integration-type-list">
|
||||
<div class="source-card" @click="handleAddIntegration(integrationType.MySQL)">
|
||||
<WorkspaceIntegrationsIcon :integration-type="integrationType.MySQL" size="md" />
|
||||
<div class="name flex-1">MySQL</div>
|
||||
<div class="action-btn">+</div>
|
||||
</div>
|
||||
<div class="source-card" @click="handleAddIntegration(integrationType.PostgreSQL)">
|
||||
<WorkspaceIntegrationsIcon :integration-type="integrationType.PostgreSQL" size="md" />
|
||||
<div class="name flex-1">PostgreSQL</div>
|
||||
<div class="action-btn">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Todo:APIs -->
|
||||
<!-- <div>
|
||||
<div>APIs</div>
|
||||
<div></div>
|
||||
</div> -->
|
||||
<div class="integration-type-wrapper">
|
||||
<div>
|
||||
<div class="integration-type-title">Others</div>
|
||||
<div class="integration-type-subtitle"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="source-card-request-integration"
|
||||
:class="{
|
||||
active: requestIntegration.isOpen,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!requestIntegration.isOpen"
|
||||
class="source-card-item border-none"
|
||||
@click="requestIntegration.isOpen = true"
|
||||
>
|
||||
<WorkspaceIntegrationsIcon integration-type="request" size="md" />
|
||||
<div class="name">Request New Integration</div>
|
||||
</div>
|
||||
<div v-show="requestIntegration.isOpen" class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="text-base font-bold text-gray-800">Request Integration</div>
|
||||
<NcButton size="xsmall" type="text" @click="requestIntegration.isOpen = false">
|
||||
<GeneralIcon icon="close" class="text-gray-600" />
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a-textarea
|
||||
v-model:value="requestIntegration.msg"
|
||||
class="!rounded-md !text-sm !min-h-[120px] max-h-[500px] nc-scrollbar-thin"
|
||||
size="large"
|
||||
hide-details
|
||||
placeholder="Provide integration name and your use-case."
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<NcButton size="small" type="secondary" @click="requestIntegration.isOpen = false">
|
||||
{{ $t('general.cancel') }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
:disabled="!requestIntegration.msg?.trim()"
|
||||
:loading="requestIntegration.isLoading"
|
||||
size="small"
|
||||
@click="saveIntegraitonRequest(requestIntegration.msg)"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.source-card-request-integration {
|
||||
@apply flex flex-col gap-4 border-1 rounded-xl p-3 w-[352px] overflow-hidden transition-all duration-300 max-w-[720px];
|
||||
|
||||
&.active {
|
||||
@apply w-full;
|
||||
}
|
||||
&:not(.active) {
|
||||
@apply cursor-pointer hover:bg-gray-50;
|
||||
}
|
||||
|
||||
.source-card-item {
|
||||
@apply flex items-center;
|
||||
|
||||
.name {
|
||||
@apply ml-4 text-md font-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.source-card-link {
|
||||
@apply !text-black !no-underline;
|
||||
.nc-new-integration-type-title {
|
||||
@apply text-sm font-weight-600 text-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.source-card {
|
||||
@apply flex items-center border-1 rounded-xl p-3 cursor-pointer hover:bg-gray-50;
|
||||
width: 352px;
|
||||
.name {
|
||||
@apply ml-4 text-md font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workspace-settings-integrations-new-available-list {
|
||||
.integration-type-wrapper {
|
||||
@apply flex flex-col gap-3;
|
||||
|
||||
.source-card:hover {
|
||||
.action-btn {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-type-title {
|
||||
@apply text-sm text-gray-500 font-weight-700;
|
||||
}
|
||||
.integration-type-subtitle {
|
||||
@apply text-sm text-gray-500 font-weight-700;
|
||||
}
|
||||
.integration-type-list {
|
||||
@apply flex gap-4 flex-wrap;
|
||||
}
|
||||
.action-btn {
|
||||
@apply hidden text-2xl text-gray-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.nc-modal-available-integrations-list {
|
||||
.nc-modal {
|
||||
@apply !p-0;
|
||||
height: min(calc(100vh - 100px), 1024px);
|
||||
max-height: min(calc(100vh - 100px), 1024px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
130
packages/nc-gui/components/workspace/integrations/view.vue
Normal file
130
packages/nc-gui/components/workspace/integrations/view.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script lang="ts" setup>
|
||||
import { useTitle } from '@vueuse/core'
|
||||
|
||||
const { isUIAllowed } = useRoles()
|
||||
|
||||
const workspaceStore = useWorkspace()
|
||||
|
||||
const { loadRoles } = useRoles()
|
||||
const { activeWorkspace: _activeWorkspace } = storeToRefs(workspaceStore)
|
||||
const { loadCollaborators } = workspaceStore
|
||||
|
||||
const { isFromIntegrationPage, integrationPaginationData, activeViewTab, loadIntegrations } = useProvideIntegrationViewStore()
|
||||
|
||||
const currentWorkspace = computedAsync(async () => {
|
||||
await loadRoles(undefined, {}, _activeWorkspace.value?.id)
|
||||
return _activeWorkspace.value
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentWorkspace.value?.title,
|
||||
(title) => {
|
||||
if (!title) return
|
||||
|
||||
const capitalizedTitle = title.charAt(0).toUpperCase() + title.slice(1)
|
||||
|
||||
useTitle(capitalizedTitle)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
isFromIntegrationPage.value = true
|
||||
|
||||
until(() => currentWorkspace.value?.id)
|
||||
.toMatch((v) => !!v)
|
||||
.then(async () => {
|
||||
await Promise.all([loadCollaborators({} as any, currentWorkspace.value!.id), loadIntegrations()])
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
isFromIntegrationPage.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentWorkspace" class="flex w-full max-w-[97.5rem] flex-col nc-workspace-integrations">
|
||||
<div class="flex gap-2 items-center min-w-0 py-4 px-6">
|
||||
<h1 class="text-base capitalize font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize">
|
||||
<span class="text-gray-500"> {{ currentWorkspace?.title }} ></span> {{ $t('general.integrations') }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<NcTabs v-model:activeKey="activeViewTab">
|
||||
<template #leftExtra>
|
||||
<div class="w-6"></div>
|
||||
</template>
|
||||
<template v-if="isUIAllowed('workspaceIntegrations')">
|
||||
<a-tab-pane key="integrations" class="w-full">
|
||||
<template #tab>
|
||||
<div class="flex flex-row items-center pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-integrations">
|
||||
<GeneralIcon icon="integration" />
|
||||
{{ $t('general.integrations') }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-[calc(100vh-92px)] p-6">
|
||||
<WorkspaceIntegrationsNewAvailableList />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</template>
|
||||
<template v-if="isUIAllowed('workspaceIntegrations')">
|
||||
<a-tab-pane key="connections" class="w-full">
|
||||
<template #tab>
|
||||
<div class="flex flex-row items-center pb-1 gap-x-1.5" data-testid="nc-workspace-settings-tab-integrations">
|
||||
<GeneralIcon icon="gitCommit" />
|
||||
{{ $t('general.connections') }}
|
||||
<div
|
||||
v-if="integrationPaginationData?.totalRows"
|
||||
class="tab-info flex-none"
|
||||
:class="{
|
||||
'bg-primary-selected': activeViewTab === 'connections',
|
||||
'bg-gray-50': activeViewTab !== 'connections',
|
||||
}"
|
||||
>
|
||||
{{ integrationPaginationData.totalRows }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-[calc(100vh-92px)] p-6">
|
||||
<WorkspaceIntegrationsList />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</template>
|
||||
</NcTabs>
|
||||
<WorkspaceIntegrationsEditOrAdd></WorkspaceIntegrationsEditOrAdd>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nc-workspace-avatar {
|
||||
@apply min-w-6 h-6 rounded-[6px] flex items-center justify-center text-white font-weight-bold uppercase;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@apply flex flex-row items-center gap-x-2;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
@apply !pl-0;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-nav-list) {
|
||||
@apply !gap-5;
|
||||
}
|
||||
:deep(.ant-tabs-tab) {
|
||||
@apply !pt-0 !pb-2.5 !ml-0;
|
||||
}
|
||||
.ant-tabs-content {
|
||||
@apply !h-full;
|
||||
}
|
||||
.ant-tabs-content-top {
|
||||
@apply !h-full;
|
||||
}
|
||||
.tab-info {
|
||||
@apply flex pl-1.25 px-1.5 py-0.75 rounded-md text-xs;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user