diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index cc180ffb6..b8ca27dc1 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -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 { Skeleton } from "../Skeleton"; @@ -9,22 +9,20 @@ export type TableContainerProps = { className?: string; } & DetailedHTMLProps, HTMLDivElement>; -export const TableContainer = ({ - children, - className, - isRounded = true, - ...props -}: TableContainerProps): JSX.Element => ( -
- {children} -
+export const TableContainer = forwardRef( + ({ children, className, isRounded = true, ...props }, ref): JSX.Element => ( +
+ {children} +
+ ) ); // main parent table diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index e748e3e5b..fde3a5ee3 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -1,5 +1,6 @@ import { useCallback } from "react"; import { useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { AxiosError } from "axios"; import { apiRequest } from "@app/config/request"; import { @@ -273,6 +274,12 @@ export const useGetProjectSecretsDetails = ( ...options, // wait for all values to be available 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({ secretPath, search, diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 78c56394f..fbd5107cd 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -72,7 +72,7 @@ export type DashboardProjectSecretsOverview = Omit< DashboardProjectSecretsOverviewResponse, "secrets" | "secretRotations" > & { - secrets?: SecretV3RawSanitized[]; + secrets?: (SecretV3RawSanitized & { sourceEnv?: string })[]; secretRotations?: (TSecretRotationV2 & { secrets: (SecretV3RawSanitized | null)[]; })[]; diff --git a/frontend/src/hooks/useResizableColWidth.tsx b/frontend/src/hooks/useResizableColWidth.tsx index f2ad80625..6af269217 100644 --- a/frontend/src/hooks/useResizableColWidth.tsx +++ b/frontend/src/hooks/useResizableColWidth.tsx @@ -1,12 +1,13 @@ -import { MouseEvent, useCallback, useEffect, useRef, useState } from "react"; +import { MouseEvent, RefObject, useCallback, useEffect, useRef, useState } from "react"; type Params = { minWidth: number; maxWidth: number; initialWidth: number; + ref: RefObject; }; -export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => { +export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth, ref }: Params) => { const [colWidth, setColWidth] = useState(initialWidth); const [isResizing, setIsResizing] = useState(false); const startX = useRef(0); @@ -63,6 +64,28 @@ export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Param }; }, [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 { colWidth, handleMouseDown, diff --git a/frontend/src/hooks/utils/secrets-overview.tsx b/frontend/src/hooks/utils/secrets-overview.tsx index e136df3f2..d77b41f5f 100644 --- a/frontend/src/hooks/utils/secrets-overview.tsx +++ b/frontend/src/hooks/utils/secrets-overview.tsx @@ -130,7 +130,11 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr const getEnvSecretKeyCount = useCallback( (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] ); diff --git a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx index 8cc9f4b4a..a11107a41 100644 --- a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx +++ b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx @@ -11,7 +11,7 @@ import { faVault } from "@fortawesome/free-solid-svg-icons"; 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 { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; @@ -31,6 +31,7 @@ export const SecretManagerLayout = () => { const { t } = useTranslation(); const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; + const location = useLocation(); const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId @@ -84,7 +85,14 @@ export const SecretManagerLayout = () => { }} > {({ isActive }) => ( - +
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 1307fcee5..16986058e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -66,6 +66,7 @@ import { CommitForm } from "./components/CommitForm"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { DynamicSecretListView } from "./components/DynamicSecretListView"; import { EnvironmentTabs } from "./components/EnvironmentTabs"; +import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; import { FolderListView } from "./components/FolderListView"; import { PitDrawer } from "./components/PitDrawer"; import { SecretDropzone } from "./components/SecretDropzone"; @@ -107,7 +108,7 @@ const Page = () => { const { permission } = useProjectPermission(); const { mutateAsync: createCommit } = useCreateCommit(); - const tableRef = useRef(null); + const tableRef = useRef(null); const [isVisible, setIsVisible] = useState(false); const { isBatchMode, pendingChanges } = useBatchMode(); @@ -251,7 +252,8 @@ const Page = () => { const { data, isPending: isDetailsLoading, - isFetching: isDetailsFetching + isFetching: isDetailsFetching, + isFetched } = useGetProjectSecretsDetails({ environment, projectId: workspaceId, @@ -272,6 +274,18 @@ const Page = () => { 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 { imports, folders, @@ -493,7 +507,8 @@ const Page = () => { minWidth: 100, maxWidth: tableRef.current ? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely - : 800 + : 800, + ref: tableRef }); useEffect(() => { @@ -767,6 +782,7 @@ const Page = () => { } /> + {!isRollbackMode ? ( <> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CompareEnvironments/CompareEnvironments.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CompareEnvironments/CompareEnvironments.tsx index b2797fd39..c3570bf5a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/CompareEnvironments/CompareEnvironments.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/CompareEnvironments/CompareEnvironments.tsx @@ -86,7 +86,6 @@ const DEFAULT_FILTER_STATE = { [RowType.SecretRotation]: false }; -const TABLE_WIDTH_OFFSET = 17; const COL_WIDTH_OFFSET = 220; export const CompareEnvironments = ({ secretPath }: Props) => { @@ -190,7 +189,6 @@ export const CompareEnvironments = ({ secretPath }: Props) => { totalSecretCount, totalDynamicSecretCount, totalSecretRotationCount, - totalImportCount, totalCount = 0, totalUniqueFoldersInPage, totalUniqueSecretsInPage, @@ -241,15 +239,15 @@ export const CompareEnvironments = ({ secretPath }: Props) => { ); const [tableWidth, setTableWidth] = useState(0); - - const tableRef = useRef(null); + const tableRef = useRef(null); const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({ initialWidth: 320, minWidth: 160, maxWidth: tableRef.current ? tableRef.current.clientWidth - COL_WIDTH_OFFSET // ensure value column can't collapse completely - : 800 + : 800, + ref: tableRef }); const handleToggleRowType = useCallback( @@ -268,19 +266,21 @@ export const CompareEnvironments = ({ secretPath }: Props) => { const isTableFiltered = isFilteredByResources; useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => { - // eslint-disable-next-line no-restricted-syntax - for (const entry of entries) { - setTableWidth(entry.contentRect.width - TABLE_WIDTH_OFFSET); - } - }); + const element = tableRef.current; + if (!element) return; - if (tableRef.current) { - resizeObserver.observe(tableRef.current); - } + const handleResize = () => { + 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 ( // 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} secretCount={totalSecretCount} folderCount={totalFolderCount} - importCount={totalImportCount} secretRotationCount={totalSecretRotationCount} /> } @@ -304,8 +303,9 @@ export const CompareEnvironments = ({ secretPath }: Props) => { onChangePerPage={handlePerPageChange} /> )} -
+
{
-
+
Name setOrderDirection((prev) => @@ -362,6 +362,7 @@ export const CompareEnvironments = ({ secretPath }: Props) => { } > @@ -380,7 +381,7 @@ export const CompareEnvironments = ({ secretPath }: Props) => { >
@@ -388,7 +389,12 @@ export const CompareEnvironments = ({ secretPath }: Props) => { {missingKeyCount > 0 && ( + {missingKeyCount} secret{missingKeyCount > 1 ? "s" : ""} missing + compared to other environments on this page + + } >
-
+
{envName} @@ -95,17 +95,22 @@ export const SecretRotationRow = ({ )}
- + setIsSecretVisible.toggle()} + > + + +
- +
{secrets.map((secret, index) => { return ( @@ -119,7 +124,7 @@ export const SecretRotationRow = ({ // eslint-disable-next-line react/no-array-index-key key={`rotation-secret-${secretRotation.id}-${index}`} > - +
- +
- + -
- + setIsSecretVisible.toggle()} + > + + +
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx index c5ff81c3d..66fd3a878 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx @@ -150,7 +150,7 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { Environments
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx new file mode 100644 index 000000000..a2169fa5a --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx @@ -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 ( +
+
onFolderCrumbClick(0)} + onKeyDown={() => null} + role="button" + tabIndex={0} + > + +
+ {(secretPath || "") + .split("/") + .filter(Boolean) + .map((path, index, arr) => ( +
onFolderCrumbClick(index + 1)} + onKeyDown={() => null} + role="button" + tabIndex={0} + > + {path} +
+ ))} +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/index.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/index.tsx new file mode 100644 index 000000000..8224cdb25 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/index.tsx @@ -0,0 +1 @@ +export { FolderBreadCrumbs } from "./FolderBreadCrumbs";
Value