20260728_2 修复rpki-explorer体验问题7项(#134):证书资源可展开、CMS对象新增EE Parsed Tab、Overview增加repo数KPI/修复Object types标签/Top repositories按objects排序(后端sort参数)/reject reasons按前缀聚类可下钻、Run选择器30s轮询

This commit is contained in:
2026-07-28 13:21:58 +08:00
parent 0d6bbec9f3
commit f7f80fe684
20 changed files with 676 additions and 70 deletions

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { useState, type ReactNode } from "react";
import { listManifestFiles, listRevokedCertificates } from "../api/service";
import type { ProjectionRecord } from "../api/schemas";
import { CopyableValue } from "./CopyableValue";
@@ -132,6 +132,34 @@ function uriLines(uris: string[]): ReactNode {
);
}
/**
* Chip list that collapses to `collapsedCount` entries with a clickable
* "+N more" toggle; expanding shows every item (plus a "Show less" toggle).
*/
export function ExpandableChipList({ items, collapsedCount = 12 }: { items: string[]; collapsedCount?: number }) {
const [expanded, setExpanded] = useState(false);
if (!items.length) return <>—</>;
const collapsible = items.length > collapsedCount;
const visible = expanded || !collapsible ? items : items.slice(0, collapsedCount);
return (
<span className="chip-list">
{visible.map((item, i) => (
<span className="chip" key={i}>{item}</span>
))}
{collapsible ? (
<button
type="button"
className="chip chip-more"
onClick={() => setExpanded((prev) => !prev)}
aria-expanded={expanded}
>
{expanded ? "Show less" : `+${items.length - collapsedCount} more`}
</button>
) : null}
</span>
);
}
function ManifestFiles({ runId, objectInstanceId }: { runId: string; objectInstanceId: string }) {
const pager = useCursorPager(`${runId}:${objectInstanceId}:mft`);
const query = useQuery({
@@ -292,7 +320,7 @@ function AspaView({ projection }: { projection: Record<string, unknown> }) {
);
}
function CertificateView({ projection }: { projection: Record<string, unknown> }) {
export function CertificateView({ projection }: { projection: Record<string, unknown> }) {
// Real API shape: the certificate payload lives in `resourceCertificate`
// with extensions nested under `extensions`; keep the older
// certificate/cer + flat-field variants working as fallbacks.
@@ -367,33 +395,11 @@ function CertificateView({ projection }: { projection: Record<string, unknown> }
/>
<MetaRow
label="IP resources"
value={
ipBlocks.length ? (
<span className="chip-list">
{ipBlocks.slice(0, 12).map((b, i) => (
<span className="chip" key={i}>{b}</span>
))}
{ipBlocks.length > 12 ? <span className="chip">+{ipBlocks.length - 12} more</span> : null}
</span>
) : (
"—"
)
}
value={<ExpandableChipList items={ipBlocks} />}
/>
<MetaRow
label="AS resources"
value={
asBlocks.length ? (
<span className="chip-list">
{asBlocks.slice(0, 12).map((b, i) => (
<span className="chip" key={i}>{b}</span>
))}
{asBlocks.length > 12 ? <span className="chip">+{asBlocks.length - 12} more</span> : null}
</span>
) : (
"—"
)
}
value={<ExpandableChipList items={asBlocks} />}
/>
<MetaRow label="Signature algorithm" value={asString(cert.signatureAlgorithm)} mono />
</dl>
@@ -426,7 +432,6 @@ export function ProjectionView({
const projection = record.projection;
const type = (record.objectType ?? "").toLowerCase();
// The query service nests the typed payload one level down:
// projection = { input, object: { roa | manifest | crl | certificate | aspa, … }, schemaVersion, tool }
// Accept a flat payload too, so older fixtures / shape variants keep working.

View File

@@ -59,6 +59,9 @@ function RunSelector() {
queryKey: ["runs", "selector"],
queryFn: () => listRuns({ limit: 25 }),
staleTime: 60_000,
// Soak runs keep producing new runs; poll so the dropdown picks them up
// without a manual page refresh.
refetchInterval: 30_000,
});
const healthQuery = useQuery({ queryKey: ["health"], queryFn: getHealth });
const latestId = healthQuery.data?.latestReadyRun ?? runsQuery.data?.items[0]?.runId;