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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user