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
@@ -14,9 +14,21 @@
14
14
  <div class="form-group text-right">
15
15
  <div class="pull-left" style="margin-top: 6px;">
16
16
  <%= link_to "Back", :back %>
17
- <span class="text-muted" style="margin-left: 20px;"> Use {start_time} and {end_time} for time ranges</span>
17
+ <% if Blazer.data_sources.size == 1 %>
18
+ <span class="text-muted" style="margin-left: 20px;"> Use {start_time} and {end_time} for time ranges</span>
19
+ <% end %>
18
20
  </div>
19
- <%= select_tag :table_names, options_for_select([["Preview table", nil]] + tables.keys), style: "margin-right: 20px; width: 240px;" %>
21
+ <%= f.select :data_source, Blazer.data_sources.values.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size == 1), style: "width: 140px;" %>
22
+ <div id="tables" style="display: inline-block; width: 260px; margin-right: 10px;" class="hide">
23
+ <%= render partial: "tables" %>
24
+ </div>
25
+ <script>
26
+ function updatePreviewSelect() {
27
+ $("#tables").load("<%= tables_queries_path %>?" + $.param({data_source: $("#query_data_source").val()}));
28
+ }
29
+ updatePreviewSelect();
30
+ $("#query_data_source").selectize().change(updatePreviewSelect);
31
+ </script>
20
32
  <%= link_to "Run", "#", class: "btn btn-info", id: "run", style: "vertical-align: top;" %>
21
33
  </div>
22
34
  </div>
@@ -99,7 +111,7 @@
99
111
  if (xhr) {
100
112
  xhr.abort();
101
113
  }
102
- xhr = $.post("<%= run_queries_path %>", $.extend({}, params, {statement: editor.getValue()}), function (data) {
114
+ xhr = $.post("<%= run_queries_path %>", $.extend({}, params, {statement: editor.getValue(), data_source: $("#query_data_source").val()}), function (data) {
103
115
  $("#results").html(data);
104
116
 
105
117
  error_line = /LINE (\d+)/g.exec($("#results").find('.alert-danger').text());
@@ -118,8 +130,7 @@
118
130
  $("#run").click();
119
131
  }
120
132
 
121
- $("#table_names").selectize({
122
- }).change( function () {
133
+ $(document).on("change", "#table_names", function () {
123
134
  var val = $(this).val();
124
135
  if (val.length > 0) {
125
136
  editor.setValue("SELECT * FROM " + val + " LIMIT 10");
@@ -0,0 +1,5 @@
1
+ <%= select_tag :table_names, options_for_select([["Preview table", nil]] + (@tables || [])), style: "width: 240px;" %>
2
+
3
+ <script>
4
+ $("#table_names").selectize({}).parent().removeClass("hide");
5
+ </script>
@@ -2,6 +2,8 @@
2
2
  <div id="header" style="margin-bottom: 20px;">
3
3
  <div class="pull-right">
4
4
  <%= link_to "New Query", new_query_path, class: "btn btn-info" %>
5
+ <%= link_to "Dashboards", dashboards_path, class: "btn btn-primary" %>
6
+ <%= link_to "Checks", checks_path, class: "btn btn-primary" %>
5
7
  </div>
6
8
  <input type="text" placeholder="Start typing a query or person" style="width: 300px; display: inline-block;" autofocus=true class="search form-control" />
7
9
  </div>
@@ -18,12 +20,16 @@
18
20
  <tr>
19
21
  <td class="query">
20
22
  <%= link_to query.name, query %>
23
+ <span style="color: #ccc;"><%= extract_vars(query.statement).join(", ") %></span>
21
24
  <% if query.created_at > 2.days.ago %>
22
25
  <small style="font-weight: bold; color: #5cb85c;">NEW</small>
23
26
  <% end %>
24
27
  <% if @trending_queries[query.id] %>
25
28
  <small style="font-weight: bold; color: #f60;">TRENDING</small>
26
29
  <% end %>
30
+ <% if @checks[query.id] %>
31
+ <small style="font-weight: bold; color: #f60;">CHECK</small>
32
+ <% end %>
27
33
  <div class="hide"><%= query.name.gsub(/\s+/, "") %></div>
28
34
  </td>
29
35
  <td class="creator text-right text-muted">
@@ -1,61 +1,76 @@
1
1
  <% if @error %>
2
2
  <div class="alert alert-danger"><%= @error %></div>
3
3
  <% elsif !@success %>
4
- <div class="alert alert-info">Can’t preview queries with variables...yet!</div>
4
+ <% if @only_chart %>
5
+ <p class="text-muted">Select variables</p>
6
+ <% else %>
7
+ <div class="alert alert-info">Can’t preview queries with variables...yet!</div>
8
+ <% end %>
5
9
  <% else %>
6
- <p class="text-muted"><%= pluralize(@rows.size, "row") %></p>
10
+ <% unless @only_chart %>
11
+ <p class="text-muted"><%= pluralize(@rows.size, "row") %></p>
12
+ <% end %>
7
13
  <% if @rows.any? %>
8
14
  <% values = @rows.first.values %>
9
- <% if values.size >= 2 && values.first.is_a?(Time) && values[1..-1].all?{|v| v.is_a?(Numeric) } %>
15
+ <% chart_id = SecureRandom.hex %>
16
+ <% if values.size >= 2 && (values.first.is_a?(Time) || values.first.is_a?(Date)) && values[1..-1].all?{|v| v.is_a?(Numeric) } %>
10
17
  <% time_k = @columns.keys.first %>
11
- <%= line_chart @columns.keys[1..-1].map{|k| {name: k, data: @rows.map{|r| [r[time_k], r[k]] }} } %>
12
- <% elsif values.size == 3 && values.first.is_a?(Time) && values[1].is_a?(String) && values[2].is_a?(Numeric) %>
18
+ <%= line_chart @columns.keys[1..-1].map{|k| {name: k, data: @rows.map{|r| [r[time_k], r[k]] }} }, id: chart_id, min: nil %>
19
+ <% elsif values.size == 3 && (values.first.is_a?(Time) || values.first.is_a?(Date)) && values[1].is_a?(String) && values[2].is_a?(Numeric) %>
13
20
  <% keys = @columns.keys %>
14
- <%= line_chart @rows.group_by { |v| v[keys[1]] }.map { |name, v| {name: name, data: v.map { |v2| [v2[keys[0]], v2[keys[2]]] } } } %>
21
+ <%= line_chart @rows.group_by { |v| v[keys[1]] }.map { |name, v| {name: name, data: v.map { |v2| [v2[keys[0]], v2[keys[2]]] } } }, id: chart_id, min: nil %>
15
22
  <% elsif values.size == 2 && values.first.is_a?(String) && values.last.is_a?(Numeric) %>
16
- <%= pie_chart @rows.map(&:values), library: {sliceVisibilityThreshold: 1 / 40.0} %>
23
+ <%= pie_chart @rows.map(&:values), library: {sliceVisibilityThreshold: 1 / 40.0}, id: chart_id %>
24
+ <% elsif @only_chart %>
25
+ <% @no_chart = true %>
17
26
  <% end %>
18
27
 
19
- <% header_width = 100 / @rows.first.keys.size.to_f %>
20
- <table class="table">
21
- <thead>
22
- <tr>
23
- <% @columns.each do |key, type| %>
24
- <th style="width: <%= header_width %>%;" data-sort="<%= type %>">
25
- <div style="min-width: <%= @min_width_types.include?(key) ? 180 : 60 %>px;">
26
- <%= key %>
27
- </div>
28
- </th>
29
- <% end %>
30
- </tr>
31
- </thead>
32
- <tbody>
33
- <% @rows.each do |row| %>
34
- <tr>
35
- <% row.each do |k, v| %>
36
- <td>
37
- <% if v.is_a?(Time) %>
38
- <% v = v.in_time_zone(Blazer.time_zone) %>
39
- <% end %>
28
+ <% unless @only_chart && !@no_chart %>
29
+ <% header_width = 100 / @rows.first.keys.size.to_f %>
30
+ <div class="results-container">
31
+ <table class="table results-table" style="margin-bottom: 0;">
32
+ <thead>
33
+ <tr>
34
+ <% @columns.each do |key, type| %>
35
+ <th style="width: <%= header_width %>%;" data-sort="<%= type %>">
36
+ <div style="min-width: <%= @min_width_types.include?(key) ? 180 : 60 %>px;">
37
+ <%= key %>
38
+ </div>
39
+ </th>
40
+ <% end %>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <% @rows.each do |row| %>
45
+ <tr>
46
+ <% row.each do |k, v| %>
47
+ <td>
48
+ <% if v.is_a?(Time) %>
49
+ <% v = v.in_time_zone(Blazer.time_zone) %>
50
+ <% end %>
40
51
 
41
- <% unless v.nil? %>
42
- <% if v == "" %>
43
- <div class="text-muted">empty string</div>
44
- <% elsif @linked_columns[k] %>
45
- <%= link_to format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
46
- <% else %>
47
- <%= format_value(k, v) %>
48
- <% end %>
52
+ <% unless v.nil? %>
53
+ <% if v == "" %>
54
+ <div class="text-muted">empty string</div>
55
+ <% elsif @linked_columns[k] %>
56
+ <%= link_to format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
57
+ <% else %>
58
+ <%= format_value(k, v) %>
59
+ <% end %>
49
60
 
50
- <% if v2 = (@boom[k] || {})[v] %>
51
- <div class="text-muted"><%= v2 %></div>
52
- <% end %>
61
+ <% if v2 = (@boom[k] || {})[v] %>
62
+ <div class="text-muted"><%= v2 %></div>
63
+ <% end %>
64
+ <% end %>
65
+ </td>
53
66
  <% end %>
54
- </td>
67
+ </tr>
55
68
  <% end %>
56
- </tr>
57
- <% end %>
58
- </tbody>
59
- </table>
69
+ </tbody>
70
+ </table>
71
+ </div>
72
+ <% end %>
73
+ <% elsif @only_chart %>
74
+ <p class="text-muted">No rows</p>
60
75
  <% end %>
61
76
  <% end %>
@@ -1,5 +1,19 @@
1
1
  <% title @query.name %>
2
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
+
3
17
  <div style="position: fixed; top: 0; left: 0; right: 0; background-color: whitesmoke; height: 60px; z-index: 1001;">
4
18
  <div class="container">
5
19
  <div class="row" style="padding-top: 13px;">
@@ -11,7 +25,7 @@
11
25
  </div>
12
26
  <div class="col-sm-3 text-right">
13
27
  <%= link_to "Edit", edit_query_path(@query, variable_params), class: "btn btn-default" %>
14
- <%= link_to "Fork", new_query_path(variable_params.merge(statement: @query.statement)), class: "btn btn-info" %>
28
+ <%= link_to "Fork", new_query_path(variable_params.merge(statement: @query.statement, data_source: @query.data_source)), class: "btn btn-info" %>
15
29
 
16
30
  <% if !@error && @success %>
17
31
  <%= button_to "Download", run_queries_path(statement: @statement, query_id: @query.id, format: "csv"), class: "btn btn-primary" %>
@@ -49,7 +63,7 @@
49
63
  <% @bind_vars.each_with_index do |var, i| %>
50
64
  <%= label_tag var, var %>
51
65
  <% if (data = @smart_vars[var]) %>
52
- <%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px;" %>
66
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
53
67
  <script>
54
68
  $("#<%= var %>").selectize({
55
69
  create: true
@@ -121,11 +135,13 @@
121
135
  picker.setStartDate(moment.tz($("#start_time").val(), timeZone));
122
136
  picker.setEndDate(moment.tz($("#end_time").val(), timeZone));
123
137
  $("#reportrange").trigger('apply.daterangepicker', picker)
138
+ } else {
139
+ var picker = $("#reportrange").data('daterangepicker');
140
+ $("#reportrange").trigger('apply.daterangepicker', picker);
141
+ submitIfCompleted($("#start_time").closest("form"));
124
142
  }
125
143
  </script>
126
144
  <% end %>
127
-
128
- <input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
129
145
  </form>
130
146
  <% end %>
131
147
 
@@ -147,18 +163,6 @@
147
163
  <script>
148
164
  hljs.initHighlightingOnLoad();
149
165
 
150
- function submitIfCompleted($form) {
151
- var completed = true;
152
- $form.find("input[name], select").each( function () {
153
- if ($(this).val() == "") {
154
- completed = false;
155
- }
156
- });
157
- if (completed) {
158
- $form.submit();
159
- }
160
- }
161
-
162
166
  $(".form-inline input, .form-inline select").change( function () {
163
167
  submitIfCompleted($(this).closest("form"));
164
168
  });
data/config/routes.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  Blazer::Engine.routes.draw do
2
2
  resources :queries, except: [:index] do
3
3
  post :run, on: :collection # err on the side of caution
4
+ get :tables, on: :collection
4
5
  end
6
+ resources :checks, except: [:show] do
7
+ get :run, on: :member
8
+ end
9
+ resources :dashboards
5
10
  root to: "queries#index"
6
11
  end
data/lib/blazer.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require "csv"
2
2
  require "chartkick"
3
3
  require "blazer/version"
4
+ require "blazer/data_source"
4
5
  require "blazer/engine"
6
+ require "blazer/tasks"
5
7
 
6
8
  module Blazer
7
9
  class << self
@@ -9,13 +11,55 @@ module Blazer
9
11
  attr_reader :time_zone
10
12
  attr_accessor :user_name
11
13
  attr_accessor :user_class
12
- attr_accessor :timeout
14
+ attr_accessor :from_email
13
15
  end
14
16
  self.audit = true
15
17
  self.user_name = :name
16
- self.timeout = 15
17
18
 
18
19
  def self.time_zone=(time_zone)
19
20
  @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
20
21
  end
22
+
23
+ def self.settings
24
+ @settings ||= YAML.load(ERB.new(File.read(Rails.root.join("config", "blazer.yml"))).result)
25
+ end
26
+
27
+ def self.data_sources
28
+ @data_sources ||= begin
29
+ ds = Hash[
30
+ settings["data_sources"].map do |id, s|
31
+ [id, Blazer::DataSource.new(id, s)]
32
+ end
33
+ ]
34
+ ds.default = ds.values.first
35
+ ds
36
+ end
37
+ end
38
+
39
+ def self.run_checks
40
+ Blazer::Check.includes(:query).find_each do |check|
41
+ rows = nil
42
+ error = nil
43
+ tries = 0
44
+ # try 3 times on timeout errors
45
+ begin
46
+ rows, error = data_sources[check.query.data_source].run_statement(check.query.statement)
47
+ tries += 1
48
+ end while error && error.include?("canceling statement due to statement timeout") && tries < 3
49
+ check.update_state(rows, error)
50
+ end
51
+ end
52
+
53
+ def self.send_failing_checks
54
+ emails = {}
55
+ Blazer::Check.includes(:query).where(state: %w[failing error]).find_each do |check|
56
+ check.split_emails.each do |email|
57
+ (emails[email] ||= []) << check
58
+ end
59
+ end
60
+
61
+ emails.each do |email, checks|
62
+ Blazer::CheckMailer.failing_checks(email, checks).deliver_later
63
+ end
64
+ end
21
65
  end
@@ -0,0 +1,70 @@
1
+ module Blazer
2
+ class DataSource
3
+ attr_reader :id, :settings, :connection_model
4
+
5
+ def initialize(id, settings)
6
+ @id = id
7
+ @settings = settings
8
+ @connection_model =
9
+ Class.new(Blazer::Connection) do
10
+ def self.name
11
+ "Blazer::Connection::#{object_id}"
12
+ end
13
+ establish_connection(settings["url"]) if settings["url"]
14
+ end
15
+ end
16
+
17
+ def name
18
+ settings["name"] || @id
19
+ end
20
+
21
+ def linked_columns
22
+ settings["linked_columns"] || {}
23
+ end
24
+
25
+ def smart_columns
26
+ settings["smart_columns"] || {}
27
+ end
28
+
29
+ def smart_variables
30
+ settings["smart_variables"] || {}
31
+ end
32
+
33
+ def timeout
34
+ settings["timeout"]
35
+ end
36
+
37
+ def run_statement(statement)
38
+ rows = []
39
+ error = nil
40
+ begin
41
+ connection_model.transaction do
42
+ connection_model.connection.execute("SET statement_timeout = #{timeout.to_i * 1000}") if timeout && postgresql?
43
+ result = connection_model.connection.select_all(statement)
44
+ result.each do |untyped_row|
45
+ row = {}
46
+ untyped_row.each do |k, v|
47
+ row[k] = result.column_types.empty? ? v : result.column_types[k].send(:type_cast, v)
48
+ end
49
+ rows << row
50
+ end
51
+ raise ActiveRecord::Rollback
52
+ end
53
+ rescue ActiveRecord::StatementInvalid => e
54
+ error = e.message.sub(/.+ERROR: /, "")
55
+ end
56
+ [rows, error]
57
+ end
58
+
59
+ def tables
60
+ default_schema = postgresql? ? "public" : connection_model.connection_config[:database]
61
+ schema = connection_model.connection_config[:schema] || default_schema
62
+ rows, error = run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_name, column_name, ordinal_position, data_type FROM information_schema.columns WHERE table_schema = ?", schema]))
63
+ Hash[rows.group_by { |r| r["table_name"] }.map { |t, f| [t, f.sort_by { |f| f["ordinal_position"] }.map { |f| f.slice("column_name", "data_type") }] }.sort_by { |t, _f| t }]
64
+ end
65
+
66
+ def postgresql?
67
+ ["PostgreSQL", "Redshift"].include?(connection_model.connection.adapter_name)
68
+ end
69
+ end
70
+ end
data/lib/blazer/engine.rb CHANGED
@@ -6,8 +6,12 @@ module Blazer
6
6
  # use a proc instead of a string
7
7
  app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/application\.(js|css)\z/ }
8
8
 
9
- Blazer.time_zone ||= Time.zone
10
- Blazer.user_class ||= User rescue nil
9
+ Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
10
+ Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
11
+ Blazer.user_name = Blazer.settings["user_name"] if Blazer.settings["user_name"]
12
+ Blazer.from_email = Blazer.settings["from_email"] if Blazer.settings["from_email"]
13
+
14
+ Blazer.user_class ||= Blazer.settings["user_class"] || User rescue nil
11
15
  Blazer::Query.belongs_to :creator, class_name: Blazer.user_class.to_s if Blazer.user_class
12
16
  end
13
17
  end
@@ -0,0 +1,12 @@
1
+ require "rake"
2
+
3
+ namespace :blazer do
4
+ desc "run checks"
5
+ task run_checks: :environment do
6
+ Blazer.run_checks
7
+ end
8
+
9
+ task send_failing_checks: :environment do
10
+ Blazer.send_failing_checks
11
+ end
12
+ end