blazer 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of blazer might be problematic. Click here for more details.

Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +94 -42
  4. data/app/assets/images/blazer/favicon.png +0 -0
  5. data/app/assets/javascripts/blazer/routes.js +3 -0
  6. data/app/assets/stylesheets/blazer/application.css +30 -6
  7. data/app/controllers/blazer/dashboards_controller.rb +0 -4
  8. data/app/controllers/blazer/queries_controller.rb +16 -7
  9. data/app/helpers/blazer/base_helper.rb +1 -1
  10. data/app/views/blazer/_nav.html.erb +0 -1
  11. data/app/views/blazer/_variables.html.erb +6 -4
  12. data/app/views/blazer/checks/_form.html.erb +7 -7
  13. data/app/views/blazer/checks/edit.html.erb +2 -0
  14. data/app/views/blazer/checks/index.html.erb +29 -3
  15. data/app/views/blazer/checks/new.html.erb +2 -0
  16. data/app/views/blazer/dashboards/_form.html.erb +4 -4
  17. data/app/views/blazer/dashboards/edit.html.erb +2 -0
  18. data/app/views/blazer/dashboards/new.html.erb +2 -0
  19. data/app/views/blazer/dashboards/show.html.erb +7 -3
  20. data/app/views/blazer/queries/_form.html.erb +11 -6
  21. data/app/views/blazer/queries/docs.html.erb +63 -70
  22. data/app/views/blazer/queries/home.html.erb +11 -4
  23. data/app/views/blazer/queries/run.html.erb +36 -6
  24. data/app/views/blazer/queries/schema.html.erb +58 -0
  25. data/app/views/blazer/queries/show.html.erb +3 -3
  26. data/app/views/layouts/blazer/application.html.erb +1 -1
  27. data/config/routes.rb +5 -1
  28. data/lib/blazer.rb +9 -0
  29. data/lib/blazer/engine.rb +2 -0
  30. data/lib/blazer/result.rb +62 -29
  31. data/lib/blazer/version.rb +1 -1
  32. data/lib/generators/blazer/templates/config.yml.tt +8 -0
  33. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71916198bf2c9162e234001aa3a76c5620c1c445d30377276dab114cbee6a4e7
4
- data.tar.gz: abc445f95958ddec14cc49e6f39ab41993d0ebe10536b0b4349bf60cfcdf63c1
3
+ metadata.gz: 9f1910484a7cc50172c7b6dface23cc593514fcf9382abc726da8b08169af1e9
4
+ data.tar.gz: 98db8372f805a616a944d7c4bf595c3a6cf4d9ee138e150eb54e7a27fed5dd48
5
5
  SHA512:
6
- metadata.gz: 45b008e981848ae5ab0b6cffffeb1954359c3c7815511d5025c8ca63def983341dbc2a0f1a97f71d9ae883ba058934656559db69feb96fbc39b7d38b13173698
7
- data.tar.gz: 73c09348ab9ac9556283585dbc0b74bef4bb58f415798023e710c5de489c9b548af4d7830acdf4250f84a0f70c3ed6b44f2636b11fe6758bee0d3058af1bda95
6
+ metadata.gz: 373a56a0559c54617a03d18df7849d68840fb6275f5b3150bc35d800ee5f3efaca15bd5d3aaec5e4df25c3a3c450e2c98673ec9a5242979ed02565fc9c6c66e3
7
+ data.tar.gz: b8118434afa964ff808252e4cb2bf2855d3088b718e18675c9efb32a7be3934082558b703354f1f160687a8416fd068c60bf88af905dd5ebe8326c9919788b7b
@@ -1,3 +1,14 @@
1
+ ## 2.0.1
2
+
3
+ - Added favicon
4
+ - Added search for checks and schema
5
+ - Added pie charts
6
+ - Added Trend anomaly detection
7
+ - Added forecasting
8
+ - Improved tooltips
9
+ - Improved docs for new installs
10
+ - Fixed error with canceling queries
11
+
1
12
  ## 2.0.0
2
13
 
3
14
  - Added support for Slack
data/README.md CHANGED
@@ -6,6 +6,8 @@ Explore your data with SQL. Easily create charts and dashboards, and share them
6
6
 
7
7
  [![Screenshot](https://blazer.dokkuapp.com/assets/screenshot-6ca3115a518b488026e48be83ba0d4c9.png)](https://blazer.dokkuapp.com)
8
8
 
9
+ Blazer 2.0 was recently released! See [instructions for upgrading](#20).
10
+
9
11
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
10
12
 
11
13
  ## Features
@@ -92,45 +94,6 @@ BLAZER_SLACK_WEBHOOK_URL=https://hooks.slack.com/...
92
94
 
93
95
  Name the webhook “Blazer” and add a cool icon.
94
96
 
95
- ## Permissions
96
-
97
- ### PostgreSQL
98
-
99
- Create a user with read only permissions:
100
-
101
- ```sql
102
- BEGIN;
103
- CREATE ROLE blazer LOGIN PASSWORD 'secret123';
104
- GRANT CONNECT ON DATABASE database_name TO blazer;
105
- GRANT USAGE ON SCHEMA public TO blazer;
106
- GRANT SELECT ON ALL TABLES IN SCHEMA public TO blazer;
107
- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO blazer;
108
- COMMIT;
109
- ```
110
-
111
- ### MySQL
112
-
113
- Create a user with read only permissions:
114
-
115
- ```sql
116
- GRANT SELECT, SHOW VIEW ON database_name.* TO blazer@’127.0.0.1′ IDENTIFIED BY ‘secret123‘;
117
- FLUSH PRIVILEGES;
118
- ```
119
-
120
- ### MongoDB
121
-
122
- Create a user with read only permissions:
123
-
124
- ```
125
- db.createUser({user: "blazer", pwd: "password", roles: ["read"]})
126
- ```
127
-
128
- Also, make sure authorization is enabled when you start the server.
129
-
130
- ### Sensitive Data
131
-
132
- Check out [Hypershield](https://github.com/ankane/hypershield) to shield sensitive data.
133
-
134
97
  ## Authentication
135
98
 
136
99
  Don’t forget to protect the dashboard in production.
@@ -171,6 +134,47 @@ end
171
134
 
172
135
  Be sure to render or redirect for unauthorized users.
173
136
 
137
+ ## Permissions
138
+
139
+ Blazer runs each query in a transaction and rolls it back to prevent queries from modifying data. As an additional line of defense, we recommend using a read only user.
140
+
141
+ ### PostgreSQL
142
+
143
+ Create a user with read only permissions:
144
+
145
+ ```sql
146
+ BEGIN;
147
+ CREATE ROLE blazer LOGIN PASSWORD 'secret123';
148
+ GRANT CONNECT ON DATABASE database_name TO blazer;
149
+ GRANT USAGE ON SCHEMA public TO blazer;
150
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO blazer;
151
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO blazer;
152
+ COMMIT;
153
+ ```
154
+
155
+ ### MySQL
156
+
157
+ Create a user with read only permissions:
158
+
159
+ ```sql
160
+ GRANT SELECT, SHOW VIEW ON database_name.* TO blazer@’127.0.0.1′ IDENTIFIED BY ‘secret123‘;
161
+ FLUSH PRIVILEGES;
162
+ ```
163
+
164
+ ### MongoDB
165
+
166
+ Create a user with read only permissions:
167
+
168
+ ```
169
+ db.createUser({user: "blazer", pwd: "password", roles: ["read"]})
170
+ ```
171
+
172
+ Also, make sure authorization is enabled when you start the server.
173
+
174
+ ## Sensitive Data
175
+
176
+ If your database contains sensitive or personal data, check out [Hypershield](https://github.com/ankane/hypershield) to shield it.
177
+
174
178
  ## Queries
175
179
 
176
180
  ### Variables
@@ -314,12 +318,20 @@ SELECT gender, zip_code, COUNT(*) FROM users GROUP BY 1, 2
314
318
 
315
319
  ### Scatter Chart
316
320
 
317
- 2 columns - both numeric
321
+ 2 columns - both numeric - [Example](https://blazer.dokkuapp.com/queries/16-scatter-chart)
318
322
 
319
323
  ```sql
320
324
  SELECT x, y FROM table
321
325
  ```
322
326
 
327
+ ### Pie Chart
328
+
329
+ 2 columns - string, numeric - and last column named `pie` - [Example](https://blazer.dokkuapp.com/queries/17-pie-chart)
330
+
331
+ ```sql
332
+ SELECT gender, COUNT(*) AS pie FROM users GROUP BY 1
333
+ ```
334
+
323
335
  ### Maps
324
336
 
325
337
  Columns named `latitude` and `longitude` or `lat` and `lon` or `lat` and `lng` - [Example](https://blazer.dokkuapp.com/queries/15-map)
@@ -360,7 +372,27 @@ Then create check with optional emails if you want to be notified. Emails are se
360
372
 
361
373
  ## Anomaly Detection
362
374
 
363
- Anomaly detection is supported thanks to Twitter’s [AnomalyDetection](https://github.com/twitter/AnomalyDetection) library.
375
+ Blazer supports two different approaches to anomaly detection.
376
+
377
+ ### Trend
378
+
379
+ [Trend](https://trendapi.org/) is easiest to set up but uses an external service.
380
+
381
+ Add [trend](https://github.com/ankane/trend) to your Gemfile:
382
+
383
+ ```ruby
384
+ gem 'trend'
385
+ ```
386
+
387
+ And add to `config/blazer.yml`:
388
+
389
+ ```yml
390
+ anomaly_checks: trend
391
+ ```
392
+
393
+ ### R
394
+
395
+ R is harder to set up but doesn’t use an external service. It uses Twitter’s [AnomalyDetection](https://github.com/twitter/AnomalyDetection) library.
364
396
 
365
397
  First, [install R](https://cloud.r-project.org/). Then, run:
366
398
 
@@ -372,13 +404,33 @@ devtools::install_github("twitter/AnomalyDetection")
372
404
  And add to `config/blazer.yml`:
373
405
 
374
406
  ```yml
375
- anomaly_checks: true
407
+ anomaly_checks: r
376
408
  ```
377
409
 
378
410
  If upgrading from version 1.4 or below, also follow the [upgrade instructions](#15).
379
411
 
380
412
  If you’re on Heroku, follow [these additional instructions](#anomaly-detection-on-heroku).
381
413
 
414
+ ## Forecasting
415
+
416
+ Blazer has experimental support for forecasting through [Trend](https://trendapi.org/).
417
+
418
+ [Example](https://blazer.dokkuapp.com/queries/18-forecast?forecast=t)
419
+
420
+ Add [trend](https://github.com/ankane/trend) to your Gemfile:
421
+
422
+ ```ruby
423
+ gem 'trend'
424
+ ```
425
+
426
+ And add to `config/blazer.yml`:
427
+
428
+ ```yml
429
+ forecasting: trend
430
+ ```
431
+
432
+ A forecast link will appear for queries that return 2 columns with types timestamp and numeric.
433
+
382
434
  ## Data Sources
383
435
 
384
436
  Blazer supports multiple data sources :tada:
@@ -6,6 +6,9 @@ var Routes = {
6
6
  return rootPath + "queries/cancel"
7
7
  },
8
8
  schema_queries_path: function(params) {
9
+ return rootPath + "queries/schema?" + $.param(params)
10
+ },
11
+ docs_queries_path: function(params) {
9
12
  return rootPath + "queries/docs?" + $.param(params)
10
13
  },
11
14
  tables_queries_path: function(params) {
@@ -12,8 +12,8 @@ pre {
12
12
  }
13
13
 
14
14
  body {
15
- padding-top: 20px;
16
- padding-bottom: 20px;
15
+ padding-top: 15px;
16
+ padding-bottom: 15px;
17
17
  }
18
18
 
19
19
  .results-table th {
@@ -146,9 +146,10 @@ input.search:focus {
146
146
  top: 0;
147
147
  left: 0;
148
148
  right: 0;
149
- background-color: whitesmoke;
150
149
  height: 60px;
151
150
  z-index: 1001;
151
+ border-bottom: solid 1px whitesmoke;
152
+ background-color: #fff;
152
153
  }
153
154
 
154
155
  .glyphicon-remove {
@@ -185,7 +186,6 @@ input.search:focus {
185
186
  }
186
187
 
187
188
  .chart-container {
188
- padding-top: 10px;
189
189
  clear: both;
190
190
  }
191
191
 
@@ -201,10 +201,34 @@ input.search:focus {
201
201
  color: red;
202
202
  }
203
203
 
204
+ .small-form {
205
+ margin-right: auto;
206
+ margin-left: auto;
207
+ max-width: 400px;
208
+ }
209
+
210
+ .alert {
211
+ padding-top: 8px;
212
+ padding-bottom: 8px;
213
+ }
214
+
215
+ h1, h2, h3, h4, p, hr, .table, .navbar, #header, .alert, .form-group {
216
+ margin-top: 0;
217
+ margin-bottom: 15px;
218
+ }
219
+
220
+ .double-margin, .chart-container {
221
+ margin-bottom: 30px;
222
+ }
223
+
204
224
  h1 {
205
- font-size: 32px;
225
+ font-size: 24px;
206
226
  }
207
227
 
208
228
  h2 {
209
- font-size: 24px;
229
+ font-size: 20px;
230
+ }
231
+
232
+ .schema-table {
233
+ max-width: 500px;
210
234
  }
@@ -2,10 +2,6 @@ module Blazer
2
2
  class DashboardsController < BaseController
3
3
  before_action :set_dashboard, only: [:show, :edit, :update, :destroy, :refresh]
4
4
 
5
- def index
6
- redirect_to root_path(filter: "dashboards")
7
- end
8
-
9
5
  def new
10
6
  @dashboard = Blazer::Dashboard.new
11
7
  end
@@ -1,16 +1,12 @@
1
1
  module Blazer
2
2
  class QueriesController < BaseController
3
3
  before_action :set_query, only: [:show, :edit, :update, :destroy, :refresh]
4
- before_action :set_data_source, only: [:tables, :docs, :cancel]
4
+ before_action :set_data_source, only: [:tables, :docs, :schema, :cancel]
5
5
 
6
6
  def home
7
- if params[:filter] == "dashboards"
8
- @queries = []
9
- else
10
- set_queries(1000)
11
- end
7
+ set_queries(1000)
12
8
 
13
- if params[:filter] && params[:filter] != "dashboards"
9
+ if params[:filter]
14
10
  @dashboards = [] # TODO show my dashboards
15
11
  else
16
12
  @dashboards = Blazer::Dashboard.order(:name)
@@ -137,6 +133,13 @@ module Blazer
137
133
  @cached_at = @result.cached_at
138
134
  @just_cached = @result.just_cached
139
135
 
136
+ @forecast = @query && @result.forecastable? && params[:forecast]
137
+ if @forecast
138
+ @result.forecast
139
+ @forecast_error = @result.forecast_error
140
+ @forecast = @forecast_error.nil?
141
+ end
142
+
140
143
  render_run
141
144
  else
142
145
  @timestamp = Time.now.to_i
@@ -181,6 +184,12 @@ module Blazer
181
184
  end
182
185
 
183
186
  def docs
187
+ @smart_variables = @data_source.smart_variables
188
+ @linked_columns = @data_source.linked_columns
189
+ @smart_columns = @data_source.smart_columns
190
+ end
191
+
192
+ def schema
184
193
  @schema = @data_source.schema
185
194
  end
186
195
 
@@ -12,7 +12,7 @@ module Blazer
12
12
  BLAZER_IMAGE_EXT = %w[png jpg jpeg gif]
13
13
 
14
14
  def blazer_format_value(key, value)
15
- if value.is_a?(Integer) && !key.to_s.end_with?("id") && !key.to_s.start_with?("id")
15
+ if value.is_a?(Numeric) && !key.to_s.end_with?("id") && !key.to_s.start_with?("id")
16
16
  number_with_delimiter(value)
17
17
  elsif value =~ BLAZER_URL_REGEX
18
18
  # see if image or link
@@ -5,7 +5,6 @@
5
5
  <span class="sr-only">Toggle Dropdown</span>
6
6
  </button>
7
7
  <ul class="dropdown-menu">
8
- <li><%= link_to "Dashboards", dashboards_path %></li>
9
8
  <li><%= link_to "Checks", checks_path %></li>
10
9
  <li role="separator" class="divider"></li>
11
10
  <li><%= link_to "New Query", new_query_path %></li>
@@ -8,7 +8,7 @@
8
8
  return moment.tz(time.format(format), timeZone)
9
9
  }
10
10
  </script>
11
- <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 10px;">
11
+ <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
12
12
  <% date_vars = ["start_time", "end_time"] %>
13
13
  <% if (date_vars - @bind_vars).empty? %>
14
14
  <% @bind_vars = @bind_vars - date_vars %>
@@ -49,9 +49,11 @@
49
49
  datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
50
50
  input.val(toDate(picker.startDate).utc().format())
51
51
  submitIfCompleted($("#<%= var %>").closest("form"))
52
- });
53
- var picker = datePicker.data("daterangepicker")
54
- datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
52
+ })
53
+ if (input.val().length > 0) {
54
+ var picker = datePicker.data("daterangepicker")
55
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
56
+ }
55
57
  })()
56
58
  </script>
57
59
  <% else %>
@@ -1,12 +1,12 @@
1
- <% unless @check.respond_to?(:invert) %>
2
- <p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
3
- <% end %>
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 %>
4
5
 
5
- <% if @check.errors.any? %>
6
- <div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
7
- <% end %>
6
+ <% if @check.errors.any? %>
7
+ <div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
8
+ <% end %>
8
9
 
9
- <%= form_for @check do |f| %>
10
10
  <div class="form-group">
11
11
  <%= f.label :query_id, "Query" %>
12
12
  <div class="hide">
@@ -1 +1,3 @@
1
+ <% blazer_title "Edit Check" %>
2
+
1
3
  <%= render partial: "form" %>
@@ -1,9 +1,26 @@
1
1
  <% blazer_title "Checks" %>
2
2
 
3
- <p style="float: right;"><%= link_to "New Check", new_check_path, class: "btn btn-info" %></p>
4
- <%= render partial: "blazer/nav" %>
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
+ <li role="separator" class="divider"></li>
14
+ <li><%= link_to "New Query", new_query_path %></li>
15
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
16
+ </ul>
17
+ </div>
18
+ </div>
5
19
 
6
- <table class="table">
20
+ <input id="search" type="text" placeholder="Start typing a query or state" style="width: 300px; display: inline-block;" class="search form-control" />
21
+ </div>
22
+
23
+ <table id="checks" class="table">
7
24
  <thead>
8
25
  <tr>
9
26
  <th>Query</th>
@@ -41,3 +58,12 @@
41
58
  <% end %>
42
59
  </tbody>
43
60
  </table>
61
+
62
+ <script>
63
+ $("#search").on("keyup", function() {
64
+ var value = $(this).val().toLowerCase()
65
+ $("#checks tbody tr").filter( function() {
66
+ $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
67
+ })
68
+ }).focus()
69
+ </script>
@@ -1 +1,3 @@
1
+ <% blazer_title "New Check" %>
2
+
1
3
  <%= render partial: "form" %>
@@ -1,8 +1,8 @@
1
- <% if @dashboard.errors.any? %>
2
- <div class="alert alert-danger"><%= @dashboard.errors.full_messages.first %></div>
3
- <% end %>
1
+ <%= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, variable_params) : dashboards_path(variable_params)), 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 %>
4
5
 
5
- <%= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, variable_params) : dashboards_path(variable_params)), html: {id: "app"} do |f| %>
6
6
  <div class="form-group">
7
7
  <%= f.label :name %>
8
8
  <%= f.text_field :name, class: "form-control" %>
@@ -1 +1,3 @@
1
+ <% blazer_title "Edit Dashboard" %>
2
+
1
3
  <%= render partial: "form" %>
@@ -1 +1,3 @@
1
+ <% blazer_title "New Dashboard" %>
2
+
1
3
  <%= render partial: "form" %>
@@ -5,7 +5,7 @@
5
5
  <div class="row" style="padding-top: 13px;">
6
6
  <div class="col-sm-9">
7
7
  <%= render partial: "blazer/nav" %>
8
- <h3 style="margin: 0; line-height: 34px; display: inline;">
8
+ <h3 style="line-height: 34px; display: inline; margin-left: 5px;">
9
9
  <%= @dashboard.name %>
10
10
  </h3>
11
11
  </div>
@@ -25,7 +25,11 @@
25
25
  </p>
26
26
  <% end %>
27
27
 
28
- <%= render partial: "blazer/variables", locals: {action: dashboard_path(@dashboard)} %>
28
+ <% if @bind_vars.any? %>
29
+ <%= render partial: "blazer/variables", locals: {action: dashboard_path(@dashboard)} %>
30
+ <% else %>
31
+ <div style="padding-bottom: 15px;"></div>
32
+ <% end %>
29
33
 
30
34
  <% @queries.each_with_index do |query, i| %>
31
35
  <div class="chart-container">
@@ -35,7 +39,7 @@
35
39
  </div>
36
40
  </div>
37
41
  <script>
38
- <%= blazer_js_var "data", {statement: query.statement, query_id: query.id, only_chart: true} %>
42
+ <%= blazer_js_var "data", {statement: query.statement, query_id: query.id, data_source: query.data_source, only_chart: true} %>
39
43
 
40
44
  runQuery(data, function (data) {
41
45
  $("#chart-<%= i %>").html(data)
@@ -12,11 +12,13 @@
12
12
  <div id="editor" :style="{ height: editorHeight }"><%= @query.statement %></div>
13
13
  </div>
14
14
  </div>
15
- <div class="form-group text-right">
16
- <div class="pull-left" style="margin-top: 9px;">
15
+ <div class="form-group text-right" style="margin-bottom: 8px;">
16
+ <div class="pull-left" style="margin-top: 8px;">
17
17
  <%= link_to "Back", :back %>
18
+ <a :href="docsPath" target="_blank" style="margin-left: 40px;">Docs</a>
19
+ <a :href="schemaPath" target="_blank" style="margin-left: 40px;">Schema</a>
18
20
  </div>
19
- <a :href="dataSourcePath" target="_blank" style="margin-right: 10px;">Docs</a>
21
+
20
22
  <%= f.select :data_source, Blazer.data_sources.values.select { |ds| q = @query.dup; q.data_source = ds.id; q.editable?(blazer_user) }.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size <= 1), style: "width: 140px;" %>
21
23
  <div id="tables" style="display: inline-block; width: 250px; margin-right: 10px;">
22
24
  <select id="table_names" style="width: 240px;" placeholder="Preview table"></select>
@@ -34,7 +36,7 @@
34
36
  <%= f.label :description %>
35
37
  <%= f.text_area :description, placeholder: "Optional", style: "height: 80px;", class: "form-control" %>
36
38
  </div>
37
- <div class="text-right">
39
+ <div class="form-group text-right">
38
40
  <%= f.submit "For Enter Press", class: "hide" %>
39
41
  <% if @query.persisted? %>
40
42
  <%= link_to "Delete", query_path(@query), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
@@ -49,7 +51,7 @@
49
51
  <% words << pluralize(dashboards_count, "dashboard") if dashboards_count > 0 %>
50
52
  <% words << pluralize(checks_count, "check") if checks_count > 0 %>
51
53
  <% if words.any? %>
52
- <div class="alert alert-info" style="margin-top: 10px; padding: 8px 12px;">
54
+ <div class="alert alert-info" style="margin-bottom: 0;">
53
55
  Part of <%= words.to_sentence %>. Be careful when editing.
54
56
  </div>
55
57
  <% end %>
@@ -79,8 +81,11 @@
79
81
  editorHeight: "180px"
80
82
  },
81
83
  computed: {
82
- dataSourcePath: function() {
84
+ schemaPath: function() {
83
85
  return Routes.schema_queries_path({data_source: this.dataSource})
86
+ },
87
+ docsPath: function() {
88
+ return Routes.docs_queries_path({data_source: this.dataSource})
84
89
  }
85
90
  },
86
91
  methods: {
@@ -1,69 +1,83 @@
1
- <% blazer_title @data_source.name %>
1
+ <% blazer_title "Docs: #{@data_source.name}" %>
2
2
 
3
- <h1><%= @data_source.name %></h1>
3
+ <h1>Docs: <%= @data_source.name %></h1>
4
+
5
+ <hr />
4
6
 
5
7
  <h2>Smart Variables</h2>
6
8
 
7
- <p>Use these variable names to get a dropdown.</p>
9
+ <% if @smart_variables.any? %>
10
+ <p>Use these variable names to get a dropdown of values.</p>
8
11
 
9
- <table class="table" style="max-width: 500px;">
10
- <thead>
11
- <tr>
12
- <th>Variable</th>
13
- </tr>
14
- </thead>
15
- <tbody>
16
- <% @data_source.smart_variables.each do |k, _| %>
12
+ <table class="table" style="max-width: 500px;">
13
+ <thead>
17
14
  <tr>
18
- <td><code>{<%= k %>}</code></td>
15
+ <th>Variable</th>
19
16
  </tr>
20
- <% end %>
21
- </tbody>
22
- </table>
17
+ </thead>
18
+ <tbody>
19
+ <% @smart_variables.each do |k, _| %>
20
+ <tr>
21
+ <td><code>{<%= k %>}</code></td>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ </table>
23
26
 
24
- <p>Use <code>{start_time}</code> and <code>{end_time}</code> for a date range selector. End a variable name with <code>_at</code> for a date selector.</p>
27
+ <p>Use <code>{start_time}</code> and <code>{end_time}</code> for a date range selector. End a variable name with <code>_at</code> for a date selector.</p>
28
+ <% else %>
29
+ <p>None set - add them in <code>config/blazer.yml</code>.</p>
30
+ <% end %>
25
31
 
26
32
  <h2>Linked Columns</h2>
27
33
 
28
- <p>Use these column names to link results to other pages.</p>
34
+ <% if @linked_columns.any? %>
35
+ <p>Use these column names to link results to other pages.</p>
29
36
 
30
- <table class="table" style="max-width: 500px;">
31
- <thead>
32
- <tr>
33
- <th style="width: 20%;">Name</th>
34
- <th>URL</th>
35
- </tr>
36
- </thead>
37
- <tbody>
38
- <% @data_source.linked_columns.each do |k, v| %>
37
+ <table class="table" style="max-width: 500px;">
38
+ <thead>
39
39
  <tr>
40
- <td><%= k %></td>
41
- <td><%= v %></td>
40
+ <th style="width: 20%;">Name</th>
41
+ <th>URL</th>
42
42
  </tr>
43
- <% end %>
44
- </tbody>
45
- </table>
43
+ </thead>
44
+ <tbody>
45
+ <% @linked_columns.each do |k, v| %>
46
+ <tr>
47
+ <td><%= k %></td>
48
+ <td><%= v %></td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
46
53
 
47
- <p>Values that match the format of a URL will be linked automatically.</p>
54
+ <p>Values that match the format of a URL will be linked automatically.</p>
55
+ <% else %>
56
+ <p>None set - add them in <code>config/blazer.yml</code>.</p>
57
+ <% end %>
48
58
 
49
59
  <h2>Smart Columns</h2>
50
60
 
51
- <p>Use these column names to show additional data.</p>
61
+ <% if @smart_columns.any? %>
62
+ <p>Use these column names to show additional data.</p>
52
63
 
53
- <table class="table" style="max-width: 500px;">
54
- <thead>
55
- <tr>
56
- <th>Name</th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- <% @data_source.smart_columns.each do |k, _| %>
64
+ <table class="table" style="max-width: 500px;">
65
+ <thead>
61
66
  <tr>
62
- <td><%= k %></td>
67
+ <th>Name</th>
63
68
  </tr>
64
- <% end %>
65
- </tbody>
66
- </table>
69
+ </thead>
70
+ <tbody>
71
+ <% @smart_columns.each do |k, _| %>
72
+ <tr>
73
+ <td><%= k %></td>
74
+ </tr>
75
+ <% end %>
76
+ </tbody>
77
+ </table>
78
+ <% else %>
79
+ <p>None set - add them in <code>config/blazer.yml</code>.</p>
80
+ <% end %>
67
81
 
68
82
  <h2>Charts</h2>
69
83
 
@@ -97,6 +111,10 @@
97
111
  <td>Scatter</td>
98
112
  <td>2 columns - both numeric</td>
99
113
  </tr>
114
+ <tr>
115
+ <td>Pie</td>
116
+ <td>2 columns - string, numeric - and last column named <code>pie</code></td>
117
+ </tr>
100
118
  <tr>
101
119
  <td>Map</td>
102
120
  <td>
@@ -111,28 +129,3 @@
111
129
  </table>
112
130
 
113
131
  <p>Use the column name <code>target</code> to draw a line for goals.</p>
114
-
115
- <h2>Schema</h2>
116
-
117
- <% @schema.each do |table| %>
118
- <table class="table" style="max-width: 500px;">
119
- <thead>
120
- <tr>
121
- <th colspan="2">
122
- <%= table[:table] %>
123
- <% if table[:schema] != "public" %>
124
- <span class="text-muted" style="font-weight: normal;"><%= table[:schema] %></span>
125
- <% end %>
126
- </th>
127
- </tr>
128
- </thead>
129
- <tbody>
130
- <% table[:columns].each do |column| %>
131
- <tr>
132
- <td style="width: 60%;"><%= column[:name] %></td>
133
- <td class="text-muted"><%= column[:data_type] %></td>
134
- </tr>
135
- <% end %>
136
- </tbody>
137
- </table>
138
- <% end %>
@@ -1,6 +1,6 @@
1
1
  <div id="queries">
2
- <div id="header" style="margin-bottom: 20px;">
3
- <div class="pull-right">
2
+ <div id="header">
3
+ <div class="pull-right" style="line-height: 34px;">
4
4
  <% if blazer_user %>
5
5
  <%= link_to "All", root_path, class: !params[:filter] ? "active" : nil, style: "margin-right: 40px;" %>
6
6
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  <%= link_to "Mine", root_path(filter: "mine"), class: params[:filter] == "mine" ? "active" : nil, style: "margin-right: 40px;" %>
12
12
  <% end %>
13
+
13
14
  <div class="btn-group">
14
15
  <%= link_to "New Query", new_query_path, class: "btn btn-info" %>
15
16
  <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -17,7 +18,6 @@
17
18
  <span class="sr-only">Toggle Dropdown</span>
18
19
  </button>
19
20
  <ul class="dropdown-menu">
20
- <li><%= link_to "Dashboards", dashboards_path %></li>
21
21
  <li><%= link_to "Checks", checks_path %></li>
22
22
  <li role="separator" class="divider"></li>
23
23
  <li><%= link_to "New Dashboard", new_dashboard_path %></li>
@@ -25,7 +25,7 @@
25
25
  </ul>
26
26
  </div>
27
27
  </div>
28
- <input type="text" v-model="searchTerm" placeholder="Start typing a query or person" style="width: 300px; display: inline-block;" autofocus=true class="search form-control" />
28
+ <input type="text" v-model="searchTerm" placeholder="Start typing a query, dashboard, or person" style="width: 300px; display: inline-block;" v-focus class="search form-control" />
29
29
  </div>
30
30
 
31
31
  <table class="table">
@@ -147,6 +147,13 @@
147
147
  return Routes.query_path(item.to_param)
148
148
  }
149
149
  }
150
+ },
151
+ directives: {
152
+ focus: {
153
+ inserted: function (el) {
154
+ el.focus()
155
+ }
156
+ }
150
157
  }
151
158
  })
152
159
  </script>
@@ -24,7 +24,7 @@
24
24
  <% end %>
25
25
  </p>
26
26
  <% end %>
27
- <p class="text-muted">
27
+ <p class="text-muted" style="margin-bottom: 10px;">
28
28
  <%= pluralize(@rows.size, "row") %>
29
29
 
30
30
  <% @checks.select(&:state).each do |check| %>
@@ -33,19 +33,46 @@
33
33
  &middot; <%= check.message %>
34
34
  <% end %>
35
35
  <% end %>
36
+
37
+ <% if @query && @result.forecastable? && !params[:forecast] %>
38
+ &middot;
39
+ <%= link_to "Forecast", query_path(@query, {forecast: "t"}.merge(variable_params)) %>
40
+ <% end %>
36
41
  </p>
37
42
  <% end %>
43
+ <% if @forecast_error %>
44
+ <div class="alert alert-danger"><%= @forecast_error %></div>
45
+ <% end %>
38
46
  <% if @rows.any? %>
39
47
  <% values = @rows.first %>
40
48
  <% chart_id = SecureRandom.hex %>
41
49
  <% column_types = @result.column_types %>
42
50
  <% chart_type = @result.chart_type %>
43
- <% chart_options = {id: chart_id, min: nil} %>
51
+ <% chart_options = {id: chart_id} %>
52
+ <% if ["line", "line2"].include?(chart_type) %>
53
+ <% chart_options.merge!(min: nil) %>
54
+ <% end %>
55
+ <% if chart_type == "scatter" %>
56
+ <% chart_options.merge!(library: {tooltips: {intersect: false}}) %>
57
+ <% elsif ["bar", "bar2"].include?(chart_type) %>
58
+ <% chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}}) %>
59
+ <% elsif chart_type != "pie" %>
60
+ <% if column_types.size == 2 || @forecast %>
61
+ <% chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}}) %>
62
+ <% else %>
63
+ <%# chartjs axis: 'x' has poor behavior with multiple series %>
64
+ <% chart_options.merge!(library: {tooltips: {intersect: false}}) %>
65
+ <% end %>
66
+ <% end %>
44
67
  <% series_library = {} %>
45
68
  <% target_index = @columns.index { |k| k.downcase == "target" } %>
46
69
  <% if target_index %>
47
70
  <% series_library[target_index - 1] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"} %>
48
71
  <% end %>
72
+ <% if @forecast %>
73
+ <% color = "#54a3ee" %>
74
+ <% series_library[1] = {borderDash: [8], borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color} %>
75
+ <% end %>
49
76
  <% if blazer_maps? && @markers.any? %>
50
77
  <div id="map" style="height: <%= @only_chart ? 300 : 500 %>px;"></div>
51
78
  <script>
@@ -76,11 +103,14 @@
76
103
  map.fitBounds(featureLayer.getBounds());
77
104
  </script>
78
105
  <% elsif chart_type == "line" %>
79
- <%= line_chart @columns[1..-1].each_with_index.map{ |k, i| {name: blazer_series_name(k), data: @rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} }, chart_options %>
106
+ <% chart_data = @columns[1..-1].each_with_index.map{ |k, i| {name: blazer_series_name(k), data: @rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} } %>
107
+ <%= line_chart chart_data, chart_options %>
80
108
  <% elsif chart_type == "line2" %>
81
109
  <%= line_chart @rows.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, chart_options %>
110
+ <% elsif chart_type == "pie" %>
111
+ <%= pie_chart @rows.map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, chart_options %>
82
112
  <% elsif chart_type == "bar" %>
83
- <%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, id: chart_id %>
113
+ <%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, chart_options %>
84
114
  <% elsif chart_type == "bar2" %>
85
115
  <% first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1) %>
86
116
  <% labels = first_20.map { |r| r[0] }.uniq %>
@@ -90,9 +120,9 @@
90
120
  <% first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s } %>
91
121
  <% end %>
92
122
  <% end %>
93
- <%= column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, id: chart_id %>
123
+ <%= column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, chart_options %>
94
124
  <% elsif chart_type == "scatter" %>
95
- <%= scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], id: chart_id %>
125
+ <%= scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], **chart_options %>
96
126
  <% elsif @only_chart %>
97
127
  <% if @rows.size == 1 && @rows.first.size == 1 %>
98
128
  <% v = @rows.first.first %>
@@ -0,0 +1,58 @@
1
+ <% blazer_title "Schema: #{@data_source.name}" %>
2
+
3
+ <h1>Schema: <%= @data_source.name %></h1>
4
+
5
+ <hr />
6
+
7
+ <div id="header">
8
+ <input id="search" type="text" placeholder="Start typing a table or column" style="width: 300px; display: inline-block;" class="search form-control" />
9
+ </div>
10
+
11
+ <% @schema.each do |table| %>
12
+ <table class="table schema-table">
13
+ <thead>
14
+ <tr>
15
+ <th colspan="2">
16
+ <%= table[:table] %>
17
+ <% if table[:schema] != "public" %>
18
+ <span class="text-muted" style="font-weight: normal;"><%= table[:schema] %></span>
19
+ <% end %>
20
+ </th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ <% table[:columns].each do |column| %>
25
+ <tr>
26
+ <td style="width: 60%;"><%= column[:name] %></td>
27
+ <td class="text-muted"><%= column[:data_type] %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ <% end %>
33
+
34
+ <script>
35
+ $("#search").on("keyup", function() {
36
+ var value = $(this).val().toLowerCase()
37
+ $(".schema-table").filter(function() {
38
+ // if found in table name, show entire table
39
+ // if just found in rows, show row
40
+
41
+ var found = $(this).find("thead").text().toLowerCase().indexOf(value) > -1
42
+
43
+ if (found) {
44
+ $(this).find("tbody tr").toggle(true)
45
+ } else {
46
+ $(this).find("tbody tr").filter(function() {
47
+ var found2 = $(this).text().toLowerCase().indexOf(value) > -1
48
+ $(this).toggle(found2)
49
+ if (found2) {
50
+ found = true
51
+ }
52
+ })
53
+ }
54
+
55
+ $(this).toggle(found)
56
+ })
57
+ }).focus()
58
+ </script>
@@ -5,7 +5,7 @@
5
5
  <div class="row" style="padding-top: 13px;">
6
6
  <div class="col-sm-9">
7
7
  <%= render partial: "blazer/nav" %>
8
- <h3 style="margin: 0; line-height: 34px; display: inline;">
8
+ <h3 style="line-height: 34px; display: inline; margin-left: 5px;">
9
9
  <%= @query.name %>
10
10
  </h3>
11
11
  </div>
@@ -14,7 +14,7 @@
14
14
  <%= link_to "Fork", new_query_path(variable_params.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"), params: {statement: @statement}, class: "btn btn-primary" %>
17
+ <%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast]), params: {statement: @statement}, class: "btn btn-primary" %>
18
18
  <% end %>
19
19
  </div>
20
20
  </div>
@@ -56,7 +56,7 @@
56
56
  $("#results").addClass("query-error").html(message)
57
57
  }
58
58
 
59
- <%= blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id) %>
59
+ <%= blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id, data_source: @query.data_source) %>
60
60
 
61
61
  runQuery(data, showRun, showError)
62
62
  </script>
@@ -4,7 +4,7 @@
4
4
  <title><%= blazer_title ? blazer_title : "Blazer" %></title>
5
5
 
6
6
  <meta charset="utf-8" />
7
-
7
+ <%= favicon_link_tag "blazer/favicon.png" %>
8
8
  <%= stylesheet_link_tag "blazer/application" %>
9
9
  <%= javascript_include_tag "blazer/application" %>
10
10
  <script>
@@ -4,13 +4,17 @@ Blazer::Engine.routes.draw do
4
4
  post :cancel, on: :collection
5
5
  post :refresh, on: :member
6
6
  get :tables, on: :collection
7
+ get :schema, on: :collection
7
8
  get :docs, on: :collection
8
9
  end
10
+
9
11
  resources :checks, except: [:show] do
10
12
  get :run, on: :member
11
13
  end
12
- resources :dashboards do
14
+
15
+ resources :dashboards, except: [:index] do
13
16
  post :refresh, on: :member
14
17
  end
18
+
15
19
  root to: "queries#home"
16
20
  end
@@ -1,11 +1,16 @@
1
+ # dependencies
1
2
  require "csv"
2
3
  require "yaml"
3
4
  require "chartkick"
4
5
  require "safely/core"
6
+
7
+ # modules
5
8
  require "blazer/version"
6
9
  require "blazer/data_source"
7
10
  require "blazer/result"
8
11
  require "blazer/run_statement"
12
+
13
+ # adapters
9
14
  require "blazer/adapters/base_adapter"
10
15
  require "blazer/adapters/athena_adapter"
11
16
  require "blazer/adapters/bigquery_adapter"
@@ -17,6 +22,8 @@ require "blazer/adapters/mongodb_adapter"
17
22
  require "blazer/adapters/presto_adapter"
18
23
  require "blazer/adapters/sql_adapter"
19
24
  require "blazer/adapters/snowflake_adapter"
25
+
26
+ # engine
20
27
  require "blazer/engine"
21
28
 
22
29
  module Blazer
@@ -35,6 +42,7 @@ module Blazer
35
42
  attr_accessor :transform_statement
36
43
  attr_accessor :check_schedules
37
44
  attr_accessor :anomaly_checks
45
+ attr_accessor :forecasting
38
46
  attr_accessor :async
39
47
  attr_accessor :images
40
48
  attr_accessor :query_viewable
@@ -46,6 +54,7 @@ module Blazer
46
54
  self.user_name = :name
47
55
  self.check_schedules = ["5 minutes", "1 hour", "1 day"]
48
56
  self.anomaly_checks = false
57
+ self.forecasting = false
49
58
  self.async = false
50
59
  self.images = false
51
60
  self.override_csp = false
@@ -6,6 +6,7 @@ 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
  app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.(eot|svg|ttf|woff)\z/ }
9
+ app.config.assets.precompile << proc { |path| path == "blazer/favicon.png" }
9
10
 
10
11
  Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
11
12
  Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
@@ -16,6 +17,7 @@ module Blazer
16
17
  Blazer.cache ||= Rails.cache
17
18
 
18
19
  Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
20
+ Blazer.forecasting = Blazer.settings["forecasting"] || false
19
21
  Blazer.async = Blazer.settings["async"] || false
20
22
  if Blazer.async
21
23
  require "blazer/run_statement_job"
@@ -1,6 +1,6 @@
1
1
  module Blazer
2
2
  class Result
3
- attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached
3
+ attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached, :forecast_error
4
4
 
5
5
  def initialize(data_source, columns, rows, error, cached_at, just_cached)
6
6
  @data_source = data_source
@@ -69,6 +69,8 @@ module Blazer
69
69
  "line"
70
70
  elsif column_types == ["time", "string", "numeric"]
71
71
  "line2"
72
+ elsif column_types == ["string", "numeric"] && @columns.last == "pie"
73
+ "pie"
72
74
  elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
73
75
  "bar"
74
76
  elsif column_types == ["string", "string", "numeric"]
@@ -79,6 +81,32 @@ module Blazer
79
81
  end
80
82
  end
81
83
 
84
+ def forecastable?
85
+ @forecastable ||= Blazer.forecasting && column_types == ["time", "numeric"] && @rows.size >= 10
86
+ end
87
+
88
+ def forecast
89
+ # TODO cache it?
90
+ # don't want to put result data (even hashed version)
91
+ # into cache without developer opt-in
92
+ forecast = Trend.forecast(Hash[@rows], count: 30)
93
+ @rows.each do |row|
94
+ row[2] = nil
95
+ end
96
+ @rows.unshift(*forecast.map { |k, v| [k, nil, v] })
97
+ @columns << "forecast"
98
+
99
+ # reset cache
100
+ @column_types = nil
101
+ @chart_type = nil
102
+
103
+ forecast
104
+ rescue => e
105
+ @forecast_error = String.new("Error generating forecast")
106
+ @forecast_error << ": #{e.message.sub("Invalid parameter: ", "")}"
107
+ nil
108
+ end
109
+
82
110
  def detect_anomaly
83
111
  anomaly = nil
84
112
  message = nil
@@ -131,39 +159,44 @@ module Blazer
131
159
  def anomaly?(series)
132
160
  series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
133
161
 
134
- csv_str =
135
- CSV.generate do |csv|
136
- csv << ["timestamp", "count"]
137
- series.each do |row|
138
- csv << row
162
+ if Blazer.anomaly_checks == "trend"
163
+ anomalies = Trend.anomalies(Hash[series])
164
+ anomalies.include?(series.last[0])
165
+ else
166
+ csv_str =
167
+ CSV.generate do |csv|
168
+ csv << ["timestamp", "count"]
169
+ series.each do |row|
170
+ csv << row
171
+ end
139
172
  end
140
- end
141
173
 
142
- r_script = %x[which Rscript].chomp
143
- type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
144
- args = [type, csv_str]
145
- raise "R not found" if r_script.empty?
146
- command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
147
- output = %x[#{command}]
148
- if output.empty?
149
- raise "Unknown R error"
150
- end
174
+ r_script = %x[which Rscript].chomp
175
+ type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
176
+ args = [type, csv_str]
177
+ raise "R not found" if r_script.empty?
178
+ command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
179
+ output = %x[#{command}]
180
+ if output.empty?
181
+ raise "Unknown R error"
182
+ end
151
183
 
152
- rows = CSV.parse(output, headers: true)
153
- error = rows.first && rows.first["x"]
154
- raise error if error
184
+ rows = CSV.parse(output, headers: true)
185
+ error = rows.first && rows.first["x"]
186
+ raise error if error
155
187
 
156
- timestamps = []
157
- if type == "ts"
158
- rows.each do |row|
159
- timestamps << Time.parse(row["timestamp"])
160
- end
161
- timestamps.include?(series.last[0].to_time)
162
- else
163
- rows.each do |row|
164
- timestamps << row["index"].to_i
188
+ timestamps = []
189
+ if type == "ts"
190
+ rows.each do |row|
191
+ timestamps << Time.parse(row["timestamp"])
192
+ end
193
+ timestamps.include?(series.last[0].to_time)
194
+ else
195
+ rows.each do |row|
196
+ timestamps << row["index"].to_i
197
+ end
198
+ timestamps.include?(series.length)
165
199
  end
166
- timestamps.include?(series.length)
167
200
  end
168
201
  end
169
202
  end
@@ -1,3 +1,3 @@
1
1
  module Blazer
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.1"
3
3
  end
@@ -60,3 +60,11 @@ check_schedules:
60
60
  - "1 day"
61
61
  - "1 hour"
62
62
  - "5 minutes"
63
+
64
+ # enable anomaly detection
65
+ # note: with trend, time series are sent to https://trendapi.org
66
+ # anomaly_checks: trend / r
67
+
68
+ # enable forecasting
69
+ # note: with trend, time series are sent to https://trendapi.org
70
+ # forecasting: trend
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blazer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-03 00:00:00.000000000 Z
11
+ date: 2019-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -109,6 +109,7 @@ files:
109
109
  - app/assets/fonts/blazer/glyphicons-halflings-regular.ttf
110
110
  - app/assets/fonts/blazer/glyphicons-halflings-regular.woff
111
111
  - app/assets/fonts/blazer/glyphicons-halflings-regular.woff2
112
+ - app/assets/images/blazer/favicon.png
112
113
  - app/assets/javascripts/blazer/Chart.js
113
114
  - app/assets/javascripts/blazer/Sortable.js
114
115
  - app/assets/javascripts/blazer/ace.js
@@ -171,6 +172,7 @@ files:
171
172
  - app/views/blazer/queries/home.html.erb
172
173
  - app/views/blazer/queries/new.html.erb
173
174
  - app/views/blazer/queries/run.html.erb
175
+ - app/views/blazer/queries/schema.html.erb
174
176
  - app/views/blazer/queries/show.html.erb
175
177
  - app/views/layouts/blazer/application.html.erb
176
178
  - config/routes.rb