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.
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/METADATA +1 -1
- virtui_manager-1.4.0.dist-info/RECORD +76 -0
- vmanager/constants.py +739 -108
- vmanager/dialog.css +24 -0
- vmanager/firmware_manager.py +4 -1
- vmanager/i18n.py +32 -0
- vmanager/libvirt_utils.py +132 -3
- vmanager/locales/de/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3158 -0
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3155 -0
- vmanager/locales/it/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3132 -0
- vmanager/locales/virtui-manager.pot +3033 -0
- vmanager/modals/bulk_modals.py +13 -12
- vmanager/modals/cache_stats_modal.py +6 -5
- vmanager/modals/capabilities_modal.py +133 -0
- vmanager/modals/config_modal.py +25 -24
- vmanager/modals/cpu_mem_pc_modals.py +22 -21
- vmanager/modals/custom_migration_modal.py +10 -9
- vmanager/modals/disk_pool_modals.py +60 -59
- vmanager/modals/host_dashboard_modal.py +137 -0
- vmanager/modals/host_stats.py +199 -0
- vmanager/modals/howto_disk_modal.py +2 -1
- vmanager/modals/howto_network_modal.py +2 -1
- vmanager/modals/howto_overlay_modal.py +2 -1
- vmanager/modals/howto_ssh_modal.py +2 -1
- vmanager/modals/howto_virtiofs_modal.py +2 -1
- vmanager/modals/input_modals.py +11 -10
- vmanager/modals/log_modal.py +2 -1
- vmanager/modals/migration_modals.py +21 -19
- vmanager/modals/network_modals.py +45 -36
- vmanager/modals/provisioning_modals.py +56 -56
- vmanager/modals/select_server_modals.py +8 -7
- vmanager/modals/selection_modals.py +7 -6
- vmanager/modals/server_modals.py +24 -23
- vmanager/modals/server_prefs_modals.py +78 -71
- vmanager/modals/utils_modals.py +10 -9
- vmanager/modals/virsh_modals.py +3 -2
- vmanager/modals/virtiofs_modals.py +6 -5
- vmanager/modals/vm_type_info_modal.py +2 -1
- vmanager/modals/vmanager_modals.py +19 -19
- vmanager/modals/vmcard_dialog.py +57 -57
- vmanager/modals/vmdetails_modals.py +115 -123
- vmanager/modals/xml_modals.py +3 -2
- vmanager/network_manager.py +4 -1
- vmanager/storage_manager.py +157 -39
- vmanager/utils.py +54 -7
- vmanager/vm_actions.py +48 -24
- vmanager/vm_migration.py +4 -1
- vmanager/vm_queries.py +67 -25
- vmanager/vm_service.py +8 -5
- vmanager/vmanager.css +55 -1
- vmanager/vmanager.py +247 -120
- vmanager/vmcard.css +3 -1
- vmanager/vmcard.py +270 -205
- vmanager/webconsole_manager.py +22 -22
- virtui_manager-1.1.6.dist-info/RECORD +0 -65
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/WHEEL +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/entry_points.txt +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/top_level.txt +0 -0
vmanager/modals/bulk_modals.py
CHANGED
|
@@ -7,6 +7,7 @@ from textual import on
|
|
|
7
7
|
from textual.widgets import Label, Button, Markdown, Static, RadioSet, RadioButton, Checkbox
|
|
8
8
|
|
|
9
9
|
from .base_modals import BaseModal
|
|
10
|
+
from ..constants import ErrorMessages, StaticText, ButtonLabels
|
|
10
11
|
|
|
11
12
|
class BulkActionModal(BaseModal[None]):
|
|
12
13
|
"""Modal screen for performing bulk actions on selected VMs."""
|
|
@@ -17,26 +18,26 @@ class BulkActionModal(BaseModal[None]):
|
|
|
17
18
|
|
|
18
19
|
def compose(self) -> ComposeResult:
|
|
19
20
|
with Vertical(id="bulk-action-dialog"):
|
|
20
|
-
yield Label(
|
|
21
|
+
yield Label(StaticText.SELECTED_VMS_BULK)
|
|
21
22
|
yield Static(classes="button-separator")
|
|
22
23
|
with ScrollableContainer():
|
|
23
24
|
all_vms = ", ".join(self.vm_names)
|
|
24
25
|
yield Markdown(all_vms, id="selected-vms-list")
|
|
25
26
|
|
|
26
|
-
yield Label(
|
|
27
|
+
yield Label(StaticText.CHOOSE_ACTION)
|
|
27
28
|
with RadioSet(id="bulk-action-radioset"):
|
|
28
|
-
yield RadioButton(
|
|
29
|
-
yield RadioButton(
|
|
30
|
-
yield RadioButton(
|
|
31
|
-
yield RadioButton(
|
|
32
|
-
yield RadioButton(
|
|
33
|
-
yield RadioButton(
|
|
29
|
+
yield RadioButton(StaticText.START_VMS, id="action_start")
|
|
30
|
+
yield RadioButton(StaticText.STOP_VMS_GRACEFUL, id="action_stop")
|
|
31
|
+
yield RadioButton(StaticText.FORCE_OFF_VMS, id="action_force_off")
|
|
32
|
+
yield RadioButton(StaticText.PAUSE_VMS, id="action_pause")
|
|
33
|
+
yield RadioButton(StaticText.DELETE_VMS, id="action_delete")
|
|
34
|
+
yield RadioButton(StaticText.EDIT_CONFIGURATION, id="action_edit_config")
|
|
34
35
|
|
|
35
|
-
yield Checkbox(
|
|
36
|
+
yield Checkbox(StaticText.DELETE_ASSOCIATED_STORAGE, id="delete-storage-checkbox")
|
|
36
37
|
|
|
37
38
|
with Horizontal():
|
|
38
|
-
yield Button(
|
|
39
|
-
yield Button(
|
|
39
|
+
yield Button(ButtonLabels.EXECUTE, variant="primary", id="execute-action-btn", classes="button-container")
|
|
40
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn", classes="button-container")
|
|
40
41
|
|
|
41
42
|
def on_mount(self) -> None:
|
|
42
43
|
"""Called when the modal is mounted to initially hide the checkbox."""
|
|
@@ -60,6 +61,6 @@ class BulkActionModal(BaseModal[None]):
|
|
|
60
61
|
result['delete_storage'] = checkbox.value
|
|
61
62
|
self.dismiss(result)
|
|
62
63
|
else:
|
|
63
|
-
self.app.show_error_message(
|
|
64
|
+
self.app.show_error_message(ErrorMessages.PLEASE_SELECT_ACTION)
|
|
64
65
|
elif event.button.id == "cancel-btn":
|
|
65
66
|
self.dismiss(None)
|
|
@@ -5,6 +5,7 @@ from textual.app import ComposeResult
|
|
|
5
5
|
from textual.widgets import Static, Button, DataTable
|
|
6
6
|
from textual.containers import Vertical, Horizontal
|
|
7
7
|
from .base_modals import BaseModal
|
|
8
|
+
from ..constants import StaticText, SuccessMessages, ButtonLabels
|
|
8
9
|
|
|
9
10
|
class CacheStatsModal(BaseModal[None]):
|
|
10
11
|
"""Modal displaying cache statistics in a table."""
|
|
@@ -15,13 +16,13 @@ class CacheStatsModal(BaseModal[None]):
|
|
|
15
16
|
|
|
16
17
|
def compose(self) -> ComposeResult:
|
|
17
18
|
with Vertical(id="cache-stats-dialog"):
|
|
18
|
-
yield Static(
|
|
19
|
+
yield Static(StaticText.CACHE_PERFORMANCE_STATISTICS, classes="dialog-title")
|
|
19
20
|
yield DataTable(id="stats-table")
|
|
20
21
|
with Vertical():
|
|
21
22
|
with Horizontal():
|
|
22
|
-
yield Button(
|
|
23
|
-
yield Button(
|
|
24
|
-
yield Button(
|
|
23
|
+
yield Button(ButtonLabels.REFRESH, id="refresh-btn", variant="primary")
|
|
24
|
+
yield Button(ButtonLabels.CLEAR_CACHES, id="clear-btn", variant="error")
|
|
25
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="default")
|
|
25
26
|
|
|
26
27
|
def on_mount(self) -> None:
|
|
27
28
|
"""Setup the table."""
|
|
@@ -58,6 +59,6 @@ class CacheStatsModal(BaseModal[None]):
|
|
|
58
59
|
elif event.button.id == "clear-btn":
|
|
59
60
|
self.cache_monitor.clear_all_caches()
|
|
60
61
|
self._update_table()
|
|
61
|
-
self.app.show_success_message(
|
|
62
|
+
self.app.show_success_message(SuccessMessages.ALL_CACHES_CLEARED)
|
|
62
63
|
elif event.button.id == "close-btn":
|
|
63
64
|
self.dismiss()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modal for displaying Host Domain Capabilities in a Tree View with Search.
|
|
3
|
+
"""
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.widgets import Tree, Button, Label, Input
|
|
8
|
+
from textual.containers import Vertical, Horizontal, Container
|
|
9
|
+
from textual.widgets.tree import TreeNode
|
|
10
|
+
|
|
11
|
+
from .base_modals import BaseModal
|
|
12
|
+
from ..libvirt_utils import get_host_domain_capabilities
|
|
13
|
+
from ..constants import ButtonLabels, StaticText
|
|
14
|
+
|
|
15
|
+
class CapabilitiesTreeModal(BaseModal[None]):
|
|
16
|
+
"""Modal to show host capabilities XML as a tree."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, conn):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.conn = conn
|
|
21
|
+
self.xml_root = None
|
|
22
|
+
|
|
23
|
+
def compose(self) -> ComposeResult:
|
|
24
|
+
with Container(id="capabilities-dialog"):
|
|
25
|
+
yield Label(StaticText.HOST_CAPABILITIES, id="dialog-title")
|
|
26
|
+
yield Input(placeholder="Search...", id="search-input")
|
|
27
|
+
yield Tree(StaticText.CAPABILITIES_TREE_LABEL, id="xml-tree")
|
|
28
|
+
with Horizontal(id="dialog-buttons"):
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn")
|
|
30
|
+
|
|
31
|
+
def on_mount(self) -> None:
|
|
32
|
+
tree = self.query_one("#xml-tree", Tree)
|
|
33
|
+
tree.show_root = False
|
|
34
|
+
self.query_one("#search-input").focus()
|
|
35
|
+
|
|
36
|
+
xml_content = get_host_domain_capabilities(self.conn)
|
|
37
|
+
if not xml_content:
|
|
38
|
+
tree.root.add("No capabilities found or error occurred.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
self.xml_root = ET.fromstring(xml_content)
|
|
43
|
+
self.update_tree("")
|
|
44
|
+
except ET.ParseError as e:
|
|
45
|
+
tree.root.add(f"Error parsing XML: {e}")
|
|
46
|
+
|
|
47
|
+
@on(Input.Changed, "#search-input")
|
|
48
|
+
def on_search_changed(self, event: Input.Changed) -> None:
|
|
49
|
+
self.update_tree(event.value)
|
|
50
|
+
|
|
51
|
+
def update_tree(self, filter_text: str) -> None:
|
|
52
|
+
tree = self.query_one("#xml-tree", Tree)
|
|
53
|
+
tree.clear()
|
|
54
|
+
|
|
55
|
+
if self.xml_root is None:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Add root manually
|
|
59
|
+
self._add_node_recursive(tree.root, self.xml_root, filter_text.strip().lower())
|
|
60
|
+
|
|
61
|
+
if not filter_text:
|
|
62
|
+
# Expand the first level if no filter
|
|
63
|
+
for node in tree.root.children:
|
|
64
|
+
node.expand()
|
|
65
|
+
|
|
66
|
+
def _matches(self, text: str | None, filter_text: str) -> bool:
|
|
67
|
+
if not filter_text:
|
|
68
|
+
return True
|
|
69
|
+
return filter_text in (text or "").lower()
|
|
70
|
+
|
|
71
|
+
def _add_node_recursive(self, parent_node: TreeNode, element: ET.Element, filter_text: str) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Recursively adds nodes. Returns True if the node (or any descendant) matches the filter
|
|
74
|
+
and should be kept.
|
|
75
|
+
"""
|
|
76
|
+
# 1. Determine if this specific element matches (tag, text, or any attribute)
|
|
77
|
+
tag_match = self._matches(element.tag, filter_text)
|
|
78
|
+
text_match = self._matches(element.text, filter_text) if element.text and element.text.strip() else False
|
|
79
|
+
|
|
80
|
+
matching_attrs = []
|
|
81
|
+
for k, v in element.attrib.items():
|
|
82
|
+
if self._matches(k, filter_text) or self._matches(v, filter_text):
|
|
83
|
+
matching_attrs.append((k, v))
|
|
84
|
+
|
|
85
|
+
self_match = tag_match or text_match or (len(matching_attrs) > 0)
|
|
86
|
+
|
|
87
|
+
# 2. Create the node optimistically
|
|
88
|
+
label = f"[b]{element.tag}[/b]"
|
|
89
|
+
|
|
90
|
+
# Show text inline if short and no children, OR if it matches
|
|
91
|
+
has_xml_children = len(element) > 0
|
|
92
|
+
text = element.text.strip() if element.text else ""
|
|
93
|
+
|
|
94
|
+
if text and (not has_xml_children or len(text) < 50):
|
|
95
|
+
label += f": {text}"
|
|
96
|
+
|
|
97
|
+
# Expand if filtering, or default closed
|
|
98
|
+
should_expand = bool(filter_text)
|
|
99
|
+
node = parent_node.add(label, expand=should_expand)
|
|
100
|
+
|
|
101
|
+
has_content = False
|
|
102
|
+
|
|
103
|
+
# 3. Add attributes
|
|
104
|
+
attrs_to_check = element.attrib.items()
|
|
105
|
+
|
|
106
|
+
for k, v in attrs_to_check:
|
|
107
|
+
is_attr_match = self._matches(k, filter_text) or self._matches(v, filter_text)
|
|
108
|
+
if not filter_text or is_attr_match or self_match:
|
|
109
|
+
# Highlight match if filter exists
|
|
110
|
+
k_display = k
|
|
111
|
+
v_display = v
|
|
112
|
+
node.add(f"[i]@{k_display}[/i]: {v_display}", allow_expand=False)
|
|
113
|
+
if is_attr_match and filter_text:
|
|
114
|
+
has_content = True
|
|
115
|
+
|
|
116
|
+
if self_match and filter_text:
|
|
117
|
+
has_content = True
|
|
118
|
+
|
|
119
|
+
# 4. Recurse children
|
|
120
|
+
for child in element:
|
|
121
|
+
if self._add_node_recursive(node, child, filter_text):
|
|
122
|
+
has_content = True
|
|
123
|
+
|
|
124
|
+
# 5. Cleanup if nothing matched
|
|
125
|
+
if filter_text and not has_content:
|
|
126
|
+
node.remove()
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
132
|
+
if event.button.id == "close-btn":
|
|
133
|
+
self.dismiss()
|
vmanager/modals/config_modal.py
CHANGED
|
@@ -8,7 +8,7 @@ from textual import on
|
|
|
8
8
|
from textual.widgets import Label, Button, Input, Checkbox, Static, Select
|
|
9
9
|
|
|
10
10
|
from ..config import save_config, get_user_config_path
|
|
11
|
-
from ..constants import AppInfo
|
|
11
|
+
from ..constants import AppInfo, WarningMessages, SuccessMessages, ErrorMessages, StaticText, ButtonLabels
|
|
12
12
|
from .base_modals import BaseModal
|
|
13
13
|
from ..utils import check_r_viewer
|
|
14
14
|
|
|
@@ -22,12 +22,12 @@ class ConfigModal(BaseModal[None]):
|
|
|
22
22
|
def compose(self) -> ComposeResult:
|
|
23
23
|
with Vertical(id="config-dialog"):
|
|
24
24
|
yield Label(f"{AppInfo.namecase} Configuration", id="config-title")
|
|
25
|
-
yield Static(
|
|
25
|
+
yield Static(StaticText.EDITING_CONFIG_PATH.format(get_user_config_path=get_user_config_path()), id="config-title-file") #classes="config-path-label")
|
|
26
26
|
with ScrollableContainer():
|
|
27
27
|
# Performance settings
|
|
28
|
-
yield Label(
|
|
28
|
+
yield Label(StaticText.PERFORMANCE, classes="config-section-label")
|
|
29
29
|
with Horizontal():
|
|
30
|
-
yield Label(
|
|
30
|
+
yield Label(StaticText.STATS_INTERVAL)
|
|
31
31
|
yield Input(
|
|
32
32
|
value=str(self.config.get("STATS_INTERVAL", 5)),
|
|
33
33
|
id="stats-interval-input",
|
|
@@ -36,14 +36,14 @@ class ConfigModal(BaseModal[None]):
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
# Logging settings
|
|
39
|
-
yield Label(
|
|
39
|
+
yield Label(StaticText.LOG_FILE_PATH)
|
|
40
40
|
yield Input(
|
|
41
41
|
value=self.config.get("LOG_FILE_PATH", ""),
|
|
42
42
|
id="log-file-path-input",
|
|
43
43
|
tooltip="Full path to the application log file"
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
yield Label(
|
|
46
|
+
yield Label(StaticText.LOGGING_LEVEL)
|
|
47
47
|
yield Select(
|
|
48
48
|
[
|
|
49
49
|
("DEBUG", "DEBUG"),
|
|
@@ -55,11 +55,11 @@ class ConfigModal(BaseModal[None]):
|
|
|
55
55
|
value=self.config.get("LOG_LEVEL", "INFO"),
|
|
56
56
|
id="log-level-select",
|
|
57
57
|
prompt="Select a logging level"
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
)
|
|
59
|
+
|
|
60
60
|
# Remote Viewer Settings
|
|
61
|
-
yield Label(
|
|
62
|
-
|
|
61
|
+
yield Label(StaticText.REMOTE_VIEWER)
|
|
62
|
+
|
|
63
63
|
viewers = []
|
|
64
64
|
if shutil.which("virtui-remote-viewer"):
|
|
65
65
|
viewers.append(("virtui-remote-viewer", "virtui-remote-viewer"))
|
|
@@ -71,7 +71,7 @@ class ConfigModal(BaseModal[None]):
|
|
|
71
71
|
current_viewer = Select.BLANK
|
|
72
72
|
|
|
73
73
|
if not viewers:
|
|
74
|
-
yield Label(
|
|
74
|
+
yield Label(StaticText.NO_REMOTE_VIEWERS_FOUND)
|
|
75
75
|
else:
|
|
76
76
|
auto_detected = check_r_viewer()
|
|
77
77
|
yield Label(f"Select Default Remote Viewer (Auto-detect: {auto_detected}):")
|
|
@@ -81,29 +81,30 @@ class ConfigModal(BaseModal[None]):
|
|
|
81
81
|
id="remote-viewer-select",
|
|
82
82
|
allow_blank=True,
|
|
83
83
|
prompt="Select a viewer"
|
|
84
|
-
|
|
84
|
+
)
|
|
85
85
|
|
|
86
|
-
# Web console settings
|
|
86
|
+
# Web console settings
|
|
87
|
+
yield Label(StaticText.WEB_CONSOLE_NOVNC, classes="config-section-label")
|
|
87
88
|
yield Checkbox(
|
|
88
|
-
|
|
89
|
+
StaticText.ENABLE_REMOTE_WEBCONSOLE,
|
|
89
90
|
self.config.get("REMOTE_WEBCONSOLE", False),
|
|
90
91
|
id="remote-webconsole-checkbox",
|
|
91
92
|
tooltip="Enable secure SSH and noVNC remote viewing for headless server environments"
|
|
92
93
|
)
|
|
93
|
-
yield Label(
|
|
94
|
+
yield Label(StaticText.WEBSOCKIFY_PATH)
|
|
94
95
|
yield Input(
|
|
95
96
|
value=self.config.get("websockify_path", "/usr/bin/websockify"),
|
|
96
97
|
id="websockify-path-input",
|
|
97
98
|
tooltip="Path to the websockify binary"
|
|
98
99
|
)
|
|
99
|
-
yield Label(
|
|
100
|
+
yield Label(StaticText.NOVNC_PATH)
|
|
100
101
|
yield Input(
|
|
101
102
|
value=self.config.get("novnc_path", "/usr/share/novnc/"),
|
|
102
103
|
id="novnc-path-input",
|
|
103
104
|
tooltip="Path to noVNC files"
|
|
104
105
|
)
|
|
105
106
|
with Horizontal(classes="port-range-container"):
|
|
106
|
-
yield Label(
|
|
107
|
+
yield Label(StaticText.WEBSOCKIFY_PORT_RANGE, classes="port-range-label")
|
|
107
108
|
yield Input(
|
|
108
109
|
value=str(self.config.get("WC_PORT_RANGE_START", 40000)),
|
|
109
110
|
id="wc-port-start-input",
|
|
@@ -120,14 +121,14 @@ class ConfigModal(BaseModal[None]):
|
|
|
120
121
|
)
|
|
121
122
|
with Vertical():
|
|
122
123
|
with Horizontal():
|
|
123
|
-
yield Label(
|
|
124
|
+
yield Label(StaticText.VNC_QUALITY)
|
|
124
125
|
yield Input(
|
|
125
126
|
value=str(self.config.get("VNC_QUALITY", 0)),
|
|
126
127
|
id="vnc-quality-input",
|
|
127
128
|
type="integer",
|
|
128
129
|
tooltip="VNC quality setting (0-9)"
|
|
129
130
|
)
|
|
130
|
-
yield Label(
|
|
131
|
+
yield Label(StaticText.VNC_COMPRESSION)
|
|
131
132
|
yield Input(
|
|
132
133
|
value=str(self.config.get("VNC_COMPRESSION", 9)),
|
|
133
134
|
id="vnc-compression-input",
|
|
@@ -136,8 +137,8 @@ class ConfigModal(BaseModal[None]):
|
|
|
136
137
|
)
|
|
137
138
|
|
|
138
139
|
with Horizontal():
|
|
139
|
-
yield Button(
|
|
140
|
-
yield Button(
|
|
140
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-config-btn")
|
|
141
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
141
142
|
|
|
142
143
|
@on(Button.Pressed)
|
|
143
144
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -160,14 +161,14 @@ class ConfigModal(BaseModal[None]):
|
|
|
160
161
|
self.config["REMOTE_VIEWER"] = viewer_select.value
|
|
161
162
|
else:
|
|
162
163
|
self.config["REMOTE_VIEWER"] = None
|
|
163
|
-
self.app.show_warning_message(
|
|
164
|
+
self.app.show_warning_message(WarningMessages.NO_REMOTE_VIEWER_SELECTED)
|
|
164
165
|
except Exception:
|
|
165
166
|
pass
|
|
166
167
|
|
|
167
168
|
save_config(self.config)
|
|
168
|
-
self.app.show_success_message(
|
|
169
|
+
self.app.show_success_message(SuccessMessages.CONFIGURATION_SAVED)
|
|
169
170
|
self.dismiss(self.config)
|
|
170
171
|
except Exception as e:
|
|
171
|
-
self.app.show_error_message(
|
|
172
|
+
self.app.show_error_message(ErrorMessages.ERROR_SAVING_CONFIGURATION_TEMPLATE.format(e=e))
|
|
172
173
|
elif event.button.id == "cancel-btn":
|
|
173
174
|
self.dismiss(None)
|
|
@@ -5,6 +5,7 @@ from textual.app import ComposeResult
|
|
|
5
5
|
from textual.containers import Horizontal, ScrollableContainer, Vertical
|
|
6
6
|
from textual.widgets import Button, Input, Label, ListView, Select
|
|
7
7
|
|
|
8
|
+
from ..constants import ErrorMessages, StaticText, ButtonLabels
|
|
8
9
|
from .base_modals import BaseModal, ValueListItem
|
|
9
10
|
from .utils_modals import InfoModal
|
|
10
11
|
|
|
@@ -18,11 +19,11 @@ class EditCpuModal(BaseModal[str | None]):
|
|
|
18
19
|
|
|
19
20
|
def compose(self) -> ComposeResult:
|
|
20
21
|
with Vertical(id="edit-cpu-dialog", classes="edit-cpu-dialog"):
|
|
21
|
-
yield Label(
|
|
22
|
+
yield Label(StaticText.ENTER_NEW_VCPU_COUNT)
|
|
22
23
|
yield Input(placeholder="e.g., 2", id="cpu-input", type="integer", value=self.current_cpu)
|
|
23
24
|
with Horizontal():
|
|
24
|
-
yield Button(
|
|
25
|
-
yield Button(
|
|
25
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-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 == "save-btn":
|
|
@@ -40,11 +41,11 @@ class EditMemoryModal(BaseModal[str | None]):
|
|
|
40
41
|
|
|
41
42
|
def compose(self) -> ComposeResult:
|
|
42
43
|
with Vertical(id="edit-memory-dialog", classes="edit-memory-dialog"):
|
|
43
|
-
yield Label(
|
|
44
|
+
yield Label(StaticText.ENTER_NEW_MEMORY_SIZE)
|
|
44
45
|
yield Input(placeholder="e.g., 2048", id="memory-input", type="integer", value=self.current_memory)
|
|
45
46
|
with Horizontal():
|
|
46
|
-
yield Button(
|
|
47
|
-
yield Button(
|
|
47
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
|
|
48
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
48
49
|
|
|
49
50
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
50
51
|
if event.button.id == "save-btn":
|
|
@@ -63,7 +64,7 @@ class SelectMachineTypeModal(BaseModal[str | None]):
|
|
|
63
64
|
|
|
64
65
|
def compose(self) -> ComposeResult:
|
|
65
66
|
with Vertical(id="select-machine-type-dialog", classes="select-machine-type-dialog"):
|
|
66
|
-
yield Label(
|
|
67
|
+
yield Label(StaticText.SELECT_MACHINE_TYPE)
|
|
67
68
|
with ScrollableContainer():
|
|
68
69
|
yield ListView(
|
|
69
70
|
*[ValueListItem(Label(mt), value=mt) for mt in self.machine_types],
|
|
@@ -71,7 +72,7 @@ class SelectMachineTypeModal(BaseModal[str | None]):
|
|
|
71
72
|
classes="machine-type-list"
|
|
72
73
|
)
|
|
73
74
|
with Horizontal():
|
|
74
|
-
yield Button(
|
|
75
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
75
76
|
|
|
76
77
|
def on_mount(self) -> None:
|
|
77
78
|
list_view = self.query_one(ListView)
|
|
@@ -102,13 +103,13 @@ class EditCpuTuneModal(BaseModal[list[dict] | None]):
|
|
|
102
103
|
current_val = "; ".join([f"{p['vcpu']}:{p['cpuset']}" for p in self.current_vcpupin])
|
|
103
104
|
|
|
104
105
|
with Vertical(id="edit-cpu-tune-dialog", classes="edit-cpu-dialog"):
|
|
105
|
-
yield Label(
|
|
106
|
-
yield Label(
|
|
106
|
+
yield Label(StaticText.ENTER_CPU_PINNING.format(max_vcpus=self.max_vcpus - 1))
|
|
107
|
+
yield Label(StaticText.CPU_PINNING_FORMAT, classes="help-text")
|
|
107
108
|
yield Input(placeholder="e.g., 0:0-1; 1:2-3", id="cputune-input", value=current_val)
|
|
108
109
|
with Horizontal():
|
|
109
|
-
yield Button(
|
|
110
|
-
yield Button(
|
|
111
|
-
yield Button(
|
|
110
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
|
|
111
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
112
|
+
yield Button(ButtonLabels.HELP, variant="default", id="help-btn")
|
|
112
113
|
|
|
113
114
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
114
115
|
if event.button.id == "save-btn":
|
|
@@ -135,9 +136,9 @@ class EditCpuTuneModal(BaseModal[list[dict] | None]):
|
|
|
135
136
|
vcpupin_list.append({'vcpu': vcpu, 'cpuset': cpuset})
|
|
136
137
|
self.dismiss(vcpupin_list)
|
|
137
138
|
except ValueError as e:
|
|
138
|
-
self.app.show_error_message(
|
|
139
|
+
self.app.show_error_message(ErrorMessages.VALIDATION_ERROR_TEMPLATE.format(error=e))
|
|
139
140
|
except Exception as e:
|
|
140
|
-
self.app.show_error_message(
|
|
141
|
+
self.app.show_error_message(ErrorMessages.INVALID_FORMAT_TEMPLATE.format(error=e))
|
|
141
142
|
elif event.button.id == "cancel-btn":
|
|
142
143
|
self.dismiss(None)
|
|
143
144
|
elif event.button.id == "help-btn":
|
|
@@ -173,14 +174,14 @@ class EditNumaTuneModal(BaseModal[dict | None]):
|
|
|
173
174
|
modes = [("strict", "strict"), ("preferred", "preferred"), ("interleave", "interleave"), ("None", "None")]
|
|
174
175
|
|
|
175
176
|
with Vertical(id="edit-numatune-dialog", classes="edit-cpu-dialog"):
|
|
176
|
-
yield Label(
|
|
177
|
+
yield Label(StaticText.NUMA_MEMORY_MODE)
|
|
177
178
|
yield Select(modes, value=self.current_mode, id="numa-mode-select", allow_blank=False)
|
|
178
|
-
yield Label(
|
|
179
|
+
yield Label(StaticText.NODESET)
|
|
179
180
|
yield Input(placeholder="e.g., 0-1", id="numa-nodeset-input", value=self.current_nodeset)
|
|
180
181
|
with Horizontal():
|
|
181
|
-
yield Button(
|
|
182
|
-
yield Button(
|
|
183
|
-
yield Button(
|
|
182
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn")
|
|
183
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
184
|
+
yield Button(ButtonLabels.HELP, variant="default", id="help-btn")
|
|
184
185
|
|
|
185
186
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
186
187
|
if event.button.id == "save-btn":
|
|
@@ -194,7 +195,7 @@ class EditNumaTuneModal(BaseModal[dict | None]):
|
|
|
194
195
|
# Validate nodeset syntax
|
|
195
196
|
if nodeset:
|
|
196
197
|
if not all(c.isdigit() or c in ',-' for c in nodeset):
|
|
197
|
-
self.app.show_error_message(
|
|
198
|
+
self.app.show_error_message(ErrorMessages.INVALID_NODESET_SYNTAX_TEMPLATE.format(nodeset=nodeset))
|
|
198
199
|
return
|
|
199
200
|
|
|
200
201
|
self.dismiss({'mode': mode, 'nodeset': nodeset})
|
|
@@ -3,6 +3,7 @@ from textual.app import ComposeResult
|
|
|
3
3
|
from textual.screen import ModalScreen
|
|
4
4
|
from textual.widgets import Button, Static, Select, Checkbox
|
|
5
5
|
from textual.containers import Vertical
|
|
6
|
+
from ..constants import StaticText, ButtonLabels
|
|
6
7
|
|
|
7
8
|
class CustomMigrationModal(ModalScreen[dict | None]):
|
|
8
9
|
"""A modal to confirm custom migration actions."""
|
|
@@ -14,12 +15,12 @@ class CustomMigrationModal(ModalScreen[dict | None]):
|
|
|
14
15
|
|
|
15
16
|
def compose(self) -> ComposeResult:
|
|
16
17
|
with Vertical(id="custom-migration-dialog"):
|
|
17
|
-
yield Static(
|
|
18
|
+
yield Static(StaticText.CUSTOM_MIGRATION_PLAN)
|
|
18
19
|
|
|
19
20
|
for i, action in enumerate(self.actions):
|
|
20
21
|
if action["type"] == "move_volume":
|
|
21
|
-
yield Static(
|
|
22
|
-
yield Static(
|
|
22
|
+
yield Static(StaticText.DISK_VOLUME_NAME.format(volume_name=action['volume_name']))
|
|
23
|
+
yield Static(StaticText.SOURCE_POOL.format(source_pool=action['source_pool']))
|
|
23
24
|
dest_pools = action.get("dest_pools", [])
|
|
24
25
|
if dest_pools:
|
|
25
26
|
yield Select(
|
|
@@ -28,16 +29,16 @@ class CustomMigrationModal(ModalScreen[dict | None]):
|
|
|
28
29
|
id=f"pool-select-{i}"
|
|
29
30
|
)
|
|
30
31
|
else:
|
|
31
|
-
yield Static(
|
|
32
|
+
yield Static(StaticText.NO_DESTINATION_POOLS)
|
|
32
33
|
elif action["type"] == "manual_copy":
|
|
33
|
-
yield Static(
|
|
34
|
-
yield Static(
|
|
34
|
+
yield Static(StaticText.DISK_PATH.format(disk_path=action['disk_path']))
|
|
35
|
+
yield Static(StaticText.ACTION_MESSAGE.format(message=action['message']))
|
|
35
36
|
|
|
36
|
-
yield Checkbox(
|
|
37
|
+
yield Checkbox(StaticText.UNDEFINE_SOURCE_VM, value=True, id="undefine-checkbox")
|
|
37
38
|
|
|
38
39
|
with Vertical(classes="modal-buttons"):
|
|
39
|
-
yield Button(
|
|
40
|
-
yield Button(
|
|
40
|
+
yield Button(ButtonLabels.CONFIRM, variant="primary", id="confirm")
|
|
41
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel")
|
|
41
42
|
|
|
42
43
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
43
44
|
if event.button.id == "confirm":
|