feat(web): hydrate dashboard tiles independently
This commit is contained in:
parent
021f2cf20e
commit
23c8f0964c
7 changed files with 642 additions and 20 deletions
|
|
@ -18,6 +18,9 @@ export interface DashboardRuntimeReady {
|
|||
document: DashboardDocument;
|
||||
schemaVersion: string;
|
||||
currentRevisionId: string;
|
||||
liveDatasourceHydration?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DashboardRuntimeEmpty {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,30 @@ import type {
|
|||
TelemetryCard,
|
||||
} from "@dimensionlab/dashboard-model";
|
||||
|
||||
export type DashboardTileReference =
|
||||
| { kind: "telemetry"; id: string }
|
||||
| { kind: "service"; id: string }
|
||||
| { kind: "module"; id: string }
|
||||
| { kind: "status"; stripId: string; id: string };
|
||||
|
||||
export type DashboardTileItem =
|
||||
| DashboardModule
|
||||
| ServiceEntry
|
||||
| StatusItem
|
||||
| TelemetryCard;
|
||||
|
||||
export type DashboardTileResolution =
|
||||
| {
|
||||
state: "ready";
|
||||
tile: DashboardTileReference;
|
||||
item: DashboardTileItem;
|
||||
}
|
||||
| {
|
||||
state: "not_found";
|
||||
tile: DashboardTileReference;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface DatasourceResolutionOptions {
|
||||
fetch?: DatasourceFetch;
|
||||
prometheusBaseUrl?: string;
|
||||
|
|
@ -46,6 +70,69 @@ export async function resolveDashboardDatasources(
|
|||
};
|
||||
}
|
||||
|
||||
export async function resolveDashboardTile(
|
||||
document: DashboardDocument,
|
||||
tile: DashboardTileReference,
|
||||
options: DatasourceResolutionOptions = {},
|
||||
): Promise<DashboardTileResolution> {
|
||||
const context = datasourceContext(options);
|
||||
|
||||
if (tile.kind === "telemetry") {
|
||||
const card = document.telemetry.find((item) => item.id === tile.id);
|
||||
if (!card) return missingTile(tile);
|
||||
|
||||
return {
|
||||
state: "ready",
|
||||
tile,
|
||||
item: await resolveTelemetryCard(card, context),
|
||||
};
|
||||
}
|
||||
|
||||
if (tile.kind === "service") {
|
||||
const service = document.serviceGroups
|
||||
.flatMap((group) => group.services)
|
||||
.find((item) => item.id === tile.id);
|
||||
if (!service) return missingTile(tile);
|
||||
|
||||
return {
|
||||
state: "ready",
|
||||
tile,
|
||||
item: await resolveService(service, context),
|
||||
};
|
||||
}
|
||||
|
||||
if (tile.kind === "module") {
|
||||
const module = document.modules?.find((item) => item.id === tile.id);
|
||||
if (!module) return missingTile(tile);
|
||||
|
||||
const item = module.id === "runtime-health-summary"
|
||||
? runtimeHealthSummary(
|
||||
module,
|
||||
await Promise.all(
|
||||
document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
||||
),
|
||||
)
|
||||
: await resolveModule(module, context);
|
||||
|
||||
return { state: "ready", tile, item };
|
||||
}
|
||||
|
||||
const strip = document.statusStrips.find((item) => item.id === tile.stripId);
|
||||
const statusItem = strip?.items.find((item) => item.id === tile.id);
|
||||
if (!strip || !statusItem) return missingTile(tile);
|
||||
|
||||
return {
|
||||
state: "ready",
|
||||
tile,
|
||||
item: await resolveStatusTile(
|
||||
statusItem,
|
||||
document.metadata.refreshIntervalSeconds,
|
||||
document.serviceGroups,
|
||||
context,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
interface DatasourceContext {
|
||||
fetch: DatasourceFetch;
|
||||
prometheusBaseUrl: string;
|
||||
|
|
@ -390,6 +477,68 @@ function resolveStatusItem(
|
|||
return structuredClone(item);
|
||||
}
|
||||
|
||||
async function resolveStatusTile(
|
||||
item: StatusItem,
|
||||
refreshIntervalSeconds: number | undefined,
|
||||
serviceGroups: ServiceGroup[],
|
||||
context: DatasourceContext,
|
||||
): Promise<StatusItem> {
|
||||
if (item.id === "system-status") {
|
||||
const resolvedGroups = await Promise.all(
|
||||
serviceGroups.map((group) => resolveServiceGroup(group, context)),
|
||||
);
|
||||
const health = serviceHealthSummary(resolvedGroups);
|
||||
return {
|
||||
...structuredClone(item),
|
||||
value: health.value,
|
||||
severity: health.severity,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.id === "last-sync") {
|
||||
return {
|
||||
...structuredClone(item),
|
||||
value: "just now",
|
||||
severity: "ok",
|
||||
};
|
||||
}
|
||||
|
||||
if (item.id === "uptime") {
|
||||
const uptime = await prometheusScalar(
|
||||
'time() - node_boot_time_seconds{job="node",host="linux-infra"}',
|
||||
context,
|
||||
).catch(() => null);
|
||||
return uptime === null
|
||||
? structuredClone(item)
|
||||
: {
|
||||
...structuredClone(item),
|
||||
value: formatDuration(uptime),
|
||||
severity: "ok",
|
||||
};
|
||||
}
|
||||
|
||||
if (item.id === "load-avg") {
|
||||
const loadAverage = await prometheusLoadAverage(context).catch(() => null);
|
||||
return loadAverage
|
||||
? {
|
||||
...structuredClone(item),
|
||||
value: loadAverage,
|
||||
severity: "neutral",
|
||||
}
|
||||
: structuredClone(item);
|
||||
}
|
||||
|
||||
if (item.id === "auto-refresh" && refreshIntervalSeconds) {
|
||||
return {
|
||||
...structuredClone(item),
|
||||
value: `${refreshIntervalSeconds}s`,
|
||||
severity: "neutral",
|
||||
};
|
||||
}
|
||||
|
||||
return structuredClone(item);
|
||||
}
|
||||
|
||||
function serviceHealthSummary(serviceGroups: ServiceGroup[]): {
|
||||
severity: Severity;
|
||||
value: string;
|
||||
|
|
@ -673,3 +822,17 @@ function formatDuration(totalSeconds: number): string {
|
|||
const minutes = Math.floor((seconds % 3_600) / 60);
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
function missingTile(tile: DashboardTileReference): DashboardTileResolution {
|
||||
return {
|
||||
state: "not_found",
|
||||
tile,
|
||||
message: `Dashboard tile not found: ${tileKey(tile)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function tileKey(tile: DashboardTileReference): string {
|
||||
return tile.kind === "status"
|
||||
? `${tile.kind}:${tile.stripId}:${tile.id}`
|
||||
: `${tile.kind}:${tile.id}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue