// @ts-nocheck import type { OptimizedSearchColumns } from '@supabase/pg-meta' import { USER_SEARCH_INDEXES } from '@supabase/pg-meta' import { keepPreviousData, useQueryClient } from '@tanstack/react-query' import AwesomeDebouncePromise from 'awesome-debounce-promise' import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' import { ExternalLinkIcon, InfoIcon, RefreshCw, Trash, Users, WandSparklesIcon, X, } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { parseAsArrayOf, parseAsString, parseAsStringEnum, useQueryState } from 'nuqs' import { UIEvent, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import { toast } from 'sonner' import { Alert, AlertDescription, AlertTitle, Button, cn, LoadingLine, ResizablePanel, ResizablePanelGroup, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { AddUserDropdown } from './AddUserDropdown' import { DeleteUserModal } from './DeleteUserModal' import { SortDropdown } from './SortDropdown' import { useAuthUsersShortcuts } from './useAuthUsersShortcuts' import { UserPanel } from './UserPanel' import type { SpecificFilterColumn } from './Users.constants' import { ColumnConfiguration, Filter, MAX_BULK_DELETE, PROVIDER_FILTER_OPTIONS, USERS_TABLE_COLUMNS, } from './Users.constants' import { formatUserColumns, formatUsersData } from './Users.utils' import { UsersFooter } from './UsersFooter' import { UsersSearch } from './UsersSearch' import { AlertError } from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FilterPopover } from '@/components/ui/FilterPopover' import { FormHeader } from '@/components/ui/Forms/FormHeader' import { InlineLink } from '@/components/ui/InlineLink' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation' import { useIndexWorkerStatusQuery } from '@/data/auth/index-worker-status-query' import { authKeys } from '@/data/auth/keys' import { useUserDeleteMutation } from '@/data/auth/user-delete-mutation' import { useUserIndexStatusesQuery } from '@/data/auth/user-search-indexes-query' import { useUsersCountQuery } from '@/data/auth/users-count-query' 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 { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { PROJECT_STATUS } from '@/lib/constants/infrastructure' import { cleanPointerEventsNoneOnBody, isAtBottom } from '@/lib/helpers' import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' const SORT_BY_VALUE_COUNT_THRESHOLD = 10_000 const IMPROVED_SEARCH_COUNT_THRESHOLD = 10_000 const INDEX_WORKER_LOGS_SEARCH_STRING = `select id, auth_logs.timestamp, metadata.level, event_message, metadata.msg as msg, metadata.error from auth_logs cross join unnest(metadata) as metadata where metadata.worker_type = 'apiworker_index_worker' and auth_logs.timestamp >= timestamp_sub(current_timestamp(), interval 3 hour) order by timestamp desc limit 100` export const UsersV2 = () => { const router = useRouter() const queryClient = useQueryClient() const { ref: projectRef } = useParams() const { data: project, isPending: isPendingProject, isError: isProjectError, } = useSelectedProjectQuery() const { data: selectedOrg } = useSelectedOrganizationQuery() const roleImpersonationState = useRoleImpersonationStateSnapshot() const gridRef = useRef(null) const searchInputRef = useRef(null) const xScroll = useRef(0) const { mutate: sendEvent } = useSendEventMutation() const { authenticationShowProviderFilter: showProviderFilter, authenticationShowSortByEmail: showSortByEmail, authenticationShowSortByPhone: showSortByPhone, authenticationShowUserTypeFilter: showUserTypeFilter, authenticationShowEmailPhoneColumns: showEmailPhoneColumns, } = useIsFeatureEnabled([ 'authentication:show_provider_filter', 'authentication:show_sort_by_email', 'authentication:show_sort_by_phone', 'authentication:show_user_type_filter', 'authentication:show_email_phone_columns', ]) const userTableColumns = useMemo(() => { if (showEmailPhoneColumns) return USERS_TABLE_COLUMNS else { return USERS_TABLE_COLUMNS.filter((col) => { if (col.id === 'email' || col.id === 'phone') return false return true }) } }, [showEmailPhoneColumns]) const [specificFilterColumn, setSpecificFilterColumn] = useQueryState( 'filter', parseAsStringEnum([ 'id', 'email', 'phone', 'name', 'freeform', ]).withDefault('email') ) const [filterUserType, setFilterUserType] = useQueryState( 'userType', parseAsStringEnum(['all', 'verified', 'unverified', 'anonymous']).withDefault('all') ) const [filterKeywords] = useQueryState('keywords', { defaultValue: '' }) const [sortByValue, setSortByValue] = useQueryState('sortBy', { defaultValue: 'created_at:desc' }) const [sortColumn, sortOrder] = sortByValue.split(':') const [selectedColumns, setSelectedColumns] = useQueryState( 'columns', parseAsArrayOf(parseAsString, ',').withDefault([]) ) const [selectedProviders, setSelectedProviders] = useQueryState( 'providers', parseAsArrayOf(parseAsString, ',').withDefault([]) ) const [selectedId, setSelectedId] = useQueryState( 'show', parseAsString.withOptions({ history: 'push', clearOnDefault: true }) ) const [improvedSearchDismissed, setImprovedSearchDismissed] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AUTH_USERS_IMPROVED_SEARCH_DISMISSED(projectRef ?? ''), 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( LOCAL_STORAGE_KEYS.AUTH_USERS_FILTER(projectRef ?? ''), 'email' ) const [ localStorageSortByValue, setLocalStorageSortByValue, { isSuccess: isLocalStorageSortByValueLoaded }, ] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AUTH_USERS_SORT_BY_VALUE(projectRef ?? ''), 'id' ) const [ columnConfiguration, setColumnConfiguration, { isSuccess: isSuccessStorage, isError: isErrorStorage, error: errorStorage }, ] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AUTH_USERS_COLUMNS_CONFIGURATION(projectRef ?? ''), null as ColumnConfiguration[] | null ) const [columns, setColumns] = useState[]>([]) const [selectedUsers, setSelectedUsers] = useState>(new Set([])) const [selectedUserToDelete, setSelectedUserToDelete] = useState() const [isDeletingUsers, setIsDeletingUsers] = useState(false) const [showFreeformWarning, setShowFreeformWarning] = useState(false) const [showCreateIndexesModal, setShowCreateIndexesModal] = useState(false) const [search, setSearch] = useState(filterKeywords) const { data: totalUsersCountData, isSuccess: isCountLoaded } = useUsersCountQuery( { projectRef, connectionString: project?.connectionString, // [Joshen] Do not change the following, these are to match the count query in UsersFooter // on initial load with no search configuration so that we only fire 1 count request at the // beginning. The count value is for all users - should disregard any search configuration keywords: '', filter: undefined, providers: [], forceExactCount: false, }, { placeholderData: keepPreviousData } ) const totalUsers = totalUsersCountData?.count ?? 0 const isCountWithinThresholdForSortBy = totalUsers <= SORT_BY_VALUE_COUNT_THRESHOLD const isImprovedUserSearchFlagEnabled = useFlag('improvedUserSearch') const { data: authConfig, isLoading: isAuthConfigLoading } = useAuthConfigQuery({ projectRef }) const { data: userSearchIndexes, isError: isUserSearchIndexesError, isLoading: isUserSearchIndexesLoading, } = useUserIndexStatusesQuery({ projectRef, connectionString: project?.connectionString }) const { data: indexWorkerStatus, isLoading: isIndexWorkerStatusLoading } = useIndexWorkerStatusQuery({ projectRef, connectionString: project?.connectionString, }) const { mutate: updateAuthConfig, isPending: isUpdatingAuthConfig } = useAuthConfigUpdateMutation( { onSuccess: () => { toast.success('Initiated creation of user search indexes') }, onError: (error) => { toast.error(`Failed to initiate creation of user search indexes: ${error?.message}`) }, } ) const handleEnableUserSearchIndexes = () => { if (!projectRef) return console.error('Project ref is required') updateAuthConfig({ projectRef: projectRef, config: { INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST: true }, }) } const userSearchIndexesAreValidAndReady = !isUserSearchIndexesError && !isUserSearchIndexesLoading && userSearchIndexes?.length === USER_SEARCH_INDEXES.length && userSearchIndexes?.every((index) => index.is_valid && index.is_ready) /** * We want to show the improved search when: * 1. The feature flag is enabled for them * 2. The user has opted in (authConfig.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST is true) * 3. The required indexes are valid and ready */ const improvedSearchEnabled = isImprovedUserSearchFlagEnabled && authConfig?.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === true && userSearchIndexesAreValidAndReady /** * We want to show users the improved search opt-in only if: * 1. The feature flag is enabled for them * 2. They have not opted in yet (authConfig.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST is false) * 3. They have < threshold number of users * 4. They have not dismissed the alert */ const isCountWithinThresholdForOptIn = isCountLoaded && totalUsers <= IMPROVED_SEARCH_COUNT_THRESHOLD const showImprovedSearchOptIn = isImprovedUserSearchFlagEnabled && authConfig?.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === false && isCountWithinThresholdForOptIn && !improvedSearchDismissed /** * We want to show an "in progress" state when: * 1. The user has opted in (authConfig.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST is true) * 2. The index worker is currently in progress */ const indexWorkerInProgress = authConfig?.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === true && indexWorkerStatus?.is_in_progress === true const { data, error, isSuccess, isPending, isLoading, isRefetching, isError, isFetchingNextPage, refetch, hasNextPage, fetchNextPage, } = useUsersInfiniteQuery( { projectRef, connectionString: project?.connectionString, keywords: filterKeywords, filter: (specificFilterColumn !== 'freeform' && !improvedSearchEnabled) || filterUserType === 'all' ? undefined : filterUserType, providers: selectedProviders, sort: sortColumn as 'id' | 'created_at' | 'email' | 'phone', order: sortOrder as 'asc' | 'desc', // improved search will always have a column specified ...(specificFilterColumn !== 'freeform' || improvedSearchEnabled ? { column: specificFilterColumn as OptimizedSearchColumns } : { column: undefined }), improvedSearchEnabled: improvedSearchEnabled, }, { placeholderData: Boolean(filterKeywords) ? keepPreviousData : undefined, // [Joshen] This is to prevent the dashboard from invalidating when refocusing as it may create // a barrage of requests to invalidate each page esp when the project has many many users. staleTime: Infinity, // NOTE(iat): query the user data only after we know whether to show improved search or not enabled: !isUserSearchIndexesLoading && !isAuthConfigLoading && !isIndexWorkerStatusLoading, } ) const { mutateAsync: deleteUser } = useUserDeleteMutation() const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages]) const selectedUser = users?.find((u) => u.id === selectedId)?.id // [Joshen] Only relevant for when selecting one user only const selectedUserFromCheckbox = users.find((u) => u.id === [...selectedUsers][0]) const telemetryProps = { sort_column: sortColumn, sort_order: sortOrder, providers: selectedProviders, user_type: filterUserType === 'all' ? undefined : filterUserType, keywords: filterKeywords, filter_column: specificFilterColumn === 'freeform' ? undefined : specificFilterColumn, } const telemetryGroups = { project: projectRef ?? 'Unknown', organization: selectedOrg?.slug ?? 'Unknown', } const updateStorageFilter = (value: SpecificFilterColumn) => { setLocalStorageFilter(value) setSpecificFilterColumn(value) if (value !== 'freeform' && !improvedSearchEnabled) { updateSortByValue('id:asc') } } const updateSortByValue = (value: string) => { if (isCountWithinThresholdForSortBy) setLocalStorageSortByValue(value) setSortByValue(value) } const onSelectImpersonateUser = async (user: User, destination: 'sql' | 'table-editor') => { await roleImpersonationState.setRole({ type: 'postgrest', role: 'authenticated', userType: 'native', user, aal: 'aal1', }) if (destination === 'sql') { router.push(`/project/${projectRef}/sql`) } else { router.push(`/project/${projectRef}/editor`) } } const handleScroll = (event: UIEvent) => { const isScrollingHorizontally = xScroll.current !== event.currentTarget.scrollLeft xScroll.current = event.currentTarget.scrollLeft if ( isLoading || isFetchingNextPage || isScrollingHorizontally || !isAtBottom(event) || !hasNextPage ) { return } fetchNextPage() } const swapColumns = (data: any[], sourceIdx: number, targetIdx: number) => { const updatedColumns = data.slice() const [removed] = updatedColumns.splice(sourceIdx, 1) updatedColumns.splice(targetIdx, 0, removed) return updatedColumns } // [Joshen] Left off here - it's tricky trying to do both column toggling and re-ordering const saveColumnConfiguration = AwesomeDebouncePromise( (event: 'resize' | 'reorder' | 'toggle', value) => { if (event === 'toggle') { const columnConfig = value.columns.map((col: any) => ({ id: col.key, width: col.width, })) setColumnConfiguration(columnConfig) } else if (event === 'resize') { const columnConfig = columns.map((col, idx) => ({ id: col.key, width: idx === value.idx ? value.width : col.width, })) setColumnConfiguration(columnConfig) } else if (event === 'reorder') { const columnConfig = value.columns.map((col: any) => ({ id: col.key, width: col.width, })) setColumnConfiguration(columnConfig) } }, 500 ) const handleDeleteUsers = async () => { if (!projectRef) return console.error('Project ref is required') const userIds = [...selectedUsers] setIsDeletingUsers(true) try { await Promise.all( userIds.map((id) => deleteUser({ projectRef, userId: id, skipInvalidation: true })) ) // [Joshen] Skip invalidation within RQ to prevent multiple requests, then invalidate once at the end await Promise.all([ queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) }), ]) toast.success( `Successfully deleted the selected ${selectedUsers.size} user${selectedUsers.size > 1 ? 's' : ''}` ) setShowDeleteModal(false) setSelectedUsers(new Set([])) if (userIds.includes(selectedUser)) setSelectedId(null) } catch (error: any) { toast.error(`Failed to delete selected users: ${error.message}`) } finally { setIsDeletingUsers(false) } } const handleRefresh = () => { refetch() sendEvent({ action: 'auth_users_search_submitted', properties: { trigger: 'refresh_button', ...telemetryProps, }, groups: telemetryGroups, }) } const { onCellKeyDown, showDeleteModal, setShowDeleteModal } = useAuthUsersShortcuts({ gridRef, searchInputRef, users, selectedUsers, setSelectedUsers, setSearch, onRefresh: handleRefresh, }) useEffect(() => { if ( !isRefetching && (isSuccessStorage || (isErrorStorage && (errorStorage as Error).message.includes('data is undefined'))) ) { const columns = formatUserColumns({ specificFilterColumn, columns: userTableColumns, config: columnConfiguration ?? [], users: users ?? [], visibleColumns: selectedColumns, setSortByValue: updateSortByValue, onSelectDeleteUser: setSelectedUserToDelete, onSelectImpersonateUser, }) setColumns(columns) if (columns.length < userTableColumns.length) { setSelectedColumns(columns.filter((col) => col.key !== 'img').map((col) => col.key)) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isSuccess, isRefetching, isSuccessStorage, isErrorStorage, errorStorage, users, selectedUsers, specificFilterColumn, ]) // [Joshen] Load URL state for filter column and sort by only once, if no respective values found in URL params useEffect(() => { if ( isLocalStorageFilterLoaded && isLocalStorageSortByValueLoaded && isCountLoaded && isCountWithinThresholdForSortBy ) { if (specificFilterColumn === 'email' && localStorageFilter !== 'email') { setSpecificFilterColumn(localStorageFilter) } if (sortByValue === 'id:asc' && localStorageSortByValue !== 'id:asc') { setSortByValue(localStorageSortByValue) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLocalStorageFilterLoaded, isLocalStorageSortByValueLoaded, isCountLoaded]) return ( <>
{showImprovedSearchOptIn && ( setImprovedSearchDismissed(true)} className="absolute top-3 right-3 opacity-30 hover:opacity-100 transition-opacity" > Dismiss Upgrade to an improved search experience
Enable faster and more reliable searching, sorting, and filtering of your users.
)} {indexWorkerInProgress && ( Index creation is in progress
The indexes are currently being created. This process may take some time depending on the number of users in your project.
)}
{selectedUsers.size > 0 ? (
} className="px-1.5" onClick={() => setSelectedUsers(new Set([]))} tooltip={{ content: { side: 'bottom', text: 'Cancel selection' } }} />
) : ( <>
{ if (value === 'freeform') { if (isCountWithinThresholdForSortBy) { updateStorageFilter(value) } else { setShowFreeformWarning(true) } } else { updateStorageFilter(value) } }} /> {showUserTypeFilter && (specificFilterColumn === 'freeform' || improvedSearchEnabled) && ( )} {showProviderFilter && (specificFilterColumn === 'freeform' || improvedSearchEnabled) && ( { setSelectedProviders(providers) sendEvent({ action: 'auth_users_search_submitted', properties: { trigger: 'provider_filter', ...telemetryProps, providers, }, groups: telemetryGroups, }) }} /> )}
{ // When adding back hidden columns: // (1) width set to default value if any // (2) they will just get appended to the end // (3) If "clearing", reset order of the columns to original let updatedConfig = (columnConfiguration ?? []).slice() if (value.length === 0) { updatedConfig = userTableColumns.map((c) => ({ id: c.id, width: c.width })) } else { value.forEach((col) => { const hasExisting = updatedConfig.find((c) => c.id === col) if (!hasExisting) updatedConfig.push({ id: col, width: userTableColumns.find((c) => c.id === col)?.width, }) }) } const updatedColumns = formatUserColumns({ specificFilterColumn, columns: userTableColumns, config: updatedConfig, users: users ?? [], visibleColumns: value, setSortByValue: updateSortByValue, onSelectDeleteUser: setSelectedUserToDelete, onSelectImpersonateUser, }) setSelectedColumns(value) setColumns(updatedColumns) saveColumnConfiguration('toggle', { columns: updatedColumns }) }} /> { const [sortColumn, sortOrder] = value.split(':') updateSortByValue(value) sendEvent({ action: 'auth_users_search_submitted', properties: { trigger: 'sort_change', ...telemetryProps, sort_column: sortColumn, sort_order: sortOrder, }, groups: telemetryGroups, }) }} showSortByEmail={showSortByEmail} showSortByPhone={showSortByPhone} improvedSearchEnabled={improvedSearchEnabled} />
} type="default" className="w-7" loading={isRefetching && !isFetchingNextPage} onClick={handleRefresh} tooltip={{ content: { side: 'bottom', text: 'Refresh' } }} aria-label="Refresh" />
)}
{ const isSelected = row.id === selectedUser return [ `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none', '[&>.rdg-cell:first-child>div]:ml-4', ].join(' ') }} rowKeyGetter={(row) => row.id} selectedRows={selectedUsers} onScroll={handleScroll} onSelectedRowsChange={(rows) => { if (rows.size > MAX_BULK_DELETE) { toast(`Only up to ${MAX_BULK_DELETE} users can be selected at a time`) } else setSelectedUsers(rows) }} onCellKeyDown={onCellKeyDown} onColumnResize={(idx, width) => saveColumnConfiguration('resize', { idx, width })} onColumnsReorder={(source, target) => { const sourceIdx = columns.findIndex((col) => col.key === source) const targetIdx = columns.findIndex((col) => col.key === target) const updatedColumns = swapColumns(columns, sourceIdx, targetIdx) setColumns(updatedColumns) saveColumnConfiguration('reorder', { columns: updatedColumns }) }} renderers={{ renderRow(id, props) { return ( { const user = users.find((u) => u.id === id) if (user) { const idx = users.indexOf(user) if (props.row.id) { setSelectedId(props.row.id) gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) } } }} /> ) }, noRowsFallback: isPendingProject ? (
) : project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY || isProjectError ? (
) : isPending ? (
) : isError ? (
) : isSuccess ? (

{filterUserType !== 'all' || filterKeywords.length > 0 ? 'No users found' : 'No users in your project'}

{filterUserType !== 'all' || filterKeywords.length > 0 ? 'There are currently no users based on the filters applied' : 'There are currently no users who signed up to your project'}

) : null, }} />
{!!selectedId && }
1 ? 's' : ''}`} loading={isDeletingUsers} confirmLabel="Delete" onCancel={() => setShowDeleteModal(false)} onConfirm={() => handleDeleteUsers()} alert={{ title: `Deleting ${selectedUsers.size === 1 ? 'a user' : 'users'} is irreversible`, description: `This will remove the selected ${selectedUsers.size === 1 ? '' : `${selectedUsers.size} `}user${selectedUsers.size > 1 ? 's' : ''} from the project and all associated data.`, }} >

This is permanent! Are you sure you want to delete the{' '} {selectedUsers.size === 1 ? '' : `selected ${selectedUsers.size} `}user {selectedUsers.size > 1 ? 's' : ''} {selectedUsers.size === 1 ? ( {' '} {selectedUserFromCheckbox?.email ?? selectedUserFromCheckbox?.phone ?? 'this user'} ) : null} ?

{ updateStorageFilter('freeform') setShowFreeformWarning(false) }} onCancel={() => setShowFreeformWarning(false)} alert={{ base: { variant: 'warning' }, title: 'Searching across all columns is not recommended with many users', description: 'This may adversely impact your database, in particular if your project has a large number of users - use with caution. Search mode will not be persisted across browser sessions as a safeguard.', }} >

This will allow you to search across user ID, email, phone number, and display name through a single input field. You will also be able to filter users by provider and sort on users across different columns.

{ handleEnableUserSearchIndexes() setShowCreateIndexesModal(false) }} onCancel={() => setShowCreateIndexesModal(false)} alert={{ title: 'Improved search experience', description: 'This will create indexes to enable faster and more reliable searching, sorting, and filtering of your users.', }} >
  • Creating these indexes may temporarily impact database performance.
  • Depending on the number of users, this may take some time to complete.
  • You can continue using the Auth Users page while the indexes are being created, but improvements will only take effect once complete.
  • You can monitor the progress in the{' '} project logs . If you encounter any issues, please contact Briven support for assistance.
{/* [Joshen] For deleting via context menu, the dialog above is dependent on the selectedUsers state */} { setSelectedUserToDelete(undefined) cleanPointerEventsNoneOnBody() }} onDeleteSuccess={() => { if (selectedUserToDelete?.id === selectedUser) setSelectedId(null) setSelectedUserToDelete(undefined) cleanPointerEventsNoneOnBody(500) }} /> ) }