#5211 "Lists load as you scroll: audit, alert history, Ansible runs" (milestone 477), per preference 172. - core/paging.py: keyset paging over (time, id), newest first, with an opaque "<iso>|<id>" cursor; a page reads one row past itself to know whether another follows. No OFFSET, so rows added while someone scrolls never shift the pages after them. - _macros.html scroll_more / scroll_end and static/scroll-more.js, written once: an IntersectionObserver with a 600px margin fetches the next page before the reader reaches the end; "Loading more…" while in flight, "End of the list." at the end, and a failed page says so with a Retry button instead of re-firing on every scroll. - Audit log: no "Load more" link and no 500-row cap. - Alert history: no 200-row stop with "pick a shorter range"; alert_svc.history takes the cursor, so the MCP tool and the page share one query. - Ansible runs: no silent cap of 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.2 KiB
HTML
104 lines
5.2 KiB
HTML
{# Shared template macros. Import with: {% from "_macros.html" import crumbs %} #}
|
||
|
||
{# Breadcrumb trail. `items` is a list of (label, href) pairs, ordered root→current.
|
||
Intermediate items with a truthy href render as links; the last item (or any
|
||
item with an empty href) renders as plain current text. #}
|
||
{% macro crumbs(items) -%}
|
||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||
{%- for label, href in items -%}
|
||
{%- if loop.last -%}
|
||
<span class="breadcrumb-cur" aria-current="page">{{ label }}</span>
|
||
{%- elif href -%}
|
||
<a href="{{ href }}">{{ label }}</a><span class="breadcrumb-sep" aria-hidden="true">›</span>
|
||
{%- else -%}
|
||
<span>{{ label }}</span><span class="breadcrumb-sep" aria-hidden="true">›</span>
|
||
{%- endif -%}
|
||
{%- endfor -%}
|
||
</nav>
|
||
{%- endmacro %}
|
||
|
||
{# A state as a labelled badge: up, down, stale, firing, pending, acknowledged,
|
||
no_data, ok, warn, crit, resolved, inactive (formatting.STATES). `state` may
|
||
be a string or an enum; `label` replaces the standard wording. #}
|
||
{% macro status_badge(state, label=none, title=none) -%}
|
||
{%- set tone, text = status_info(state) -%}
|
||
<span class="badge badge-{{ tone }}"{% if title %} title="{{ title }}"{% endif %}>{{ label or text }}</span>
|
||
{%- endmacro %}
|
||
|
||
{# A state as a coloured dot. The dot is never the only signal: its label is
|
||
read out to a screen reader, and shown beside it when `show_label`. #}
|
||
{% macro status_dot(state, label=none, show_label=false) -%}
|
||
{%- set tone, text = status_info(state) -%}
|
||
<span class="dot dot-{{ tone }}" aria-hidden="true" title="{{ label or text }}"></span>
|
||
{%- if show_label %}<span class="dot-label">{{ label or text }}</span>{% else %}<span class="sr-only">{{ label or text }}</span>{% endif %}
|
||
{%- endmacro %}
|
||
|
||
{# Threshold colouring: `threshold_style(value, kind)` (an inline style) and
|
||
`status_class(value, kind)` ('ok' / 'warn' / 'crit' / 'unknown') are jinja
|
||
globals reading the cutoffs from Settings → Thresholds. #}
|
||
|
||
{# One headline number. `state` (a STATES name, or a status_class result)
|
||
colours the value; `href` makes the whole tile a link. #}
|
||
{% macro stat_tile(label, value, state=none, hint=none, href=none) -%}
|
||
{%- set tone = status_info(state)[0] if state else none -%}
|
||
{%- set body -%}
|
||
<span class="stat-tile-label">{{ label }}</span>
|
||
<span class="stat-tile-value">{{ value }}</span>
|
||
{%- if hint %}<span class="stat-tile-hint">{{ hint }}</span>{% endif %}
|
||
{%- endset -%}
|
||
{%- if href -%}
|
||
<a href="{{ href }}" class="stat-tile{% if tone %} stat-{{ tone }}{% endif %}">{{ body }}</a>
|
||
{%- else -%}
|
||
<div class="stat-tile{% if tone %} stat-{{ tone }}{% endif %}">{{ body }}</div>
|
||
{%- endif -%}
|
||
{%- endmacro %}
|
||
|
||
{# What a list shows when it has nothing in it, with an optional next step. #}
|
||
{% macro empty_state(message, action_label=none, action_href=none) -%}
|
||
<div class="empty-state">
|
||
<p>{{ message }}</p>
|
||
{%- if action_label and action_href %}<a class="btn btn-ghost btn-sm" href="{{ action_href }}">{{ action_label }}</a>{% endif %}
|
||
</div>
|
||
{%- endmacro %}
|
||
|
||
{# The page's <h1>, with its actions to the right:
|
||
{% call page_header("Alerts") %}<a class="btn" href="…">New rule</a>{% endcall %} #}
|
||
{% macro page_header(title, subtitle=none) -%}
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title">{{ title }}</h1>
|
||
{%- if subtitle %}<p class="page-subtitle">{{ subtitle }}</p>{% endif %}
|
||
</div>
|
||
{%- if caller is defined %}<div class="page-actions">{{ caller() }}</div>{% endif %}
|
||
</div>
|
||
{%- endmacro %}
|
||
|
||
{# A button that shows only an icon. The label is required: it is the button's
|
||
accessible name and its tooltip, and rendering one without it raises. #}
|
||
{% macro icon_button(icon, label, href=none, cls="btn-ghost") -%}
|
||
{%- set name = required(label, "icon_button label") -%}
|
||
{%- if href -%}
|
||
<a class="btn btn-sm btn-icon {{ cls }}" href="{{ href }}" aria-label="{{ name }}" title="{{ name }}">{{ icon }}</a>
|
||
{%- else -%}
|
||
<button type="{{ kwargs.get('type', 'button') }}" class="btn btn-sm btn-icon {{ cls }}" aria-label="{{ name }}" title="{{ name }}"
|
||
{%- if kwargs.get('confirm') %} data-confirm="{{ kwargs.confirm }}"{% endif %}>{{ icon }}</button>
|
||
{%- endif -%}
|
||
{%- endmacro %}
|
||
|
||
{# A list that continues itself (#5211, pref 172; static/scroll-more.js).
|
||
scroll_more ends a page while there are more rows: it fetches `url` (the
|
||
next page, built with core.paging's cursor) when it comes near the
|
||
viewport, and is replaced by that page's rows. scroll_end ends the last
|
||
page. Pass `cols` inside a table body, to render as a full-width row. #}
|
||
{% macro scroll_more(url, cols=none) -%}
|
||
{%- if cols %}<tr data-scroll-more hx-get="{{ url }}" hx-trigger="scroll-more" hx-swap="outerHTML"><td colspan="{{ cols }}">
|
||
{%- else %}<div data-scroll-more hx-get="{{ url }}" hx-trigger="scroll-more" hx-swap="outerHTML">{% endif -%}
|
||
<span class="field-hint" role="status" aria-live="polite" data-scroll-status>Loading more…</span>
|
||
{%- if cols %}</td></tr>{% else %}</div>{% endif %}
|
||
{%- endmacro %}
|
||
|
||
{% macro scroll_end(cols=none, text="End of the list.") -%}
|
||
{%- if cols %}<tr><td colspan="{{ cols }}"><span class="field-hint" role="status">{{ text }}</span></td></tr>
|
||
{%- else %}<p class="field-hint" role="status">{{ text }}</p>{% endif %}
|
||
{%- endmacro %}
|