sql-jarvis 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/CHANGELOG.md +228 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +775 -0
- data/Rakefile +1 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
- data/app/assets/javascripts/blazer/Chart.js +14145 -0
- data/app/assets/javascripts/blazer/Sortable.js +1144 -0
- data/app/assets/javascripts/blazer/ace.js +6 -0
- data/app/assets/javascripts/blazer/ace/ace.js +11 -0
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
- data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
- data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
- data/app/assets/javascripts/blazer/application.js +79 -0
- data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
- data/app/assets/javascripts/blazer/chartkick.js +1693 -0
- data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
- data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
- data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
- data/app/assets/javascripts/blazer/jquery.js +10308 -0
- data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
- data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
- data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
- data/app/assets/javascripts/blazer/moment.js +3043 -0
- data/app/assets/javascripts/blazer/queries.js +110 -0
- data/app/assets/javascripts/blazer/routes.js +23 -0
- data/app/assets/javascripts/blazer/selectize.js +3667 -0
- data/app/assets/javascripts/blazer/stupidtable.js +114 -0
- data/app/assets/javascripts/blazer/vue.js +7515 -0
- data/app/assets/stylesheets/blazer/application.css +198 -0
- data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
- data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
- data/app/assets/stylesheets/blazer/github.css +125 -0
- data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
- data/app/controllers/blazer/base_controller.rb +103 -0
- data/app/controllers/blazer/checks_controller.rb +56 -0
- data/app/controllers/blazer/dashboards_controller.rb +105 -0
- data/app/controllers/blazer/queries_controller.rb +325 -0
- data/app/helpers/blazer/base_helper.rb +57 -0
- data/app/mailers/blazer/check_mailer.rb +27 -0
- data/app/models/blazer/audit.rb +6 -0
- data/app/models/blazer/check.rb +95 -0
- data/app/models/blazer/connection.rb +5 -0
- data/app/models/blazer/dashboard.rb +13 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +31 -0
- data/app/models/blazer/record.rb +5 -0
- data/app/views/blazer/_nav.html.erb +16 -0
- data/app/views/blazer/_variables.html.erb +102 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
- data/app/views/blazer/checks/_form.html.erb +71 -0
- data/app/views/blazer/checks/edit.html.erb +1 -0
- data/app/views/blazer/checks/index.html.erb +40 -0
- data/app/views/blazer/checks/new.html.erb +1 -0
- data/app/views/blazer/dashboards/_form.html.erb +76 -0
- data/app/views/blazer/dashboards/edit.html.erb +1 -0
- data/app/views/blazer/dashboards/new.html.erb +1 -0
- data/app/views/blazer/dashboards/show.html.erb +47 -0
- data/app/views/blazer/queries/_form.html.erb +240 -0
- data/app/views/blazer/queries/edit.html.erb +2 -0
- data/app/views/blazer/queries/home.html.erb +152 -0
- data/app/views/blazer/queries/new.html.erb +2 -0
- data/app/views/blazer/queries/run.html.erb +163 -0
- data/app/views/blazer/queries/schema.html.erb +18 -0
- data/app/views/blazer/queries/show.html.erb +73 -0
- data/app/views/layouts/blazer/application.html.erb +24 -0
- data/blazer.gemspec +26 -0
- data/config/routes.rb +16 -0
- data/lib/blazer.rb +185 -0
- data/lib/blazer/adapters/athena_adapter.rb +128 -0
- data/lib/blazer/adapters/base_adapter.rb +53 -0
- data/lib/blazer/adapters/bigquery_adapter.rb +67 -0
- data/lib/blazer/adapters/drill_adapter.rb +28 -0
- data/lib/blazer/adapters/elasticsearch_adapter.rb +49 -0
- data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
- data/lib/blazer/adapters/presto_adapter.rb +45 -0
- data/lib/blazer/adapters/sql_adapter.rb +182 -0
- data/lib/blazer/data_source.rb +193 -0
- data/lib/blazer/detect_anomalies.R +19 -0
- data/lib/blazer/engine.rb +47 -0
- data/lib/blazer/result.rb +170 -0
- data/lib/blazer/run_statement.rb +40 -0
- data/lib/blazer/run_statement_job.rb +21 -0
- data/lib/blazer/version.rb +3 -0
- data/lib/generators/blazer/install_generator.rb +39 -0
- data/lib/generators/blazer/templates/config.yml +62 -0
- data/lib/generators/blazer/templates/install.rb +45 -0
- data/lib/tasks/blazer.rake +10 -0
- metadata +211 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module Blazer
|
2
|
+
module BaseHelper
|
3
|
+
def blazer_title(title = nil)
|
4
|
+
if title
|
5
|
+
content_for(:title) { title }
|
6
|
+
else
|
7
|
+
content_for?(:title) ? content_for(:title) : nil
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
BLAZER_URL_REGEX = /\Ahttps?:\/\/[\S]+\z/
|
12
|
+
BLAZER_IMAGE_EXT = %w[png jpg jpeg gif]
|
13
|
+
|
14
|
+
def blazer_format_value(key, value)
|
15
|
+
if value.is_a?(Integer) && !key.to_s.end_with?("id") && !key.to_s.start_with?("id")
|
16
|
+
number_with_delimiter(value)
|
17
|
+
elsif value =~ BLAZER_URL_REGEX
|
18
|
+
# see if image or link
|
19
|
+
if Blazer.images && (key.include?("image") || BLAZER_IMAGE_EXT.include?(value.split(".").last.split("?").first.try(:downcase)))
|
20
|
+
link_to value, target: "_blank" do
|
21
|
+
image_tag value, referrerpolicy: "no-referrer"
|
22
|
+
end
|
23
|
+
else
|
24
|
+
link_to value, value, target: "_blank"
|
25
|
+
end
|
26
|
+
else
|
27
|
+
value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def blazer_maps?
|
32
|
+
Blazer.mapbox_access_token.present?
|
33
|
+
end
|
34
|
+
|
35
|
+
def blazer_js_var(name, value)
|
36
|
+
"var #{name} = #{blazer_json_escape(value.to_json(root: false))};".html_safe
|
37
|
+
end
|
38
|
+
|
39
|
+
JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
|
40
|
+
JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
|
41
|
+
|
42
|
+
# Prior to version 4.1 of rails double quotes were inadventently removed in json_escape.
|
43
|
+
# This adds the correct json_escape functionality to rails versions < 4.1
|
44
|
+
def blazer_json_escape(s)
|
45
|
+
if Rails::VERSION::STRING < "4.1"
|
46
|
+
result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
|
47
|
+
s.html_safe? ? result.html_safe : result
|
48
|
+
else
|
49
|
+
json_escape(s)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def blazer_series_name(k)
|
54
|
+
k.nil? ? "null" : k.to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Blazer
|
2
|
+
class CheckMailer < ActionMailer::Base
|
3
|
+
include ActionView::Helpers::TextHelper
|
4
|
+
|
5
|
+
default from: Blazer.from_email if Blazer.from_email
|
6
|
+
layout false
|
7
|
+
|
8
|
+
def state_change(check, state, state_was, rows_count, error, columns, rows, column_types, check_type)
|
9
|
+
@check = check
|
10
|
+
@state = state
|
11
|
+
@state_was = state_was
|
12
|
+
@rows_count = rows_count
|
13
|
+
@error = error
|
14
|
+
@columns = columns
|
15
|
+
@rows = rows
|
16
|
+
@column_types = column_types
|
17
|
+
@check_type = check_type
|
18
|
+
mail to: check.emails, reply_to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def failing_checks(email, checks)
|
22
|
+
@checks = checks
|
23
|
+
# add reply_to for mailing lists
|
24
|
+
mail to: email, reply_to: email, subject: "#{pluralize(checks.size, "Check")} Failing"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Check < Record
|
3
|
+
belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(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 update_state(result)
|
18
|
+
check_type =
|
19
|
+
if respond_to?(:check_type)
|
20
|
+
self.check_type
|
21
|
+
elsif respond_to?(:invert)
|
22
|
+
invert ? "missing_data" : "bad_data"
|
23
|
+
else
|
24
|
+
"bad_data"
|
25
|
+
end
|
26
|
+
|
27
|
+
message = result.error
|
28
|
+
|
29
|
+
self.state =
|
30
|
+
if result.timed_out?
|
31
|
+
"timed out"
|
32
|
+
elsif result.error
|
33
|
+
"error"
|
34
|
+
elsif check_type == "anomaly"
|
35
|
+
anomaly, message = result.detect_anomaly
|
36
|
+
if anomaly.nil?
|
37
|
+
"error"
|
38
|
+
elsif anomaly
|
39
|
+
"failing"
|
40
|
+
else
|
41
|
+
"passing"
|
42
|
+
end
|
43
|
+
elsif result.rows.any?
|
44
|
+
check_type == "missing_data" ? "passing" : "failing"
|
45
|
+
else
|
46
|
+
check_type == "missing_data" ? "failing" : "passing"
|
47
|
+
end
|
48
|
+
|
49
|
+
self.last_run_at = Time.now if respond_to?(:last_run_at=)
|
50
|
+
self.message = message if respond_to?(:message=)
|
51
|
+
|
52
|
+
if respond_to?(:timeouts=)
|
53
|
+
if result.timed_out?
|
54
|
+
self.timeouts += 1
|
55
|
+
self.state = "disabled" if timeouts >= 3
|
56
|
+
else
|
57
|
+
self.timeouts = 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# do not notify on creation, except when not passing
|
62
|
+
if (state_was != "new" || state != "passing") && state != state_was && emails.present?
|
63
|
+
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
|
64
|
+
end
|
65
|
+
save! if changed?
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def set_state
|
71
|
+
self.state ||= "new"
|
72
|
+
end
|
73
|
+
|
74
|
+
def fix_emails
|
75
|
+
# some people like doing ; instead of ,
|
76
|
+
# but we know what they mean, so let's fix it
|
77
|
+
# also, some people like to use whitespace
|
78
|
+
if emails.present?
|
79
|
+
self.emails = emails.strip.gsub(/[;\s]/, ",").gsub(/,+/, ", ")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate_emails
|
84
|
+
unless split_emails.all? { |e| e =~ /\A\S+@\S+\.\S+\z/ }
|
85
|
+
errors.add(:base, "Invalid emails")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate_variables
|
90
|
+
if query.variables.any?
|
91
|
+
errors.add(:base, "Query can't have variables")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Dashboard < Record
|
3
|
+
belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(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 to_param
|
10
|
+
[id, name.gsub("'", "").parameterize].join("-")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Query < Record
|
3
|
+
belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(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 :named, -> { where("blazer_queries.name <> ''") }
|
12
|
+
|
13
|
+
def to_param
|
14
|
+
[id, name].compact.join("-").gsub("'", "").parameterize
|
15
|
+
end
|
16
|
+
|
17
|
+
def friendly_name
|
18
|
+
name.to_s.sub(/\A[#\*]/, "").gsub(/\[.+\]/, "").strip
|
19
|
+
end
|
20
|
+
|
21
|
+
def editable?(user)
|
22
|
+
editable = !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == try(:creator)
|
23
|
+
editable &&= Blazer.query_editable.call(self, user) if Blazer.query_editable
|
24
|
+
editable
|
25
|
+
end
|
26
|
+
|
27
|
+
def variables
|
28
|
+
Blazer.extract_vars(statement)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
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 "Dashboards", dashboards_path %></li>
|
9
|
+
<li><%= link_to "Checks", checks_path %></li>
|
10
|
+
<li role="separator" class="divider"></li>
|
11
|
+
<li><%= link_to "New Query", new_query_path %></li>
|
12
|
+
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
13
|
+
<% check_params = @query ? {query_id: @query.id} : {} %>
|
14
|
+
<li><%= link_to "New Check", new_check_path(check_params) %></li>
|
15
|
+
</ul>
|
16
|
+
</div>
|
@@ -0,0 +1,102 @@
|
|
1
|
+
<% if @bind_vars.any? %>
|
2
|
+
<form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 10px;">
|
3
|
+
<% date_vars = ["start_time", "end_time"] %>
|
4
|
+
<% if (date_vars - @bind_vars).empty? %>
|
5
|
+
<% @bind_vars = @bind_vars - date_vars %>
|
6
|
+
<% else %>
|
7
|
+
<% date_vars = nil %>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<% @bind_vars.each_with_index do |var, i| %>
|
11
|
+
<%= label_tag var, var %>
|
12
|
+
<% if (data = @smart_vars[var]) %>
|
13
|
+
<%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
|
14
|
+
<script>
|
15
|
+
$("#<%= var %>").selectize({
|
16
|
+
create: true
|
17
|
+
});
|
18
|
+
</script>
|
19
|
+
<% else %>
|
20
|
+
<%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
|
21
|
+
<% if var.end_with?("_at") %>
|
22
|
+
<script>
|
23
|
+
$("#<%= var %>").daterangepicker({singleDatePicker: true, locale: {format: "YYYY-MM-DD"}, autoUpdateInput: false});
|
24
|
+
// hack to start with empty date
|
25
|
+
$("#<%= var %>").on("apply.daterangepicker", function(ev, picker) {
|
26
|
+
$(this).val(picker.startDate.format("YYYY-MM-DD"));
|
27
|
+
$(this).change();
|
28
|
+
});
|
29
|
+
</script>
|
30
|
+
<% end %>
|
31
|
+
<% end %>
|
32
|
+
<% end %>
|
33
|
+
|
34
|
+
<% if date_vars %>
|
35
|
+
<% date_vars.each do |var| %>
|
36
|
+
<%= hidden_field_tag var, params[var] %>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<%= label_tag nil, date_vars.join(" & ") %>
|
40
|
+
<div class="selectize-control single" style="width: 300px;">
|
41
|
+
<div id="reportrange" class="selectize-input" style="display: inline-block;">
|
42
|
+
<span>Select a time range</span>
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<script>
|
47
|
+
<%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
|
48
|
+
var format = "YYYY-MM-DD"
|
49
|
+
var now = moment.tz(timeZone)
|
50
|
+
|
51
|
+
function dateStr(daysAgo) {
|
52
|
+
return now.clone().subtract(daysAgo || 0, "days").format(format)
|
53
|
+
}
|
54
|
+
|
55
|
+
function toDate(time) {
|
56
|
+
return moment.tz(time.format(format), timeZone)
|
57
|
+
}
|
58
|
+
|
59
|
+
function setTimeInputs(start, end) {
|
60
|
+
$("#start_time").val(toDate(start).utc().format())
|
61
|
+
$("#end_time").val(toDate(end).endOf("day").utc().format())
|
62
|
+
}
|
63
|
+
|
64
|
+
$("#reportrange").daterangepicker(
|
65
|
+
{
|
66
|
+
ranges: {
|
67
|
+
"Today": [dateStr(), dateStr()],
|
68
|
+
"Last 7 Days": [dateStr(6), dateStr()],
|
69
|
+
"Last 30 Days": [dateStr(29), dateStr()]
|
70
|
+
},
|
71
|
+
locale: {
|
72
|
+
format: format
|
73
|
+
},
|
74
|
+
startDate: dateStr(29),
|
75
|
+
endDate: dateStr(),
|
76
|
+
opens: "right"
|
77
|
+
},
|
78
|
+
function(start, end) {
|
79
|
+
setTimeInputs(start, end)
|
80
|
+
submitIfCompleted($("#start_time").closest("form"))
|
81
|
+
}
|
82
|
+
).on("apply.daterangepicker", function(ev, picker) {
|
83
|
+
setTimeInputs(picker.startDate, picker.endDate)
|
84
|
+
$("#reportrange span").html(toDate(picker.startDate).format("MMMM D, YYYY") + " - " + toDate(picker.endDate).format("MMMM D, YYYY"))
|
85
|
+
})
|
86
|
+
|
87
|
+
if ($("#start_time").val().length > 0) {
|
88
|
+
var picker = $("#reportrange").data("daterangepicker")
|
89
|
+
picker.setStartDate(moment.tz($("#start_time").val(), timeZone))
|
90
|
+
picker.setEndDate(moment.tz($("#end_time").val(), timeZone))
|
91
|
+
$("#reportrange").trigger("apply.daterangepicker", picker)
|
92
|
+
} else {
|
93
|
+
var picker = $("#reportrange").data("daterangepicker")
|
94
|
+
$("#reportrange").trigger("apply.daterangepicker", picker)
|
95
|
+
submitIfCompleted($("#start_time").closest("form"))
|
96
|
+
}
|
97
|
+
</script>
|
98
|
+
<% end %>
|
99
|
+
|
100
|
+
<input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
|
101
|
+
</form>
|
102
|
+
<% end %>
|
@@ -0,0 +1,47 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
</head>
|
4
|
+
<body style="font-family: 'Helvetica Neue', Arial, Helvetica; font-size: 14px; color: #333;">
|
5
|
+
<p><%= link_to "View", query_url(@check.query_id) %></p>
|
6
|
+
<% if @error %>
|
7
|
+
<p><%= @error %></p>
|
8
|
+
<% elsif @rows_count > 0 && @check_type == "bad_data" %>
|
9
|
+
<p>
|
10
|
+
<% if @rows_count <= 10 %>
|
11
|
+
<%= pluralize(@rows_count, "row") %>
|
12
|
+
<% else %>
|
13
|
+
Showing 10 of <%= @rows_count %> rows
|
14
|
+
<% end %>
|
15
|
+
</p>
|
16
|
+
<table style="width: 100%; border-spacing: 0; border-collapse: collapse;">
|
17
|
+
<thead>
|
18
|
+
<tr>
|
19
|
+
<% @columns.first(5).each do |column| %>
|
20
|
+
<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) %>%;">
|
21
|
+
<%= column %>
|
22
|
+
</th>
|
23
|
+
<% end %>
|
24
|
+
</tr>
|
25
|
+
</thead>
|
26
|
+
<tbody>
|
27
|
+
<% @rows.first(10).each do |row| %>
|
28
|
+
<tr>
|
29
|
+
<% @columns.first(5).each_with_index do |column, i| %>
|
30
|
+
<td style="padding: 8px; line-height: 1.4; vertical-align: top; border-top: 1px solid #ddd;">
|
31
|
+
<% value = row[i] %>
|
32
|
+
<% if @column_types[i] == "time" && value.to_s.length > 10 %>
|
33
|
+
<% value = Time.parse(value).in_time_zone(Blazer.time_zone) rescue value %>
|
34
|
+
<% end %>
|
35
|
+
<%= value %>
|
36
|
+
</td>
|
37
|
+
<% end %>
|
38
|
+
</tr>
|
39
|
+
<% end %>
|
40
|
+
</tbody>
|
41
|
+
</table>
|
42
|
+
<% if @columns.size > 5 %>
|
43
|
+
<p style="color: #999;">Only first 5 columns shown</p>
|
44
|
+
<% end %>
|
45
|
+
<% end %>
|
46
|
+
</body>
|
47
|
+
</html>
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<% unless @check.respond_to?(:invert) %>
|
2
|
+
<p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<% if @check.errors.any? %>
|
6
|
+
<div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<%= form_for @check do |f| %>
|
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.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
|
+
<p class="text-muted">Emails are sent when a check starts failing, and when it starts passing again.
|
64
|
+
<p>
|
65
|
+
<% if @check.persisted? %>
|
66
|
+
<%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
67
|
+
<% end %>
|
68
|
+
<%= f.submit "Save", class: "btn btn-success" %>
|
69
|
+
<%= link_to "Back", :back, class: "btn btn-link" %>
|
70
|
+
</p>
|
71
|
+
<% end %>
|