pg_peek 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 +217 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/pg_peek/application.js +147 -0
- data/app/assets/stylesheets/pg_peek/application.css +362 -0
- data/app/controllers/pg_peek/activity_controller.rb +16 -0
- data/app/controllers/pg_peek/application_controller.rb +36 -0
- data/app/controllers/pg_peek/databases_controller.rb +96 -0
- data/app/controllers/pg_peek/endpoints_controller.rb +10 -0
- data/app/controllers/pg_peek/indexes_controller.rb +7 -0
- data/app/controllers/pg_peek/jobs_controller.rb +44 -0
- data/app/controllers/pg_peek/pg_stat_statements_controller.rb +11 -0
- data/app/controllers/pg_peek/queries_controller.rb +13 -0
- data/app/controllers/pg_peek/tables_controller.rb +6 -0
- data/app/helpers/pg_peek/application_helper.rb +179 -0
- data/app/helpers/pg_peek/report_helper.rb +58 -0
- data/app/jobs/pg_peek/application_job.rb +4 -0
- data/app/mailers/pg_peek/application_mailer.rb +6 -0
- data/app/models/pg_peek/application_record.rb +3 -0
- data/app/models/pg_peek/database.rb +131 -0
- data/app/models/pg_peek/pg_stat_statements.rb +132 -0
- data/app/models/pg_peek/sql_comment.rb +25 -0
- data/app/models/pg_peek/table.rb +266 -0
- data/app/queries/pg_peek/pg14/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg14/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg14/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg14/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg14/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg14/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg14/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg14/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg15/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg15/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg15/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg15/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg15/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg15/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg15/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg15/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg16/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg16/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg16/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg16/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg16/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg16/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg16/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg16/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg17/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg17/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg17/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg17/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg17/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg17/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg17/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg17/tables/stats.sql +32 -0
- data/app/queries/pg_peek/pg18/activity/sessions.sql +26 -0
- data/app/queries/pg_peek/pg18/activity/summary.sql +8 -0
- data/app/queries/pg_peek/pg18/activity/waiting_locks.sql +11 -0
- data/app/queries/pg_peek/pg18/database/vitals.sql +11 -0
- data/app/queries/pg_peek/pg18/indexes/all.sql +17 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/endpoints.sql +43 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/jobs.sql +12 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers.sql +13 -0
- data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_job.sql +14 -0
- data/app/queries/pg_peek/pg18/tables/dead_tuples.sql +11 -0
- data/app/queries/pg_peek/pg18/tables/inline_stats.sql +9 -0
- data/app/queries/pg_peek/pg18/tables/stats.sql +32 -0
- data/app/reports/pg_peek/column.rb +27 -0
- data/app/reports/pg_peek/report.rb +63 -0
- data/app/reports/pg_peek/reports/blocked.rb +60 -0
- data/app/reports/pg_peek/reports/databases.rb +47 -0
- data/app/reports/pg_peek/reports/endpoints.rb +35 -0
- data/app/reports/pg_peek/reports/indexes.rb +44 -0
- data/app/reports/pg_peek/reports/job_queries.rb +14 -0
- data/app/reports/pg_peek/reports/jobs.rb +21 -0
- data/app/reports/pg_peek/reports/queries.rb +23 -0
- data/app/reports/pg_peek/reports/sessions.rb +88 -0
- data/app/reports/pg_peek/reports/tables.rb +25 -0
- data/app/views/layouts/pg_peek/application.html.erb +56 -0
- data/app/views/pg_peek/activity/show.html.erb +70 -0
- data/app/views/pg_peek/authentication_required.html.erb +31 -0
- data/app/views/pg_peek/databases/index.html.erb +43 -0
- data/app/views/pg_peek/databases/show.html.erb +169 -0
- data/app/views/pg_peek/endpoints/index.html.erb +24 -0
- data/app/views/pg_peek/indexes/index.html.erb +31 -0
- data/app/views/pg_peek/jobs/index.html.erb +51 -0
- data/app/views/pg_peek/jobs/show.html.erb +17 -0
- data/app/views/pg_peek/pg_stat_statements/_preload_instructions.html.erb +17 -0
- data/app/views/pg_peek/pg_stat_statements/not_preloaded.html.erb +5 -0
- data/app/views/pg_peek/queries/index.html.erb +10 -0
- data/app/views/pg_peek/reports/_report.html.erb +32 -0
- data/app/views/pg_peek/tables/show.html.erb +128 -0
- data/config/routes.rb +16 -0
- data/lib/generators/pg_peek/install/install_generator.rb +40 -0
- data/lib/generators/pg_peek/install/templates/README +31 -0
- data/lib/generators/pg_peek/install/templates/initializer.rb +38 -0
- data/lib/pg_peek/configuration.rb +59 -0
- data/lib/pg_peek/engine.rb +7 -0
- data/lib/pg_peek/query_loader.rb +59 -0
- data/lib/pg_peek/version.rb +3 -0
- data/lib/pg_peek.rb +10 -0
- data/lib/tasks/pg_peek_tasks.rake +4 -0
- metadata +192 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: edfdc4a86128d1eec3f1c69e731780b9ef71afdd071d76cde2756eee57d55ff8
|
|
4
|
+
data.tar.gz: 666c7f0204c05e295f6c6950e32362720e273d579ec3eec6c557fd2769ab14c0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4ecde1526a25bd02fe30c47dbbe62ff2058ce2d8f6b1ff741ffd50b2eea26236e102646f98b67dabf4d9f6d566717a838c04c15be895d5d3b335a59bde342945
|
|
7
|
+
data.tar.gz: 44fa0fbf8d8ccd408c9e967b78ec29503fc94f605beb961cdc8480137773e7b6145af740990c039ac4f09be1c7664d03c204290190c0f9cd94a1e674b9517d15
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Tomasz Mazur
|
|
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,217 @@
|
|
|
1
|
+
# PgPeek
|
|
2
|
+
|
|
3
|
+
A Rails Engine that provides a web UI for monitoring and analyzing PostgreSQL query performance. Mount it into your Rails application for easy database introspection and slow query analysis.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Database Overview**: View PostgreSQL version, installed extensions, and table listings
|
|
8
|
+
- **Query Performance Analysis**: Identify slow queries using the `pg_stat_statements` extension
|
|
9
|
+
- **SQLcommenter Support**: Parses and displays SQLcommenter tags from queries
|
|
10
|
+
- **Endpoint and Active Job Breakdowns**: Groups query cost by controller/action and by job class, with a per-job drill-down into its query shapes
|
|
11
|
+
- **Multi-Database Support**: Works with multiple database configurations (primary, replica, etc.)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add this line to your application's Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "pg_peek"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
And then execute:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then run the installer, which mounts the engine at `/pg_peek` and creates the
|
|
28
|
+
initializer:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bin/rails generate pg_peek:install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pass `--skip-routes` if you would rather mount the engine yourself, for example
|
|
35
|
+
inside an authenticated scope.
|
|
36
|
+
|
|
37
|
+
Now map each database in your `database.yml` to the ActiveRecord class that
|
|
38
|
+
connects to it:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/pg_peek.rb
|
|
42
|
+
Rails.application.config.pg_peek.tap do |config|
|
|
43
|
+
config.connections = {
|
|
44
|
+
"primary" => "ApplicationRecord"
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This step is required. A database without an entry cannot be opened and is not
|
|
50
|
+
listed.
|
|
51
|
+
|
|
52
|
+
Visit `/pg_peek` in your application to access the dashboard.
|
|
53
|
+
|
|
54
|
+
### Authentication
|
|
55
|
+
|
|
56
|
+
pg_peek exposes your schema, every normalized query in the database, and an
|
|
57
|
+
endpoint that resets query statistics. Outside development and test it refuses
|
|
58
|
+
to serve until you say how it is protected.
|
|
59
|
+
|
|
60
|
+
Give it credentials and it asks for them over HTTP basic:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
PG_PEEK_USERNAME=peek PG_PEEK_PASSWORD=...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
or equivalently in `config/credentials.yml.enc`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
pg_peek:
|
|
70
|
+
username: peek
|
|
71
|
+
password: ...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Credentials, once set, apply in **every** environment — including a development
|
|
75
|
+
server exposed through a tunnel.
|
|
76
|
+
|
|
77
|
+
If you would rather mount the engine behind authentication you already have, say
|
|
78
|
+
so and pg_peek stays out of the way:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# config/initializers/pg_peek.rb
|
|
82
|
+
config.public_dashboard = true
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# config/routes.rb
|
|
87
|
+
constraints ->(request) { request.session[:admin_id].present? } do
|
|
88
|
+
mount PgPeek::Engine, at: "/pg_peek"
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Development and test are exempt when no credentials are set, so local work is
|
|
93
|
+
never interrupted.
|
|
94
|
+
|
|
95
|
+
### Enabling pg_stat_statements
|
|
96
|
+
|
|
97
|
+
Query analysis needs the `pg_stat_statements` extension, and enabling it is two
|
|
98
|
+
separate steps on two different things. Doing only the first is the most common
|
|
99
|
+
way to end up with a dashboard that shows nothing.
|
|
100
|
+
|
|
101
|
+
**1. Load the module into the server.** This is PostgreSQL configuration, not
|
|
102
|
+
database state: a migration cannot do it, and it takes effect only after a
|
|
103
|
+
restart.
|
|
104
|
+
|
|
105
|
+
```sql
|
|
106
|
+
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
or the equivalent line in `postgresql.conf`. Then restart PostgreSQL. On a
|
|
110
|
+
managed service this is usually a parameter-group setting; in Docker, pass
|
|
111
|
+
`-c shared_preload_libraries=pg_stat_statements` to the server command.
|
|
112
|
+
|
|
113
|
+
**2. Create the extension in each database.** A migration, so it is tracked in
|
|
114
|
+
your schema and applied per environment:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
class EnablePgStatStatements < ActiveRecord::Migration[8.1]
|
|
118
|
+
def change
|
|
119
|
+
enable_extension "pg_stat_statements"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For a secondary database, generate it into that database's migrations path:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
bin/rails generate migration EnablePgStatStatements --database analytics
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If you do step 2 without step 1, the extension reports itself as installed but
|
|
131
|
+
its views cannot be queried. pg_peek detects this and shows the instructions
|
|
132
|
+
above in place of every page that needs statistics, so you will not be left
|
|
133
|
+
guessing — but you will be left waiting for a restart.
|
|
134
|
+
|
|
135
|
+
A restart also clears all collected statistics, so expect an empty dashboard
|
|
136
|
+
until your application has run for a while afterwards.
|
|
137
|
+
|
|
138
|
+
### Requirements
|
|
139
|
+
|
|
140
|
+
- Rails 8.1+
|
|
141
|
+
- PostgreSQL 14+ with `pg_stat_statements` extension (for query analysis features)
|
|
142
|
+
- Only officially supported PostgreSQL versions are supported
|
|
143
|
+
|
|
144
|
+
### Why not pg_stat_monitor?
|
|
145
|
+
|
|
146
|
+
pg_peek reads `pg_stat_statements` only. This is deliberate, not an omission.
|
|
147
|
+
|
|
148
|
+
Percona's `pg_stat_monitor` has two genuine advantages: it parses SQLcommenter
|
|
149
|
+
tags into a column of their own, and it keeps time-bucketed history rather than
|
|
150
|
+
one counter since the last reset. Both would simplify this engine.
|
|
151
|
+
|
|
152
|
+
But it is not part of PostgreSQL's contrib, so it is unavailable on Heroku and
|
|
153
|
+
most managed PostgreSQL. And its bucketed figures are not comparable with
|
|
154
|
+
`pg_stat_statements`' cumulative ones: supporting both would give two different
|
|
155
|
+
answers to "what does this endpoint cost" depending on which extension happened
|
|
156
|
+
to be installed. One data source that works everywhere beats two that disagree.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
### Prerequisites
|
|
161
|
+
|
|
162
|
+
- Ruby (see `.ruby-version`)
|
|
163
|
+
- Docker and Docker Compose (for PostgreSQL)
|
|
164
|
+
|
|
165
|
+
### Setup
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Install dependencies
|
|
169
|
+
bundle install
|
|
170
|
+
|
|
171
|
+
# Start PostgreSQL 18 (default for development)
|
|
172
|
+
docker compose up postgres18 -d
|
|
173
|
+
|
|
174
|
+
# Setup the dummy app database
|
|
175
|
+
bin/rails db:setup
|
|
176
|
+
|
|
177
|
+
# Start the development server
|
|
178
|
+
bin/rails s
|
|
179
|
+
|
|
180
|
+
# Visit http://localhost:3000/pg_peek
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Testing Against Other PostgreSQL Versions
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Run databases
|
|
187
|
+
docker compose up -d
|
|
188
|
+
|
|
189
|
+
# PostgreSQL 14
|
|
190
|
+
bin/test-pg14
|
|
191
|
+
|
|
192
|
+
# PostgreSQL 15
|
|
193
|
+
bin/test-pg15
|
|
194
|
+
|
|
195
|
+
# PostgreSQL 16
|
|
196
|
+
bin/test-pg16
|
|
197
|
+
|
|
198
|
+
# PostgreSQL 17
|
|
199
|
+
bin/test-pg17
|
|
200
|
+
|
|
201
|
+
# PostgreSQL 18
|
|
202
|
+
bin/test-pg18
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Testing
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Run the full test suite
|
|
209
|
+
bin/test
|
|
210
|
+
|
|
211
|
+
# Code style check
|
|
212
|
+
bin/rubocop
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
require "bundler/gem_tasks"
|
|
7
|
+
|
|
8
|
+
require "rake/testtask"
|
|
9
|
+
|
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
|
11
|
+
t.libs << "test"
|
|
12
|
+
t.pattern = "test/**/*_test.rb"
|
|
13
|
+
t.verbose = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task default: :test
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// pg_peek — no framework, no CDN, no import map. Three small behaviours.
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
// Theme -------------------------------------------------------------------
|
|
6
|
+
function applyTheme(theme) {
|
|
7
|
+
document.documentElement.dataset.theme = theme;
|
|
8
|
+
|
|
9
|
+
var meta = document.querySelector('meta[name="color-scheme"]');
|
|
10
|
+
if (meta) meta.content = theme;
|
|
11
|
+
|
|
12
|
+
document.querySelectorAll("[data-theme-toggle]").forEach(function (button) {
|
|
13
|
+
button.textContent = "theme: " + theme;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function storedTheme() {
|
|
18
|
+
try {
|
|
19
|
+
return localStorage.getItem("pg_peek.theme");
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return null; // Private windows and blocked site data both throw.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toggleTheme() {
|
|
26
|
+
var next = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
|
|
27
|
+
applyTheme(next);
|
|
28
|
+
try {
|
|
29
|
+
localStorage.setItem("pg_peek.theme", next);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
/* Nothing to do: the toggle still works for this page view. */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function typing() {
|
|
36
|
+
var el = document.activeElement;
|
|
37
|
+
return el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function initTheme() {
|
|
41
|
+
var preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
42
|
+
applyTheme(storedTheme() || preferred);
|
|
43
|
+
|
|
44
|
+
document.querySelectorAll("[data-theme-toggle]").forEach(function (button) {
|
|
45
|
+
button.addEventListener("click", function (event) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
toggleTheme();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
document.addEventListener("keydown", function (event) {
|
|
52
|
+
if (event.key === "t" && !typing() && !event.metaKey && !event.ctrlKey) toggleTheme();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Filtering ---------------------------------------------------------------
|
|
57
|
+
function initFilter() {
|
|
58
|
+
var input = document.querySelector("[data-filter-input]");
|
|
59
|
+
if (!input) return;
|
|
60
|
+
|
|
61
|
+
// Scoped to the nearest [data-filter] ancestor when there is one, so a
|
|
62
|
+
// page with several report tables -- the overview, with endpoints, jobs,
|
|
63
|
+
// queries and tables all on it -- filters only the one the input actually
|
|
64
|
+
// belongs to, rather than every row on the page.
|
|
65
|
+
var scope = input.closest("[data-filter]") || document;
|
|
66
|
+
var items = Array.prototype.slice.call(scope.querySelectorAll("[data-filter-value]"));
|
|
67
|
+
var count = scope.querySelector("[data-filter-count]");
|
|
68
|
+
var empty = scope.querySelector("[data-filter-empty]");
|
|
69
|
+
var noun = input.dataset.filterNoun || "row";
|
|
70
|
+
var timer;
|
|
71
|
+
|
|
72
|
+
function render() {
|
|
73
|
+
var query = input.value.trim().toLowerCase();
|
|
74
|
+
var matched = 0;
|
|
75
|
+
|
|
76
|
+
items.forEach(function (item) {
|
|
77
|
+
var hit = !query || item.dataset.filterValue.indexOf(query) !== -1;
|
|
78
|
+
item.hidden = !hit;
|
|
79
|
+
if (hit) matched++;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (count) count.textContent = matched + " " + noun + (matched === 1 ? "" : "s");
|
|
83
|
+
if (empty) empty.hidden = matched > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
input.addEventListener("input", function () {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = setTimeout(render, 120);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
document.addEventListener("keydown", function (event) {
|
|
92
|
+
if (event.key === "/" && document.activeElement !== input) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
input.focus();
|
|
95
|
+
} else if (event.key === "Escape" && document.activeElement === input) {
|
|
96
|
+
input.value = "";
|
|
97
|
+
render();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
render();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Confirmation ------------------------------------------------------------
|
|
105
|
+
// Previously data-confirm, which needed rails-ujs the engine never shipped:
|
|
106
|
+
// the reset button asked nothing and simply reset.
|
|
107
|
+
function initConfirm() {
|
|
108
|
+
document.querySelectorAll("form[data-confirm]").forEach(function (form) {
|
|
109
|
+
form.addEventListener("submit", function (event) {
|
|
110
|
+
if (!window.confirm(form.dataset.confirm)) event.preventDefault();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Database switcher -------------------------------------------------------
|
|
116
|
+
function initSwitcher() {
|
|
117
|
+
document.querySelectorAll("[data-switcher]").forEach(function (select) {
|
|
118
|
+
select.addEventListener("change", function () {
|
|
119
|
+
window.location.assign(select.value);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Reload ------------------------------------------------------------------
|
|
125
|
+
// The activity page is a snapshot; "r" refreshes it the way top does.
|
|
126
|
+
function initReload() {
|
|
127
|
+
document.addEventListener("keydown", function (event) {
|
|
128
|
+
if (event.key === "r" && !typing() && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
129
|
+
window.location.reload();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function init() {
|
|
135
|
+
initTheme();
|
|
136
|
+
initFilter();
|
|
137
|
+
initConfirm();
|
|
138
|
+
initSwitcher();
|
|
139
|
+
initReload();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (document.readyState === "loading") {
|
|
143
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
144
|
+
} else {
|
|
145
|
+
init();
|
|
146
|
+
}
|
|
147
|
+
})();
|