feature: add col resize to secret dashboard env view

This commit is contained in:
Scott Wilson
2025-07-24 10:18:57 -07:00
parent 9bc24487b3
commit 83e59ae160
5 changed files with 124 additions and 18 deletions
@@ -0,0 +1,71 @@
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
type Params = {
minWidth: number;
maxWidth: number;
initialWidth: number;
};
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => {
const [colWidth, setColWidth] = useState(initialWidth);
const [isResizing, setIsResizing] = useState(false);
const startX = useRef(0);
const startWidth = useRef(0);
const handleMouseDown = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
startX.current = e.clientX;
startWidth.current = colWidth;
},
[colWidth]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isResizing) return;
const deltaX = e.clientX - startX.current;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + deltaX));
setColWidth(newWidth);
},
[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 = "ew-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 {
colWidth,
handleMouseDown,
isResizing
};
};
@@ -1,5 +1,5 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
@@ -53,6 +53,7 @@ import { PendingAction } from "@app/hooks/api/secretFolders/types";
import { useCreateCommit } from "@app/hooks/api/secrets/mutations"; import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
import { SecretV3RawSanitized } from "@app/hooks/api/types"; import { SecretV3RawSanitized } from "@app/hooks/api/types";
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies"; import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal"; import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView"; import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
@@ -104,6 +105,8 @@ const Page = () => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { mutateAsync: createCommit } = useCreateCommit(); const { mutateAsync: createCommit } = useCreateCommit();
const tableRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const { isBatchMode, pendingChanges } = useBatchMode(); const { isBatchMode, pendingChanges } = useBatchMode();
const { loadPendingChanges, setExistingKeys } = useBatchModeActions(); const { loadPendingChanges, setExistingKeys } = useBatchModeActions();
@@ -483,6 +486,14 @@ const Page = () => {
handlePopUpClose("snapshots"); handlePopUpClose("snapshots");
}, []); }, []);
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
initialWidth: 320,
minWidth: 100,
maxWidth: tableRef.current
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
: 800
});
useEffect(() => { useEffect(() => {
// restore filters for path if set // restore filters for path if set
const restore = filterHistory.get(secretPath); const restore = filterHistory.get(secretPath);
@@ -787,6 +798,7 @@ const Page = () => {
} }
/> />
<div <div
ref={tableRef}
className={twMerge( className={twMerge(
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300", "thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
isNotEmpty && "rounded-b-none" isNotEmpty && "rounded-b-none"
@@ -820,20 +832,34 @@ const Page = () => {
/> />
</div> </div>
</Tooltip> </Tooltip>
<div <div className="relative">
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4" <div
role="button" tabIndex={-1}
tabIndex={0} role="button"
onClick={handleSortToggle} className={`absolute -right-[0.05rem] z-40 h-full w-0.5 cursor-ew-resize hover:bg-blue-400/20 ${
onKeyDown={(evt) => { isResizing ? "bg-blue-400/75" : "bg-transparent"
if (evt.key === "Enter") handleSortToggle(); }`}
}} onMouseDown={handleMouseDown}
>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/> />
<div className="pointer-events-none absolute -right-[0.04rem] top-2 z-30">
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
</div>
<div
className="flex flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
style={{ width: colWidth }}
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
Key
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
</div> </div>
<div className="flex-grow px-4 py-2">Value</div> <div className="flex-grow px-4 py-2">Value</div>
</div> </div>
@@ -927,6 +953,7 @@ const Page = () => {
)} )}
{canReadSecret && Boolean(mergedSecrets?.length) && ( {canReadSecret && Boolean(mergedSecrets?.length) && (
<SecretListView <SecretListView
colWidth={colWidth}
secrets={mergedSecrets} secrets={mergedSecrets}
tags={tags} tags={tags}
isVisible={isVisible} isVisible={isVisible}
@@ -235,7 +235,7 @@ export const FolderListView = ({
)} )}
</div> </div>
{isPending ? ( {isPending ? (
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3"> <div className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-3 py-3">
<IconButton <IconButton
ariaLabel="edit-folder" ariaLabel="edit-folder"
variant="plain" variant="plain"
@@ -90,6 +90,7 @@ type Props = {
}[]; }[];
isPending?: boolean; isPending?: boolean;
pendingAction?: PendingAction; pendingAction?: PendingAction;
colWidth: number;
}; };
export const SecretItem = memo( export const SecretItem = memo(
@@ -108,7 +109,8 @@ export const SecretItem = memo(
handleSecretShare, handleSecretShare,
importedBy, importedBy,
isPending, isPending,
pendingAction pendingAction,
colWidth
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret" "editSecret"
@@ -383,7 +385,10 @@ export const SecretItem = memo(
</> </>
)} )}
</div> </div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2"> <div
className="flex h-11 flex-shrink-0 items-center px-4 py-2"
style={{ width: colWidth }}
>
<Controller <Controller
name="key" name="key"
control={control} control={control}
@@ -51,6 +51,7 @@ type Props = {
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
colWidth: number;
}; };
export const SecretListView = ({ export const SecretListView = ({
@@ -62,7 +63,8 @@ export const SecretListView = ({
isVisible, isVisible,
isProtectedBranch = false, isProtectedBranch = false,
usedBySecretSyncs, usedBySecretSyncs,
importedBy importedBy,
colWidth
}: Props) => { }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -554,6 +556,7 @@ export const SecretListView = ({
))} ))}
{secrets.map((secret) => ( {secrets.map((secret) => (
<SecretItem <SecretItem
colWidth={colWidth}
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
tags={wsTags} tags={wsTags}