mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
## Problem Some aria-label and Tooltip are missing for icon-only buttons. The components are used in - project settings / infrastructure - integrations / data API / settings ## Solution Improving accessibility by adding `aria-label` and `Tooltip`. <!-- ## Preview links If relevant, include links to changed pages for easy review access. Copy the preview base URL from the Vercel bot comment on this PR. Use the following table as an example template. | Site | Live | Preview | Search for | | -------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------- | | WWW | [/blog/your-post](https://supabase.com/blog/your-post) | [/blog/your-post](https://zone-www-dot-com-git-branch-name-supabase.vercel.app/blog/your-post) | unique phrase from the change | | Docs | [/docs/guides/your-page](https://supabase.com/docs/guides/your-page) | [/docs/guides/your-page](https://docs-git-branch-name-supabase.vercel.app/docs/guides/your-page) | unique phrase from the change | | Studio | [/dashboard](https://supabase.com/dashboard) | [/dashboard](https://studio-git-branch-name-supabase.vercel.app/dashboard) | unique phrase from the change | | Design system | [/design-system](https://supabase.com/design-system) | [/design-system](https://design-system-git-branch-name-supabase.vercel.app/design-system) | unique phrase from the change | | UI library | [/library](https://supabase.com/library) | [/library](https://ui-library-git-branch-name-supabase.vercel.app/library) | unique phrase from the change | | Knowledge base | [/kb/guides/your-page](https://supabase.com/kb/guides/your-page) | [/kb/guides/your-page](https://kb-git-branch-name-supabase.vercel.app/kb/guides/your-page) | unique phrase from the change | --> <!-- ## Additional context No visual changes have been made. --> ## Review instructions Provide a clear numbered procedure that the PR reviewer can walk through. 1. Open the pages where the updated components are used 2. See that the tooltips appear correctly when interacting with the relevant elements ## Checklist Check all before review: - [X] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [ ] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which references [WORD_LIST](https://github.com/supabase/supabase/blob/master/apps/docs/WORD_LIST.md) and the docs [CONTRIBUTING](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Accessibility** * API function, schema, and table selectors have clear accessible names and tooltips. * Selector status summaries are announced politely by screen readers, including schema loading states and available counts. * **UI** * Simplified the layout of the “Automatically expose new tables” setting without changing its behavior. * Corrected the transition styling on the infrastructure flow button; its reduced-motion behavior remains unchanged. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
227 lines
8.0 KiB
TypeScript
227 lines
8.0 KiB
TypeScript
import { Check, ChevronsUpDown } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import {
|
|
Button,
|
|
cn,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
ScrollArea,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { getExposedSchemaCounts } from './ExposedSchemaSelector.utils'
|
|
import { useSchemasQuery } from '@/data/database/schemas-query'
|
|
import {
|
|
MULTIGRES_SCHEMA_NAME,
|
|
useHighAvailability,
|
|
useSchemasFilteredForHighAvailability,
|
|
} from '@/hooks/misc/useHighAvailability'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
|
|
import { pluralize } from '@/lib/helpers'
|
|
|
|
/**
|
|
* [Joshen] This would only affect graphql_public and pgmq_public, given that they're intended
|
|
* to be public, we can let users expose them via the API, but not let them adjust the schema via the dashboard
|
|
* */
|
|
export const internalSchemasCannotExpose = new Set(
|
|
INTERNAL_SCHEMAS.filter((x) => !x.endsWith('_public'))
|
|
)
|
|
|
|
interface ExposedSchemaSelectorProps {
|
|
/**
|
|
* When true the dropdown can still be opened to inspect which schemas are exposed (e.g.
|
|
* self-hosted, where schemas are managed via PGRST_DB_SCHEMAS), but schemas can't be toggled.
|
|
*/
|
|
readOnly?: boolean
|
|
selectedSchemas: string[]
|
|
onToggleSchema: (schema: string) => void
|
|
}
|
|
|
|
export const ExposedSchemaSelector = ({
|
|
readOnly = false,
|
|
selectedSchemas,
|
|
onToggleSchema,
|
|
}: ExposedSchemaSelectorProps) => {
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const { isHighAvailability } = useHighAvailability()
|
|
|
|
const {
|
|
data: allSchemas,
|
|
isPending,
|
|
isError,
|
|
isSuccess,
|
|
} = useSchemasQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
|
|
const visibleSchemas = useSchemasFilteredForHighAvailability(allSchemas)
|
|
const schemas = useMemo(
|
|
() =>
|
|
visibleSchemas
|
|
.filter((s) => !internalSchemasCannotExpose.has(s.name))
|
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
[visibleSchemas]
|
|
)
|
|
|
|
// Persisted selections go through the same HA filtering as the schema list, so a
|
|
// multigres schema exposed in the config doesn't render as a "missing" schema row.
|
|
const visibleSelectedSchemas = useMemo(
|
|
() =>
|
|
isHighAvailability
|
|
? selectedSchemas.filter((schema) => schema !== MULTIGRES_SCHEMA_NAME)
|
|
: selectedSchemas,
|
|
[selectedSchemas, isHighAvailability]
|
|
)
|
|
|
|
const missingExposedSchema = useMemo(
|
|
() => visibleSelectedSchemas.filter((schema) => !schemas.some((s) => s.name === schema)),
|
|
[schemas, visibleSelectedSchemas]
|
|
)
|
|
|
|
const selectedSet = useMemo(() => new Set(visibleSelectedSchemas), [visibleSelectedSchemas])
|
|
const { selectedCount, totalCount } = getExposedSchemaCounts({
|
|
visibleSchemas: schemas.map((s) => s.name),
|
|
selectedSchemas: visibleSelectedSchemas,
|
|
protectedSchemas: internalSchemasCannotExpose,
|
|
})
|
|
|
|
let schemasSummary: string
|
|
|
|
if (isSuccess) {
|
|
schemasSummary = `${selectedCount} of ${totalCount} ${pluralize(totalCount, 'schema')} exposed`
|
|
} else {
|
|
schemasSummary = 'Loading schemas...'
|
|
}
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen} modal={false}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
size="small"
|
|
className="w-full [&>span]:w-full pr-1! space-x-1"
|
|
iconRight={
|
|
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
|
|
}
|
|
aria-label="Select schemas"
|
|
// Tooltip repeats the label; the description would read the name twice
|
|
aria-describedby={undefined}
|
|
>
|
|
<div className="w-full flex gap-1">
|
|
<p className="text-foreground-lighter" aria-live="polite">
|
|
{schemasSummary}
|
|
</p>
|
|
</div>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" align="end">
|
|
Select schemas
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<PopoverContent
|
|
className="p-0 min-w-[200px] pointer-events-auto"
|
|
side="bottom"
|
|
align="start"
|
|
sameWidthAsTrigger
|
|
>
|
|
<Command>
|
|
<CommandInput className="text-xs" placeholder="Find schema..." />
|
|
<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 schemas</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<CommandEmpty>
|
|
<p className="text-xs text-center text-foreground-lighter py-3">
|
|
No schemas found
|
|
</p>
|
|
</CommandEmpty>
|
|
<ScrollArea className={schemas.length > 7 ? 'h-[210px]' : ''}>
|
|
{missingExposedSchema.map((schema) => (
|
|
<CommandItem
|
|
key={schema}
|
|
value={schema}
|
|
className={cn('w-full', readOnly ? 'cursor-default' : 'cursor-pointer')}
|
|
onSelect={() => {
|
|
if (readOnly) return
|
|
onToggleSchema(schema)
|
|
}}
|
|
>
|
|
<div className="w-full flex flex-col">
|
|
<div className="w-full flex items-center gap-x-2">
|
|
<Check size={16} className="text-primary shrink-0" />
|
|
<span className="truncate">{schema}</span>
|
|
</div>
|
|
{internalSchemasCannotExpose.has(schema) ? (
|
|
<span className="pl-6 text-warning text-xs tracking-tight">
|
|
This schema is protected and should not be exposed
|
|
</span>
|
|
) : (
|
|
<span className="pl-6 text-foreground-lighter text-xs tracking-tight">
|
|
This schema does not exist and can be safely removed
|
|
</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
{schemas.map((schema) => {
|
|
const isExposed = selectedSet.has(schema.name)
|
|
|
|
return (
|
|
<CommandItem
|
|
key={schema.id}
|
|
value={schema.name}
|
|
className={cn('w-full', readOnly ? 'cursor-default' : 'cursor-pointer')}
|
|
onSelect={() => {
|
|
if (readOnly) return
|
|
onToggleSchema(schema.name)
|
|
}}
|
|
>
|
|
<div
|
|
className={cn('w-full flex items-center gap-x-2', !isExposed && 'ml-6')}
|
|
>
|
|
{isExposed && <Check size={16} className="text-primary shrink-0" />}
|
|
<span className="truncate">{schema.name}</span>
|
|
</div>
|
|
</CommandItem>
|
|
)
|
|
})}
|
|
</ScrollArea>
|
|
</>
|
|
)}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|