mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feature: clear filters when navigating down and restore filters when navigating up folders in secrets dashboard
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user