diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index a53ba6652..49fbd4ded 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -3,7 +3,7 @@ from django.utils.functional import classproperty from debug_toolbar import settings as dt_settings -from debug_toolbar.utils import get_name_from_obj +from debug_toolbar.utils import HealthLevel, get_name_from_obj class Panel: @@ -129,6 +129,19 @@ def scripts(self): """ return [] + @property + def health_level(self): + """ + Returns the health level of the panel as a `ToolbarHealthLevel` enum value. + + This property is used by the toolbar to determine the overall health status of each panel. + The default implementation returns `ToolbarHealthLevel.NONE`, indicating no issues. + + Subclasses should override this property to provide custom health logic, returning + `ToolbarHealthLevel.WARNING` or `ToolbarHealthLevel.ERROR` as appropriate based on panel-specific conditions. + """ + return HealthLevel.NONE + # Panel early initialization @classmethod diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py index 030ca1ee1..53204c96f 100644 --- a/debug_toolbar/panels/alerts.py +++ b/debug_toolbar/panels/alerts.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from debug_toolbar.panels import Panel -from debug_toolbar.utils import is_processable_html_response +from debug_toolbar.utils import HealthLevel, is_processable_html_response class FormParser(HTMLParser): @@ -92,6 +92,16 @@ def nav_subtitle(self): else: return "" + @property + def health_level(self): + """ + Return the health level of the panel based on the alerts. + """ + if not self.get_stats().get("alerts"): + return HealthLevel.NONE + + return HealthLevel.CRITICAL + def add_alert(self, alert): self.alerts.append(alert) diff --git a/debug_toolbar/panels/sql/panel.py b/debug_toolbar/panels/sql/panel.py index 45143ef94..5c84c8768 100644 --- a/debug_toolbar/panels/sql/panel.py +++ b/debug_toolbar/panels/sql/panel.py @@ -19,7 +19,7 @@ is_select_query, reformat_sql, ) -from debug_toolbar.utils import render_stacktrace +from debug_toolbar.utils import HealthLevel, render_stacktrace def get_isolation_level_display(vendor, level): @@ -186,6 +186,21 @@ def title(self): count, ) % {"count": count} + @property + def health_level(self): + """ + Return the health level of the SQL panel. + This is determined by the number of slow queries recorded. + """ + stats = self.get_stats() + slow_queries = len([1 for q in stats.get("queries", []) if q.get("is_slow")]) + if slow_queries > 10: + return HealthLevel.CRITICAL + elif slow_queries > 0: + return HealthLevel.WARNING + + return super().health_level + template = "debug_toolbar/panels/sql.html" @classmethod diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index 43b432069..714b06ae6 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -34,6 +34,13 @@ --djdt-button-border-color: var(--djdt-table-border-color); --djdt-pre-border-color: var(--djdt-table-border-color); --djdt-raw-border-color: var(--djdt-table-border-color); + + --djdt-health-background-color-1: #4b3f1b; + --djdt-health-color-1: #ffe761; + --djdt-health-border-color-1: #ffcc00; + --djdt-health-background-color-2: #5a2327; + --djdt-health-color-2: #ffb3b3; + --djdt-health-border-color-2: #ff0000; } #djDebug[data-theme="dark"] { @@ -56,6 +63,13 @@ --djdt-button-border-color: var(--djdt-table-border-color); --djdt-pre-border-color: var(--djdt-table-border-color); --djdt-raw-border-color: var(--djdt-table-border-color); + + --djdt-health-background-color-1: #4b3f1b; + --djdt-health-color-1: #ffe761; + --djdt-health-border-color-1: #ffcc00; + --djdt-health-background-color-2: #5a2327; + --djdt-health-color-2: #ffb3b3; + --djdt-health-border-color-2: #ff0000; } /* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */ @@ -377,6 +391,29 @@ animation: spin 2s linear infinite; } +/* Panel and Toolbar hidden button health states */ +#djDebug .djdt-health-1 { + /* The background can be shadowed by #djDebugToolbar li.djdt-active a:hover */ + background: var(--djdt-health-background-color-1) !important; + color: var(--djdt-health-color-1); +} + +#djDebug .djdt-health-2 { + /* The background can be shadowed by #djDebugToolbar li.djdt-active a:hover */ + background: var(--djdt-health-background-color-2) !important; + color: var(--djdt-health-color-2); +} + +#djDebug .djdt-toolbarhandle-health-1 { + /* The border-color is shadowed by the default #djShowToolBarButton border */ + border-color: var(--djdt-health-border-color-1) !important; +} + +#djDebug .djdt-toolbarhandle-health-2 { + /* The border-color is shadowed by the default #djShowToolBarButton border */ + border-color: var(--djdt-health-border-color-2) !important; +} + @keyframes spin { 0% { transform: rotate(0deg); diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index b7562ecba..110d12589 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -31,7 +31,7 @@
-
+
DJDT
diff --git a/debug_toolbar/templates/debug_toolbar/includes/panel_button.html b/debug_toolbar/templates/debug_toolbar/includes/panel_button.html index bc6f03ad9..68232ec09 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/panel_button.html +++ b/debug_toolbar/templates/debug_toolbar/includes/panel_button.html @@ -1,6 +1,6 @@ {% load i18n %} -
  • +
  • {% if panel.has_content and panel.enabled %} diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index 6ebc74234..ff77329ef 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -20,6 +20,7 @@ from debug_toolbar import APP_NAME, settings as dt_settings from debug_toolbar.store import get_store +from debug_toolbar.utils import HealthLevel logger = logging.getLogger(__name__) @@ -72,6 +73,17 @@ def csp_nonce(self): """ return getattr(self.request, "csp_nonce", None) + @property + def health_level(self): + """ + Return the maximum health level across all panels. + This is used to color the toolbar hidden button. + """ + if not self.panels: + return HealthLevel.NONE + + return max(panel.health_level for panel in self.enabled_panels) + def get_panel_by_id(self, panel_id): """ Get the panel with the given id, which is the class name by default. diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index f4b3eac38..2047196ef 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -6,6 +6,7 @@ import sys import warnings from collections.abc import Sequence +from enum import IntEnum from pprint import PrettyPrinter, pformat from typing import Any @@ -401,3 +402,18 @@ def is_processable_html_response(response): and content_encoding == "" and content_type in _HTML_TYPES ) + + +class HealthLevel(IntEnum): + """ + Represents the health or alert level for a panel or the toolbar as a whole. + + Used to indicate the severity of issues detected by panels, allowing the UI to reflect + warning or critical states (e.g., via colorization). Panels should return one of these + levels from their `health_level` property. The toolbar will aggregate the maximum level + across all panels to determine the overall toolbar health state. + """ + + NONE = 0 + WARNING = 1 + CRITICAL = 2