diff --git a/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx new file mode 100644 index 000000000..77ba59c33 --- /dev/null +++ b/frontend/src/components/v2/HeaderResizer/HeaderResizer.tsx @@ -0,0 +1,36 @@ +import { MouseEventHandler } from "react"; + +export const HeaderResizer = ({ + onMouseDown, + isActive, + scrollOffset, + heightOffset +}: { + onMouseDown: MouseEventHandler; + isActive: boolean; + scrollOffset: number; + heightOffset: number; +}) => { + 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 caa4a76f8..b224c1b6d 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -5,6 +5,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 78582db2d..255196f86 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"; @@ -58,6 +58,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, @@ -73,7 +74,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, @@ -97,6 +105,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"; @@ -142,6 +151,8 @@ const DEFAULT_FILTER_STATE = { [RowType.SecretRotation]: true }; +const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120; + export const OverviewPage = () => { const { t } = useTranslation(); @@ -159,7 +170,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; @@ -861,6 +872,22 @@ export const OverviewPage = () => { ); }, [importedByEnvs, selectedEntries, selectedKeysCount]); + const storedHeight = Number.parseInt( + localStorage.getItem("overview-header-height") ?? DEFAULT_COLLAPSED_HEADER_HEIGHT.toString(), + 10 + ); + const { headerHeight, handleMouseDown, isResizing } = useResizableHeaderHeight({ + initialHeight: Number.isNaN(storedHeight) ? DEFAULT_COLLAPSED_HEADER_HEIGHT : storedHeight, + minHeight: DEFAULT_COLLAPSED_HEADER_HEIGHT, + maxHeight: 288 + }); + + const debouncedHeaderHeight = useDebounce(headerHeight); + + useEffect(() => { + localStorage.setItem("overview-header-height", debouncedHeaderHeight.toString()); + }, [debouncedHeaderHeight]); + if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) { return (
@@ -892,7 +919,7 @@ export const OverviewPage = () => { -
+
{ - + {/*