pgbouncerhero 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +24 -0
- data/README.md +125 -0
- data/app/assets/images/pgbouncerhero/short-paragraph.png +0 -0
- data/app/assets/javascripts/pgbouncerhero/application.js +2 -0
- data/app/assets/stylesheets/pgbouncerhero/application.css.scss +15 -0
- data/app/controllers/pg_bouncer_hero/application_controller.rb +27 -0
- data/app/controllers/pg_bouncer_hero/database_controller.rb +70 -0
- data/app/controllers/pg_bouncer_hero/home_controller.rb +6 -0
- data/app/helpers/pg_bouncer_hero/application_helper.rb +29 -0
- data/app/views/layouts/pg_bouncer_hero/application.html.haml +20 -0
- data/app/views/pg_bouncer_hero/database/_actions.html.haml +10 -0
- data/app/views/pg_bouncer_hero/database/_clients.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_conf.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_databases.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_menu.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_pools.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_stats.html.haml +20 -0
- data/app/views/pg_bouncer_hero/database/clients.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/conf.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/databases.html.haml +5 -0
- data/app/views/pg_bouncer_hero/database/pools.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/stats.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/summary.js.haml +3 -0
- data/app/views/pg_bouncer_hero/home/_card.html.haml +15 -0
- data/app/views/pg_bouncer_hero/home/_card_content.html.haml +26 -0
- data/app/views/pg_bouncer_hero/home/_card_loading_content.html.haml +5 -0
- data/app/views/pg_bouncer_hero/home/index.html.haml +31 -0
- data/app/views/shared/_alert.html.haml +4 -0
- data/app/views/shared/_flash_messages.html.haml +2 -0
- data/config/routes.rb +16 -0
- data/doc/screenshot-1.png +0 -0
- data/doc/screenshot-2.png +0 -0
- data/doc/screenshot-3.png +0 -0
- data/lib/generators/pgbouncerhero/config_generator.rb +13 -0
- data/lib/generators/pgbouncerhero/templates/config.yml +16 -0
- data/lib/pgbouncerhero.rb +65 -0
- data/lib/pgbouncerhero/connection.rb +29 -0
- data/lib/pgbouncerhero/database.rb +55 -0
- data/lib/pgbouncerhero/engine.rb +20 -0
- data/lib/pgbouncerhero/group.rb +21 -0
- data/lib/pgbouncerhero/methods/basics.rb +41 -0
- data/lib/pgbouncerhero/version.rb +3 -0
- data/pgbouncerhero.gemspec +32 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 054de6f1d8d3846419f355c4512129e06385c8af
|
4
|
+
data.tar.gz: 1254bddb23ead79c688f283b07e179b41a328fd9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf35ed3c426b20262a6be30a555eba7547c8cd1c89db7fe200a76babe5f4842298df9b0b7eb4ce0a9b3efb555d46c392c67627f4aed826417997ff4830fe9a71
|
7
|
+
data.tar.gz: fb73c61c6dda241124a59db1820083cfcf24d4cf25ae16f3c524feb7f05485d1054ec74bed889a2faad6847773603aec8d565003f4d81f8468e92de8c9374204
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2017 Quentin Rousseau <contact@quent.in>
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person
|
6
|
+
obtaining a copy of this software and associated documentation
|
7
|
+
files (the "Software"), to deal in the Software without
|
8
|
+
restriction, including without limitation the rights to use,
|
9
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the
|
11
|
+
Software is furnished to do so, subject to the following
|
12
|
+
conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
19
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
21
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
22
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
23
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# PgBouncerHero
|
2
|
+
|
3
|
+
A graphical user interface for your PGBouncers.
|
4
|
+
|
5
|
+
[See it in action](https://pgbouncerhero-demo.herokuapp.com/). [Source Code](https://github.com/kwent/pgbouncerhero-demo).
|
6
|
+
|
7
|
+
[](https://pgbouncerhero-demo.herokuapp.com/)
|
8
|
+
[](https://pgbouncerhero-demo.herokuapp.com/)
|
9
|
+
[](https://pgbouncerhero-demo.herokuapp.com/)
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
PgBouncerHero is available as a Rails engine.
|
14
|
+
|
15
|
+
Add those dependencies to your application’s Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'pgbouncerhero'
|
19
|
+
gem 'jquery-rails'
|
20
|
+
gem 'sass-rails'
|
21
|
+
gem 'semantic-ui-sass'
|
22
|
+
gem 'haml-rails'
|
23
|
+
gem 'pg'
|
24
|
+
```
|
25
|
+
|
26
|
+
And mount the engine in your `config/routes.rb`:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
mount PgBouncerHero::Engine, at: "pgbouncerhero"
|
30
|
+
```
|
31
|
+
|
32
|
+
Add the following line in your `application.css`:
|
33
|
+
|
34
|
+
```
|
35
|
+
*= require pgbouncerhero/application
|
36
|
+
```
|
37
|
+
|
38
|
+
Add the following lines in your `application.js`:
|
39
|
+
|
40
|
+
```
|
41
|
+
//= require jquery
|
42
|
+
//= require jquery_ujs
|
43
|
+
//= require pgbouncerhero/application
|
44
|
+
```
|
45
|
+
|
46
|
+
### Basic Authentication
|
47
|
+
|
48
|
+
Set the following variables in your environment or an initializer.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
ENV["PGBOUNCERHERO_USERNAME"] = "zelda"
|
52
|
+
ENV["PGBOUNCERHERO_PASSWORD"] = "triforce"
|
53
|
+
```
|
54
|
+
|
55
|
+
### Devise
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
authenticate :user, -> (user) { user.admin? } do
|
59
|
+
mount PgBouncerHero::Engine, at: "pgbouncerhero"
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## One PgBouncer
|
64
|
+
|
65
|
+
```bash
|
66
|
+
export PGBOUNCERHERO_DATABASE_URL=postgres://user:password@host:port/pgbouncer
|
67
|
+
```
|
68
|
+
|
69
|
+
## Multiple PgBouncers
|
70
|
+
|
71
|
+
Create `config/pgbouncerhero.yml` with:
|
72
|
+
|
73
|
+
```yml
|
74
|
+
default: &default
|
75
|
+
pgbouncers:
|
76
|
+
production:
|
77
|
+
master:
|
78
|
+
url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %>
|
79
|
+
slave:
|
80
|
+
url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %>
|
81
|
+
staging:
|
82
|
+
master:
|
83
|
+
url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %>
|
84
|
+
slave:
|
85
|
+
url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %>
|
86
|
+
|
87
|
+
development:
|
88
|
+
<<: *default
|
89
|
+
|
90
|
+
production:
|
91
|
+
<<: *default
|
92
|
+
```
|
93
|
+
|
94
|
+
# Authors
|
95
|
+
|
96
|
+
- [Quentin Rousseau](https://github.com/kwent)
|
97
|
+
|
98
|
+
# License
|
99
|
+
|
100
|
+
```plain
|
101
|
+
Copyright (c) 2017 Quentin Rousseau <contact@quent.in>
|
102
|
+
|
103
|
+
MIT License
|
104
|
+
|
105
|
+
Permission is hereby granted, free of charge, to any person
|
106
|
+
obtaining a copy of this software and associated documentation
|
107
|
+
files (the "Software"), to deal in the Software without
|
108
|
+
restriction, including without limitation the rights to use,
|
109
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
110
|
+
copies of the Software, and to permit persons to whom the
|
111
|
+
Software is furnished to do so, subject to the following
|
112
|
+
conditions:
|
113
|
+
|
114
|
+
The above copyright notice and this permission notice shall be
|
115
|
+
included in all copies or substantial portions of the Software.
|
116
|
+
|
117
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
118
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
119
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
120
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
121
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
122
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
123
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
124
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
125
|
+
```
|
Binary file
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
layout "pg_bouncer_hero/application"
|
4
|
+
|
5
|
+
protect_from_forgery
|
6
|
+
|
7
|
+
http_basic_authenticate_with name: ENV["PGBOUNCERHERO_USERNAME"], password: ENV["PGBOUNCERHERO_PASSWORD"] if ENV["PGBOUNCERHERO_PASSWORD"]
|
8
|
+
|
9
|
+
if respond_to?(:before_action)
|
10
|
+
before_action :set_database
|
11
|
+
else
|
12
|
+
before_filter :set_database
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def set_database
|
18
|
+
@groups = PgBouncerHero.groups
|
19
|
+
if params[:group] && params[:database]
|
20
|
+
@database = PgBouncerHero.groups[params[:group]].databases.find { |db| db.id.to_s == params[:database].to_s }
|
21
|
+
else
|
22
|
+
@group = @groups.first
|
23
|
+
@database = @groups.first.last.databases.first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class DatabaseController < ApplicationController
|
3
|
+
def summary
|
4
|
+
if @database.connection
|
5
|
+
@dbs = @database.summary
|
6
|
+
else
|
7
|
+
flash[:error] = "#{@database.name} does not look online."
|
8
|
+
end
|
9
|
+
end
|
10
|
+
def databases
|
11
|
+
if @database.connection
|
12
|
+
@dbs = @database.databases
|
13
|
+
else
|
14
|
+
flash[:error] = "#{@database.name} does not look online."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
def stats
|
18
|
+
if @database.connection
|
19
|
+
@stats = @database.stats
|
20
|
+
else
|
21
|
+
flash[:error] = "#{@database.name} does not look online."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
def pools
|
25
|
+
if @database.connection
|
26
|
+
@pools = @database.pools
|
27
|
+
else
|
28
|
+
flash[:error] = "#{@database.name} does not look online."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def clients
|
32
|
+
if @database.connection
|
33
|
+
@clients = @database.clients
|
34
|
+
else
|
35
|
+
flash[:error] = "#{@database.name} does not look online."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
def conf
|
39
|
+
if @database.connection
|
40
|
+
@conf = @database.conf
|
41
|
+
else
|
42
|
+
flash[:error] = "#{@database.name} does not look online."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
def reload
|
46
|
+
if @database.connection
|
47
|
+
@database.reload
|
48
|
+
flash[:success] = "#{@database.name} has been reloaded."
|
49
|
+
else
|
50
|
+
flash[:error] = "#{@database.name} does not look online."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
def suspend
|
54
|
+
if @database.connection
|
55
|
+
@database.suspend
|
56
|
+
flash[:success] = "#{@database.name} has been suspended."
|
57
|
+
else
|
58
|
+
flash[:error] = "#{@database.name} does not look online."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
def shutdown
|
62
|
+
if @database.connection
|
63
|
+
@database.shutdown
|
64
|
+
flash[:success] = "#{@database.name} has been shutdown."
|
65
|
+
else
|
66
|
+
flash[:error] = "#{@database.name} does not look online."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
module ApplicationHelper
|
3
|
+
def is_active(action_name)
|
4
|
+
params[:action] == action_name ? 'active' : nil
|
5
|
+
end
|
6
|
+
def alert_class_for(flash_type)
|
7
|
+
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
|
18
|
+
end
|
19
|
+
end
|
20
|
+
def humanize_ms(millis)
|
21
|
+
[[1000, :ms], [60, :s], [60, :min], [24, :h], [1000, :d]].map{ |count, name|
|
22
|
+
if millis > 0
|
23
|
+
millis, n = millis.divmod(count)
|
24
|
+
"#{n.to_i} #{name}"
|
25
|
+
end
|
26
|
+
}.compact.reverse.join(' ')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
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"}
|
7
|
+
%title= [@groups.size > 1 ? "#{@database.group.name} | #{@database.name}" : "PgBouncerHero", @title].compact.join(" / ")
|
8
|
+
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': true
|
9
|
+
= csrf_meta_tags
|
10
|
+
%body
|
11
|
+
.ui.fixed.menu
|
12
|
+
.ui.container
|
13
|
+
= link_to'PGBouncerHero', root_path, class: 'header item'
|
14
|
+
.pusher
|
15
|
+
.ui.main.container
|
16
|
+
#flash.row= render partial: 'shared/flash_messages', flash: flash
|
17
|
+
= yield
|
18
|
+
= javascript_include_tag 'application'
|
19
|
+
- if content_for? :inline_script
|
20
|
+
= yield :inline_script
|
@@ -0,0 +1,10 @@
|
|
1
|
+
.ui.active.segment
|
2
|
+
|
3
|
+
%h4.ui.header
|
4
|
+
.content Actions
|
5
|
+
|
6
|
+
%button.ui.negative.basic.button Pause
|
7
|
+
%button.ui.positive.basic.button Resume
|
8
|
+
%button.ui.negative.basic.button Disable
|
9
|
+
%button.ui.positive.basic.button Enable
|
10
|
+
%button.ui.negative.basic.button Kill
|
@@ -0,0 +1,15 @@
|
|
1
|
+
.ui.pointing.menu
|
2
|
+
.header.item
|
3
|
+
= "#{database.group.name} > #{database.name}"
|
4
|
+
= link_to "Databases", databases_path(database: database.name.parameterize), class: "item #{is_active('databases')}"
|
5
|
+
= link_to "Stats", stats_path(database: database.name.parameterize), class: "item #{is_active('stats')}"
|
6
|
+
= link_to "Pools", pools_path(database: database.name.parameterize), class: "item #{is_active('pools')}"
|
7
|
+
= link_to "Clients", clients_path(database: database.name.parameterize), class: "item #{is_active('clients')}"
|
8
|
+
= link_to "Configuration", conf_path(database: database.name.parameterize), class: "item #{is_active('conf')}"
|
9
|
+
.right.menu
|
10
|
+
.item
|
11
|
+
= link_to 'Reload', reload_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic positive button', data: { confirm: 'Are you sure?' }
|
12
|
+
.item
|
13
|
+
= link_to 'Suspend', suspend_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic primary button', data: { confirm: 'Are you sure?' }
|
14
|
+
.item
|
15
|
+
= link_to 'Shutdown', shutdown_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic negative button', data: { confirm: 'Are you sure?' }
|
@@ -0,0 +1,20 @@
|
|
1
|
+
.ui.active.segment
|
2
|
+
|
3
|
+
%h4.ui.header
|
4
|
+
.content Stats
|
5
|
+
|
6
|
+
%table.ui.compact.table
|
7
|
+
%thead
|
8
|
+
%tr
|
9
|
+
- stats.first.keys.each do |key|
|
10
|
+
%th= key.titleize
|
11
|
+
%tbody
|
12
|
+
- stats.each do |row|
|
13
|
+
%tr
|
14
|
+
- row.each do |k,v|
|
15
|
+
- if ['total_received', 'total_sent', 'avg_recv', 'avg_sent'].include? k
|
16
|
+
%td= number_to_human_size(v)
|
17
|
+
- elsif ['total_query_time', 'avg_query'].include? k
|
18
|
+
%td= humanize_ms(v.to_i)
|
19
|
+
- else
|
20
|
+
%td= v
|
@@ -0,0 +1,3 @@
|
|
1
|
+
$("#segment_content_#{@database.group.name.parameterize}_#{@database.name.parameterize}").replaceWith("#{j(render partial: 'pg_bouncer_hero/home/card_content', locals: {database: @database, id: "segment_content_#{@database.group.name.parameterize}_#{@database.name.parameterize}"})}")
|
2
|
+
$("#segment_refreshed_#{@database.group.name.parameterize}_#{@database.name.parameterize}").css('display', '')
|
3
|
+
$("#segment_content_#{@database.group.name.parameterize}_#{@database.name.parameterize} .ui.progress").each(function(idx, el) { $(el).progress({ text: {active: '{value} of {total} connections'}, autoSuccess: false }); });
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- connected = database.connection
|
2
|
+
.eight.wide.column{id: "segment_href_#{database.group.name.parameterize}_#{database.name.parameterize}", data: {href: summary_path(group: database.group.name.parameterize, database: database.name.parameterize)}}
|
3
|
+
= link_to databases_path(group: database.group.name.parameterize, database: database.name.parameterize), style: 'text-decoration: none !important; color: inherit !important' do
|
4
|
+
.ui.segments{style: 'margin-bottom: 10px;'}
|
5
|
+
.ui.segment{style: 'height: 60px;'}
|
6
|
+
%div{style: 'float: left;'}
|
7
|
+
%div{style: 'font-size: 16px; font-weight: bold;'}= database.name
|
8
|
+
%div{style: 'font-size: 12px; color: grey'}= database.host
|
9
|
+
%button.ui.button.basic.button{style: 'float: right;', class: connected ? 'positive': 'negative'}= connected ? 'Online' : 'Offline'
|
10
|
+
- if connected
|
11
|
+
= render partial: partial, locals: {database: database, id: "segment_content_#{database.group.name.parameterize}_#{database.name.parameterize}"}
|
12
|
+
.ui.segment{id: "segment_refreshed_#{database.group.name.parameterize}_#{database.name.parameterize}", style: 'height: 35px; display: none;'}
|
13
|
+
%span.right.floated{id: "segment_refreshed_span_#{database.group.name.parameterize}_#{database.name.parameterize}", style: 'float:right; margin-top: -6px; color: grey'}
|
14
|
+
Last refresh:
|
15
|
+
= Time.now.strftime("%H:%M:%S GMT%z (%Z)") # Same as new Date().toTimeString() in JS
|
@@ -0,0 +1,26 @@
|
|
1
|
+
.ui.horizontal.segments{id: id, style: 'background-color: white !important;'}
|
2
|
+
.ui.segment
|
3
|
+
- summary = database.summary
|
4
|
+
%div
|
5
|
+
%i.users.icon
|
6
|
+
- number = summary.select { |row| row['list'] == 'users' }.first['items']
|
7
|
+
= pluralize(number, 'user')
|
8
|
+
%div
|
9
|
+
%i.database.icon
|
10
|
+
- number = summary.select { |row| row['list'] == 'databases' }.first['items'].to_i - 1 # Removed pgbouncer
|
11
|
+
= pluralize(number, 'database')
|
12
|
+
%div
|
13
|
+
%i.plug.icon
|
14
|
+
- number = summary.select { |row| row['list'] == 'pools' }.first['items'].to_i - 1 # Removed pgbouncer
|
15
|
+
= pluralize(number, 'pool')
|
16
|
+
.ui.segment
|
17
|
+
%ul{style: 'margin: 0; padding-left: 5px;'}
|
18
|
+
- summary.select{ |row| row.key?(:databases_details) }.first[:databases_details].each do |db|
|
19
|
+
%li{style: 'margin: 0; list-style-type: none;'}
|
20
|
+
%span
|
21
|
+
%i.database.icon
|
22
|
+
%strong=db['name']
|
23
|
+
.ui.progress.small.success{data:{value: db['current_connections'], total: db['max_connections']}, style: 'float:right; width: 150px; margin-top:4px;'}
|
24
|
+
.bar
|
25
|
+
.progress
|
26
|
+
.label Connections
|
@@ -0,0 +1,31 @@
|
|
1
|
+
.ui.pointing.menu
|
2
|
+
.header.item
|
3
|
+
Overview
|
4
|
+
|
5
|
+
.ui.grid
|
6
|
+
- @groups.each do |_, group|
|
7
|
+
.sixteen.wide.row
|
8
|
+
.sixteen.wide.column
|
9
|
+
%h3.ui.dividing.header{style: 'margin-bottom: 10px;'}= group.name
|
10
|
+
= render partial: "card", collection: group.databases, as: :database, locals: {partial: 'card_loading_content'}
|
11
|
+
|
12
|
+
:javascript
|
13
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
14
|
+
// First
|
15
|
+
$("[id^='segment_href']").each(function(idx, el) {
|
16
|
+
$.get($(el).data('href'));
|
17
|
+
});
|
18
|
+
// Refresh every minutes
|
19
|
+
setInterval(function (){
|
20
|
+
$("[id^='segment_href']").each(function(idx, el) {
|
21
|
+
var el = $(el)
|
22
|
+
var id = el.attr('id');
|
23
|
+
var href = el.data('href');
|
24
|
+
var refreshed_id = id.replace('href', 'refreshed_span');
|
25
|
+
document.getElementById(refreshed_id).innerHTML = 'Refreshing...';
|
26
|
+
$.get(href, function(data) {
|
27
|
+
document.getElementById(refreshed_id).innerHTML = 'Last refresh: ' + new Date().toTimeString();
|
28
|
+
});
|
29
|
+
});
|
30
|
+
}, 60 * 1000)
|
31
|
+
});
|
data/config/routes.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
PgBouncerHero::Engine.routes.draw do
|
2
|
+
root to: "home#index"
|
3
|
+
scope path: ":group", constraints: proc { |req| (PgBouncerHero.groups.keys.map(&:parameterize) + [nil]).include?(req.params[:group]) } do
|
4
|
+
scope path: ":database", constraints: proc { |req| (PgBouncerHero.groups[req.params[:group]].databases.map(&:name).map(&:parameterize) + [nil]).include?(req.params[:database]) } do
|
5
|
+
get :summary, controller: :database
|
6
|
+
get :databases, controller: :database
|
7
|
+
get :stats, controller: :database
|
8
|
+
get :pools, controller: :database
|
9
|
+
get :clients, controller: :database
|
10
|
+
get :conf, controller: :database
|
11
|
+
post :reload, controller: :database
|
12
|
+
post :suspend, controller: :database
|
13
|
+
post :shutdown, controller: :database
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module PgBouncerHero
|
4
|
+
module Generators
|
5
|
+
class ConfigGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_initializer
|
9
|
+
template "config.yml", "config/pgbouncerhero.yml"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
pgbouncers:
|
2
|
+
production:
|
3
|
+
master:
|
4
|
+
# eg. postgres://user:password@host:port/pgbouncer
|
5
|
+
# url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %>
|
6
|
+
# Add more databases
|
7
|
+
# slave:
|
8
|
+
# url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %>
|
9
|
+
# staging:
|
10
|
+
# master:
|
11
|
+
# url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %>
|
12
|
+
# slave:
|
13
|
+
# url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %>
|
14
|
+
|
15
|
+
# Time zone (defaults to app time zone)
|
16
|
+
# time_zone: "Pacific Time (US & Canada)"
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "pgbouncerhero/version"
|
2
|
+
require "active_record"
|
3
|
+
|
4
|
+
# methods
|
5
|
+
require "pgbouncerhero/methods/basics"
|
6
|
+
|
7
|
+
require "pgbouncerhero/group"
|
8
|
+
require "pgbouncerhero/engine" if defined?(Rails)
|
9
|
+
|
10
|
+
# models
|
11
|
+
require "pgbouncerhero/connection"
|
12
|
+
|
13
|
+
module PgBouncerHero
|
14
|
+
# settings
|
15
|
+
class << self
|
16
|
+
attr_accessor :env
|
17
|
+
end
|
18
|
+
self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
19
|
+
|
20
|
+
class << self
|
21
|
+
extend Forwardable
|
22
|
+
|
23
|
+
def time_zone=(time_zone)
|
24
|
+
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
def time_zone
|
28
|
+
@time_zone || Time.zone
|
29
|
+
end
|
30
|
+
|
31
|
+
def config
|
32
|
+
Thread.current[:PgBouncerHero_config] ||= begin
|
33
|
+
path = "config/pgbouncerhero.yml"
|
34
|
+
|
35
|
+
config = YAML.load(ERB.new(File.read(path)).result) if File.exist?(path)
|
36
|
+
config ||= {}
|
37
|
+
|
38
|
+
if config[env]
|
39
|
+
config[env]
|
40
|
+
elsif config["pgbouncers"] # preferred format
|
41
|
+
config
|
42
|
+
else
|
43
|
+
{
|
44
|
+
"pgbouncers" => {
|
45
|
+
"default" => {
|
46
|
+
"primary" => {
|
47
|
+
"url" => ENV["PGBOUNCERHERO_DATABASE_URL"]
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def groups
|
57
|
+
@groups ||= begin
|
58
|
+
mapped = config['pgbouncers'].map do |group_id, hash|
|
59
|
+
[group_id.parameterize, PgBouncerHero::Group.new(group_id, config['pgbouncers'])]
|
60
|
+
end
|
61
|
+
Hash[mapped]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
def initialize(host, port, user, password, dbname)
|
5
|
+
@host = host
|
6
|
+
@port = port
|
7
|
+
@user = user
|
8
|
+
@password = password
|
9
|
+
@dbname = dbname
|
10
|
+
end
|
11
|
+
|
12
|
+
def connection
|
13
|
+
begin
|
14
|
+
PG.connect(
|
15
|
+
host: @host,
|
16
|
+
port: @port,
|
17
|
+
user: @user,
|
18
|
+
password: @password,
|
19
|
+
dbname: @dbname,
|
20
|
+
connect_timeout: 5
|
21
|
+
)
|
22
|
+
rescue => e
|
23
|
+
Rails.logger.info(e)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class Database
|
3
|
+
|
4
|
+
include Methods::Basics
|
5
|
+
|
6
|
+
attr_reader :id, :config, :group
|
7
|
+
|
8
|
+
def initialize(group, id, config)
|
9
|
+
@id = id
|
10
|
+
@config = config || {}
|
11
|
+
@url = URI.parse(config["url"])
|
12
|
+
@group = group
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
@name ||= id.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def connection
|
20
|
+
connection_model.new(host, port, user, password, dbname).connection
|
21
|
+
end
|
22
|
+
|
23
|
+
def host
|
24
|
+
@url.host if @url
|
25
|
+
end
|
26
|
+
|
27
|
+
def port
|
28
|
+
@url.port if @url
|
29
|
+
end
|
30
|
+
|
31
|
+
def user
|
32
|
+
@url.user if @url
|
33
|
+
end
|
34
|
+
|
35
|
+
def password
|
36
|
+
@url.password if @url
|
37
|
+
end
|
38
|
+
|
39
|
+
def dbname
|
40
|
+
@url.path[1..-1] if @url
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def connection_model
|
46
|
+
@connection_model ||= begin
|
47
|
+
Class.new(PgBouncerHero::Connection) do
|
48
|
+
def self.name
|
49
|
+
"PgBouncerHero::Connection::Database#{object_id}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace PgBouncerHero
|
4
|
+
|
5
|
+
initializer "pgbouncerhero", group: :all do |app|
|
6
|
+
if defined?(Sprockets) && Sprockets::VERSION >= "4"
|
7
|
+
app.config.assets.precompile << "pgbouncerhero/application.js"
|
8
|
+
app.config.assets.precompile << "pgbouncerhero/application.css"
|
9
|
+
app.config.assets.precompile << "pgbouncerhero/short-paragraph.png"
|
10
|
+
else
|
11
|
+
# use a proc instead of a string
|
12
|
+
app.config.assets.precompile << proc { |path| path == "pgbouncerhero/application.js" }
|
13
|
+
app.config.assets.precompile << proc { |path| path == "pgbouncerhero/application.css" }
|
14
|
+
app.config.assets.precompile << proc { |path| path == "pgbouncerhero/short-paragraph.png" }
|
15
|
+
end
|
16
|
+
|
17
|
+
PgBouncerHero.time_zone = PgBouncerHero.config["time_zone"] if PgBouncerHero.config["time_zone"]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "pgbouncerhero/database"
|
2
|
+
|
3
|
+
module PgBouncerHero
|
4
|
+
class Group
|
5
|
+
|
6
|
+
attr_reader :id, :config, :databases
|
7
|
+
|
8
|
+
def initialize(id, config)
|
9
|
+
@id = id
|
10
|
+
@config = config || {}
|
11
|
+
@databases = config[id].map do |k, v|
|
12
|
+
PgBouncerHero::Database.new(self, k, config[id][k])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
@name ||= id.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
module Methods
|
3
|
+
module Basics
|
4
|
+
def summary
|
5
|
+
l = lists
|
6
|
+
d = databases
|
7
|
+
l = l.as_json
|
8
|
+
d = d.as_json.reject { |a| a['name'] == 'pgbouncer' }
|
9
|
+
l.push({databases_details: d})
|
10
|
+
l
|
11
|
+
end
|
12
|
+
def databases
|
13
|
+
connection.exec("SHOW databases")
|
14
|
+
end
|
15
|
+
def stats
|
16
|
+
connection.exec("SHOW stats")
|
17
|
+
end
|
18
|
+
def lists
|
19
|
+
connection.exec("SHOW lists")
|
20
|
+
end
|
21
|
+
def pools
|
22
|
+
connection.exec("SHOW pools")
|
23
|
+
end
|
24
|
+
def clients
|
25
|
+
connection.exec("SHOW clients")
|
26
|
+
end
|
27
|
+
def conf
|
28
|
+
connection.exec("SHOW config")
|
29
|
+
end
|
30
|
+
def reload
|
31
|
+
connection.exec("RELOAD")
|
32
|
+
end
|
33
|
+
def suspend
|
34
|
+
connection.exec("SUSPEND")
|
35
|
+
end
|
36
|
+
def shutdown
|
37
|
+
connection.exec("SHUTDOWN")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "pgbouncerhero/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pgbouncerhero"
|
8
|
+
spec.version = PgBouncerHero::VERSION
|
9
|
+
spec.authors = ["Quentin Rousseau"]
|
10
|
+
spec.email = ["contact@quent.in"]
|
11
|
+
spec.summary = "A graphical user interface for your PGBouncers"
|
12
|
+
spec.description = "A graphical user interface for your PGBouncers"
|
13
|
+
spec.homepage = "https://github.com/kwent/pgbouncerhero"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "minitest"
|
23
|
+
spec.add_runtime_dependency "sass-rails"
|
24
|
+
spec.add_runtime_dependency "semantic-ui-sass"
|
25
|
+
spec.add_runtime_dependency "haml-rails"
|
26
|
+
|
27
|
+
if RUBY_PLATFORM == "java"
|
28
|
+
spec.add_runtime_dependency "pg_jruby"
|
29
|
+
else
|
30
|
+
spec.add_runtime_dependency "pg"
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pgbouncerhero
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Quentin Rousseau
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-02-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sass-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: semantic-ui-sass
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: haml-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: A graphical user interface for your PGBouncers
|
112
|
+
email:
|
113
|
+
- contact@quent.in
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- CHANGELOG.md
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- app/assets/images/pgbouncerhero/short-paragraph.png
|
124
|
+
- app/assets/javascripts/pgbouncerhero/application.js
|
125
|
+
- app/assets/stylesheets/pgbouncerhero/application.css.scss
|
126
|
+
- app/controllers/pg_bouncer_hero/application_controller.rb
|
127
|
+
- app/controllers/pg_bouncer_hero/database_controller.rb
|
128
|
+
- app/controllers/pg_bouncer_hero/home_controller.rb
|
129
|
+
- app/helpers/pg_bouncer_hero/application_helper.rb
|
130
|
+
- app/views/layouts/pg_bouncer_hero/application.html.haml
|
131
|
+
- app/views/pg_bouncer_hero/database/_actions.html.haml
|
132
|
+
- app/views/pg_bouncer_hero/database/_clients.html.haml
|
133
|
+
- app/views/pg_bouncer_hero/database/_conf.html.haml
|
134
|
+
- app/views/pg_bouncer_hero/database/_databases.html.haml
|
135
|
+
- app/views/pg_bouncer_hero/database/_menu.html.haml
|
136
|
+
- app/views/pg_bouncer_hero/database/_pools.html.haml
|
137
|
+
- app/views/pg_bouncer_hero/database/_stats.html.haml
|
138
|
+
- app/views/pg_bouncer_hero/database/clients.html.haml
|
139
|
+
- app/views/pg_bouncer_hero/database/conf.html.haml
|
140
|
+
- app/views/pg_bouncer_hero/database/databases.html.haml
|
141
|
+
- app/views/pg_bouncer_hero/database/pools.html.haml
|
142
|
+
- app/views/pg_bouncer_hero/database/stats.html.haml
|
143
|
+
- app/views/pg_bouncer_hero/database/summary.js.haml
|
144
|
+
- app/views/pg_bouncer_hero/home/_card.html.haml
|
145
|
+
- app/views/pg_bouncer_hero/home/_card_content.html.haml
|
146
|
+
- app/views/pg_bouncer_hero/home/_card_loading_content.html.haml
|
147
|
+
- app/views/pg_bouncer_hero/home/index.html.haml
|
148
|
+
- app/views/shared/_alert.html.haml
|
149
|
+
- app/views/shared/_flash_messages.html.haml
|
150
|
+
- config/routes.rb
|
151
|
+
- doc/screenshot-1.png
|
152
|
+
- doc/screenshot-2.png
|
153
|
+
- doc/screenshot-3.png
|
154
|
+
- lib/generators/pgbouncerhero/config_generator.rb
|
155
|
+
- lib/generators/pgbouncerhero/templates/config.yml
|
156
|
+
- lib/pgbouncerhero.rb
|
157
|
+
- lib/pgbouncerhero/connection.rb
|
158
|
+
- lib/pgbouncerhero/database.rb
|
159
|
+
- lib/pgbouncerhero/engine.rb
|
160
|
+
- lib/pgbouncerhero/group.rb
|
161
|
+
- lib/pgbouncerhero/methods/basics.rb
|
162
|
+
- lib/pgbouncerhero/version.rb
|
163
|
+
- pgbouncerhero.gemspec
|
164
|
+
homepage: https://github.com/kwent/pgbouncerhero
|
165
|
+
licenses:
|
166
|
+
- MIT
|
167
|
+
metadata: {}
|
168
|
+
post_install_message:
|
169
|
+
rdoc_options: []
|
170
|
+
require_paths:
|
171
|
+
- lib
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
requirements: []
|
183
|
+
rubyforge_project:
|
184
|
+
rubygems_version: 2.5.1
|
185
|
+
signing_key:
|
186
|
+
specification_version: 4
|
187
|
+
summary: A graphical user interface for your PGBouncers
|
188
|
+
test_files: []
|