mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: completed secret manager integration list and detail page
This commit is contained in:
@@ -63,6 +63,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
OverviewPage: setRoute(
|
||||
"/secret-manager/$projectId/overview",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/overview"
|
||||
),
|
||||
IntegrationDetailsByIDPage: setRoute(
|
||||
"/secret-manager/$projectId/integrations/$integrationId",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||
)
|
||||
},
|
||||
CertManager: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { TReactQueryOptions } from "@app/types/reactQuery";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import {
|
||||
@@ -530,11 +531,7 @@ export const useGetIntegrationAuthApps = (
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
},
|
||||
options?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof fetchIntegrationAuthApps>>,
|
||||
unknown,
|
||||
Awaited<ReturnType<typeof fetchIntegrationAuthApps>>
|
||||
>
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
|
||||
@@ -806,11 +803,7 @@ export const useGetIntegrationAuthOctopusDeploySpaces = (integrationAuthId: stri
|
||||
|
||||
export const useGetIntegrationAuthOctopusDeployScopeValues = (
|
||||
params: TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
||||
options?: UseQueryOptions<
|
||||
TOctopusDeployVariableSetScopeValues,
|
||||
unknown,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
>
|
||||
options?: TReactQueryOptions["options"]
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthOctopusDeployScopeValues(params),
|
||||
@@ -828,7 +821,7 @@ export const useGetIntegrationAuthBitBucketEnvironments = (
|
||||
workspaceSlug: string;
|
||||
repoSlug: string;
|
||||
},
|
||||
options?: UseQueryOptions<BitBucketEnvironment[]>
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments(
|
||||
|
||||
@@ -105,6 +105,19 @@ export enum OctopusDeployScope {
|
||||
// tenant, variable set
|
||||
}
|
||||
|
||||
export type CircleCIOrganization = {
|
||||
name: string;
|
||||
slug: string;
|
||||
projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
contexts: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
||||
integrationAuthId: string;
|
||||
spaceId: string;
|
||||
@@ -125,3 +138,8 @@ export type TOctopusDeployVariableSetScopeValues = {
|
||||
Name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export enum CircleCiScope {
|
||||
Context = "context",
|
||||
Project = "project"
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export type TIntegration = {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
azureLabel?: string;
|
||||
|
||||
kmsKeyId?: string;
|
||||
secretSuffix?: string;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faChevronLeft, faEllipsis, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useWorkspace } from "@app/context";
|
||||
import { useGetIntegration } from "@app/hooks/api";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
|
||||
import { IntegrationAuditLogsSection } from "./components/IntegrationAuditLogsSection";
|
||||
import { IntegrationConnectionSection } from "./components/IntegrationConnectionSection";
|
||||
import { IntegrationDetailsSection } from "./components/IntegrationDetailsSection";
|
||||
import { IntegrationSettingsSection } from "./components/IntegrationSettingsSection";
|
||||
import { integrationSlugNameMapping } from "./IntegrationsDetailsByIDPage.utils";
|
||||
|
||||
export const IntegrationDetailsByIDPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const integrationId = useParams({
|
||||
from: ROUTE_PATHS.SecretManager.IntegrationDetailsByIDPage.id,
|
||||
select: (el) => el.integrationId
|
||||
});
|
||||
|
||||
const { data: integration } = useGetIntegration(integrationId, {
|
||||
refetchInterval: 4000
|
||||
});
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const { mutateAsync: syncIntegration } = useSyncIntegration();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Integration Details | Infisical</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{integration ? (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="mb-4"
|
||||
>
|
||||
Integrations
|
||||
</Button>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-3xl font-semibold text-white">
|
||||
{integrationSlugNameMapping[integration.integration]} Integration
|
||||
</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await syncIntegration({
|
||||
id: integration.id,
|
||||
lastUsed: integration.lastUsed!,
|
||||
workspaceId: projectId!
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faRefresh} />
|
||||
Manually Sync
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
Delete Integration
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="mr-4 w-96">
|
||||
<IntegrationDetailsSection integration={integration} />
|
||||
<IntegrationConnectionSection integration={integration} />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<IntegrationSettingsSection integration={integration} />
|
||||
<IntegrationAuditLogsSection integration={integration} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<EmptyState title="Error: Unable to fetch integration." className="py-12" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export const integrationSlugNameMapping: Record<string, string> = {
|
||||
"azure-key-vault": "Azure Key Vault",
|
||||
"aws-parameter-store": "AWS Parameter Store",
|
||||
"aws-secret-manager": "AWS Secrets Manager",
|
||||
heroku: "Heroku",
|
||||
vercel: "Vercel",
|
||||
netlify: "Netlify",
|
||||
github: "GitHub",
|
||||
gitlab: "GitLab",
|
||||
render: "Render",
|
||||
"laravel-forge": "Laravel Forge",
|
||||
railway: "Railway",
|
||||
flyio: "Fly.io",
|
||||
circleci: "CircleCI",
|
||||
databricks: "Databricks",
|
||||
travisci: "TravisCI",
|
||||
supabase: "Supabase",
|
||||
checkly: "Checkly",
|
||||
qovery: "Qovery",
|
||||
"terraform-cloud": "Terraform Cloud",
|
||||
teamcity: "TeamCity",
|
||||
"hashicorp-vault": "Vault",
|
||||
"cloudflare-pages": "Cloudflare Pages",
|
||||
"cloudflare-workers": "Cloudflare Workers",
|
||||
codefresh: "Codefresh",
|
||||
"digital-ocean-app-platform": "Digital Ocean App Platform",
|
||||
bitbucket: "Bitbucket",
|
||||
"cloud-66": "Cloud 66",
|
||||
northflank: "Northflank",
|
||||
windmill: "Windmill",
|
||||
"gcp-secret-manager": "GCP Secret Manager",
|
||||
"hasura-cloud": "Hasura Cloud",
|
||||
rundeck: "Rundeck",
|
||||
"azure-devops": "Azure DevOps",
|
||||
"azure-app-configuration": "Azure App Configuration",
|
||||
"octopus-deploy": "Octopus Deploy"
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { EmptyState } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection";
|
||||
|
||||
// Add more events if needed
|
||||
const INTEGRATION_EVENTS = [EventType.INTEGRATION_SYNCED];
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
};
|
||||
|
||||
export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return subscription?.auditLogs ? (
|
||||
<div className="h-full w-full min-w-[51rem] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Displaying audit logs from the last {auditLogsRetentionDays} days
|
||||
</p>
|
||||
</div>
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { integrationId: integration.id },
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||
eventType: INTEGRATION_EVENTS
|
||||
}}
|
||||
filterClassName="bg-mineshaft-900 static"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full min-w-[51rem] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4 opacity-60">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
className="rounded-lg"
|
||||
title={
|
||||
<div>
|
||||
<p>
|
||||
Please{" "}
|
||||
{subscription && subscription.slug !== null ? (
|
||||
<Link to="/organization/billing" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
className="cursor-pointer font-medium text-primary-500 transition-all hover:text-primary-600"
|
||||
target="_blank"
|
||||
>
|
||||
upgrade your subscription
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
href="https://infisical.com/scheduledemo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<a
|
||||
className="cursor-pointer font-medium text-primary-500 transition-all hover:text-primary-600"
|
||||
target="_blank"
|
||||
>
|
||||
upgrade your subscription
|
||||
</a>
|
||||
</a>
|
||||
)}
|
||||
to view integration logs
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import { integrationSlugNameMapping } from "../IntegrationsDetailsByIDPage.utils";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
};
|
||||
|
||||
export const IntegrationConnectionSection = ({ integration }: Props) => {
|
||||
const specifcQoveryDetails = () => {
|
||||
if (integration.integration !== "qovery") return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
|
||||
<div className="text-sm text-mineshaft-300">{integration?.owner || "-"}</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Project" />
|
||||
<div className="text-sm text-mineshaft-300">{integration?.targetService || "-"}</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<FormLabel
|
||||
className="text-sm font-semibold text-mineshaft-300"
|
||||
label="Target Environment"
|
||||
/>
|
||||
<div className="text-sm text-mineshaft-300">{integration?.targetEnvironment || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isNotAwsManagerOneToOneDetails = () => {
|
||||
const isAwsSecretManagerOneToOne =
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE;
|
||||
|
||||
if (isAwsSecretManagerOneToOne) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formLabel = () => {
|
||||
switch (integration.integration) {
|
||||
case "qovery":
|
||||
return integration.scope;
|
||||
case "circleci":
|
||||
if (integration.scope === CircleCiScope.Context) {
|
||||
return "Context";
|
||||
}
|
||||
|
||||
return "Project";
|
||||
case "terraform-cloud":
|
||||
return "Project";
|
||||
case "aws-secret-manager":
|
||||
return "Secret";
|
||||
case "aws-parameter-store":
|
||||
case "rundeck":
|
||||
return "Path";
|
||||
case "bitbucket":
|
||||
return "Repository";
|
||||
case "octopus-deploy":
|
||||
return "Project";
|
||||
case "github":
|
||||
if (["github-env", "github-repo"].includes(integration.scope!)) {
|
||||
return "Repository";
|
||||
}
|
||||
return "Organization";
|
||||
|
||||
default:
|
||||
return "App";
|
||||
}
|
||||
};
|
||||
|
||||
const contents = () => {
|
||||
switch (integration.integration) {
|
||||
case "hashicorp-vault":
|
||||
return `${integration.app} - path: ${integration.path}`;
|
||||
case "github":
|
||||
if (integration.scope === "github-org") {
|
||||
return `${integration.owner}`;
|
||||
}
|
||||
return `${integration.owner}/${integration.app}`;
|
||||
case "aws-parameter-store":
|
||||
case "rundeck":
|
||||
return `${integration.path}`;
|
||||
|
||||
default:
|
||||
return `${integration.app}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label={formLabel()} />
|
||||
<div className="text-sm text-mineshaft-300">{contents()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const targetEnvironmentDetails = () => {
|
||||
if (integration.integration === "bitbucket") {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Workspace" />
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "octopus-deploy") {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Space" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetEnvironment}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<FormLabel
|
||||
className="text-sm font-semibold text-mineshaft-300"
|
||||
label="Target Environment"
|
||||
/>
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const generalIntegrationSpecificDetails = () => {
|
||||
if (integration.integration === "checkly" && integration.targetService) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Group" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "circleci" && integration.owner) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.owner}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "terraform-cloud" && integration.targetService) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Category" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "checkly" || integration.integration === "github") {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Suffix" />
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integration?.metadata?.secretSuffix || "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "bitbucket" && integration.targetServiceId) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel
|
||||
className="text-sm font-semibold text-mineshaft-300"
|
||||
label="Deployment Environment"
|
||||
/>
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Connection</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<FormLabel className="my-2" label="Source" />
|
||||
|
||||
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Environment" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.environment.name}</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Path" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.secretPath}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormLabel className="my-2" label="Destination" />
|
||||
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Platform" />
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integrationSlugNameMapping?.[integration.integration]}
|
||||
</div>
|
||||
|
||||
{specifcQoveryDetails()}
|
||||
{isNotAwsManagerOneToOneDetails()}
|
||||
{targetEnvironmentDetails()}
|
||||
{generalIntegrationSpecificDetails()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { faCalendarCheck, faCheckCircle, faCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import { integrationSlugNameMapping } from "../IntegrationsDetailsByIDPage.utils";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
};
|
||||
|
||||
export const IntegrationDetailsSection = ({ integration }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Integration Details</h3>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Sync Status</p>
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={twMerge(
|
||||
"mr-2 text-sm font-medium",
|
||||
integration.isSynced ? "text-green-500" : "text-red-500"
|
||||
)}
|
||||
>
|
||||
{integration.isSynced ? "Synced" : "Not Synced"}
|
||||
</p>
|
||||
<FontAwesomeIcon
|
||||
size="sm"
|
||||
className={twMerge(integration.isSynced ? "text-green-500" : "text-red-500")}
|
||||
icon={integration.isSynced ? faCheckCircle : faCircleXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{integration.lastUsed && (
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Latest Successful Sync</p>
|
||||
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
|
||||
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
|
||||
<FontAwesomeIcon icon={faCalendarCheck} className="pr-2 pt-0.5 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{!integration.isSynced && integration.syncMessage && (
|
||||
<>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Latest Sync Error</p>
|
||||
<p className="text-sm text-mineshaft-300">{integration.syncMessage}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import { OctopusDeployScopeValues } from "./OctopusDeployScopeValues";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
};
|
||||
|
||||
type Metadata = NonNullable<TIntegrationWithEnv["metadata"]>;
|
||||
type MetadataKey = keyof Metadata;
|
||||
type MetadataValue<K extends MetadataKey> = Metadata[K];
|
||||
|
||||
const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]>, string> = {
|
||||
githubVisibility: "Github Visibility",
|
||||
githubVisibilityRepoIds: "Github Visibility Repo Ids",
|
||||
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
|
||||
secretAWSTag: "Tags For Secrets Stored In AWS",
|
||||
azureLabel: "Azure Label",
|
||||
kmsKeyId: "AWS KMS Key ID",
|
||||
secretSuffix: "Secret Suffix",
|
||||
secretPrefix: "Secret Prefix",
|
||||
syncBehavior: "Secrets Sync behavior",
|
||||
initialSyncBehavior: "Initial Sync Behavior",
|
||||
mappingBehavior: "Secrets Mapping Behavior",
|
||||
scope: "Scope",
|
||||
org: "Organization",
|
||||
project: "Project",
|
||||
environment: "Environment",
|
||||
shouldDisableDelete: "AWS Secret Deletion Disabled",
|
||||
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
|
||||
shouldProtectSecrets: "GitLab Secret Protection Enabled",
|
||||
shouldEnableDelete: "GitHub Secret Deletion Enabled",
|
||||
octopusDeployScopeValues: "Octopus Deploy Scope Values",
|
||||
awsIamRole: "AWS IAM Role",
|
||||
region: "Region"
|
||||
} as const;
|
||||
|
||||
export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||
const renderValue = <K extends MetadataKey>(key: K, value: MetadataValue<K>) => {
|
||||
if (!value) return null;
|
||||
|
||||
if (key === "octopusDeployScopeValues")
|
||||
return <OctopusDeployScopeValues integration={integration} />;
|
||||
|
||||
// If it's a boolean, we render a generic "Yes" or "No" response.
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "Yes" : "No";
|
||||
}
|
||||
|
||||
// When the value is an object or array, or array of objects, we need to handle some special cases.
|
||||
if (typeof value === "object") {
|
||||
if (key === "secretAWSTag") {
|
||||
return (value as MetadataValue<"secretAWSTag">)!.map(({ key: tagKey, value: tagValue }) => (
|
||||
<p key={tagKey} className="text-sm text-gray-200">
|
||||
{tagKey}={tagValue}
|
||||
</p>
|
||||
));
|
||||
}
|
||||
|
||||
if (key === "githubVisibilityRepoIds") {
|
||||
return (value as string[]).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.length ? value : "N/A";
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!integration.metadata || Object.keys(integration.metadata).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<p className="text-lg font-semibold text-gray-200">Integration Settings</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{integration.metadata &&
|
||||
Object.entries(integration.metadata).map(([key, value]) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<p className="text-sm text-gray-400">
|
||||
{!!value && metadataMappings[key as keyof typeof metadataMappings]}
|
||||
</p>
|
||||
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { FormLabel, Spinner } from "@app/components/v2";
|
||||
import { useGetIntegrationAuthOctopusDeployScopeValues } from "@app/hooks/api/integrationAuth/queries";
|
||||
import {
|
||||
OctopusDeployScope,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
} from "@app/hooks/api/integrationAuth/types";
|
||||
import { TIntegration, TOctopusDeployScopeValues } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type OctopusDeployScopeValuesProps = {
|
||||
integration: TIntegration;
|
||||
};
|
||||
|
||||
// remove plural since Octopus Deploy can decide whether they want to use singular or plural...
|
||||
const modifyKey = (key: keyof TOctopusDeployVariableSetScopeValues) => {
|
||||
switch (key) {
|
||||
case "Processes":
|
||||
return "ProcessOwner";
|
||||
default:
|
||||
return key.substring(0, key.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
export const OctopusDeployScopeValues = ({ integration }: OctopusDeployScopeValuesProps) => {
|
||||
const hasScopeValues = Boolean(
|
||||
Object.values(integration.metadata?.octopusDeployScopeValues ?? {}).some(
|
||||
(values) => values.length > 0
|
||||
)
|
||||
);
|
||||
|
||||
const { data: scopeValues = {}, isPending } = useGetIntegrationAuthOctopusDeployScopeValues(
|
||||
{
|
||||
scope: OctopusDeployScope.Project,
|
||||
spaceId: integration.targetEnvironmentId!,
|
||||
resourceId: integration.appId!,
|
||||
integrationAuthId: integration.integrationAuthId
|
||||
},
|
||||
{
|
||||
enabled: hasScopeValues
|
||||
}
|
||||
);
|
||||
|
||||
if (!integration.metadata?.octopusDeployScopeValues)
|
||||
return <span className="text-sm text-mineshaft-400">Not Configured</span>;
|
||||
|
||||
if (isPending) return <Spinner size="sm" className="ml-2 mt-2" />;
|
||||
|
||||
const scopeValuesMap = new Map(
|
||||
Object.entries(scopeValues).map(([key, values]) => [
|
||||
modifyKey(key as keyof TOctopusDeployVariableSetScopeValues),
|
||||
new Map((values as { Name: string; Id: string }[]).map((value) => [value.Id, value.Name]))
|
||||
])
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(integration.metadata.octopusDeployScopeValues).map(([key, values]) => {
|
||||
if (!values.length) return null;
|
||||
|
||||
const getLabel = (scope: string) => {
|
||||
switch (scope as keyof TOctopusDeployScopeValues) {
|
||||
case "Role":
|
||||
return "Target Tags";
|
||||
case "Machine":
|
||||
return "Targets";
|
||||
case "ProcessOwner":
|
||||
return "Processes";
|
||||
case "Action":
|
||||
return "Steps";
|
||||
default:
|
||||
return `${scope}s`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4" key={key}>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-200" label={getLabel(key)} />
|
||||
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{values
|
||||
.map((value) => scopeValuesMap.get(key)?.get(value))
|
||||
.map((name) => (
|
||||
<p key={name}>{name}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||
)({
|
||||
component: IntegrationDetailsByIDPage
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { ContentLoader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteIntegration,
|
||||
useDeleteIntegrationAuths,
|
||||
useGetCloudIntegrations,
|
||||
useGetWorkspaceAuthorizations,
|
||||
useGetWorkspaceIntegrations
|
||||
} from "@app/hooks/api";
|
||||
import { IntegrationAuth } from "@app/hooks/api/types";
|
||||
|
||||
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
|
||||
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
|
||||
import { InfrastructureIntegrationSection } from "./components/InfrastructureIntegrationSection/InfrastructureIntegrationSection";
|
||||
import { IntegrationsSection } from "./components/IntegrationsSection";
|
||||
import { redirectForProviderAuth } from "./IntegrationsListPage.utils";
|
||||
|
||||
enum IntegrationView {
|
||||
List = "list",
|
||||
New = "new"
|
||||
}
|
||||
|
||||
const Page = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { environments, id: workspaceId } = currentWorkspace;
|
||||
const [view, setView] = useState<IntegrationView>(IntegrationView.New);
|
||||
|
||||
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;
|
||||
}, [])
|
||||
);
|
||||
|
||||
// mutation
|
||||
const {
|
||||
data: integrations,
|
||||
isPending: isIntegrationLoading,
|
||||
isFetching: isIntegrationFetching,
|
||||
isFetched: isIntegrationsFetched
|
||||
} = useGetWorkspaceIntegrations(workspaceId);
|
||||
|
||||
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
|
||||
const {
|
||||
mutateAsync: deleteIntegrationAuths,
|
||||
isSuccess: isDeleteIntegrationAuthSuccess,
|
||||
reset: resetDeleteIntegrationAuths
|
||||
} = useDeleteIntegrationAuths();
|
||||
|
||||
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
|
||||
const isIntegrationsEmpty = !integrations?.length;
|
||||
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
|
||||
// details: so on successfully deleting an integration auth, immediately integration list is refeteched
|
||||
// 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 &&
|
||||
isIntegrationsEmpty
|
||||
) {
|
||||
resetDeleteIntegrationAuths();
|
||||
}
|
||||
}, [
|
||||
isIntegrationFetching,
|
||||
isDeleteIntegrationAuthSuccess,
|
||||
isIntegrationAuthFetching,
|
||||
isIntegrationsAuthorizedEmpty,
|
||||
isIntegrationsEmpty
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setView(integrations?.length ? IntegrationView.List : IntegrationView.New);
|
||||
}, [isIntegrationsFetched]);
|
||||
|
||||
const handleProviderIntegration = async (provider: string) => {
|
||||
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
|
||||
if (!selectedCloudIntegration) return;
|
||||
|
||||
try {
|
||||
redirectForProviderAuth(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,
|
||||
cb: () => void
|
||||
) => {
|
||||
try {
|
||||
await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
|
||||
if (cb) cb();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Deleted integration"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete integration"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
|
||||
const integrationAuthForProvider = integrationAuths?.[provider];
|
||||
if (!integrationAuthForProvider) return;
|
||||
|
||||
try {
|
||||
await deleteIntegrationAuths({
|
||||
integration: provider,
|
||||
workspaceId
|
||||
});
|
||||
if (cb) cb();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Revoked provider authentication"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to revoke provider authentication"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isIntegrationLoading || isCloudIntegrationsLoading)
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ContentLoader text={["Loading integrations..."]} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
||||
<div className="relative">
|
||||
{view === IntegrationView.List ? (
|
||||
<motion.div
|
||||
key="view-integrations"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="w-full"
|
||||
>
|
||||
<IntegrationsSection
|
||||
cloudIntegrations={cloudIntegrations}
|
||||
onAddIntegration={() => setView(IntegrationView.New)}
|
||||
isLoading={isIntegrationLoading}
|
||||
integrations={integrations}
|
||||
environments={environments}
|
||||
onIntegrationDelete={handleIntegrationDelete}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="add-integration"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="w-full"
|
||||
>
|
||||
<CloudIntegrationSection
|
||||
onViewActiveIntegrations={
|
||||
integrations?.length ? () => setView(IntegrationView.List) : undefined
|
||||
}
|
||||
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
|
||||
cloudIntegrations={cloudIntegrations}
|
||||
integrationAuths={integrationAuths}
|
||||
onIntegrationStart={handleProviderIntegrationStart}
|
||||
onIntegrationRevoke={handleIntegrationAuthRevoke}
|
||||
/>
|
||||
<FrameworkIntegrationSection />
|
||||
<InfrastructureIntegrationSection />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IntegrationsListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
passThrough={false}
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
<Page />
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
|
||||
|
||||
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
if (!PRIVATE_KEY) {
|
||||
throw new Error("Private Key missing");
|
||||
}
|
||||
|
||||
const WORKSPACE_KEY = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: WORKSPACE_KEY,
|
||||
publicKey: botPublicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
return { encryptedKey: ciphertext, nonce };
|
||||
};
|
||||
|
||||
export const createIntegrationMissingEnvVarsNotification = (
|
||||
slug: string,
|
||||
type: "cloud" | "cicd" = "cloud",
|
||||
hashtag?: string
|
||||
) =>
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: (
|
||||
<a
|
||||
href={`https://infisical.com/docs/integrations/${type}/${slug}${
|
||||
hashtag ? `#${hashtag}` : ""
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Click here to view docs
|
||||
</a>
|
||||
),
|
||||
title: "Missing Environment Variables"
|
||||
});
|
||||
|
||||
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
|
||||
try {
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
|
||||
let link = "";
|
||||
switch (integrationOption.slug) {
|
||||
case "gcp-secret-manager":
|
||||
link = `${window.location.origin}/integrations/gcp-secret-manager/authorize`;
|
||||
break;
|
||||
case "azure-key-vault":
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
||||
break;
|
||||
case "azure-app-configuration":
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
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}`;
|
||||
break;
|
||||
case "aws-parameter-store":
|
||||
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
|
||||
break;
|
||||
case "aws-secret-manager":
|
||||
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
|
||||
break;
|
||||
case "heroku":
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||
break;
|
||||
case "vercel":
|
||||
if (!integrationOption.clientSlug) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
||||
break;
|
||||
case "netlify":
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
||||
break;
|
||||
case "github":
|
||||
link = `${window.location.origin}/integrations/github/auth-mode-selection`;
|
||||
break;
|
||||
case "gitlab":
|
||||
link = `${window.location.origin}/integrations/gitlab/authorize`;
|
||||
break;
|
||||
case "render":
|
||||
link = `${window.location.origin}/integrations/render/authorize`;
|
||||
break;
|
||||
case "flyio":
|
||||
link = `${window.location.origin}/integrations/flyio/authorize`;
|
||||
break;
|
||||
case "circleci":
|
||||
link = `${window.location.origin}/integrations/circleci/authorize`;
|
||||
break;
|
||||
case "databricks":
|
||||
link = `${window.location.origin}/integrations/databricks/authorize`;
|
||||
break;
|
||||
case "laravel-forge":
|
||||
link = `${window.location.origin}/integrations/laravel-forge/authorize`;
|
||||
break;
|
||||
case "travisci":
|
||||
link = `${window.location.origin}/integrations/travisci/authorize`;
|
||||
break;
|
||||
case "supabase":
|
||||
link = `${window.location.origin}/integrations/supabase/authorize`;
|
||||
break;
|
||||
case "checkly":
|
||||
link = `${window.location.origin}/integrations/checkly/authorize`;
|
||||
break;
|
||||
case "qovery":
|
||||
link = `${window.location.origin}/integrations/qovery/authorize`;
|
||||
break;
|
||||
case "railway":
|
||||
link = `${window.location.origin}/integrations/railway/authorize`;
|
||||
break;
|
||||
case "terraform-cloud":
|
||||
link = `${window.location.origin}/integrations/terraform-cloud/authorize`;
|
||||
break;
|
||||
case "hashicorp-vault":
|
||||
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
|
||||
break;
|
||||
case "cloudflare-pages":
|
||||
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
|
||||
break;
|
||||
case "cloudflare-workers":
|
||||
link = `${window.location.origin}/integrations/cloudflare-workers/authorize`;
|
||||
break;
|
||||
case "bitbucket":
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
|
||||
return;
|
||||
}
|
||||
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}`;
|
||||
break;
|
||||
case "codefresh":
|
||||
link = `${window.location.origin}/integrations/codefresh/authorize`;
|
||||
break;
|
||||
case "digital-ocean-app-platform":
|
||||
link = `${window.location.origin}/integrations/digital-ocean-app-platform/authorize`;
|
||||
break;
|
||||
case "cloud-66":
|
||||
link = `${window.location.origin}/integrations/cloud-66/authorize`;
|
||||
break;
|
||||
case "northflank":
|
||||
link = `${window.location.origin}/integrations/northflank/authorize`;
|
||||
break;
|
||||
case "windmill":
|
||||
link = `${window.location.origin}/integrations/windmill/authorize`;
|
||||
break;
|
||||
case "teamcity":
|
||||
link = `${window.location.origin}/integrations/teamcity/authorize`;
|
||||
break;
|
||||
case "hasura-cloud":
|
||||
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
|
||||
break;
|
||||
case "rundeck":
|
||||
link = `${window.location.origin}/integrations/rundeck/authorize`;
|
||||
break;
|
||||
case "azure-devops":
|
||||
link = `${window.location.origin}/integrations/azure-devops/authorize`;
|
||||
break;
|
||||
case "octopus-deploy":
|
||||
link = `${window.location.origin}/integrations/octopus-deploy/authorize`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (link !== "") {
|
||||
window.location.assign(link);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
|
||||
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;
|
||||
@@ -0,0 +1,216 @@
|
||||
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 { NoEnvironmentsBanner } from "@app/components/integrations/NoEnvironmentsBanner";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
Input,
|
||||
Skeleton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
|
||||
|
||||
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 };
|
||||
|
||||
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 { currentWorkspace } = useWorkspace();
|
||||
|
||||
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
||||
|
||||
const sortedCloudIntegrations = useMemo(() => {
|
||||
const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (currentWorkspace?.environments.length === 0) {
|
||||
return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false }));
|
||||
}
|
||||
|
||||
return sortedIntegrations;
|
||||
}, [cloudIntegrations, currentWorkspace?.environments]);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredIntegrations = sortedCloudIntegrations?.filter((cloudIntegration) =>
|
||||
cloudIntegration.name.toLowerCase().includes(search.toLowerCase().trim())
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-5">
|
||||
{currentWorkspace?.environments.length === 0 && (
|
||||
<NoEnvironmentsBanner projectId={currentWorkspace.id} />
|
||||
)}
|
||||
</div>
|
||||
<div className="m-4 mt-7 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-semibold">{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-6 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) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`group relative ${
|
||||
cloudIntegration.isAvailable
|
||||
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
|
||||
: "opacity-50"
|
||||
} flex h-32 flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
|
||||
onClick={() => {
|
||||
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}
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${cloudIntegration.image}`}
|
||||
height={60}
|
||||
width={60}
|
||||
className="mt-auto"
|
||||
alt="integration logo"
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{cloudIntegration.name}
|
||||
</div>
|
||||
{cloudIntegration.isAvailable &&
|
||||
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
|
||||
<div className="absolute right-0 top-0 z-30 h-full">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute right-0 top-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-bl-md rounded-tr-md bg-primary px-2 py-0.5 text-xs 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 right-0 top-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 hover:opacity-100 group-hover:h-full"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="xl" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</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 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";
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faKeyboard } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faComputer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import frameworks from "../json/frameworkIntegrations.json";
|
||||
|
||||
export const FrameworkIntegrationSection = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortedFrameworks = frameworks.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
|
||||
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
|
||||
</div>
|
||||
<div className="mx-6 mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
{sortedFrameworks.map((framework) => (
|
||||
<a
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
href={framework.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
{framework?.image && (
|
||||
<img
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={60}
|
||||
width={60}
|
||||
className="mt-auto"
|
||||
alt="integration logo"
|
||||
/>
|
||||
)}
|
||||
{framework?.name && (
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{framework.name}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
key="framework-integration-more"
|
||||
href="https://infisical.com/docs/cli/commands/run"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
<FontAwesomeIcon className="mt-auto text-5xl text-white/90" icon={faKeyboard} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
CLI
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
key="framework-integration-more"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
<FontAwesomeIcon className="mt-auto text-5xl text-white/90" icon={faComputer} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
SDKs
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection";
|
||||
@@ -0,0 +1,45 @@
|
||||
import integrations from "../json/infrastructureIntegrations.json";
|
||||
|
||||
export const InfrastructureIntegrationSection = () => {
|
||||
const sortedIntegrations = integrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Click on of the integration to read the documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<a
|
||||
key={`framework-integration-${integration.slug}`}
|
||||
href={integration.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 w-full cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="group relative flex h-32 w-full cursor-pointer flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
key={integration?.name}
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${integration.image}.png`}
|
||||
height={integration?.name ? 60 : 90}
|
||||
width={integration?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
/>
|
||||
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{integration?.name && integration.name}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { InfrastructureIntegrationSection } from "./InfrastructureIntegrationSection";
|
||||
@@ -0,0 +1,136 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, Checkbox, DeleteActionModal } from "@app/components/v2";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { TCloudIntegration, TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
import { IntegrationsTable } from "./components";
|
||||
|
||||
type Props = {
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
integrations?: TIntegration[];
|
||||
cloudIntegrations?: TCloudIntegration[];
|
||||
isLoading?: boolean;
|
||||
onIntegrationDelete: (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => Promise<void>;
|
||||
workspaceId: string;
|
||||
onAddIntegration: () => void;
|
||||
};
|
||||
|
||||
export const IntegrationsSection = ({
|
||||
integrations = [],
|
||||
environments = [],
|
||||
isLoading,
|
||||
onIntegrationDelete,
|
||||
workspaceId,
|
||||
onAddIntegration,
|
||||
cloudIntegrations = []
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteConfirmation",
|
||||
"deleteSecretsConfirmation"
|
||||
] as const);
|
||||
|
||||
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
|
||||
|
||||
return (
|
||||
<div className="mx-6 mb-8">
|
||||
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Integrations</h1>
|
||||
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
|
||||
</div>
|
||||
<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-semibold text-mineshaft-100">Active Integrations</p>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={onAddIntegration}
|
||||
>
|
||||
Add Integration
|
||||
</Button>
|
||||
</div>
|
||||
<IntegrationsTable
|
||||
cloudIntegrations={cloudIntegrations}
|
||||
integrations={integrations}
|
||||
isLoading={isLoading}
|
||||
workspaceId={workspaceId}
|
||||
environments={environments}
|
||||
onDeleteIntegration={(integration) => {
|
||||
setShouldDeleteSecrets.off();
|
||||
handlePopUpOpen("deleteConfirmation", integration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteConfirmation.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
|
||||
} integration for ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.app || "this project"
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
|
||||
deleteKey={
|
||||
((popUp?.deleteConfirmation?.data as TIntegration)?.integration ===
|
||||
"azure-app-configuration" &&
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.app
|
||||
?.split("//")[1]
|
||||
?.split(".")[0]) ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.app ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.owner ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.path ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
|
||||
""
|
||||
}
|
||||
onDeleteApproved={async () => {
|
||||
if (shouldDeleteSecrets) {
|
||||
handlePopUpOpen("deleteSecretsConfirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
false,
|
||||
() => handlePopUpClose("deleteConfirmation")
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
id="delete-integration-secrets"
|
||||
checkIndicatorBg="text-white"
|
||||
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
|
||||
>
|
||||
Delete previously synced secrets from the destination
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</DeleteActionModal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretsConfirmation.isOpen}
|
||||
title={`Are you sure you also want to delete secrets on ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration
|
||||
}?`}
|
||||
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
true,
|
||||
() => {
|
||||
handlePopUpClose("deleteSecretsConfirmation");
|
||||
handlePopUpClose("deleteConfirmation");
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
import { IntegrationMappingBehavior, TIntegration } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegration;
|
||||
};
|
||||
|
||||
const FIELD_CLASSNAME =
|
||||
"truncate rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200";
|
||||
|
||||
export const getIntegrationDestination = (integration: TIntegration) =>
|
||||
(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && `${integration.path}`) ||
|
||||
(integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) ||
|
||||
integration.app ||
|
||||
"-";
|
||||
|
||||
export const IntegrationDetails = ({ integration }: Props) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
{integration.integration === "octopus-deploy" && (
|
||||
<div>
|
||||
<FormLabel label="Space" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "qovery" && (
|
||||
<>
|
||||
<div>
|
||||
<FormLabel label="Org" />
|
||||
<div className={FIELD_CLASSNAME}>{integration?.owner || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel label="Project" />
|
||||
<div className={FIELD_CLASSNAME}>{integration?.targetService || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel label="Env" />
|
||||
<div className={FIELD_CLASSNAME}>{integration?.targetEnvironment || "-"}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!(
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
|
||||
) && (
|
||||
<div>
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "circleci" &&
|
||||
(integration.scope === CircleCiScope.Context ? "Context" : "Project")) ||
|
||||
(integration.integration === "bitbucket" && "Repository") ||
|
||||
(integration.integration === "octopus-deploy" && "Project") ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
"App"
|
||||
}
|
||||
/>
|
||||
<div className={FIELD_CLASSNAME}>{getIntegrationDestination(integration)}</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "vercel" ||
|
||||
integration.integration === "netlify" ||
|
||||
integration.integration === "railway" ||
|
||||
integration.integration === "gitlab" ||
|
||||
integration.integration === "teamcity" ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")) && (
|
||||
<div>
|
||||
<FormLabel label="Target Environment" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "bitbucket" && (
|
||||
<>
|
||||
{integration.targetServiceId && (
|
||||
<div>
|
||||
<FormLabel label="Environment" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetService || integration.targetServiceId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<FormLabel label="Workspace" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{integration.integration === "checkly" && integration.targetService && (
|
||||
<div>
|
||||
<FormLabel label="Group" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.targetService}</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "circleci" && integration.owner && (
|
||||
<div>
|
||||
<FormLabel label="Organization" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.owner}</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||
<div>
|
||||
<FormLabel label="Category" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.targetService}</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "checkly" || integration.integration === "github") &&
|
||||
integration?.metadata?.secretSuffix && (
|
||||
<div>
|
||||
<FormLabel label="Secret Suffix" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.metadata.secretSuffix}</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "github" && integration.metadata?.githubVisibility ? (
|
||||
<div className="mt-2 text-xs text-mineshaft-200">
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{integration.metadata?.githubVisibility === "selected"
|
||||
? "* Syncing to selected repositories in the organization. "
|
||||
: integration.metadata?.githubVisibility === "private"
|
||||
? "* Syncing to all private repositories in the organization"
|
||||
: "* Syncing to all public and private repositories in the organization"}
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faCalendarCheck,
|
||||
faCheck,
|
||||
faInfoCircle,
|
||||
faRefresh,
|
||||
faTrash,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { TCloudIntegration } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
import { getIntegrationDestination, IntegrationDetails } from "./IntegrationDetails";
|
||||
|
||||
type IProps = {
|
||||
integration: TIntegration;
|
||||
environment?: { name: string; slug: string; id: string };
|
||||
onRemoveIntegration: VoidFunction;
|
||||
onManualSyncIntegration: VoidFunction;
|
||||
cloudIntegration: TCloudIntegration;
|
||||
};
|
||||
|
||||
export const IntegrationRow = ({
|
||||
integration,
|
||||
environment,
|
||||
onRemoveIntegration,
|
||||
onManualSyncIntegration,
|
||||
cloudIntegration
|
||||
}: IProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { id, secretPath, syncMessage, isSynced } = integration;
|
||||
|
||||
const failureMessage = useMemo(() => {
|
||||
if (isSynced === false) {
|
||||
if (syncMessage)
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(syncMessage), null, 2);
|
||||
} catch {
|
||||
return syncMessage;
|
||||
}
|
||||
|
||||
return "An Unknown Error Occurred.";
|
||||
}
|
||||
return null;
|
||||
}, [isSynced, syncMessage]);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/$integrationId",
|
||||
params: {
|
||||
integrationId: integration.id,
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
})
|
||||
}
|
||||
className={twMerge(
|
||||
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
|
||||
isSynced === false && "bg-red/5 hover:bg-red/10"
|
||||
)}
|
||||
key={`integration-${id}`}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${cloudIntegration?.name} integration`}
|
||||
src={`/images/integrations/${cloudIntegration?.image}`}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span className="hidden lg:inline">{cloudIntegration?.name}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<Tooltip side="top" className="max-w-2xl break-words" content={secretPath}>
|
||||
<p className="truncate">{secretPath}</p>
|
||||
</Tooltip>{" "}
|
||||
</Td>
|
||||
<Td>{environment?.name ?? "-"}</Td>
|
||||
<Td className="!min-w-[5rem] max-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate">{getIntegrationDestination(integration)}</p>
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="min-w-[20rem] max-w-lg"
|
||||
content={<IntegrationDetails integration={integration} />}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{" "}
|
||||
{typeof integration.isSynced !== "boolean" ? (
|
||||
<Badge variant="primary" key={integration.id}>
|
||||
Pending Sync
|
||||
</Badge>
|
||||
) : (
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<div className="flex flex-col gap-2 py-1">
|
||||
{integration.lastUsed && (
|
||||
<div>
|
||||
<div
|
||||
className={`mb-2 flex self-start ${!isSynced ? "text-yellow" : "text-green"}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCalendarCheck}
|
||||
className="ml-1 pr-1.5 pt-0.5 text-sm"
|
||||
/>
|
||||
<div className="text-xs">Last Synced</div>
|
||||
</div>
|
||||
<div className="rounded bg-mineshaft-600 p-2 text-xs">
|
||||
{format(new Date(integration.lastUsed!), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{failureMessage && (
|
||||
<div>
|
||||
<div className="mb-2 flex self-start text-red">
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
|
||||
<div className="text-xs">Failure Reason</div>
|
||||
</div>
|
||||
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-min whitespace-nowrap">
|
||||
<Badge variant={integration.isSynced ? "success" : "danger"} key={integration.id}>
|
||||
<div className="flex items-center space-x-1">
|
||||
<FontAwesomeIcon icon={integration.isSynced ? faCheck : faWarning} />
|
||||
<div>{integration.isSynced ? "Synced" : "Not Synced"}</div>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2 whitespace-nowrap">
|
||||
<Tooltip className="max-w-sm text-center" content="Manually Sync">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualSyncIntegration();
|
||||
}}
|
||||
ariaLabel="sync"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRefresh} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveIntegration();
|
||||
}}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="px-1" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,448 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheck,
|
||||
faClock,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faPlug,
|
||||
faSearch,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
import { TCloudIntegration, TIntegration } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import { getIntegrationDestination } from "./IntegrationDetails";
|
||||
import { IntegrationRow } from "./IntegrationRow";
|
||||
|
||||
type Props = {
|
||||
integrations?: TIntegration[];
|
||||
cloudIntegrations?: TCloudIntegration[];
|
||||
workspaceId: string;
|
||||
isLoading?: boolean;
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
onDeleteIntegration: (integration: TIntegration) => void;
|
||||
};
|
||||
|
||||
enum IntegrationsOrderBy {
|
||||
App = "app",
|
||||
Status = "status",
|
||||
SecretPath = "secretPath",
|
||||
Environment = "environment",
|
||||
Destination = "destination"
|
||||
}
|
||||
|
||||
enum IntegrationStatus {
|
||||
Synced = "synced",
|
||||
NotSynced = "not-synced",
|
||||
PendingSync = "pending-sync"
|
||||
}
|
||||
|
||||
type IntegrationFilters = {
|
||||
environmentIds: string[];
|
||||
integrations: string[];
|
||||
status: IntegrationStatus[];
|
||||
};
|
||||
|
||||
const STATUS_ICON_MAP = {
|
||||
[IntegrationStatus.Synced]: { icon: faCheck, className: "text-green" },
|
||||
[IntegrationStatus.NotSynced]: { icon: faWarning, className: "text-red" },
|
||||
[IntegrationStatus.PendingSync]: { icon: faClock, className: "text-yellow" }
|
||||
};
|
||||
|
||||
export const IntegrationsTable = ({
|
||||
integrations = [],
|
||||
cloudIntegrations = [],
|
||||
workspaceId,
|
||||
environments,
|
||||
onDeleteIntegration,
|
||||
isLoading
|
||||
}: Props) => {
|
||||
const { mutate: syncIntegration } = useSyncIntegration();
|
||||
|
||||
const initialFilters = useMemo(
|
||||
() => ({
|
||||
environmentIds: environments.map((env) => env.id),
|
||||
integrations: [...new Set(integrations.map(({ integration }) => integration))],
|
||||
status: Object.values(IntegrationStatus)
|
||||
}),
|
||||
[environments, integrations]
|
||||
);
|
||||
|
||||
const [filters, setFilters] = useState<IntegrationFilters>(initialFilters);
|
||||
|
||||
const cloudIntegrationMap = useMemo(() => {
|
||||
return new Map(
|
||||
cloudIntegrations.map((cloudIntegration) => [cloudIntegration.slug, cloudIntegration])
|
||||
);
|
||||
}, [cloudIntegrations]);
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<IntegrationsOrderBy>(IntegrationsOrderBy.App, { initPerPage: 20 });
|
||||
|
||||
useEffect(() => {
|
||||
if (integrations?.some((integration) => integration.isSynced === false))
|
||||
setOrderBy(IntegrationsOrderBy.Status);
|
||||
}, []);
|
||||
|
||||
const environmentMap = new Map(environments.map((env) => [env.id, env]));
|
||||
|
||||
const filteredIntegrations = useMemo(
|
||||
() =>
|
||||
integrations
|
||||
.filter((integration) => {
|
||||
const { secretPath, envId, isSynced } = integration;
|
||||
|
||||
if (!filters.status.includes(IntegrationStatus.Synced) && isSynced) return false;
|
||||
if (!filters.status.includes(IntegrationStatus.NotSynced) && isSynced === false)
|
||||
return false;
|
||||
if (
|
||||
!filters.status.includes(IntegrationStatus.PendingSync) &&
|
||||
typeof isSynced !== "boolean"
|
||||
)
|
||||
return false;
|
||||
|
||||
if (!filters.integrations.includes(integration.integration)) return false;
|
||||
|
||||
if (!filters.environmentIds.includes(envId)) return false;
|
||||
|
||||
return (
|
||||
integration.integration
|
||||
.replace("-", " ")
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase()) ||
|
||||
secretPath.replace("-", " ").toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
getIntegrationDestination(integration)
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase()) ||
|
||||
environmentMap
|
||||
.get(envId)
|
||||
?.name.replace("-", " ")
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase())
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [integrationOne, integrationTwo] =
|
||||
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case IntegrationsOrderBy.SecretPath:
|
||||
return integrationOne.secretPath
|
||||
.toLowerCase()
|
||||
.localeCompare(integrationTwo.secretPath.toLowerCase());
|
||||
case IntegrationsOrderBy.Environment:
|
||||
return (environmentMap.get(integrationOne.envId)?.name ?? "-")
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
(environmentMap.get(integrationTwo.envId)?.name ?? "-").toLowerCase()
|
||||
);
|
||||
case IntegrationsOrderBy.Destination:
|
||||
return getIntegrationDestination(integrationOne)
|
||||
.toLowerCase()
|
||||
.localeCompare(getIntegrationDestination(integrationTwo).toLowerCase());
|
||||
case IntegrationsOrderBy.Status:
|
||||
if (typeof integrationOne.isSynced !== "boolean") return 1; // Place undefined at the end
|
||||
if (typeof integrationTwo.isSynced !== "boolean") return -1;
|
||||
|
||||
return Number(integrationOne.isSynced) - Number(integrationTwo.isSynced);
|
||||
case IntegrationsOrderBy.App:
|
||||
default:
|
||||
return integrationOne.integration
|
||||
.toLowerCase()
|
||||
.localeCompare(integrationTwo.integration.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[integrations, orderDirection, search, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredIntegrations.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: IntegrationsOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: IntegrationsOrderBy) =>
|
||||
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: IntegrationsOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered =
|
||||
filters.integrations.length !== initialFilters.integrations.length ||
|
||||
filters.environmentIds.length !== initialFilters.environmentIds.length ||
|
||||
filters.status.length !== initialFilters.status.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search integrations..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Environments"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<Tooltip content="Filter Integrations" className="mb-2">
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
||||
{Object.values(IntegrationStatus).map((status) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
status: prev.status.includes(status)
|
||||
? prev.status.filter((s) => s !== status)
|
||||
: [...prev.status, status]
|
||||
}));
|
||||
}}
|
||||
key={status}
|
||||
icon={
|
||||
filters.status.includes(status) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={STATUS_ICON_MAP[status].icon}
|
||||
className={STATUS_ICON_MAP[status].className}
|
||||
/>
|
||||
<span className="capitalize">{status.replace("-", " ")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuLabel>Integration</DropdownMenuLabel>
|
||||
{[...new Set(integrations.map(({ integration }) => integration))].map((integration) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
integrations: prev.integrations.includes(integration)
|
||||
? prev.integrations.filter((i) => i !== integration)
|
||||
: [...prev.integrations, integration]
|
||||
}));
|
||||
}}
|
||||
key={integration}
|
||||
icon={
|
||||
filters.integrations.includes(integration) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${cloudIntegrationMap.get(integration)!.name} integration`}
|
||||
src={`/images/integrations/${cloudIntegrationMap.get(integration)!.image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span className="capitalize">{cloudIntegrationMap.get(integration)!.name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||
{environments.map((env) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
environmentIds: prev.environmentIds.includes(env.id)
|
||||
? prev.environmentIds.filter((i) => i !== env.id)
|
||||
: [...prev.environmentIds, env.id]
|
||||
}));
|
||||
}}
|
||||
key={env.id}
|
||||
icon={
|
||||
filters.environmentIds.includes(env.id) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<span className="capitalize">{env.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-[25%]">
|
||||
<div className="flex items-center">
|
||||
Integration
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.App)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.App)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.App)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Source Path
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.SecretPath)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.SecretPath)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.SecretPath)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Source Environment
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.Environment)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.Environment)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.Environment)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Destination
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.Destination)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.Destination)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.Destination)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.Status)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.Status)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.Status)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{filteredIntegrations.slice(offset, perPage * page).map((integration) => (
|
||||
<IntegrationRow
|
||||
cloudIntegration={cloudIntegrationMap.get(integration.integration)!}
|
||||
key={`integration-${integration.id}`}
|
||||
onManualSyncIntegration={() => {
|
||||
syncIntegration({
|
||||
workspaceId,
|
||||
id: integration.id,
|
||||
lastUsed: integration.lastUsed as string
|
||||
});
|
||||
}}
|
||||
onRemoveIntegration={() => onDeleteIntegration(integration)}
|
||||
integration={integration}
|
||||
environment={environmentMap.get(integration.envId)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredIntegrations.length) && (
|
||||
<Pagination
|
||||
count={filteredIntegrations.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !filteredIntegrations?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
integrations.length
|
||||
? "No integrations match search..."
|
||||
: "This project has no integrations configured"
|
||||
}
|
||||
icon={integrations.length ? faSearch : faPlug}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./IntegrationsTable";
|
||||
@@ -0,0 +1 @@
|
||||
export { IntegrationsSection } from "./IntegrationsSection";
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"name": "React",
|
||||
"slug": "react",
|
||||
"image": "React",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/react"
|
||||
},
|
||||
{
|
||||
"name": "Vue",
|
||||
"slug": "vue",
|
||||
"image": "Vue",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/vue"
|
||||
},
|
||||
{
|
||||
"name": "Express",
|
||||
"slug": "express",
|
||||
"image": "Express",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/express"
|
||||
},
|
||||
{
|
||||
"name": "Next.js",
|
||||
"slug": "nextjs",
|
||||
"image": "Next.js",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nextjs"
|
||||
},
|
||||
{
|
||||
"name": "SvelteKit",
|
||||
"slug": "sveltekit",
|
||||
"image": "SvelteKit",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/sveltekit"
|
||||
},
|
||||
{
|
||||
"name": "Django",
|
||||
"slug": "django",
|
||||
"image": "Django",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/django"
|
||||
},
|
||||
{
|
||||
"name": "NestJS",
|
||||
"slug": "nestjs",
|
||||
"image": "NestJS",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nestjs"
|
||||
},
|
||||
{
|
||||
"name": "Nuxt",
|
||||
"slug": "nuxt",
|
||||
"image": "Nuxt",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nuxt"
|
||||
},
|
||||
{
|
||||
"name": "Gatsby",
|
||||
"slug": "gatsby",
|
||||
"image": "Gatsby",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/gatsby"
|
||||
},
|
||||
{
|
||||
"name": "Remix",
|
||||
"slug": "remix",
|
||||
"image": "Remix",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/remix"
|
||||
},
|
||||
{
|
||||
"name": "Vite",
|
||||
"slug": "vite",
|
||||
"image": "Vite",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/vite"
|
||||
},
|
||||
{
|
||||
"name": "Fiber",
|
||||
"slug": "fiber",
|
||||
"image": "Fiber",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/fiber"
|
||||
},
|
||||
{
|
||||
"name": "Flask",
|
||||
"slug": "flask",
|
||||
"image": "Flask",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/flask"
|
||||
},
|
||||
{
|
||||
"name": "Laravel",
|
||||
"slug": "laravel",
|
||||
"image": "Laravel",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/laravel"
|
||||
},
|
||||
{
|
||||
"name": "Rails",
|
||||
"slug": "rails",
|
||||
"image": "Rails",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/rails"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"name": "Docker",
|
||||
"slug": "docker",
|
||||
"image": "Docker",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
|
||||
},
|
||||
{
|
||||
"name": "Docker Compose",
|
||||
"slug": "docker-compose",
|
||||
"image": "Docker Compose",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||
},
|
||||
{
|
||||
"name": "Kubernetes",
|
||||
"slug": "kubernetes",
|
||||
"image": "Kubernetes",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
|
||||
},
|
||||
{
|
||||
"name": "Terraform",
|
||||
"slug": "terraform",
|
||||
"image": "Terraform",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
|
||||
},
|
||||
{
|
||||
"name": "Jenkins",
|
||||
"slug": "jenkins",
|
||||
"image": "Jenkins",
|
||||
"docsLink": "https://infisical.com/docs/integrations/cicd/jenkins"
|
||||
},
|
||||
{
|
||||
"name": "Infisical Agent",
|
||||
"slug": "agent",
|
||||
"image": "Agent",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/infisical-agent"
|
||||
},
|
||||
{
|
||||
"name": "Amazon ECS",
|
||||
"slug": "ecs",
|
||||
"image": "ECS",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/ecs-with-agent"
|
||||
},
|
||||
{
|
||||
"name": "Ansible",
|
||||
"slug": "ansible",
|
||||
"image": "Ansible",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/ansible"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import { IntegrationsListPage } from './IntegrationsListPage'
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/',
|
||||
)({
|
||||
component: IntegrationsListPage,
|
||||
})
|
||||
@@ -89,7 +89,9 @@ import { Route as projectMemberDetailsByIDPageRouteCertManagerImport } from './p
|
||||
import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager'
|
||||
import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/route'
|
||||
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
|
||||
import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route'
|
||||
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
||||
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
|
||||
import { Route as organizationAppConnectionsGithubOauthCallbackPageRouteImport } from './pages/organization/AppConnections/GithubOauthCallbackPage/route'
|
||||
|
||||
// Create Virtual Routes
|
||||
@@ -120,6 +122,10 @@ const AuthenticateInjectOrgDetailsKmsProjectIdImport = createFileRoute(
|
||||
const AuthenticateInjectOrgDetailsCertManagerProjectIdImport = createFileRoute(
|
||||
'/_authenticate/_inject-org-details/cert-manager/$projectId',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations',
|
||||
)()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@@ -431,6 +437,15 @@ const projectAccessControlPageRouteSshRoute =
|
||||
getParentRoute: () => sshLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute =
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsImport.update(
|
||||
{
|
||||
id: '/integrations',
|
||||
path: '/integrations',
|
||||
getParentRoute: () => secretManagerLayoutRoute,
|
||||
} as any,
|
||||
)
|
||||
|
||||
const projectAccessControlPageRouteSecretManagerRoute =
|
||||
projectAccessControlPageRouteSecretManagerImport.update({
|
||||
id: '/access-management',
|
||||
@@ -685,6 +700,14 @@ const secretManagerSecretDashboardPageRouteRoute =
|
||||
getParentRoute: () => secretManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const secretManagerIntegrationsDetailsByIDPageRouteRoute =
|
||||
secretManagerIntegrationsDetailsByIDPageRouteImport.update({
|
||||
id: '/$integrationId',
|
||||
path: '/$integrationId',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute,
|
||||
} as any)
|
||||
|
||||
const certManagerCertAuthDetailsByIDPageRouteRoute =
|
||||
certManagerCertAuthDetailsByIDPageRouteImport.update({
|
||||
id: '/ca/$caId',
|
||||
@@ -692,6 +715,14 @@ const certManagerCertAuthDetailsByIDPageRouteRoute =
|
||||
getParentRoute: () => certManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const secretManagerIntegrationsListPageRouteRoute =
|
||||
secretManagerIntegrationsListPageRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAppConnectionsGithubOauthCallbackPageRouteRoute =
|
||||
organizationAppConnectionsGithubOauthCallbackPageRouteImport.update({
|
||||
id: '/app-connections/github/oauth/callback',
|
||||
@@ -1186,6 +1217,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport
|
||||
parentRoute: typeof secretManagerLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations': {
|
||||
id: '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations'
|
||||
path: '/integrations'
|
||||
fullPath: '/secret-manager/$projectId/integrations'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsImport
|
||||
parentRoute: typeof secretManagerLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management': {
|
||||
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management'
|
||||
path: '/access-management'
|
||||
@@ -1193,6 +1231,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof projectAccessControlPageRouteSshImport
|
||||
parentRoute: typeof sshLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/': {
|
||||
id: '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/'
|
||||
path: '/'
|
||||
fullPath: '/secret-manager/$projectId/integrations/'
|
||||
preLoaderRoute: typeof secretManagerIntegrationsListPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId': {
|
||||
id: '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId'
|
||||
path: '/ca/$caId'
|
||||
@@ -1200,6 +1245,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport
|
||||
parentRoute: typeof certManagerLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': {
|
||||
id: '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId'
|
||||
path: '/$integrationId'
|
||||
fullPath: '/secret-manager/$projectId/integrations/$integrationId'
|
||||
preLoaderRoute: typeof secretManagerIntegrationsDetailsByIDPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': {
|
||||
id: '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug'
|
||||
path: '/secrets/$envSlug'
|
||||
@@ -1467,6 +1519,24 @@ const AuthenticateInjectOrgDetailsKmsProjectIdRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsKmsProjectIdRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteChildren {
|
||||
secretManagerIntegrationsListPageRouteRoute: typeof secretManagerIntegrationsListPageRouteRoute
|
||||
secretManagerIntegrationsDetailsByIDPageRouteRoute: typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteChildren: AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteChildren =
|
||||
{
|
||||
secretManagerIntegrationsListPageRouteRoute:
|
||||
secretManagerIntegrationsListPageRouteRoute,
|
||||
secretManagerIntegrationsDetailsByIDPageRouteRoute:
|
||||
secretManagerIntegrationsDetailsByIDPageRouteRoute,
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteChildren,
|
||||
)
|
||||
|
||||
interface secretManagerLayoutRouteChildren {
|
||||
secretManagerIPAllowlistPageRouteRoute: typeof secretManagerIPAllowlistPageRouteRoute
|
||||
secretManagerSecretApprovalsPageRouteRoute: typeof secretManagerSecretApprovalsPageRouteRoute
|
||||
@@ -1474,6 +1544,7 @@ interface secretManagerLayoutRouteChildren {
|
||||
secretManagerSecretRotationPageRouteRoute: typeof secretManagerSecretRotationPageRouteRoute
|
||||
secretManagerSettingsPageRouteRoute: typeof secretManagerSettingsPageRouteRoute
|
||||
projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
|
||||
secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute
|
||||
projectIdentityDetailsByIDPageRouteSecretManagerRoute: typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
|
||||
projectMemberDetailsByIDPageRouteSecretManagerRoute: typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
|
||||
@@ -1491,6 +1562,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = {
|
||||
secretManagerSettingsPageRouteRoute: secretManagerSettingsPageRouteRoute,
|
||||
projectAccessControlPageRouteSecretManagerRoute:
|
||||
projectAccessControlPageRouteSecretManagerRoute,
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute:
|
||||
AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren,
|
||||
secretManagerSecretDashboardPageRouteRoute:
|
||||
secretManagerSecretDashboardPageRouteRoute,
|
||||
projectIdentityDetailsByIDPageRouteSecretManagerRoute:
|
||||
@@ -1786,8 +1859,11 @@ export interface FileRoutesByFullPath {
|
||||
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
||||
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
'/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
|
||||
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||
'/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
'/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
'/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||
@@ -1864,7 +1940,9 @@ export interface FileRoutesByTo {
|
||||
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||
'/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
'/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
'/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute
|
||||
'/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||
@@ -1954,8 +2032,11 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
||||
'/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId': typeof sshSshCaByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||
@@ -2037,8 +2118,11 @@ export interface FileRouteTypes {
|
||||
| '/cert-manager/$projectId/access-management'
|
||||
| '/kms/$projectId/access-management'
|
||||
| '/secret-manager/$projectId/access-management'
|
||||
| '/secret-manager/$projectId/integrations'
|
||||
| '/ssh/$projectId/access-management'
|
||||
| '/secret-manager/$projectId/integrations/'
|
||||
| '/cert-manager/$projectId/ca/$caId'
|
||||
| '/secret-manager/$projectId/integrations/$integrationId'
|
||||
| '/secret-manager/$projectId/secrets/$envSlug'
|
||||
| '/ssh/$projectId/ca/$caId'
|
||||
| '/cert-manager/$projectId/identities/$identityId'
|
||||
@@ -2114,7 +2198,9 @@ export interface FileRouteTypes {
|
||||
| '/kms/$projectId/access-management'
|
||||
| '/secret-manager/$projectId/access-management'
|
||||
| '/ssh/$projectId/access-management'
|
||||
| '/secret-manager/$projectId/integrations'
|
||||
| '/cert-manager/$projectId/ca/$caId'
|
||||
| '/secret-manager/$projectId/integrations/$integrationId'
|
||||
| '/secret-manager/$projectId/secrets/$envSlug'
|
||||
| '/ssh/$projectId/ca/$caId'
|
||||
| '/cert-manager/$projectId/identities/$identityId'
|
||||
@@ -2202,8 +2288,11 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations'
|
||||
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management'
|
||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/'
|
||||
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId'
|
||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId'
|
||||
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug'
|
||||
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId'
|
||||
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId'
|
||||
@@ -2542,6 +2631,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secret-rotation",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/settings",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId",
|
||||
@@ -2649,14 +2739,30 @@ export const routeTree = rootRoute
|
||||
"filePath": "project/AccessControlPage/route-secret-manager.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/",
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management": {
|
||||
"filePath": "project/AccessControlPage/route-ssh.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/": {
|
||||
"filePath": "secret-manager/IntegrationsListPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId": {
|
||||
"filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId": {
|
||||
"filePath": "secret-manager/IntegrationsDetailsByIDPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug": {
|
||||
"filePath": "secret-manager/SecretDashboardPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
|
||||
|
||||
@@ -40,6 +40,10 @@ const secretManagerRoutes = route("/secret-manager/$projectId", [
|
||||
route("/approval", "secret-manager/SecretApprovalsPage/route.tsx"),
|
||||
route("/secret-rotation", "secret-manager/SecretRotationPage/route.tsx"),
|
||||
route("/settings", "secret-manager/SettingsPage/route.tsx"),
|
||||
route("/integrations", [
|
||||
index("secret-manager/IntegrationsListPage/route.tsx"),
|
||||
route("/$integrationId", "secret-manager/IntegrationsDetailsByIDPage/route.tsx")
|
||||
]),
|
||||
route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"),
|
||||
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"),
|
||||
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"),
|
||||
|
||||
Reference in New Issue
Block a user