blazer 2.2.8 → 2.4.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +131 -168
  5. data/app/assets/stylesheets/blazer/application.css +4 -0
  6. data/app/controllers/blazer/base_controller.rb +8 -0
  7. data/app/controllers/blazer/dashboards_controller.rb +2 -0
  8. data/app/controllers/blazer/queries_controller.rb +119 -3
  9. data/app/controllers/blazer/uploads_controller.rb +147 -0
  10. data/app/models/blazer/query.rb +9 -2
  11. data/app/models/blazer/upload.rb +11 -0
  12. data/app/models/blazer/uploads_connection.rb +7 -0
  13. data/app/views/blazer/_nav.html.erb +3 -0
  14. data/app/views/blazer/checks/_form.html.erb +1 -1
  15. data/app/views/blazer/checks/index.html.erb +3 -0
  16. data/app/views/blazer/dashboards/_form.html.erb +1 -1
  17. data/app/views/blazer/dashboards/show.html.erb +1 -1
  18. data/app/views/blazer/queries/_caching.html.erb +16 -0
  19. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  20. data/app/views/blazer/queries/docs.html.erb +6 -0
  21. data/app/views/blazer/queries/home.html.erb +3 -0
  22. data/app/views/blazer/queries/run.html.erb +15 -17
  23. data/app/views/blazer/queries/show.html.erb +1 -1
  24. data/app/views/blazer/uploads/_form.html.erb +27 -0
  25. data/app/views/blazer/uploads/edit.html.erb +3 -0
  26. data/app/views/blazer/uploads/index.html.erb +55 -0
  27. data/app/views/blazer/uploads/new.html.erb +3 -0
  28. data/config/routes.rb +5 -0
  29. data/lib/blazer.rb +24 -0
  30. data/lib/blazer/adapters/base_adapter.rb +8 -0
  31. data/lib/blazer/adapters/druid_adapter.rb +3 -3
  32. data/lib/blazer/adapters/hive_adapter.rb +45 -0
  33. data/lib/blazer/adapters/ignite_adapter.rb +54 -0
  34. data/lib/blazer/adapters/spark_adapter.rb +9 -0
  35. data/lib/blazer/adapters/sql_adapter.rb +64 -1
  36. data/lib/blazer/data_source.rb +2 -2
  37. data/lib/blazer/version.rb +1 -1
  38. data/lib/generators/blazer/templates/config.yml.tt +6 -0
  39. data/lib/generators/blazer/templates/install.rb.tt +1 -0
  40. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  41. data/lib/generators/blazer/uploads_generator.rb +18 -0
  42. data/lib/tasks/blazer.rake +9 -0
  43. metadata +18 -32
@@ -10,6 +10,9 @@
10
10
  </button>
11
11
  <ul class="dropdown-menu">
12
12
  <li><%= link_to "Home", root_path %></li>
13
+ <% if Blazer.uploads? %>
14
+ <li><%= link_to "Uploads", uploads_path %></li>
15
+ <% end %>
13
16
  <li role="separator" class="divider"></li>
14
17
  <li><%= link_to "New Query", new_query_path %></li>
15
18
  <li><%= link_to "New Dashboard", new_dashboard_path %></li>
@@ -31,7 +31,7 @@
31
31
  <% end %>
32
32
 
33
33
  <script>
34
- <%= blazer_js_var "queries", Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
34
+ <%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
35
35
  <%= blazer_js_var "dashboardQueries", @queries || @dashboard.dashboard_queries.order(:position).map(&:query) %>
36
36
 
37
37
  var app = new Vue({
@@ -39,7 +39,7 @@
39
39
  </div>
40
40
  </div>
41
41
  <script>
42
- <%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true} %>
42
+ <%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]} %>
43
43
 
44
44
  runQuery(data, function (data) {
45
45
  $("#chart-<%= i %>").html(data)
@@ -0,0 +1,16 @@
1
+ <% if @cached_at || @just_cached %>
2
+ <p class="text-muted" style="float: right;">
3
+ <% if @cached_at %>
4
+ Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
5
+ <% elsif params[:query_id] %>
6
+ Cached just now
7
+ <% if @data_source.cache_mode == "slow" %>
8
+ (over <%= "%g" % @data_source.cache_slow_threshold %>s)
9
+ <% end %>
10
+ <% end %>
11
+
12
+ <% if @query && params[:query_id] %>
13
+ <%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
14
+ <% end %>
15
+ </p>
16
+ <% end %>
@@ -0,0 +1,48 @@
1
+ <% unless @only_chart %>
2
+ <%= render partial: "caching" %>
3
+ <p class="text-muted" style="margin-bottom: 10px;">
4
+ <%= pluralize(@rows.size, "cohort") %>
5
+ </p>
6
+ <% end %>
7
+ <% if @rows.any? %>
8
+ <div class="results-container">
9
+ <table class="table results-table">
10
+ <thead>
11
+ <tr>
12
+ <th style="min-width: 100px;">Cohort</th>
13
+ <% 12.times do |i| %>
14
+ <th style="width: 7.5%; text-align: right;"><%= @conversion_period.titleize %> <%= i + 1 %></th>
15
+ <% end %>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% @rows.each do |row| %>
20
+ <tr>
21
+ <td>
22
+ <%= row[0] %>
23
+ <div style="font-size: 12px; color: #999;"><%= row[1] == 1 ? "1 user" : "#{number_with_delimiter(row[1])} users" %></div>
24
+ </td>
25
+ <% 12.times do |i| %>
26
+ <td style="text-align: right;">
27
+ <% num = row[i + 2] %>
28
+ <% if num %>
29
+ <% denom = row[1] %>
30
+ <% if denom > 0 %>
31
+ <%= (100.0 * num / denom).round %>%
32
+ <% else %>
33
+ -
34
+ <% end %>
35
+ <div style="font-size: 12px; color: #999;"><%= number_with_delimiter(num) %></div>
36
+ <% else %>
37
+ -
38
+ <% end %>
39
+ </td>
40
+ <% end %>
41
+ </tr>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+ </div>
46
+ <% elsif @only_chart %>
47
+ <p class="text-muted">No cohorts</p>
48
+ <% end %>
@@ -129,3 +129,9 @@
129
129
  </table>
130
130
 
131
131
  <p>Use the column name <code>target</code> to draw a line for goals.</p>
132
+
133
+ <% if @data_source.supports_cohort_analysis? %>
134
+ <h2>Cohort Analysis</h2>
135
+
136
+ <p>Create a query with the comment <code>/* cohort analysis */</code>. The result should have columns named <code>user_id</code> and <code>conversion_time</code> and optionally <code>cohort_time</code>.</p>
137
+ <% end %>
@@ -19,6 +19,9 @@
19
19
  </button>
20
20
  <ul class="dropdown-menu">
21
21
  <li><%= link_to "Checks", checks_path %></li>
22
+ <% if Blazer.uploads? %>
23
+ <li><%= link_to "Uploads", uploads_path %></li>
24
+ <% end %>
22
25
  <li role="separator" class="divider"></li>
23
26
  <li><%= link_to "New Dashboard", new_dashboard_path %></li>
24
27
  <li><%= link_to "New Check", new_check_path %></li>
@@ -6,25 +6,20 @@
6
6
  <% else %>
7
7
  <div class="alert alert-info">Can’t preview queries with variables...yet!</div>
8
8
  <% end %>
9
+ <% elsif @cohort_analysis %>
10
+ <% if @cohort_error %>
11
+ <div class="alert alert-info"><%= @cohort_error %></div>
12
+ <% else %>
13
+ <%= render partial: "cohorts" %>
14
+ <% end %>
9
15
  <% else %>
10
16
  <% unless @only_chart %>
11
- <% if @cached_at || @just_cached %>
12
- <p class="text-muted" style="float: right;">
13
- <% if @cached_at %>
14
- Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
15
- <% elsif params[:query_id] %>
16
- Cached just now
17
- <% if @data_source.cache_mode == "slow" %>
18
- (over <%= "%g" % @data_source.cache_slow_threshold %>s)
19
- <% end %>
20
- <% end %>
21
-
22
- <% if @query && params[:query_id] %>
23
- <%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
24
- <% end %>
25
- </p>
26
- <% end %>
17
+ <%= render partial: "caching" %>
27
18
  <p class="text-muted" style="margin-bottom: 10px;">
19
+ <% if @row_limit && @rows.size > @row_limit %>
20
+ First
21
+ <% @rows = @rows.first(@row_limit) %>
22
+ <% end %>
28
23
  <%= pluralize(@rows.size, "row") %>
29
24
 
30
25
  <% @checks.select(&:state).each do |check| %>
@@ -43,6 +38,9 @@
43
38
  <% if @forecast_error %>
44
39
  <div class="alert alert-danger"><%= @forecast_error %></div>
45
40
  <% end %>
41
+ <% if @cohort_error %>
42
+ <div class="alert alert-info"><%= @cohort_error %></div>
43
+ <% end %>
46
44
  <% if @rows.any? %>
47
45
  <% values = @rows.first %>
48
46
  <% chart_id = SecureRandom.hex %>
@@ -147,7 +145,7 @@
147
145
  <% elsif @columns == ["PLAN"] && @data_source.adapter == "druid" %>
148
146
  <pre><code><%= @rows[0][0] %></code></pre>
149
147
  <% else %>
150
- <table class="table results-table" style="margin-bottom: 0;">
148
+ <table class="table results-table">
151
149
  <thead>
152
150
  <tr>
153
151
  <% @columns.each_with_index do |key, i| %>
@@ -14,7 +14,7 @@
14
14
  <%= link_to "Fork", new_query_path(variable_params(@query).merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %>
15
15
 
16
16
  <% if !@error && @success %>
17
- <%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast]), params: {statement: @statement}, class: "btn btn-primary" %>
17
+ <%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast], cohort_period: params[:cohort_period]), params: {statement: @statement}, class: "btn btn-primary" %>
18
18
  <% end %>
19
19
  </div>
20
20
  </div>
@@ -0,0 +1,27 @@
1
+ <%= form_for @upload, html: {class: "small-form"} do |f| %>
2
+ <% if @upload.errors.any? %>
3
+ <div class="alert alert-danger"><%= @upload.errors.full_messages.first %></div>
4
+ <% elsif !@upload.persisted? %>
5
+ <p>Create a database table from a CSV file. The table will be created in the <code><%= Blazer.settings["uploads"]["schema"] %></code> schema.</p>
6
+ <% end %>
7
+
8
+ <div class="form-group">
9
+ <%= f.label :table %>
10
+ <%= f.text_field :table, class: "form-control" %>
11
+ </div>
12
+ <div class="form-group">
13
+ <%= f.label :description %>
14
+ <%= f.text_area :description, placeholder: "Optional", style: "height: 60px;", class: "form-control" %>
15
+ </div>
16
+ <div class="form-group">
17
+ <%= f.label :file %>
18
+ <%= f.file_field :file, accept: "text/csv", style: "margin-top: 6px; margin-bottom: 21px;" %>
19
+ </div>
20
+ <p>
21
+ <% if @upload.persisted? %>
22
+ <%= link_to "Delete", upload_path(@upload), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
23
+ <% end %>
24
+ <%= f.submit "Save", class: "btn btn-success" %>
25
+ <%= link_to "Back", :back, class: "btn btn-link" %>
26
+ </p>
27
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "Edit Upload" %>
2
+
3
+ <%= render partial: "form" %>
@@ -0,0 +1,55 @@
1
+ <% blazer_title "Uploads" %>
2
+
3
+ <div id="header">
4
+ <div class="pull-right" style="line-height: 34px;">
5
+ <div class="btn-group">
6
+ <%= link_to "New Upload", new_upload_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
+ <li><%= link_to "Checks", checks_path %></li>
14
+ <li role="separator" class="divider"></li>
15
+ <li><%= link_to "New Query", new_query_path %></li>
16
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
17
+ <li><%= link_to "New Check", new_check_path %></li>
18
+ </ul>
19
+ </div>
20
+ </div>
21
+
22
+ <input id="search" type="text" placeholder="Start typing a table or person" style="width: 300px; display: inline-block;" class="search form-control" />
23
+ </div>
24
+
25
+ <table id="uploads" class="table">
26
+ <thead>
27
+ <tr>
28
+ <th>Table</th>
29
+ <th style="width: 60%;"></th>
30
+ <% if Blazer.user_class %>
31
+ <th style="width: 20%; text-align: right;">Mastermind</th>
32
+ <% end%>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @uploads.each do |upload| %>
37
+ <tr>
38
+ <td><%= link_to upload.table, edit_upload_path(upload) %></td>
39
+ <td><%= truncate(upload.description, length: 100, separator: " ") %></td>
40
+ <% if Blazer.user_class %>
41
+ <td class="creator"><%= blazer_user && upload.creator == blazer_user ? "You" : upload.creator.try(Blazer.user_name) %></td>
42
+ <% end %>
43
+ </tr>
44
+ <% end %>
45
+ </tbody>
46
+ </table>
47
+
48
+ <script>
49
+ $("#search").on("keyup", function() {
50
+ var value = $(this).val().toLowerCase()
51
+ $("#uploads tbody tr").filter( function() {
52
+ $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
53
+ })
54
+ }).focus()
55
+ </script>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "New Upload" %>
2
+
3
+ <%= render partial: "form" %>
data/config/routes.rb CHANGED
@@ -16,5 +16,10 @@ Blazer::Engine.routes.draw do
16
16
  post :refresh, on: :member
17
17
  end
18
18
 
19
+ if Blazer.uploads?
20
+ resources :uploads do
21
+ end
22
+ end
23
+
19
24
  root to: "queries#home"
20
25
  end
data/lib/blazer.rb CHANGED
@@ -18,12 +18,15 @@ require "blazer/adapters/cassandra_adapter"
18
18
  require "blazer/adapters/drill_adapter"
19
19
  require "blazer/adapters/druid_adapter"
20
20
  require "blazer/adapters/elasticsearch_adapter"
21
+ require "blazer/adapters/hive_adapter"
22
+ require "blazer/adapters/ignite_adapter"
21
23
  require "blazer/adapters/influxdb_adapter"
22
24
  require "blazer/adapters/mongodb_adapter"
23
25
  require "blazer/adapters/neo4j_adapter"
24
26
  require "blazer/adapters/presto_adapter"
25
27
  require "blazer/adapters/salesforce_adapter"
26
28
  require "blazer/adapters/soda_adapter"
29
+ require "blazer/adapters/spark_adapter"
27
30
  require "blazer/adapters/sql_adapter"
28
31
  require "blazer/adapters/snowflake_adapter"
29
32
 
@@ -32,6 +35,7 @@ require "blazer/engine"
32
35
 
33
36
  module Blazer
34
37
  class Error < StandardError; end
38
+ class UploadError < Error; end
35
39
  class TimeoutNotSupported < Error; end
36
40
 
37
41
  class << self
@@ -206,6 +210,23 @@ module Blazer
206
210
  slack_webhook_url.present?
207
211
  end
208
212
 
213
+ def self.uploads?
214
+ settings.key?("uploads")
215
+ end
216
+
217
+ def self.uploads_connection
218
+ raise "Empty url for uploads" unless settings.dig("uploads", "url")
219
+ Blazer::UploadsConnection.connection
220
+ end
221
+
222
+ def self.uploads_schema
223
+ settings.dig("uploads", "schema") || "uploads"
224
+ end
225
+
226
+ def self.uploads_table_name(name)
227
+ uploads_connection.quote_table_name("#{uploads_schema}.#{name}")
228
+ end
229
+
209
230
  def self.adapters
210
231
  @adapters ||= {}
211
232
  end
@@ -221,11 +242,14 @@ Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
221
242
  Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
222
243
  Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
223
244
  Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
245
+ Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
246
+ Blazer.register_adapter "ignite", Blazer::Adapters::IgniteAdapter
224
247
  Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
225
248
  Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
226
249
  Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
227
250
  Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
228
251
  Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
229
252
  Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
253
+ Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
230
254
  Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
231
255
  Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter
@@ -43,6 +43,14 @@ module Blazer
43
43
  true # optional
44
44
  end
45
45
 
46
+ def supports_cohort_analysis?
47
+ false # optional
48
+ end
49
+
50
+ def cohort_analysis_statement(statement, period:, days:)
51
+ # optional
52
+ end
53
+
46
54
  protected
47
55
 
48
56
  def settings
@@ -42,9 +42,9 @@ module Blazer
42
42
  end
43
43
  end
44
44
  end
45
- rescue => e
46
- error = e.message
47
- end
45
+ rescue => e
46
+ error = e.message
47
+ end
48
48
 
49
49
  [columns, rows, error]
50
50
  end
@@ -0,0 +1,45 @@
1
+ module Blazer
2
+ module Adapters
3
+ class HiveAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ columns = []
6
+ rows = []
7
+ error = nil
8
+
9
+ begin
10
+ result = client.execute("#{statement} /*#{comment}*/")
11
+ columns = result.any? ? result.first.keys : []
12
+ rows = result.map(&:values)
13
+ rescue => e
14
+ error = e.message
15
+ end
16
+
17
+ [columns, rows, error]
18
+ end
19
+
20
+ def tables
21
+ client.execute("SHOW TABLES").map { |r| r["tab_name"] }
22
+ end
23
+
24
+ def preview_statement
25
+ "SELECT * FROM {table} LIMIT 10"
26
+ end
27
+
28
+ protected
29
+
30
+ def client
31
+ @client ||= begin
32
+ uri = URI.parse(settings["url"])
33
+ Hexspace::Client.new(
34
+ host: uri.host,
35
+ port: uri.port,
36
+ username: uri.user,
37
+ password: uri.password,
38
+ database: uri.path.sub(/\A\//, ""),
39
+ mode: uri.scheme.to_sym
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,54 @@
1
+ module Blazer
2
+ module Adapters
3
+ class IgniteAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ columns = []
6
+ rows = []
7
+ error = nil
8
+
9
+ begin
10
+ result = client.query("#{statement} /*#{comment}*/", schema: default_schema, statement_type: :select, timeout: data_source.timeout)
11
+ columns = result.any? ? result.first.keys : []
12
+ rows = result.map(&:values)
13
+ rescue => e
14
+ error = e.message
15
+ end
16
+
17
+ [columns, rows, error]
18
+ end
19
+
20
+ def preview_statement
21
+ "SELECT * FROM {table} LIMIT 10"
22
+ end
23
+
24
+ def tables
25
+ sql = "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('INFORMATION_SCHEMA', 'SYS')"
26
+ result = data_source.run_statement(sql)
27
+ result.rows.reject { |row| row[1].start_with?("__") }.map do |row|
28
+ (row[0] == default_schema ? row[1] : "#{row[0]}.#{row[1]}").downcase
29
+ end
30
+ end
31
+
32
+ # TODO figure out error
33
+ # Table `__T0` can be accessed only within Ignite query context.
34
+ # def schema
35
+ # sql = "SELECT table_schema, table_name, column_name, data_type, ordinal_position FROM information_schema.columns WHERE table_schema NOT IN ('INFORMATION_SCHEMA', 'SYS')"
36
+ # result = data_source.run_statement(sql)
37
+ # result.rows.group_by { |r| [r[0], r[1]] }.map { |k, vs| {schema: k[0], table: k[1], columns: vs.sort_by { |v| v[2] }.map { |v| {name: v[2], data_type: v[3]} }} }.sort_by { |t| [t[:schema] == default_schema ? "" : t[:schema], t[:table]] }
38
+ # end
39
+
40
+ private
41
+
42
+ def default_schema
43
+ "PUBLIC"
44
+ end
45
+
46
+ def client
47
+ @client ||= begin
48
+ uri = URI(settings["url"])
49
+ Ignite::Client.new(host: uri.host, port: uri.port, username: uri.user, password: uri.password)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end