pg_insights 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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +183 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/javascripts/pg_insights/application.js +436 -0
  6. data/app/assets/javascripts/pg_insights/health.js +104 -0
  7. data/app/assets/javascripts/pg_insights/results/chart_renderer.js +126 -0
  8. data/app/assets/javascripts/pg_insights/results/table_manager.js +378 -0
  9. data/app/assets/javascripts/pg_insights/results/view_toggles.js +25 -0
  10. data/app/assets/javascripts/pg_insights/results.js +13 -0
  11. data/app/assets/stylesheets/pg_insights/application.css +750 -0
  12. data/app/assets/stylesheets/pg_insights/health.css +501 -0
  13. data/app/assets/stylesheets/pg_insights/results.css +682 -0
  14. data/app/controllers/pg_insights/application_controller.rb +4 -0
  15. data/app/controllers/pg_insights/health_controller.rb +110 -0
  16. data/app/controllers/pg_insights/insights_controller.rb +77 -0
  17. data/app/controllers/pg_insights/queries_controller.rb +44 -0
  18. data/app/helpers/pg_insights/application_helper.rb +4 -0
  19. data/app/helpers/pg_insights/insights_helper.rb +190 -0
  20. data/app/jobs/pg_insights/application_job.rb +4 -0
  21. data/app/jobs/pg_insights/health_check_job.rb +45 -0
  22. data/app/jobs/pg_insights/health_check_scheduler_job.rb +52 -0
  23. data/app/jobs/pg_insights/recurring_health_checks_job.rb +49 -0
  24. data/app/models/pg_insights/application_record.rb +5 -0
  25. data/app/models/pg_insights/health_check_result.rb +46 -0
  26. data/app/models/pg_insights/query.rb +10 -0
  27. data/app/services/pg_insights/health_check_service.rb +298 -0
  28. data/app/services/pg_insights/insight_query_service.rb +21 -0
  29. data/app/views/layouts/pg_insights/application.html.erb +58 -0
  30. data/app/views/pg_insights/health/index.html.erb +324 -0
  31. data/app/views/pg_insights/insights/_chart_view.html.erb +25 -0
  32. data/app/views/pg_insights/insights/_column_panel.html.erb +18 -0
  33. data/app/views/pg_insights/insights/_query_examples.html.erb +32 -0
  34. data/app/views/pg_insights/insights/_query_panel.html.erb +36 -0
  35. data/app/views/pg_insights/insights/_result.html.erb +15 -0
  36. data/app/views/pg_insights/insights/_results_info.html.erb +19 -0
  37. data/app/views/pg_insights/insights/_results_panel.html.erb +13 -0
  38. data/app/views/pg_insights/insights/_results_table.html.erb +45 -0
  39. data/app/views/pg_insights/insights/_stats_view.html.erb +3 -0
  40. data/app/views/pg_insights/insights/_table_controls.html.erb +21 -0
  41. data/app/views/pg_insights/insights/_table_view.html.erb +5 -0
  42. data/app/views/pg_insights/insights/index.html.erb +5 -0
  43. data/config/default_queries.yml +85 -0
  44. data/config/routes.rb +22 -0
  45. data/lib/generators/pg_insights/clean_generator.rb +74 -0
  46. data/lib/generators/pg_insights/install_generator.rb +176 -0
  47. data/lib/pg_insights/engine.rb +40 -0
  48. data/lib/pg_insights/version.rb +3 -0
  49. data/lib/pg_insights.rb +83 -0
  50. data/lib/tasks/pg_insights.rake +172 -0
  51. metadata +124 -0
@@ -0,0 +1,750 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+
17
+ /* PG Insights Styles */
18
+
19
+ /* Reset and Base Styles */
20
+ * {
21
+ margin: 0;
22
+ padding: 0;
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
28
+ line-height: 1.5;
29
+ color: #2c3e50;
30
+ background: #f8fafc;
31
+ min-height: 100vh;
32
+ }
33
+
34
+ /* Layout Structure */
35
+ .insights-layout {
36
+ min-height: 100vh;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ /* Header */
42
+ .top-header {
43
+ background: linear-gradient(135deg, #00979D 0%, #00838a 100%);
44
+ color: white;
45
+ padding: 8px 0;
46
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
47
+ position: sticky;
48
+ top: 0;
49
+ z-index: 100;
50
+ }
51
+
52
+ .header-content {
53
+ max-width: 1400px;
54
+ margin: 0 auto;
55
+ padding: 0 20px;
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ }
60
+
61
+ .header-left {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 12px;
65
+ }
66
+
67
+ .header-title {
68
+ font-size: 18px;
69
+ font-weight: 700;
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 8px;
73
+ margin: 0;
74
+ }
75
+
76
+ .header-title .icon {
77
+ font-size: 20px;
78
+ }
79
+
80
+ .header-subtitle {
81
+ font-size: 12px;
82
+ opacity: 0.8;
83
+ font-weight: 500;
84
+ background: rgba(255,255,255,0.1);
85
+ padding: 2px 8px;
86
+ border-radius: 12px;
87
+ }
88
+
89
+ .header-nav {
90
+ display: flex;
91
+ gap: 4px;
92
+ }
93
+
94
+ .nav-link {
95
+ color: rgba(255,255,255,0.8);
96
+ text-decoration: none;
97
+ padding: 6px 12px;
98
+ border-radius: 6px;
99
+ font-size: 13px;
100
+ font-weight: 500;
101
+ transition: all 0.2s ease;
102
+ }
103
+
104
+ .nav-link:hover {
105
+ color: white;
106
+ background: rgba(255,255,255,0.1);
107
+ }
108
+
109
+ .nav-link.active {
110
+ color: white;
111
+ background: rgba(255,255,255,0.15);
112
+ font-weight: 600;
113
+ }
114
+
115
+ /* Flash Messages */
116
+ .flash-container {
117
+ max-width: 1400px;
118
+ margin: 0 auto;
119
+ padding: 10px 20px 0;
120
+ }
121
+
122
+ .flash {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ padding: 10px 16px;
127
+ border-radius: 8px;
128
+ font-size: 14px;
129
+ margin-bottom: 10px;
130
+ }
131
+
132
+ .flash-error {
133
+ background: #fef2f2;
134
+ color: #dc2626;
135
+ border: 1px solid #fecaca;
136
+ }
137
+
138
+ .flash-success {
139
+ background: #f0fdf4;
140
+ color: #16a34a;
141
+ border: 1px solid #bbf7d0;
142
+ }
143
+
144
+ .flash-icon {
145
+ font-size: 16px;
146
+ }
147
+
148
+ /* Main Content */
149
+ .main-content {
150
+ flex: 1;
151
+ max-width: 1400px;
152
+ margin: 0 auto;
153
+ padding: 20px;
154
+ width: 100%;
155
+ }
156
+
157
+ /* Form Header Actions */
158
+ .form-header {
159
+ display: flex;
160
+ justify-content: space-between;
161
+ align-items: center;
162
+ margin-bottom: 12px;
163
+ }
164
+
165
+ .header-actions {
166
+ display: flex;
167
+ gap: 4px;
168
+ }
169
+
170
+ .btn-icon {
171
+ width: 32px;
172
+ height: 32px;
173
+ border: none;
174
+ border-radius: 6px;
175
+ font-size: 14px;
176
+ cursor: pointer;
177
+ transition: all 0.2s ease;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ opacity: 0.7;
182
+ }
183
+
184
+ .btn-icon:hover {
185
+ opacity: 1;
186
+ transform: translateY(-1px);
187
+ }
188
+
189
+ .btn-icon.btn-copy {
190
+ background: #3b82f6;
191
+ color: white;
192
+ }
193
+
194
+ .btn-icon.btn-save {
195
+ background: #10b981;
196
+ color: white;
197
+ }
198
+
199
+ .btn-icon:disabled {
200
+ opacity: 0.4;
201
+ cursor: not-allowed;
202
+ transform: none;
203
+ }
204
+
205
+ /* Remove old query-actions styles */
206
+
207
+ /* Responsive Design */
208
+ @media (max-width: 768px) {
209
+ .header-content {
210
+ /* Keep horizontal layout even on mobile */
211
+ flex-direction: row;
212
+ gap: 8px;
213
+ padding: 0 15px;
214
+ flex-wrap: wrap;
215
+ }
216
+
217
+ .header-left {
218
+ /* Remove align-self to prevent unwanted positioning */
219
+ flex: 1;
220
+ min-width: 200px;
221
+ }
222
+
223
+ .header-nav {
224
+ /* Remove align-self to prevent unwanted positioning */
225
+ flex-shrink: 0;
226
+ }
227
+
228
+ .nav-link {
229
+ padding: 4px 8px;
230
+ font-size: 12px;
231
+ }
232
+
233
+ .main-content {
234
+ padding: 15px;
235
+ }
236
+
237
+ .header-title {
238
+ font-size: 16px;
239
+ }
240
+
241
+ .header-subtitle {
242
+ font-size: 11px;
243
+ }
244
+ }
245
+
246
+ @media (max-width: 480px) {
247
+ .header-content {
248
+ padding: 0 10px;
249
+ gap: 6px;
250
+ }
251
+
252
+ .header-left {
253
+ min-width: 150px;
254
+ }
255
+
256
+ .header-nav {
257
+ gap: 2px;
258
+ }
259
+
260
+ .nav-link {
261
+ padding: 4px 6px;
262
+ font-size: 11px;
263
+ }
264
+
265
+ .main-content {
266
+ padding: 10px;
267
+ }
268
+
269
+ .header-title {
270
+ font-size: 14px;
271
+ }
272
+
273
+ .header-subtitle {
274
+ display: none; /* Hide subtitle on very small screens */
275
+ }
276
+ }
277
+
278
+
279
+
280
+
281
+ /* Split-Screen Layout */
282
+ .insights-container {
283
+ display: flex;
284
+ height: calc(100vh - 120px); /* Account for header */
285
+ gap: 16px;
286
+ overflow: hidden;
287
+ }
288
+
289
+ /* Left Panel: Query Section */
290
+ .query-panel {
291
+ width: 480px;
292
+ min-width: 420px;
293
+ max-width: 600px;
294
+ flex-shrink: 0;
295
+ display: flex;
296
+ flex-direction: column;
297
+ }
298
+
299
+ .query-section {
300
+ background: white;
301
+ border-radius: 8px;
302
+ border: 1px solid #e2e8f0;
303
+ padding: 20px;
304
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
305
+ height: 100%;
306
+ display: flex;
307
+ flex-direction: column;
308
+ overflow: hidden;
309
+ }
310
+
311
+ .form-header {
312
+ display: flex;
313
+ justify-content: space-between;
314
+ align-items: center;
315
+ margin-bottom: 8px;
316
+ }
317
+
318
+ .form-subheader {
319
+ margin-bottom: 12px;
320
+ }
321
+
322
+ .form-header-left {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 12px;
326
+ }
327
+
328
+ .preview-tables-dropdown {
329
+ display: flex;
330
+ align-items: center;
331
+ }
332
+
333
+ .table-select {
334
+ padding: 6px 12px;
335
+ border: 1px solid #d1d5db;
336
+ border-radius: 6px;
337
+ font-size: 13px;
338
+ color: #374151;
339
+ background: white;
340
+ cursor: pointer;
341
+ transition: all 0.2s ease;
342
+ min-width: 160px;
343
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
344
+
345
+ &:hover {
346
+ border-color: #00979D;
347
+ box-shadow: 0 1px 3px rgba(0, 151, 157, 0.1);
348
+ }
349
+
350
+ &:focus {
351
+ outline: none;
352
+ border-color: #00979D;
353
+ box-shadow: 0 0 0 3px rgba(0, 151, 157, 0.1);
354
+ }
355
+
356
+ option {
357
+ padding: 6px 12px;
358
+ color: #374151;
359
+ background: white;
360
+ }
361
+
362
+ option:first-child {
363
+ color: #6b7280;
364
+ font-style: italic;
365
+ }
366
+ }
367
+
368
+ .form-label {
369
+ display: block;
370
+ font-weight: 600;
371
+ color: #374151;
372
+ font-size: 16px;
373
+ margin-bottom: 0;
374
+ }
375
+
376
+ .form-content {
377
+ flex: 1;
378
+ margin-bottom: 16px;
379
+ min-height: 0;
380
+ }
381
+
382
+ .sql-editor {
383
+ width: 100%;
384
+ height: 100%;
385
+ min-height: 200px;
386
+ padding: 12px;
387
+ border: 2px solid #e5e7eb;
388
+ border-radius: 6px;
389
+ font-family: 'Monaco', 'Menlo', 'Consolas', 'SF Mono', monospace;
390
+ font-size: 13px;
391
+ line-height: 1.4;
392
+ background: #fafbfc;
393
+ color: #1f2937;
394
+ resize: none;
395
+ transition: border-color 0.2s ease;
396
+
397
+ &:focus {
398
+ outline: none;
399
+ border-color: #00979D;
400
+ background: white;
401
+ box-shadow: 0 0 0 3px rgba(0, 151, 157, 0.1);
402
+ }
403
+
404
+ &::placeholder {
405
+ color: #9ca3af;
406
+ opacity: 0.8;
407
+ }
408
+ }
409
+
410
+ .form-actions {
411
+ display: flex;
412
+ gap: 12px;
413
+ margin-bottom: 16px;
414
+ flex-shrink: 0;
415
+ }
416
+
417
+ .btn {
418
+ flex: 1;
419
+ padding: 10px 16px;
420
+ border-radius: 6px;
421
+ font-weight: 600;
422
+ font-size: 14px;
423
+ border: none;
424
+ cursor: pointer;
425
+ transition: all 0.2s ease;
426
+ text-align: center;
427
+ text-decoration: none;
428
+
429
+ &.btn-primary {
430
+ background: linear-gradient(135deg, #00979D 0%, #00838a 100%);
431
+ color: white;
432
+
433
+ &:hover {
434
+ background: linear-gradient(135deg, #00838a 0%, #00767a 100%);
435
+ transform: translateY(-1px);
436
+ box-shadow: 0 2px 8px rgba(0, 151, 157, 0.3);
437
+ }
438
+
439
+ &:disabled {
440
+ background: #9ca3af !important;
441
+ cursor: not-allowed !important;
442
+ transform: none !important;
443
+ box-shadow: none !important;
444
+ opacity: 0.6;
445
+
446
+ &:hover {
447
+ background: #9ca3af !important;
448
+ transform: none !important;
449
+ box-shadow: none !important;
450
+ }
451
+ }
452
+ }
453
+
454
+ &.btn-secondary {
455
+ background: #6b7280;
456
+ color: white;
457
+
458
+ &:hover {
459
+ background: #4b5563;
460
+ transform: translateY(-1px);
461
+ }
462
+ }
463
+ }
464
+
465
+ /* Query Examples Section */
466
+ .query-examples-section {
467
+ flex-shrink: 0;
468
+ margin-bottom: 12px;
469
+ }
470
+
471
+ .examples-header {
472
+ display: flex;
473
+ justify-content: space-between;
474
+ align-items: center;
475
+ margin-bottom: 12px;
476
+ }
477
+
478
+ .examples-label {
479
+ font-size: 12px;
480
+ font-weight: 600;
481
+ color: #374151;
482
+ text-transform: uppercase;
483
+ letter-spacing: 0.5px;
484
+ }
485
+
486
+ .category-filters {
487
+ display: flex;
488
+ gap: 4px;
489
+ background: #f1f5f9;
490
+ padding: 2px;
491
+ border-radius: 6px;
492
+ }
493
+
494
+ .filter-btn {
495
+ padding: 4px 8px;
496
+ border: none;
497
+ background: transparent;
498
+ color: #64748b;
499
+ font-size: 10px;
500
+ font-weight: 500;
501
+ border-radius: 4px;
502
+ cursor: pointer;
503
+ transition: all 0.2s ease;
504
+
505
+ &.active {
506
+ background: white;
507
+ color: #00979D;
508
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
509
+ }
510
+
511
+ &:hover:not(.active) {
512
+ background: rgba(255,255,255,0.5);
513
+ color: #475569;
514
+ }
515
+ }
516
+
517
+ .query-examples-grid {
518
+ display: grid;
519
+ grid-template-columns: 1fr;
520
+ gap: 6px;
521
+ max-height: 180px;
522
+ overflow-y: auto;
523
+
524
+ /* Scrollbar styling */
525
+ &::-webkit-scrollbar {
526
+ width: 4px;
527
+ }
528
+
529
+ &::-webkit-scrollbar-thumb {
530
+ background: #cbd5e1;
531
+ border-radius: 2px;
532
+ }
533
+
534
+ &::-webkit-scrollbar-track {
535
+ background: #f1f5f9;
536
+ }
537
+ }
538
+
539
+ .query-example-btn {
540
+ padding: 8px 10px;
541
+ border: 1px solid #e5e7eb;
542
+ background: white;
543
+ border-radius: 6px;
544
+ cursor: pointer;
545
+ transition: all 0.2s ease;
546
+ text-align: left;
547
+ position: relative;
548
+
549
+ &:hover {
550
+ border-color: #00979D;
551
+ background: #f0f9ff;
552
+ transform: translateY(-1px);
553
+ box-shadow: 0 2px 4px rgba(0, 151, 157, 0.1);
554
+ }
555
+
556
+ &.hidden {
557
+ display: none;
558
+ }
559
+ }
560
+
561
+ .query-btn-content {
562
+ display: flex;
563
+ flex-direction: column;
564
+ gap: 2px;
565
+ }
566
+
567
+ .query-name {
568
+ font-size: 12px;
569
+ font-weight: 600;
570
+ color: #374151;
571
+ line-height: 1.2;
572
+ }
573
+
574
+ .query-desc {
575
+ font-size: 10px;
576
+ color: #6b7280;
577
+ line-height: 1.3;
578
+ }
579
+
580
+ .query-category-badge {
581
+ font-size: 9px;
582
+ font-weight: 500;
583
+ text-transform: uppercase;
584
+ letter-spacing: 0.3px;
585
+ padding: 2px 6px;
586
+ border-radius: 10px;
587
+ align-self: flex-start;
588
+ margin-top: 2px;
589
+
590
+ &.database {
591
+ background: #dcfce7;
592
+ color: #166534;
593
+ }
594
+
595
+ &.business {
596
+ background: #e0f2fe;
597
+ color: #0c4a6e;
598
+ }
599
+
600
+ &.saved {
601
+ background: #fef2f2;
602
+ color: #00979d;
603
+ }
604
+ }
605
+
606
+ .query-info {
607
+ text-align: center;
608
+ flex-shrink: 0;
609
+
610
+ small {
611
+ color: #6b7280;
612
+ font-size: 11px;
613
+ line-height: 1.3;
614
+ }
615
+ }
616
+
617
+ /* Right Panel: Results Section */
618
+ .results-panel {
619
+ flex: 1;
620
+ min-width: 0;
621
+ display: flex;
622
+ flex-direction: column;
623
+ }
624
+
625
+ /* Empty Results State */
626
+ .empty-results {
627
+ background: white;
628
+ border-radius: 8px;
629
+ border: 1px solid #e2e8f0;
630
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
631
+ height: 100%;
632
+ display: flex;
633
+ align-items: center;
634
+ justify-content: center;
635
+ }
636
+
637
+ .empty-content {
638
+ text-align: center;
639
+ color: #6b7280;
640
+
641
+ h3 {
642
+ font-size: 18px;
643
+ font-weight: 600;
644
+ color: #374151;
645
+ margin: 0 0 8px 0;
646
+ }
647
+
648
+ p {
649
+ font-size: 14px;
650
+ margin: 0;
651
+ max-width: 300px;
652
+ }
653
+ }
654
+
655
+ .empty-icon {
656
+ font-size: 48px;
657
+ margin-bottom: 16px;
658
+ opacity: 0.5;
659
+ }
660
+
661
+ /* Results Section Styling (when results exist) */
662
+ .results-section {
663
+ height: 100%;
664
+ background: white;
665
+ border-radius: 8px;
666
+ border: 1px solid #e2e8f0;
667
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
668
+ display: flex;
669
+ flex-direction: column;
670
+ overflow: hidden;
671
+ }
672
+
673
+ /* Validation Error Styling */
674
+ .validation-error {
675
+ margin-bottom: 12px;
676
+ padding: 12px 14px;
677
+ background: #fef2f2;
678
+ border: 1px solid #fecaca;
679
+ border-radius: 6px;
680
+ animation: slideDown 0.3s ease-out;
681
+ color: #dc2626;
682
+ font-size: 13px;
683
+ font-weight: 500;
684
+ line-height: 1.4;
685
+ }
686
+
687
+ @keyframes slideDown {
688
+ from {
689
+ opacity: 0;
690
+ transform: translateY(-5px);
691
+ }
692
+ to {
693
+ opacity: 1;
694
+ transform: translateY(0);
695
+ }
696
+ }
697
+
698
+ /* Responsive Design */
699
+ @media (max-width: 1200px) {
700
+ .query-panel {
701
+ width: 420px;
702
+ min-width: 380px;
703
+ }
704
+ }
705
+
706
+ @media (max-width: 768px) {
707
+ .insights-container {
708
+ flex-direction: column;
709
+ height: calc(100vh - 120px);
710
+ }
711
+
712
+ .query-panel {
713
+ width: 100%;
714
+ min-width: auto;
715
+ max-width: none;
716
+ height: 60%;
717
+ flex-shrink: 0;
718
+ }
719
+
720
+ .results-panel {
721
+ height: 40%;
722
+ min-height: 200px;
723
+ }
724
+
725
+ .sql-editor {
726
+ font-size: 14px; /* Prevent zoom on iOS */
727
+ }
728
+
729
+ .form-actions {
730
+ flex-direction: row;
731
+ }
732
+
733
+ .query-examples-grid {
734
+ max-height: 120px;
735
+ }
736
+ }
737
+
738
+ @media (max-width: 480px) {
739
+ .query-section {
740
+ padding: 16px;
741
+ }
742
+
743
+ .query-panel {
744
+ height: 70%;
745
+ }
746
+
747
+ .results-panel {
748
+ height: 30%;
749
+ }
750
+ }