From 57e0fec82bb820711b2051a6db23005e51601f38 Mon Sep 17 00:00:00 2001 From: Charles Thompson Date: Mon, 2 Dec 2024 00:17:15 -0500 Subject: [PATCH] Add maximize panel feature --- dolphie/DataTypes.py | 22 +++--- dolphie/Dolphie.tcss | 127 +++++++++++++++++++++++++----- dolphie/Modules/CommandManager.py | 24 +++++- dolphie/Modules/TabManager.py | 39 +++++---- dolphie/Widgets/modal.py | 42 ++++++---- dolphie/Widgets/tab_setup.py | 71 ----------------- dolphie/app.py | 41 ++++++++++ pyproject.toml | 2 +- 8 files changed, 230 insertions(+), 138 deletions(-) diff --git a/dolphie/DataTypes.py b/dolphie/DataTypes.py index df75789..dadab38 100644 --- a/dolphie/DataTypes.py +++ b/dolphie/DataTypes.py @@ -66,21 +66,22 @@ def get_sorted_replicas(self) -> List[Replica]: @dataclass class Panel: name: str + display_name: str visible: bool = False class Panels: def __init__(self): - self.dashboard = Panel("dashboard") - self.processlist = Panel("processlist") - self.graphs = Panel("graphs") - self.replication = Panel("replication") - self.metadata_locks = Panel("metadata_locks") - self.ddl = Panel("ddl") - self.pfs_metrics = Panel("pfs_metrics") - self.proxysql_hostgroup_summary = Panel("proxysql_hostgroup_summary") - self.proxysql_mysql_query_rules = Panel("proxysql_mysql_query_rules") - self.proxysql_command_stats = Panel("proxysql_command_stats") + self.dashboard = Panel("dashboard", "Dashboard") + self.processlist = Panel("processlist", "Processlist") + self.graphs = Panel("graphs", "Graph Metrics") + self.replication = Panel("replication", "Replication") + self.metadata_locks = Panel("metadata_locks", "Metadata Locks") + self.ddl = Panel("ddl", "DDL") + self.pfs_metrics = Panel("pfs_metrics", "Performance Schema Metrics") + self.proxysql_hostgroup_summary = Panel("proxysql_hostgroup_summary", "Hostgroup Summary") + self.proxysql_mysql_query_rules = Panel("proxysql_mysql_query_rules", "MySQL Query Rules") + self.proxysql_command_stats = Panel("proxysql_command_stats", "Command Stats") def get_panel(self, panel_name: str) -> Panel: return self.__dict__.get(panel_name, None) @@ -220,3 +221,4 @@ class HotkeyCommands: rename_tab = "rename_tab" refresh_interval = "refresh_interval" replay_seek = "replay_seek" + maximize_panel = "maximize_panel" diff --git a/dolphie/Dolphie.tcss b/dolphie/Dolphie.tcss index 5142c79..1b71c4f 100644 --- a/dolphie/Dolphie.tcss +++ b/dolphie/Dolphie.tcss @@ -14,7 +14,7 @@ background: #0a0e1b; } Graph { - height: 16; + height: 100%; } Horizontal { height: auto; @@ -38,7 +38,12 @@ DataTable { & > .datatable--even-row { background: #0f1525; } + & > .datatable--header { + background: transparent; + color: #c5c7d2; + } } + LoadingIndicator { color: #8fb0ee; height: auto; @@ -60,7 +65,6 @@ SpinnerWidget { height: auto; content-align: center middle; } - CommandList { text-style: none; border-bottom: hkey #1c2440; @@ -95,7 +99,6 @@ CommandInput, CommandInput:focus { padding-left: 0 !important; margin: 0 1 !important; } - TopBar { dock: top; background: #192036; @@ -140,7 +143,6 @@ TopBar { } } - .replay_buttons { height: auto; width: 65; @@ -182,7 +184,28 @@ TopBar { margin-top: 0; } } - +#panel_graphs { + & .metric_graph_container, .metric_graph_container2 { + height: 20; + } + & .metric_graph_stats { + width: 100%; + content-align: center middle; + } +} +Screen.-maximized-view { + height: 100%; + & .metric_graph_container, .metric_graph_container2, TabPane { + height: 1fr !important; + } + & Tabs { + margin-top: 0; + } + & DataTable { + height: 100% !important; + max-height: 100% !important; + } +} #pfs_metrics_file_io_datatable, #pfs_metrics_table_io_waits_datatable, #proxysql_hostgroup_summary_datatable, #proxysql_mysql_query_rules_datatable { overflow-x: auto; @@ -357,12 +380,6 @@ Input { } } -.datatable--header { - background: transparent; - color: #c5c7d2; - text-style: bold; -} - .button_container { height: auto; width: 100%; @@ -380,11 +397,6 @@ Input { } } -.stats_data { - width: 100%; - content-align: center middle; -} - Sparkline { & > .sparkline--max-color { color: #869fd9; @@ -460,10 +472,18 @@ Switch { } } -Checkbox .toggle--button { - color: #0f1525; - text-style: bold; - background: #343d56; +Checkbox { + background: #131626; + border: none; + padding-left: 2; + padding-bottom: 1; + content-align: left middle; + + & .toggle--button { + color: #0f1525; + text-style: bold; + background: #343d56; + } } RadioSet { @@ -549,4 +569,71 @@ Toast { &.-error .toast--title { color: #ed6363; } +} + +Select { + margin: 0 2; + margin-bottom: 1; + width: 100%; + + & > SelectOverlay { + background: #111322; + + &:focus { + background-tint: transparent; + } + } + + &:focus > SelectCurrent { + border: tall #43548b; + background-tint: transparent; + background: #151729; + } +} + +SelectCurrent { + background: #111322; + border: tall #252e49; + + & Static#label { + color: #606e88; + } + + &.-has-value Static#label { + color: #e9e9e9; + } +} + +Select > OptionList { + background: #111322; + border: tall #252e49; + width: 100%; + height: 15; + margin: 0 1 0 1; + + &:focus { + margin: 0; + height: auto; + max-height: 15; + border: tall #3c476b; + } + + & > .option-list--option-highlighted { + text-style: none; + background: #131626; + } + &:focus > .option-list--option-highlighted { + background: #283048; + } + & > .option-list--option-hover { + background: #283048; + } + & > .option-list--option-hover-highlighted { + background: #283048; + text-style: none; + } + &:focus > .option-list--option-hover-highlighted { + background: #283048; + text-style: none; + } } \ No newline at end of file diff --git a/dolphie/Modules/CommandManager.py b/dolphie/Modules/CommandManager.py index d00bf10..6a7d536 100644 --- a/dolphie/Modules/CommandManager.py +++ b/dolphie/Modules/CommandManager.py @@ -68,6 +68,7 @@ def __init__(self): "E": {"human_key": "E", "description": "Export the processlist to a CSV file"}, "k": {"human_key": "k", "description": "Kill thread by its ID"}, "K": {"human_key": "K", "description": "Kill threads by parameter(s)"}, + "M": {"human_key": "M", "description": "Maximize a panel"}, "q": {"human_key": "q", "description": "Quit"}, "r": {"human_key": "r", "description": "Set the refresh interval"}, "R": {"human_key": "R", "description": "Reset all metrics"}, @@ -114,6 +115,7 @@ def __init__(self): "E": {"human_key": "E", "description": "Export the processlist to a CSV file"}, "k": {"human_key": "k", "description": "Kill thread by its ID"}, "K": {"human_key": "K", "description": "Kill threads by parameter(s)"}, + "M": {"human_key": "M", "description": "Maximize a panel"}, "q": {"human_key": "q", "description": "Quit"}, "r": {"human_key": "r", "description": "Set the refresh interval"}, "R": {"human_key": "R", "description": "Reset all metrics"}, @@ -163,12 +165,19 @@ def __init__(self): "placeholder_4": {"human_key": "", "description": ""}, "p": {"human_key": "p", "description": "Toggle pause of replay"}, "S": {"human_key": "S", "description": "Seek to a specific time in the replay"}, - "left_square_bracket": {"human_key": "[", "description": "Seek to the previous refresh interval"}, - "right_square_bracket": {"human_key": "]", "description": "Seek to the next refresh interval"}, + "left_square_bracket": { + "human_key": "[", + "description": "Seek to previous refresh interval in the replay", + }, + "right_square_bracket": { + "human_key": "]", + "description": "Seek to next refresh interval in the replay", + }, "placeholder_5": {"human_key": "", "description": ""}, "c": {"human_key": "c", "description": "Clear all filters set"}, "f": {"human_key": "f", "description": "Filter threads by field(s)"}, "E": {"human_key": "E", "description": "Export the processlist to a CSV file"}, + "M": {"human_key": "M", "description": "Maximize a panel"}, "q": {"human_key": "q", "description": "Quit"}, "r": {"human_key": "r", "description": "Set the refresh interval"}, } @@ -196,12 +205,19 @@ def __init__(self): "placeholder_4": {"human_key": "", "description": ""}, "p": {"human_key": "p", "description": "Toggle pause of replay"}, "S": {"human_key": "S", "description": "Seek to a specific time in the replay"}, - "left_square_bracket": {"human_key": "[", "description": "Seek to the previous refresh interval"}, - "right_square_bracket": {"human_key": "]", "description": "Seek to the next refresh interval"}, + "left_square_bracket": { + "human_key": "[", + "description": "Seek to previous refresh interval in the replay", + }, + "right_square_bracket": { + "human_key": "]", + "description": "Seek to next refresh interval in the replay", + }, "placeholder_5": {"human_key": "", "description": ""}, "c": {"human_key": "c", "description": "Clear all filters set"}, "f": {"human_key": "f", "description": "Filter threads by field(s)"}, "E": {"human_key": "E", "description": "Export the processlist to a CSV file"}, + "M": {"human_key": "M", "description": "Maximize a panel"}, "q": {"human_key": "q", "description": "Quit"}, "r": {"human_key": "r", "description": "Set the refresh interval"}, } diff --git a/dolphie/Modules/TabManager.py b/dolphie/Modules/TabManager.py index 4fceef9..85ce617 100644 --- a/dolphie/Modules/TabManager.py +++ b/dolphie/Modules/TabManager.py @@ -466,7 +466,7 @@ async def create_ui_widgets(self): TabPane( "Table I/O Waits Summary", Label( - ":bulb: Format for each metric: Wait time (Operations count)", + ":bulb: [highlight]Format for each metric: Wait time (Operations count)", id="pfs_metrics_format", ), DataTable(id="pfs_metrics_table_io_waits_datatable", show_cursor=False), @@ -532,14 +532,12 @@ async def create_tab( graph_names = metric_instance.graphs graph_tab_name = metric_instance.graph_tab_name - graph_tab = self.app.query(f"#graph_tab_{metric_tab_name}") - if not graph_tab: + if not self.app.query(f"#graph_tab_{metric_tab_name}"): await self.app.query_one("#metric_graph_tabs", TabbedContent).add_pane( TabPane( graph_tab_name, - Label(id=f"stats_{metric_tab_name}", classes="stats_data"), - Horizontal(id=f"graph_container_{metric_tab_name}"), - Horizontal(id=f"graph_container2_{metric_tab_name}"), + Label(id=f"metric_graph_stats_{metric_tab_name}", classes="metric_graph_stats"), + Horizontal(id=f"metric_graph_container_{metric_tab_name}", classes="metric_graph_container"), Horizontal( id=f"switch_container_{metric_tab_name}", classes="switch_container switch_container", @@ -550,16 +548,25 @@ async def create_tab( ) # Save references to the labels - setattr(tab, metric_tab_name, self.app.query_one(f"#stats_{metric_tab_name}")) + setattr(tab, metric_tab_name, self.app.query_one(f"#metric_graph_stats_{metric_tab_name}")) for graph_name in graph_names: - graph = self.app.query(f"#{graph_name}") - if not graph: - graph_container = ( - "graph_container2" - if graph_name in ["graph_system_network", "graph_system_disk_io"] - else "graph_container" - ) + graph_container = ( + "metric_graph_container2" + if graph_name in ["graph_system_network", "graph_system_disk_io"] + else "metric_graph_container" + ) + + if not self.app.query(f"#{graph_name}"): + # Add graph_container2 only if it's needed + if not self.app.query(f"#{graph_container}_{metric_tab_name}"): + if graph_container == "metric_graph_container2": + await self.app.query_one(f"#graph_tab_{metric_tab_name}", TabPane).mount( + Horizontal( + id=f"{graph_container}_{metric_tab_name}", classes="metric_graph_container2" + ), + after=1, + ) await self.app.query_one(f"#{graph_container}_{metric_tab_name}", Horizontal).mount( MetricManager.Graph(id=f"{graph_name}", classes="panel_data") @@ -569,9 +576,7 @@ async def create_tab( setattr(tab, graph_name, self.app.query_one(f"#{graph_name}")) for metric, metric_data in metric_instance.__dict__.items(): - switch = self.app.query(f"#switch_container_{metric_tab_name} #{metric_instance_name}-{metric}") - - if not switch: + if not self.app.query(f"#switch_container_{metric_tab_name} #{metric_instance_name}-{metric}"): if ( isinstance(metric_data, MetricManager.MetricData) and metric_data.graphable diff --git a/dolphie/Widgets/modal.py b/dolphie/Widgets/modal.py index f1d18df..0316cd5 100644 --- a/dolphie/Widgets/modal.py +++ b/dolphie/Widgets/modal.py @@ -4,7 +4,7 @@ from textual.binding import Binding from textual.containers import Horizontal, Vertical from textual.screen import ModalScreen -from textual.widgets import Button, Checkbox, Input, Label, Static +from textual.widgets import Button, Checkbox, Input, Label, Select, Static from dolphie.DataTypes import ConnectionSource, HotkeyCommands from dolphie.Widgets.autocomplete import AutoComplete, Dropdown, DropdownItem @@ -26,21 +26,11 @@ class CommandModal(ModalScreen): } } - & #filter_container { + & .command_container { width: auto; height: auto; - & Input { - width: 60; - border-title-color: #d2d2d2; - } - } - - & #kill_container { - width: auto; - height: auto; - - & Input { + & Input, Select { width: 60; border-title-color: #d2d2d2; } @@ -83,6 +73,7 @@ def __init__( message, connection_source: ConnectionSource = None, processlist_data=None, + maximize_panel_options=None, host_cache_data=None, max_replay_timestamp=None, ): @@ -99,11 +90,19 @@ def __init__( sorted_keys = sorted(processlist_data.keys(), key=lambda x: int(x)) self.dropdown_items = [DropdownItem(thread_id) for thread_id in sorted_keys] + self.maximize_panel_select_options = maximize_panel_options or [] + def compose(self) -> ComposeResult: with Vertical(): with Vertical(): yield Label(f"[b]{self.message}[/b]") - with Vertical(id="filter_container"): + + with Vertical(id="maximize_panel_container", classes="command_container"): + yield Select( + options=self.maximize_panel_select_options, id="maximize_panel_select", prompt="Select a Panel" + ) + yield Label("[dark_gray][b]Note[/b]: Press [b highlight]ESC[/b highlight] to exit maximized panel") + with Vertical(id="filter_container", classes="command_container"): yield AutoComplete( Input(id="filter_by_username_input"), Dropdown(id="filter_by_username_dropdown_items", items=[]) ) @@ -119,7 +118,7 @@ def compose(self) -> ComposeResult: ) yield Input(id="filter_by_query_time_input") yield Input(id="filter_by_query_text_input") - with Vertical(id="kill_container"): + with Vertical(id="kill_container", classes="command_container"): yield AutoComplete( Input(id="kill_by_username_input"), Dropdown(id="kill_by_username_dropdown_items", items=[]) ) @@ -142,10 +141,12 @@ def compose(self) -> ComposeResult: def on_mount(self): input = self.query_one("#modal_input", Input) + maximize_panel_container = self.query_one("#maximize_panel_container", Vertical) filter_container = self.query_one("#filter_container", Vertical) kill_container = self.query_one("#kill_container", Vertical) self.query_one("#error_response", Static).display = False + maximize_panel_container.display = False filter_container.display = False kill_container.display = False @@ -187,6 +188,9 @@ def on_mount(self): sleeping_queries_checkbox.toggle() input.placeholder = "Select an option from above" + elif self.command == HotkeyCommands.maximize_panel: + input.display = False + maximize_panel_container.display = True elif self.command == HotkeyCommands.rename_tab: input.placeholder = "Colors can be added by wrapping them in []" input.styles.width = 50 @@ -234,6 +238,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: HotkeyCommands.rename_tab, HotkeyCommands.thread_kill_by_parameter, HotkeyCommands.thread_filter, + HotkeyCommands.maximize_panel, ]: self.update_error_response("Input cannot be empty") return @@ -333,6 +338,13 @@ def on_button_pressed(self, event: Button.Pressed) -> None: return self.dismiss(modal_input) + elif self.command == HotkeyCommands.maximize_panel: + maximize_panel = self.query_one("#maximize_panel_select", Select).value + if maximize_panel == Select.BLANK: + self.update_error_response("Please select a panel to maximize") + return + + self.dismiss(maximize_panel) else: self.dismiss(modal_input) diff --git a/dolphie/Widgets/tab_setup.py b/dolphie/Widgets/tab_setup.py index cb36151..26bd490 100644 --- a/dolphie/Widgets/tab_setup.py +++ b/dolphie/Widgets/tab_setup.py @@ -66,12 +66,8 @@ class TabSetupModal(ModalScreen): } & RadioSet { - background: #131626; - border: none; padding-bottom: 1; align: center middle; - width: 100%; - layout: horizontal; } & AutoComplete { @@ -121,73 +117,6 @@ class TabSetupModal(ModalScreen): content-align: left middle; } - & Select { - margin: 0 2; - margin-bottom: 1; - width: 100%; - - & > SelectOverlay { - background: #111322; - - &:focus { - background-tint: transparent; - } - } - - &:focus > SelectCurrent { - border: tall #43548b; - background-tint: transparent; - background: #151729; - } - } - - & SelectCurrent { - background: #111322; - border: tall #252e49; - - & Static#label { - color: #606e88; - } - - &.-has-value Static#label { - color: #e9e9e9; - } - } - - & Select > OptionList { - background: #111322; - border: tall #252e49; - width: 100%; - height: 15; - margin: 0 1 0 1; - - &:focus { - margin: 0; - height: auto; - max-height: 15; - border: tall #3c476b; - } - - & > .option-list--option-highlighted { - text-style: none; - background: #131626; - } - &:focus > .option-list--option-highlighted { - background: #283048; - } - & > .option-list--option-hover { - background: #283048; - } - & > .option-list--option-hover-highlighted { - background: #283048; - text-style: none; - } - &:focus > .option-list--option-hover-highlighted { - background: #283048; - text-style: none; - } - } - & #replay_file { & > SelectOverlay > .option-list--option { padding: 0; diff --git a/dolphie/app.py b/dolphie/app.py index 4bf7107..cf0d834 100755 --- a/dolphie/app.py +++ b/dolphie/app.py @@ -1497,6 +1497,47 @@ def command_get_input(data): else: self.run_command_in_worker(key=key, dolphie=dolphie) + elif key == "M": + + def command_get_input(filter_data): + panel = filter_data + + widget = None + if panel == "processlist": + widget = tab.processlist_datatable + elif panel == "graphs": + widget = tab.metric_graph_tabs + elif panel == "metadata_locks": + widget = tab.metadata_locks_datatable + elif panel == "ddl": + widget = tab.ddl_datatable + elif panel == "pfs_metrics": + widget = tab.pfs_metrics_tabs + elif panel == "proxysql_hostgroup_summary": + widget = tab.proxysql_hostgroup_summary_datatable + elif panel == "proxysql_mysql_query_rules": + widget = tab.proxysql_mysql_query_rules_datatable + elif panel == "proxysql_command_stats": + widget = tab.proxysql_command_stats_datatable + + if widget: + self.screen.maximize(widget) + + panel_options = [ + (panel.display_name, panel.name) + for panel in tab.dolphie.panels.get_all_panels() + if panel.visible and panel.name not in ["dashboard"] + ] + + self.app.push_screen( + CommandModal( + command=HotkeyCommands.maximize_panel, + maximize_panel_options=panel_options, + message="Maximize a Panel", + ), + command_get_input, + ) + elif key == "p": if dolphie.replay_file: self.query_one("#pause_button", Button).press() diff --git a/pyproject.toml b/pyproject.toml index ace72c0..6853a44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dolphie" -version = "6.6.1" +version = "6.6.2" license = "GPL-3.0-or-later" description = "Your single pane of glass for real-time analytics into MySQL/MariaDB & ProxySQL" authors = ["Charles Thompson <01charles.t@gmail.com>"]