Files
FabledSteward/tests/core/test_dashboard_defaults.py
T
bvandeusenandClaude Opus 5.5 4dcf958022
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 55s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m28s
feat(dashboard): widgets fit their content; wide screens get more grid columns (#5249)
Each widget is as tall as its content, never taller than its saved height
(where it scrolls). The dashboard is capped at 2400px instead of 1400px.
Past 12 columns' worth of width it adds columns (one per 110px, up to 24)
and places widgets again in reading order at their saved widths, so more sit
side by side instead of stretching. Edit mode shows and saves the
12-column layout, so a layout doesn't depend on the screen it was edited on.

The share view uses the same layout code (static/dash-layout.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:10:42 -04:00

120 lines
6.3 KiB
Python

"""The dashboard starts from a curated layout, the duplicate widgets are gone,
and the Overview says what each number counts (#5213)."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from quart import render_template, session
from steward.core.status import StatusEntry
from steward.core.widgets import RECOMMENDED, RETIRED, WIDGET_REGISTRY, recommended_layout
from steward.status.routes import overview_alerts
from tests.test_style_tokens import hex_colours
ALL_ON = {p: {"enabled": True} for p in ("docker", "host_agent", "traefik", "unifi", "snmp")}
def _overlap(a, b):
return (a["x"] < b["x"] + b["w"] and b["x"] < a["x"] + a["w"]
and a["y"] < b["y"] + b["h"] and b["y"] < a["y"] + a["h"])
def test_retired_widgets_point_at_the_one_that_replaced_them():
for old, new in RETIRED.items():
assert old not in WIDGET_REGISTRY and new in WIDGET_REGISTRY
assert all(key in WIDGET_REGISTRY for key, *_ in RECOMMENDED)
def test_the_recommended_layout_fills_rows_without_overlap():
items = recommended_layout(ALL_ON)
assert [i["key"] for i in items] == [key for key, *_ in RECOMMENDED]
assert items[0] == {"key": "status_overview", "x": 0, "y": 0, "w": 12, "h": 3, "config": {}}
assert (items[1]["x"], items[1]["y"], items[2]["x"], items[2]["y"]) == (0, 3, 6, 3)
for i, a in enumerate(items):
assert a["x"] + a["w"] <= 12
assert not any(_overlap(a, b) for b in items[i + 1:]), a
def test_the_recommended_layout_skips_what_is_not_enabled():
keys = [i["key"] for i in recommended_layout({})]
assert keys == ["status_overview", "hosts_overview", "monitors", "uptime_summary"]
# uptime no longer fits beside the monitors, so it starts the next row
items = recommended_layout({})
assert (items[3]["x"], items[3]["y"]) == (0, 10)
def test_overview_alerts_put_the_breaching_first():
from steward.models.alerts import AlertStateEnum
ack = SimpleNamespace(state=AlertStateEnum.acknowledged, last_transition_at=None, no_data_since=None)
silent = SimpleNamespace(state=AlertStateEnum.inactive, last_transition_at=None,
no_data_since=datetime(2026, 10, 7, tzinfo=timezone.utc))
firing = SimpleNamespace(state=AlertStateEnum.firing, last_transition_at=datetime(2026, 10, 6, tzinfo=timezone.utc),
no_data_since=None)
rows = overview_alerts([(ack, "r-ack"), (silent, "r-silent"), (firing, "r-firing")])
assert [(r["rule"], r["label"]) for r in rows] == [
("r-firing", "firing"), ("r-silent", "no_data"), ("r-ack", "acknowledged")]
assert rows[1]["since"] == silent.no_data_since # silent since the series stopped
assert rows[0]["since"] == firing.last_transition_at
async def test_the_overview_labels_its_counts_and_lists_what_is_wrong(app):
now = datetime.now(timezone.utc)
rule = SimpleNamespace(id="r1", name="nas disk full")
alerts = [{"rule": rule, "label": "firing", "since": now - timedelta(minutes=12)}]
entries = [
StatusEntry(kind="http", key="1", name="web", status="up", detail_url="/monitors/1"),
StatusEntry(kind="tcp", key="2", name="ssh-nas", status="down", detail_url="/monitors/2",
since=now - timedelta(hours=2)),
]
async with app.test_request_context("/status/widget"):
session["user_id"] = "u1"
page = await render_template("status/widget.html", entries=entries, up=1, down=1, pending=0,
total_hosts=3, alerts=alerts, widget_id="1")
assert ">Monitors<" in page and ">Alerts<" in page and ">firing<" in page and ">hosts<" in page
assert '<span class="badge badge-crit">Firing</span>' in page
assert 'href="/alerts/history?rule=r1"' in page and "12m ago" in page
assert '<span class="badge badge-crit">Down</span>' in page and "2h ago" in page
assert page.index("nas disk full") < page.index("ssh-nas") # alerts, then monitors
assert ">pending<" not in page # a zero pending count is not shown
assert not hex_colours(page), hex_colours(page)
async def test_a_quiet_overview_says_so(app):
async with app.test_request_context("/status/widget"):
page = await render_template("status/widget.html", entries=[], up=0, down=0, pending=0,
total_hosts=1, alerts=[], widget_id="1")
assert "No monitors configured, and no alert is firing." in page
assert ">host<" in page and 'href="/alerts/"' not in page # the share view has no links
async def test_the_hosts_widget_shows_load(app):
host = SimpleNamespace(id="h1", name="web-1")
agent = {"web-1": {"cpu_pct": 12.0, "mem_used_pct": 40.0, "load_1m": 0.734, "fresh": True}}
async with app.test_request_context("/hosts/overview/widget"):
page = await render_template("hosts/overview_widget.html", hosts=[host], host_status={},
uptime={}, agent=agent, cpu_sparks={})
assert ">load</span> 0.73" in page
async def test_a_widget_polls_at_its_own_pace(app):
defn = {**WIDGET_REGISTRY["uptime_summary"]}
item = SimpleNamespace(db=SimpleNamespace(id=7, grid_x=0, grid_y=0, grid_w=6, grid_h=5),
title="Uptime", defn=defn, hx_url="/uptime/widget")
async with app.test_request_context("/"):
page = await render_template("dashboard/_grid_item.html", item=item, can_edit=False, poll_interval=30)
assert "every 300s" in page
defn.pop("poll_seconds")
async with app.test_request_context("/"):
page = await render_template("dashboard/_grid_item.html", item=item, can_edit=False, poll_interval=30)
assert "every 30s" in page
async def test_a_grid_item_carries_its_saved_layout(app):
"""#5249: the saved 12-column place and height, which the page fits to
content in view mode and restores in edit mode."""
item = SimpleNamespace(db=SimpleNamespace(id=7, grid_x=6, grid_y=3, grid_w=6, grid_h=5),
title="Uptime", defn={"poll": False, "detail_url": "/x"}, hx_url="/uptime/widget")
async with app.test_request_context("/"):
page = await render_template("dashboard/_grid_item.html", item=item, can_edit=False, poll_interval=30)
assert 'data-x="6" data-y="3"' in page and 'data-w="6" data-h="5"' in page
assert 'gs-x="6" gs-y="3"' in page