pgbouncerhero 2.0.0 → 3.0.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +79 -58
  4. data/app/assets/builds/pgbouncerhero/application.css +2 -0
  5. data/app/assets/stylesheets/pgbouncerhero/application.css +1 -0
  6. data/app/controllers/pg_bouncer_hero/application_controller.rb +2 -6
  7. data/app/controllers/pg_bouncer_hero/database_controller.rb +22 -16
  8. data/app/controllers/pg_bouncer_hero/home_controller.rb +0 -2
  9. data/app/helpers/pg_bouncer_hero/application_helper.rb +19 -13
  10. data/app/javascript/pgbouncerhero/application.js +6 -0
  11. data/app/javascript/pgbouncerhero/controllers/polling_controller.js +27 -0
  12. data/app/views/layouts/pg_bouncer_hero/application.html.erb +19 -22
  13. data/app/views/pg_bouncer_hero/database/_actions.html.erb +9 -9
  14. data/app/views/pg_bouncer_hero/database/_clients.html.erb +21 -23
  15. data/app/views/pg_bouncer_hero/database/_conf.html.erb +21 -23
  16. data/app/views/pg_bouncer_hero/database/_databases.html.erb +21 -23
  17. data/app/views/pg_bouncer_hero/database/_menu.html.erb +14 -18
  18. data/app/views/pg_bouncer_hero/database/_pools.html.erb +21 -23
  19. data/app/views/pg_bouncer_hero/database/_stats.html.erb +27 -33
  20. data/app/views/pg_bouncer_hero/database/summary.html.erb +3 -0
  21. data/app/views/pg_bouncer_hero/home/_card.html.erb +9 -22
  22. data/app/views/pg_bouncer_hero/home/_card_content.html.erb +33 -33
  23. data/app/views/pg_bouncer_hero/home/_card_loading_content.html.erb +5 -5
  24. data/app/views/pg_bouncer_hero/home/index.html.erb +13 -37
  25. data/app/views/shared/_alert.html.erb +2 -5
  26. data/config/importmap.rb +5 -0
  27. data/config/routes.rb +2 -2
  28. data/lib/generators/pgbouncerhero/templates/config.yml +8 -8
  29. data/lib/pgbouncerhero/connection.rb +11 -15
  30. data/lib/pgbouncerhero/database.rb +1 -2
  31. data/lib/pgbouncerhero/engine.rb +13 -10
  32. data/lib/pgbouncerhero/group.rb +0 -2
  33. data/lib/pgbouncerhero/methods/basics.rb +2 -2
  34. data/lib/pgbouncerhero/version.rb +1 -1
  35. data/lib/pgbouncerhero.rb +11 -13
  36. metadata +120 -52
  37. data/.gitignore +0 -19
  38. data/Gemfile +0 -4
  39. data/app/assets/images/pgbouncerhero/short-paragraph.png +0 -0
  40. data/app/assets/javascripts/pgbouncerhero/application.js +0 -3
  41. data/app/assets/stylesheets/pgbouncerhero/application.css.scss +0 -15
  42. data/app/views/pg_bouncer_hero/database/summary.js.erb +0 -5
  43. data/doc/screenshot-1.png +0 -0
  44. data/doc/screenshot-2.png +0 -0
  45. data/doc/screenshot-3.png +0 -0
  46. data/pgbouncerhero.gemspec +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0beaacfe8514247e5d6aa9a5a2cee2432f830a265911faaab2998fd406c23aab
4
- data.tar.gz: cf85e4ce7b62d81ab756be4031fc1aa268f9823329fd108f372c0f930c6fb068
3
+ metadata.gz: 269d1da746f222ec69b19046e30b54eda65b082be5be4df7177dc44007c71042
4
+ data.tar.gz: 9653ffded27e1f8580f6de55f2385fedcd0ac3e52ce5fa607a1cab5bcff3b766
5
5
  SHA512:
6
- metadata.gz: '09add1cb73ceb2c23e35f7261eeece28bb87d503087cb7d61d26937f78e0843b7df1205542512b1bef76d8a0244e3ca8f5b5ce0b55259e327ef36f8a7fcc0c2b'
7
- data.tar.gz: 3f7caf907cfc1334b6f319c5d85104a71c41f70780fbac948f1bd790ee4c01bf99a774c56dc90652d223f2ba8c3adbb0241cbf3b95c084ed0d58e660212c81dc
6
+ metadata.gz: 946980cd130e213f922110a77258beda36d4973cd9d3215f840bb62fde4960c0e84e479076c8dc5853b4c16f69c67b5546d3dd61f21e9e009ed0b1f15f422625
7
+ data.tar.gz: 85d1f29c93363ff5dafe39cd0140229b40f623cd5c9433e1c68c4e18a9181e4eaa0bbc0765b35ed03bf981521c6995c0c9ae3d46b1b0737cc595cfa13ab3eb43
data/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## 3.0.0
2
+
3
+ **Breaking Changes:**
4
+ - Requires Ruby >= 3.2 and Rails >= 7.2
5
+ - Removed jQuery, Semantic UI, and Sprockets dependencies
6
+ - Replaced with Hotwire (Turbo + Stimulus), Tailwind CSS 4, Propshaft, and importmap-rails
7
+ - Removed JRuby support
8
+ - Host apps must use Propshaft and importmap-rails
9
+
10
+ **New:**
11
+ - Modern Tailwind CSS 4 UI with responsive design
12
+ - Turbo Frames for lazy-loading database cards (replaces jQuery AJAX polling)
13
+ - Stimulus controller for 60-second auto-refresh
14
+ - RuboCop (rubocop-rails-omakase) for Ruby linting
15
+ - Herb for ERB template linting
16
+ - Minitest test suite with dummy Rails app
17
+ - Appraisal for multi-Rails version testing (7.2, 8.0, 8.1)
18
+ - GitHub Actions CI (Ruby 3.2/3.3/3.4/4.0 x Rails 7.2/8.0/8.1)
19
+ - Gem metadata (source_code_uri, changelog_uri, bug_tracker_uri, rubygems_mfa_required)
20
+
21
+ **Improved:**
22
+ - `rescue Exception` replaced with `rescue StandardError`
23
+ - `YAML.load` replaced with `YAML.safe_load`
24
+ - Removed deprecated `require_dependency`
25
+ - Removed `before_filter` fallback (Rails 3/4 compat)
26
+ - Config caching uses module-level `@config` instead of `Thread.current`
27
+ - Removed `html_safe` on flash messages
28
+ - Updated config template terminology: `master`/`slave` to `primary`/`replica`
29
+
30
+ ## 2.0.0
31
+
32
+ - Easier setup
33
+ - Automatically require jquery
34
+ - Automatically require semantic-ui-sass
35
+ - Use pgbouncerhero/application stylesheets and javascript
36
+
1
37
  ## 1.0.3
2
38
 
3
39
  - Drop Haml dependency
data/README.md CHANGED
@@ -1,21 +1,28 @@
1
1
  # PgBouncerHero
2
2
 
3
- A graphical user interface for your PGBouncers.
3
+ [![Gem Version](https://badge.fury.io/rb/pgbouncerhero.svg)](https://badge.fury.io/rb/pgbouncerhero)
4
+ [![CI](https://github.com/kwent/pgbouncerhero/actions/workflows/test.yml/badge.svg)](https://github.com/kwent/pgbouncerhero/actions/workflows/test.yml)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-ruby.svg)](https://www.ruby-lang.org)
4
6
 
5
- [See it in action](https://pgbouncerhero-demo.herokuapp.com/). [Source Code](https://github.com/kwent/pgbouncerhero-demo).
7
+ A graphical user interface for your PgBouncers.
6
8
 
7
- [![Screenshot1](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-1.png?raw=true)](https://pgbouncerhero-demo.herokuapp.com/)
8
- [![Screenshot2](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-2.png?raw=true)](https://pgbouncerhero-demo.herokuapp.com/)
9
- [![Screenshot2](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-3.png?raw=true)](https://pgbouncerhero-demo.herokuapp.com/)
9
+ [![Screenshot1](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-1.png?raw=true)](https://github.com/kwent/pgbouncerhero)
10
+ [![Screenshot2](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-2.png?raw=true)](https://github.com/kwent/pgbouncerhero)
11
+ [![Screenshot3](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-3.png?raw=true)](https://github.com/kwent/pgbouncerhero)
10
12
 
11
- ## Installation
13
+ ## Requirements
14
+
15
+ - Ruby >= 3.2
16
+ - Rails >= 7.2
17
+ - Propshaft (asset pipeline)
18
+ - importmap-rails
12
19
 
13
- PgBouncerHero is available as a Rails engine.
20
+ ## Installation
14
21
 
15
- Add those dependencies to your applications Gemfile:
22
+ Add to your application's Gemfile:
16
23
 
17
24
  ```ruby
18
- gem 'pgbouncerhero'
25
+ gem "pgbouncerhero"
19
26
  ```
20
27
 
21
28
  And mount the engine in your `config/routes.rb`:
@@ -36,7 +43,7 @@ ENV["PGBOUNCERHERO_PASSWORD"] = "triforce"
36
43
  ### Devise
37
44
 
38
45
  ```ruby
39
- authenticate :user, -> (user) { user.admin? } do
46
+ authenticate :user, ->(user) { user.admin? } do
40
47
  mount PgBouncerHero::Engine, at: "pgbouncerhero"
41
48
  end
42
49
  ```
@@ -49,58 +56,72 @@ export PGBOUNCERHERO_DATABASE_URL=postgres://user:password@host:port/pgbouncer
49
56
 
50
57
  ## Multiple PgBouncers
51
58
 
52
- Create `config/pgbouncerhero.yml` with:
59
+ Generate a config file:
60
+
61
+ ```bash
62
+ rails generate pgbouncerhero:config
63
+ ```
64
+
65
+ Or create `config/pgbouncerhero.yml` manually:
53
66
 
54
67
  ```yml
55
- default: &default
56
- pgbouncers:
57
- production:
58
- master:
59
- url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %>
60
- slave:
61
- url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %>
62
- staging:
63
- master:
64
- url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %>
65
- slave:
66
- url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %>
67
-
68
- development:
69
- <<: *default
70
-
71
- production:
72
- <<: *default
68
+ pgbouncers:
69
+ production:
70
+ primary:
71
+ url: <%= ENV["PGBOUNCER_PRODUCTION_PRIMARY_DATABASE_URL"] %>
72
+ replica:
73
+ url: <%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
74
+ staging:
75
+ primary:
76
+ url: <%= ENV["PGBOUNCER_STAGING_PRIMARY_DATABASE_URL"] %>
77
+ replica:
78
+ url: <%= ENV["PGBOUNCER_STAGING_REPLICA_DATABASE_URL"] %>
73
79
  ```
74
80
 
75
- # Authors
81
+ ## Development
76
82
 
77
- - [Quentin Rousseau](https://github.com/kwent)
83
+ Start PostgreSQL and PgBouncer with Docker:
84
+
85
+ ```bash
86
+ docker compose up -d
87
+ ```
88
+
89
+ Run the dummy Rails app:
90
+
91
+ ```bash
92
+ PGBOUNCERHERO_DATABASE_URL=postgres://pgbouncer:pgbouncer@localhost:6432/pgbouncer \
93
+ bundle exec rackup test/dummy/config.ru -p 3000
94
+ ```
78
95
 
79
- # License
80
-
81
- ```plain
82
- Copyright (c) 2022 Quentin Rousseau <contact@quent.in>
83
-
84
- MIT License
85
-
86
- Permission is hereby granted, free of charge, to any person
87
- obtaining a copy of this software and associated documentation
88
- files (the "Software"), to deal in the Software without
89
- restriction, including without limitation the rights to use,
90
- copy, modify, merge, publish, distribute, sublicense, and/or sell
91
- copies of the Software, and to permit persons to whom the
92
- Software is furnished to do so, subject to the following
93
- conditions:
94
-
95
- The above copyright notice and this permission notice shall be
96
- included in all copies or substantial portions of the Software.
97
-
98
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
99
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
100
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
101
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
102
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
103
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
104
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
105
- OTHER DEALINGS IN THE SOFTWARE.
96
+ Then open http://localhost:3000/pgbouncerhero.
97
+
98
+ Run the test suite:
99
+
100
+ ```bash
101
+ bundle exec rake # tests + rubocop + herb
102
+ bundle exec appraisal rake test # tests across Rails 7.2, 8.0, 8.1
103
+ ```
104
+
105
+ Stop Docker when done:
106
+
107
+ ```bash
108
+ docker compose down
106
109
  ```
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork it
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am "Add some feature"`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create a new Pull Request
118
+
119
+ ## Authors
120
+
121
+ - [Quentin Rousseau](https://github.com/kwent)
122
+
123
+ ## License
124
+
125
+ Copyright (c) 2025 Quentin Rousseau
126
+
127
+ 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,15 +2,11 @@ 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
- if respond_to?(:before_action)
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
 
@@ -1,5 +1,3 @@
1
- require_dependency 'pg_bouncer_hero/application_controller'
2
-
3
1
  module PgBouncerHero
4
2
  class DatabaseController < ApplicationController
5
3
  def summary
@@ -9,6 +7,7 @@ module PgBouncerHero
9
7
  flash[:error] = "#{@database.name} does not look online."
10
8
  end
11
9
  end
10
+
12
11
  def databases
13
12
  if @database.connection
14
13
  @dbs = @database.databases
@@ -16,6 +15,7 @@ module PgBouncerHero
16
15
  flash[:error] = "#{@database.name} does not look online."
17
16
  end
18
17
  end
18
+
19
19
  def stats
20
20
  if @database.connection
21
21
  @stats = @database.stats
@@ -23,6 +23,7 @@ module PgBouncerHero
23
23
  flash[:error] = "#{@database.name} does not look online."
24
24
  end
25
25
  end
26
+
26
27
  def pools
27
28
  if @database.connection
28
29
  @pools = @database.pools
@@ -30,6 +31,7 @@ module PgBouncerHero
30
31
  flash[:error] = "#{@database.name} does not look online."
31
32
  end
32
33
  end
34
+
33
35
  def clients
34
36
  if @database.connection
35
37
  @clients = @database.clients
@@ -37,6 +39,7 @@ module PgBouncerHero
37
39
  flash[:error] = "#{@database.name} does not look online."
38
40
  end
39
41
  end
42
+
40
43
  def conf
41
44
  if @database.connection
42
45
  @conf = @database.conf
@@ -44,29 +47,32 @@ module PgBouncerHero
44
47
  flash[:error] = "#{@database.name} does not look online."
45
48
  end
46
49
  end
50
+
47
51
  def reload
48
- if @database.connection
49
- @database.reload
50
- flash[:success] = "#{@database.name} has been reloaded."
51
- else
52
- flash[:error] = "#{@database.name} does not look online."
53
- end
52
+ execute_admin_command(:reload, "reloaded")
54
53
  end
54
+
55
55
  def suspend
56
- if @database.connection
57
- @database.suspend
58
- flash[:success] = "#{@database.name} has been suspended."
59
- else
60
- flash[:error] = "#{@database.name} does not look online."
61
- end
56
+ execute_admin_command(:suspend, "suspended")
62
57
  end
58
+
63
59
  def shutdown
60
+ execute_admin_command(:shutdown, "shut down")
61
+ end
62
+
63
+ private
64
+
65
+ def execute_admin_command(command, past_tense)
64
66
  if @database.connection
65
- @database.shutdown
66
- flash[:success] = "#{@database.name} has been shutdown."
67
+ @database.public_send(command)
68
+ flash[:success] = "#{@database.name} has been #{past_tense}."
67
69
  else
68
70
  flash[:error] = "#{@database.name} does not look online."
69
71
  end
72
+ rescue PG::Error => e
73
+ flash[:error] = "#{@database.name}: #{e.message.strip}"
74
+ ensure
75
+ redirect_back fallback_location: root_path
70
76
  end
71
77
  end
72
78
  end
@@ -1,5 +1,3 @@
1
- require_dependency 'pg_bouncer_hero/application_controller'
2
-
3
1
  module PgBouncerHero
4
2
  class HomeController < ApplicationController
5
3
  def index
@@ -1,29 +1,35 @@
1
1
  module PgBouncerHero
2
2
  module ApplicationHelper
3
3
  def is_active(action_name)
4
- params[:action] == action_name ? 'active' : nil
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 'success'
9
- 'success'
10
- when 'error'
11
- 'error'
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,6 @@
1
+ import "@hotwired/turbo-rails"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
4
+
5
+ const application = Application.start()
6
+ eagerLoadControllersFrom("pgbouncerhero/controllers", application)
@@ -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 PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
- <html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
3
  <head>
4
- <meta charset="utf-8"/>
5
- <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"/>
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
- <%= stylesheet_link_tag 'pgbouncerhero/application', media: 'all', 'data-turbolinks-track': true %>
11
- <%= javascript_include_tag "pgbouncerhero/application" %>
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
- <div class="ui fixed menu">
16
- <div class="ui container">
17
- <%= link_to'PGBouncerHero', root_path, class: 'header item' %>
18
- </div>
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
- <% if content_for? :inline_script %>
28
- <%= yield :inline_script %>
29
- <% end %>
30
- </div>
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>
@@ -1,10 +1,10 @@
1
- <div class="ui active segment">
2
- <h4 class="ui header">
3
- <div class="content">Actions</div>
4
- </h4>
5
- <button class="ui negative basic button">Pause</button>
6
- <button class="ui positive basic button">Resume</button>
7
- <button class="ui negative basic button">Disable</button>
8
- <button class="ui positive basic button">Enable</button>
9
- <button class="ui negative basic button">Kill</button>
1
+ <div class="bg-white rounded-lg border border-gray-200 shadow-sm p-4">
2
+ <h4 class="text-base font-semibold text-gray-900 mb-3">Actions</h4>
3
+ <div class="flex flex-wrap gap-2">
4
+ <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-red-300 text-red-700 hover:bg-red-50">Pause</button>
5
+ <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-green-300 text-green-700 hover:bg-green-50">Resume</button>
6
+ <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-red-300 text-red-700 hover:bg-red-50">Disable</button>
7
+ <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-green-300 text-green-700 hover:bg-green-50">Enable</button>
8
+ <button class="px-3 py-1.5 text-sm font-medium rounded-md border border-red-300 text-red-700 hover:bg-red-50">Kill</button>
9
+ </div>
10
10
  </div>
@@ -1,27 +1,25 @@
1
- <div class="ui active segment">
2
- <h4 class="ui header">
3
- <div class="content">Clients</div>
4
- </h4>
5
- <table class="ui compact table">
6
- <thead>
7
- <tr>
8
- <% clients.first.keys.each do |key| %>
9
- <th>
10
- <%= key.titleize %>
11
- </th>
12
- <% end %>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% clients.each do |row| %>
1
+ <div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
2
+ <div class="px-4 py-3 border-b border-gray-200">
3
+ <h4 class="text-base font-semibold text-gray-900">Clients</h4>
4
+ </div>
5
+ <div class="overflow-x-auto">
6
+ <table class="min-w-full divide-y divide-gray-200">
7
+ <thead class="bg-gray-50">
17
8
  <tr>
18
- <% row.each do |k,v| %>
19
- <td>
20
- <%= v %>
21
- </td>
9
+ <% clients.first.keys.each do |key| %>
10
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><%= key.titleize %></th>
22
11
  <% end %>
23
12
  </tr>
24
- <% end %>
25
- </tbody>
26
- </table>
13
+ </thead>
14
+ <tbody class="bg-white divide-y divide-gray-200">
15
+ <% clients.each do |row| %>
16
+ <tr class="hover:bg-gray-50">
17
+ <% row.each do |_k, v| %>
18
+ <td class="px-4 py-2 text-sm text-gray-700 whitespace-nowrap"><%= v %></td>
19
+ <% end %>
20
+ </tr>
21
+ <% end %>
22
+ </tbody>
23
+ </table>
24
+ </div>
27
25
  </div>
@@ -1,27 +1,25 @@
1
- <div class="ui active segment">
2
- <h4 class="ui header">
3
- <div class="content">Confs</div>
4
- </h4>
5
- <table class="ui compact table">
6
- <thead>
7
- <tr>
8
- <% conf.first.keys.each do |key| %>
9
- <th>
10
- <%= key.titleize %>
11
- </th>
12
- <% end %>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% conf.each do |row| %>
1
+ <div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
2
+ <div class="px-4 py-3 border-b border-gray-200">
3
+ <h4 class="text-base font-semibold text-gray-900">Configuration</h4>
4
+ </div>
5
+ <div class="overflow-x-auto">
6
+ <table class="min-w-full divide-y divide-gray-200">
7
+ <thead class="bg-gray-50">
17
8
  <tr>
18
- <% row.each do |k,v| %>
19
- <td>
20
- <%= v %>
21
- </td>
9
+ <% conf.first.keys.each do |key| %>
10
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><%= key.titleize %></th>
22
11
  <% end %>
23
12
  </tr>
24
- <% end %>
25
- </tbody>
26
- </table>
13
+ </thead>
14
+ <tbody class="bg-white divide-y divide-gray-200">
15
+ <% conf.each do |row| %>
16
+ <tr class="hover:bg-gray-50">
17
+ <% row.each do |_k, v| %>
18
+ <td class="px-4 py-2 text-sm text-gray-700 whitespace-nowrap"><%= v %></td>
19
+ <% end %>
20
+ </tr>
21
+ <% end %>
22
+ </tbody>
23
+ </table>
24
+ </div>
27
25
  </div>