diff --git a/Makefile b/Makefile index 94ca286d..1ed64e35 100644 --- a/Makefile +++ b/Makefile @@ -203,14 +203,16 @@ install: ## Open a Web Browser to the installation procedure # Follow ./glances/outputs/static/README.md for more information # =================================================================== +webui webui%: DIR = glances/outputs/static/ + webui: ## Build the Web UI - cd glances/outputs/static/ && npm ci && npm run build + cd $(DIR) && npm ci && npm run build webui-audit: ## Audit the Web UI - cd glances/outputs/static/ && npm audit + cd $(DIR) && npm audit webui-audit-fix: ## Fix audit the Web UI - cd glances/outputs/static/ && npm audit fix && npm ci && npm run build + cd $(DIR) && npm audit fix && npm ci && npm run build # =================================================================== # Packaging diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 96c4e1fe..89a14b65 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -922,15 +922,15 @@ class _GlancesCurses: return None - def set_upper_left_pos(self, plugin_stats): - screen_x = self.term_window.getmaxyx()[1] - screen_y = self.term_window.getmaxyx()[0] + def setup_upper_left_pos(self, plugin_stats): + screen_y, screen_x = self.term_window.getmaxyx() if plugin_stats['align'] == 'right': # Right align (last column) display_x = screen_x - self.get_stats_display_width(plugin_stats) else: display_x = self.column + if plugin_stats['align'] == 'bottom': # Bottom (last line) display_y = screen_y - self.get_stats_display_height(plugin_stats) @@ -939,104 +939,7 @@ class _GlancesCurses: return display_y, display_x - def check_opt_and_add(self, display_optional, display_additional): - def neither_optional_nor_additional(m): - has_optional = not display_optional and m['optional'] - has_additional = not display_additional and m['additional'] - - return any([has_optional, has_additional]) - - return neither_optional_nor_additional - - def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0): - """Display the plugin_stats on the screen. - - :param plugin_stats: - :param display_optional: display the optional stats if True - :param display_additional: display additional stats if True - :param max_y: do not display line > max_y - :param add_space: add x space (line) after the plugin - """ - # Exit if: - # - the plugin_stats message is empty - # - the display tag = False - conditions = [plugin_stats is None, not plugin_stats['msgdict'], not plugin_stats['display']] - if any(conditions): - # Exit - return 0 - - # Set the upper/left position of the message - display_y, display_x = self.set_upper_left_pos(plugin_stats) - - helper = { - 'goto next and ret first col': self.goto_next_and_ret_first_col(display_x), - 'neither opt nor add?': self.check_opt_and_add(display_optional, display_additional), - 'x overbound?': self.x_overbound, - 'y overbound?': self.y_overbound(max_y), - } - - init = {'x': display_x, 'x max': display_x, 'y': display_y} - y, x, x_max = self.display_msg(plugin_stats, init, helper) - - # Compute the next Glances column/line position - self.next_column = max(self.next_column, x_max + self.space_between_column) - self.next_line = max(self.next_line, y + self.space_between_line) - - # Have empty lines after the plugins - self.next_line += add_space - return None - - def y_overbound(self, max_y): - screen_y = self.term_window.getmaxyx()[0] - - return lambda y: y < 0 or (y + 1 > screen_y) or (y > max_y) - - def x_overbound(self, m, x): - screen_x = self.term_window.getmaxyx()[1] - - return x < 0 or not m['splittable'] and (x + len(m['msg']) > screen_x) - - def goto_next_and_ret_first_col(self, display_x): - return lambda y: (y + 1, display_x) - - def display_msg(self, plugin_stats, init, helper): - y, x, x_max = init['y'], init['x'], init['x max'] - for m in plugin_stats['msgdict']: - # New line - try: - if m['msg'].startswith('\n'): - y, x = helper['goto next and ret first col'](y) - continue - except Exception: - # Avoid exception (see issue #1692) - pass - if helper['x overbound?'](m, x) or helper['neither opt nor add?'](m): - continue - if helper['y overbound?'](y): - break - x, x_max = self.display_stats_with_current_size(m, y, x, x_max) - - return y, x, x_max - - def display_stats_with_current_size(self, m, y, x, x_max): - # Is it possible to display the stat with the current screen size - # !!! Crash if not try/except... Why ??? - screen_x = self.term_window.getmaxyx()[1] - try: - self.term_window.addnstr( - y, - x, - m['msg'], - # Do not display outside the screen - screen_x - x, - self.colors_list[m['decoration']], - ) - except Exception: - pass - else: - return self.add_new_colum(m, x, x_max) - - def add_new_colum(self, m, x, x_max): + def get_next_x_and_x_max(self, m, x, x_max): # New column # Python 2: we need to decode to get real screen size because # UTF-8 special tree chars occupy several bytes. @@ -1052,6 +955,94 @@ class _GlancesCurses: return x, x_max + def display_stats_with_current_size(self, m, y, x): + screen_x = self.term_window.getmaxyx()[1] + self.term_window.addnstr( + y, + x, + m['msg'], + # Do not display outside the screen + screen_x - x, + self.colors_list[m['decoration']], + ) + + def display_stats(self, plugin_stats, init, helper): + y, x, x_max = init + for m in plugin_stats['msgdict']: + # New line + try: + if m['msg'].startswith('\n'): + y, x = helper['goto next, add first col'](y, x) + continue + except Exception: + # Avoid exception (see issue #1692) + pass + # Do not display outside the screen + if x < 0: + continue + if helper['x overbound?'](m, x): + continue + if helper['y overbound?'](y): + break + # If display_optional = False do not display optional stats + if helper['display optional?'](m): + continue + # If display_additional = False do not display additional stats + if helper['display additional?'](m): + continue + # Is it possible to display the stat with the current screen size + # !!! Crash if not try/except... Why ??? + try: + self.display_stats_with_current_size(m, y, x) + except Exception: + pass + else: + x, x_max = self.get_next_x_and_x_max(m, x, x_max) + + return y, x, x_max + + def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0): + """Display the plugin_stats on the screen. + + :param plugin_stats: + :param display_optional: display the optional stats if True + :param display_additional: display additional stats if True + :param max_y: do not display line > max_y + :param add_space: add x space (line) after the plugin + """ + # Exit if: + # - the plugin_stats message is empty + # - the display tag = False + if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']: + # Exit + return 0 + + # Get the screen size + screen_y, screen_x = self.term_window.getmaxyx() + + # Set the upper/left position of the message + display_y, display_x = self.setup_upper_left_pos(plugin_stats) + + helper = { + 'goto next, add first col': lambda y, x: (y + 1, display_x), + 'x overbound?': lambda m, x: not m['splittable'] and (x + len(m['msg']) > screen_x), + 'y overbound?': lambda y: y < 0 or (y + 1 > screen_y) or (y > max_y), + 'display optional?': lambda m: not display_optional and m['optional'], + 'display additional?': lambda m: not display_additional and m['additional'], + } + + # Display + init = display_y, display_x, display_x + y, x, x_max = self.display_stats(plugin_stats, init, helper) + + # Compute the next Glances column/line position + self.next_column = max(self.next_column, x_max + self.space_between_column) + self.next_line = max(self.next_line, y + self.space_between_line) + + # Have empty lines after the plugins + self.next_line += add_space + return None + def clear(self): """Erase the content of the screen. The difference is that clear() also calls clearok(). clearok() diff --git a/glances/plugins/alert/__init__.py b/glances/plugins/alert/__init__.py index 9bc26816..b78c215d 100644 --- a/glances/plugins/alert/__init__.py +++ b/glances/plugins/alert/__init__.py @@ -9,6 +9,7 @@ """Alert plugin.""" from datetime import datetime +from functools import reduce from glances.events_list import glances_events @@ -123,58 +124,92 @@ class PluginModel(GlancesPluginModel): # Set the stats to the glances_events self.stats = glances_events.get() + def build_hdr_msg(self, ret): + def cond(elem): + return elem['end'] == -1 and 'global_msg' in elem + + global_message = [elem['global_msg'] for elem in self.stats if cond(elem)] + title = global_message[0] if len(global_message) > 0 else "EVENTS history" + + ret.append(self.curse_add_line(title, "TITLE")) + + return ret + + def add_new_line(self, ret, alert): + ret.append(self.curse_new_line()) + + return ret + + def add_start_time(self, ret, alert): + timezone = datetime.now().astimezone().tzinfo + alert_dt = datetime.fromtimestamp(alert['begin'], tz=timezone) + ret.append(self.curse_add_line(alert_dt.strftime("%Y-%m-%d %H:%M:%S(%z)"))) + + return ret + + def add_duration(self, ret, alert): + if alert['end'] > 0: + # If finished display duration + end = datetime.fromtimestamp(alert['end']) + begin = datetime.fromtimestamp(alert['begin']) + msg = f' ({end - begin})' + else: + msg = ' (ongoing)' + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_add_line(" - ")) + + return ret + + def add_infos(self, ret, alert): + if alert['end'] > 0: + # If finished do not display status + msg = '{} on {}'.format(alert['state'], alert['type']) + ret.append(self.curse_add_line(msg)) + else: + msg = str(alert['type']) + ret.append(self.curse_add_line(msg, decoration=alert['state'])) + + return ret + + def add_min_mean_max(self, ret, alert): + if self.approx_equal(alert['min'], alert['max'], tolerance=0.1): + msg = ' ({:.1f})'.format(alert['avg']) + else: + msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert['min'], alert['avg'], alert['max']) + ret.append(self.curse_add_line(msg)) + + return ret + + def add_top_proc(self, ret, alert): + top_process = ', '.join(alert['top']) + if top_process != '': + msg = f': {top_process}' + ret.append(self.curse_add_line(msg)) + + return ret + + def loop_over_alert(self, init, alert): + steps = [ + self.add_new_line, + self.add_start_time, + self.add_duration, + self.add_infos, + self.add_min_mean_max, + self.add_top_proc, + ] + + return reduce(lambda ret, step: step(ret, alert), steps, init) + def msg_curse(self, args=None, max_width=None): """Return the dict to display in the curse interface.""" # Init the return message - ret = [] + init = [] # Only process if display plugin enable... if not self.stats or self.is_disabled(): - return ret + return init - # Build the string message - # Header with the global message - global_message = [e['global_msg'] for e in self.stats if (e['end'] == -1 and 'global_msg' in e)] - if len(global_message) > 0: - ret.append(self.curse_add_line(global_message[0], "TITLE")) - else: - ret.append(self.curse_add_line("EVENTS history", "TITLE")) - # Loop over alerts - for alert in self.stats: - # New line - ret.append(self.curse_new_line()) - # Start - alert_dt = datetime.fromtimestamp(alert['begin'], tz=datetime.now().astimezone().tzinfo) - ret.append(self.curse_add_line(alert_dt.strftime("%Y-%m-%d %H:%M:%S(%z)"))) - # Duration - if alert['end'] > 0: - # If finished display duration - msg = ' ({})'.format(datetime.fromtimestamp(alert['end']) - datetime.fromtimestamp(alert['begin'])) - else: - msg = ' (ongoing)' - ret.append(self.curse_add_line(msg)) - ret.append(self.curse_add_line(" - ")) - # Infos - if alert['end'] > 0: - # If finished do not display status - msg = '{} on {}'.format(alert['state'], alert['type']) - ret.append(self.curse_add_line(msg)) - else: - msg = str(alert['type']) - ret.append(self.curse_add_line(msg, decoration=alert['state'])) - # Min / Mean / Max - if self.approx_equal(alert['min'], alert['max'], tolerance=0.1): - msg = ' ({:.1f})'.format(alert['avg']) - else: - msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert['min'], alert['avg'], alert['max']) - ret.append(self.curse_add_line(msg)) - # Top processes - top_process = ', '.join(alert['top']) - if top_process != '': - msg = f': {top_process}' - ret.append(self.curse_add_line(msg)) - - return ret + return reduce(self.loop_over_alert, self.stats, self.build_hdr_msg(init)) def approx_equal(self, a, b, tolerance=0.0): """Compare a with b using the tolerance (if numerical)."""