From e0a3d50f5eee92637014563433efeb417146caee Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Mon, 27 Apr 2026 02:40:54 +0000 Subject: [PATCH] M-029 Pass 1 batch 2: migrate 5 two-mutation pages to useTrackedMutation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drains 10 more useMutation sites (52 -> 42). Each migration declares explicit invalidates per the M-009 contract. Pages migrated: - DashboardPage.tsx previewDigest + sendDigest both 'noop' (read-only preview / fire-and-forget email — no client cache impact) - DiscoveryPage.tsx claim + dismiss both invalidate [['discovered-certificates'], ['discovery-summary']] - NotificationsPage.tsx markRead + requeue both invalidate [['notifications']] - TargetDetailPage.tsx update + testConnection both invalidate [['target', id]] - TargetsPage.tsx createTarget + deleteTarget both invalidate [['targets']] Verification: legacy useMutation count 52 -> 42 (-10) useTrackedMutation count 4 -> 14 (+10) Closes 14 of 56 sites toward M-029 Pass 1 completion. --- web/src/pages/DashboardPage.tsx | 13 ++++++++++--- web/src/pages/DiscoveryPage.tsx | 16 ++++++---------- web/src/pages/NotificationsPage.tsx | 12 ++++++------ web/src/pages/TargetDetailPage.tsx | 14 ++++++-------- web/src/pages/TargetsPage.tsx | 10 ++++++---- 5 files changed, 34 insertions(+), 31 deletions(-) diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx index 97dbc2b..6632729 100644 --- a/web/src/pages/DashboardPage.tsx +++ b/web/src/pages/DashboardPage.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 { useNavigate, useSearchParams } from 'react-router-dom'; import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, @@ -81,15 +82,21 @@ function DigestCard() { const [previewHtml, setPreviewHtml] = useState(null); const [showPreview, setShowPreview] = useState(false); - const previewMutation = useMutation({ + const previewMutation = useTrackedMutation({ mutationFn: previewDigest, + invalidates: 'noop', + noopReason: 'previewDigest is read-only — server renders HTML; no cached query touched', onSuccess: (html) => { setPreviewHtml(html); setShowPreview(true); }, }); - const sendMutation = useMutation({ mutationFn: sendDigest }); + const sendMutation = useTrackedMutation({ + mutationFn: sendDigest, + invalidates: 'noop', + noopReason: 'sendDigest dispatches an email server-side; no cached client query reflects digest-send state', + }); return ( <> diff --git a/web/src/pages/DiscoveryPage.tsx b/web/src/pages/DiscoveryPage.tsx index 360583a..012db88 100644 --- a/web/src/pages/DiscoveryPage.tsx +++ b/web/src/pages/DiscoveryPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getDiscoveredCertificates, getDiscoverySummary, @@ -110,7 +111,6 @@ export default function DiscoveryPage() { const [agentFilter, setAgentFilter] = useState(''); const [claimingCert, setClaimingCert] = useState(null); const [showScans, setShowScans] = useState(false); - const queryClient = useQueryClient(); const params: Record = {}; if (statusFilter) params.status = statusFilter; @@ -139,22 +139,18 @@ export default function DiscoveryPage() { queryFn: () => getAgents({ per_page: '200' }), }); - const claimMutation = useMutation({ + const claimMutation = useTrackedMutation({ mutationFn: ({ id, managedCertId }: { id: string; managedCertId: string }) => claimDiscoveredCertificate(id, managedCertId), + invalidates: [['discovered-certificates'], ['discovery-summary']], onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['discovered-certificates'] }); - queryClient.invalidateQueries({ queryKey: ['discovery-summary'] }); setClaimingCert(null); }, }); - const dismissMutation = useMutation({ + const dismissMutation = useTrackedMutation({ mutationFn: dismissDiscoveredCertificate, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['discovered-certificates'] }); - queryClient.invalidateQueries({ queryKey: ['discovery-summary'] }); - }, + invalidates: [['discovered-certificates'], ['discovery-summary']], }); const formatExpiry = (notAfter?: string) => { diff --git a/web/src/pages/NotificationsPage.tsx b/web/src/pages/NotificationsPage.tsx index 87c21cd..5554eaa 100644 --- a/web/src/pages/NotificationsPage.tsx +++ b/web/src/pages/NotificationsPage.tsx @@ -1,5 +1,6 @@ import { useState, useMemo } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getNotifications, markNotificationRead, requeueNotification } from '../api/client'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; @@ -22,7 +23,6 @@ export default function NotificationsPage() { const [typeFilter, setTypeFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [activeTab, setActiveTab] = useState('all'); - const queryClient = useQueryClient(); const { data, isLoading, error, refetch } = useQuery({ // I-005: queryKey carries the active tab so TanStack Query treats @@ -43,9 +43,9 @@ export default function NotificationsPage() { refetchInterval: 30000, }); - const markRead = useMutation({ + const markRead = useTrackedMutation({ mutationFn: markNotificationRead, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['notifications'] }), + invalidates: [['notifications']], }); // I-005: requeue a dead notification. Invalidates both tab cache entries @@ -60,9 +60,9 @@ export default function NotificationsPage() { // assertion fails on the extra argument. Keep the arrow even if the context // object later becomes structurally empty — the contract pins a single-arg // call and the page must not leak mutation machinery into API boundaries. - const requeue = useMutation({ + const requeue = useTrackedMutation({ mutationFn: (id: string) => requeueNotification(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['notifications'] }), + invalidates: [['notifications']], }); const notifications = data?.data || []; diff --git a/web/src/pages/TargetDetailPage.tsx b/web/src/pages/TargetDetailPage.tsx index 36e034e..0dd88ca 100644 --- a/web/src/pages/TargetDetailPage.tsx +++ b/web/src/pages/TargetDetailPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useParams, Link } from 'react-router-dom'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getTarget, getJobs, updateTarget, testTargetConnection } from '../api/client'; import PageHeader from '../components/PageHeader'; import StatusBadge from '../components/StatusBadge'; @@ -70,23 +71,20 @@ function SourceBadge({ source }: { source?: string }) { export default function TargetDetailPage() { const { id } = useParams<{ id: string }>(); - const queryClient = useQueryClient(); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(''); - const updateMutation = useMutation({ + const updateMutation = useTrackedMutation({ mutationFn: (data: Partial<{ name: string }>) => updateTarget(id!, data), + invalidates: [['target', id]], onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['target', id] }); setIsEditing(false); }, }); - const testMutation = useMutation({ + const testMutation = useTrackedMutation({ mutationFn: () => testTargetConnection(id!), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['target', id] }); - }, + invalidates: [['target', id]], }); const { data: target, isLoading, error, refetch } = useQuery({ diff --git a/web/src/pages/TargetsPage.tsx b/web/src/pages/TargetsPage.tsx index e10586f..993607a 100644 --- a/web/src/pages/TargetsPage.tsx +++ b/web/src/pages/TargetsPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { getTargets, createTarget, deleteTarget, getAgents } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; @@ -242,13 +243,14 @@ function CreateTargetWizard({ onClose, onSuccess }: { onClose: () => void; onSuc return result; }; - const mutation = useMutation({ + const mutation = useTrackedMutation({ mutationFn: () => createTarget({ name, type: targetType, agent_id: agentId, config: buildConfigPayload(), }), + invalidates: [['targets']], onSuccess: () => onSuccess(), onError: (err: Error) => setError(err.message), }); @@ -407,9 +409,9 @@ export default function TargetsPage() { queryFn: () => getTargets(), }); - const deleteMutation = useMutation({ + const deleteMutation = useTrackedMutation({ mutationFn: deleteTarget, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['targets'] }), + invalidates: [['targets']], }); const columns: Column[] = [