pghero 3.1.0 → 3.7.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -1
  3. data/LICENSE.txt +1 -1
  4. data/README.md +2 -2
  5. data/app/assets/javascripts/pghero/Chart.bundle.js +23379 -19766
  6. data/app/assets/javascripts/pghero/application.js +27 -12
  7. data/app/assets/javascripts/pghero/chartkick.js +834 -764
  8. data/app/assets/javascripts/pghero/highlight.min.js +440 -0
  9. data/app/assets/javascripts/pghero/jquery.js +318 -197
  10. data/app/assets/javascripts/pghero/nouislider.js +676 -1066
  11. data/app/assets/stylesheets/pghero/application.css +108 -2
  12. data/app/assets/stylesheets/pghero/nouislider.css +4 -10
  13. data/app/controllers/pg_hero/home_controller.rb +53 -16
  14. data/app/helpers/pg_hero/home_helper.rb +3 -3
  15. data/app/views/layouts/pg_hero/application.html.erb +3 -3
  16. data/app/views/pg_hero/home/_connections_table.html.erb +1 -1
  17. data/app/views/pg_hero/home/_live_queries_table.html.erb +8 -8
  18. data/app/views/pg_hero/home/_queries_table.html.erb +5 -5
  19. data/app/views/pg_hero/home/_query_stats_slider.html.erb +8 -8
  20. data/app/views/pg_hero/home/_suggested_index.html.erb +6 -5
  21. data/app/views/pg_hero/home/connections.html.erb +12 -12
  22. data/app/views/pg_hero/home/explain.html.erb +2 -2
  23. data/app/views/pg_hero/home/index.html.erb +22 -20
  24. data/app/views/pg_hero/home/index_bloat.html.erb +6 -6
  25. data/app/views/pg_hero/home/maintenance.html.erb +3 -3
  26. data/app/views/pg_hero/home/queries.html.erb +7 -5
  27. data/app/views/pg_hero/home/relation_space.html.erb +4 -4
  28. data/app/views/pg_hero/home/show_query.html.erb +35 -31
  29. data/app/views/pg_hero/home/space.html.erb +50 -46
  30. data/app/views/pg_hero/home/system.html.erb +18 -18
  31. data/app/views/pg_hero/home/tune.html.erb +2 -2
  32. data/lib/generators/pghero/query_stats_generator.rb +1 -0
  33. data/lib/generators/pghero/space_stats_generator.rb +1 -0
  34. data/lib/pghero/database.rb +2 -2
  35. data/lib/pghero/engine.rb +4 -3
  36. data/lib/pghero/methods/basic.rb +26 -31
  37. data/lib/pghero/methods/connections.rb +4 -4
  38. data/lib/pghero/methods/constraints.rb +1 -1
  39. data/lib/pghero/methods/explain.rb +4 -3
  40. data/lib/pghero/methods/indexes.rb +8 -8
  41. data/lib/pghero/methods/kill.rb +1 -1
  42. data/lib/pghero/methods/maintenance.rb +3 -3
  43. data/lib/pghero/methods/queries.rb +2 -2
  44. data/lib/pghero/methods/query_stats.rb +34 -24
  45. data/lib/pghero/methods/replication.rb +2 -2
  46. data/lib/pghero/methods/sequences.rb +10 -5
  47. data/lib/pghero/methods/settings.rb +8 -1
  48. data/lib/pghero/methods/space.rb +20 -14
  49. data/lib/pghero/methods/suggested_indexes.rb +14 -7
  50. data/lib/pghero/methods/system.rb +12 -6
  51. data/lib/pghero/methods/tables.rb +4 -5
  52. data/lib/pghero/version.rb +1 -1
  53. data/lib/pghero.rb +35 -36
  54. data/lib/tasks/pghero.rake +11 -1
  55. data/licenses/LICENSE-chart.js.txt +1 -1
  56. data/licenses/LICENSE-date-fns.txt +21 -20
  57. data/licenses/LICENSE-kurkle-color.txt +9 -0
  58. metadata +8 -11
  59. data/app/assets/javascripts/pghero/highlight.pack.js +0 -2
@@ -187,7 +187,7 @@ hr {
187
187
  }
188
188
 
189
189
  #slider-container {
190
- padding: 6px 140px 20px 140px;
190
+ padding: 6px 8px 14px 8px;
191
191
  }
192
192
 
193
193
  .queries-table th a, .space-table th a {
@@ -195,7 +195,7 @@ hr {
195
195
  }
196
196
 
197
197
  #slider {
198
- margin-bottom: 20px;
198
+ margin: 0px 14px 18px 14px;
199
199
  }
200
200
 
201
201
  #range-start {
@@ -483,6 +483,12 @@ body {
483
483
  line-height: 300px;
484
484
  text-align: center;
485
485
  color: #999;
486
+ margin-bottom: 20px;
487
+ }
488
+
489
+ .pie-chart {
490
+ height: 260px;
491
+ line-height: 260px;
486
492
  }
487
493
 
488
494
  .unused-index {
@@ -497,8 +503,18 @@ body {
497
503
  font-weight: bold;
498
504
  }
499
505
 
506
+ .origins-table {
507
+ table-layout: auto;
508
+ }
509
+
500
510
  .origin {
501
511
  word-break: break-word;
512
+ width: 90%;
513
+ }
514
+
515
+ .origin-pct {
516
+ text-align: right;
517
+ width: 10%;
502
518
  }
503
519
 
504
520
  .duplicate-indexes td {
@@ -512,3 +528,93 @@ body {
512
528
  .no-outline:focus {
513
529
  outline: none;
514
530
  }
531
+
532
+ .width-10 {
533
+ width: 10%;
534
+ }
535
+
536
+ .width-15 {
537
+ width: 15%;
538
+ }
539
+
540
+ .width-20 {
541
+ width: 20%;
542
+ }
543
+
544
+ .width-25 {
545
+ width: 25%;
546
+ }
547
+
548
+ .width-33 {
549
+ width: 33.33%;
550
+ }
551
+
552
+ .right-5 {
553
+ margin-right: 5px;
554
+ }
555
+
556
+ .query-row {
557
+ border-top: none;
558
+ padding: 0;
559
+ }
560
+
561
+ .query-pre {
562
+ margin-top: 1em;
563
+ }
564
+
565
+ .query-code {
566
+ max-height: 230px;
567
+ overflow: hidden;
568
+ }
569
+
570
+ .break-all {
571
+ word-break: break-all;
572
+ }
573
+
574
+ .migration {
575
+ display: none;
576
+ }
577
+
578
+ .migration pre {
579
+ overflow: scroll;
580
+ white-space: pre;
581
+ word-break: normal;
582
+ }
583
+
584
+ .space-index {
585
+ font-style: italic;
586
+ }
587
+
588
+ a.relation-link {
589
+ color: inherit;
590
+ }
591
+
592
+ .push-left {
593
+ float: left;
594
+ }
595
+
596
+ .clear-both {
597
+ clear: both;
598
+ }
599
+
600
+ .create-index {
601
+ color: #eee;
602
+ background-color: #333;
603
+ }
604
+
605
+ .hide {
606
+ display: none;
607
+ }
608
+
609
+ .show-details {
610
+ float: right;
611
+ color: #f0ad4e;
612
+ margin-top: 0px;
613
+ padding: 10px;
614
+ cursor: pointer;
615
+ }
616
+
617
+ .details pre {
618
+ color: #f0ad4e;
619
+ background-color: #333;
620
+ }
@@ -1,4 +1,3 @@
1
- /*! nouislider - 14.6.1 - 8/17/2020 */
2
1
  /* Functional styling;
3
2
  * These styles are required for noUiSlider to function.
4
3
  * You don't need to change these rules to apply your design.
@@ -39,20 +38,14 @@
39
38
  z-index: 1;
40
39
  top: 0;
41
40
  right: 0;
41
+ height: 100%;
42
+ width: 100%;
42
43
  -ms-transform-origin: 0 0;
43
44
  -webkit-transform-origin: 0 0;
44
45
  -webkit-transform-style: preserve-3d;
45
46
  transform-origin: 0 0;
46
47
  transform-style: flat;
47
48
  }
48
- .noUi-connect {
49
- height: 100%;
50
- width: 100%;
51
- }
52
- .noUi-origin {
53
- height: 10%;
54
- width: 10%;
55
- }
56
49
  /* Offset direction
57
50
  */
58
51
  .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin {
@@ -63,6 +56,7 @@
63
56
  * connect elements.
64
57
  */
65
58
  .noUi-vertical .noUi-origin {
59
+ top: -100%;
66
60
  width: 0;
67
61
  }
68
62
  .noUi-horizontal .noUi-origin {
@@ -103,7 +97,7 @@
103
97
  width: 28px;
104
98
  height: 34px;
105
99
  right: -6px;
106
- top: -17px;
100
+ bottom: -17px;
107
101
  }
108
102
  .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle {
109
103
  left: -17px;
@@ -46,11 +46,18 @@ module PgHero
46
46
 
47
47
  @transaction_id_danger = @database.transaction_id_danger(threshold: 1500000000)
48
48
 
49
- @readable_sequences, @unreadable_sequences = @database.sequences.partition { |s| s[:readable] }
49
+ sequences, @sequences_timeout = rescue_timeout([]) { @database.sequences }
50
+ @readable_sequences, @unreadable_sequences = sequences.partition { |s| s[:readable] }
50
51
 
51
52
  @sequence_danger = @database.sequence_danger(threshold: (params[:sequence_threshold] || 0.9).to_f, sequences: @readable_sequences)
52
53
 
53
- @indexes = @database.indexes
54
+ @indexes, @indexes_timeout =
55
+ if @sequences_timeout
56
+ # skip indexes for faster loading
57
+ [[], true]
58
+ else
59
+ rescue_timeout([]) { @database.indexes }
60
+ end
54
61
  @invalid_indexes = @database.invalid_indexes(indexes: @indexes)
55
62
  @invalid_constraints = @database.invalid_constraints
56
63
  @duplicate_indexes = @database.duplicate_indexes(indexes: @indexes)
@@ -80,7 +87,7 @@ module PgHero
80
87
  @days = (params[:days] || 7).to_i
81
88
  @database_size = @database.database_size
82
89
  @only_tables = params[:tables].present?
83
- @relation_sizes = @only_tables ? @database.table_sizes : @database.relation_sizes
90
+ @relation_sizes, @sizes_timeout = rescue_timeout([]) { @only_tables ? @database.table_sizes : @database.relation_sizes }
84
91
  @space_stats_enabled = @database.space_stats_enabled? && !@only_tables
85
92
  if @space_stats_enabled
86
93
  space_growth = @database.space_growth(days: @days, relation_sizes: @relation_sizes)
@@ -157,8 +164,11 @@ module PgHero
157
164
  )
158
165
  end
159
166
 
160
- @indexes = @database.indexes
161
- set_suggested_indexes
167
+ if !@historical_query_stats_enabled || request.xhr?
168
+ set_suggested_indexes
169
+ else
170
+ @debug = params[:debug].present?
171
+ end
162
172
 
163
173
  # fix back button issue with caching
164
174
  response.headers["Cache-Control"] = "must-revalidate, no-store, no-cache, private"
@@ -178,7 +188,7 @@ module PgHero
178
188
  @explainable_query = stats[:explainable_query]
179
189
 
180
190
  if @show_details
181
- query_hash_stats = @database.query_hash_stats(@query_hash, user: @user)
191
+ query_hash_stats = @database.query_hash_stats(@query_hash, user: @user, current: true)
182
192
 
183
193
  @chart_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), (r[:total_minutes] * 60 * 1000).round] }, library: chart_library_options}]
184
194
  @chart2_data = [{name: "Value", data: query_hash_stats.map { |r| [r[:captured_at].change(sec: 0), r[:average_time].round(1)] }, library: chart_library_options}]
@@ -193,7 +203,8 @@ module PgHero
193
203
 
194
204
  if @tables.any?
195
205
  @row_counts = @database.table_stats(table: @tables).to_h { |i| [i[:table], i[:estimated_rows]] }
196
- @indexes_by_table = @database.indexes.group_by { |i| i[:table] }
206
+ indexes, @indexes_timeout = rescue_timeout([]) { @database.indexes }
207
+ @indexes_by_table = indexes.group_by { |i| i[:table] }
197
208
  end
198
209
  else
199
210
  render_text "Unknown query", status: :not_found
@@ -239,9 +250,16 @@ module PgHero
239
250
  stats =
240
251
  case @database.system_stats_provider
241
252
  when :azure
242
- [
243
- {name: "IO Consumption", data: @database.azure_stats("io_consumption_percent", **system_params), library: chart_library_options}
244
- ]
253
+ if @database.send(:azure_flexible_server?)
254
+ [
255
+ {name: "Read IOPS", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
256
+ {name: "Write IOPS", data: @database.write_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options}
257
+ ]
258
+ else
259
+ [
260
+ {name: "IO Consumption", data: @database.azure_stats("io_consumption_percent", **system_params), library: chart_library_options}
261
+ ]
262
+ end
245
263
  when :gcp
246
264
  [
247
265
  {name: "Read Ops", data: @database.read_iops_stats(**system_params).map { |k, v| [k, v ? v.round : v] }, library: chart_library_options},
@@ -258,7 +276,7 @@ module PgHero
258
276
 
259
277
  def free_space_stats
260
278
  render json: [
261
- {name: "Free Space", data: @database.free_space_stats(duration: 14.days, period: 1.hour), library: chart_library_options},
279
+ {name: "Free Space", data: @database.free_space_stats(duration: 14.days, period: 1.hour), library: chart_library_options}
262
280
  ]
263
281
  end
264
282
 
@@ -276,12 +294,14 @@ module PgHero
276
294
  # need to prevent CSRF and DoS
277
295
  if request.post? && @query.present?
278
296
  begin
297
+ generic_plan = @database.server_version_num >= 160000 && @query.include?("$1")
298
+
279
299
  explain_options =
280
300
  case params[:commit]
281
301
  when "Analyze"
282
302
  {analyze: true}
283
303
  when "Visualize"
284
- if @explain_analyze_enabled
304
+ if @explain_analyze_enabled && !generic_plan
285
305
  {analyze: true, costs: true, verbose: true, buffers: true, format: "json"}
286
306
  else
287
307
  {costs: true, verbose: true, format: "json"}
@@ -290,6 +310,8 @@ module PgHero
290
310
  {}
291
311
  end
292
312
 
313
+ explain_options[:generic_plan] = true if generic_plan
314
+
293
315
  if explain_options[:analyze] && !@explain_analyze_enabled
294
316
  render_text "Explain analyze not enabled", status: :bad_request
295
317
  return
@@ -303,8 +325,10 @@ module PgHero
303
325
  @error =
304
326
  if message == "Unsafe statement"
305
327
  "Unsafe statement"
306
- elsif message.start_with?("PG::ProtocolViolation: ERROR: bind message supplies 0 parameters")
328
+ elsif message.start_with?("PG::UndefinedParameter")
307
329
  "Can't explain queries with bind parameters"
330
+ elsif message.include?("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")
331
+ "Can't analyze queries with bind parameters"
308
332
  elsif message.start_with?("PG::SyntaxError")
309
333
  "Syntax error with query"
310
334
  elsif message.start_with?("PG::QueryCanceled")
@@ -395,6 +419,7 @@ module PgHero
395
419
  redirect_backward alert: "The database user does not have permission to enable query stats"
396
420
  end
397
421
 
422
+ # TODO disable if historical query stats enabled?
398
423
  def reset_query_stats
399
424
  success =
400
425
  if @database.server_version_num >= 120000
@@ -440,14 +465,18 @@ module PgHero
440
465
  end
441
466
 
442
467
  def set_suggested_indexes(min_average_time = 0, min_calls = 0)
468
+ if @database.suggested_indexes_enabled? && !@indexes
469
+ @indexes, @indexes_timeout = rescue_timeout([]) { @database.indexes }
470
+ end
471
+
443
472
  @suggested_indexes_by_query =
444
- if @database.suggested_indexes_enabled?
445
- @database.suggested_indexes_by_query(query_stats: @query_stats.select { |qs| qs[:average_time] >= min_average_time && qs[:calls] >= min_calls })
473
+ if !@indexes_timeout && @database.suggested_indexes_enabled?
474
+ @database.suggested_indexes_by_query(query_stats: @query_stats.select { |qs| qs[:average_time] >= min_average_time && qs[:calls] >= min_calls }, indexes: @indexes)
446
475
  else
447
476
  {}
448
477
  end
449
478
 
450
- @suggested_indexes = @database.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query, indexes: @indexes)
479
+ @suggested_indexes = @database.suggested_indexes(suggested_indexes_by_query: @suggested_indexes_by_query)
451
480
  @query_stats_by_query = @query_stats.index_by { |q| q[:query] }
452
481
  @debug = params[:debug].present?
453
482
  end
@@ -495,5 +524,13 @@ module PgHero
495
524
  redirect_to root_path, alert: "Query stats not enabled"
496
525
  end
497
526
  end
527
+
528
+ # rescue QueryCanceled for case when
529
+ # statement timeout is less than lock timeout
530
+ def rescue_timeout(default)
531
+ [yield, false]
532
+ rescue ActiveRecord::LockWaitTimeout, ActiveRecord::QueryCanceled
533
+ [default, true]
534
+ end
498
535
  end
499
536
  end
@@ -5,15 +5,15 @@ module PgHero
5
5
  if schema && schema != "public"
6
6
  ident = "#{schema}.#{table}"
7
7
  end
8
- if ident =~ /\A[a-z0-9_]+\z/
8
+ if /\A[a-z0-9_]+\z/.match?(ident)
9
9
  ident
10
10
  else
11
11
  @database.quote_ident(ident)
12
12
  end
13
13
  end
14
14
 
15
- def pghero_js_var(name, value)
16
- "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
15
+ def pghero_js_value(value)
16
+ json_escape(value.to_json(root: false)).html_safe
17
17
  end
18
18
 
19
19
  def pghero_remove_index(query)
@@ -5,12 +5,12 @@
5
5
 
6
6
  <meta charset="utf-8" />
7
7
  <%= favicon_link_tag "pghero/favicon.png" %>
8
- <% if defined?(Propshaft::Railtie) %>
8
+ <% if defined?(Propshaft::Railtie) && Rails.application.assets.is_a?(Propshaft::Assembly) %>
9
9
  <%= stylesheet_link_tag "pghero/nouislider", "pghero/arduino-light", "pghero/application" %>
10
- <%= javascript_include_tag "pghero/jquery", "pghero/nouislider", "pghero/Chart.bundle", "pghero/chartkick", "pghero/highlight.pack", "pghero/application" %>
10
+ <%= javascript_include_tag "pghero/jquery", "pghero/nouislider", "pghero/Chart.bundle", "pghero/chartkick", "pghero/highlight.min", "pghero/application", nonce: true %>
11
11
  <% else %>
12
12
  <%= stylesheet_link_tag "pghero/application" %>
13
- <%= javascript_include_tag "pghero/application" %>
13
+ <%= javascript_include_tag "pghero/application", nonce: true %>
14
14
  <% end %>
15
15
  </head>
16
16
  <body>
@@ -2,7 +2,7 @@
2
2
  <thead>
3
3
  <tr>
4
4
  <th>Top Sources</th>
5
- <th style="width: 20%;">Connections</th>
5
+ <th class="width-20">Connections</th>
6
6
  </tr>
7
7
  </thead>
8
8
  <tbody>
@@ -1,10 +1,10 @@
1
1
  <table class="table queries">
2
2
  <thead>
3
3
  <tr>
4
- <th style="width: 25%;">Pid</th>
5
- <th style="width: 25%;">Duration</th>
6
- <th style="width: 25%;">State</th>
7
- <th style="width: 25%;"></th>
4
+ <th class="width-25">Pid</th>
5
+ <th class="width-25">Duration</th>
6
+ <th class="width-25">State</th>
7
+ <th class="width-25"></th>
8
8
  </tr>
9
9
  </thead>
10
10
  <tbody>
@@ -39,15 +39,15 @@
39
39
  </td>
40
40
  </tr>
41
41
  <tr>
42
- <td colspan="6" style="border-top: none; padding: 0;">
42
+ <td colspan="4" class="query-row">
43
43
  <%= query[:source] %> <span class="text-muted"><%= query[:user] %></span>
44
- <pre style="margin-top: 1em;"><code><%= query[:query] %></code></pre>
44
+ <pre class="query-pre"><code><%= query[:query] %></code></pre>
45
45
  </td>
46
46
  </tr>
47
47
  <% end %>
48
48
  </tbody>
49
49
  </table>
50
50
 
51
- <script>
51
+ <%= javascript_tag nonce: true do %>
52
52
  highlightQueries();
53
- </script>
53
+ <% end %>
@@ -2,9 +2,9 @@
2
2
  <% unless local_assigns[:xhr] %>
3
3
  <thead>
4
4
  <tr>
5
- <th style="width: 33.33%;"><%= local_assigns[:sort_headers] ? link_to("Total Time", {sort: nil}, data: {sort: nil}) : "Total Time" %></th>
6
- <th style="width: 33.33%;"><%= local_assigns[:sort_headers] ? link_to("Average Time", {sort: "average_time"}, data: {sort: "average_time"}) : "Average Time" %></th>
7
- <th style="width: 33.33%;"><%= local_assigns[:sort_headers] ? link_to("Calls", {sort: "calls"}, data: {sort: "calls"}) : "Calls" %></th>
5
+ <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Total Time", {sort: nil}, data: {sort: nil}) : "Total Time" %></th>
6
+ <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Average Time", {sort: "average_time"}, data: {sort: "average_time"}) : "Average Time" %></th>
7
+ <th class="width-33"><%= local_assigns[:sort_headers] ? link_to("Calls", {sort: "calls"}, data: {sort: "calls"}) : "Calls" %></th>
8
8
  </tr>
9
9
  </thead>
10
10
  <% end %>
@@ -57,8 +57,8 @@
57
57
  </td>
58
58
  </tr>
59
59
  <tr>
60
- <td colspan="3" style="border-top: none; padding: 0;">
61
- <pre><code style="max-height: 230px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><%= query[:query] %></code></pre>
60
+ <td colspan="3" class="query-row">
61
+ <pre><code class="query-code"><%= query[:query] %></code></pre>
62
62
  <% if query[:query] == "<insufficient privilege>" %>
63
63
  <p class="text-muted">For security reasons, only superusers can see queries executed by other users.</p>
64
64
  <% end %>
@@ -4,13 +4,13 @@
4
4
  <div id="range-start"></div>
5
5
  </div>
6
6
 
7
- <script>
8
- <%= pghero_js_var("sort", @sort) %>
9
- <%= pghero_js_var("minAverageTime", @min_average_time) %>
10
- <%= pghero_js_var("minCalls", @min_calls) %>
11
- <%= pghero_js_var("debug", @debug) %>
12
- <%= pghero_js_var("startAt", params[:start_at] ? @start_at.to_i * 1000 : nil) %>
13
- <%= pghero_js_var("endAt", @end_at.to_i * 1000) %>
7
+ <%= javascript_tag nonce: true do %>
8
+ var sort = <%= pghero_js_value(@sort) %>;
9
+ var minAverageTime = <%= pghero_js_value(@min_average_time) %>;
10
+ var minCalls = <%= pghero_js_value(@min_calls) %>;
11
+ var debug = <%= pghero_js_value(@debug) %>;
12
+ var startAt = <%= pghero_js_value(params[:start_at] ? @start_at.to_i * 1000 : nil) %>;
13
+ var endAt = <%= pghero_js_value(@end_at.to_i * 1000) %>;
14
14
 
15
15
  initSlider();
16
- </script>
16
+ <% end %>
@@ -1,13 +1,14 @@
1
1
  <% if index && !details[:covering_index] %>
2
2
  <% unless @debug %>
3
- <div style="float: right; color: #f0ad4e; margin-top: 0px; padding: 10px; cursor: pointer;" onclick="document.getElementById('details-<%= index.object_id %>').style.display = 'block'; this.style.display = 'none';">Details</div>
3
+ <div class="show-details">Details</div>
4
4
  <% end %>
5
- <code><pre style="color: #eee; background-color: #333;">CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)</pre></code>
5
+ <code><pre class="create-index">CREATE INDEX CONCURRENTLY ON <%= index[:table] %><% if index[:using] %> USING <%= index[:using] %><% end %> (<%= index[:columns].join(", ") %>)</pre></code>
6
6
  <% end %>
7
- <div id="details-<%= index.object_id %>" style="<%= "display: none;" unless @debug %>">
8
- <code><pre style="color: #f0ad4e; background-color: #333;"><% if details[:explanation] %><%= details[:explanation] %>
7
+ <div class="details<%= " hide" unless @debug %>">
8
+ <code><pre><% if details[:explanation] %><%= details[:explanation] %>
9
9
  <% end %><% if details[:row_estimates] %>Rows: <%= details[:rows] %>
10
- Row progression: <%= details[:row_progression].to_a.join(", ") %>
10
+ <% if details[:row_progression] %>Row progression: <%= details[:row_progression].to_a.join(", ") %>
11
+ <% end %>
11
12
 
12
13
  Row estimates
13
14
  <%= details[:row_estimates].to_a.map { |k, v| "- #{k}: #{v}" }.join("\n") %><% end %><% if details[:table_indexes] %>
@@ -6,25 +6,25 @@
6
6
  <% if @total_connections > 0 %>
7
7
  <h3>By Database</h3>
8
8
 
9
- <div id="chart-1" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
10
- <script>
11
- new Chartkick.PieChart("chart-1", <%= json_escape(@connections_by_database.to_json).html_safe %>);
12
- </script>
9
+ <div id="chart-1" class="chart pie-chart">Loading...</div>
10
+ <%= javascript_tag nonce: true do %>
11
+ new Chartkick.PieChart("chart-1", <%= pghero_js_value(@connections_by_database) %>);
12
+ <% end %>
13
13
 
14
14
  <h3>By User</h3>
15
15
 
16
- <div id="chart-2" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
17
- <script>
18
- new Chartkick.PieChart("chart-2", <%= json_escape(@connections_by_user.to_json).html_safe %>);
19
- </script>
16
+ <div id="chart-2" class="chart pie-chart">Loading...</div>
17
+ <%= javascript_tag nonce: true do %>
18
+ new Chartkick.PieChart("chart-2", <%= pghero_js_value(@connections_by_user) %>);
19
+ <% end %>
20
20
 
21
21
  <% if @connections_by_ssl_status %>
22
22
  <h3>By Security</h3>
23
23
 
24
- <div id="chart-3" class="chart" style="height: 260px; line-height: 260px; margin-bottom: 20px;">Loading...</div>
25
- <script>
26
- new Chartkick.PieChart("chart-3", <%= json_escape(@connections_by_ssl_status.to_json).html_safe %>);
27
- </script>
24
+ <div id="chart-3" class="chart pie-chart">Loading...</div>
25
+ <%= javascript_tag nonce: true do %>
26
+ new Chartkick.PieChart("chart-3", <%= pghero_js_value(@connections_by_ssl_status) %>);
27
+ <% end %>
28
28
  <% end %>
29
29
 
30
30
  <%= render partial: "connections_table", locals: {connection_sources: @connection_sources} %>
@@ -4,9 +4,9 @@
4
4
  <%= form_tag explain_path do %>
5
5
  <div class="field"><%= text_area_tag :query, @query, placeholder: "Enter a SQL query" %></div>
6
6
  <p>
7
- <%= submit_tag "Explain", class: "btn btn-info", style: "margin-right: 10px;" %>
7
+ <%= submit_tag "Explain", class: "btn btn-info right-5" %>
8
8
  <% if @explain_analyze_enabled %>
9
- <%= submit_tag "Analyze", class: "btn btn-danger", style: "margin-right: 10px;" %>
9
+ <%= submit_tag "Analyze", class: "btn btn-danger right-5" %>
10
10
  <% end %>
11
11
  <%= submit_tag "Visualize", class: "btn btn-danger" %>
12
12
  </p>