add: виджета для Backup со списком...
This commit is contained in:
parent
1b70c30eaf
commit
1c1100de7d
@ -10,10 +10,16 @@ from textual.css.query import DOMQuery
|
|||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Button, Header, Footer, Static, Log, Label, ListView, ListItem, OptionList
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- КОНФИГ_ПЕРЕМЕННЕЫ которые использоваться в приложении (возможно стоит перенести в другой файл) ---
|
||||||
|
DB_FOR_BACKUP = "db_4b"
|
||||||
|
|
||||||
# --- Кастомный обработчик логирования (для Textual) ---
|
# --- Кастомный обработчик логирования (для Textual) ---
|
||||||
class TextualLogHandler(logging.Handler):
|
class TextualLogHandler(logging.Handler):
|
||||||
@ -42,7 +48,7 @@ class TextualLogHandler(logging.Handler):
|
|||||||
class MainMenu(Static):
|
class MainMenu(Static):
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Horizontal(
|
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="Restore (Dump\u2192DB)", id="restore"),
|
||||||
Button(label="Copy (DB\u2192DB)", id="copy", variant="error"),
|
Button(label="Copy (DB\u2192DB)", id="copy", variant="error"),
|
||||||
Button(label="Service", id="service"),
|
Button(label="Service", id="service"),
|
||||||
@ -53,8 +59,18 @@ class MainMenu(Static):
|
|||||||
# --- Виджет для "встроенного" выбора ---
|
# --- Виджет для "встроенного" выбора ---
|
||||||
class SelectionWidget(Static): # Наследуем от Static
|
class SelectionWidget(Static): # Наследуем от Static
|
||||||
"""
|
"""
|
||||||
Виджет для отображения опций выбора бэкапа внутри основного экрана.
|
Виджет для отображения опций выбора бекапа внутри основного экрана.
|
||||||
"""
|
"""
|
||||||
|
@property
|
||||||
|
def app(self) -> "PGanecApp":
|
||||||
|
"""
|
||||||
|
Возвращает ссылку на приложение PGanecApp , к которому принадлежит этот виджет. Нужно для доступа к методам
|
||||||
|
и состоянию приложения (и статического анализатора, т.к. класс PGanecApp описан ниже и "отсюда не виден").
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return super().app # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
app_config: Optional[Dict[str, Any]] = None,
|
app_config: Optional[Dict[str, Any]] = None,
|
||||||
action_type=None,
|
action_type=None,
|
||||||
@ -62,18 +78,40 @@ class SelectionWidget(Static): # Наследуем от Static
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.app_config = app_config if app_config is not None else {}
|
self.app_config = app_config if app_config is not None else {}
|
||||||
self.action_type = action_type # Сохраняем тип действия
|
self.action_type = action_type # Сохраняем тип действия
|
||||||
|
self._button_id_to_data_map: Dict[str, Any] = {} # Карта для хранения значений опций
|
||||||
# SelectionWidget(Static)
|
# SelectionWidget(Static)
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
|
# options: List[Tuple[str, str]] = [] # Список опций для выбора
|
||||||
|
self._button_id_to_data_map.clear() # Очищаем карту значений
|
||||||
|
num_count = 1
|
||||||
if self.action_type == "backup":
|
if self.action_type == "backup":
|
||||||
yield Label("Выберите BD-сервер для бэкапа:")
|
yield Label("Выберите BD-сервер для бэкапа:")
|
||||||
|
# ... тут будет список серверов
|
||||||
if "servers" in self.app_config:
|
if "servers" in self.app_config:
|
||||||
logger.info(f"TUI: Обнаружено {len(self.app_config['servers'])} серверов в конфигурации.")
|
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":
|
elif self.action_type == "restore":
|
||||||
yield Label("Выберите том для восстановления:")
|
yield Label("Выберите том для восстановления:")
|
||||||
if "servers" in self.app_config:
|
if "targets" in self.app_config:
|
||||||
logger.info(f"TUI: Обнаружено {len(self.app_config['targets'])} томов в конфигурации.")
|
logger.info(f"TUI: Обнаружено {len(self.app_config['targets'])} томов в конфигурации.")
|
||||||
|
yield Static("Тут будет список...", classes="placeholder-text") # Временная заглушка
|
||||||
# ... тут будет список томов
|
# ... тут будет список томов
|
||||||
elif self.action_type == "copy":
|
elif self.action_type == "copy":
|
||||||
yield Label("Выберите BD-сервер (откуда копировать):")
|
yield Label("Выберите BD-сервер (откуда копировать):")
|
||||||
@ -82,22 +120,54 @@ class SelectionWidget(Static): # Наследуем от Static
|
|||||||
yield Label("Служебные функции:")
|
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:
|
def on_mount(self) -> None:
|
||||||
|
"""
|
||||||
|
Вызывается после монтирования виджета в DOM.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
logger.info(f"Виджет '{self.id}' (SelectionWidget) смонтирован.")
|
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()
|
# self.query_one(OptionList).focus() или self.query_one(Button).focus()
|
||||||
|
|
||||||
|
|
||||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
if event.button.id == "cancel_backup_selection":
|
button_id = event.button.id
|
||||||
logger.info("Нажата отмена в SelectionWidget.")
|
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() # Удаляем сам виджет
|
await self.remove() # Удаляем сам виджет
|
||||||
self.app.selection_widget_active = False # <-- Сброс флага что виджет активен
|
self.app.selection_widget_active = False # <-- Сброс флага что виджет активен
|
||||||
# Можно также послать сообщение приложению, чтобы оно обновило состояние, если нужно
|
# Можно также послать сообщение приложению, чтобы оно обновило состояние, если нужно
|
||||||
# self.app.post_message(BackupSelectionCancelled())
|
# self.app.post_message(BackupSelectionCancelled())
|
||||||
|
self.app.set_main_menu_enabled(True) # Включаем верхнее меню обратно
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -109,7 +179,7 @@ class PGanecApp(App):
|
|||||||
height: 1fr; /* 1fr означает "одна доля доступного пространства" */
|
height: 1fr; /* 1fr означает "одна доля доступного пространства" */
|
||||||
overflow-y: auto; /* Если контент не влезет, появится прокрутка */
|
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 { width: 100%; height: 3; align: left top; padding: 0; }
|
||||||
#menu Button {
|
#menu Button {
|
||||||
align: center middle;
|
align: center middle;
|
||||||
@ -140,19 +210,21 @@ class PGanecApp(App):
|
|||||||
background: $panel; /* Используем системный цвет панели */
|
background: $panel; /* Используем системный цвет панели */
|
||||||
/* height: auto; /* Высота будет по содержимому */
|
/* height: auto; /* Высота будет по содержимому */
|
||||||
}
|
}
|
||||||
SelectionWidget .selection-widget-label {
|
|
||||||
margin-bottom: 1;
|
|
||||||
text-style: bold;
|
|
||||||
}
|
|
||||||
SelectionWidget .placeholder-text {
|
SelectionWidget .placeholder-text {
|
||||||
padding: 1;
|
padding: 0;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
SelectionWidget Button { /* Стили для кнопок внутри виджета */
|
Button.list-button { /* Стили для кнопок внутри виджета */
|
||||||
width: auto; /* Чтобы кнопка была по размеру текста */
|
width: auto;
|
||||||
background: 15%;
|
height: 1;
|
||||||
margin-top: 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; }
|
Footer { dock: bottom; height: 1; }
|
||||||
Header { dock: top; height: 1; }
|
Header { dock: top; height: 1; }
|
||||||
@ -205,6 +277,18 @@ class PGanecApp(App):
|
|||||||
yield Log(id="app_log_viewer", highlight=True) # Виджет лога
|
yield Log(id="app_log_viewer", highlight=True) # Виджет лога
|
||||||
yield Footer()
|
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:
|
async def show_selection_widget(self, action_type: str) -> None:
|
||||||
"""Показывает виджет выбора для ."""
|
"""Показывает виджет выбора для ."""
|
||||||
# Сначала удаляем старый виджет, если он есть, чтобы не было дублей
|
# Сначала удаляем старый виджет, если он есть, чтобы не было дублей
|
||||||
@ -223,6 +307,7 @@ class PGanecApp(App):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.selection_widget_active = True # Выставляем флаг, что виджет активен
|
self.selection_widget_active = True # Выставляем флаг, что виджет активен
|
||||||
|
self.set_main_menu_enabled(False) # Отключаем верхнее меню
|
||||||
# Находим контейнер, куда его добавить.
|
# Находим контейнер, куда его добавить.
|
||||||
# Будем добавлять после MainMenu внутри #main_content_area
|
# Будем добавлять после MainMenu внутри #main_content_area
|
||||||
main_content_area = self.query_one("#main_content_area", Vertical)
|
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:
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""
|
"""
|
||||||
Обработчик нажатия (Enter или Click) кнопки в верхнем меню.
|
Обработчик нажатия (Enter или Click) кнопки в верхнем меню.
|
||||||
:param event:
|
:param event: Полученное событие нажатия кнопки на верхнем меню.
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
button_id = event.button.id
|
button_id = event.button.id
|
||||||
@ -316,10 +401,10 @@ class PGanecApp(App):
|
|||||||
logger.debug("TUI: Что-то нажато (Enter), но это не кнопка.")
|
logger.debug("TUI: Что-то нажато (Enter), но это не кнопка.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if focused.parent and isinstance(focused.parent, SelectionWidget):
|
# if focused.parent and isinstance(focused.parent, SelectionWidget):
|
||||||
await focused.parent.on_button_pressed(Button.Pressed(focused))
|
# await focused.parent.on_button_pressed(Button.Pressed(focused))
|
||||||
else:
|
# else:
|
||||||
await self.on_button_pressed(Button.Pressed(focused))
|
# await self.on_button_pressed(Button.Pressed(focused))
|
||||||
|
|
||||||
# --- Обработчики быстрых клавиш ---
|
# --- Обработчики быстрых клавиш ---
|
||||||
# Для 'q' (Quit) уже есть в BINDINGS срабатывающий метод action_quit, он сработает автоматически (не ясно почему).
|
# Для 'q' (Quit) уже есть в BINDINGS срабатывающий метод action_quit, он сработает автоматически (не ясно почему).
|
||||||
|
Loading…
x
Reference in New Issue
Block a user