fix(dashboard): address observability review findings
This commit is contained in:
parent
2d2b905d18
commit
bfb9a91e49
11 changed files with 188 additions and 10 deletions
|
|
@ -548,7 +548,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
|
||||||
datasource: uptimeMonitor(27),
|
datasource: uptimeMonitor(27),
|
||||||
detail: "fallback - exporter health from Prometheus",
|
detail: "fallback - exporter health from Prometheus",
|
||||||
}),
|
}),
|
||||||
]),
|
], "grid"),
|
||||||
],
|
],
|
||||||
statusStrips: [
|
statusStrips: [
|
||||||
{
|
{
|
||||||
|
|
@ -635,12 +635,17 @@ function service(seed: ServiceSeed): ServiceEntry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function group(id: string, title: string, services: ServiceEntry[]): ServiceGroup {
|
function group(
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
services: ServiceEntry[],
|
||||||
|
layout: ServiceGroup["layout"] = "list",
|
||||||
|
): ServiceGroup {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
layout,
|
||||||
layout: "list",
|
|
||||||
services,
|
services,
|
||||||
|
title,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,68 @@ describe("dashboard runtime loader", () => {
|
||||||
expect(store.listRevisions()).toHaveLength(1);
|
expect(store.listRevisions()).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("refreshes an existing initial seed when the bundled seed changes", async () => {
|
||||||
|
const store = await createTestStore();
|
||||||
|
const oldSeed = olderDimensionLabSeed();
|
||||||
|
store.seedDashboardIfEmpty(oldSeed, {
|
||||||
|
actor: "initial-seed",
|
||||||
|
message: "load initial dashboard document",
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = loadDashboardRuntime(store, {
|
||||||
|
refreshSeedDocument: true,
|
||||||
|
seedDocument: dimensionLabDashboardFixture,
|
||||||
|
seedIfEmpty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runtime.state).toBe("ready");
|
||||||
|
if (runtime.state !== "ready") throw new Error("expected ready dashboard");
|
||||||
|
expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).toContain(
|
||||||
|
"auto-refresh",
|
||||||
|
);
|
||||||
|
expect(runtime.document.serviceGroups.flatMap((group) => group.services).map((service) => service.id)).toContain(
|
||||||
|
"prompt-registry",
|
||||||
|
);
|
||||||
|
expect(store.listRevisions()).toHaveLength(2);
|
||||||
|
expect(store.getActiveDashboard()?.revision.actor).toBe("initial-seed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not refresh a dashboard after a user-authored revision", async () => {
|
||||||
|
const store = await createTestStore();
|
||||||
|
const oldSeed = olderDimensionLabSeed();
|
||||||
|
store.seedDashboardIfEmpty(oldSeed, {
|
||||||
|
actor: "initial-seed",
|
||||||
|
message: "load initial dashboard document",
|
||||||
|
});
|
||||||
|
store.commitDashboard(
|
||||||
|
{
|
||||||
|
...oldSeed,
|
||||||
|
metadata: {
|
||||||
|
...oldSeed.metadata,
|
||||||
|
title: "Custom Dashboard",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actor: "agent",
|
||||||
|
message: "customize dashboard",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const runtime = loadDashboardRuntime(store, {
|
||||||
|
refreshSeedDocument: true,
|
||||||
|
seedDocument: dimensionLabDashboardFixture,
|
||||||
|
seedIfEmpty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runtime.state).toBe("ready");
|
||||||
|
if (runtime.state !== "ready") throw new Error("expected ready dashboard");
|
||||||
|
expect(runtime.document.metadata.title).toBe("Custom Dashboard");
|
||||||
|
expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).not.toContain(
|
||||||
|
"auto-refresh",
|
||||||
|
);
|
||||||
|
expect(store.listRevisions()).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
test("returns empty state when no dashboard is active and seeding is disabled", async () => {
|
test("returns empty state when no dashboard is active and seeding is disabled", async () => {
|
||||||
const store = await createTestStore();
|
const store = await createTestStore();
|
||||||
|
|
||||||
|
|
@ -69,3 +131,20 @@ async function createTestStore() {
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function olderDimensionLabSeed() {
|
||||||
|
const document = structuredClone(dimensionLabDashboardFixture);
|
||||||
|
document.statusStrips = document.statusStrips.map((strip) => ({
|
||||||
|
...strip,
|
||||||
|
items: strip.items.filter((item) => item.id !== "auto-refresh"),
|
||||||
|
}));
|
||||||
|
document.serviceGroups = document.serviceGroups.map((group) =>
|
||||||
|
group.id === "ai-automation"
|
||||||
|
? {
|
||||||
|
...group,
|
||||||
|
services: group.services.filter((service) => service.id !== "prompt-registry"),
|
||||||
|
}
|
||||||
|
: group,
|
||||||
|
);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ export interface DashboardRuntimeInvalid {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardRuntimeOptions {
|
export interface DashboardRuntimeOptions {
|
||||||
|
refreshSeedDocument?: boolean;
|
||||||
seedIfEmpty?: boolean;
|
seedIfEmpty?: boolean;
|
||||||
seedDocument?: DashboardDocument;
|
seedDocument?: DashboardDocument;
|
||||||
}
|
}
|
||||||
|
|
@ -54,8 +55,20 @@ export function loadDashboardRuntime(
|
||||||
const dashboardStore = store || createDashboardStore();
|
const dashboardStore = store || createDashboardStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const seedDocument = options.seedDocument || dimensionLabDashboardFixture;
|
||||||
const active = dashboardStore.getActiveDashboard();
|
const active = dashboardStore.getActiveDashboard();
|
||||||
if (active) {
|
if (active) {
|
||||||
|
if (
|
||||||
|
options.refreshSeedDocument &&
|
||||||
|
shouldRefreshSeedDashboard(active, seedDocument)
|
||||||
|
) {
|
||||||
|
const refreshed = dashboardStore.commitDashboard(seedDocument, {
|
||||||
|
actor: "initial-seed",
|
||||||
|
message: "refresh bundled dashboard document",
|
||||||
|
});
|
||||||
|
return readyRuntimeState(refreshed.document, refreshed.id);
|
||||||
|
}
|
||||||
|
|
||||||
return readyRuntimeState(active.document, active.currentRevisionId);
|
return readyRuntimeState(active.document, active.currentRevisionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +82,7 @@ export function loadDashboardRuntime(
|
||||||
}
|
}
|
||||||
|
|
||||||
const seeded = dashboardStore.seedDashboardIfEmpty(
|
const seeded = dashboardStore.seedDashboardIfEmpty(
|
||||||
options.seedDocument || dimensionLabDashboardFixture,
|
seedDocument,
|
||||||
{
|
{
|
||||||
actor: "initial-seed",
|
actor: "initial-seed",
|
||||||
message: "load initial dashboard document",
|
message: "load initial dashboard document",
|
||||||
|
|
@ -113,3 +126,22 @@ function invalidRuntimeState(errors: string[]): DashboardRuntimeInvalid {
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRefreshSeedDashboard(
|
||||||
|
active: { document: DashboardDocument; revision: { actor: string } },
|
||||||
|
seedDocument: DashboardDocument,
|
||||||
|
): boolean {
|
||||||
|
if (active.revision.actor !== "initial-seed") return false;
|
||||||
|
if (!isBundledDimensionLabSeed(active.document, seedDocument)) return false;
|
||||||
|
return JSON.stringify(active.document) !== JSON.stringify(seedDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBundledDimensionLabSeed(
|
||||||
|
document: DashboardDocument,
|
||||||
|
seedDocument: DashboardDocument,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
document.metadata.title === seedDocument.metadata.title &&
|
||||||
|
document.metadata.description === seedDocument.metadata.description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,12 +113,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-frame__panels :global(.service-panel[data-model-id="runtime-health"]) {
|
.dashboard-frame__panels :global(.service-panel[data-layout="grid"]) {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-frame__panels
|
.dashboard-frame__panels
|
||||||
:global(.service-panel[data-model-id="runtime-health"] .panel__body) {
|
:global(.service-panel[data-layout="grid"] .panel__body) {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-frame__panels
|
.dashboard-frame__panels
|
||||||
:global(.service-panel[data-model-id="runtime-health"] .panel__body) {
|
:global(.service-panel[data-layout="grid"] .panel__body) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
let { group }: { group: UiServiceGroup } = $props();
|
let { group }: { group: UiServiceGroup } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="service-panel" data-model-id={group.id}>
|
<div class="service-panel" data-layout={group.layout || "list"} data-model-id={group.id}>
|
||||||
<Panel title={group.title}>
|
<Panel title={group.title}>
|
||||||
{#if group.summary?.length}
|
{#if group.summary?.length}
|
||||||
<StatusStrip id={`${group.id}:summary`} items={group.summary} compact />
|
<StatusStrip id={`${group.id}:summary`} items={group.summary} compact />
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ describe("dashboard model renderer", () => {
|
||||||
expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
|
expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
|
||||||
dimensionLabDashboardFixture.layout.serviceGroups,
|
dimensionLabDashboardFixture.layout.serviceGroups,
|
||||||
);
|
);
|
||||||
|
expect(dashboard.serviceGroups.find((group) => group.id === "runtime-health")?.layout).toBe(
|
||||||
|
"grid",
|
||||||
|
);
|
||||||
expect(dashboard.modules.map((module) => module.id)).toEqual([
|
expect(dashboard.modules.map((module) => module.id)).toEqual([
|
||||||
"weather-amsterdam",
|
"weather-amsterdam",
|
||||||
"runtime-health-summary",
|
"runtime-health-summary",
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ function telemetryToUi(card: TelemetryCard): UiTelemetryCard {
|
||||||
function serviceGroupToUi(group: ServiceGroup): UiServiceGroup {
|
function serviceGroupToUi(group: ServiceGroup): UiServiceGroup {
|
||||||
return {
|
return {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
|
layout: group.layout,
|
||||||
title: group.title,
|
title: group.title,
|
||||||
services: group.services.map(serviceToUi),
|
services: group.services.map(serviceToUi),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export interface UiServiceRow {
|
||||||
|
|
||||||
export interface UiServiceGroup {
|
export interface UiServiceGroup {
|
||||||
id: string;
|
id: string;
|
||||||
|
layout?: "grid" | "list";
|
||||||
title: string;
|
title: string;
|
||||||
services: UiServiceRow[];
|
services: UiServiceRow[];
|
||||||
summary?: UiStatusItem[];
|
summary?: UiStatusItem[];
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ import { loadDashboardRuntime } from "$lib/server/dashboard";
|
||||||
import { resolveDashboardDatasources } from "$lib/server/datasources";
|
import { resolveDashboardDatasources } from "$lib/server/datasources";
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
const dashboard = loadDashboardRuntime(undefined, { seedIfEmpty: true });
|
const dashboard = loadDashboardRuntime(undefined, {
|
||||||
|
refreshSeedDocument: true,
|
||||||
|
seedIfEmpty: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (dashboard.state !== "ready") {
|
if (dashboard.state !== "ready") {
|
||||||
return { dashboard };
|
return { dashboard };
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { invalidateAll } from "$app/navigation";
|
||||||
import {
|
import {
|
||||||
DashboardFrame,
|
DashboardFrame,
|
||||||
SystemState,
|
SystemState,
|
||||||
dashboardDocumentToUiDashboard,
|
dashboardDocumentToUiDashboard,
|
||||||
} from "$lib/ui";
|
} from "$lib/ui";
|
||||||
|
import { onDestroy } from "svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
@ -29,6 +32,30 @@
|
||||||
let pageDescription = $derived(
|
let pageDescription = $derived(
|
||||||
dashboard?.subtitle || stateSubtitle || stateMessage,
|
dashboard?.subtitle || stateSubtitle || stateMessage,
|
||||||
);
|
);
|
||||||
|
let refreshTimer: number | undefined;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
if (refreshTimer) {
|
||||||
|
window.clearInterval(refreshTimer);
|
||||||
|
refreshTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshIntervalSeconds =
|
||||||
|
data.dashboard.state === "ready"
|
||||||
|
? data.dashboard.document.metadata.refreshIntervalSeconds
|
||||||
|
: undefined;
|
||||||
|
if (!refreshIntervalSeconds) return;
|
||||||
|
|
||||||
|
refreshTimer = window.setInterval(() => {
|
||||||
|
void invalidateAll();
|
||||||
|
}, refreshIntervalSeconds * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (refreshTimer) window.clearInterval(refreshTimer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,33 @@ test.describe("dashboard page QA gate", () => {
|
||||||
const metrics = await page.evaluate(() => {
|
const metrics = await page.evaluate(() => {
|
||||||
const footer = document.querySelector("[data-model-id='footer-status']");
|
const footer = document.querySelector("[data-model-id='footer-status']");
|
||||||
const runtime = document.querySelector("[data-model-id='runtime-health']");
|
const runtime = document.querySelector("[data-model-id='runtime-health']");
|
||||||
|
const visibleItems = [
|
||||||
|
...document.querySelectorAll(".telemetry-card"),
|
||||||
|
...document.querySelectorAll(".service-row"),
|
||||||
|
...document.querySelectorAll(".footer-cell"),
|
||||||
|
].map((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
bottom: rect.bottom,
|
||||||
|
height: rect.height,
|
||||||
|
id: element.getAttribute("data-model-id"),
|
||||||
|
top: rect.top,
|
||||||
|
width: rect.width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
clippedItems: visibleItems.filter((item) =>
|
||||||
|
item.top < 0 ||
|
||||||
|
item.bottom > window.innerHeight ||
|
||||||
|
item.width <= 0 ||
|
||||||
|
item.height <= 0
|
||||||
|
),
|
||||||
|
footerCellCount: document.querySelectorAll(".footer-cell").length,
|
||||||
scrollWidth: document.documentElement.scrollWidth,
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
scrollHeight: document.documentElement.scrollHeight,
|
scrollHeight: document.documentElement.scrollHeight,
|
||||||
|
serviceRowCount: document.querySelectorAll(".service-row").length,
|
||||||
|
telemetryCardCount: document.querySelectorAll(".telemetry-card").length,
|
||||||
footerBottom: footer?.getBoundingClientRect().bottom ?? 0,
|
footerBottom: footer?.getBoundingClientRect().bottom ?? 0,
|
||||||
runtimeBottom: runtime?.getBoundingClientRect().bottom ?? 0,
|
runtimeBottom: runtime?.getBoundingClientRect().bottom ?? 0,
|
||||||
};
|
};
|
||||||
|
|
@ -77,6 +100,10 @@ test.describe("dashboard page QA gate", () => {
|
||||||
expect(metrics.scrollHeight).toBeLessThanOrEqual(956);
|
expect(metrics.scrollHeight).toBeLessThanOrEqual(956);
|
||||||
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
|
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
|
||||||
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
|
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
|
||||||
|
expect(metrics.telemetryCardCount).toBe(16);
|
||||||
|
expect(metrics.serviceRowCount).toBe(28);
|
||||||
|
expect(metrics.footerCellCount).toBe(5);
|
||||||
|
expect(metrics.clippedItems).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps the first screen usable on mobile", async ({ page }, testInfo) => {
|
test("keeps the first screen usable on mobile", async ({ page }, testInfo) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue