pgbouncerhero 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +155 -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 +6 -9
  7. data/app/controllers/pg_bouncer_hero/database_controller.rb +39 -13
  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 +20 -18
  18. data/app/views/pg_bouncer_hero/database/_pools.html.erb +21 -23
  19. data/app/views/pg_bouncer_hero/database/_state.html.erb +11 -0
  20. data/app/views/pg_bouncer_hero/database/_stats.html.erb +27 -33
  21. data/app/views/pg_bouncer_hero/database/state.html.erb +4 -0
  22. data/app/views/pg_bouncer_hero/database/summary.html.erb +3 -0
  23. data/app/views/pg_bouncer_hero/home/_card.html.erb +9 -22
  24. data/app/views/pg_bouncer_hero/home/_card_content.html.erb +33 -33
  25. data/app/views/pg_bouncer_hero/home/_card_loading_content.html.erb +5 -5
  26. data/app/views/pg_bouncer_hero/home/index.html.erb +13 -37
  27. data/app/views/shared/_alert.html.erb +2 -5
  28. data/config/importmap.rb +5 -0
  29. data/config/routes.rb +10 -2
  30. data/lib/generators/pgbouncerhero/templates/config.yml +13 -8
  31. data/lib/pgbouncerhero/connection.rb +53 -16
  32. data/lib/pgbouncerhero/database.rb +69 -4
  33. data/lib/pgbouncerhero/engine.rb +13 -10
  34. data/lib/pgbouncerhero/group.rb +0 -2
  35. data/lib/pgbouncerhero/methods/basics.rb +28 -12
  36. data/lib/pgbouncerhero/version.rb +1 -1
  37. data/lib/pgbouncerhero.rb +124 -41
  38. metadata +187 -46
  39. data/.gitignore +0 -19
  40. data/Gemfile +0 -4
  41. data/app/assets/images/pgbouncerhero/short-paragraph.png +0 -0
  42. data/app/assets/javascripts/pgbouncerhero/application.js +0 -3
  43. data/app/assets/stylesheets/pgbouncerhero/application.css.scss +0 -15
  44. data/app/views/pg_bouncer_hero/database/summary.js.erb +0 -5
  45. data/doc/screenshot-1.png +0 -0
  46. data/doc/screenshot-2.png +0 -0
  47. data/doc/screenshot-3.png +0 -0
  48. data/pgbouncerhero.gemspec +0 -32
@@ -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>
@@ -1,27 +1,25 @@
1
- <div class="ui active segment">
2
- <h4 class="ui header">
3
- <div class="content">Databases</div>
4
- </h4>
5
- <table class="ui compact table">
6
- <thead>
7
- <tr>
8
- <% databases.first.keys.each do |key| %>
9
- <th>
10
- <%= key.titleize %>
11
- </th>
12
- <% end %>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% databases.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">Databases</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
+ <% databases.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
+ <% databases.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,21 +1,23 @@
1
- <div class="ui pointing menu">
2
- <div class="header item">
3
- <%= "#{database.group.name} > #{database.name}" %>
4
- </div>
5
- <%= link_to "Databases", databases_path(database: database.name.parameterize), class: "item #{is_active('databases')}" %>
6
- <%= link_to "Stats", stats_path(database: database.name.parameterize), class: "item #{is_active('stats')}" %>
7
- <%= link_to "Pools", pools_path(database: database.name.parameterize), class: "item #{is_active('pools')}" %>
8
- <%= link_to "Clients", clients_path(database: database.name.parameterize), class: "item #{is_active('clients')}" %>
9
- <%= link_to "Configuration", conf_path(database: database.name.parameterize), class: "item #{is_active('conf')}" %>
10
- <div class="right menu">
11
- <div class="item">
12
- <%= link_to 'Reload', reload_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic positive button', data: { confirm: 'Are you sure?' } %>
13
- </div>
14
- <div class="item">
15
- <%= link_to 'Suspend', suspend_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic primary button', data: { confirm: 'Are you sure?' } %>
1
+ <nav class="bg-white rounded-lg border border-gray-200 shadow-sm mb-6">
2
+ <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between flex-wrap gap-2">
3
+ <div class="flex items-center gap-4 flex-wrap">
4
+ <span class="font-semibold text-gray-900"><%= "#{database.group.name} > #{database.name}" %></span>
5
+ <%= link_to "Databases", databases_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('databases') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
6
+ <%= link_to "Stats", stats_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('stats') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
7
+ <%= link_to "Pools", pools_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('pools') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
8
+ <%= link_to "Clients", clients_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('clients') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
9
+ <%= link_to "Configuration", conf_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('conf') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
10
+ <%= link_to "State", state_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('state') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
16
11
  </div>
17
- <div class="item">
18
- <%= link_to 'Shutdown', shutdown_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic negative button', data: { confirm: 'Are you sure?' } %>
12
+ <div class="flex items-center gap-2">
13
+ <% if PgBouncerHero.read_only? %>
14
+ <span class="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-gray-300 ring-inset">Read-only</span>
15
+ <% else %>
16
+ <%= button_to "Reload", reload_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-green-300 text-green-700 hover:bg-green-50", form: { data: { turbo_confirm: "Reload PgBouncer configuration?" } } %>
17
+ <%= button_to "Suspend", suspend_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-blue-300 text-blue-700 hover:bg-blue-50", form: { data: { turbo_confirm: "Suspend all PgBouncer socket processing until Resume is selected?" } } %>
18
+ <%= button_to "Resume", resume_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-green-300 text-green-700 hover:bg-green-50", form: { data: { turbo_confirm: "Resume PgBouncer socket processing?" } } %>
19
+ <%= button_to "Graceful shutdown", shutdown_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-red-300 text-red-700 hover:bg-red-50", form: { data: { turbo_confirm: "Stop accepting new clients and shut down after existing clients disconnect?" } } %>
20
+ <% end %>
19
21
  </div>
20
22
  </div>
21
- </div>
23
+ </nav>
@@ -1,27 +1,25 @@
1
- <div class="ui active segment">
2
- <h4 class="ui header">
3
- <div class="content">Pools</div>
4
- </h4>
5
- <table class="ui compact table">
6
- <thead>
7
- <tr>
8
- <% pools.first.keys.each do |key| %>
9
- <th>
10
- <%= key.titleize %>
11
- </th>
12
- <% end %>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% pools.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">Pools</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
+ <% pools.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
+ <% pools.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>
@@ -0,0 +1,11 @@
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">State</h4>
3
+ <dl class="grid grid-cols-1 sm:grid-cols-2 gap-4">
4
+ <% state.each do |row| %>
5
+ <div>
6
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider"><%= row["key"].titleize %></dt>
7
+ <dd class="mt-1 text-sm text-gray-900"><%= row["value"] %></dd>
8
+ </div>
9
+ <% end %>
10
+ </dl>
11
+ </div>
@@ -1,37 +1,31 @@
1
- <div class="ui active segment">
2
- <h4 class="ui header">
3
- <div class="content">Stats</div>
4
- </h4>
5
- <table class="ui compact table">
6
- <thead>
7
- <tr>
8
- <% stats.first.keys.each do |key| %>
9
- <th>
10
- <%= key.titleize %>
11
- </th>
12
- <% end %>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% stats.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">Stats</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
- <% if ['total_received', 'total_sent', 'avg_recv', 'avg_sent'].include? k %>
20
- <td>
21
- <%= number_to_human_size(v) %>
22
- </td>
23
- <% elsif ['total_query_time', 'avg_query'].include? k %>
24
- <td>
25
- <%= humanize_ms(v.to_i) %>
26
- </td>
27
- <% else %>
28
- <td>
29
- <%= v %>
30
- </td>
31
- <% end %>
9
+ <% stats.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>
32
11
  <% end %>
33
12
  </tr>
34
- <% end %>
35
- </tbody>
36
- </table>
13
+ </thead>
14
+ <tbody class="bg-white divide-y divide-gray-200">
15
+ <% stats.each do |row| %>
16
+ <tr class="hover:bg-gray-50">
17
+ <% row.each do |k, v| %>
18
+ <% if %w[total_received total_sent avg_recv avg_sent].include?(k) %>
19
+ <td class="px-4 py-2 text-sm text-gray-700 whitespace-nowrap"><%= number_to_human_size(v) %></td>
20
+ <% elsif %w[total_query_time avg_query].include?(k) %>
21
+ <td class="px-4 py-2 text-sm text-gray-700 whitespace-nowrap"><%= humanize_ms(v.to_i) %></td>
22
+ <% else %>
23
+ <td class="px-4 py-2 text-sm text-gray-700 whitespace-nowrap"><%= v %></td>
24
+ <% end %>
25
+ <% end %>
26
+ </tr>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
30
+ </div>
37
31
  </div>
@@ -0,0 +1,4 @@
1
+ <%= render partial: "menu", locals: {database: @database} %>
2
+ <% if @state %>
3
+ <%= render partial: "state", locals: {state: @state} %>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <turbo-frame id="card_<%= @database.group.name.parameterize %>_<%= @database.name.parameterize %>">
2
+ <%= render partial: "pg_bouncer_hero/home/card_content", locals: { database: @database } %>
3
+ </turbo-frame>
@@ -1,26 +1,13 @@
1
- <div class="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) %>">
2
- <%= link_to databases_path(group: database.group.name.parameterize, database: database.name.parameterize), style: 'text-decoration: none !important; color: inherit !important' do %>
3
- <div class="ui segments" style="margin-bottom: 10px;">
4
- <div class="ui segment" style="height: 60px;">
5
- <div style="float: left;">
6
- <div style="font-size: 16px; font-weight: bold;">
7
- <%= database.name %>
8
- </div>
9
- <div style="font-size: 12px; color: grey">
10
- <%= database.host %>
11
- </div>
12
- </div>
13
- <button class="grey ui button basic button" id="segment_status_<%= database.group.name.parameterize %>_<%= database.name.parameterize %>" style="float: right;">
14
- <%= 'N/A' %>
15
- </button>
16
- </div>
17
- <%= render partial: partial, locals: {database: database, id: "segment_content_#{database.group.name.parameterize}_#{database.name.parameterize}"} %>
18
- <div class="ui segment" id="segment_refreshed_<%= database.group.name.parameterize %>_<%= database.name.parameterize %>" style="height: 35px; display: none;">
19
- <span class="right floated" id="segment_refreshed_span_<%= database.group.name.parameterize %>_<%= database.name.parameterize %>" style="float:right; margin-top: -6px; color: grey">
20
- Last refresh:
21
- <%= Time.now.strftime("%H:%M:%S GMT%z (%Z)") %>
22
- </span>
1
+ <div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
2
+ <%= link_to databases_path(group: database.group.name.parameterize, database: database.name.parameterize), class: "block no-underline text-inherit" do %>
3
+ <div class="px-4 py-3 flex items-center justify-between border-b border-gray-100">
4
+ <div>
5
+ <div class="text-base font-bold text-gray-900"><%= database.name %></div>
6
+ <div class="text-xs text-gray-500"><%= database.host %></div>
23
7
  </div>
24
8
  </div>
25
9
  <% end %>
10
+ <turbo-frame id="card_<%= database.group.name.parameterize %>_<%= database.name.parameterize %>" src="<%= summary_path(group: database.group.name.parameterize, database: database.name.parameterize) %>" loading="lazy">
11
+ <%= render partial: "card_loading_content" %>
12
+ </turbo-frame>
26
13
  </div>
@@ -1,43 +1,43 @@
1
- <% connected = database.connection %>
2
- <% if connected %>
3
- <div class="ui horizontal segments" id="<%= id %>" style="background-color: white !important;">
4
- <div class="ui segment">
5
- <% summary = database.summary %>
1
+ <% if database.connection %>
2
+ <% summary = database.summary %>
3
+ <div class="px-4 py-3 flex items-start gap-6">
4
+ <div class="space-y-1 text-sm text-gray-700">
6
5
  <div>
7
- <i class="users icon"></i>
8
- <% number = summary.select { |row| row['list'] == 'users' }.first['items'] %>
9
- <%= pluralize(number, 'user') %>
6
+ <% number = summary.select { |row| row["list"] == "users" }.first["items"] %>
7
+ <%= pluralize(number, "user") %>
10
8
  </div>
11
9
  <div>
12
- <i class="database icon"></i>
13
- <% number = summary.select { |row| row['list'] == 'databases' }.first['items'].to_i - 1 %>
14
- <%= pluralize(number, 'database') %>
10
+ <% number = summary.select { |row| row["list"] == "databases" }.first["items"].to_i - 1 %>
11
+ <%= pluralize(number, "database") %>
15
12
  </div>
16
13
  <div>
17
- <i class="plug icon"></i>
18
- <% number = summary.select { |row| row['list'] == 'pools' }.first['items'].to_i - 1 %>
19
- <%= pluralize(number, 'pool') %>
14
+ <% number = summary.select { |row| row["list"] == "pools" }.first["items"].to_i - 1 %>
15
+ <%= pluralize(number, "pool") %>
20
16
  </div>
21
17
  </div>
22
- <div class="ui segment">
23
- <ul style="margin: 0; padding-left: 5px;">
24
- <% summary.select{ |row| row.key?(:databases_details) }.first[:databases_details].each do |db| %>
25
- <li style="margin: 0; list-style-type: none;">
26
- <span>
27
- <i class="database icon"></i>
28
- <strong>
29
- <%= db['name'] %>
30
- </strong>
31
- <div class="ui progress small success" style="float:right; width: 150px; margin-top:4px;" data-value="<%= db['current_connections'] %>" data-total="<%= db['max_connections'] %>">
32
- <div class="bar">
33
- <div class="progress"></div>
34
- </div>
35
- <div class="label">Connections</div>
36
- </div>
37
- </span>
38
- </li>
39
- <% end %>
40
- </ul>
18
+ <div class="flex-1 space-y-2">
19
+ <% summary.select { |row| row.key?(:databases_details) }.first[:databases_details].each do |db| %>
20
+ <div class="flex items-center gap-2 text-sm">
21
+ <span class="font-medium text-gray-900 w-24 truncate"><%= db["name"] %></span>
22
+ <div class="flex-1">
23
+ <% current = db["current_connections"].to_i %>
24
+ <% max = db["max_connections"].to_i %>
25
+ <% pct = max > 0 ? (current.to_f / max * 100).round : 0 %>
26
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
27
+ <div class="bg-green-500 h-2.5 rounded-full" style="width: <%= pct %>%"></div>
28
+ </div>
29
+ <div class="text-xs text-gray-500 mt-0.5"><%= current %> / <%= max %> connections</div>
30
+ </div>
31
+ </div>
32
+ <% end %>
41
33
  </div>
42
34
  </div>
35
+ <div class="px-4 py-2 border-t border-gray-100 flex items-center justify-between">
36
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">Online</span>
37
+ <span class="text-xs text-gray-400">Last refresh: <%= Time.now.strftime("%H:%M:%S %Z") %></span>
38
+ </div>
39
+ <% else %>
40
+ <div class="px-4 py-6 text-center">
41
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-red-600/20 ring-inset">Offline</span>
42
+ </div>
43
43
  <% end %>
@@ -1,7 +1,7 @@
1
- <div class="ui segment" id="<%= id %>">
2
- <div class="ui active inverted dimmer">
3
- <div class="ui medium text loader">Loading...</div>
1
+ <div class="px-4 py-6 animate-pulse">
2
+ <div class="space-y-3">
3
+ <div class="h-3 bg-gray-200 rounded w-3/4"></div>
4
+ <div class="h-3 bg-gray-200 rounded w-1/2"></div>
5
+ <div class="h-3 bg-gray-200 rounded w-2/3"></div>
4
6
  </div>
5
- <%= image_tag('pgbouncerhero/short-paragraph.png', class: 'ui wireframe image') %>
6
- <%= image_tag('pgbouncerhero/short-paragraph.png', class: 'ui wireframe image') %>
7
7
  </div>
@@ -1,44 +1,20 @@
1
- <div class="ui pointing menu">
2
- <div class="header item">
3
- Overview
1
+ <div class="bg-white rounded-lg border border-gray-200 shadow-sm mb-6">
2
+ <div class="px-4 py-3 border-b border-gray-200">
3
+ <h2 class="text-lg font-semibold text-gray-900">Overview</h2>
4
4
  </div>
5
5
  </div>
6
- <div class="ui grid">
6
+
7
+ <div data-controller="polling" data-polling-interval-value="60000">
7
8
  <% @groups.each do |_, group| %>
8
- <div class="sixteen wide row">
9
- <div class="sixteen wide column">
10
- <h3 class="ui dividing header" style="margin-bottom: 10px;">
11
- <%= group.name %>
12
- </h3>
9
+ <div class="mb-8">
10
+ <h3 class="text-xl font-semibold text-gray-900 mb-4 pb-2 border-b border-gray-200">
11
+ <%= group.name %>
12
+ </h3>
13
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
14
+ <% group.databases.each do |database| %>
15
+ <%= render partial: "card", locals: { database: database } %>
16
+ <% end %>
13
17
  </div>
14
- <%= render partial: "card", collection: group.databases, as: :database, locals: {partial: 'card_loading_content', lazy: true} %>
15
18
  </div>
16
19
  <% end %>
17
20
  </div>
18
- <script>
19
-
20
- document.addEventListener("DOMContentLoaded", function(event) {
21
- // First
22
- $("[id^='segment_href']").each(function(idx, el) {
23
- $.get($(el).data('href'));
24
- });
25
- // Refresh every minutes
26
- setInterval(function (){
27
- $("[id^='segment_href']").each(function(idx, el) {
28
- var el = $(el)
29
- var id = el.attr('id');
30
- var href = el.data('href');
31
- var refreshed_id = id.replace('href', 'refreshed_span');
32
- var refreshed_div = document.getElementById(refreshed_id);
33
- if (refreshed_div) {
34
- refreshed_div.innerHTML = 'Refreshing...';
35
- $.get(href, function(data) {
36
- refreshed_div.innerHTML = 'Last refresh: ' + new Date().toTimeString();
37
- });
38
- }
39
- });
40
- }, 60 * 1000)
41
- });
42
-
43
-
44
- </script>
@@ -1,6 +1,3 @@
1
- <div class="ui <%= class_name %> message alert">
2
- <% if defined?(closable) && closable && closable == true %>
3
- <i class="close icon"></i>
4
- <% end %>
5
- <%= message.html_safe %>
1
+ <div class="rounded-md p-4 mb-4 <%= alert_style_for(class_name) %>">
2
+ <p class="text-sm"><%= message %></p>
6
3
  </div>
@@ -0,0 +1,5 @@
1
+ pin "pgbouncerhero/application", to: "pgbouncerhero/application.js"
2
+ pin "@hotwired/turbo-rails", to: "turbo.min.js"
3
+ pin "@hotwired/stimulus", to: "stimulus.min.js"
4
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
5
+ pin_all_from PgBouncerHero::Engine.root.join("app/javascript/pgbouncerhero/controllers"), under: "pgbouncerhero/controllers"
data/config/routes.rb CHANGED
@@ -1,15 +1,23 @@
1
1
  PgBouncerHero::Engine.routes.draw do
2
+ group_exists = ->(request) { PgBouncerHero.groups.key?(request.params[:group]) }
3
+ database_exists = lambda do |request|
4
+ group = PgBouncerHero.groups[request.params[:group]]
5
+ group&.databases&.any? { |database| database.name.parameterize == request.params[:database] }
6
+ end
7
+
2
8
  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
9
+ scope path: ":group", constraints: group_exists do
10
+ scope path: ":database", constraints: database_exists do
5
11
  get :summary, controller: :database
6
12
  get :databases, controller: :database
7
13
  get :stats, controller: :database
8
14
  get :pools, controller: :database
9
15
  get :clients, controller: :database
10
16
  get :conf, controller: :database
17
+ get :state, controller: :database
11
18
  post :reload, controller: :database
12
19
  post :suspend, controller: :database
20
+ post :resume, controller: :database
13
21
  post :shutdown, controller: :database
14
22
  end
15
23
  end
@@ -1,16 +1,21 @@
1
1
  pgbouncers:
2
2
  production:
3
- master:
3
+ primary:
4
4
  # eg. postgres://user:password@host:port/pgbouncer
5
- # url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %>
5
+ # url: <%%= ENV["PGBOUNCER_PRODUCTION_PRIMARY_DATABASE_URL"] %>
6
+ # pool_size: 5
7
+ # pool_timeout: 5
6
8
  # Add more databases
7
- # slave:
8
- # url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %>
9
+ # replica:
10
+ # url: <%%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
9
11
  # staging:
10
- # master:
11
- # url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %>
12
- # slave:
13
- # url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %>
12
+ # primary:
13
+ # url: <%%= ENV["PGBOUNCER_STAGING_PRIMARY_DATABASE_URL"] %>
14
+ # replica:
15
+ # url: <%%= ENV["PGBOUNCER_STAGING_REPLICA_DATABASE_URL"] %>
14
16
 
15
17
  # Time zone (defaults to app time zone)
16
18
  # time_zone: "Pacific Time (US & Canada)"
19
+
20
+ # Hide and reject Reload, Suspend, Resume, and Shutdown commands
21
+ # read_only: true