active-query-explorer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,433 @@
1
+ var state = { data: [], activeClass: null };
2
+ var filters = { search: "", namespace: "", params: "" };
3
+ var executeUrl = document.body.getAttribute("data-execute-url");
4
+ var queriesUrl = document.body.getAttribute("data-queries-url");
5
+
6
+ // -- Data fetching --
7
+
8
+ function fetchQueries() {
9
+ var main = document.getElementById("main");
10
+ main.innerHTML = '<div class="loading">Loading queries...</div>';
11
+
12
+ fetch(queriesUrl)
13
+ .then(function(r) { return r.json(); })
14
+ .then(function(data) {
15
+ state.data = data;
16
+ populateNamespaceFilter();
17
+ document.getElementById("toolbar").style.display = "flex";
18
+ applyFilters();
19
+ })
20
+ .catch(function(err) {
21
+ main.innerHTML = '<div class="empty-state"><h2>Error loading queries</h2><p>' + escapeHtml(err.message) + '</p></div>';
22
+ });
23
+ }
24
+
25
+ function refresh() {
26
+ fetchQueries();
27
+ }
28
+
29
+ // -- Filtering --
30
+
31
+ function getFilteredData() {
32
+ var search = filters.search.toLowerCase();
33
+ var ns = filters.namespace;
34
+ var paramFilter = filters.params;
35
+
36
+ var result = [];
37
+ state.data.forEach(function(group) {
38
+ if (ns && group.namespace !== ns) return;
39
+
40
+ var filteredObjects = [];
41
+ group.query_objects.forEach(function(qo) {
42
+ var filteredQueries = qo.queries.filter(function(q) {
43
+ // Param filter
44
+ var paramCount = q.params ? q.params.length : 0;
45
+ if (paramFilter === "with" && paramCount === 0) return false;
46
+ if (paramFilter === "without" && paramCount > 0) return false;
47
+
48
+ // Text search
49
+ if (search) {
50
+ var haystack = [
51
+ qo.class_name,
52
+ q.name,
53
+ q.description || "",
54
+ (q.params || []).map(function(p) { return p.name; }).join(" ")
55
+ ].join(" ").toLowerCase();
56
+ return haystack.indexOf(search) >= 0;
57
+ }
58
+ return true;
59
+ });
60
+
61
+ if (filteredQueries.length > 0) {
62
+ filteredObjects.push({
63
+ class_name: qo.class_name,
64
+ source_location: qo.source_location,
65
+ queries: filteredQueries
66
+ });
67
+ }
68
+ });
69
+
70
+ if (filteredObjects.length > 0) {
71
+ result.push({ namespace: group.namespace, query_objects: filteredObjects });
72
+ }
73
+ });
74
+ return result;
75
+ }
76
+
77
+ function applyFilters() {
78
+ filters.search = document.getElementById("search-input").value;
79
+ filters.namespace = document.getElementById("namespace-filter").value;
80
+
81
+ var filtered = getFilteredData();
82
+ renderSidebar(filtered);
83
+ renderMain(filtered);
84
+ renderFilterSummary(filtered);
85
+ }
86
+
87
+ function setParamFilter(btn) {
88
+ var group = btn.parentElement;
89
+ group.querySelectorAll(".toggle-btn").forEach(function(b) { b.classList.remove("active"); });
90
+ btn.classList.add("active");
91
+ filters.params = btn.getAttribute("data-value");
92
+ applyFilters();
93
+ }
94
+
95
+ function clearFilters() {
96
+ document.getElementById("search-input").value = "";
97
+ document.getElementById("namespace-filter").value = "";
98
+ filters = { search: "", namespace: "", params: "" };
99
+ var toggleBtns = document.querySelectorAll(".toggle-group .toggle-btn");
100
+ toggleBtns.forEach(function(b) { b.classList.remove("active"); });
101
+ toggleBtns[0].classList.add("active");
102
+ applyFilters();
103
+ }
104
+
105
+ function populateNamespaceFilter() {
106
+ var select = document.getElementById("namespace-filter");
107
+ select.innerHTML = '<option value="">All namespaces</option>';
108
+ state.data.forEach(function(group) {
109
+ var ns = group.namespace || "(root)";
110
+ var opt = document.createElement("option");
111
+ opt.value = group.namespace;
112
+ opt.textContent = ns;
113
+ select.appendChild(opt);
114
+ });
115
+ }
116
+
117
+ function renderFilterSummary(filtered) {
118
+ var summary = document.getElementById("filter-summary");
119
+ var isFiltered = filters.search || filters.namespace || filters.params;
120
+
121
+ if (!isFiltered) {
122
+ summary.style.display = "none";
123
+ return;
124
+ }
125
+
126
+ var totalQueries = 0;
127
+ var totalObjects = 0;
128
+ filtered.forEach(function(g) {
129
+ g.query_objects.forEach(function(qo) {
130
+ totalObjects++;
131
+ totalQueries += qo.queries.length;
132
+ });
133
+ });
134
+
135
+ var parts = [];
136
+ if (filters.search) parts.push('matching "' + escapeHtml(filters.search) + '"');
137
+ if (filters.namespace) parts.push("in " + escapeHtml(filters.namespace));
138
+ if (filters.params === "with") parts.push("with parameters");
139
+ if (filters.params === "without") parts.push("without parameters");
140
+
141
+ summary.style.display = "block";
142
+ summary.innerHTML = "Showing " + totalQueries + " queries across " + totalObjects + " objects " + parts.join(", ") +
143
+ '<a class="clear-link" onclick="clearFilters()">Clear filters</a>';
144
+ }
145
+
146
+ // -- Rendering --
147
+
148
+ function renderSidebar(data) {
149
+ var nav = document.getElementById("sidebar-nav");
150
+ if (data.length === 0) {
151
+ nav.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:13px;">No matches</div>';
152
+ return;
153
+ }
154
+
155
+ var html = "";
156
+ data.forEach(function(group) {
157
+ var ns = group.namespace || "(root)";
158
+ html += '<div class="namespace-group">';
159
+ html += '<button class="namespace-toggle open" onclick="toggleNamespace(this)">';
160
+ html += '<span class="arrow">&#9654;</span> ' + highlightText(ns);
161
+ html += '</button>';
162
+ html += '<div class="namespace-children open">';
163
+ group.query_objects.forEach(function(qo) {
164
+ var shortName = qo.class_name.split("::").pop();
165
+ html += '<a class="query-class-link" data-class="' + escapeAttr(qo.class_name) + '" onclick="scrollToClass(\'' + escapeAttr(qo.class_name) + '\')">';
166
+ html += highlightText(shortName);
167
+ html += '</a>';
168
+ });
169
+ html += '</div></div>';
170
+ });
171
+ nav.innerHTML = html;
172
+ }
173
+
174
+ function renderMain(data) {
175
+ var main = document.getElementById("main");
176
+
177
+ if (state.data.length === 0) {
178
+ main.innerHTML = '<div class="empty-state"><h2>No queries registered</h2><p>Define query objects that include ActiveQuery::Base to see them here.</p></div>';
179
+ return;
180
+ }
181
+
182
+ if (data.length === 0) {
183
+ main.innerHTML = '<div class="empty-state"><h2>No matching queries</h2><p>Try adjusting your search or filters.</p></div>';
184
+ return;
185
+ }
186
+
187
+ var total = 0;
188
+ var totalObjects = 0;
189
+ data.forEach(function(g) {
190
+ g.query_objects.forEach(function(qo) {
191
+ totalObjects++;
192
+ total += qo.queries.length;
193
+ });
194
+ });
195
+
196
+ var html = '<div class="main-header"><h1>Query Explorer</h1>';
197
+ html += '<p>' + total + ' queries across ' + totalObjects + ' objects</p></div>';
198
+
199
+ data.forEach(function(group) {
200
+ group.query_objects.forEach(function(qo) {
201
+ html += renderQueryObject(qo);
202
+ });
203
+ });
204
+
205
+ main.innerHTML = html;
206
+ }
207
+
208
+ function renderQueryObject(qo) {
209
+ var id = qo.class_name.replace(/::/g, "-");
210
+ var html = '<div class="query-object" id="qo-' + escapeAttr(id) + '">';
211
+
212
+ html += '<div class="query-object-header">';
213
+ html += '<h2>' + highlightText(qo.class_name) + '</h2>';
214
+ if (qo.source_location) {
215
+ var shortPath = shortenPath(qo.source_location.file);
216
+ html += '<span class="source-location">' + escapeHtml(shortPath) + ':' + qo.source_location.line + '</span>';
217
+ }
218
+ html += '</div>';
219
+
220
+ if (qo.queries.length === 0) {
221
+ html += '<div style="padding:14px 18px;"><span class="no-params">No queries defined</span></div>';
222
+ } else {
223
+ qo.queries.forEach(function(q, i) {
224
+ html += renderQueryCard(q, id + "-" + i, qo.class_name);
225
+ });
226
+ }
227
+
228
+ html += '</div>';
229
+ return html;
230
+ }
231
+
232
+ function renderQueryCard(q, cardId, className) {
233
+ var paramCount = q.params ? q.params.length : 0;
234
+ var html = '<div class="query-card">';
235
+
236
+ html += '<div class="query-card-header" onclick="toggleCard(\'' + cardId + '\')">';
237
+ html += '<span class="arrow" id="arrow-' + cardId + '">&#9654;</span>';
238
+ html += '<span class="query-name">:' + highlightText(q.name) + '</span>';
239
+ if (q.description) {
240
+ html += '<span class="query-description">' + highlightText(q.description) + '</span>';
241
+ }
242
+ if (paramCount > 0) {
243
+ html += '<span class="query-param-count">' + paramCount + ' param' + (paramCount > 1 ? 's' : '') + '</span>';
244
+ }
245
+ html += '</div>';
246
+
247
+ html += '<div class="query-card-body" id="body-' + cardId + '">';
248
+ if (paramCount > 0) {
249
+ html += '<div class="params-section"><h4>Parameters</h4>';
250
+ html += '<table class="params-table"><thead><tr>';
251
+ html += '<th>Name</th><th>Type</th><th>Required</th><th>Default</th>';
252
+ html += '</tr></thead><tbody>';
253
+ q.params.forEach(function(p) {
254
+ html += '<tr>';
255
+ html += '<td class="param-name">' + highlightText(p.name) + '</td>';
256
+ html += '<td><span class="type-tag">' + escapeHtml(p.type || "any") + '</span></td>';
257
+ html += '<td>';
258
+ if (p.optional) {
259
+ html += '<span class="badge badge-optional">optional</span>';
260
+ } else {
261
+ html += '<span class="badge badge-required">required</span>';
262
+ }
263
+ html += '</td>';
264
+ html += '<td>' + (p.default != null ? escapeHtml(String(p.default)) : '<span style="color:var(--text-muted)">—</span>') + '</td>';
265
+ html += '</tr>';
266
+ });
267
+ html += '</tbody></table></div>';
268
+ } else {
269
+ html += '<span class="no-params">No parameters</span>';
270
+ }
271
+
272
+ // Execute form
273
+ html += '<div class="execute-section">';
274
+ html += '<div class="execute-form" id="form-' + cardId + '">';
275
+ if (paramCount > 0) {
276
+ q.params.forEach(function(p) {
277
+ var placeholder = p.optional ? "optional" : "required";
278
+ if (p.default != null) placeholder = "default: " + p.default;
279
+ html += '<div class="form-field">';
280
+ html += '<label>' + escapeHtml(p.name) + ' <span class="type-tag" style="font-size:10px;padding:0 4px;">' + escapeHtml(p.type || "any") + '</span></label>';
281
+ html += '<input type="text" name="' + escapeAttr(p.name) + '" placeholder="' + escapeAttr(placeholder) + '"';
282
+ if (!p.optional) html += ' required';
283
+ html += '>';
284
+ html += '</div>';
285
+ });
286
+ }
287
+ html += '<button class="execute-btn" onclick="executeQuery(\'' + escapeAttr(className) + '\', \'' + escapeAttr(q.name) + '\', \'' + cardId + '\')">Execute</button>';
288
+ html += '</div>';
289
+ html += '<div class="result-section" id="result-' + cardId + '" style="display:none;">';
290
+ html += '<h4>Result</h4>';
291
+ html += '<div class="result-output" id="result-output-' + cardId + '"></div>';
292
+ html += '<div class="result-meta" id="result-meta-' + cardId + '"></div>';
293
+ html += '</div>';
294
+ html += '</div>';
295
+
296
+ html += '</div>';
297
+
298
+ html += '</div>';
299
+ return html;
300
+ }
301
+
302
+ // -- UI helpers --
303
+
304
+ function toggleCard(cardId) {
305
+ var body = document.getElementById("body-" + cardId);
306
+ var header = body.previousElementSibling;
307
+ body.classList.toggle("open");
308
+ header.classList.toggle("open");
309
+ }
310
+
311
+ function toggleNamespace(btn) {
312
+ btn.classList.toggle("open");
313
+ var children = btn.nextElementSibling;
314
+ children.classList.toggle("open");
315
+ }
316
+
317
+ function scrollToClass(className) {
318
+ var id = "qo-" + className.replace(/::/g, "-");
319
+ var el = document.getElementById(id);
320
+ if (el) {
321
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
322
+ }
323
+
324
+ document.querySelectorAll(".query-class-link").forEach(function(link) {
325
+ link.classList.toggle("active", link.getAttribute("data-class") === className);
326
+ });
327
+ }
328
+
329
+ function shortenPath(path) {
330
+ var idx = path.indexOf("/app/");
331
+ return idx >= 0 ? path.substring(idx + 1) : path;
332
+ }
333
+
334
+ // -- Text utilities --
335
+
336
+ function escapeHtml(str) {
337
+ var div = document.createElement("div");
338
+ div.appendChild(document.createTextNode(String(str)));
339
+ return div.innerHTML;
340
+ }
341
+
342
+ function escapeAttr(str) {
343
+ return String(str).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
344
+ }
345
+
346
+ function highlightText(str) {
347
+ var text = escapeHtml(str);
348
+ if (!filters.search) return text;
349
+
350
+ var search = escapeHtml(filters.search);
351
+ var regex = new RegExp("(" + search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
352
+ return text.replace(regex, '<span class="highlight">$1</span>');
353
+ }
354
+
355
+ // -- Keyboard shortcut --
356
+
357
+ document.addEventListener("keydown", function(e) {
358
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
359
+ e.preventDefault();
360
+ var input = document.getElementById("search-input");
361
+ if (input) input.focus();
362
+ }
363
+ });
364
+
365
+ // -- Query execution --
366
+
367
+ function executeQuery(className, queryName, cardId) {
368
+ var form = document.getElementById("form-" + cardId);
369
+ var btn = form.querySelector(".execute-btn");
370
+ var resultSection = document.getElementById("result-" + cardId);
371
+ var resultOutput = document.getElementById("result-output-" + cardId);
372
+ var resultMeta = document.getElementById("result-meta-" + cardId);
373
+
374
+ var args = {};
375
+ var inputs = form.querySelectorAll("input");
376
+ inputs.forEach(function(input) {
377
+ if (input.value !== "") {
378
+ args[input.name] = input.value;
379
+ }
380
+ });
381
+
382
+ btn.disabled = true;
383
+ btn.textContent = "Running...";
384
+ resultSection.style.display = "block";
385
+ resultOutput.className = "result-output";
386
+ resultOutput.textContent = "Executing...";
387
+ resultMeta.textContent = "";
388
+
389
+ var startTime = performance.now();
390
+
391
+ var csrfToken = document.querySelector('meta[name="csrf-token"]');
392
+ var headers = { "Content-Type": "application/json" };
393
+ if (csrfToken) headers["X-CSRF-Token"] = csrfToken.getAttribute("content");
394
+
395
+ fetch(executeUrl, {
396
+ method: "POST",
397
+ headers: headers,
398
+ body: JSON.stringify({
399
+ query_class: className,
400
+ query_name: queryName,
401
+ args: args
402
+ })
403
+ })
404
+ .then(function(r) { return r.json().then(function(data) { return { ok: r.ok, data: data }; }); })
405
+ .then(function(resp) {
406
+ var elapsed = Math.round(performance.now() - startTime);
407
+
408
+ if (resp.ok) {
409
+ var result = resp.data.result;
410
+ var text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
411
+ resultOutput.className = "result-output";
412
+ resultOutput.textContent = text;
413
+
414
+ var meta = elapsed + "ms";
415
+ if (Array.isArray(result)) meta += " \u00b7 " + result.length + " row" + (result.length !== 1 ? "s" : "");
416
+ resultMeta.textContent = meta;
417
+ } else {
418
+ resultOutput.className = "result-output result-error";
419
+ resultOutput.textContent = resp.data.error || "Unknown error";
420
+ resultMeta.textContent = elapsed + "ms";
421
+ }
422
+ })
423
+ .catch(function(err) {
424
+ resultOutput.className = "result-output result-error";
425
+ resultOutput.textContent = "Network error: " + err.message;
426
+ })
427
+ .finally(function() {
428
+ btn.disabled = false;
429
+ btn.textContent = "Execute";
430
+ });
431
+ }
432
+
433
+ fetchQueries();