import { PermissionAction } from '@supabase/shared-types/out/constants' import { partition, sortBy } from 'lodash' import { Plus, Search, X } from 'lucide-react' import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' import { useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { Badge, Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { CreateRolePanel } from './CreateRolePanel' import { RoleRow } from './RoleRow' import { RoleRowSkeleton } from './RoleRowSkeleton' import { BRIVEN_ROLES } from './Roles.constants' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { NoSearchResults } from '@/components/ui/NoSearchResults' import { Shortcut } from '@/components/ui/Shortcut' import { SparkBar } from '@/components/ui/SparkBar' import { useDatabaseRoleDeleteMutation } from '@/data/database-roles/database-role-delete-mutation' import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' import { useMaxConnectionsQuery } from '@/data/database/max-connections-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { onSearchInputEscape } from '@/lib/keyboard' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' type BRIVEN_ROLE = (typeof BRIVEN_ROLES)[number] export const RolesList = () => { const { data: project } = useSelectedProjectQuery() const [filterString, setFilterString] = useState('') const [filterType, setFilterType] = useState<'all' | 'active'>('all') const searchInputRef = useRef(null) const { can: canUpdateRoles } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'roles' ) useShortcut( SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => { searchInputRef.current?.focus() searchInputRef.current?.select() }, { label: 'Search roles' } ) useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => { setFilterString('') setFilterType('all') }) const { data: maxConnData } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const maxConnectionLimit = maxConnData?.maxConnections const { data, isPending: isLoading, isSuccess, } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { mutate: deleteDatabaseRole, isPending: isDeleting, isSuccess: isSuccessDelete, } = useDatabaseRoleDeleteMutation({ onSuccess: () => { toast.success(`Successfully deleted role`) setSelectedRoleIdToDelete(null) }, }) const [isCreatingRole, setIsCreatingRole] = useQueryState( 'new', parseAsBoolean.withDefault(false) ) const [selectedRoleIdToDelete, setSelectedRoleIdToDelete] = useQueryState('delete', parseAsString) const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase()) const roleToDelete = roles?.find((role) => role.id.toString() === selectedRoleIdToDelete) const filteredRoles = ( filterType === 'active' ? roles.filter((role) => role.activeConnections > 0) : roles ).filter((role) => role.name.includes(filterString)) const [brivenRoles, otherRoles] = partition(filteredRoles, (role) => BRIVEN_ROLES.includes(role.name as BRIVEN_ROLE) ) const totalActiveConnections = roles .map((role) => role.activeConnections) .reduce((a, b) => a + b, 0) // order the roles with active connections by number of connections, most connections first const rolesWithActiveConnections = sortBy( roles.filter((role) => role.activeConnections > 0), (r) => -r.activeConnections ) const deleteRole = async () => { if (!project) return console.error('Project is required') if (!roleToDelete) return console.error('Failed to delete role: role is missing') deleteDatabaseRole({ projectRef: project.ref, connectionString: project.connectionString, id: roleToDelete.id, }) } useEffect(() => { if (isSuccess && !!selectedRoleIdToDelete && !roleToDelete && !isSuccessDelete) { toast('Role cannot be found') setSelectedRoleIdToDelete(null) } }, [isSuccess, selectedRoleIdToDelete, roleToDelete, isSuccessDelete, setSelectedRoleIdToDelete]) return ( <>
} value={filterString} onChange={(event) => setFilterString(event.target.value)} onKeyDown={onSearchInputEscape(filterString, setFilterString)} actions={ filterString && ( ) } />
0.9 * maxConnectionLimit ? 'bg-destructive' : totalActiveConnections > 0.75 * maxConnectionLimit ? 'bg-warning' : undefined } labelTop={ Number.isInteger(maxConnectionLimit) ? `${totalActiveConnections}/${maxConnectionLimit}` : `${totalActiveConnections}` } labelTopClass="text-xs" labelBottom="Active connections" labelBottomClass="text-xs" />

Connections by roles:

{rolesWithActiveConnections.map((role) => (
{role.name}: {role.activeConnections}
))}
{canUpdateRoles ? ( setIsCreatingRole(true)} side="bottom" > ) : ( } tooltip={{ content: { side: 'bottom', text: 'You need additional permissions to add a new role', }, }} > Add role )}

Roles managed by Briven

Protected
{isLoading ? Array.from({ length: 5 }).map((_, i) => ) : brivenRoles.map((role) => ( ))}

Other database roles

{isLoading ? Array.from({ length: 3 }).map((_, i) => ) : otherRoles.map((role) => ( ))}
{filterString.length > 0 && filteredRoles.length === 0 && ( setFilterString('')} /> )} setIsCreatingRole(false)} /> setSelectedRoleIdToDelete(null)} title={`Confirm to delete role "${roleToDelete?.name}"`} onConfirm={deleteRole} >

This will automatically revoke any membership of this role in other roles, and this action cannot be undone.

) }