mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
Improve referenced secret deletion message component
This commit is contained in:
@@ -578,6 +578,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
importedBy: z
|
importedBy: z
|
||||||
.object({
|
.object({
|
||||||
envName: z.string(),
|
envName: z.string(),
|
||||||
|
envSlug: z.string(),
|
||||||
folders: z
|
folders: z
|
||||||
.object({
|
.object({
|
||||||
folderName: z.string(),
|
folderName: z.string(),
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ export const secretImportServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
|
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
|
||||||
|
console.log({ importedBy });
|
||||||
const deepPaths: { path: string; folderId: string }[] = [];
|
const deepPaths: { path: string; folderId: string }[] = [];
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ type Props = {
|
|||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
onDeleteApproved: () => Promise<void>;
|
onDeleteApproved: () => Promise<void>;
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
formContent?: ReactNode;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
deletionMessage?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteActionModal = ({
|
export const DeleteActionModal = ({
|
||||||
@@ -28,6 +30,8 @@ export const DeleteActionModal = ({
|
|||||||
title,
|
title,
|
||||||
subTitle = "This action is irreversible.",
|
subTitle = "This action is irreversible.",
|
||||||
buttonText = "Delete",
|
buttonText = "Delete",
|
||||||
|
formContent,
|
||||||
|
deletionMessage,
|
||||||
children
|
children
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [inputData, setInputData] = useState("");
|
const [inputData, setInputData] = useState("");
|
||||||
@@ -79,6 +83,7 @@ export const DeleteActionModal = ({
|
|||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
|
{formContent}
|
||||||
<form
|
<form
|
||||||
onSubmit={(evt) => {
|
onSubmit={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@@ -88,7 +93,11 @@ export const DeleteActionModal = ({
|
|||||||
<FormControl
|
<FormControl
|
||||||
label={
|
label={
|
||||||
<div className="break-words pb-2 text-sm">
|
<div className="break-words pb-2 text-sm">
|
||||||
Type <span className="font-bold">{deleteKey}</span> to perform this action
|
{deletionMessage || (
|
||||||
|
<>
|
||||||
|
Type <span className="font-bold">{deleteKey}</span> to perform this action
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export type DashboardProjectSecretsDetailsResponse = {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
importedBy?: {
|
importedBy?: {
|
||||||
envName: string;
|
envName: string;
|
||||||
|
envSlug: string;
|
||||||
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
+101
-151
@@ -1,24 +1,24 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo } from "react";
|
||||||
import {
|
import { faFileImport, faKey, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
faChevronLeft,
|
|
||||||
faChevronRight,
|
|
||||||
faFolder,
|
|
||||||
faKey,
|
|
||||||
faWarning
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
|
||||||
interface FlatItem {
|
interface FlatItem {
|
||||||
type: "folder" | "secret";
|
type: "folder" | "secret";
|
||||||
path: string;
|
path: string;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
envName: string;
|
||||||
|
envSlug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CollapsibleSecretImportsProps {
|
interface CollapsibleSecretImportsProps {
|
||||||
importedBy?: {
|
importedBy?: {
|
||||||
envName: string;
|
envName: string;
|
||||||
|
envSlug: string;
|
||||||
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -26,22 +26,49 @@ interface CollapsibleSecretImportsProps {
|
|||||||
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
||||||
importedBy = []
|
importedBy = []
|
||||||
}) => {
|
}) => {
|
||||||
const [currentEnvIndex, setCurrentEnvIndex] = useState(0);
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const truncatePath = (path: string, maxLength = 45): string =>
|
const truncatePath = (path: string, maxLength = 24): string => {
|
||||||
path.length <= maxLength ? path : `...${path.slice(-(maxLength - 3))}`;
|
if (path.length <= maxLength) return path;
|
||||||
|
|
||||||
const groupedItems = useMemo(() => {
|
const endPortion = path.slice(-(maxLength - 3));
|
||||||
return importedBy.map((env) => {
|
const slashIndex = endPortion.indexOf("/");
|
||||||
const envItems: FlatItem[] = [];
|
|
||||||
|
|
||||||
|
if (slashIndex !== -1) {
|
||||||
|
return `...${endPortion.slice(slashIndex)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `...${endPortion}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePathClick = (item: FlatItem) => {
|
||||||
|
let pathToNavigate;
|
||||||
|
if (item.type === "folder") {
|
||||||
|
pathToNavigate = item.path;
|
||||||
|
} else {
|
||||||
|
const lastSlashIndex = item.path.lastIndexOf("/");
|
||||||
|
pathToNavigate = lastSlashIndex > 0 ? item.path.substring(0, lastSlashIndex) : "/";
|
||||||
|
}
|
||||||
|
const encodedPath = encodeURIComponent(pathToNavigate);
|
||||||
|
window.open(
|
||||||
|
`/secret-manager/${currentWorkspace.id}/secrets/${item.envSlug}?secretPath=${encodedPath}`,
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedItems = useMemo(() => {
|
||||||
|
const items: FlatItem[] = [];
|
||||||
|
|
||||||
|
importedBy.forEach((env) => {
|
||||||
env.folders.forEach((folder) => {
|
env.folders.forEach((folder) => {
|
||||||
if (folder.folderImported) {
|
if (folder.folderImported) {
|
||||||
envItems.push({
|
items.push({
|
||||||
type: "folder",
|
type: "folder",
|
||||||
path: folder.folderName,
|
path: folder.folderName,
|
||||||
id: `folder-${env.envName}-${folder.folderName}`,
|
id: `folder-${env.envName}-${folder.folderName}`,
|
||||||
reference: "Imported"
|
reference: "Imported",
|
||||||
|
envName: env.envName,
|
||||||
|
envSlug: env.envSlug
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,160 +77,83 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
const secretPath =
|
const secretPath =
|
||||||
folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`;
|
folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`;
|
||||||
|
|
||||||
envItems.push({
|
items.push({
|
||||||
type: "secret",
|
type: "secret",
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
secretKey: secret,
|
secretKey: secret,
|
||||||
id: `secret-${env.envName}-${secretPath}`,
|
id: `secret-${env.envName}-${secretPath}`,
|
||||||
reference: "Referenced"
|
reference: "Referenced",
|
||||||
|
envName: env.envName,
|
||||||
|
envSlug: env.envSlug
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return items.sort((a, b) => {
|
||||||
envName: env.envName,
|
const envCompare = a.envName.localeCompare(b.envName);
|
||||||
items: envItems.sort((a, b) => {
|
if (envCompare !== 0) return envCompare;
|
||||||
const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`;
|
|
||||||
const bPath = b.path.startsWith("/") ? b.path : `/${b.path}`;
|
|
||||||
|
|
||||||
const aSegments = aPath.split("/").filter(Boolean).length;
|
const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`;
|
||||||
const bSegments = bPath.split("/").filter(Boolean).length;
|
const bPath = b.path.startsWith("/") ? b.path : `/${b.path}`;
|
||||||
|
|
||||||
if (aSegments !== bSegments) {
|
const aSegments = aPath.split("/").filter(Boolean).length;
|
||||||
return aSegments - bSegments;
|
const bSegments = bPath.split("/").filter(Boolean).length;
|
||||||
}
|
|
||||||
|
|
||||||
return aPath.localeCompare(bPath);
|
if (aSegments !== bSegments) {
|
||||||
})
|
return aSegments - bSegments;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return aPath.localeCompare(bPath);
|
||||||
});
|
});
|
||||||
}, [importedBy]);
|
}, [importedBy]);
|
||||||
|
|
||||||
const totalEnvironments = groupedItems.length;
|
|
||||||
const currentItems = totalEnvironments > 0 ? groupedItems[currentEnvIndex]?.items || [] : [];
|
|
||||||
|
|
||||||
const handlePrevEnvironment = () => {
|
|
||||||
if (currentEnvIndex > 0) {
|
|
||||||
setCurrentEnvIndex(currentEnvIndex - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextEnvironment = () => {
|
|
||||||
if (currentEnvIndex < totalEnvironments - 1) {
|
|
||||||
setCurrentEnvIndex(currentEnvIndex + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPaginationButton = (
|
|
||||||
direction: "prev" | "next",
|
|
||||||
onClick: () => void,
|
|
||||||
disabled: boolean
|
|
||||||
) => {
|
|
||||||
const icon = direction === "prev" ? faChevronLeft : faChevronRight;
|
|
||||||
const label = direction === "prev" ? "Previous environment" : "Next environment";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`rounded-md border p-2 ${
|
|
||||||
disabled
|
|
||||||
? "border-mineshaft-700 text-gray-600"
|
|
||||||
: "border-mineshaft-600 text-gray-400 hover:bg-mineshaft-800"
|
|
||||||
}`}
|
|
||||||
aria-label={label}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 overflow-hidden rounded-lg border border-mineshaft-600 bg-mineshaft-900 shadow-lg">
|
<div className="mb-4 w-full">
|
||||||
<div className="p-4">
|
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20">
|
||||||
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20">
|
<div className="flex items-start gap-3 p-4">
|
||||||
<div className="flex items-start gap-3 p-4">
|
<div className="mt-0.5 flex-shrink-0 text-red-500">
|
||||||
<div className="mt-0.5 flex-shrink-0 text-red-500">
|
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
||||||
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
</div>
|
||||||
</div>
|
<div className="w-full">
|
||||||
<div className="w-full">
|
<p className="text-sm font-semibold text-red-500">
|
||||||
<p className="text-sm font-semibold text-red-500">
|
The following resources will be affected by this change
|
||||||
Warning: The following resources will be affected by this change
|
</p>
|
||||||
</p>
|
|
||||||
<ul className="mt-2 list-disc space-y-2 pl-5 text-xs text-gray-300">
|
|
||||||
<li>Deleting will remove it from all folders where it's imported</li>
|
|
||||||
<li>
|
|
||||||
Any secrets referencing this will display their reference syntax (like{" "}
|
|
||||||
<code className="rounded px-1 py-0.5 font-mono text-red-500">
|
|
||||||
{"{env.secretPath.key}"}
|
|
||||||
</code>
|
|
||||||
) instead of actual values
|
|
||||||
</li>
|
|
||||||
<li>Secrets referencing this will not be automatically deleted</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{totalEnvironments > 0 && (
|
<div className="max-h-64 overflow-y-auto rounded-md border border-mineshaft-700">
|
||||||
<div className="mb-2 px-1">
|
<Table>
|
||||||
<h3 className="text-md font-medium text-gray-200">
|
<THead className="sticky -top-1 bg-bunker-800">
|
||||||
Environment: {groupedItems[currentEnvIndex]?.envName}
|
<Th className="px-4">Type</Th>
|
||||||
</h3>
|
<Th className="px-4">Environment</Th>
|
||||||
</div>
|
<Th className="truncate px-4">Path</Th>
|
||||||
)}
|
<Th className="px-4">Usage</Th>
|
||||||
|
</THead>
|
||||||
<div className="h-48 overflow-y-auto rounded-md border border-mineshaft-700">
|
<TBody>
|
||||||
<table className="w-full">
|
{flattenedItems.map((item) => (
|
||||||
<thead className="border-b border-mineshaft-700 bg-mineshaft-800">
|
<Tr
|
||||||
<tr>
|
key={item.id}
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Type</th>
|
onClick={() => handlePathClick(item)}
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Path</th>
|
className="cursor-pointer hover:bg-mineshaft-700"
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Usage</th>
|
title={`Navigate to ${item.path}`}
|
||||||
</tr>
|
>
|
||||||
</thead>
|
<Td>
|
||||||
<tbody className="divide-y divide-mineshaft-700">
|
<FontAwesomeIcon
|
||||||
{currentItems.map((item) => (
|
icon={item.type === "secret" ? faKey : faFileImport}
|
||||||
<tr key={item.id} className="hover:bg-mineshaft-800">
|
className={`h-4 w-4 ${item.type === "secret" ? "text-gray-400" : "text-green-700"}`}
|
||||||
<td className="px-4 py-3">
|
aria-hidden="true"
|
||||||
<FontAwesomeIcon
|
/>
|
||||||
icon={item.type === "secret" ? faKey : faFolder}
|
</Td>
|
||||||
className={`h-4 w-4 ${item.type === "secret" ? "text-gray-400" : "text-yellow-500"}`}
|
<Td className="px-4">{item.envName}</Td>
|
||||||
aria-hidden="true"
|
<Td className="truncate px-4">{truncatePath(item.path)}</Td>
|
||||||
/>
|
<Td className="px-4">{item.reference}</Td>
|
||||||
</td>
|
</Tr>
|
||||||
<td className="px-4 py-3 text-sm text-gray-300">{truncatePath(item.path)}</td>
|
))}
|
||||||
<td className="px-4 py-3 text-sm text-gray-400">{item.reference}</td>
|
</TBody>
|
||||||
</tr>
|
</Table>
|
||||||
))}
|
|
||||||
{currentItems.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="px-4 py-6 text-center text-sm text-gray-500">
|
|
||||||
No items in this environment
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{totalEnvironments > 1 && (
|
|
||||||
<div className="mt-4 flex items-center justify-between px-4">
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Environment {currentEnvIndex + 1} of {totalEnvironments}
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
{renderPaginationButton("prev", handlePrevEnvironment, currentEnvIndex === 0)}
|
|
||||||
{renderPaginationButton(
|
|
||||||
"next",
|
|
||||||
handleNextEnvironment,
|
|
||||||
currentEnvIndex === totalEnvironments - 1
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+15
-5
@@ -31,6 +31,7 @@ type Props = {
|
|||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
importedBy?: {
|
importedBy?: {
|
||||||
envName: string;
|
envName: string;
|
||||||
|
envSlug: string;
|
||||||
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@@ -363,11 +364,20 @@ export const SecretListView = ({
|
|||||||
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
|
||||||
onDeleteApproved={handleSecretDelete}
|
onDeleteApproved={handleSecretDelete}
|
||||||
buttonText="Delete Secret"
|
buttonText="Delete Secret"
|
||||||
>
|
formContent={
|
||||||
{importedBy && importedBy.length > 0 && (
|
importedBy &&
|
||||||
<CollapsibleSecretImports importedBy={importedBy} />
|
importedBy.length > 0 && <CollapsibleSecretImports importedBy={importedBy} />
|
||||||
)}
|
}
|
||||||
</DeleteActionModal>
|
deletionMessage={
|
||||||
|
<>
|
||||||
|
Type the secret key{" "}
|
||||||
|
<span className="font-bold">
|
||||||
|
"{(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}"
|
||||||
|
</span>{" "}
|
||||||
|
below to perform this action
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<SecretDetailSidebar
|
<SecretDetailSidebar
|
||||||
environment={environment}
|
environment={environment}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
|
|||||||
Reference in New Issue
Block a user