Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 183 additions & 2 deletions apps/studio/components/interfaces/Auth/Users/UsersV2.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { useQueryClient } from '@tanstack/react-query'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { RefreshCw, Trash, Users, X } from 'lucide-react'
import {
ExternalLinkIcon,
InfoIcon,
RefreshCw,
Trash,
Users,
WandSparklesIcon,
X,
} from 'lucide-react'
import { UIEvent, useEffect, useMemo, useRef, useState } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import { toast } from 'sonner'
import pgMeta from '@supabase/pg-meta'

import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import AlertError from 'components/ui/AlertError'
import { APIDocsButton } from 'components/ui/APIDocsButton'
Expand All @@ -26,6 +35,9 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers'
import { parseAsArrayOf, parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
cn,
LoadingLine,
Expand Down Expand Up @@ -57,8 +69,23 @@ import {
import { formatUserColumns, formatUsersData } from './Users.utils'
import { UsersFooter } from './UsersFooter'
import { UsersSearch } from './UsersSearch'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useUserIndexStatusesQuery } from 'data/auth/user-search-indexes-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useIndexWorkerStatusQuery } from 'data/auth/index-worker-status-query'
import { InlineLink } from 'components/ui/InlineLink'
import Link from 'next/link'

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 queryClient = useQueryClient()
Expand Down Expand Up @@ -149,6 +176,7 @@ export const UsersV2 = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [isDeletingUsers, setIsDeletingUsers] = useState(false)
const [showFreeformWarning, setShowFreeformWarning] = useState(false)
const [showCreateIndexesModal, setShowCreateIndexesModal] = useState(false)

const { data: totalUsersCountData, isSuccess: isCountLoaded } = useUsersCountQuery(
{
Expand Down Expand Up @@ -325,6 +353,74 @@ export const UsersV2 = () => {
}
}

const isImprovedUserSearchEnabled = useFlag('improvedUserSearch')
const { data: authConfig } = useAuthConfigQuery({ projectRef })
const {
data: userSearchIndexes,
isError: isUserSearchIndexesError,
isLoading: isUserSearchIndexesLoading,
} = useUserIndexStatusesQuery({ projectRef, connectionString: project?.connectionString })
const { data: indexWorkerStatus } = 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 === pgMeta.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 _showImprovedSearch =
isImprovedUserSearchEnabled &&
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
*/
const isCountWithinThresholdForOptIn = totalUsers <= IMPROVED_SEARCH_COUNT_THRESHOLD
const showImprovedSearchOptIn =
isImprovedUserSearchEnabled &&
authConfig?.INDEX_WORKER_ENSURE_USER_SEARCH_INDEXES_EXIST === false &&
isCountWithinThresholdForOptIn

/**
* 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

useEffect(() => {
if (
!isRefetching &&
Expand Down Expand Up @@ -378,6 +474,50 @@ export const UsersV2 = () => {
<>
<div className="h-full flex flex-col">
<FormHeader className="py-4 px-6 !mb-0" title="Users" />

{showImprovedSearchOptIn && (
<Alert_Shadcn_ className="rounded-none mb-0 border-0 border-t">
<InfoIcon className="size-4" />
<AlertTitle_Shadcn_>Opt-in to an improved search experience</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex justify-between items-center">
<div>
Creating the necessary indexes will provide a safer and more performant search
experience.
</div>
<Button
icon={<WandSparklesIcon />}
onClick={() => setShowCreateIndexesModal(true)}
loading={isUpdatingAuthConfig}
type="default"
>
Create indexes
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}

{indexWorkerInProgress && (
<Alert_Shadcn_ className="rounded-none mb-0 border-0 border-t">
<InfoIcon className="size-4" />
<AlertTitle_Shadcn_>Index creation is in progress</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex justify-between items-center">
<div>
The indexes are currently being created. This process may take some time depending
on the number of users in your project.
</div>

<Button type="link" iconRight={<ExternalLinkIcon />} asChild>
<Link
href={`/project/${projectRef}/logs/explorer?q=${encodeURI(INDEX_WORKER_LOGS_SEARCH_STRING)}`}
target="_blank"
>
View logs
</Link>
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}

<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">
{selectedUsers.size > 0 ? (
<div className="flex items-center gap-x-2">
Expand Down Expand Up @@ -753,6 +893,47 @@ export const UsersV2 = () => {
</p>
</ConfirmationModal>

<ConfirmationModal
size="medium"
visible={showCreateIndexesModal}
confirmLabel="Create indexes"
title="Create user search indexes"
onConfirm={() => {
handleEnableUserSearchIndexes()
setShowCreateIndexesModal(false)
}}
onCancel={() => setShowCreateIndexesModal(false)}
alert={{
title: 'Create user search indexes',
description:
'This process will create indexes on the auth.users table to improve search performance and enable better sorting and filtering capabilities.',
}}
>
<ul className="text-sm list-disc pl-4 my-3 flex flex-col gap-2">
<li className="marker:text-foreground-light">
Creating these indexes may temporarily impact database performance.
</li>
<li className="marker:text-foreground-light">
Depending on the size of your `auth.users` table, this operation may take some time to
complete.
</li>
<li className="marker:text-foreground-light">
You may continue to use the Auth Users page while the indexes are being created, but
search performance improvements will only take effect once the process is complete.
</li>
<li className="marker:text-foreground-light">
You can monitor the progress in the{' '}
<InlineLink
href={`/project/${projectRef}/logs/explorer?q=${encodeURI(INDEX_WORKER_LOGS_SEARCH_STRING)}`}
target="_blank"
>
project logs
</InlineLink>
. If you encounter any issues, please contact Supabase support for assistance.
</li>
</ul>
</ConfirmationModal>

{/* [Joshen] For deleting via context menu, the dialog above is dependent on the selectedUsers state */}
<DeleteUserModal
visible={!!selectedUserToDelete}
Expand Down
15 changes: 9 additions & 6 deletions apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import { AiIconAnimation, Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
import { Row } from 'ui-patterns'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { Markdown } from '../Markdown'

export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: boolean }) => {
const { ref: projectRef } = useParams()
Expand All @@ -37,12 +38,12 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo
const totalErrors = errorLints.length

const titleContent = useMemo(() => {
if (totalErrors === 0) return <h2>Assistant found no issues</h2>
if (totalErrors === 0) return <h2>Advisor found no issues</h2>
const issuesText = totalErrors === 1 ? 'issue' : 'issues'
const numberDisplay = totalErrors.toString()
return (
<h2>
Assistant found {numberDisplay} {issuesText}
Advisor found {numberDisplay} {issuesText}
</h2>
)
}, [totalErrors])
Expand Down Expand Up @@ -138,8 +139,10 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo
/>
</CardHeader>
<CardContent className="p-6 pt-16 flex flex-col justify-end flex-1 overflow-auto">
{lint.detail ? lint.detail.substring(0, 100) : lint.title}
{lint.detail && lint.detail.length > 100 && '...'}
<h3 className="mb-1">{lint.title}</h3>
<Markdown className="leading-6 text-sm text-foreground-light">
{lint.detail && lint.detail.replace(/\\`/g, '`')}
</Markdown>
</CardContent>
</Card>
)
Expand All @@ -155,8 +158,8 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo

function EmptyState() {
return (
<Card className="bg-transparent">
<CardContent className="flex flex-col items-center justify-center gap-2 p-16">
<Card className="bg-transparent h-64">
<CardContent className="flex flex-col items-center justify-center gap-2 p-16 h-full">
<Shield size={20} strokeWidth={1.5} className="text-foreground-muted" />
<p className="text-sm text-foreground-light text-center">
No security or performance errors found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { useRouter } from 'next/router'
import { useState } from 'react'

import { useParams } from 'common'
import { NavigationItem, PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { useCronJobQuery } from 'data/database-cron-jobs/database-cron-job-query'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
import {
BreadcrumbItem_Shadcn_ as BreadcrumbItem,
BreadcrumbLink_Shadcn_ as BreadcrumbLink,
BreadcrumbList_Shadcn_ as BreadcrumbList,
BreadcrumbPage_Shadcn_ as BreadcrumbPage,
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
Button,
cn,
CodeBlock,
Expand All @@ -21,6 +25,15 @@ import {
TooltipTrigger,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import {
PageHeader,
PageHeaderAside,
PageHeaderBreadcrumb,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { CreateCronJobSheet } from './CreateCronJobSheet/CreateCronJobSheet'
import { isSecondsFormat, parseCronJobCommand } from './CronJobs.utils'
Expand Down Expand Up @@ -60,24 +73,6 @@ export const CronJobPage = () => {
},
})

const breadcrumbItems = [
{
label: 'Integrations',
href: `/project/${ref}/integrations`,
},
{
label: 'Cron',
href: pageId
? `/project/${ref}/integrations/${id}/${pageId}`
: `/project/${ref}/integrations/${id}`,
},
{
label: childLabel ?? job?.jobname ?? '',
},
]

const navigationItems: NavigationItem[] = []

const pageTitle = childLabel || childId || 'Cron Job'

const pageSubtitle = job ? (
Expand Down Expand Up @@ -164,17 +159,37 @@ export const CronJobPage = () => {

return (
<>
<PageLayout
title={pageTitle}
size="full"
breadcrumbs={breadcrumbItems}
navigationItems={navigationItems}
secondaryActions={secondaryActions}
subtitle={isLoading ? <ShimmeringLoader className="py-0 h-[20px] w-96" /> : pageSubtitle}
className="border-b-0"
>
<PreviousRunsTab />
</PageLayout>
<PageHeader size="full" className="pb-6">
<PageHeaderBreadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/project/${ref}/integrations`}>Integrations</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/project/${ref}/integrations/${id}/${pageId}`}>Cron</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{childLabel ?? job?.jobname ?? 'Cron Job'}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageHeaderBreadcrumb>
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>{pageTitle}</PageHeaderTitle>
<PageHeaderDescription>
{isLoading ? <ShimmeringLoader className="py-0 h-[20px] w-96" /> : pageSubtitle}
</PageHeaderDescription>
</PageHeaderSummary>
{secondaryActions.length > 0 && <PageHeaderAside>{secondaryActions}</PageHeaderAside>}
</PageHeaderMeta>
</PageHeader>
<PreviousRunsTab />

<Sheet open={isEditSheetOpen} onOpenChange={setIsEditSheetOpen}>
<SheetContent size="lg">
Expand Down
Loading
Loading