mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 10:28:50 +00:00
feat: select all on page for secrets tables and fix multipage select behavior for actions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -25,6 +25,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -67,7 +68,7 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||
import { SecretType, TSecretFolder } from "@app/hooks/api/types";
|
||||
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
||||
import { SecretOverviewDynamicSecretRow } from "@app/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow";
|
||||
@@ -133,8 +134,9 @@ export const SecretOverviewPage = () => {
|
||||
>(new Map());
|
||||
|
||||
const [selectedEntries, setSelectedEntries] = useState<{
|
||||
[EntryType.FOLDER]: Record<string, boolean>;
|
||||
[EntryType.SECRET]: Record<string, boolean>;
|
||||
// selectedEntries[name/key][envSlug][resource]
|
||||
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
|
||||
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
|
||||
}>({
|
||||
[EntryType.FOLDER]: {},
|
||||
[EntryType.SECRET]: {}
|
||||
@@ -152,23 +154,6 @@ export const SecretOverviewPage = () => {
|
||||
orderBy
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
|
||||
|
||||
const toggleSelectedEntry = useCallback(
|
||||
(type: EntryType, key: string) => {
|
||||
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
||||
const newChecks = { ...selectedEntries };
|
||||
|
||||
// remove selection if its present else add it
|
||||
if (isChecked) {
|
||||
delete newChecks[type][key];
|
||||
} else {
|
||||
newChecks[type][key] = true;
|
||||
}
|
||||
|
||||
setSelectedEntries(newChecks);
|
||||
},
|
||||
[selectedEntries]
|
||||
);
|
||||
|
||||
const resetSelectedEntries = useCallback(() => {
|
||||
setSelectedEntries({
|
||||
[EntryType.FOLDER]: {},
|
||||
@@ -551,6 +536,83 @@ export const SecretOverviewPage = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
const allRowsSelectedOnPage = useMemo(() => {
|
||||
if (
|
||||
(!secrets?.length ||
|
||||
secrets?.every((secret) => selectedEntries[EntryType.SECRET][secret.key])) &&
|
||||
(!folders?.length ||
|
||||
folders?.every((folder) => selectedEntries[EntryType.FOLDER][folder.name]))
|
||||
)
|
||||
return { isChecked: true, isIndeterminate: false };
|
||||
|
||||
if (
|
||||
secrets?.some((secret) => selectedEntries[EntryType.SECRET][secret.key]) ||
|
||||
folders?.some((folder) => selectedEntries[EntryType.FOLDER][folder.name])
|
||||
)
|
||||
return { isChecked: true, isIndeterminate: true };
|
||||
|
||||
return { isChecked: false, isIndeterminate: false };
|
||||
}, [selectedEntries, secrets, folders]);
|
||||
|
||||
const toggleSelectedEntry = useCallback(
|
||||
(type: EntryType, key: string) => {
|
||||
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
||||
const newChecks = { ...selectedEntries };
|
||||
|
||||
// remove selection if its present else add it
|
||||
if (isChecked) {
|
||||
delete newChecks[type][key];
|
||||
} else {
|
||||
newChecks[type][key] = {};
|
||||
userAvailableEnvs.forEach((env) => {
|
||||
const resource =
|
||||
type === EntryType.SECRET
|
||||
? getSecretByKey(env.slug, key)
|
||||
: getFolderByNameAndEnv(key, env.slug);
|
||||
|
||||
if (resource) newChecks[type][key][env.slug] = resource;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedEntries(newChecks);
|
||||
},
|
||||
[selectedEntries, getFolderByNameAndEnv, getSecretByKey]
|
||||
);
|
||||
|
||||
const toggleSelectAllRows = () => {
|
||||
const newChecks = { ...selectedEntries };
|
||||
|
||||
userAvailableEnvs.forEach((env) => {
|
||||
secrets?.forEach((secret) => {
|
||||
if (allRowsSelectedOnPage.isChecked) {
|
||||
delete newChecks[EntryType.SECRET][secret.key];
|
||||
} else {
|
||||
if (!newChecks[EntryType.SECRET][secret.key])
|
||||
newChecks[EntryType.SECRET][secret.key] = {};
|
||||
|
||||
const resource = getSecretByKey(env.slug, secret.key);
|
||||
|
||||
if (resource) newChecks[EntryType.SECRET][secret.key][env.slug] = resource;
|
||||
}
|
||||
});
|
||||
|
||||
folders?.forEach((folder) => {
|
||||
if (allRowsSelectedOnPage.isChecked) {
|
||||
delete newChecks[EntryType.FOLDER][folder.name];
|
||||
} else {
|
||||
if (!newChecks[EntryType.FOLDER][folder.name])
|
||||
newChecks[EntryType.FOLDER][folder.name] = {};
|
||||
|
||||
const resource = getFolderByNameAndEnv(folder.name, env.slug);
|
||||
|
||||
if (resource) newChecks[EntryType.FOLDER][folder.name][env.slug] = resource;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setSelectedEntries(newChecks);
|
||||
};
|
||||
|
||||
if (isWorkspaceLoading || (isProjectV3 && isOverviewLoading)) {
|
||||
return (
|
||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
@@ -799,8 +861,6 @@ export const SecretOverviewPage = () => {
|
||||
</div>
|
||||
<SelectionPanel
|
||||
secretPath={secretPath}
|
||||
getSecretByKey={getSecretByKey}
|
||||
getFolderByNameAndEnv={getFolderByNameAndEnv}
|
||||
selectedEntries={selectedEntries}
|
||||
resetSelectedEntries={resetSelectedEntries}
|
||||
/>
|
||||
@@ -810,7 +870,28 @@ export const SecretOverviewPage = () => {
|
||||
<THead>
|
||||
<Tr className="sticky top-0 z-20 border-0">
|
||||
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
|
||||
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
|
||||
<div className="flex items-center border-b border-r border-mineshaft-600 pr-5 pl-3 pt-3.5 pb-3">
|
||||
<Tooltip
|
||||
className="max-w-[20rem] whitespace-nowrap capitalize"
|
||||
content={
|
||||
totalCount > 0
|
||||
? `${
|
||||
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
|
||||
} all folders and secrets on page`
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Checkbox
|
||||
isDisabled={totalCount === 0}
|
||||
id="checkbox-select-all-rows"
|
||||
isChecked={allRowsSelectedOnPage.isChecked}
|
||||
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
|
||||
onCheckedChange={toggleSelectAllRows}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="mx-2 h-4 w-[1px] bg-mineshaft-400" />
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
@@ -938,7 +1019,7 @@ export const SecretOverviewPage = () => {
|
||||
<SecretOverviewFolderRow
|
||||
folderName={folderName}
|
||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||
isSelected={selectedEntries.folder[folderName]}
|
||||
isSelected={Boolean(selectedEntries.folder[folderName])}
|
||||
onToggleFolderSelect={() =>
|
||||
toggleSelectedEntry(EntryType.FOLDER, folderName)
|
||||
}
|
||||
@@ -960,7 +1041,7 @@ export const SecretOverviewPage = () => {
|
||||
))}
|
||||
{secKeys.map((key, index) => (
|
||||
<SecretOverviewTableRow
|
||||
isSelected={selectedEntries.secret[key]}
|
||||
isSelected={Boolean(selectedEntries.secret[key])}
|
||||
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
|
||||
secretPath={secretPath}
|
||||
getImportedSecretByKey={getImportedSecretByKey}
|
||||
|
||||
Reference in New Issue
Block a user