feat(hosts): a host's health tiles span the card and the gaps match; Add host is a dialog on the list (#5290, #5291)
#5290 "Host page: the health tiles span the card's width; even spacing above Ansible": the four tiles take equal quarters (halves on a phone) and their sparklines stretch across them; the Monitors + Agent row drops the Agent card's own margin, so the gap above Ansible is 1rem like every other. #5291 "Add host opens as a dialog over the Hosts list": a native <dialog> on /hosts/, opened by Add host or /hosts/?add=1. A name clash re-renders the list with it open, the error and what was typed. GET /hosts/new is gone; hosts/form.html is Edit only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ async def test_ansible_comes_before_the_docker_list(app, monkeypatch):
|
||||
"hosts/detail.html", host=host, monitors=[], uptime=None, alerts=[],
|
||||
linked_target=None, linkable_targets=[], ansible_sources=[])
|
||||
docker = page.index('hx-get="/plugins/docker/host/h1"')
|
||||
# #5290: Monitors and Agent share a row whose cards drop their own margin.
|
||||
assert '<div class="host-pair">' in page
|
||||
assert page.index(">Ansible</h3>") < docker
|
||||
assert page.index('hx-get="/plugins/host_agent/panel/h1"') < docker
|
||||
# Monitors are added on their own form, filed under this host (#5277);
|
||||
@@ -91,13 +93,40 @@ async def test_a_host_without_an_agent_can_get_a_manual_install(app):
|
||||
assert 'action="/plugins/host_agent/fleet/h1/register"' in page and ">Install manually</button>" in page
|
||||
|
||||
|
||||
async def test_add_host_sends_a_clash_back_with_what_was_typed(app):
|
||||
"""#5283: an inline error and the typed values, not a bare text page."""
|
||||
async with app.test_request_context("/hosts/new"):
|
||||
async def test_add_host_is_a_dialog_on_the_hosts_list(app):
|
||||
"""#5291: the form was two fields on a page of its own."""
|
||||
async with app.test_request_context("/hosts/"):
|
||||
session["user_role"] = "operator"
|
||||
page = await render_template("hosts/form.html", host=None, error="A host named nas already exists.",
|
||||
values={"name": "nas", "address": "10.0.0.9"})
|
||||
closed = await render_template("hosts/list.html", hosts=[], host_status={}, uptime={}, agent={}, add=None)
|
||||
dialog = re.search(r'<dialog id="add-host".*?</dialog>', closed, re.S).group(0)
|
||||
assert 'action="/hosts/"' in dialog and ">Cancel</button>" in dialog
|
||||
assert re.search(r'<input[^>]*name="address"[^>]*>', dialog).group(0).count("required") == 0
|
||||
assert "showModal()\">Add host</button>" in closed
|
||||
assert "getElementById('add-host').showModal();</script>" not in closed
|
||||
assert "/hosts/new" not in closed
|
||||
|
||||
async with app.test_request_context("/hosts/"):
|
||||
session["user_role"] = "viewer"
|
||||
viewer = await render_template("hosts/list.html", hosts=[], host_status={}, uptime={}, agent={}, add={})
|
||||
assert "<dialog" not in viewer and ">Add host<" not in viewer
|
||||
|
||||
|
||||
async def test_add_host_sends_a_clash_back_with_what_was_typed(app):
|
||||
"""#5283, #5291: the list again, the dialog open, an inline error and the typed values."""
|
||||
async with app.test_request_context("/hosts/"):
|
||||
session["user_role"] = "operator"
|
||||
page = await render_template(
|
||||
"hosts/list.html", hosts=[], host_status={}, uptime={}, agent={},
|
||||
add={"values": {"name": "nas", "address": "10.0.0.9"}, "error": "A host named nas already exists."})
|
||||
assert 'role="alert">A host named nas already exists.<' in page
|
||||
assert 'value="nas"' in page and 'value="10.0.0.9"' in page
|
||||
assert re.search(r'<input[^>]*name="address"[^>]*>', page).group(0).count("required") == 0
|
||||
assert '<a href="/hosts/">Hosts</a>' in page and ">Add host</span>" in page
|
||||
assert "getElementById('add-host').showModal();</script>" in page
|
||||
|
||||
|
||||
async def test_edit_host_keeps_its_page(app):
|
||||
host = SimpleNamespace(id="h1", name="nas", address="10.0.0.5")
|
||||
async with app.test_request_context("/hosts/h1/edit"):
|
||||
session["user_role"] = "operator"
|
||||
page = await render_template("hosts/form.html", host=host)
|
||||
assert 'action="/hosts/h1"' in page and 'value="nas"' in page and 'value="10.0.0.5"' in page
|
||||
assert '<a href="/hosts/h1">nas</a>' in page and ">Save</button>" in page
|
||||
|
||||
@@ -69,3 +69,12 @@ def test_sparkline_svg_handles_edge_cases():
|
||||
assert "polyline" in sparkline_svg([1, 2, 3])
|
||||
assert "polyline" in sparkline_svg([4, 4, 4]) # flat series, no div-by-zero
|
||||
assert "polyline" in sparkline_svg([1, None, 3]) # None values filtered
|
||||
|
||||
|
||||
def test_a_sparkline_can_stretch_across_its_tile():
|
||||
"""#5290: the host page's tiles are wide; the line fills them, stroke kept."""
|
||||
wide = sparkline_svg([1, 2, 3], height=24, fill_width=True)
|
||||
assert 'width="100%"' in wide and 'preserveAspectRatio="none"' in wide
|
||||
assert 'vector-effect="non-scaling-stroke"' in wide
|
||||
fixed = sparkline_svg([1, 2, 3])
|
||||
assert 'width="80"' in fixed and "preserveAspectRatio" not in fixed
|
||||
|
||||
Reference in New Issue
Block a user