diff --git a/frontend-v2/src/const/routes.ts b/frontend-v2/src/const/routes.ts index 8a63c4f1b..1299ebc12 100644 --- a/frontend-v2/src/const/routes.ts +++ b/frontend-v2/src/const/routes.ts @@ -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: { diff --git a/frontend-v2/src/hooks/api/integrationAuth/queries.tsx b/frontend-v2/src/hooks/api/integrationAuth/queries.tsx index 813fa4cb8..ed5a91365 100644 --- a/frontend-v2/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend-v2/src/hooks/api/integrationAuth/queries.tsx @@ -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>, - unknown, - Awaited> - > + 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 + options?: TReactQueryOptions["options"] ) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments( diff --git a/frontend-v2/src/hooks/api/integrationAuth/types.ts b/frontend-v2/src/hooks/api/integrationAuth/types.ts index 58a643dff..e2dee6067 100644 --- a/frontend-v2/src/hooks/api/integrationAuth/types.ts +++ b/frontend-v2/src/hooks/api/integrationAuth/types.ts @@ -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" +} diff --git a/frontend-v2/src/hooks/api/integrations/types.ts b/frontend-v2/src/hooks/api/integrations/types.ts index 0346b065a..7054befc7 100644 --- a/frontend-v2/src/hooks/api/integrations/types.ts +++ b/frontend-v2/src/hooks/api/integrations/types.ts @@ -41,6 +41,7 @@ export type TIntegration = { key: string; value: string; }[]; + azureLabel?: string; kmsKeyId?: string; secretSuffix?: string; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx new file mode 100644 index 000000000..84150e792 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.tsx @@ -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 ( + <> + + Integration Details | Infisical + + + + +
+ {integration ? ( +
+ +
+

+ {integrationSlugNameMapping[integration.integration]} Integration +

+ + +
+ + + +
+
+ + { + await syncIntegration({ + id: integration.id, + lastUsed: integration.lastUsed!, + workspaceId: projectId! + }); + }} + > +
+ + Manually Sync +
+
+ + {(isAllowed) => ( + {}} + disabled={!isAllowed} + > +
+ + Delete Integration +
+
+ )} +
+
+
+
+
+
+ + +
+
+ + +
+
+
+ ) : ( +
+ +
+ )} +
+ + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.utils.ts b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.utils.ts new file mode 100644 index 000000000..e4eea6bff --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/IntegrationsDetailsByIDPage.utils.ts @@ -0,0 +1,37 @@ +export const integrationSlugNameMapping: Record = { + "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" +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx new file mode 100644 index 000000000..5bb644956 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationAuditLogsSection.tsx @@ -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 ? ( +
+
+

Integration Logs

+

+ Displaying audit logs from the last {auditLogsRetentionDays} days +

+
+ +
+ ) : ( +
+
+

Integration Logs

+
+ +

+ Please{" "} + {subscription && subscription.slug !== null ? ( + + + upgrade your subscription + + + ) : ( + + + upgrade your subscription + + + )} + to view integration logs +

+
+ } + /> + + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationConnectionSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationConnectionSection.tsx new file mode 100644 index 000000000..8829c8d70 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationConnectionSection.tsx @@ -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 ( +
+
+ +
{integration?.owner || "-"}
+
+
+ +
{integration?.targetService || "-"}
+
+
+ +
{integration?.targetEnvironment || "-"}
+
+
+ ); + }; + + 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 ( +
+ +
{contents()}
+
+ ); + }; + + const targetEnvironmentDetails = () => { + if (integration.integration === "bitbucket") { + return ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ ); + } + + if (integration.integration === "octopus-deploy") { + return ( +
+ +
{integration.targetEnvironment}
+
+ ); + } + + if ( + ["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) || + (integration.integration === "github" && integration.scope === "github-env") + ) { + return ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ ); + } + + return null; + }; + + const generalIntegrationSpecificDetails = () => { + if (integration.integration === "checkly" && integration.targetService) { + return ( +
+ +
{integration.targetService}
+
+ ); + } + + if (integration.integration === "circleci" && integration.owner) { + return ( +
+ +
{integration.owner}
+
+ ); + } + + if (integration.integration === "terraform-cloud" && integration.targetService) { + return ( +
+ +
{integration.targetService}
+
+ ); + } + + if (integration.integration === "checkly" || integration.integration === "github") { + return ( +
+ +
+ {integration?.metadata?.secretSuffix || "-"} +
+
+ ); + } + + if (integration.integration === "bitbucket" && integration.targetServiceId) { + return ( +
+ +
{integration.targetService}
+
+ ); + } + + return null; + }; + + return ( +
+
+

Connection

+
+ +
+ + +
+
+ +
{integration.environment.name}
+
+
+ +
{integration.secretPath}
+
+
+ + +
+ +
+ {integrationSlugNameMapping?.[integration.integration]} +
+ + {specifcQoveryDetails()} + {isNotAwsManagerOneToOneDetails()} + {targetEnvironmentDetails()} + {generalIntegrationSpecificDetails()} +
+
+
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationDetailsSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationDetailsSection.tsx new file mode 100644 index 000000000..440555b2a --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationDetailsSection.tsx @@ -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 ( +
+
+
+

Integration Details

+
+
+
+
+

Name

+

+ {integrationSlugNameMapping[integration.integration]} +

+
+
+

Sync Status

+
+

+ {integration.isSynced ? "Synced" : "Not Synced"} +

+ +
+
+ {integration.lastUsed && ( +
+

Latest Successful Sync

+
+ {format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")} + +
+
+ )} + +
+ {!integration.isSynced && integration.syncMessage && ( + <> +

Latest Sync Error

+

{integration.syncMessage}

+ + )} +
+
+
+
+
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx new file mode 100644 index 000000000..51a87b052 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx @@ -0,0 +1,99 @@ +import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types"; + +import { OctopusDeployScopeValues } from "./OctopusDeployScopeValues"; + +type Props = { + integration: TIntegrationWithEnv; +}; + +type Metadata = NonNullable; +type MetadataKey = keyof Metadata; +type MetadataValue = Metadata[K]; + +const metadataMappings: Record, 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 = (key: K, value: MetadataValue) => { + if (!value) return null; + + if (key === "octopusDeployScopeValues") + return ; + + // 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 }) => ( +

+ {tagKey}={tagValue} +

+ )); + } + + 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 ( +
+
+

Integration Settings

+
+
+ {integration.metadata && + Object.entries(integration.metadata).map(([key, value]) => ( +
+

+ {!!value && metadataMappings[key as keyof typeof metadataMappings]} +

+

{renderValue(key as MetadataKey, value)}

+
+ ))} +
+
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/OctopusDeployScopeValues.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/OctopusDeployScopeValues.tsx new file mode 100644 index 000000000..7892429b1 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/OctopusDeployScopeValues.tsx @@ -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 Not Configured; + + if (isPending) return ; + + 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 ( +
+ + +
+ {values + .map((value) => scopeValuesMap.get(key)?.get(value)) + .map((name) => ( +

{name}

+ ))} +
+
+ ); + })} + + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx new file mode 100644 index 000000000..14661061d --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsDetailsByIDPage/route.tsx @@ -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 +}); diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx new file mode 100644 index 000000000..2fca1ca83 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -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.New); + + const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } = + useGetCloudIntegrations(); + + const { + data: integrationAuths, + isPending: isIntegrationAuthLoading, + isFetching: isIntegrationAuthFetching + } = useGetWorkspaceAuthorizations( + workspaceId, + useCallback((data: IntegrationAuth[]) => { + const groupBy: Record = {}; + 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 ( +
+ +
+ ); + + return ( +
+
+ {view === IntegrationView.List ? ( + + setView(IntegrationView.New)} + isLoading={isIntegrationLoading} + integrations={integrations} + environments={environments} + onIntegrationDelete={handleIntegrationDelete} + workspaceId={workspaceId} + /> + + ) : ( + + setView(IntegrationView.List) : undefined + } + isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading} + cloudIntegrations={cloudIntegrations} + integrationAuths={integrationAuths} + onIntegrationStart={handleProviderIntegrationStart} + onIntegrationRevoke={handleIntegrationAuthRevoke} + /> + + + + )} +
+
+ ); +}; + +export const IntegrationsListPage = () => { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("integrations.title") })} + + + + + + + + + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx new file mode 100644 index 000000000..ce31f743f --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx @@ -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: ( + + Click here to view docs + + ), + 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}`; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx new file mode 100644 index 000000000..6926c2d55 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -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; + 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 ( +
+
+ {currentWorkspace?.environments.length === 0 && ( + + )} +
+
+ {onViewActiveIntegrations && ( + + )} +
+
+

{t("integrations.cloud-integrations")}

+

{t("integrations.click-to-start")}

+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search cloud integrations..." + containerClassName="flex-1 h-min text-base" + /> +
+
+
+ {isLoading && + Array.from({ length: 12 }).map((_, index) => ( + + ))} + + {!isLoading && filteredIntegrations.length ? ( + filteredIntegrations.map((cloudIntegration) => ( +
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} + > + integration logo +
+ {cloudIntegration.name} +
+ {cloudIntegration.isAvailable && + Boolean(integrationAuths?.[cloudIntegration.slug]) && ( +
+
+
+ + Authorized +
+ +
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" + > + +
+
+
+
+ )} +
+ )) + ) : ( + + )} +
+ {isEmpty && ( +
+ {Array.from({ length: 16 }).map((_, index) => ( +
+ ))} +
+ )} + handlePopUpToggle("deleteConfirmation", isOpen)} + deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""} + onDeleteApproved={async () => { + onIntegrationRevoke( + (popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider, + () => handlePopUpClose("deleteConfirmation") + ); + }} + /> +
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/index.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/index.tsx new file mode 100644 index 000000000..62f7a006c --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/index.tsx @@ -0,0 +1 @@ +export { CloudIntegrationSection } from "./CloudIntegrationSection"; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx new file mode 100644 index 000000000..2d23aac3d --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx @@ -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 ( + <> +
+

{t("integrations.framework-integrations")}

+

{t("integrations.click-to-setup")}

+
+ + + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/FrameworkIntegrationSection/index.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/FrameworkIntegrationSection/index.tsx new file mode 100644 index 000000000..1738b8179 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/FrameworkIntegrationSection/index.tsx @@ -0,0 +1 @@ +export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection"; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/InfrastructureIntegrationSection/InfrastructureIntegrationSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/InfrastructureIntegrationSection/InfrastructureIntegrationSection.tsx new file mode 100644 index 000000000..53e2e46ae --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/InfrastructureIntegrationSection/InfrastructureIntegrationSection.tsx @@ -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 ( + <> +
+

Infrastructure Integrations

+

+ Click on of the integration to read the documentation. +

+
+ + + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/InfrastructureIntegrationSection/index.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/InfrastructureIntegrationSection/index.tsx new file mode 100644 index 000000000..8ca5a43c7 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/InfrastructureIntegrationSection/index.tsx @@ -0,0 +1 @@ +export { InfrastructureIntegrationSection } from "./InfrastructureIntegrationSection"; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/IntegrationsSection.tsx new file mode 100644 index 000000000..2708226f6 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -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; + 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 ( +
+
+

Integrations

+

Manage integrations with third-party services.

+
+
+
+

Active Integrations

+ +
+ { + setShouldDeleteSecrets.off(); + handlePopUpOpen("deleteConfirmation", integration); + }} + /> +
+ 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" && ( +
+ setShouldDeleteSecrets.toggle()} + > + Delete previously synced secrets from the destination + +
+ )} +
+ handlePopUpToggle("deleteSecretsConfirmation", isOpen)} + deleteKey="confirm" + onDeleteApproved={async () => { + await onIntegrationDelete( + (popUp?.deleteConfirmation.data as TIntegration).id, + true, + () => { + handlePopUpClose("deleteSecretsConfirmation"); + handlePopUpClose("deleteConfirmation"); + } + ); + }} + /> +
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationDetails.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationDetails.tsx new file mode 100644 index 000000000..4974ab824 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationDetails.tsx @@ -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 ( +
+ {integration.integration === "octopus-deploy" && ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ )} + {integration.integration === "qovery" && ( + <> +
+ +
{integration?.owner || "-"}
+
+
+ +
{integration?.targetService || "-"}
+
+
+ +
{integration?.targetEnvironment || "-"}
+
+ + )} + {!( + integration.integration === "aws-secret-manager" && + integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE + ) && ( +
+ +
{getIntegrationDestination(integration)}
+
+ )} + {(integration.integration === "vercel" || + integration.integration === "netlify" || + integration.integration === "railway" || + integration.integration === "gitlab" || + integration.integration === "teamcity" || + (integration.integration === "github" && integration.scope === "github-env")) && ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ )} + {integration.integration === "bitbucket" && ( + <> + {integration.targetServiceId && ( +
+ +
+ {integration.targetService || integration.targetServiceId} +
+
+ )} +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ + )} + {integration.integration === "checkly" && integration.targetService && ( +
+ +
{integration.targetService}
+
+ )} + {integration.integration === "circleci" && integration.owner && ( +
+ +
{integration.owner}
+
+ )} + {integration.integration === "terraform-cloud" && integration.targetService && ( +
+ +
{integration.targetService}
+
+ )} + {(integration.integration === "checkly" || integration.integration === "github") && + integration?.metadata?.secretSuffix && ( +
+ +
{integration.metadata.secretSuffix}
+
+ )} + {integration.integration === "github" && integration.metadata?.githubVisibility ? ( +
+ {/* 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"} +
+ ) : undefined} +
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationRow.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationRow.tsx new file mode 100644 index 000000000..fab6b50ac --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationRow.tsx @@ -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 ( + + 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}`} + > + +
+ {`${cloudIntegration?.name} + {cloudIntegration?.name} +
+ + + +

{secretPath}

+
{" "} + + {environment?.name ?? "-"} + +
+

{getIntegrationDestination(integration)}

+ } + > + + +
+ + + {" "} + {typeof integration.isSynced !== "boolean" ? ( + + Pending Sync + + ) : ( + + {integration.lastUsed && ( +
+
+ +
Last Synced
+
+
+ {format(new Date(integration.lastUsed!), "yyyy-MM-dd, hh:mm aaa")} +
+
+ )} + {failureMessage && ( +
+
+ +
Failure Reason
+
+
{failureMessage}
+
+ )} +
+ } + > +
+ +
+ +
{integration.isSynced ? "Synced" : "Not Synced"}
+
+
+
+ + )} + + +
+ + { + e.stopPropagation(); + onManualSyncIntegration(); + }} + ariaLabel="sync" + colorSchema="secondary" + variant="plain" + > + + + + + {(isAllowed: boolean) => ( + + { + e.stopPropagation(); + onRemoveIntegration(); + }} + ariaLabel="delete" + isDisabled={!isAllowed} + colorSchema="danger" + variant="plain" + > + + + + )} + +
+ + + ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationsTable.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationsTable.tsx new file mode 100644 index 000000000..ea2ecd888 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/IntegrationsTable.tsx @@ -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(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.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 ( +
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search integrations..." + className="flex-1" + /> + + + + + + + + + + Status + {Object.values(IntegrationStatus).map((status) => ( + { + 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) && ( + + ) + } + iconPos="right" + > +
+ + {status.replace("-", " ")} +
+
+ ))} + Integration + {[...new Set(integrations.map(({ integration }) => integration))].map((integration) => ( + { + 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) && ( + + ) + } + iconPos="right" + > +
+ {`${cloudIntegrationMap.get(integration)!.name} + {cloudIntegrationMap.get(integration)!.name} +
+
+ ))} + Environment + {environments.map((env) => ( + { + 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) && ( + + ) + } + iconPos="right" + > + {env.name} + + ))} +
+
+
+ + + + + + + + + + + + + {filteredIntegrations.slice(offset, perPage * page).map((integration) => ( + { + syncIntegration({ + workspaceId, + id: integration.id, + lastUsed: integration.lastUsed as string + }); + }} + onRemoveIntegration={() => onDeleteIntegration(integration)} + integration={integration} + environment={environmentMap.get(integration.envId)} + /> + ))} + +
+
+ Integration + handleSort(IntegrationsOrderBy.App)} + > + + +
+
+
+ Source Path + handleSort(IntegrationsOrderBy.SecretPath)} + > + + +
+
+
+ Source Environment + handleSort(IntegrationsOrderBy.Environment)} + > + + +
+
+
+ Destination + handleSort(IntegrationsOrderBy.Destination)} + > + + +
+
+
+ Status + handleSort(IntegrationsOrderBy.Status)} + > + + +
+
+
+ {Boolean(filteredIntegrations.length) && ( + + )} + {!isLoading && !filteredIntegrations?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/index.ts b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/index.ts new file mode 100644 index 000000000..d9567592c --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/components/index.ts @@ -0,0 +1 @@ +export * from "./IntegrationsTable"; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/index.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/index.tsx new file mode 100644 index 000000000..6f40d79fa --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/IntegrationsSection/index.tsx @@ -0,0 +1 @@ +export { IntegrationsSection } from "./IntegrationsSection"; diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/json/frameworkIntegrations.json b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/json/frameworkIntegrations.json new file mode 100644 index 000000000..c96c0a0b4 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/json/frameworkIntegrations.json @@ -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" + } +] \ No newline at end of file diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/json/infrastructureIntegrations.json b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/json/infrastructureIntegrations.json new file mode 100644 index 000000000..657cd17eb --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/components/json/infrastructureIntegrations.json @@ -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" + } +] \ No newline at end of file diff --git a/frontend-v2/src/pages/secret-manager/IntegrationsListPage/route.tsx b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/route.tsx new file mode 100644 index 000000000..7012affc6 --- /dev/null +++ b/frontend-v2/src/pages/secret-manager/IntegrationsListPage/route.tsx @@ -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, +}) diff --git a/frontend-v2/src/routeTree.gen.ts b/frontend-v2/src/routeTree.gen.ts index dc475c0ea..8086da184 100644 --- a/frontend-v2/src/routeTree.gen.ts +++ b/frontend-v2/src/routeTree.gen.ts @@ -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" diff --git a/frontend-v2/src/routes.ts b/frontend-v2/src/routes.ts index 9a941fc59..47417f652 100644 --- a/frontend-v2/src/routes.ts +++ b/frontend-v2/src/routes.ts @@ -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"),