mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:21:32 +00:00
M-029 Pass 2: migrate CertificatesPage to useListParams (Pass 2 complete)
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('<key>', 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
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTrackedMutation } from '../hooks/useTrackedMutation';
|
import { useTrackedMutation } from '../hooks/useTrackedMutation';
|
||||||
|
import { useListParams } from '../hooks/useListParams';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getCertificates, createCertificate, revokeCertificate, getOwners, getTeams, getRenewalPolicies, getProfiles, getIssuers, bulkRevokeCertificates, bulkRenewCertificates, bulkReassignCertificates } from '../api/client';
|
import { getCertificates, createCertificate, revokeCertificate, getOwners, getTeams, getRenewalPolicies, getProfiles, getIssuers, bulkRevokeCertificates, bulkRenewCertificates, bulkReassignCertificates } from '../api/client';
|
||||||
import { useAuth } from '../components/AuthProvider';
|
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
|
// with 403, but we also hide the button in the GUI to avoid a misleading
|
||||||
// affordance. Authoritative gate remains server-side.
|
// affordance. Authoritative gate remains server-side.
|
||||||
const { admin } = useAuth();
|
const { admin } = useAuth();
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
// M-029 Pass 2 (Audit M-010): filter / sort / pagination state migrated
|
||||||
const [envFilter, setEnvFilter] = useState('');
|
// from 9 local useState hooks to useListParams — URL-resident state is
|
||||||
const [issuerFilter, setIssuerFilter] = useState('');
|
// deep-linkable, browser-back-correct, and the hook auto-resets page
|
||||||
const [ownerFilter, setOwnerFilter] = useState('');
|
// to 1 on filter / sort / pageSize change (preserving the F-1 contract
|
||||||
const [profileFilter, setProfileFilter] = useState('');
|
// that previously had to be hand-rolled at every onChange site).
|
||||||
// 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-
|
// F-1 closure (cat-e-610251c8f72d) preserved: the 8 operator-facing
|
||||||
// facing filters added: team_id (already first-class elsewhere),
|
// filters (status / environment / issuer_id / owner_id / profile_id /
|
||||||
// expires_before (drives the "expiring in N days" workflow), and a
|
// team_id / expires_before / sort) all flow through filters[] with
|
||||||
// sort selector (defaults to backend ordering). Audit-recommended
|
// their existing keys. Default page size stays at 50 to match the
|
||||||
// minimum-add per the closure rationale; remaining filters
|
// pre-migration F-1 baseline (the hook's global default is 25, but
|
||||||
// (agent_id, expires_after, created_after, updated_after, cursor,
|
// the page-level default takes precedence).
|
||||||
// fields, sort_desc) are deferred until a consumer use case
|
const { params: listParams, setPage, setPageSize, setFilter } = useListParams({ pageSize: 50 });
|
||||||
// demands them — over-stuffing the toolbar is its own UX cost.
|
const statusFilter = listParams.filters.status ?? '';
|
||||||
const [teamFilter, setTeamFilter] = useState('');
|
const envFilter = listParams.filters.environment ?? '';
|
||||||
const [expiresBefore, setExpiresBefore] = useState('');
|
const issuerFilter = listParams.filters.issuer_id ?? '';
|
||||||
const [sortBy, setSortBy] = useState('');
|
const ownerFilter = listParams.filters.owner_id ?? '';
|
||||||
// F-1 closure (cat-k-e85d1099b2d7): pre-F-1 the page rendered the
|
const profileFilter = listParams.filters.profile_id ?? '';
|
||||||
// first 50 certs returned by the backend with no way to advance.
|
const teamFilter = listParams.filters.team_id ?? '';
|
||||||
// The reusable DataTable pagination prop (added in this same
|
const expiresBefore = listParams.filters.expires_before ?? '';
|
||||||
// commit) takes the page + per_page state declared here.
|
const sortBy = listParams.filters.sort ?? '';
|
||||||
const [page, setPage] = useState(1);
|
const page = listParams.page;
|
||||||
const [perPage, setPerPage] = useState(50);
|
const perPage = listParams.pageSize;
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [showBulkRevoke, setShowBulkRevoke] = useState(false);
|
const [showBulkRevoke, setShowBulkRevoke] = useState(false);
|
||||||
@@ -564,7 +565,7 @@ export default function CertificatesPage() {
|
|||||||
<div className="px-6 py-3 flex gap-3 border-b border-surface-border/50">
|
<div className="px-6 py-3 flex gap-3 border-b border-surface-border/50">
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={e => setStatusFilter(e.target.value)}
|
onChange={e => setFilter('status', e.target.value)}
|
||||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
<option value="">All statuses</option>
|
<option value="">All statuses</option>
|
||||||
@@ -577,7 +578,7 @@ export default function CertificatesPage() {
|
|||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={envFilter}
|
value={envFilter}
|
||||||
onChange={e => setEnvFilter(e.target.value)}
|
onChange={e => setFilter('environment', e.target.value)}
|
||||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
<option value="">All environments</option>
|
<option value="">All environments</option>
|
||||||
@@ -587,7 +588,7 @@ export default function CertificatesPage() {
|
|||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={issuerFilter}
|
value={issuerFilter}
|
||||||
onChange={e => setIssuerFilter(e.target.value)}
|
onChange={e => setFilter('issuer_id', e.target.value)}
|
||||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
<option value="">All issuers</option>
|
<option value="">All issuers</option>
|
||||||
@@ -597,7 +598,7 @@ export default function CertificatesPage() {
|
|||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={ownerFilter}
|
value={ownerFilter}
|
||||||
onChange={e => setOwnerFilter(e.target.value)}
|
onChange={e => setFilter('owner_id', e.target.value)}
|
||||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
<option value="">All owners</option>
|
<option value="">All owners</option>
|
||||||
@@ -607,7 +608,7 @@ export default function CertificatesPage() {
|
|||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={profileFilter}
|
value={profileFilter}
|
||||||
onChange={e => setProfileFilter(e.target.value)}
|
onChange={e => setFilter('profile_id', e.target.value)}
|
||||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
<option value="">All profiles</option>
|
<option value="">All profiles</option>
|
||||||
@@ -618,7 +619,7 @@ export default function CertificatesPage() {
|
|||||||
{/* F-1 closure (cat-e-610251c8f72d): team / expires_before / sort */}
|
{/* F-1 closure (cat-e-610251c8f72d): team / expires_before / sort */}
|
||||||
<select
|
<select
|
||||||
value={teamFilter}
|
value={teamFilter}
|
||||||
onChange={e => { 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"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
<option value="">All teams</option>
|
<option value="">All teams</option>
|
||||||
@@ -629,13 +630,13 @@ export default function CertificatesPage() {
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={expiresBefore}
|
value={expiresBefore}
|
||||||
onChange={e => { setExpiresBefore(e.target.value); setPage(1); }}
|
onChange={e => setFilter('expires_before', e.target.value)}
|
||||||
title="Expires before (drives the 'expiring in N days' workflow)"
|
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"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={e => { setSortBy(e.target.value); setPage(1); }}
|
onChange={e => setFilter('sort', e.target.value)}
|
||||||
title="Sort order"
|
title="Sort order"
|
||||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||||
>
|
>
|
||||||
@@ -664,7 +665,10 @@ export default function CertificatesPage() {
|
|||||||
perPage,
|
perPage,
|
||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
onPerPageChange: (n) => { setPerPage(n); setPage(1); },
|
// useListParams.setPageSize auto-drops the page param from
|
||||||
|
// the URL (page resets to 1 implicitly), preserving the
|
||||||
|
// F-1 contract without a manual setPage(1) call.
|
||||||
|
onPerPageChange: setPageSize,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user