feat: completed secret manager integration list and detail page

This commit is contained in:
=
2025-01-01 20:41:40 +05:30
parent a337e6badd
commit 6c5db3a187
31 changed files with 2748 additions and 12 deletions

View File

@@ -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: {

View File

@@ -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(

View File

@@ -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"
}

View File

@@ -41,6 +41,7 @@ export type TIntegration = {
key: string;
value: string;
}[];
azureLabel?: string;
kmsKeyId?: string;
secretSuffix?: string;

View File

@@ -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>
</>
);
};

View File

@@ -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"
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
})}
</>
);
};

View File

@@ -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
});

View File

@@ -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>
</>
);
};

View File

@@ -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}`;

View File

@@ -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>
);
};

View File

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

View File

@@ -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>
</>
);
};

View File

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

View File

@@ -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>
</>
);
};

View File

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

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -0,0 +1 @@
export * from "./IntegrationsTable";

View File

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

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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,
})

View File

@@ -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"

View File

@@ -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"),