mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #5030 from Infisical/fix/show-native-integrations-tab-and-add-button
fix(native-integrations): rollback native integration tab
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
@@ -14,7 +12,6 @@ import {
|
|||||||
useProject
|
useProject
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useGetWorkspaceIntegrations } from "@app/hooks/api";
|
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||||
|
|
||||||
@@ -35,9 +32,6 @@ export const IntegrationsListPage = () => {
|
|||||||
from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id
|
from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: integrations } = useGetWorkspaceIntegrations(currentProject.id);
|
|
||||||
const hasNativeIntegrations = Boolean(integrations?.length);
|
|
||||||
|
|
||||||
const updateSelectedTab = (tab: string) => {
|
const updateSelectedTab = (tab: string) => {
|
||||||
navigate({
|
navigate({
|
||||||
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
|
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
|
||||||
@@ -77,11 +71,9 @@ export const IntegrationsListPage = () => {
|
|||||||
<Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
<Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
||||||
Infrastructure Integrations
|
Infrastructure Integrations
|
||||||
</Tab>
|
</Tab>
|
||||||
{hasNativeIntegrations && (
|
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||||
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
|
Native Integrations
|
||||||
Native Integrations
|
</Tab>
|
||||||
</Tab>
|
|
||||||
)}
|
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={IntegrationsListPageTabs.SecretSyncs}>
|
<TabPanel value={IntegrationsListPageTabs.SecretSyncs}>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
@@ -98,47 +90,15 @@ export const IntegrationsListPage = () => {
|
|||||||
<TabPanel value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
<TabPanel value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
||||||
<InfrastructureIntegrationTab />
|
<InfrastructureIntegrationTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
{hasNativeIntegrations && (
|
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||||
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
|
<ProjectPermissionCan
|
||||||
<div className="mb-4 flex items-start rounded-md border border-yellow-600/75 bg-yellow-900/20 px-3 py-2">
|
renderGuardBanner
|
||||||
<div className="flex text-sm text-yellow-100">
|
I={ProjectPermissionActions.Read}
|
||||||
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
|
a={ProjectPermissionSub.Integrations}
|
||||||
<div>
|
>
|
||||||
<p className="font-medium">
|
<NativeIntegrationsTab />
|
||||||
We're moving Native Integrations to{" "}
|
</ProjectPermissionCan>
|
||||||
<a
|
</TabPanel>
|
||||||
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'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>
|
|
||||||
)}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { NavigateFn } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { localStorageService } from "@app/helpers/localStorage";
|
||||||
|
import { TCloudIntegration } from "@app/hooks/api/types";
|
||||||
|
|
||||||
export const createIntegrationMissingEnvVarsNotification = (
|
export const createIntegrationMissingEnvVarsNotification = (
|
||||||
slug: string,
|
slug: string,
|
||||||
@@ -21,3 +27,349 @@ export const createIntegrationMissingEnvVarsNotification = (
|
|||||||
),
|
),
|
||||||
title: "Missing Environment Variables"
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CloudIntegrationSection } from "./CloudIntegrationSection";
|
||||||
@@ -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 { createNotification } from "@app/components/notifications";
|
||||||
import { Checkbox, DeleteActionModal, Spinner } from "@app/components/v2";
|
import { Button, Checkbox, DeleteActionModal, Spinner } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useOrganization, useProject } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useDeleteIntegration,
|
useDeleteIntegration,
|
||||||
@@ -14,26 +17,38 @@ import {
|
|||||||
import { IntegrationAuth } from "@app/hooks/api/integrationAuth/types";
|
import { IntegrationAuth } from "@app/hooks/api/integrationAuth/types";
|
||||||
import { TIntegration } from "@app/hooks/api/integrations/types";
|
import { TIntegration } from "@app/hooks/api/integrations/types";
|
||||||
|
|
||||||
|
import { redirectForProviderAuth } from "../../IntegrationsListPage.utils";
|
||||||
|
import { CloudIntegrationSection } from "../CloudIntegrationSection";
|
||||||
import { IntegrationsTable } from "./IntegrationsTable";
|
import { IntegrationsTable } from "./IntegrationsTable";
|
||||||
|
|
||||||
|
enum IntegrationView {
|
||||||
|
List = "list",
|
||||||
|
New = "new"
|
||||||
|
}
|
||||||
|
|
||||||
export const NativeIntegrationsTab = () => {
|
export const NativeIntegrationsTab = () => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { environments, id: workspaceId } = currentProject;
|
const { environments, id: workspaceId } = currentProject;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } =
|
const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } =
|
||||||
useGetCloudIntegrations();
|
useGetCloudIntegrations();
|
||||||
|
|
||||||
const { data: integrationAuths, isFetching: isIntegrationAuthFetching } =
|
const {
|
||||||
useGetWorkspaceAuthorizations(
|
data: integrationAuths,
|
||||||
workspaceId,
|
isPending: isIntegrationAuthLoading,
|
||||||
useCallback((data: IntegrationAuth[]) => {
|
isFetching: isIntegrationAuthFetching
|
||||||
const groupBy: Record<string, IntegrationAuth> = {};
|
} = useGetWorkspaceAuthorizations(
|
||||||
data.forEach((el) => {
|
workspaceId,
|
||||||
groupBy[el.integration] = el;
|
useCallback((data: IntegrationAuth[]) => {
|
||||||
});
|
const groupBy: Record<string, IntegrationAuth> = {};
|
||||||
return groupBy;
|
data.forEach((el) => {
|
||||||
}, [])
|
groupBy[el.integration] = el;
|
||||||
);
|
});
|
||||||
|
return groupBy;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
// mutation
|
// mutation
|
||||||
const {
|
const {
|
||||||
@@ -43,8 +58,11 @@ export const NativeIntegrationsTab = () => {
|
|||||||
} = useGetWorkspaceIntegrations(workspaceId);
|
} = useGetWorkspaceIntegrations(workspaceId);
|
||||||
|
|
||||||
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
|
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
|
||||||
|
const {
|
||||||
const { reset: resetDeleteIntegrationAuths } = useDeleteIntegrationAuths();
|
mutateAsync: deleteIntegrationAuths,
|
||||||
|
isSuccess: isDeleteIntegrationAuthSuccess,
|
||||||
|
reset: resetDeleteIntegrationAuths
|
||||||
|
} = useDeleteIntegrationAuths();
|
||||||
|
|
||||||
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
|
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
|
||||||
const isIntegrationsEmpty = !integrations?.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
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
isDeleteIntegrationAuthSuccess &&
|
||||||
!isIntegrationFetching &&
|
!isIntegrationFetching &&
|
||||||
!isIntegrationAuthFetching &&
|
!isIntegrationAuthFetching &&
|
||||||
isIntegrationsAuthorizedEmpty &&
|
isIntegrationsAuthorizedEmpty &&
|
||||||
@@ -62,11 +81,29 @@ export const NativeIntegrationsTab = () => {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isIntegrationFetching,
|
isIntegrationFetching,
|
||||||
|
isDeleteIntegrationAuthSuccess,
|
||||||
isIntegrationAuthFetching,
|
isIntegrationAuthFetching,
|
||||||
isIntegrationsAuthorizedEmpty,
|
isIntegrationsAuthorizedEmpty,
|
||||||
isIntegrationsEmpty
|
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 (
|
const handleIntegrationDelete = async (
|
||||||
integrationId: string,
|
integrationId: string,
|
||||||
shouldDeleteIntegrationSecrets: boolean,
|
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([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"deleteConfirmation",
|
"deleteConfirmation",
|
||||||
"deleteSecretsConfirmation"
|
"deleteSecretsConfirmation"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const [view, setView] = useState<IntegrationView>(IntegrationView.List);
|
||||||
|
|
||||||
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
|
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
|
||||||
|
|
||||||
if (isIntegrationLoading || isCloudIntegrationsLoading)
|
if (isIntegrationLoading || isCloudIntegrationsLoading)
|
||||||
@@ -96,10 +150,18 @@ export const NativeIntegrationsTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{integrations?.length && (
|
{view === IntegrationView.List ? (
|
||||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Native Integrations</p>
|
<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>
|
</div>
|
||||||
<IntegrationsTable
|
<IntegrationsTable
|
||||||
cloudIntegrations={cloudIntegrations}
|
cloudIntegrations={cloudIntegrations}
|
||||||
@@ -113,6 +175,15 @@ export const NativeIntegrationsTab = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<CloudIntegrationSection
|
||||||
|
onIntegrationStart={handleProviderIntegrationStart}
|
||||||
|
onIntegrationRevoke={handleIntegrationAuthRevoke}
|
||||||
|
integrationAuths={integrationAuths}
|
||||||
|
cloudIntegrations={cloudIntegrations}
|
||||||
|
isLoading={isIntegrationAuthLoading || isCloudIntegrationsLoading}
|
||||||
|
onViewActiveIntegrations={() => setView(IntegrationView.List)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteConfirmation.isOpen}
|
isOpen={popUp.deleteConfirmation.isOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user