From 7d3a62cc4c8eff1f3f69314bfdfac486dd5e416c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Mon, 20 May 2024 20:56:29 +0800 Subject: [PATCH 1/6] feat: added integration sync status --- ...40520064127_add-integration-sync-status.ts | 43 ++++++++++++++++++ backend/src/db/schemas/integrations.ts | 5 ++- backend/src/services/secret/secret-queue.ts | 45 +++++++++++++------ frontend/src/hooks/api/integrations/types.ts | 3 ++ .../IntegrationsSection.tsx | 36 ++++++++++++--- 5 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 backend/src/db/migrations/20240520064127_add-integration-sync-status.ts diff --git a/backend/src/db/migrations/20240520064127_add-integration-sync-status.ts b/backend/src/db/migrations/20240520064127_add-integration-sync-status.ts new file mode 100644 index 000000000..74b828714 --- /dev/null +++ b/backend/src/db/migrations/20240520064127_add-integration-sync-status.ts @@ -0,0 +1,43 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasIsSyncedColumn = await knex.schema.hasColumn(TableName.Integration, "isSynced"); + const hasSyncMessageColumn = await knex.schema.hasColumn(TableName.Integration, "syncMessage"); + const hasLastSyncJobId = await knex.schema.hasColumn(TableName.Integration, "lastSyncJobId"); + + await knex.schema.alterTable(TableName.Integration, (t) => { + if (!hasIsSyncedColumn) { + t.boolean("isSynced").nullable(); + } + + if (!hasSyncMessageColumn) { + t.text("syncMessage").nullable(); + } + + if (!hasLastSyncJobId) { + t.string("lastSyncJobId").nullable(); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasIsSyncedColumn = await knex.schema.hasColumn(TableName.Integration, "isSynced"); + const hasSyncMessageColumn = await knex.schema.hasColumn(TableName.Integration, "syncMessage"); + const hasLastSyncJobId = await knex.schema.hasColumn(TableName.Integration, "lastSyncJobId"); + + await knex.schema.alterTable(TableName.Integration, (t) => { + if (hasIsSyncedColumn) { + t.dropColumn("isSynced"); + } + + if (hasSyncMessageColumn) { + t.dropColumn("syncMessage"); + } + + if (hasLastSyncJobId) { + t.dropColumn("lastSyncJobId"); + } + }); +} diff --git a/backend/src/db/schemas/integrations.ts b/backend/src/db/schemas/integrations.ts index 203498c85..47cf9e627 100644 --- a/backend/src/db/schemas/integrations.ts +++ b/backend/src/db/schemas/integrations.ts @@ -28,7 +28,10 @@ export const IntegrationsSchema = z.object({ secretPath: z.string().default("/"), createdAt: z.date(), updatedAt: z.date(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + isSynced: z.boolean().nullable().optional(), + syncMessage: z.string().nullable().optional(), + lastSyncJobId: z.string().nullable().optional() }); export type TIntegrations = z.infer; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index e1c8d61af..f3e3f1731 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -463,20 +463,37 @@ export const secretQueueFactory = ({ }); } - await syncIntegrationSecrets({ - createManySecretsRawFn, - updateManySecretsRawFn, - integrationDAL, - integration, - integrationAuth, - secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, - accessId: accessId as string, - accessToken, - appendices: { - prefix: metadata?.secretPrefix || "", - suffix: metadata?.secretSuffix || "" - } - }); + try { + await syncIntegrationSecrets({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, + integration, + integrationAuth, + secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, + accessId: accessId as string, + accessToken, + appendices: { + prefix: metadata?.secretPrefix || "", + suffix: metadata?.secretSuffix || "" + } + }); + + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: "", + isSynced: true + }); + } catch (err: unknown) { + logger.info("Secret integration sync error:", err); + await integrationDAL.updateById(integration.id, { + lastSyncJobId: job.id, + lastUsed: new Date(), + syncMessage: (err as Error)?.message, + isSynced: false + }); + } } logger.info("Secret integration sync ended: %s", job.id); diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index a67ce15a8..345e41b1a 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -29,6 +29,9 @@ export type TIntegration = { secretPath: string; createdAt: string; updatedAt: string; + lastUsed?: string; + isSynced?: boolean; + syncMessage?: string; __v: number; metadata?: { secretSuffix?: string; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 185cc411f..46ef496ea 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; -import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faArrowRight, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; import { integrationSlugNameMapping } from "public/data/frequentConstants"; import { ProjectPermissionCan } from "@app/components/permissions"; @@ -74,7 +75,7 @@ export const IntegrationsSection = ({ )} {!isLoading && isBotActive && ( -
+
{integrations?.map((integration) => (
-
+
{(integration.integration === "hashicorp-vault" && `${integration.app} - path: ${integration.path}`) || (integration.scope === "github-org" && `${integration.owner}`) || - (integration.integration === "aws-parameter-store" && `${integration.path}`) || + (integration.integration === "aws-parameter-store" && + `${integration.path}`) || (integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) || integration.app} @@ -188,6 +190,28 @@ export const IntegrationsSection = ({ )}
+ {!!integration.isSynced && !!integration.lastUsed && ( +
+
+ Last sync: {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} +
+ + + +
+ )} handlePopUpToggle("deleteConfirmation", isOpen)} deleteKey={ (popUp?.deleteConfirmation?.data as TIntegration)?.app || From 9253c6932592be5f607bbc4d210ce482e55511b6 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 21 May 2024 02:35:23 +0800 Subject: [PATCH 2/6] misc: finalized ui design of integration sync status --- .../IntegrationsSection.tsx | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 46ef496ea..05968e086 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { faArrowRight, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons"; +import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { integrationSlugNameMapping } from "public/data/frequentConstants"; @@ -13,6 +14,7 @@ import { FormLabel, IconButton, Skeleton, + Tag, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; @@ -189,35 +191,51 @@ export const IntegrationsSection = ({
)}
-
- {!!integration.isSynced && !!integration.lastUsed && ( -
-
- Last sync: {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} -
+
+ {integration.isSynced != null && integration.lastUsed != null && ( + +
+ +
Last sync
+
+
+ {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} +
+ {!integration.isSynced && ( + <> +
+ +
Fail reason
+
+
+ {integration.syncMessage} +
+ + )} +
} > - +
Sync Status
-
+ )} {(isAllowed: boolean) => ( -
+
handlePopUpOpen("deleteConfirmation", integration)} From 629bd9b7c6fb50d33de1c659a1a8fcb77eeaf05c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 21 May 2024 13:56:44 +0800 Subject: [PATCH 3/6] added support for manual syncing of integrations --- .../ee/services/audit-log/audit-log-types.ts | 21 ++++ backend/src/lib/api-docs/constants.ts | 3 + .../server/routes/v1/integration-router.ts | 61 +++++++++- .../integration/integration-service.ts | 34 +++++- .../services/integration/integration-types.ts | 4 + .../src/hooks/api/integrations/queries.tsx | 33 ++++- .../IntegrationsSection.tsx | 114 ++++++++++++------ 7 files changed, 229 insertions(+), 41 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 87546882e..e512389d7 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -51,6 +51,7 @@ export enum EventType { UNAUTHORIZE_INTEGRATION = "unauthorize-integration", CREATE_INTEGRATION = "create-integration", DELETE_INTEGRATION = "delete-integration", + MANUAL_SYNC_INTEGRATION = "manual-sync-integration", ADD_TRUSTED_IP = "add-trusted-ip", UPDATE_TRUSTED_IP = "update-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip", @@ -281,6 +282,25 @@ interface DeleteIntegrationEvent { }; } +interface ManualSyncIntegrationEvent { + type: EventType.MANUAL_SYNC_INTEGRATION; + metadata: { + integrationId: string; + integration: string; + environment: string; + secretPath: string; + url?: string; + app?: string; + appId?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + path?: string; + region?: string; + }; +} + interface AddTrustedIPEvent { type: EventType.ADD_TRUSTED_IP; metadata: { @@ -791,6 +811,7 @@ export type Event = | UnauthorizeIntegrationEvent | CreateIntegrationEvent | DeleteIntegrationEvent + | ManualSyncIntegrationEvent | AddTrustedIPEvent | UpdateTrustedIPEvent | DeleteTrustedIPEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f9a5ef312..8acb5c95a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -649,6 +649,9 @@ export const INTEGRATION = { }, DELETE: { integrationId: "The ID of the integration object." + }, + SYNC: { + integrationId: "The ID of the integration object to manually sync" } }; diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index e40e3f799..1fd92df3a 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -262,5 +262,64 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { } }); - // TODO(akhilmhdh-pg): manual sync + server.route({ + method: "POST", + url: "/:integrationId/sync", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Manually trigger sync of an integration by integration id", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + integrationId: z.string().trim().describe(INTEGRATION.SYNC.integrationId) + }), + response: { + 200: z.object({ + integration: IntegrationsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const integration = await server.services.integration.syncIntegration({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: integration.projectId, + event: { + type: EventType.MANUAL_SYNC_INTEGRATION, + // eslint-disable-next-line + metadata: shake({ + integrationId: integration.id, + integration: integration.integration, + environment: integration.environment.slug, + secretPath: integration.secretPath, + url: integration.url, + app: integration.app, + appId: integration.appId, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId, + targetService: integration.targetService, + targetServiceId: integration.targetServiceId, + path: integration.path, + region: integration.region + // eslint-disable-next-line + }) as any + } + }); + + return { integration }; + } + }); }; diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index f4f8cdb44..eff73c1b6 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -9,7 +9,12 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TIntegrationDALFactory } from "./integration-dal"; -import { TCreateIntegrationDTO, TDeleteIntegrationDTO, TUpdateIntegrationDTO } from "./integration-types"; +import { + TCreateIntegrationDTO, + TDeleteIntegrationDTO, + TSyncIntegrationDTO, + TUpdateIntegrationDTO +} from "./integration-types"; type TIntegrationServiceFactoryDep = { integrationDAL: TIntegrationDALFactory; @@ -201,10 +206,35 @@ export const integrationServiceFactory = ({ return integrations; }; + const syncIntegration = async ({ id, actorId, actor, actorOrgId, actorAuthMethod }: TSyncIntegrationDTO) => { + const integration = await integrationDAL.findById(id); + if (!integration) { + throw new BadRequestError({ message: "Integration not found" }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integration.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + + await secretQueueService.syncIntegrations({ + environment: integration.environment.slug, + secretPath: integration.secretPath, + projectId: integration.projectId + }); + + return { ...integration, envId: integration.environment.id }; + }; + return { createIntegration, updateIntegration, deleteIntegration, - listIntegrationByProject + listIntegrationByProject, + syncIntegration }; }; diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 50d0de762..1c8772478 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -59,3 +59,7 @@ export type TUpdateIntegrationDTO = { export type TDeleteIntegrationDTO = { id: string; } & Omit; + +export type TSyncIntegrationDTO = { + id: string; +} & Omit; diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 2d4c8a313..2e90a5ba5 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -1,9 +1,11 @@ +import { MutableRefObject } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; -import { TCloudIntegration } from "./types"; +import { TCloudIntegration, TIntegration } from "./types"; export const integrationQueryKeys = { getIntegrations: () => ["integrations"] as const @@ -110,3 +112,32 @@ export const useDeleteIntegration = () => { } }); }; + +export const useSyncIntegration = (pollingRef: MutableRefObject) => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({ + mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`), + onSuccess: (_, { id, workspaceId, lastUsed }) => { + // eslint-disable-next-line no-param-reassign + pollingRef.current = setInterval(() => { + const integrations: TIntegration[] | undefined = queryClient.getQueryData( + workspaceKeys.getWorkspaceIntegrations(workspaceId) + ); + + const integration = integrations?.find((entry) => entry.id === id); + if (!integration || integration.lastUsed !== lastUsed) { + createNotification({ + text: "Integration successfully synced", + type: "success" + }); + clearInterval(pollingRef.current as NodeJS.Timeout); + // eslint-disable-next-line no-param-reassign + pollingRef.current = null; + return; + } + queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); + }, 3500); + } + }); +}; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 05968e086..abe48e022 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -1,6 +1,7 @@ +import { useEffect, useRef } from "react"; import Link from "next/link"; import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons"; -import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faArrowRight, faRefresh, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { integrationSlugNameMapping } from "public/data/frequentConstants"; @@ -9,6 +10,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { Alert, AlertDescription, + Button, DeleteActionModal, EmptyState, FormLabel, @@ -19,6 +21,7 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; +import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; import { TIntegration } from "@app/hooks/api/types"; type Props = { @@ -42,6 +45,17 @@ export const IntegrationsSection = ({ "deleteConfirmation" ] as const); + const syncPollingRef = useRef(null); + const { mutate: syncIntegration } = useSyncIntegration(syncPollingRef); + + useEffect(() => { + return () => { + if (syncPollingRef.current) { + clearInterval(syncPollingRef.current); + } + }; + }, []); + return (
@@ -193,49 +207,75 @@ export const IntegrationsSection = ({
{integration.isSynced != null && integration.lastUsed != null && ( - - -
- -
Last sync
-
-
- {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} -
- {!integration.isSynced && ( - <> -
- -
Fail reason
-
-
- {integration.syncMessage} -
- - )} -
- } + <> + -
Sync Status
- -
+ +
+ +
Last sync
+
+
+ {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} +
+ {!integration.isSynced && ( + <> +
+ +
Fail reason
+
+
+ {integration.syncMessage} +
+ + )} +
+ } + > +
Sync Status
+
+ + {!integration.isSynced && integration.lastUsed != null && ( +
+ + + +
+ )} + )} {(isAllowed: boolean) => ( -
+
handlePopUpOpen("deleteConfirmation", integration)} From d6dae049590169828d6d4d202e965baef7656083 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 21 May 2024 14:01:15 +0800 Subject: [PATCH 4/6] misc: removed unnecessary notification --- frontend/src/hooks/api/integrations/queries.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 2e90a5ba5..f5c0e9580 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -1,7 +1,6 @@ import { MutableRefObject } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; @@ -127,10 +126,6 @@ export const useSyncIntegration = (pollingRef: MutableRefObject entry.id === id); if (!integration || integration.lastUsed !== lastUsed) { - createNotification({ - text: "Integration successfully synced", - type: "success" - }); clearInterval(pollingRef.current as NodeJS.Timeout); // eslint-disable-next-line no-param-reassign pollingRef.current = null; From 9b404c215bb7eb17e509d3c2b0a6ff01b5d1c6a7 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 21 May 2024 16:04:36 +0800 Subject: [PATCH 5/6] adjustment: ui changes to sync button --- .../components/IntegrationsSection/IntegrationsSection.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index abe48e022..fc231f0fa 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -259,11 +259,11 @@ export const IntegrationsSection = ({ }) } isLoading={!!syncPollingRef.current} - className="max-w-[2.5rem]" - isDisabled={!!syncPollingRef.current} + className="max-w-[2.5rem] bg-mineshaft-500" colorSchema="primary" + variant="outline" > - +
From b9a9b6b4d9f53e7dd145fe6a6467a547defd6dfa Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 22 May 2024 00:06:06 +0800 Subject: [PATCH 6/6] misc: applied ui/ux changes --- .../src/hooks/api/integrations/queries.tsx | 29 ++-- frontend/src/hooks/api/workspace/queries.tsx | 3 +- .../IntegrationsSection.tsx | 127 ++++++++---------- 3 files changed, 65 insertions(+), 94 deletions(-) diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index f5c0e9580..9a1ee6fbf 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -1,10 +1,10 @@ -import { MutableRefObject } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; -import { TCloudIntegration, TIntegration } from "./types"; +import { TCloudIntegration } from "./types"; export const integrationQueryKeys = { getIntegrations: () => ["integrations"] as const @@ -112,27 +112,14 @@ export const useDeleteIntegration = () => { }); }; -export const useSyncIntegration = (pollingRef: MutableRefObject) => { - const queryClient = useQueryClient(); - +export const useSyncIntegration = () => { return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({ mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`), - onSuccess: (_, { id, workspaceId, lastUsed }) => { - // eslint-disable-next-line no-param-reassign - pollingRef.current = setInterval(() => { - const integrations: TIntegration[] | undefined = queryClient.getQueryData( - workspaceKeys.getWorkspaceIntegrations(workspaceId) - ); - - const integration = integrations?.find((entry) => entry.id === id); - if (!integration || integration.lastUsed !== lastUsed) { - clearInterval(pollingRef.current as NodeJS.Timeout); - // eslint-disable-next-line no-param-reassign - pollingRef.current = null; - return; - } - queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); - }, 3500); + onSuccess: () => { + createNotification({ + text: "Successfully triggered manual sync", + type: "success" + }); } }); }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index cca544df3..71dbb8e01 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -198,7 +198,8 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => useQuery({ queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId), queryFn: () => fetchWorkspaceIntegrations(workspaceId), - enabled: Boolean(workspaceId) + enabled: Boolean(workspaceId), + refetchInterval: 4000 }); export const createWorkspace = ({ diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index fc231f0fa..a6e3a45e7 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -1,7 +1,6 @@ -import { useEffect, useRef } from "react"; import Link from "next/link"; import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons"; -import { faArrowRight, faRefresh, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faArrowRight, faRefresh, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { integrationSlugNameMapping } from "public/data/frequentConstants"; @@ -45,16 +44,7 @@ export const IntegrationsSection = ({ "deleteConfirmation" ] as const); - const syncPollingRef = useRef(null); - const { mutate: syncIntegration } = useSyncIntegration(syncPollingRef); - - useEffect(() => { - return () => { - if (syncPollingRef.current) { - clearInterval(syncPollingRef.current); - } - }; - }, []); + const { mutate: syncIntegration } = useSyncIntegration(); return (
@@ -207,69 +197,62 @@ export const IntegrationsSection = ({
{integration.isSynced != null && integration.lastUsed != null && ( - <> - - -
- -
Last sync
-
-
- {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} -
- {!integration.isSynced && ( - <> -
- -
Fail reason
-
-
- {integration.syncMessage} -
- - )} + + +
+ +
Last sync
- } - > -
Sync Status
-
-
- {!integration.isSynced && integration.lastUsed != null && ( -
- - - +
+ {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} +
+ {!integration.isSynced && ( + <> +
+ +
Fail reason
+
+
+ {integration.syncMessage} +
+ + )} +
+ } + > +
+
Sync Status
+ {!integration.isSynced && }
- )} - +
+
)} +
+ + + +