blazer 3.4.0 → 3.5.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +23 -27
  4. data/app/assets/javascripts/blazer/Sortable.js +826 -1162
  5. data/app/assets/javascripts/blazer/ace/ace.js +15775 -13360
  6. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1411 -668
  7. data/app/assets/javascripts/blazer/ace/mode-sql.js +12 -6
  8. data/app/assets/javascripts/blazer/ace/theme-twilight.js +3 -3
  9. data/app/assets/javascripts/blazer/application.js +94 -52
  10. data/app/assets/javascripts/blazer/chart.umd.js +6 -6
  11. data/app/assets/javascripts/blazer/daterangepicker.js +193 -199
  12. data/app/assets/javascripts/blazer/highlight.min.js +235 -373
  13. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +116 -82
  14. data/app/assets/javascripts/blazer/moment.js +368 -365
  15. data/app/assets/javascripts/blazer/queries.js +68 -58
  16. data/app/assets/javascripts/blazer/routes.js +11 -11
  17. data/app/assets/javascripts/blazer/tablesort.js +42 -0
  18. data/app/assets/javascripts/blazer/tom-select.base.js +4175 -0
  19. data/app/assets/stylesheets/blazer/application.css +114 -5
  20. data/app/assets/stylesheets/blazer/tom-select.css +502 -0
  21. data/app/controllers/blazer/base_controller.rb +1 -1
  22. data/app/controllers/blazer/checks_controller.rb +5 -2
  23. data/app/controllers/blazer/queries_controller.rb +3 -1
  24. data/app/helpers/blazer/base_helper.rb +1 -1
  25. data/app/models/blazer/check.rb +35 -57
  26. data/app/views/blazer/_nav.html.erb +1 -1
  27. data/app/views/blazer/_variables.html.erb +57 -42
  28. data/app/views/blazer/checks/_form.html.erb +20 -16
  29. data/app/views/blazer/checks/index.html.erb +17 -20
  30. data/app/views/blazer/dashboards/_form.html.erb +63 -48
  31. data/app/views/blazer/dashboards/show.html.erb +23 -16
  32. data/app/views/blazer/queries/_caching.html.erb +1 -1
  33. data/app/views/blazer/queries/_chart.html.erb +8 -0
  34. data/app/views/blazer/queries/_form.html.erb +153 -180
  35. data/app/views/blazer/queries/_map.html.erb +8 -0
  36. data/app/views/blazer/queries/docs.html.erb +6 -6
  37. data/app/views/blazer/queries/edit.html.erb +1 -0
  38. data/app/views/blazer/queries/home.html.erb +101 -102
  39. data/app/views/blazer/queries/new.html.erb +1 -0
  40. data/app/views/blazer/queries/run.html.erb +23 -34
  41. data/app/views/blazer/queries/schema.html.erb +20 -20
  42. data/app/views/blazer/queries/show.html.erb +19 -12
  43. data/app/views/blazer/uploads/index.html.erb +10 -10
  44. data/app/views/layouts/blazer/application.html.erb +3 -3
  45. data/lib/blazer/adapters/athena_adapter.rb +12 -6
  46. data/lib/blazer/adapters/clickhouse_adapter.rb +136 -0
  47. data/lib/blazer/adapters/druid_adapter.rb +0 -2
  48. data/lib/blazer/adapters/snowflake2_adapter.rb +198 -0
  49. data/lib/blazer/adapters/soda_adapter.rb +0 -2
  50. data/lib/blazer/adapters/sql_adapter.rb +4 -0
  51. data/lib/blazer/adapters.rb +2 -0
  52. data/lib/blazer/check_types.rb +20 -0
  53. data/lib/blazer/email_notifier.rb +49 -0
  54. data/lib/blazer/result.rb +15 -0
  55. data/lib/blazer/slack_notifier.rb +39 -7
  56. data/lib/blazer/version.rb +1 -1
  57. data/lib/blazer.rb +32 -26
  58. data/lib/generators/blazer/templates/config.yml.tt +0 -2
  59. data/licenses/LICENSE-chart.js.txt +1 -1
  60. metadata +12 -31
  61. data/app/assets/javascripts/blazer/bootstrap.js +0 -2580
  62. data/app/assets/javascripts/blazer/jquery.js +0 -10872
  63. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +0 -325
  64. data/app/assets/javascripts/blazer/rails-ujs.js +0 -746
  65. data/app/assets/javascripts/blazer/selectize.js +0 -3891
  66. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +0 -13
  67. data/app/assets/javascripts/blazer/stupidtable.js +0 -281
  68. data/app/assets/javascripts/blazer/vue.global.prod.js +0 -1
  69. data/app/assets/stylesheets/blazer/selectize.css +0 -403
  70. data/licenses/LICENSE-jquery.txt +0 -20
  71. data/licenses/LICENSE-rails-ujs.txt +0 -20
  72. data/licenses/LICENSE-stickytableheaders.txt +0 -20
  73. data/licenses/LICENSE-stupidtable.txt +0 -19
  74. data/licenses/LICENSE-vue.txt +0 -21
  75. /data/licenses/{LICENSE-selectize.txt → LICENSE-tom-select.txt} +0 -0
@@ -29,7 +29,7 @@ module Blazer
29
29
  end
30
30
 
31
31
  def blazer_js_var(name, value)
32
- "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
32
+ "const #{name} = #{json_escape(value.to_json(root: false))};".html_safe
33
33
  end
34
34
 
35
35
  def blazer_series_name(k)
@@ -10,48 +10,16 @@ module Blazer
10
10
  before_validation :set_state
11
11
  before_validation :fix_emails
12
12
 
13
- def split_emails
14
- emails.to_s.downcase.split(",").map(&:strip)
15
- end
16
-
17
- def split_slack_channels
18
- if Blazer.slack?
19
- slack_channels.to_s.downcase.split(",").map(&:strip)
20
- else
21
- []
22
- end
23
- end
24
-
25
13
  def update_state(result)
26
- check_type =
27
- if respond_to?(:check_type)
28
- self.check_type
29
- elsif respond_to?(:invert)
30
- invert ? "missing_data" : "bad_data"
31
- else
32
- "bad_data"
33
- end
34
-
35
- message = result.error
14
+ check_type = computed_check_type
36
15
 
37
- self.state =
16
+ self.state, message =
38
17
  if result.timed_out?
39
- "timed out"
18
+ ["timed out", result.error]
40
19
  elsif result.error
41
- "error"
42
- elsif check_type == "anomaly"
43
- anomaly, message = result.detect_anomaly
44
- if anomaly.nil?
45
- "error"
46
- elsif anomaly
47
- "failing"
48
- else
49
- "passing"
50
- end
51
- elsif result.rows.any?
52
- check_type == "missing_data" ? "passing" : "failing"
20
+ ["error", result.error]
53
21
  else
54
- check_type == "missing_data" ? "failing" : "passing"
22
+ Blazer.check_types.fetch(check_type).fetch(:block).call(result)
55
23
  end
56
24
 
57
25
  self.last_run_at = Time.now if respond_to?(:last_run_at=)
@@ -68,37 +36,47 @@ module Blazer
68
36
 
69
37
  # do not notify on creation, except when not passing
70
38
  if (state_was != "new" || state != "passing") && state != state_was
71
- Blazer::CheckMailer.state_change(self, state, state_was, result.rows.size, message, result.columns, result.rows.first(10).as_json, result.column_types, check_type).deliver_now if emails.present?
72
- Blazer::SlackNotifier.state_change(self, state, state_was, result.rows.size, message, check_type)
39
+ Blazer.notifiers.each do |notifier|
40
+ notifier.state_change(check: self, state:, state_was:, result:, message:, check_type:)
41
+ end
73
42
  end
74
43
  save! if changed?
75
44
  end
76
45
 
46
+ def computed_check_type
47
+ if respond_to?(:check_type)
48
+ check_type
49
+ elsif respond_to?(:invert)
50
+ invert ? "missing_data" : "bad_data"
51
+ else
52
+ "bad_data"
53
+ end
54
+ end
55
+
77
56
  private
78
57
 
79
- def set_state
80
- self.state ||= "new"
81
- end
58
+ def set_state
59
+ self.state ||= "new"
60
+ end
82
61
 
83
- def fix_emails
84
- # some people like doing ; instead of ,
85
- # but we know what they mean, so let's fix it
86
- # also, some people like to use whitespace
87
- if emails.present?
88
- self.emails = emails.strip.gsub(/[;\s]/, ",").gsub(/,+/, ", ")
89
- end
62
+ # TODO rename
63
+ def fix_emails
64
+ Blazer.notifiers.each do |notifier|
65
+ notifier.before_validation(self) if notifier.respond_to?(:before_validation)
90
66
  end
67
+ end
91
68
 
92
- def validate_emails
93
- unless split_emails.all? { |e| e =~ /\A\S+@\S+\.\S+\z/ }
94
- errors.add(:base, "Invalid emails")
95
- end
69
+ # TODO rename
70
+ def validate_emails
71
+ Blazer.notifiers.each do |notifier|
72
+ notifier.validate(self) if notifier.respond_to?(:validate)
96
73
  end
74
+ end
97
75
 
98
- def validate_variables
99
- if query.variables.any?
100
- errors.add(:base, "Query can't have variables")
101
- end
76
+ def validate_variables
77
+ if query && query.variables.any?
78
+ errors.add(:base, "Query can't have variables")
102
79
  end
80
+ end
103
81
  end
104
82
  end
@@ -1,4 +1,4 @@
1
- <div class="btn-group" style="vertical-align: top; margin-right: 5px;">
1
+ <div class="btn-group nav-btn-group">
2
2
  <%= link_to "Home", root_path, class: "btn btn-primary" %>
3
3
  <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
4
4
  <span class="caret"></span>
@@ -2,14 +2,14 @@
2
2
  <% var_params = request.query_parameters %>
3
3
  <%= javascript_tag nonce: true do %>
4
4
  <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
5
- var now = moment.tz(timeZone)
6
- var format = "YYYY-MM-DD"
5
+ const now = moment.tz(timeZone)
6
+ const format = "YYYY-MM-DD"
7
7
 
8
8
  function toDate(time) {
9
9
  return moment.tz(time.format(format), timeZone)
10
10
  }
11
11
  <% end %>
12
- <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
12
+ <%= tag.form id: "bind", method: "get", action: action, class: "form-inline", style: "margin-bottom: 15px;" do %>
13
13
  <% date_vars = ["start_time", "end_time"] %>
14
14
  <% if (date_vars - @bind_vars).empty? %>
15
15
  <% @bind_vars = @bind_vars - date_vars %>
@@ -22,39 +22,47 @@
22
22
  <% if (data = @smart_vars[var]) %>
23
23
  <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
24
24
  <%= javascript_tag nonce: true do %>
25
- $("#<%= var %>").selectize({
26
- create: true
27
- });
25
+ (function () {
26
+ <%= blazer_js_var "varId", var %>
27
+ new TomSelect(document.getElementById(varId), {
28
+ create: true,
29
+ onChange: function () {
30
+ submitIfCompleted(this.input.closest("form"))
31
+ this.blur()
32
+ }
33
+ })
34
+ })()
28
35
  <% end %>
29
36
  <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
30
37
  <%= hidden_field_tag var, var_params[var] %>
31
38
 
32
- <div class="selectize-control single" style="width: 200px;">
33
- <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
34
- <span>Select a date</span>
35
- </div>
39
+ <div class="ts-wrapper single" style="width: 200px;">
40
+ <%= tag.div id: "#{var}-select", class: "ts-control" do %>
41
+ <div class="item">Select a date</div>
42
+ <% end %>
36
43
  </div>
37
44
 
38
45
  <%= javascript_tag nonce: true do %>
39
- (function() {
40
- var input = $("#<%= var %>")
41
- var datePicker = $("#<%= var %>-select")
42
- datePicker.daterangepicker({
46
+ (function () {
47
+ <%= blazer_js_var "varId", var %>
48
+ <%= blazer_js_var "selectId", "#{var}-select" %>
49
+ const input = document.getElementById(varId)
50
+ const datePicker = document.getElementById(selectId)
51
+ const picker = new DateRangePicker(datePicker, {
43
52
  singleDatePicker: true,
44
53
  locale: {format: format},
45
54
  autoUpdateInput: false,
46
55
  autoApply: true,
47
- startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
56
+ startDate: input.value.length > 0 ? moment.tz(input.value, timeZone) : now
48
57
  })
49
58
  // hack to start with empty date
50
- datePicker.on("apply.daterangepicker", function(ev, picker) {
51
- datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
52
- input.val(toDate(picker.startDate).utc().format())
53
- submitIfCompleted($("#<%= var %>").closest("form"))
59
+ datePicker.addEventListener("apply.daterangepicker", function () {
60
+ datePicker.querySelector(".item").textContent = toDate(picker.startDate).format("MMMM D, YYYY")
61
+ input.value = toDate(picker.startDate).utc().format()
62
+ submitIfCompleted(input.closest("form"))
54
63
  })
55
- if (input.val().length > 0) {
56
- var picker = datePicker.data("daterangepicker")
57
- datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
64
+ if (input.value.length > 0) {
65
+ datePicker.querySelector(".item").textContent = toDate(picker.startDate).format("MMMM D, YYYY")
58
66
  }
59
67
  })()
60
68
  <% end %>
@@ -69,23 +77,32 @@
69
77
  <% end %>
70
78
 
71
79
  <%= label_tag nil, date_vars.join(" & ") %>
72
- <div class="selectize-control single" style="width: 300px;">
73
- <div id="reportrange" class="selectize-input" style="display: inline-block;">
74
- <span>Select a time range</span>
80
+ <div class="ts-wrapper single" style="width: 300px;">
81
+ <div class="ts-control" id="reportrange">
82
+ <div class="item">Select a time range</div>
75
83
  </div>
76
84
  </div>
77
85
 
78
86
  <%= javascript_tag nonce: true do %>
87
+ const startTime = document.getElementById("start_time")
88
+ const endTime = document.getElementById("end_time")
89
+ const reportrange = document.getElementById("reportrange")
90
+
79
91
  function dateStr(daysAgo) {
80
92
  return now.clone().subtract(daysAgo || 0, "days").format(format)
81
93
  }
82
94
 
83
95
  function setTimeInputs(start, end) {
84
- $("#start_time").val(toDate(start).utc().format())
85
- $("#end_time").val(toDate(end).endOf("day").utc().format())
96
+ startTime.value = toDate(start).utc().format()
97
+ endTime.value = toDate(end).endOf("day").utc().format()
98
+ }
99
+
100
+ function applyDateRangePicker(picker) {
101
+ setTimeInputs(picker.startDate, picker.endDate)
102
+ reportrange.querySelector(".item").textContent = toDate(picker.startDate).format("MMMM D, YYYY") + " - " + toDate(picker.endDate).format("MMMM D, YYYY")
86
103
  }
87
104
 
88
- $("#reportrange").daterangepicker(
105
+ const picker = new DateRangePicker(reportrange,
89
106
  {
90
107
  ranges: {
91
108
  "Today": [dateStr(), dateStr()],
@@ -100,28 +117,26 @@
100
117
  opens: "right",
101
118
  alwaysShowCalendars: true
102
119
  },
103
- function(start, end) {
120
+ function (start, end) {
104
121
  setTimeInputs(start, end)
105
- submitIfCompleted($("#start_time").closest("form"))
122
+ submitIfCompleted(startTime.closest("form"))
106
123
  }
107
- ).on("apply.daterangepicker", function(ev, picker) {
108
- setTimeInputs(picker.startDate, picker.endDate)
109
- $("#reportrange span").html(toDate(picker.startDate).format("MMMM D, YYYY") + " - " + toDate(picker.endDate).format("MMMM D, YYYY"))
124
+ )
125
+ reportrange.addEventListener("apply.daterangepicker", function () {
126
+ applyDateRangePicker(picker)
110
127
  })
111
128
 
112
- if ($("#start_time").val().length > 0) {
113
- var picker = $("#reportrange").data("daterangepicker")
114
- picker.setStartDate(moment.tz($("#start_time").val(), timeZone))
115
- picker.setEndDate(moment.tz($("#end_time").val(), timeZone))
116
- $("#reportrange").trigger("apply.daterangepicker", picker)
129
+ if (startTime.value.length > 0) {
130
+ picker.setStartDate(moment.tz(startTime.value, timeZone))
131
+ picker.setEndDate(moment.tz(endTime.value, timeZone))
132
+ applyDateRangePicker(picker)
117
133
  } else {
118
- var picker = $("#reportrange").data("daterangepicker")
119
- $("#reportrange").trigger("apply.daterangepicker", picker)
120
- submitIfCompleted($("#start_time").closest("form"))
134
+ applyDateRangePicker(picker)
135
+ submitIfCompleted(startTime.closest("form"))
121
136
  }
122
137
  <% end %>
123
138
  <% end %>
124
139
 
125
140
  <input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
126
- </form>
141
+ <% end %>
127
142
  <% end %>
@@ -1,3 +1,14 @@
1
+ <%= javascript_tag nonce: true do %>
2
+ function createSelect(id, options) {
3
+ const element = document.getElementById(id)
4
+ const onChange = function () {
5
+ this.blur()
6
+ }
7
+ new TomSelect(element, {onChange, ...options})
8
+ show(element.closest(".hide"))
9
+ }
10
+ <% end %>
11
+
1
12
  <%= form_for @check, html: {class: "small-form"} do |f| %>
2
13
  <% unless @check.respond_to?(:check_type) || @check.respond_to?(:invert) %>
3
14
  <p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
@@ -15,8 +26,7 @@
15
26
  <%= javascript_tag nonce: true do %>
16
27
  <%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
17
28
  <%= blazer_js_var "items", [@check.query_id].compact %>
18
-
19
- $("#check_query_id").selectize({options: queries, items: items, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
29
+ createSelect("check_query_id", {options: queries, items: items, highlight: false, maxOptions: 100})
20
30
  <% end %>
21
31
  </div>
22
32
 
@@ -24,12 +34,11 @@
24
34
  <div class="form-group">
25
35
  <%= f.label :check_type, "Alert if" %>
26
36
  <div class="hide">
27
- <% check_options = [["Any results (bad data)", "bad_data"], ["No results (missing data)", "missing_data"]] %>
28
- <% check_options << ["Anomaly (most recent data point)", "anomaly"] if Blazer.anomaly_checks %>
37
+ <% check_options = Blazer.check_types.reject { |k, _| k == "anomaly" && !Blazer.anomaly_checks }.map { |k, v| [v[:label], k] } %>
29
38
  <%= f.select :check_type, check_options %>
30
39
  </div>
31
40
  <%= javascript_tag nonce: true do %>
32
- $("#check_check_type").selectize({}).parent().removeClass("hide");
41
+ createSelect("check_check_type", {})
33
42
  <% end %>
34
43
  </div>
35
44
  <% elsif @check.respond_to?(:invert) %>
@@ -39,7 +48,7 @@
39
48
  <%= f.select :invert, [["Any results (bad data)", false], ["No results (missing data)", true]] %>
40
49
  </div>
41
50
  <%= javascript_tag nonce: true do %>
42
- $("#check_invert").selectize({}).parent().removeClass("hide");
51
+ createSelect("check_invert", {})
43
52
  <% end %>
44
53
  </div>
45
54
  <% end %>
@@ -51,24 +60,19 @@
51
60
  <%= f.select :schedule, Blazer.check_schedules.map { |v| [v, v] } %>
52
61
  </div>
53
62
  <%= javascript_tag nonce: true do %>
54
- $("#check_schedule").selectize({}).parent().removeClass("hide");
63
+ createSelect("check_schedule", {})
55
64
  <% end %>
56
65
  </div>
57
66
  <% end %>
58
67
 
59
- <div class="form-group">
60
- <%= f.label :emails %>
61
- <%= f.text_field :emails, placeholder: "Optional, comma separated", class: "form-control" %>
62
- </div>
63
-
64
- <% if Blazer.slack? %>
68
+ <% Blazer.notifiers.flat_map(&:fields).each do |field| %>
65
69
  <div class="form-group">
66
- <%= f.label :slack_channels %>
67
- <%= f.text_field :slack_channels, placeholder: "Optional, comma separated", class: "form-control" %>
70
+ <%= f.label field %>
71
+ <%= f.text_field field, placeholder: "Optional, comma separated", class: "form-control" %>
68
72
  </div>
69
73
  <% end %>
70
74
 
71
- <p class="text-muted">Emails <%= Blazer.slack? ? "and Slack notifications " : nil %>are sent when a check starts failing, and when it starts passing again.
75
+ <p class="text-muted">Notifications are sent when a check starts failing, and when it starts passing again.
72
76
  <p>
73
77
  <% if @check.persisted? %>
74
78
  <%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
@@ -1,7 +1,7 @@
1
1
  <% blazer_title "Checks" %>
2
2
 
3
3
  <div id="header">
4
- <div class="pull-right" style="line-height: 34px;">
4
+ <div class="pull-right header-div">
5
5
  <div class="btn-group">
6
6
  <%= link_to "New Check", new_check_path, class: "btn btn-info" %>
7
7
  <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -20,17 +20,17 @@
20
20
  </div>
21
21
  </div>
22
22
 
23
- <input id="search" type="text" placeholder="Start typing a query or state" style="width: 300px; display: inline-block;" class="search form-control" />
23
+ <input id="search" type="text" placeholder="Start typing a query or state" class="search form-control" autofocus />
24
24
  </div>
25
25
 
26
26
  <table id="checks" class="table">
27
27
  <thead>
28
28
  <tr>
29
29
  <th>Query</th>
30
- <th style="width: 10%;">State</th>
31
- <th style="width: 10%;">Run</th>
32
- <th style="width: 20%;">Notify</th>
33
- <th style="width: 15%;"></th>
30
+ <th class="state-header">State</th>
31
+ <th class="run-header">Run</th>
32
+ <th class="notify-header">Notify</th>
33
+ <th class="actions-header"></th>
34
34
  </tr>
35
35
  </thead>
36
36
  <tbody>
@@ -39,21 +39,18 @@
39
39
  <td><%= link_to check.query.name, check.query %> <span class="text-muted"><%= check.try(:check_type).to_s.gsub("_", " ") %></span></td>
40
40
  <td>
41
41
  <% if check.state %>
42
- <small class="check-state <%= check.state.parameterize.gsub("-", "_") %>"><%= check.state.upcase %></small>
42
+ <%= tag.small check.state.upcase, class: ["check-state", check.state.parameterize.gsub("-", "_")] %>
43
43
  <% end %>
44
44
  </td>
45
45
  <td><%= check.schedule if check.respond_to?(:schedule) %></td>
46
46
  <td>
47
- <ul class="list-unstyled" style="margin-bottom: 0; word-break: break-all;">
48
- <% check.split_emails.each do |email| %>
49
- <li><%= email %></li>
50
- <% end %>
51
- <% check.split_slack_channels.each do |channel| %>
52
- <li><%= channel %></li>
47
+ <ul class="list-unstyled notifiers">
48
+ <% Blazer.notifiers.flat_map { |n| n.notify_list(check) }.each do |item| %>
49
+ <li><%= item %></li>
53
50
  <% end %>
54
51
  </ul>
55
52
  </td>
56
- <td style="text-align: right; padding: 1px;">
53
+ <td class="check-actions">
57
54
  <%= link_to "Edit", edit_check_path(check), class: "btn btn-info" %>
58
55
  <%= button_to "Run Now", refresh_query_path(check.query), class: "btn btn-primary" %>
59
56
  </td>
@@ -63,10 +60,10 @@
63
60
  </table>
64
61
 
65
62
  <%= javascript_tag nonce: true do %>
66
- $("#search").on("keyup", function() {
67
- var value = $(this).val().toLowerCase()
68
- $("#checks tbody tr").filter( function() {
69
- $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
70
- })
71
- }).focus()
63
+ document.getElementById("search").addEventListener("input", function () {
64
+ const value = this.value.toLowerCase()
65
+ for (const check of document.querySelectorAll("#checks tbody tr")) {
66
+ toggle(check, check.textContent.toLowerCase().includes(value))
67
+ }
68
+ })
72
69
  <% end %>
@@ -7,21 +7,15 @@
7
7
  <%= f.label :name %>
8
8
  <%= f.text_field :name, class: "form-control" %>
9
9
  </div>
10
- <div class="form-group" v-show="queries.length">
10
+ <div class="form-group hide" id="queries-group">
11
11
  <%= f.label :charts %>
12
- <ul id="queries" class="list-group">
13
- <li class="list-group-item" v-for="(query, index) in queries" :key="query.id" v-cloak>
14
- <span class="glyphicon glyphicon-remove" aria-hidden="true" v-on:click="remove(index)"></span>
15
- {{ query.name }}
16
- <input type="hidden" name="query_ids[]" :value="query.id">
17
- </li>
18
- </ul>
12
+ <ul id="queries" class="list-group"></ul>
19
13
  </div>
20
- <div class="form-group" v-cloak>
14
+ <div class="form-group hide">
21
15
  <%= f.label :query_id, "Add Chart" %>
22
16
  <%= select_tag :query_id, nil, {include_blank: true, placeholder: "Select chart"} %>
23
17
  </div>
24
- <p style="padding-bottom: 140px;" v-cloak>
18
+ <p class="hide" id="dashboard-actions">
25
19
  <% if @dashboard.persisted? %>
26
20
  <%= link_to "Delete", dashboard_path(@dashboard), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
27
21
  <% end %>
@@ -32,51 +26,72 @@
32
26
 
33
27
  <%= javascript_tag nonce: true do %>
34
28
  <%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
35
- <%= blazer_js_var "dashboardQueries", @queries || @dashboard.dashboard_queries.order(:position).map(&:query) %>
29
+ <%= blazer_js_var "dashboardQueries", (@queries || @dashboard.dashboard_queries.order(:position).map(&:query)).as_json(only: [:id, :name]) %>
36
30
 
37
- var app = Vue.createApp({
38
- data: function() {
39
- return {
40
- queries: dashboardQueries
41
- }
42
- },
43
- methods: {
44
- remove: function(index) {
45
- this.queries.splice(index, 1)
31
+ const queriesList = document.getElementById("queries")
32
+ const queriesGroup = document.getElementById("queries-group")
33
+ const querySelect = document.getElementById("query_id")
34
+ const actions = document.getElementById("dashboard-actions")
35
+
36
+ function addQuery(query) {
37
+ const li = document.createElement("li")
38
+ li.classList.add("list-group-item")
39
+ li.setAttribute("data-query-id", query.id)
40
+
41
+ const span = document.createElement("span")
42
+ span.classList.add("glyphicon", "glyphicon-remove")
43
+ span.setAttribute("aria-hidden", "true")
44
+
45
+ const input = document.createElement("input")
46
+ input.setAttribute("type", "hidden")
47
+ input.setAttribute("name", "query_ids[]")
48
+ input.setAttribute("value", query.id)
49
+
50
+ li.append(span, query.name, input)
51
+ queriesList.append(li)
52
+ show(queriesGroup)
53
+ }
54
+
55
+ for (const query of dashboardQueries) {
56
+ addQuery(query)
57
+ }
58
+
59
+ queriesList.addEventListener("click", function (e) {
60
+ const target = e.target.closest(".glyphicon-remove")
61
+ if (target) {
62
+ target.closest("li").remove()
63
+
64
+ if (queriesList.querySelectorAll("li").length == 0) {
65
+ hide(queriesGroup)
46
66
  }
47
- },
48
- mounted: function() {
49
- var app = this
50
- $("#query_id").selectize({
51
- options: queries,
52
- highlight: false,
53
- maxOptions: 100,
54
- onChange: function(val) {
55
- if (val) {
56
- var item = this.getItem(val)
67
+ }
68
+ })
57
69
 
58
- // if duplicate query is added, remove the first one
59
- for (var i = 0; i < app.queries.length; i++) {
60
- if (app.queries[i].id == val) {
61
- app.queries.splice(i, 1)
62
- break
63
- }
64
- }
70
+ new TomSelect(querySelect, {
71
+ options: queries,
72
+ highlight: false,
73
+ maxOptions: 100,
74
+ onChange: function (id) {
75
+ if (id) {
76
+ const item = this.getItem(id)
65
77
 
66
- app.queries.push({id: val, name: item.text()})
67
- this.setValue("")
78
+ // if duplicate query is added, remove the first one
79
+ for (const query of queriesList.querySelectorAll("li")) {
80
+ if (query.getAttribute("data-query-id") == id) {
81
+ query.remove()
82
+ break
68
83
  }
69
84
  }
70
- })
71
- }
72
- })
73
- app.config.compilerOptions.whitespace = "preserve"
74
- app.mount("#app")
75
85
 
76
- Sortable.create($("#queries").get(0), {
77
- onEnd: function(e) {
78
- var app = window.app._component.data()
79
- app.queries.splice(e.newIndex, 0, app.queries.splice(e.oldIndex, 1)[0])
86
+ addQuery({id: id, name: item.textContent})
87
+ this.setValue("")
88
+ this.blur()
89
+ }
80
90
  }
81
91
  })
92
+
93
+ show(querySelect.closest(".hide"))
94
+ show(actions)
95
+
96
+ Sortable.create(queriesList)
82
97
  <% end %>