logging gui has working filters and sorting with better widgets size on open

This commit is contained in:
iLLiCiTiT 2020-07-26 01:21:20 +02:00
parent 320ffd2a3e
commit 0c56ed576d
4 changed files with 106 additions and 256 deletions

View file

@ -8,9 +8,9 @@ class LogsWindow(QtWidgets.QWidget):
super(LogsWindow, self).__init__(parent)
self.setStyleSheet(style.load_stylesheet())
self.resize(1200, 800)
logs_widget = LogsWidget(parent=self)
self.resize(1400, 800)
log_detail = OutputWidget(parent=self)
logs_widget = LogsWidget(log_detail, parent=self)
main_layout = QtWidgets.QHBoxLayout()
@ -18,8 +18,6 @@ class LogsWindow(QtWidgets.QWidget):
log_splitter.setOrientation(QtCore.Qt.Horizontal)
log_splitter.addWidget(logs_widget)
log_splitter.addWidget(log_detail)
log_splitter.setStretchFactor(0, 65)
log_splitter.setStretchFactor(1, 35)
main_layout.addWidget(log_splitter)
@ -33,5 +31,5 @@ class LogsWindow(QtWidgets.QWidget):
def on_selection_changed(self):
index = self.logs_widget.selected_log()
node = index.data(self.logs_widget.model.NodeRole)
self.log_detail.set_detail(node)
logs = index.data(self.logs_widget.model.ROLE_LOGS)
self.log_detail.set_detail(logs)

View file

@ -1,94 +0,0 @@
import contextlib
from Qt import QtCore
def _iter_model_rows(
model, column, include_root=False
):
"""Iterate over all row indices in a model"""
indices = [QtCore.QModelIndex()] # start iteration at root
for index in indices:
# Add children to the iterations
child_rows = model.rowCount(index)
for child_row in range(child_rows):
child_index = model.index(child_row, column, index)
indices.append(child_index)
if not include_root and not index.isValid():
continue
yield index
@contextlib.contextmanager
def preserve_states(
tree_view, column=0, role=None,
preserve_expanded=True, preserve_selection=True,
expanded_role=QtCore.Qt.DisplayRole, selection_role=QtCore.Qt.DisplayRole
):
"""Preserves row selection in QTreeView by column's data role.
This function is created to maintain the selection status of
the model items. When refresh is triggered the items which are expanded
will stay expanded and vise versa.
tree_view (QWidgets.QTreeView): the tree view nested in the application
column (int): the column to retrieve the data from
role (int): the role which dictates what will be returned
Returns:
None
"""
# When `role` is set then override both expanded and selection roles
if role:
expanded_role = role
selection_role = role
model = tree_view.model()
selection_model = tree_view.selectionModel()
flags = selection_model.Select | selection_model.Rows
expanded = set()
if preserve_expanded:
for index in _iter_model_rows(
model, column=column, include_root=False
):
if tree_view.isExpanded(index):
value = index.data(expanded_role)
expanded.add(value)
selected = None
if preserve_selection:
selected_rows = selection_model.selectedRows()
if selected_rows:
selected = set(row.data(selection_role) for row in selected_rows)
try:
yield
finally:
if expanded:
for index in _iter_model_rows(
model, column=0, include_root=False
):
value = index.data(expanded_role)
is_expanded = value in expanded
# skip if new index was created meanwhile
if is_expanded is None:
continue
tree_view.setExpanded(index, is_expanded)
if selected:
# Go through all indices, select the ones with similar data
for index in _iter_model_rows(
model, column=column, include_root=False
):
value = index.data(selection_role)
state = value in selected
if state:
tree_view.scrollTo(index) # Ensure item is visible
selection_model.select(index, flags)

View file

@ -1,21 +1,20 @@
import collections
from Qt import QtCore
from Qt import QtCore, QtGui
from pype.api import Logger
from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME
log = Logger().get_logger("LogModel", "LoggingModule")
class LogModel(QtCore.QAbstractItemModel):
COLUMNS = [
class LogModel(QtGui.QStandardItemModel):
COLUMNS = (
"process_name",
"hostname",
"hostip",
"username",
"system_name",
"started"
]
)
colums_mapping = {
"process_name": "Process Name",
"process_id": "Process Id",
@ -25,30 +24,52 @@ class LogModel(QtCore.QAbstractItemModel):
"system_name": "System name",
"started": "Started at"
}
process_keys = [
process_keys = (
"process_id", "hostname", "hostip",
"username", "system_name", "process_name"
]
log_keys = [
)
log_keys = (
"timestamp", "level", "thread", "threadName", "message", "loggerName",
"fileName", "module", "method", "lineNumber"
]
)
default_value = "- Not set -"
NodeRole = QtCore.Qt.UserRole + 1
ROLE_LOGS = QtCore.Qt.UserRole + 2
def __init__(self, parent=None):
super(LogModel, self).__init__(parent)
self._root_node = Node()
self.log_by_process = None
self.dbcon = None
# Crash if connection is not possible to skip this module
database = _bootstrap_mongo_log()
if LOG_COLLECTION_NAME in database.list_collection_names():
self.dbcon = database[LOG_COLLECTION_NAME]
def add_log(self, log):
node = Node(log)
self._root_node.add_child(node)
def headerData(self, section, orientation, role):
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
if section < len(self.COLUMNS):
key = self.COLUMNS[section]
return self.colums_mapping.get(key, key)
super(LogModel, self).headerData(section, orientation, role)
def add_process_logs(self, process_logs):
items = []
first_item = True
for key in self.COLUMNS:
display_value = str(process_logs[key])
item = QtGui.QStandardItem(display_value)
if first_item:
first_item = False
item.setData(process_logs["_logs"], self.ROLE_LOGS)
items.append(item)
self.appendRow(items)
def refresh(self):
self.log_by_process = collections.defaultdict(list)
@ -65,16 +86,13 @@ class LogModel(QtCore.QAbstractItemModel):
continue
if process_id not in self.process_info:
proc_dict = {}
proc_dict = {"_logs": []}
for key in self.process_keys:
proc_dict[key] = (
item.get(key) or self.default_value
)
self.process_info[process_id] = proc_dict
if "_logs" not in self.process_info[process_id]:
self.process_info[process_id]["_logs"] = []
log_item = {}
for key in self.log_keys:
log_item[key] = item.get(key) or self.default_value
@ -89,114 +107,29 @@ class LogModel(QtCore.QAbstractItemModel):
item["_logs"], key=lambda item: item["timestamp"]
)
item["started"] = item["_logs"][0]["timestamp"]
self.add_log(item)
self.add_process_logs(item)
self.endResetModel()
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
node = index.internalPointer()
column = index.column()
class LogsFilterProxy(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(LogsFilterProxy, self).__init__(*args, **kwargs)
self.col_usernames = None
self.filter_usernames = set()
key = self.COLUMNS[column]
if key == "started":
return str(node.get(key, None))
return node.get(key, None)
def update_users_filter(self, users):
self.filter_usernames = set()
for user in users or tuple():
self.filter_usernames.add(user)
self.invalidateFilter()
if role == self.NodeRole:
return index.internalPointer()
def index(self, row, column, parent):
"""Return index for row/column under parent"""
if not parent.isValid():
parent_node = self._root_node
else:
parent_node = parent.internalPointer()
child_item = parent_node.child(row)
if child_item:
return self.createIndex(row, column, child_item)
return QtCore.QModelIndex()
def rowCount(self, parent):
node = self._root_node
if parent.isValid():
node = parent.internalPointer()
return node.childCount()
def columnCount(self, parent):
return len(self.COLUMNS)
def parent(self, index):
return QtCore.QModelIndex()
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section < len(self.COLUMNS):
key = self.COLUMNS[section]
return self.colums_mapping.get(key, key)
super(LogModel, self).headerData(section, orientation, role)
def flags(self, index):
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
def clear(self):
self.beginResetModel()
self._root_node = Node()
self.endResetModel()
class Node(dict):
"""A node that can be represented in a tree view.
The node can store data just like a dictionary.
>>> data = {"name": "John", "score": 10}
>>> node = Node(data)
>>> assert node["name"] == "John"
"""
def __init__(self, data=None):
super(Node, self).__init__()
self._children = list()
self._parent = None
if data is not None:
assert isinstance(data, dict)
self.update(data)
def childCount(self):
return len(self._children)
def child(self, row):
if row >= len(self._children):
log.warning("Invalid row as child: {0}".format(row))
return
return self._children[row]
def children(self):
return self._children
def parent(self):
return self._parent
def row(self):
"""
Returns:
int: Index of this node under parent"""
if self._parent is not None:
siblings = self.parent().children()
return siblings.index(self)
def add_child(self, child):
"""Add a child to this node"""
child._parent = self
self._children.append(child)
def filterAcceptsRow(self, source_row, source_parent):
if self.col_usernames is not None:
index = self.sourceModel().index(
source_row, self.col_usernames, source_parent
)
user = index.data(QtCore.Qt.DisplayRole)
if user not in self.filter_usernames:
return False
return True

View file

@ -1,6 +1,6 @@
from Qt import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import QVariant
from .models import LogModel
from .models import LogModel, LogsFilterProxy
class SearchComboBox(QtWidgets.QComboBox):
@ -193,54 +193,37 @@ class LogsWidget(QtWidgets.QWidget):
active_changed = QtCore.Signal()
def __init__(self, parent=None):
def __init__(self, detail_widget, parent=None):
super(LogsWidget, self).__init__(parent=parent)
model = LogModel()
proxy_model = LogsFilterProxy()
proxy_model.setSourceModel(model)
proxy_model.col_usernames = model.COLUMNS.index("username")
filter_layout = QtWidgets.QHBoxLayout()
# user_filter = SearchComboBox(self, "Users")
user_filter = CustomCombo("Users", self)
users = model.dbcon.distinct("user")
users = model.dbcon.distinct("username")
user_filter.populate(users)
user_filter.selection_changed.connect(self.user_changed)
proxy_model.update_users_filter(users)
level_filter = CustomCombo("Levels", self)
# levels = [(level, True) for level in model.dbcon.distinct("level")]
levels = model.dbcon.distinct("level")
level_filter.addItems(levels)
level_filter.selection_changed.connect(self.level_changed)
date_from_label = QtWidgets.QLabel("From:")
date_filter_from = QtWidgets.QDateTimeEdit()
date_from_layout = QtWidgets.QVBoxLayout()
date_from_layout.addWidget(date_from_label)
date_from_layout.addWidget(date_filter_from)
# now = datetime.datetime.now()
# QtCore.QDateTime(
# now.year,
# now.month,
# now.day,
# now.hour,
# now.minute,
# second=0,
# msec=0,
# timeSpec=0
# )
date_to_label = QtWidgets.QLabel("To:")
date_filter_to = QtWidgets.QDateTimeEdit()
date_to_layout = QtWidgets.QVBoxLayout()
date_to_layout.addWidget(date_to_label)
date_to_layout.addWidget(date_filter_to)
detail_widget.update_level_filter(levels)
filter_layout.addWidget(user_filter)
filter_layout.addWidget(level_filter)
filter_layout.addLayout(date_from_layout)
filter_layout.addLayout(date_to_layout)
spacer = QtWidgets.QWidget()
filter_layout.addWidget(spacer, 1)
view = QtWidgets.QTreeView(self)
view.setAllColumnsShowFocus(True)
@ -250,6 +233,8 @@ class LogsWidget(QtWidgets.QWidget):
layout.addLayout(filter_layout)
layout.addWidget(view)
view.setModel(proxy_model)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setSortingEnabled(True)
view.sortByColumn(
@ -257,24 +242,36 @@ class LogsWidget(QtWidgets.QWidget):
QtCore.Qt.AscendingOrder
)
view.setModel(model)
view.pressed.connect(self._on_activated)
# prepare
model.refresh()
# Store to memory
self.model = model
self.proxy_model = proxy_model
self.view = view
self.user_filter = user_filter
self.level_filter = level_filter
self.detail_widget = detail_widget
def _on_activated(self, *args, **kwargs):
self.active_changed.emit()
def user_changed(self):
checked_values = set()
for action in self.user_filter.items():
print(action)
if action.isChecked():
checked_values.add(action.text())
self.proxy_model.update_users_filter(checked_values)
def level_changed(self):
checked_values = set()
for action in self.level_filter.items():
if action.isChecked():
checked_values.add(action.text())
self.detail_widget.update_level_filter(checked_values)
def on_context_menu(self, point):
# TODO will be any actions? it's ready
@ -309,13 +306,29 @@ class OutputWidget(QtWidgets.QWidget):
self.setLayout(layout)
self.output_text = output_text
self.las_logs = None
self.filter_levels = set()
def update_level_filter(self, levels):
self.filter_levels = set()
for level in levels or tuple():
self.filter_levels.add(level.lower())
self.set_detail(self.las_logs)
def add_line(self, line):
self.output_text.append(line)
def set_detail(self, node):
def set_detail(self, logs):
self.las_logs = logs
self.output_text.clear()
for log in node["_logs"]:
if not logs:
return
for log in logs:
level = log["level"].lower()
if level not in self.filter_levels:
continue
line_f = "<font color=\"White\">{message}"
if level == "debug":