Refactor IntegrationsListPage: streamline Native Integrations tab and add Cloud Integration section

- Removed conditional rendering for Native Integrations tab and integrated it directly into the main layout.
- Introduced CloudIntegrationSection component to manage cloud integrations, including search functionality and integration actions.
- Enhanced integration authorization handling and added navigation for provider authentication.
- Improved state management for integration views and loading states.
This commit is contained in:
Victor Santos
2025-12-11 18:09:34 -03:00
parent e35124a8c4
commit 51c70efcfe
5 changed files with 711 additions and 69 deletions

View File

@@ -1,7 +1,5 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
@@ -14,7 +12,6 @@ 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";
@@ -35,9 +32,6 @@ 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,
@@ -77,11 +71,9 @@ export const IntegrationsListPage = () => {
<Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
Infrastructure Integrations
</Tab>
{hasNativeIntegrations && (
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
Native Integrations
</Tab>
)}
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
Native Integrations
</Tab>
</TabList>
<TabPanel value={IntegrationsListPageTabs.SecretSyncs}>
<ProjectPermissionCan
@@ -98,47 +90,15 @@ export const IntegrationsListPage = () => {
<TabPanel value={IntegrationsListPageTabs.InfrastructureIntegrations}>
<InfrastructureIntegrationTab />
</TabPanel>
{hasNativeIntegrations && (
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
<div className="mb-4 flex items-start rounded-md border border-yellow-600/75 bg-yellow-900/20 px-3 py-2">
<div className="flex text-sm text-yellow-100">
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
<div>
<p className="font-medium">
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>
.
</p>
<p className="mt-0.5 text-yellow-100/80">
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>
.
</p>
</div>
</div>
</div>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Integrations}
>
<NativeIntegrationsTab />
</ProjectPermissionCan>
</TabPanel>
)}
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Integrations}
>
<NativeIntegrationsTab />
</ProjectPermissionCan>
</TabPanel>
</Tabs>
</div>
</div>

View File

@@ -1,4 +1,10 @@
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,
@@ -21,3 +27,349 @@ 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

@@ -0,0 +1,258 @@
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

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

View File

@@ -1,8 +1,11 @@
import { useCallback, useEffect } from "react";
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 { createNotification } from "@app/components/notifications";
import { Checkbox, DeleteActionModal, Spinner } from "@app/components/v2";
import { useProject } from "@app/context";
import { Button, Checkbox, DeleteActionModal, Spinner } from "@app/components/v2";
import { useOrganization, useProject } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useDeleteIntegration,
@@ -14,26 +17,38 @@ 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, isFetching: isIntegrationAuthFetching } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
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;
}, [])
);
// mutation
const {
@@ -43,8 +58,11 @@ export const NativeIntegrationsTab = () => {
} = useGetWorkspaceIntegrations(workspaceId);
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const { reset: resetDeleteIntegrationAuths } = useDeleteIntegrationAuths();
const {
mutateAsync: deleteIntegrationAuths,
isSuccess: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuths
} = useDeleteIntegrationAuths();
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
const isIntegrationsEmpty = !integrations?.length;
@@ -53,6 +71,7 @@ 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 &&
@@ -62,11 +81,29 @@ 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,
@@ -80,11 +117,28 @@ 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)
@@ -96,10 +150,18 @@ export const NativeIntegrationsTab = () => {
return (
<>
{integrations?.length && (
{view === IntegrationView.List ? (
<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}
@@ -113,6 +175,15 @@ 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}