This commit is contained in:
Mateusz Gruszczyński
2026-03-05 15:53:33 +01:00
commit e8f6c4c609
74 changed files with 4482 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block title %}Dashboards - MikroMon{% endblock %}
{% block content %}
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h1 class="h3 mb-0">Dashboards</h1>
<div class="text-muted">Your monitoring dashboards.</div>
</div>
<a class="btn btn-primary" href="{{ url_for('dashboards.new') }}"><i class="fa-solid fa-plus me-1"></i>New dashboard</a>
</div>
<div class="row g-3">
{% for d in dashboards %}
<div class="col-12 col-md-6 col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="fw-semibold">{{ d.name }}</div>
<div class="text-muted small">{{ d.description or '' }}</div>
</div>
<div class="card-footer bg-white border-0 pt-0 pb-3 px-3">
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('dashboards.view', dashboard_id=d.id) }}">Open <i class="fa-solid fa-arrow-right ms-1"></i></a>
</div>
</div>
</div>
{% else %}
<div class="col-12"><div class="alert alert-info mb-0">No dashboards yet. Create the first one.</div></div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}New dashboard - MikroMon{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12 col-lg-7">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="h4 mb-3"><i class="fa-solid fa-plus me-2"></i>New dashboard</h1>
<form method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label">Name</label>
{{ form.name(class_="form-control") }}
</div>
<div class="mb-3">
<label class="form-label">Description</label>
{{ form.description(class_="form-control") }}
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" type="submit">Create</button>
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.index') }}">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}{{ dashboard.name }} - Public - MikroMon{% endblock %}
{% block head %}
<script>
window.MIKROMON = {
dashboardId: {{ dashboard.id }},
publicToken: "{{ token }}"
};
</script>
<style>
.navbar { display:none }
</style>
{% endblock %}
{% block content %}
<div class="mb-3">
<h1 class="h3 mb-0">{{ dashboard.name }}</h1>
{% if dashboard.description %}
<div class="text-muted">{{ dashboard.description }}</div>
{% endif %}
<div class="text-muted small mt-2">
<i class="fa-solid fa-link me-1"></i>
Public view (read-only)
</div>
</div>
<div class="row g-3">
{% for w in widgets %}
<div class="col-12 col-lg-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white">
<div class="fw-semibold">{{ w.title }}</div>
</div>
<div class="card-body">
{% if w.widget_type == 'table' %}
<div class="table-responsive">
<table class="table table-sm align-middle mb-0" data-table-widget="{{ w.id }}">
<thead class="table-light"></thead>
<tbody></tbody>
</table>
</div>
{% else %}
<div class="chart-wrap">
<canvas id="chart-{{ w.id }}"></canvas>
</div>
<div class="text-muted small mt-2" id="meta-{{ w.id }}">
Waiting for data...
</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info mb-0">
No widgets available.
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,65 @@
{% extends "base.html" %}
{% block title %}Sharing - {{ dashboard.name }} - MikroMon{% endblock %}
{% block content %}
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h1 class="h4 mb-0">Sharing</h1>
<div class="text-muted">Dashboard: <span class="fw-semibold">{{ dashboard.name }}</span></div>
</div>
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.view', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
</div>
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card shadow-sm">
<div class="card-body">
<div class="fw-semibold mb-2"><i class="fa-solid fa-user-plus me-2"></i>Share with user</div>
<form method="post" action="{{ url_for('dashboards.share_post', dashboard_id=dashboard.id) }}">
{{ form.hidden_tag() }}
<div class="row g-2">
<div class="col-12 col-md-7">{{ form.email(class_="form-control", placeholder="email@example.com") }}</div>
<div class="col-12 col-md-3">{{ form.permission(class_="form-select") }}</div>
<div class="col-12 col-md-2"><button class="btn btn-primary w-100" type="submit">OK</button></div>
</div>
</form>
<hr class="my-3">
<div class="fw-semibold mb-2"><i class="fa-solid fa-people-group me-2"></i>Current shares</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light"><tr><th>Email</th><th>Permission</th></tr></thead>
<tbody>
{% for s in shares %}
<tr><td>{{ s.user.email }}</td><td><span class="badge text-bg-secondary">{{ s.permission }}</span></td></tr>
{% else %}
<tr><td colspan="2" class="text-muted">None.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card shadow-sm">
<div class="card-body">
<div class="fw-semibold mb-2"><i class="fa-solid fa-link me-2"></i>Public link</div>
{% if public %}
<div class="alert alert-success small">
<a href="{{ url_for('dashboards.public_view', token=public.token) }}" target="_blank">{{ url_for('dashboards.public_view', token=public.token, _external=true) }}</a>
</div>
{% else %}
<div class="alert alert-secondary small">No active public link.</div>
{% endif %}
<form method="post" action="{{ url_for('dashboards.share_public', dashboard_id=dashboard.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-primary" type="submit"><i class="fa-solid fa-rotate me-1"></i>Create / refresh</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}{{ dashboard.name }} - MikroMon{% endblock %}
{% block head %}
<script>window.MIKROMON={dashboardId: {{ dashboard.id }}, publicToken: null};</script>
{% endblock %}
{% block content %}
<div class="d-flex align-items-start justify-content-between mb-3 flex-wrap gap-2">
<div>
<h1 class="h3 mb-0">{{ dashboard.name }}</h1>
{% if dashboard.description %}<div class="text-muted">{{ dashboard.description }}</div>{% endif %}
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.share', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-share-nodes me-1"></i>Share</a>
<a class="btn btn-primary" href="{{ url_for('dashboards.widget_new', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-plus me-1"></i>Add widget</a>
</div>
</div>
<div class="row g-3">
{% for w in widgets %}
<div class="col-12 col-lg-{{ w.col_span or 6 }}">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex align-items-center justify-content-between">
<div class="fw-semibold">{{ w.title }}</div>
<form method="post" action="{{ url_for('dashboards.widget_delete', dashboard_id=dashboard.id, widget_id=w.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-sm btn-outline-danger" type="submit" title="Delete"><i class="fa-solid fa-trash"></i></button>
</form>
</div>
<div class="card-body">
{% if w.widget_type == 'table' %}
<div class="table-responsive">
<table class="table table-sm align-middle mb-0" data-table-widget="{{ w.id }}">
<thead class="table-light"></thead>
<tbody></tbody>
</table>
</div>
{% else %}
<div class="chart-wrap" style="height: {{ w.height_px or 260 }}px;"><canvas id="chart-{{ w.id }}"></canvas></div>
<div class="text-muted small mt-2" id="meta-{{ w.id }}">Waiting for data…</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="col-12"><div class="alert alert-info mb-0">No widgets yet. Add the first one.</div></div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,156 @@
{% extends "base.html" %}
{% block title %}Add widget - {{ dashboard.name }} - MikroMon{% endblock %}
{% block content %}
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h1 class="h4 mb-0">Add widget</h1>
<div class="text-muted">Dashboard: <span class="fw-semibold">{{ dashboard.name }}</span></div>
</div>
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.view', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
</div>
<div class="row g-3">
<div class="col-12 col-lg-7">
<div class="card shadow-sm">
<div class="card-body p-4">
<form method="post" novalidate id="widgetForm">
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label">Title</label>
{{ form.title(class_="form-control") }}
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label">Preset</label>
{{ form.preset_key(class_="form-select", id="presetSelect") }}
</div>
<div class="col-12 col-md-6">
<label class="form-label">Device</label>
<select class="form-select" name="device_id" id="deviceSelect" required>
<option value="">— select —</option>
{% for d in devices %}
<option value="{{ d.id }}">{{ d.name }} ({{ d.host }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="mt-3 d-none" id="itemWrap">
<label class="form-label" id="itemLabel">Item</label>
<select class="form-select" id="itemSelect">
<option value="">— select —</option>
</select>
<div class="form-text">E.g. interface / queue (depends on preset).</div>
</div>
<div class="mt-3">
<label class="form-label">Refresh (seconds)</label>
{{ form.refresh_seconds(class_="form-control", type="number", min="1") }}
</div>
<div class="mt-3">
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advJson">
<i class="fa-solid fa-code me-1"></i>Advanced (JSON)
</button>
<div class="collapse mt-2" id="advJson">
{{ form.query_json(class_="form-control font-monospace", rows="6", placeholder='{"connector":"rest","endpoint":"/...","params":{}}') }}
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button class="btn btn-primary" type="submit"><i class="fa-solid fa-plus me-1"></i>Add</button>
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.view', dashboard_id=dashboard.id) }}">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-12 col-lg-5">
<div class="card shadow-sm">
<div class="card-body p-4">
<div class="fw-semibold mb-2"><i class="fa-solid fa-wand-magic-sparkles me-2"></i>Wizard mode</div>
<div class="text-muted small">
If the preset requires selecting an item (e.g. interface), the list will load automatically after choosing a device.
If you fill the JSON manually, the JSON will be used.
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
(function(){
const presets = {{ presets|tojson }};
const presetSelect = document.getElementById('presetSelect');
const deviceSelect = document.getElementById('deviceSelect');
const itemWrap = document.getElementById('itemWrap');
const itemSelect = document.getElementById('itemSelect');
const itemLabel = document.getElementById('itemLabel');
function presetNeedsItem(p){
if(!p) return false;
const params = p.params || {};
return Object.prototype.hasOwnProperty.call(params, 'name');
}
async function loadItems(){
const presetKey = presetSelect.value;
const deviceId = deviceSelect.value;
const p = presets[presetKey];
if(!deviceId || !presetNeedsItem(p)){
itemWrap.classList.add('d-none');
itemSelect.innerHTML = '<option value="">— select —</option>';
return;
}
itemWrap.classList.remove('d-none');
itemLabel.textContent = String(presetKey).includes('queue') ? 'Queue' : 'Interface / item';
itemSelect.innerHTML = '<option value="">Loading…</option>';
try{
const url = new URL("{{ url_for('api.preset_items') }}", window.location.origin);
url.searchParams.set('device_id', deviceId);
url.searchParams.set('preset_key', presetKey);
const r = await fetch(url.toString(), {credentials:'same-origin'});
const j = await r.json();
const items = (j && j.data && j.data.items) ? j.data.items : [];
itemSelect.innerHTML = '<option value="">— select —</option>' + items.map(x => `<option value="${x}">${x}</option>`).join('');
}catch(e){
itemSelect.innerHTML = '<option value="">Load error</option>';
}
}
presetSelect?.addEventListener('change', loadItems);
deviceSelect?.addEventListener('change', loadItems);
document.getElementById('widgetForm')?.addEventListener('submit', ()=>{
const adv = document.getElementById('{{ form.query_json.id }}');
if(!adv) return;
if(adv.value && adv.value.trim()) return;
const presetKey = presetSelect.value;
const p = presets[presetKey];
if(!p) return;
const q = {
connector: p.connector || 'rest',
endpoint: p.endpoint,
params: Object.assign({}, (p.params||{})),
extract: p.extract || {},
preset_key: presetKey
};
if(presetNeedsItem(p) && itemSelect.value){
q.params.name = itemSelect.value;
}
adv.value = JSON.stringify(q);
});
loadItems();
})();
</script>
{% endblock %}