mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 23:58:56 +00:00
feat(web): expand CertificatesPage filters + reusable DataTable pagination (F-1 master)
Closes two 2026-04-24 audit findings (P2):
- cat-e-610251c8f72d: CertificatesPage exposed only 5 of the
backend handler's 17 supported query filters. Audit recommended
minimum-add: team_id (already first-class elsewhere),
expires_before (drives the "expiring in N days" workflow), and
sort (sort by notAfter for the most common operator triage).
Fix: 3 new useState hooks + 3 new filter UIs in the toolbar +
3 new param wires. Remaining filters (agent_id, expires_after,
created_after, updated_after, cursor, fields, sort_desc) deferred
until a consumer use case demands them — over-stuffing the
toolbar is its own UX cost.
- cat-k-e85d1099b2d7: CertificatesPage rendered the first 50
certs returned by the backend with no way to advance. Backend
response carries {data, total, page, per_page} — a pure render
gap. Fix: lifted pagination into the reusable DataTable
component as an opt-in `pagination?` prop. CertificatesPage is
the first consumer; TargetsPage / IssuersPage / OwnersPage /
others can adopt by passing the same prop.
DataTable changes:
- New `PaginationProps` interface (page, perPage, total,
onPageChange, onPerPageChange?, perPageOptions?).
- New optional `pagination?` prop on DataTable.
- New `PaginationControls` subcomponent rendered in the table
footer when `pagination` is set and `total > 0`. Renders
"Showing X–Y of Z" + per-page selector + page counter +
Prev/Next buttons. Disabling logic guards both boundaries.
CertificatesPage changes:
- 3 new filter useState hooks: teamFilter, expiresBefore, sortBy.
- 2 new pagination useState hooks: page (1), perPage (50).
- Added 4th cohort hook: getTeams via useQuery (mirrors the
existing issuers/owners/profiles filter-data pattern).
- params object gains team_id, expires_before, sort, page, per_page.
- 3 new filter UIs in the toolbar (team select, expires_before
date picker, sort select).
- DataTable gets the new pagination prop.
- Filter changes reset page=1 to keep results visible.
Verification:
- tsc --noEmit — clean
- vitest run — 9 files, 302 tests passing (no regression)
- golangci-lint v2.11.4 run ./... — 0 issues
- All sibling guardrails (S-1, G-3, D-1+D-2, B-1, L-1, H-1, C-1) pass
Audit findings closed:
- cat-e-610251c8f72d (P2)
- cat-k-e85d1099b2d7 (P2)
Deferred follow-ups:
- 8 backend filters (agent_id, expires_after, created_after,
updated_after, cursor, fields, sort_desc, plus secondary sort
fields) deferred until consumer demand justifies UI weight.
- TargetsPage / IssuersPage / OwnersPage / etc. opt-in to the
pagination prop incrementally — DataTable now supports it; per-
page adoption is a follow-up commit each.
- CertificatesPage Vitest coverage of the new filter+pagination
paths deferred to the per-page test campaign (cat-s2-c24a548076c6).
This commit is contained in:
@@ -398,6 +398,24 @@ export default function CertificatesPage() {
|
||||
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);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [showBulkRevoke, setShowBulkRevoke] = useState(false);
|
||||
@@ -407,13 +425,21 @@ export default function CertificatesPage() {
|
||||
const { data: issuersData } = useQuery({ queryKey: ['issuers-filter'], queryFn: () => getIssuers({ per_page: '100' }) });
|
||||
const { data: ownersData } = useQuery({ queryKey: ['owners-filter'], queryFn: () => getOwners({ per_page: '100' }) });
|
||||
const { data: profilesData } = useQuery({ queryKey: ['profiles-filter'], queryFn: () => getProfiles({ per_page: '100' }) });
|
||||
// F-1 closure: hydrate the team filter dropdown.
|
||||
const { data: teamsFilterData } = useQuery({ queryKey: ['teams-filter'], queryFn: () => getTeams({ per_page: '100' }) });
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
if (statusFilter) params.status = statusFilter;
|
||||
if (envFilter) params.environment = envFilter;
|
||||
if (issuerFilter) params.issuer_id = issuerFilter;
|
||||
if (ownerFilter) params.owner_id = ownerFilter;
|
||||
if (profileFilter) params.profile_id = profileFilter;
|
||||
if (statusFilter) params.status = statusFilter;
|
||||
if (envFilter) params.environment = envFilter;
|
||||
if (issuerFilter) params.issuer_id = issuerFilter;
|
||||
if (ownerFilter) params.owner_id = ownerFilter;
|
||||
if (profileFilter) params.profile_id = profileFilter;
|
||||
if (teamFilter) params.team_id = teamFilter;
|
||||
if (expiresBefore) params.expires_before = expiresBefore;
|
||||
if (sortBy) params.sort = sortBy;
|
||||
// Pagination (F-1) — re-fetch on page / per_page change.
|
||||
params.page = String(page);
|
||||
params.per_page = String(perPage);
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['certificates', params],
|
||||
@@ -587,6 +613,36 @@ export default function CertificatesPage() {
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* F-1 closure (cat-e-610251c8f72d): team / expires_before / sort */}
|
||||
<select
|
||||
value={teamFilter}
|
||||
onChange={e => { setTeamFilter(e.target.value); setPage(1); }}
|
||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||
>
|
||||
<option value="">All teams</option>
|
||||
{teamsFilterData?.data?.map(t => (
|
||||
<option key={t.id} value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="date"
|
||||
value={expiresBefore}
|
||||
onChange={e => { setExpiresBefore(e.target.value); setPage(1); }}
|
||||
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"
|
||||
/>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={e => { setSortBy(e.target.value); setPage(1); }}
|
||||
title="Sort order"
|
||||
className="bg-white border border-surface-border rounded px-3 py-1.5 text-sm text-ink"
|
||||
>
|
||||
<option value="">Default sort</option>
|
||||
<option value="notAfter">Expires soonest</option>
|
||||
<option value="-notAfter">Expires latest</option>
|
||||
<option value="createdAt">Created earliest</option>
|
||||
<option value="-createdAt">Created latest</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{error ? (
|
||||
@@ -601,6 +657,13 @@ export default function CertificatesPage() {
|
||||
selectable
|
||||
selectedKeys={selectedIds}
|
||||
onSelectionChange={setSelectedIds}
|
||||
pagination={{
|
||||
page,
|
||||
perPage,
|
||||
total: data?.total ?? 0,
|
||||
onPageChange: setPage,
|
||||
onPerPageChange: (n) => { setPerPage(n); setPage(1); },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user