From 2057e76706b5cbd78d8517864cd6bb090dbe2874 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Mon, 27 Apr 2026 02:37:25 +0000 Subject: [PATCH] M-029 Pass 1 batch 1: migrate 4 single-mutation pages to useTrackedMutation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/src/pages/AgentsPage.tsx | 9 ++++----- web/src/pages/CertificatesPage.tsx | 6 ++++-- web/src/pages/DigestPage.tsx | 7 +++++-- web/src/pages/IssuerDetailPage.tsx | 7 ++++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/web/src/pages/AgentsPage.tsx b/web/src/pages/AgentsPage.tsx index 971c455..591733d 100644 --- a/web/src/pages/AgentsPage.tsx +++ b/web/src/pages/AgentsPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getAgents, listRetiredAgents, @@ -45,7 +46,6 @@ type ModalMode = export default function AgentsPage() { const navigate = useNavigate(); - const qc = useQueryClient(); const [tab, setTab] = useState('active'); const [modal, setModal] = useState({ kind: 'closed' }); @@ -67,12 +67,11 @@ export default function AgentsPage() { // and we invalidate both queries on success so the retired tab refreshes and // the active tab drops the row. 409s are converted into modal.mode=blocked so // the operator can escalate to force; everything else becomes modal.mode=error. - const mutation = useMutation({ + const mutation = useTrackedMutation({ mutationFn: (input: { agent: Agent; force?: boolean; reason?: string }) => retireAgent(input.agent.id, { force: input.force, reason: input.reason }), + invalidates: [['agents'], ['agents', 'retired']], onSuccess: () => { - qc.invalidateQueries({ queryKey: ['agents'] }); - qc.invalidateQueries({ queryKey: ['agents', 'retired'] }); setModal({ kind: 'closed' }); }, }); diff --git a/web/src/pages/CertificatesPage.tsx b/web/src/pages/CertificatesPage.tsx index 6530a39..a616957 100644 --- a/web/src/pages/CertificatesPage.tsx +++ b/web/src/pages/CertificatesPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { useNavigate } from 'react-router-dom'; import { getCertificates, createCertificate, revokeCertificate, getOwners, getTeams, getRenewalPolicies, getProfiles, getIssuers, bulkRevokeCertificates, bulkRenewCertificates, bulkReassignCertificates } from '../api/client'; import { useAuth } from '../components/AuthProvider'; @@ -73,7 +74,7 @@ function CreateCertificateModal({ onClose, onSuccess }: { onClose: () => void; o : `${Math.round(selectedProfile.max_ttl_seconds / 86400)}d` : null; - const mutation = useMutation({ + const mutation = useTrackedMutation({ mutationFn: () => { const payload: Record = { ...form }; // Convert comma-separated SANs to array @@ -95,6 +96,7 @@ function CreateCertificateModal({ onClose, onSuccess }: { onClose: () => void; o } return createCertificate(payload); }, + invalidates: [['certificates']], onSuccess: () => onSuccess(), onError: (err: Error) => setError(err.message), }); diff --git a/web/src/pages/DigestPage.tsx b/web/src/pages/DigestPage.tsx index 6ea6073..ff29372 100644 --- a/web/src/pages/DigestPage.tsx +++ b/web/src/pages/DigestPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; -import { useQuery, useMutation } from '@tanstack/react-query'; +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'; @@ -13,8 +14,10 @@ export default function DigestPage() { retry: false, }); - const sendMutation = useMutation({ + 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), }); diff --git a/web/src/pages/IssuerDetailPage.tsx b/web/src/pages/IssuerDetailPage.tsx index fb6e29d..5281636 100644 --- a/web/src/pages/IssuerDetailPage.tsx +++ b/web/src/pages/IssuerDetailPage.tsx @@ -1,5 +1,6 @@ import { useParams, useNavigate } from 'react-router-dom'; -import { useQuery, useMutation } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getIssuer, testIssuerConnection, getCertificates } from '../api/client'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; @@ -46,9 +47,9 @@ export default function IssuerDetailPage() { enabled: !!id, }); - const testMutation = useMutation({ + const testMutation = useTrackedMutation({ mutationFn: () => testIssuerConnection(id!), - onSuccess: () => refetch(), + invalidates: [['issuer', id]], }); if (error) {