mirror of
https://github.com/supabase/supabase.git
synced 2026-06-11 15:10:18 +08:00
## What kind of change does this PR introduce? Feature, bug fix, and docs update. Addresses the AlertDialog async action behaviour discussed in Slack and follow-up PR feedback. ## What is the current behavior? `AlertDialogAction` delegates directly to Radix, so confirm actions close the dialog immediately on click. Async mutation flows have to use `asChild` with `event.preventDefault()` and a custom loading button to keep the dialog open while work is in flight. ## What is the new behavior? - `AlertDialogAction` now accepts async handlers and a controlled `loading` prop. Promise-returning actions keep the dialog open, show the existing Button loading state, disable cancel/dismissal while pending, close on success, and stay open on rejection. - Existing workaround usages in Studio have been migrated to the direct action API (see 'To test' callsite list below) - design-system docs now include async action examples and `AlertDialogBody` guidance for inline feedback https://github.com/user-attachments/assets/1af66410-e9f9-4231-9c6d-fe650bd717a4 ## Additional context - [ ] Once #45572 is rebased onto this change, `ResetTemplateDialog` should use `AlertDialogAction loading={isResettingTemplate}` with a promise-returning reset handler instead of a plain loading `Button` in `AlertDialogFooter`. ## To test - [x] On Studio API Keys settings, use a project with no publishable or secret API keys, click the “Create API keys” banner action, and confirm the Alert Dialog stays open with loading until the default publishable and secret keys are created. - [x] Delete a JIT database access rule and confirm the Alert Dialog stays open with loading until deletion succeeds, and stays open with inline feedback if it fails. - [x] With temporary access disabled and existing rules configured, enable temporary access and confirm the “This will activate existing rules” Alert Dialog stays open with loading until the configuration update succeeds, and stays open with inline feedback if it fails. - [x] Disable external replication and confirm the Alert Dialog stays open with loading until the mutation succeeds. - [x] Enable Index Advisor and confirm the Alert Dialog stays open with loading until the mutation succeeds, and stays open with inline feedback if it fails. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Alert dialogs support async actions with built-in loading, dismissal blocking while pending, and preserved dialog on error. * Two interactive examples demonstrating async success and error flows. * **Improvements** * Dialogs now surface inline error messages and consistent loading/confirm behavior across flows (create keys, replication, JIT DB access, index advisor). * Minor UI refinements for action controls. * **Documentation** * Docs updated with async-action guidance and inline-error recommendations. * **Tests** * New test suite validating async dialog behaviors. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45960) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
214 lines
7.6 KiB
TypeScript
214 lines
7.6 KiB
TypeScript
import { EllipsisVertical, Pencil, Plus, Trash2 } from 'lucide-react'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Skeleton,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
|
|
import type { JitUserRule } from './JitDbAccess.types'
|
|
import { getJitStatusDisplay } from './JitDbAccess.utils'
|
|
|
|
interface JitDbAccessRulesTableProps {
|
|
users: JitUserRule[]
|
|
isLoading?: boolean
|
|
canUpdate: boolean
|
|
disableActions?: boolean
|
|
allProjectMembersHaveRules?: boolean
|
|
onAddRule: () => void
|
|
onEditRule: (user: JitUserRule) => void
|
|
onDeleteRule: (user: JitUserRule) => void
|
|
}
|
|
|
|
export function JitDbAccessRulesTable({
|
|
users,
|
|
isLoading = false,
|
|
canUpdate,
|
|
disableActions = false,
|
|
allProjectMembersHaveRules = false,
|
|
onAddRule,
|
|
onEditRule,
|
|
onDeleteRule,
|
|
}: JitDbAccessRulesTableProps) {
|
|
const addDisabled = disableActions || !canUpdate || allProjectMembersHaveRules
|
|
const addRuleTooltip = !canUpdate
|
|
? 'Additional permissions required'
|
|
: allProjectMembersHaveRules
|
|
? 'All project members already have temporary access rules'
|
|
: undefined
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="space-y-4 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-4 w-32" />
|
|
<Skeleton className="h-4 w-64" />
|
|
</div>
|
|
<Skeleton className="h-9 w-24" />
|
|
</div>
|
|
<Skeleton className="h-32 w-full" />
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent className="space-y-4 p-0">
|
|
<div className="flex items-center justify-between px-4 pb-2 pt-6">
|
|
<div>
|
|
<h3 className="text-sm text-foreground">Temporary access rules</h3>
|
|
<p className="text-sm text-foreground-light">
|
|
Manage member access, allowed roles, and expiry settings.
|
|
</p>
|
|
</div>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="inline-flex">
|
|
<Button type="default" icon={<Plus />} onClick={onAddRule} disabled={addDisabled}>
|
|
Add rule
|
|
</Button>
|
|
</span>
|
|
</TooltipTrigger>
|
|
{addRuleTooltip && <TooltipContent side="bottom">{addRuleTooltip}</TooltipContent>}
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<Table className="border-t">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Member</TableHead>
|
|
<TableHead>Roles</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="w-1">
|
|
<span className="sr-only">Actions</span>
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{users.length === 0 ? (
|
|
<TableRow className="[&>td]:hover:bg-inherit">
|
|
<TableCell colSpan={4}>
|
|
<p className="text-sm text-foreground">No rules yet</p>
|
|
<p className="text-sm text-foreground-lighter">
|
|
Add your first temporary access rule above
|
|
</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
users.map((user) => {
|
|
const statusDisplay = getJitStatusDisplay(user.status)
|
|
const enabledGrants = user.grants.filter((grant) => grant.enabled)
|
|
const rowIsInteractive = canUpdate && !disableActions
|
|
|
|
return (
|
|
<TableRow
|
|
key={user.id}
|
|
className={rowIsInteractive ? 'relative inset-focus cursor-pointer' : undefined}
|
|
onClick={
|
|
rowIsInteractive
|
|
? (event) => {
|
|
if ((event.target as HTMLElement).closest('button')) return
|
|
onEditRule(user)
|
|
}
|
|
: undefined
|
|
}
|
|
onKeyDown={
|
|
rowIsInteractive
|
|
? (event) => {
|
|
if ((event.target as HTMLElement).closest('button')) return
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault()
|
|
onEditRule(user)
|
|
}
|
|
}
|
|
: undefined
|
|
}
|
|
tabIndex={rowIsInteractive ? 0 : undefined}
|
|
>
|
|
<TableCell className="text-sm">
|
|
{user.name && <p>{user.name}</p>}
|
|
<p className="text-foreground-lighter">{user.email}</p>
|
|
</TableCell>
|
|
<TableCell className="text-sm text-foreground-light">
|
|
{enabledGrants.length} role{enabledGrants.length === 1 ? '' : 's'}
|
|
</TableCell>
|
|
<TableCell className="text-sm text-foreground-light">
|
|
{statusDisplay.badges.length > 0 ? (
|
|
<span className="flex flex-wrap gap-1.5">
|
|
{statusDisplay.badges.map((badge) => (
|
|
<Badge key={badge.label} variant={badge.variant}>
|
|
{badge.label}
|
|
</Badge>
|
|
))}
|
|
</span>
|
|
) : null}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
icon={<EllipsisVertical />}
|
|
aria-label="More actions"
|
|
type="default"
|
|
size="tiny"
|
|
className="w-7 hit-area-2"
|
|
disabled={!canUpdate || disableActions}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" side="bottom" className="w-40">
|
|
<DropdownMenuItem
|
|
className="gap-x-2"
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
onEditRule(user)
|
|
}}
|
|
disabled={!canUpdate || disableActions}
|
|
>
|
|
<Pencil size={14} className="text-foreground-lighter" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
className="gap-x-2"
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
onDeleteRule(user)
|
|
}}
|
|
disabled={!canUpdate || disableActions}
|
|
>
|
|
<Trash2 size={14} className="text-foreground-lighter" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|