Skip to content

Commit cb25d23

Browse files
authored
feat(auth): allow opting in to creating indexes on auth.users (supabase#40955)
* feat(auth): allow opting in to creating indexes on `auth.users` * chore: organize imports. use alert above table. * chore: use default type to avoid 2 primary actions
1 parent 9d5cf03 commit cb25d23

File tree

7 files changed

+336
-2
lines changed

7 files changed

+336
-2
lines changed

apps/studio/components/interfaces/Auth/Users/UsersV2.tsx

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { useQueryClient } from '@tanstack/react-query'
22
import 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'
412
import { UIEvent, useEffect, useMemo, useRef, useState } from 'react'
513
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
614
import { toast } from 'sonner'
15+
import pgMeta from '@supabase/pg-meta'
716

817
import 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'
1019
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
1120
import AlertError from 'components/ui/AlertError'
1221
import { APIDocsButton } from 'components/ui/APIDocsButton'
@@ -26,6 +35,9 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
2635
import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers'
2736
import { parseAsArrayOf, parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'
2837
import {
38+
Alert_Shadcn_,
39+
AlertDescription_Shadcn_,
40+
AlertTitle_Shadcn_,
2941
Button,
3042
cn,
3143
LoadingLine,
@@ -57,8 +69,23 @@ import {
5769
import { formatUserColumns, formatUsersData } from './Users.utils'
5870
import { UsersFooter } from './UsersFooter'
5971
import { 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

6179
const 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

6390
export 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}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pgMeta from '@supabase/pg-meta'
2+
import { useQuery } from '@tanstack/react-query'
3+
4+
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
5+
import { UseCustomQueryOptions } from 'types'
6+
import { authKeys } from './keys'
7+
8+
type IndexWorkerStatusVariables = {
9+
projectRef?: string
10+
connectionString?: string | null
11+
}
12+
type IndexWorkerStatusData = {
13+
is_in_progress: boolean
14+
}
15+
export type IndexWorkerStatusError = ExecuteSqlError
16+
17+
export async function getIndexWorkerStatus(
18+
{ projectRef, connectionString }: IndexWorkerStatusVariables,
19+
signal?: AbortSignal
20+
): Promise<IndexWorkerStatusData> {
21+
const sql = pgMeta.getIndexWorkerStatusSQL()
22+
23+
const { result } = await executeSql<IndexWorkerStatusData[]>(
24+
{
25+
projectRef,
26+
connectionString,
27+
sql,
28+
queryKey: ['index-worker-status'],
29+
},
30+
signal
31+
)
32+
33+
return result[0]
34+
}
35+
36+
export const useIndexWorkerStatusQuery = <TData = IndexWorkerStatusData>(
37+
{ projectRef, connectionString }: IndexWorkerStatusVariables,
38+
{
39+
enabled = true,
40+
...options
41+
}: UseCustomQueryOptions<IndexWorkerStatusData, IndexWorkerStatusError, TData> = {}
42+
) =>
43+
useQuery<IndexWorkerStatusData, IndexWorkerStatusError, TData>({
44+
queryKey: authKeys.indexWorkerStatus(projectRef),
45+
queryFn: ({ signal }) =>
46+
getIndexWorkerStatus(
47+
{
48+
projectRef,
49+
connectionString,
50+
},
51+
signal
52+
),
53+
enabled: enabled && typeof projectRef !== 'undefined',
54+
...options,
55+
})

apps/studio/data/auth/keys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export const authKeys = {
4040
}
4141
) => ['projects', projectRef, 'users-count', params] as const,
4242

43+
usersIndexStatuses: (projectRef: string | undefined) =>
44+
['projects', projectRef, 'users-index-statuses'] as const,
45+
indexWorkerStatus: (projectRef: string | undefined) =>
46+
['projects', projectRef, 'index-worker-status'] as const,
4347
authConfig: (projectRef: string | undefined) => ['projects', projectRef, 'auth-config'] as const,
4448
accessToken: () => ['access-token'] as const,
4549
overviewMetrics: (projectRef: string | undefined) =>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pgMeta from '@supabase/pg-meta'
2+
import { useQuery } from '@tanstack/react-query'
3+
4+
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
5+
import { UseCustomQueryOptions } from 'types'
6+
import { authKeys } from './keys'
7+
8+
type UsersIndexStatusesVariables = {
9+
projectRef?: string
10+
connectionString?: string | null
11+
}
12+
type UsersIndexStatusesData = {
13+
index_name: string
14+
is_valid: boolean
15+
is_ready: boolean
16+
}[]
17+
export type UsersIndexStatusesError = ExecuteSqlError
18+
19+
export async function getUserIndexStatuses(
20+
{ projectRef, connectionString }: UsersIndexStatusesVariables,
21+
signal?: AbortSignal
22+
): Promise<UsersIndexStatusesData> {
23+
const sql = pgMeta.getIndexStatusesSQL()
24+
25+
const { result } = await executeSql<UsersIndexStatusesData>(
26+
{
27+
projectRef,
28+
connectionString,
29+
sql,
30+
queryKey: ['index-statuses'],
31+
},
32+
signal
33+
)
34+
35+
return result
36+
}
37+
38+
export const useUserIndexStatusesQuery = <TData = UsersIndexStatusesData>(
39+
{ projectRef, connectionString }: UsersIndexStatusesVariables,
40+
{
41+
enabled = true,
42+
...options
43+
}: UseCustomQueryOptions<UsersIndexStatusesData, UsersIndexStatusesError, TData> = {}
44+
) =>
45+
useQuery<UsersIndexStatusesData, UsersIndexStatusesError, TData>({
46+
queryKey: authKeys.usersIndexStatuses(projectRef),
47+
queryFn: ({ signal }) =>
48+
getUserIndexStatuses(
49+
{
50+
projectRef,
51+
connectionString,
52+
},
53+
signal
54+
),
55+
enabled: enabled && typeof projectRef !== 'undefined',
56+
...options,
57+
})

packages/pg-meta/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import version from './pg-meta-version'
1717
import indexes from './pg-meta-indexes'
1818
import columnPrivileges from './pg-meta-column-privileges'
1919
import * as query from './query/index'
20+
import { getIndexStatusesSQL, USER_SEARCH_INDEXES } from './sql/studio/get-index-statuses'
21+
import { getIndexWorkerStatusSQL } from './sql/studio/get-index-worker-status'
2022

2123
export default {
2224
roles,
@@ -38,4 +40,7 @@ export default {
3840
indexes,
3941
columnPrivileges,
4042
query,
43+
getIndexWorkerStatusSQL,
44+
getIndexStatusesSQL,
45+
USER_SEARCH_INDEXES,
4146
}

0 commit comments

Comments
 (0)