Files
certctl/web/src/pages/DigestPage.tsx
T
Shankar 08ffbadb97 M-029 Pass 1 batch 1: migrate 4 single-mutation pages to useTrackedMutation
Drains the Bundle 8 useMutation backlog (56 -> 52). Each migration declares

explicit invalidates per the M-009 contract; the wrapper invalidates BEFORE

calling the caller's onSuccess so user code drops the redundant qc.invalidateQueries.

Pages migrated:

  - AgentsPage.tsx        invalidates: [['agents'], ['agents', 'retired']]

  - CertificatesPage.tsx  invalidates: [['certificates']]

  - DigestPage.tsx        invalidates: 'noop' (sendDigest is a server-side email

                            dispatch — no client query reflects digest-send state)

  - IssuerDetailPage.tsx  invalidates: [['issuer', id]] (testIssuerConnection

                            updates last_tested_at server-side)

Verification:

  legacy useMutation count   56 -> 52 (-4 sites)

  useTrackedMutation count    0 ->  4 (+4 sites)

  invalidation surface      82 -> 84 (+2; DigestPage is noop, AgentsPage

                                  collapses 2 invalidates into 1 array, others +1)

Closes 4 of 56 sites toward M-029 Pass 1 completion.
2026-04-27 02:37:25 +00:00

114 lines
4.2 KiB
TypeScript

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTrackedMutation } from '../hooks/useTrackedMutation';
import { previewDigest, sendDigest } from '../api/client';
import PageHeader from '../components/PageHeader';
import ErrorState from '../components/ErrorState';
export default function DigestPage() {
const [showConfirm, setShowConfirm] = useState(false);
const { data: html, isLoading, error, refetch } = useQuery({
queryKey: ['digest-preview'],
queryFn: previewDigest,
retry: false,
});
const sendMutation = useTrackedMutation({
mutationFn: sendDigest,
invalidates: 'noop',
noopReason: 'sendDigest dispatches an email server-side; no cached client query reflects digest-send state',
onSuccess: () => setShowConfirm(false),
});
return (
<>
<PageHeader
title="Certificate Digest"
subtitle="Preview and send the scheduled certificate digest email"
action={
<button
onClick={() => setShowConfirm(true)}
disabled={!html || sendMutation.isPending}
className="btn btn-primary text-xs disabled:opacity-50"
>
Send Digest Now
</button>
}
/>
<div className="flex-1 overflow-y-auto px-6 py-4">
{sendMutation.isSuccess && (
<div className="mb-4 px-4 py-2.5 bg-emerald-50 border border-emerald-200 rounded-lg text-sm text-emerald-700">
Digest sent successfully.
</div>
)}
{sendMutation.isError && (
<div className="mb-4 px-4 py-2.5 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
Failed to send digest: {(sendMutation.error as Error).message}
</div>
)}
{isLoading && (
<div className="flex items-center justify-center py-20">
<div className="text-sm text-ink-muted">Loading digest preview...</div>
</div>
)}
{error && (
<ErrorState
error={error as Error}
onRetry={() => refetch()}
/>
)}
{html && (
<div className="bg-white border border-surface-border rounded-lg shadow-sm overflow-hidden">
<div className="px-4 py-2.5 bg-surface border-b border-surface-border flex items-center justify-between">
<span className="text-xs text-ink-muted font-medium">Email Preview</span>
<button
onClick={() => refetch()}
className="text-xs text-brand-400 hover:text-brand-500"
>
Refresh
</button>
</div>
<iframe
srcDoc={html}
title="Digest Preview"
className="w-full border-0"
style={{ minHeight: '600px' }}
sandbox="allow-same-origin"
/>
</div>
)}
</div>
{showConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowConfirm(false)}>
<div className="bg-white rounded-lg shadow-xl w-full max-w-sm mx-4" onClick={e => e.stopPropagation()}>
<div className="px-6 py-4 border-b border-surface-border">
<h3 className="text-lg font-semibold text-ink">Send Digest</h3>
<p className="text-sm text-ink-muted mt-1">
This will send the certificate digest email to all configured recipients.
</p>
</div>
<div className="px-6 py-3 border-t border-surface-border flex justify-end gap-2">
<button onClick={() => setShowConfirm(false)} className="px-4 py-2 text-sm text-ink-muted hover:text-ink rounded border border-surface-border">
Cancel
</button>
<button
onClick={() => sendMutation.mutate()}
disabled={sendMutation.isPending}
className="px-4 py-2 text-sm text-white bg-brand-500 hover:bg-brand-600 rounded disabled:opacity-50"
>
{sendMutation.isPending ? 'Sending...' : 'Send'}
</button>
</div>
</div>
</div>
)}
</>
);
}