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>
126 lines
6.2 KiB
HTML
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 %}
|