config_view.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # -*- coding: utf-8 -*-
  2. """
  3. Config-View: Remote-Konfiguration lesen und schreiben.
  4. Zeigt die Konfiguration als Baum und erlaubt das Editieren
  5. einzelner Werte mit typ-spezifischen Editoren und Validierung.
  6. """
  7. from __future__ import annotations
  8. from textual.app import ComposeResult
  9. from textual.containers import Vertical
  10. from textual.widgets import Static, Tree
  11. from textual.widgets.tree import TreeNode
  12. from trixy_core.tui.views.base import MainView
  13. from trixy_core.tui.config.field_registry import (
  14. EditorType,
  15. FieldMeta,
  16. get_field_registry,
  17. infer_field_meta,
  18. )
  19. from trixy_core.tui.widgets.config_editors import ConfigEditorPanel
  20. class ConfigView(MainView):
  21. """
  22. F2: Config-Ansicht.
  23. Zeigt die Remote-Konfiguration als Baum-Struktur.
  24. Werte koennen selektiert und mit typ-spezifischen
  25. Editoren bearbeitet werden.
  26. """
  27. TITLE = "Config"
  28. VIEW_ID = "config"
  29. def __init__(self, **kwargs) -> None:
  30. super().__init__(**kwargs)
  31. self._config_data: dict = {}
  32. self._modified: dict[str, object] = {}
  33. self._selected_path: str = ""
  34. self._field_registry: dict[str, FieldMeta] = {}
  35. self._dynamic_options: dict[str, list[tuple[str, str]]] = {}
  36. def compose(self) -> ComposeResult:
  37. yield Tree("Konfiguration", id="config-tree")
  38. yield ConfigEditorPanel(id="config-editor")
  39. yield Static("", id="config-status")
  40. async def on_activate(self) -> None:
  41. """Laedt die Konfiguration und baut die Registry auf."""
  42. if not self.connection:
  43. return
  44. # Registry basierend auf Instanz-Typ laden
  45. instance_type = self.connection.instance_type or "server"
  46. self._field_registry = get_field_registry(instance_type)
  47. # Dynamische Optionen einmalig abfragen
  48. await self._fetch_dynamic_options()
  49. # Konfiguration laden
  50. await self._load_config()
  51. async def _fetch_dynamic_options(self) -> None:
  52. """Fragt dynamische Feld-Optionen vom Server ab."""
  53. if not self.connection:
  54. return
  55. # Alle benoetigten dynamischen Quellen sammeln
  56. sources: set[str] = set()
  57. for meta in self._field_registry.values():
  58. if meta.dynamic_source:
  59. sources.add(meta.dynamic_source)
  60. if not sources:
  61. return
  62. try:
  63. options = await self.connection.request_field_options(list(sources))
  64. if options:
  65. self._dynamic_options = options
  66. except Exception:
  67. pass
  68. async def _load_config(self) -> None:
  69. """Laedt die Remote-Konfiguration."""
  70. if not self.connection:
  71. return
  72. try:
  73. data = await self.connection.request_config()
  74. except Exception:
  75. self._set_status("Fehler beim Laden der Konfiguration", error=True)
  76. return
  77. if data is None:
  78. return
  79. self._config_data = data
  80. self._build_tree()
  81. def _build_tree(self) -> None:
  82. """Baut den Konfigurationsbaum auf."""
  83. tree = self.query_one("#config-tree", Tree)
  84. tree.clear()
  85. self._add_tree_nodes(tree.root, self._config_data, "")
  86. tree.root.expand()
  87. def _add_tree_nodes(self, parent: TreeNode, data: dict, prefix: str) -> None:
  88. """Fuegt Knoten rekursiv zum Baum hinzu."""
  89. for key, value in sorted(data.items()):
  90. path = f"{prefix}.{key}" if prefix else key
  91. if isinstance(value, dict):
  92. node = parent.add(f"[bold]{key}[/bold]", data={"path": path, "is_section": True})
  93. self._add_tree_nodes(node, value, path)
  94. node.expand()
  95. elif isinstance(value, list):
  96. display = f"{key} = {value}"
  97. if path in self._modified:
  98. display = f"[yellow]{key}[/yellow] = [yellow]{value}[/yellow] *"
  99. parent.add_leaf(display, data={"path": path, "value": value, "is_section": False})
  100. else:
  101. # Markierung fuer modifizierte Werte
  102. if path in self._modified:
  103. display = f"[yellow]{key}[/yellow] = [yellow]{value}[/yellow] *"
  104. else:
  105. display = f"{key} = {value}"
  106. parent.add_leaf(display, data={"path": path, "value": value, "is_section": False})
  107. async def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
  108. """Wird aufgerufen wenn ein Baum-Knoten selektiert wird."""
  109. node_data = event.node.data
  110. if not node_data or node_data.get("is_section"):
  111. return
  112. path = node_data.get("path", "")
  113. value = node_data.get("value", "")
  114. self._selected_path = path
  115. # FieldMeta ermitteln (Registry oder Inferenz)
  116. meta = self._field_registry.get(path)
  117. if meta is None:
  118. meta = infer_field_meta(value)
  119. # Dynamische Optionen injizieren
  120. if meta.dynamic_source and meta.dynamic_source in self._dynamic_options:
  121. meta = FieldMeta(
  122. editor=meta.editor,
  123. choices=self._dynamic_options[meta.dynamic_source],
  124. min_value=meta.min_value,
  125. max_value=meta.max_value,
  126. step=meta.step,
  127. placeholder=meta.placeholder,
  128. requires_restart=meta.requires_restart,
  129. dynamic_source=meta.dynamic_source,
  130. hint=meta.hint,
  131. )
  132. # Editor anzeigen
  133. editor = self.query_one("#config-editor", ConfigEditorPanel)
  134. await editor.show_editor(path, value, meta)
  135. self._set_status(f"Bearbeite: {path}")
  136. async def on_config_editor_panel_submitted(
  137. self, event: ConfigEditorPanel.Submitted
  138. ) -> None:
  139. """Verarbeitet validierte Werte vom Editor-Panel."""
  140. if event.error:
  141. self._set_status(f"Fehler: {event.error}", error=True)
  142. return
  143. if not self.connection:
  144. return
  145. try:
  146. result = await self.connection.write_config(
  147. config_name="",
  148. key_path=event.path,
  149. value=event.validated_value,
  150. )
  151. if result and result.success:
  152. self._modified[event.path] = event.validated_value
  153. msg = "Gespeichert"
  154. if event.requires_restart or (result and result.requires_restart):
  155. msg += " (Neustart erforderlich)"
  156. self._set_status(msg)
  157. # Baum neu laden
  158. await self._load_config()
  159. else:
  160. error = result.error if result else "Unbekannter Fehler"
  161. self._set_status(f"Fehler: {error}", error=True)
  162. except Exception as e:
  163. self._set_status(f"Fehler: {e}", error=True)
  164. def _set_status(self, text: str, error: bool = False) -> None:
  165. """Setzt die Statuszeile."""
  166. status = self.query_one("#config-status", Static)
  167. if error:
  168. status.update(f"[red]{text}[/red]")
  169. else:
  170. status.update(f"[green]{text}[/green]")