Skip to content

Commit a27e9d7

Browse files
committed
Add JS for SQLite query display in QM 4.0 shadow DOM
Inject <details> elements into QM 4.0's Preact-rendered DB queries panel inside the shadow DOM. Waits for DOMContentLoaded so QM has attached the shadow root, then uses a debounced MutationObserver to re-inject after panel switches and re-renders.
1 parent f8b3cb9 commit a27e9d7

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const STYLE = `
2+
details.qm-sqlite {
3+
margin: 6px 0 0;
4+
}
5+
details.qm-sqlite summary {
6+
cursor: pointer;
7+
}
8+
details.qm-sqlite ol {
9+
margin: 6px 0 0;
10+
padding-left: 24px;
11+
list-style: decimal;
12+
}
13+
`;
14+
15+
const container = document.getElementById( 'query-monitor-container' );
16+
const sqliteData = window.QueryMonitorData?.data?.sqlite?.data?.queries;
17+
18+
if ( container && sqliteData ) {
19+
// QM attaches the shadow root in its own DOMContentLoaded listener.
20+
// Our module is loaded after QM's, so our listener fires after QM's.
21+
document.addEventListener( 'DOMContentLoaded', () => {
22+
const shadowRoot = container.shadowRoot;
23+
if ( ! shadowRoot ) {
24+
return;
25+
}
26+
inject( shadowRoot, sqliteData );
27+
28+
// Re-inject after Preact re-renders (panel switches, filters, etc.).
29+
// Debounced to avoid excessive work during rapid DOM updates.
30+
let timer;
31+
new MutationObserver( () => {
32+
clearTimeout( timer );
33+
timer = setTimeout( () => inject( shadowRoot, sqliteData ), 100 );
34+
} ).observe( shadowRoot, { childList: true, subtree: true } );
35+
} );
36+
}
37+
38+
function inject( shadowRoot, data ) {
39+
const panel = shadowRoot.getElementById( 'qm-db_queries' );
40+
if ( ! panel ) {
41+
return;
42+
}
43+
44+
if ( ! shadowRoot.querySelector( 'style.qm-sqlite-style' ) ) {
45+
const style = document.createElement( 'style' );
46+
style.className = 'qm-sqlite-style';
47+
style.textContent = STYLE;
48+
shadowRoot.appendChild( style );
49+
}
50+
51+
// Match by SQL rather than row position — robust to filtering, sorting, etc.
52+
for ( const code of panel.querySelectorAll( 'td.qm-cell-sql > code' ) ) {
53+
const cell = code.parentElement;
54+
const key = code.innerText.replace( /\s+/g, ' ' ).trim();
55+
const queries = data[ key ];
56+
const existing = cell.querySelector( 'details.qm-sqlite' );
57+
58+
// Preact may recycle DOM nodes on filter/sort, leaving stale details
59+
// from a previous query. Remove them when the SQL key no longer matches.
60+
if ( existing ) {
61+
if ( queries?.length && existing.dataset.sqliteKey === key ) {
62+
continue;
63+
}
64+
existing.remove();
65+
}
66+
67+
if ( queries?.length ) {
68+
cell.append( buildDetails( key, queries ) );
69+
}
70+
}
71+
}
72+
73+
function buildDetails( key, queries ) {
74+
const details = document.createElement( 'details' );
75+
details.className = 'qm-sqlite';
76+
details.dataset.sqliteKey = key;
77+
// Prevent QM's row click handlers from firing when toggling.
78+
details.addEventListener( 'click', ( e ) => e.stopPropagation() );
79+
80+
const summary = document.createElement( 'summary' );
81+
summary.textContent = `Executed ${ queries.length } SQLite ${ queries.length === 1 ? 'Query' : 'Queries' }`;
82+
83+
const ol = document.createElement( 'ol' );
84+
for ( const sql of queries ) {
85+
const li = document.createElement( 'li' );
86+
li.className = 'qm-sqlite-query';
87+
const code = document.createElement( 'code' );
88+
code.textContent = sql;
89+
li.append( code );
90+
ol.append( li );
91+
}
92+
93+
details.append( summary, ol );
94+
return details;
95+
}

0 commit comments

Comments
 (0)