11import { useQueryClient } from '@tanstack/react-query'
22import AwesomeDebouncePromise from 'awesome-debounce-promise'
3- import { RefreshCw , Trash , Users , X } from 'lucide-react'
3+ import {
4+ ExternalLinkIcon ,
5+ InfoIcon ,
6+ RefreshCw ,
7+ Trash ,
8+ Users ,
9+ WandSparklesIcon ,
10+ X ,
11+ } from 'lucide-react'
412import { UIEvent , useEffect , useMemo , useRef , useState } from 'react'
513import DataGrid , { Column , DataGridHandle , Row } from 'react-data-grid'
614import { toast } from 'sonner'
15+ import pgMeta from '@supabase/pg-meta'
716
817import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
9- import { LOCAL_STORAGE_KEYS , useParams } from 'common'
18+ import { LOCAL_STORAGE_KEYS , useFlag , useParams } from 'common'
1019import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
1120import AlertError from 'components/ui/AlertError'
1221import { APIDocsButton } from 'components/ui/APIDocsButton'
@@ -26,6 +35,9 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
2635import { cleanPointerEventsNoneOnBody , isAtBottom } from 'lib/helpers'
2736import { parseAsArrayOf , parseAsString , parseAsStringEnum , useQueryState } from 'nuqs'
2837import {
38+ Alert_Shadcn_ ,
39+ AlertDescription_Shadcn_ ,
40+ AlertTitle_Shadcn_ ,
2941 Button ,
3042 cn ,
3143 LoadingLine ,
@@ -57,8 +69,23 @@ import {
5769import { formatUserColumns , formatUsersData } from './Users.utils'
5870import { UsersFooter } from './UsersFooter'
5971import { UsersSearch } from './UsersSearch'
72+ import { useAuthConfigQuery } from 'data/auth/auth-config-query'
73+ import { useUserIndexStatusesQuery } from 'data/auth/user-search-indexes-query'
74+ import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
75+ import { useIndexWorkerStatusQuery } from 'data/auth/index-worker-status-query'
76+ import { InlineLink } from 'components/ui/InlineLink'
77+ import Link from 'next/link'
6078
6179const SORT_BY_VALUE_COUNT_THRESHOLD = 10_000
80+ const IMPROVED_SEARCH_COUNT_THRESHOLD = 10_000
81+
82+ const INDEX_WORKER_LOGS_SEARCH_STRING = `select id, auth_logs.timestamp, metadata.level, event_message, metadata.msg as msg, metadata.error
83+ from auth_logs
84+ cross join unnest(metadata) as metadata
85+ where metadata.worker_type = 'apiworker_index_worker'
86+ and auth_logs.timestamp >= timestamp_sub(current_timestamp(), interval 3 hour)
87+ order by timestamp desc
88+ limit 100`
6289
6390export const UsersV2 = ( ) => {
6491 const queryClient = useQueryClient ( )
@@ -149,6 +176,7 @@ export const UsersV2 = () => {
149176 const [ showDeleteModal , setShowDeleteModal ] = useState ( false )
150177 const [ isDeletingUsers , setIsDeletingUsers ] = useState ( false )
151178 const [ showFreeformWarning , setShowFreeformWarning ] = useState ( false )
179+ const [ showCreateIndexesModal , setShowCreateIndexesModal ] = useState ( false )
152180
153181 const { data : totalUsersCountData , isSuccess : isCountLoaded } = useUsersCountQuery (
154182 {
@@ -325,6 +353,74 @@ export const UsersV2 = () => {
325353 }
326354 }
327355
356+ const isImprovedUserSearchEnabled = useFlag ( 'improvedUserSearch' )
357+ const { data : authConfig } = useAuthConfigQuery ( { projectRef } )
358+ const {
359+ data : userSearchIndexes ,
360+ isError : isUserSearchIndexesError ,
361+ isLoading : isUserSearchIndexesLoading ,
362+ } = useUserIndexStatusesQuery ( { projectRef, connectionString : project ?. connectionString } )
363+ const { data : indexWorkerStatus } = useIndexWorkerStatusQuery ( {
364+ projectRef,
365+ connectionString : project ?. connectionString ,
366+ } )
367+ const { mutate : updateAuthConfig , isPending : isUpdatingAuthConfig } = useAuthConfigUpdateMutation (
368+ {
369+ onSuccess : ( ) => {
370+ toast . success ( 'Initiated creation of user search indexes' )
371+ } ,
372+ onError : ( error ) => {
373+ toast . error ( `Failed to initiate creation of user search indexes: ${ error ?. message } ` )
374+ } ,
375+ }
376+ )
377+
378+ const handleEnableUserSearchIndexes = ( ) => {
379+ if ( ! projectRef ) return console . error ( 'Project ref is required' )
380+ updateAuthConfig ( {
381+ projectRef : projectRef ,
382+ config : { INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST : true } ,
383+ } )
384+ }
385+
386+ const userSearchIndexesAreValidAndReady =
387+ ! isUserSearchIndexesError &&
388+ ! isUserSearchIndexesLoading &&
389+ userSearchIndexes ?. length === pgMeta . USER_SEARCH_INDEXES . length &&
390+ userSearchIndexes ?. every ( ( index ) => index . is_valid && index . is_ready )
391+
392+ /**
393+ * We want to show the improved search when:
394+ * 1. The feature flag is enabled for them
395+ * 2. The user has opted in (authConfig.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST is true)
396+ * 3. The required indexes are valid and ready
397+ */
398+ const _showImprovedSearch =
399+ isImprovedUserSearchEnabled &&
400+ authConfig ?. INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === true &&
401+ userSearchIndexesAreValidAndReady
402+
403+ /**
404+ * We want to show users the improved search opt-in only if:
405+ * 1. The feature flag is enabled for them
406+ * 2. They have not opted in yet (authConfig.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST is false)
407+ * 3. They have < threshold number of users
408+ */
409+ const isCountWithinThresholdForOptIn = totalUsers <= IMPROVED_SEARCH_COUNT_THRESHOLD
410+ const showImprovedSearchOptIn =
411+ isImprovedUserSearchEnabled &&
412+ authConfig ?. INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === false &&
413+ isCountWithinThresholdForOptIn
414+
415+ /**
416+ * We want to show an "in progress" state when:
417+ * 1. The user has opted in (authConfig.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST is true)
418+ * 2. The index worker is currently in progress
419+ */
420+ const indexWorkerInProgress =
421+ authConfig ?. INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === true &&
422+ indexWorkerStatus ?. is_in_progress === true
423+
328424 useEffect ( ( ) => {
329425 if (
330426 ! isRefetching &&
@@ -378,6 +474,50 @@ export const UsersV2 = () => {
378474 < >
379475 < div className = "h-full flex flex-col" >
380476 < FormHeader className = "py-4 px-6 !mb-0" title = "Users" />
477+
478+ { showImprovedSearchOptIn && (
479+ < Alert_Shadcn_ className = "rounded-none mb-0 border-0 border-t" >
480+ < InfoIcon className = "size-4" />
481+ < AlertTitle_Shadcn_ > Opt-in to an improved search experience</ AlertTitle_Shadcn_ >
482+ < AlertDescription_Shadcn_ className = "flex justify-between items-center" >
483+ < div >
484+ Creating the necessary indexes will provide a safer and more performant search
485+ experience.
486+ </ div >
487+ < Button
488+ icon = { < WandSparklesIcon /> }
489+ onClick = { ( ) => setShowCreateIndexesModal ( true ) }
490+ loading = { isUpdatingAuthConfig }
491+ type = "default"
492+ >
493+ Create indexes
494+ </ Button >
495+ </ AlertDescription_Shadcn_ >
496+ </ Alert_Shadcn_ >
497+ ) }
498+
499+ { indexWorkerInProgress && (
500+ < Alert_Shadcn_ className = "rounded-none mb-0 border-0 border-t" >
501+ < InfoIcon className = "size-4" />
502+ < AlertTitle_Shadcn_ > Index creation is in progress</ AlertTitle_Shadcn_ >
503+ < AlertDescription_Shadcn_ className = "flex justify-between items-center" >
504+ < div >
505+ The indexes are currently being created. This process may take some time depending
506+ on the number of users in your project.
507+ </ div >
508+
509+ < Button type = "link" iconRight = { < ExternalLinkIcon /> } asChild >
510+ < Link
511+ href = { `/project/${ projectRef } /logs/explorer?q=${ encodeURI ( INDEX_WORKER_LOGS_SEARCH_STRING ) } ` }
512+ target = "_blank"
513+ >
514+ View logs
515+ </ Link >
516+ </ Button >
517+ </ AlertDescription_Shadcn_ >
518+ </ Alert_Shadcn_ >
519+ ) }
520+
381521 < div className = "bg-surface-200 py-3 px-4 md:px-6 flex flex-col lg:flex-row lg:items-start justify-between gap-2 border-t" >
382522 { selectedUsers . size > 0 ? (
383523 < div className = "flex items-center gap-x-2" >
@@ -753,6 +893,47 @@ export const UsersV2 = () => {
753893 </ p >
754894 </ ConfirmationModal >
755895
896+ < ConfirmationModal
897+ size = "medium"
898+ visible = { showCreateIndexesModal }
899+ confirmLabel = "Create indexes"
900+ title = "Create user search indexes"
901+ onConfirm = { ( ) => {
902+ handleEnableUserSearchIndexes ( )
903+ setShowCreateIndexesModal ( false )
904+ } }
905+ onCancel = { ( ) => setShowCreateIndexesModal ( false ) }
906+ alert = { {
907+ title : 'Create user search indexes' ,
908+ description :
909+ 'This process will create indexes on the auth.users table to improve search performance and enable better sorting and filtering capabilities.' ,
910+ } }
911+ >
912+ < ul className = "text-sm list-disc pl-4 my-3 flex flex-col gap-2" >
913+ < li className = "marker:text-foreground-light" >
914+ Creating these indexes may temporarily impact database performance.
915+ </ li >
916+ < li className = "marker:text-foreground-light" >
917+ Depending on the size of your `auth.users` table, this operation may take some time to
918+ complete.
919+ </ li >
920+ < li className = "marker:text-foreground-light" >
921+ You may continue to use the Auth Users page while the indexes are being created, but
922+ search performance improvements will only take effect once the process is complete.
923+ </ li >
924+ < li className = "marker:text-foreground-light" >
925+ You can monitor the progress in the{ ' ' }
926+ < InlineLink
927+ href = { `/project/${ projectRef } /logs/explorer?q=${ encodeURI ( INDEX_WORKER_LOGS_SEARCH_STRING ) } ` }
928+ target = "_blank"
929+ >
930+ project logs
931+ </ InlineLink >
932+ . If you encounter any issues, please contact Supabase support for assistance.
933+ </ li >
934+ </ ul >
935+ </ ConfirmationModal >
936+
756937 { /* [Joshen] For deleting via context menu, the dialog above is dependent on the selectedUsers state */ }
757938 < DeleteUserModal
758939 visible = { ! ! selectedUserToDelete }
0 commit comments