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:
Pranav C
2024-08-04 15:36:20 +05:30
committed by GitHub
parent 2b26e1a5b9
commit 9de25471b8
130 changed files with 8238 additions and 1618 deletions

View 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>

View File

@@ -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>

View 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>

View 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>&nbsp;</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">
&nbsp;
<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>

View 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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>