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,562 @@
1
+ :root {
2
+ --bg: #f8f9fa;
3
+ --surface: #ffffff;
4
+ --border: #dee2e6;
5
+ --text: #212529;
6
+ --text-muted: #6c757d;
7
+ --primary: #4361ee;
8
+ --primary-light: #eef0ff;
9
+ --tag-bg: #e9ecef;
10
+ --code-bg: #f1f3f5;
11
+ --success: #2b9348;
12
+ --sidebar-w: 280px;
13
+ }
14
+
15
+ * { margin: 0; padding: 0; box-sizing: border-box; }
16
+
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
19
+ background: var(--bg);
20
+ color: var(--text);
21
+ line-height: 1.5;
22
+ }
23
+
24
+ /* Layout */
25
+ .layout {
26
+ display: flex;
27
+ height: 100vh;
28
+ }
29
+
30
+ /* Sidebar */
31
+ .sidebar {
32
+ width: var(--sidebar-w);
33
+ min-width: var(--sidebar-w);
34
+ background: var(--surface);
35
+ border-right: 1px solid var(--border);
36
+ overflow-y: auto;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ .sidebar-header {
42
+ padding: 16px;
43
+ border-bottom: 1px solid var(--border);
44
+ font-weight: 600;
45
+ font-size: 15px;
46
+ color: var(--primary);
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ }
51
+
52
+ .sidebar-header button {
53
+ background: none;
54
+ border: 1px solid var(--border);
55
+ border-radius: 4px;
56
+ padding: 4px 8px;
57
+ cursor: pointer;
58
+ font-size: 12px;
59
+ color: var(--text-muted);
60
+ }
61
+
62
+ .sidebar-header button:hover {
63
+ background: var(--bg);
64
+ }
65
+
66
+ .sidebar-nav {
67
+ flex: 1;
68
+ overflow-y: auto;
69
+ padding: 8px 0;
70
+ }
71
+
72
+ .namespace-group {
73
+ margin-bottom: 4px;
74
+ }
75
+
76
+ .namespace-toggle {
77
+ display: flex;
78
+ align-items: center;
79
+ width: 100%;
80
+ padding: 6px 16px;
81
+ background: none;
82
+ border: none;
83
+ cursor: pointer;
84
+ font-size: 13px;
85
+ font-weight: 600;
86
+ color: var(--text);
87
+ text-align: left;
88
+ }
89
+
90
+ .namespace-toggle:hover {
91
+ background: var(--bg);
92
+ }
93
+
94
+ .namespace-toggle .arrow {
95
+ display: inline-block;
96
+ width: 16px;
97
+ font-size: 10px;
98
+ color: var(--text-muted);
99
+ transition: transform 0.15s;
100
+ }
101
+
102
+ .namespace-toggle.open .arrow {
103
+ transform: rotate(90deg);
104
+ }
105
+
106
+ .namespace-children {
107
+ display: none;
108
+ }
109
+
110
+ .namespace-children.open {
111
+ display: block;
112
+ }
113
+
114
+ .query-class-link {
115
+ display: block;
116
+ padding: 4px 16px 4px 32px;
117
+ font-size: 13px;
118
+ color: var(--text-muted);
119
+ text-decoration: none;
120
+ cursor: pointer;
121
+ white-space: nowrap;
122
+ overflow: hidden;
123
+ text-overflow: ellipsis;
124
+ }
125
+
126
+ .query-class-link:hover,
127
+ .query-class-link.active {
128
+ background: var(--primary-light);
129
+ color: var(--primary);
130
+ }
131
+
132
+ /* Main */
133
+ .main {
134
+ flex: 1;
135
+ overflow-y: auto;
136
+ padding: 24px 32px;
137
+ }
138
+
139
+ .main-header {
140
+ margin-bottom: 24px;
141
+ }
142
+
143
+ .main-header h1 {
144
+ font-size: 22px;
145
+ font-weight: 600;
146
+ }
147
+
148
+ .main-header p {
149
+ color: var(--text-muted);
150
+ font-size: 14px;
151
+ margin-top: 4px;
152
+ }
153
+
154
+ /* Query object card */
155
+ .query-object {
156
+ background: var(--surface);
157
+ border: 1px solid var(--border);
158
+ border-radius: 8px;
159
+ margin-bottom: 20px;
160
+ }
161
+
162
+ .query-object-header {
163
+ padding: 14px 18px;
164
+ border-bottom: 1px solid var(--border);
165
+ display: flex;
166
+ align-items: baseline;
167
+ gap: 12px;
168
+ }
169
+
170
+ .query-object-header h2 {
171
+ font-size: 16px;
172
+ font-weight: 600;
173
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
174
+ }
175
+
176
+ .source-location {
177
+ font-size: 12px;
178
+ color: var(--text-muted);
179
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
180
+ }
181
+
182
+ /* Individual query card */
183
+ .query-card {
184
+ border-bottom: 1px solid var(--border);
185
+ }
186
+
187
+ .query-card:last-child {
188
+ border-bottom: none;
189
+ }
190
+
191
+ .query-card-header {
192
+ padding: 12px 18px;
193
+ cursor: pointer;
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 10px;
197
+ user-select: none;
198
+ }
199
+
200
+ .query-card-header:hover {
201
+ background: var(--bg);
202
+ }
203
+
204
+ .query-card-header .arrow {
205
+ font-size: 10px;
206
+ color: var(--text-muted);
207
+ display: inline-block;
208
+ width: 14px;
209
+ transition: transform 0.15s;
210
+ }
211
+
212
+ .query-card-header.open .arrow {
213
+ transform: rotate(90deg);
214
+ }
215
+
216
+ .query-name {
217
+ font-weight: 600;
218
+ font-size: 14px;
219
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
220
+ color: var(--primary);
221
+ }
222
+
223
+ .query-description {
224
+ font-size: 13px;
225
+ color: var(--text-muted);
226
+ }
227
+
228
+ .query-param-count {
229
+ font-size: 11px;
230
+ background: var(--tag-bg);
231
+ padding: 1px 7px;
232
+ border-radius: 10px;
233
+ color: var(--text-muted);
234
+ margin-left: auto;
235
+ white-space: nowrap;
236
+ }
237
+
238
+ .query-card-body {
239
+ display: none;
240
+ padding: 0 18px 16px 42px;
241
+ }
242
+
243
+ .query-card-body.open {
244
+ display: block;
245
+ }
246
+
247
+ /* Params table */
248
+ .params-section h4 {
249
+ font-size: 12px;
250
+ text-transform: uppercase;
251
+ letter-spacing: 0.5px;
252
+ color: var(--text-muted);
253
+ margin-bottom: 8px;
254
+ }
255
+
256
+ .params-table {
257
+ width: 100%;
258
+ border-collapse: collapse;
259
+ font-size: 13px;
260
+ }
261
+
262
+ .params-table th {
263
+ text-align: left;
264
+ padding: 6px 12px 6px 0;
265
+ font-weight: 600;
266
+ font-size: 11px;
267
+ text-transform: uppercase;
268
+ letter-spacing: 0.5px;
269
+ color: var(--text-muted);
270
+ border-bottom: 1px solid var(--border);
271
+ }
272
+
273
+ .params-table td {
274
+ padding: 6px 12px 6px 0;
275
+ border-bottom: 1px solid var(--bg);
276
+ }
277
+
278
+ .params-table .param-name {
279
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
280
+ font-weight: 500;
281
+ }
282
+
283
+ .type-tag {
284
+ display: inline-block;
285
+ background: var(--code-bg);
286
+ padding: 1px 6px;
287
+ border-radius: 3px;
288
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
289
+ font-size: 12px;
290
+ }
291
+
292
+ .badge {
293
+ display: inline-block;
294
+ font-size: 11px;
295
+ padding: 1px 6px;
296
+ border-radius: 3px;
297
+ }
298
+
299
+ .badge-required {
300
+ background: #fff3cd;
301
+ color: #856404;
302
+ }
303
+
304
+ .badge-optional {
305
+ background: #d4edda;
306
+ color: var(--success);
307
+ }
308
+
309
+ .no-params {
310
+ font-size: 13px;
311
+ color: var(--text-muted);
312
+ font-style: italic;
313
+ }
314
+
315
+ /* Execute form */
316
+ .execute-section {
317
+ margin-top: 14px;
318
+ padding-top: 14px;
319
+ border-top: 1px solid var(--border);
320
+ }
321
+
322
+ .execute-form {
323
+ display: flex;
324
+ flex-wrap: wrap;
325
+ gap: 10px;
326
+ align-items: flex-end;
327
+ }
328
+
329
+ .form-field {
330
+ display: flex;
331
+ flex-direction: column;
332
+ gap: 3px;
333
+ }
334
+
335
+ .form-field label {
336
+ font-size: 11px;
337
+ font-weight: 600;
338
+ color: var(--text-muted);
339
+ text-transform: uppercase;
340
+ letter-spacing: 0.3px;
341
+ }
342
+
343
+ .form-field input {
344
+ padding: 6px 10px;
345
+ border: 1px solid var(--border);
346
+ border-radius: 4px;
347
+ font-size: 13px;
348
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
349
+ min-width: 160px;
350
+ }
351
+
352
+ .form-field input:focus {
353
+ outline: none;
354
+ border-color: var(--primary);
355
+ box-shadow: 0 0 0 2px var(--primary-light);
356
+ }
357
+
358
+ .execute-btn {
359
+ padding: 6px 16px;
360
+ background: var(--primary);
361
+ color: #fff;
362
+ border: none;
363
+ border-radius: 4px;
364
+ font-size: 13px;
365
+ font-weight: 500;
366
+ cursor: pointer;
367
+ }
368
+
369
+ .execute-btn:hover {
370
+ opacity: 0.9;
371
+ }
372
+
373
+ .execute-btn:disabled {
374
+ opacity: 0.5;
375
+ cursor: not-allowed;
376
+ }
377
+
378
+ /* Result display */
379
+ .result-section {
380
+ margin-top: 12px;
381
+ }
382
+
383
+ .result-section h4 {
384
+ font-size: 12px;
385
+ text-transform: uppercase;
386
+ letter-spacing: 0.5px;
387
+ color: var(--text-muted);
388
+ margin-bottom: 6px;
389
+ }
390
+
391
+ .result-output {
392
+ background: var(--code-bg);
393
+ border: 1px solid var(--border);
394
+ border-radius: 4px;
395
+ padding: 12px;
396
+ font-family: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
397
+ font-size: 12px;
398
+ line-height: 1.6;
399
+ max-height: 300px;
400
+ overflow: auto;
401
+ white-space: pre-wrap;
402
+ word-break: break-word;
403
+ }
404
+
405
+ .result-error {
406
+ background: #fff5f5;
407
+ border-color: #f5c6cb;
408
+ color: #842029;
409
+ }
410
+
411
+ .result-meta {
412
+ font-size: 11px;
413
+ color: var(--text-muted);
414
+ margin-top: 4px;
415
+ }
416
+
417
+ /* Search & filters */
418
+ .toolbar {
419
+ padding: 12px 32px;
420
+ background: var(--surface);
421
+ border-bottom: 1px solid var(--border);
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 12px;
425
+ flex-wrap: wrap;
426
+ }
427
+
428
+ .search-box {
429
+ flex: 1;
430
+ min-width: 200px;
431
+ position: relative;
432
+ }
433
+
434
+ .search-box input {
435
+ width: 100%;
436
+ padding: 7px 12px 7px 32px;
437
+ border: 1px solid var(--border);
438
+ border-radius: 6px;
439
+ font-size: 13px;
440
+ background: var(--bg);
441
+ }
442
+
443
+ .search-box input:focus {
444
+ outline: none;
445
+ border-color: var(--primary);
446
+ box-shadow: 0 0 0 2px var(--primary-light);
447
+ background: var(--surface);
448
+ }
449
+
450
+ .search-icon {
451
+ position: absolute;
452
+ left: 10px;
453
+ top: 50%;
454
+ transform: translateY(-50%);
455
+ color: var(--text-muted);
456
+ font-size: 13px;
457
+ pointer-events: none;
458
+ }
459
+
460
+ .filter-group {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 4px;
464
+ }
465
+
466
+ .filter-group label {
467
+ font-size: 11px;
468
+ font-weight: 600;
469
+ text-transform: uppercase;
470
+ letter-spacing: 0.3px;
471
+ color: var(--text-muted);
472
+ margin-right: 4px;
473
+ }
474
+
475
+ .filter-select {
476
+ padding: 6px 10px;
477
+ border: 1px solid var(--border);
478
+ border-radius: 4px;
479
+ font-size: 12px;
480
+ background: var(--bg);
481
+ color: var(--text);
482
+ cursor: pointer;
483
+ }
484
+
485
+ .filter-select:focus {
486
+ outline: none;
487
+ border-color: var(--primary);
488
+ }
489
+
490
+ .toggle-group {
491
+ display: flex;
492
+ border: 1px solid var(--border);
493
+ border-radius: 4px;
494
+ overflow: hidden;
495
+ }
496
+
497
+ .toggle-btn {
498
+ padding: 5px 10px;
499
+ font-size: 12px;
500
+ border: none;
501
+ background: var(--bg);
502
+ color: var(--text-muted);
503
+ cursor: pointer;
504
+ border-right: 1px solid var(--border);
505
+ }
506
+
507
+ .toggle-btn:last-child {
508
+ border-right: none;
509
+ }
510
+
511
+ .toggle-btn.active {
512
+ background: var(--primary);
513
+ color: #fff;
514
+ }
515
+
516
+ .toggle-btn:hover:not(.active) {
517
+ background: var(--border);
518
+ }
519
+
520
+ .filter-summary {
521
+ font-size: 12px;
522
+ color: var(--text-muted);
523
+ padding: 0 32px 8px;
524
+ background: var(--surface);
525
+ border-bottom: 1px solid var(--border);
526
+ }
527
+
528
+ .filter-summary .clear-link {
529
+ color: var(--primary);
530
+ cursor: pointer;
531
+ text-decoration: none;
532
+ margin-left: 8px;
533
+ }
534
+
535
+ .filter-summary .clear-link:hover {
536
+ text-decoration: underline;
537
+ }
538
+
539
+ .highlight {
540
+ background: #fff3bf;
541
+ border-radius: 2px;
542
+ }
543
+
544
+ /* Empty state */
545
+ .empty-state {
546
+ text-align: center;
547
+ padding: 60px 20px;
548
+ color: var(--text-muted);
549
+ }
550
+
551
+ .empty-state h2 {
552
+ font-size: 18px;
553
+ margin-bottom: 8px;
554
+ color: var(--text);
555
+ }
556
+
557
+ /* Loading */
558
+ .loading {
559
+ text-align: center;
560
+ padding: 60px;
561
+ color: var(--text-muted);
562
+ }
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveQueryExplorer
4
+ class QueriesController < ActionController::Base
5
+ layout false
6
+
7
+ VALID_QUERY_NAME = /\A[a-zA-Z_]\w*\z/
8
+
9
+ def index
10
+ respond_to do |format|
11
+ format.html
12
+ format.json { render json: discovery.grouped_queries }
13
+ format.text { render plain: text_formatter.format(discovery.grouped_queries) }
14
+ end
15
+ end
16
+
17
+ def execute
18
+ query_name = params[:query_name].to_s
19
+ unless query_name.match?(VALID_QUERY_NAME)
20
+ raise ArgumentError, "Invalid query name: #{query_name}"
21
+ end
22
+
23
+ klass = discovery.find_query_class!(params[:query_class])
24
+ query_def = discovery.find_query_def!(klass, query_name.to_sym)
25
+
26
+ result = executor.execute(klass, query_name.to_sym, params[:args], query_def[:args_def] || {})
27
+
28
+ render json: { result: serializer.serialize(result) }
29
+ rescue ArgumentError, NameError => e
30
+ render json: { error: e.message }, status: :unprocessable_entity
31
+ rescue => e
32
+ Rails.logger.error("ActiveQueryExplorer: #{e.class}: #{e.message}\n#{e.backtrace&.first(10)&.join("\n")}")
33
+ render json: { error: "#{e.class}: #{e.message}" }, status: :internal_server_error
34
+ end
35
+
36
+ private
37
+
38
+ def discovery
39
+ @discovery ||= ActiveQueryExplorer.discovery_class.new
40
+ end
41
+
42
+ def executor
43
+ @executor ||= ActiveQueryExplorer.executor_class.new
44
+ end
45
+
46
+ def serializer
47
+ @serializer ||= ActiveQueryExplorer.serializer_class.new
48
+ end
49
+
50
+ def text_formatter
51
+ @text_formatter ||= ActiveQueryExplorer::QueryTextFormatter.new
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <title>ActiveQuery Explorer</title>
8
+ <%= stylesheet_link_tag "active_query_explorer/application", media: "all" %>
9
+ </head>
10
+ <body data-queries-url="<%= active_query_explorer.queries_path(format: :json) %>"
11
+ data-execute-url="<%= active_query_explorer.execute_queries_path(format: :json) %>">
12
+ <div class="layout">
13
+ <aside class="sidebar">
14
+ <div class="sidebar-header">
15
+ <span>ActiveQuery</span>
16
+ <button onclick="refresh()" title="Reload queries">Refresh</button>
17
+ </div>
18
+ <nav class="sidebar-nav" id="sidebar-nav"></nav>
19
+ </aside>
20
+ <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
21
+ <div class="toolbar" id="toolbar" style="display:none;">
22
+ <div class="search-box">
23
+ <span class="search-icon">&#128269;</span>
24
+ <input type="text" id="search-input" placeholder="Search queries, descriptions, classes..." oninput="applyFilters()">
25
+ </div>
26
+ <div class="filter-group">
27
+ <label>Namespace</label>
28
+ <select class="filter-select" id="namespace-filter" onchange="applyFilters()">
29
+ <option value="">All</option>
30
+ </select>
31
+ </div>
32
+ <div class="filter-group">
33
+ <label>Params</label>
34
+ <div class="toggle-group">
35
+ <button class="toggle-btn active" data-value="" onclick="setParamFilter(this)">All</button>
36
+ <button class="toggle-btn" data-value="with" onclick="setParamFilter(this)">With</button>
37
+ <button class="toggle-btn" data-value="without" onclick="setParamFilter(this)">Without</button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <div id="filter-summary" class="filter-summary" style="display:none;"></div>
42
+ <main class="main" id="main">
43
+ <div class="loading">Loading queries...</div>
44
+ </main>
45
+ </div>
46
+ </div>
47
+
48
+ <%= javascript_include_tag "active_query_explorer/application" %>
49
+ </body>
50
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveQueryExplorer::Engine.routes.draw do
4
+ resources :queries, only: [:index] do
5
+ post :execute, on: :collection
6
+ end
7
+
8
+ root to: "queries#index"
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveQueryExplorer
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace ActiveQueryExplorer
6
+
7
+ initializer "active_query_explorer.assets" do |app|
8
+ app.config.assets.paths << root.join("app", "assets", "stylesheets")
9
+ app.config.assets.paths << root.join("app", "assets", "javascripts")
10
+ app.config.assets.precompile += %w[
11
+ active_query_explorer/application.css
12
+ active_query_explorer/application.js
13
+ ]
14
+ end
15
+
16
+ initializer "active_query_explorer.eager_load_queries" do
17
+ ActiveSupport.on_load(:after_initialize) do
18
+ ActiveQueryExplorer.query_paths.each do |dir|
19
+ # Standard Rails app paths
20
+ path = Rails.root.join("app", dir)
21
+ Rails.autoloaders.main.eager_load_dir(path.to_s) if path.exist?
22
+
23
+ # Packwerk / packs paths (packs/**/app/queries)
24
+ Dir.glob(Rails.root.join("packs", "**", "app", dir).to_s).each do |pack_path|
25
+ Rails.autoloaders.main.eager_load_dir(pack_path) if File.directory?(pack_path)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end