From f4b5dbfc681f3b4e1997daf0bab5d76a46ae19c4 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 22 Jan 2021 16:46:20 -0600 Subject: [PATCH 1/2] Support JS events when loading a panel. This fixes the problem of the Timer Panel not inserting the browser timings section after being loaded via the HistoryPanel. These events could be wired into to better render panels or support a more dynamic toolbar and/or panel. --- debug_toolbar/panels/__init__.py | 4 + .../static/debug_toolbar/js/timer.js | 120 ++++++++++-------- .../static/debug_toolbar/js/toolbar.js | 16 ++- .../static/debug_toolbar/js/utils.js | 3 +- .../debug_toolbar/includes/panel_content.html | 2 +- docs/changes.rst | 5 + docs/panels.rst | 24 ++++ 7 files changed, 115 insertions(+), 59 deletions(-) diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index ec3445c1e..02bb2ceb6 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -107,6 +107,10 @@ def content(self): def scripts(self): """ Scripts used by the HTML content of the panel when it's displayed. + + When a panel is loaded on the frontend, a JavaScript event will be + dispatched of the format ``djdt.panel.[panel_id]``. The scripts used can + listen for this event to execute functionality on panel load. """ return [] diff --git a/debug_toolbar/static/debug_toolbar/js/timer.js b/debug_toolbar/static/debug_toolbar/js/timer.js index 1d4ac19d8..42fe3c58a 100644 --- a/debug_toolbar/static/debug_toolbar/js/timer.js +++ b/debug_toolbar/static/debug_toolbar/js/timer.js @@ -1,59 +1,67 @@ -const timingOffset = performance.timing.navigationStart, - timingEnd = performance.timing.loadEventEnd, - totalTime = timingEnd - timingOffset; -function getLeft(stat) { - return ((performance.timing[stat] - timingOffset) / totalTime) * 100.0; -} -function getCSSWidth(stat, endStat) { - let width = - ((performance.timing[endStat] - performance.timing[stat]) / totalTime) * - 100.0; - // Calculate relative percent (same as sql panel logic) - width = (100.0 * width) / (100.0 - getLeft(stat)); - return width < 1 ? "2px" : width + "%"; -} -function addRow(tbody, stat, endStat) { - const row = document.createElement("tr"); - if (endStat) { - // Render a start through end bar - row.innerHTML = - "" + - stat.replace("Start", "") + - "" + - '' + - "" + - (performance.timing[stat] - timingOffset) + - " (+" + - (performance.timing[endStat] - performance.timing[stat]) + - ")"; - row.querySelector("rect").setAttribute( - "width", - getCSSWidth(stat, endStat) - ); - } else { - // Render a point in time - row.innerHTML = - "" + - stat + - "" + - '' + - "" + - (performance.timing[stat] - timingOffset) + - ""; - row.querySelector("rect").setAttribute("width", 2); +function insertBrowserTiming() { + const timingOffset = performance.timing.navigationStart, + timingEnd = performance.timing.loadEventEnd, + totalTime = timingEnd - timingOffset; + function getLeft(stat) { + return ((performance.timing[stat] - timingOffset) / totalTime) * 100.0; + } + function getCSSWidth(stat, endStat) { + let width = + ((performance.timing[endStat] - performance.timing[stat]) / + totalTime) * + 100.0; + // Calculate relative percent (same as sql panel logic) + width = (100.0 * width) / (100.0 - getLeft(stat)); + return width < 1 ? "2px" : width + "%"; } - row.querySelector("rect").setAttribute("x", getLeft(stat)); - tbody.appendChild(row); + function addRow(tbody, stat, endStat) { + const row = document.createElement("tr"); + if (endStat) { + // Render a start through end bar + row.innerHTML = + "" + + stat.replace("Start", "") + + "" + + '' + + "" + + (performance.timing[stat] - timingOffset) + + " (+" + + (performance.timing[endStat] - performance.timing[stat]) + + ")"; + row.querySelector("rect").setAttribute( + "width", + getCSSWidth(stat, endStat) + ); + } else { + // Render a point in time + row.innerHTML = + "" + + stat + + "" + + '' + + "" + + (performance.timing[stat] - timingOffset) + + ""; + row.querySelector("rect").setAttribute("width", 2); + } + row.querySelector("rect").setAttribute("x", getLeft(stat)); + tbody.appendChild(row); + } + + const tbody = document.getElementById("djDebugBrowserTimingTableBody"); + // This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) + addRow(tbody, "domainLookupStart", "domainLookupEnd"); + addRow(tbody, "connectStart", "connectEnd"); + addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd + addRow(tbody, "responseStart", "responseEnd"); + addRow(tbody, "domLoading", "domComplete"); // Spans the events below + addRow(tbody, "domInteractive"); + addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); + addRow(tbody, "loadEventStart", "loadEventEnd"); + document + .getElementById("djDebugBrowserTiming") + .classList.remove("djdt-hidden"); } -const tbody = document.getElementById("djDebugBrowserTimingTableBody"); -// This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) -addRow(tbody, "domainLookupStart", "domainLookupEnd"); -addRow(tbody, "connectStart", "connectEnd"); -addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd -addRow(tbody, "responseStart", "responseEnd"); -addRow(tbody, "domLoading", "domComplete"); // Spans the events below -addRow(tbody, "domInteractive"); -addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); -addRow(tbody, "loadEventStart", "loadEventEnd"); -document.getElementById("djDebugBrowserTiming").classList.remove("djdt-hidden"); +const djDebug = document.getElementById("djDebug"); +djDebug.addEventListener("djdt.panel.TimerPanel", insertBrowserTiming, false); diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 56e07cae0..bfddf141d 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -38,12 +38,26 @@ const djdt = { djDebug.dataset.renderPanelUrl, window.location ); + const onLoadEvent = current.dataset.onLoadEvent; url.searchParams.append("store_id", store_id); url.searchParams.append("panel_id", this.className); ajax(url).then(function (data) { inner.previousElementSibling.remove(); // Remove AJAX loader inner.innerHTML = data.content; - $$.executeScripts(data.scripts); + if (data.scripts.length > 0) { + let raisedEvent = false; + $$.executeScripts(data.scripts, function () { + // Attempt to raise the event only once. + if (!raisedEvent) { + raisedEvent = true; + djDebug.dispatchEvent( + new Event(onLoadEvent) + ); + } + }); + } else { + djDebug.dispatchEvent(new Event(onLoadEvent)); + } }); } } diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index 98e7e4122..4209ac8e7 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -23,12 +23,13 @@ const $$ = { visible(element) { return !element.classList.contains("djdt-hidden"); }, - executeScripts(scripts) { + executeScripts(scripts, onLoad) { scripts.forEach(function (script) { const el = document.createElement("script"); el.type = "module"; el.src = script; el.async = true; + el.onload = onLoad; document.head.appendChild(el); }); }, diff --git a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html index 2c1a1b195..52c6606f4 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html +++ b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html @@ -1,7 +1,7 @@ {% load static %} {% if panel.has_content and panel.enabled %} -
+

{{ panel.title }}

diff --git a/docs/changes.rst b/docs/changes.rst index 6207b6797..4c153c585 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -9,6 +9,11 @@ Next version * Added ``PRETTIFY_SQL`` configuration option to support controlling SQL token grouping. By default it's set to True. When set to False, a performance improvement can be seen by the SQL panel. +* Support JavaScript event when a panel loads of the format + ``djdt.panel.[PanelId]`` where PanelId is the panel's python class' + ``panel_id`` property. Listening for this event corrects the bug + in the Timer Panel in which it doesn't insert the browser timings + after being switching requests in the History Panel. 3.2 (2020-12-03) diff --git a/docs/panels.rst b/docs/panels.rst index c21e90801..274f0ef76 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -402,3 +402,27 @@ common methods available. .. js:function:: djdt.show_toolbar Shows the toolbar. + +Events +^^^^^^ + +.. js:attribute:: djdt.panel.[panel_id] + + ``[panel_id]`` is the ``panel_id`` property of the panel's Python class. + + This is an event raised when a panel is loaded for the first time. This + will not be raised on every render of the panel as the panel's content + is cached after the first load. This event can be useful when creating + custom scripts to process the HTML further. + + An example of this for the ``CustomPanel`` would be: + +.. code-block:: javascript + + function addCustomMetrics() { + // Logic to process/add custom metrics here. + } + // The panel events are dispatched on the #djDebug element. + const djDebug = document.getElementById("djDebug"); + // When the event djdt.panel.CustomPanel is raised, call AddCustomMetrics + djDebug.addEventListener("djdt.panel.CustomPanel", addCustomMetrics, false); From e766af7804d0080e6d8d919be714f862e22accc5 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Sun, 24 Jan 2021 13:39:12 -0600 Subject: [PATCH 2/2] Support panel rendered JS event. This fixes the problem of the Timer Panel not inserting the browser timings section after being loaded via the HistoryPanel. These events could be wired into to better render panels or support a more dynamic toolbar and/or panel. --- debug_toolbar/panels/__init__.py | 6 ++-- .../static/debug_toolbar/js/timer.js | 36 +++++++++++-------- .../static/debug_toolbar/js/toolbar.js | 32 ++++++++--------- .../static/debug_toolbar/js/utils.js | 18 ++++++++-- .../debug_toolbar/includes/panel_content.html | 2 +- docs/panels.rst | 29 ++++++++------- 6 files changed, 74 insertions(+), 49 deletions(-) diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index 02bb2ceb6..8fd433c63 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -108,9 +108,9 @@ def scripts(self): """ Scripts used by the HTML content of the panel when it's displayed. - When a panel is loaded on the frontend, a JavaScript event will be - dispatched of the format ``djdt.panel.[panel_id]``. The scripts used can - listen for this event to execute functionality on panel load. + When a panel is rendered on the frontend, the ``djdt.panel.render`` + JavaScript event will be dispatched. The scripts can listen for + this event to support dynamic functionality. """ return [] diff --git a/debug_toolbar/static/debug_toolbar/js/timer.js b/debug_toolbar/static/debug_toolbar/js/timer.js index 42fe3c58a..70d3fe5a2 100644 --- a/debug_toolbar/static/debug_toolbar/js/timer.js +++ b/debug_toolbar/static/debug_toolbar/js/timer.js @@ -1,4 +1,7 @@ +import { $$ } from "./utils.js"; + function insertBrowserTiming() { + console.log(["inserted"]); const timingOffset = performance.timing.navigationStart, timingEnd = performance.timing.loadEventEnd, totalTime = timingEnd - timingOffset; @@ -48,20 +51,25 @@ function insertBrowserTiming() { tbody.appendChild(row); } - const tbody = document.getElementById("djDebugBrowserTimingTableBody"); - // This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) - addRow(tbody, "domainLookupStart", "domainLookupEnd"); - addRow(tbody, "connectStart", "connectEnd"); - addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd - addRow(tbody, "responseStart", "responseEnd"); - addRow(tbody, "domLoading", "domComplete"); // Spans the events below - addRow(tbody, "domInteractive"); - addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); - addRow(tbody, "loadEventStart", "loadEventEnd"); - document - .getElementById("djDebugBrowserTiming") - .classList.remove("djdt-hidden"); + const browserTiming = document.getElementById("djDebugBrowserTiming"); + // Determine if the browser timing section has already been rendered. + if (browserTiming.classList.contains("djdt-hidden")) { + const tbody = document.getElementById("djDebugBrowserTimingTableBody"); + // This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) + addRow(tbody, "domainLookupStart", "domainLookupEnd"); + addRow(tbody, "connectStart", "connectEnd"); + addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd + addRow(tbody, "responseStart", "responseEnd"); + addRow(tbody, "domLoading", "domComplete"); // Spans the events below + addRow(tbody, "domInteractive"); + addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); + addRow(tbody, "loadEventStart", "loadEventEnd"); + browserTiming.classList.remove("djdt-hidden"); + } } const djDebug = document.getElementById("djDebug"); -djDebug.addEventListener("djdt.panel.TimerPanel", insertBrowserTiming, false); +// Insert the browser timing now since it's possible for this +// script to miss the initial panel load event. +insertBrowserTiming(); +$$.onPanelRender(djDebug, "TimerPanel", insertBrowserTiming); diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index bfddf141d..e4e59f184 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -20,7 +20,8 @@ const djdt = { if (!this.className) { return; } - const current = document.getElementById(this.className); + const panelId = this.className; + const current = document.getElementById(panelId); if ($$.visible(current)) { djdt.hide_panels(); } else { @@ -38,27 +39,24 @@ const djdt = { djDebug.dataset.renderPanelUrl, window.location ); - const onLoadEvent = current.dataset.onLoadEvent; url.searchParams.append("store_id", store_id); - url.searchParams.append("panel_id", this.className); + url.searchParams.append("panel_id", panelId); ajax(url).then(function (data) { inner.previousElementSibling.remove(); // Remove AJAX loader inner.innerHTML = data.content; - if (data.scripts.length > 0) { - let raisedEvent = false; - $$.executeScripts(data.scripts, function () { - // Attempt to raise the event only once. - if (!raisedEvent) { - raisedEvent = true; - djDebug.dispatchEvent( - new Event(onLoadEvent) - ); - } - }); - } else { - djDebug.dispatchEvent(new Event(onLoadEvent)); - } + $$.executeScripts(data.scripts); + djDebug.dispatchEvent( + new CustomEvent("djdt.panel.render", { + detail: { panelId: panelId }, + }) + ); }); + } else { + djDebug.dispatchEvent( + new CustomEvent("djdt.panel.render", { + detail: { panelId: panelId }, + }) + ); } } } diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index 4209ac8e7..08091b894 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -7,6 +7,21 @@ const $$ = { } }); }, + onPanelRender(root, panelId, fn) { + /* + This is a helper function to attach a handler for a `djdt.panel.render` + event of a specific panel. + + root: The container element that the listener should be attached to. + panelId: The Id of the panel. + fn: A function to execute when the event is triggered. + */ + root.addEventListener("djdt.panel.render", function (event) { + if (event.detail.panelId === panelId) { + fn.call(event); + } + }); + }, show(element) { element.classList.remove("djdt-hidden"); }, @@ -23,13 +38,12 @@ const $$ = { visible(element) { return !element.classList.contains("djdt-hidden"); }, - executeScripts(scripts, onLoad) { + executeScripts(scripts) { scripts.forEach(function (script) { const el = document.createElement("script"); el.type = "module"; el.src = script; el.async = true; - el.onload = onLoad; document.head.appendChild(el); }); }, diff --git a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html index 52c6606f4..2c1a1b195 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html +++ b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html @@ -1,7 +1,7 @@ {% load static %} {% if panel.has_content and panel.enabled %} -
+

{{ panel.title }}

diff --git a/docs/panels.rst b/docs/panels.rst index 274f0ef76..dfa5ec92a 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -184,9 +184,9 @@ URL: https://github.com/danyi1212/django-windowsauth Path: ``windows_auth.panels.LDAPPanel`` -LDAP Operations performed during the request, including timing, request and response messages, +LDAP Operations performed during the request, including timing, request and response messages, the entries received, write changes list, stack-tracing and error debugging. -This panel also shows connection usage metrics when it is collected. +This panel also shows connection usage metrics when it is collected. `Check out the docs `_. Line Profiler @@ -406,23 +406,28 @@ common methods available. Events ^^^^^^ -.. js:attribute:: djdt.panel.[panel_id] +.. js:attribute:: djdt.panel.render - ``[panel_id]`` is the ``panel_id`` property of the panel's Python class. - - This is an event raised when a panel is loaded for the first time. This - will not be raised on every render of the panel as the panel's content - is cached after the first load. This event can be useful when creating - custom scripts to process the HTML further. + This is an event raised when a panel is rendered. It has the property + ``detail.panelId`` which identifies which panel has been loaded. This + event can be useful when creating custom scripts to process the HTML + further. An example of this for the ``CustomPanel`` would be: .. code-block:: javascript + import { $$ } from "./utils.js"; function addCustomMetrics() { // Logic to process/add custom metrics here. + + // Be sure to cover the case of this function being called twice + // due to file being loaded asynchronously. } - // The panel events are dispatched on the #djDebug element. const djDebug = document.getElementById("djDebug"); - // When the event djdt.panel.CustomPanel is raised, call AddCustomMetrics - djDebug.addEventListener("djdt.panel.CustomPanel", addCustomMetrics, false); + $$.onPanelRender(djDebug, "CustomPanel", addCustomMetrics); + // Since a panel's scripts are loaded asynchronously, it's possible that + // the above statement would occur after the djdt.panel.render event has + // been raised. To account for that, the rendering function should be + // called here as well. + addCustomMetrics();