fix(web): validate restored dashboard tile shapes
This commit is contained in:
parent
88c448258d
commit
bac809952a
2 changed files with 122 additions and 7 deletions
|
|
@ -213,7 +213,21 @@ describe("dashboard refresh lifecycle", () => {
|
||||||
tiles: [
|
tiles: [
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
item: { id: "infra-ram", detail: "live" },
|
item: {
|
||||||
|
id: "incomplete",
|
||||||
|
detail: "live",
|
||||||
|
},
|
||||||
|
savedAt: 1_000,
|
||||||
|
tile: { kind: "telemetry", id: "incomplete" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: {
|
||||||
|
id: "infra-ram",
|
||||||
|
label: "Infra RAM",
|
||||||
|
value: { kind: "percent", value: 42 },
|
||||||
|
detail: "live",
|
||||||
|
severity: "ok",
|
||||||
|
},
|
||||||
savedAt: 1_000,
|
savedAt: 1_000,
|
||||||
tile: { kind: "telemetry", id: "infra-ram" },
|
tile: { kind: "telemetry", id: "infra-ram" },
|
||||||
},
|
},
|
||||||
|
|
@ -242,6 +256,8 @@ describe("dashboard refresh lifecycle", () => {
|
||||||
tile: { kind: "telemetry", id: "infra-ram" },
|
tile: { kind: "telemetry", id: "infra-ram" },
|
||||||
item: {
|
item: {
|
||||||
id: "infra-ram",
|
id: "infra-ram",
|
||||||
|
label: "Infra RAM",
|
||||||
|
value: { kind: "percent", value: 42 },
|
||||||
detail: "live - stale 15s",
|
detail: "live - stale 15s",
|
||||||
severity: "stale",
|
severity: "stale",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,7 @@ function isDashboardTileSnapshotRecord(
|
||||||
typeof value.savedAt === "number" &&
|
typeof value.savedAt === "number" &&
|
||||||
Number.isFinite(value.savedAt) &&
|
Number.isFinite(value.savedAt) &&
|
||||||
isDashboardTileReference(value.tile) &&
|
isDashboardTileReference(value.tile) &&
|
||||||
isDashboardTileSnapshotItem(value.item)
|
isDashboardTileSnapshotItem(value.tile, value.item)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,19 +369,118 @@ function isDashboardTileReference(
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDashboardTileSnapshotItem(
|
function isDashboardTileSnapshotItem(
|
||||||
|
tile: DashboardTileReference,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is DashboardTileSnapshotItem {
|
): value is DashboardTileSnapshotItem {
|
||||||
return (
|
if (!isSnapshotRecord(value) || value.id !== tile.id) return false;
|
||||||
isSnapshotRecord(value) &&
|
|
||||||
typeof value.id === "string" &&
|
switch (tile.kind) {
|
||||||
(value.detail === undefined || typeof value.detail === "string")
|
case "telemetry":
|
||||||
);
|
return (
|
||||||
|
typeof value.label === "string" &&
|
||||||
|
isMetricValue(value.value) &&
|
||||||
|
isSeverity(value.severity) &&
|
||||||
|
isOptionalString(value.detail) &&
|
||||||
|
isOptionalString(value.description) &&
|
||||||
|
isOptionalString(value.icon) &&
|
||||||
|
isOptionalNumberArray(value.sparkline)
|
||||||
|
);
|
||||||
|
case "service":
|
||||||
|
return (
|
||||||
|
typeof value.label === "string" &&
|
||||||
|
typeof value.description === "string" &&
|
||||||
|
isSeverity(value.severity) &&
|
||||||
|
isOptionalString(value.detail) &&
|
||||||
|
isOptionalString(value.icon) &&
|
||||||
|
isOptionalLink(value.link)
|
||||||
|
);
|
||||||
|
case "module":
|
||||||
|
return (
|
||||||
|
(value.kind === "summary" ||
|
||||||
|
value.kind === "weather" ||
|
||||||
|
value.kind === "custom") &&
|
||||||
|
isOptionalString(value.title) &&
|
||||||
|
isOptionalString(value.label) &&
|
||||||
|
isOptionalString(value.value) &&
|
||||||
|
isOptionalString(value.detail) &&
|
||||||
|
isOptionalString(value.icon) &&
|
||||||
|
(value.severity === undefined || isSeverity(value.severity))
|
||||||
|
);
|
||||||
|
case "status":
|
||||||
|
return (
|
||||||
|
typeof value.label === "string" &&
|
||||||
|
typeof value.value === "string" &&
|
||||||
|
isOptionalLink(value.link) &&
|
||||||
|
(value.severity === undefined || isSeverity(value.severity))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSnapshotRecord(value: unknown): value is Record<string, unknown> {
|
function isSnapshotRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null;
|
return typeof value === "object" && value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMetricValue(value: unknown): boolean {
|
||||||
|
if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false;
|
||||||
|
|
||||||
|
if (value.kind === "text") {
|
||||||
|
return (
|
||||||
|
typeof value.value === "string" &&
|
||||||
|
isOptionalString(value.unit)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericKinds = ["bytes", "latency", "number", "percent", "temperature"];
|
||||||
|
if (!numericKinds.includes(value.kind)) return false;
|
||||||
|
if (typeof value.value !== "number" || !Number.isFinite(value.value)) return false;
|
||||||
|
if (value.kind === "percent" && (value.value < 0 || value.value > 100)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const precision = value.precision;
|
||||||
|
return (
|
||||||
|
isOptionalString(value.unit) &&
|
||||||
|
(precision === undefined ||
|
||||||
|
(typeof precision === "number" &&
|
||||||
|
Number.isInteger(precision) &&
|
||||||
|
precision >= 0 &&
|
||||||
|
precision <= 4))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSeverity(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
value === "neutral" ||
|
||||||
|
value === "ok" ||
|
||||||
|
value === "warning" ||
|
||||||
|
value === "danger" ||
|
||||||
|
value === "stale" ||
|
||||||
|
value === "unavailable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalString(value: unknown): boolean {
|
||||||
|
return value === undefined || typeof value === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalNumberArray(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
value === undefined ||
|
||||||
|
(Array.isArray(value) &&
|
||||||
|
value.every((item) => typeof item === "number" && Number.isFinite(item)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalLink(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
value === undefined ||
|
||||||
|
(isSnapshotRecord(value) &&
|
||||||
|
typeof value.href === "string" &&
|
||||||
|
isOptionalString(value.label) &&
|
||||||
|
(value.external === undefined || typeof value.external === "boolean"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function staleDashboardTileDetail(
|
function staleDashboardTileDetail(
|
||||||
detail: string | undefined,
|
detail: string | undefined,
|
||||||
ageMs: number,
|
ageMs: number,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue