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.

Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +261 -45
  4. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  5. data/app/assets/javascripts/blazer/application.js +2 -1
  6. data/app/assets/javascripts/blazer/chartkick.js +935 -0
  7. data/app/assets/javascripts/blazer/selectize.js +391 -201
  8. data/app/assets/stylesheets/blazer/application.css +17 -2
  9. data/app/assets/stylesheets/blazer/selectize.default.css +3 -2
  10. data/app/controllers/blazer/base_controller.rb +48 -0
  11. data/app/controllers/blazer/checks_controller.rb +51 -0
  12. data/app/controllers/blazer/dashboards_controller.rb +94 -0
  13. data/app/controllers/blazer/queries_controller.rb +29 -101
  14. data/app/helpers/blazer/{queries_helper.rb → base_helper.rb} +1 -1
  15. data/app/mailers/blazer/check_mailer.rb +21 -0
  16. data/app/models/blazer/check.rb +28 -0
  17. data/app/models/blazer/connection.rb +0 -1
  18. data/app/models/blazer/dashboard.rb +12 -0
  19. data/app/models/blazer/dashboard_query.rb +9 -0
  20. data/app/models/blazer/query.rb +5 -0
  21. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  22. data/app/views/blazer/check_mailer/state_change.html.erb +6 -0
  23. data/app/views/blazer/checks/_form.html.erb +28 -0
  24. data/app/views/blazer/checks/edit.html.erb +1 -0
  25. data/app/views/blazer/checks/index.html.erb +41 -0
  26. data/app/views/blazer/checks/new.html.erb +1 -0
  27. data/app/views/blazer/checks/run.html.erb +9 -0
  28. data/app/views/blazer/dashboards/_form.html.erb +86 -0
  29. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  30. data/app/views/blazer/dashboards/index.html.erb +21 -0
  31. data/app/views/blazer/dashboards/new.html.erb +1 -0
  32. data/app/views/blazer/dashboards/show.html.erb +148 -0
  33. data/app/views/blazer/queries/_form.html.erb +16 -5
  34. data/app/views/blazer/queries/_tables.html +5 -0
  35. data/app/views/blazer/queries/index.html.erb +6 -0
  36. data/app/views/blazer/queries/run.html.erb +59 -44
  37. data/app/views/blazer/queries/show.html.erb +20 -16
  38. data/config/routes.rb +5 -0
  39. data/lib/blazer.rb +46 -2
  40. data/lib/blazer/data_source.rb +70 -0
  41. data/lib/blazer/engine.rb +6 -2
  42. data/lib/blazer/tasks.rb +12 -0
  43. data/lib/blazer/version.rb +1 -1
  44. data/lib/generators/blazer/templates/config.yml +26 -6
  45. data/lib/generators/blazer/templates/install.rb +21 -0
  46. metadata +27 -3
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- module QueriesHelper
2
+ module BaseHelper
3
3
  def title(title = nil)
4
4
  if title
5
5
  content_for(:title) { title }
@@ -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
@@ -1,6 +1,5 @@
1
1
  module Blazer
2
2
  class Connection < ActiveRecord::Base
3
- establish_connection ENV["BLAZER_DATABASE_URL"] if ENV["BLAZER_DATABASE_URL"]
4
3
  self.abstract_class = true
5
4
  end
6
5
  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
@@ -0,0 +1,9 @@
1
+ module Blazer
2
+ class DashboardQuery < ActiveRecord::Base
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
@@ -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,6 @@
1
+ <ul>
2
+ <% @checks.each do |check| %>
3
+ <li><%= link_to check.query.name, query_url(check.query_id) %></li>
4
+ <% end %>
5
+ </ul>
6
+ <p><%= link_to "Manage checks", checks_url %></p>
@@ -0,0 +1,6 @@
1
+ <p><%= link_to "View", query_url(@check.query_id) %></p>
2
+ <% if @error %>
3
+ <p><%= @error %></p>
4
+ <% elsif @rows_count > 0 %>
5
+ <p><%= pluralize(@rows_count, "row") %></p>
6
+ <% 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>