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>
539 lines
19 KiB
TypeScript
539 lines
19 KiB
TypeScript
import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
|
||
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
|
||
import { Loader2 } from 'lucide-react'
|
||
import Link from 'next/link'
|
||
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
|
||
import { useEffect, useMemo, useState } from 'react'
|
||
import { toast } from 'sonner'
|
||
import {
|
||
AlertDialog,
|
||
AlertDialogAction,
|
||
AlertDialogBody,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle,
|
||
Button,
|
||
Card,
|
||
CardContent,
|
||
Switch,
|
||
Tooltip,
|
||
TooltipContent,
|
||
TooltipTrigger,
|
||
} from 'ui'
|
||
import {
|
||
PageSection,
|
||
PageSectionContent,
|
||
PageSectionMeta,
|
||
PageSectionSummary,
|
||
PageSectionTitle,
|
||
} from 'ui-patterns'
|
||
import { Admonition } from 'ui-patterns/admonition'
|
||
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
|
||
|
||
import type { JitUserRule, SheetMode } from './JitDbAccess.types'
|
||
import {
|
||
getAssignableJitRoleOptions,
|
||
getJitMemberOptions,
|
||
mapJitMembersToUserRules,
|
||
} from './JitDbAccess.utils'
|
||
import { JitDbAccessDeleteDialog } from './JitDbAccessDeleteDialog'
|
||
import { JitDbAccessRuleSheet } from './JitDbAccessRuleSheet'
|
||
import { JitDbAccessRulesTable } from './JitDbAccessRulesTable'
|
||
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
||
import AlertError from '@/components/ui/AlertError'
|
||
import { DocsButton } from '@/components/ui/DocsButton'
|
||
import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
|
||
import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
|
||
import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query'
|
||
import { useJitDbAccessMembersQuery } from '@/data/jit-db-access/jit-db-access-members-query'
|
||
import { useJitDbAccessQuery } from '@/data/jit-db-access/jit-db-access-query'
|
||
import { useJitDbAccessRevokeMutation } from '@/data/jit-db-access/jit-db-access-revoke-mutation'
|
||
import { useJitDbAccessUpdateMutation } from '@/data/jit-db-access/jit-db-access-update-mutation'
|
||
import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
|
||
import { useProjectMembersQuery } from '@/data/projects/project-members-query'
|
||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||
import { DOCS_URL } from '@/lib/constants'
|
||
|
||
export const JitDbAccessConfiguration = () => {
|
||
const { ref } = useParams()
|
||
const { data: project } = useSelectedProjectQuery()
|
||
const { data: organization } = useSelectedOrganizationQuery()
|
||
|
||
const parentProjectRef = project?.parent_project_ref
|
||
|
||
const [enabled, setEnabled] = useState(false)
|
||
const [, setShowCreateRuleSheet] = useQueryState('jit_new', parseAsBoolean.withDefault(false))
|
||
const [ruleIdToEdit, setRuleIdToEdit] = useQueryState('jit_edit', parseAsString)
|
||
const [showEnableJitDialog, setShowEnableJitDialog] = useState(false)
|
||
const [enableJitError, setEnableJitError] = useState<string | null>(null)
|
||
const [selectedUserToDelete, setSelectedUserToDelete] = useState<JitUserRule | null>(null)
|
||
const [deleteRuleError, setDeleteRuleError] = useState<string | null>(null)
|
||
|
||
const {
|
||
data: jitDbAccessConfiguration,
|
||
error: jitDbAccessConfigurationError,
|
||
isError: isErrorJitDbAccessConfiguration,
|
||
isLoading: isLoadingConfiguration,
|
||
isSuccess: isSuccessConfiguration,
|
||
} = useJitDbAccessQuery({ projectRef: ref })
|
||
|
||
const {
|
||
data: jitMembers,
|
||
error: jitMembersError,
|
||
isError: isErrorJitMembers,
|
||
isLoading: isLoadingJitMembers,
|
||
} = useJitDbAccessMembersQuery({ projectRef: ref })
|
||
|
||
const { data: projectMembers, isLoading: isLoadingProjectMembers } = useProjectMembersQuery({
|
||
projectRef: ref,
|
||
})
|
||
|
||
const { data: organizationMembers, isLoading: isLoadingOrganizationMembers } =
|
||
useOrganizationMembersQuery({ slug: organization?.slug })
|
||
|
||
const { data: databaseRoles, isLoading: isLoadingDatabaseRoles } = useDatabaseRolesQuery({
|
||
projectRef: ref,
|
||
connectionString: project?.connectionString,
|
||
})
|
||
|
||
const { can: canUpdateJitDbAccess } = useAsyncCheckPermissions(
|
||
PermissionAction.UPDATE,
|
||
'projects',
|
||
{ resource: { project_id: project?.id } }
|
||
)
|
||
|
||
const { mutateAsync: updateJitDbAccess, isPending: isUpdatingJitDbAccess } =
|
||
useJitDbAccessUpdateMutation({
|
||
onSuccess: (_, variables) => {
|
||
const nextEnabled = variables.requestedConfig.state === 'enabled'
|
||
|
||
if (nextEnabled) {
|
||
toast.success('Temporary access enabled')
|
||
} else {
|
||
toast.success(
|
||
activeRuleCount > 0
|
||
? `Temporary access disabled. ${activeRuleCount} configured member${activeRuleCount === 1 ? '' : 's'} can no longer request temporary database access.`
|
||
: 'Temporary access disabled'
|
||
)
|
||
}
|
||
},
|
||
onError: () => {},
|
||
})
|
||
|
||
const { mutateAsync: revokeUserAccess, isPending: isRevokingAccess } =
|
||
useJitDbAccessRevokeMutation({
|
||
onSuccess: (_, variables) => {
|
||
toast.success('Successfully revoked user access')
|
||
setSelectedUserToDelete(null)
|
||
if (ruleIdToEdit === variables.userId) resetSheetState()
|
||
},
|
||
onError: () => {},
|
||
})
|
||
|
||
const isMutating = isUpdatingJitDbAccess || isRevokingAccess
|
||
const disableRuleActions = isMutating || isLoadingDatabaseRoles || isLoadingOrganizationMembers
|
||
const isRulesLoading = isLoadingJitMembers || isLoadingProjectMembers
|
||
|
||
const initialIsEnabled =
|
||
jitDbAccessConfiguration?.state === 'enabled'
|
||
? jitDbAccessConfiguration?.appliedSuccessfully
|
||
: false
|
||
const isJitDbAccessUnavailable = jitDbAccessConfiguration?.state === 'unavailable'
|
||
const unavailableReason = isJitDbAccessUnavailable
|
||
? jitDbAccessConfiguration.unavailableReason
|
||
: undefined
|
||
|
||
const roleOptions = useMemo(() => getAssignableJitRoleOptions(databaseRoles), [databaseRoles])
|
||
|
||
const users = useMemo(
|
||
() => mapJitMembersToUserRules(jitMembers, projectMembers, roleOptions),
|
||
[jitMembers, projectMembers, roleOptions]
|
||
)
|
||
|
||
const allMembers = useMemo(
|
||
() => getJitMemberOptions(organizationMembers, projectMembers),
|
||
[organizationMembers, projectMembers]
|
||
)
|
||
|
||
const editingUser = useMemo(
|
||
() => users.find((user) => user.id === ruleIdToEdit) ?? null,
|
||
[users, ruleIdToEdit]
|
||
)
|
||
|
||
const sheetMode: SheetMode = ruleIdToEdit ? 'edit' : 'add'
|
||
|
||
const membersWithRules = useMemo(() => new Set(users.map((user) => user.memberId)), [users])
|
||
|
||
const availableMembersForAdd = useMemo(
|
||
() => allMembers.filter((member) => !membersWithRules.has(member.id)),
|
||
[allMembers, membersWithRules]
|
||
)
|
||
|
||
const memberOptionsForSheet = useMemo(() => {
|
||
if (sheetMode !== 'edit') return availableMembersForAdd
|
||
if (!editingUser) return allMembers
|
||
|
||
if (allMembers.some((member) => member.id === editingUser.memberId)) return allMembers
|
||
|
||
return [
|
||
{
|
||
id: editingUser.memberId,
|
||
email: editingUser.email,
|
||
name: editingUser.name,
|
||
},
|
||
...allMembers,
|
||
]
|
||
}, [sheetMode, availableMembersForAdd, allMembers, editingUser])
|
||
|
||
const activeRuleCount = useMemo(
|
||
() => users.filter((user) => user.status.active > 0).length,
|
||
[users]
|
||
)
|
||
|
||
const resetSheetState = () => {
|
||
setShowCreateRuleSheet(false)
|
||
setRuleIdToEdit(null)
|
||
}
|
||
|
||
const getErrorMessage = (error: unknown) =>
|
||
error instanceof Error ? error.message : 'An unknown error occurred'
|
||
|
||
const submitJitToggle = async (nextEnabled: boolean) => {
|
||
if (!ref) throw new Error('Project ref is required')
|
||
|
||
setEnabled(nextEnabled)
|
||
|
||
try {
|
||
await updateJitDbAccess({
|
||
projectRef: ref,
|
||
requestedConfig: { state: nextEnabled ? 'enabled' : 'disabled' },
|
||
})
|
||
} catch (error) {
|
||
setEnabled(initialIsEnabled ?? false)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
const handleJitToggleChange = (checked: boolean) => {
|
||
if (isJitDbAccessUnavailable || !canUpdateJitDbAccess) return
|
||
|
||
if (checked && !enabled) {
|
||
if (activeRuleCount > 0) {
|
||
setEnableJitError(null)
|
||
return setShowEnableJitDialog(true)
|
||
}
|
||
return void submitJitToggle(true).catch((error) => {
|
||
toast.error(`Failed to update temporary access: ${getErrorMessage(error)}`)
|
||
})
|
||
}
|
||
|
||
if (!checked && enabled) {
|
||
void submitJitToggle(false).catch((error) => {
|
||
toast.error(`Failed to update temporary access: ${getErrorMessage(error)}`)
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleConfirmEnableJit = async () => {
|
||
setEnableJitError(null)
|
||
|
||
try {
|
||
await submitJitToggle(true)
|
||
} catch (error) {
|
||
setEnableJitError(getErrorMessage(error))
|
||
throw error
|
||
}
|
||
}
|
||
|
||
const openAddRuleSheet = () => {
|
||
if (!canUpdateJitDbAccess) return
|
||
setRuleIdToEdit(null)
|
||
setShowCreateRuleSheet(true)
|
||
}
|
||
|
||
const openEditRuleSheet = (user: JitUserRule) => {
|
||
if (!canUpdateJitDbAccess) return
|
||
setShowCreateRuleSheet(false)
|
||
setRuleIdToEdit(user.id)
|
||
}
|
||
|
||
const openDeleteDialog = (user: JitUserRule) => {
|
||
if (!canUpdateJitDbAccess) return
|
||
setDeleteRuleError(null)
|
||
setSelectedUserToDelete(user)
|
||
}
|
||
|
||
const handleConfirmDelete = async () => {
|
||
setDeleteRuleError(null)
|
||
|
||
try {
|
||
if (!ref) throw new Error('Project ref is required')
|
||
if (!selectedUserToDelete) throw new Error('User is required')
|
||
|
||
await revokeUserAccess({ projectRef: ref, userId: selectedUserToDelete.memberId })
|
||
} catch (error) {
|
||
setDeleteRuleError(getErrorMessage(error))
|
||
throw error
|
||
}
|
||
}
|
||
|
||
const switchDisabled = isLoadingConfiguration || isUpdatingJitDbAccess || !canUpdateJitDbAccess
|
||
const switchTooltipText = !canUpdateJitDbAccess ? 'Additional permissions required' : undefined
|
||
|
||
const showToggleFailedWarning =
|
||
isSuccessConfiguration &&
|
||
jitDbAccessConfiguration?.state !== 'unavailable' &&
|
||
!jitDbAccessConfiguration.appliedSuccessfully
|
||
|
||
const projectReference = ref ? (
|
||
<>
|
||
This project <code className="text-code-inline">{ref}</code>
|
||
</>
|
||
) : (
|
||
'This project'
|
||
)
|
||
const unavailableTitle =
|
||
unavailableReason === 'postgres_upgrade_required'
|
||
? 'Postgres upgrade required'
|
||
: unavailableReason === 'manual_migration_required'
|
||
? 'Migration required'
|
||
: 'Temporary access unavailable'
|
||
const unavailableDescription =
|
||
unavailableReason === 'postgres_upgrade_required'
|
||
? 'must be upgraded to Postgres 17 or later before temporary access can be enabled.'
|
||
: unavailableReason === 'manual_migration_required'
|
||
? 'must be migrated before temporary access can be enabled. Contact support to migrate this project.'
|
||
: 'This feature is currently unavailable for this project. Contact support if you need help enabling it.'
|
||
|
||
useEffect(() => {
|
||
if (!isLoadingConfiguration && jitDbAccessConfiguration) {
|
||
setEnabled(initialIsEnabled ?? false)
|
||
}
|
||
}, [initialIsEnabled, isLoadingConfiguration, jitDbAccessConfiguration])
|
||
|
||
return (
|
||
<>
|
||
<PageSection id="jit-db-access-configuration">
|
||
<PageSectionMeta>
|
||
<PageSectionSummary>
|
||
<PageSectionTitle>
|
||
<span className="flex items-center gap-x-4">
|
||
Temporary access
|
||
<FeaturePreviewBadge featureKey={LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS} />
|
||
</span>
|
||
</PageSectionTitle>
|
||
</PageSectionSummary>
|
||
<DocsButton href={`${DOCS_URL}/guides/platform/temporary-access`} />
|
||
</PageSectionMeta>
|
||
|
||
<PageSectionContent className="space-y-4">
|
||
{parentProjectRef && (
|
||
<Admonition
|
||
type="note"
|
||
title="Managed in the main branch"
|
||
description={
|
||
<>
|
||
Temporary access rules are configured in the main branch and apply across all
|
||
preview branches. Return to the{' '}
|
||
<InlineLink href={`/project/${parentProjectRef}/settings/database`}>
|
||
main branch
|
||
</InlineLink>{' '}
|
||
to manage your access rules.
|
||
</>
|
||
}
|
||
/>
|
||
)}
|
||
|
||
{!parentProjectRef && isErrorJitDbAccessConfiguration && (
|
||
<AlertError
|
||
projectRef={ref}
|
||
subject="Failed to load temporary access"
|
||
error={jitDbAccessConfigurationError as { message: string } | null}
|
||
showInstructions={false}
|
||
/>
|
||
)}
|
||
|
||
{!parentProjectRef && !isErrorJitDbAccessConfiguration && isJitDbAccessUnavailable && (
|
||
<Admonition
|
||
type="note"
|
||
layout="responsive"
|
||
title={unavailableTitle}
|
||
description={
|
||
unavailableReason === 'temporarily_unavailable' ? (
|
||
unavailableDescription
|
||
) : (
|
||
<>
|
||
{projectReference} {unavailableDescription}
|
||
</>
|
||
)
|
||
}
|
||
actions={
|
||
unavailableReason === 'postgres_upgrade_required' && ref ? (
|
||
<Button type="default" asChild>
|
||
<Link href={`/project/${ref}/settings/infrastructure`}>Upgrade Postgres</Link>
|
||
</Button>
|
||
) : (
|
||
<Button type="default" asChild>
|
||
<SupportLink
|
||
queryParams={{
|
||
category: SupportCategories.PROBLEM,
|
||
projectRef: ref,
|
||
subject: unavailableTitle,
|
||
}}
|
||
>
|
||
Contact support
|
||
</SupportLink>
|
||
</Button>
|
||
)
|
||
}
|
||
/>
|
||
)}
|
||
|
||
{!parentProjectRef && !isErrorJitDbAccessConfiguration && !isJitDbAccessUnavailable && (
|
||
<Card>
|
||
<CardContent className="space-y-4">
|
||
<FormLayout
|
||
layout="flex-row-reverse"
|
||
label="Allow temporary access"
|
||
description="Let project members request temporary database access."
|
||
>
|
||
<div className="flex w-fit shrink-0 items-center justify-end gap-2">
|
||
{(isLoadingConfiguration || isUpdatingJitDbAccess) && (
|
||
<Loader2
|
||
className="animate-spin text-foreground-muted/50"
|
||
strokeWidth={2}
|
||
size={16}
|
||
/>
|
||
)}
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
{/* [Joshen] Added div as tooltip is messing with data state property of toggle */}
|
||
<div>
|
||
<Switch
|
||
size="large"
|
||
checked={enabled}
|
||
onCheckedChange={handleJitToggleChange}
|
||
disabled={switchDisabled}
|
||
/>
|
||
</div>
|
||
</TooltipTrigger>
|
||
{switchTooltipText && (
|
||
<TooltipContent side="bottom">{switchTooltipText}</TooltipContent>
|
||
)}
|
||
</Tooltip>
|
||
</div>
|
||
</FormLayout>
|
||
</CardContent>
|
||
{showToggleFailedWarning && (
|
||
<Admonition
|
||
type="warning"
|
||
layout="horizontal"
|
||
title="Temporary access update didn’t apply"
|
||
description={
|
||
<>
|
||
The change didn’t apply. Try enabling or disabling temporary access again, or{' '}
|
||
<SupportLink
|
||
queryParams={{
|
||
category: SupportCategories.DASHBOARD_BUG,
|
||
subject: 'Temporary access was not updated successfully',
|
||
}}
|
||
className={InlineLinkClassName}
|
||
>
|
||
contact support
|
||
</SupportLink>{' '}
|
||
if the issue persists.
|
||
</>
|
||
}
|
||
className="mb-0 rounded-none border-0"
|
||
/>
|
||
)}
|
||
</Card>
|
||
)}
|
||
|
||
{!parentProjectRef && enabled && !isJitDbAccessUnavailable && !isUpdatingJitDbAccess && (
|
||
<>
|
||
{isErrorJitMembers && (
|
||
<AlertError
|
||
projectRef={ref}
|
||
subject="Failed to load temporary access rules"
|
||
error={jitMembersError as { message: string } | null}
|
||
showInstructions={false}
|
||
/>
|
||
)}
|
||
|
||
<JitDbAccessRulesTable
|
||
users={users}
|
||
isLoading={isRulesLoading}
|
||
canUpdate={!!canUpdateJitDbAccess}
|
||
disableActions={disableRuleActions}
|
||
allProjectMembersHaveRules={availableMembersForAdd.length === 0}
|
||
onAddRule={openAddRuleSheet}
|
||
onEditRule={openEditRuleSheet}
|
||
onDeleteRule={openDeleteDialog}
|
||
/>
|
||
</>
|
||
)}
|
||
</PageSectionContent>
|
||
</PageSection>
|
||
|
||
<JitDbAccessRuleSheet
|
||
memberOptions={memberOptionsForSheet}
|
||
membersWithRules={membersWithRules}
|
||
availableMembersForAddCount={availableMembersForAdd.length}
|
||
/>
|
||
|
||
<JitDbAccessDeleteDialog
|
||
user={selectedUserToDelete}
|
||
isDeleting={isRevokingAccess}
|
||
error={deleteRuleError}
|
||
onClose={() => {
|
||
setDeleteRuleError(null)
|
||
setSelectedUserToDelete(null)
|
||
}}
|
||
onConfirm={handleConfirmDelete}
|
||
/>
|
||
|
||
<AlertDialog open={showEnableJitDialog} onOpenChange={setShowEnableJitDialog}>
|
||
<AlertDialogContent size="small">
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>This will activate existing rules</AlertDialogTitle>
|
||
<AlertDialogDescription asChild>
|
||
<div className="text-sm">
|
||
<p>
|
||
Enabling temporary access will allow {activeRuleCount} pre-configured member
|
||
{activeRuleCount === 1 ? '' : 's'} to request temporary database access
|
||
immediately.
|
||
</p>
|
||
</div>
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
{enableJitError && (
|
||
<AlertDialogBody>
|
||
<Admonition
|
||
type="destructive"
|
||
title="Unable to enable temporary access"
|
||
description={enableJitError}
|
||
/>
|
||
</AlertDialogBody>
|
||
)}
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel disabled={isUpdatingJitDbAccess}>Cancel</AlertDialogCancel>
|
||
<AlertDialogAction
|
||
variant="warning"
|
||
loading={isUpdatingJitDbAccess}
|
||
onClick={handleConfirmEnableJit}
|
||
>
|
||
Enable temporary access
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</>
|
||
)
|
||
}
|