Source code for exlab_wizard.ui.components.operations_modal

"""In-flight operations panel (Frontend Spec §9.5).

A modal reachable from the Sync segment of the bottom status bar (when
any session is suspended in ``INPUT_REQUIRED``) and from a toolbar
``[Operations...]`` button. Backed by ``GET /api/v1/operations``
(Backend §4.6.1).
"""

from __future__ import annotations

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

from exlab_wizard.logging import get_logger

_log = get_logger(__name__)


STATE_RUNNING = "running"
STATE_SUSPENDED = "suspended"
STATE_COMPLETED = "completed"

_STATE_GLYPH: dict[str, str] = {
    STATE_RUNNING: "play_arrow",
    STATE_SUSPENDED: "pause",
    STATE_COMPLETED: "check",
}


[docs] @dataclass(frozen=True) class OperationRow: """A single row in the operations panel.""" operation_id: str state: str started_at: str equipment: str project: str run: str plugin: str | None = None
[docs] def operation_columns() -> list[dict[str, Any]]: """Column definitions for the NiceGUI table (Frontend §9.5).""" return [ {"name": "state", "label": "State", "field": "state", "align": "left"}, {"name": "started_at", "label": "Started", "field": "started_at", "align": "left"}, {"name": "equipment", "label": "Equipment", "field": "equipment", "align": "left"}, {"name": "project", "label": "Project", "field": "project", "align": "left"}, {"name": "run", "label": "Run", "field": "run", "align": "left"}, {"name": "plugin", "label": "Plugin", "field": "plugin", "align": "left"}, ]
[docs] def sort_rows(rows: list[OperationRow]) -> list[OperationRow]: """Suspended rows first (oldest first), then running, then completed. Per Frontend §9.5: suspended-row default-sort is by Started-at oldest first so the operator clears the longest-pending input first. """ state_priority = {STATE_SUSPENDED: 0, STATE_RUNNING: 1, STATE_COMPLETED: 2} return sorted( rows, key=lambda r: (state_priority.get(r.state, 99), r.started_at), )
[docs] def state_glyph(state: str) -> str: """Map an operation state to its NiceGUI icon name.""" return _STATE_GLYPH.get(state, "circle")
[docs] def operations_modal( rows: list[OperationRow], *, on_resume: Callable[[str], None], on_cancel: Callable[[str], None], on_view_log: Callable[[str], None], ) -> Any: """Build the operations modal.""" sorted_rows = sort_rows(rows) payload = { "columns": operation_columns(), "rows": [r.__dict__ for r in sorted_rows], } try: from nicegui import ui except Exception: return payload dialog = ui.dialog() with ( dialog, ui.card().style( "min-width: 720px; " "padding: 1.5rem; " "background: var(--color-surface); " "border-radius: var(--radius-md); " "box-shadow: var(--shadow-md);" ), ): ui.label("Operations").style( "font-family: var(--font-display); " "font-size: var(--text-lg); " "color: var(--color-heading); " "font-weight: 600;" ) for row in sorted_rows: with ( ui.row() .classes("items-center w-full") .style( "padding: 0.5rem 0; border-bottom: 1px solid var(--color-rule); gap: 0.5rem;" ) ): ui.icon(state_glyph(row.state)).style("color: var(--color-muted); font-size: 1rem;") ui.label(row.started_at).style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-muted);" ) ui.label(row.equipment).style("font-family: var(--font-body);") ui.label(row.project).style("font-family: var(--font-body);") ui.label(row.run).style("font-family: var(--font-body);") if row.plugin: ui.label(row.plugin).style( "font-family: var(--font-mono); font-size: var(--text-xs);" ) if row.state == STATE_SUSPENDED: ui.button( "Resume", on_click=lambda _evt, oid=row.operation_id: on_resume(oid), ).props("flat") ui.button( "Cancel", on_click=lambda _evt, oid=row.operation_id: on_cancel(oid), ).props("flat") ui.button( "View log", on_click=lambda _evt, oid=row.operation_id: on_view_log(oid), ).props("flat") return dialog