Some checks failed
Windows project source guards / test (push) Failing after 45s
- AdaptiveButtonRow: следит за шириной ряда (с учётом переполнения прокручиваемой страницы) и сворачивает кнопки со значком с конца ряда в квадратные значки, текст — в подсказку; появится место — возвращает. - fluent_fixes: у PushButton в режиме «только значок» значок рисуется по центру (оригинал сдвигал его под подпись — значок съезжал и обрезался). - Применено: строка сервера на панели (вместо кнопок-близнецов), панель инструментов подписок, логов и конфигов; их кнопкам добавлены значки. Проверено рендером на win10: на 860 px «Сбросить к шаблону» сворачивается в значок, остальные ряды помещаются с подписями.
131 lines
5.3 KiB
Python
131 lines
5.3 KiB
Python
from __future__ import annotations
|
||
|
||
from PyQt6.QtCore import QTimer, pyqtSignal
|
||
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
|
||
from qfluentwidgets import BodyLabel, PlainTextEdit, PrimaryPushButton, PushButton, SearchLineEdit, SubtitleLabel
|
||
from qfluentwidgets import FluentIcon as FIF
|
||
from .adaptive_buttons import AdaptiveButtonRow
|
||
|
||
|
||
class LogsPage(QWidget):
|
||
clear_requested = pyqtSignal()
|
||
export_diag_requested = pyqtSignal()
|
||
|
||
def __init__(self, parent: QWidget | None = None):
|
||
super().__init__(parent)
|
||
self.setObjectName("logs")
|
||
self._lines: list[str] = []
|
||
self._pending_lines: list[str] = []
|
||
self._error_lines: list[str] = []
|
||
self._error_records: tuple = ()
|
||
self._error_lines_stale = False
|
||
self._full_refresh_needed = False
|
||
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(24, 20, 24, 20)
|
||
root.setSpacing(12)
|
||
|
||
root.addWidget(SubtitleLabel("Логи и диагностика", self))
|
||
|
||
toolbar = QHBoxLayout()
|
||
self.search = SearchLineEdit(self)
|
||
self.search.setPlaceholderText("Фильтр логов")
|
||
self.clear_btn = PushButton(FIF.BROOM, "Очистить", self)
|
||
self.export_btn = PrimaryPushButton(FIF.SHARE, "Экспорт диагностики", self)
|
||
|
||
toolbar.addWidget(self.search, 1)
|
||
self.errors_btn = PushButton(FIF.INFO, "Ошибки ядер", self)
|
||
self.errors_btn.setCheckable(True)
|
||
toolbar.addWidget(self.errors_btn)
|
||
toolbar.addWidget(self.clear_btn)
|
||
toolbar.addWidget(self.export_btn)
|
||
root.addLayout(toolbar)
|
||
# Не хватает места — подписи уходят в подсказки, остаются значки.
|
||
self._toolbar_fit = AdaptiveButtonRow(toolbar, [self.export_btn, self.errors_btn, self.clear_btn])
|
||
|
||
root.addWidget(BodyLabel("Логи работы", self))
|
||
self.log_edit = PlainTextEdit(self)
|
||
self.log_edit.setReadOnly(True)
|
||
self.log_edit.document().setMaximumBlockCount(2000)
|
||
root.addWidget(self.log_edit, 1)
|
||
|
||
self._refresh_timer = QTimer(self)
|
||
self._refresh_timer.setSingleShot(True)
|
||
self._refresh_timer.setInterval(200)
|
||
self._refresh_timer.timeout.connect(self._flush_updates)
|
||
|
||
self.search.textChanged.connect(self._schedule_full_refresh)
|
||
self.errors_btn.toggled.connect(self._schedule_full_refresh)
|
||
self.clear_btn.clicked.connect(self.clear_requested)
|
||
self.export_btn.clicked.connect(self.export_diag_requested)
|
||
|
||
def append_line(self, line: str) -> None:
|
||
self._lines.append(line)
|
||
if len(self._lines) > 5000:
|
||
self._lines = self._lines[-5000:]
|
||
self._pending_lines.append(line)
|
||
if self.search.text().strip():
|
||
self._full_refresh_needed = True
|
||
self._schedule_flush()
|
||
|
||
def set_lines(self, lines: list[str]) -> None:
|
||
self._lines = list(lines)
|
||
self._pending_lines.clear()
|
||
self._schedule_full_refresh()
|
||
|
||
def set_error_records(self, records) -> None:
|
||
# Форматируем лениво — только когда открыта вкладка «Ошибки ядер».
|
||
self._error_records = tuple(records)
|
||
self._error_lines_stale = True
|
||
if self.errors_btn.isChecked():
|
||
self._schedule_full_refresh()
|
||
|
||
def _formatted_error_lines(self) -> list[str]:
|
||
if getattr(self, "_error_lines_stale", False):
|
||
self._error_lines = [
|
||
f"[{r.failure.component}][{r.failure.stage}] {r.failure.message}\n"
|
||
f"{r.failure.code}; событий: {r.occurrences}"
|
||
for r in self._error_records
|
||
]
|
||
self._error_lines_stale = False
|
||
return self._error_lines
|
||
|
||
def clear_view(self) -> None:
|
||
self._lines = []
|
||
self._pending_lines.clear()
|
||
self._full_refresh_needed = False
|
||
self._refresh_timer.stop()
|
||
self.log_edit.clear()
|
||
|
||
def _schedule_flush(self) -> None:
|
||
if not self._refresh_timer.isActive():
|
||
self._refresh_timer.start()
|
||
|
||
def _schedule_full_refresh(self) -> None:
|
||
self._full_refresh_needed = True
|
||
self._schedule_flush()
|
||
|
||
def _flush_updates(self) -> None:
|
||
query = self.search.text().strip().lower()
|
||
if self.errors_btn.isChecked():
|
||
self.log_edit.document().setMaximumBlockCount(0)
|
||
self.log_edit.setPlainText("\n\n".join(
|
||
line for line in self._formatted_error_lines() if not query or query in line.lower()
|
||
))
|
||
self._pending_lines.clear()
|
||
self._full_refresh_needed = False
|
||
return
|
||
self.log_edit.document().setMaximumBlockCount(2000)
|
||
if self._full_refresh_needed or query:
|
||
if not query:
|
||
data = self._lines
|
||
else:
|
||
data = [line for line in self._lines if query in line.lower()]
|
||
self.log_edit.setPlainText("\n".join(data[-2000:]))
|
||
else:
|
||
if self._pending_lines:
|
||
self.log_edit.appendPlainText("\n".join(self._pending_lines))
|
||
self._pending_lines.clear()
|
||
self._full_refresh_needed = False
|
||
vbar = self.log_edit.verticalScrollBar()
|
||
vbar.setValue(vbar.maximum())
|