Source code for exlab_wizard.ui.components.credential_field

"""Credential field component (Frontend Spec §7.4.1).

Three resting / transient states:

* **not_set**  -- ``Status: Not set``  with a ``[Set]`` button.
* **set**      -- ``Status: Set ✓``    with ``[Replace]`` and ``[Clear]``.
* **editing**  -- inline password input with Save / Cancel.

Never displays a stored secret.
"""

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_NOT_SET = "not_set"
STATE_SET = "set"
STATE_EDITING = "editing"


[docs] @dataclass class CredentialState: """In-memory state for a credential row. ``resting`` records which resting state (``not_set`` / ``set``) the row should collapse back to when the operator cancels an edit -- a fresh *Set* cancels back to ``not_set``, a *Replace* cancels back to ``set``. """ state: str = STATE_NOT_SET resting: str = STATE_NOT_SET
[docs] def begin_edit(state: CredentialState) -> CredentialState: """Return the editing state, remembering ``state`` as the resting target. Idempotent: calling it on an already-editing row leaves ``resting`` untouched so a stray double-click does not lose the original resting state. """ if state.state == STATE_EDITING: return CredentialState(state=STATE_EDITING, resting=state.resting) return CredentialState(state=STATE_EDITING, resting=state.state)
[docs] def cancel_edit(state: CredentialState) -> CredentialState: """Return the resting state to collapse to when an edit is cancelled.""" return CredentialState(state=state.resting)
[docs] def commit_edit(state: CredentialState, typed_value: str) -> tuple[CredentialState, bool]: """Resolve a Save click. Returns ``(new_state, should_save)``. A non-empty ``typed_value`` transitions the row to ``set`` and signals the caller to invoke its ``on_save`` callback; an empty value has nothing to persist, so the row stays in the editor and ``should_save`` is ``False``. """ if not typed_value: return CredentialState(state=STATE_EDITING, resting=state.resting), False return CredentialState(state=STATE_SET), True
[docs] def clear_credential(state: CredentialState) -> CredentialState: """Return the state after the operator confirms a Clear action.""" del state # the row always collapses to not_set regardless of prior state return CredentialState(state=STATE_NOT_SET)
[docs] def credential_props(state: CredentialState) -> dict[str, Any]: """Compute the visible labels / button names for a state. Returns a dict with ``state``, ``label``, ``primary_button``, ``secondary_button`` (optional), and ``input_visible`` keys. """ if state.state == STATE_NOT_SET: return { "state": STATE_NOT_SET, "label": "Status: Not set", "primary_button": "Set", "secondary_button": None, "input_visible": False, } if state.state == STATE_SET: return { "state": STATE_SET, "label": "Status: Set", "primary_button": "Replace", "secondary_button": "Clear", "input_visible": False, } return { "state": STATE_EDITING, "label": "Status: Editing", "primary_button": "Save", "secondary_button": "Cancel", "input_visible": True, }
[docs] def credential_field( *, label: str, on_save: Callable[[str], None], on_clear: Callable[[], None], initial_state: CredentialState | None = None, data_testid: str | None = None, ) -> Any: """Build a credential row (Frontend Spec §7.4.1). Three states drive the row: ``not_set`` shows a ``[Set]`` button; ``set`` shows ``[Replace]`` + ``[Clear]``; ``editing`` reveals an inline password input with Save / Cancel. ``on_save`` is invoked with the typed password when the operator clicks Save with a non-empty value; ``on_clear`` is invoked when the operator confirms the Clear action. The stored secret is never displayed. The row re-renders itself in place on every state transition via a NiceGUI ``@ui.refreshable`` body, so the input box, status line, and buttons always reflect the current state. ``data_testid``, when supplied, is the base for the row's e2e hooks: ``<base>-status`` / ``-primary`` / ``-secondary`` / ``-input`` / ``-save`` / ``-cancel`` / ``-clear-confirm``. """ state = initial_state or CredentialState() payload = {"label": label, "props": credential_props(state)} try: from nicegui import ui except Exception: return payload def _tid(suffix: str) -> str: """Return the ``data-testid`` props fragment for a sub-element.""" return f'data-testid="{data_testid}-{suffix}"' if data_testid else "" container = ui.column().classes("w-full").style("gap: 0.25rem;") with container: ui.label(label).style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "letter-spacing: 0.08em; " "text-transform: uppercase; " "color: var(--color-muted);" ) def _apply(new_state: CredentialState) -> None: """Adopt ``new_state`` onto the live state and re-render the row.""" state.state = new_state.state state.resting = new_state.resting _row.refresh() def _confirm_clear() -> None: """Open the Clear confirmation dialog (Frontend Spec §7.4.1).""" with ui.dialog() as dialog, ui.card(): ui.label( "Remove the stored password? You will be prompted to " "re-enter it on the next API call." ) with ui.row().classes("justify-end w-full"): ui.button("Cancel", on_click=dialog.close).props("flat") def _do_clear() -> None: on_clear() dialog.close() _apply(clear_credential(state)) ui.button("Remove password", on_click=_do_clear).props( f"color=negative {_tid('clear-confirm')}" ) dialog.open() @ui.refreshable def _row() -> None: props = credential_props(state) with ui.row().classes("items-center").style("gap: var(--sp-2);"): if props["input_visible"]: password_input = ui.input(label="Password", password=True).props(_tid("input")) def _save() -> None: value = password_input.value or "" new_state, should_save = commit_edit(state, value) if should_save: on_save(value) _apply(new_state) ui.button("Save", on_click=_save).props(f"flat {_tid('save')}") ui.button("Cancel", on_click=lambda: _apply(cancel_edit(state))).props( f"flat {_tid('cancel')}" ) else: ui.label(props["label"]).props(_tid("status")).style( "font-family: var(--font-mono); font-size: var(--text-sm);" ) ui.button( props["primary_button"], on_click=lambda: _apply(begin_edit(state)), ).props(f"flat {_tid('primary')}") if props["secondary_button"]: ui.button(props["secondary_button"], on_click=_confirm_clear).props( f"flat {_tid('secondary')}" ) _row() return container