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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +183 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/pg_insights/application.js +436 -0
- data/app/assets/javascripts/pg_insights/health.js +104 -0
- data/app/assets/javascripts/pg_insights/results/chart_renderer.js +126 -0
- data/app/assets/javascripts/pg_insights/results/table_manager.js +378 -0
- data/app/assets/javascripts/pg_insights/results/view_toggles.js +25 -0
- data/app/assets/javascripts/pg_insights/results.js +13 -0
- data/app/assets/stylesheets/pg_insights/application.css +750 -0
- data/app/assets/stylesheets/pg_insights/health.css +501 -0
- data/app/assets/stylesheets/pg_insights/results.css +682 -0
- data/app/controllers/pg_insights/application_controller.rb +4 -0
- data/app/controllers/pg_insights/health_controller.rb +110 -0
- data/app/controllers/pg_insights/insights_controller.rb +77 -0
- data/app/controllers/pg_insights/queries_controller.rb +44 -0
- data/app/helpers/pg_insights/application_helper.rb +4 -0
- data/app/helpers/pg_insights/insights_helper.rb +190 -0
- data/app/jobs/pg_insights/application_job.rb +4 -0
- data/app/jobs/pg_insights/health_check_job.rb +45 -0
- data/app/jobs/pg_insights/health_check_scheduler_job.rb +52 -0
- data/app/jobs/pg_insights/recurring_health_checks_job.rb +49 -0
- data/app/models/pg_insights/application_record.rb +5 -0
- data/app/models/pg_insights/health_check_result.rb +46 -0
- data/app/models/pg_insights/query.rb +10 -0
- data/app/services/pg_insights/health_check_service.rb +298 -0
- data/app/services/pg_insights/insight_query_service.rb +21 -0
- data/app/views/layouts/pg_insights/application.html.erb +58 -0
- data/app/views/pg_insights/health/index.html.erb +324 -0
- data/app/views/pg_insights/insights/_chart_view.html.erb +25 -0
- data/app/views/pg_insights/insights/_column_panel.html.erb +18 -0
- data/app/views/pg_insights/insights/_query_examples.html.erb +32 -0
- data/app/views/pg_insights/insights/_query_panel.html.erb +36 -0
- data/app/views/pg_insights/insights/_result.html.erb +15 -0
- data/app/views/pg_insights/insights/_results_info.html.erb +19 -0
- data/app/views/pg_insights/insights/_results_panel.html.erb +13 -0
- data/app/views/pg_insights/insights/_results_table.html.erb +45 -0
- data/app/views/pg_insights/insights/_stats_view.html.erb +3 -0
- data/app/views/pg_insights/insights/_table_controls.html.erb +21 -0
- data/app/views/pg_insights/insights/_table_view.html.erb +5 -0
- data/app/views/pg_insights/insights/index.html.erb +5 -0
- data/config/default_queries.yml +85 -0
- data/config/routes.rb +22 -0
- data/lib/generators/pg_insights/clean_generator.rb +74 -0
- data/lib/generators/pg_insights/install_generator.rb +176 -0
- data/lib/pg_insights/engine.rb +40 -0
- data/lib/pg_insights/version.rb +3 -0
- data/lib/pg_insights.rb +83 -0
- data/lib/tasks/pg_insights.rake +172 -0
- 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
|
+
[](https://badge.fury.io/rb/pg_insights)
|
6
|
+
[](https://github.com/mezbahalam/pg_insights/actions/workflows/ci.yml)
|
7
|
+
[](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,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
|
+
});
|