import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FormDefinition, IntegrationType, PaginatedType } from 'nocodb-sdk' import { ClientType, IntegrationsType, SyncDataType } from 'nocodb-sdk' import GeneralBaseLogo from '~/components/general/BaseLogo.vue' import type { IntegrationStoreEvents as IntegrationStoreEventsTypes } from '#imports' enum IntegrationsPageMode { LIST, ADD, EDIT, } const integrationType: Record<'PostgreSQL' | 'MySQL' | 'SQLITE' | 'OpenAI', ClientType | SyncDataType> = { PostgreSQL: ClientType.PG, MySQL: ClientType.MYSQL, SQLITE: ClientType.SQLITE, OpenAI: SyncDataType.OPENAI, } type IntegrationsSubType = (typeof integrationType)[keyof typeof integrationType] function getStaticInitializor(type: IntegrationsSubType) { const genericValues = { payload: {}, } switch (type) { case integrationType.PostgreSQL: return { ...genericValues, type: integrationType.PostgreSQL, title: 'PostgreSQL', logo: h(GeneralBaseLogo, { 'source-type': 'pg', 'class': 'logo', }), } case integrationType.MySQL: return { ...genericValues, type: integrationType.MySQL, title: 'MySQL', logo: h(GeneralBaseLogo, { 'source-type': 'mysql2', 'class': 'logo', }), } case integrationType.SQLITE: return { ...genericValues, type: integrationType.SQLITE, title: 'SQLite', logo: h(GeneralBaseLogo, { 'source-type': 'sqlite3', 'class': 'logo', }), } } } const integrationForms: Record = {} const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState(() => { const router = useRouter() const route = router.currentRoute const { api } = useApi() const pageMode = ref(null) const activeIntegration = ref(null) const activeIntegrationItem = ref(null) const workspaceStore = useWorkspace() const { activeWorkspaceId } = storeToRefs(workspaceStore) const integrations = ref([]) const searchQuery = ref('') const integrationsCategoryFilter = ref([]) const integrationPaginationData = ref({ page: 1, pageSize: 25, totalRows: 0 }) const deleteConfirmText = ref() const isLoadingIntegrations = ref(false) const eventBus = useEventBus(Symbol('integrationStore')) const { $api, $e } = useNuxtApp() const { t } = useI18n() const { aiIntegrations } = useNocoAi() const isFromIntegrationPage = ref(false) const integrationsRefreshKey = ref(0) const requestIntegration = ref({ isOpen: false, msg: '', isLoading: false, }) const successConfirmModal = ref({ isOpen: false, title: t('msg.success.connectionAdded'), connectionTitle: '', description: t('msg.success.connectionAddedDesc'), }) const activeViewTab = computed({ get() { return (route.value.query?.tab as string) ?? 'integrations' }, set(tab: string) { if (requestIntegration.value.isOpen) { requestIntegration.value.isOpen = false } router.push({ query: { ...route.value.query, tab } }) }, }) const loadIntegrations = async ( databaseOnly = false, baseId = undefined, page: number = integrationPaginationData.value.page!, limit: number = integrationPaginationData.value.pageSize!, ) => { try { if (!activeWorkspaceId.value) return isLoadingIntegrations.value = true if (!databaseOnly && limit * (page - 1) > integrationPaginationData.value.totalRows!) { integrationPaginationData.value.page = 1 page = 1 } const { list, pageInfo } = await api.integration.list( databaseOnly ? { type: IntegrationsType.Database, includeDatabaseInfo: true, baseId, } : { offset: limit * (page - 1), limit, ...(searchQuery.value.trim() ? { query: searchQuery.value } : {}), }, ) integrations.value = list if (!databaseOnly) { integrationPaginationData.value.totalRows = pageInfo.totalRows ?? 0 } } catch (e) { await message.error(await extractSdkResponseErrorMsg(e)) integrations.value = [] integrationPaginationData.value.totalRows = 0 integrationPaginationData.value.page = 1 } finally { isLoadingIntegrations.value = false } } const addIntegration = async (integration: IntegrationItemType) => { activeIntegration.value = integration.dynamic ? integration : getStaticInitializor(integration.subType) activeIntegrationItem.value = integration if (integration.dynamic === true && !(integration.subType in integrationForms)) { const integrationInfo = await $api.integrations.info(integration.type, integration.subType) if (integrationInfo?.form) { integrationForms[integration.subType] = integrationInfo.form activeIntegrationItem.value.form = integrationInfo.form } } else if (integration.dynamic === true) { activeIntegrationItem.value.form = integrationForms[integration.subType] } pageMode.value = IntegrationsPageMode.ADD $e('c:integration:add') } const deleteIntegration = async (integration: IntegrationType, force = false) => { if (!integration?.id) return $e('a:integration:delete') try { await api.integration.delete(integration.id, { query: force ? { force: 'true' } : {}, }) if (integration.type === IntegrationsType.Ai) { aiIntegrations.value = aiIntegrations.value.filter((i) => i.id !== integration.id) } await loadIntegrations() // await message.success(`Connection ${integration.title} deleted successfully`) return true } catch (e) { const error = await extractSdkResponseErrorMsgv2(e) if (error.error === NcErrorType.INTEGRATION_NOT_FOUND) { await message.error(error.message?.replace(integration.id, integration.title!)) window.location.reload() return } await message.error(await extractSdkResponseErrorMsg(e)) } deleteConfirmText.value = null } const updateIntegration = async (integration: IntegrationType) => { if (!integration.id) return $e('a:integration:update') try { await api.integration.update(integration.id, integration) if (integration.type === IntegrationsType.Ai) { aiIntegrations.value = aiIntegrations.value.map((i) => { if (i.id === integration.id) { i.title = integration.title } return i }) } await loadIntegrations() pageMode.value = null activeIntegration.value = null await message.success(`Connection "${integration.title}" updated successfully`) } catch (e) { await message.error(await extractSdkResponseErrorMsg(e)) } } const setDefaultIntegration = async (integration: IntegrationType) => { if (!integration.id) return $e('a:integration:set-default') try { await api.integration.setDefault(integration.id) if (integration.type === IntegrationsType.Ai) { aiIntegrations.value = aiIntegrations.value.map((i) => { if (i.id === integration.id) { i.is_default = true } else { i.is_default = false } return i }) } await loadIntegrations() pageMode.value = null activeIntegration.value = null await message.success(`Connection "${integration.title}" set as default successfully`) } catch (e) { await message.error(await extractSdkResponseErrorMsg(e)) } } const saveIntegration = async ( integration: IntegrationType, mode: 'create' | 'duplicate' = 'create', loadDatasourceInfo = false, baseId: string | undefined = undefined, ) => { if (mode === 'create') { $e('a:integration:create') } else { $e('a:integration:duplicate') } try { const response = await api.integration.create(integration) if (response && response?.id) { if (!loadDatasourceInfo) { integrations.value.push(response) } if (response.type === IntegrationsType.Ai) { aiIntegrations.value.push({ id: response.id, title: response.title, is_default: response.is_default, type: response.type, sub_type: response.sub_type, }) } } await loadIntegrations(loadDatasourceInfo, baseId) if (mode === 'create') { eventBus.emit(IntegrationStoreEvents.INTEGRATION_ADD, response) } pageMode.value = null activeIntegration.value = null if (response?.title && mode === 'create') { if (isFromIntegrationPage.value) { activeViewTab.value = 'connections' if (response.type === IntegrationsType.Database) { successConfirmModal.value.connectionTitle = response.title ?? '' successConfirmModal.value.isOpen = true } } else { await message.success(`Connection "${response.title}" created successfully`) } } } catch (e) { await message.error(await extractSdkResponseErrorMsg(e)) } } const duplicateIntegration = async (integration: IntegrationType) => { if (!integration?.id) return try { isLoadingIntegrations.value = true saveIntegration( { title: integration.title, config: {}, type: integration.type, copy_from_id: integration.id, }, 'duplicate', ) } catch (e) { await message.error(await extractSdkResponseErrorMsg(e)) } finally { isLoadingIntegrations.value = false } } const getIntegration = async ( integration: IntegrationType, options?: { includeConfig?: boolean includeSources?: boolean }, ) => { if (!integration?.id) return try { const integrationWithConfig = await api.integration.read(integration.id, { ...(options || {}), }) return integrationWithConfig } catch (e) { const error = await extractSdkResponseErrorMsgv2(e) if (error.error === NcErrorType.INTEGRATION_NOT_FOUND) { await message.error(error.message?.replace(integration.id!, integration.title!)) window.location.reload() return } await message.error(await extractSdkResponseErrorMsg(e)) } } const editIntegration = async (integration: IntegrationType) => { if (!integration?.id) return try { const integrationWithConfig = await getIntegration(integration, { includeConfig: true }) activeIntegration.value = integrationWithConfig const integrationItem = allIntegrations.find( (item) => item.type === integration.type && item.subType === integration.sub_type, )! activeIntegrationItem.value = integrationItem if (integrationItem.dynamic === true && !(integrationItem.subType in integrationForms)) { const integrationInfo = await $api.integrations.info(integrationItem.type, integrationItem.subType) if (integrationInfo?.form) { integrationForms[integrationItem.subType] = integrationInfo.form activeIntegrationItem.value.form = integrationInfo.form } } else if (integrationItem.dynamic === true) { activeIntegrationItem.value.form = integrationForms[integrationItem.subType] } pageMode.value = IntegrationsPageMode.EDIT $e('c:integration:edit') } catch {} } const saveIntegrationRequest = async (msg: string) => { if (!msg?.trim()) return requestIntegration.value.isLoading = true try { $e('a:integration:new-request', { value: requestIntegration.value.msg, }) requestIntegration.value.isLoading = false requestIntegration.value.isOpen = false requestIntegration.value.msg = '' await message.success('Your request has been successfully submitted') } catch (e) { requestIntegration.value.isLoading = false await message.error(await extractSdkResponseErrorMsg(e)) } } const listIntegrationByType = async (type: IntegrationsType) => { if (!activeWorkspaceId.value) return const { list } = await api.integration.list({ type, }) return list } const loadDynamicIntegrations = async () => { if (integrationsInitialized.value) return integrationsInitialized.value = true const dynamicIntegrations = (await $api.integrations.list()) as { type: IntegrationsType subType: string meta: { title?: string icon?: string description?: string order?: number } }[] dynamicIntegrations.sort((a, b) => (a.meta.order ?? Infinity) - (b.meta.order ?? Infinity)) for (const di of dynamicIntegrations) { let icon: FunctionalComponent | VNode if (di.meta.icon) { if (di.meta.icon in iconMap) { icon = iconMap[di.meta.icon as keyof typeof iconMap] } else { if (isValidURL(di.meta.icon)) { icon = h('img', { src: di.meta.icon, alt: di.meta.title || di.subType, }) } } } else { icon = iconMap.puzzle } const integration: IntegrationItemType = { title: di.meta.title || di.subType, subType: di.subType, icon, type: di.type, isAvailable: true, dynamic: true, } allIntegrations.push(integration) integrationsRefreshKey.value++ } } const integrationsIconMap = computed(() => { // eslint-disable-next-line no-unused-expressions integrationsRefreshKey.value const map: Record = {} for (const integration of allIntegrations) { map[integration.subType] = integration.icon } return map }) return { IntegrationsPageMode, integrationType, pageMode, activeIntegration, activeIntegrationItem, integrationsRefreshKey, integrations, isLoadingIntegrations, deleteConfirmText, eventBus, requestIntegration, integrationPaginationData, activeViewTab, isFromIntegrationPage, successConfirmModal, searchQuery, integrationsCategoryFilter, addIntegration, loadIntegrations, deleteIntegration, updateIntegration, saveIntegration, editIntegration, duplicateIntegration, saveIntegrationRequest, getIntegration, setDefaultIntegration, integrationsIconMap, listIntegrationByType, loadDynamicIntegrations, } }, 'integrations-store') export { useProvideIntegrationViewStore } export function useIntegrationStore() { const integrationStore = _useIntegrationStore() if (integrationStore == null) return useProvideIntegrationViewStore() return integrationStore }