Source code for kstlib.monitoring.table
"""MonitorTable render type for tabular status displays.
A ``MonitorTable`` renders as an HTML ``<table>`` with striped rows,
styled headers, and support for ``StatusCell`` values within cells.
"""
from __future__ import annotations
import html
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from kstlib.monitoring._styles import (
TABLE_BORDER_COLOR,
TABLE_FONT_FAMILY,
TABLE_HEADER_BG,
TABLE_HEADER_TEXT,
TABLE_ROW_BG,
TABLE_ROW_TEXT,
TABLE_STRIPE_BG,
)
from kstlib.monitoring.cell import StatusCell
from kstlib.monitoring.exceptions import RenderError
if TYPE_CHECKING:
from kstlib.monitoring.types import CellValue
[docs]
@dataclass(slots=True)
class MonitorTable:
"""A table rendered as an HTML ``<table>`` with striped rows.
This is the only mutable render type: rows are added via :meth:`add_row`.
Attributes:
headers: Column headers.
title: Optional caption rendered above the table.
Examples:
>>> from kstlib.monitoring.table import MonitorTable
>>> t = MonitorTable(headers=["Service", "Status"])
>>> t.add_row(["API", "OK"])
>>> "<table" in t.render()
True
"""
headers: list[str]
title: str = ""
_rows: list[list[CellValue | StatusCell]] = field(default_factory=list, init=False, repr=False)
[docs]
def add_row(self, row: list[CellValue | StatusCell]) -> None:
"""Append a row to the table.
Args:
row: List of cell values matching the number of headers.
Raises:
RenderError: If the row length does not match the header count.
"""
if len(row) != len(self.headers):
msg = f"Row has {len(row)} cells but table has {len(self.headers)} headers"
raise RenderError(msg)
self._rows.append(row)
@property
def row_count(self) -> int:
"""Return the number of data rows."""
return len(self._rows)
def _render_cell(self, cell: CellValue | StatusCell, *, inline_css: bool) -> str:
"""Render a single cell value as an HTML ``<td>`` element."""
rendered = cell.render(inline_css=inline_css) if isinstance(cell, StatusCell) else html.escape(str(cell))
if inline_css:
td_style = f"padding:8px 12px;border-bottom:1px solid {TABLE_BORDER_COLOR}"
return f'<td style="{td_style}">{rendered}</td>'
return f"<td>{rendered}</td>"
def _render_header(self, text: str, *, inline_css: bool) -> str:
"""Render a single header as an HTML ``<th>`` element."""
escaped = html.escape(text)
if inline_css:
th_style = f"background:{TABLE_HEADER_BG};color:{TABLE_HEADER_TEXT};padding:8px 12px;text-align:left"
return f'<th style="{th_style}">{escaped}</th>'
return f"<th>{escaped}</th>"
[docs]
def render(self, *, inline_css: bool = False) -> str:
"""Render the table as an HTML ``<table>``.
Args:
inline_css: If True, use inline styles instead of CSS classes.
Returns:
HTML ``<table>`` string.
"""
parts: list[str] = []
if self.title:
escaped_title = html.escape(self.title)
parts.append(f"<h3>{escaped_title}</h3>")
if inline_css:
table_style = f"border-collapse:collapse;width:100%;font-family:{TABLE_FONT_FAMILY};background:{TABLE_ROW_BG};color:{TABLE_ROW_TEXT}"
parts.append(f'<table style="{table_style}">')
else:
parts.append('<table class="monitor-table">')
# Header row
parts.append("<thead><tr>")
parts.extend(self._render_header(h, inline_css=inline_css) for h in self.headers)
parts.append("</tr></thead>")
# Data rows
parts.append("<tbody>")
for idx, row in enumerate(self._rows):
if inline_css and idx % 2 == 1:
parts.append(f'<tr style="background:{TABLE_STRIPE_BG}">')
else:
parts.append("<tr>")
parts.extend(self._render_cell(c, inline_css=inline_css) for c in row)
parts.append("</tr>")
parts.append("</tbody>")
parts.append("</table>")
return "".join(parts)
__all__ = [
"MonitorTable",
]