ae03f09234
Operator chose a single canonical summary. Repurpose the status_overview widget into 'Overview' (host count + monitor up/down/pending + Alerts link; key kept so existing dashboards upgrade in place) and remove the redundant fixed top strip from the dashboard view. Drops _get_summary_stats and its now-unused PingResult/DnsResult imports (rule 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
6.0 KiB
HTML
138 lines
6.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ dashboard.name }} — Steward{% endblock %}
|
|
{% block head %}
|
|
{# Gridstack renders the grid for everyone (static for viewers); editors also get
|
|
the in-place drag-resize + bottom drawer. #}
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridstack@12.6.0/dist/gridstack.min.css">
|
|
{% endblock %}
|
|
{% block content %}
|
|
|
|
{# ── Header ────────────────────────────────────────────────────────────────── #}
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;gap:1rem;flex-wrap:wrap;">
|
|
<div style="display:flex;align-items:center;gap:0.75rem;">
|
|
<h1 class="page-title" style="margin-bottom:0;">{{ dashboard.name }}</h1>
|
|
{% if dashboard.is_default %}
|
|
<span class="badge badge-gold" style="font-size:0.7rem;">Default</span>
|
|
{% endif %}
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:0.75rem;">
|
|
{# Dashboard switcher — shown when more than one dashboard exists #}
|
|
{% if all_dashboards | length > 1 %}
|
|
<select onchange="window.location='/d/'+this.value"
|
|
style="font-size:0.82rem;padding:0.3rem 0.6rem;background:var(--bg-card);border:1px solid var(--border-mid);border-radius:5px;color:var(--text);cursor:pointer;">
|
|
{% for d in all_dashboards %}
|
|
<option value="{{ d.id }}" {% if d.id == dashboard.id %}selected{% endif %}>{{ d.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% endif %}
|
|
{% if can_edit %}
|
|
<button type="button" id="dash-edit-toggle" class="btn btn-ghost btn-sm">Edit</button>
|
|
<a href="/d/{{ dashboard.id }}/share" class="btn btn-ghost btn-sm">Share</a>
|
|
{% endif %}
|
|
<a href="/dashboards/" class="btn btn-ghost btn-sm">Dashboards</a>
|
|
</div>
|
|
</div>
|
|
|
|
{# The old fixed summary strip (Hosts/Ping/DNS/Alerts) was merged into the single
|
|
'Overview' widget (status_overview) so there's one canonical summary, not two. #}
|
|
|
|
{# ── Widget grid (Gridstack; static until Edit is toggled) ──────────────────── #}
|
|
{% if not widgets %}
|
|
<div id="dash-empty" class="card" style="text-align:center;padding:3rem;">
|
|
<div style="font-size:1.1rem;color:var(--text-muted);margin-bottom:1rem;">No widgets on this dashboard yet.</div>
|
|
{% if can_edit %}
|
|
<button type="button" class="btn" onclick="document.getElementById('dash-edit-toggle').click()">Add Widgets</button>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
<div class="grid-stack" data-dash-id="{{ dashboard.id }}" data-can-edit="{{ '1' if can_edit else '0' }}">
|
|
{% for item in widgets %}
|
|
{% include "dashboard/_grid_item.html" %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if can_edit %}{% include "dashboard/_widget_drawer.html" %}{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="https://cdn.jsdelivr.net/npm/gridstack@12.6.0/dist/gridstack-all.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
var gridEl = document.querySelector('.grid-stack');
|
|
if (!gridEl || typeof GridStack === 'undefined') return;
|
|
var dashId = gridEl.dataset.dashId;
|
|
var canEdit = gridEl.dataset.canEdit === '1';
|
|
var editing = false;
|
|
|
|
var grid = GridStack.init({
|
|
column: 12, cellHeight: 70, margin: 8, float: false,
|
|
staticGrid: true, // view mode: positioned but inert
|
|
handle: '.panel-drag',
|
|
resizable: { handles: 'e, se, s' },
|
|
columnOpts: { breakpointForWindow: true, breakpoints: [{ w: 700, c: 1 }] },
|
|
}, gridEl);
|
|
|
|
if (!canEdit) return; // viewers get the static grid only
|
|
|
|
function saveLayout() {
|
|
var items = grid.save(false).map(function (n) {
|
|
return { id: parseInt(n.id, 10), x: n.x, y: n.y, w: n.w, h: n.h };
|
|
});
|
|
fetch('/d/' + dashId + '/edit/layout', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ items: items }),
|
|
});
|
|
}
|
|
grid.on('change', function () { if (editing) saveLayout(); });
|
|
|
|
function setEditing(on) {
|
|
editing = on;
|
|
grid.setStatic(!on);
|
|
document.body.classList.toggle('dash-editing', on);
|
|
var btn = document.getElementById('dash-edit-toggle');
|
|
if (btn) btn.textContent = on ? 'Done' : 'Edit';
|
|
}
|
|
var toggle = document.getElementById('dash-edit-toggle');
|
|
if (toggle) toggle.addEventListener('click', function () { setEditing(!editing); });
|
|
var done = document.getElementById('dash-edit-done');
|
|
if (done) done.addEventListener('click', function () { setEditing(false); });
|
|
|
|
// Add widget — POST the form, insert the returned grid item in place.
|
|
document.querySelectorAll('.add-widget-form').forEach(function (form) {
|
|
form.addEventListener('submit', function (ev) {
|
|
ev.preventDefault();
|
|
fetch(form.action, { method: 'POST', body: new FormData(form) })
|
|
.then(function (r) { return r.status === 204 ? '' : r.text(); })
|
|
.then(function (html) {
|
|
if (!html) return;
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = html.trim();
|
|
var el = tmp.firstElementChild;
|
|
if (!el) return;
|
|
gridEl.appendChild(el);
|
|
grid.makeWidget(el);
|
|
if (window.htmx) window.htmx.process(el); // load its live data
|
|
var empty = document.getElementById('dash-empty');
|
|
if (empty) empty.style.display = 'none';
|
|
var det = form.closest('details');
|
|
if (det) det.open = false;
|
|
});
|
|
});
|
|
});
|
|
|
|
// Remove widget — delegated; POST then drop the grid item.
|
|
gridEl.addEventListener('click', function (ev) {
|
|
var rm = ev.target.closest('[data-remove]');
|
|
if (!rm) return;
|
|
ev.preventDefault();
|
|
var item = rm.closest('.grid-stack-item');
|
|
fetch('/d/' + dashId + '/edit/remove/' + rm.dataset.remove, { method: 'POST' })
|
|
.then(function () { if (item) grid.removeWidget(item); });
|
|
});
|
|
|
|
// Deep link: /d/<id>?edit=1 opens straight into edit mode (new dashboards do this).
|
|
if (new URLSearchParams(window.location.search).get('edit') === '1') setEditing(true);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|