Source code for exlab_wizard.ui.components.session_progress

"""Wizard session-progress bar (Frontend Spec §10.1, §9.3).

Drives a phase indicator on the wizard's Confirm & Create step from the
Backend §4.6.2 ``progress`` WebSocket event. The phase enum is fixed by
Backend §4.7; the labels come from Frontend §10.1.

When the active phase is ``running_plugins`` and the event carries
``current``/``total``, a sub-row is rendered per Frontend §9.3.
"""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any

from exlab_wizard.logging import get_logger

_log = get_logger(__name__)


# Phase identifiers from Backend §4.7. The ordered tuple is the canonical
# render order for the progress bar.
PHASES: tuple[str, ...] = (
    "validating_inputs",
    "rendering_template",
    "running_plugins",
    "writing_cache",
    "post_validation",
    "queueing_sync",
)

PHASE_LABELS: dict[str, str] = {
    "validating_inputs": "Validating inputs",
    "rendering_template": "Rendering template",
    "running_plugins": "Running plugins",
    "writing_cache": "Writing cache",
    "post_validation": "Validating post-creation",
    "queueing_sync": "Queueing NAS sync",
}


[docs] @dataclass(frozen=True) class PhaseRow: """One renderable row in the session-progress component.""" phase: str label: str fraction: float is_active: bool is_done: bool
[docs] def compute_phase_rows( *, active_phase: str | None, completed: Iterable[str] = (), ) -> list[PhaseRow]: """Return one :class:`PhaseRow` per phase, in canonical order. A phase is *done* when it appears in ``completed``; *active* when it matches ``active_phase``; *pending* otherwise. The fractional value is 1.0 for done, 0.5 for active (a soft visual cue while the indeterminate sub-progress fills in), 0.0 for pending. """ completed_set = set(completed) rows: list[PhaseRow] = [] for phase in PHASES: is_done = phase in completed_set is_active = phase == active_phase and not is_done if is_done: fraction = 1.0 elif is_active: fraction = 0.5 else: fraction = 0.0 rows.append( PhaseRow( phase=phase, label=PHASE_LABELS[phase], fraction=fraction, is_active=is_active, is_done=is_done, ) ) return rows
[docs] def session_progress( *, active_phase: str | None, completed: Iterable[str] = (), plugin_current: int | None = None, plugin_total: int | None = None, plugin_name: str | None = None, ) -> Any: """Build the progress bar for the Confirm & Create step. The wizard reaches into this and re-renders it on every progress event; in tests we just call :func:`compute_phase_rows` and assert on the data shape. """ rows = compute_phase_rows(active_phase=active_phase, completed=completed) payload: dict[str, Any] = { "rows": [row.__dict__ for row in rows], "plugin_sub_row": None, } if ( active_phase == "running_plugins" and plugin_current is not None and plugin_total is not None ): payload["plugin_sub_row"] = { "name": plugin_name or "plugin", "current": plugin_current, "total": plugin_total, "fraction": (plugin_current / plugin_total if plugin_total > 0 else 0.0), } try: from nicegui import ui except Exception: return payload column = ui.column().classes("w-full").style("gap: 0.5rem;") with column: for row in rows: with ui.row().classes("items-center w-full").style("gap: 0.5rem;"): style = "font-family: var(--font-body); font-size: var(--text-sm);" if row.is_done: style += " color: var(--color-success);" elif row.is_active: style += " color: var(--color-heading); font-weight: 500;" else: style += " color: var(--color-muted);" ui.label(row.label).style(style) ui.linear_progress(value=row.fraction, show_value=False).props( "color=primary track-color=rule" ).style("flex-grow: 1;") plugin_row = payload["plugin_sub_row"] if plugin_row is not None: with ( ui.row().classes("items-center w-full").style("padding-left: 1.25rem; gap: 0.5rem;") ): ui.label( f"{plugin_row['name']} -- " f"{plugin_row['current']} of {plugin_row['total']} plugins" ).style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-muted);" ) ui.linear_progress(value=plugin_row["fraction"], show_value=False).props( "color=info" ).style("flex-grow: 1;") return column