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:
@@ -16,12 +16,13 @@ import { CopyableValue } from "../components/CopyableValue";
|
||||
import { DataTable, type Column } from "../components/DataTable";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
import { Panel } from "../components/Panel";
|
||||
import { ProjectionView } from "../components/ProjectionView";
|
||||
import { CertificateView, ProjectionView } from "../components/ProjectionView";
|
||||
import { StatusPill } from "../components/StatusPill";
|
||||
import { EmptyBlock, ErrorBlock, LoadingBlock, Notice } from "../components/StateBlock";
|
||||
import { TabPanel, Tabs } from "../components/Tabs";
|
||||
import { WorkflowStatus } from "../components/WorkflowStatus";
|
||||
import { formatInt, objectTypeLabel } from "../lib/format";
|
||||
import { eeCertificateProjection } from "../lib/projection";
|
||||
import { withRunParam } from "../lib/run";
|
||||
import { useExportJob } from "../lib/useExportJob";
|
||||
import { useRun } from "../lib/useRun";
|
||||
@@ -212,10 +213,11 @@ export default function ObjectDetailPage() {
|
||||
queryFn: () => getObject(runId, objectInstanceId),
|
||||
});
|
||||
|
||||
// Always fetch the parsed projection: the Parsed tab needs it, and the
|
||||
// conditional "EE Parsed" tab derives from it (CMS-signed objects only).
|
||||
const projectionQuery = useQuery({
|
||||
queryKey: ["object-parsed", runId, objectInstanceId],
|
||||
queryFn: () => getObjectProjection(runId, objectInstanceId),
|
||||
enabled: tab === "parsed",
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
|
||||
@@ -251,6 +253,7 @@ export default function ObjectDetailPage() {
|
||||
}
|
||||
|
||||
const object = objectQuery.data;
|
||||
const eeProjection = eeCertificateProjection(projectionQuery.data ?? null);
|
||||
|
||||
const setTab = (next: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
@@ -351,6 +354,7 @@ export default function ObjectDetailPage() {
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ id: "parsed", label: "Parsed" },
|
||||
...(eeProjection ? [{ id: "ee", label: "EE Parsed" }] : []),
|
||||
{ id: "validation", label: "Validation" },
|
||||
{ id: "chain", label: "Chain" },
|
||||
]}
|
||||
@@ -377,6 +381,17 @@ export default function ObjectDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
{eeProjection ? (
|
||||
<TabPanel id="ee" active={tab}>
|
||||
<div style={{ padding: 16 }}>
|
||||
<CertificateView projection={eeProjection} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
) : tab === "ee" ? (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Notice kind="info">This object has no embedded EE certificate in its CMS wrapper.</Notice>
|
||||
</div>
|
||||
) : null}
|
||||
<TabPanel id="validation" active={tab}>
|
||||
<ValidationTab runId={runId} objectInstanceId={objectInstanceId} />
|
||||
</TabPanel>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { Clock, GitBranch, Timer } from "lucide-react";
|
||||
import {
|
||||
getStatsObjectTypes,
|
||||
getStatsOverview,
|
||||
getStatsReasons,
|
||||
getStatsValidation,
|
||||
listRepos,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
objectTypeLabel,
|
||||
truncateMiddle,
|
||||
} from "../lib/format";
|
||||
import { clusterReasons } from "../lib/reasons";
|
||||
import { withRunParam } from "../lib/run";
|
||||
import { useRun, runErrorTitle } from "../lib/useRun";
|
||||
|
||||
@@ -61,7 +63,7 @@ export default function OverviewPage() {
|
||||
const { runId, runQuery } = useRun();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [validationQuery, typesQuery, reasonsQuery, reposQuery] = useQueries({
|
||||
const [validationQuery, typesQuery, reasonsQuery, reposQuery, overviewQuery] = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ["stats", runId, "validation"],
|
||||
@@ -80,7 +82,12 @@ export default function OverviewPage() {
|
||||
},
|
||||
{
|
||||
queryKey: ["repos", runId, "top"],
|
||||
queryFn: () => listRepos(runId, { limit: 8 }),
|
||||
queryFn: () => listRepos(runId, { limit: 8, sort: "objects", order: "desc" }),
|
||||
staleTime: 60_000,
|
||||
},
|
||||
{
|
||||
queryKey: ["stats", runId, "overview"],
|
||||
queryFn: () => getStatsOverview(runId),
|
||||
staleTime: 60_000,
|
||||
},
|
||||
],
|
||||
@@ -109,13 +116,10 @@ export default function OverviewPage() {
|
||||
.slice(0, 8);
|
||||
}, [typesQuery.data]);
|
||||
|
||||
const reasonRows = useMemo(() => {
|
||||
const map = reasonsQuery.data ?? {};
|
||||
return Object.entries(map)
|
||||
.map(([reason, count]) => ({ reason, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 8);
|
||||
}, [reasonsQuery.data]);
|
||||
const reasonRows = useMemo(
|
||||
() => clusterReasons(reasonsQuery.data ?? {}),
|
||||
[reasonsQuery.data],
|
||||
);
|
||||
|
||||
const maxReason = reasonRows[0]?.count ?? 0;
|
||||
|
||||
@@ -224,6 +228,11 @@ export default function OverviewPage() {
|
||||
value={formatInt(counts?.publicationPoints)}
|
||||
to={withRunParam("/publication-points", runId)}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Repositories"
|
||||
value={formatInt(overviewQuery.data?.repos)}
|
||||
to={withRunParam("/repositories", runId)}
|
||||
/>
|
||||
<KpiCard
|
||||
label="Rejected"
|
||||
value={formatInt(counts?.rejectedObjects)}
|
||||
@@ -304,7 +313,7 @@ export default function OverviewPage() {
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
width={104}
|
||||
width={150}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tick={{ fontSize: 12, fill: "var(--text-2)" }}
|
||||
@@ -325,7 +334,7 @@ export default function OverviewPage() {
|
||||
<div className="overview-bottom">
|
||||
<Panel
|
||||
title="Top repositories"
|
||||
subtitle="First repositories by index order"
|
||||
subtitle="Repositories with the most objects"
|
||||
tools={
|
||||
<Link className="btn small" to={withRunParam("/repositories", runId)}>
|
||||
View all
|
||||
@@ -353,7 +362,7 @@ export default function OverviewPage() {
|
||||
|
||||
<Panel
|
||||
title="Top reject reasons"
|
||||
subtitle="Click a reason to inspect matching objects"
|
||||
subtitle="Grouped by reason prefix — click to inspect matching objects"
|
||||
tools={
|
||||
<Link className="btn small" to={withRunParam("/validation", runId)}>
|
||||
Validation
|
||||
@@ -370,12 +379,17 @@ export default function OverviewPage() {
|
||||
<div className="reason-list">
|
||||
{reasonRows.map((row) => (
|
||||
<Link
|
||||
key={row.reason}
|
||||
key={row.key}
|
||||
className="reason-row"
|
||||
to={withRunParam(`/validation?reason=${encodeURIComponent(row.reason)}`, runId)}
|
||||
title={row.reason}
|
||||
to={withRunParam(`/validation?reason=${encodeURIComponent(row.key)}`, runId)}
|
||||
title={row.key}
|
||||
>
|
||||
<span className="reason-text">{row.reason}</span>
|
||||
<span className="reason-text">
|
||||
{row.key}
|
||||
{row.variants > 1 ? (
|
||||
<span className="text-faint text-small"> ×{row.variants}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="reason-count">{formatInt(row.count)}</span>
|
||||
<span className="reason-bar" aria-hidden="true">
|
||||
<span style={{ width: `${maxReason ? (row.count / maxReason) * 100 : 0}%` }} />
|
||||
|
||||
Reference in New Issue
Block a user