diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 99b0b230a9..7827bdaf2e 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -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) diff --git a/pype/modules/logging/gui/lib.py b/pype/modules/logging/gui/lib.py deleted file mode 100644 index 85782e071e..0000000000 --- a/pype/modules/logging/gui/lib.py +++ /dev/null @@ -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) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index ce1fa236a9..b739739b6f 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -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 diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index cf20066397..f567cae674 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -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 = "{message}" if level == "debug":