Files
FabledSteward/steward/templates/ansible/inventory/index.html
T
bvandeusenandClaude Opus 5.5 955450d33f
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Successful in 1m22s
CI / publish (push) Successful in 1m36s
fix(ui): section tabs above the breadcrumb, so the Ansible tab strip holds still (#5214)
The tabs were drawn in each page's content block, which base.html renders
after the breadcrumb, so on every nested Ansible page the strip moved down a
line. base.html now has a section_tabs block above the breadcrumb, and every
Ansible page fills it. A guard in test_breadcrumbs.py fails any page that calls
a tabs macro outside that block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:28:02 -04:00

126 lines
6.2 KiB
HTML

{# ansible/inventory/index.html — the Inventory tab (#5333): a verdict line,
every target (address, the Steward host it is linked to, its groups), then
every group (its members, the schedules that run on it). The whole row opens
the target or group, where it is edited and deleted. Add target and Add
group open small dialogs; a failed add reopens its dialog with the error. #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs, page_header, empty_state %}
{% from "ansible/_tabs.html" import ansible_tabs %}
{% block title %}Ansible Inventory — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Inventory", "")]) }}{% endblock %}
{% block section_tabs %}{{ ansible_tabs("inventory") }}{% endblock %}
{% block content %}
{% set can_edit = session.user_role in ('operator', 'admin') %}
{% call page_header("Inventory") %}
{% if can_edit %}
<button type="button" class="btn btn-ghost" onclick="document.getElementById('add-group').showModal()">Add group</button>
<button type="button" class="btn" onclick="document.getElementById('add-target').showModal()">Add target</button>
{% endif %}
{% endcall %}
{% if not targets and not groups %}
<div class="card">{{ empty_state("No targets yet. A target is a machine Ansible can reach: a name, an address, and the groups it is in.") }}</div>
{% else %}
<p class="page-verdict">
{{ verdict.targets }} target{{ 's' if verdict.targets != 1 }} in {{ verdict.groups }} group{{ 's' if verdict.groups != 1 }}
{%- if verdict.ungrouped %} · <span class="tone-warn">{{ verdict.ungrouped }} in no group</span>{% endif %}
{%- if verdict.unlinked %} · {{ verdict.unlinked }} not linked to a Steward host{% endif %}
</p>
<section class="card-flush" aria-labelledby="inv-targets">
<h2 id="inv-targets" class="table-caption">Targets</h2>
{% if targets %}
<table class="table">
<thead><tr><th>Target</th><th>Steward host</th><th>Groups</th></tr></thead>
<tbody>
{% for t in targets %}
{% set h = hosts.get(t.host_id) %}
<tr class="row-link">
<td class="cell-name">
<a class="row-target" href="/ansible/inventory/targets/{{ t.id }}">{{ t.name }}</a>
<div class="cell-sub mono">{{ t.address }}</div>
</td>
<td>{% if h %}<a class="row-inner" href="/hosts/{{ h.id }}">{{ h.name }}</a>{% else %}<span class="tone-dim">—</span>{% endif %}</td>
<td>
{%- for g in t.groups %}<span class="chip">{{ g.name }}</span>{% else %}<span class="tone-warn">none</span>{% endfor -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="card-flush-note tone-muted">No targets yet.</p>
{% endif %}
</section>
<section class="card-flush" aria-labelledby="inv-groups">
<h2 id="inv-groups" class="table-caption">Groups</h2>
{% if groups %}
<table class="table">
<thead><tr><th>Group</th><th>Members</th><th>Schedules</th></tr></thead>
<tbody>
{% for g in groups %}
{% set used = users.get('steward:group:' ~ g.id, []) %}
<tr class="row-link">
<td class="cell-name"><a class="row-target" href="/ansible/inventory/groups/{{ g.id }}">{{ g.name }}</a></td>
<td>
{%- for t in g.targets|sort(attribute='name') %}<span class="chip">{{ t.name }}</span>{% else %}<span class="tone-dim">no members</span>{% endfor -%}
</td>
<td>
{%- for s in used %}<a class="row-inner" href="/ansible/schedules/{{ s.id }}">{{ s.name }}</a>{{ ", " if not loop.last }}{% else %}<span class="tone-dim">—</span>{% endfor -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="card-flush-note tone-muted">No groups yet. A group is what a run or a schedule targets, and what the Hosts list filters by.</p>
{% endif %}
</section>
{% endif %}
{% if can_edit %}
{% set a = add or {} %}
{% set v = a.get('values') or {} %}
<dialog id="add-target" class="modal" aria-labelledby="add-target-title">
<h2 id="add-target-title" class="modal-title">Add target</h2>
{% if a.kind == 'target' and a.error %}<p class="field-error" role="alert">{{ a.error }}</p>{% endif %}
<form method="post" action="/ansible/inventory/targets">{{ csrf_field() }}
<div class="field">
<label class="field-label" for="add-target-name">Name</label>
<input class="input" id="add-target-name" type="text" name="name" required placeholder="web-01"
value="{{ v.name if a.kind == 'target' else '' }}">
<span class="field-hint">What plays call it: <code>hosts: web-01</code>.</span>
</div>
<div class="field">
<label class="field-label" for="add-target-address">Address</label>
<input class="input mono" id="add-target-address" type="text" name="address" required placeholder="192.168.1.10"
value="{{ v.address if a.kind == 'target' else '' }}">
<span class="field-hint">An IP or DNS name. Groups, variables and the Steward host are set on its page next.</span>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-sm btn-ghost" onclick="this.closest('dialog').close()">Cancel</button>
<button type="submit" class="btn btn-sm">Add target</button>
</div>
</form>
</dialog>
<dialog id="add-group" class="modal" aria-labelledby="add-group-title">
<h2 id="add-group-title" class="modal-title">Add group</h2>
{% if a.kind == 'group' and a.error %}<p class="field-error" role="alert">{{ a.error }}</p>{% endif %}
<form method="post" action="/ansible/inventory/groups">{{ csrf_field() }}
<div class="field">
<label class="field-label" for="add-group-name">Name</label>
<input class="input" id="add-group-name" type="text" name="name" required placeholder="webservers"
value="{{ v.name if a.kind == 'group' else '' }}">
<span class="field-hint">Members and variables are set on its page next.</span>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-sm btn-ghost" onclick="this.closest('dialog').close()">Cancel</button>
<button type="submit" class="btn btn-sm">Add group</button>
</div>
</form>
</dialog>
{% if a.kind in ('target', 'group') %}<script>document.getElementById('add-{{ a.kind }}').showModal();</script>{% endif %}
{% endif %}
{% endblock %}