mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 18:15:53 +00:00
feat: renamed old frontend and fixed some minor bugs
This commit is contained in:
+142
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+37
@@ -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"
|
||||
};
|
||||
+85
@@ -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>
|
||||
);
|
||||
};
|
||||
+233
@@ -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>
|
||||
);
|
||||
};
|
||||
+70
@@ -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>
|
||||
);
|
||||
};
|
||||
+99
@@ -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>
|
||||
);
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { FormLabel, Spinner } from "@app/components/v2";
|
||||
import { useGetIntegrationAuthOctopusDeployScopeValues } from "@app/hooks/api/integrationAuth/queries";
|
||||
import {
|
||||
OctopusDeployScope,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
} from "@app/hooks/api/integrationAuth/types";
|
||||
import { TIntegration, TOctopusDeployScopeValues } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type OctopusDeployScopeValuesProps = {
|
||||
integration: TIntegration;
|
||||
};
|
||||
|
||||
// remove plural since Octopus Deploy can decide whether they want to use singular or plural...
|
||||
const modifyKey = (key: keyof TOctopusDeployVariableSetScopeValues) => {
|
||||
switch (key) {
|
||||
case "Processes":
|
||||
return "ProcessOwner";
|
||||
default:
|
||||
return key.substring(0, key.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
export const OctopusDeployScopeValues = ({ integration }: OctopusDeployScopeValuesProps) => {
|
||||
const hasScopeValues = Boolean(
|
||||
Object.values(integration.metadata?.octopusDeployScopeValues ?? {}).some(
|
||||
(values) => values.length > 0
|
||||
)
|
||||
);
|
||||
|
||||
const { data: scopeValues = {}, isPending } = useGetIntegrationAuthOctopusDeployScopeValues(
|
||||
{
|
||||
scope: OctopusDeployScope.Project,
|
||||
spaceId: integration.targetEnvironmentId!,
|
||||
resourceId: integration.appId!,
|
||||
integrationAuthId: integration.integrationAuthId
|
||||
},
|
||||
{
|
||||
enabled: hasScopeValues
|
||||
}
|
||||
);
|
||||
|
||||
if (!integration.metadata?.octopusDeployScopeValues)
|
||||
return <span className="text-sm text-mineshaft-400">Not Configured</span>;
|
||||
|
||||
if (isPending) return <Spinner size="sm" className="ml-2 mt-2" />;
|
||||
|
||||
const scopeValuesMap = new Map(
|
||||
Object.entries(scopeValues).map(([key, values]) => [
|
||||
modifyKey(key as keyof TOctopusDeployVariableSetScopeValues),
|
||||
new Map((values as { Name: string; Id: string }[]).map((value) => [value.Id, value.Name]))
|
||||
])
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(integration.metadata.octopusDeployScopeValues).map(([key, values]) => {
|
||||
if (!values.length) return null;
|
||||
|
||||
const getLabel = (scope: string) => {
|
||||
switch (scope as keyof TOctopusDeployScopeValues) {
|
||||
case "Role":
|
||||
return "Target Tags";
|
||||
case "Machine":
|
||||
return "Targets";
|
||||
case "ProcessOwner":
|
||||
return "Processes";
|
||||
case "Action":
|
||||
return "Steps";
|
||||
default:
|
||||
return `${scope}s`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4" key={key}>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-200" label={getLabel(key)} />
|
||||
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{values
|
||||
.map((value) => scopeValuesMap.get(key)?.get(value))
|
||||
.map((name) => (
|
||||
<p key={name}>{name}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { IntegrationDetailsByIDPage } from "./IntegrationsDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId"
|
||||
)({
|
||||
component: IntegrationDetailsByIDPage
|
||||
});
|
||||
Reference in New Issue
Block a user