#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>
133 lines
7.3 KiB
Python
133 lines
7.3 KiB
Python
"""The host page: what sits where (#5214)."""
|
||
import re
|
||
from types import SimpleNamespace
|
||
|
||
from quart import render_template, session
|
||
|
||
|
||
async def test_ansible_comes_before_the_docker_list(app, monkeypatch):
|
||
# The Docker list can run to dozens of containers; Ansible under it was
|
||
# easy to miss, so it sits above.
|
||
monkeypatch.setitem(app.config, "PLUGINS", {"docker": {"enabled": True}})
|
||
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
|
||
async with app.test_request_context("/hosts/h1"):
|
||
session["user_role"] = "operator"
|
||
page = await render_template(
|
||
"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);
|
||
# each row's own page has the labelled Edit/Pause/Delete.
|
||
assert 'href="/monitors/new?host_id=h1"' in page
|
||
assert "row-actions" not in page and 'action="/monitors/add"' not in page
|
||
|
||
|
||
async def test_adding_a_monitor_from_a_host_page_says_whose_it_is(app):
|
||
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
|
||
async with app.test_request_context("/monitors/new?host_id=h1"):
|
||
session["user_role"] = "operator"
|
||
page = await render_template(
|
||
"monitors/form.html", monitor=None, host=host, mtype="icmp", config={}, types=[],
|
||
values={"name": "", "target": "10.0.0.5", "host_id": "h1", "check_interval_seconds": 0})
|
||
assert '<a href="/hosts/h1">box</a>' in page # crumb and hint name the host
|
||
assert 'name="host_id" value="h1"' in page and 'value="10.0.0.5"' in page
|
||
assert 'href="/hosts/h1" class="btn btn-sm btn-ghost">Cancel' in page
|
||
|
||
|
||
async def test_the_agent_card_keeps_its_actions_under_manage(app):
|
||
"""#5278: "Remove" said nothing about what it removed."""
|
||
from datetime import datetime, timezone
|
||
from tests._templates import render_plugin_template
|
||
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
|
||
reg = SimpleNamespace(agent_version="1.8.0", last_seen_at=datetime.now(timezone.utc))
|
||
target = SimpleNamespace(id="t1", name="box")
|
||
async with app.test_request_context("/plugins/host_agent/panel/h1"):
|
||
session["user_role"] = "admin"
|
||
page = await render_plugin_template(
|
||
app, "host_agent", "panel.html", host=host, reg=reg, reporting=True, stale=False,
|
||
target=target, ansible_available=True, managed_key_set=True)
|
||
manage = page.index(">Manage agent</summary>")
|
||
assert page.index("Full metrics →") < manage
|
||
for action in (">Update agent</button>", ">Rotate token</button>", ">Unregister agent</button>"):
|
||
assert page.index(action) > manage
|
||
assert ">Remove</button>" not in page
|
||
|
||
|
||
async def test_the_hosts_list_says_what_is_wrong_and_labels_its_figures(app):
|
||
"""#5279–#5281: labelled CPU and Memory, one uptime figure, status with how
|
||
long, no SLA tab and no per-row edit/delete."""
|
||
from datetime import datetime, timedelta, timezone
|
||
now = datetime.now(timezone.utc)
|
||
down = SimpleNamespace(id="h1", name="nas", address="10.0.0.5")
|
||
quiet = SimpleNamespace(id="h2", name="pi", address="")
|
||
async with app.test_request_context("/hosts/"):
|
||
session["user_role"] = "admin"
|
||
page = await render_template(
|
||
"hosts/list.html", hosts=[down, quiet],
|
||
host_status={"h1": {"state": "down", "latency_ms": None, "count": 2,
|
||
"down_since": now - timedelta(minutes=12)}},
|
||
uptime={"h1": {"24h": 91.5, "7d": 98.0, "30d": 99.42}},
|
||
agent={"pi": {"fresh": False, "last_at": now - timedelta(minutes=7),
|
||
"cpu_pct": 3.0, "mem_used_pct": 41.0}})
|
||
assert re.findall(r"<th[^>]*>([^<]+)</th>", page) == [
|
||
"Name", "Address", "Status", "Latency", "Monitors", "30d uptime", "CPU", "Memory"]
|
||
assert "for 12m" in page and "agent silent" in page and "for 7m" in page
|
||
assert "99.42%" in page and 'title="24h 91.50% · 7d 98.00%"' in page
|
||
assert ">41%<" in page and "tone-dim" in page # a silent agent's last reading, greyed
|
||
assert "/hosts/uptime" not in page and ">Delete<" not in page and ">Edit<" not in page
|
||
|
||
|
||
async def test_a_host_without_an_agent_can_get_a_manual_install(app):
|
||
"""#5282: the manual install moved here from the fleet's add-host form."""
|
||
from tests._templates import render_plugin_template
|
||
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
|
||
async with app.test_request_context("/plugins/host_agent/panel/h1"):
|
||
session["user_role"] = "admin"
|
||
page = await render_plugin_template(
|
||
app, "host_agent", "panel.html", host=host, reg=None, reporting=False, stale=False,
|
||
target=None, ansible_available=False, managed_key_set=False)
|
||
assert 'action="/plugins/host_agent/fleet/h1/register"' in page and ">Install manually</button>" in page
|
||
|
||
|
||
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"
|
||
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 "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
|