Files
Danny White 476d4a5851 refactor(ui): drop redundant Button variant="default" props (#50161)
## What kind of change does this PR introduce?

Mechanical cleanup on top of the Button default-variant change (#50160).

## What is the current behavior?

Many callsites still pass `variant="default"` even though that is now
the component default.

## What is the new behavior?

Removes redundant static `variant="default"` from legacy `Button` and
`ButtonTooltip` callsites. Keeps explicit defaults where they document
the API:

- `button-default.tsx` and `button-sizes.tsx` demos
- `DocsButton`, which pins neutral styling at the wrapper boundary

## To test

Studio:

- [Auth → Rate
Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits):
dirty the form so Cancel appears; Cancel stays neutral, Save stays green
- [Project Settings → API
Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys):
`DocsButton` in the header actions stays neutral

Design system:

- [Design system →
Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button):
`button-default` / `button-sizes` still show explicit default styling;
Primary (green) is restricted to the Primary section (and `asChild`)

WWW:

- [www → Brand
assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets):
Download logo kit / Download button kit stay neutral
2026-09-11 17:05:26 +10:00

299 lines
12 KiB
TypeScript

import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks'
import { Check, ChevronsUpDown, CircleAlert, Info } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
Button,
cn,
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
ScrollArea,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { exposedTableCountsQueryOptions } from '@/data/privileges/exposed-table-counts-query'
import { exposedTablesInfiniteQueryOptions } from '@/data/privileges/exposed-tables-infinite-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { pluralize } from '@/lib/helpers'
interface ExposedTableSelectorProps {
disabled?: boolean
selectedSchemas: string[]
pendingAddTableIds: number[]
pendingRemoveTableIds: number[]
onTogglePendingAdd: (tableId: number) => void
onTogglePendingRemove: (tableId: number) => void
}
export const ExposedTableSelector = ({
disabled = false,
selectedSchemas,
pendingAddTableIds,
pendingRemoveTableIds,
onTogglePendingAdd,
onTogglePendingRemove,
}: ExposedTableSelectorProps) => {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 300)
const { data: project } = useSelectedProjectQuery()
const scrollRootRef = useRef<HTMLDivElement | null>(null)
const [sentinelRef, entry] = useIntersectionObserver({
root: scrollRootRef.current,
threshold: 0,
rootMargin: '0px',
})
const { data: countsData, isPending: isCountsPending } = useQuery({
...exposedTableCountsQueryOptions({
projectRef: project?.ref,
connectionString: project?.connectionString,
selectedSchemas,
}),
placeholderData: keepPreviousData,
})
const pendingCount = pendingAddTableIds.length + pendingRemoveTableIds.length
const totalCount = countsData?.total_count ?? 0
const grantsCount = countsData?.grants_count ?? 0
const { data, isPending, isError, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage } =
useInfiniteQuery({
...exposedTablesInfiniteQueryOptions({
projectRef: project?.ref,
connectionString: project?.connectionString,
search: search.length === 0 ? undefined : debouncedSearch || undefined,
}),
placeholderData: search.length > 0 ? keepPreviousData : undefined,
})
const tables = useMemo(() => data?.pages.flatMap((page) => page.tables) ?? [], [data?.pages])
const pendingAddSet = useMemo(() => new Set(pendingAddTableIds), [pendingAddTableIds])
const pendingRemoveSet = useMemo(() => new Set(pendingRemoveTableIds), [pendingRemoveTableIds])
useEffect(() => {
if (!isPending && !isFetching && entry?.isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
}, [entry?.isIntersecting, hasNextPage, isFetching, isFetchingNextPage, isPending, fetchNextPage])
return (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<Button
size="small"
disabled={disabled}
className="w-full [&>span]:w-full pr-1! space-x-1"
iconRight={<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />}
>
<div className="w-full flex gap-1">
<p className="text-foreground-lighter">
{isCountsPending
? 'Loading tables...'
: totalCount === 0
? 'No tables available'
: `${grantsCount} of ${totalCount} tables exposed${
pendingCount > 0
? `, ${pendingCount} pending ${pluralize(pendingCount, 'change')}`
: ''
}`}
</p>
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 min-w-[200px] pointer-events-auto"
side="bottom"
align="start"
sameWidthAsTrigger
>
<Command shouldFilter={false}>
<CommandInput
className="text-xs"
placeholder="Find table..."
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandGroup>
{isPending ? (
<>
<div className="px-2 py-1">
<ShimmeringLoader className="py-2" />
</div>
<div className="px-2 py-1 w-4/5">
<ShimmeringLoader className="py-2" />
</div>
</>
) : isError ? (
<div className="flex items-center py-3 justify-center">
<p className="text-xs text-foreground-lighter">Failed to retrieve tables</p>
</div>
) : (
<>
{tables.length === 0 && (
<p className="text-xs text-center text-foreground-lighter py-3">
{search.length > 0 ? 'No tables found' : 'No tables available'}
</p>
)}
<ScrollArea ref={scrollRootRef} className={tables.length > 7 ? 'h-[210px]' : ''}>
{tables.map((table) => {
const isSchemaExposed = selectedSchemas.includes(table.schema)
const hasPendingAdd = pendingAddSet.has(table.id)
const hasPendingRemove = pendingRemoveSet.has(table.id)
const isCustomTable = table.status === 'custom'
const isGranted = table.status === 'granted'
const isCustomNeutral = isCustomTable && !hasPendingAdd && !hasPendingRemove
const isExposed =
isSchemaExposed &&
(isCustomTable
? hasPendingAdd
: isGranted
? !hasPendingRemove
: hasPendingAdd)
const customGrantsTooltip = getCustomGrantsTooltip({
hasPendingAdd,
hasPendingRemove,
})
return (
<CommandItem
key={table.id}
value={`${table.schema}.${table.name}-${table.id}`}
className={cn(
'w-full',
isSchemaExposed ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed!'
)}
onSelect={() => {
if (!isSchemaExposed) return
if (isCustomTable) {
if (hasPendingAdd) {
onTogglePendingAdd(table.id)
onTogglePendingRemove(table.id)
} else if (hasPendingRemove) {
onTogglePendingRemove(table.id)
onTogglePendingAdd(table.id)
} else {
onTogglePendingAdd(table.id)
}
return
}
if (isGranted) {
onTogglePendingRemove(table.id)
} else {
onTogglePendingAdd(table.id)
}
}}
>
<div className="w-full flex items-center gap-x-2">
<div className="w-4 shrink-0 flex items-center justify-center">
{isExposed && <Check size={16} className="text-brand shrink-0" />}
{!isSchemaExposed && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
tabIndex={-1}
aria-label="Schema not exposed"
className="inline-flex items-center text-foreground-muted hover:text-foreground-light"
>
<Info size={14} />
</button>
</TooltipTrigger>
<TooltipContent side="left" className="max-w-[320px] text-xs">
The schema "{table.schema}" must be exposed before enabling this
table.
</TooltipContent>
</Tooltip>
)}
</div>
<span
className={cn(
'truncate',
(!isSchemaExposed || isCustomNeutral) && 'text-foreground-muted',
isCustomNeutral && isSchemaExposed && 'text-warning'
)}
>
{`${table.schema}.${table.name}`}
</span>
<div className="ml-auto flex items-center gap-x-2">
{isCustomTable && (
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'shrink-0 flex items-center justify-center hover:text-foreground-light',
isCustomNeutral && isSchemaExposed
? 'text-warning'
: 'text-foreground-muted'
)}
>
<CircleAlert size={14} />
</div>
</TooltipTrigger>
<TooltipContent
side="right"
className="max-w-[320px] text-xs pointer-events-none"
>
{customGrantsTooltip}
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</CommandItem>
)
})}
<div ref={sentinelRef} className="h-1 -mt-1" />
{hasNextPage && (
<div className="px-2 py-1">
<ShimmeringLoader className="py-2" />
</div>
)}
</ScrollArea>
</>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
const getCustomGrantsTooltip = ({
hasPendingAdd,
hasPendingRemove,
}: {
hasPendingAdd: boolean
hasPendingRemove: boolean
}) => {
if (hasPendingAdd) {
return 'This table has custom grants. Saving will override them with standard Data API grants for anon, authenticated, and service_role. Select again to revoke all grants instead.'
}
if (hasPendingRemove) {
return 'This table has custom grants. Saving will revoke all grants for anon, authenticated, and service_role. Select again to override with standard Data API grants instead.'
}
return 'This table has custom grants. Select it to override with standard Data API grants for anon, authenticated, and service_role.'
}