[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a digit stands for itself. The form, though, locked the custom profile into its three dropdowns. Each now gets a final "free value..." entry revealing an input below it. An invalid entry overwrites nothing: the resource falls back to the catalog value instead of freezing the view. The 3 vCPU preset was missing too, between 2 and 4; it comes from the same constant, so both interfaces gain it together. --- FR --- La CLI acceptait déjà une valeur tapée — une lettre choisit une suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait le profil personnalisé dans ses trois listes déroulantes. Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une saisie sous elle. Une entrée invalide n'écrase rien : la ressource retombe sur celle du catalogue plutôt que de bloquer la vue. Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même constante, donc les deux interfaces l'offrent ensemble. Assisted-by: Claude Opus 5
This commit is contained in:
parent
36fc4e319a
commit
7650710295
3 changed files with 86 additions and 10 deletions
|
|
@ -245,6 +245,16 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
result = {"spec": None}
|
result = {"spec": None}
|
||||||
|
|
||||||
AUTO = "__auto__"
|
AUTO = "__auto__"
|
||||||
|
# Dernier choix de chaque liste de ressources : il ne porte pas de valeur,
|
||||||
|
# il révèle la saisie libre placée juste dessous.
|
||||||
|
FREE = "__free__"
|
||||||
|
RES_FIELDS = {
|
||||||
|
"vcpus": ("#f_vcpus", "#c_vcpus"),
|
||||||
|
"ram": ("#f_ram", "#c_ram"),
|
||||||
|
"disk": ("#f_disk", "#c_disk"),
|
||||||
|
}
|
||||||
|
SELECT_TO_FIELD = {sel[1:]: f for f, (sel, _i) in RES_FIELDS.items()}
|
||||||
|
INPUT_TO_FIELD = {inp[1:]: f for f, (_s, inp) in RES_FIELDS.items()}
|
||||||
|
|
||||||
def entry_label(e):
|
def entry_label(e):
|
||||||
star = " *" if e.get("default") else ""
|
star = " *" if e.get("default") else ""
|
||||||
|
|
@ -330,6 +340,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
.grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; }
|
.grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; }
|
||||||
SelectionList { height: 10; border: solid $panel; }
|
SelectionList { height: 10; border: solid $panel; }
|
||||||
RadioSet { height: auto; layout: horizontal; }
|
RadioSet { height: auto; layout: horizontal; }
|
||||||
|
.freeval { display: none; }
|
||||||
#reslabel { color: $text-muted; }
|
#reslabel { color: $text-muted; }
|
||||||
EditVMScreen { align: center middle; }
|
EditVMScreen { align: center middle; }
|
||||||
#editbox {
|
#editbox {
|
||||||
|
|
@ -364,6 +375,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
self.arch = ctx.get("native") or arches[0]
|
self.arch = ctx.get("native") or arches[0]
|
||||||
self.profile = "1"
|
self.profile = "1"
|
||||||
self.custom = {}
|
self.custom = {}
|
||||||
|
self._free = {}
|
||||||
self.overrides = {}
|
self.overrides = {}
|
||||||
self.vms = []
|
self.vms = []
|
||||||
self.rows = []
|
self.rows = []
|
||||||
|
|
@ -385,27 +397,52 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
for label in ("x1", "x2", "x3", "x4"):
|
for label in ("x1", "x2", "x3", "x4"):
|
||||||
yield RadioButton(label, value=label == "x1")
|
yield RadioButton(label, value=label == "x1")
|
||||||
yield RadioButton(t("custom"))
|
yield RadioButton(t("custom"))
|
||||||
|
# Chaque ressource offre ses suggestions plus « libre… »,
|
||||||
|
# qui révèle une saisie. C'est l'équivalent TUI de la règle
|
||||||
|
# de la CLI : une lettre choisit une suggestion, un chiffre
|
||||||
|
# vaut pour lui-même.
|
||||||
yield Select(
|
yield Select(
|
||||||
[(str(c), c) for c in ctx["cpu_presets"]],
|
[(str(c), c) for c in ctx["cpu_presets"]]
|
||||||
|
+ [(t("free value…"), FREE)],
|
||||||
prompt=t("vCPU"),
|
prompt=t("vCPU"),
|
||||||
id="f_vcpus",
|
id="f_vcpus",
|
||||||
disabled=True,
|
disabled=True,
|
||||||
)
|
)
|
||||||
|
yield Input(
|
||||||
|
placeholder=t("vCPU"),
|
||||||
|
id="c_vcpus",
|
||||||
|
classes="freeval",
|
||||||
|
disabled=True,
|
||||||
|
)
|
||||||
yield Select(
|
yield Select(
|
||||||
[
|
[
|
||||||
(f"{m} ({m // 1024}G)", m)
|
(f"{m} ({m // 1024}G)", m)
|
||||||
for m in ctx["ram_presets"]
|
for m in ctx["ram_presets"]
|
||||||
],
|
]
|
||||||
|
+ [(t("free value…"), FREE)],
|
||||||
prompt=t("RAM (MB)"),
|
prompt=t("RAM (MB)"),
|
||||||
id="f_ram",
|
id="f_ram",
|
||||||
disabled=True,
|
disabled=True,
|
||||||
)
|
)
|
||||||
|
yield Input(
|
||||||
|
placeholder=t("RAM (MB)"),
|
||||||
|
id="c_ram",
|
||||||
|
classes="freeval",
|
||||||
|
disabled=True,
|
||||||
|
)
|
||||||
yield Select(
|
yield Select(
|
||||||
[(d, d) for d in ctx["disk_presets"]],
|
[(d, d) for d in ctx["disk_presets"]]
|
||||||
|
+ [(t("free value…"), FREE)],
|
||||||
prompt=t("Disk"),
|
prompt=t("Disk"),
|
||||||
id="f_disk",
|
id="f_disk",
|
||||||
disabled=True,
|
disabled=True,
|
||||||
)
|
)
|
||||||
|
yield Input(
|
||||||
|
placeholder=t("Disk (e.g. 250G, 1.5T)"),
|
||||||
|
id="c_disk",
|
||||||
|
classes="freeval",
|
||||||
|
disabled=True,
|
||||||
|
)
|
||||||
yield Static("ERPLibre", classes="grouptitle")
|
yield Static("ERPLibre", classes="grouptitle")
|
||||||
yield Checkbox(
|
yield Checkbox(
|
||||||
t("Install ERPLibre"),
|
t("Install ERPLibre"),
|
||||||
|
|
@ -588,19 +625,50 @@ def run_deploy_form(ctx, run_app: bool = True):
|
||||||
index = event.radio_set.pressed_index
|
index = event.radio_set.pressed_index
|
||||||
self.profile = "custom" if index == 4 else str(index + 1)
|
self.profile = "custom" if index == 4 else str(index + 1)
|
||||||
custom = self.profile == "custom"
|
custom = self.profile == "custom"
|
||||||
for wid in ("#f_vcpus", "#f_ram", "#f_disk"):
|
for field, (sel, inp) in RES_FIELDS.items():
|
||||||
self.query_one(wid, Select).disabled = not custom
|
self.query_one(sel, Select).disabled = not custom
|
||||||
|
free = custom and self._free.get(field)
|
||||||
|
self._show_free(field, free)
|
||||||
self._recompute()
|
self._recompute()
|
||||||
|
|
||||||
|
def _show_free(self, field, visible) -> None:
|
||||||
|
"""Montre ou cache la saisie libre d'une ressource."""
|
||||||
|
widget = self.query_one(RES_FIELDS[field][1], Input)
|
||||||
|
widget.display = bool(visible)
|
||||||
|
widget.disabled = not visible
|
||||||
|
|
||||||
def on_selection_list_selected_changed(self, event) -> None:
|
def on_selection_list_selected_changed(self, event) -> None:
|
||||||
self._recompute()
|
self._recompute()
|
||||||
|
|
||||||
def on_select_changed(self, event) -> None:
|
def on_select_changed(self, event) -> None:
|
||||||
mapping = {"f_vcpus": "vcpus", "f_ram": "ram", "f_disk": "disk"}
|
field = SELECT_TO_FIELD.get(event.select.id)
|
||||||
field = mapping.get(event.select.id)
|
if not field:
|
||||||
|
return
|
||||||
|
if event.value is FREE:
|
||||||
|
# La valeur retenue est celle de la saisie, pas ce choix-ci.
|
||||||
|
self._free[field] = True
|
||||||
|
self._show_free(field, True)
|
||||||
|
self.query_one(RES_FIELDS[field][1], Input).focus()
|
||||||
|
self._apply_free(field)
|
||||||
|
elif event.value is not SELECT_NULL:
|
||||||
|
self._free[field] = False
|
||||||
|
self._show_free(field, False)
|
||||||
|
self.custom[field] = event.value
|
||||||
|
self._recompute()
|
||||||
|
|
||||||
|
def _apply_free(self, field) -> None:
|
||||||
|
"""Relit la saisie libre. Une valeur invalide n'écrase rien : le
|
||||||
|
profil retombe alors sur celle du catalogue."""
|
||||||
|
raw = self.query_one(RES_FIELDS[field][1], Input).value.strip()
|
||||||
|
if field == "disk":
|
||||||
|
self.custom[field] = parse_disk(raw) or ""
|
||||||
|
else:
|
||||||
|
self.custom[field] = positive_int(raw, 0)
|
||||||
|
|
||||||
|
def on_input_changed(self, event) -> None:
|
||||||
|
field = INPUT_TO_FIELD.get(event.input.id)
|
||||||
if field:
|
if field:
|
||||||
if event.value is not SELECT_NULL:
|
self._apply_free(field)
|
||||||
self.custom[field] = event.value
|
|
||||||
self._recompute()
|
self._recompute()
|
||||||
|
|
||||||
def on_checkbox_changed(self, event) -> None:
|
def on_checkbox_changed(self, event) -> None:
|
||||||
|
|
|
||||||
|
|
@ -4507,7 +4507,7 @@ class TODO:
|
||||||
# KVM autorise plus de vCPU que de cœurs (surengagement) : on prévient
|
# KVM autorise plus de vCPU que de cœurs (surengagement) : on prévient
|
||||||
# plutôt que d'écrêter, contrairement au multiplicateur x1..x4 qui, lui,
|
# plutôt que d'écrêter, contrairement au multiplicateur x1..x4 qui, lui,
|
||||||
# est un calcul automatique et se borne aux cœurs de l'hôte.
|
# est un calcul automatique et se borne aux cœurs de l'hôte.
|
||||||
_QEMU_CPU_PRESETS = (1, 2, 4, 6, 8, 16, 24, 32)
|
_QEMU_CPU_PRESETS = (1, 2, 3, 4, 6, 8, 16, 24, 32)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _plural(word, count):
|
def _plural(word, count):
|
||||||
|
|
|
||||||
|
|
@ -1973,6 +1973,14 @@ TRANSLATIONS = {
|
||||||
"fr": "RAM (Mo)",
|
"fr": "RAM (Mo)",
|
||||||
"en": "RAM (MB)",
|
"en": "RAM (MB)",
|
||||||
},
|
},
|
||||||
|
"free value…": {
|
||||||
|
"fr": "valeur libre…",
|
||||||
|
"en": "free value…",
|
||||||
|
},
|
||||||
|
"Disk (e.g. 250G, 1.5T)": {
|
||||||
|
"fr": "Disque (ex. 250G, 1,5T)",
|
||||||
|
"en": "Disk (e.g. 250G, 1.5T)",
|
||||||
|
},
|
||||||
"all archs": {
|
"all archs": {
|
||||||
"fr": "toutes",
|
"fr": "toutes",
|
||||||
"en": "all",
|
"en": "all",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue