mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
Improve referenced secret deletion message component
This commit is contained in:
@@ -581,6 +581,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
folders: z
|
folders: z
|
||||||
.object({
|
.object({
|
||||||
folderName: z.string(),
|
folderName: z.string(),
|
||||||
|
folderImported: z.boolean(),
|
||||||
secrets: z.array(z.string()).optional()
|
secrets: z.array(z.string()).optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
@@ -856,6 +857,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (secrets?.length || secretRotations?.length) {
|
if (secrets?.length || secretRotations?.length) {
|
||||||
const secretCount =
|
const secretCount =
|
||||||
(secrets?.length ?? 0) +
|
(secrets?.length ?? 0) +
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
[folderName: string]: {
|
[folderName: string]: {
|
||||||
secrets: string[];
|
secrets: string[];
|
||||||
folderId: string;
|
folderId: string;
|
||||||
|
folderImported: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -251,11 +252,13 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!updatedAcc[env].folders[folder]) {
|
if (!updatedAcc[env].folders[folder]) {
|
||||||
updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId };
|
updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId, folderImported: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("secretId" in item && item.secretId) {
|
if ("secretId" in item && item.secretId) {
|
||||||
updatedAcc[env].folders[folder].secrets = [...updatedAcc[env].folders[folder].secrets, item.secretId];
|
updatedAcc[env].folders[folder].secrets = [...updatedAcc[env].folders[folder].secrets, item.secretId];
|
||||||
|
} else {
|
||||||
|
updatedAcc[env].folders[folder].folderImported = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedAcc;
|
return updatedAcc;
|
||||||
@@ -271,6 +274,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
folderName,
|
folderName,
|
||||||
folderId: folderData.folderId,
|
folderId: folderData.folderId,
|
||||||
|
folderImported: folderData.folderImported,
|
||||||
...(hasSecrets && { secrets: folderData.secrets })
|
...(hasSecrets && { secrets: folderData.secrets })
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -843,6 +843,7 @@ export const secretImportServiceFactory = ({
|
|||||||
...el,
|
...el,
|
||||||
folders: el.folders.map((folderItem) => ({
|
folders: el.folders.map((folderItem) => ({
|
||||||
folderId: folderItem.folderId,
|
folderId: folderItem.folderId,
|
||||||
|
folderImported: folderItem.folderImported,
|
||||||
secrets: folderItem.secrets,
|
secrets: folderItem.secrets,
|
||||||
folderName: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}`
|
folderName: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}`
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export type FolderInfo = {
|
|||||||
folderName: string;
|
folderName: string;
|
||||||
secrets?: string[];
|
secrets?: string[];
|
||||||
folderId: string;
|
folderId: string;
|
||||||
|
folderImported: boolean;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export type DashboardProjectSecretsDetailsResponse = {
|
|||||||
totalSecretCount?: number;
|
totalSecretCount?: number;
|
||||||
totalSecretRotationCount?: number;
|
totalSecretRotationCount?: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
importedBy?: { envName: string; folders: { folderName: string; secrets?: string[] }[] }[];
|
importedBy?: {
|
||||||
|
envName: string;
|
||||||
|
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardProjectSecretsByKeys = {
|
export type DashboardProjectSecretsByKeys = {
|
||||||
|
|||||||
+157
-149
@@ -1,201 +1,209 @@
|
|||||||
import React, { KeyboardEvent, useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faChevronDown,
|
faChevronLeft,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCodeBranch,
|
|
||||||
faFolder,
|
faFolder,
|
||||||
faKey,
|
faKey,
|
||||||
faWarning
|
faWarning
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
interface Folder {
|
|
||||||
folderName: string;
|
|
||||||
secrets?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Environment {
|
|
||||||
envName: string;
|
|
||||||
folders: Folder[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FlatItem {
|
interface FlatItem {
|
||||||
type: "folder" | "secret" | "environment";
|
type: "folder" | "secret";
|
||||||
path: string;
|
path: string;
|
||||||
secretKey?: string;
|
secretKey?: string;
|
||||||
envName?: string;
|
reference: string;
|
||||||
depth?: number;
|
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CollapsibleSecretImportsProps {
|
interface CollapsibleSecretImportsProps {
|
||||||
importedBy?: Environment[];
|
importedBy?: {
|
||||||
|
envName: string;
|
||||||
|
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
||||||
importedBy = []
|
importedBy = []
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedEnvs, setExpandedEnvs] = useState<Record<string, boolean>>(
|
const [currentEnvIndex, setCurrentEnvIndex] = useState(0);
|
||||||
importedBy.reduce((acc, env) => ({ ...acc, [env.envName]: true }), {})
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleEnvironment = (envName: string) => {
|
const truncatePath = (path: string, maxLength = 45): string =>
|
||||||
setExpandedEnvs((prev) => ({
|
path.length <= maxLength ? path : `...${path.slice(-(maxLength - 3))}`;
|
||||||
...prev,
|
|
||||||
[envName]: !prev[envName]
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (envName: string, e: KeyboardEvent<HTMLButtonElement>) => {
|
const groupedItems = useMemo(() => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleEnvironment(envName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const truncatePath = (path: string, maxLength: number = 45) => {
|
|
||||||
if (path.length <= maxLength) return path;
|
|
||||||
return `...${path.slice(-(maxLength - 3))}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const processedEnvironments = useMemo(() => {
|
|
||||||
return importedBy.map((env) => {
|
return importedBy.map((env) => {
|
||||||
const items: FlatItem[] = [
|
const envItems: FlatItem[] = [];
|
||||||
{
|
|
||||||
type: "environment",
|
|
||||||
path: env.envName,
|
|
||||||
envName: env.envName,
|
|
||||||
id: `env-${env.envName}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
env.folders.forEach((folder) => {
|
env.folders.forEach((folder) => {
|
||||||
const folderId = `folder-${env.envName}-${folder.folderName}`;
|
if (folder.folderImported) {
|
||||||
items.push({
|
envItems.push({
|
||||||
type: "folder",
|
type: "folder",
|
||||||
path: folder.folderName,
|
path: folder.folderName,
|
||||||
id: folderId
|
id: `folder-${env.envName}-${folder.folderName}`,
|
||||||
});
|
reference: "Imported"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (folder.secrets && folder.secrets.length > 0) {
|
if (folder.secrets?.length) {
|
||||||
folder.secrets.forEach((secret) => {
|
folder.secrets.forEach((secret) => {
|
||||||
const secretPath =
|
const secretPath =
|
||||||
folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`;
|
folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`;
|
||||||
const secretId = `secret-${env.envName}-${secretPath}`;
|
|
||||||
|
|
||||||
items.push({
|
envItems.push({
|
||||||
type: "secret",
|
type: "secret",
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
secretKey: secret,
|
secretKey: secret,
|
||||||
id: secretId
|
id: `secret-${env.envName}-${secretPath}`,
|
||||||
|
reference: "Referenced"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [envItem, ...rest] = items;
|
|
||||||
rest.sort((a, b) => {
|
|
||||||
const aSegments = a.path.split("/").filter(Boolean);
|
|
||||||
const bSegments = b.path.split("/").filter(Boolean);
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(aSegments.length, bSegments.length); i += 1) {
|
|
||||||
if (aSegments[i] !== bSegments[i]) {
|
|
||||||
return aSegments[i].localeCompare(bSegments[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return aSegments.length - bSegments.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...env,
|
envName: env.envName,
|
||||||
items: [envItem, ...rest]
|
items: envItems.sort((a, b) => {
|
||||||
|
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 bSegments = bPath.split("/").filter(Boolean).length;
|
||||||
|
|
||||||
|
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-red-900/20 shadow-lg">
|
<div className="mt-4 overflow-hidden rounded-lg border border-mineshaft-600 bg-mineshaft-900 shadow-lg">
|
||||||
<div className="flex items-start gap-3 p-4">
|
<div className="p-4">
|
||||||
<div className="mt-0.5 flex-shrink-0 text-red-500">
|
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20">
|
||||||
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
<div className="flex items-start gap-3 p-4">
|
||||||
|
<div className="mt-0.5 flex-shrink-0 text-red-500">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="text-sm font-semibold text-red-500">
|
||||||
|
Warning: The following resources will be affected by this change
|
||||||
|
</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 className="w-full">
|
|
||||||
<p className="text-sm font-semibold text-red-500">
|
{totalEnvironments > 0 && (
|
||||||
Warning: The following resources will be affected by this change
|
<div className="mb-2 px-1">
|
||||||
</p>
|
<h3 className="text-md font-medium text-gray-200">
|
||||||
<ul className="mt-2 list-disc space-y-2 pl-5 text-xs text-gray-300">
|
Environment: {groupedItems[currentEnvIndex]?.envName}
|
||||||
<li>Deleting will remove it from all folders where it's imported</li>
|
</h3>
|
||||||
<li>
|
</div>
|
||||||
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}"}
|
<div className="h-48 overflow-y-auto rounded-md border border-mineshaft-700">
|
||||||
</code>
|
<table className="w-full">
|
||||||
) instead of actual values
|
<thead className="border-b border-mineshaft-700 bg-mineshaft-800">
|
||||||
</li>
|
<tr>
|
||||||
<li>Secrets referencing this will not be automatically deleted</li>
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Type</th>
|
||||||
</ul>
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Path</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Usage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-mineshaft-700">
|
||||||
|
{currentItems.map((item) => (
|
||||||
|
<tr key={item.id} className="hover:bg-mineshaft-800">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={item.type === "secret" ? faKey : faFolder}
|
||||||
|
className={`h-4 w-4 ${item.type === "secret" ? "text-gray-400" : "text-yellow-500"}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-gray-850 max-h-64 overflow-y-auto">
|
{totalEnvironments > 1 && (
|
||||||
<div className="w-full divide-y divide-gray-800">
|
<div className="mt-4 flex items-center justify-between px-4">
|
||||||
{processedEnvironments.map((env) => (
|
<div className="text-sm text-gray-400">
|
||||||
<div key={env.envName} className="w-full">
|
Environment {currentEnvIndex + 1} of {totalEnvironments}
|
||||||
<button
|
</div>
|
||||||
type="button"
|
<div className="flex space-x-2">
|
||||||
onClick={() => toggleEnvironment(env.envName)}
|
{renderPaginationButton("prev", handlePrevEnvironment, currentEnvIndex === 0)}
|
||||||
onKeyDown={(e) => handleKeyDown(env.envName, e)}
|
{renderPaginationButton(
|
||||||
className="flex w-full cursor-pointer items-center px-4 py-3 text-left text-gray-200 transition-colors hover:bg-red-900/40"
|
"next",
|
||||||
aria-expanded={expandedEnvs[env.envName]}
|
handleNextEnvironment,
|
||||||
aria-controls={`env-content-${env.envName}`}
|
currentEnvIndex === totalEnvironments - 1
|
||||||
>
|
|
||||||
<div className="flex flex-1 items-center">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCodeBranch}
|
|
||||||
className="mr-3 h-3.5 w-3.5 text-red-500"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-medium">{env.envName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded-full transition-colors">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={expandedEnvs[env.envName] ? faChevronDown : faChevronRight}
|
|
||||||
className="h-3 w-3 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{expandedEnvs[env.envName] && (
|
|
||||||
<div id={`env-content-${env.envName}`} className="bg-mineshaft-850">
|
|
||||||
{env.items.slice(1).map((item) => {
|
|
||||||
const isSecret = item.type === "secret";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="flex items-center px-6 py-2 text-gray-300 transition-colors"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={isSecret ? faKey : faFolder}
|
|
||||||
className={`mr-2.5 h-3 w-3 ${isSecret ? "text-red-400" : "text-red-500"}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="max-w-md truncate text-xs">
|
|
||||||
{truncatePath(item.path)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-1
@@ -29,7 +29,10 @@ type Props = {
|
|||||||
tags?: WsTag[];
|
tags?: WsTag[];
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
importedBy?: { envName: string; folders: { folderName: string; secrets?: string[] }[] }[];
|
importedBy?: {
|
||||||
|
envName: string;
|
||||||
|
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretListView = ({
|
export const SecretListView = ({
|
||||||
|
|||||||
Reference in New Issue
Block a user