Add filter imports and minor improvements to allSecretImports logic

This commit is contained in:
carlosmonastyrski
2025-03-17 22:23:31 -03:00
parent ad110f490c
commit e67a8f9c05
4 changed files with 76 additions and 54 deletions
+10 -3
View File
@@ -1,5 +1,4 @@
import { SecretV3Raw } from "../secrets/types"; import { SecretV3Raw } from "../secrets/types";
import { WsTag } from "../types";
import { WorkspaceEnv } from "../workspace/types"; import { WorkspaceEnv } from "../workspace/types";
export type TSecretImport = { export type TSecretImport = {
@@ -92,7 +91,7 @@ export type TDeleteSecretImportDTO = {
path?: string; path?: string;
}; };
export type SecretImportData = { export type TSecretImportMultiEnvData = {
currentEnv: string; currentEnv: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -104,7 +103,15 @@ export type SecretImportData = {
key: string; key: string;
value: string; value: string;
secretValueHidden: boolean; secretValueHidden: boolean;
tags?: WsTag[]; tags?: {
id: string;
slug: string;
color?: string;
projectId: string;
createdAt: string;
updatedAt: string;
__v: number;
}[];
comment?: string; comment?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
+14 -18
View File
@@ -1,8 +1,7 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types"; import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types";
import { SecretImportData } from "../api/secretImports/types";
type FolderNameAndDescription = { type FolderNameAndDescription = {
name: string; name: string;
@@ -99,31 +98,28 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
return { secKeys, getSecretByKey, getEnvSecretKeyCount }; return { secKeys, getSecretByKey, getEnvSecretKeyCount };
}; };
export const useSecretImportOverview = (secretImports: SecretImportData[][] | undefined) => { export const useSecretImportOverview = (secretImports: TSecretImportMultiEnvData[] | undefined) => {
const uniqueEnvSecretPaths = useMemo(() => { const uniqueEnvSecretPaths = useMemo(() => {
const uniqueMap: Record<string, SecretImportData[]> = {}; const uniqueMap = new Map<string, TSecretImportMultiEnvData>();
secretImports?.forEach((importData) => { secretImports?.forEach((importData) => {
importData?.forEach((envImport) => { if (importData) {
if (envImport) { const key = `${importData.environment}-${importData.secretPath}`;
const key = `${envImport.environment}-${envImport.secretPath}`; if (!uniqueMap.has(key)) {
const existing = uniqueMap[key]; uniqueMap.set(key, importData);
uniqueMap[key] = existing ? [...existing, envImport] : [envImport];
} }
}); }
}); });
return uniqueMap; return Array.from(uniqueMap.values());
}, [secretImports]); }, [secretImports]);
const isSecretImportPresent = useCallback( const isSecretImportPresent = useCallback(
(sourceEnv: string, targetEnv: string, secretPath: string) => { (sourceEnv: string, targetEnv: string, secretPath: string) => {
return ( return (
secretImports?.some((importData) => secretImports?.some(
importData?.some( (importData) =>
(envImport) => importData?.currentEnv === sourceEnv &&
envImport?.currentEnv === sourceEnv && importData?.environment === targetEnv &&
envImport?.environment === targetEnv && importData?.secretPath === secretPath
envImport?.secretPath === secretPath
)
) ?? false ) ?? false
); );
}, },
@@ -7,6 +7,7 @@ import {
faAngleDown, faAngleDown,
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faFileImport,
faFingerprint, faFingerprint,
faFolder, faFolder,
faFolderBlank, faFolderBlank,
@@ -67,7 +68,7 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries"; import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types"; import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { SecretImportData } from "@app/hooks/api/secretImports/types"; import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types";
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types"; import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { import {
@@ -100,7 +101,8 @@ export enum EntryType {
enum RowType { enum RowType {
Folder = "folder", Folder = "folder",
DynamicSecret = "dynamic", DynamicSecret = "dynamic",
Secret = "secret" Secret = "secret",
Import = "import"
} }
type Filter = { type Filter = {
@@ -110,7 +112,8 @@ type Filter = {
const DEFAULT_FILTER_STATE = { const DEFAULT_FILTER_STATE = {
[RowType.Folder]: true, [RowType.Folder]: true,
[RowType.DynamicSecret]: true, [RowType.DynamicSecret]: true,
[RowType.Secret]: true [RowType.Secret]: true,
[RowType.Import]: true
}; };
export const OverviewPage = () => { export const OverviewPage = () => {
@@ -203,7 +206,18 @@ export const OverviewPage = () => {
environments: (userAvailableEnvs || []).map(({ slug }) => slug) environments: (userAvailableEnvs || []).map(({ slug }) => slug)
}); });
const secretImportsData = useMemo( const secretImportsData = useMemo(
() => secretImports?.map((s) => s.data as SecretImportData[]) ?? [], () =>
(secretImports?.map((s) => s.data as TSecretImportMultiEnvData[]) ?? [])
?.flatMap((s) => s ?? [])
?.filter((secretImport) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, {
environment: secretImport.currentEnv,
secretPath
})
)
),
[secretImports] [secretImports]
); );
const { uniqueEnvSecretPaths, isSecretImportPresent } = const { uniqueEnvSecretPaths, isSecretImportPresent } =
@@ -768,6 +782,19 @@ export const OverviewPage = () => {
</Button> </Button>
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuLabel>Filter project resources</DropdownMenuLabel> <DropdownMenuLabel>Filter project resources</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
handleToggleRowType(RowType.Import);
}}
icon={filter[RowType.Import] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
<span>Imports</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@@ -1071,16 +1098,19 @@ export const OverviewPage = () => {
key={`overview-${dynamicSecretName}-${index + 1}`} key={`overview-${dynamicSecretName}-${index + 1}`}
/> />
))} ))}
{Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => ( {filter.import &&
<SecretOverviewImportListView Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => (
secretImport={secretImportsAllEnvs[0]} <SecretOverviewImportListView
isImportedSecretPresentInEnv={isSecretImportPresent} secretImport={secretImportsAllEnvs}
environments={visibleEnvs} isImportedSecretPresentInEnv={isSecretImportPresent}
key={`overview-${key}`} environments={visibleEnvs}
scrollOffset={debouncedScrollOffset} key={`overview-${key}`}
allSecretImports={secretImportsData} scrollOffset={debouncedScrollOffset}
/> allSecretImports={(
))} secretImportsData?.flatMap((s) => s ?? []) ?? []
).filter(Boolean)}
/>
))}
{secKeys.map((key, index) => ( {secKeys.map((key, index) => (
<SecretOverviewTableRow <SecretOverviewTableRow
isSelected={Boolean(selectedEntries.secret[key])} isSelected={Boolean(selectedEntries.secret[key])}
@@ -1,17 +1,15 @@
import { subject } from "@casl/ability";
import { faCheck, faFileImport, faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faFileImport, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { EmptyState, SecretInput, TableContainer, Td, Tr } from "@app/components/v2"; import { EmptyState, SecretInput, TableContainer, Td, Tr } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretImportData } from "@app/hooks/api/secretImports/types"; import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types";
import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem"; import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem";
import { computeImportedSecretRows } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView"; import { computeImportedSecretRows } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView";
type Props = { type Props = {
secretImport: SecretImportData; secretImport: TSecretImportMultiEnvData;
environments: { name: string; slug: string }[]; environments: { name: string; slug: string }[];
isImportedSecretPresentInEnv: ( isImportedSecretPresentInEnv: (
sourceEnv: string, sourceEnv: string,
@@ -19,7 +17,7 @@ type Props = {
secretPath: string secretPath: string
) => boolean; ) => boolean;
scrollOffset: number; scrollOffset: number;
allSecretImports: SecretImportData[][] | undefined; allSecretImports: TSecretImportMultiEnvData[];
}; };
export const SecretOverviewImportListView = ({ export const SecretOverviewImportListView = ({
@@ -27,27 +25,18 @@ export const SecretOverviewImportListView = ({
environments = [], environments = [],
isImportedSecretPresentInEnv, isImportedSecretPresentInEnv,
scrollOffset, scrollOffset,
allSecretImports allSecretImports = []
}: Props) => { }: Props) => {
const [isFormExpanded, setIsFormExpanded] = useToggle(); const [isFormExpanded, setIsFormExpanded] = useToggle();
const { permission } = useProjectPermission();
const environmentImportDetails = secretImport.environmentInfo; const environmentImportDetails = secretImport.environmentInfo;
const totalCols = environments.length + 1; const totalCols = environments.length + 1;
const canReadSecretImports = permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, {
environment: environmentImportDetails.slug,
secretPath: secretImport.secretPath
})
);
const computeImportedSecrets = const computeImportedSecrets =
canReadSecretImports && allSecretImports allSecretImports.length > 0
? computeImportedSecretRows( ? computeImportedSecretRows(
environmentImportDetails.slug, environmentImportDetails.slug,
secretImport.secretPath, secretImport.secretPath,
(allSecretImports?.flatMap((s) => s ?? []) ?? []).filter(Boolean) as SecretImportData[] allSecretImports
) )
: []; : [];
return ( return (
@@ -55,7 +44,7 @@ export const SecretOverviewImportListView = ({
<Tr <Tr
isHoverable isHoverable
isSelectable isSelectable
onClick={() => canReadSecretImports && setIsFormExpanded.toggle()} onClick={() => setIsFormExpanded.toggle()}
className={`group ${isFormExpanded ? "border-t-2 border-mineshaft-500" : ""}`} 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"> <Td className="sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding px-0 py-0 group-hover:bg-mineshaft-700">
@@ -106,7 +95,7 @@ export const SecretOverviewImportListView = ({
); );
})} })}
</Tr> </Tr>
{canReadSecretImports && isFormExpanded && ( {isFormExpanded && (
<Tr> <Tr>
<Td <Td
colSpan={totalCols} colSpan={totalCols}