finery 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +426 -0
  3. data/CONTRIBUTING.md +49 -0
  4. data/LICENSE.txt +25 -0
  5. data/README.md +1144 -0
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  11. data/app/assets/images/blazer/favicon.png +0 -0
  12. data/app/assets/javascripts/blazer/Sortable.js +3709 -0
  13. data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
  14. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
  15. data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
  16. data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
  17. data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
  18. data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
  19. data/app/assets/javascripts/blazer/ace.js +6 -0
  20. data/app/assets/javascripts/blazer/application.js +87 -0
  21. data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
  22. data/app/assets/javascripts/blazer/chart.umd.js +13 -0
  23. data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
  24. data/app/assets/javascripts/blazer/chartjs-plugin-annotation.min.js +7 -0
  25. data/app/assets/javascripts/blazer/chartkick.js +2570 -0
  26. data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
  27. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  28. data/app/assets/javascripts/blazer/highlight.min.js +466 -0
  29. data/app/assets/javascripts/blazer/jquery.js +10872 -0
  30. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  31. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  32. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
  33. data/app/assets/javascripts/blazer/moment.js +5685 -0
  34. data/app/assets/javascripts/blazer/queries.js +130 -0
  35. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  36. data/app/assets/javascripts/blazer/routes.js +26 -0
  37. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  38. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  40. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  41. data/app/assets/stylesheets/blazer/application.css +243 -0
  42. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  43. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  44. data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
  45. data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
  46. data/app/assets/stylesheets/blazer/github.css +125 -0
  47. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  48. data/app/controllers/blazer/base_controller.rb +133 -0
  49. data/app/controllers/blazer/checks_controller.rb +56 -0
  50. data/app/controllers/blazer/dashboards_controller.rb +99 -0
  51. data/app/controllers/blazer/queries_controller.rb +468 -0
  52. data/app/controllers/blazer/uploads_controller.rb +147 -0
  53. data/app/helpers/blazer/base_helper.rb +83 -0
  54. data/app/models/blazer/audit.rb +6 -0
  55. data/app/models/blazer/check.rb +104 -0
  56. data/app/models/blazer/connection.rb +5 -0
  57. data/app/models/blazer/dashboard.rb +17 -0
  58. data/app/models/blazer/dashboard_query.rb +9 -0
  59. data/app/models/blazer/query.rb +42 -0
  60. data/app/models/blazer/record.rb +5 -0
  61. data/app/models/blazer/upload.rb +11 -0
  62. data/app/models/blazer/uploads_connection.rb +7 -0
  63. data/app/views/blazer/_nav.html.erb +18 -0
  64. data/app/views/blazer/_variables.html.erb +127 -0
  65. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  66. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  67. data/app/views/blazer/checks/_form.html.erb +79 -0
  68. data/app/views/blazer/checks/edit.html.erb +3 -0
  69. data/app/views/blazer/checks/index.html.erb +72 -0
  70. data/app/views/blazer/checks/new.html.erb +3 -0
  71. data/app/views/blazer/dashboards/_form.html.erb +82 -0
  72. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  73. data/app/views/blazer/dashboards/new.html.erb +3 -0
  74. data/app/views/blazer/dashboards/show.html.erb +53 -0
  75. data/app/views/blazer/queries/_caching.html.erb +16 -0
  76. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  77. data/app/views/blazer/queries/_form.html.erb +255 -0
  78. data/app/views/blazer/queries/docs.html.erb +147 -0
  79. data/app/views/blazer/queries/edit.html.erb +2 -0
  80. data/app/views/blazer/queries/home.html.erb +169 -0
  81. data/app/views/blazer/queries/new.html.erb +2 -0
  82. data/app/views/blazer/queries/run.html.erb +188 -0
  83. data/app/views/blazer/queries/schema.html.erb +55 -0
  84. data/app/views/blazer/queries/show.html.erb +72 -0
  85. data/app/views/blazer/uploads/_form.html.erb +27 -0
  86. data/app/views/blazer/uploads/edit.html.erb +3 -0
  87. data/app/views/blazer/uploads/index.html.erb +55 -0
  88. data/app/views/blazer/uploads/new.html.erb +3 -0
  89. data/app/views/layouts/blazer/application.html.erb +25 -0
  90. data/config/routes.rb +25 -0
  91. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  92. data/lib/blazer/adapters/base_adapter.rb +76 -0
  93. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  94. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  95. data/lib/blazer/adapters/clickhouse_adapter.rb +84 -0
  96. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  97. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  98. data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
  99. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  100. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  101. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  102. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  103. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  104. data/lib/blazer/adapters/presto_adapter.rb +54 -0
  105. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  106. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  107. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  108. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  109. data/lib/blazer/adapters/sql_adapter.rb +324 -0
  110. data/lib/blazer/adapters.rb +18 -0
  111. data/lib/blazer/annotations.rb +47 -0
  112. data/lib/blazer/anomaly_detectors.rb +22 -0
  113. data/lib/blazer/check_mailer.rb +27 -0
  114. data/lib/blazer/data_source.rb +270 -0
  115. data/lib/blazer/engine.rb +42 -0
  116. data/lib/blazer/forecasters.rb +7 -0
  117. data/lib/blazer/result.rb +178 -0
  118. data/lib/blazer/result_cache.rb +71 -0
  119. data/lib/blazer/run_statement.rb +44 -0
  120. data/lib/blazer/run_statement_job.rb +20 -0
  121. data/lib/blazer/slack_notifier.rb +94 -0
  122. data/lib/blazer/statement.rb +77 -0
  123. data/lib/blazer/version.rb +3 -0
  124. data/lib/blazer.rb +286 -0
  125. data/lib/finery.rb +3 -0
  126. data/lib/generators/blazer/install_generator.rb +22 -0
  127. data/lib/generators/blazer/templates/config.yml.tt +83 -0
  128. data/lib/generators/blazer/templates/install.rb.tt +47 -0
  129. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  130. data/lib/generators/blazer/uploads_generator.rb +18 -0
  131. data/lib/tasks/blazer.rake +20 -0
  132. data/lib/tasks/finery.rake +20 -0
  133. data/licenses/LICENSE-ace.txt +24 -0
  134. data/licenses/LICENSE-bootstrap.txt +21 -0
  135. data/licenses/LICENSE-chart.js.txt +9 -0
  136. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  137. data/licenses/LICENSE-chartkick.js.txt +22 -0
  138. data/licenses/LICENSE-date-fns.txt +21 -0
  139. data/licenses/LICENSE-daterangepicker.txt +21 -0
  140. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  141. data/licenses/LICENSE-highlight.js.txt +29 -0
  142. data/licenses/LICENSE-jquery.txt +20 -0
  143. data/licenses/LICENSE-kurkle-color.txt +9 -0
  144. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  145. data/licenses/LICENSE-moment-timezone.txt +20 -0
  146. data/licenses/LICENSE-moment.txt +22 -0
  147. data/licenses/LICENSE-rails-ujs.txt +20 -0
  148. data/licenses/LICENSE-selectize.txt +202 -0
  149. data/licenses/LICENSE-sortable.txt +21 -0
  150. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  151. data/licenses/LICENSE-stupidtable.txt +19 -0
  152. data/licenses/LICENSE-vue.txt +21 -0
  153. metadata +250 -0
@@ -0,0 +1,104 @@
1
+ module Blazer
2
+ class Check < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+ belongs_to :query
5
+
6
+ validates :query_id, presence: true
7
+ validate :validate_emails
8
+ validate :validate_variables, if: -> { query_id_changed? }
9
+
10
+ before_validation :set_state
11
+ before_validation :fix_emails
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
+ 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
36
+
37
+ self.state =
38
+ if result.timed_out?
39
+ "timed out"
40
+ 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"
53
+ else
54
+ check_type == "missing_data" ? "failing" : "passing"
55
+ end
56
+
57
+ self.last_run_at = Time.now if respond_to?(:last_run_at=)
58
+ self.message = message if respond_to?(:message=)
59
+
60
+ if respond_to?(:timeouts=)
61
+ if result.timed_out?
62
+ self.timeouts += 1
63
+ self.state = "disabled" if timeouts >= 3
64
+ else
65
+ self.timeouts = 0
66
+ end
67
+ end
68
+
69
+ # do not notify on creation, except when not passing
70
+ 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)
73
+ end
74
+ save! if changed?
75
+ end
76
+
77
+ private
78
+
79
+ def set_state
80
+ self.state ||= "new"
81
+ end
82
+
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
90
+ end
91
+
92
+ def validate_emails
93
+ unless split_emails.all? { |e| e =~ /\A\S+@\S+\.\S+\z/ }
94
+ errors.add(:base, "Invalid emails")
95
+ end
96
+ end
97
+
98
+ def validate_variables
99
+ if query.variables.any?
100
+ errors.add(:base, "Query can't have variables")
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,5 @@
1
+ module Blazer
2
+ class Connection < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ module Blazer
2
+ class Dashboard < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+ has_many :dashboard_queries, dependent: :destroy
5
+ has_many :queries, through: :dashboard_queries
6
+
7
+ validates :name, presence: true
8
+
9
+ def variables
10
+ queries.flat_map { |q| q.variables }.uniq
11
+ end
12
+
13
+ def to_param
14
+ [id, name.gsub("'", "").parameterize].join("-")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module Blazer
2
+ class DashboardQuery < Record
3
+ belongs_to :dashboard
4
+ belongs_to :query
5
+
6
+ validates :dashboard_id, presence: true
7
+ validates :query_id, presence: true
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ module Blazer
2
+ class Query < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+ has_many :checks, dependent: :destroy
5
+ has_many :dashboard_queries, dependent: :destroy
6
+ has_many :dashboards, through: :dashboard_queries
7
+ has_many :audits
8
+
9
+ validates :statement, presence: true
10
+
11
+ scope :active, -> { column_names.include?("status") ? where(status: ["active", nil]) : all }
12
+ scope :named, -> { where.not(name: "") }
13
+
14
+ def to_param
15
+ [id, name].compact.join("-").gsub("'", "").parameterize
16
+ end
17
+
18
+ def friendly_name
19
+ name.to_s.sub(/\A[#\*]/, "").gsub(/\[.+\]/, "").strip
20
+ end
21
+
22
+ def editable?(user)
23
+ !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == try(:creator)
24
+ end
25
+
26
+ def variables
27
+ # don't require data_source to be loaded
28
+ variables = Statement.new(statement).variables
29
+ variables += ["cohort_period"] if cohort_analysis?
30
+ variables
31
+ end
32
+
33
+ def cohort_analysis?
34
+ # don't require data_source to be loaded
35
+ Statement.new(statement).cohort_analysis?
36
+ end
37
+
38
+ def statement_object
39
+ Statement.new(statement, data_source)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module Blazer
2
+ class Record < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module Blazer
2
+ class Upload < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+
5
+ validates :table, presence: true, uniqueness: true, format: {with: /\A[a-z0-9_]+\z/, message: "can only contain lowercase letters, numbers, and underscores"}, length: {maximum: 63}
6
+
7
+ def table_name
8
+ Blazer.uploads_table_name(table)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Blazer
2
+ class UploadsConnection < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ establish_connection Blazer.settings["uploads"]["url"] if Blazer.uploads?
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ <div class="btn-group" style="vertical-align: top; margin-right: 5px;">
2
+ <%= link_to "Home", root_path, class: "btn btn-primary" %>
3
+ <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
4
+ <span class="caret"></span>
5
+ <span class="sr-only">Toggle Dropdown</span>
6
+ </button>
7
+ <ul class="dropdown-menu">
8
+ <li><%= link_to "Checks", checks_path %></li>
9
+ <% if Blazer.uploads? %>
10
+ <li><%= link_to "Uploads", uploads_path %></li>
11
+ <% end %>
12
+ <li role="separator" class="divider"></li>
13
+ <li><%= link_to "New Query", new_query_path %></li>
14
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
15
+ <% check_params = @query ? {query_id: @query.id} : {} %>
16
+ <li><%= link_to "New Check", new_check_path(check_params) %></li>
17
+ </ul>
18
+ </div>
@@ -0,0 +1,127 @@
1
+ <% if @bind_vars.any? %>
2
+ <% var_params = request.query_parameters %>
3
+ <script>
4
+ <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
5
+ var now = moment.tz(timeZone)
6
+ var format = "YYYY-MM-DD"
7
+
8
+ function toDate(time) {
9
+ return moment.tz(time.format(format), timeZone)
10
+ }
11
+ </script>
12
+ <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
13
+ <% date_vars = ["start_time", "end_time"] %>
14
+ <% if (date_vars - @bind_vars).empty? %>
15
+ <% @bind_vars = @bind_vars - date_vars %>
16
+ <% else %>
17
+ <% date_vars = nil %>
18
+ <% end %>
19
+
20
+ <% @bind_vars.each_with_index do |var, i| %>
21
+ <%= label_tag var, var %>
22
+ <% if (data = @smart_vars[var]) %>
23
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
24
+ <script>
25
+ $("#<%= var %>").selectize({
26
+ create: true
27
+ });
28
+ </script>
29
+ <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
30
+ <%= hidden_field_tag var, var_params[var] %>
31
+
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>
36
+ </div>
37
+
38
+ <script>
39
+ (function() {
40
+ var input = $("#<%= var %>")
41
+ var datePicker = $("#<%= var %>-select")
42
+ datePicker.daterangepicker({
43
+ singleDatePicker: true,
44
+ locale: {format: format},
45
+ autoUpdateInput: false,
46
+ autoApply: true,
47
+ startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
48
+ })
49
+ // 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"))
54
+ })
55
+ if (input.val().length > 0) {
56
+ var picker = datePicker.data("daterangepicker")
57
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
58
+ }
59
+ })()
60
+ </script>
61
+ <% else %>
62
+ <%= text_field_tag var, var_params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !var_params[var], class: "form-control" %>
63
+ <% end %>
64
+ <% end %>
65
+
66
+ <% if date_vars %>
67
+ <% date_vars.each do |var| %>
68
+ <%= hidden_field_tag var, var_params[var] %>
69
+ <% end %>
70
+
71
+ <%= 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>
75
+ </div>
76
+ </div>
77
+
78
+ <script>
79
+ function dateStr(daysAgo) {
80
+ return now.clone().subtract(daysAgo || 0, "days").format(format)
81
+ }
82
+
83
+ function setTimeInputs(start, end) {
84
+ $("#start_time").val(toDate(start).utc().format())
85
+ $("#end_time").val(toDate(end).endOf("day").utc().format())
86
+ }
87
+
88
+ $("#reportrange").daterangepicker(
89
+ {
90
+ ranges: {
91
+ "Today": [dateStr(), dateStr()],
92
+ "Last 7 Days": [dateStr(6), dateStr()],
93
+ "Last 30 Days": [dateStr(29), dateStr()]
94
+ },
95
+ locale: {
96
+ format: format
97
+ },
98
+ startDate: dateStr(29),
99
+ endDate: dateStr(),
100
+ opens: "right",
101
+ alwaysShowCalendars: true
102
+ },
103
+ function(start, end) {
104
+ setTimeInputs(start, end)
105
+ submitIfCompleted($("#start_time").closest("form"))
106
+ }
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"))
110
+ })
111
+
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)
117
+ } else {
118
+ var picker = $("#reportrange").data("daterangepicker")
119
+ $("#reportrange").trigger("apply.daterangepicker", picker)
120
+ submitIfCompleted($("#start_time").closest("form"))
121
+ }
122
+ </script>
123
+ <% end %>
124
+
125
+ <input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
126
+ </form>
127
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <ul>
2
+ <% @checks.each do |check| %>
3
+ <%# check queries shouldn't have variables, but in any case, don't pass them to url helpers %>
4
+ <li><%= link_to check.query.name, query_url(check.query_id) %> <%= check.state %></li>
5
+ <% end %>
6
+ </ul>
7
+ <p><%= link_to "Manage checks", checks_url %></p>
@@ -0,0 +1,48 @@
1
+ <html>
2
+ <head>
3
+ </head>
4
+ <body style="font-family: 'Helvetica Neue', Arial, Helvetica; font-size: 14px; color: #333;">
5
+ <%# check queries shouldn't have variables, but in any case, don't pass them to url helpers %>
6
+ <p><%= link_to "View", query_url(@check.query_id) %></p>
7
+ <% if @error %>
8
+ <p><%= @error %></p>
9
+ <% elsif @rows_count > 0 && @check_type == "bad_data" %>
10
+ <p>
11
+ <% if @rows_count <= 10 %>
12
+ <%= pluralize(@rows_count, "row") %>
13
+ <% else %>
14
+ Showing 10 of <%= @rows_count %> rows
15
+ <% end %>
16
+ </p>
17
+ <table style="width: 100%; border-spacing: 0; border-collapse: collapse;">
18
+ <thead>
19
+ <tr>
20
+ <% @columns.first(5).each do |column| %>
21
+ <th style="padding: 8px; line-height: 1.4; text-align: left; vertical-align: bottom; border-bottom: 2px solid #ddd; width: <%= (100 / @columns.size).round(2) %>%;">
22
+ <%= column %>
23
+ </th>
24
+ <% end %>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% @rows.first(10).each do |row| %>
29
+ <tr>
30
+ <% @columns.first(5).each_with_index do |column, i| %>
31
+ <td style="padding: 8px; line-height: 1.4; vertical-align: top; border-top: 1px solid #ddd;">
32
+ <% value = row[i] %>
33
+ <% if @column_types[i] == "time" && value.to_s.length > 10 %>
34
+ <% value = Time.parse(value).in_time_zone(Blazer.time_zone) rescue value %>
35
+ <% end %>
36
+ <%= value %>
37
+ </td>
38
+ <% end %>
39
+ </tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
43
+ <% if @columns.size > 5 %>
44
+ <p style="color: #999;">Only first 5 columns shown</p>
45
+ <% end %>
46
+ <% end %>
47
+ </body>
48
+ </html>
@@ -0,0 +1,79 @@
1
+ <%= form_for @check, html: {class: "small-form"} do |f| %>
2
+ <% unless @check.respond_to?(:check_type) || @check.respond_to?(:invert) %>
3
+ <p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
4
+ <% end %>
5
+
6
+ <% if @check.errors.any? %>
7
+ <div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
8
+ <% end %>
9
+
10
+ <div class="form-group">
11
+ <%= f.label :query_id, "Query" %>
12
+ <div class="hide">
13
+ <%= f.select :query_id, [], {include_blank: true} %>
14
+ </div>
15
+ <script>
16
+ <%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
17
+ <%= 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");
20
+ </script>
21
+ </div>
22
+
23
+ <% if @check.respond_to?(:check_type) %>
24
+ <div class="form-group">
25
+ <%= f.label :check_type, "Alert if" %>
26
+ <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 %>
29
+ <%= f.select :check_type, check_options %>
30
+ </div>
31
+ <script>
32
+ $("#check_check_type").selectize({}).parent().removeClass("hide");
33
+ </script>
34
+ </div>
35
+ <% elsif @check.respond_to?(:invert) %>
36
+ <div class="form-group">
37
+ <%= f.label :invert, "Fails if" %>
38
+ <div class="hide">
39
+ <%= f.select :invert, [["Any results (bad data)", false], ["No results (missing data)", true]] %>
40
+ </div>
41
+ <script>
42
+ $("#check_invert").selectize({}).parent().removeClass("hide");
43
+ </script>
44
+ </div>
45
+ <% end %>
46
+
47
+ <% if @check.respond_to?(:schedule) && Blazer.check_schedules %>
48
+ <div class="form-group">
49
+ <%= f.label :schedule, "Run every" %>
50
+ <div class="hide">
51
+ <%= f.select :schedule, Blazer.check_schedules.map { |v| [v, v] } %>
52
+ </div>
53
+ <script>
54
+ $("#check_schedule").selectize({}).parent().removeClass("hide");
55
+ </script>
56
+ </div>
57
+ <% end %>
58
+
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? %>
65
+ <div class="form-group">
66
+ <%= f.label :slack_channels %>
67
+ <%= f.text_field :slack_channels, placeholder: "Optional, comma separated", class: "form-control" %>
68
+ </div>
69
+ <% end %>
70
+
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.
72
+ <p>
73
+ <% if @check.persisted? %>
74
+ <%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
75
+ <% end %>
76
+ <%= f.submit "Save", class: "btn btn-success" %>
77
+ <%= link_to "Back", :back, class: "btn btn-link" %>
78
+ </p>
79
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "Edit Check" %>
2
+
3
+ <%= render partial: "form" %>
@@ -0,0 +1,72 @@
1
+ <% blazer_title "Checks" %>
2
+
3
+ <div id="header">
4
+ <div class="pull-right" style="line-height: 34px;">
5
+ <div class="btn-group">
6
+ <%= link_to "New Check", new_check_path, class: "btn btn-info" %>
7
+ <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
8
+ <span class="caret"></span>
9
+ <span class="sr-only">Toggle Dropdown</span>
10
+ </button>
11
+ <ul class="dropdown-menu">
12
+ <li><%= link_to "Home", root_path %></li>
13
+ <% if Blazer.uploads? %>
14
+ <li><%= link_to "Uploads", uploads_path %></li>
15
+ <% end %>
16
+ <li role="separator" class="divider"></li>
17
+ <li><%= link_to "New Query", new_query_path %></li>
18
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
19
+ </ul>
20
+ </div>
21
+ </div>
22
+
23
+ <input id="search" type="text" placeholder="Start typing a query or state" style="width: 300px; display: inline-block;" class="search form-control" />
24
+ </div>
25
+
26
+ <table id="checks" class="table">
27
+ <thead>
28
+ <tr>
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>
34
+ </tr>
35
+ </thead>
36
+ <tbody>
37
+ <% @checks.each do |check| %>
38
+ <tr>
39
+ <td><%= link_to check.query.name, check.query %> <span class="text-muted"><%= check.try(:check_type).to_s.gsub("_", " ") %></span></td>
40
+ <td>
41
+ <% if check.state %>
42
+ <small class="check-state <%= check.state.parameterize.gsub("-", "_") %>"><%= check.state.upcase %></small>
43
+ <% end %>
44
+ </td>
45
+ <td><%= check.schedule if check.respond_to?(:schedule) %></td>
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>
53
+ <% end %>
54
+ </ul>
55
+ </td>
56
+ <td style="text-align: right; padding: 1px;">
57
+ <%= link_to "Edit", edit_check_path(check), class: "btn btn-info" %>
58
+ <%= button_to "Run Now", refresh_query_path(check.query), class: "btn btn-primary" %>
59
+ </td>
60
+ </tr>
61
+ <% end %>
62
+ </tbody>
63
+ </table>
64
+
65
+ <script>
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()
72
+ </script>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "New Check" %>
2
+
3
+ <%= render partial: "form" %>
@@ -0,0 +1,82 @@
1
+ <%= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, params: variable_params(@dashboard)) : dashboards_path(params: variable_params(@dashboard))), html: {id: "app", class: "small-form"} do |f| %>
2
+ <% if @dashboard.errors.any? %>
3
+ <div class="alert alert-danger"><%= @dashboard.errors.full_messages.first %></div>
4
+ <% end %>
5
+
6
+ <div class="form-group">
7
+ <%= f.label :name %>
8
+ <%= f.text_field :name, class: "form-control" %>
9
+ </div>
10
+ <div class="form-group" v-show="queries.length">
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>
19
+ </div>
20
+ <div class="form-group" v-cloak>
21
+ <%= f.label :query_id, "Add Chart" %>
22
+ <%= select_tag :query_id, nil, {include_blank: true, placeholder: "Select chart"} %>
23
+ </div>
24
+ <p style="padding-bottom: 140px;" v-cloak>
25
+ <% if @dashboard.persisted? %>
26
+ <%= link_to "Delete", dashboard_path(@dashboard), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
27
+ <% end %>
28
+ <%= f.submit "Save", class: "btn btn-success" %>
29
+ <%= link_to "Back", :back, class: "btn btn-link" %>
30
+ </p>
31
+ <% end %>
32
+
33
+ <script>
34
+ <%= 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) %>
36
+
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)
46
+ }
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)
57
+
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
+ }
65
+
66
+ app.queries.push({id: val, name: item.text()})
67
+ this.setValue("")
68
+ }
69
+ }
70
+ })
71
+ }
72
+ })
73
+ app.config.compilerOptions.whitespace = "preserve"
74
+ app.mount("#app")
75
+
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])
80
+ }
81
+ })
82
+ </script>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "Edit Dashboard" %>
2
+
3
+ <%= render partial: "form" %>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "New Dashboard" %>
2
+
3
+ <%= render partial: "form" %>