fix: add DeleteCertificateDialog component with confirmation dialog for certificate deletion
- Implement DeleteCertificateDialog component to handle certificate deletion confirmation. - Add tests for DeleteCertificateDialog covering various scenarios including rendering, confirmation, and cancellation. - Update translation files for multiple languages to include new strings related to certificate deletion. - Create end-to-end tests for certificate deletion UX, including button visibility, confirmation dialog, and success/failure scenarios.
This commit is contained in:
80
frontend/src/components/dialogs/DeleteCertificateDialog.tsx
Normal file
80
frontend/src/components/dialogs/DeleteCertificateDialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '../ui/Button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../ui/Dialog'
|
||||
|
||||
import type { Certificate } from '../../api/certificates'
|
||||
|
||||
interface DeleteCertificateDialogProps {
|
||||
certificate: Certificate | null
|
||||
open: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
isDeleting: boolean
|
||||
}
|
||||
|
||||
function getWarningKey(cert: Certificate): string {
|
||||
if (cert.status === 'expired') return 'certificates.deleteConfirmExpired'
|
||||
if (cert.provider === 'letsencrypt-staging') return 'certificates.deleteConfirmStaging'
|
||||
return 'certificates.deleteConfirmCustom'
|
||||
}
|
||||
|
||||
export default function DeleteCertificateDialog({
|
||||
certificate,
|
||||
open,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
isDeleting,
|
||||
}: DeleteCertificateDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!certificate) return null
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onCancel() }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('certificates.deleteTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{certificate.name || certificate.domain}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 space-y-4">
|
||||
<div className="flex items-start gap-3 rounded-lg border border-red-900/50 bg-red-900/10 p-4">
|
||||
<AlertTriangle className="h-5 w-5 shrink-0 text-red-400 mt-0.5" />
|
||||
<p className="text-sm text-gray-300">
|
||||
{t(getWarningKey(certificate))}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
<dt className="text-gray-500">{t('certificates.domain')}</dt>
|
||||
<dd className="text-white">{certificate.domain}</dd>
|
||||
<dt className="text-gray-500">{t('certificates.status')}</dt>
|
||||
<dd className="text-white capitalize">{certificate.status}</dd>
|
||||
<dt className="text-gray-500">{t('certificates.provider')}</dt>
|
||||
<dd className="text-white">{certificate.provider}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={onCancel} disabled={isDeleting}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button variant="danger" onClick={onConfirm} isLoading={isDeleting}>
|
||||
{t('certificates.deleteButton')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
import DeleteCertificateDialog from '../../dialogs/DeleteCertificateDialog'
|
||||
|
||||
import type { Certificate } from '../../../api/certificates'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: { language: 'en', changeLanguage: vi.fn() },
|
||||
}),
|
||||
}))
|
||||
|
||||
const baseCert: Certificate = {
|
||||
id: 1,
|
||||
name: 'Test Cert',
|
||||
domain: 'test.example.com',
|
||||
issuer: 'Custom CA',
|
||||
expires_at: '2026-01-01T00:00:00Z',
|
||||
status: 'valid',
|
||||
provider: 'custom',
|
||||
}
|
||||
|
||||
describe('DeleteCertificateDialog', () => {
|
||||
it('renders warning text for custom cert', () => {
|
||||
render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={baseCert}
|
||||
open={true}
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('certificates.deleteConfirmCustom')).toBeInTheDocument()
|
||||
expect(screen.getByText('certificates.deleteTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders warning text for staging cert', () => {
|
||||
const staging: Certificate = { ...baseCert, provider: 'letsencrypt-staging', status: 'untrusted' }
|
||||
render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={staging}
|
||||
open={true}
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('certificates.deleteConfirmStaging')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders warning text for expired cert', () => {
|
||||
const expired: Certificate = { ...baseCert, provider: 'letsencrypt', status: 'expired' }
|
||||
render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={expired}
|
||||
open={true}
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('certificates.deleteConfirmExpired')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onCancel when Cancel is clicked', async () => {
|
||||
const onCancel = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={baseCert}
|
||||
open={true}
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={onCancel}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'common.cancel' }))
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls onConfirm when Delete is clicked', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={baseCert}
|
||||
open={true}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={vi.fn()}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'certificates.deleteButton' }))
|
||||
expect(onConfirm).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders nothing when certificate is null', () => {
|
||||
const { container } = render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={null}
|
||||
open={true}
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
it('renders expired warning for expired staging cert (priority ordering)', () => {
|
||||
const expiredStaging: Certificate = { ...baseCert, provider: 'letsencrypt-staging', status: 'expired' }
|
||||
render(
|
||||
<DeleteCertificateDialog
|
||||
certificate={expiredStaging}
|
||||
open={true}
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
isDeleting={false}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('certificates.deleteConfirmExpired')).toBeInTheDocument()
|
||||
expect(screen.queryByText('certificates.deleteConfirmStaging')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user