blazer_xlsx 3.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +442 -0
- data/CONTRIBUTING.md +42 -0
- data/LICENSE.txt +22 -0
- data/README.md +1093 -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/images/blazer/favicon.png +0 -0
- data/app/assets/javascripts/blazer/Sortable.js +3709 -0
- data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
- data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
- data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
- data/app/assets/javascripts/blazer/ace.js +6 -0
- data/app/assets/javascripts/blazer/application.js +84 -0
- data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
- data/app/assets/javascripts/blazer/chart.umd.js +13 -0
- data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
- data/app/assets/javascripts/blazer/chartkick.js +2570 -0
- data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
- data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
- data/app/assets/javascripts/blazer/highlight.min.js +466 -0
- data/app/assets/javascripts/blazer/jquery.js +10872 -0
- data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
- data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
- data/app/assets/javascripts/blazer/moment.js +5685 -0
- data/app/assets/javascripts/blazer/queries.js +130 -0
- data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
- data/app/assets/javascripts/blazer/routes.js +26 -0
- data/app/assets/javascripts/blazer/selectize.js +3891 -0
- data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
- data/app/assets/javascripts/blazer/stupidtable.js +281 -0
- data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
- data/app/assets/stylesheets/blazer/application.css +243 -0
- data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
- data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
- data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
- data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
- data/app/assets/stylesheets/blazer/github.css +125 -0
- data/app/assets/stylesheets/blazer/selectize.css +403 -0
- data/app/controllers/blazer/base_controller.rb +135 -0
- data/app/controllers/blazer/checks_controller.rb +56 -0
- data/app/controllers/blazer/dashboards_controller.rb +99 -0
- data/app/controllers/blazer/queries_controller.rb +472 -0
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/helpers/blazer/base_helper.rb +39 -0
- data/app/models/blazer/audit.rb +6 -0
- data/app/models/blazer/check.rb +104 -0
- data/app/models/blazer/connection.rb +5 -0
- data/app/models/blazer/dashboard.rb +17 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +42 -0
- data/app/models/blazer/record.rb +5 -0
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +18 -0
- data/app/views/blazer/_variables.html.erb +127 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
- data/app/views/blazer/checks/_form.html.erb +79 -0
- data/app/views/blazer/checks/edit.html.erb +3 -0
- data/app/views/blazer/checks/index.html.erb +72 -0
- data/app/views/blazer/checks/new.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +82 -0
- data/app/views/blazer/dashboards/edit.html.erb +3 -0
- data/app/views/blazer/dashboards/new.html.erb +3 -0
- data/app/views/blazer/dashboards/show.html.erb +53 -0
- data/app/views/blazer/queries/_caching.html.erb +16 -0
- data/app/views/blazer/queries/_cohorts.html.erb +48 -0
- data/app/views/blazer/queries/_form.html.erb +255 -0
- data/app/views/blazer/queries/docs.html.erb +147 -0
- data/app/views/blazer/queries/edit.html.erb +2 -0
- data/app/views/blazer/queries/home.html.erb +169 -0
- data/app/views/blazer/queries/new.html.erb +2 -0
- data/app/views/blazer/queries/run.html.erb +183 -0
- data/app/views/blazer/queries/schema.html.erb +55 -0
- data/app/views/blazer/queries/show.html.erb +72 -0
- data/app/views/blazer/uploads/_form.html.erb +27 -0
- data/app/views/blazer/uploads/edit.html.erb +3 -0
- data/app/views/blazer/uploads/index.html.erb +55 -0
- data/app/views/blazer/uploads/new.html.erb +3 -0
- data/app/views/layouts/blazer/application.html.erb +25 -0
- data/config/routes.rb +25 -0
- data/lib/blazer/adapters/athena_adapter.rb +182 -0
- data/lib/blazer/adapters/base_adapter.rb +76 -0
- data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
- data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
- data/lib/blazer/adapters/drill_adapter.rb +38 -0
- data/lib/blazer/adapters/druid_adapter.rb +102 -0
- data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
- data/lib/blazer/adapters/hive_adapter.rb +55 -0
- data/lib/blazer/adapters/ignite_adapter.rb +64 -0
- data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
- data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
- data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
- data/lib/blazer/adapters/presto_adapter.rb +54 -0
- data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
- data/lib/blazer/adapters/soda_adapter.rb +105 -0
- data/lib/blazer/adapters/spark_adapter.rb +14 -0
- data/lib/blazer/adapters/sql_adapter.rb +353 -0
- data/lib/blazer/adapters.rb +17 -0
- data/lib/blazer/anomaly_detectors.rb +22 -0
- data/lib/blazer/check_mailer.rb +27 -0
- data/lib/blazer/data_source.rb +266 -0
- data/lib/blazer/engine.rb +42 -0
- data/lib/blazer/forecasters.rb +7 -0
- data/lib/blazer/result.rb +178 -0
- data/lib/blazer/result_cache.rb +71 -0
- data/lib/blazer/run_statement.rb +45 -0
- data/lib/blazer/run_statement_job.rb +20 -0
- data/lib/blazer/slack_notifier.rb +94 -0
- data/lib/blazer/statement.rb +77 -0
- data/lib/blazer/version.rb +3 -0
- data/lib/blazer.rb +282 -0
- data/lib/generators/blazer/install_generator.rb +22 -0
- data/lib/generators/blazer/templates/config.yml.tt +79 -0
- data/lib/generators/blazer/templates/install.rb.tt +47 -0
- data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
- data/lib/generators/blazer/uploads_generator.rb +18 -0
- data/lib/tasks/blazer.rake +20 -0
- data/licenses/LICENSE-ace.txt +24 -0
- data/licenses/LICENSE-bootstrap.txt +21 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-date-fns.txt +21 -0
- data/licenses/LICENSE-daterangepicker.txt +21 -0
- data/licenses/LICENSE-fuzzysearch.txt +20 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
- data/licenses/LICENSE-moment-timezone.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-rails-ujs.txt +20 -0
- data/licenses/LICENSE-selectize.txt +202 -0
- data/licenses/LICENSE-sortable.txt +21 -0
- data/licenses/LICENSE-stickytableheaders.txt +20 -0
- data/licenses/LICENSE-stupidtable.txt +19 -0
- data/licenses/LICENSE-vue.txt +21 -0
- metadata +271 -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,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,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,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,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,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,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>
|