improvements: address feedback and additional bugs

This commit is contained in:
Scott Wilson
2025-08-28 15:10:28 -07:00
parent 49b5ab8126
commit e7138f1be9
13 changed files with 200 additions and 75 deletions
+15 -17
View File
@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react"; import { DetailedHTMLProps, forwardRef, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Skeleton } from "../Skeleton"; import { Skeleton } from "../Skeleton";
@@ -9,22 +9,20 @@ export type TableContainerProps = {
className?: string; className?: string;
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>; } & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export const TableContainer = ({ export const TableContainer = forwardRef<HTMLDivElement, TableContainerProps>(
children, ({ children, className, isRounded = true, ...props }, ref): JSX.Element => (
className, <div
isRounded = true, ref={ref}
...props className={twMerge(
}: TableContainerProps): JSX.Element => ( "relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter",
<div isRounded && "rounded-lg",
className={twMerge( className
"relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter", )}
isRounded && "rounded-lg", {...props}
className >
)} {children}
{...props} </div>
> )
{children}
</div>
); );
// main parent table // main parent table
@@ -1,5 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { import {
@@ -273,6 +274,12 @@ export const useGetProjectSecretsDetails = (
...options, ...options,
// wait for all values to be available // wait for all values to be available
enabled: Boolean(projectId) && (options?.enabled ?? true), enabled: Boolean(projectId) && (options?.enabled ?? true),
retry: (count, error) => {
// don't retry 404s
if (error instanceof AxiosError && error.status === 404) return false;
return count <= 5;
},
queryKey: dashboardKeys.getProjectSecretsDetails({ queryKey: dashboardKeys.getProjectSecretsDetails({
secretPath, secretPath,
search, search,
+1 -1
View File
@@ -72,7 +72,7 @@ export type DashboardProjectSecretsOverview = Omit<
DashboardProjectSecretsOverviewResponse, DashboardProjectSecretsOverviewResponse,
"secrets" | "secretRotations" "secrets" | "secretRotations"
> & { > & {
secrets?: SecretV3RawSanitized[]; secrets?: (SecretV3RawSanitized & { sourceEnv?: string })[];
secretRotations?: (TSecretRotationV2 & { secretRotations?: (TSecretRotationV2 & {
secrets: (SecretV3RawSanitized | null)[]; secrets: (SecretV3RawSanitized | null)[];
})[]; })[];
+25 -2
View File
@@ -1,12 +1,13 @@
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react"; import { MouseEvent, RefObject, useCallback, useEffect, useRef, useState } from "react";
type Params = { type Params = {
minWidth: number; minWidth: number;
maxWidth: number; maxWidth: number;
initialWidth: number; initialWidth: number;
ref: RefObject<HTMLTableElement>;
}; };
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => { export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth, ref }: Params) => {
const [colWidth, setColWidth] = useState(initialWidth); const [colWidth, setColWidth] = useState(initialWidth);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const startX = useRef(0); const startX = useRef(0);
@@ -63,6 +64,28 @@ export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Param
}; };
}, [isResizing, handleMouseMove, handleMouseUp]); }, [isResizing, handleMouseMove, handleMouseUp]);
useEffect(() => {
const element = ref?.current;
if (!element) return;
const handleResize = () => {
if (colWidth > maxWidth) {
setColWidth(Math.max(maxWidth, minWidth));
} else if (ref.current?.clientWidth && colWidth > ref.current.clientWidth * 0.9) {
// this else is a fallback to ensure col is always visible
setColWidth(initialWidth);
}
};
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(element);
// eslint-disable-next-line consistent-return
return () => {
resizeObserver.disconnect();
};
}, [ref, maxWidth, colWidth]);
return { return {
colWidth, colWidth,
handleMouseDown, handleMouseDown,
@@ -130,7 +130,11 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
const getEnvSecretKeyCount = useCallback( const getEnvSecretKeyCount = useCallback(
(env: string) => { (env: string) => {
return secrets?.filter((secret) => secret.env === env).length ?? 0; return (
secrets?.filter((secret) =>
secret.sourceEnv ? secret.sourceEnv === env : secret.env === env
).length ?? 0
);
}, },
[secrets] [secrets]
); );
@@ -11,7 +11,7 @@ import {
faVault faVault
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
@@ -31,6 +31,7 @@ export const SecretManagerLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || ""; const projectSlug = currentWorkspace?.slug || "";
const location = useLocation();
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId workspaceId
@@ -84,7 +85,14 @@ export const SecretManagerLayout = () => {
}} }}
> >
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive}> <MenuItem
isSelected={
isActive ||
location.pathname.startsWith(
`/projects/secret-management/${currentWorkspace.id}/secrets`
)
}
>
<div className="mx-1 flex gap-2"> <div className="mx-1 flex gap-2">
<div className="w-6"> <div className="w-6">
<FontAwesomeIcon icon={faVault} /> <FontAwesomeIcon icon={faVault} />
@@ -66,6 +66,7 @@ import { CommitForm } from "./components/CommitForm";
import { CreateSecretForm } from "./components/CreateSecretForm"; import { CreateSecretForm } from "./components/CreateSecretForm";
import { DynamicSecretListView } from "./components/DynamicSecretListView"; import { DynamicSecretListView } from "./components/DynamicSecretListView";
import { EnvironmentTabs } from "./components/EnvironmentTabs"; import { EnvironmentTabs } from "./components/EnvironmentTabs";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { FolderListView } from "./components/FolderListView"; import { FolderListView } from "./components/FolderListView";
import { PitDrawer } from "./components/PitDrawer"; import { PitDrawer } from "./components/PitDrawer";
import { SecretDropzone } from "./components/SecretDropzone"; import { SecretDropzone } from "./components/SecretDropzone";
@@ -107,7 +108,7 @@ const Page = () => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { mutateAsync: createCommit } = useCreateCommit(); const { mutateAsync: createCommit } = useCreateCommit();
const tableRef = useRef<HTMLDivElement>(null); const tableRef = useRef<HTMLTableElement>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const { isBatchMode, pendingChanges } = useBatchMode(); const { isBatchMode, pendingChanges } = useBatchMode();
@@ -251,7 +252,8 @@ const Page = () => {
const { const {
data, data,
isPending: isDetailsLoading, isPending: isDetailsLoading,
isFetching: isDetailsFetching isFetching: isDetailsFetching,
isFetched
} = useGetProjectSecretsDetails({ } = useGetProjectSecretsDetails({
environment, environment,
projectId: workspaceId, projectId: workspaceId,
@@ -272,6 +274,18 @@ const Page = () => {
tags: filter.tags tags: filter.tags
}); });
useEffect(() => {
// if switching tabs in a folder path that doesn't exist in a separate env we navigate to the root
if (!data && isFetched) {
navigate({
search: (prev) => ({
...prev,
secretPath: "/"
})
});
}
}, [data, isFetched]);
const { const {
imports, imports,
folders, folders,
@@ -493,7 +507,8 @@ const Page = () => {
minWidth: 100, minWidth: 100,
maxWidth: tableRef.current maxWidth: tableRef.current
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely ? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
: 800 : 800,
ref: tableRef
}); });
useEffect(() => { useEffect(() => {
@@ -767,6 +782,7 @@ const Page = () => {
} }
/> />
<SecretV2MigrationSection /> <SecretV2MigrationSection />
<FolderBreadCrumbs secretPath={secretPath} />
<EnvironmentTabs secretPath={secretPath} /> <EnvironmentTabs secretPath={secretPath} />
{!isRollbackMode ? ( {!isRollbackMode ? (
<> <>
@@ -86,7 +86,6 @@ const DEFAULT_FILTER_STATE = {
[RowType.SecretRotation]: false [RowType.SecretRotation]: false
}; };
const TABLE_WIDTH_OFFSET = 17;
const COL_WIDTH_OFFSET = 220; const COL_WIDTH_OFFSET = 220;
export const CompareEnvironments = ({ secretPath }: Props) => { export const CompareEnvironments = ({ secretPath }: Props) => {
@@ -190,7 +189,6 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
totalSecretCount, totalSecretCount,
totalDynamicSecretCount, totalDynamicSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
totalImportCount,
totalCount = 0, totalCount = 0,
totalUniqueFoldersInPage, totalUniqueFoldersInPage,
totalUniqueSecretsInPage, totalUniqueSecretsInPage,
@@ -241,15 +239,15 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
); );
const [tableWidth, setTableWidth] = useState(0); const [tableWidth, setTableWidth] = useState(0);
const tableRef = useRef<HTMLTableElement>(null);
const tableRef = useRef<HTMLDivElement>(null);
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({ const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
initialWidth: 320, initialWidth: 320,
minWidth: 160, minWidth: 160,
maxWidth: tableRef.current maxWidth: tableRef.current
? tableRef.current.clientWidth - COL_WIDTH_OFFSET // ensure value column can't collapse completely ? tableRef.current.clientWidth - COL_WIDTH_OFFSET // ensure value column can't collapse completely
: 800 : 800,
ref: tableRef
}); });
const handleToggleRowType = useCallback( const handleToggleRowType = useCallback(
@@ -268,19 +266,21 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
const isTableFiltered = isFilteredByResources; const isTableFiltered = isFilteredByResources;
useEffect(() => { useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => { const element = tableRef.current;
// eslint-disable-next-line no-restricted-syntax if (!element) return;
for (const entry of entries) {
setTableWidth(entry.contentRect.width - TABLE_WIDTH_OFFSET);
}
});
if (tableRef.current) { const handleResize = () => {
resizeObserver.observe(tableRef.current); setTableWidth(element.clientWidth - 1);
} };
return () => resizeObserver.disconnect(); const resizeObserver = new ResizeObserver(handleResize);
}, []); resizeObserver.observe(element);
// eslint-disable-next-line consistent-return
return () => {
resizeObserver.disconnect();
};
}, [tableRef]);
return ( return (
// scott: this is reverse to fix z-indexing bug of dropdown with sticky table cols; couldn't resolve with flex-col // scott: this is reverse to fix z-indexing bug of dropdown with sticky table cols; couldn't resolve with flex-col
@@ -292,7 +292,6 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
dynamicSecretCount={totalDynamicSecretCount} dynamicSecretCount={totalDynamicSecretCount}
secretCount={totalSecretCount} secretCount={totalSecretCount}
folderCount={totalFolderCount} folderCount={totalFolderCount}
importCount={totalImportCount}
secretRotationCount={totalSecretRotationCount} secretRotationCount={totalSecretRotationCount}
/> />
} }
@@ -304,8 +303,9 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
onChangePerPage={handlePerPageChange} onChangePerPage={handlePerPageChange}
/> />
)} )}
<div ref={tableRef} className="thin-scrollbar flex flex-1 flex-col overflow-y-auto"> <div className="thin-scrollbar flex flex-1 flex-col overflow-y-auto">
<TableContainer <TableContainer
ref={tableRef}
className={twMerge( className={twMerge(
"mt-4 flex flex-1 flex-col border-mineshaft-500 bg-mineshaft-700", "mt-4 flex flex-1 flex-col border-mineshaft-500 bg-mineshaft-700",
!isTableEmpty && "rounded-b-none" !isTableEmpty && "rounded-b-none"
@@ -347,11 +347,11 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
<div className="pointer-events-none absolute -right-[0.02rem] top-[0.67rem] z-30"> <div className="pointer-events-none absolute -right-[0.02rem] top-[0.67rem] z-30">
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" /> <div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
</div> </div>
<div className="flex h-full items-center border-b-2 border-r border-mineshaft-500 bg-mineshaft-700 bg-clip-padding p-0 px-4 py-2.5"> <div className="flex h-full items-center border-b-2 border-r border-mineshaft-500 bg-mineshaft-700 bg-clip-padding p-0 px-4 py-2.5 text-sm normal-case">
Name Name
<IconButton <IconButton
variant="plain" variant="plain"
className="ml-2" className="ml-1 mt-[0.1rem]"
ariaLabel="sort" ariaLabel="sort"
onClick={() => onClick={() =>
setOrderDirection((prev) => setOrderDirection((prev) =>
@@ -362,6 +362,7 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
} }
> >
<FontAwesomeIcon <FontAwesomeIcon
className="h-3"
icon={orderDirection === "asc" ? faArrowDown : faArrowUp} icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
/> />
</IconButton> </IconButton>
@@ -380,7 +381,7 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
> >
<div <div
className={twMerge( className={twMerge(
"flex h-full w-full items-center justify-center gap-x-2 border-b-2 border-mineshaft-500 bg-mineshaft-700 p-0 px-4 py-3 text-center", "flex h-full w-full items-center justify-center gap-x-2 border-b-2 border-mineshaft-500 bg-mineshaft-700 p-0 px-4 py-2.5 text-center text-sm normal-case",
index < compareEnvironments.length - 1 && "border-r" index < compareEnvironments.length - 1 && "border-r"
)} )}
> >
@@ -388,7 +389,12 @@ export const CompareEnvironments = ({ secretPath }: Props) => {
{missingKeyCount > 0 && ( {missingKeyCount > 0 && (
<Tooltip <Tooltip
className="max-w-none lowercase" className="max-w-none lowercase"
content={`${missingKeyCount} secrets missing\n compared to other environments`} content={
<>
{missingKeyCount} secret{missingKeyCount > 1 ? "s" : ""} missing
compared to other environments on this page
</>
}
> >
<Badge <Badge
variant="primary" variant="primary"
@@ -2,7 +2,7 @@ import { faEye, faEyeSlash, faInfoCircle, faRotate } from "@fortawesome/free-sol
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Button, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2"; import { IconButton, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur"; import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
@@ -70,13 +70,13 @@ export const SecretRotationRow = ({
<Td <Td
colSpan={totalCols} colSpan={totalCols}
style={{ minWidth: tableWidth, maxWidth: tableWidth }} style={{ minWidth: tableWidth, maxWidth: tableWidth }}
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0" className="sticky left-0 bg-clip-padding px-0 py-0"
> >
<div <div
style={{ minWidth: tableWidth, maxWidth: tableWidth }} style={{ minWidth: tableWidth, maxWidth: tableWidth }}
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0" className="sticky left-0 bg-clip-padding px-0 py-0"
> >
<div className="flex !h-[40px] items-center justify-between gap-x-2 px-4"> <div className="flex !h-[40px] items-center justify-between gap-x-2 bg-mineshaft-800 px-4">
<div className="w-full"> <div className="w-full">
<div className="flex w-full flex-wrap items-center gap-x-2.5"> <div className="flex w-full flex-wrap items-center gap-x-2.5">
<span>{envName}</span> <span>{envName}</span>
@@ -95,17 +95,22 @@ export const SecretRotationRow = ({
)} )}
</div> </div>
</div> </div>
<Button <Tooltip
variant="plain" side="left"
colorSchema="secondary" content={isSecretVisible ? "Hide Values" : "Reveal Values"}
leftIcon={<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />}
onClick={() => setIsSecretVisible.toggle()}
> >
{isSecretVisible ? "Hide Values" : "Reveal Values"} <IconButton
</Button> variant="plain"
colorSchema="secondary"
ariaLabel={isSecretVisible ? "Hide Values" : "Reveal Values"}
onClick={() => setIsSecretVisible.toggle()}
>
<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />
</IconButton>
</Tooltip>
</div> </div>
<TableContainer className="rounded-none border-0"> <TableContainer className="rounded-none border-0">
<table className="secret-table w-full border-b-0"> <table className="secret-table w-full border-b-0 !bg-mineshaft-900">
<tbody className="!last:border-b-0 w-full border-t-2 border-mineshaft-600"> <tbody className="!last:border-b-0 w-full border-t-2 border-mineshaft-600">
{secrets.map((secret, index) => { {secrets.map((secret, index) => {
return ( return (
@@ -119,7 +124,7 @@ export const SecretRotationRow = ({
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
key={`rotation-secret-${secretRotation.id}-${index}`} key={`rotation-secret-${secretRotation.id}-${index}`}
> >
<tr className="hover:bg-mineshaft-700/70"> <tr className="hover:bg-mineshaft-800/50">
<td <td
style={{ style={{
width: colWidth width: colWidth
@@ -9,7 +9,7 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, TableContainer, Td, Tooltip, Tr } from "@app/components/v2"; import { IconButton, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useProjectPermission } from "@app/context"; import { useProjectPermission } from "@app/context";
import { import {
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
@@ -122,16 +122,16 @@ export const SecretRow = ({
<Td <Td
colSpan={totalCols} colSpan={totalCols}
style={{ minWidth: tableWidth, maxWidth: tableWidth }} style={{ minWidth: tableWidth, maxWidth: tableWidth }}
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0" className="sticky left-0 bg-clip-padding px-0 py-0"
> >
<div <div
style={{ minWidth: tableWidth, maxWidth: tableWidth }} style={{ minWidth: tableWidth, maxWidth: tableWidth }}
className="sticky left-0 bg-mineshaft-800 bg-clip-padding px-0 py-0" className="sticky left-0 bg-clip-padding px-0 py-0"
> >
<TableContainer className="rounded-none border-0"> <TableContainer className="rounded-none border-0">
<table className="secret-table"> <table className="secret-table bg-mineshaft-900">
<thead> <thead>
<tr className="h-10 border-b-2 border-mineshaft-600"> <tr className="h-10 border-b-2 border-mineshaft-600 bg-mineshaft-800">
<th <th
style={{ style={{
width: colWidth width: colWidth
@@ -143,15 +143,20 @@ export const SecretRow = ({
<th style={{ padding: "0.5rem 1rem" }} className="border-none"> <th style={{ padding: "0.5rem 1rem" }} className="border-none">
Value Value
</th> </th>
<div className="absolute right-3 top-[1px] ml-auto mr-1 mt-1 w-min"> <div className="absolute right-3 top-[4px] ml-auto mr-1 mt-1 w-min">
<Button <Tooltip
variant="plain" side="left"
colorSchema="secondary" content={isSecretVisible ? "Hide Values" : "Reveal Values"}
leftIcon={<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />}
onClick={() => setIsSecretVisible.toggle()}
> >
{isSecretVisible ? "Hide Values" : "Reveal Values"} <IconButton
</Button> variant="plain"
colorSchema="secondary"
ariaLabel={isSecretVisible ? "Hide Values" : "Reveal Values"}
onClick={() => setIsSecretVisible.toggle()}
>
<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />
</IconButton>
</Tooltip>
</div> </div>
</tr> </tr>
</thead> </thead>
@@ -150,7 +150,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => {
<DropdownMenuContent <DropdownMenuContent
className="thin-scrollbar max-h-[70vh] overflow-y-auto" className="thin-scrollbar max-h-[70vh] overflow-y-auto"
sideOffset={2} sideOffset={2}
align="end" align="center"
> >
<DropdownMenuLabel>Environments</DropdownMenuLabel> <DropdownMenuLabel>Environments</DropdownMenuLabel>
<div className="thin-scrollbar max-h-[40vh] overflow-auto"> <div className="thin-scrollbar max-h-[40vh] overflow-auto">
@@ -0,0 +1,52 @@
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
type Props = {
secretPath: string;
};
export const FolderBreadCrumbs = ({ secretPath = "/" }: Props) => {
const navigate = useNavigate({
from: "/projects/secret-management/$projectId/secrets/$envSlug"
});
const onFolderCrumbClick = (index: number) => {
const newSecPath = `/${secretPath.split("/").filter(Boolean).slice(0, index).join("/")}`;
if (secretPath === newSecPath) return;
navigate({
search: (prev) => ({ ...prev, secretPath: newSecPath })
});
};
return (
<div className="mb-3 flex flex-wrap items-center gap-x-2 gap-y-3">
<div
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 py-1 pl-5 pr-2 text-sm hover:bg-mineshaft-600"
onClick={() => onFolderCrumbClick(0)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
<FontAwesomeIcon icon={faFolderOpen} className="text-primary-700" />
</div>
{(secretPath || "")
.split("/")
.filter(Boolean)
.map((path, index, arr) => (
<div
key={`secret-path-${index + 1}`}
className={`breadcrumb relative z-20 ${
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
onClick={() => onFolderCrumbClick(index + 1)}
onKeyDown={() => null}
role="button"
tabIndex={0}
>
{path}
</div>
))}
</div>
);
};
@@ -0,0 +1 @@
export { FolderBreadCrumbs } from "./FolderBreadCrumbs";