From 99f52a6c3c0e186907e8fa00975990fe31013300 Mon Sep 17 00:00:00 2001 From: Shankar Date: Mon, 27 Apr 2026 02:59:35 +0000 Subject: [PATCH] M-029 Pass 2: migrate CertificatesPage to useListParams (Pass 2 complete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M-029 Pass 2 surface turned out to be much smaller than the audit estimated: the only page with real UI-driven pagination + filter state stored in useState was CertificatesPage. Most other pages either fetch filter-dropdown data with hardcoded per_page (sidecars, not pagination) or use useSearchParams directly already. So Pass 2 is a single-page migration. What changed: - 9 useState hooks (statusFilter, envFilter, issuerFilter, ownerFilter, profileFilter, teamFilter, expiresBefore, sortBy, page, perPage) collapse into a single useListParams({ pageSize: 50 }) call. - All filter onChange handlers now call setFilter('', value). - setFilter automatically resets page to 1 on every filter / sort change, so the manual setPage(1) calls at three sites (team / expires_before / sort) are no longer needed — the F-1 contract is now enforced by the hook, not by hand-rolled setPage calls scattered through onChange. - Pagination handler simplified: onPerPageChange: setPageSize (the hook drops the page param from the URL when pageSize changes). Behavior preserved: - The 8 filter keys (status / environment / issuer_id / owner_id / profile_id / team_id / expires_before / sort) still flow through getCertificates with the same param names — pinned by the existing CertificatesPage.test.tsx F-1 contract tests. - Default pageSize stays at 50 (matches the F-1 baseline; the hook's global default is 25 but the per-page override takes precedence). - Page reset on filter / per_page change preserved (now hook-enforced). Side benefit: filter / sort / pagination state is now URL-resident (browser deep-link + back-button correct). Sharing a filtered list view is now a URL copy, not a 'recreate this filter combo by hand' message. Verification: legacy useMutation count still 0 (Pass 1 invariant intact) CertificatesPage useListParams 0 -> 1 site CertificatesPage local pagination removed --- web/src/pages/CertificatesPage.tsx | 68 ++++++++++++++++-------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/web/src/pages/CertificatesPage.tsx b/web/src/pages/CertificatesPage.tsx index a616957..8818e70 100644 --- a/web/src/pages/CertificatesPage.tsx +++ b/web/src/pages/CertificatesPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTrackedMutation } from '../hooks/useTrackedMutation'; +import { useListParams } from '../hooks/useListParams'; 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'; @@ -395,29 +396,29 @@ export default function CertificatesPage() { // with 403, but we also hide the button in the GUI to avoid a misleading // affordance. Authoritative gate remains server-side. const { admin } = useAuth(); - const [statusFilter, setStatusFilter] = useState(''); - const [envFilter, setEnvFilter] = useState(''); - const [issuerFilter, setIssuerFilter] = useState(''); - const [ownerFilter, setOwnerFilter] = useState(''); - const [profileFilter, setProfileFilter] = useState(''); - // F-1 closure (cat-e-610251c8f72d): pre-F-1 the page exposed only 5 of - // the backend handler's 17 supported query filters. Three new operator- - // facing filters added: team_id (already first-class elsewhere), - // expires_before (drives the "expiring in N days" workflow), and a - // sort selector (defaults to backend ordering). Audit-recommended - // minimum-add per the closure rationale; remaining filters - // (agent_id, expires_after, created_after, updated_after, cursor, - // fields, sort_desc) are deferred until a consumer use case - // demands them — over-stuffing the toolbar is its own UX cost. - const [teamFilter, setTeamFilter] = useState(''); - const [expiresBefore, setExpiresBefore] = useState(''); - const [sortBy, setSortBy] = useState(''); - // F-1 closure (cat-k-e85d1099b2d7): pre-F-1 the page rendered the - // first 50 certs returned by the backend with no way to advance. - // The reusable DataTable pagination prop (added in this same - // commit) takes the page + per_page state declared here. - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(50); + // M-029 Pass 2 (Audit M-010): filter / sort / pagination state migrated + // from 9 local useState hooks to useListParams — URL-resident state is + // deep-linkable, browser-back-correct, and the hook auto-resets page + // to 1 on filter / sort / pageSize change (preserving the F-1 contract + // that previously had to be hand-rolled at every onChange site). + // + // F-1 closure (cat-e-610251c8f72d) preserved: the 8 operator-facing + // filters (status / environment / issuer_id / owner_id / profile_id / + // team_id / expires_before / sort) all flow through filters[] with + // their existing keys. Default page size stays at 50 to match the + // pre-migration F-1 baseline (the hook's global default is 25, but + // the page-level default takes precedence). + const { params: listParams, setPage, setPageSize, setFilter } = useListParams({ pageSize: 50 }); + const statusFilter = listParams.filters.status ?? ''; + const envFilter = listParams.filters.environment ?? ''; + const issuerFilter = listParams.filters.issuer_id ?? ''; + const ownerFilter = listParams.filters.owner_id ?? ''; + const profileFilter = listParams.filters.profile_id ?? ''; + const teamFilter = listParams.filters.team_id ?? ''; + const expiresBefore = listParams.filters.expires_before ?? ''; + const sortBy = listParams.filters.sort ?? ''; + const page = listParams.page; + const perPage = listParams.pageSize; const [showCreate, setShowCreate] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showBulkRevoke, setShowBulkRevoke] = useState(false); @@ -564,7 +565,7 @@ export default function CertificatesPage() {
{ setTeamFilter(e.target.value); setPage(1); }} + onChange={e => setFilter('team_id', e.target.value)} className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink" > @@ -629,13 +630,13 @@ export default function CertificatesPage() { { setExpiresBefore(e.target.value); setPage(1); }} + onChange={e => setFilter('expires_before', e.target.value)} title="Expires before (drives the 'expiring in N days' workflow)" className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink" />