Files
terdut-server/internal/web/static/js/alerts.js
T
Niklas Ye 7412456c5a Pad the settings cards, and make status and timeline readable at a glance
The Team settings cards (Escalation, Sources, Members, Switches, Single
sign-on) printed their text flush against the border with the button in the
corner: .card has never had padding and these never added any. A card with
a header row now pads itself, with the title left, the button right and a
divider before the content.

The incident page no longer carries the primary action twice. The copy up
by the status is gone and the sticky bar keeps it; Note is the timeline's
link, with Copy in the bar for a resolved incident, and the phone's More
sheet drops what the bar already shows. In the queue, a row's title wraps
to two lines so the namespace that tells rows apart is no longer cut off,
zero counts on the filter chips are dimmed, and a row omits the status the
filter already states and the team once the queue is narrowed to one.

Status colours failed 4.5:1 against their own fill in the light theme
(warning 3.6, info 4.1, critical 4.4, ok 4.45, snooze 4.49), so the light
tokens are darker; the dark theme already passed and is unchanged. Severity
badges now carry a shape as well as a colour, and each timeline event has an
icon. An escalation that ran out of levels, a failed notification and a
silent heartbeat stand out in amber or red.

Web UI only: no endpoint or JSON shape changed, so nothing to mirror in
terdut-tui.
2026-10-08 14:07:24 +02:00

96 lines
2.9 KiB
JavaScript

// The alert feed: Alertmanager's own records, read-only. Each row leads to the
// incident it belongs to, which is where anything can be done about it.
import * as api from './api.js';
import { h, clear, badge, severityBadge, emptyState, spinner } from './ui.js';
import { age, labelSummary } from './format.js';
const FILTERS = [
{ id: 'firing', label: 'Firing', query: { status: 'firing' } },
{ id: 'resolved', label: 'Resolved', query: { status: 'resolved' } },
{ id: 'all', label: 'All', query: {} },
{ id: 'archived', label: 'Archived', query: { archived: 'true' } },
];
const view = () => document.getElementById('view-alerts');
let filter = 'firing';
let items = null;
let error = null;
export function show() {
render();
refresh();
}
export async function refresh() {
const requested = filter;
const f = FILTERS.find((x) => x.id === filter);
try {
const result = await api.alerts({ ...f.query, limit: 200 });
if (requested !== filter) return;
items = result;
error = null;
} catch (err) {
if (requested !== filter) return;
error = err.message;
}
render();
}
function setFilter(id) {
if (id === filter) return;
filter = id;
items = null;
render();
refresh();
}
function render() {
const chips = h('div', { class: 'chips', role: 'tablist', 'aria-label': 'Filter' },
FILTERS.map((f) => h('button', {
class: 'chip',
type: 'button',
role: 'tab',
'aria-selected': String(f.id === filter),
onclick: () => setFilter(f.id),
text: f.label,
})));
let body;
if (error && !items) body = h('div', { class: 'load-error', text: error });
else if (!items) body = spinner();
else if (!items.length) {
body = filter === 'firing'
? emptyState('Nothing firing', 'No alerts are currently firing.', 'checkCircle')
: emptyState('No alerts', 'None match this filter.', 'bell');
}
else {
body = h('div', { class: 'list' },
error && h('div', { class: 'load-error', text: `Showing older data: ${error}` }),
items.map(row));
}
clear(view(), h('div', {}, chips, body));
}
function row(a) {
const summary = (a.annotations && a.annotations.summary) || '';
const sev = a.labels && a.labels.severity;
const labels = labelSummary(Object.fromEntries(
Object.entries(a.labels || {}).filter(([k]) => k !== 'severity')));
const linked = a.incident_id != null;
return h(linked ? 'a' : 'div', {
class: `row st-${a.status} ${linked ? '' : 'no-link'}`,
href: linked ? `/incidents/${a.incident_id}` : null,
},
h('div', { class: 'row-title', text: a.name }),
h('div', { class: 'row-age', title: a.starts_at, text: age(a.status === 'firing' ? a.starts_at : a.received_at) }),
h('div', { class: 'row-meta' },
badge(a.status === 'firing' ? 'Firing' : 'Resolved', `st-${a.status}`),
sev && severityBadge(sev),
summary && h('span', { text: summary }),
labels && h('span', { class: 'labels', text: labels }),
),
);
}