From 0ea4f24bb556cdea9366d803ebf467a67f910809 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 20 Oct 2025 12:52:51 +0800 Subject: [PATCH] Chore/persist auth users search configuration (#39649) * Perist auth users search configuration * nit * Clean up * Swap more params to use URL state * Clean up --- .../interfaces/Auth/Users/UsersSearch.tsx | 8 +- .../interfaces/Auth/Users/UsersV2.tsx | 117 +++++++++++------- packages/common/constants/local-storage.ts | 4 +- .../src/sql/studio/get-users-paginated.ts | 15 ++- 4 files changed, 88 insertions(+), 56 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/UsersSearch.tsx b/apps/studio/components/interfaces/Auth/Users/UsersSearch.tsx index 843b507f2655f..afa53d1650426 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersSearch.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersSearch.tsx @@ -1,4 +1,4 @@ -import { X } from 'lucide-react' +import { Search, X } from 'lucide-react' import { SetStateAction } from 'react' import { @@ -36,8 +36,8 @@ export const UsersSearch = ({ }: UsersSearchProps) => { return (
-
- Search +
+
1 && 'pr-6' )} diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 3eda04f696287..cbe1a7e379bda 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -15,11 +15,13 @@ import { FormHeader } from 'components/ui/Forms/FormHeader' import { authKeys } from 'data/auth/keys' import { useUserDeleteMutation } from 'data/auth/user-delete-mutation' import { User, useUsersInfiniteQuery } from 'data/auth/users-infinite-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers' +import { parseAsArrayOf, parseAsString, parseAsStringEnum, useQueryState } from 'nuqs' import { Button, cn, @@ -51,7 +53,6 @@ import { import { formatUserColumns, formatUsersData } from './Users.utils' import { UsersFooter } from './UsersFooter' import { UsersSearch } from './UsersSearch' -import { useSendEventMutation } from 'data/telemetry/send-event-mutation' export const UsersV2 = () => { const queryClient = useQueryClient() @@ -87,24 +88,33 @@ export const UsersV2 = () => { } }, [showEmailPhoneColumns]) - const [specificFilterColumn, setSpecificFilterColumn] = useState< - 'id' | 'email' | 'phone' | 'freeform' - >('id' as const) - - const [columns, setColumns] = useState[]>([]) - const [search, setSearch] = useState('') - const [filter, setFilter] = useState('all') - const [filterKeywords, setFilterKeywords] = useState('') - const [selectedColumns, setSelectedColumns] = useState([]) - const [selectedProviders, setSelectedProviders] = useState([]) - const [sortByValue, setSortByValue] = useState('id:asc') + const [specificFilterColumn, setSpecificFilterColumn] = useQueryState( + 'filter', + parseAsStringEnum(['id', 'email', 'phone', 'freeform']).withDefault('id') + ) + const [filterUserType, setFilterUserType] = useQueryState( + 'userType', + parseAsStringEnum(['all', 'verified', 'unverified', 'anonymous']).withDefault('all') + ) + const [filterKeywords, setFilterKeywords] = useQueryState('keywords', { defaultValue: '' }) + const [sortByValue, setSortByValue] = useQueryState('id:asc', { defaultValue: 'id:asc' }) + const [sortColumn, sortOrder] = sortByValue.split(':') + const [selectedColumns, setSelectedColumns] = useQueryState( + 'columns', + parseAsArrayOf(parseAsString, ',').withDefault([]) + ) + const [selectedProviders, setSelectedProviders] = useQueryState( + 'providers', + parseAsArrayOf(parseAsString, ',').withDefault([]) + ) - const [selectedUser, setSelectedUser] = useState() - const [selectedUsers, setSelectedUsers] = useState>(new Set([])) - const [selectedUserToDelete, setSelectedUserToDelete] = useState() - const [showDeleteModal, setShowDeleteModal] = useState(false) - const [isDeletingUsers, setIsDeletingUsers] = useState(false) - const [showFreeformWarning, setShowFreeformWarning] = useState(false) + // [Joshen] Opting to store filter column, into local storage for now, which will initialize + // the page when landing on auth users page only if no query params for filter column provided + const [localStorageFilter, setLocalStorageFilter, { isSuccess: isLocalStorageFilterLoaded }] = + useLocalStorageQuery<'id' | 'email' | 'phone' | 'freeform'>( + LOCAL_STORAGE_KEYS.AUTH_USERS_FILTER(projectRef ?? ''), + 'id' + ) const [ columnConfiguration, @@ -115,7 +125,14 @@ export const UsersV2 = () => { null as ColumnConfiguration[] | null ) - const [sortColumn, sortOrder] = sortByValue.split(':') + const [columns, setColumns] = useState[]>([]) + const [search, setSearch] = useState('') + const [selectedUser, setSelectedUser] = useState() + const [selectedUsers, setSelectedUsers] = useState>(new Set([])) + const [selectedUserToDelete, setSelectedUserToDelete] = useState() + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [isDeletingUsers, setIsDeletingUsers] = useState(false) + const [showFreeformWarning, setShowFreeformWarning] = useState(false) const { data, @@ -133,7 +150,10 @@ export const UsersV2 = () => { projectRef, connectionString: project?.connectionString, keywords: filterKeywords, - filter: specificFilterColumn !== 'freeform' || filter === 'all' ? undefined : filter, + filter: + specificFilterColumn !== 'freeform' || filterUserType === 'all' + ? undefined + : filterUserType, providers: selectedProviders, sort: sortColumn as 'id' | 'created_at' | 'email' | 'phone', order: sortOrder as 'asc' | 'desc', @@ -155,11 +175,18 @@ export const UsersV2 = () => { // [Joshen] Only relevant for when selecting one user only const selectedUserFromCheckbox = users.find((u) => u.id === [...selectedUsers][0]) + const searchInvalid = + !search || specificFilterColumn === 'freeform' || specificFilterColumn === 'email' + ? false + : specificFilterColumn === 'id' + ? !search.match(UUIDV4_LEFT_PREFIX_REGEX) + : !search.match(PHONE_NUMBER_LEFT_PREFIX_REGEX) + const telemetryProps = { sort_column: sortColumn, sort_order: sortOrder, providers: selectedProviders, - user_type: filter === 'all' ? undefined : filter, + user_type: filterUserType === 'all' ? undefined : filterUserType, keywords: filterKeywords, filter_column: specificFilterColumn === 'freeform' ? undefined : specificFilterColumn, } @@ -274,18 +301,22 @@ export const UsersV2 = () => { specificFilterColumn, ]) - const searchInvalid = - !search || specificFilterColumn === 'freeform' || specificFilterColumn === 'email' - ? false - : specificFilterColumn === 'id' - ? !search.match(UUIDV4_LEFT_PREFIX_REGEX) - : !search.match(PHONE_NUMBER_LEFT_PREFIX_REGEX) + // [Joshen] Load URL state for filter column only once, if no filter column found in URL params + useEffect(() => { + if (specificFilterColumn === 'id' && localStorageFilter !== 'id') { + setSpecificFilterColumn(localStorageFilter) + } + }, []) + + useEffect(() => { + setLocalStorageFilter(specificFilterColumn) + }, [specificFilterColumn]) return ( <>
-
+
{selectedUsers.size > 0 ? (