{# Reusable Jinja macros for paginated + sortable tables on /ui/. Caller passes a ``SortState`` and a ``PageState`` from ``bty.web._table_state``. The macros render Bootstrap-flavoured table-header links and a pagination footer that preserve every other query param via ``preserved`` so an active filter survives a click on a column header. #} {# Copy-to-clipboard chip. - ``value``: the full string to copy (sha256, MAC, image-ref, URL, ...). - ``shown`` (optional): the truncated text rendered inside ````. Defaults to the first 8 chars of ``value`` (matches the existing sha256 short-hash convention). Pass ``shown=value`` to render the full string. - ``title`` (optional): tooltip override; defaults to ``Click to copy {full-value} to the clipboard.``. The click handler lives in ``bty-utils.js`` and binds via event-delegation so a freshly-swapped DOM node (htmx / SSE) needs no re-bind. The icon pulses to a check mark on success. #} {% macro copy_chip(value, shown=None, title=None) %} {% if value %} {% set _label = shown if shown is not none else value[:8] %} {% set _title = title or ("Click to copy " ~ value ~ " to the clipboard.") %} {% else %} - {% endif %} {% endmacro %} {# Sortable column header. - ``label``: human text shown in the header cell. - ``column``: SQL column key matching one of the page's allowlist entries (see _table_state.parse_sort). - ``sort``: the SortState parsed for this request. - ``preserved``: query-param dict to keep (filters, per_page, ...). - ``th_attrs`` (optional): extra attributes for the ```` (e.g. ``style="white-space: nowrap;"``) so narrow columns stay narrow. The active column gets an arrow indicator (up triangle = asc, down triangle = desc). All other columns show a faded up-down glyph to hint the header is clickable. #} {% macro sort_header(label, column, sort, preserved, th_attrs='') %} {% set qs = build_query_string( preserved, {'sort': column, 'dir': sort.next_direction(column), 'page': None} ) %} {{ label }} {% if sort.is_active(column) %} {% else %} {% endif %} {% endmacro %} {# Pagination footer. - ``page``: the PageState (see _table_state.parse_pagination). - ``preserved``: query-param dict to keep across the nav links. - ``label`` (optional): noun for the "Showing X-Y of Z" line; defaults to "rows". Renders nothing when there's only one page and no per-page choice the operator could change. Always renders the totals line when there are rows. #} {# Compact pagination control meant to sit next to a search box on the same toolbar row, above the table. Same logic as ``pagination_footer`` but with smaller margins and the "Showing X-Y of Z" line rendered as muted small text inline rather than on its own line, so the whole control fits on one row at the regular sub-3kpx widths the bty operator UI targets. Drops gracefully into a smaller block on narrow viewports via the parent's ``flex-wrap``. #} {% macro pagination_inline(page, preserved, label='rows') %}
{% if page.total == 0 %} No {{ label }}. {% else %} {{ page.first_row }}{{ page.last_row }} of {{ page.total }} {{ label }} {% endif %} {% if page.last_page > 1 %}
  • ««
  • «
  • {% set nums = page.numbered_pages() %} {% if nums[0] > 1 %}
  • 1
  • {% if nums[0] > 2 %}
  • {% endif %} {% endif %} {% for n in nums %}
  • {{ n }}
  • {% endfor %} {% if nums[-1] < page.last_page %} {% if nums[-1] < page.last_page - 1 %}
  • {% endif %}
  • {{ page.last_page }}
  • {% endif %}
  • »
  • »»
{% endif %} {# Per-page selector. Submits via plain GET so the choice is just another query param. Re-emits the preserved params as hidden inputs and DROPS ``page`` so a per-page change always lands on page 1. #}
{% for k, v in preserved.items() %} {% if v and k not in ('per_page', 'page') %} {% endif %} {% endfor %}
{% endmacro %} {% macro pagination_footer(page, preserved, label='rows') %} {# Always render the footer so an empty (filtered / searched) view shows the operator a "No