mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-05 07:30:33 +00:00
UI changes on reference secret warning
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import React, { KeyboardEvent, useMemo, useState } from "react";
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
faCodeBranch,
|
||||
faFolder,
|
||||
faKey,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface Folder {
|
||||
folderName: string;
|
||||
secrets?: string[];
|
||||
}
|
||||
|
||||
interface Environment {
|
||||
envName: string;
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
interface FlatItem {
|
||||
type: "folder" | "secret" | "environment";
|
||||
path: string;
|
||||
secretKey?: string;
|
||||
envName?: string;
|
||||
depth?: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface CollapsibleSecretImportsProps {
|
||||
importedBy?: Environment[];
|
||||
}
|
||||
|
||||
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
||||
importedBy = []
|
||||
}) => {
|
||||
const [expandedEnvs, setExpandedEnvs] = useState<Record<string, boolean>>(
|
||||
importedBy.reduce((acc, env) => ({ ...acc, [env.envName]: true }), {})
|
||||
);
|
||||
|
||||
const toggleEnvironment = (envName: string) => {
|
||||
setExpandedEnvs((prev) => ({
|
||||
...prev,
|
||||
[envName]: !prev[envName]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleKeyDown = (envName: string, e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
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) => {
|
||||
const items: FlatItem[] = [
|
||||
{
|
||||
type: "environment",
|
||||
path: env.envName,
|
||||
envName: env.envName,
|
||||
id: `env-${env.envName}`
|
||||
}
|
||||
];
|
||||
|
||||
env.folders.forEach((folder) => {
|
||||
const folderId = `folder-${env.envName}-${folder.folderName}`;
|
||||
items.push({
|
||||
type: "folder",
|
||||
path: folder.folderName,
|
||||
id: folderId
|
||||
});
|
||||
|
||||
if (folder.secrets && folder.secrets.length > 0) {
|
||||
folder.secrets.forEach((secret) => {
|
||||
const secretPath =
|
||||
folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`;
|
||||
const secretId = `secret-${env.envName}-${secretPath}`;
|
||||
|
||||
items.push({
|
||||
type: "secret",
|
||||
path: secretPath,
|
||||
secretKey: secret,
|
||||
id: secretId
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
...env,
|
||||
items: [envItem, ...rest]
|
||||
};
|
||||
});
|
||||
}, [importedBy]);
|
||||
|
||||
return (
|
||||
<div className="mt-4 overflow-hidden rounded-lg border border-mineshaft-600 bg-red-900/20 shadow-lg">
|
||||
<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 className="scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-gray-850 max-h-64 overflow-y-auto">
|
||||
<div className="w-full divide-y divide-gray-800">
|
||||
{processedEnvironments.map((env) => (
|
||||
<div key={env.envName} className="w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleEnvironment(env.envName)}
|
||||
onKeyDown={(e) => handleKeyDown(env.envName, e)}
|
||||
className="flex w-full cursor-pointer items-center px-4 py-3 text-left text-gray-200 transition-colors hover:bg-red-900/40"
|
||||
aria-expanded={expandedEnvs[env.envName]}
|
||||
aria-controls={`env-content-${env.envName}`}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
@@ -1,151 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
import { SecretTreeView } from "../ActionBar/ReplicateFolderFromBoard/SecretTreeView";
|
||||
|
||||
interface Folder {
|
||||
folderName: string;
|
||||
secrets?: string[];
|
||||
}
|
||||
|
||||
interface Environment {
|
||||
envName: string;
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
interface SecretDeletionImpactProps {
|
||||
importedBy?: Environment[];
|
||||
}
|
||||
|
||||
interface SecretItem {
|
||||
id: string;
|
||||
secretKey: string;
|
||||
secretValue?: string;
|
||||
secretPath?: string;
|
||||
}
|
||||
|
||||
interface FolderStructure {
|
||||
items: SecretItem[];
|
||||
subFolders: {
|
||||
[key: string]: FolderStructure;
|
||||
};
|
||||
}
|
||||
|
||||
export const SecretDeletionImpact: React.FC<SecretDeletionImpactProps> = ({ importedBy = [] }) => {
|
||||
const [treeData, setTreeData] = useState<FolderStructure | null>(null);
|
||||
const [selectedEnv, setSelectedEnv] = useState<string>(importedBy[0]?.envName || "");
|
||||
|
||||
const handleEnvironmentChange = (value: string) => {
|
||||
setSelectedEnv(value);
|
||||
};
|
||||
|
||||
function transformImportedDataToTreeView(
|
||||
environments: Environment[],
|
||||
selectedEnvironment: string
|
||||
): FolderStructure | null {
|
||||
const environment = environments.find((env) => env.envName === selectedEnvironment);
|
||||
if (!environment) return null;
|
||||
|
||||
const rootStructure: FolderStructure = {
|
||||
items: [],
|
||||
subFolders: {}
|
||||
};
|
||||
|
||||
environment.folders.forEach((folder) => {
|
||||
const path = folder.folderName;
|
||||
const pathParts = path.split("/").filter((part) => part !== "");
|
||||
|
||||
if (pathParts.length === 0) {
|
||||
if (folder.secrets && folder.secrets.length > 0) {
|
||||
folder.secrets.forEach((secret) => {
|
||||
rootStructure.items.push({
|
||||
id: `${selectedEnvironment}:${path}:${secret}`,
|
||||
secretKey: secret,
|
||||
secretPath: path
|
||||
});
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let current = rootStructure;
|
||||
|
||||
pathParts.forEach((part, index) => {
|
||||
if (!current.subFolders[part]) {
|
||||
current.subFolders[part] = {
|
||||
items: [],
|
||||
subFolders: {}
|
||||
};
|
||||
}
|
||||
|
||||
current = current.subFolders[part];
|
||||
|
||||
const isLastPart = index === pathParts.length - 1;
|
||||
if (isLastPart && folder.secrets && folder.secrets.length > 0) {
|
||||
folder.secrets.forEach((secret) => {
|
||||
current.items.push({
|
||||
id: `${selectedEnvironment}:${path}:${secret}`,
|
||||
secretKey: secret,
|
||||
secretPath: path
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return rootStructure;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const transformedData = transformImportedDataToTreeView(importedBy, selectedEnv);
|
||||
setTreeData(transformedData);
|
||||
}, [selectedEnv, importedBy]);
|
||||
|
||||
return (
|
||||
<div className="mt-4 max-h-[50vh] overflow-y-auto rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-5 shadow-lg">
|
||||
<div className="mb-4 flex items-start gap-3 rounded-md border border-red-800 bg-red-900/20 p-3">
|
||||
<div className="flex-shrink-0 text-red-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-300">
|
||||
Warning: This secret is imported by another folder
|
||||
</p>
|
||||
<ul className="mt-1 list-disc pl-5 text-xs text-red-300">
|
||||
<li>Deleting will remove it from all locations where it's imported</li>
|
||||
<li className="mt-1">
|
||||
Any dependent secrets will display their reference syntax (like{" "}
|
||||
{"{env.secretPath.key}"}) instead of actual values
|
||||
</li>
|
||||
<li className="mt-1">Dependent secrets themselves will not be automatically deleted</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SecretTreeView data={treeData} basePath="/" onChange={() => {}} isDisabled />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Select
|
||||
className="w-44 rounded-md border border-mineshaft-600 bg-mineshaft-700 text-gray-200"
|
||||
onValueChange={handleEnvironmentChange}
|
||||
defaultValue={selectedEnv}
|
||||
>
|
||||
{importedBy.map((env) => (
|
||||
<SelectItem
|
||||
value={env.envName}
|
||||
key={env.envName}
|
||||
className="data-[highlighted]:bg-mineshaft-600"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-200">{env.envName}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -16,10 +16,10 @@ import { WsTag } from "@app/hooks/api/types";
|
||||
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal";
|
||||
|
||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { SecretDetailSidebar } from "./SecretDetailSidebar";
|
||||
import { SecretItem } from "./SecretItem";
|
||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||
import { SecretDeletionImpact } from "./SecretDeletionImpact";
|
||||
|
||||
type Props = {
|
||||
secrets?: SecretV3RawSanitized[];
|
||||
@@ -361,7 +361,9 @@ export const SecretListView = ({
|
||||
onDeleteApproved={handleSecretDelete}
|
||||
buttonText="Delete Secret"
|
||||
>
|
||||
{importedBy && importedBy.length > 0 && <SecretDeletionImpact importedBy={importedBy} />}
|
||||
{importedBy && importedBy.length > 0 && (
|
||||
<CollapsibleSecretImports importedBy={importedBy} />
|
||||
)}
|
||||
</DeleteActionModal>
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
|
Reference in New Issue
Block a user