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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3575be5a5a333d43c44c555f5f736c83e5458788143d92b5ca5ec765e3cbd083
4
+ data.tar.gz: be967ed421de1a002b52ef7b476fd58a02c866e6cf429ddeef053672831679a5
5
+ SHA512:
6
+ metadata.gz: b1756de4bb9c8aa2bceab70f936d79a85d678376917f0976bf2b9fa90ef4cc54fe4683387166ba6b93fe2e72f50d879a7185c89f292a87b60f782e92790c38df
7
+ data.tar.gz: 17594af5eeeb6ed8812a6a7659635ce5efa032c6f67bb882fd8caf52dc617fe54b2dd2773c6883047be386c736f0826d6224cece06ebe06b0c6e0a5e49a2203c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright MezbahAlam
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # PgInsights
2
+
3
+ **PostgreSQL performance monitoring for Rails apps**
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/pg_insights.svg)](https://badge.fury.io/rb/pg_insights)
6
+ [![CI](https://github.com/mezbahalam/pg_insights/actions/workflows/ci.yml/badge.svg)](https://github.com/mezbahalam/pg_insights/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ PgInsights is a Rails engine that gives you a web dashboard for monitoring your PostgreSQL database performance. Think of it as a lightweight alternative to external monitoring tools that lives right inside your Rails app.
10
+
11
+ ## Why I built this
12
+
13
+ I got tired of switching between different tools to check database performance. Sometimes you just want to quickly see which indexes aren't being used, or find slow queries without setting up a whole monitoring infrastructure. PgInsights gives you that - a simple dashboard you can access at `/pg_insights` in your Rails app.
14
+
15
+ ## What you get
16
+
17
+ **Health Dashboard**
18
+ - Find unused indexes that are wasting space
19
+ - Spot tables that might need indexes (high sequential scans)
20
+ - Identify slow queries (if you have pg_stat_statements enabled)
21
+ - Check for table bloat that needs cleanup
22
+ - Review PostgreSQL configuration settings
23
+
24
+ **Query Runner**
25
+ - Run your own SELECT queries safely
26
+ - Built-in queries for common performance checks
27
+ - Save queries you use frequently
28
+ - Results displayed as tables or charts
29
+
30
+ **Smart execution**
31
+ - Runs health checks in background jobs if you have them set up
32
+ - Falls back to running directly if you don't
33
+ - Caches results so repeated visits are fast
34
+ - Configurable timeouts to prevent slow queries from hanging
35
+
36
+ ## Installation
37
+
38
+ Add to your Gemfile:
39
+
40
+ ```ruby
41
+ gem 'pg_insights'
42
+ ```
43
+
44
+ Run the installer:
45
+
46
+ ```bash
47
+ bundle install
48
+ rails generate pg_insights:install
49
+ rails db:migrate
50
+ ```
51
+
52
+ That's it. Visit `/pg_insights` in your browser.
53
+
54
+ ## Configuration
55
+
56
+ The engine works out of the box, but you can customize it:
57
+
58
+ ```ruby
59
+ # config/initializers/pg_insights.rb
60
+ PgInsights.configure do |config|
61
+ # Run health checks in background (default: true)
62
+ config.enable_background_jobs = true
63
+
64
+ # How long to cache results (default: 5 minutes)
65
+ config.health_cache_expiry = 10.minutes
66
+
67
+ # Timeout for health check queries (default: 10 seconds)
68
+ config.health_check_timeout = 15.seconds
69
+
70
+ # Queue name for background jobs (default: :pg_insights_health)
71
+ config.background_job_queue = :low_priority
72
+ end
73
+ ```
74
+
75
+ ## How Background Jobs Work
76
+
77
+ **PgInsights uses on-demand background jobs, not automatic scheduling.**
78
+
79
+ ### When health checks run:
80
+ - ✅ **When you visit the health dashboard `/pg_insights/health`** and cached data is older than `health_cache_expiry` (default: 5 minutes)
81
+ - ✅ **When you click the "Refresh" button** in the dashboard
82
+ - ✅ **When you run** `rails pg_insights:health_check` manually
83
+ - ❌ **NOT automatically** - PgInsights doesn't run background jobs on its own
84
+
85
+ ### How caching works:
86
+ ```
87
+ Visit at 2:00 PM → Runs health checks, caches results for 5 minutes
88
+ Visit at 2:03 PM → Uses cached results (still fresh)
89
+ Visit at 2:06 PM → Data is stale, triggers new background jobs
90
+ ```
91
+
92
+ ### Background job setup (optional but recommended):
93
+
94
+ If your app has background jobs (Sidekiq, Resque, etc.), PgInsights will use them for better performance:
95
+
96
+ ```bash
97
+ # Check if background jobs are working
98
+ rails pg_insights:status
99
+ ```
100
+
101
+ **Without background jobs**: Health checks run synchronously when you visit the page (slower but works)
102
+ **With background jobs**: Health checks run asynchronously (faster, non-blocking)
103
+
104
+ ### Optional: Automatic recurring checks
105
+
106
+ If you want health checks to run automatically (not just on-demand), set up a scheduler:
107
+
108
+ ```ruby
109
+ # Using whenever (runs every hour)
110
+ every 1.hour do
111
+ runner "PgInsights::RecurringHealthChecksJob.perform_later"
112
+ end
113
+
114
+ # Using sidekiq-cron
115
+ Sidekiq::Cron::Job.create(
116
+ name: 'PgInsights Health Checks',
117
+ cron: '0 * * * *',
118
+ class: 'PgInsights::RecurringHealthChecksJob'
119
+ )
120
+ ```
121
+
122
+ **Note**: Even with automatic scheduling, the jobs are smart - they only run expensive queries if the cached data is actually stale.
123
+
124
+ ## Usage
125
+
126
+ Navigate to `/pg_insights` in your app. The interface is pretty straightforward:
127
+
128
+ - **Main page**: Run queries and see results as tables or charts
129
+ - **Health tab**: Database performance overview
130
+ - **Query examples**: Pre-built queries for common checks
131
+
132
+ All queries are read-only (SELECT statements only) and have timeouts to prevent issues.
133
+
134
+ ## Available rake tasks
135
+
136
+ ```bash
137
+ rails pg_insights:status # Check configuration
138
+ rails pg_insights:health_check # Run health checks manually
139
+ rails pg_insights:stats # Show usage statistics
140
+ rails pg_insights:clear_data # Clear stored data and caches
141
+ ```
142
+
143
+ ## Safety
144
+
145
+ - Only SELECT queries allowed
146
+ - Query timeouts prevent long-running queries
147
+ - Focuses on public schema by default
148
+ - No modification of your data
149
+
150
+ ## Uninstalling
151
+
152
+ ```bash
153
+ rails generate pg_insights:clean
154
+ rails db:rollback STEP=2
155
+ # Remove gem from Gemfile
156
+ ```
157
+
158
+ ## Requirements
159
+
160
+ - Rails 6.1+
161
+ - PostgreSQL
162
+ - For slow query detection: pg_stat_statements extension (optional)
163
+
164
+ ## Contributing
165
+
166
+ Found a bug or have an idea? Open an issue or send a pull request. The codebase is pretty straightforward.
167
+
168
+ Development setup:
169
+
170
+ ```bash
171
+ git clone https://github.com/mezbahalam/pg_insights.git
172
+ cd pg_insights
173
+ bundle install
174
+ bundle exec rake spec
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT License. See [LICENSE](MIT-LICENSE) file.
180
+
181
+ ---
182
+
183
+ Built by [Mezbah Alam](https://github.com/mezbahalam). Inspired by pg_hero and other database monitoring tools.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,436 @@
1
+ //= require chartkick
2
+ //= require Chart.bundle
3
+ //= require_tree .
4
+
5
+
6
+ // PG Insights JavaScript
7
+ document.addEventListener('DOMContentLoaded', function() {
8
+ const InsightsApp = {
9
+ // Configuration - will be set from data attribute
10
+ config: {
11
+ queries: [],
12
+ savedQueries: []
13
+ },
14
+ // Keep track of the currently loaded query
15
+ currentQueryState: {
16
+ id: null,
17
+ type: null, // 'built-in' or 'saved'
18
+ name: ''
19
+ },
20
+
21
+ // Initialize the application
22
+ init() {
23
+ this.loadQueriesFromDataAttribute();
24
+ this.loadSavedQueries();
25
+ this.bindEvents();
26
+ this.validateInitialQuery();
27
+ this.setupQueryExamples();
28
+ this.loadTableNames();
29
+ },
30
+
31
+ // Load queries from data attribute
32
+ loadQueriesFromDataAttribute() {
33
+ const container = document.querySelector('.insights-container');
34
+ if (container && container.dataset.queries) {
35
+ try {
36
+ this.config.queries = JSON.parse(container.dataset.queries);
37
+ } catch (e) {
38
+ console.error('Failed to parse queries data:', e);
39
+ this.config.queries = [];
40
+ }
41
+ }
42
+ },
43
+
44
+ // Load saved queries from localStorage (keeping existing method)
45
+ loadSavedQueries() {
46
+ // Keep this for backward compatibility if needed elsewhere
47
+ },
48
+
49
+ // Copy current query functionality
50
+ copyCurrentQuery() {
51
+ const textarea = document.querySelector('.sql-editor');
52
+ const btn = document.querySelector('.btn-icon.btn-copy');
53
+
54
+ if (!textarea?.value.trim()) return;
55
+
56
+ btn.disabled = true;
57
+ btn.textContent = '✓';
58
+
59
+ navigator.clipboard.writeText(textarea.value).then(() => {
60
+ setTimeout(() => {
61
+ btn.disabled = false;
62
+ btn.textContent = '📋';
63
+ }, 1000);
64
+ });
65
+ },
66
+
67
+ // Save or Update the current query
68
+ saveCurrentQuery() {
69
+ const textarea = document.querySelector('.sql-editor');
70
+ const sql = textarea?.value.trim();
71
+ if (!sql) return;
72
+
73
+ const isUpdate = this.currentQueryState.type === 'saved';
74
+ let name;
75
+
76
+ if (isUpdate) {
77
+ name = prompt("Update query name, or confirm current name:", this.currentQueryState.name);
78
+ } else {
79
+ name = prompt("Enter a name for this new saved query:");
80
+ }
81
+
82
+ if (!name) return; // User cancelled prompt
83
+
84
+ const method = isUpdate ? 'PATCH' : 'POST';
85
+ const url = isUpdate ? `/pg_insights/queries/${this.currentQueryState.id}` : '/pg_insights/queries';
86
+
87
+ const body = {
88
+ query: {
89
+ name: name.trim(),
90
+ sql: sql,
91
+ // For now, description is not editable in the UI
92
+ description: isUpdate ? (this.currentQueryState.description || '') : 'User saved query',
93
+ category: 'saved'
94
+ }
95
+ };
96
+
97
+ const btn = document.querySelector('.btn-icon.btn-save');
98
+ btn.disabled = true;
99
+ btn.textContent = '⏳';
100
+
101
+ fetch(url, {
102
+ method: method,
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
106
+ },
107
+ body: JSON.stringify(body)
108
+ })
109
+ .then(response => {
110
+ if (!response.ok) {
111
+ return response.json().then(err => { throw err; });
112
+ }
113
+ return response.json();
114
+ })
115
+ .then(data => {
116
+ if (data.success) {
117
+ btn.textContent = '✓';
118
+ location.reload(); // Easiest way to show updated query list
119
+ }
120
+ })
121
+ .catch(error => {
122
+ console.error('Save query error:', error);
123
+ btn.textContent = '✗';
124
+ const errorMessage = error.errors ? error.errors.join(', ') : 'A server error occurred.';
125
+ alert(`Failed to save query: ${errorMessage}`);
126
+
127
+ // Restore button after a delay on failure
128
+ setTimeout(() => {
129
+ btn.disabled = false;
130
+ const icon = this.currentQueryState.type === 'saved' ? '📝' : '💾';
131
+ btn.textContent = icon;
132
+ }, 2000);
133
+ });
134
+ },
135
+
136
+ // Query validation
137
+ validateQuery(sql) {
138
+ if (!sql || !sql.trim()) {
139
+ return { valid: false, message: "Please enter a SQL query" };
140
+ }
141
+
142
+ const trimmedSql = sql.trim();
143
+
144
+ // Check if it starts with SELECT (case insensitive)
145
+ if (!trimmedSql.toLowerCase().startsWith('select')) {
146
+ return { valid: false, message: "Only SELECT statements are allowed" };
147
+ }
148
+
149
+ // Check for semicolons
150
+ if (trimmedSql.includes(';')) {
151
+ return { valid: false, message: "Semicolons (;) are not allowed. Please use a single SELECT statement only." };
152
+ }
153
+
154
+ // Check for forbidden keywords
155
+ const forbiddenWords = /\b(insert|update|delete|alter|drop|create|grant|revoke)\b/i;
156
+ const match = trimmedSql.match(forbiddenWords);
157
+ if (match) {
158
+ return { valid: false, message: `${match[1].toUpperCase()} statements are not allowed. Only SELECT queries are permitted.` };
159
+ }
160
+
161
+ return { valid: true };
162
+ },
163
+
164
+ // UI Updates
165
+ updateExecuteButton(isValid) {
166
+ const executeBtn = document.getElementById('execute-btn');
167
+ if (executeBtn) {
168
+ executeBtn.disabled = !isValid;
169
+ executeBtn.title = isValid ? 'Execute query' : 'Please fix query errors first';
170
+ }
171
+ },
172
+
173
+ validateAndUpdateUI(sql) {
174
+ const validation = this.validateQuery(sql);
175
+
176
+ if (validation.valid) {
177
+ this.hideValidationError();
178
+ this.updateExecuteButton(true);
179
+ } else {
180
+ this.showValidationError(validation.message);
181
+ this.updateExecuteButton(false);
182
+ }
183
+
184
+ return validation;
185
+ },
186
+
187
+ showValidationError(message) {
188
+ // Remove existing error
189
+ const existingError = document.querySelector('.validation-error');
190
+ if (existingError) {
191
+ existingError.remove();
192
+ }
193
+
194
+ // Create and show new error
195
+ const errorDiv = document.createElement('div');
196
+ errorDiv.className = 'validation-error';
197
+ errorDiv.innerHTML = `<span>⚠️ ${message}</span>`;
198
+
199
+ const textarea = document.querySelector('.sql-editor');
200
+ if (textarea && textarea.parentNode) {
201
+ textarea.parentNode.insertBefore(errorDiv, textarea.nextSibling);
202
+ }
203
+ },
204
+
205
+ hideValidationError() {
206
+ const existingError = document.querySelector('.validation-error');
207
+ if (existingError) {
208
+ existingError.remove();
209
+ }
210
+ },
211
+
212
+ // Query management
213
+ clearQuery() {
214
+ const textarea = document.querySelector('.sql-editor');
215
+ if (textarea) {
216
+ textarea.value = '';
217
+ textarea.focus();
218
+ this.validateAndUpdateUI('');
219
+ }
220
+
221
+ // Reset state
222
+ this.currentQueryState = { id: null, type: null, name: '' };
223
+
224
+ // Reset save button
225
+ const saveBtn = document.querySelector('.btn-icon.btn-save');
226
+ if (saveBtn) {
227
+ saveBtn.innerHTML = '💾';
228
+ saveBtn.title = 'Save query';
229
+ }
230
+ },
231
+
232
+ // Load table names for preview dropdown
233
+ loadTableNames() {
234
+ fetch('/pg_insights/table_names')
235
+ .then(res => res.json())
236
+ .then(data => {
237
+ const select = document.getElementById('table-preview-select');
238
+ if (select && data.tables) {
239
+ // Clear existing options except the first one
240
+ while (select.children.length > 1) {
241
+ select.removeChild(select.lastChild);
242
+ }
243
+
244
+ // Add table options
245
+ data.tables.forEach(table => {
246
+ const option = document.createElement('option');
247
+ option.value = table;
248
+ option.textContent = table;
249
+ select.appendChild(option);
250
+ });
251
+ }
252
+ })
253
+ .catch(error => {
254
+ console.error('Failed to load table names:', error);
255
+ });
256
+ },
257
+
258
+ // Preview selected table
259
+ previewTable(tableName) {
260
+ if (!tableName) return;
261
+
262
+ const sql = `SELECT * FROM ${tableName} LIMIT 10`;
263
+ const textarea = document.querySelector('.sql-editor');
264
+
265
+ if (textarea) {
266
+ textarea.value = sql;
267
+
268
+ // Validate the query
269
+ this.validateAndUpdateUI(sql);
270
+
271
+ // Auto-resize textarea
272
+ textarea.style.height = 'auto';
273
+ textarea.style.height = Math.max(160, textarea.scrollHeight) + 'px';
274
+
275
+ // Auto-execute the query
276
+ const executeBtn = document.getElementById('execute-btn');
277
+ if (executeBtn && !executeBtn.disabled) {
278
+ executeBtn.click();
279
+ }
280
+ }
281
+
282
+ // Reset the dropdown to the default option
283
+ const select = document.getElementById('table-preview-select');
284
+ if (select) {
285
+ select.value = '';
286
+ }
287
+ },
288
+
289
+ loadQueryById(queryId) {
290
+ const query = this.config.queries.find(q => q.id.toString() === queryId.toString());
291
+
292
+ if (!query) {
293
+ console.error('Query not found:', queryId);
294
+ return;
295
+ }
296
+
297
+ // Set current state
298
+ this.currentQueryState.id = query.id;
299
+ this.currentQueryState.name = query.name;
300
+ this.currentQueryState.description = query.description;
301
+ // Database IDs are numbers, built-in IDs are strings
302
+ this.currentQueryState.type = (typeof query.id === 'number') ? 'saved' : 'built-in';
303
+
304
+ // Update save button
305
+ const saveBtn = document.querySelector('.btn-icon.btn-save');
306
+ if (saveBtn) {
307
+ if (this.currentQueryState.type === 'saved') {
308
+ saveBtn.innerHTML = '📝';
309
+ saveBtn.title = 'Update saved query';
310
+ } else {
311
+ saveBtn.innerHTML = '💾';
312
+ saveBtn.title = 'Save query as new';
313
+ }
314
+ }
315
+
316
+ const textarea = document.querySelector('.sql-editor');
317
+ if (textarea) {
318
+ textarea.value = query.sql;
319
+ textarea.focus();
320
+
321
+ // Validate the loaded query
322
+ this.validateAndUpdateUI(query.sql);
323
+
324
+ // Trigger auto-resize if available
325
+ const event = new Event('input', { bubbles: true });
326
+ textarea.dispatchEvent(event);
327
+ }
328
+ },
329
+
330
+ // Query examples setup
331
+ setupQueryExamples() {
332
+ // Setup category filtering
333
+ document.querySelectorAll('.filter-btn').forEach(button => {
334
+ button.addEventListener('click', () => {
335
+ const category = button.getAttribute('data-category');
336
+
337
+ // Update active state
338
+ document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
339
+ button.classList.add('active');
340
+
341
+ // Filter query buttons
342
+ document.querySelectorAll('.query-example-btn').forEach(queryBtn => {
343
+ const queryCategory = queryBtn.getAttribute('data-category');
344
+ if (category === 'all' || queryCategory === category) {
345
+ queryBtn.classList.remove('hidden');
346
+ } else {
347
+ queryBtn.classList.add('hidden');
348
+ }
349
+ });
350
+ });
351
+ });
352
+
353
+ // Setup query example button clicks
354
+ document.querySelectorAll('.query-example-btn').forEach(button => {
355
+ button.addEventListener('click', () => {
356
+ const queryId = button.getAttribute('data-query-id');
357
+ if (queryId) {
358
+ this.loadQueryById(queryId);
359
+ }
360
+ });
361
+ });
362
+ },
363
+
364
+ // Event binding
365
+ bindEvents() {
366
+ const textarea = document.querySelector('.sql-editor');
367
+ const executeBtn = document.getElementById('execute-btn');
368
+
369
+ // Real-time validation on input
370
+ if (textarea) {
371
+ textarea.addEventListener('input', () => {
372
+ // Auto-resize
373
+ textarea.style.height = 'auto';
374
+ textarea.style.height = Math.max(160, textarea.scrollHeight) + 'px';
375
+
376
+ // Instant validation
377
+ this.validateAndUpdateUI(textarea.value);
378
+ });
379
+
380
+ // Validation on paste
381
+ textarea.addEventListener('paste', () => {
382
+ // Small delay to let paste complete
383
+ setTimeout(() => {
384
+ this.validateAndUpdateUI(textarea.value);
385
+ }, 10);
386
+ });
387
+ }
388
+
389
+ // Prevent form submission if button is disabled
390
+ if (executeBtn) {
391
+ executeBtn.addEventListener('click', (event) => {
392
+ if (executeBtn.disabled) {
393
+ event.preventDefault();
394
+ if (textarea) {
395
+ textarea.focus();
396
+ }
397
+ return false;
398
+ }
399
+ });
400
+ }
401
+
402
+ // Global functions - expose methods to global scope for onclick handlers
403
+ window.clearQuery = () => this.clearQuery();
404
+ window.copyCurrentQuery = () => this.copyCurrentQuery();
405
+ window.saveCurrentQuery = () => this.saveCurrentQuery();
406
+
407
+ // Table preview dropdown
408
+ const tableSelect = document.getElementById('table-preview-select');
409
+ if (tableSelect) {
410
+ tableSelect.addEventListener('change', (event) => {
411
+ const tableName = event.target.value;
412
+ if (tableName) {
413
+ this.previewTable(tableName);
414
+ }
415
+ });
416
+ }
417
+ },
418
+
419
+ // Initial validation
420
+ validateInitialQuery() {
421
+ const textarea = document.querySelector('.sql-editor');
422
+ if (textarea) {
423
+ const initialValue = textarea.value.trim();
424
+ if (initialValue) {
425
+ this.validateAndUpdateUI(initialValue);
426
+ } else {
427
+ this.updateExecuteButton(false);
428
+ textarea.focus();
429
+ }
430
+ }
431
+ },
432
+ };
433
+
434
+ // Initialize the application
435
+ InsightsApp.init();
436
+ });