blazer 0.0.8 → 1.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.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +261 -45
- data/app/assets/javascripts/blazer/Sortable.js +1144 -0
- data/app/assets/javascripts/blazer/application.js +2 -1
- data/app/assets/javascripts/blazer/chartkick.js +935 -0
- data/app/assets/javascripts/blazer/selectize.js +391 -201
- data/app/assets/stylesheets/blazer/application.css +17 -2
- data/app/assets/stylesheets/blazer/selectize.default.css +3 -2
- data/app/controllers/blazer/base_controller.rb +48 -0
- data/app/controllers/blazer/checks_controller.rb +51 -0
- data/app/controllers/blazer/dashboards_controller.rb +94 -0
- data/app/controllers/blazer/queries_controller.rb +29 -101
- data/app/helpers/blazer/{queries_helper.rb → base_helper.rb} +1 -1
- data/app/mailers/blazer/check_mailer.rb +21 -0
- data/app/models/blazer/check.rb +28 -0
- data/app/models/blazer/connection.rb +0 -1
- data/app/models/blazer/dashboard.rb +12 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +5 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +6 -0
- data/app/views/blazer/checks/_form.html.erb +28 -0
- data/app/views/blazer/checks/edit.html.erb +1 -0
- data/app/views/blazer/checks/index.html.erb +41 -0
- data/app/views/blazer/checks/new.html.erb +1 -0
- data/app/views/blazer/checks/run.html.erb +9 -0
- data/app/views/blazer/dashboards/_form.html.erb +86 -0
- data/app/views/blazer/dashboards/edit.html.erb +1 -0
- data/app/views/blazer/dashboards/index.html.erb +21 -0
- data/app/views/blazer/dashboards/new.html.erb +1 -0
- data/app/views/blazer/dashboards/show.html.erb +148 -0
- data/app/views/blazer/queries/_form.html.erb +16 -5
- data/app/views/blazer/queries/_tables.html +5 -0
- data/app/views/blazer/queries/index.html.erb +6 -0
- data/app/views/blazer/queries/run.html.erb +59 -44
- data/app/views/blazer/queries/show.html.erb +20 -16
- data/config/routes.rb +5 -0
- data/lib/blazer.rb +46 -2
- data/lib/blazer/data_source.rb +70 -0
- data/lib/blazer/engine.rb +6 -2
- data/lib/blazer/tasks.rb +12 -0
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml +26 -6
- data/lib/generators/blazer/templates/install.rb +21 -0
- metadata +27 -3
@@ -0,0 +1,21 @@
|
|
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
|
+
|
7
|
+
def state_change(check, state, state_was, rows_count, error)
|
8
|
+
@check = check
|
9
|
+
@state = state
|
10
|
+
@state_was = state_was
|
11
|
+
@rows_count = rows_count
|
12
|
+
@error = error
|
13
|
+
mail to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def failing_checks(email, checks)
|
17
|
+
@checks = checks
|
18
|
+
mail to: email, subject: "#{pluralize(checks.size, "Check")} Failing"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Check < ActiveRecord::Base
|
3
|
+
belongs_to :query
|
4
|
+
|
5
|
+
validates :query_id, presence: true
|
6
|
+
|
7
|
+
def split_emails
|
8
|
+
emails.to_s.split(",").map(&:strip)
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_state(rows, error)
|
12
|
+
self.state =
|
13
|
+
if error
|
14
|
+
"error"
|
15
|
+
elsif rows.any?
|
16
|
+
"failing"
|
17
|
+
else
|
18
|
+
"passing"
|
19
|
+
end
|
20
|
+
|
21
|
+
# do not notify on creation, except when not passing
|
22
|
+
if (state_was || state != "passing") && state != state_was && emails.present?
|
23
|
+
Blazer::CheckMailer.state_change(self, state, state_was, rows.size, error).deliver_later
|
24
|
+
end
|
25
|
+
save! if changed?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Dashboard < ActiveRecord::Base
|
3
|
+
has_many :dashboard_queries, dependent: :destroy
|
4
|
+
has_many :queries, through: :dashboard_queries
|
5
|
+
|
6
|
+
validates :name, presence: true
|
7
|
+
|
8
|
+
def to_param
|
9
|
+
[id, name.gsub("'", "").parameterize].join("-")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/app/models/blazer/query.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Blazer
|
2
2
|
class Query < ActiveRecord::Base
|
3
3
|
belongs_to :creator, class_name: Blazer.user_class.to_s if Blazer.user_class
|
4
|
+
has_many :checks, dependent: :destroy
|
4
5
|
|
5
6
|
validates :name, presence: true
|
6
7
|
validates :statement, presence: true
|
@@ -8,5 +9,9 @@ module Blazer
|
|
8
9
|
def to_param
|
9
10
|
[id, name.gsub("'", "").parameterize].join("-")
|
10
11
|
end
|
12
|
+
|
13
|
+
def friendly_name
|
14
|
+
name.gsub(/\[.+\]/, "").strip
|
15
|
+
end
|
11
16
|
end
|
12
17
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
|
2
|
+
|
3
|
+
<% if @check.errors.any? %>
|
4
|
+
<div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<%= form_for @check do |f| %>
|
8
|
+
<div class="form-group">
|
9
|
+
<%= f.label :query_id, "Query" %>
|
10
|
+
<div class="hide">
|
11
|
+
<%= f.select :query_id, Blazer::Query.order(:name).map { |q| [q.name, q.id] }, {include_blank: true} %>
|
12
|
+
</div>
|
13
|
+
<script>
|
14
|
+
$("#check_query_id").selectize().parents(".hide").removeClass("hide");
|
15
|
+
</script>
|
16
|
+
</div>
|
17
|
+
<div class="form-group">
|
18
|
+
<%= f.label :emails %>
|
19
|
+
<%= f.text_field :emails, placeholder: "Optional, comma separated", class: "form-control" %>
|
20
|
+
</div>
|
21
|
+
<p class="text-muted">Emails are sent when a check starts failing, and when it starts passing again.
|
22
|
+
<p>
|
23
|
+
<% if @check.persisted? %>
|
24
|
+
<%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
25
|
+
<% end %>
|
26
|
+
<%= f.submit "Save", class: "btn btn-success" %>
|
27
|
+
</p>
|
28
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render partial: "form" %>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<% title "Checks" %>
|
2
|
+
|
3
|
+
<p style="float: right;"><%= link_to "New Check", new_check_path, class: "btn btn-info" %></p>
|
4
|
+
<p>
|
5
|
+
<%= link_to "Home", root_path, class: "btn btn-primary", style: "margin-right: 10px;" %>
|
6
|
+
</p>
|
7
|
+
|
8
|
+
<% colors = {failing: "red", passing: "#5cb85c", error: "#666"} %>
|
9
|
+
<table class="table">
|
10
|
+
<thead>
|
11
|
+
<tr>
|
12
|
+
<th>Query</th>
|
13
|
+
<th style="width: 15%;">State</th>
|
14
|
+
<th style="width: 20%;">Emails</th>
|
15
|
+
<th style="width: 15%;"></th>
|
16
|
+
</tr>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<% @checks.each do |check| %>
|
20
|
+
<tr>
|
21
|
+
<td><%= link_to check.query.name, check.query %></td>
|
22
|
+
<td>
|
23
|
+
<% if check.state %>
|
24
|
+
<small style="font-weight: bold; color: <%= colors[check.state.to_sym] %>;"><%= check.state.upcase %></small>
|
25
|
+
<% end %>
|
26
|
+
</td>
|
27
|
+
<td>
|
28
|
+
<ul class="list-unstyled" style="margin-bottom: 0;">
|
29
|
+
<% check.split_emails.each do |email| %>
|
30
|
+
<li><%= email %></li>
|
31
|
+
<% end %>
|
32
|
+
</ul>
|
33
|
+
</td>
|
34
|
+
<td style="text-align: right; padding: 1px;">
|
35
|
+
<%= link_to "Edit", edit_check_path(check), class: "btn btn-info" %>
|
36
|
+
<%= link_to "Run Now", run_check_path(check), class: "btn btn-primary" %>
|
37
|
+
</td>
|
38
|
+
</tr>
|
39
|
+
<% end %>
|
40
|
+
</tbody>
|
41
|
+
</table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render partial: "form" %>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<p style="text-muted">Running check...</p>
|
2
|
+
|
3
|
+
<script>
|
4
|
+
$.post("<%= run_queries_path %>", <%= json_escape({statement: @query.statement, query_id: @query.id}.to_json).html_safe %>, function (data) {
|
5
|
+
setTimeout( function () {
|
6
|
+
window.location.href = "<%= checks_path %>";
|
7
|
+
}, 200);
|
8
|
+
});
|
9
|
+
</script>
|
@@ -0,0 +1,86 @@
|
|
1
|
+
<% if @dashboard.errors.any? %>
|
2
|
+
<div class="alert alert-danger"><%= @dashboard.errors.full_messages.first %></div>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<style>
|
6
|
+
.glyphicon-remove {
|
7
|
+
cursor: pointer;
|
8
|
+
color: #d9534f;
|
9
|
+
display: none;
|
10
|
+
}
|
11
|
+
|
12
|
+
li:hover .glyphicon-remove {
|
13
|
+
display: inline;
|
14
|
+
}
|
15
|
+
</style>
|
16
|
+
|
17
|
+
<%= form_for @dashboard do |f| %>
|
18
|
+
<div class="form-group">
|
19
|
+
<%= f.label :name %>
|
20
|
+
<%= f.text_field :name, class: "form-control" %>
|
21
|
+
</div>
|
22
|
+
<div class="form-group <%= "hide" if (@queries || @dashboard.queries).empty? %>">
|
23
|
+
<%= f.label :charts %>
|
24
|
+
<ul class="list-group">
|
25
|
+
<% (@queries || @dashboard.dashboard_queries.order(:position).map(&:query)).each do |query| %>
|
26
|
+
<li class="list-group-item">
|
27
|
+
<span class="glyphicon glyphicon-remove" aria-hidden="true" style="float: right; margin-top: 3px;"></span>
|
28
|
+
<%= query.name %>
|
29
|
+
<%= hidden_field_tag "query_ids[]", query.id %>
|
30
|
+
</li>
|
31
|
+
<% end %>
|
32
|
+
</ul>
|
33
|
+
</div>
|
34
|
+
<div class="form-group">
|
35
|
+
<%= f.label :query_id, "Add Chart" %>
|
36
|
+
<div class="hide">
|
37
|
+
<%= select_tag :query_id, options_for_select(Blazer::Query.order(:name).map { |q| [q.name, q.id] }, {include_blank: true}) %>
|
38
|
+
</div>
|
39
|
+
<script>
|
40
|
+
$("#query_id").selectize({allowEmptyOption: true}).parents(".hide").removeClass("hide");
|
41
|
+
$("#query_id").change( function () {
|
42
|
+
var $option = $(this).find("option:selected");
|
43
|
+
if ($option.val() !== "") {
|
44
|
+
// console.log($option.val());
|
45
|
+
// console.log($option.text());
|
46
|
+
var $li = $("<li></li>");
|
47
|
+
$li.addClass("list-group-item");
|
48
|
+
$li.text($option.text());
|
49
|
+
$li.prepend('<span class="glyphicon glyphicon-remove" aria-hidden="true" style="float: right; margin-top: 3px;"></span><input type="hidden" name="query_ids[]" id="query_ids_" value="' + $option.val() + '">');
|
50
|
+
$(".list-group").append($li);
|
51
|
+
$(this).val("");
|
52
|
+
$(".form-group").removeClass("hide");
|
53
|
+
}
|
54
|
+
});
|
55
|
+
</script>
|
56
|
+
</div>
|
57
|
+
<p>
|
58
|
+
<% if @dashboard.persisted? %>
|
59
|
+
<%= link_to "Delete", dashboard_path(@dashboard), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
60
|
+
<% end %>
|
61
|
+
<%= f.submit "Save", class: "btn btn-success" %>
|
62
|
+
</p>
|
63
|
+
<% end %>
|
64
|
+
|
65
|
+
<script>
|
66
|
+
$(".list-group").on("click", ".glyphicon-remove", function () {
|
67
|
+
$(this).parents("li:first").remove();
|
68
|
+
});
|
69
|
+
Sortable.create($(".list-group").get(0));
|
70
|
+
|
71
|
+
// $("form").submit( function () {
|
72
|
+
// var query_ids = $("li").map( function () {
|
73
|
+
// return $(this).attr("data-query-id");
|
74
|
+
// });
|
75
|
+
// console.log(query_ids.join(","));
|
76
|
+
// return false;
|
77
|
+
// });
|
78
|
+
|
79
|
+
// var editableList = Sortable.create($(".list-group").get(0), {
|
80
|
+
// filter: '.js-remove',
|
81
|
+
// onFilter: function (evt) {
|
82
|
+
// var el = editableList.closest(evt.item); // get dragged item
|
83
|
+
// el && el.parentNode.removeChild(el);
|
84
|
+
// }
|
85
|
+
// });
|
86
|
+
</script>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render partial: "form" %>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<% title "Dashboards" %>
|
2
|
+
|
3
|
+
<p style="float: right;"><%= link_to "New Dashboard", new_dashboard_path, class: "btn btn-info" %></p>
|
4
|
+
<p>
|
5
|
+
<%= link_to "Home", root_path, class: "btn btn-primary", style: "margin-right: 10px;" %>
|
6
|
+
</p>
|
7
|
+
|
8
|
+
<table class="table">
|
9
|
+
<thead>
|
10
|
+
<tr>
|
11
|
+
<th>Dashboard</th>
|
12
|
+
</tr>
|
13
|
+
</thead>
|
14
|
+
<tbody>
|
15
|
+
<% @dashboards.each do |dashboard| %>
|
16
|
+
<tr>
|
17
|
+
<td><%= link_to dashboard.name, dashboard %></td>
|
18
|
+
</tr>
|
19
|
+
<% end %>
|
20
|
+
</tbody>
|
21
|
+
</table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render partial: "form" %>
|
@@ -0,0 +1,148 @@
|
|
1
|
+
<% title @dashboard.name %>
|
2
|
+
|
3
|
+
<script>
|
4
|
+
function submitIfCompleted($form) {
|
5
|
+
var completed = true;
|
6
|
+
$form.find("input[name], select").each( function () {
|
7
|
+
if ($(this).val() == "") {
|
8
|
+
completed = false;
|
9
|
+
}
|
10
|
+
});
|
11
|
+
if (completed) {
|
12
|
+
$form.submit();
|
13
|
+
}
|
14
|
+
}
|
15
|
+
</script>
|
16
|
+
|
17
|
+
<div style="position: fixed; top: 0; left: 0; right: 0; background-color: whitesmoke; height: 60px; z-index: 1001;">
|
18
|
+
<div class="container">
|
19
|
+
<div class="row" style="padding-top: 13px;">
|
20
|
+
<div class="col-sm-9">
|
21
|
+
<%= link_to "Back", dashboards_path, class: "btn btn-primary", style: "vertical-align: top; margin-right: 5px;" %>
|
22
|
+
<h3 style="margin: 0; line-height: 34px; display: inline-block;">
|
23
|
+
<%= @dashboard.name %>
|
24
|
+
</h3>
|
25
|
+
</div>
|
26
|
+
<div class="col-sm-3 text-right">
|
27
|
+
<%= link_to "Edit", edit_dashboard_path(@dashboard), class: "btn btn-info" %>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<div style="margin-bottom: 60px;"></div>
|
34
|
+
|
35
|
+
<% if @bind_vars.any? %>
|
36
|
+
<form id="bind" method="get" action="<%= url_for(params) %>" class="form-inline" style="margin-bottom: 10px;">
|
37
|
+
<% date_vars = ["start_time", "end_time"] %>
|
38
|
+
<% if (date_vars - @bind_vars).empty? %>
|
39
|
+
<% @bind_vars = @bind_vars - date_vars %>
|
40
|
+
<% else %>
|
41
|
+
<% date_vars = nil %>
|
42
|
+
<% end %>
|
43
|
+
|
44
|
+
<% @bind_vars.each_with_index do |var, i| %>
|
45
|
+
<%= label_tag var, var %>
|
46
|
+
<% if (data = @smart_vars[var]) %>
|
47
|
+
<%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
|
48
|
+
<script>
|
49
|
+
$("#<%= var %>").selectize({
|
50
|
+
create: true
|
51
|
+
});
|
52
|
+
</script>
|
53
|
+
<% else %>
|
54
|
+
<%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
|
55
|
+
<% if var.end_with?("_at") %>
|
56
|
+
<script>
|
57
|
+
$("#<%= var %>").datepicker({format: "yyyy-mm-dd", autoclose: true, todayBtn: "linked"})
|
58
|
+
</script>
|
59
|
+
<% end %>
|
60
|
+
<% end %>
|
61
|
+
<% end %>
|
62
|
+
|
63
|
+
<% if date_vars %>
|
64
|
+
<% date_vars.each do |var| %>
|
65
|
+
<%= hidden_field_tag var, params[var] %>
|
66
|
+
<% end %>
|
67
|
+
|
68
|
+
<%= label_tag nil, date_vars.join(" & ") %>
|
69
|
+
<div class="selectize-control single" style="width: 300px;">
|
70
|
+
<div id="reportrange" class="selectize-input" style="display: inline-block;">
|
71
|
+
<span>Select a time range</span>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
|
75
|
+
<script>
|
76
|
+
var timeZone = "<%= Blazer.time_zone.tzinfo.name %>";
|
77
|
+
var format = "YYYY-MM-DD";
|
78
|
+
var now = moment.tz(timeZone);
|
79
|
+
|
80
|
+
function dateStr(daysAgo) {
|
81
|
+
return now.clone().subtract(daysAgo || 0, "days").format(format);
|
82
|
+
}
|
83
|
+
|
84
|
+
function toDate(time) {
|
85
|
+
return moment.tz(time.format(format), timeZone);
|
86
|
+
}
|
87
|
+
|
88
|
+
function setTimeInputs(start, end) {
|
89
|
+
$("#start_time").val(toDate(start).utc().format());
|
90
|
+
$("#end_time").val(toDate(end).endOf("day").utc().format());
|
91
|
+
}
|
92
|
+
|
93
|
+
$('#reportrange').daterangepicker(
|
94
|
+
{
|
95
|
+
ranges: {
|
96
|
+
"Today": [dateStr(), dateStr()],
|
97
|
+
"Last 7 Days": [dateStr(6), dateStr()],
|
98
|
+
"Last 30 Days": [dateStr(29), dateStr()]
|
99
|
+
},
|
100
|
+
format: format,
|
101
|
+
startDate: dateStr(29),
|
102
|
+
endDate: dateStr(),
|
103
|
+
opens: "left"
|
104
|
+
},
|
105
|
+
function(start, end) {
|
106
|
+
setTimeInputs(start, end);
|
107
|
+
submitIfCompleted($("#start_time").closest("form"));
|
108
|
+
}
|
109
|
+
).on('apply.daterangepicker', function(ev, picker) {
|
110
|
+
setTimeInputs(picker.startDate, picker.endDate);
|
111
|
+
$('#reportrange span').html(toDate(picker.startDate).format('MMMM D, YYYY') + ' - ' + toDate(picker.endDate).format('MMMM D, YYYY'));
|
112
|
+
})
|
113
|
+
|
114
|
+
if ($("#start_time").val().length > 0) {
|
115
|
+
var picker = $("#reportrange").data('daterangepicker');
|
116
|
+
picker.setStartDate(moment.tz($("#start_time").val(), timeZone));
|
117
|
+
picker.setEndDate(moment.tz($("#end_time").val(), timeZone));
|
118
|
+
$("#reportrange").trigger('apply.daterangepicker', picker)
|
119
|
+
} else {
|
120
|
+
var picker = $("#reportrange").data('daterangepicker');
|
121
|
+
$("#reportrange").trigger('apply.daterangepicker', picker);
|
122
|
+
submitIfCompleted($("#start_time").closest("form"));
|
123
|
+
}
|
124
|
+
</script>
|
125
|
+
<% end %>
|
126
|
+
</form>
|
127
|
+
<% end %>
|
128
|
+
|
129
|
+
<% @queries.each_with_index do |query, i| %>
|
130
|
+
<div style="padding-top: 10px;">
|
131
|
+
<h4 style="text-align: center;"><%= link_to query.friendly_name, query_path(query, variable_params), target: "_blank", style: "color: inherit;" %></h4>
|
132
|
+
<div id="chart-<%= i %>" class="chart">
|
133
|
+
<p class="text-muted">Loading...</p>
|
134
|
+
</div>
|
135
|
+
</div>
|
136
|
+
<script>
|
137
|
+
$.post("<%= run_queries_path %>", <%= json_escape({statement: query.statement, query_id: query.id, only_chart: true}.to_json).html_safe %>, function (data) {
|
138
|
+
$("#chart-<%= i %>").html(data);
|
139
|
+
$("#chart-<%= i %> table").stupidtable();
|
140
|
+
});
|
141
|
+
</script>
|
142
|
+
<% end %>
|
143
|
+
|
144
|
+
<script>
|
145
|
+
$(".form-inline input, .form-inline select").change( function () {
|
146
|
+
submitIfCompleted($(this).closest("form"));
|
147
|
+
});
|
148
|
+
</script>
|