sqlite_dashboard 1.0.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,1274 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>SQLite Dashboard</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
11
+
12
+ <!-- CodeMirror CSS -->
13
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/monokai.min.css">
15
+
16
+ <style>
17
+ :root {
18
+ --primary-color: #2563eb;
19
+ --primary-hover: #1d4ed8;
20
+ --sidebar-bg: #1e293b;
21
+ --sidebar-text: #e2e8f0;
22
+ --sidebar-hover: #334155;
23
+ --main-bg: #f8fafc;
24
+ --card-bg: #ffffff;
25
+ --border-color: #e2e8f0;
26
+ --text-primary: #0f172a;
27
+ --text-secondary: #64748b;
28
+ --success-bg: #dcfce7;
29
+ --success-border: #86efac;
30
+ --success-text: #166534;
31
+ --error-bg: #fee2e2;
32
+ --error-border: #fca5a5;
33
+ --error-text: #991b1b;
34
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
35
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
36
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
37
+ }
38
+
39
+ body {
40
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
41
+ background-color: var(--main-bg);
42
+ color: var(--text-primary);
43
+ }
44
+
45
+ .sqlite-dashboard-container {
46
+ height: 100vh;
47
+ overflow: hidden;
48
+ }
49
+
50
+ .sidebar {
51
+ background-color: var(--sidebar-bg);
52
+ border-right: 2px solid #334155;
53
+ height: 100vh;
54
+ overflow-y: auto;
55
+ box-shadow: var(--shadow-lg);
56
+ }
57
+
58
+ .sidebar h6 {
59
+ color: var(--sidebar-text);
60
+ font-weight: 600;
61
+ letter-spacing: 0.025em;
62
+ }
63
+
64
+ .sidebar .btn-outline-secondary {
65
+ color: var(--sidebar-text);
66
+ border-color: #475569;
67
+ font-size: 0.875rem;
68
+ padding: 0.5rem 1rem;
69
+ transition: all 0.2s ease;
70
+ }
71
+
72
+ .sidebar .btn-outline-secondary:hover {
73
+ background-color: var(--sidebar-hover);
74
+ border-color: #64748b;
75
+ color: #f1f5f9;
76
+ transform: translateX(-2px);
77
+ }
78
+
79
+ .main-content {
80
+ height: 100vh;
81
+ display: flex;
82
+ flex-direction: column;
83
+ background-color: var(--main-bg);
84
+ }
85
+
86
+ .query-console {
87
+ background-color: var(--card-bg);
88
+ border-bottom: 2px solid var(--border-color);
89
+ padding: 1.5rem;
90
+ flex-shrink: 0;
91
+ box-shadow: var(--shadow-sm);
92
+ }
93
+
94
+ .query-console .form-label {
95
+ color: var(--text-primary);
96
+ font-size: 0.875rem;
97
+ font-weight: 600;
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.05em;
100
+ margin-bottom: 0.75rem;
101
+ }
102
+
103
+ .query-textarea {
104
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
105
+ font-size: 13px;
106
+ min-height: 120px;
107
+ }
108
+
109
+ .CodeMirror {
110
+ border: 2px solid var(--border-color);
111
+ border-radius: 0.5rem;
112
+ height: 150px;
113
+ font-size: 14px;
114
+ box-shadow: var(--shadow-sm);
115
+ transition: all 0.2s ease;
116
+ }
117
+
118
+ .CodeMirror-focused {
119
+ border-color: var(--primary-color);
120
+ outline: 0;
121
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
122
+ }
123
+
124
+ .btn-primary {
125
+ background-color: var(--primary-color);
126
+ border-color: var(--primary-color);
127
+ padding: 0.625rem 1.25rem;
128
+ font-weight: 500;
129
+ transition: all 0.2s ease;
130
+ box-shadow: var(--shadow-sm);
131
+ }
132
+
133
+ .btn-primary:hover {
134
+ background-color: var(--primary-hover);
135
+ border-color: var(--primary-hover);
136
+ transform: translateY(-1px);
137
+ box-shadow: var(--shadow-md);
138
+ }
139
+
140
+ .btn-outline-secondary {
141
+ border-color: var(--border-color);
142
+ color: var(--text-secondary);
143
+ padding: 0.625rem 1.25rem;
144
+ font-weight: 500;
145
+ transition: all 0.2s ease;
146
+ }
147
+
148
+ .btn-outline-secondary:hover {
149
+ background-color: var(--main-bg);
150
+ border-color: var(--text-secondary);
151
+ color: var(--text-primary);
152
+ }
153
+
154
+ .results-container {
155
+ flex: 1;
156
+ overflow: auto;
157
+ padding: 1.5rem;
158
+ background-color: var(--main-bg);
159
+ }
160
+
161
+ .table-wrapper {
162
+ overflow-x: auto;
163
+ max-width: 100%;
164
+ border: 2px solid var(--border-color);
165
+ border-radius: 0.5rem;
166
+ margin-bottom: 1rem;
167
+ background-color: var(--card-bg);
168
+ box-shadow: var(--shadow-md);
169
+ }
170
+
171
+ .table-wrapper table {
172
+ margin-bottom: 0;
173
+ }
174
+
175
+ .table {
176
+ color: var(--text-primary);
177
+ }
178
+
179
+ .table thead {
180
+ background-color: var(--sidebar-bg);
181
+ color: var(--sidebar-text);
182
+ font-weight: 600;
183
+ text-transform: uppercase;
184
+ font-size: 0.75rem;
185
+ letter-spacing: 0.05em;
186
+ }
187
+
188
+ .table thead th {
189
+ padding: 1rem 0.75rem;
190
+ border-bottom: 2px solid #334155;
191
+ }
192
+
193
+ .table tbody tr {
194
+ transition: all 0.15s ease;
195
+ }
196
+
197
+ .table-striped tbody tr:nth-of-type(odd) {
198
+ background-color: #f8fafc;
199
+ }
200
+
201
+ .table-hover tbody tr:hover {
202
+ background-color: #e0f2fe;
203
+ transform: scale(1.001);
204
+ }
205
+
206
+ .table tbody td {
207
+ padding: 0.875rem 0.75rem;
208
+ border-color: var(--border-color);
209
+ font-size: 0.875rem;
210
+ }
211
+
212
+ .export-controls {
213
+ display: flex;
214
+ justify-content: flex-end;
215
+ margin-bottom: 1rem;
216
+ }
217
+
218
+ .btn-group {
219
+ box-shadow: var(--shadow-sm);
220
+ border-radius: 0.375rem;
221
+ }
222
+
223
+ .btn-group .btn {
224
+ border-radius: 0;
225
+ }
226
+
227
+ .btn-group .btn:first-child {
228
+ border-top-left-radius: 0.375rem;
229
+ border-bottom-left-radius: 0.375rem;
230
+ }
231
+
232
+ .btn-group .btn:last-child {
233
+ border-top-right-radius: 0.375rem;
234
+ border-bottom-right-radius: 0.375rem;
235
+ }
236
+
237
+ .btn-success {
238
+ background-color: #16a34a;
239
+ border-color: #16a34a;
240
+ color: white;
241
+ font-weight: 500;
242
+ transition: all 0.2s ease;
243
+ }
244
+
245
+ .btn-success:hover {
246
+ background-color: #15803d;
247
+ border-color: #15803d;
248
+ transform: translateY(-1px);
249
+ box-shadow: var(--shadow-md);
250
+ }
251
+
252
+ .btn-info {
253
+ background-color: #0ea5e9;
254
+ border-color: #0ea5e9;
255
+ color: white;
256
+ font-weight: 500;
257
+ transition: all 0.2s ease;
258
+ }
259
+
260
+ .btn-info:hover {
261
+ background-color: #0284c7;
262
+ border-color: #0284c7;
263
+ transform: translateY(-1px);
264
+ box-shadow: var(--shadow-md);
265
+ }
266
+
267
+ .pagination-controls {
268
+ display: grid;
269
+ grid-template-columns: 1fr auto 1fr;
270
+ align-items: center;
271
+ padding: 1rem 1.5rem;
272
+ gap: 1.5rem;
273
+ background-color: var(--card-bg);
274
+ border-radius: 0.5rem;
275
+ margin-bottom: 1rem;
276
+ box-shadow: var(--shadow-sm);
277
+ border: 2px solid var(--border-color);
278
+ }
279
+
280
+ @media (max-width: 768px) {
281
+ .pagination-controls {
282
+ grid-template-columns: 1fr;
283
+ gap: 1rem;
284
+ }
285
+
286
+ .rows-per-page {
287
+ justify-content: center;
288
+ }
289
+
290
+ .pagination-info {
291
+ text-align: center;
292
+ order: 3;
293
+ }
294
+
295
+ .pagination-buttons {
296
+ justify-content: center;
297
+ order: 2;
298
+ }
299
+ }
300
+
301
+ .pagination-info {
302
+ color: var(--text-secondary);
303
+ font-weight: 500;
304
+ font-size: 0.875rem;
305
+ text-align: center;
306
+ white-space: nowrap;
307
+ }
308
+
309
+ .pagination-buttons {
310
+ display: flex;
311
+ gap: 0.5rem;
312
+ align-items: center;
313
+ justify-content: flex-end;
314
+ }
315
+
316
+ .pagination-buttons .btn {
317
+ padding: 0.5rem 0.75rem;
318
+ border-width: 2px;
319
+ min-width: 2.5rem;
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ }
324
+
325
+ .pagination-buttons span {
326
+ font-weight: 500;
327
+ font-size: 0.875rem;
328
+ color: var(--text-primary);
329
+ }
330
+
331
+ .rows-per-page {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 0.75rem;
335
+ justify-content: flex-start;
336
+ }
337
+
338
+ .rows-per-page label {
339
+ font-weight: 500;
340
+ font-size: 0.875rem;
341
+ color: var(--text-secondary);
342
+ margin: 0;
343
+ white-space: nowrap;
344
+ }
345
+
346
+ .rows-per-page select {
347
+ width: auto;
348
+ min-width: 80px;
349
+ border: 2px solid var(--border-color);
350
+ border-radius: 0.375rem;
351
+ padding: 0.375rem 0.75rem;
352
+ font-weight: 500;
353
+ background-color: var(--card-bg);
354
+ transition: all 0.2s ease;
355
+ }
356
+
357
+ .rows-per-page select:focus {
358
+ border-color: var(--primary-color);
359
+ outline: 0;
360
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
361
+ }
362
+
363
+ .table-sidebar {
364
+ list-style: none;
365
+ padding: 0;
366
+ margin: 0;
367
+ }
368
+
369
+ .table-sidebar li {
370
+ padding: 0.75rem 1rem;
371
+ cursor: pointer;
372
+ border-bottom: 1px solid #334155;
373
+ color: var(--sidebar-text);
374
+ transition: all 0.2s ease;
375
+ font-size: 0.875rem;
376
+ display: flex;
377
+ align-items: center;
378
+ gap: 0.5rem;
379
+ }
380
+
381
+ .table-sidebar li i {
382
+ color: #94a3b8;
383
+ width: 16px;
384
+ }
385
+
386
+ .table-sidebar li:hover {
387
+ background-color: var(--sidebar-hover);
388
+ padding-left: 1.25rem;
389
+ color: #f1f5f9;
390
+ }
391
+
392
+ .table-sidebar li.active {
393
+ background-color: var(--primary-color);
394
+ color: white;
395
+ border-left: 4px solid #60a5fa;
396
+ font-weight: 600;
397
+ }
398
+
399
+ .table-sidebar li.active i {
400
+ color: #93c5fd;
401
+ }
402
+
403
+ .error-message {
404
+ background-color: var(--error-bg);
405
+ border: 2px solid var(--error-border);
406
+ border-left: 4px solid #dc2626;
407
+ color: var(--error-text);
408
+ padding: 1.25rem;
409
+ border-radius: 0.5rem;
410
+ box-shadow: var(--shadow-sm);
411
+ font-size: 0.875rem;
412
+ }
413
+
414
+ .error-message i {
415
+ margin-right: 0.5rem;
416
+ }
417
+
418
+ .success-message {
419
+ background-color: var(--success-bg);
420
+ border: 2px solid var(--success-border);
421
+ border-left: 4px solid #16a34a;
422
+ color: var(--success-text);
423
+ padding: 1.25rem;
424
+ border-radius: 0.5rem;
425
+ box-shadow: var(--shadow-sm);
426
+ font-size: 0.875rem;
427
+ }
428
+
429
+ .success-message i {
430
+ margin-right: 0.5rem;
431
+ }
432
+
433
+ /* Database index page */
434
+ .databases-index-page {
435
+ background-color: var(--main-bg);
436
+ }
437
+
438
+ .index-sidebar {
439
+ position: relative;
440
+ }
441
+
442
+ .sidebar-brand {
443
+ display: flex;
444
+ align-items: center;
445
+ gap: 1rem;
446
+ padding: 1rem;
447
+ background-color: rgba(37, 99, 235, 0.1);
448
+ border-radius: 0.5rem;
449
+ margin-bottom: 1.5rem;
450
+ }
451
+
452
+ .sidebar-brand i {
453
+ font-size: 2rem;
454
+ color: var(--primary-color);
455
+ }
456
+
457
+ .sidebar-brand h6 {
458
+ color: var(--sidebar-text);
459
+ font-weight: 600;
460
+ font-size: 0.875rem;
461
+ }
462
+
463
+ .sidebar-brand small {
464
+ color: #94a3b8;
465
+ font-size: 0.75rem;
466
+ }
467
+
468
+ .sidebar-stats {
469
+ background-color: rgba(255, 255, 255, 0.05);
470
+ border-radius: 0.5rem;
471
+ padding: 1rem;
472
+ }
473
+
474
+ .stat-item {
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 1rem;
478
+ }
479
+
480
+ .stat-item i {
481
+ font-size: 1.5rem;
482
+ color: var(--primary-color);
483
+ }
484
+
485
+ .stat-value {
486
+ font-size: 1.5rem;
487
+ font-weight: 700;
488
+ color: var(--sidebar-text);
489
+ line-height: 1;
490
+ }
491
+
492
+ .stat-label {
493
+ font-size: 0.75rem;
494
+ color: #94a3b8;
495
+ text-transform: uppercase;
496
+ letter-spacing: 0.05em;
497
+ }
498
+
499
+ .database-list {
500
+ list-style: none;
501
+ padding: 0;
502
+ margin: 0;
503
+ }
504
+
505
+ .database-list li {
506
+ margin-bottom: 0.25rem;
507
+ }
508
+
509
+ .database-link {
510
+ display: flex;
511
+ align-items: center;
512
+ gap: 0.75rem;
513
+ padding: 0.75rem;
514
+ color: var(--sidebar-text);
515
+ text-decoration: none;
516
+ border-radius: 0.375rem;
517
+ transition: all 0.2s ease;
518
+ font-size: 0.875rem;
519
+ }
520
+
521
+ .database-link i:first-child {
522
+ color: #94a3b8;
523
+ width: 16px;
524
+ }
525
+
526
+ .database-link span {
527
+ flex: 1;
528
+ }
529
+
530
+ .database-link .ms-auto {
531
+ opacity: 0;
532
+ transition: opacity 0.2s ease;
533
+ }
534
+
535
+ .database-link:hover {
536
+ background-color: var(--sidebar-hover);
537
+ color: #f1f5f9;
538
+ padding-left: 1rem;
539
+ }
540
+
541
+ .database-link:hover .ms-auto {
542
+ opacity: 1;
543
+ }
544
+
545
+ .index-main-content {
546
+ padding: 2rem 3rem;
547
+ }
548
+
549
+ .content-header {
550
+ margin-bottom: 2.5rem;
551
+ padding-bottom: 1.5rem;
552
+ border-bottom: 2px solid var(--border-color);
553
+ }
554
+
555
+ .page-title {
556
+ font-size: 2rem;
557
+ font-weight: 700;
558
+ color: var(--text-primary);
559
+ margin: 0 0 0.5rem 0;
560
+ display: flex;
561
+ align-items: center;
562
+ gap: 0.75rem;
563
+ }
564
+
565
+ .page-title i {
566
+ color: var(--primary-color);
567
+ }
568
+
569
+ .page-subtitle {
570
+ color: var(--text-secondary);
571
+ font-size: 1rem;
572
+ margin: 0;
573
+ }
574
+
575
+ .databases-grid {
576
+ display: grid;
577
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
578
+ gap: 1.5rem;
579
+ }
580
+
581
+ .database-card-modern {
582
+ background-color: var(--card-bg);
583
+ border: 2px solid var(--border-color);
584
+ border-radius: 0.75rem;
585
+ padding: 1.5rem;
586
+ transition: all 0.2s ease;
587
+ box-shadow: var(--shadow-sm);
588
+ display: flex;
589
+ flex-direction: column;
590
+ gap: 1rem;
591
+ }
592
+
593
+ .database-card-modern:hover {
594
+ transform: translateY(-4px);
595
+ box-shadow: var(--shadow-lg);
596
+ border-color: var(--primary-color);
597
+ }
598
+
599
+ .database-card-icon {
600
+ width: 60px;
601
+ height: 60px;
602
+ background-color: rgba(37, 99, 235, 0.1);
603
+ border-radius: 0.75rem;
604
+ display: flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ font-size: 1.75rem;
608
+ color: var(--primary-color);
609
+ }
610
+
611
+ .database-card-content {
612
+ flex: 1;
613
+ }
614
+
615
+ .database-card-title {
616
+ font-size: 1.25rem;
617
+ font-weight: 700;
618
+ color: var(--text-primary);
619
+ margin: 0 0 0.75rem 0;
620
+ }
621
+
622
+ .database-card-path {
623
+ color: var(--text-secondary);
624
+ font-size: 0.875rem;
625
+ margin: 0;
626
+ display: flex;
627
+ align-items: center;
628
+ gap: 0.5rem;
629
+ word-break: break-all;
630
+ }
631
+
632
+ .database-card-path i {
633
+ flex-shrink: 0;
634
+ color: #94a3b8;
635
+ }
636
+
637
+ .database-card-actions {
638
+ padding-top: 1rem;
639
+ border-top: 2px solid var(--border-color);
640
+ }
641
+
642
+ .btn-card-action {
643
+ width: 100%;
644
+ display: flex;
645
+ align-items: center;
646
+ justify-content: center;
647
+ gap: 0.5rem;
648
+ padding: 0.75rem 1.25rem;
649
+ }
650
+
651
+ .btn-card-action i {
652
+ transition: transform 0.2s ease;
653
+ }
654
+
655
+ .btn-card-action:hover i {
656
+ transform: translateX(4px);
657
+ }
658
+
659
+ /* Empty state */
660
+ .empty-state-modern {
661
+ max-width: 700px;
662
+ margin: 4rem auto;
663
+ text-align: center;
664
+ background-color: var(--card-bg);
665
+ padding: 3rem 2rem;
666
+ border-radius: 0.75rem;
667
+ border: 2px solid var(--border-color);
668
+ box-shadow: var(--shadow-sm);
669
+ }
670
+
671
+ .empty-state-modern .empty-state-icon {
672
+ width: 80px;
673
+ height: 80px;
674
+ background-color: var(--main-bg);
675
+ border-radius: 50%;
676
+ display: inline-flex;
677
+ align-items: center;
678
+ justify-content: center;
679
+ font-size: 2.5rem;
680
+ color: #94a3b8;
681
+ margin-bottom: 1.5rem;
682
+ }
683
+
684
+ .empty-state-modern h3 {
685
+ font-size: 1.5rem;
686
+ font-weight: 700;
687
+ color: var(--text-primary);
688
+ margin-bottom: 0.5rem;
689
+ }
690
+
691
+ .empty-state-modern p {
692
+ color: var(--text-secondary);
693
+ margin-bottom: 2rem;
694
+ }
695
+
696
+ .empty-state-code {
697
+ background-color: var(--main-bg);
698
+ border: 2px solid var(--border-color);
699
+ border-radius: 0.5rem;
700
+ padding: 1.5rem;
701
+ text-align: left;
702
+ }
703
+
704
+ .empty-state-code pre {
705
+ margin: 0;
706
+ background-color: transparent;
707
+ border: none;
708
+ padding: 0;
709
+ }
710
+
711
+ .empty-state-code code {
712
+ color: var(--text-primary);
713
+ font-size: 0.875rem;
714
+ }
715
+
716
+ /* Legacy card support */
717
+ .card {
718
+ border: 2px solid var(--border-color);
719
+ border-radius: 0.5rem;
720
+ transition: all 0.2s ease;
721
+ box-shadow: var(--shadow-sm);
722
+ background-color: var(--card-bg);
723
+ }
724
+
725
+ .card:hover {
726
+ transform: translateY(-4px);
727
+ box-shadow: var(--shadow-lg);
728
+ border-color: var(--primary-color);
729
+ }
730
+
731
+ .card-title {
732
+ color: var(--text-primary);
733
+ font-weight: 600;
734
+ }
735
+
736
+ .card-text {
737
+ color: var(--text-secondary);
738
+ }
739
+
740
+ /* Empty state styling */
741
+ .text-muted.text-center {
742
+ color: var(--text-secondary) !important;
743
+ }
744
+
745
+ .text-white {
746
+ color: white !important;
747
+ }
748
+
749
+ .text-muted.text-center i {
750
+ color: var(--border-color);
751
+ }
752
+
753
+ /* Alert styling */
754
+ .alert-warning {
755
+ background-color: #fef3c7;
756
+ border: 2px solid #fde047;
757
+ border-left: 4px solid #eab308;
758
+ color: #713f12;
759
+ border-radius: 0.5rem;
760
+ }
761
+
762
+ .alert-warning .alert-heading {
763
+ font-weight: 600;
764
+ color: #713f12;
765
+ }
766
+
767
+ .alert-warning pre {
768
+ background-color: #fefce8;
769
+ border: 1px solid #fde047;
770
+ border-radius: 0.375rem;
771
+ padding: 1rem;
772
+ color: #854d0e;
773
+ }
774
+
775
+ /* Scrollbar styling */
776
+ .sidebar::-webkit-scrollbar,
777
+ .results-container::-webkit-scrollbar {
778
+ width: 8px;
779
+ }
780
+
781
+ .sidebar::-webkit-scrollbar-track {
782
+ background: #0f172a;
783
+ }
784
+
785
+ .sidebar::-webkit-scrollbar-thumb {
786
+ background: #475569;
787
+ border-radius: 4px;
788
+ }
789
+
790
+ .sidebar::-webkit-scrollbar-thumb:hover {
791
+ background: #64748b;
792
+ }
793
+
794
+ .results-container::-webkit-scrollbar-track {
795
+ background: #f1f5f9;
796
+ }
797
+
798
+ .results-container::-webkit-scrollbar-thumb {
799
+ background: #cbd5e1;
800
+ border-radius: 4px;
801
+ }
802
+
803
+ .results-container::-webkit-scrollbar-thumb:hover {
804
+ background: #94a3b8;
805
+ }
806
+ </style>
807
+
808
+ <!-- CodeMirror JavaScript -->
809
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>
810
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/sql/sql.min.js"></script>
811
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/edit/matchbrackets.min.js"></script>
812
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/edit/closebrackets.min.js"></script>
813
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/hint/show-hint.min.js"></script>
814
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/hint/sql-hint.min.js"></script>
815
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/hint/show-hint.min.css">
816
+
817
+ <script type="module">
818
+ import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
819
+
820
+ // Query Executor Controller
821
+ class QueryExecutorController extends Controller {
822
+ static targets = ["queryInput"]
823
+
824
+ initialize() {
825
+ this.currentPage = 1
826
+ this.rowsPerPage = 25
827
+ this.allData = null
828
+ }
829
+
830
+ connect() {
831
+ // Initialize CodeMirror on the textarea
832
+ if (this.hasQueryInputTarget && !this.editor) {
833
+ this.editor = CodeMirror.fromTextArea(this.queryInputTarget, {
834
+ mode: 'text/x-sql',
835
+ theme: 'default',
836
+ lineNumbers: true,
837
+ matchBrackets: true,
838
+ autoCloseBrackets: true,
839
+ lineWrapping: true,
840
+ extraKeys: {
841
+ "Ctrl-Space": "autocomplete",
842
+ "Cmd-Enter": () => this.executeQuery(),
843
+ "Ctrl-Enter": () => this.executeQuery()
844
+ }
845
+ })
846
+
847
+ // Set initial value
848
+ this.editor.setValue(this.queryInputTarget.value || "SELECT * FROM sqlite_master WHERE type='table';")
849
+
850
+ // Store editor reference globally for table selector
851
+ window.sqlEditor = this.editor
852
+ }
853
+ }
854
+
855
+ execute(event) {
856
+ event.preventDefault()
857
+ this.executeQuery()
858
+ }
859
+
860
+ executeQuery() {
861
+ const form = this.element
862
+ const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
863
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
864
+
865
+ // Create FormData and add the query
866
+ const formData = new FormData()
867
+ formData.append('query', query)
868
+
869
+ fetch(form.action, {
870
+ method: form.method || 'POST',
871
+ body: formData,
872
+ headers: {
873
+ 'X-CSRF-Token': csrfToken,
874
+ 'Accept': 'application/json'
875
+ }
876
+ })
877
+ .then(response => response.json())
878
+ .then(data => {
879
+ console.log("Query response:", data)
880
+ this.allData = data
881
+ this.currentPage = 1
882
+ this.displayResults()
883
+ })
884
+ .catch(error => {
885
+ console.error("Query error:", error)
886
+ this.displayError(error.message || "An error occurred")
887
+ })
888
+ }
889
+
890
+ displayResults() {
891
+ const resultsDiv = document.getElementById("query-results")
892
+
893
+ if (!this.allData) return
894
+
895
+ if (this.allData.error) {
896
+ this.displayError(this.allData.error)
897
+ return
898
+ }
899
+
900
+ if (this.allData.columns && this.allData.columns.length > 0) {
901
+ const totalRows = this.allData.rows.length
902
+ const totalPages = Math.ceil(totalRows / this.rowsPerPage)
903
+ const startIndex = (this.currentPage - 1) * this.rowsPerPage
904
+ const endIndex = Math.min(startIndex + this.rowsPerPage, totalRows)
905
+ const pageRows = this.allData.rows.slice(startIndex, endIndex)
906
+
907
+ let html = `
908
+ <div class="export-controls">
909
+ <div class="btn-group">
910
+ <button class="btn btn-sm btn-success" onclick="window.queryController.showExportModal('csv')">
911
+ <i class="fas fa-file-csv"></i> Export CSV
912
+ </button>
913
+ <button class="btn btn-sm btn-info" onclick="window.queryController.showExportModal('json')">
914
+ <i class="fas fa-file-code"></i> Export JSON
915
+ </button>
916
+ </div>
917
+ </div>
918
+ <div class="pagination-controls">
919
+ <div class="rows-per-page">
920
+ <label for="rows-per-page">Rows per page:</label>
921
+ <select id="rows-per-page" class="form-select form-select-sm" onchange="window.queryController.changeRowsPerPage(this.value)">
922
+ <option value="10" ${this.rowsPerPage == 10 ? 'selected' : ''}>10</option>
923
+ <option value="25" ${this.rowsPerPage == 25 ? 'selected' : ''}>25</option>
924
+ <option value="50" ${this.rowsPerPage == 50 ? 'selected' : ''}>50</option>
925
+ <option value="100" ${this.rowsPerPage == 100 ? 'selected' : ''}>100</option>
926
+ <option value="500" ${this.rowsPerPage == 500 ? 'selected' : ''}>500</option>
927
+ </select>
928
+ </div>
929
+ <div class="pagination-info">
930
+ Showing ${startIndex + 1} to ${endIndex} of ${totalRows} rows
931
+ </div>
932
+ <div class="pagination-buttons">
933
+ <button class="btn btn-sm btn-outline-secondary"
934
+ onclick="window.queryController.firstPage()"
935
+ ${this.currentPage === 1 ? 'disabled' : ''}>
936
+ <i class="fas fa-angle-double-left"></i>
937
+ </button>
938
+ <button class="btn btn-sm btn-outline-secondary"
939
+ onclick="window.queryController.previousPage()"
940
+ ${this.currentPage === 1 ? 'disabled' : ''}>
941
+ <i class="fas fa-angle-left"></i>
942
+ </button>
943
+ <span class="px-3">Page ${this.currentPage} of ${totalPages}</span>
944
+ <button class="btn btn-sm btn-outline-secondary"
945
+ onclick="window.queryController.nextPage()"
946
+ ${this.currentPage === totalPages ? 'disabled' : ''}>
947
+ <i class="fas fa-angle-right"></i>
948
+ </button>
949
+ <button class="btn btn-sm btn-outline-secondary"
950
+ onclick="window.queryController.lastPage()"
951
+ ${this.currentPage === totalPages ? 'disabled' : ''}>
952
+ <i class="fas fa-angle-double-right"></i>
953
+ </button>
954
+ </div>
955
+ </div>
956
+ <div class="table-wrapper">
957
+ <table class="table table-striped table-hover table-bordered table-sm">
958
+ <thead class="table-dark sticky-top">
959
+ <tr>
960
+ ${this.allData.columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}
961
+ </tr>
962
+ </thead>
963
+ <tbody>
964
+ ${pageRows.map(row =>
965
+ `<tr>${row.map(val => `<td>${this.escapeHtml(String(val || ''))}</td>`).join('')}</tr>`
966
+ ).join('')}
967
+ </tbody>
968
+ </table>
969
+ </div>
970
+ `
971
+ resultsDiv.innerHTML = html
972
+
973
+ // Store reference for pagination controls
974
+ window.queryController = this
975
+ } else if (this.allData.message) {
976
+ resultsDiv.innerHTML = `
977
+ <div class="success-message">
978
+ <i class="fas fa-check-circle"></i> ${this.escapeHtml(this.allData.message)}
979
+ </div>
980
+ `
981
+ } else {
982
+ resultsDiv.innerHTML = `
983
+ <div class="text-muted text-center py-5">
984
+ <p>No results returned</p>
985
+ </div>
986
+ `
987
+ }
988
+ }
989
+
990
+ changeRowsPerPage(value) {
991
+ this.rowsPerPage = parseInt(value)
992
+ this.currentPage = 1
993
+ this.displayResults()
994
+ }
995
+
996
+ firstPage() {
997
+ this.currentPage = 1
998
+ this.displayResults()
999
+ }
1000
+
1001
+ previousPage() {
1002
+ if (this.currentPage > 1) {
1003
+ this.currentPage--
1004
+ this.displayResults()
1005
+ }
1006
+ }
1007
+
1008
+ nextPage() {
1009
+ const totalPages = Math.ceil(this.allData.rows.length / this.rowsPerPage)
1010
+ if (this.currentPage < totalPages) {
1011
+ this.currentPage++
1012
+ this.displayResults()
1013
+ }
1014
+ }
1015
+
1016
+ lastPage() {
1017
+ this.currentPage = Math.ceil(this.allData.rows.length / this.rowsPerPage)
1018
+ this.displayResults()
1019
+ }
1020
+
1021
+ displayError(error) {
1022
+ const resultsDiv = document.getElementById("query-results")
1023
+ resultsDiv.innerHTML = `
1024
+ <div class="error-message">
1025
+ <i class="fas fa-exclamation-circle"></i> <strong>Error:</strong> ${this.escapeHtml(error)}
1026
+ </div>
1027
+ `
1028
+ }
1029
+
1030
+ escapeHtml(text) {
1031
+ const div = document.createElement('div')
1032
+ div.textContent = text
1033
+ return div.innerHTML
1034
+ }
1035
+
1036
+ clear() {
1037
+ if (this.editor) {
1038
+ this.editor.setValue("")
1039
+ this.editor.focus()
1040
+ } else {
1041
+ this.queryInputTarget.value = ""
1042
+ this.queryInputTarget.focus()
1043
+ }
1044
+ }
1045
+
1046
+ showExportModal(type = 'csv') {
1047
+ const existingModal = document.getElementById('export-modal')
1048
+ if (existingModal) {
1049
+ existingModal.remove()
1050
+ }
1051
+
1052
+ let modalBody = ''
1053
+ let modalTitle = ''
1054
+ let downloadButton = ''
1055
+
1056
+ if (type === 'csv') {
1057
+ modalTitle = 'Export to CSV'
1058
+ modalBody = `
1059
+ <div class="mb-3">
1060
+ <label for="csv-separator" class="form-label">Separator</label>
1061
+ <select id="csv-separator" class="form-select">
1062
+ <option value=",">Comma (,)</option>
1063
+ <option value=";">Semicolon (;)</option>
1064
+ <option value="\t">Tab</option>
1065
+ <option value="|">Pipe (|)</option>
1066
+ </select>
1067
+ </div>
1068
+ <div class="form-check">
1069
+ <input class="form-check-input" type="checkbox" id="csv-headers" checked>
1070
+ <label class="form-check-label" for="csv-headers">
1071
+ Include headers as first row
1072
+ </label>
1073
+ </div>
1074
+ `
1075
+ downloadButton = `
1076
+ <button type="button" class="btn btn-success" onclick="window.queryController.exportCSV()">
1077
+ <i class="fas fa-download"></i> Download CSV
1078
+ </button>
1079
+ `
1080
+ } else if (type === 'json') {
1081
+ modalTitle = 'Export to JSON'
1082
+ modalBody = `
1083
+ <div class="mb-3">
1084
+ <label for="json-format" class="form-label">Format</label>
1085
+ <select id="json-format" class="form-select">
1086
+ <option value="array">Array of Objects</option>
1087
+ <option value="object">Object with Columns & Rows</option>
1088
+ </select>
1089
+ <small class="form-text text-muted">
1090
+ Array: [{"col1": "val1"}, ...] | Object: {"columns": [...], "rows": [...]}
1091
+ </small>
1092
+ </div>
1093
+ <div class="form-check">
1094
+ <input class="form-check-input" type="checkbox" id="json-pretty" checked>
1095
+ <label class="form-check-label" for="json-pretty">
1096
+ Pretty print (formatted with indentation)
1097
+ </label>
1098
+ </div>
1099
+ `
1100
+ downloadButton = `
1101
+ <button type="button" class="btn btn-info" onclick="window.queryController.exportJSON()">
1102
+ <i class="fas fa-download"></i> Download JSON
1103
+ </button>
1104
+ `
1105
+ }
1106
+
1107
+ const modalHtml = `
1108
+ <div class="modal fade" id="export-modal" tabindex="-1">
1109
+ <div class="modal-dialog">
1110
+ <div class="modal-content">
1111
+ <div class="modal-header">
1112
+ <h5 class="modal-title">${modalTitle}</h5>
1113
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
1114
+ </div>
1115
+ <div class="modal-body">
1116
+ ${modalBody}
1117
+ </div>
1118
+ <div class="modal-footer">
1119
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
1120
+ ${downloadButton}
1121
+ </div>
1122
+ </div>
1123
+ </div>
1124
+ </div>
1125
+ `
1126
+
1127
+ document.body.insertAdjacentHTML('beforeend', modalHtml)
1128
+ const modal = new bootstrap.Modal(document.getElementById('export-modal'))
1129
+ modal.show()
1130
+ }
1131
+
1132
+ exportCSV() {
1133
+ const separator = document.getElementById('csv-separator').value
1134
+ const includeHeaders = document.getElementById('csv-headers').checked
1135
+ const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
1136
+ const form = this.element
1137
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
1138
+
1139
+ const formData = new FormData()
1140
+ formData.append('query', query)
1141
+ formData.append('separator', separator)
1142
+ formData.append('include_headers', includeHeaders)
1143
+
1144
+ fetch(form.action.replace('/execute_query', '/export_csv'), {
1145
+ method: 'POST',
1146
+ body: formData,
1147
+ headers: {
1148
+ 'X-CSRF-Token': csrfToken
1149
+ }
1150
+ })
1151
+ .then(response => {
1152
+ if (!response.ok) {
1153
+ return response.json().then(err => { throw new Error(err.error || 'Export failed') })
1154
+ }
1155
+ return response.blob()
1156
+ })
1157
+ .then(blob => {
1158
+ const url = window.URL.createObjectURL(blob)
1159
+ const a = document.createElement('a')
1160
+ a.href = url
1161
+ a.download = `export_${new Date().getTime()}.csv`
1162
+ document.body.appendChild(a)
1163
+ a.click()
1164
+ window.URL.revokeObjectURL(url)
1165
+ a.remove()
1166
+
1167
+ // Close modal
1168
+ const modal = bootstrap.Modal.getInstance(document.getElementById('export-modal'))
1169
+ if (modal) modal.hide()
1170
+ })
1171
+ .catch(error => {
1172
+ alert('Export error: ' + error.message)
1173
+ })
1174
+ }
1175
+
1176
+ exportJSON() {
1177
+ const format = document.getElementById('json-format').value
1178
+ const prettyPrint = document.getElementById('json-pretty').checked
1179
+ const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
1180
+ const form = this.element
1181
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
1182
+
1183
+ const formData = new FormData()
1184
+ formData.append('query', query)
1185
+ formData.append('format', format)
1186
+ formData.append('pretty_print', prettyPrint)
1187
+
1188
+ fetch(form.action.replace('/execute_query', '/export_json'), {
1189
+ method: 'POST',
1190
+ body: formData,
1191
+ headers: {
1192
+ 'X-CSRF-Token': csrfToken
1193
+ }
1194
+ })
1195
+ .then(response => {
1196
+ if (!response.ok) {
1197
+ return response.json().then(err => { throw new Error(err.error || 'Export failed') })
1198
+ }
1199
+ return response.blob()
1200
+ })
1201
+ .then(blob => {
1202
+ const url = window.URL.createObjectURL(blob)
1203
+ const a = document.createElement('a')
1204
+ a.href = url
1205
+ a.download = `export_${new Date().getTime()}.json`
1206
+ document.body.appendChild(a)
1207
+ a.click()
1208
+ window.URL.revokeObjectURL(url)
1209
+ a.remove()
1210
+
1211
+ // Close modal
1212
+ const modal = bootstrap.Modal.getInstance(document.getElementById('export-modal'))
1213
+ if (modal) modal.hide()
1214
+ })
1215
+ .catch(error => {
1216
+ alert('Export error: ' + error.message)
1217
+ })
1218
+ }
1219
+ }
1220
+
1221
+ // Table Selector Controller
1222
+ class TableSelectorController extends Controller {
1223
+ static targets = ["tableItem"]
1224
+
1225
+ selectTable(event) {
1226
+ const tableName = event.currentTarget.dataset.tableName
1227
+
1228
+ // Remove active class from all items
1229
+ this.tableItemTargets.forEach(item => {
1230
+ item.classList.remove("active")
1231
+ })
1232
+
1233
+ // Add active class to clicked item
1234
+ event.currentTarget.classList.add("active")
1235
+
1236
+ // Update query in CodeMirror editor
1237
+ if (window.sqlEditor) {
1238
+ window.sqlEditor.setValue(`SELECT * FROM ${tableName} LIMIT 100;`)
1239
+ } else {
1240
+ // Fallback to regular textarea
1241
+ const queryInput = document.getElementById("query")
1242
+ if (queryInput) {
1243
+ queryInput.value = `SELECT * FROM ${tableName} LIMIT 100;`
1244
+ }
1245
+ }
1246
+
1247
+ // Trigger the query execution
1248
+ const form = document.querySelector('[data-controller="query-executor"]')
1249
+ if (form) {
1250
+ const submitButton = form.querySelector('button[type="submit"]')
1251
+ if (submitButton) {
1252
+ submitButton.click()
1253
+ } else {
1254
+ form.requestSubmit()
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ // Initialize Stimulus
1261
+ window.Stimulus = Application.start()
1262
+ Stimulus.register("query-executor", QueryExecutorController)
1263
+ Stimulus.register("table-selector", TableSelectorController)
1264
+ </script>
1265
+ </head>
1266
+
1267
+ <body>
1268
+ <div class="sqlite-dashboard-container">
1269
+ <%= yield %>
1270
+ </div>
1271
+
1272
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
1273
+ </body>
1274
+ </html>