Prometheus gains a "Widget templates" tab beside Targets. It lists the built-in templates and an admin's own, any that no longer pass the checker first. A template opens as its YAML beside a live preview drawn over a chosen target exactly as the dashboard widget draws it (routes.draw, now shared by both). An admin writes or pastes a new one, reads a .yaml upload into the editor to look over, duplicates any template under a free name, edits and deletes their own; anyone can export the YAML. Built-in templates are read-only. Custom templates are rows in prometheus_widget_templates (migration prometheus_002_templates), kept in backups, keyed "custom:<id>" so a rename keeps its widgets. Saving needs a template that passes the checker and a name no other template has. Create, update and delete are audited and link to the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.7 KiB
HTML
69 lines
3.7 KiB
HTML
{# plugins/prometheus/templates/prometheus/series.html — one stored series
|
|
of a target (milestone 506): its newest value and a chart over a chosen
|
|
range. A *_timestamp_seconds series charts as moments, not numbers. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, page_header %}
|
|
{% block title %}{{ metric }} — {{ target.name }} — Steward{% endblock %}
|
|
{% from "prometheus/_tabs.html" import prometheus_tabs %}
|
|
{% block section_tabs %}{{ prometheus_tabs("targets") }}{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Prometheus", "/plugins/prometheus/"), (target.name, "/plugins/prometheus/targets/" ~ target.id), (metric, "")]) }}{% endblock %}
|
|
{% block content %}
|
|
{% call page_header(metric, labels or none) %}
|
|
{% for h in ranges %}
|
|
<a href="?resource={{ resource|urlencode }}&metric={{ metric|urlencode }}&hours={{ h }}" class="range-btn{% if hours == h %} active{% endif %}"{% if hours == h %} aria-current="true"{% endif %}>{% if h < 168 %}{{ h }}h{% else %}7d{% endif %}</a>
|
|
{% endfor %}
|
|
{% endcall %}
|
|
|
|
<p class="page-verdict">Now: <strong>{% if now.moment %}{{ now.moment|ago }}{% else %}{{ now.text }}{% endif %}</strong></p>
|
|
|
|
<section class="card" aria-label="{{ metric }} over the last {{ hours }} hours">
|
|
<div style="position:relative;height:260px;">
|
|
<canvas id="prom-chart" role="img" aria-label="{{ metric }} over the last {{ hours }} hours"></canvas>
|
|
</div>
|
|
</section>
|
|
<p class="field-hint">To be told when it crosses a line, add an alert rule on <strong>Prometheus targets</strong> → <span class="mono">{{ resource }}</span> → <span class="mono">{{ metric }}</span>.
|
|
<a href="/alerts/rules/new">New alert rule</a></p>
|
|
|
|
<script>
|
|
(function () {
|
|
var points = {{ points_json | safe }};
|
|
var hours = {{ hours }};
|
|
var moments = {{ "true" if stamp_axis else "false" }};
|
|
var el = document.getElementById("prom-chart");
|
|
if (points.length < 2) {
|
|
el.parentElement.innerHTML = '<p class="field-hint" style="margin:0;">Not enough samples yet for a chart.</p>';
|
|
return;
|
|
}
|
|
function stamp(iso) {
|
|
var d = new Date(iso);
|
|
return hours > 24
|
|
? d.toLocaleDateString([], { weekday: "short", day: "numeric" }) + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false })
|
|
: d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
}
|
|
function moment(v) {
|
|
return new Date(v * 1000).toLocaleString([], { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hour12: false });
|
|
}
|
|
new Chart(el.getContext("2d"), {
|
|
type: "line",
|
|
data: { labels: points.map(function (p) { return p[0]; }),
|
|
datasets: [{ data: points.map(function (p) { return p[1]; }), borderColor: stewardColor("--blue"),
|
|
backgroundColor: "transparent", borderWidth: 1.5, pointRadius: 0, tension: 0.2 }] },
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false, animation: false,
|
|
interaction: { mode: "index", intersect: false },
|
|
plugins: { legend: { display: false },
|
|
tooltip: { callbacks: {
|
|
title: function (items) { return items.length ? new Date(points[items[0].dataIndex][0]).toLocaleString([], { hour12: false }) : ""; },
|
|
label: function (item) { return moments ? moment(item.parsed.y) : String(item.parsed.y); } } } },
|
|
scales: {
|
|
x: { ticks: { maxTicksLimit: 8, maxRotation: 0, font: { size: 10 },
|
|
callback: function (v) { return stamp(points[v][0]); } }, grid: { display: false } },
|
|
y: { ticks: { font: { size: 10 }, maxTicksLimit: 5,
|
|
callback: function (v) { return moments ? moment(v) : v; } } }
|
|
}
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|