diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 629d94a37..45b45b01a 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -77,7 +77,7 @@ export const InfisicalSecretInput = forwardRef( const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; - const debouncedValue = useDebounce(value, 500); + const [debouncedValue] = useDebounce(value, 500); const [highlightedIndex, setHighlightedIndex] = useState(-1); diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index b453456d7..bbe63bc6e 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -33,7 +33,7 @@ export const SecretPathInput = ({ const [suggestions, setSuggestions] = useState([]); 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 || ""; diff --git a/frontend/src/hooks/useDebounce.tsx b/frontend/src/hooks/useDebounce.tsx index 318763210..8537ee348 100644 --- a/frontend/src/hooks/useDebounce.tsx +++ b/frontend/src/hooks/useDebounce.tsx @@ -1,7 +1,10 @@ -import { useEffect, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; // Ref: https://usehooks.com/useDebounce/ -export const useDebounce = (value: T, delay = 500): T => { +export const useDebounce = ( + value: T, + delay = 500 +): [T, Dispatch>] => { // State and setters for debounced value const [debouncedValue, setDebouncedValue] = useState(value); @@ -22,5 +25,5 @@ export const useDebounce = (value: T, delay = 500): T => { [value, delay] // Only re-call effect if value or delay changes ); - return debouncedValue; + return [debouncedValue, setDebouncedValue]; }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 180bd5e40..cb3bae93f 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -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 || ""; diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx index 516f248f0..1731e6dd5 100644 --- a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -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(); diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx index 1f8cc996d..6cd2189c4 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx @@ -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 ?? ""; diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 20dd42ab9..4a6548bb4 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -79,7 +79,7 @@ export const SecretMainPage = () => { ProjectPermissionSub.SecretRollback ); - const [filter, setFilter] = useState({ + 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(defaultFilterState); + const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter); + const [filterHistory, setFilterHistory] = useState>(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 ; } + 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 (
@@ -339,6 +371,7 @@ export const SecretMainPage = () => { environment={environment} workspaceId={workspaceId} secretPath={secretPath} + onNavigateToFolder={handleResetFilter} /> )} {canReadSecret && dynamicSecrets?.length && ( diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 1c00f6823..333f5f0c6 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -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 = ({ } - value={search} + leftIcon={ + + } + value={filter.searchFilter} onChange={(evt) => { - setSearch(evt.target.value); - debouncedOnSearch(evt.target.value); + onSearchChange(evt.target.value); }} />
diff --git a/frontend/src/views/SecretMainPage/components/FolderListView/FolderListView.tsx b/frontend/src/views/SecretMainPage/components/FolderListView/FolderListView.tsx index e4f74cedc..e1368f524 100644 --- a/frontend/src/views/SecretMainPage/components/FolderListView/FolderListView.tsx +++ b/frontend/src/views/SecretMainPage/components/FolderListView/FolderListView.tsx @@ -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 ( diff --git a/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx b/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx index 547507bdf..3a2c0a40e 100644 --- a/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretDropzone/CopySecretsFromBoard.tsx @@ -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, diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 972210bdf..db852bd74 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -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(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({ - [RowType.Folder]: true, - [RowType.DynamicSecret]: true, - [RowType.Secret]: true - }); + const [filter, setFilter] = useState(DEFAULT_FILTER_STATE); + const [filterHistory, setFilterHistory] = useState< + Map + >(new Map()); const [selectedEntries, setSelectedEntries] = useState<{ [EntryType.FOLDER]: Record; @@ -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={} + leftIcon={ + + } /> {userAvailableEnvs.length > 0 && ( diff --git a/frontend/src/views/SecretOverviewPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx b/frontend/src/views/SecretOverviewPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx index 05ec048a5..6317d4413 100644 --- a/frontend/src/views/SecretOverviewPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx +++ b/frontend/src/views/SecretOverviewPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx @@ -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; + const newSecPath = `/${secretPath.split("/").filter(Boolean).slice(0, index).join("/")}`; + if (secretPath === newSecPath) return; + const query = { ...router.query, secretPath: newSecPath } as Record; // 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 ( diff --git a/frontend/src/views/admin/DashboardPage/UserPanel.tsx b/frontend/src/views/admin/DashboardPage/UserPanel.tsx index 2475ef14b..672f7f663 100644 --- a/frontend/src/views/admin/DashboardPage/UserPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/UserPanel.tsx @@ -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({