virtui-manager 1.1.6__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/METADATA +1 -1
  2. virtui_manager-1.4.0.dist-info/RECORD +76 -0
  3. vmanager/constants.py +739 -108
  4. vmanager/dialog.css +24 -0
  5. vmanager/firmware_manager.py +4 -1
  6. vmanager/i18n.py +32 -0
  7. vmanager/libvirt_utils.py +132 -3
  8. vmanager/locales/de/LC_MESSAGES/virtui-manager.mo +0 -0
  9. vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3158 -0
  10. vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
  11. vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3155 -0
  12. vmanager/locales/it/LC_MESSAGES/virtui-manager.mo +0 -0
  13. vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3132 -0
  14. vmanager/locales/virtui-manager.pot +3033 -0
  15. vmanager/modals/bulk_modals.py +13 -12
  16. vmanager/modals/cache_stats_modal.py +6 -5
  17. vmanager/modals/capabilities_modal.py +133 -0
  18. vmanager/modals/config_modal.py +25 -24
  19. vmanager/modals/cpu_mem_pc_modals.py +22 -21
  20. vmanager/modals/custom_migration_modal.py +10 -9
  21. vmanager/modals/disk_pool_modals.py +60 -59
  22. vmanager/modals/host_dashboard_modal.py +137 -0
  23. vmanager/modals/host_stats.py +199 -0
  24. vmanager/modals/howto_disk_modal.py +2 -1
  25. vmanager/modals/howto_network_modal.py +2 -1
  26. vmanager/modals/howto_overlay_modal.py +2 -1
  27. vmanager/modals/howto_ssh_modal.py +2 -1
  28. vmanager/modals/howto_virtiofs_modal.py +2 -1
  29. vmanager/modals/input_modals.py +11 -10
  30. vmanager/modals/log_modal.py +2 -1
  31. vmanager/modals/migration_modals.py +21 -19
  32. vmanager/modals/network_modals.py +45 -36
  33. vmanager/modals/provisioning_modals.py +56 -56
  34. vmanager/modals/select_server_modals.py +8 -7
  35. vmanager/modals/selection_modals.py +7 -6
  36. vmanager/modals/server_modals.py +24 -23
  37. vmanager/modals/server_prefs_modals.py +78 -71
  38. vmanager/modals/utils_modals.py +10 -9
  39. vmanager/modals/virsh_modals.py +3 -2
  40. vmanager/modals/virtiofs_modals.py +6 -5
  41. vmanager/modals/vm_type_info_modal.py +2 -1
  42. vmanager/modals/vmanager_modals.py +19 -19
  43. vmanager/modals/vmcard_dialog.py +57 -57
  44. vmanager/modals/vmdetails_modals.py +115 -123
  45. vmanager/modals/xml_modals.py +3 -2
  46. vmanager/network_manager.py +4 -1
  47. vmanager/storage_manager.py +157 -39
  48. vmanager/utils.py +54 -7
  49. vmanager/vm_actions.py +48 -24
  50. vmanager/vm_migration.py +4 -1
  51. vmanager/vm_queries.py +67 -25
  52. vmanager/vm_service.py +8 -5
  53. vmanager/vmanager.css +55 -1
  54. vmanager/vmanager.py +247 -120
  55. vmanager/vmcard.css +3 -1
  56. vmanager/vmcard.py +270 -205
  57. vmanager/webconsole_manager.py +22 -22
  58. virtui_manager-1.1.6.dist-info/RECORD +0 -65
  59. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/WHEEL +0 -0
  60. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/entry_points.txt +0 -0
  61. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/licenses/LICENSE +0 -0
  62. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,199 @@
1
+ """
2
+ Host Stats modals
3
+ """
4
+ import logging
5
+ import threading
6
+ from textual.app import ComposeResult
7
+ from textual.widgets import Static, Label
8
+ from textual.reactive import reactive
9
+ from textual.events import Click, Message
10
+ from textual.worker import get_current_worker
11
+
12
+ from ..libvirt_utils import get_host_resources, get_active_vm_allocation
13
+ from ..utils import extract_server_name_from_uri
14
+
15
+ class SingleHostStat(Static):
16
+ """
17
+ Displays stats for a single host.
18
+ """
19
+ server_name = reactive("")
20
+
21
+ class ServerLabelClicked(Message):
22
+ """Posted when the server label is clicked."""
23
+ def __init__(self, server_uri: str, server_name: str) -> None:
24
+ super().__init__()
25
+ self.server_uri = server_uri
26
+ self.server_name = server_name
27
+
28
+ DEFAULT_CSS = """
29
+ SingleHostStat {
30
+ layout: horizontal;
31
+ height: auto;
32
+ padding: 0 1;
33
+ background: $boost;
34
+ align-vertical: middle;
35
+ }
36
+ .stat-label {
37
+ color: $text;
38
+ }
39
+ """
40
+
41
+ def __init__(self, uri: str, name: str, vm_service, server_color: str = "white"):
42
+ super().__init__()
43
+ self.uri = uri
44
+ self.server_name = name
45
+ self.vm_service = vm_service
46
+ self.server_color = server_color
47
+ self.server_label = Label("", id=f"single_host_stat_label_{self.server_name.replace(' ', '_').replace('.', '_')}")
48
+ self.cpu_label = Label("", classes="stat-label")
49
+ self.mem_label = Label("", classes="stat-label")
50
+ self.host_res = None
51
+
52
+ def compose(self) -> ComposeResult:
53
+ self.server_label.styles.color = self.server_color
54
+ self.server_label.styles.text_style = "bold"
55
+ self.server_label.update(f"{self.server_name} ")
56
+ yield self.server_label
57
+ yield self.cpu_label
58
+ yield Label(" ")
59
+ yield self.mem_label
60
+
61
+ def on_click(self, event: Click) -> None:
62
+ """Called when the user clicks on a widget."""
63
+ if event.control.id == self.server_label.id:
64
+ self.post_message(self.ServerLabelClicked(self.uri, self.server_name))
65
+
66
+ def update_stats(self):
67
+ """Fetches and updates stats for this host."""
68
+ def _fetch_and_update():
69
+ try:
70
+ # Check cancellation before potentially expensive op
71
+ try:
72
+ if get_current_worker().is_cancelled:
73
+ return
74
+ except Exception:
75
+ pass
76
+
77
+ conn = self.vm_service.connect(self.uri)
78
+ if not conn:
79
+ if threading.current_thread() is threading.main_thread():
80
+ self.cpu_label.update("Offline")
81
+ self.mem_label.update("Offline")
82
+ else:
83
+ self.app.call_from_thread(self.cpu_label.update, "Offline")
84
+ self.app.call_from_thread(self.mem_label.update, "Offline")
85
+ return
86
+
87
+ if self.host_res is None:
88
+ self.host_res = get_host_resources(conn)
89
+
90
+ current_alloc = get_active_vm_allocation(conn)
91
+
92
+ # Check cancellation again after expensive op
93
+ try:
94
+ if get_current_worker().is_cancelled:
95
+ return
96
+ except Exception:
97
+ pass
98
+
99
+ total_cpus = self.host_res.get('total_cpus', 1)
100
+ total_mem = self.host_res.get('available_memory', 1) # MB
101
+
102
+ used_cpus = current_alloc.get('active_allocated_vcpus', 0)
103
+ used_mem = current_alloc.get('active_allocated_memory', 0) # MB
104
+
105
+ cpu_pct = (used_cpus / total_cpus) * 100
106
+ mem_pct = (used_mem / total_mem) * 100
107
+ # Format memory string (GB if > 1024 MB)
108
+ def fmt_mem(mb):
109
+ if mb >= 1024:
110
+ return f"{mb/1024:.1f}G"
111
+ return f"{mb}M"
112
+
113
+ # UI Updates need to be on main thread
114
+ def _update_ui():
115
+ def get_status_bck(pct):
116
+ if pct >= 90:
117
+ return ("red")
118
+ if pct >= 75:
119
+ return ("orange")
120
+ if pct >= 55:
121
+ return ("yellow")
122
+ return ("green")
123
+
124
+ self.cpu_label.update(f"{used_cpus}/{total_cpus}CPU")
125
+ self.cpu_label.styles.background = get_status_bck(cpu_pct)
126
+
127
+ self.mem_label.update(f"{fmt_mem(used_mem)}/{fmt_mem(total_mem)}")
128
+ self.mem_label.styles.background = get_status_bck(mem_pct)
129
+
130
+ if threading.current_thread() is threading.main_thread():
131
+ _update_ui()
132
+ else:
133
+ self.app.call_from_thread(_update_ui)
134
+
135
+ except Exception as e:
136
+ logging.error(f"Error updating host stats for {self.name}: {e}")
137
+ if threading.current_thread() is threading.main_thread():
138
+ self.cpu_label.update("Err")
139
+ self.mem_label.update("Err")
140
+ else:
141
+ self.app.call_from_thread(self.cpu_label.update, "Err")
142
+ self.app.call_from_thread(self.mem_label.update, "Err")
143
+
144
+ _fetch_and_update()
145
+
146
+ class HostStats(Static):
147
+ """
148
+ Container for multiple SingleHostStat widgets.
149
+ """
150
+ DEFAULT_CSS = """
151
+ HostStats {
152
+ layout: grid;
153
+ grid-size: 3;
154
+ overflow-y: auto;
155
+ margin-bottom: 0;
156
+ margin-top: 0;
157
+ display: none;
158
+ }
159
+ """
160
+
161
+ def __init__(self, vm_service, get_server_color_callback):
162
+ super().__init__()
163
+ self.vm_service = vm_service
164
+ self.get_server_color = get_server_color_callback
165
+ self.active_hosts = {}
166
+
167
+ def update_hosts(self, active_uris, servers):
168
+ """
169
+ Reconciles the list of active hosts.
170
+ """
171
+ current_uris = set(active_uris)
172
+ existing_uris = set(self.active_hosts.keys())
173
+
174
+ # Remove stale
175
+ for uri in existing_uris - current_uris:
176
+ widget = self.active_hosts.pop(uri)
177
+ widget.remove()
178
+
179
+ # Add new hosts
180
+ for uri in current_uris - existing_uris:
181
+ name = self._get_server_name(uri, servers)
182
+ color = self.get_server_color(uri)
183
+ widget = SingleHostStat(uri, name, self.vm_service, color)
184
+ self.active_hosts[uri] = widget
185
+ self.mount(widget)
186
+ self.app.set_timer(0.5, widget.update_stats)
187
+
188
+ def _get_server_name(self, uri: str, servers) -> str:
189
+ """Helper to get server name from URI."""
190
+ if servers:
191
+ for s in servers:
192
+ if s['uri'] == uri:
193
+ return s.get('name', extract_server_name_from_uri(uri))
194
+ return extract_server_name_from_uri(uri)
195
+
196
+ def refresh_stats(self):
197
+ """Triggers update on all children."""
198
+ for widget in self.active_hosts.values():
199
+ widget.update_stats()
@@ -8,6 +8,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
8
8
  from textual.widgets import Button, Markdown
9
9
  from textual import on
10
10
  from .base_modals import BaseModal
11
+ from ..constants import ButtonLabels
11
12
 
12
13
  class HowToDiskModal(BaseModal[None]):
13
14
  """A modal to display instructions for managing VM disks."""
@@ -25,7 +26,7 @@ class HowToDiskModal(BaseModal[None]):
25
26
  with ScrollableContainer(id="howto-disk-content"):
26
27
  yield Markdown(content, id="howto-disk-markdown")
27
28
  with Horizontal(id="dialog-buttons"):
28
- yield Button("Close", id="close-btn", variant="primary")
29
+ yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
29
30
 
30
31
  @on(Button.Pressed)
31
32
  def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -7,6 +7,7 @@ from textual.app import ComposeResult
7
7
  from textual.containers import Vertical, Horizontal, ScrollableContainer
8
8
  from textual.widgets import Button, Markdown
9
9
  from textual import on
10
+ from ..constants import ButtonLabels
10
11
  from .base_modals import BaseModal
11
12
 
12
13
  class HowToNetworkModal(BaseModal[None]):
@@ -25,7 +26,7 @@ class HowToNetworkModal(BaseModal[None]):
25
26
  with ScrollableContainer(id="howto-network-content"):
26
27
  yield Markdown(content, id="howto-network-markdown")
27
28
  with Horizontal(id="dialog-buttons"):
28
- yield Button("Close", id="close-btn", variant="primary")
29
+ yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
29
30
 
30
31
  @on(Button.Pressed)
31
32
  def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -8,6 +8,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
8
8
  from textual.widgets import Button, Markdown
9
9
  from textual import on
10
10
  from .base_modals import BaseModal
11
+ from ..constants import ButtonLabels
11
12
 
12
13
  class HowToOverlayModal(BaseModal[None]):
13
14
  """A modal to display instructions for disk overlays."""
@@ -25,7 +26,7 @@ class HowToOverlayModal(BaseModal[None]):
25
26
  with ScrollableContainer(id="howto-overlay-content"):
26
27
  yield Markdown(content, id="howto-overlay-markdown")
27
28
  with Horizontal(id="dialog-buttons"):
28
- yield Button("Close", id="close-btn", variant="primary")
29
+ yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
29
30
 
30
31
  @on(Button.Pressed)
31
32
  def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -7,6 +7,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
7
7
  from textual.widgets import Button, Markdown
8
8
  from textual import on
9
9
  from .base_modals import BaseModal
10
+ from ..constants import ButtonLabels
10
11
 
11
12
  class HowToSSHModal(BaseModal[None]):
12
13
  """A modal to display instructions for using an ssh-agent."""
@@ -24,7 +25,7 @@ class HowToSSHModal(BaseModal[None]):
24
25
  with ScrollableContainer(id="howto-ssh-content"):
25
26
  yield Markdown(content, id="howto-ssh-markdown")
26
27
  with Horizontal(id="dialog-buttons"):
27
- yield Button("Close", id="close-btn", variant="primary")
28
+ yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
28
29
 
29
30
  @on(Button.Pressed)
30
31
  def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -8,6 +8,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
8
8
  from textual.widgets import Button, Markdown
9
9
  from textual import on
10
10
  from .base_modals import BaseModal
11
+ from ..constants import ButtonLabels
11
12
 
12
13
  class HowToVirtIOFSModal(BaseModal[None]):
13
14
  """A modal to display instructions for using VirtIO-FS."""
@@ -25,7 +26,7 @@ class HowToVirtIOFSModal(BaseModal[None]):
25
26
  with ScrollableContainer(id="howto-virtiofs-content"):
26
27
  yield Markdown(content, id="howto-virtiofs-markdown")
27
28
  with Horizontal(id="dialog-buttons"):
28
- yield Button("Close", id="close-btn", variant="primary")
29
+ yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
29
30
 
30
31
  @on(Button.Pressed)
31
32
  def on_button_pressed(self, event: Button.Pressed) -> None:
@@ -7,6 +7,7 @@ from textual.app import ComposeResult
7
7
  from textual.containers import Vertical, Horizontal
8
8
  from textual import on
9
9
  from .base_modals import BaseModal
10
+ from ..constants import ButtonLabels, StaticText
10
11
 
11
12
  class InputModal(BaseModal[str | None]):
12
13
  """A generic modal for getting text input from the user."""
@@ -21,8 +22,8 @@ class InputModal(BaseModal[str | None]):
21
22
  yield Label(self.prompt)
22
23
  yield Input(value=self.initial_value, id="text-input", restrict=self.restrict)
23
24
  with Horizontal():
24
- yield Button("OK", variant="primary", id="ok-btn")
25
- yield Button("Cancel", variant="default", id="cancel-btn")
25
+ yield Button(ButtonLabels.OK, variant="primary", id="ok-btn")
26
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
26
27
 
27
28
  def on_button_pressed(self, event: Button.Pressed) -> None:
28
29
  if event.button.id == "ok-btn":
@@ -40,7 +41,7 @@ class AddInputDeviceModal(BaseModal[None]):
40
41
 
41
42
  def compose(self) -> ComposeResult:
42
43
  with Vertical(id="add-input-container"):
43
- yield Label("Input Device")
44
+ yield Label(StaticText.INPUT_DEVICE)
44
45
  yield Select(
45
46
  [(t, t) for t in self.available_types],
46
47
  prompt="Input Type",
@@ -53,8 +54,8 @@ class AddInputDeviceModal(BaseModal[None]):
53
54
  )
54
55
  with Vertical():
55
56
  with Horizontal():
56
- yield Button("Add", variant="primary", id="add-input", disabled=True)
57
- yield Button("Cancel", variant="default", id="cancel-input")
57
+ yield Button(ButtonLabels.ADD, variant="primary", id="add-input", disabled=True)
58
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-input")
58
59
 
59
60
  @on(Select.Changed)
60
61
  def on_select_changed(self) -> None:
@@ -82,26 +83,26 @@ class AddChannelModal(BaseModal[dict | None]):
82
83
 
83
84
  def compose(self) -> ComposeResult:
84
85
  with Vertical(id="add-channel-container"):
85
- yield Label("Add Channel Device")
86
+ yield Label(StaticText.ADD_CHANNEL_DEVICE)
86
87
  yield Select(
87
88
  [("unix", "unix"), ("virtio", "virtio"), ("spicevmc", "spicevmc")],
88
89
  prompt="Channel Type",
89
90
  id="channel-type-select",
90
91
  value="unix"
91
92
  )
92
- yield Label("Standard Target Names:")
93
+ yield Label(StaticText.STANDARD_TARGET_NAMES)
93
94
  yield Select(
94
95
  [],
95
96
  id="target-preset-select",
96
97
  prompt="Select a standard target or type below",
97
98
  value=Select.BLANK
98
99
  )
99
- yield Label("Target Name:")
100
+ yield Label(StaticText.TARGET_NAME)
100
101
  yield Input(placeholder="Target Name (e.g. org.qemu.guest_agent.0)", id="target-name-input")
101
102
 
102
103
  with Horizontal():
103
- yield Button("Add", variant="primary", id="add-channel-btn")
104
- yield Button("Cancel", variant="default", id="cancel-channel-btn")
104
+ yield Button(ButtonLabels.ADD, variant="primary", id="add-channel-btn")
105
+ yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-channel-btn")
105
106
 
106
107
  def on_mount(self) -> None:
107
108
  # Initialize presets for default type (unix)
@@ -6,6 +6,7 @@ from textual.widgets import Button, Label, TextArea
6
6
  from textual.containers import Vertical, Horizontal
7
7
 
8
8
  from .base_modals import BaseModal
9
+ from ..constants import ButtonLabels
9
10
 
10
11
  class LogModal(BaseModal[None]):
11
12
  """ Modal Screen to show Log"""
@@ -22,7 +23,7 @@ class LogModal(BaseModal[None]):
22
23
  text_area.load_text(self.log_content)
23
24
  yield text_area
24
25
  with Horizontal():
25
- yield Button("Close", variant="default", id="cancel-btn", classes="Buttonpage")
26
+ yield Button(ButtonLabels.CLOSE, variant="default", id="cancel-btn", classes="Buttonpage")
26
27
 
27
28
  def on_mount(self) -> None:
28
29
  """Called when the modal is mounted."""
@@ -11,6 +11,7 @@ from textual.screen import ModalScreen
11
11
  from textual.widgets import Button, Static, Select, Checkbox, Label, ProgressBar
12
12
  from textual import on, work
13
13
 
14
+ from ..constants import ErrorMessages, StaticText, ButtonLabels
14
15
  from ..vm_actions import check_server_migration_compatibility, check_vm_migration_compatibility
15
16
  from ..storage_manager import find_shared_storage_pools
16
17
  from ..utils import extract_server_name_from_uri
@@ -74,38 +75,39 @@ class MigrationModal(ModalScreen):
74
75
 
75
76
  with Vertical(id="migration-dialog",):
76
77
  with Vertical(id="migration-content-wrapper"):
77
- yield Label(f"[{migration_type}] Migrate VMs: [b]{vm_names}[/b]")
78
- yield Static("Select destination server:")
78
+ yield Label(StaticText.MIGRATE_VMS_TITLE.format(migration_type=migration_type, vm_names=vm_names))
79
+ yield Static(StaticText.SELECT_DESTINATION_SERVER)
79
80
  yield Select(dest_servers, id="dest-server-select", prompt="Destination...", value=default_dest_uri, allow_blank=False)
80
81
 
81
- yield Static("Migration Options:")
82
+ yield Static(StaticText.MIGRATION_OPTIONS)
82
83
  with Horizontal(classes="checkbox-container"):
83
- yield Checkbox("Copy storage all", id="copy-storage-all", tooltip="Copy all disk files during migration", value=False)
84
- yield Checkbox("Unsafe migration", id="unsafe", tooltip="Perform unsafe migration (may lose data)", disabled=not self.is_live)
85
- yield Checkbox("Persistent migration", id="persistent", tooltip="Keep VM persistent on destination", value=True)
84
+ yield Checkbox(StaticText.COPY_STORAGE_ALL, id="copy-storage-all", tooltip="Copy all disk files during migration", value=False)
85
+ yield Checkbox(StaticText.UNSAFE_MIGRATION, id="unsafe", tooltip="Perform unsafe migration (may lose data)", disabled=not self.is_live)
86
+ yield Checkbox(StaticText.PERSISTENT_MIGRATION, id="persistent", tooltip="Keep VM persistent on destination", value=True)
86
87
  with Horizontal(classes="checkbox-container"):
87
- yield Checkbox("Compress data", id="compress", tooltip="Compress data during migration", disabled=not self.is_live)
88
- yield Checkbox("Tunnelled migration", id="tunnelled", tooltip="Tunnel migration data through libvirt daemon", disabled=not self.is_live)
89
- yield Checkbox("Custom migration", id="custom", tooltip="Use custom migration workflow", value=False)
90
- yield Static("Compatibility Check Results / Migration Log:")
88
+ yield Checkbox(StaticText.COMPRESS_DATA, id="compress", tooltip="Compress data during migration", disabled=not self.is_live)
89
+ yield Checkbox(StaticText.TUNNELLED_MIGRATION, id="tunnelled", tooltip="Tunnel migration data through libvirt daemon", disabled=not self.is_live)
90
+ yield Checkbox(StaticText.CUSTOM_MIGRATION, id="custom", tooltip="Use custom migration workflow", value=False)
91
+ yield Static(StaticText.COMPATIBILITY_CHECK_RESULTS)
91
92
  yield ProgressBar(total=100, show_eta=False, id="migration-progress")
92
93
  yield Static(id="results-log")
93
94
  yield Grid(
94
95
  ScrollableContainer(
95
- Static("[b]VMs [green]Ready[/] for Migration[/b]", classes="summary-title"),
96
+ Static(StaticText.VMS_READY_FOR_MIGRATION, classes="summary-title"),
96
97
  Static(id="can-migrate-list"),
97
98
  ),
98
99
  ScrollableContainer(
99
- Static("[b]VMs [red]Not[/] Ready for Migration[/b]", classes="summary-title"),
100
+ Static(StaticText.VMS_NOT_READY_FOR_MIGRATION, classes="summary-title"),
100
101
  Static(id="cannot-migrate-list"),
101
102
  ),
102
103
  id="migration-summary-grid"
103
104
  )
104
105
 
105
106
  with Horizontal(classes="modal-buttons"):
106
- yield Button("Check Compatibility", variant="primary", id="check", classes="Buttonpage")
107
- yield Button("Start Migration", variant="success", id="start", disabled=True, classes="Buttonpage")
108
- yield Button("Close", variant="default", id="close", disabled=False, classes="close-button")
107
+ yield Button(ButtonLabels.CHECK_COMPATIBILITY, variant="primary", id="check", classes="Buttonpage")
108
+ yield Button(ButtonLabels.START_MIGRATION, variant="success", id="start", disabled=True, classes="Buttonpage")
109
+ yield Button(ButtonLabels.CLOSE, variant="default", id="close", disabled=False, classes="close-button")
110
+
109
111
 
110
112
  def _lock_controls(self, lock: bool):
111
113
  self.query_one("#check").disabled = lock
@@ -385,7 +387,7 @@ class MigrationModal(ModalScreen):
385
387
 
386
388
  write_log("\n[bold]--- Migration process finished ---[/]")
387
389
  self.app.call_from_thread(lambda: setattr(progress_bar.styles, "display", "none"))
388
- self.app.call_from_thread(self.app.refresh_vm_list)
390
+ self.app.call_from_thread(self.app.refresh_vm_list, force=True)
389
391
  self.app.call_from_thread(final_ui_state)
390
392
 
391
393
  @on(Checkbox.Changed, "#custom")
@@ -403,17 +405,17 @@ class MigrationModal(ModalScreen):
403
405
  def on_button_pressed(self, event: Button.Pressed):
404
406
  if event.button.id == "check":
405
407
  if not self.dest_conn:
406
- self.app.show_error_message("Please select a destination server.")
408
+ self.app.show_error_message(ErrorMessages.SELECT_DESTINATION_SERVER)
407
409
  return
408
410
  self._clear_log()
409
411
  self.run_compatibility_checks()
410
412
 
411
413
  elif event.button.id == "start":
412
414
  if not self.compatibility_checked:
413
- self.app.show_error_message("Please run compatibility check first.")
415
+ self.app.show_error_message(ErrorMessages.RUN_COMPATIBILITY_CHECK_FIRST)
414
416
  return
415
417
  if not self.checks_passed:
416
- self.app.show_error_message("Cannot start migration due to compatibility errors.")
418
+ self.app.show_error_message(ErrorMessages.MIGRATION_COMPATIBILITY_ERRORS)
417
419
  return
418
420
 
419
421
  self._clear_log()