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:
@@ -690,7 +690,7 @@ async def host_vitals(host_id: str):
|
||||
# At-a-glance disk = the root filesystem (what people actually care about),
|
||||
# not the "worst" mount which is opaque. Worst stays on the full-metrics page.
|
||||
disk_root = (latest.get(host.name + ":/", {}) or {}).get("disk_used_pct")
|
||||
sparks = {k: sparkline_svg(v) for k, v in series_raw.items()}
|
||||
sparks = {k: sparkline_svg(v, height=24, fill_width=True) for k, v in series_raw.items()}
|
||||
# Normalize load by core count so it's comparable across hardware (load/core
|
||||
# as %). Cores derived from the per-core CPU sub-resources already collected.
|
||||
cores = sum(1 for k in latest if k.startswith(host.name + ":core"))
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{%- if reg.agent_version %}{{ " · " if not stale }}agent v{{ reg.agent_version }}{% endif -%}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-strip" style="margin-bottom:0;">
|
||||
<div class="stat-strip host-health-tiles" style="margin-bottom:0;">
|
||||
{{ stat_tile("CPU", pct(cpu), state=status_class(cpu, 'cpu'), hint=sparks.cpu|safe) }}
|
||||
{{ stat_tile("Memory", pct(mem), state=status_class(mem, 'mem'), hint=sparks.mem|safe) }}
|
||||
{{ stat_tile("Disk /", pct(disk_root), state=status_class(disk_root, 'disk'), hint=sparks.disk|safe) }}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<p style="color:var(--text-muted);font-size:0.84rem;margin:-0.4rem 0 1.25rem;">
|
||||
Bulk agent operations across your inventory. For a single host, use its
|
||||
<a href="/hosts/">host page</a>: provisioning, a manual install command and
|
||||
metrics live there. New hosts are added with <a href="/hosts/new">Add host</a>.
|
||||
metrics live there. New hosts are added with <a href="/hosts/?add=1">Add host</a>.
|
||||
</p>
|
||||
|
||||
{% if new_token %}
|
||||
@@ -54,7 +54,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="empty">No agents registered yet. <a href="/hosts/new">Add a host</a>, then install its agent from the host's page.</td></tr>
|
||||
<tr><td colspan="7" class="empty">No agents registered yet. <a href="/hosts/?add=1">Add a host</a>, then install its agent from the host's page.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+11
-6
@@ -77,11 +77,15 @@ async def collect_status(db) -> list[StatusEntry]:
|
||||
|
||||
|
||||
def sparkline_svg(values: list[float], width: int = 80, height: int = 20,
|
||||
stroke: str = "#6060c0") -> str:
|
||||
"""Tiny inline SVG polyline for a response-time series."""
|
||||
stroke: str = "#6060c0", fill_width: bool = False) -> str:
|
||||
"""Tiny inline SVG polyline for a response-time series. With fill_width
|
||||
the line stretches across its container (CSS sets the width) and keeps a
|
||||
1.5px stroke."""
|
||||
vals = [v for v in values if v is not None]
|
||||
size = (f'width="100%" height="{height}" preserveAspectRatio="none"' if fill_width
|
||||
else f'width="{width}" height="{height}"')
|
||||
if len(vals) < 2:
|
||||
return f'<svg width="{width}" height="{height}"></svg>'
|
||||
return f'<svg {size}></svg>'
|
||||
mn, mx = min(vals), max(vals)
|
||||
if mx == mn:
|
||||
mx = mn + 1.0
|
||||
@@ -92,10 +96,11 @@ def sparkline_svg(values: list[float], width: int = 80, height: int = 20,
|
||||
y = height - (v - mn) / (mx - mn) * (height - 2) - 1
|
||||
pts.append(f"{x:.1f},{y:.1f}")
|
||||
poly = " ".join(pts)
|
||||
style = "display:block;" if fill_width else "vertical-align:middle;"
|
||||
keep = ' vector-effect="non-scaling-stroke"' if fill_width else ""
|
||||
return (
|
||||
f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" '
|
||||
f'style="vertical-align:middle;">'
|
||||
f'<polyline points="{poly}" fill="none" stroke="{stroke}" stroke-width="1.5"/>'
|
||||
f'<svg {size} viewBox="0 0 {width} {height}" style="{style}">'
|
||||
f'<polyline points="{poly}" fill="none" stroke="{stroke}" stroke-width="1.5"{keep}/>'
|
||||
f'</svg>'
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +41,13 @@ async def overview_widget():
|
||||
@hosts_bp.get("/")
|
||||
@require_role(UserRole.viewer)
|
||||
async def list_hosts():
|
||||
# ?add=1 opens the Add host dialog on load (#5291).
|
||||
return await _list_page(add={} if request.args.get("add") else None)
|
||||
|
||||
|
||||
async def _list_page(add: dict | None = None):
|
||||
"""The Hosts list. `add` opens the Add host dialog: {} for an empty form,
|
||||
or {"values": form, "error": msg} when a submit comes back."""
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
result = await db.execute(select(Host).order_by(Host.name))
|
||||
hosts = result.scalars().all()
|
||||
@@ -57,6 +64,7 @@ async def list_hosts():
|
||||
host_status=host_status,
|
||||
uptime=uptime,
|
||||
agent=agent,
|
||||
add=add,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,12 +101,6 @@ async def host_detail(host_id: str):
|
||||
)
|
||||
|
||||
|
||||
@hosts_bp.get("/new")
|
||||
@require_role(UserRole.operator)
|
||||
async def new_host():
|
||||
return await render_template("hosts/form.html", host=None)
|
||||
|
||||
|
||||
@hosts_bp.post("/")
|
||||
@require_role(UserRole.operator)
|
||||
async def create_host():
|
||||
@@ -112,8 +114,7 @@ async def create_host():
|
||||
try:
|
||||
await host_svc.check_name_free(db, host.name)
|
||||
except ValueError as exc:
|
||||
return await render_template("hosts/form.html", host=None, values=form,
|
||||
error=str(exc)), 400
|
||||
return await _list_page(add={"values": form, "error": str(exc)}), 400
|
||||
db.add(host)
|
||||
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
|
||||
"host.created", entity_type="host", entity_id=host.name,
|
||||
|
||||
@@ -299,6 +299,23 @@ a.stat-tile:hover { border-color: var(--border-mid); background: var(--bg-elevat
|
||||
/* The host page's health header (_host_vitals.html). */
|
||||
.host-health-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.75rem; margin-bottom: 0.75rem; }
|
||||
.host-health-summary { font-size: 0.92rem; }
|
||||
/* Its four tiles share the card's width (two a row on a phone); each sparkline
|
||||
stretches across its tile. */
|
||||
.host-health-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.host-health-tiles .stat-tile { min-width: 0; }
|
||||
.host-health-tiles .stat-tile-hint { margin-top: 0.25rem; }
|
||||
@media (max-width: 640px) { .host-health-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
/* Monitors and Agent side by side: equal height, and the row keeps the same
|
||||
1rem gap below it as every other card (the panel's own card margin would add
|
||||
a second). */
|
||||
.host-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
||||
.host-pair > * { min-width: 0; }
|
||||
.host-pair .card { margin-bottom: 0; height: 100%; }
|
||||
/* A small form over the page (native <dialog>; Esc closes it). */
|
||||
.modal { width: min(560px, calc(100vw - 2rem)); padding: 1.25rem; color: var(--text); background: var(--bg-card);
|
||||
border: 1px solid var(--border); border-radius: var(--fs-radius-md); box-shadow: var(--fs-shadow-3); }
|
||||
.modal::backdrop { background: var(--fs-overlay); }
|
||||
.modal-title { font-family: var(--font-serif); font-size: 1.25rem; font-weight: 500; margin: 0 0 1rem; }
|
||||
/* A filled bar for a percentage; the fill takes its grade from status_class. */
|
||||
.meter { height: 8px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
||||
.meter-fill { height: 100%; }
|
||||
|
||||
@@ -51,10 +51,10 @@
|
||||
hx-swap="innerHTML"></div>
|
||||
|
||||
{# ── Monitors + Agent, side by side on wide screens, stacked when narrow ────── #}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:1rem;align-items:start;margin-bottom:1rem;">
|
||||
<div class="host-pair">
|
||||
|
||||
{# ── Monitors ─────────────────────────────────────────────────────────────── #}
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.5rem;">
|
||||
<h3 class="section-title" style="margin-bottom:0;">Monitors</h3>
|
||||
{% if session.user_role in ['operator', 'admin'] %}
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
{# ── Agent (host_agent management panel) ──────────────────────────────────── #}
|
||||
<div hx-get="/plugins/host_agent/panel/{{ host.id }}" hx-trigger="load" hx-swap="innerHTML">
|
||||
<div class="card" style="margin-bottom:0;"><span style="color:var(--text-muted);font-size:0.85rem;">Loading agent…</span></div>
|
||||
<div class="card"><span style="color:var(--text-muted);font-size:0.85rem;">Loading agent…</span></div>
|
||||
</div>
|
||||
</div>{# Monitors + Agent grid #}
|
||||
|
||||
|
||||
@@ -1,41 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs, page_header %}
|
||||
{# Add a host (host is none) or edit one; `values` holds what was typed when
|
||||
an error sends the form back. Laid out like the add-monitor form (#5283). #}
|
||||
{% block title %}{{ "Edit " ~ host.name if host else "Add host" }} — Steward{% endblock %}
|
||||
{% block breadcrumb %}
|
||||
{%- if host -%}
|
||||
{{ crumbs([("Hosts", "/hosts/"), (host.name, "/hosts/" ~ host.id), ("Edit", "")]) }}
|
||||
{%- else -%}
|
||||
{{ crumbs([("Hosts", "/hosts/"), ("Add host", "")]) }}
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
{# Edit a host; `values` holds what was typed when a clash sends the form back.
|
||||
Adding one is a dialog on the Hosts list (#5291). #}
|
||||
{% block title %}Edit {{ host.name }} — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "/hosts/" ~ host.id), ("Edit", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
{% set v = values or {} %}
|
||||
{{ page_header("Edit host" if host else "Add host") }}
|
||||
{{ page_header("Edit host") }}
|
||||
|
||||
<div class="card" style="max-width:760px;">
|
||||
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="{{ '/hosts/' ~ host.id if host else '/hosts/' }}" style="display:grid;gap:0.6rem;">{{ csrf_field() }}
|
||||
<form method="post" action="/hosts/{{ host.id }}" style="display:grid;gap:0.6rem;">{{ csrf_field() }}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.6rem;align-items:end;">
|
||||
<div>
|
||||
<label class="field-label" for="h-name">Name <span class="tone-crit" aria-hidden="true">*</span></label>
|
||||
<input type="text" id="h-name" name="name" value="{{ v.name if v.name is defined else (host.name if host else '') }}"
|
||||
<input type="text" id="h-name" name="name" value="{{ v.name if v.name is defined else host.name }}"
|
||||
placeholder="nas, web-1, …" autocomplete="off" required autofocus class="input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label" for="h-address">Address</label>
|
||||
<input type="text" id="h-address" name="address" value="{{ v.address if v.address is defined else (host.address if host else '') }}"
|
||||
<input type="text" id="h-address" name="address" value="{{ v.address if v.address is defined else host.address }}"
|
||||
placeholder="hostname or IP; empty resolves by name" autocomplete="off" class="input">
|
||||
</div>
|
||||
</div>
|
||||
<p class="field-hint" style="margin:0;">
|
||||
{% if host %}Renaming carries the host's agent and container history, and the alert rules on it, to the new name.
|
||||
{% else %}Next, on the host's page: add monitors, install its agent, and link an Ansible target.{% endif %}
|
||||
Renaming carries the host's agent and container history, and the alert rules on it, to the new name.
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button type="submit" class="btn btn-sm">{{ "Save" if host else "Add host" }}</button>
|
||||
<a href="{{ '/hosts/' ~ host.id if host else '/hosts/' }}" class="btn btn-sm btn-ghost">Cancel</a>
|
||||
<button type="submit" class="btn btn-sm">Save</button>
|
||||
<a href="/hosts/{{ host.id }}" class="btn btn-sm btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -4,20 +4,24 @@
|
||||
gone quiet, then monitors not yet checked, then the healthy. Status says
|
||||
what is wrong and for how long; editing and deleting a host happen on its
|
||||
own page (#5281). Uptime is one figure, 30 days, with the shorter windows
|
||||
on hover: Monitors is where uptime is read in full (#5280). #}
|
||||
on hover: Monitors is where uptime is read in full (#5280). Add host is a
|
||||
dialog over this list (#5291); `add` opens it, carrying a clash's error. #}
|
||||
{% macro pct(v, kind, stale) -%}
|
||||
{%- if v is none %}<span class="tone-dim">—</span>
|
||||
{%- else %}<span class="tone-{{ 'dim' if stale else status_class(v, kind) }}">{{ '%.0f'|format(v) }}%</span>{% endif -%}
|
||||
{%- endmacro %}
|
||||
{% block title %}Hosts — Steward{% endblock %}
|
||||
{% block content %}
|
||||
{% set can_add = session.user_role in ['operator', 'admin'] %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Hosts</h1>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
{% if session.user_role == 'admin' %}
|
||||
<a class="btn btn-ghost" href="/plugins/host_agent/fleet/">Agent fleet</a>
|
||||
{% endif %}
|
||||
<a class="btn" href="/hosts/new">Add host</a>
|
||||
{% if can_add %}
|
||||
<button type="button" class="btn" onclick="document.getElementById('add-host').showModal()">Add host</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if hosts %}
|
||||
@@ -77,7 +81,33 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<p style="color:var(--text-muted);">No hosts yet. <a href="/hosts/new">Summon one to the table.</a></p>
|
||||
<p style="color:var(--text-muted);">No hosts yet.{% if can_add %} <a href="/hosts/?add=1" onclick="document.getElementById('add-host').showModal();return false;">Summon one to the table.</a>{% endif %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_add %}
|
||||
{% set v = (add or {}).get('values') or {} %}
|
||||
<dialog id="add-host" class="modal" aria-labelledby="add-host-title">
|
||||
<h2 id="add-host-title" class="modal-title">Add host</h2>
|
||||
{% if add and add.error %}<p class="field-error" role="alert">{{ add.error }}</p>{% endif %}
|
||||
<form method="post" action="/hosts/" style="display:grid;gap:0.75rem;">{{ csrf_field() }}
|
||||
<div>
|
||||
<label class="field-label" for="h-name">Name <span class="tone-crit" aria-hidden="true">*</span></label>
|
||||
<input type="text" id="h-name" name="name" value="{{ v.name or '' }}"
|
||||
placeholder="nas, web-1, …" autocomplete="off" required autofocus class="input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label" for="h-address">Address</label>
|
||||
<input type="text" id="h-address" name="address" value="{{ v.address or '' }}"
|
||||
placeholder="hostname or IP; empty resolves by name" autocomplete="off" class="input">
|
||||
</div>
|
||||
<p class="field-hint" style="margin:0;">Next, on the host's page: add monitors, install its agent, and link an Ansible target.</p>
|
||||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;">
|
||||
<button type="button" class="btn btn-sm btn-ghost" onclick="this.closest('dialog').close()">Cancel</button>
|
||||
<button type="submit" class="btn btn-sm">Add host</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
{% if add is not none %}<script>document.getElementById('add-host').showModal();</script>{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% from "_macros.html" import status_dot %}
|
||||
{% if not rows %}
|
||||
<div class="w-verdict" style="color:var(--text-muted);">
|
||||
No hosts yet.{% if session.user_id %} <a href="/hosts/new">Add one →</a>{% endif %}
|
||||
No hosts yet.{% if session.user_id %} <a href="/hosts/?add=1">Add one →</a>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% set problems = rows | selectattr("state", "in", ["down", "crit", "stale", "warn"]) | list %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,8 +31,8 @@ async def test_silent_agents_come_first_and_the_table_leads_the_page(app):
|
||||
new_token=None, install_url=None, ansible_available=False,
|
||||
deploy_targets=[], deploy_groups=[], managed_key_set=False)
|
||||
|
||||
# Hosts are added in one place, Hosts → Add host (#5282).
|
||||
assert "fleet/add-host" not in page and 'href="/hosts/new"' in page
|
||||
# Hosts are added in one place, the Add host dialog on Hosts (#5282, #5291).
|
||||
assert "fleet/add-host" not in page and 'href="/hosts/?add=1"' in page
|
||||
assert '<span class="badge badge-warn">Silent</span>' in page
|
||||
assert '<span class="badge badge-warn">Never reported</span>' in page
|
||||
assert '<span class="badge badge-ok">Reporting</span>' in page
|
||||
|
||||
Reference in New Issue
Block a user