From 1a7b810badad9d0d331bf085299ff79ec7717f41 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 1 Jul 2025 17:22:49 -0700 Subject: [PATCH 1/4] improvement: allow users to expand collapsed environment view header --- .../v2/HeaderResizer/HeaderResizer.tsx | 28 +++++++ .../src/components/v2/HeaderResizer/index.tsx | 0 frontend/src/components/v2/Table/Table.tsx | 12 ++- frontend/src/hooks/index.ts | 1 + .../src/hooks/useResizableHeaderHeight.tsx | 71 ++++++++++++++++ .../OverviewPage/OverviewPage.tsx | 83 +++++++++++++++---- .../SecretOverviewTableRow.tsx | 6 +- 7 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx create mode 100644 frontend/src/components/v2/HeaderResizer/index.tsx create mode 100644 frontend/src/hooks/useResizableHeaderHeight.tsx diff --git a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx new file mode 100644 index 000000000..1f50133e9 --- /dev/null +++ b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx @@ -0,0 +1,28 @@ +import { MouseEventHandler } from "react"; + +export const HeaderResizer = ({ + onMouseDown, + isActive +}: { + onMouseDown: MouseEventHandler; + isActive: boolean; +}) => { + return ( + <> +
+
+
+
+ + ); +}; diff --git a/frontend/src/components/v2/HeaderResizer/index.tsx b/frontend/src/components/v2/HeaderResizer/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index 1d5a9c394..cc180ffb6 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -45,10 +45,14 @@ export const Table = ({ children, className }: TableProps): JSX.Element => ( export type THeadProps = { children: ReactNode; className?: string; + style?: React.CSSProperties; }; -export const THead = ({ children, className }: THeadProps): JSX.Element => ( - +export const THead = ({ children, className, style }: THeadProps): JSX.Element => ( + {children} ); @@ -96,14 +100,16 @@ export const Tr = ({ export type ThProps = { children?: ReactNode; className?: string; + style?: React.CSSProperties; }; -export const Th = ({ children, className }: ThProps): JSX.Element => ( +export const Th = ({ children, className, style }: ThProps): JSX.Element => ( {children} diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 9e5eff713..0ebdb9843 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -4,6 +4,7 @@ export { usePagination } from "./usePagination"; export { usePersistentState } from "./usePersistentState"; export { usePopUp } from "./usePopUp"; export { useResetPageHelper } from "./useResetPageHelper"; +export * from "./useResizableHeaderHeight"; export { useSyntaxHighlight } from "./useSyntaxHighlight"; export { useTimedReset } from "./useTimedReset"; export { useToggle } from "./useToggle"; diff --git a/frontend/src/hooks/useResizableHeaderHeight.tsx b/frontend/src/hooks/useResizableHeaderHeight.tsx new file mode 100644 index 000000000..9624e9052 --- /dev/null +++ b/frontend/src/hooks/useResizableHeaderHeight.tsx @@ -0,0 +1,71 @@ +import { MouseEvent, useCallback, useEffect, useRef, useState } from "react"; + +type Params = { + minHeight: number; + maxHeight: number; + initialHeight: number; +}; + +export const useResizableHeaderHeight = ({ minHeight, maxHeight, initialHeight }: Params) => { + const [headerHeight, setHeaderHeight] = useState(initialHeight); + const [isResizing, setIsResizing] = useState(false); + const startY = useRef(0); + const startHeight = useRef(0); + + const handleMouseDown = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsResizing(true); + startY.current = e.clientY; + startHeight.current = headerHeight; + }, + [headerHeight] + ); + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (!isResizing) return; + + const deltaY = e.clientY - startY.current; + const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight.current + deltaY)); + + setHeaderHeight(newHeight); + }, + [isResizing] + ); + + const handleMouseUp = useCallback(() => { + setIsResizing(false); + }, []); + + useEffect(() => { + if (isResizing) { + document.addEventListener( + "mousemove", + // @ts-expect-error native discrepancy + handleMouseMove + ); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.cursor = "ns-resize"; + document.body.style.userSelect = "none"; + } + + return () => { + document.removeEventListener( + "mousemove", + // @ts-expect-error native discrepancy + handleMouseMove + ); + document.removeEventListener("mouseup", handleMouseUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + }, [isResizing, handleMouseMove, handleMouseUp]); + + return { + headerHeight, + handleMouseDown, + isResizing + }; +}; diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 045ee58cd..eb160236c 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -57,6 +57,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { HeaderResizer } from "@app/components/v2/HeaderResizer/HeaderResizer"; import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, @@ -72,7 +73,14 @@ import { PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; -import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks"; +import { + useDebounce, + usePagination, + usePopUp, + useResetPageHelper, + useResizableHeaderHeight, + useToggle +} from "@app/hooks"; import { useCreateFolder, useCreateSecretV3, @@ -860,6 +868,19 @@ export const OverviewPage = () => { ); }, [importedByEnvs, selectedEntries, selectedKeysCount]); + const storedHeight = Number.parseInt(localStorage.getItem("overview-header-height") ?? "96"); + const { headerHeight, handleMouseDown, isResizing } = useResizableHeaderHeight({ + initialHeight: Number.isNaN(storedHeight) ? 96 : storedHeight, + minHeight: 96, + maxHeight: 192 + }); + + const debouncedHeaderHeight = useDebounce(headerHeight); + + useEffect(() => { + localStorage.setItem("overview-header-height", debouncedHeaderHeight.toString()); + }, [debouncedHeaderHeight]); + if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) { return (
@@ -1188,15 +1209,17 @@ export const OverviewPage = () => { className="thin-scrollbar rounded-b-none" > - + + {collapseEnvironments && ( + + )} {canViewOverviewPage && isOverviewLoading && ( diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index c4f6a7c76..63955e381 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -1,8 +1,8 @@ import { subject } from "@casl/ability"; +import { faCircle } from "@fortawesome/free-regular-svg-icons"; import { faAngleDown, faCheck, - faCircle, faCodeBranch, faEye, faEyeSlash, @@ -148,7 +148,7 @@ export const SecretOverviewTableRow = ({ "border-r border-mineshaft-600 px-0 py-3 group-hover:bg-mineshaft-700", isFormExpanded && "border-t-2 border-mineshaft-500", (isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "", - isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "", + isSecretPresent && isSecretEmpty && !isSecretImported ? "text-mineshaft-400" : "", !isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : "" )} > @@ -174,7 +174,7 @@ export const SecretOverviewTableRow = ({ )} {isSecretEmpty && ( - + )} From aa560b81990ba56b1afbb1b7d35ab6c42184ccba Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 2 Jul 2025 10:57:14 -0700 Subject: [PATCH 2/4] improvement: address feedback --- .../v2/HeaderResizer/HeaderResizer.tsx | 9 ++- .../OverviewPage/OverviewPage.tsx | 80 ++++++++++--------- .../OverviewPage/components/utils/index.ts | 30 +++++++ 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx index 1f50133e9..694ac10a0 100644 --- a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx +++ b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx @@ -2,10 +2,12 @@ import { MouseEventHandler } from "react"; export const HeaderResizer = ({ onMouseDown, - isActive + isActive, + scrollOffset }: { onMouseDown: MouseEventHandler; isActive: boolean; + scrollOffset: number; }) => { return ( <> @@ -20,7 +22,10 @@ export const HeaderResizer = ({ transform: "translateY(50%)" }} /> -
+
diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index eb160236c..0c673c491 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -104,6 +104,7 @@ import { useSecretRotationOverview } from "@app/hooks/utils"; import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow"; +import { getHeaderStyle } from "@app/pages/secret-manager/OverviewPage/components/utils"; import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm"; import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm"; @@ -149,6 +150,8 @@ const DEFAULT_FILTER_STATE = { [RowType.SecretRotation]: true }; +const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120; + export const OverviewPage = () => { const { t } = useTranslation(); @@ -868,11 +871,14 @@ export const OverviewPage = () => { ); }, [importedByEnvs, selectedEntries, selectedKeysCount]); - const storedHeight = Number.parseInt(localStorage.getItem("overview-header-height") ?? "96"); + const storedHeight = Number.parseInt( + localStorage.getItem("overview-header-height") ?? DEFAULT_COLLAPSED_HEADER_HEIGHT.toString(), + 10 + ); const { headerHeight, handleMouseDown, isResizing } = useResizableHeaderHeight({ - initialHeight: Number.isNaN(storedHeight) ? 96 : storedHeight, - minHeight: 96, - maxHeight: 192 + initialHeight: Number.isNaN(storedHeight) ? DEFAULT_COLLAPSED_HEADER_HEIGHT : storedHeight, + minHeight: DEFAULT_COLLAPSED_HEADER_HEIGHT, + maxHeight: 288 }); const debouncedHeaderHeight = useDebounce(headerHeight); @@ -986,7 +992,10 @@ export const OverviewPage = () => { - + {/*
{collapseEnvironments && ( - + )} @@ -1554,9 +1562,9 @@ export const OverviewPage = () => { style={{ height: "45px" }} /> - {visibleEnvs?.map(({ name, slug }) => ( + {visibleEnvs?.map(({ name, slug }, i) => (
{ const importedSecKeyCount = getEnvImportedSecretKeyCount(slug); const missingKeyCount = secKeys.length - envSecKeyCount - importedSecKeyCount; + const isLast = index === visibleEnvs.length - 1; + return (
{ className={twMerge( "border-b border-mineshaft-600", collapseEnvironments - ? "relative h-24 w-[2.9rem]" + ? "relative min-w-[2.9rem]" : "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5", - collapseEnvironments && - index === visibleEnvs.length - 1 && - "overflow-clip" + collapseEnvironments && isLast && "overflow-clip" )} + style={{ height: collapseEnvironments ? headerHeight : undefined }} >
{ className={twMerge( "border-b border-mineshaft-600", collapseEnvironments - ? "relative min-w-[2.9rem]" + ? "relative" : "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5", collapseEnvironments && isLast && "overflow-clip" )} - style={{ height: collapseEnvironments ? headerHeight : undefined }} + style={{ + height: collapseEnvironments ? headerHeight : undefined, + minWidth: collapseEnvironments ? "2.9rem" : undefined, + width: collapseEnvironments && isLast ? headerHeight * 0.3 : undefined + }} >
{ className={twMerge( "duration-100 hover:text-mineshaft-100", collapseEnvironments - ? "absolute -rotate-[72.25deg] text-left !text-[12px] font-normal" + ? "absolute -rotate-[72.75deg] text-left text-sm font-normal" : "flex items-center text-center text-sm font-medium" )} - style={ - // eslint-disable-next-line no-nested-ternary - collapseEnvironments - ? isLast - ? { - bottom: 42, - left: 0, - width: 86 - } - : { - width: headerHeight * 0.9, - bottom: headerHeight * 0.45, - left: 24 - (headerHeight * 0.9) / 3.05 - } - : undefined - } + style={getHeaderStyle({ + collapseEnvironments, + isLast, + headerHeight + })} onClick={() => handleExploreEnvClick(slug)} >

{name}

@@ -1393,7 +1397,11 @@ export const OverviewPage = () => { })}
({ width: "calc(100vw - 355px)", // 350px accounts for sidebar and margin maxWidth: "1270px" // largest width of table on ultra-wide }); + +type GetHeaderStyleParams = { + collapseEnvironments: boolean; + isLast: boolean; + headerHeight: number; +}; + +export const getHeaderStyle = ({ + collapseEnvironments, + isLast, + headerHeight +}: GetHeaderStyleParams) => { + if (!collapseEnvironments) return undefined; + + // scott: this is mostly trial/error to keep centered with skew + if (isLast) { + return { + width: headerHeight * 0.42, + bottom: headerHeight * 0.222, + left: 2 + }; + } + + return { + // scott: this is mostly trial/error to keep centered with skew + width: headerHeight * 0.9, + bottom: headerHeight * 0.45, + left: 24 - (headerHeight * 0.9) / 2.985 + }; +}; From 833963af0c61d09ad2b3e65dda1196cee6363c1e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 2 Jul 2025 11:01:51 -0700 Subject: [PATCH 3/4] improvement: remove additional relative and adjust handle position --- frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx | 2 +- frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx index 694ac10a0..59ccc384a 100644 --- a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx +++ b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx @@ -23,7 +23,7 @@ export const HeaderResizer = ({ }} />
diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 0c673c491..5cf3b4890 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -1212,7 +1212,7 @@ export const OverviewPage = () => { secretsToDeleteKeys={secretsToDeleteKeys} usedBySecretSyncs={usedBySecretSyncs} /> -
+
setScrollOffset(e.currentTarget.scrollLeft)} className="thin-scrollbar rounded-b-none" From 3daae6f965bb75dacad74054c3e2d7f6f7b7023e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 2 Jul 2025 11:10:37 -0700 Subject: [PATCH 4/4] improvement: adjust header drag to use table container for positioning --- .../components/v2/HeaderResizer/HeaderResizer.tsx | 13 ++++++++----- .../secret-manager/OverviewPage/OverviewPage.tsx | 14 ++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx index 59ccc384a..77ba59c33 100644 --- a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx +++ b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx @@ -3,28 +3,31 @@ import { MouseEventHandler } from "react"; export const HeaderResizer = ({ onMouseDown, isActive, - scrollOffset + scrollOffset, + heightOffset }: { onMouseDown: MouseEventHandler; isActive: boolean; scrollOffset: number; + heightOffset: number; }) => { return ( <>
diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 5cf3b4890..086b31a31 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { subject } from "@casl/ability"; @@ -169,7 +169,7 @@ export const OverviewPage = () => { const [scrollOffset, setScrollOffset] = useState(0); const [debouncedScrollOffset] = useDebounce(scrollOffset); const { permission } = useProjectPermission(); - + const tableRef = useRef(null); const { currentWorkspace } = useWorkspace(); const isProjectV3 = currentWorkspace?.version === ProjectVersion.V3; const workspaceId = currentWorkspace?.id as string; @@ -925,7 +925,7 @@ export const OverviewPage = () => { -
+
{ secretsToDeleteKeys={secretsToDeleteKeys} usedBySecretSyncs={usedBySecretSyncs} /> -
+
setScrollOffset(e.currentTarget.scrollLeft)} className="thin-scrollbar rounded-b-none" > - + { onMouseDown={handleMouseDown} isActive={isResizing} scrollOffset={scrollOffset} + heightOffset={(tableRef.current?.clientTop ?? 0) + headerHeight - 2.5} /> )}