feature: clear filters when navigating down and restore filters when navigating up folders in secrets dashboard

This commit is contained in:
Scott Wilson
2024-10-01 09:26:25 -07:00
parent acde0867a0
commit fbf52850e8
13 changed files with 122 additions and 52 deletions

View File

@@ -77,7 +77,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const debouncedValue = useDebounce(value, 500);
const [debouncedValue] = useDebounce(value, 500);
const [highlightedIndex, setHighlightedIndex] = useState(-1);

View File

@@ -33,7 +33,7 @@ export const SecretPathInput = ({
const [suggestions, setSuggestions] = useState<string[]>([]);
const [isInputFocused, setIsInputFocus] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const debouncedInputValue = useDebounce(inputValue, 200);
const [debouncedInputValue] = useDebounce(inputValue, 200);
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";

View File

@@ -1,7 +1,10 @@
import { useEffect, useState } from "react";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
// Ref: https://usehooks.com/useDebounce/
export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
export const useDebounce = <T extends unknown>(
value: T,
delay = 500
): [T, Dispatch<SetStateAction<T>>] => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
@@ -22,5 +25,5 @@ export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
return [debouncedValue, setDebouncedValue];
};

View File

@@ -60,7 +60,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState(OrgIdentityOrderBy.Name);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search);
const [debouncedSearch] = useDebounce(search);
const organizationId = currentOrg?.id || "";

View File

@@ -37,7 +37,7 @@ export const OrgAdminProjects = withPermission(
() => {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search);
const [debouncedSearch] = useDebounce(search);
const [perPage, setPerPage] = useState(25);
const router = useRouter();
const orgAdminAccessProject = useOrgAdminAccessProject();

View File

@@ -72,7 +72,7 @@ export const IdentityTab = withProjectPermission(
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState(ProjectIdentityOrderBy.Name);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search);
const [debouncedSearch] = useDebounce(search);
const workspaceId = currentWorkspace?.id ?? "";

View File

@@ -79,7 +79,7 @@ export const SecretMainPage = () => {
ProjectPermissionSub.SecretRollback
);
const [filter, setFilter] = useState<Filter>({
const defaultFilterState = {
tags: {},
searchFilter: (router.query.searchFilter as string) || "",
include: {
@@ -88,8 +88,11 @@ export const SecretMainPage = () => {
[RowType.DynamicSecret]: canReadSecret,
[RowType.Secret]: canReadSecret
}
});
const debouncedSearchFilter = useDebounce(filter.searchFilter);
};
const [filter, setFilter] = useState<Filter>(defaultFilterState);
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
// change filters if permissions change at different paths/env
useEffect(() => {
@@ -255,10 +258,39 @@ export const SecretMainPage = () => {
if (totalCount < paginationOffset) setPage(1);
}, [totalCount]);
useEffect(() => {
// restore filters for path if set
const restore = filterHistory.get(secretPath);
setFilter(restore ?? defaultFilterState);
setDebouncedSearchFilter(restore?.searchFilter ?? "");
const { searchFilter, ...query } = router.query;
// this is a temp work around until we fully transition state to query params,
// setting the initial search filter by query and then moving it to internal state
if (router.query.searchFilter) {
router.push({
pathname: router.pathname,
query
});
}
}, [secretPath]);
if (isDetailsLoading) {
return <ContentLoader text={LOADER_TEXT} />;
}
const handleResetFilter = () => {
// store for breadcrumb nav to restore previously used filters
setFilterHistory((prev) => {
const curr = new Map(prev);
curr.set(secretPath, filter);
return curr;
});
setFilter(defaultFilterState);
setDebouncedSearchFilter("");
};
return (
<StoreProvider>
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
@@ -339,6 +371,7 @@ export const SecretMainPage = () => {
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadSecret && dynamicSecrets?.length && (

View File

@@ -1,4 +1,3 @@
import { useState } from "react";
import { TypeOptions } from "react-toastify";
import { subject } from "@casl/ability";
import {
@@ -56,7 +55,6 @@ import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { debounce } from "@app/lib/fn/debounce";
import {
PopUpNames,
@@ -118,7 +116,6 @@ export const ActionBar = ({
] as const);
const { subscription } = useSubscription();
const { openPopUp } = usePopUpAction();
const [search, setSearch] = useState(filter.searchFilter);
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
@@ -134,8 +131,6 @@ export const ActionBar = ({
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
);
const debouncedOnSearch = debounce(onSearchChange, 500);
const handleFolderCreate = async (folderName: string) => {
try {
await createFolder({
@@ -286,11 +281,15 @@ export const ActionBar = ({
<Input
className="bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by folder name, key name, comment..."
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
value={search}
leftIcon={
<FontAwesomeIcon
className={filter.searchFilter ? "text-primary" : ""}
icon={faMagnifyingGlass}
/>
}
value={filter.searchFilter}
onChange={(evt) => {
setSearch(evt.target.value);
debouncedOnSearch(evt.target.value);
onSearchChange(evt.target.value);
}}
/>
</div>

View File

@@ -18,13 +18,15 @@ type Props = {
environment: string;
workspaceId: string;
secretPath?: string;
onNavigateToFolder: (path: string) => void;
};
export const FolderListView = ({
folders = [],
environment,
workspaceId,
secretPath = "/"
secretPath = "/",
onNavigateToFolder
}: Props) => {
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"updateFolder",
@@ -88,13 +90,16 @@ export const FolderListView = ({
};
const handleFolderClick = (name: string) => {
router.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: `${router.query?.secretPath || ""}/${name}`
}
});
const path = `${router.query?.secretPath || ""}/${name}`;
router
.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: path
}
})
.then(() => onNavigateToFolder(path));
};
return (

View File

@@ -84,7 +84,7 @@ export const CopySecretsFromBoard = ({
const envCopySecPath = watch("secretPath");
const selectedEnvSlug = watch("environment");
const debouncedEnvCopySecretPath = useDebounce(envCopySecPath);
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,

View File

@@ -97,11 +97,16 @@ type Filter = {
};
const INIT_PER_PAGE = 20;
const DEFAULT_FILTER_STATE = {
[RowType.Folder]: true,
[RowType.DynamicSecret]: true,
[RowType.Secret]: true
};
export const SecretOverviewPage = () => {
const { t } = useTranslation();
const router = useRouter();
// this is to set expandable table width
// coz when overflow the table goes to the right
const parentTableRef = useRef<HTMLTableElement>(null);
@@ -121,14 +126,13 @@ export const SecretOverviewPage = () => {
const workspaceId = currentWorkspace?.id as string;
const projectSlug = currentWorkspace?.slug as string;
const [searchFilter, setSearchFilter] = useState("");
const debouncedSearchFilter = useDebounce(searchFilter);
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
const secretPath = (router.query?.secretPath as string) || "/";
const [filter, setFilter] = useState<Filter>({
[RowType.Folder]: true,
[RowType.DynamicSecret]: true,
[RowType.Secret]: true
});
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
const [filterHistory, setFilterHistory] = useState<
Map<string, { filter: Filter; searchFilter: string }>
>(new Map());
const [selectedEntries, setSelectedEntries] = useState<{
[EntryType.FOLDER]: Record<string, boolean>;
@@ -195,7 +199,7 @@ export const SecretOverviewPage = () => {
useEffect(() => {
setVisibleEnvs(userAvailableEnvs);
}, [userAvailableEnvs, secretPath]);
}, [userAvailableEnvs]);
const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } =
useGetImportedSecretsAllEnvs({
@@ -456,16 +460,35 @@ export const SecretOverviewPage = () => {
}
};
const handleResetSearch = () => setSearchFilter("");
const handleResetSearch = (path: string) => {
const restore = filterHistory.get(path);
setFilter(restore?.filter ?? DEFAULT_FILTER_STATE);
const search = restore?.searchFilter ?? "";
setSearchFilter(search);
setDebouncedSearchFilter(search);
};
const handleFolderClick = (path: string) => {
router.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: `${router.query?.secretPath || ""}/${path}`
}
// store for breadcrumb nav to restore previously used filters
setFilterHistory((prev) => {
const curr = new Map(prev);
curr.set(secretPath, { filter, searchFilter });
return curr;
});
router
.push({
pathname: router.pathname,
query: {
...router.query,
secretPath: `${router.query?.secretPath || ""}/${path}`
}
})
.then(() => {
setFilter(DEFAULT_FILTER_STATE);
setSearchFilter("");
setDebouncedSearchFilter("");
});
};
const handleExploreEnvClick = async (slug: string) => {
@@ -544,7 +567,9 @@ export const SecretOverviewPage = () => {
const isTableEmpty = totalCount === 0;
const isTableFiltered = Boolean(Object.values(filter).filter((enabled) => !enabled).length);
const isTableFiltered =
Boolean(Object.values(filter).filter((enabled) => !enabled).length) ||
userAvailableEnvs.length !== visibleEnvs.length;
if (!isProjectV3)
return (
@@ -706,7 +731,12 @@ export const SecretOverviewPage = () => {
placeholder="Search by secret/folder name..."
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
leftIcon={
<FontAwesomeIcon
icon={faMagnifyingGlass}
className={searchFilter ? "text-primary" : ""}
/>
}
/>
</div>
{userAvailableEnvs.length > 0 && (

View File

@@ -4,16 +4,16 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type Props = {
secretPath: string;
onResetSearch: () => void;
onResetSearch: (path: string) => void;
};
export const FolderBreadCrumbs = ({ secretPath = "/", onResetSearch }: Props) => {
const router = useRouter();
const onFolderCrumbClick = (index: number) => {
const newSecPath = secretPath.split("/").filter(Boolean).slice(0, index).join("/");
if (secretPath === `/${newSecPath}`) return;
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
const newSecPath = `/${secretPath.split("/").filter(Boolean).slice(0, index).join("/")}`;
if (secretPath === newSecPath) return;
const query = { ...router.query, secretPath: newSecPath } as Record<string, string>;
// root condition
if (index === 0) delete query.secretPath;
router
@@ -21,7 +21,7 @@ export const FolderBreadCrumbs = ({ secretPath = "/", onResetSearch }: Props) =>
pathname: router.pathname,
query
})
.then(() => onResetSearch());
.then(() => onResetSearch(newSecPath));
};
return (

View File

@@ -38,7 +38,7 @@ const UserPanelTable = ({
const [searchUserFilter, setSearchUserFilter] = useState("");
const { user } = useUser();
const userId = user?.id || "";
const debounedSearchTerm = useDebounce(searchUserFilter, 500);
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
const { subscription } = useSubscription();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({