add: виджета для Backup со списком...

This commit is contained in:
Sergei Erjemin 2025-06-13 18:28:09 +03:00
parent 1b70c30eaf
commit 1c1100de7d

View File

@ -10,10 +10,16 @@ from textual.css.query import DOMQuery
from textual.reactive import reactive
from textual.screen import Screen
from textual.widgets import Button, Header, Footer, Static, Log, Label, ListView, ListItem, OptionList
from textual.widget import Widget
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .tui import PGanecApp
# --- Настройки и инициирование логирования модуля ---
logger = logging.getLogger(__name__)
# --- КОНФИГ_ПЕРЕМЕННЕЫ которые использоваться в приложении (возможно стоит перенести в другой файл) ---
DB_FOR_BACKUP = "db_4b"
# --- Кастомный обработчик логирования (для Textual) ---
class TextualLogHandler(logging.Handler):
@ -42,7 +48,7 @@ class TextualLogHandler(logging.Handler):
class MainMenu(Static):
def compose(self) -> ComposeResult:
yield Horizontal(
Button(label="Backup (DB\u2192Dump)", id="backup"),
Button(label="Backup (DB\u2192Dump)", id="backup", classes="main-menu-button"),
Button(label="Restore (Dump\u2192DB)", id="restore"),
Button(label="Copy (DB\u2192DB)", id="copy", variant="error"),
Button(label="Service", id="service"),
@ -53,8 +59,18 @@ class MainMenu(Static):
# --- Виджет для "встроенного" выбора ---
class SelectionWidget(Static): # Наследуем от Static
"""
Виджет для отображения опций выбора бэкапа внутри основного экрана.
Виджет для отображения опций выбора бекапа внутри основного экрана.
"""
@property
def app(self) -> "PGanecApp":
"""
Возвращает ссылку на приложение PGanecApp , к которому принадлежит этот виджет. Нужно для доступа к методам
и состоянию приложения (и статического анализатора, т.к. класс PGanecApp описан ниже и "отсюда не виден").
:return:
"""
return super().app # type: ignore
def __init__(self,
app_config: Optional[Dict[str, Any]] = None,
action_type=None,
@ -62,18 +78,40 @@ class SelectionWidget(Static): # Наследуем от Static
super().__init__(**kwargs)
self.app_config = app_config if app_config is not None else {}
self.action_type = action_type # Сохраняем тип действия
self._button_id_to_data_map: Dict[str, Any] = {} # Карта для хранения значений опций
# SelectionWidget(Static)
def compose(self) -> ComposeResult:
# options: List[Tuple[str, str]] = [] # Список опций для выбора
self._button_id_to_data_map.clear() # Очищаем карту значений
num_count = 1
if self.action_type == "backup":
yield Label("Выберите BD-сервер для бэкапа:")
# ... тут будет список серверов
if "servers" in self.app_config:
logger.info(f"TUI: Обнаружено {len(self.app_config['servers'])} серверов в конфигурации.")
# ... тут будет список серверов
for server_conf in self.app_config["servers"]:
server_name = server_conf.get("name")
button_id = server_conf.get("id")
if not button_id:
button_id = f"server_{num_count:02d}" # Генерируем временный id, если не указан
logger.warning(
f"Сервер '{server_name}' не имеет 'id' в yaml-конфигурации. Установлен id='{button_id}'.")
# continue # пропускаем?
if button_id in self._button_id_to_data_map:
logger.error(
f"TUI: Дублирующийся ID кнопки '{button_id}' для сервера '{server_name}' из yaml-конфигурации. ПРОРУСКАЕМ.")
continue
# Добавляем кнопку для каждого сервера
yield Button(label=f"{num_count:02d}: {server_name}", id=button_id, classes="list-button")
self._button_id_to_data_map[button_id] = {"type": DB_FOR_BACKUP, "config": server_conf}
num_count += 1
elif self.action_type == "restore":
yield Label("Выберите том для восстановления:")
if "servers" in self.app_config:
if "targets" in self.app_config:
logger.info(f"TUI: Обнаружено {len(self.app_config['targets'])} томов в конфигурации.")
yield Static("Тут будет список...", classes="placeholder-text") # Временная заглушка
# ... тут будет список томов
elif self.action_type == "copy":
yield Label("Выберите BD-сервер (откуда копировать):")
@ -82,22 +120,54 @@ class SelectionWidget(Static): # Наследуем от Static
yield Label("Служебные функции:")
# ... тут будут служебные функции
# Кнопка "Отмена" в конце любого списка
yield Button(label=f"{num_count:02d}: Отмена", id="cancel_widget", classes="list-button")
yield Static("Тут будет список чего-то...", classes="placeholder-text") # Временная заглушка
yield Button("Отмена", id="cancel_backup_selection", variant="default")
def on_mount(self) -> None:
"""
Вызывается после монтирования виджета в DOM.
:return:
"""
logger.info(f"Виджет '{self.id}' (SelectionWidget) смонтирован.")
try:
# Пытаемся установить фокус на первую кнопку-опцию
# Ищем первую кнопку с классом "list-button" внутри этого виджета
first_option_button = self.query_one("Button.list-button", Button)
first_option_button.focus()
logger.debug(f"TUI: SelectionWidget: Фокус на кнопке-опцию: `{first_option_button.id}`")
except Exception as e:
# except DOMQuery.DoesNotExist:
logger.error(f"SelectionWidget: Ошибка при установке фокуса в Виджете: {e}")
# logger.warning(f"SelectionWidget: OptionList не найден для установки фокуса (action_type: {self.action_type}).")
# Можно сразу установить фокус на первый элемент, если он есть
# self.query_one(OptionList).focus() или self.query_one(Button).focus()
async def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "cancel_backup_selection":
logger.info("Нажата отмена в SelectionWidget.")
button_id = event.button.id
button_data = self._button_id_to_data_map.get(button_id) # <--- Извлекаем данные по ID кнопки
logger.info(f"SelectionWidget ({self.action_type}): нажата кнопка-опция id='{button_id}', data={button_data}")
if button_data:
action_type_from_data = button_data.get("type")
if action_type_from_data == DB_FOR_BACKUP:
server_config = button_data.get("config") # <--- Получаем полную конфигурацию сервера
logger.info(
f"SelectionWidget ({self.action_type}): выбран сервер {server_config.get('name') if isinstance(server_config, dict) else server_config}. Далее - выбор БД/дампа.")
# Тут будет логика для перехода к следующему шагу, используя server_config
self.app.bell()
# ... другие обработчики ...
if event.button.id == "cancel_widget":
logger.debug("Нажата 'Отмена' в SelectionWidget.")
await self.remove() # Удаляем сам виджет
self.app.selection_widget_active = False # <-- Сброс флага что виджет активен
# Можно также послать сообщение приложению, чтобы оно обновило состояние, если нужно
# self.app.post_message(BackupSelectionCancelled())
self.app.set_main_menu_enabled(True) # Включаем верхнее меню обратно
@ -109,7 +179,7 @@ class PGanecApp(App):
height: 1fr; /* 1fr означает "одна доля доступного пространства" */
overflow-y: auto; /* Если контент не влезет, появится прокрутка */
}
#title { width: 100%; text-align: left; background: purple; padding: 0 1; }
#title { width: 100%; text-align: left; background: purple; padding: 0; }
#menu { width: 100%; height: 3; align: left top; padding: 0; }
#menu Button {
align: center middle;
@ -140,19 +210,21 @@ class PGanecApp(App):
background: $panel; /* Используем системный цвет панели */
/* height: auto; /* Высота будет по содержимому */
}
SelectionWidget .selection-widget-label {
margin-bottom: 1;
text-style: bold;
}
SelectionWidget .placeholder-text {
padding: 1;
padding: 0;
color: $text-muted;
}
SelectionWidget Button { /* Стили для кнопок внутри виджета */
width: auto; /* Чтобы кнопка была по размеру текста */
background: 15%;
margin-top: 1;
Button.list-button { /* Стили для кнопок внутри виджета */
width: auto;
height: 1;
align: left middle;
border: hidden;
background: transparent;
color: $text;
margin-left: 0; padding-left: 1;
}
Button#cancel_widget {margin-left: -1; color: red; } /* Отдельный стиль для кнопки "Отмена" */
Footer { dock: bottom; height: 1; }
Header { dock: top; height: 1; }
@ -205,6 +277,18 @@ class PGanecApp(App):
yield Log(id="app_log_viewer", highlight=True) # Виджет лога
yield Footer()
def set_main_menu_enabled(self, enabled: bool):
"""
Включает или отключает кнопки верхнего меню
:param enabled:
:return:
"""
menu = self.query_one(MainMenu)
for btn in menu.query(Button):
btn.disabled = not enabled
async def show_selection_widget(self, action_type: str) -> None:
"""Показывает виджет выбора для ."""
# Сначала удаляем старый виджет, если он есть, чтобы не было дублей
@ -223,6 +307,7 @@ class PGanecApp(App):
)
self.selection_widget_active = True # Выставляем флаг, что виджет активен
self.set_main_menu_enabled(False) # Отключаем верхнее меню
# Находим контейнер, куда его добавить.
# Будем добавлять после MainMenu внутри #main_content_area
main_content_area = self.query_one("#main_content_area", Vertical)
@ -238,7 +323,7 @@ class PGanecApp(App):
async def on_button_pressed(self, event: Button.Pressed) -> None:
"""
Обработчик нажатия (Enter или Click) кнопки в верхнем меню.
:param event:
:param event: Полученное событие нажатия кнопки на верхнем меню.
:return:
"""
button_id = event.button.id
@ -316,10 +401,10 @@ class PGanecApp(App):
logger.debug("TUI: Что-то нажато (Enter), но это не кнопка.")
return
if focused.parent and isinstance(focused.parent, SelectionWidget):
await focused.parent.on_button_pressed(Button.Pressed(focused))
else:
await self.on_button_pressed(Button.Pressed(focused))
# if focused.parent and isinstance(focused.parent, SelectionWidget):
# await focused.parent.on_button_pressed(Button.Pressed(focused))
# else:
# await self.on_button_pressed(Button.Pressed(focused))
# --- Обработчики быстрых клавиш ---
# Для 'q' (Quit) уже есть в BINDINGS срабатывающий метод action_quit, он сработает автоматически (не ясно почему).