bnb_blazer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.docker-development-vars +1 -0
- data/.gitignore +31 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +54 -0
- data/Gemfile.lock +233 -0
- data/LICENSE.txt +21 -0
- data/README.md +35 -0
- data/Rakefile +7 -0
- data/app/assets/config/manifest.js +3 -0
- data/app/assets/images/.keep +0 -0
- data/app/assets/images/bnb_intel_logo.png +0 -0
- data/app/assets/stylesheets/application.css +83 -0
- data/app/channels/application_cable/channel.rb +4 -0
- data/app/channels/application_cable/connection.rb +4 -0
- data/app/controllers/application_controller.rb +2 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/helpers/application_helper.rb +2 -0
- data/app/jobs/application_job.rb +2 -0
- data/app/mailers/application_mailer.rb +4 -0
- data/app/models/application_record.rb +3 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/user.rb +2 -0
- data/app/views/blazer/_variables.html.haml +104 -0
- data/app/views/blazer/checks/_form.html.haml +51 -0
- data/app/views/blazer/checks/edit.html.haml +2 -0
- data/app/views/blazer/checks/index.html.haml +50 -0
- data/app/views/blazer/checks/new.html.haml +2 -0
- data/app/views/blazer/dashboards/_form.html.haml +64 -0
- data/app/views/blazer/dashboards/edit.html.haml +2 -0
- data/app/views/blazer/dashboards/new.html.haml +2 -0
- data/app/views/blazer/dashboards/show.html.haml +30 -0
- data/app/views/blazer/queries/_form.html.haml +226 -0
- data/app/views/blazer/queries/edit.html.haml +2 -0
- data/app/views/blazer/queries/home.html.haml +142 -0
- data/app/views/blazer/queries/new.html.haml +2 -0
- data/app/views/blazer/queries/run.html.haml +152 -0
- data/app/views/blazer/queries/show.html.haml +45 -0
- data/app/views/layouts/blazer/_footer.html.haml +2 -0
- data/app/views/layouts/blazer/_navbar.html.haml +25 -0
- data/app/views/layouts/blazer/application.html.haml +21 -0
- data/bin/bundle +3 -0
- data/bin/console +14 -0
- data/bin/rails +9 -0
- data/bin/rake +9 -0
- data/bin/setup +36 -0
- data/bin/spring +17 -0
- data/bin/update +31 -0
- data/bin/yarn +11 -0
- data/bnb_blazer.gemspec +28 -0
- data/config/application.rb +19 -0
- data/config/blazer.yml +79 -0
- data/config/boot.rb +4 -0
- data/config/cable.yml +10 -0
- data/config/credentials.yml.enc +1 -0
- data/config/database.yml +19 -0
- data/config/environment.rb +5 -0
- data/config/environments/development.rb +61 -0
- data/config/environments/production.rb +94 -0
- data/config/environments/test.rb +46 -0
- data/config/initializers/application_controller_renderer.rb +8 -0
- data/config/initializers/assets.rb +14 -0
- data/config/initializers/backtrace_silencers.rb +7 -0
- data/config/initializers/content_security_policy.rb +25 -0
- data/config/initializers/cookies_serializer.rb +5 -0
- data/config/initializers/filter_parameter_logging.rb +4 -0
- data/config/initializers/inflections.rb +16 -0
- data/config/initializers/mime_types.rb +4 -0
- data/config/initializers/wrap_parameters.rb +14 -0
- data/config/locales/en.yml +33 -0
- data/config/puma.rb +37 -0
- data/config/routes.rb +3 -0
- data/config/spring.rb +6 -0
- data/config/storage.yml +34 -0
- data/config.ru +5 -0
- data/db/migrate/20210309231658_install_blazer.rb +47 -0
- data/db/migrate/20210309231908_create_users.rb +13 -0
- data/db/schema.rb +85 -0
- data/db/seeds.rb +45 -0
- data/docker-compose.yml +22 -0
- data/lib/assets/.keep +0 -0
- data/lib/bnb_blazer/bnb_blazer.rb +6 -0
- data/lib/bnb_blazer/version.rb +3 -0
- data/lib/tasks/.keep +0 -0
- data/log/.keep +0 -0
- data/package.json +5 -0
- data/public/404.html +67 -0
- data/public/422.html +67 -0
- data/public/500.html +66 -0
- data/public/apple-touch-icon-precomposed.png +0 -0
- data/public/apple-touch-icon.png +0 -0
- data/public/favicon.ico +0 -0
- data/public/robots.txt +1 -0
- data/tmp/.keep +0 -0
- data/vendor/.keep +0 -0
- metadata +156 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
= form_for @check, html: {class: "small-form"} do |f|
|
2
|
+
- unless @check.respond_to?(:check_type) || @check.respond_to?(:invert)
|
3
|
+
%p.text-muted Checks are designed to identify bad data. A check fails if there are any results.
|
4
|
+
- if @check.errors.any?
|
5
|
+
.alert.alert-danger= @check.errors.full_messages.first
|
6
|
+
.form-group
|
7
|
+
= f.label :query_id, "Query"
|
8
|
+
.hide
|
9
|
+
= f.select :query_id, [], {include_blank: true}
|
10
|
+
:javascript
|
11
|
+
#{blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} }}
|
12
|
+
#{blazer_js_var "items", [@check.query_id].compact}
|
13
|
+
|
14
|
+
$("#check_query_id").selectize({options: queries, items: items, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
|
15
|
+
- if @check.respond_to?(:check_type)
|
16
|
+
.form-group
|
17
|
+
= f.label :check_type, "Alert if"
|
18
|
+
.hide
|
19
|
+
- check_options = [["Any results (bad data)", "bad_data"], ["No results (missing data)", "missing_data"]]
|
20
|
+
- check_options << ["Anomaly (most recent data point)", "anomaly"] if Blazer.anomaly_checks
|
21
|
+
= f.select :check_type, check_options
|
22
|
+
:javascript
|
23
|
+
$("#check_check_type").selectize({}).parent().removeClass("hide");
|
24
|
+
- elsif @check.respond_to?(:invert)
|
25
|
+
.form-group
|
26
|
+
= f.label :invert, "Fails if"
|
27
|
+
.hide
|
28
|
+
= f.select :invert, [["Any results (bad data)", false], ["No results (missing data)", true]]
|
29
|
+
:javascript
|
30
|
+
$("#check_invert").selectize({}).parent().removeClass("hide");
|
31
|
+
- if @check.respond_to?(:schedule) && Blazer.check_schedules
|
32
|
+
.form-group
|
33
|
+
= f.label :schedule, "Run every"
|
34
|
+
.hide
|
35
|
+
= f.select :schedule, Blazer.check_schedules.map { |v| [v, v] }
|
36
|
+
:javascript
|
37
|
+
$("#check_schedule").selectize({}).parent().removeClass("hide");
|
38
|
+
.form-group
|
39
|
+
= f.label :emails
|
40
|
+
= f.text_field :emails, placeholder: "Optional, comma separated", class: "form-control"
|
41
|
+
- if Blazer.slack?
|
42
|
+
.form-group
|
43
|
+
= f.label :slack_channels
|
44
|
+
= f.text_field :slack_channels, placeholder: "Optional, comma separated", class: "form-control"
|
45
|
+
%p.text-muted
|
46
|
+
Emails #{Blazer.slack? ? "and Slack notifications " : nil}are sent when a check starts failing, and when it starts passing again.
|
47
|
+
%p
|
48
|
+
- if @check.persisted?
|
49
|
+
= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger"
|
50
|
+
= f.submit "Save", class: "btn btn-success"
|
51
|
+
= link_to "Back", :back, class: "btn btn-link"
|
@@ -0,0 +1,50 @@
|
|
1
|
+
- blazer_title "Checks"
|
2
|
+
#header
|
3
|
+
.pull-right{:style => "line-height: 34px;"}
|
4
|
+
.btn-group
|
5
|
+
= link_to "New Check", new_check_path, class: "btn btn-info"
|
6
|
+
%button.btn.btn-info.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :type => "button"}
|
7
|
+
%span.caret
|
8
|
+
%span.sr-only Toggle Dropdown
|
9
|
+
%ul.dropdown-menu
|
10
|
+
%li= link_to "Home", root_path
|
11
|
+
- if Blazer.uploads?
|
12
|
+
%li= link_to "Uploads", uploads_path
|
13
|
+
%li.divider{:role => "separator"}
|
14
|
+
%li= link_to "New Query", new_query_path
|
15
|
+
%li= link_to "New Dashboard", new_dashboard_path
|
16
|
+
%input#search.search.form-control{:placeholder => "Start typing a query or state", :style => "width: 300px; display: inline-block;", :type => "text"}/
|
17
|
+
%table#checks.table
|
18
|
+
%thead
|
19
|
+
%tr
|
20
|
+
%th Query
|
21
|
+
%th{:style => "width: 10%;"} State
|
22
|
+
%th{:style => "width: 10%;"} Run
|
23
|
+
%th{:style => "width: 20%;"} Notify
|
24
|
+
%th{:style => "width: 15%;"}
|
25
|
+
%tbody
|
26
|
+
- @checks.each do |check|
|
27
|
+
%tr
|
28
|
+
%td
|
29
|
+
= link_to check.query.name, check.query
|
30
|
+
%span.text-muted= check.try(:check_type).to_s.gsub("_", " ")
|
31
|
+
%td
|
32
|
+
- if check.state
|
33
|
+
%small{:class => "check-state #{check.state.parameterize.gsub("-", "_")}"}= check.state.upcase
|
34
|
+
%td= check.schedule if check.respond_to?(:schedule)
|
35
|
+
%td
|
36
|
+
%ul.list-unstyled{:style => "margin-bottom: 0; word-break: break-all;"}
|
37
|
+
- check.split_emails.each do |email|
|
38
|
+
%li= email
|
39
|
+
- check.split_slack_channels.each do |channel|
|
40
|
+
%li= channel
|
41
|
+
%td{:style => "text-align: right; padding: 1px;"}
|
42
|
+
= link_to "Edit", edit_check_path(check), class: "btn btn-info"
|
43
|
+
= link_to "Run Now", query_path(check.query), class: "btn btn-primary"
|
44
|
+
:javascript
|
45
|
+
$("#search").on("keyup", function() {
|
46
|
+
var value = $(this).val().toLowerCase()
|
47
|
+
$("#checks tbody tr").filter( function() {
|
48
|
+
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
|
49
|
+
})
|
50
|
+
}).focus()
|
@@ -0,0 +1,64 @@
|
|
1
|
+
= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, variable_params(@dashboard)) : dashboards_path(variable_params(@dashboard))), html: {id: "app", class: "small-form"} do |f|
|
2
|
+
- if @dashboard.errors.any?
|
3
|
+
.alert.alert-danger= @dashboard.errors.full_messages.first
|
4
|
+
.form-group
|
5
|
+
= f.label :name
|
6
|
+
= f.text_field :name, class: "form-control"
|
7
|
+
.form-group{"v-show" => "queries.length"}
|
8
|
+
= f.label :charts
|
9
|
+
%ul#queries.list-group
|
10
|
+
%li.list-group-item{":key" => "query.id", "v-cloak" => "", "v-for" => "(query, index) in queries"}
|
11
|
+
%span.glyphicon.glyphicon-remove{"aria-hidden" => "true", "v-on:click" => "remove(index)"}
|
12
|
+
{{ query.name }}
|
13
|
+
%input{":value" => "query.id", :name => "query_ids[]", :type => "hidden"}/
|
14
|
+
.form-group{"v-cloak" => ""}
|
15
|
+
= f.label :query_id, "Add Chart"
|
16
|
+
= select_tag :query_id, nil, {include_blank: true, placeholder: "Select chart"}
|
17
|
+
%p{:style => "padding-bottom: 140px;", "v-cloak" => ""}
|
18
|
+
- if @dashboard.persisted?
|
19
|
+
= link_to "Delete", dashboard_path(@dashboard), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger"
|
20
|
+
= f.submit "Save", class: "btn btn-success"
|
21
|
+
= link_to "Back", :back, class: "btn btn-link"
|
22
|
+
:javascript
|
23
|
+
#{blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} }}
|
24
|
+
#{blazer_js_var "dashboardQueries", @queries || @dashboard.dashboard_queries.order(:position).map(&:query)}
|
25
|
+
|
26
|
+
var app = new Vue({
|
27
|
+
el: "#app",
|
28
|
+
data: {
|
29
|
+
queries: dashboardQueries
|
30
|
+
},
|
31
|
+
methods: {
|
32
|
+
remove: function(index) {
|
33
|
+
this.queries.splice(index, 1)
|
34
|
+
}
|
35
|
+
},
|
36
|
+
mounted: function() {
|
37
|
+
$("#query_id").selectize({
|
38
|
+
options: queries,
|
39
|
+
highlight: false,
|
40
|
+
maxOptions: 100,
|
41
|
+
onChange: function(val) {
|
42
|
+
if (val) {
|
43
|
+
var item = this.getItem(val)
|
44
|
+
|
45
|
+
// if duplicate query is added, remove the first one
|
46
|
+
for (var i = 0; i < app.queries.length; i++) {
|
47
|
+
if (app.queries[i].id == val) {
|
48
|
+
app.queries.splice(i, 1)
|
49
|
+
break
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
app.queries.push({id: val, name: item.text()})
|
54
|
+
this.setValue("")
|
55
|
+
}
|
56
|
+
}
|
57
|
+
})
|
58
|
+
}
|
59
|
+
})
|
60
|
+
Sortable.create($("#queries").get(0), {
|
61
|
+
onEnd: function(e) {
|
62
|
+
app.queries.splice(e.newIndex, 0, app.queries.splice(e.oldIndex, 1)[0])
|
63
|
+
}
|
64
|
+
})
|
@@ -0,0 +1,30 @@
|
|
1
|
+
- blazer_title @dashboard.name
|
2
|
+
%nav.navbar.navbar-default.navbar-fixed-top
|
3
|
+
.container
|
4
|
+
= render layout: "layouts/blazer/navbar" do
|
5
|
+
%li= link_to "Edit", edit_dashboard_path(@dashboard, variable_params(@dashboard))
|
6
|
+
|
7
|
+
%div{:style => "margin-bottom: 70px;"}
|
8
|
+
%h3.text-center=@dashboard.name
|
9
|
+
- if @data_sources.any? { |ds| ds.cache_mode != "off" }
|
10
|
+
%p.text-muted{:style => "float: right;"}
|
11
|
+
Some queries may be cached
|
12
|
+
\#{link_to "Refresh", refresh_dashboard_path(@dashboard, variable_params(@dashboard)), method: :post}
|
13
|
+
- if @bind_vars.any?
|
14
|
+
= render partial: "blazer/variables", locals: {action: dashboard_path(@dashboard)}
|
15
|
+
- else
|
16
|
+
%div{:style => "padding-bottom: 15px;"}
|
17
|
+
- @queries.each_with_index do |query, i|
|
18
|
+
.chart-container
|
19
|
+
%h4= link_to query.friendly_name, query_path(query, variable_params(query)), target: "_blank"
|
20
|
+
.chart{:id => "chart-#{i}"}
|
21
|
+
%p.text-muted Loading...
|
22
|
+
:javascript
|
23
|
+
#{blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]}}
|
24
|
+
|
25
|
+
runQuery(data, function (data) {
|
26
|
+
$("#chart-#{i}").html(data)
|
27
|
+
$("#chart-#{i} table").stupidtable(stupidtableCustomSettings)
|
28
|
+
}, function (message) {
|
29
|
+
$("#chart-#{i}").addClass("query-error").html(message)
|
30
|
+
});
|
@@ -0,0 +1,226 @@
|
|
1
|
+
- if @query.errors.any?
|
2
|
+
.alert.alert-danger= @query.errors.full_messages.first
|
3
|
+
#app{"v-cloak" => ""}
|
4
|
+
= form_for @query, url: (@query.persisted? ? query_path(@query, variable_params(@query)) : queries_path(variable_params(@query))), html: {autocomplete: "off"} do |f|
|
5
|
+
.row
|
6
|
+
#statement-box.col-xs-8
|
7
|
+
.form-group
|
8
|
+
= f.hidden_field :statement
|
9
|
+
#editor-container
|
10
|
+
#editor{":style" => "{ height: editorHeight }"}= @query.statement
|
11
|
+
.form-group.text-right{:style => "margin-bottom: 8px;"}
|
12
|
+
.pull-left{:style => "margin-top: 8px;"}
|
13
|
+
= link_to "Back", :back
|
14
|
+
%a{":href" => "docsPath", :style => "margin-left: 40px;", :target => "_blank"} Docs
|
15
|
+
%a{":href" => "schemaPath", :style => "margin-left: 40px;", :target => "_blank"} Schema
|
16
|
+
= 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;"
|
17
|
+
#tables{:style => "display: inline-block; width: 250px; margin-right: 10px;"}
|
18
|
+
%select#table_names{:placeholder => "Preview table", :style => "width: 240px;"}
|
19
|
+
%a.btn.btn-info{:style => "vertical-align: top; width: 70px;", "v-if" => "!running", "v-on:click" => "run"} Run
|
20
|
+
%a.btn.btn-danger{:style => "vertical-align: top; width: 70px;", "v-if" => "running", "v-on:click" => "cancel"} Cancel
|
21
|
+
.col-xs-4
|
22
|
+
.form-group
|
23
|
+
= f.label :name
|
24
|
+
= f.text_field :name, class: "form-control"
|
25
|
+
.form-group
|
26
|
+
= f.label :description
|
27
|
+
= f.text_area :description, placeholder: "Optional", style: "height: 80px;", class: "form-control"
|
28
|
+
.form-group.text-right
|
29
|
+
= f.submit "For Enter Press", class: "hide"
|
30
|
+
- if @query.persisted?
|
31
|
+
= link_to "Delete", query_path(@query), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger"
|
32
|
+
= f.submit "Fork", class: "btn btn-info"
|
33
|
+
= f.submit @query.persisted? ? "Update" : "Create", class: "btn btn-success"
|
34
|
+
- if @query.persisted?
|
35
|
+
- dashboards_count = @query.dashboards.count
|
36
|
+
- checks_count = @query.checks.count
|
37
|
+
- words = []
|
38
|
+
- words << pluralize(dashboards_count, "dashboard") if dashboards_count > 0
|
39
|
+
- words << pluralize(checks_count, "check") if checks_count > 0
|
40
|
+
- if words.any?
|
41
|
+
.alert.alert-info
|
42
|
+
Part of #{words.to_sentence}. Be careful when editing.
|
43
|
+
#results
|
44
|
+
%p.text-muted{"v-if" => "running"} Loading...
|
45
|
+
#results-html{":class" => "{ 'query-error': error }", "v-if" => "!running"}
|
46
|
+
:javascript
|
47
|
+
#{blazer_js_var "params", variable_params(@query)}
|
48
|
+
#{blazer_js_var "previewStatement", Hash[Blazer.data_sources.map { |k, v| [k, (v.preview_statement rescue "")] }]}
|
49
|
+
|
50
|
+
var app = new Vue({
|
51
|
+
el: "#app",
|
52
|
+
data: {
|
53
|
+
running: false,
|
54
|
+
results: "",
|
55
|
+
error: false,
|
56
|
+
dataSource: "",
|
57
|
+
selectize: null,
|
58
|
+
editorHeight: "180px"
|
59
|
+
},
|
60
|
+
computed: {
|
61
|
+
schemaPath: function() {
|
62
|
+
return Routes.schema_queries_path({data_source: this.dataSource})
|
63
|
+
},
|
64
|
+
docsPath: function() {
|
65
|
+
return Routes.docs_queries_path({data_source: this.dataSource})
|
66
|
+
}
|
67
|
+
},
|
68
|
+
methods: {
|
69
|
+
run: function(e) {
|
70
|
+
this.running = true
|
71
|
+
this.results = ""
|
72
|
+
this.error = false
|
73
|
+
cancelAllQueries()
|
74
|
+
|
75
|
+
var data = $.extend({}, params, {statement: this.getSQL(), data_source: $("#query_data_source").val()})
|
76
|
+
|
77
|
+
var _this = this
|
78
|
+
|
79
|
+
runQuery(data, function (data) {
|
80
|
+
_this.running = false
|
81
|
+
_this.showResults(data)
|
82
|
+
|
83
|
+
errorLine = _this.getErrorLine()
|
84
|
+
if (errorLine) {
|
85
|
+
editor.getSession().addGutterDecoration(errorLine - 1, "error")
|
86
|
+
editor.scrollToLine(errorLine, true, true, function () {})
|
87
|
+
editor.gotoLine(errorLine, 0, true)
|
88
|
+
editor.focus()
|
89
|
+
}
|
90
|
+
}, function (data) {
|
91
|
+
_this.running = false
|
92
|
+
_this.error = true
|
93
|
+
_this.showResults(data)
|
94
|
+
})
|
95
|
+
},
|
96
|
+
cancel: function(e) {
|
97
|
+
this.running = false
|
98
|
+
cancelAllQueries()
|
99
|
+
},
|
100
|
+
updateDataSource: function(dataSource) {
|
101
|
+
this.dataSource = dataSource
|
102
|
+
var selectize = this.selectize
|
103
|
+
selectize.clearOptions()
|
104
|
+
|
105
|
+
if (this.tablesXhr) {
|
106
|
+
this.tablesXhr.abort()
|
107
|
+
}
|
108
|
+
|
109
|
+
this.tablesXhr = $.getJSON(Routes.tables_queries_path({data_source: this.dataSource}), function(data) {
|
110
|
+
var newOptions = []
|
111
|
+
for (var i = 0; i < data.length; i++) {
|
112
|
+
var table = data[i]
|
113
|
+
if (typeof table === "object") {
|
114
|
+
newOptions.push({text: table.table, value: table.value})
|
115
|
+
} else {
|
116
|
+
newOptions.push({text: table, value: table})
|
117
|
+
}
|
118
|
+
}
|
119
|
+
selectize.clearOptions()
|
120
|
+
selectize.addOption(newOptions)
|
121
|
+
selectize.refreshOptions(false)
|
122
|
+
})
|
123
|
+
},
|
124
|
+
showEditor: function() {
|
125
|
+
var _this = this
|
126
|
+
|
127
|
+
editor = ace.edit("editor")
|
128
|
+
editor.setTheme("ace/theme/twilight")
|
129
|
+
editor.getSession().setMode("ace/mode/sql")
|
130
|
+
editor.setOptions({
|
131
|
+
enableBasicAutocompletion: false,
|
132
|
+
enableSnippets: false,
|
133
|
+
enableLiveAutocompletion: false,
|
134
|
+
highlightActiveLine: false,
|
135
|
+
fontSize: 12,
|
136
|
+
minLines: 10
|
137
|
+
})
|
138
|
+
editor.renderer.setShowGutter(true)
|
139
|
+
editor.renderer.setPrintMarginColumn(false)
|
140
|
+
editor.renderer.setPadding(10)
|
141
|
+
editor.getSession().setUseWrapMode(true)
|
142
|
+
editor.commands.addCommand({
|
143
|
+
name: "run",
|
144
|
+
bindKey: {win: "Ctrl-Enter", mac: "Command-Enter"},
|
145
|
+
exec: function(editor) {
|
146
|
+
_this.run()
|
147
|
+
},
|
148
|
+
readOnly: false // false if this command should not apply in readOnly mode
|
149
|
+
})
|
150
|
+
// fix command+L
|
151
|
+
editor.commands.removeCommands(["gotoline", "find"])
|
152
|
+
|
153
|
+
this.editor = editor
|
154
|
+
|
155
|
+
editor.getSession().on("change", function () {
|
156
|
+
$("#query_statement").val(editor.getValue())
|
157
|
+
_this.adjustHeight()
|
158
|
+
})
|
159
|
+
this.adjustHeight()
|
160
|
+
editor.focus()
|
161
|
+
},
|
162
|
+
adjustHeight: function() {
|
163
|
+
// https://stackoverflow.com/questions/11584061/
|
164
|
+
var editor = this.editor
|
165
|
+
var lines = editor.getSession().getScreenLength()
|
166
|
+
if (lines < 9) {
|
167
|
+
lines = 9
|
168
|
+
}
|
169
|
+
|
170
|
+
this.editorHeight = ((lines + 1) * 16).toString() + "px"
|
171
|
+
|
172
|
+
Vue.nextTick(function () {
|
173
|
+
editor.resize()
|
174
|
+
})
|
175
|
+
},
|
176
|
+
getSQL: function() {
|
177
|
+
var selectedText = editor.getSelectedText()
|
178
|
+
var text = selectedText.length < 10 ? editor.getValue() : selectedText
|
179
|
+
return text.replace(/\n/g, "\r\n")
|
180
|
+
},
|
181
|
+
getErrorLine: function() {
|
182
|
+
var editor = this.editor
|
183
|
+
var errorLine = this.results.substring(0, 100).includes("alert-danger") && /LINE (\d+)/g.exec(this.results)
|
184
|
+
|
185
|
+
if (errorLine) {
|
186
|
+
errorLine = parseInt(errorLine[1], 10)
|
187
|
+
if (editor.getSelectedText().length >= 10) {
|
188
|
+
errorLine += editor.getSelectionRange().start.row
|
189
|
+
}
|
190
|
+
return errorLine
|
191
|
+
}
|
192
|
+
},
|
193
|
+
showResults(data) {
|
194
|
+
// can't do it the Vue way due to script tags in results
|
195
|
+
// this.results = data
|
196
|
+
|
197
|
+
Vue.nextTick(function () {
|
198
|
+
$("#results-html").html(data)
|
199
|
+
})
|
200
|
+
}
|
201
|
+
},
|
202
|
+
mounted: function() {
|
203
|
+
var _this = this
|
204
|
+
|
205
|
+
var $select = $("#table_names").selectize({})
|
206
|
+
var selectize = $select[0].selectize
|
207
|
+
selectize.on("change", function(val) {
|
208
|
+
editor.setValue(previewStatement[_this.dataSource].replace("{table}", val), 1)
|
209
|
+
_this.run()
|
210
|
+
selectize.clear(true)
|
211
|
+
selectize.blur()
|
212
|
+
})
|
213
|
+
this.selectize = selectize
|
214
|
+
|
215
|
+
this.updateDataSource($("#query_data_source").val())
|
216
|
+
|
217
|
+
var $dsSelect = $("#query_data_source").selectize({})
|
218
|
+
var dsSelectize = $dsSelect[0].selectize
|
219
|
+
dsSelectize.on("change", function(val) {
|
220
|
+
_this.updateDataSource(val)
|
221
|
+
dsSelectize.blur()
|
222
|
+
})
|
223
|
+
|
224
|
+
this.showEditor()
|
225
|
+
}
|
226
|
+
})
|
@@ -0,0 +1,142 @@
|
|
1
|
+
#queries
|
2
|
+
#header
|
3
|
+
.pull-right{:style => "line-height: 34px;"}
|
4
|
+
- if blazer_user
|
5
|
+
= link_to "All", root_path, class: !params[:filter] ? "active" : nil, style: "margin-right: 40px;"
|
6
|
+
- if Blazer.audit
|
7
|
+
= link_to "Viewed", root_path(filter: "viewed"), class: params[:filter] == "viewed" ? "active" : nil, style: "margin-right: 40px;"
|
8
|
+
= link_to "Mine", root_path(filter: "mine"), class: params[:filter] == "mine" ? "active" : nil, style: "margin-right: 40px;"
|
9
|
+
.btn-group
|
10
|
+
= link_to "New Query", new_query_path, class: "btn btn-info"
|
11
|
+
%button.btn.btn-info.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :type => "button"}
|
12
|
+
%span.caret
|
13
|
+
%span.sr-only Toggle Dropdown
|
14
|
+
%ul.dropdown-menu
|
15
|
+
%li= link_to "Checks", checks_path
|
16
|
+
- if Blazer.uploads?
|
17
|
+
%li= link_to "Uploads", uploads_path
|
18
|
+
%li.divider{:role => "separator"}
|
19
|
+
%li= link_to "New Dashboard", new_dashboard_path
|
20
|
+
%li= link_to "New Check", new_check_path
|
21
|
+
%input.search.form-control{:placeholder => "Start typing a query, dashboard, or person", :style => "width: 300px; display: inline-block;", :type => "text", "v-focus" => "", "v-model" => "searchTerm"}/
|
22
|
+
%table.table#branded-table
|
23
|
+
%thead
|
24
|
+
%tr
|
25
|
+
%th Name
|
26
|
+
- if Blazer.user_class
|
27
|
+
%th{:style => "text-align: right;"} Mastermind
|
28
|
+
%tbody.list{"v-cloak" => ""}
|
29
|
+
%tr{"v-for" => "query in visibleItems"}
|
30
|
+
%td
|
31
|
+
%a{":class" => "{ dashboard: query.dashboard }", ":href" => "itemPath(query)"} {{ query.name }}
|
32
|
+
%span.vars {{ query.vars }}
|
33
|
+
- if Blazer.user_class
|
34
|
+
%td.creator {{ query.creator }}
|
35
|
+
%p.text-muted{"v-if" => "more"} Loading...
|
36
|
+
:javascript
|
37
|
+
#{blazer_js_var "dashboards", @dashboards}
|
38
|
+
#{blazer_js_var "queries", @queries}
|
39
|
+
#{blazer_js_var "more", @more}
|
40
|
+
|
41
|
+
var prepareSearch = function (list) {
|
42
|
+
var i, q, searchStr
|
43
|
+
for (i = 0; i < list.length; i++) {
|
44
|
+
q = list[i]
|
45
|
+
searchStr = q.name + q.creator
|
46
|
+
if (q.creator === "You") {
|
47
|
+
searchStr += "mine me"
|
48
|
+
}
|
49
|
+
q.searchStr = prepareQuery(searchStr)
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
var prepareQuery = function (str) {
|
54
|
+
return str.toLowerCase()
|
55
|
+
}
|
56
|
+
|
57
|
+
var app = new Vue({
|
58
|
+
el: "#queries",
|
59
|
+
data: {
|
60
|
+
searchTerm: "",
|
61
|
+
more: more,
|
62
|
+
updateCounter: 0
|
63
|
+
},
|
64
|
+
created: function() {
|
65
|
+
this.listItems = dashboards.concat(queries)
|
66
|
+
|
67
|
+
prepareSearch(this.listItems)
|
68
|
+
|
69
|
+
this.queryIds = {}
|
70
|
+
for (i = 0; i < queries.length; i++) {
|
71
|
+
this.queryIds[queries[i].id] = true
|
72
|
+
}
|
73
|
+
|
74
|
+
if (this.more) {
|
75
|
+
var _this = this
|
76
|
+
|
77
|
+
$.getJSON(Routes.queries_path(), function (data) {
|
78
|
+
var i, j, newValues, val, size = 500;
|
79
|
+
|
80
|
+
var newValues = []
|
81
|
+
for (j = 0; j < data.length; j++) {
|
82
|
+
val = data[j]
|
83
|
+
if (val && !_this.queryIds[val.id]) {
|
84
|
+
newValues.push(val)
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
prepareSearch(newValues)
|
89
|
+
|
90
|
+
_this.listItems = _this.listItems.concat(newValues)
|
91
|
+
_this.more = false
|
92
|
+
// hack to get to update
|
93
|
+
_this.updateCounter++
|
94
|
+
})
|
95
|
+
}
|
96
|
+
},
|
97
|
+
computed: {
|
98
|
+
visibleItems: function () {
|
99
|
+
// hack to get to update
|
100
|
+
this.updateCounter
|
101
|
+
|
102
|
+
var pageSize = 200
|
103
|
+
var q, i
|
104
|
+
|
105
|
+
if (this.searchTerm.length > 0) {
|
106
|
+
var term = prepareQuery(this.searchTerm)
|
107
|
+
var items = []
|
108
|
+
var fuzzyItems = []
|
109
|
+
for (i = 0; i < this.listItems.length; i++) {
|
110
|
+
q = this.listItems[i]
|
111
|
+
if (q.searchStr.indexOf(term) !== -1) {
|
112
|
+
items.push(q)
|
113
|
+
if (items.length == pageSize) {
|
114
|
+
break
|
115
|
+
}
|
116
|
+
} else if (fuzzysearch(term, q.searchStr)) {
|
117
|
+
fuzzyItems.push(q)
|
118
|
+
}
|
119
|
+
}
|
120
|
+
return items.concat(fuzzyItems).slice(0, pageSize)
|
121
|
+
} else {
|
122
|
+
return this.listItems.slice(0, pageSize)
|
123
|
+
}
|
124
|
+
}
|
125
|
+
},
|
126
|
+
methods: {
|
127
|
+
itemPath: function (item) {
|
128
|
+
if (item.dashboard) {
|
129
|
+
return Routes.dashboard_path(item.to_param)
|
130
|
+
} else {
|
131
|
+
return Routes.query_path(item.to_param)
|
132
|
+
}
|
133
|
+
}
|
134
|
+
},
|
135
|
+
directives: {
|
136
|
+
focus: {
|
137
|
+
inserted: function (el) {
|
138
|
+
el.focus()
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
})
|