UI changes on reference secret warning

This commit is contained in:
carlosmonastyrski
2025-04-09 17:36:57 -03:00
parent ab566bcbe4
commit b121ec891f
3 changed files with 206 additions and 153 deletions

View File

@@ -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&apos;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>
);
};

View File

@@ -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&apos;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>
);
};

View File

@@ -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}