## Summary What changed and why? ## Change Type - [ ] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Duy Le Huu <duylh19@fpt.com> Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -39,8 +39,9 @@ _UNBOUNDED_PAGE_SIZE = 100_000
|
||||
# day-sharded JSONL — unbounded start/end means EVERY day file ever written
|
||||
# gets re-read and re-parsed on EVERY tick, which is what actually made
|
||||
# Monitoring "gây nặng khi log lớn" (see DF-006): the slowness was never in
|
||||
# rendering (EventTable already caps display at 300 rows — see
|
||||
# shared/event_table.py::_MAX_ROWS), it was this repeated full-history read.
|
||||
# rendering (EventTable paginates client-side, 5-100 rows/page — see
|
||||
# shared/event_table.py::_DEFAULT_PAGE_SIZE), it was this repeated full-history
|
||||
# read.
|
||||
# 30 days is a live-monitoring window, not a hard retention limit — nothing
|
||||
# is deleted, older days are simply not re-read on every 3s tick.
|
||||
_LOG_WINDOW_DAYS = 30
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, QRect, QSize, Qt
|
||||
from PySide6.QtCore import QEvent, QObject, QRect, QSize, Qt, Signal
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
from PySide6.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget
|
||||
|
||||
@@ -17,8 +17,8 @@ from ....ui.icons import DOT_GREEN, DOT_RED, icon
|
||||
from .badges import action_label
|
||||
from .formatters import agent_avatar_icon, fmt_event_time
|
||||
|
||||
_MAX_ROWS = 300
|
||||
PAGE_SIZE_OPTIONS = (50, 100, 300, 500, 1000)
|
||||
_DEFAULT_PAGE_SIZE = 20
|
||||
PAGE_SIZE_OPTIONS = (5, 10, 20, 50, 100)
|
||||
|
||||
|
||||
class _TimeItem(QTableWidgetItem):
|
||||
@@ -63,6 +63,12 @@ class EventTable(QTableWidget):
|
||||
"secret_in_output": "warning",
|
||||
}
|
||||
|
||||
# Emitted whenever the rendered page changes (new data, page-size change,
|
||||
# or prev/next navigation) — args are (current_page, page_count), both
|
||||
# 1-based-friendly in that current_page is 0-indexed but page_count is a
|
||||
# plain count. filter_scaffold.py's pager label/buttons listen to this.
|
||||
page_changed = Signal(int, int)
|
||||
|
||||
def __init__(self, show_result: bool = True):
|
||||
# Security Events drops the result column entirely (see _ACTION_TINTS).
|
||||
"""Bảng sự kiện dùng chung của các tab Giám sát.
|
||||
@@ -71,8 +77,10 @@ class EventTable(QTableWidget):
|
||||
là thất bại nên cột ấy chỉ tốn chỗ.
|
||||
"""
|
||||
self._show_result = show_result
|
||||
self._page_size = _MAX_ROWS
|
||||
self._page_size = _DEFAULT_PAGE_SIZE
|
||||
self._current_page = 0
|
||||
self._last_events: List[dict] = []
|
||||
self._sorted_events: List[dict] = []
|
||||
super().__init__(0, 7 if show_result else 6)
|
||||
self.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||
self.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
@@ -105,21 +113,56 @@ class EventTable(QTableWidget):
|
||||
"""Số dòng đang hiển thị mỗi trang."""
|
||||
return self._page_size
|
||||
|
||||
def page_count(self) -> int:
|
||||
"""Tổng số trang với dữ liệu và số dòng/trang hiện tại (tối thiểu 1)."""
|
||||
if not self._sorted_events:
|
||||
return 1
|
||||
return -(-len(self._sorted_events) // self._page_size) # ceil div
|
||||
|
||||
def current_page(self) -> int:
|
||||
"""Trang đang hiển thị, đánh số từ 0."""
|
||||
return self._current_page
|
||||
|
||||
def go_to_page(self, page: int) -> None:
|
||||
"""Nhảy tới một trang cụ thể (đánh số từ 0), tự kẹp trong khoảng hợp lệ."""
|
||||
self._current_page = page
|
||||
self._render_current_page()
|
||||
|
||||
def next_page(self) -> None:
|
||||
"""Sang trang kế — không làm gì nếu đã ở trang cuối."""
|
||||
self.go_to_page(self._current_page + 1)
|
||||
|
||||
def prev_page(self) -> None:
|
||||
"""Về trang trước — không làm gì nếu đã ở trang đầu."""
|
||||
self.go_to_page(self._current_page - 1)
|
||||
|
||||
def set_page_size(self, n: int) -> None:
|
||||
"""Đổi số dòng hiển thị mỗi trang rồi vẽ lại với dữ liệu đã có sẵn
|
||||
(không cần refresh lại từ nguồn — set_events() đã lưu lại lần đổ gần nhất)."""
|
||||
"""Đổi số dòng hiển thị mỗi trang, quay về trang đầu, rồi vẽ lại với dữ
|
||||
liệu đã có sẵn (không cần refresh lại từ nguồn)."""
|
||||
self._page_size = n
|
||||
self.set_events(self._last_events)
|
||||
self._current_page = 0
|
||||
self._render_current_page()
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng: mới nhất lên đầu, cắt ở ``self._page_size``
|
||||
(đổi được qua ``set_page_size`` — control "Số dòng/trang" ở filter_scaffold.py).
|
||||
"""Đổ danh sách sự kiện vào bảng: mới nhất lên đầu, chia trang theo
|
||||
``self._page_size`` — xem qua trang khác bằng ``next_page``/``prev_page``
|
||||
(nút tiến/lùi ở filter_scaffold.py), không còn bị cắt bỏ vĩnh viễn như
|
||||
trước (DF-006)."""
|
||||
self._last_events = events
|
||||
self._sorted_events = sorted(events, key=lambda e: e.get("ts", ""), reverse=True)
|
||||
self._current_page = 0
|
||||
self._render_current_page()
|
||||
|
||||
def _render_current_page(self) -> None:
|
||||
"""Vẽ đúng một trang (theo ``self._current_page``/``self._page_size``)
|
||||
từ ``self._sorted_events`` đã sắp sẵn.
|
||||
|
||||
Tắt sắp xếp trong lúc đổ dữ liệu — để bật, Qt sắp lại sau mỗi dòng và việc
|
||||
nạp chậm đi theo bậc hai.
|
||||
"""
|
||||
self._last_events = events
|
||||
events = sorted(events, key=lambda e: e.get("ts", ""), reverse=True)[:self._page_size]
|
||||
self._current_page = max(0, min(self._current_page, self.page_count() - 1))
|
||||
start = self._current_page * self._page_size
|
||||
events = self._sorted_events[start:start + self._page_size]
|
||||
self.setSortingEnabled(False)
|
||||
self.setRowCount(len(events))
|
||||
for row, ev in enumerate(events):
|
||||
@@ -164,6 +207,7 @@ class EventTable(QTableWidget):
|
||||
self.setItem(row, col, item)
|
||||
self.setSortingEnabled(True)
|
||||
self.apply_filter(getattr(self, "_filter_needle", ""))
|
||||
self.page_changed.emit(self._current_page, self.page_count())
|
||||
|
||||
def apply_filter(self, needle: str) -> None:
|
||||
"""Ẩn/hiện dòng theo từ khoá tìm kiếm (không phân biệt hoa thường)."""
|
||||
|
||||
@@ -95,20 +95,52 @@ def build_filter_scaffold(
|
||||
if with_page_size and isinstance(table, EventTable):
|
||||
# DF-006: the item-per-page count was never surfaced anywhere in
|
||||
# the UI (design called for it) — EventTable already trims to a
|
||||
# page size internally (default 300), this just makes that
|
||||
# number visible AND user-choosable instead of a fixed constant.
|
||||
# page size internally, this just makes that number visible AND
|
||||
# user-choosable instead of a fixed constant.
|
||||
page_size_lbl = QLabel(tr("monitoring.page_size_label"))
|
||||
page_size_combo = QComboBox()
|
||||
for n in PAGE_SIZE_OPTIONS:
|
||||
page_size_combo.addItem(str(n), n)
|
||||
current = table.page_size()
|
||||
page_size_combo.setCurrentIndex(
|
||||
PAGE_SIZE_OPTIONS.index(current) if current in PAGE_SIZE_OPTIONS else 2)
|
||||
PAGE_SIZE_OPTIONS.index(current) if current in PAGE_SIZE_OPTIONS else 0)
|
||||
page_size_combo.currentIndexChanged.connect(
|
||||
lambda i: table.set_page_size(page_size_combo.itemData(i)))
|
||||
row.addWidget(page_size_lbl)
|
||||
row.addWidget(page_size_combo)
|
||||
parts.update(page_size_label=page_size_lbl, page_size_combo=page_size_combo)
|
||||
|
||||
# DF-006 follow-up: trimming to a page size alone silently dropped
|
||||
# every row past it with no way back to see them — prev/next
|
||||
# buttons plus a "trang X/Y" indicator make the rest reachable.
|
||||
page_prev_btn = QPushButton()
|
||||
page_prev_btn.setIcon(icon("chevron-left"))
|
||||
page_prev_btn.setCursor(Qt.PointingHandCursor)
|
||||
bind_tip(page_prev_btn, "monitoring.page_prev")
|
||||
page_next_btn = QPushButton()
|
||||
page_next_btn.setIcon(icon("chevron-right"))
|
||||
page_next_btn.setCursor(Qt.PointingHandCursor)
|
||||
bind_tip(page_next_btn, "monitoring.page_next")
|
||||
page_indicator_lbl = QLabel()
|
||||
|
||||
def _refresh_pager(cur: int = None, total: int = None) -> None:
|
||||
if cur is None or total is None:
|
||||
cur, total = table.current_page(), table.page_count()
|
||||
page_indicator_lbl.setText(tr("monitoring.page_indicator", page=cur + 1, total=total))
|
||||
page_prev_btn.setEnabled(cur > 0)
|
||||
page_next_btn.setEnabled(cur < total - 1)
|
||||
|
||||
page_prev_btn.clicked.connect(table.prev_page)
|
||||
page_next_btn.clicked.connect(table.next_page)
|
||||
table.page_changed.connect(_refresh_pager)
|
||||
_refresh_pager()
|
||||
|
||||
row.addWidget(page_prev_btn)
|
||||
row.addWidget(page_indicator_lbl)
|
||||
row.addWidget(page_next_btn)
|
||||
parts.update(
|
||||
page_size_label=page_size_lbl, page_size_combo=page_size_combo,
|
||||
page_prev_btn=page_prev_btn, page_next_btn=page_next_btn,
|
||||
page_indicator_label=page_indicator_lbl, page_pager_refresh=_refresh_pager)
|
||||
lay.addLayout(row)
|
||||
parts.update(filter_edit=search, ai_filter_btn=ai_btn)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class ActionLogsTab(QWidget):
|
||||
self.ai_filter_btn = parts["ai_filter_btn"]
|
||||
self.detail_panel = parts["detail_panel"]
|
||||
self.page_size_label = parts["page_size_label"]
|
||||
self.page_pager_refresh = parts["page_pager_refresh"]
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng."""
|
||||
@@ -47,6 +48,7 @@ class ActionLogsTab(QWidget):
|
||||
self.title_lbl.setText(tr(self.title_key))
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
self.page_size_label.setText(tr("monitoring.page_size_label"))
|
||||
self.page_pager_refresh()
|
||||
|
||||
def _start_ai_filter(self, search: QLineEdit, ai_btn: QPushButton) -> None:
|
||||
"""Nhờ AI dịch câu tìm kiếm tự nhiên thành từ khoá lọc."""
|
||||
|
||||
@@ -34,6 +34,7 @@ class McpTab(QWidget):
|
||||
self.ai_filter_btn = parts["ai_filter_btn"]
|
||||
self.detail_panel = parts["detail_panel"]
|
||||
self.page_size_label = parts["page_size_label"]
|
||||
self.page_pager_refresh = parts["page_pager_refresh"]
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng."""
|
||||
@@ -47,6 +48,7 @@ class McpTab(QWidget):
|
||||
self.title_lbl.setText(tr(self.title_key))
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
self.page_size_label.setText(tr("monitoring.page_size_label"))
|
||||
self.page_pager_refresh()
|
||||
|
||||
def _start_ai_filter(self, search: QLineEdit, ai_btn: QPushButton) -> None:
|
||||
"""Nhờ AI dịch câu tìm kiếm tự nhiên thành từ khoá lọc."""
|
||||
|
||||
@@ -40,6 +40,7 @@ class SecurityEventsTab(QWidget):
|
||||
self.ai_filter_btn = parts["ai_filter_btn"]
|
||||
self.detail_panel = parts["detail_panel"]
|
||||
self.page_size_label = parts["page_size_label"]
|
||||
self.page_pager_refresh = parts["page_pager_refresh"]
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng."""
|
||||
@@ -53,6 +54,7 @@ class SecurityEventsTab(QWidget):
|
||||
self.title_lbl.setText(tr(self.title_key))
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
self.page_size_label.setText(tr("monitoring.page_size_label"))
|
||||
self.page_pager_refresh()
|
||||
|
||||
def _start_ai_filter(self, search: QLineEdit, ai_btn: QPushButton) -> None:
|
||||
"""Nhờ AI dịch câu tìm kiếm tự nhiên thành từ khoá lọc."""
|
||||
|
||||
Reference in New Issue
Block a user