mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 02:26:26 +00:00
Show when folders have imports in secret overview page
This commit is contained in:
@@ -167,6 +167,7 @@ export const useGetImportedSecretsAllEnvs = ({
|
|||||||
select: useCallback(
|
select: useCallback(
|
||||||
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) =>
|
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) =>
|
||||||
data.map((el) => ({
|
data.map((el) => ({
|
||||||
|
currentEnv: env,
|
||||||
environment: el.environment,
|
environment: el.environment,
|
||||||
secretPath: el.secretPath,
|
secretPath: el.secretPath,
|
||||||
environmentInfo: el.environmentInfo,
|
environmentInfo: el.environmentInfo,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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 = {
|
||||||
@@ -90,3 +91,23 @@ export type TDeleteSecretImportDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
path?: 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;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useCallback, useMemo } from "react";
|
|||||||
|
|
||||||
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
|
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
|
||||||
|
|
||||||
|
import { SecretImportData } from "../api/secretImports/types";
|
||||||
|
|
||||||
type FolderNameAndDescription = {
|
type FolderNameAndDescription = {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -96,3 +98,40 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
|
|||||||
|
|
||||||
return { secKeys, getSecretByKey, getEnvSecretKeyCount };
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -67,15 +67,22 @@ 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 { 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 { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
import {
|
||||||
|
useDynamicSecretOverview,
|
||||||
|
useFolderOverview,
|
||||||
|
useSecretImportOverview,
|
||||||
|
useSecretOverview
|
||||||
|
} from "@app/hooks/utils";
|
||||||
|
|
||||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
||||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
|
import { SecretOverviewImportListView } from "./components/SecretOverviewImportListView";
|
||||||
import {
|
import {
|
||||||
SecretNoAccessOverviewTableRow,
|
SecretNoAccessOverviewTableRow,
|
||||||
SecretOverviewTableRow
|
SecretOverviewTableRow
|
||||||
@@ -185,12 +192,22 @@ export const OverviewPage = () => {
|
|||||||
setVisibleEnvs(userAvailableEnvs);
|
setVisibleEnvs(userAvailableEnvs);
|
||||||
}, [userAvailableEnvs]);
|
}, [userAvailableEnvs]);
|
||||||
|
|
||||||
const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } =
|
const {
|
||||||
useGetImportedSecretsAllEnvs({
|
secretImports,
|
||||||
projectId: workspaceId,
|
isImportedSecretPresentInEnv,
|
||||||
path: secretPath,
|
getImportedSecretByKey,
|
||||||
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
|
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(
|
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
||||||
{
|
{
|
||||||
@@ -1054,6 +1071,16 @@ export const OverviewPage = () => {
|
|||||||
key={`overview-${dynamicSecretName}-${index + 1}`}
|
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) => (
|
{secKeys.map((key, index) => (
|
||||||
<SecretOverviewTableRow
|
<SecretOverviewTableRow
|
||||||
isSelected={Boolean(selectedEntries.secret[key])}
|
isSelected={Boolean(selectedEntries.secret[key])}
|
||||||
|
|||||||
+160
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { SecretOverviewImportListView } from "./SecretOverviewImportListView";
|
||||||
Reference in New Issue
Block a user