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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +79 -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 +2 -6
- data/app/controllers/pg_bouncer_hero/database_controller.rb +22 -16
- 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 +14 -18
- data/app/views/pg_bouncer_hero/database/_pools.html.erb +21 -23
- data/app/views/pg_bouncer_hero/database/_stats.html.erb +27 -33
- 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 +2 -2
- data/lib/generators/pgbouncerhero/templates/config.yml +8 -8
- data/lib/pgbouncerhero/connection.rb +11 -15
- data/lib/pgbouncerhero/database.rb +1 -2
- data/lib/pgbouncerhero/engine.rb +13 -10
- data/lib/pgbouncerhero/group.rb +0 -2
- data/lib/pgbouncerhero/methods/basics.rb +2 -2
- data/lib/pgbouncerhero/version.rb +1 -1
- data/lib/pgbouncerhero.rb +11 -13
- metadata +120 -52
- 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: 269d1da746f222ec69b19046e30b54eda65b082be5be4df7177dc44007c71042
|
|
4
|
+
data.tar.gz: 9653ffded27e1f8580f6de55f2385fedcd0ac3e52ce5fa607a1cab5bcff3b766
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
+
- Propshaft (asset pipeline)
|
|
18
|
+
- importmap-rails
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
## Installation
|
|
14
21
|
|
|
15
|
-
Add
|
|
22
|
+
Add to your application's Gemfile:
|
|
16
23
|
|
|
17
24
|
```ruby
|
|
18
|
-
gem
|
|
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, ->
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
81
|
+
## Development
|
|
76
82
|
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
66
|
-
flash[:success] = "#{@database.name} has been
|
|
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,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>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
<h4 class="
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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="
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
</
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
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
|
-
<%
|
|
19
|
-
<
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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="
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
</
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
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
|
-
<%
|
|
19
|
-
<
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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>
|