import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { Edit, MoreVertical, Plus, Power, PowerOff, Search, Trash, X } from 'lucide-react' import { parseAsBoolean, parseAsStringLiteral, useQueryState } from 'nuqs' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { Badge, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, HoverCard, HoverCardContent, HoverCardTrigger, InputGroup, InputGroupAddon, InputGroupInput, Table, TableBody, TableCell, TableHead, TableHeader, TableHeadSort, TableRow, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { CreateOrUpdateCustomProviderSheet } from './CreateOrUpdateCustomProviderSheet' import { CUSTOM_PROVIDER_ENABLED_OPTIONS, CUSTOM_PROVIDER_TYPE_OPTIONS, filterCustomProviders, getNextPlanForCustomProviders, } from './customProviders.utils' import { DeleteCustomProviderModal } from './DeleteCustomProviderModal' import { DisableCustomProviderModal } from './DisableCustomProviderModal' import AlertError from '@/components/ui/AlertError' import { FilterPopover } from '@/components/ui/FilterPopover' import { Shortcut } from '@/components/ui/Shortcut' import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useOAuthCustomProviderUpdateMutation } from '@/data/oauth-custom-providers/oauth-custom-provider-update-mutation' import { useOAuthCustomProvidersQuery } from '@/data/oauth-custom-providers/oauth-custom-providers-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { onSearchInputEscape } from '@/lib/keyboard' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' const CUSTOM_PROVIDERS_SORT_VALUES = [ 'name:asc', 'name:desc', 'identifier:asc', 'identifier:desc', 'provider_type:asc', 'provider_type:desc', 'created_at:asc', 'created_at:desc', ] as const type CustomProvidersSort = (typeof CUSTOM_PROVIDERS_SORT_VALUES)[number] type CustomProvidersSortColumn = CustomProvidersSort extends `${infer Column}:${string}` ? Column : unknown type CustomProvidersSortOrder = CustomProvidersSort extends `${string}:${infer Order}` ? Order : unknown const NewProviderButton = ({ canCreateProvider, setShowCreateSheet, }: { canCreateProvider: boolean setShowCreateSheet: (show: boolean) => void }) => { return ( ) } export const CustomAuthProvidersList = () => { const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() const { data: authConfig, isPending: isAuthConfigLoading } = useAuthConfigQuery({ projectRef }) const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef }) const nextPlan = getNextPlanForCustomProviders(organization?.plan?.id) const isCustomProvidersEnabled = !!authConfig?.CUSTOM_OAUTH_ENABLED const providerLimit = authConfig?.CUSTOM_OAUTH_MAX_PROVIDERS || 0 const queryClient = useQueryClient() const { mutate: updateAuthConfig, isPending: isEnabling } = useAuthConfigUpdateMutation({ onSuccess: () => { toast.success('Custom providers have been enabled') // Invalidate and refetch custom providers query so it retries after enabling queryClient.invalidateQueries({ queryKey: ['projects', projectRef, 'oauth-custom-providers'], }) }, }) const { mutate: updateCustomProvider } = useOAuthCustomProviderUpdateMutation({ onSuccess: () => { toast.success('Custom provider enabled') }, }) const handleEnableCustomProviders = () => { if (projectRef) { updateAuthConfig({ projectRef, config: { CUSTOM_OAUTH_ENABLED: true } }) } } const [selectedProviderToEdit, setSelectedProviderToEdit] = useState(null) const [selectedProviderToDelete, setSelectedProviderToDelete] = useState(null) const [selectedProviderToDisable, setSelectedProviderToDisable] = useState(null) const [filteredProviderTypes, setFilteredProviderTypes] = useState([]) const [filteredEnabledStatuses, setFilteredEnabledStatuses] = useState([]) const searchInputRef = useRef(null) const { data: customProviders, isLoading: isPending, isError, error, } = useOAuthCustomProvidersQuery({ projectRef }) const providerCount = customProviders?.length ?? 0 const atProviderLimit = providerLimit !== Infinity && providerCount >= providerLimit const [showCreateSheet, setShowCreateSheet] = useQueryState( 'new', parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) ) // Prevent opening the create sheet if custom providers are disabled or at plan limit useEffect(() => { if (showCreateSheet && (!isCustomProvidersEnabled || atProviderLimit)) { setShowCreateSheet(false) } }, [showCreateSheet, atProviderLimit, isCustomProvidersEnabled, setShowCreateSheet]) const [filterString, setFilterString] = useState('') const [sort, setSort] = useQueryState( 'sort', parseAsStringLiteral(CUSTOM_PROVIDERS_SORT_VALUES).withDefault('name:asc') ) const providerToEdit = useMemo( () => customProviders?.find((p) => p.id === selectedProviderToEdit), [customProviders, selectedProviderToEdit] ) const providerToDelete = useMemo( () => customProviders?.find((p) => p.id === selectedProviderToDelete), [customProviders, selectedProviderToDelete] ) const providerToDisable = useMemo( () => customProviders?.find((p) => p.id === selectedProviderToDisable), [customProviders, selectedProviderToDisable] ) const filteredAndSortedCustomProviders = useMemo(() => { const filtered = filterCustomProviders({ providers: customProviders ?? [], searchString: filterString, providerTypes: filteredProviderTypes, enabledStatuses: filteredEnabledStatuses, }) const [sortCol, sortOrder] = sort.split(':') as [ CustomProvidersSortColumn, CustomProvidersSortOrder, ] const orderMultiplier = sortOrder === 'asc' ? 1 : -1 return filtered.sort((a, b) => { if (sortCol === 'name') { return a.name.localeCompare(b.name) * orderMultiplier } if (sortCol === 'identifier') { return a.identifier.localeCompare(b.identifier) * orderMultiplier } if (sortCol === 'provider_type') { return a.provider_type.localeCompare(b.provider_type) * orderMultiplier } if (sortCol === 'created_at') { return ( (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier ) } return 0 }) }, [customProviders, filterString, filteredProviderTypes, filteredEnabledStatuses, sort]) const hasActiveFilters = filterString.length > 0 || filteredProviderTypes.length > 0 || filteredEnabledStatuses.length > 0 const handleResetFilters = () => { setFilterString('') setFilteredProviderTypes([]) setFilteredEnabledStatuses([]) } useShortcut( SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => { searchInputRef.current?.focus() searchInputRef.current?.select() }, { label: 'Search custom providers' } ) useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, handleResetFilters) const handleSortChange = (column: CustomProvidersSortColumn) => { const [currentCol, currentOrder] = sort.split(':') as [ CustomProvidersSortColumn, CustomProvidersSortOrder, ] if (currentCol === column) { // Cycle through: asc -> desc -> no sort (default) if (currentOrder === 'asc') { setSort(`${column}:desc` as CustomProvidersSort) } else { // Reset to default sort (name:asc) setSort('name:asc') } } else { // New column, start with asc setSort(`${column}:asc` as CustomProvidersSort) } } const isCreateOrUpdateSheetVisible = isCustomProvidersEnabled && (showCreateSheet || !!providerToEdit) const canCreateProvider = isCustomProvidersEnabled && !atProviderLimit if (isAuthConfigLoading || (isCustomProvidersEnabled && isPending)) { return } if (!isCustomProvidersEnabled) { return ( ) } if (isError) { if (error?.message?.includes('Custom providers are not enabled')) { return ( ) } return } return ( <>
setFilterString(e.target.value)} onKeyDown={onSearchInputEscape(filterString, setFilterString)} /> {hasActiveFilters && (
{!isCustomProvidersEnabled || atProviderLimit ? ( {!isCustomProvidersEnabled ? (

Custom providers are not enabled for this project.

) : ( <>

You've reached the limit of {providerLimit} providers for your plan.

)}
) : ( setShowCreateSheet(true)} side="bottom" > )}
Name Identifier Type Enabled
{filteredAndSortedCustomProviders.length === 0 && (

No custom providers found

)} {filteredAndSortedCustomProviders.length > 0 && filteredAndSortedCustomProviders.map((provider) => ( {provider.identifier} {provider.provider_type} {provider.enabled ? 'Enabled' : 'Disabled'}
))}
{ setShowCreateSheet(false) setSelectedProviderToEdit(null) }} /> setSelectedProviderToDelete(null)} /> setSelectedProviderToDisable(null)} /> ) }