style: bring the vulnerabilities page onto the house page shape
Every other page under app/(app) opens with `p-4 sm:p-6 lg:p-8` and the layout adds none of its own, so this page alone sat flush against the shell edge. Its h1 was text-xl where every other page is text-2xl. The findings list was a stack of separately bordered cards; it is now rows inside one Card, separated by border-border-soft, matching the monitors and workflows lists. Loading is the shared spinner rather than a line of text, the error is the shared danger strip, and the empty state uses the same proportions as the monitors one.
This commit is contained in:
@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, vulnerabilities, type FindingState, type Severity, type VulnFinding } from "@/lib/api";
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
import { Button } from "@/components/ui";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
import { AcceptDialog } from "@/components/vulnerabilities/AcceptDialog";
|
||||
import { DBFreshness } from "@/components/vulnerabilities/DBFreshness";
|
||||
import { FindingRow } from "@/components/vulnerabilities/FindingRow";
|
||||
@@ -78,12 +78,12 @@ export default function VulnerabilitiesPage() {
|
||||
const total = SEVERITY_ORDER.reduce((n, s) => n + (counts[s] ?? 0), 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-text-primary">Vulnerabilities</h1>
|
||||
<h1 className="text-2xl font-bold text-text-primary">Vulnerabilities</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">
|
||||
Installed packages matched against distribution security advisories.
|
||||
{total} open finding{total !== 1 ? "s" : ""} · installed packages matched against distribution security advisories
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
@@ -93,32 +93,36 @@ export default function VulnerabilitiesPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DBFreshness summary={summary.data} />
|
||||
<div className="mb-4">
|
||||
<DBFreshness summary={summary.data} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 rounded-lg border border-border bg-surface px-5 py-4">
|
||||
{/* Severity counts double as the filter. They are always the whole
|
||||
fleet's open counts, never the filtered view's, so switching
|
||||
state cannot make the fleet look better than it is. */}
|
||||
<div className="mb-4 flex flex-wrap gap-2 rounded-lg border border-border bg-surface px-4 py-3 sm:px-5">
|
||||
{SEVERITY_ORDER.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setSeverity(severity === s ? "" : s)}
|
||||
className={`flex items-center gap-2 rounded px-2 py-1 text-left transition-colors ${
|
||||
severity === s ? "bg-surface-2" : "hover:bg-surface-2"
|
||||
aria-pressed={severity === s}
|
||||
className={`flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-left transition-colors ${
|
||||
severity === s ? "border-accent bg-surface-2" : "border-transparent hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<SeverityBadge severity={s} />
|
||||
<span className="font-mono text-lg font-semibold tabular-nums text-text-primary">{counts[s] ?? 0}</span>
|
||||
</button>
|
||||
))}
|
||||
<span className="ml-auto self-center font-mono text-[10px] uppercase tracking-[0.16em] text-text-tertiary">
|
||||
{total} open
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="mb-4 flex gap-2">
|
||||
{STATES.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setState(s)}
|
||||
className={`rounded border px-3 py-1.5 text-sm capitalize transition-colors ${
|
||||
aria-pressed={state === s}
|
||||
className={`rounded-lg border px-3 py-1.5 text-sm capitalize transition-colors ${
|
||||
state === s ? "border-accent text-accent" : "border-border text-text-secondary hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
@@ -127,33 +131,42 @@ export default function VulnerabilitiesPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{groups.isLoading && <p className="text-sm text-text-secondary">Loading…</p>}
|
||||
{groups.error && <p className="text-sm text-danger">{(groups.error as Error).message}</p>}
|
||||
|
||||
{groups.data && groups.data.length === 0 && (
|
||||
<div className="rounded-lg border border-border bg-surface px-5 py-8 text-center">
|
||||
<p className="text-sm text-text-secondary">No {state} findings.</p>
|
||||
<p className="mt-1 text-xs text-text-tertiary">
|
||||
Servers report packages hourly. A server whose distribution has no advisory feed is reported as unsupported on its own
|
||||
page rather than counted here.
|
||||
</p>
|
||||
{groups.error && (
|
||||
<div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">
|
||||
{(groups.error as Error).message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{groups.data?.map((g) => (
|
||||
<FindingRow
|
||||
key={g.cve_id}
|
||||
group={g}
|
||||
serverName={serverName}
|
||||
canAct={isAdmin}
|
||||
onAccept={setAccepting}
|
||||
onUnaccept={(f) => unaccept.mutate(f.id)}
|
||||
onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)}
|
||||
applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Card padding={false}>
|
||||
{groups.isLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
|
||||
</div>
|
||||
) : groups.data && groups.data.length > 0 ? (
|
||||
groups.data.map((g) => (
|
||||
<FindingRow
|
||||
key={g.cve_id}
|
||||
group={g}
|
||||
serverName={serverName}
|
||||
canAct={isAdmin}
|
||||
onAccept={setAccepting}
|
||||
onUnaccept={(f) => unaccept.mutate(f.id)}
|
||||
onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)}
|
||||
applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="px-6 py-14 text-center">
|
||||
<p className="text-[15px] font-semibold text-text-primary">
|
||||
No {state} findings{severity ? ` at ${severity} severity` : ""}.
|
||||
</p>
|
||||
<p className="mx-auto mt-2 max-w-[52ch] text-sm text-text-secondary">
|
||||
Servers report their packages hourly. A server whose distribution has no advisory feed is reported as unsupported
|
||||
on its own page rather than counted as clean here.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{accepting && (
|
||||
<AcceptDialog
|
||||
|
||||
Reference in New Issue
Block a user