pgbouncerhero 2.0.0 → 3.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 +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +155 -58
- data/app/assets/builds/pgbouncerhero/application.css +2 -0
- data/app/assets/stylesheets/pgbouncerhero/application.css +1 -0
- data/app/controllers/pg_bouncer_hero/application_controller.rb +6 -9
- data/app/controllers/pg_bouncer_hero/database_controller.rb +39 -13
- data/app/controllers/pg_bouncer_hero/home_controller.rb +0 -2
- data/app/helpers/pg_bouncer_hero/application_helper.rb +19 -13
- data/app/javascript/pgbouncerhero/application.js +6 -0
- data/app/javascript/pgbouncerhero/controllers/polling_controller.js +27 -0
- data/app/views/layouts/pg_bouncer_hero/application.html.erb +19 -22
- data/app/views/pg_bouncer_hero/database/_actions.html.erb +9 -9
- data/app/views/pg_bouncer_hero/database/_clients.html.erb +21 -23
- data/app/views/pg_bouncer_hero/database/_conf.html.erb +21 -23
- data/app/views/pg_bouncer_hero/database/_databases.html.erb +21 -23
- data/app/views/pg_bouncer_hero/database/_menu.html.erb +20 -18
- data/app/views/pg_bouncer_hero/database/_pools.html.erb +21 -23
- data/app/views/pg_bouncer_hero/database/_state.html.erb +11 -0
- data/app/views/pg_bouncer_hero/database/_stats.html.erb +27 -33
- data/app/views/pg_bouncer_hero/database/state.html.erb +4 -0
- data/app/views/pg_bouncer_hero/database/summary.html.erb +3 -0
- data/app/views/pg_bouncer_hero/home/_card.html.erb +9 -22
- data/app/views/pg_bouncer_hero/home/_card_content.html.erb +33 -33
- data/app/views/pg_bouncer_hero/home/_card_loading_content.html.erb +5 -5
- data/app/views/pg_bouncer_hero/home/index.html.erb +13 -37
- data/app/views/shared/_alert.html.erb +2 -5
- data/config/importmap.rb +5 -0
- data/config/routes.rb +10 -2
- data/lib/generators/pgbouncerhero/templates/config.yml +13 -8
- data/lib/pgbouncerhero/connection.rb +53 -16
- data/lib/pgbouncerhero/database.rb +69 -4
- data/lib/pgbouncerhero/engine.rb +13 -10
- data/lib/pgbouncerhero/group.rb +0 -2
- data/lib/pgbouncerhero/methods/basics.rb +28 -12
- data/lib/pgbouncerhero/version.rb +1 -1
- data/lib/pgbouncerhero.rb +124 -41
- metadata +187 -46
- data/.gitignore +0 -19
- data/Gemfile +0 -4
- data/app/assets/images/pgbouncerhero/short-paragraph.png +0 -0
- data/app/assets/javascripts/pgbouncerhero/application.js +0 -3
- data/app/assets/stylesheets/pgbouncerhero/application.css.scss +0 -15
- data/app/views/pg_bouncer_hero/database/summary.js.erb +0 -5
- data/doc/screenshot-1.png +0 -0
- data/doc/screenshot-2.png +0 -0
- data/doc/screenshot-3.png +0 -0
- data/pgbouncerhero.gemspec +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 913492602328ce8fb2d3731212ccd7cef95c2a9429fbe6d4112d9a65d5853b35
|
|
4
|
+
data.tar.gz: 5475f266d924090db51b7a242cc7a7e9b90e917dd485e562f346d7f471d3e9bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e6ceb49a3f0c1b28a2edeb0939fdc7bd9f4284c02c5361f879ecedd7cb9af17f1862f5943fb75b9c79e03f5d75d0242fefc4c8ee7b67cf43b62c1ee0414c345
|
|
7
|
+
data.tar.gz: 96bc7a41d920340e9fce91c6686b6819a71393713b8e4e559e95cf790ebcae3b5d7fba13a29b62d32342aced8dceb8376c0db92b2c29f3678252199ae4c2f4e4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## 3.1.0 - 2026-08-23
|
|
4
|
+
|
|
5
|
+
**New:**
|
|
6
|
+
- Read-only mode for hiding and server-side blocking PgBouncer administrative commands
|
|
7
|
+
- PgBouncer state visibility and Resume controls
|
|
8
|
+
- Integration coverage across supported PgBouncer 1.23, 1.24, and 1.25 releases
|
|
9
|
+
- Bounded, thread-safe connection pools per PgBouncer with configurable size and checkout timeout
|
|
10
|
+
- Explicit `Database#with_connection` leases for running multiple commands on one connection
|
|
11
|
+
- Real PgBouncer integration coverage for admin queries, summaries, reloads, authentication, and reconnection
|
|
12
|
+
- A dedicated CI integration job backed by the repository's Docker Compose stack
|
|
13
|
+
- Configurable `PgBouncerHero.config_path`, resolved from the host application's `Rails.root` by default
|
|
14
|
+
- `PgBouncerHero.reset!` for reloading configuration and connections safely
|
|
15
|
+
- Request-level engine coverage and gem-package validation in CI
|
|
16
|
+
|
|
17
|
+
**Improved:**
|
|
18
|
+
- Use `SHUTDOWN WAIT_FOR_CLIENTS` for graceful shutdowns initiated from the dashboard
|
|
19
|
+
- Reconnect stale or failed pooled connections automatically and close every pool on reset
|
|
20
|
+
- Pin PostgreSQL and PgBouncer development images and wait for container health before testing
|
|
21
|
+
- Let Dependabot maintain Docker image versions alongside gems and GitHub Actions
|
|
22
|
+
- Protect PgBouncer connections from concurrent use while allowing bounded parallel queries
|
|
23
|
+
- Resolve parameterized group and database names consistently in routes and controllers
|
|
24
|
+
- Load Rails engine dependencies explicitly and declare Propshaft as a runtime dependency
|
|
25
|
+
- Cache Appraisal dependencies directly in the Ruby/Rails CI matrix
|
|
26
|
+
- Pin GitHub Actions to their current immutable release commits
|
|
27
|
+
- Add read-only workflow permissions, cancellation of superseded runs, and job timeouts
|
|
28
|
+
|
|
29
|
+
## 3.0.0
|
|
30
|
+
|
|
31
|
+
**Breaking Changes:**
|
|
32
|
+
- Requires Ruby >= 3.2 and Rails >= 7.2
|
|
33
|
+
- Removed jQuery, Semantic UI, and Sprockets dependencies
|
|
34
|
+
- Replaced with Hotwire (Turbo + Stimulus), Tailwind CSS 4, Propshaft, and importmap-rails
|
|
35
|
+
- Removed JRuby support
|
|
36
|
+
- Host apps must use Propshaft and importmap-rails
|
|
37
|
+
|
|
38
|
+
**New:**
|
|
39
|
+
- Modern Tailwind CSS 4 UI with responsive design
|
|
40
|
+
- Turbo Frames for lazy-loading database cards (replaces jQuery AJAX polling)
|
|
41
|
+
- Stimulus controller for 60-second auto-refresh
|
|
42
|
+
- RuboCop (rubocop-rails-omakase) for Ruby linting
|
|
43
|
+
- Herb for ERB template linting
|
|
44
|
+
- Minitest test suite with dummy Rails app
|
|
45
|
+
- Appraisal for multi-Rails version testing (7.2, 8.0, 8.1)
|
|
46
|
+
- GitHub Actions CI (Ruby 3.2/3.3/3.4/4.0 x Rails 7.2/8.0/8.1)
|
|
47
|
+
- Gem metadata (source_code_uri, changelog_uri, bug_tracker_uri, rubygems_mfa_required)
|
|
48
|
+
|
|
49
|
+
**Improved:**
|
|
50
|
+
- `rescue Exception` replaced with `rescue StandardError`
|
|
51
|
+
- `YAML.load` replaced with `YAML.safe_load`
|
|
52
|
+
- Removed deprecated `require_dependency`
|
|
53
|
+
- Removed `before_filter` fallback (Rails 3/4 compat)
|
|
54
|
+
- Config caching uses module-level `@config` instead of `Thread.current`
|
|
55
|
+
- Removed `html_safe` on flash messages
|
|
56
|
+
- Updated config template terminology: `master`/`slave` to `primary`/`replica`
|
|
57
|
+
|
|
58
|
+
## 2.0.0
|
|
59
|
+
|
|
60
|
+
- Easier setup
|
|
61
|
+
- Automatically require jquery
|
|
62
|
+
- Automatically require semantic-ui-sass
|
|
63
|
+
- Use pgbouncerhero/application stylesheets and javascript
|
|
64
|
+
|
|
1
65
|
## 1.0.3
|
|
2
66
|
|
|
3
67
|
- Drop Haml dependency
|
data/README.md
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
# PgBouncerHero
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/pgbouncerhero)
|
|
4
|
+
[](https://github.com/kwent/pgbouncerhero/actions/workflows/test.yml)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
A graphical user interface for your PgBouncers.
|
|
6
8
|
|
|
7
|
-
[](https://
|
|
8
|
-
[](https://
|
|
9
|
-
[](https://github.com/kwent/pgbouncerhero)
|
|
10
|
+
[](https://github.com/kwent/pgbouncerhero)
|
|
11
|
+
[](https://github.com/kwent/pgbouncerhero)
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Ruby >= 3.2
|
|
16
|
+
- Rails >= 7.2
|
|
17
|
+
- PgBouncer >= 1.23
|
|
18
|
+
- Propshaft (asset pipeline)
|
|
19
|
+
- importmap-rails
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
## Installation
|
|
14
22
|
|
|
15
|
-
Add
|
|
23
|
+
Add to your application's Gemfile:
|
|
16
24
|
|
|
17
25
|
```ruby
|
|
18
|
-
gem
|
|
26
|
+
gem "pgbouncerhero"
|
|
19
27
|
```
|
|
20
28
|
|
|
21
29
|
And mount the engine in your `config/routes.rb`:
|
|
@@ -36,7 +44,7 @@ ENV["PGBOUNCERHERO_PASSWORD"] = "triforce"
|
|
|
36
44
|
### Devise
|
|
37
45
|
|
|
38
46
|
```ruby
|
|
39
|
-
authenticate :user, ->
|
|
47
|
+
authenticate :user, ->(user) { user.admin? } do
|
|
40
48
|
mount PgBouncerHero::Engine, at: "pgbouncerhero"
|
|
41
49
|
end
|
|
42
50
|
```
|
|
@@ -49,58 +57,147 @@ export PGBOUNCERHERO_DATABASE_URL=postgres://user:password@host:port/pgbouncer
|
|
|
49
57
|
|
|
50
58
|
## Multiple PgBouncers
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
Generate a config file:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
rails generate pgbouncerhero:config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or create `config/pgbouncerhero.yml` manually:
|
|
53
67
|
|
|
54
68
|
```yml
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
production:
|
|
72
|
-
<<: *default
|
|
69
|
+
pgbouncers:
|
|
70
|
+
production:
|
|
71
|
+
primary:
|
|
72
|
+
url: <%= ENV["PGBOUNCER_PRODUCTION_PRIMARY_DATABASE_URL"] %>
|
|
73
|
+
pool_size: 5
|
|
74
|
+
pool_timeout: 5
|
|
75
|
+
replica:
|
|
76
|
+
url: <%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
|
|
77
|
+
staging:
|
|
78
|
+
primary:
|
|
79
|
+
url: <%= ENV["PGBOUNCER_STAGING_PRIMARY_DATABASE_URL"] %>
|
|
80
|
+
replica:
|
|
81
|
+
url: <%= ENV["PGBOUNCER_STAGING_REPLICA_DATABASE_URL"] %>
|
|
82
|
+
|
|
83
|
+
# Optional: allow monitoring while blocking all administrative commands
|
|
84
|
+
read_only: true
|
|
73
85
|
```
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
Environment-specific top-level keys are also supported. PgBouncerHero reads
|
|
88
|
+
`config/pgbouncerhero.yml` from the host application's `Rails.root` by default.
|
|
89
|
+
An initializer can point to a different file:
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
```ruby
|
|
92
|
+
PgBouncerHero.config_path = Rails.root.join("config/pgbouncerhero.production.yml")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Changing `PgBouncerHero.env` or `config_path` resets cached groups and closes
|
|
96
|
+
their open connections. Call `PgBouncerHero.reset!` after changing a config
|
|
97
|
+
file at runtime, or `PgBouncerHero.disconnect!` when only the connections need
|
|
98
|
+
to be closed. Stale connections reconnect automatically.
|
|
99
|
+
|
|
100
|
+
The PostgreSQL connection timeout defaults to five seconds and can be changed
|
|
101
|
+
with `PGBOUNCERHERO_TIMEOUT`.
|
|
102
|
+
|
|
103
|
+
PgBouncerHero keeps a bounded, lazily created connection pool for each
|
|
104
|
+
configured PgBouncer. The pool defaults to five connections with a five-second
|
|
105
|
+
checkout timeout. Set `PGBOUNCERHERO_POOL_SIZE` and
|
|
106
|
+
`PGBOUNCERHERO_POOL_TIMEOUT` to change the defaults for every PgBouncer, or set
|
|
107
|
+
`pool_size` and `pool_timeout` on an individual database entry as shown above.
|
|
108
|
+
Per-database settings take precedence over environment variables.
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
The
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
Code that needs to issue multiple commands on the same PostgreSQL connection
|
|
111
|
+
can lease one explicitly:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
database.with_connection do |connection|
|
|
115
|
+
connection.exec("SHOW VERSION")
|
|
116
|
+
connection.exec("SHOW CONFIG")
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`database.connection` remains available as a connection-compatible proxy for
|
|
121
|
+
existing integrations. Connections are checked when borrowed, and stale or
|
|
122
|
+
failed connections are discarded and recreated automatically.
|
|
123
|
+
|
|
124
|
+
### Safe Administration
|
|
125
|
+
|
|
126
|
+
The dashboard shows PgBouncer's current state and provides Reload, Suspend,
|
|
127
|
+
Resume, and graceful shutdown controls. Dashboard shutdown uses
|
|
128
|
+
`SHUTDOWN WAIT_FOR_CLIENTS`: PgBouncer stops accepting new clients and exits
|
|
129
|
+
after existing clients disconnect. The Ruby API keeps `database.shutdown` as
|
|
130
|
+
the immediate command for compatibility and also accepts `:immediate`,
|
|
131
|
+
`:wait_for_clients`, or `:wait_for_servers`. State visibility requires
|
|
132
|
+
PgBouncer 1.19 or newer; graceful shutdown modes require PgBouncer 1.23 or
|
|
133
|
+
newer.
|
|
134
|
+
|
|
135
|
+
Set `read_only: true` at the top level of `config/pgbouncerhero.yml`, or set
|
|
136
|
+
`PGBOUNCERHERO_READ_ONLY=true`, to hide and server-side reject every
|
|
137
|
+
administrative command. The environment variable takes precedence over YAML.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
Start PostgreSQL and PgBouncer with Docker:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
docker compose up --detach --wait
|
|
106
145
|
```
|
|
146
|
+
|
|
147
|
+
Run the dummy Rails app:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
PGBOUNCERHERO_DATABASE_URL=postgres://pgbouncer:pgbouncer@localhost:6432/pgbouncer \
|
|
151
|
+
bundle exec rackup test/dummy/config.ru -p 3000
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Then open http://localhost:3000/pgbouncerhero.
|
|
155
|
+
|
|
156
|
+
Run the test suite:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
bundle exec rake # unit/integration-free tests + rubocop + herb
|
|
160
|
+
bundle exec appraisal rake test # tests across Rails 7.2, 8.0, and 8.1
|
|
161
|
+
bundle exec rake build # build and validate the gem package
|
|
162
|
+
bundle exec rake test:integration # real PgBouncer admin-console tests
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The integration task expects PostgreSQL and PgBouncer from the Compose stack.
|
|
166
|
+
Override `PGBOUNCERHERO_INTEGRATION_URL` to test another PgBouncer instance.
|
|
167
|
+
The default is `postgres://pgbouncer:pgbouncer@127.0.0.1:6432/pgbouncer`.
|
|
168
|
+
CI runs this suite against PgBouncer 1.23, 1.24, and 1.25.
|
|
169
|
+
|
|
170
|
+
Stop Docker when done:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
docker compose down --volumes
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Releasing
|
|
177
|
+
|
|
178
|
+
Releases use a scoped RubyGems OIDC API Key Role through GitHub Actions, with no
|
|
179
|
+
long-lived RubyGems API key. The role is restricted to the `pgbouncerhero` gem,
|
|
180
|
+
the `kwent/pgbouncerhero` repository, and the `rubygems.org` audience. Store its
|
|
181
|
+
role token in the `RUBYGEMS_OIDC_ROLE` GitHub Actions secret. Then run the
|
|
182
|
+
**Release** workflow on `master` and enter the version already committed in
|
|
183
|
+
`lib/pgbouncerhero/version.rb`. The workflow verifies the version and test
|
|
184
|
+
suite, exchanges GitHub's OIDC identity for a short-lived credential, publishes
|
|
185
|
+
the gem, pushes the version tag, and creates the matching GitHub release.
|
|
186
|
+
|
|
187
|
+
## Contributing
|
|
188
|
+
|
|
189
|
+
1. Fork it
|
|
190
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
191
|
+
3. Commit your changes (`git commit -am "Add some feature"`)
|
|
192
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
193
|
+
5. Create a new Pull Request
|
|
194
|
+
|
|
195
|
+
## Authors
|
|
196
|
+
|
|
197
|
+
- [Quentin Rousseau](https://github.com/kwent)
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
Copyright (c) 2025 Quentin Rousseau
|
|
202
|
+
|
|
203
|
+
MIT License. See [LICENSE.txt](LICENSE.txt) for details.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.fixed{position:fixed}.static{position:static}.sticky{position:sticky}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.right-0{right:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-50{z-index:50}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-14{height:calc(var(--spacing) * 14)}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-2\/3{width:66.6667%}.w-3\/4{width:75%}.w-24{width:calc(var(--spacing) * 24)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.min-w-full{min-width:100%}.flex-1{flex:1}.table-auto{table-layout:auto}.animate-pulse{animation:var(--animate-pulse)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-yellow-200{border-color:var(--color-yellow-200)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-500{background-color:var(--color-green-500)}.bg-red-50{background-color:var(--color-red-50)}.bg-white{background-color:var(--color-white)}.bg-yellow-50{background-color:var(--color-yellow-50)}.p-4{padding:calc(var(--spacing) * 4)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.pt-20{padding-top:calc(var(--spacing) * 20)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-inherit{color:inherit}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-green-600\/20{--tw-ring-color:#00a54433}@supports (color:color-mix(in lab, red, red)){.ring-green-600\/20{--tw-ring-color:color-mix(in oklab, var(--color-green-600) 20%, transparent)}}.ring-red-600\/20{--tw-ring-color:#e4001433}@supports (color:color-mix(in lab, red, red)){.ring-red-600\/20{--tw-ring-color:color-mix(in oklab, var(--color-red-600) 20%, transparent)}}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}}@media (min-width:40rem){.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes pulse{50%{opacity:.5}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -2,25 +2,22 @@ module PgBouncerHero
|
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
3
|
layout "pg_bouncer_hero/application"
|
|
4
4
|
|
|
5
|
-
protect_from_forgery
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
6
|
|
|
7
7
|
http_basic_authenticate_with name: ENV["PGBOUNCERHERO_USERNAME"], password: ENV["PGBOUNCERHERO_PASSWORD"] if ENV["PGBOUNCERHERO_PASSWORD"]
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
before_action :set_database
|
|
11
|
-
else
|
|
12
|
-
before_filter :set_database
|
|
13
|
-
end
|
|
9
|
+
before_action :set_database
|
|
14
10
|
|
|
15
11
|
protected
|
|
16
12
|
|
|
17
13
|
def set_database
|
|
18
14
|
@groups = PgBouncerHero.groups
|
|
19
15
|
if params[:group] && params[:database]
|
|
20
|
-
@
|
|
16
|
+
@group = @groups.fetch(params[:group])
|
|
17
|
+
@database = @group.databases.find { |database| database.name.parameterize == params[:database] }
|
|
21
18
|
else
|
|
22
|
-
@group = @groups.first
|
|
23
|
-
@database = @
|
|
19
|
+
_group_id, @group = @groups.first
|
|
20
|
+
@database = @group.databases.first
|
|
24
21
|
end
|
|
25
22
|
end
|
|
26
23
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
require_dependency 'pg_bouncer_hero/application_controller'
|
|
2
|
-
|
|
3
1
|
module PgBouncerHero
|
|
4
2
|
class DatabaseController < ApplicationController
|
|
3
|
+
before_action :ensure_writable!, only: %i[reload suspend resume shutdown]
|
|
4
|
+
|
|
5
5
|
def summary
|
|
6
6
|
if @database.connection
|
|
7
7
|
@dbs = @database.summary
|
|
@@ -9,6 +9,7 @@ module PgBouncerHero
|
|
|
9
9
|
flash[:error] = "#{@database.name} does not look online."
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
|
+
|
|
12
13
|
def databases
|
|
13
14
|
if @database.connection
|
|
14
15
|
@dbs = @database.databases
|
|
@@ -16,6 +17,7 @@ module PgBouncerHero
|
|
|
16
17
|
flash[:error] = "#{@database.name} does not look online."
|
|
17
18
|
end
|
|
18
19
|
end
|
|
20
|
+
|
|
19
21
|
def stats
|
|
20
22
|
if @database.connection
|
|
21
23
|
@stats = @database.stats
|
|
@@ -23,6 +25,7 @@ module PgBouncerHero
|
|
|
23
25
|
flash[:error] = "#{@database.name} does not look online."
|
|
24
26
|
end
|
|
25
27
|
end
|
|
28
|
+
|
|
26
29
|
def pools
|
|
27
30
|
if @database.connection
|
|
28
31
|
@pools = @database.pools
|
|
@@ -30,6 +33,7 @@ module PgBouncerHero
|
|
|
30
33
|
flash[:error] = "#{@database.name} does not look online."
|
|
31
34
|
end
|
|
32
35
|
end
|
|
36
|
+
|
|
33
37
|
def clients
|
|
34
38
|
if @database.connection
|
|
35
39
|
@clients = @database.clients
|
|
@@ -37,6 +41,7 @@ module PgBouncerHero
|
|
|
37
41
|
flash[:error] = "#{@database.name} does not look online."
|
|
38
42
|
end
|
|
39
43
|
end
|
|
44
|
+
|
|
40
45
|
def conf
|
|
41
46
|
if @database.connection
|
|
42
47
|
@conf = @database.conf
|
|
@@ -44,29 +49,50 @@ module PgBouncerHero
|
|
|
44
49
|
flash[:error] = "#{@database.name} does not look online."
|
|
45
50
|
end
|
|
46
51
|
end
|
|
47
|
-
|
|
52
|
+
|
|
53
|
+
def state
|
|
48
54
|
if @database.connection
|
|
49
|
-
@database.
|
|
50
|
-
flash[:success] = "#{@database.name} has been reloaded."
|
|
55
|
+
@state = @database.state
|
|
51
56
|
else
|
|
52
57
|
flash[:error] = "#{@database.name} does not look online."
|
|
53
58
|
end
|
|
54
59
|
end
|
|
60
|
+
|
|
61
|
+
def reload
|
|
62
|
+
execute_admin_command(:reload, "reloaded")
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
def suspend
|
|
56
|
-
|
|
57
|
-
@database.suspend
|
|
58
|
-
flash[:success] = "#{@database.name} has been suspended."
|
|
59
|
-
else
|
|
60
|
-
flash[:error] = "#{@database.name} does not look online."
|
|
61
|
-
end
|
|
66
|
+
execute_admin_command(:suspend, "suspended")
|
|
62
67
|
end
|
|
68
|
+
|
|
69
|
+
def resume
|
|
70
|
+
execute_admin_command(:resume, "resumed")
|
|
71
|
+
end
|
|
72
|
+
|
|
63
73
|
def shutdown
|
|
74
|
+
execute_admin_command(:shutdown, "asked to shut down after its clients disconnect", :wait_for_clients)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def ensure_writable!
|
|
80
|
+
return unless PgBouncerHero.read_only?
|
|
81
|
+
|
|
82
|
+
render plain: "PgBouncerHero is configured as read-only.", status: :forbidden
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def execute_admin_command(command, past_tense, *arguments)
|
|
64
86
|
if @database.connection
|
|
65
|
-
@database.
|
|
66
|
-
flash[:success] = "#{@database.name} has been
|
|
87
|
+
@database.public_send(command, *arguments)
|
|
88
|
+
flash[:success] = "#{@database.name} has been #{past_tense}."
|
|
67
89
|
else
|
|
68
90
|
flash[:error] = "#{@database.name} does not look online."
|
|
69
91
|
end
|
|
92
|
+
rescue PG::Error => e
|
|
93
|
+
flash[:error] = "#{@database.name}: #{e.message.strip}"
|
|
94
|
+
ensure
|
|
95
|
+
redirect_back fallback_location: root_path
|
|
70
96
|
end
|
|
71
97
|
end
|
|
72
98
|
end
|
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
module PgBouncerHero
|
|
2
2
|
module ApplicationHelper
|
|
3
3
|
def is_active(action_name)
|
|
4
|
-
params[:action] == action_name ?
|
|
4
|
+
params[:action] == action_name ? "active" : nil
|
|
5
5
|
end
|
|
6
|
+
|
|
6
7
|
def alert_class_for(flash_type)
|
|
7
8
|
case flash_type
|
|
8
|
-
when
|
|
9
|
-
|
|
10
|
-
when
|
|
11
|
-
|
|
12
|
-
when 'notice'
|
|
13
|
-
'info'
|
|
14
|
-
when 'warning'
|
|
15
|
-
'warning'
|
|
16
|
-
else
|
|
17
|
-
nil
|
|
9
|
+
when "success" then "success"
|
|
10
|
+
when "error" then "error"
|
|
11
|
+
when "notice" then "info"
|
|
12
|
+
when "warning" then "warning"
|
|
18
13
|
end
|
|
19
14
|
end
|
|
15
|
+
|
|
16
|
+
def alert_style_for(class_name)
|
|
17
|
+
case class_name
|
|
18
|
+
when "success" then "bg-green-50 text-green-800 border border-green-200"
|
|
19
|
+
when "error" then "bg-red-50 text-red-800 border border-red-200"
|
|
20
|
+
when "info" then "bg-blue-50 text-blue-800 border border-blue-200"
|
|
21
|
+
when "warning" then "bg-yellow-50 text-yellow-800 border border-yellow-200"
|
|
22
|
+
else "bg-gray-50 text-gray-800 border border-gray-200"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def humanize_ms(millis)
|
|
21
|
-
[[1000, :ms], [60, :s], [60, :min], [24, :h], [1000, :d]].map{ |count, name|
|
|
27
|
+
[ [ 1000, :ms ], [ 60, :s ], [ 60, :min ], [ 24, :h ], [ 1000, :d ] ].map { |count, name|
|
|
22
28
|
if millis > 0
|
|
23
29
|
millis, n = millis.divmod(count)
|
|
24
30
|
"#{n.to_i} #{name}"
|
|
25
31
|
end
|
|
26
|
-
}.compact.reverse.join(
|
|
32
|
+
}.compact.reverse.join(" ")
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = { interval: { type: Number, default: 60000 } }
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.startPolling()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
disconnect() {
|
|
11
|
+
this.stopPolling()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
startPolling() {
|
|
15
|
+
this.timer = setInterval(() => {
|
|
16
|
+
this.element.querySelectorAll("turbo-frame[src]").forEach(frame => {
|
|
17
|
+
frame.reload()
|
|
18
|
+
})
|
|
19
|
+
}, this.intervalValue)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
stopPolling() {
|
|
23
|
+
if (this.timer) {
|
|
24
|
+
clearInterval(this.timer)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
<!DOCTYPE html
|
|
2
|
-
<html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="utf-8"
|
|
5
|
-
<meta content="
|
|
6
|
-
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport"/>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
6
|
<title>
|
|
8
7
|
<%= [@groups.size > 1 ? "#{@database.group.name} | #{@database.name}" : "PgBouncerHero", @title].compact.join(" / ") %>
|
|
9
8
|
</title>
|
|
10
|
-
|
|
11
|
-
<%=
|
|
9
|
+
<link rel="icon" href="data:,">
|
|
10
|
+
<%= stylesheet_link_tag "pgbouncerhero/application" %>
|
|
11
|
+
<%= javascript_importmap_tags("pgbouncerhero/application", importmap: PgBouncerHero.importmap) %>
|
|
12
12
|
<%= csrf_meta_tags %>
|
|
13
13
|
</head>
|
|
14
|
-
<body>
|
|
15
|
-
<
|
|
16
|
-
<div class="
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</div>
|
|
20
|
-
<div class="pusher">
|
|
21
|
-
<div class="ui main container">
|
|
22
|
-
<div class="row" id="flash">
|
|
23
|
-
<%= render partial: 'shared/flash_messages', flash: flash %>
|
|
14
|
+
<body class="bg-gray-100 min-h-screen">
|
|
15
|
+
<nav class="bg-white border-b border-gray-200 fixed top-0 left-0 right-0 z-50 shadow-sm">
|
|
16
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
17
|
+
<div class="flex items-center h-14">
|
|
18
|
+
<%= link_to "PgBouncerHero", root_path, class: "text-lg font-bold text-gray-900 hover:text-gray-700" %>
|
|
24
19
|
</div>
|
|
25
|
-
<%= yield %>
|
|
26
20
|
</div>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
</nav>
|
|
22
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-8">
|
|
23
|
+
<div id="flash">
|
|
24
|
+
<%= render partial: "shared/flash_messages", flash: flash %>
|
|
25
|
+
</div>
|
|
26
|
+
<%= yield %>
|
|
27
|
+
</main>
|
|
31
28
|
</body>
|
|
32
29
|
</html>
|