refactor: deprecate native integrations in favor of secret syncs; update API routes and frontend components accordingly

This commit is contained in:
Victor Santos
2025-11-25 20:26:18 -03:00
parent 5bd8fc9ae4
commit dd37fa3114
16 changed files with 149 additions and 875 deletions

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, INTEGRATION_AUTH } from "@app/lib/api-docs";
import { ForbiddenRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,6 +11,9 @@ import { Integrations } from "@app/services/integration-auth/integration-list";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
const NATIVE_INTEGRATION_DEPRECATION_MESSAGE =
"We're moving Native Integrations to Secret Syncs. Check the documentation at https://infisical.com/docs/integrations/secret-syncs/overview. If the integration you need isn't available in the Secret Syncs, please get in touch with us at team@infisical.com.";
export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -333,27 +337,33 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
})
}
},
handler: async (req) => {
const integrationAuth = await server.services.integrationAuth.saveIntegrationToken({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body
handler: async (_) => {
throw new ForbiddenRequestError({
message: NATIVE_INTEGRATION_DEPRECATION_MESSAGE
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.AUTHORIZE_INTEGRATION,
metadata: {
integration: integrationAuth.integration
}
}
});
return { integrationAuth };
// We are keeping the old response commented out for an easy revert on the API if we need to before the full phase out.
// const integrationAuth = await server.services.integrationAuth.saveIntegrationToken({
// actorId: req.permission.id,
// actor: req.permission.type,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId,
// projectId: req.body.workspaceId,
// ...req.body
// });
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: req.body.workspaceId,
// event: {
// type: EventType.AUTHORIZE_INTEGRATION,
// metadata: {
// integration: integrationAuth.integration
// }
// }
// });
// return { integrationAuth };
}
});

View File

@@ -3,17 +3,19 @@ import { z } from "zod";
import { IntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, INTEGRATION } from "@app/lib/api-docs";
import { ForbiddenRequestError } from "@app/lib/errors";
import { removeTrailingSlash, shake } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { Integrations } from "@app/services/integration-auth/integration-list";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
import {} from "../sanitizedSchemas";
const NATIVE_INTEGRATION_DEPRECATION_MESSAGE =
"We're moving Native Integrations to Secret Syncs. Check the documentation at https://infisical.com/docs/integrations/secret-syncs/overview. If the integration you need isn't available in the Secret Syncs, please get in touch with us at team@infisical.com.";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -66,52 +68,58 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { integration, integrationAuth } = await server.services.integration.createIntegration({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
handler: async (_) => {
throw new ForbiddenRequestError({
message: NATIVE_INTEGRATION_DEPRECATION_MESSAGE
});
const createIntegrationEventProperty = shake({
integrationId: integration.id.toString(),
integration: integration.integration,
environment: req.body.sourceEnvironment,
secretPath: req.body.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
}) as TIntegrationCreatedEvent["properties"];
// We are keeping the old response commented out for an easy revert on the API if we need to before the full phase out.
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: integrationAuth.projectId,
event: {
type: EventType.CREATE_INTEGRATION,
// eslint-disable-next-line
metadata: createIntegrationEventProperty
}
});
// const { integration, integrationAuth } = await server.services.integration.createIntegration({
// actorId: req.permission.id,
// actor: req.permission.type,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId,
// ...req.body
// });
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IntegrationCreated,
organizationId: req.permission.orgId,
distinctId: getTelemetryDistinctId(req),
properties: {
...createIntegrationEventProperty,
projectId: integrationAuth.projectId,
...req.auditLogInfo
}
});
return { integration };
// const createIntegrationEventProperty = shake({
// integrationId: integration.id.toString(),
// integration: integration.integration,
// environment: req.body.sourceEnvironment,
// secretPath: req.body.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
// }) as TIntegrationCreatedEvent["properties"];
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: integrationAuth.projectId,
// event: {
// type: EventType.CREATE_INTEGRATION,
// // eslint-disable-next-line
// metadata: createIntegrationEventProperty
// }
// });
// await server.services.telemetry.sendPostHogEvents({
// event: PostHogEventTypes.IntegrationCreated,
// organizationId: req.permission.orgId,
// distinctId: getTelemetryDistinctId(req),
// properties: {
// ...createIntegrationEventProperty,
// projectId: integrationAuth.projectId,
// ...req.auditLogInfo
// }
// });
// return { integration };
}
});

View File

@@ -1,32 +0,0 @@
---
title: "Create Auth"
openapi: "POST /api/v1/integration-auth/access-token"
---
## Integration Authentication Parameters
The integration authentication endpoint is generic and can be used for all native integrations.
For specific integration parameters for a given service, please review the respective documentation below.
<Tabs>
<Tab title="AWS Secrets manager">
<ParamField body="integration" type="string" initialValue="aws-secret-manager" required>
This value must be **aws-secret-manager**.
</ParamField>
<ParamField body="workspaceId" type="string" required>
Infisical project id for the integration.
</ParamField>
<ParamField body="accessId" type="string" required>
The AWS IAM User Access ID.
</ParamField>
<ParamField body="accessToken" type="string" required>
The AWS IAM User Access Secret Key.
</ParamField>
</Tab>
<Tab title="GCP Secrets manager">
Coming Soon
</Tab>
<Tab title="Heroku">
Coming Soon
</Tab>
</Tabs>

View File

@@ -1,40 +0,0 @@
---
title: "Create"
openapi: "POST /api/v1/integration"
---
## Integration Parameters
The integration creation endpoint is generic and can be used for all native integrations.
For specific integration parameters for a given service, please review the respective documentation below.
<Tabs>
<Tab title="AWS Secrets manager">
<ParamField body="integrationAuthId" type="string" required>
The ID of the integration auth object for authentication with AWS.
Refer [Create Integration Auth](./create-auth) for more info
</ParamField>
<ParamField body="isActive" type="boolean">
Whether the integration should be active or inactive
</ParamField>
<ParamField body="app" type="string" required>
The secret name used when saving secret in AWS SSM. Used for naming and can be arbitrary.
</ParamField>
<ParamField body="region" type="string" required>
The AWS region of the SSM. Example: `us-east-1`
</ParamField>
<ParamField body="sourceEnvironment" type="string" required>
The Infisical environment slug from where secrets will be synced from. Example: `dev`
</ParamField>
<ParamField body="secretPath" type="string" required>
The Infisical folder path from where secrets will be synced from. Example: `/some/path`. The root of the environment is `/`.
</ParamField>
</Tab>
<Tab title="GCP Secrets manager">
Coming Soon
</Tab>
<Tab title="Heroku">
Coming Soon
</Tab>
</Tabs>

View File

@@ -1,4 +0,0 @@
---
title: "Delete Auth By ID"
openapi: "DELETE /api/v1/integration-auth/{integrationAuthId}"
---

View File

@@ -1,4 +0,0 @@
---
title: "Delete Auth"
openapi: "DELETE /api/v1/integration-auth"
---

View File

@@ -1,4 +0,0 @@
---
title: "Delete"
openapi: "DELETE /api/v1/integration/{integrationId}"
---

View File

@@ -1,4 +0,0 @@
---
title: "Get Auth By ID"
openapi: "GET /api/v1/integration-auth/{integrationAuthId}"
---

View File

@@ -1,4 +0,0 @@
---
title: "List Auth"
openapi: "GET /api/v1/workspace/{workspaceId}/authorizations"
---

View File

@@ -1,4 +0,0 @@
---
title: "List Project Integrations"
openapi: "GET /api/v1/workspace/{workspaceId}/integrations"
---

View File

@@ -1,4 +0,0 @@
---
title: "Update"
openapi: "PATCH /api/v1/integration/{integrationId}"
---

View File

@@ -3,7 +3,15 @@ import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import {
Alert,
AlertDescription,
PageHeader,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
@@ -12,6 +20,7 @@ import {
useProject
} from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { useGetWorkspaceIntegrations } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/projects/types";
import { IntegrationsListPageTabs } from "@app/types/integrations";
@@ -32,6 +41,9 @@ export const IntegrationsListPage = () => {
from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id
});
const { data: integrations } = useGetWorkspaceIntegrations(currentProject.id);
const hasNativeIntegrations = Boolean(integrations?.length);
const updateSelectedTab = (tab: string) => {
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
@@ -65,9 +77,11 @@ export const IntegrationsListPage = () => {
<Tab variant="project" value={IntegrationsListPageTabs.SecretSyncs}>
Secret Syncs
</Tab>
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
Native Integrations
</Tab>
{hasNativeIntegrations && (
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
Native Integrations
</Tab>
)}
<Tab variant="project" value={IntegrationsListPageTabs.FrameworkIntegrations}>
Framework Integrations
</Tab>
@@ -84,15 +98,39 @@ export const IntegrationsListPage = () => {
<SecretSyncsTab />
</ProjectPermissionCan>
</TabPanel>
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Integrations}
>
<NativeIntegrationsTab />
</ProjectPermissionCan>
</TabPanel>
{hasNativeIntegrations && (
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
<Alert variant="warning" className="mb-4" hideTitle>
<AlertDescription>
We&apos;re moving Native Integrations to{" "}
<a
href="https://infisical.com/docs/integrations/secret-syncs/overview"
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-mineshaft-100"
>
Secret Syncs
</a>
. If the integration you need isn&apos;t available in the Secret Syncs menu,
please get in touch with us at{" "}
<a
href="mailto:team@infisical.com"
className="underline underline-offset-2 hover:text-mineshaft-100"
>
team@infisical.com
</a>
.
</AlertDescription>
</Alert>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Integrations}
>
<NativeIntegrationsTab />
</ProjectPermissionCan>
</TabPanel>
)}
<TabPanel value={IntegrationsListPageTabs.FrameworkIntegrations}>
<FrameworkIntegrationTab />
</TabPanel>

View File

@@ -1,10 +1,4 @@
import crypto from "crypto";
import { NavigateFn } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { localStorageService } from "@app/helpers/localStorage";
import { TCloudIntegration } from "@app/hooks/api/types";
export const createIntegrationMissingEnvVarsNotification = (
slug: string,
@@ -27,349 +21,3 @@ export const createIntegrationMissingEnvVarsNotification = (
),
title: "Missing Environment Variables"
});
export const redirectForProviderAuth = (
orgId: string,
projectId: string,
navigate: NavigateFn,
integrationOption: TCloudIntegration
) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorageService.setIntegrationProjectId(projectId);
switch (integrationOption.slug) {
case "gcp-secret-manager":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/gcp-secret-manager/authorize",
params: {
orgId,
projectId
}
});
break;
case "azure-key-vault": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
return;
}
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/authorize",
params: {
orgId,
projectId
},
search: {
clientId: integrationOption.clientId,
state
}
});
break;
}
case "azure-app-configuration": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
return;
}
const link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-app-configuration/oauth2/callback&response_mode=query&scope=https://azconfig.io/.default openid offline_access&state=${state}`;
window.location.assign(link);
break;
}
case "aws-parameter-store":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-parameter-store/authorize",
params: {
orgId,
projectId
}
});
break;
case "aws-secret-manager":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-secret-manager/authorize",
params: {
orgId,
projectId
}
});
break;
case "heroku": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
return;
}
const link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
window.location.assign(link);
break;
}
case "vercel": {
if (!integrationOption.clientSlug) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
return;
}
const link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
window.location.assign(link);
break;
}
case "netlify": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
return;
}
const link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
window.location.assign(link);
break;
}
case "github":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/github/auth-mode-selection",
params: {
orgId,
projectId
}
});
break;
case "gitlab":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/gitlab/authorize",
params: {
orgId,
projectId
}
});
break;
case "render":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/render/authorize",
params: {
orgId,
projectId
}
});
break;
case "flyio":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/flyio/authorize",
params: {
orgId,
projectId
}
});
break;
case "circleci":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/circleci/authorize",
params: {
orgId,
projectId
}
});
break;
case "databricks":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/databricks/authorize",
params: {
orgId,
projectId
}
});
break;
case "laravel-forge":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/laravel-forge/authorize",
params: {
orgId,
projectId
}
});
break;
case "travisci":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/travisci/authorize",
params: {
orgId,
projectId
}
});
break;
case "supabase":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/supabase/authorize",
params: {
orgId,
projectId
}
});
break;
case "checkly":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/checkly/authorize",
params: {
orgId,
projectId
}
});
break;
case "qovery":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/qovery/authorize",
params: {
orgId,
projectId
}
});
break;
case "railway":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/railway/authorize",
params: {
orgId,
projectId
}
});
break;
case "terraform-cloud":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/terraform-cloud/authorize",
params: {
orgId,
projectId
}
});
break;
case "hashicorp-vault":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/hashicorp-vault/authorize",
params: {
orgId,
projectId
}
});
break;
case "cloudflare-pages":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-pages/authorize",
params: {
orgId,
projectId
}
});
break;
case "cloudflare-workers":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-workers/authorize",
params: {
orgId,
projectId
}
});
break;
case "bitbucket": {
if (!integrationOption.clientId) {
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
return;
}
const link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`;
window.location.assign(link);
break;
}
case "codefresh":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/codefresh/authorize",
params: {
orgId,
projectId
}
});
break;
case "digital-ocean-app-platform":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/authorize",
params: {
orgId,
projectId
}
});
break;
case "cloud-66":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloud-66/authorize",
params: {
orgId,
projectId
}
});
break;
case "northflank":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/northflank/authorize",
params: {
orgId,
projectId
}
});
break;
case "windmill":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/windmill/authorize",
params: {
orgId,
projectId
}
});
break;
case "teamcity":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/teamcity/authorize",
params: {
orgId,
projectId
}
});
break;
case "hasura-cloud":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/hasura-cloud/authorize",
params: {
orgId,
projectId
}
});
break;
case "rundeck":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/rundeck/authorize",
params: {
orgId,
projectId
}
});
break;
case "azure-devops":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-devops/authorize",
params: {
orgId,
projectId
}
});
break;
case "octopus-deploy":
navigate({
to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/octopus-deploy/authorize",
params: {
orgId,
projectId
}
});
break;
default:
break;
}
} catch (err) {
console.error(err);
}
};

View File

@@ -1,258 +0,0 @@
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
faCheck,
faChevronLeft,
faMagnifyingGlass,
faSearch,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { NoEnvironmentsBanner } from "@app/components/integrations/NoEnvironmentsBanner";
import { createNotification } from "@app/components/notifications";
import {
Button,
DeleteActionModal,
EmptyState,
Input,
Skeleton,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useProject,
useProjectPermission
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type Props = {
isLoading?: boolean;
integrationAuths?: Record<string, IntegrationAuth>;
cloudIntegrations?: TCloudIntegration[];
onIntegrationStart: (slug: string) => void;
// cb: handle popUpClose child->parent communication pattern
onIntegrationRevoke: (slug: string, cb: () => void) => void;
onViewActiveIntegrations?: () => void;
};
type TRevokeIntegrationPopUp = { provider: string };
const SECRET_SYNCS = Object.values(SecretSync) as string[];
const isSecretSyncAvailable = (type: string) => SECRET_SYNCS.includes(type);
export const CloudIntegrationSection = ({
isLoading,
cloudIntegrations = [],
integrationAuths = {},
onIntegrationStart,
onIntegrationRevoke,
onViewActiveIntegrations
}: Props) => {
const { t } = useTranslation();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
const { permission } = useProjectPermission();
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
const navigate = useNavigate();
const isEmpty = !isLoading && !cloudIntegrations?.length;
const sortedCloudIntegrations = useMemo(() => {
const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
if (currentProject?.environments.length === 0) {
return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false }));
}
return sortedIntegrations;
}, [cloudIntegrations, currentProject?.environments]);
const [search, setSearch] = useState("");
const filteredIntegrations = sortedCloudIntegrations?.filter((cloudIntegration) =>
cloudIntegration.name.toLowerCase().includes(search.toLowerCase().trim())
);
return (
<div>
{currentProject?.environments.length === 0 && (
<div className="px-5">
<NoEnvironmentsBanner projectId={currentProject.id} />
</div>
)}
<div className="m-4 mt-0 flex flex-col items-start justify-between px-2 text-xl">
{onViewActiveIntegrations && (
<Button
variant="link"
onClick={onViewActiveIntegrations}
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
>
Back to Integrations
</Button>
)}
<div className="flex w-full flex-col justify-between gap-4 whitespace-nowrap lg:flex-row lg:items-end lg:gap-8">
<div className="flex-1">
<h1 className="text-3xl font-medium">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search cloud integrations..."
containerClassName="flex-1 h-min text-base"
/>
</div>
</div>
<div className="mx-2 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
))}
{!isLoading && filteredIntegrations.length ? (
filteredIntegrations.map((cloudIntegration) => {
const syncSlug = cloudIntegration.syncSlug ?? cloudIntegration.slug;
const isSyncAvailable = isSecretSyncAvailable(syncSlug);
return (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className={`group relative ${
cloudIntegration.isAvailable
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
: "opacity-50"
} flex h-36 flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3`}
onClick={() => {
if (isSyncAvailable) {
navigate({
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
params: {
orgId: currentOrg.id,
projectId: currentProject.id
},
search: {
selectedTab: IntegrationsListPageTabs.SecretSyncs,
addSync: syncSlug as SecretSync
}
});
return;
}
if (!cloudIntegration.isAvailable) return;
if (
permission.cannot(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
)
) {
createNotification({
type: "error",
text: "You do not have permission to create an integration"
});
return;
}
onIntegrationStart(cloudIntegration.slug);
}}
key={cloudIntegration.slug}
>
<div className="m-auto flex flex-col items-center">
<img
src={`/images/integrations/${cloudIntegration.image}`}
height={60}
width={60}
className="mt-auto"
alt="integration logo"
/>
<div
className={`mt-2 max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200 ${isSyncAvailable ? "mb-4" : ""}`}
>
{cloudIntegration.name}
</div>
</div>
{cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-30 h-full">
<div className="relative h-full">
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden rounded-tr-md rounded-bl-md bg-primary px-2 py-0.5 text-xs whitespace-nowrap text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
<Tooltip content="Revoke Access">
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async (event) => {
event.stopPropagation();
handlePopUpOpen("deleteConfirmation", {
provider: cloudIntegration.slug
});
}}
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 group-hover:h-full hover:opacity-100"
>
<FontAwesomeIcon icon={faXmark} size="xl" />
</div>
</Tooltip>
</div>
</div>
)}
{isSyncAvailable && (
<div className="absolute bottom-0 left-0 z-30 h-full w-full">
<div className="relative h-full">
<div className="absolute bottom-0 left-0 w-full flex-row overflow-hidden rounded-br-md rounded-bl-md bg-yellow/20 px-2 py-0.5 text-center text-xs whitespace-nowrap text-yellow">
Secret Sync Available
</div>
</div>
</div>
)}
</div>
);
})
) : (
<EmptyState
className="col-span-full h-32 w-full rounded-md bg-transparent pt-14"
title="No cloud integrations match search..."
icon={faSearch}
/>
)}
</div>
{isEmpty && (
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{Array.from({ length: 16 }).map((_, index) => (
<div
key={`dummy-cloud-integration-${index + 1}`}
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
/>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure you want to revoke access ${
(popUp?.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider || " "
}?`}
subTitle="This will remove all the secret integration of this provider!!!"
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""}
onDeleteApproved={async () => {
onIntegrationRevoke(
(popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider,
() => handlePopUpClose("deleteConfirmation")
);
}}
/>
</div>
);
};

View File

@@ -1 +0,0 @@
export { CloudIntegrationSection } from "./CloudIntegrationSection";

View File

@@ -1,11 +1,8 @@
import { useCallback, useEffect, useState } from "react";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect } from "react";
import { createNotification } from "@app/components/notifications";
import { Button, Checkbox, DeleteActionModal, Spinner } from "@app/components/v2";
import { useOrganization, useProject } from "@app/context";
import { Checkbox, DeleteActionModal, Spinner } from "@app/components/v2";
import { useProject } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useDeleteIntegration,
@@ -17,38 +14,26 @@ import {
import { IntegrationAuth } from "@app/hooks/api/integrationAuth/types";
import { TIntegration } from "@app/hooks/api/integrations/types";
import { redirectForProviderAuth } from "../../IntegrationsListPage.utils";
import { CloudIntegrationSection } from "../CloudIntegrationSection";
import { IntegrationsTable } from "./IntegrationsTable";
enum IntegrationView {
List = "list",
New = "new"
}
export const NativeIntegrationsTab = () => {
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
const { environments, id: workspaceId } = currentProject;
const navigate = useNavigate();
const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const {
data: integrationAuths,
isPending: isIntegrationAuthLoading,
isFetching: isIntegrationAuthFetching
} = useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
const { data: integrationAuths, isFetching: isIntegrationAuthFetching } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
// mutation
const {
@@ -58,11 +43,8 @@ export const NativeIntegrationsTab = () => {
} = useGetWorkspaceIntegrations(workspaceId);
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuths,
isSuccess: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuths
} = useDeleteIntegrationAuths();
const { reset: resetDeleteIntegrationAuths } = useDeleteIntegrationAuths();
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
const isIntegrationsEmpty = !integrations?.length;
@@ -71,7 +53,6 @@ export const NativeIntegrationsTab = () => {
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook for isSuccess to go back to false
useEffect(() => {
if (
isDeleteIntegrationAuthSuccess &&
!isIntegrationFetching &&
!isIntegrationAuthFetching &&
isIntegrationsAuthorizedEmpty &&
@@ -81,29 +62,11 @@ export const NativeIntegrationsTab = () => {
}
}, [
isIntegrationFetching,
isDeleteIntegrationAuthSuccess,
isIntegrationAuthFetching,
isIntegrationsAuthorizedEmpty,
isIntegrationsEmpty
]);
const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return;
try {
redirectForProviderAuth(currentOrg.id, currentProject.id, navigate, selectedCloudIntegration);
} catch (error) {
console.error(error);
}
};
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
handleProviderIntegration(provider);
};
const handleIntegrationDelete = async (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
@@ -117,28 +80,11 @@ export const NativeIntegrationsTab = () => {
});
};
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return;
await deleteIntegrationAuths({
integration: provider,
workspaceId
});
if (cb) cb();
createNotification({
type: "success",
text: "Revoked provider authentication"
});
};
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation",
"deleteSecretsConfirmation"
] as const);
const [view, setView] = useState<IntegrationView>(IntegrationView.List);
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
if (isIntegrationLoading || isCloudIntegrationsLoading)
@@ -150,18 +96,10 @@ export const NativeIntegrationsTab = () => {
return (
<>
{view === IntegrationView.List ? (
{integrations?.length && (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-medium text-mineshaft-100">Native Integrations</p>
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setView(IntegrationView.New)}
>
Add Integration
</Button>
</div>
<IntegrationsTable
cloudIntegrations={cloudIntegrations}
@@ -175,15 +113,6 @@ export const NativeIntegrationsTab = () => {
}}
/>
</div>
) : (
<CloudIntegrationSection
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
integrationAuths={integrationAuths}
cloudIntegrations={cloudIntegrations}
isLoading={isIntegrationAuthLoading || isCloudIntegrationsLoading}
onViewActiveIntegrations={() => setView(IntegrationView.List)}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}