import AwesomeDebouncePromise from 'awesome-debounce-promise' import { ArrowDown, ArrowUp, Loader2, RefreshCw, Search, Users, X } from 'lucide-react' import { UIEvent, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' import APIDocsButton from 'components/ui/APIDocsButton' import { FilterPopover } from 'components/ui/FilterPopover' import { FormHeader } from 'components/ui/Forms/FormHeader' import { useUsersCountQuery } from 'data/auth/users-count-query' import { useUsersInfiniteQuery } from 'data/auth/users-infinite-query' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, LoadingLine, ResizablePanel, ResizablePanelGroup, Select_Shadcn_, SelectContent_Shadcn_, SelectGroup_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import AddUserDropdown from './AddUserDropdown' import { UserPanel } from './UserPanel' import { PROVIDER_FILTER_OPTIONS } from './Users.constants' import { formatUserColumns, formatUsersData, isAtBottom } from './Users.utils' type Filter = 'all' | 'verified' | 'unverified' | 'anonymous' export type UsersTableColumn = { id: string name: string minWidth?: number width?: number resizable?: boolean } export type ColumnConfiguration = { id: string; width?: number } export const USERS_TABLE_COLUMNS: UsersTableColumn[] = [ { id: 'img', name: '', minWidth: 65, width: 65, resizable: false }, { id: 'id', name: 'UID', width: 280 }, { id: 'name', name: 'Display name', minWidth: 0, width: 150 }, { id: 'email', name: 'Email', width: 300 }, { id: 'phone', name: 'Phone' }, { id: 'providers', name: 'Providers', minWidth: 150 }, { id: 'provider_type', name: 'Provider type', minWidth: 150 }, { id: 'created_at', name: 'Created at', width: 260 }, { id: 'last_sign_in_at', name: 'Last sign in at', width: 260 }, ] // [Joshen] Just naming it as V2 as its a rewrite of the old one, to make it easier for reviews // Can change it to remove V2 thereafter export const UsersV2 = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() const gridRef = useRef(null) const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled() 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 [selectedRow, setSelectedRow] = useState() const [sortByValue, setSortByValue] = useState('created_at:desc') const [ columnConfiguration, setColumnConfiguration, { isSuccess: isSuccessStorage, isError: isErrorStorage, error: errorStorage }, ] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AUTH_USERS_COLUMNS_CONFIGURATION(projectRef ?? ''), null as ColumnConfiguration[] | null ) const [sortColumn, sortOrder] = sortByValue.split(':') const { data, error, isSuccess, isLoading, isRefetching, isError, isFetchingNextPage, refetch, fetchNextPage, } = useUsersInfiniteQuery( { projectRef, connectionString: project?.connectionString, keywords: filterKeywords, filter: filter === 'all' ? undefined : filter, providers: selectedProviders, sort: sortColumn as 'created_at' | 'email' | 'phone', order: sortOrder as 'asc' | 'desc', }, { keepPreviousData: Boolean(filterKeywords), } ) const { data: countData } = useUsersCountQuery({ projectRef, connectionString: project?.connectionString, keywords: filterKeywords, filter: filter === 'all' ? undefined : filter, providers: selectedProviders, }) // eslint-disable-next-line react-hooks/exhaustive-deps const totalUsers = useMemo(() => countData?.result[0].count ?? 0, [countData?.result[0].count]) const users = useMemo(() => data?.pages.flatMap((page) => page.result), [data?.pages]) const handleScroll = (event: UIEvent) => { if (isLoading || !isAtBottom(event)) return fetchNextPage() } const clearSearch = () => { setSearch('') setFilterKeywords('') } 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 ) useEffect(() => { if ( !isRefetching && (isSuccessStorage || (isErrorStorage && (errorStorage as Error).message.includes('data is undefined'))) ) { const columns = formatUserColumns({ config: columnConfiguration ?? [], users: users ?? [], visibleColumns: selectedColumns, setSortByValue, }) setColumns(columns) if (columns.length < USERS_TABLE_COLUMNS.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]) return (
} placeholder="Search email, phone or UID" value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={(e) => { if (e.code === 'Enter') { setSearch(search.trim()) setFilterKeywords(search.trim()) } }} actions={[ search && ( Sort by created at Ascending Descending Sort by last sign in at Ascending Descending Sort by email Ascending Descending Sort by phone Ascending Descending
{isNewAPIDocsEnabled && }
{ const isSelected = idx === selectedRow return [ `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', '[&>.rdg-cell:first-child>div]:ml-4', ].join(' ') }} onScroll={handleScroll} 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(idx, props) { return ( { if (typeof idx === 'number' && idx >= 0) { setSelectedRow(idx) gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) } }} /> ) }, noRowsFallback: isLoading ? (
) : isError ? (
) : (

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

{filter !== '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'}

), }} />
{selectedRow !== undefined && ( setSelectedRow(undefined)} /> )}
{isLoading || isRefetching ? 'Loading users...' : `Total: ${totalUsers} users`} {(isLoading || isRefetching || isFetchingNextPage) && ( Loading... )}
) }