Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/24126-system-overview-metric-height.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/platform-objects': patch
---

Clause-②: no

fix(platform-objects): the System Overview dashboard's six metric tiles declare `layout.h: 1`, one row tall, where they declared `2`

The `system_overview` dashboard the Setup app ships (`SystemOverviewDashboard`) authored every one of its six `type: 'metric'` tiles (Total Users, Organizations, Active Sessions, Packages Installed, Login Events, Config Changes) at `layout.h: 2`. The console draws a widget over the grid rows its `h` names, each row floors at 5rem and grows to its content, and a metric card fills its box, so each tile drew a box about twice the height of its content: the title and the number in the top half, the lower half empty. The platform draws what the metadata declares, so the authored size is what is corrected.

- **FROM:** the six metric tiles declare `layout: { …, h: 2 }`.
- **TO:** the six metric tiles declare `layout: { …, h: 1 }`; each now draws at content height.
- **Unchanged:** every `x`, `y` and `w`, the chart and table widgets (`h: 4`), every widget id, filter and binding, and every export.
- **An org that already published an overlay of `system_overview`** is served that overlay as stored (ADR-0126 Regime O), so it keeps the tile heights it was saved with.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// System Overview metric tile height (the maintainer's ruling B on
// objectstack-ai/objectui#12704).
//
// The console draws a dashboard's positioned grid with `gridRow: span h` over
// rows that floor at 5rem and grow to their content, and a metric card fills
// its box. A KPI tile that declares `h: 2` therefore draws a box about twice the
// height of its content: the title and the number sit in the top half and the
// lower half is empty. The ruling is that the platform draws what the metadata
// declares (ADR-0078 forbids an `h` that is read and not honoured), so the
// AUTHORED size is what gets corrected, not the renderer: every metric tile on
// the shipped board is ONE row tall.
//
// What is pinned is the property "a KPI tile is one row", read off `type`
// rather than off a list of ids, so a metric tile added to the board later is
// held to it as well. Only `h` is pinned. `x`, `y` and `w` place the tile and
// are a separate decision this file has no opinion on.
//
// The pin carries its OPPOSITE DIRECTION, because "every metric tile is one row"
// has two ways to pass vacuously:
//
// - it also holds if the read finds no metric tile at all (a renamed key, a
// board that moved elsewhere) ⇒ the six tiles are named and the read must
// return exactly them;
// - it also holds if the fix were a blanket flattening of the whole board ⇒
// the chart and table widgets must stay taller than a tile, because they
// are the widgets that genuinely need the rows.

import { describe, it, expect } from 'vitest';
import { SystemOverviewDashboard } from './system_overview.dashboard.js';

interface Widget {
id?: string;
type?: string;
layout?: { x: number; y: number; w: number; h: number };
}
const widgets: Widget[] = (SystemOverviewDashboard as unknown as { widgets?: Widget[] }).widgets ?? [];

const metricTiles = widgets.filter((w) => w.type === 'metric');
const otherWidgets = widgets.filter((w) => w.type !== 'metric');

describe('System Overview metric tiles are one row tall', () => {
// Control for the assertion below: if this read ever returns nothing, the
// `h` check is a loop over an empty array and passes whatever the board says.
it('the read finds the six KPI tiles', () => {
expect(metricTiles.map((w) => w.id)).toEqual([
'widget_total_users',
'widget_organizations',
'widget_active_sessions',
'widget_packages_installed',
'widget_login_events',
'widget_config_changes',
]);
});

it('every metric tile declares layout.h of 1', () => {
const offenders = metricTiles
.filter((w) => w.layout?.h !== 1)
.map((w) => `${w.id} → layout.h=${String(w.layout?.h)}`);
expect(offenders, 'metric tiles taller than one row').toEqual([]);
});

// Opposite direction: the correction is for the KPI tiles, not the board.
it('opposite direction: the chart and table widgets stay taller than a tile', () => {
expect(otherWidgets.length, 'the board still carries non-metric widgets').toBeGreaterThan(0);
const flattened = otherWidgets
.filter((w) => !(typeof w.layout?.h === 'number' && w.layout.h > 1))
.map((w) => `${w.id} → layout.h=${String(w.layout?.h)}`);
expect(flattened, 'non-metric widgets that lost their height').toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const SystemOverviewDashboard = Dashboard.create({
dataset: 'sys_user_metrics', values: ['user_count'],
title: 'Total Users',
type: 'metric',
layout: { x: 0, y: 0, w: 3, h: 2 },
layout: { x: 0, y: 0, w: 3, h: 1 },
// #7531 — a TOTAL must not move when the date bar moves. The dashboard's
// `created_at` global filter is broadcast into every widget's analytics
// query (#2501), so before this opt-out the tile reported "users created
Expand All @@ -67,7 +67,7 @@ export const SystemOverviewDashboard = Dashboard.create({
// `requiresService: 'org-scoping'` — gate this KPI the same way so the
// overview doesn't dangle a metric the admin can't act on.
requiresService: 'org-scoping',
layout: { x: 3, y: 0, w: 3, h: 2 },
layout: { x: 3, y: 0, w: 3, h: 1 },
// #7613 — the same #2501 fan-out as Total Users above, on the same row.
// `sys_organization.created_at` exists, so the date bar landed on it and
// the tile reported "organizations created in the last 7 days" under a
Expand All @@ -83,7 +83,7 @@ export const SystemOverviewDashboard = Dashboard.create({
dataset: 'sys_session_metrics', values: ['session_count'],
title: 'Active Sessions',
type: 'metric',
layout: { x: 6, y: 0, w: 3, h: 2 },
layout: { x: 6, y: 0, w: 3, h: 1 },
// #7531 — "Active Sessions" counted EVERY `sys_session` row: the dataset
// is a bare count and the widget carried no predicate, so a signed-out or
// long-expired session was still reported as active. `sys_session` can
Expand Down Expand Up @@ -113,7 +113,7 @@ export const SystemOverviewDashboard = Dashboard.create({
// Cloud-only object — only registered when service-tenant is loaded.
// Hide this widget gracefully in single-environment runtimes.
requiresObject: 'sys_package_installation',
layout: { x: 9, y: 0, w: 3, h: 2 },
layout: { x: 9, y: 0, w: 3, h: 1 },
// Which installations count: the ones that are actually installed. This
// predicate is the tile's own, and it is ORTHOGONAL to the opt-out below
// — the two answer different questions and both stand.
Expand Down Expand Up @@ -162,7 +162,7 @@ export const SystemOverviewDashboard = Dashboard.create({
dataset: 'sys_audit_log_metrics', values: ['event_count'],
title: 'Login Events',
type: 'metric',
layout: { x: 0, y: 2, w: 6, h: 2 },
layout: { x: 0, y: 2, w: 6, h: 1 },
filter: { action: 'login' },
colorVariant: 'blue',
description: 'Authentication events recorded by the audit log',
Expand All @@ -172,7 +172,7 @@ export const SystemOverviewDashboard = Dashboard.create({
dataset: 'sys_audit_log_metrics', values: ['event_count'],
title: 'Config Changes',
type: 'metric',
layout: { x: 6, y: 2, w: 6, h: 2 },
layout: { x: 6, y: 2, w: 6, h: 1 },
filter: { action: 'config_change' },
colorVariant: 'blue',
description: 'System configuration modifications',
Expand Down
Loading