Coverage for src/lexigram/admin/dashboard/content_renderer.py: 97%
75 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Host-side content renderer for structured widget content.
3``WidgetController`` is the only ``lexigram-ui`` caller for widget fragments —
4this module is the single dispatcher that turns a ``WidgetContent`` variant
5into an HTML string, so contributors never build markup themselves. Every
6tone-to-color decision lives in the ``_TONE_*`` mapping tables below.
7"""
9from __future__ import annotations
11from typing import TYPE_CHECKING, Any
13from lexigram.contracts.admin.health_payload import HealthCheckPayload
14from lexigram.contracts.admin.widget_content import (
15 ChartContent,
16 EmptyContent,
17 MessageContent,
18 StatContent,
19 TableContent,
20 Tone,
21 WidgetContent,
22)
23from lexigram.contracts.core.health import HealthStatus
24from lexigram.ui import (
25 AreaChart,
26 Badge,
27 BarChart,
28 ChartDataPoint,
29 EmptyState,
30 LineChart,
31 PieChart,
32 el,
33 render_to_string,
34)
36if TYPE_CHECKING:
37 from lexigram.ui.atoms.badge import BadgeVariant
39_TONE_TEXT_CLASS: dict[Tone, str] = {
40 Tone.DEFAULT: "text-foreground",
41 Tone.PRIMARY: "text-primary",
42 Tone.SUCCESS: "text-success",
43 Tone.WARNING: "text-warning",
44 Tone.DANGER: "text-destructive",
45 Tone.INFO: "text-info",
46}
48_TONE_BADGE_VARIANT: dict[Tone, BadgeVariant] = {
49 Tone.DEFAULT: "default",
50 Tone.PRIMARY: "primary",
51 Tone.SUCCESS: "success",
52 Tone.WARNING: "warning",
53 Tone.DANGER: "danger",
54 Tone.INFO: "info",
55}
57_HEALTH_STATUS_TONE: dict[HealthStatus, Tone] = {
58 HealthStatus.HEALTHY: Tone.SUCCESS,
59 HealthStatus.DEGRADED: Tone.WARNING,
60 HealthStatus.UNHEALTHY: Tone.DANGER,
61 HealthStatus.STARTING: Tone.INFO,
62 HealthStatus.UNKNOWN: Tone.DEFAULT,
63}
65_TONE_CHART_COLOR: dict[Tone, str] = {
66 Tone.DEFAULT: "blue",
67 Tone.PRIMARY: "primary",
68 Tone.SUCCESS: "green",
69 Tone.WARNING: "yellow",
70 Tone.DANGER: "red",
71 Tone.INFO: "teal",
72}
74_CHART_COMPONENT: dict[str, type[Any]] = {
75 "bar": BarChart,
76 "line": LineChart,
77 "pie": PieChart,
78 "area": AreaChart,
79}
81_STAT_CLASS = (
82 "rounded-lg border border-border bg-card p-4 flex flex-col gap-1 min-w-0 shadow-sm"
83)
84_STAT_LABEL_CLASS = (
85 "text-xs font-semibold text-muted-foreground uppercase tracking-wider truncate"
86)
87_STAT_VALUE_CLASS = "text-2xl font-bold tabular-nums"
88_STAT_DELTA_CLASS = "text-xs font-medium text-muted-foreground"
89_TABLE_CLASS = "min-w-full divide-y divide-border border-separate border-spacing-0"
90_TABLE_STYLE = "table-layout: auto; min-width: 100%; width: max-content;"
91_TABLE_CONTAINER_CLASS = (
92 "overflow-x-auto overflow-y-auto shadow-sm ring-1 ring-border dark:ring-border "
93 "rounded-lg bg-muted-50"
94)
95_TABLE_CONTAINER_STYLE = (
96 "max-height: min(70vh, calc(100vh - 18rem)); min-height: 200px;"
97)
98_TABLE_HEAD_ROW_CLASS = "bg-muted dark:bg-card-50 border-b border-border"
99_TABLE_ROW_CLASS = (
100 "hover:bg-muted dark:hover:bg-card-80 transition-shadow duration-150 "
101 "border-b border-border last:border-0 group"
102)
103_TABLE_BODY_CLASS = "bg-card divide-y divide-border"
104_TABLE_HEADING_CLASS = (
105 "px-6 py-3 text-left text-xs font-medium uppercase tracking-wider "
106 "text-muted-foreground sticky top-0 z-20 bg-muted dark:bg-background group"
107)
108_TABLE_CELL_CLASS = "px-6 py-4 whitespace-nowrap align-middle"
109_TABLE_ZEBRA_CLASS = "bg-muted-30"
110_HEALTH_BADGE_CLASS = "health-check-badge"
111_HEALTH_DETAIL_CLASS = "text-sm text-muted-foreground"
114def render_content(content: WidgetContent) -> str:
115 """Render a ``WidgetContent`` variant into an HTML fragment.
117 Args:
118 content: Structured widget content produced by a contributor.
120 Returns:
121 Rendered HTML string.
123 Raises:
124 TypeError: If *content* is not a recognized ``WidgetContent`` variant.
125 """
126 if isinstance(content, StatContent):
127 return _render_stat_content(content)
128 if isinstance(content, TableContent):
129 return _render_table_content(content)
130 if isinstance(content, HealthCheckPayload):
131 return _render_health_content(content)
132 if isinstance(content, MessageContent):
133 return render_to_string(
134 el("p", content.text, class_=_TONE_TEXT_CLASS.get(content.tone, ""))
135 )
136 if isinstance(content, EmptyContent):
137 return render_to_string(
138 EmptyState(title=content.title, message=content.message, icon=content.icon)
139 )
140 if isinstance(content, ChartContent):
141 return _render_chart_content(content)
142 raise TypeError(f"unhandled WidgetContent variant: {type(content)!r}")
145def _render_stat_content(content: StatContent) -> str:
146 """Render a stat card grid (single stat or N-stats)."""
147 cards: list[Any] = []
148 for stat in content.stats:
149 value_cls = f"{_STAT_VALUE_CLASS} {_TONE_TEXT_CLASS.get(stat.tone, '')}".strip()
150 children: list[Any] = [
151 el("p", stat.label, class_=_STAT_LABEL_CLASS),
152 el("p", stat.value, class_=value_cls),
153 ]
154 if stat.delta:
155 children.append(el("p", stat.delta, class_=_STAT_DELTA_CLASS))
156 cards.append(el("div", *children, class_=_STAT_CLASS))
157 return render_to_string(
158 el("div", *cards, class_="grid grid-cols-1 sm:grid-cols-2 gap-4")
159 )
162def _render_table_content(content: TableContent) -> str:
163 """Render a table with per-cell ``Tone`` text classes."""
164 if not content.rows:
165 return render_to_string(
166 el("p", content.empty_message, class_="text-sm text-muted-foreground")
167 )
168 headings = [
169 el("th", column, class_=_TABLE_HEADING_CLASS) for column in content.columns
170 ]
171 body_rows: list[Any] = []
172 for index, row in enumerate(content.rows):
173 row_class = _TABLE_ROW_CLASS
174 if index % 2 == 1:
175 row_class += " " + _TABLE_ZEBRA_CLASS
176 cells = [
177 el(
178 "td",
179 cell.text,
180 class_=_TONE_TEXT_CLASS.get(cell.tone, "") + " " + _TABLE_CELL_CLASS,
181 )
182 for cell in row
183 ]
184 body_rows.append(el("tr", *cells, class_=row_class))
185 return render_to_string(
186 el(
187 "div",
188 el(
189 "table",
190 el(
191 "thead",
192 el("tr", *headings, class_=_TABLE_HEAD_ROW_CLASS),
193 ),
194 el("tbody", *body_rows, class_=_TABLE_BODY_CLASS),
195 class_=_TABLE_CLASS,
196 style=_TABLE_STYLE,
197 ),
198 class_=_TABLE_CONTAINER_CLASS,
199 style=_TABLE_CONTAINER_STYLE,
200 )
201 )
204def _render_health_content(payload: HealthCheckPayload) -> str:
205 """Render a health check as a status badge plus optional detail."""
206 tone = _HEALTH_STATUS_TONE[payload.status]
207 children: list[Any] = [
208 Badge(text=payload.status.value, variant=_TONE_BADGE_VARIANT[tone])
209 ]
210 if payload.detail:
211 children.append(el("span", f" — {payload.detail}", class_=_HEALTH_DETAIL_CLASS))
212 if payload.latency_ms is not None:
213 children.append(
214 el("span", f" — {payload.latency_ms:.0f}ms", class_=_HEALTH_DETAIL_CLASS)
215 )
216 return render_to_string(el("div", *children, class_=_HEALTH_BADGE_CLASS))
219def _render_chart_content(content: ChartContent) -> str:
220 """Render chart points with the declared chart primitive and tone colors."""
221 chart_cls = _CHART_COMPONENT[content.chart_type]
222 points = [
223 ChartDataPoint(
224 label=point.label,
225 value=point.value,
226 color=_TONE_CHART_COLOR[point.tone],
227 secondary_value=point.secondary_value,
228 )
229 for point in content.points
230 ]
231 return render_to_string(chart_cls(points))
234__all__ = ["render_content"]