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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +94 -42
- data/app/assets/images/blazer/favicon.png +0 -0
- data/app/assets/javascripts/blazer/routes.js +3 -0
- data/app/assets/stylesheets/blazer/application.css +30 -6
- data/app/controllers/blazer/dashboards_controller.rb +0 -4
- data/app/controllers/blazer/queries_controller.rb +16 -7
- data/app/helpers/blazer/base_helper.rb +1 -1
- data/app/views/blazer/_nav.html.erb +0 -1
- data/app/views/blazer/_variables.html.erb +6 -4
- data/app/views/blazer/checks/_form.html.erb +7 -7
- data/app/views/blazer/checks/edit.html.erb +2 -0
- data/app/views/blazer/checks/index.html.erb +29 -3
- data/app/views/blazer/checks/new.html.erb +2 -0
- data/app/views/blazer/dashboards/_form.html.erb +4 -4
- data/app/views/blazer/dashboards/edit.html.erb +2 -0
- data/app/views/blazer/dashboards/new.html.erb +2 -0
- data/app/views/blazer/dashboards/show.html.erb +7 -3
- data/app/views/blazer/queries/_form.html.erb +11 -6
- data/app/views/blazer/queries/docs.html.erb +63 -70
- data/app/views/blazer/queries/home.html.erb +11 -4
- data/app/views/blazer/queries/run.html.erb +36 -6
- data/app/views/blazer/queries/schema.html.erb +58 -0
- data/app/views/blazer/queries/show.html.erb +3 -3
- data/app/views/layouts/blazer/application.html.erb +1 -1
- data/config/routes.rb +5 -1
- data/lib/blazer.rb +9 -0
- data/lib/blazer/engine.rb +2 -0
- data/lib/blazer/result.rb +62 -29
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml.tt +8 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f1910484a7cc50172c7b6dface23cc593514fcf9382abc726da8b08169af1e9
|
4
|
+
data.tar.gz: 98db8372f805a616a944d7c4bf595c3a6cf4d9ee138e150eb54e7a27fed5dd48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 373a56a0559c54617a03d18df7849d68840fb6275f5b3150bc35d800ee5f3efaca15bd5d3aaec5e4df25c3a3c450e2c98673ec9a5242979ed02565fc9c6c66e3
|
7
|
+
data.tar.gz: b8118434afa964ff808252e4cb2bf2855d3088b718e18675c9efb32a7be3934082558b703354f1f160687a8416fd068c60bf88af905dd5ebe8326c9919788b7b
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
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:
|
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:
|
Binary file
|
@@ -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:
|
16
|
-
padding-bottom:
|
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:
|
225
|
+
font-size: 24px;
|
206
226
|
}
|
207
227
|
|
208
228
|
h2 {
|
209
|
-
font-size:
|
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
|
-
|
8
|
-
@queries = []
|
9
|
-
else
|
10
|
-
set_queries(1000)
|
11
|
-
end
|
7
|
+
set_queries(1000)
|
12
8
|
|
13
|
-
if params[:filter]
|
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?(
|
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:
|
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
|
-
|
54
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
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,9 +1,26 @@
|
|
1
1
|
<% blazer_title "Checks" %>
|
2
2
|
|
3
|
-
<
|
4
|
-
|
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
|
-
<
|
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,8 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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" %>
|
@@ -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="
|
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
|
-
|
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:
|
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
|
-
|
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-
|
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
|
-
|
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
|
3
|
+
<h1>Docs: <%= @data_source.name %></h1>
|
4
|
+
|
5
|
+
<hr />
|
4
6
|
|
5
7
|
<h2>Smart Variables</h2>
|
6
8
|
|
7
|
-
|
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
|
-
|
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
|
-
<
|
15
|
+
<th>Variable</th>
|
19
16
|
</tr>
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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
|
-
<
|
41
|
-
<
|
40
|
+
<th style="width: 20%;">Name</th>
|
41
|
+
<th>URL</th>
|
42
42
|
</tr>
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
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
|
-
<
|
67
|
+
<th>Name</th>
|
63
68
|
</tr>
|
64
|
-
|
65
|
-
|
66
|
-
|
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"
|
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;"
|
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
|
· <%= check.message %>
|
34
34
|
<% end %>
|
35
35
|
<% end %>
|
36
|
+
|
37
|
+
<% if @query && @result.forecastable? && !params[:forecast] %>
|
38
|
+
·
|
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
|
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
|
-
|
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]] } } },
|
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]] }} },
|
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],
|
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="
|
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>
|
data/config/routes.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/blazer.rb
CHANGED
@@ -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
|
data/lib/blazer/engine.rb
CHANGED
@@ -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"
|
data/lib/blazer/result.rb
CHANGED
@@ -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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
184
|
+
rows = CSV.parse(output, headers: true)
|
185
|
+
error = rows.first && rows.first["x"]
|
186
|
+
raise error if error
|
155
187
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
data/lib/blazer/version.rb
CHANGED
@@ -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.
|
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-
|
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
|