diff --git a/backend/src/api/integration/index.ts b/backend/src/api/integration/index.ts index 553f7f3d11..3ffc448443 100644 --- a/backend/src/api/integration/index.ts +++ b/backend/src/api/integration/index.ts @@ -29,6 +29,11 @@ export default (app) => { `/tenant/:tenantId/integration/autocomplete`, safeWrap(require('./integrationAutocomplete').default), ) + app.get(`/tenant/:tenantId/integration/global`, safeWrap(require('./integrationGlobal').default)) + app.get( + `/tenant/:tenantId/integration/global/status`, + safeWrap(require('./integrationGlobalStatus').default), + ) app.get(`/tenant/:tenantId/integration`, safeWrap(require('./integrationList').default)) app.get(`/tenant/:tenantId/integration/:id`, safeWrap(require('./integrationFind').default)) diff --git a/backend/src/api/integration/integrationGlobal.ts b/backend/src/api/integration/integrationGlobal.ts new file mode 100644 index 0000000000..2491129c7b --- /dev/null +++ b/backend/src/api/integration/integrationGlobal.ts @@ -0,0 +1,13 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + const payload = await new IntegrationService(req).findGlobalIntegrations( + req.params.tenantId, + req.query, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/integrationGlobalStatus.ts b/backend/src/api/integration/integrationGlobalStatus.ts new file mode 100644 index 0000000000..005a6a8e89 --- /dev/null +++ b/backend/src/api/integration/integrationGlobalStatus.ts @@ -0,0 +1,14 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + + const payload = await new IntegrationService(req).findGlobalIntegrationsStatusCount( + req.params.tenantId, + req.query, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/database/repositories/integrationRepository.ts b/backend/src/database/repositories/integrationRepository.ts index 4e7551c99c..4617d53e16 100644 --- a/backend/src/database/repositories/integrationRepository.ts +++ b/backend/src/database/repositories/integrationRepository.ts @@ -3,6 +3,13 @@ import Sequelize, { QueryTypes } from 'sequelize' import { captureApiChange, integrationConnectAction } from '@crowd/audit-logs' import { Error404 } from '@crowd/common' +import { + fetchGlobalIntegrations, + fetchGlobalIntegrationsCount, + fetchGlobalIntegrationsStatusCount, + fetchGlobalNotConnectedIntegrations, + fetchGlobalNotConnectedIntegrationsCount, +} from '@crowd/data-access-layer/src/integrations' import { IntegrationRunState, PlatformType } from '@crowd/types' import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' @@ -393,6 +400,64 @@ class IntegrationRepository { }) } + /** + * Finds global integrations based on the provided parameters. + * + * @param {string} tenantId - The ID of the tenant for which integrations are to be found. + * @param {Object} filters - An object containing various filter options. + * @param {string} [filters.platform=null] - The platform to filter integrations by. + * @param {string[]} [filters.status=['done']] - The status of the integrations to be filtered. + * @param {string} [filters.query=''] - The search query to filter integrations. + * @param {number} [filters.limit=20] - The maximum number of integrations to return. + * @param {number} [filters.offset=0] - The offset for pagination. + * @param {IRepositoryOptions} options - The repository options for querying. + * @returns {Promise} The result containing the rows of integrations and metadata about the query. + */ + static async findGlobalIntegrations( + tenantId: string, + { platform = null, status = ['done'], query = '', limit = 20, offset = 0 }, + options: IRepositoryOptions, + ) { + const qx = SequelizeRepository.getQueryExecutor(options) + if (status.includes('not-connected')) { + const rows = await fetchGlobalNotConnectedIntegrations( + qx, + tenantId, + platform, + query, + limit, + offset, + ) + const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, query) + return { rows, count: +result.count, limit: +limit, offset: +offset } + } + + const rows = await fetchGlobalIntegrations(qx, tenantId, status, platform, query, limit, offset) + const [result] = await fetchGlobalIntegrationsCount(qx, tenantId, status, platform, query) + return { rows, count: +result.count, limit: +limit, offset: +offset } + } + + /** + * Retrieves the count of global integrations statuses for a specified tenant and platform. + * This method aggregates the count of different integration statuses including a 'not-connected' status. + * + * @param {string} tenantId - The unique identifier for the tenant. + * @param {Object} param1 - The optional parameters. + * @param {string|null} [param1.platform=null] - The platform to filter the integrations. Default is null. + * @param {IRepositoryOptions} options - The options for the repository operations. + * @return {Promise>} A promise that resolves to an array of objects containing the statuses and their counts. + */ + static async findGlobalIntegrationsStatusCount( + tenantId: string, + { platform = null }, + options: IRepositoryOptions, + ) { + const qx = SequelizeRepository.getQueryExecutor(options) + const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, '') + const rows = await fetchGlobalIntegrationsStatusCount(qx, tenantId, platform) + return [...rows, { status: 'not-connected', count: +result.count }] + } + static async findAndCountAll( { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, options: IRepositoryOptions, diff --git a/backend/src/services/integrationService.ts b/backend/src/services/integrationService.ts index 170bd79de5..120c631c13 100644 --- a/backend/src/services/integrationService.ts +++ b/backend/src/services/integrationService.ts @@ -298,6 +298,28 @@ export default class IntegrationService { return IntegrationRepository.findAndCountAll(args, this.options) } + /** + * Retrieves global integrations for the specified tenant. + * + * @param {string} tenantId - The unique identifier of the tenant. + * @param {any} args - Additional arguments that define search criteria or constraints. + * @return {Promise} A promise that resolves to the list of global integrations matching the criteria. + */ + async findGlobalIntegrations(tenantId: string, args: any) { + return IntegrationRepository.findGlobalIntegrations(tenantId, args, this.options) + } + + /** + * Fetches the global count of integration statuses for a given tenant. + * + * @param {string} tenantId - The ID of the tenant for which to fetch the count. + * @param {Object} args - Additional arguments to refine the query. + * @return {Promise} A promise that resolves to the count of global integration statuses. + */ + async findGlobalIntegrationsStatusCount(tenantId: string, args: any) { + return IntegrationRepository.findGlobalIntegrationsStatusCount(tenantId, args, this.options) + } + async query(data) { const advancedFilter = data.filter const orderBy = data.orderBy diff --git a/frontend/src/modules/admin/modules/integration/components/integration-list.vue b/frontend/src/modules/admin/modules/integration/components/integration-list.vue deleted file mode 100644 index a5f16a0a2c..0000000000 --- a/frontend/src/modules/admin/modules/integration/components/integration-list.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - diff --git a/frontend/src/modules/admin/modules/integration/components/status/integration-platform-select.vue b/frontend/src/modules/admin/modules/integration/components/status/integration-platform-select.vue new file mode 100644 index 0000000000..81a7b3545c --- /dev/null +++ b/frontend/src/modules/admin/modules/integration/components/status/integration-platform-select.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/integration/config/status/connecting.ts b/frontend/src/modules/admin/modules/integration/config/status/connecting.ts index 93b7fa5ddc..0270e8a13b 100644 --- a/frontend/src/modules/admin/modules/integration/config/status/connecting.ts +++ b/frontend/src/modules/admin/modules/integration/config/status/connecting.ts @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con const connecting: IntegrationStatusConfig = { key: 'connecting', show: (integration: any) => integration.status === 'in-progress', + statuses: ['in-progress'], status: { text: 'Connecting', icon: 'loader-4-line animate-spin', diff --git a/frontend/src/modules/admin/modules/integration/config/status/done.ts b/frontend/src/modules/admin/modules/integration/config/status/done.ts index dea904221d..b7221d45bd 100644 --- a/frontend/src/modules/admin/modules/integration/config/status/done.ts +++ b/frontend/src/modules/admin/modules/integration/config/status/done.ts @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con const done: IntegrationStatusConfig = { key: 'done', show: (integration: any) => integration.status === 'done', + statuses: ['done'], status: { text: 'Connected', icon: 'checkbox-circle-fill', diff --git a/frontend/src/modules/admin/modules/integration/config/status/error.ts b/frontend/src/modules/admin/modules/integration/config/status/error.ts index d41eb5ec82..2588899aa9 100644 --- a/frontend/src/modules/admin/modules/integration/config/status/error.ts +++ b/frontend/src/modules/admin/modules/integration/config/status/error.ts @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con const error: IntegrationStatusConfig = { key: 'error', show: (integration: any) => integration.status === 'error', + statuses: ['error'], status: { text: 'Connection failed', icon: 'error-warning-fill', diff --git a/frontend/src/modules/admin/modules/integration/config/status/index.ts b/frontend/src/modules/admin/modules/integration/config/status/index.ts index 3f8f4c3da5..b8901c9e69 100644 --- a/frontend/src/modules/admin/modules/integration/config/status/index.ts +++ b/frontend/src/modules/admin/modules/integration/config/status/index.ts @@ -1,12 +1,13 @@ import done from './done'; import error from './error'; import waitingForAction from './waiting-for-action'; -import waitingApproval from './waiting-approval'; import connecting from './connecting'; +import notConnected from './not-connected'; export interface IntegrationStatusConfig { key: string; show: (integration: any) => boolean; + statuses: string[], status: { text: string; icon: string; @@ -27,7 +28,6 @@ export const lfIntegrationStatuses: Record = { done, error, waitingForAction, - waitingApproval, connecting, }; @@ -36,6 +36,7 @@ export const lfIntegrationStatusesTabs: Record connecting, waitingForAction, error, + notConnected, }; export const getIntegrationStatus = (integration: any): IntegrationStatusConfig => { diff --git a/frontend/src/modules/admin/modules/integration/config/status/not-connected.ts b/frontend/src/modules/admin/modules/integration/config/status/not-connected.ts new file mode 100644 index 0000000000..797f79c364 --- /dev/null +++ b/frontend/src/modules/admin/modules/integration/config/status/not-connected.ts @@ -0,0 +1,23 @@ +import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/config/status/index'; + +const notConnected: IntegrationStatusConfig = { + key: 'notConnected', + show: (integration: any) => !integration || integration.status === 'not-connected', + statuses: ['not-connected'], + status: { + text: 'Not-connected', + icon: '', + color: 'text-gray-600', + }, + actionBar: { + background: 'bg-gray-50', + color: 'text-gray-900', + }, + tabs: { + text: 'Not connected', + empty: 'No integrations to be connected', + badge: 'bg-gray-100', + }, +}; + +export default notConnected; diff --git a/frontend/src/modules/admin/modules/integration/config/status/waiting-approval.ts b/frontend/src/modules/admin/modules/integration/config/status/waiting-approval.ts deleted file mode 100644 index 38dca7a97b..0000000000 --- a/frontend/src/modules/admin/modules/integration/config/status/waiting-approval.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/config/status/index'; - -const waitingApproval: IntegrationStatusConfig = { - key: 'waitingApproval', - show: (integration: any) => integration.status === 'waiting-approval', - status: { - text: 'Waiting for approval', - icon: 'time-fill', - color: 'text-gray-500', - }, - actionBar: { - background: 'bg-gray-50', - color: 'text-gray-600', - }, - tabs: { - text: 'Waiting approval', - empty: 'No integrations waiting approval', - badge: 'bg-gray-50', - }, -}; - -export default waitingApproval; diff --git a/frontend/src/modules/admin/modules/integration/config/status/waiting-for-action.ts b/frontend/src/modules/admin/modules/integration/config/status/waiting-for-action.ts index 45c19204b6..33bec2afa5 100644 --- a/frontend/src/modules/admin/modules/integration/config/status/waiting-for-action.ts +++ b/frontend/src/modules/admin/modules/integration/config/status/waiting-for-action.ts @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con const waitingForAction: IntegrationStatusConfig = { key: 'waitingForAction', show: (integration: any) => ['pending-action', 'mapping'].includes(integration.status), + statuses: ['pending-action', 'mapping'], status: { text: 'Action required', icon: 'alert-fill', diff --git a/frontend/src/modules/admin/modules/integration/pages/integration-list.page.vue b/frontend/src/modules/admin/modules/integration/pages/integration-list.page.vue index 24a95da950..c6e76e45a2 100644 --- a/frontend/src/modules/admin/modules/integration/pages/integration-list.page.vue +++ b/frontend/src/modules/admin/modules/integration/pages/integration-list.page.vue @@ -13,7 +13,7 @@ > - {{ getSegmentName(grandparentId) }} + {{ getSegmentName(grandparentId) || subproject?.grandparentName }} @@ -30,31 +30,111 @@ Connect with the data sources where interactions happen within your community.

+ + +
+
+ + + All integrations + + +
+ {{ status.tabs.text }} +
+ {{ getIntegrationCountPerStatus[key] }} +
+
+
+
+
+
+
- + +
+ + + +
+ + diff --git a/frontend/src/modules/admin/modules/users/services/users.service.ts b/frontend/src/modules/admin/modules/users/services/users.service.ts index c463e0ec67..b396be53cf 100644 --- a/frontend/src/modules/admin/modules/users/services/users.service.ts +++ b/frontend/src/modules/admin/modules/users/services/users.service.ts @@ -14,4 +14,30 @@ export class UsersService { return response.data; } + + static async fetchGlobalIntegrations(query: any) { + const tenantId = AuthService.getTenantId(); + + const response = await authAxios.get( + `/tenant/${tenantId}/integration/global`, + { + params: query, + }, + ); + + return response.data; + } + + static async fetchGlobalIntegrationStatusCount(query: any) { + const tenantId = AuthService.getTenantId(); + + const response = await authAxios.get( + `/tenant/${tenantId}/integration/global/status`, + { + params: query, + }, + ); + + return response.data; + } } diff --git a/frontend/src/modules/lf/segments/pages/lf-admin-panel-page.vue b/frontend/src/modules/lf/segments/pages/lf-admin-panel-page.vue index 317dcf07e7..b716a37ebb 100644 --- a/frontend/src/modules/lf/segments/pages/lf-admin-panel-page.vue +++ b/frontend/src/modules/lf/segments/pages/lf-admin-panel-page.vue @@ -12,6 +12,11 @@ v-if="activeTab === 'project-groups'" /> + + + props.modelValue, () => { }); const readHash = () => { + if (!props.fragment) { + return; + } const hash = route?.hash.replace('#', ''); if (hash && hash !== model.value) { emit('update:modelValue', hash); diff --git a/services/libs/data-access-layer/src/integrations/index.ts b/services/libs/data-access-layer/src/integrations/index.ts new file mode 100644 index 0000000000..b186495bf7 --- /dev/null +++ b/services/libs/data-access-layer/src/integrations/index.ts @@ -0,0 +1,223 @@ +import { IIntegration } from '@crowd/types' + +import { QueryExecutor } from '../queryExecutor' + +/** + * Fetches a list of global integrations based on the provided filters. + * + * @param {QueryExecutor} qx - The query executor object to perform database queries. + * @param {string} tenantId - The ID of the tenant for which integrations are being fetched. + * @param {string[]} status - An array of status values to filter the integrations. + * @param {string | null} platform - The platform to filter integrations, or null to include all platforms. + * @param {string} query - A search string to filter segment names. + * @param {number} limit - The maximum number of results to return. + * @param {number} offset - The number of results to skip before starting to collect the result set. + * @return {Promise} A promise that resolves to the list of integrations matching the filters. + */ +export async function fetchGlobalIntegrations( + qx: QueryExecutor, + tenantId: string, + status: string[], + platform: string | null, + query: string, + limit: number, + offset: number, +): Promise { + return qx.select( + ` + SELECT i.id, + i.platform, + i.status, + i.settings, + i."segmentId", + s.name, + s."parentId", + s."parentName", + s."grandparentId", + s."grandparentName" + FROM "integrations" i + JOIN segments s ON i."segmentId" = s.id + WHERE i."status" = ANY ($(status)::text[]) + AND i."deletedAt" IS NULL + AND i."tenantId" = $(tenantId) + AND ($(platform) IS NULL OR i."platform" = $(platform)) + AND s.name ILIKE $(query) + LIMIT $(limit) OFFSET $(offset) + `, + { + status, + platform, + tenantId, + query: `%${query}%`, + limit, + offset, + }, + ) +} + +/** + * Fetches the count of global integrations based on the specified criteria. + * + * @param {QueryExecutor} qx - The query executor to run the database query. + * @param {string} tenantId - The tenant identifier to filter integrations. + * @param {string[]} status - The array of statuses to filter integrations. + * @param {string|null} platform - The platform to filter by, or null for all platforms. + * @param {string} query - The query string to filter segment names. + * @return {Promise<{ count: number }[]>} The promise that resolves to an array with the count of integrations. + */ +export async function fetchGlobalIntegrationsCount( + qx: QueryExecutor, + tenantId: string, + status: string[], + platform: string | null, + query: string, +): Promise<{ count: number }[]> { + return qx.select( + ` + SELECT COUNT(*) + FROM "integrations" i + JOIN segments s ON i."segmentId" = s.id + WHERE i."status" = ANY ($(status)::text[]) + AND i."deletedAt" IS NULL + AND i."tenantId" = $(tenantId) + AND ($(platform) IS NULL OR i."platform" = $(platform)) + AND s.name ILIKE $(query) + `, + { + status, + platform, + tenantId, + query: `%${query}%`, + }, + ) +} + +/** + * Fetches a list of global integrations that are not connected. + * + * @param {QueryExecutor} qx - The query executor to run the queries. + * @param {string} tenantId - The tenant ID to filter the integrations. + * @param {string | null} platform - The specific platform to filter the integrations, or null for all platforms. + * @param {string} query - The query string to filter by integration name. + * @param {number} limit - The maximum number of integrations to return. + * @param {number} offset - The number of integrations to skip before starting to collect the result set. + * + * @return {Promise} A promise that resolves to an array of integrations not connected to the specified platform. + */ +export async function fetchGlobalNotConnectedIntegrations( + qx: QueryExecutor, + tenantId: string, + platform: string | null, + query: string, + limit: number, + offset: number, +): Promise { + return qx.select( + ` + WITH unique_platforms AS (SELECT DISTINCT platform + FROM public.integrations), + connected_platforms AS (SELECT i.platform, s.id as "segmentId" + FROM integrations i + JOIN "segments" s ON i."segmentId" = s.id + WHERE i."deletedAt" IS NULL) + SELECT up.platform, + s.id as "segmentId", + s.name, + s."parentId", + s."parentName", + s."grandparentId", + s."grandparentName" + FROM unique_platforms up + JOIN segments s ON true + LEFT JOIN connected_platforms cp + ON up.platform = cp.platform AND s.id = cp."segmentId" + WHERE cp.platform IS NULL + AND s."parentId" IS NOT NULL + AND s."grandparentId" IS NOT NULL + AND s."tenantId" = $(tenantId) + AND ($(platform) IS NULL OR up."platform" = $(platform)) + AND s.name ILIKE $(query) + LIMIT $(limit) OFFSET $(offset) + `, + { + platform, + tenantId, + query: `%${query}%`, + limit, + offset, + }, + ) +} + +/** + * Fetches the count of global integrations that are not connected. + * + * @param {QueryExecutor} qx - The query executor used to perform SQL queries. + * @param {string} tenantId - The ID of the tenant for whom integrations need to be fetched. + * @param {string|null} platform - The platform to filter results by (optional). + * @param {string} query - The name pattern to filter segments by. + * @return {Promise<{count: number}[]>} - A promise that resolves to an array of objects containing the count of not connected integrations. + */ +export async function fetchGlobalNotConnectedIntegrationsCount( + qx: QueryExecutor, + tenantId: string, + platform: string | null, + query: string, +): Promise<{ count: number }[]> { + return qx.select( + ` + WITH unique_platforms AS (SELECT DISTINCT platform + FROM public.integrations), + connected_platforms AS (SELECT i.platform, s.id as "segmentId" + FROM integrations i + JOIN "segments" s ON i."segmentId" = s.id + WHERE i."deletedAt" IS NULL) + SELECT COUNT(*) + FROM unique_platforms up + JOIN segments s ON true + LEFT JOIN connected_platforms cp + ON up.platform = cp.platform AND s.id = cp."segmentId" + WHERE cp.platform IS NULL + AND s."parentId" IS NOT NULL + AND s."grandparentId" IS NOT NULL + AND s."tenantId" = $(tenantId) + AND ($(platform) IS NULL OR up."platform" = $(platform)) + AND s.name ILIKE $(query) + `, + { + platform, + tenantId, + query: `%${query}%`, + }, + ) +} + +/** + * Fetches the count of integrations grouped by their status for a given tenant and optional platform. + * + * @param {QueryExecutor} qx - The query executor used to perform the database query. + * @param {string} tenantId - The ID of the tenant for which to fetch the integrations status count. + * @param {string | null} platform - The platform to filter the integrations by, or null if no platform filter is to be applied. + * @return {Promise<{status: string, count: number}[]>} A promise that resolves to an array of objects, each containing a status and the corresponding count of integrations. + */ +export async function fetchGlobalIntegrationsStatusCount( + qx: QueryExecutor, + tenantId: string, + platform: string | null, +): Promise<{ status: string; count: number }[]> { + return qx.select( + ` + SELECT i.status, + COUNT(*) AS count + FROM "integrations" i + WHERE i."deletedAt" IS NULL + AND i."tenantId" = $(tenantId) + AND ($(platform) IS NULL OR i."platform" = $(platform)) + GROUP BY i.status + `, + { + platform, + tenantId, + }, + ) +}