71e4724286
The inventory CRUD UI (/ansible/inventory/targets + /groups) existed but was unreachable from the main Ansible pages — only a buried text hint pointed to it. Add an "Inventory" button to the Runs, Browse, and Schedules headers, and "← Ansible" back-links on the inventory target/group pages, so the targeting features (manual runs, schedules, Deploy-via-Ansible) are findable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
8.2 KiB
HTML
158 lines
8.2 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Browse Playbooks — Steward{% endblock %}
|
|
{% block content %}
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
|
<h1 class="page-title" style="margin-bottom:0;">Browse Playbooks</h1>
|
|
<div style="display:flex;gap:0.5rem;">
|
|
{% if session.user_role == 'admin' %}
|
|
<a href="/ansible/playbooks/new" class="btn btn-sm">New playbook</a>
|
|
{% endif %}
|
|
<a href="/ansible/inventory/targets" class="btn btn-ghost btn-sm">Inventory</a>
|
|
<a href="/ansible/schedules" class="btn btn-ghost btn-sm">Schedules</a>
|
|
<a href="/ansible/" class="btn btn-ghost btn-sm">← Run History</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if view_contents is defined %}
|
|
<div class="card">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;">
|
|
<h3 class="section-title">{{ view_path }}</h3>
|
|
<div style="display:flex;gap:0.5rem;">
|
|
{% if view_editable and session.user_role == 'admin' %}
|
|
<a href="/ansible/playbooks/edit/{{ view_source }}/{{ view_path }}" class="btn btn-sm">Edit</a>
|
|
<form method="post" action="/ansible/playbooks/delete" style="display:inline;"
|
|
onsubmit="return confirm('Delete {{ view_path }}?');">
|
|
<input type="hidden" name="source_name" value="{{ view_source }}">
|
|
<input type="hidden" name="playbook_path" value="{{ view_path }}">
|
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
|
</form>
|
|
{% endif %}
|
|
<a href="/ansible/browse" class="btn btn-ghost btn-sm">← Back to browse</a>
|
|
</div>
|
|
</div>
|
|
<pre style="background:var(--bg);padding:1rem;border-radius:4px;overflow-x:auto;font-size:0.8rem;color:var(--text-muted);max-height:600px;overflow-y:auto;">{{ view_contents }}</pre>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if not source_data %}
|
|
{% if view_contents is not defined %}
|
|
<div class="card">
|
|
<p style="color:var(--text-muted);">No Ansible sources configured. <a href="/settings/ansible/">Add sources in Settings → Ansible</a>.</p>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
{% for sd in source_data %}
|
|
<div class="card" style="margin-bottom:1.5rem;">
|
|
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
|
|
<h3 class="section-title">{{ sd.source.name }}</h3>
|
|
<span style="color:var(--text-muted);font-size:0.8rem;background:var(--bg-elevated);padding:0.2rem 0.5rem;border-radius:3px;">{{ sd.source.type }}</span>
|
|
<span style="color:var(--text-dim);font-size:0.8rem;">{{ sd.source.path }}</span>
|
|
</div>
|
|
|
|
{% if not sd.playbooks %}
|
|
<p style="color:#606080;font-size:0.9rem;">No playbooks found at this path.</p>
|
|
{% else %}
|
|
<table class="table" style="margin-bottom:1rem;">
|
|
<thead>
|
|
<tr>
|
|
<th>Playbook</th>
|
|
<th style="width:120px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for pb in sd.playbooks %}
|
|
<tr>
|
|
<td style="font-family:ui-monospace,monospace;font-size:0.88rem;">{{ pb }}</td>
|
|
<td class="td-actions">
|
|
<a href="/ansible/browse/{{ sd.source.name }}/{{ pb }}" class="btn btn-sm btn-ghost" style="margin-right:0.5rem;">View</a>
|
|
{% if sd.editable and session.user_role == 'admin' %}
|
|
<a href="/ansible/playbooks/edit/{{ sd.source.name }}/{{ pb }}" class="btn btn-sm btn-ghost" style="margin-right:0.5rem;">Edit</a>
|
|
{% endif %}
|
|
<a href="#run-form-{{ sd.source.name }}"
|
|
onclick="document.getElementById('run-form-{{ sd.source.name }}').style.display='block';
|
|
document.getElementById('run-playbook-{{ sd.source.name }}').value='{{ pb }}';
|
|
document.getElementById('run-playbook-display-{{ sd.source.name }}').value='{{ pb }}';
|
|
return false;"
|
|
class="btn btn-sm btn-ghost">Run</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
{# Run trigger form for this source #}
|
|
<div id="run-form-{{ sd.source.name }}" style="display:none;background:var(--bg-elevated);border-radius:6px;padding:1rem;border:1px solid var(--border-mid);">
|
|
<h4 class="section-title" style="margin-bottom:0.75rem;">Trigger Run</h4>
|
|
<form hx-post="/ansible/runs" hx-target="#run-result-{{ sd.source.name }}" hx-swap="innerHTML">
|
|
<input type="hidden" name="source_name" value="{{ sd.source.name }}">
|
|
<input type="hidden" id="run-playbook-{{ sd.source.name }}" name="playbook_path" value="">
|
|
<div class="form-group">
|
|
<label>Playbook</label>
|
|
<input type="text" id="run-playbook-display-{{ sd.source.name }}"
|
|
readonly style="color:var(--text-muted);" value="">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Run Against</label>
|
|
<select name="inventory_scope">
|
|
{% if targets %}
|
|
<optgroup label="All Targets">
|
|
<option value="steward:all">All targets ({{ targets | length }})</option>
|
|
</optgroup>
|
|
{% if groups %}
|
|
<optgroup label="Group">
|
|
{% for grp in groups %}
|
|
<option value="steward:group:{{ grp.id }}">Group: {{ grp.name }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
{% endif %}
|
|
<optgroup label="Single Target">
|
|
{% for tgt in targets %}
|
|
<option value="steward:target:{{ tgt.id }}">{{ tgt.name }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
{% endif %}
|
|
{% if sd.inventories %}
|
|
<optgroup label="Repo Inventory File">
|
|
{% for inv in sd.inventories %}
|
|
<option value="repo:{{ sd.source.name }}:{{ inv }}">{{ sd.source.name }}/{{ inv }}</option>
|
|
{% endfor %}
|
|
</optgroup>
|
|
{% endif %}
|
|
{% if not targets and not sd.inventories %}
|
|
<option value="" disabled>No targets or inventory files — add targets at /ansible/inventory/targets</option>
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Extra vars <span style="color:var(--text-muted);font-weight:normal;">(optional, one key=value per line)</span></label>
|
|
<textarea name="extra_vars" rows="2" placeholder="version=1.2.3 restart=true"
|
|
style="width:100%;font-family:ui-monospace,monospace;font-size:0.85rem;"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Limit to host(s) <span style="color:var(--text-muted);font-weight:normal;">(optional, --limit)</span></label>
|
|
<input type="text" name="limit" placeholder="web01,db*">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Tags <span style="color:var(--text-muted);font-weight:normal;">(optional, --tags)</span></label>
|
|
<input type="text" name="tags" placeholder="deploy,config">
|
|
</div>
|
|
<div class="form-group">
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:normal;">
|
|
<input type="checkbox" name="check">
|
|
Dry-run (--check --diff) — preview changes without applying
|
|
</label>
|
|
</div>
|
|
<div style="display:flex;gap:0.75rem;align-items:center;">
|
|
<button type="submit" class="btn">Execute</button>
|
|
<a href="#" onclick="document.getElementById('run-form-{{ sd.source.name }}').style.display='none';return false;"
|
|
class="btn btn-ghost btn-sm">Cancel</a>
|
|
</div>
|
|
</form>
|
|
<div id="run-result-{{ sd.source.name }}" style="margin-top:1rem;"></div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endblock %}
|