Show when folders have imports in secret overview page

This commit is contained in:
carlosmonastyrski
2025-03-17 19:13:06 -03:00
parent 8de7261c9a
commit ad110f490c
6 changed files with 256 additions and 7 deletions

View File

@@ -167,6 +167,7 @@ export const useGetImportedSecretsAllEnvs = ({
select: useCallback(
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) =>
data.map((el) => ({
currentEnv: env,
environment: el.environment,
secretPath: el.secretPath,
environmentInfo: el.environmentInfo,

View File

@@ -1,4 +1,5 @@
import { SecretV3Raw } from "../secrets/types";
import { WsTag } from "../types";
import { WorkspaceEnv } from "../workspace/types";
export type TSecretImport = {
@@ -90,3 +91,23 @@ export type TDeleteSecretImportDTO = {
environment: string;
path?: string;
};
export type SecretImportData = {
currentEnv: string;
environment: string;
secretPath: string;
environmentInfo: WorkspaceEnv;
folderId: string;
secrets: {
id: string;
env: string;
key: string;
value: string;
secretValueHidden: boolean;
tags?: WsTag[];
comment?: string;
createdAt: string;
updatedAt: string;
version: number;
}[];
};

View File

@@ -2,6 +2,8 @@ import { useCallback, useMemo } from "react";
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
import { SecretImportData } from "../api/secretImports/types";
type FolderNameAndDescription = {
name: string;
description?: string;
@@ -96,3 +98,40 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
return { secKeys, getSecretByKey, getEnvSecretKeyCount };
};
export const useSecretImportOverview = (secretImports: SecretImportData[][] | undefined) => {
const uniqueEnvSecretPaths = useMemo(() => {
const uniqueMap: Record<string, SecretImportData[]> = {};
secretImports?.forEach((importData) => {
importData?.forEach((envImport) => {
if (envImport) {
const key = `${envImport.environment}-${envImport.secretPath}`;
const existing = uniqueMap[key];
uniqueMap[key] = existing ? [...existing, envImport] : [envImport];
}
});
});
return uniqueMap;
}, [secretImports]);
const isSecretImportPresent = useCallback(
(sourceEnv: string, targetEnv: string, secretPath: string) => {
return (
secretImports?.some((importData) =>
importData?.some(
(envImport) =>
envImport?.currentEnv === sourceEnv &&
envImport?.environment === targetEnv &&
envImport?.secretPath === secretPath
)
) ?? false
);
},
[secretImports]
);
return {
uniqueEnvSecretPaths,
isSecretImportPresent
};
};

View File

@@ -67,15 +67,22 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { SecretImportData } from "@app/hooks/api/secretImports/types";
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
import {
useDynamicSecretOverview,
useFolderOverview,
useSecretImportOverview,
useSecretOverview
} from "@app/hooks/utils";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewImportListView } from "./components/SecretOverviewImportListView";
import {
SecretNoAccessOverviewTableRow,
SecretOverviewTableRow
@@ -185,12 +192,22 @@ export const OverviewPage = () => {
setVisibleEnvs(userAvailableEnvs);
}, [userAvailableEnvs]);
const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } =
useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const {
secretImports,
isImportedSecretPresentInEnv,
getImportedSecretByKey,
getEnvImportedSecretKeyCount
} = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const secretImportsData = useMemo(
() => secretImports?.map((s) => s.data as SecretImportData[]) ?? [],
[secretImports]
);
const { uniqueEnvSecretPaths, isSecretImportPresent } =
useSecretImportOverview(secretImportsData);
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
{
@@ -1054,6 +1071,16 @@ export const OverviewPage = () => {
key={`overview-${dynamicSecretName}-${index + 1}`}
/>
))}
{Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => (
<SecretOverviewImportListView
secretImport={secretImportsAllEnvs[0]}
isImportedSecretPresentInEnv={isSecretImportPresent}
environments={visibleEnvs}
key={`overview-${key}`}
scrollOffset={debouncedScrollOffset}
allSecretImports={secretImportsData}
/>
))}
{secKeys.map((key, index) => (
<SecretOverviewTableRow
isSelected={Boolean(selectedEntries.secret[key])}

View File

@@ -0,0 +1,160 @@
import { subject } from "@casl/ability";
import { faCheck, faFileImport, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { EmptyState, SecretInput, TableContainer, Td, Tr } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import { SecretImportData } from "@app/hooks/api/secretImports/types";
import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem";
import { computeImportedSecretRows } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView";
type Props = {
secretImport: SecretImportData;
environments: { name: string; slug: string }[];
isImportedSecretPresentInEnv: (
sourceEnv: string,
targetEnv: string,
secretPath: string
) => boolean;
scrollOffset: number;
allSecretImports: SecretImportData[][] | undefined;
};
export const SecretOverviewImportListView = ({
secretImport,
environments = [],
isImportedSecretPresentInEnv,
scrollOffset,
allSecretImports
}: Props) => {
const [isFormExpanded, setIsFormExpanded] = useToggle();
const { permission } = useProjectPermission();
const environmentImportDetails = secretImport.environmentInfo;
const totalCols = environments.length + 1;
const canReadSecretImports = permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, {
environment: environmentImportDetails.slug,
secretPath: secretImport.secretPath
})
);
const computeImportedSecrets =
canReadSecretImports && allSecretImports
? computeImportedSecretRows(
environmentImportDetails.slug,
secretImport.secretPath,
(allSecretImports?.flatMap((s) => s ?? []) ?? []).filter(Boolean) as SecretImportData[]
)
: [];
return (
<>
<Tr
isHoverable
isSelectable
onClick={() => canReadSecretImports && setIsFormExpanded.toggle()}
className={`group ${isFormExpanded ? "border-t-2 border-mineshaft-500" : ""}`}
>
<Td className="sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding px-0 py-0 group-hover:bg-mineshaft-700">
<div className="group flex cursor-pointer">
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
<FontAwesomeIcon icon={faFileImport} />
</div>
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
<EnvFolderIcon
env={environmentImportDetails.slug || ""}
secretPath={secretImport.secretPath || ""}
/>
</div>
</div>
</Td>
{environments.map(({ slug }, i) => {
const isPresent = isImportedSecretPresentInEnv(
slug,
secretImport.environment,
secretImport.secretPath
);
return (
<Td
key={`sec-overview-${slug}-${i + 1}-value`}
className={twMerge(
"px-0 py-0 group-hover:bg-mineshaft-700",
isPresent ? "text-green-600" : "text-red-600"
)}
>
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]">
<div className="flex justify-center">
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={
isImportedSecretPresentInEnv(
slug,
secretImport.environment,
secretImport.secretPath
)
? faCheck
: faXmark
}
/>
</div>
</div>
</Td>
);
})}
</Tr>
{canReadSecretImports && isFormExpanded && (
<Tr>
<Td
colSpan={totalCols}
className={`bg-bunker-600 px-0 py-0 ${
isFormExpanded && "border-b-2 border-mineshaft-500"
}`}
>
<div
className="ml-2 p-2"
style={{
marginLeft: scrollOffset,
width: "100%"
}}
>
<TableContainer>
<table className="secret-table">
<thead>
<tr>
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
{/* <td style={{ padding: "0.25rem 1rem" }}>Override</td> */}
</tr>
</thead>
<tbody>
{computeImportedSecrets?.length === 0 && (
<tr>
<td colSpan={3}>
<EmptyState title="No secrets found" icon={faKey} />
</td>
</tr>
)}
{computeImportedSecrets.map(({ key, value }, index) => (
<tr key={`${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<SecretInput value={value} isReadOnly />
</td>
</tr>
))}
</tbody>
</table>
</TableContainer>
</div>
</Td>
</Tr>
)}
</>
);
};

View File

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