mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add referenced secret delete warning to overview page
This commit is contained in:
@@ -142,6 +142,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
importedByEnvs: z
|
||||||
|
.object({
|
||||||
|
environment: z.string(),
|
||||||
|
importedBy: z
|
||||||
|
.object({
|
||||||
|
environment: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
folders: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
isImported: z.boolean(),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretId: z.string(),
|
||||||
|
referencedSecretKey: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
@@ -471,6 +499,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importedByEnvs = [];
|
||||||
|
|
||||||
|
for await (const environment of environments) {
|
||||||
|
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secrets: secrets?.filter((s) => s.environment === environment)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (importedBy) {
|
||||||
|
importedByEnvs.push({
|
||||||
|
environment,
|
||||||
|
importedBy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folders,
|
folders,
|
||||||
dynamicSecrets,
|
dynamicSecrets,
|
||||||
@@ -482,6 +532,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalImportCount,
|
totalImportCount,
|
||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
|
importedByEnvs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
(totalDynamicSecretCount ?? 0) +
|
(totalDynamicSecretCount ?? 0) +
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type DashboardProjectSecretsOverviewResponse = {
|
|||||||
totalUniqueDynamicSecretsInPage: number;
|
totalUniqueDynamicSecretsInPage: number;
|
||||||
totalUniqueFoldersInPage: number;
|
totalUniqueFoldersInPage: number;
|
||||||
totalUniqueSecretImportsInPage: number;
|
totalUniqueSecretImportsInPage: number;
|
||||||
|
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
||||||
totalUniqueSecretRotationsInPage: number;
|
totalUniqueSecretRotationsInPage: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,13 +42,15 @@ export type DashboardProjectSecretsDetailsResponse = {
|
|||||||
totalSecretCount?: number;
|
totalSecretCount?: number;
|
||||||
totalSecretRotationCount?: number;
|
totalSecretRotationCount?: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
importedBy?: {
|
importedBy?: ProjectSecretsImportedBy[];
|
||||||
environment: { name: string; slug: string };
|
};
|
||||||
folders: {
|
|
||||||
name: string;
|
export type ProjectSecretsImportedBy = {
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
environment: { name: string; slug: string };
|
||||||
isImported: boolean;
|
folders: {
|
||||||
}[];
|
name: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||||
|
isImported: boolean;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,8 @@ export const OverviewPage = () => {
|
|||||||
totalUniqueSecretsInPage,
|
totalUniqueSecretsInPage,
|
||||||
totalUniqueSecretImportsInPage,
|
totalUniqueSecretImportsInPage,
|
||||||
totalUniqueDynamicSecretsInPage,
|
totalUniqueDynamicSecretsInPage,
|
||||||
totalUniqueSecretRotationsInPage
|
totalUniqueSecretRotationsInPage,
|
||||||
|
importedByEnvs
|
||||||
} = overview ?? {};
|
} = overview ?? {};
|
||||||
|
|
||||||
const secretImportsShaped = secretImports
|
const secretImportsShaped = secretImports
|
||||||
@@ -1042,6 +1043,7 @@ export const OverviewPage = () => {
|
|||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
selectedEntries={selectedEntries}
|
selectedEntries={selectedEntries}
|
||||||
resetSelectedEntries={resetSelectedEntries}
|
resetSelectedEntries={resetSelectedEntries}
|
||||||
|
importedByEnvs={importedByEnvs}
|
||||||
/>
|
/>
|
||||||
<div className="thin-scrollbar mt-4">
|
<div className="thin-scrollbar mt-4">
|
||||||
<TableContainer
|
<TableContainer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
||||||
|
import { ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
|
||||||
import {
|
import {
|
||||||
SecretType,
|
SecretType,
|
||||||
SecretV3RawSanitized,
|
SecretV3RawSanitized,
|
||||||
@@ -21,6 +23,7 @@ import {
|
|||||||
TSecretFolder
|
TSecretFolder
|
||||||
} from "@app/hooks/api/types";
|
} from "@app/hooks/api/types";
|
||||||
import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components";
|
import { MoveSecretsModal } from "@app/pages/secret-manager/OverviewPage/components/SelectionPanel/components";
|
||||||
|
import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports";
|
||||||
|
|
||||||
export enum EntryType {
|
export enum EntryType {
|
||||||
FOLDER = "folder",
|
FOLDER = "folder",
|
||||||
@@ -34,9 +37,15 @@ type Props = {
|
|||||||
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
|
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
|
||||||
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
|
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
|
||||||
};
|
};
|
||||||
|
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => {
|
export const SelectionPanel = ({
|
||||||
|
secretPath,
|
||||||
|
resetSelectedEntries,
|
||||||
|
selectedEntries,
|
||||||
|
importedByEnvs
|
||||||
|
}: Props) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
@@ -72,6 +81,81 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const secretsToDeleteKeys = useMemo(() => {
|
||||||
|
return Object.values(selectedEntries.secret).flatMap((entries) =>
|
||||||
|
Object.values(entries).map((secret) => secret.key)
|
||||||
|
);
|
||||||
|
}, [selectedEntries]);
|
||||||
|
|
||||||
|
const filterAndMergeEnvironments = (
|
||||||
|
envNames: string[],
|
||||||
|
envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
|
||||||
|
): ProjectSecretsImportedBy[] => {
|
||||||
|
const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
|
||||||
|
|
||||||
|
if (filteredEnvs.length === 0) return [];
|
||||||
|
|
||||||
|
const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
|
||||||
|
const groupedBySlug: Record<string, ProjectSecretsImportedBy[]> = {};
|
||||||
|
|
||||||
|
allImportedBy.forEach((item) => {
|
||||||
|
const { slug } = item.environment;
|
||||||
|
if (!groupedBySlug[slug]) groupedBySlug[slug] = [];
|
||||||
|
groupedBySlug[slug].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedImportedBy = Object.values(groupedBySlug).map((group) => {
|
||||||
|
const { environment } = group[0];
|
||||||
|
const allFolders = group.flatMap((item) => item.folders);
|
||||||
|
|
||||||
|
const foldersByName: Record<string, (typeof allFolders)[number][]> = {};
|
||||||
|
allFolders.forEach((folder) => {
|
||||||
|
if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
|
||||||
|
foldersByName[folder.name].push(folder);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedFolders = Object.entries(foldersByName).map(([name, folders]) => {
|
||||||
|
const isImported = folders.some((folder) => folder.isImported);
|
||||||
|
const allSecrets = folders.flatMap((folder) => folder.secrets || []);
|
||||||
|
|
||||||
|
const uniqueSecrets: { secretId: string; referencedSecretKey: string }[] = [];
|
||||||
|
const secretIds = new Set<string>();
|
||||||
|
|
||||||
|
allSecrets
|
||||||
|
.filter((secret) => secretsToDeleteKeys.includes(secret.referencedSecretKey))
|
||||||
|
.forEach((secret) => {
|
||||||
|
if (!secretIds.has(secret.secretId)) {
|
||||||
|
secretIds.add(secret.secretId);
|
||||||
|
uniqueSecrets.push(secret);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
isImported,
|
||||||
|
...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
environment,
|
||||||
|
folders: mergedFolders.filter(
|
||||||
|
(folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedImportedBy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importedBy = useMemo(() => {
|
||||||
|
if (selectedKeysCount === 0 || !importedByEnvs) return null;
|
||||||
|
return filterAndMergeEnvironments(
|
||||||
|
Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
|
||||||
|
importedByEnvs
|
||||||
|
);
|
||||||
|
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
|
||||||
|
|
||||||
const getDeleteModalTitle = () => {
|
const getDeleteModalTitle = () => {
|
||||||
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
|
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
|
||||||
return "Do you want to delete the selected secrets and folders across environments?";
|
return "Do you want to delete the selected secrets and folders across environments?";
|
||||||
@@ -241,6 +325,15 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
|||||||
title={getDeleteModalTitle()}
|
title={getDeleteModalTitle()}
|
||||||
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
|
||||||
onDeleteApproved={handleBulkDelete}
|
onDeleteApproved={handleBulkDelete}
|
||||||
|
formContent={
|
||||||
|
importedBy &&
|
||||||
|
importedBy.some((element) => element.folders.length > 0) && (
|
||||||
|
<CollapsibleSecretImports
|
||||||
|
importedBy={importedBy}
|
||||||
|
secretsToDelete={secretsToDeleteKeys}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,12 +81,12 @@ import {
|
|||||||
useSelectedSecrets
|
useSelectedSecrets
|
||||||
} from "../../SecretMainPage.store";
|
} from "../../SecretMainPage.store";
|
||||||
import { Filter, RowType } from "../../SecretMainPage.types";
|
import { Filter, RowType } from "../../SecretMainPage.types";
|
||||||
|
import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports";
|
||||||
import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard";
|
import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard";
|
||||||
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
||||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||||
import { FolderForm } from "./FolderForm";
|
import { FolderForm } from "./FolderForm";
|
||||||
import { MoveSecretsModal } from "./MoveSecretsModal";
|
import { MoveSecretsModal } from "./MoveSecretsModal";
|
||||||
import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports";
|
|
||||||
|
|
||||||
type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>;
|
type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>;
|
||||||
type TParsedFolderEnv = Record<
|
type TParsedFolderEnv = Record<
|
||||||
|
|||||||
Reference in New Issue
Block a user