mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
logging gui has working filters and sorting with better widgets size on open
This commit is contained in:
parent
320ffd2a3e
commit
0c56ed576d
4 changed files with 106 additions and 256 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue