fix(web): ignore malformed dashboard tile snapshots
This commit is contained in:
parent
4ce42fb52b
commit
88c448258d
3 changed files with 112 additions and 1 deletions
|
|
@ -45,6 +45,11 @@ type DashboardTileResponse =
|
||||||
state: "not_found";
|
state: "not_found";
|
||||||
tile: DashboardTileReference;
|
tile: DashboardTileReference;
|
||||||
message: string;
|
message: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
state: "disabled";
|
||||||
|
tile: DashboardTileReference;
|
||||||
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
|
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,15 @@ describe("dashboard refresh lifecycle", () => {
|
||||||
},
|
},
|
||||||
schemaVersion: "dashboard.v1",
|
schemaVersion: "dashboard.v1",
|
||||||
});
|
});
|
||||||
|
store.saveTile({
|
||||||
|
currentRevisionId: "revision-a",
|
||||||
|
response: {
|
||||||
|
state: "disabled",
|
||||||
|
tile: { kind: "module", id: "disabled-module" },
|
||||||
|
message: "Disabled",
|
||||||
|
},
|
||||||
|
schemaVersion: "dashboard.v1",
|
||||||
|
});
|
||||||
|
|
||||||
now = 61_000;
|
now = 61_000;
|
||||||
|
|
||||||
|
|
@ -193,6 +202,53 @@ describe("dashboard refresh lifecycle", () => {
|
||||||
schemaVersion: "dashboard.v1",
|
schemaVersion: "dashboard.v1",
|
||||||
})).toEqual([]);
|
})).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ignores malformed stored tile snapshots", () => {
|
||||||
|
const storage = createMemoryStorage();
|
||||||
|
storage.setItem(
|
||||||
|
"dimensionlab.dashboard.tiles.v1",
|
||||||
|
JSON.stringify({
|
||||||
|
currentRevisionId: "revision-a",
|
||||||
|
schemaVersion: "dashboard.v1",
|
||||||
|
tiles: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
item: { id: "infra-ram", detail: "live" },
|
||||||
|
savedAt: 1_000,
|
||||||
|
tile: { kind: "telemetry", id: "infra-ram" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: { id: "broken-detail", detail: 42 },
|
||||||
|
savedAt: 1_000,
|
||||||
|
tile: { kind: "telemetry", id: "broken-detail" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
version: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = createDashboardTileSnapshotStore(storage, {
|
||||||
|
now: () => 16_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.restore({
|
||||||
|
currentRevisionId: "revision-a",
|
||||||
|
schemaVersion: "dashboard.v1",
|
||||||
|
})).toEqual([
|
||||||
|
{
|
||||||
|
ageMs: 15_000,
|
||||||
|
response: {
|
||||||
|
state: "ready",
|
||||||
|
tile: { kind: "telemetry", id: "infra-ram" },
|
||||||
|
item: {
|
||||||
|
id: "infra-ram",
|
||||||
|
detail: "live - stale 15s",
|
||||||
|
severity: "stale",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitFor(predicate: () => boolean) {
|
async function waitFor(predicate: () => boolean) {
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,12 @@ export function createDashboardTileSnapshotStore(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload as DashboardTileSnapshotPayload;
|
return {
|
||||||
|
currentRevisionId: payload.currentRevisionId,
|
||||||
|
schemaVersion: payload.schemaVersion,
|
||||||
|
tiles: payload.tiles.filter(isDashboardTileSnapshotRecord),
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +337,51 @@ function dashboardTileSnapshotKey(tile: DashboardTileReference): string {
|
||||||
return JSON.stringify(tile);
|
return JSON.stringify(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDashboardTileSnapshotRecord(
|
||||||
|
value: unknown,
|
||||||
|
): value is DashboardTileSnapshotRecord {
|
||||||
|
if (!isSnapshotRecord(value)) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof value.savedAt === "number" &&
|
||||||
|
Number.isFinite(value.savedAt) &&
|
||||||
|
isDashboardTileReference(value.tile) &&
|
||||||
|
isDashboardTileSnapshotItem(value.item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDashboardTileReference(
|
||||||
|
value: unknown,
|
||||||
|
): value is DashboardTileReference {
|
||||||
|
if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false;
|
||||||
|
|
||||||
|
switch (value.kind) {
|
||||||
|
case "telemetry":
|
||||||
|
case "module":
|
||||||
|
return typeof value.id === "string";
|
||||||
|
case "service":
|
||||||
|
return typeof value.groupId === "string" && typeof value.id === "string";
|
||||||
|
case "status":
|
||||||
|
return typeof value.stripId === "string" && typeof value.id === "string";
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDashboardTileSnapshotItem(
|
||||||
|
value: unknown,
|
||||||
|
): value is DashboardTileSnapshotItem {
|
||||||
|
return (
|
||||||
|
isSnapshotRecord(value) &&
|
||||||
|
typeof value.id === "string" &&
|
||||||
|
(value.detail === undefined || typeof value.detail === "string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSnapshotRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
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