20260617 增加RPKI Explorer前端
This commit is contained in:
101
ui/rpki-explorer/src/api/client.ts
Normal file
101
ui/rpki-explorer/src/api/client.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export interface ApiEnvelope<T> {
|
||||
data: T;
|
||||
page: { nextCursor: string | null; limit: number } | null;
|
||||
meta: { runId: string | null; schemaVersion: number };
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
export type JsonQuery = Record<string, string | number | boolean | null | undefined>;
|
||||
|
||||
export function withQuery(path: string, query?: JsonQuery): string {
|
||||
if (!query) {
|
||||
return path;
|
||||
}
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
search.set(key, String(value));
|
||||
}
|
||||
const queryString = search.toString();
|
||||
return queryString ? `${path}?${queryString}` : path;
|
||||
}
|
||||
|
||||
export async function getJson<T>(path: string, query?: JsonQuery): Promise<ApiEnvelope<T>> {
|
||||
const response = await fetch(withQuery(path, query));
|
||||
const body = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
if (!response.ok) {
|
||||
throw new ApiError(body.error ?? `Request failed: ${response.status}`, response.status);
|
||||
}
|
||||
return normalizeEnvelope<T>(body);
|
||||
}
|
||||
|
||||
export async function postJson<T>(path: string, body?: unknown): Promise<ApiEnvelope<T>> {
|
||||
const response = await fetch(path, {
|
||||
body: JSON.stringify(body ?? {}),
|
||||
headers: { "content-type": "application/json" },
|
||||
method: "POST"
|
||||
});
|
||||
const responseBody = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
if (!response.ok) {
|
||||
throw new ApiError(responseBody.error ?? `Request failed: ${response.status}`, response.status);
|
||||
}
|
||||
return normalizeEnvelope<T>(responseBody);
|
||||
}
|
||||
|
||||
export async function getBinary(path: string): Promise<Blob> {
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
throw new ApiError(body.error ?? `Request failed: ${response.status}`, response.status);
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
export function normalizeEnvelope<T>(value: unknown): ApiEnvelope<T> {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return {
|
||||
data: value as T,
|
||||
page: null,
|
||||
meta: { runId: null, schemaVersion: 1 }
|
||||
};
|
||||
}
|
||||
const record = value as {
|
||||
data?: T;
|
||||
page?: { nextCursor?: string | null; limit?: number } | null;
|
||||
meta?: { runId?: string | null; schemaVersion?: number } | null;
|
||||
};
|
||||
return {
|
||||
data: record.data as T,
|
||||
page: record.page
|
||||
? {
|
||||
nextCursor: record.page.nextCursor ?? null,
|
||||
limit: record.page.limit ?? 0
|
||||
}
|
||||
: null,
|
||||
meta: {
|
||||
runId: record.meta?.runId ?? null,
|
||||
schemaVersion: record.meta?.schemaVersion ?? 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeApiError(error: unknown): Error {
|
||||
if (error instanceof ApiError) {
|
||||
return error;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error;
|
||||
}
|
||||
return new Error("Unknown API error");
|
||||
}
|
||||
301
ui/rpki-explorer/src/api/queryService.ts
Normal file
301
ui/rpki-explorer/src/api/queryService.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { getBinary, getJson, postJson } from "./client";
|
||||
|
||||
export interface ServiceInfo {
|
||||
service: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface RunRecord {
|
||||
schemaVersion: number;
|
||||
runId: string;
|
||||
runSeq: number | null;
|
||||
runDir: string;
|
||||
validationTime: string | null;
|
||||
syncMode: string | null;
|
||||
startedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
wallMs: number | null;
|
||||
artifactPaths: Record<string, string>;
|
||||
counts: {
|
||||
publicationPoints: number;
|
||||
objects: number;
|
||||
freshObjects: number;
|
||||
cachedObjects: number;
|
||||
rejectedObjects: number;
|
||||
freshRejectedObjects: number;
|
||||
cachedRejectedObjects: number;
|
||||
trustAnchors: number;
|
||||
vrps: number;
|
||||
aspas: number;
|
||||
warnings: number;
|
||||
};
|
||||
indexStatus: string;
|
||||
indexError: string | null;
|
||||
}
|
||||
|
||||
export interface RepositoryRecord {
|
||||
schemaVersion: number;
|
||||
runId: string;
|
||||
repoId: string;
|
||||
uri: string;
|
||||
host: string;
|
||||
transport: string;
|
||||
publicationPoints: number;
|
||||
objects: number;
|
||||
rejectedObjects: number;
|
||||
downloadBytes: number | null;
|
||||
syncDurationMsTotal: number;
|
||||
phases: Record<string, number>;
|
||||
terminalStates: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface PublicationPointRecord {
|
||||
schemaVersion: number;
|
||||
runId: string;
|
||||
ppId: string;
|
||||
repoId: string;
|
||||
nodeId: number | null;
|
||||
parentNodeId: number | null;
|
||||
rsyncBaseUri: string | null;
|
||||
manifestRsyncUri: string | null;
|
||||
publicationPointRsyncUri: string | null;
|
||||
rrdpNotificationUri: string | null;
|
||||
source: string | null;
|
||||
repoSyncSource: string | null;
|
||||
repoSyncPhase: string | null;
|
||||
repoSyncDurationMs: number | null;
|
||||
repoSyncError: string | null;
|
||||
repoTerminalState: string | null;
|
||||
thisUpdate: string | null;
|
||||
nextUpdate: string | null;
|
||||
verifiedAt: string | null;
|
||||
objects: number;
|
||||
rejectedObjects: number;
|
||||
warnings: number;
|
||||
}
|
||||
|
||||
export interface ObjectInstanceRecord {
|
||||
schemaVersion: number;
|
||||
runId: string;
|
||||
objectInstanceId: string;
|
||||
uri: string;
|
||||
uriHash: string;
|
||||
sha256: string;
|
||||
objectType: string;
|
||||
result: string;
|
||||
detailSummary: string | null;
|
||||
repoId: string;
|
||||
ppId: string;
|
||||
sourceSection: string;
|
||||
rejected: boolean;
|
||||
rejectReason: string | null;
|
||||
}
|
||||
|
||||
export interface ObjectProjectionRecord {
|
||||
schemaVersion?: number;
|
||||
objectType?: string;
|
||||
sha256?: string;
|
||||
uri?: string;
|
||||
parseStatus?: string;
|
||||
errorSummary?: string | null;
|
||||
projection?: unknown;
|
||||
}
|
||||
|
||||
export interface ValidationIssueRecord {
|
||||
stage?: string;
|
||||
severity?: string;
|
||||
reasonCode?: string;
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
export interface ObjectValidationRecord {
|
||||
objectInstanceId: string;
|
||||
uri: string;
|
||||
sha256: string;
|
||||
objectType: string;
|
||||
finalStatus: string;
|
||||
auditResult: string;
|
||||
detailSummary: string | null;
|
||||
rejected: boolean;
|
||||
rejectReason: string | null;
|
||||
sourceSection: string;
|
||||
explainAvailable: boolean;
|
||||
authoritative: boolean;
|
||||
fileValidation: {
|
||||
status: string;
|
||||
stage: string;
|
||||
issues: ValidationIssueRecord[];
|
||||
detailSummary: string | null;
|
||||
};
|
||||
chainValidation: {
|
||||
status: string;
|
||||
stage: string;
|
||||
issues: ValidationIssueRecord[];
|
||||
note?: string;
|
||||
edgesPage?: { nextCursor: string | null; limit: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChainEdgeRecord {
|
||||
relation: string;
|
||||
fromUri: string;
|
||||
toUri: string;
|
||||
toObjectInstanceId: string | null;
|
||||
toSha256: string | null;
|
||||
status: string;
|
||||
evidence: unknown;
|
||||
}
|
||||
|
||||
export interface ValidationExplainRecord {
|
||||
schemaVersion: number;
|
||||
explainVersion: number;
|
||||
runId: string;
|
||||
objectInstanceId: string;
|
||||
uri: string;
|
||||
sha256: string;
|
||||
objectType: string;
|
||||
finalStatus: string;
|
||||
auditResult: string;
|
||||
detailSummary: string | null;
|
||||
authoritative: boolean;
|
||||
explainMode: string;
|
||||
generatedAt: string;
|
||||
parsevalidate: unknown;
|
||||
chainvalidate: unknown;
|
||||
chainEdges: ChainEdgeRecord[];
|
||||
}
|
||||
|
||||
export interface ObjectUriIndexRecord {
|
||||
runId: string;
|
||||
uri: string;
|
||||
sha256: string;
|
||||
objectInstanceId: string;
|
||||
repoId: string;
|
||||
ppId: string;
|
||||
}
|
||||
|
||||
export interface ExportJobRecord {
|
||||
schemaVersion: number;
|
||||
jobId: string;
|
||||
runId: string;
|
||||
scope: string;
|
||||
repoId: string | null;
|
||||
ppId: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
finishedAt: string | null;
|
||||
outputPath: string | null;
|
||||
objectCount: number;
|
||||
bytesWritten: number;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export type CountsByKey = Record<string, number>;
|
||||
|
||||
export async function getServiceInfo() {
|
||||
return getJson<ServiceInfo>("/api/v1");
|
||||
}
|
||||
|
||||
export async function getLatestRun() {
|
||||
return getJson<RunRecord>("/api/v1/latest_run");
|
||||
}
|
||||
|
||||
export async function getRunSummary(runId: string) {
|
||||
return getJson<Record<string, number>>(`/api/v1/runs/${runId}/summary`);
|
||||
}
|
||||
|
||||
export async function getStatsObjectTypes(runId: string) {
|
||||
return getJson<CountsByKey>(`/api/v1/runs/${runId}/stats/object-types`);
|
||||
}
|
||||
|
||||
export async function getStatsValidation(runId: string) {
|
||||
return getJson<CountsByKey>(`/api/v1/runs/${runId}/stats/validation`);
|
||||
}
|
||||
|
||||
export async function getStatsReasons(runId: string) {
|
||||
return getJson<CountsByKey>(`/api/v1/runs/${runId}/stats/reasons`);
|
||||
}
|
||||
|
||||
export async function getStatsDownloads(runId: string) {
|
||||
return getJson<Record<string, unknown>>(`/api/v1/runs/${runId}/stats/downloads`);
|
||||
}
|
||||
|
||||
export async function listRepos(runId: string, limit = 5, cursor?: string | null) {
|
||||
return getJson<RepositoryRecord[]>(`/api/v1/runs/${runId}/repos`, { limit, cursor: cursor ?? undefined });
|
||||
}
|
||||
|
||||
export async function listPublicationPointsForRepo(runId: string, repoId: string, limit = 50, cursor?: string | null) {
|
||||
return getJson<PublicationPointRecord[]>(`/api/v1/runs/${runId}/repos/${repoId}/publication-points`, {
|
||||
limit,
|
||||
cursor: cursor ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
export async function listObjectsForPublicationPoint(runId: string, ppId: string, limit = 50, cursor?: string | null) {
|
||||
return getJson<ObjectInstanceRecord[]>(`/api/v1/runs/${runId}/publication-points/${ppId}/objects`, {
|
||||
limit,
|
||||
cursor: cursor ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
export async function getObject(runId: string, objectInstanceId: string) {
|
||||
return getJson<ObjectInstanceRecord>(`/api/v1/runs/${runId}/objects/${objectInstanceId}`);
|
||||
}
|
||||
|
||||
export async function getObjectByUri(runId: string, uri: string) {
|
||||
return getJson<ObjectUriIndexRecord>(`/api/v1/runs/${runId}/objects/by-uri`, { uri });
|
||||
}
|
||||
|
||||
export async function getObjectParsed(runId: string, objectInstanceId: string) {
|
||||
return getJson<ObjectProjectionRecord | null>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed`);
|
||||
}
|
||||
|
||||
export async function getObjectValidation(runId: string, objectInstanceId: string) {
|
||||
return getJson<ObjectValidationRecord>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/validation`);
|
||||
}
|
||||
|
||||
export async function getObjectChain(runId: string, objectInstanceId: string) {
|
||||
return getJson<ChainEdgeRecord[]>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/chain`);
|
||||
}
|
||||
|
||||
export async function listManifestFiles(runId: string, objectInstanceId: string, limit = 50, cursor?: string | null) {
|
||||
return getJson<unknown[]>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed/manifest-files`, {
|
||||
limit,
|
||||
cursor: cursor ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
export async function listRevokedCertificates(runId: string, objectInstanceId: string, limit = 50, cursor?: string | null) {
|
||||
return getJson<unknown[]>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/parsed/revoked-certs`, {
|
||||
limit,
|
||||
cursor: cursor ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
export async function explainObjectValidation(runId: string, objectInstanceId: string, forceRefresh = false) {
|
||||
return postJson<ValidationExplainRecord>(`/api/v1/runs/${runId}/objects/${objectInstanceId}/validation/explain`, {
|
||||
forceRefresh
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadRawObject(runId: string, objectInstanceId: string) {
|
||||
return getBinary(`/api/v1/runs/${runId}/objects/${objectInstanceId}/raw`);
|
||||
}
|
||||
|
||||
export async function createObjectSetExport(runId: string, objectInstanceIds: string[]) {
|
||||
return postJson<ExportJobRecord>(`/api/v1/runs/${runId}/exports`, {
|
||||
objectInstanceIds,
|
||||
scope: "object_set"
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPublicationPointExport(runId: string, ppId: string) {
|
||||
return postJson<ExportJobRecord>(`/api/v1/runs/${runId}/exports`, {
|
||||
ppId,
|
||||
scope: "publication_point"
|
||||
});
|
||||
}
|
||||
|
||||
export async function getExportJob(runId: string, jobId: string) {
|
||||
return getJson<ExportJobRecord>(`/api/v1/runs/${runId}/exports/${jobId}`);
|
||||
}
|
||||
Reference in New Issue
Block a user