mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 03:26:46 +00:00
feature: add col resize to secret dashboard env view
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
|||||||
+7
-2
@@ -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}
|
||||||
|
|||||||
+4
-1
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user