finery 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +426 -0
  3. data/CONTRIBUTING.md +49 -0
  4. data/LICENSE.txt +25 -0
  5. data/README.md +1144 -0
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  11. data/app/assets/images/blazer/favicon.png +0 -0
  12. data/app/assets/javascripts/blazer/Sortable.js +3709 -0
  13. data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
  14. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
  15. data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
  16. data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
  17. data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
  18. data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
  19. data/app/assets/javascripts/blazer/ace.js +6 -0
  20. data/app/assets/javascripts/blazer/application.js +87 -0
  21. data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
  22. data/app/assets/javascripts/blazer/chart.umd.js +13 -0
  23. data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
  24. data/app/assets/javascripts/blazer/chartjs-plugin-annotation.min.js +7 -0
  25. data/app/assets/javascripts/blazer/chartkick.js +2570 -0
  26. data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
  27. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  28. data/app/assets/javascripts/blazer/highlight.min.js +466 -0
  29. data/app/assets/javascripts/blazer/jquery.js +10872 -0
  30. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  31. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  32. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
  33. data/app/assets/javascripts/blazer/moment.js +5685 -0
  34. data/app/assets/javascripts/blazer/queries.js +130 -0
  35. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  36. data/app/assets/javascripts/blazer/routes.js +26 -0
  37. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  38. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  40. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  41. data/app/assets/stylesheets/blazer/application.css +243 -0
  42. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  43. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  44. data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
  45. data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
  46. data/app/assets/stylesheets/blazer/github.css +125 -0
  47. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  48. data/app/controllers/blazer/base_controller.rb +133 -0
  49. data/app/controllers/blazer/checks_controller.rb +56 -0
  50. data/app/controllers/blazer/dashboards_controller.rb +99 -0
  51. data/app/controllers/blazer/queries_controller.rb +468 -0
  52. data/app/controllers/blazer/uploads_controller.rb +147 -0
  53. data/app/helpers/blazer/base_helper.rb +83 -0
  54. data/app/models/blazer/audit.rb +6 -0
  55. data/app/models/blazer/check.rb +104 -0
  56. data/app/models/blazer/connection.rb +5 -0
  57. data/app/models/blazer/dashboard.rb +17 -0
  58. data/app/models/blazer/dashboard_query.rb +9 -0
  59. data/app/models/blazer/query.rb +42 -0
  60. data/app/models/blazer/record.rb +5 -0
  61. data/app/models/blazer/upload.rb +11 -0
  62. data/app/models/blazer/uploads_connection.rb +7 -0
  63. data/app/views/blazer/_nav.html.erb +18 -0
  64. data/app/views/blazer/_variables.html.erb +127 -0
  65. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  66. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  67. data/app/views/blazer/checks/_form.html.erb +79 -0
  68. data/app/views/blazer/checks/edit.html.erb +3 -0
  69. data/app/views/blazer/checks/index.html.erb +72 -0
  70. data/app/views/blazer/checks/new.html.erb +3 -0
  71. data/app/views/blazer/dashboards/_form.html.erb +82 -0
  72. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  73. data/app/views/blazer/dashboards/new.html.erb +3 -0
  74. data/app/views/blazer/dashboards/show.html.erb +53 -0
  75. data/app/views/blazer/queries/_caching.html.erb +16 -0
  76. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  77. data/app/views/blazer/queries/_form.html.erb +255 -0
  78. data/app/views/blazer/queries/docs.html.erb +147 -0
  79. data/app/views/blazer/queries/edit.html.erb +2 -0
  80. data/app/views/blazer/queries/home.html.erb +169 -0
  81. data/app/views/blazer/queries/new.html.erb +2 -0
  82. data/app/views/blazer/queries/run.html.erb +188 -0
  83. data/app/views/blazer/queries/schema.html.erb +55 -0
  84. data/app/views/blazer/queries/show.html.erb +72 -0
  85. data/app/views/blazer/uploads/_form.html.erb +27 -0
  86. data/app/views/blazer/uploads/edit.html.erb +3 -0
  87. data/app/views/blazer/uploads/index.html.erb +55 -0
  88. data/app/views/blazer/uploads/new.html.erb +3 -0
  89. data/app/views/layouts/blazer/application.html.erb +25 -0
  90. data/config/routes.rb +25 -0
  91. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  92. data/lib/blazer/adapters/base_adapter.rb +76 -0
  93. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  94. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  95. data/lib/blazer/adapters/clickhouse_adapter.rb +84 -0
  96. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  97. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  98. data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
  99. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  100. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  101. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  102. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  103. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  104. data/lib/blazer/adapters/presto_adapter.rb +54 -0
  105. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  106. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  107. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  108. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  109. data/lib/blazer/adapters/sql_adapter.rb +324 -0
  110. data/lib/blazer/adapters.rb +18 -0
  111. data/lib/blazer/annotations.rb +47 -0
  112. data/lib/blazer/anomaly_detectors.rb +22 -0
  113. data/lib/blazer/check_mailer.rb +27 -0
  114. data/lib/blazer/data_source.rb +270 -0
  115. data/lib/blazer/engine.rb +42 -0
  116. data/lib/blazer/forecasters.rb +7 -0
  117. data/lib/blazer/result.rb +178 -0
  118. data/lib/blazer/result_cache.rb +71 -0
  119. data/lib/blazer/run_statement.rb +44 -0
  120. data/lib/blazer/run_statement_job.rb +20 -0
  121. data/lib/blazer/slack_notifier.rb +94 -0
  122. data/lib/blazer/statement.rb +77 -0
  123. data/lib/blazer/version.rb +3 -0
  124. data/lib/blazer.rb +286 -0
  125. data/lib/finery.rb +3 -0
  126. data/lib/generators/blazer/install_generator.rb +22 -0
  127. data/lib/generators/blazer/templates/config.yml.tt +83 -0
  128. data/lib/generators/blazer/templates/install.rb.tt +47 -0
  129. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  130. data/lib/generators/blazer/uploads_generator.rb +18 -0
  131. data/lib/tasks/blazer.rake +20 -0
  132. data/lib/tasks/finery.rake +20 -0
  133. data/licenses/LICENSE-ace.txt +24 -0
  134. data/licenses/LICENSE-bootstrap.txt +21 -0
  135. data/licenses/LICENSE-chart.js.txt +9 -0
  136. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  137. data/licenses/LICENSE-chartkick.js.txt +22 -0
  138. data/licenses/LICENSE-date-fns.txt +21 -0
  139. data/licenses/LICENSE-daterangepicker.txt +21 -0
  140. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  141. data/licenses/LICENSE-highlight.js.txt +29 -0
  142. data/licenses/LICENSE-jquery.txt +20 -0
  143. data/licenses/LICENSE-kurkle-color.txt +9 -0
  144. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  145. data/licenses/LICENSE-moment-timezone.txt +20 -0
  146. data/licenses/LICENSE-moment.txt +22 -0
  147. data/licenses/LICENSE-rails-ujs.txt +20 -0
  148. data/licenses/LICENSE-selectize.txt +202 -0
  149. data/licenses/LICENSE-sortable.txt +21 -0
  150. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  151. data/licenses/LICENSE-stupidtable.txt +19 -0
  152. data/licenses/LICENSE-vue.txt +21 -0
  153. metadata +250 -0
@@ -0,0 +1,2 @@
1
+ <% blazer_title "New Query" %>
2
+ <%= render partial: "form" %>
@@ -0,0 +1,188 @@
1
+ <% if @error %>
2
+ <div class="alert alert-danger"><%= @error.first(200) %></div>
3
+ <% elsif !@success %>
4
+ <% if @only_chart %>
5
+ <p class="text-muted">Select variables</p>
6
+ <% else %>
7
+ <div class="alert alert-info">Can’t preview queries with variables...yet!</div>
8
+ <% end %>
9
+ <% elsif @cohort_analysis %>
10
+ <% if @cohort_error %>
11
+ <div class="alert alert-info"><%= @cohort_error %></div>
12
+ <% else %>
13
+ <%= render partial: "cohorts" %>
14
+ <% end %>
15
+ <% else %>
16
+ <% unless @only_chart %>
17
+ <%= render partial: "caching" %>
18
+ <p class="text-muted" style="margin-bottom: 10px;">
19
+ <% if @row_limit && @rows.size > @row_limit %>
20
+ First
21
+ <% @rows = @rows.first(@row_limit) %>
22
+ <% end %>
23
+ <%= pluralize(@rows.size, "row") %>
24
+
25
+ <% @checks.select(&:state).each do |check| %>
26
+ &middot; <small class="check-state <%= check.state.parameterize.gsub("-", "_") %>"><%= link_to check.state.upcase, edit_check_path(check) %></small>
27
+ <% if check.try(:message) %>
28
+ &middot; <%= check.message %>
29
+ <% end %>
30
+ <% end %>
31
+
32
+ <% if @query && @result.forecastable? && !params[:forecast] %>
33
+ &middot;
34
+ <%= link_to "Forecast", query_path(@query, params: {forecast: "t"}.merge(variable_params(@query))) %>
35
+ <% end %>
36
+ </p>
37
+ <% end %>
38
+ <% if @forecast_error %>
39
+ <div class="alert alert-danger"><%= @forecast_error %></div>
40
+ <% end %>
41
+ <% if @cohort_error %>
42
+ <div class="alert alert-info"><%= @cohort_error %></div>
43
+ <% end %>
44
+ <% if @rows.any? %>
45
+ <% values = @rows.first %>
46
+ <% chart_id = SecureRandom.hex %>
47
+ <% column_types = @result.column_types %>
48
+ <% chart_type = @result.chart_type %>
49
+ <% chart_options = {id: chart_id} %>
50
+ <% if ["line", "line2"].include?(chart_type) %>
51
+ <% chart_options.merge!(min: nil) %>
52
+ <% end %>
53
+ <% if chart_type == "scatter" %>
54
+ <% chart_options.merge!(library: {tooltips: {intersect: false}}) %>
55
+ <% elsif ["bar", "bar2"].include?(chart_type) %>
56
+ <% chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}}) %>
57
+ <% elsif chart_type != "pie" %>
58
+ <% if column_types.size == 2 || @forecast %>
59
+ <% chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}}) %>
60
+ <% else %>
61
+ <%# chartjs axis: 'x' has poor behavior with multiple series %>
62
+ <% chart_options.merge!(library: {tooltips: {intersect: false}}) %>
63
+ <% end %>
64
+ <% end %>
65
+ <% if ["line", "line2"].include?(chart_type) %>
66
+ <% chart_options[:library].merge!({
67
+ annotation: { drawTime: "beforeDatasetsDraw", annotations: blazer_format_annotations(@annotations), }
68
+ }) %>
69
+ <% end %>
70
+ <% series_library = {} %>
71
+ <% target_index = @columns.index { |k| k.downcase == "target" } %>
72
+ <% if target_index %>
73
+ <% color = "#109618" %>
74
+ <% series_library[target_index - 1] = {pointStyle: "line", pointBorderWidth: 0, hitRadius: 5, borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color} %>
75
+ <% end %>
76
+ <% if @forecast %>
77
+ <% color = "#54a3ee" %>
78
+ <% series_library[1] = {borderDash: [8], borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color} %>
79
+ <% end %>
80
+ <% if @markers.any? %>
81
+ <% map_id = SecureRandom.hex %>
82
+ <%= content_tag :div, nil, id: map_id, style: "height: #{@only_chart ? 300 : 500}px;" %>
83
+ <script>
84
+ <%= blazer_js_var "mapboxAccessToken", Blazer.mapbox_access_token %>
85
+ <%= blazer_js_var "markers", @markers %>
86
+ <%= blazer_js_var "mapId", map_id %>
87
+ new Mapkick.Map(mapId, markers, {accessToken: mapboxAccessToken, tooltips: {hover: false, html: true}});
88
+ </script>
89
+ <% elsif @geojson.any? %>
90
+ <% map_id = SecureRandom.hex %>
91
+ <%= content_tag :div, nil, id: map_id, style: "height: #{@only_chart ? 300 : 500}px;" %>
92
+ <script>
93
+ <%= blazer_js_var "mapboxAccessToken", Blazer.mapbox_access_token %>
94
+ <%= blazer_js_var "geojson", @geojson %>
95
+ <%= blazer_js_var "mapId", map_id %>
96
+ new Mapkick.AreaMap(mapId, geojson, {accessToken: mapboxAccessToken, tooltips: {hover: false, html: true}});
97
+ </script>
98
+ <% elsif chart_type == "line" %>
99
+ <% 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]} } %>
100
+ <%= line_chart chart_data, **chart_options %>
101
+ <% elsif chart_type == "line2" %>
102
+ <%= line_chart @rows.group_by { |r| v = r[1]; (@smart_values[@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 %>
103
+ <% elsif chart_type == "pie" %>
104
+ <%= pie_chart @rows.map { |r| [(@smart_values[@columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, **chart_options %>
105
+ <% elsif chart_type == "bar" %>
106
+ <%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@smart_values[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, **chart_options %>
107
+ <% elsif chart_type == "bar2" %>
108
+ <% first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1) %>
109
+ <% labels = first_20.map { |r| r[0] }.uniq %>
110
+ <% series = first_20.map { |r| r[1] }.uniq %>
111
+ <% labels.each do |l| %>
112
+ <% series.each do |s| %>
113
+ <% first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s } %>
114
+ <% end %>
115
+ <% end %>
116
+ <%= column_chart first_20.group_by { |r| v = r[1]; (@smart_values[@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]; [(@smart_values[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, **chart_options %>
117
+ <% elsif chart_type == "scatter" %>
118
+ <%= scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], **chart_options %>
119
+ <% elsif @only_chart %>
120
+ <% if @rows.size == 1 && @rows.first.size == 1 %>
121
+ <% v = @rows.first.first %>
122
+ <% if v.is_a?(String) && v == "" %>
123
+ <div class="text-muted">empty string</div>
124
+ <% else %>
125
+ <p style="font-size: 160px;"><%= blazer_format_value(@columns.first, v) %></p>
126
+ <% end %>
127
+ <% else %>
128
+ <% @no_chart = true %>
129
+ <% end %>
130
+ <% end %>
131
+
132
+ <% unless @only_chart && !@no_chart %>
133
+ <% header_width = 100 / @columns.size.to_f %>
134
+ <div class="results-container">
135
+ <% if @columns == ["QUERY PLAN"] %>
136
+ <pre><code><%= @rows.map { |r| r[0] }.join("\n") %></code></pre>
137
+ <% elsif @columns == ["PLAN"] && @data_source.adapter == "druid" %>
138
+ <pre><code><%= @rows[0][0] %></code></pre>
139
+ <% else %>
140
+ <table class="table results-table">
141
+ <thead>
142
+ <tr>
143
+ <% @columns.each_with_index do |key, i| %>
144
+ <% type = @column_types[i] %>
145
+ <th style="width: <%= header_width %>%;" data-sort="<%= type %>">
146
+ <div style="min-width: <%= @min_width_types.include?(i) ? 180 : 60 %>px;">
147
+ <%= key %>
148
+ </div>
149
+ </th>
150
+ <% end %>
151
+ </tr>
152
+ </thead>
153
+ <tbody>
154
+ <% @rows.each do |row| %>
155
+ <tr>
156
+ <% row.each_with_index do |v, i| %>
157
+ <% k = @columns[i] %>
158
+ <td>
159
+ <% if v.is_a?(Time) %>
160
+ <% v = blazer_time_value(@data_source, k, v) %>
161
+ <% end %>
162
+
163
+ <% unless v.nil? %>
164
+ <% if v.is_a?(String) && v == "" %>
165
+ <div class="text-muted">empty string</div>
166
+ <% elsif @linked_columns[k] %>
167
+ <%= link_to blazer_format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
168
+ <% else %>
169
+ <%= blazer_format_value(k, v) %>
170
+ <% end %>
171
+ <% end %>
172
+
173
+ <% if v2 = (@smart_values[k] || {})[v.nil? ? v : v.to_s] %>
174
+ <div class="text-muted"><%= v2 %></div>
175
+ <% end %>
176
+ </td>
177
+ <% end %>
178
+ </tr>
179
+ <% end %>
180
+ </tbody>
181
+ </table>
182
+ <% end %>
183
+ </div>
184
+ <% end %>
185
+ <% elsif @only_chart %>
186
+ <p class="text-muted">No rows</p>
187
+ <% end %>
188
+ <% end %>
@@ -0,0 +1,55 @@
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
+ <% if table[:schema] != "public" %><%= table[:schema] %>.<% end %><%= table[:table] %>
17
+ </th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% table[:columns].each do |column| %>
22
+ <tr>
23
+ <td style="width: 60%;"><%= column[:name] %></td>
24
+ <td class="text-muted"><%= column[:data_type] %></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ <% end %>
30
+
31
+ <script>
32
+ $("#search").on("keyup", function() {
33
+ var value = $(this).val().toLowerCase()
34
+ $(".schema-table").filter(function() {
35
+ // if found in table name, show entire table
36
+ // if just found in rows, show row
37
+
38
+ var found = $(this).find("thead").text().toLowerCase().indexOf(value) > -1
39
+
40
+ if (found) {
41
+ $(this).find("tbody tr").toggle(true)
42
+ } else {
43
+ $(this).find("tbody tr").filter(function() {
44
+ var found2 = $(this).text().toLowerCase().indexOf(value) > -1
45
+ $(this).toggle(found2)
46
+ if (found2) {
47
+ found = true
48
+ }
49
+ })
50
+ }
51
+
52
+ $(this).toggle(found)
53
+ })
54
+ }).focus()
55
+ </script>
@@ -0,0 +1,72 @@
1
+ <% blazer_title @query.name %>
2
+
3
+ <div class="topbar">
4
+ <div class="container">
5
+ <div class="row" style="padding-top: 13px;">
6
+ <div class="col-sm-9">
7
+ <%= render partial: "blazer/nav" %>
8
+ <h3 style="line-height: 34px; display: inline; margin-left: 5px;">
9
+ <%= @query.name %>
10
+ </h3>
11
+ </div>
12
+ <div class="col-sm-3 text-right">
13
+ <%= link_to "Edit", edit_query_path(@query, params: variable_params(@query)), class: "btn btn-default", disabled: !@query.editable?(blazer_user) %>
14
+ <%= link_to "Fork", new_query_path(params: {variables: variable_params(@query), fork_query_id: @query.id, data_source: @query.data_source, name: @query.name}), class: "btn btn-info" %>
15
+
16
+ <% if !@error && @success %>
17
+ <%= button_to "Download", run_queries_path(format: "csv"), params: @run_data, class: "btn btn-primary" %>
18
+ <% end %>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div style="margin-bottom: 60px;"></div>
25
+
26
+ <% if @sql_errors.any? %>
27
+ <div class="alert alert-danger">
28
+ <ul>
29
+ <% @sql_errors.each do |message| %>
30
+ <li><%= message %></li>
31
+ <% end %>
32
+ </ul>
33
+ </div>
34
+ <% end %>
35
+
36
+ <% if @query.description.present? %>
37
+ <p style="white-space: pre-line;"><%= @query.description %></p>
38
+ <% end %>
39
+
40
+ <%= render partial: "blazer/variables", locals: {action: query_path(@query)} %>
41
+
42
+ <pre id="code"><code><%= @statement.display_statement %></code></pre>
43
+
44
+ <% if @success %>
45
+ <div id="results">
46
+ <p class="text-muted">Loading...</p>
47
+ </div>
48
+
49
+ <script>
50
+ function showRun(data) {
51
+ $("#results").html(data)
52
+ $("#results table").stupidtable(stupidtableCustomSettings).stickyTableHeaders({fixedOffset: 60})
53
+ }
54
+
55
+ function showError(message) {
56
+ $("#results").addClass("query-error").html(message)
57
+ }
58
+
59
+ <%= blazer_js_var "data", @run_data %>
60
+
61
+ runQuery(data, showRun, showError)
62
+ </script>
63
+ <% end %>
64
+
65
+ <script>
66
+ // do not highlight really long queries
67
+ // this can lead to performance issues
68
+ var code = $("#code code")
69
+ if (code.text().length < 10000) {
70
+ hljs.highlightElement(code.get(0))
71
+ }
72
+ </script>
@@ -0,0 +1,27 @@
1
+ <%= form_for @upload, html: {class: "small-form"} do |f| %>
2
+ <% if @upload.errors.any? %>
3
+ <div class="alert alert-danger"><%= @upload.errors.full_messages.first %></div>
4
+ <% elsif !@upload.persisted? %>
5
+ <p>Create a database table from a CSV file. The table will be created in the <code><%= Blazer.settings["uploads"]["schema"] %></code> schema.</p>
6
+ <% end %>
7
+
8
+ <div class="form-group">
9
+ <%= f.label :table %>
10
+ <%= f.text_field :table, class: "form-control" %>
11
+ </div>
12
+ <div class="form-group">
13
+ <%= f.label :description %>
14
+ <%= f.text_area :description, placeholder: "Optional", style: "height: 60px;", class: "form-control" %>
15
+ </div>
16
+ <div class="form-group">
17
+ <%= f.label :file %>
18
+ <%= f.file_field :file, accept: "text/csv", style: "margin-top: 6px; margin-bottom: 21px;" %>
19
+ </div>
20
+ <p>
21
+ <% if @upload.persisted? %>
22
+ <%= link_to "Delete", upload_path(@upload), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
23
+ <% end %>
24
+ <%= f.submit "Save", class: "btn btn-success" %>
25
+ <%= link_to "Back", :back, class: "btn btn-link" %>
26
+ </p>
27
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "Edit Upload" %>
2
+
3
+ <%= render partial: "form" %>
@@ -0,0 +1,55 @@
1
+ <% blazer_title "Uploads" %>
2
+
3
+ <div id="header">
4
+ <div class="pull-right" style="line-height: 34px;">
5
+ <div class="btn-group">
6
+ <%= link_to "New Upload", new_upload_path, class: "btn btn-info" %>
7
+ <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
8
+ <span class="caret"></span>
9
+ <span class="sr-only">Toggle Dropdown</span>
10
+ </button>
11
+ <ul class="dropdown-menu">
12
+ <li><%= link_to "Home", root_path %></li>
13
+ <li><%= link_to "Checks", checks_path %></li>
14
+ <li role="separator" class="divider"></li>
15
+ <li><%= link_to "New Query", new_query_path %></li>
16
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
17
+ <li><%= link_to "New Check", new_check_path %></li>
18
+ </ul>
19
+ </div>
20
+ </div>
21
+
22
+ <input id="search" type="text" placeholder="Start typing a table or person" style="width: 300px; display: inline-block;" class="search form-control" />
23
+ </div>
24
+
25
+ <table id="uploads" class="table">
26
+ <thead>
27
+ <tr>
28
+ <th>Table</th>
29
+ <th style="width: 60%;"></th>
30
+ <% if Blazer.user_class %>
31
+ <th style="width: 20%; text-align: right;">Mastermind</th>
32
+ <% end%>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @uploads.each do |upload| %>
37
+ <tr>
38
+ <td><%= link_to upload.table, edit_upload_path(upload) %></td>
39
+ <td><%= truncate(upload.description, length: 100, separator: " ") %></td>
40
+ <% if Blazer.user_class %>
41
+ <td class="creator"><%= blazer_user && upload.creator == blazer_user ? "You" : upload.creator.try(Blazer.user_name) %></td>
42
+ <% end %>
43
+ </tr>
44
+ <% end %>
45
+ </tbody>
46
+ </table>
47
+
48
+ <script>
49
+ $("#search").on("keyup", function() {
50
+ var value = $(this).val().toLowerCase()
51
+ $("#uploads tbody tr").filter( function() {
52
+ $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
53
+ })
54
+ }).focus()
55
+ </script>
@@ -0,0 +1,3 @@
1
+ <% blazer_title "New Upload" %>
2
+
3
+ <%= render partial: "form" %>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= blazer_title ? blazer_title : "Blazer" %></title>
5
+
6
+ <meta charset="utf-8" />
7
+ <%= favicon_link_tag "blazer/favicon.png" %>
8
+ <% if defined?(Propshaft::Railtie) %>
9
+ <%= stylesheet_link_tag "blazer/bootstrap-propshaft", "blazer/bootstrap", "blazer/selectize", "blazer/github", "blazer/daterangepicker", "blazer/application" %>
10
+ <%= javascript_include_tag "blazer/jquery", "blazer/rails-ujs", "blazer/stupidtable", "blazer/stupidtable-custom-settings", "blazer/jquery.stickytableheaders", "blazer/selectize", "blazer/highlight.min", "blazer/moment", "blazer/moment-timezone-with-data", "blazer/daterangepicker", "blazer/chart.umd", "blazer/chartjs-adapter-date-fns.bundle", "blazer/chartkick", "blazer/mapkick.bundle", "blazer/ace/ace", "blazer/ace/ext-language_tools", "blazer/ace/theme-twilight", "blazer/ace/mode-sql", "blazer/ace/snippets/text", "blazer/ace/snippets/sql", "blazer/Sortable", "blazer/bootstrap", "blazer/vue.global.prod", "blazer/routes", "blazer/queries", "blazer/fuzzysearch", "blazer/application" %>
11
+ <% else %>
12
+ <%= stylesheet_link_tag "blazer/application" %>
13
+ <%= javascript_include_tag "blazer/application" %>
14
+ <% end %>
15
+ <script>
16
+ <%= blazer_js_var "rootPath", root_path %>
17
+ </script>
18
+ <%= csrf_meta_tags %>
19
+ </head>
20
+ <body>
21
+ <div class="container">
22
+ <%= yield %>
23
+ </div>
24
+ </body>
25
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,25 @@
1
+ Blazer::Engine.routes.draw do
2
+ resources :queries do
3
+ post :run, on: :collection # err on the side of caution
4
+ post :cancel, on: :collection
5
+ post :refresh, on: :member
6
+ get :tables, on: :collection
7
+ get :schema, on: :collection
8
+ get :docs, on: :collection
9
+ end
10
+
11
+ resources :checks, except: [:show] do
12
+ get :run, on: :member
13
+ end
14
+
15
+ resources :dashboards, except: [:index] do
16
+ post :refresh, on: :member
17
+ end
18
+
19
+ if Blazer.uploads?
20
+ resources :uploads do
21
+ end
22
+ end
23
+
24
+ root to: "queries#home"
25
+ end
@@ -0,0 +1,182 @@
1
+ module Blazer
2
+ module Adapters
3
+ class AthenaAdapter < BaseAdapter
4
+ def run_statement(statement, comment, bind_params = [])
5
+ require "digest/md5"
6
+
7
+ columns = []
8
+ rows = []
9
+ error = nil
10
+
11
+ begin
12
+ # use empty? since any? doesn't work for [nil]
13
+ if !bind_params.empty?
14
+ request_token = Digest::MD5.hexdigest([statement, bind_params.to_json, data_source.id, settings["workgroup"]].compact.join("/"))
15
+ statement_name = "blazer_#{request_token}"
16
+ begin
17
+ client.create_prepared_statement({
18
+ statement_name: statement_name,
19
+ work_group: settings["workgroup"],
20
+ query_statement: statement
21
+ })
22
+ rescue Aws::Athena::Errors::InvalidRequestException => e
23
+ raise e unless e.message.include?("already exists in WorkGroup")
24
+ end
25
+ using_statement = bind_params.map { |v| data_source.quote(v) }.join(", ")
26
+ statement = "EXECUTE #{statement_name} USING #{using_statement}"
27
+ else
28
+ request_token = Digest::MD5.hexdigest([statement, data_source.id, settings["workgroup"]].compact.join("/"))
29
+ end
30
+
31
+ query_options = {
32
+ query_string: statement,
33
+ # use token so we fetch cached results after query is run
34
+ client_request_token: request_token,
35
+ query_execution_context: {
36
+ database: database,
37
+ }
38
+ }
39
+
40
+ if settings["output_location"]
41
+ query_options[:result_configuration] = {output_location: settings["output_location"]}
42
+ end
43
+
44
+ if settings["workgroup"]
45
+ query_options[:work_group] = settings["workgroup"]
46
+ end
47
+
48
+ resp = client.start_query_execution(**query_options)
49
+ query_execution_id = resp.query_execution_id
50
+
51
+ timeout = data_source.timeout || 300
52
+ stop_at = Time.now + timeout
53
+ resp = nil
54
+
55
+ begin
56
+ resp = client.get_query_results(
57
+ query_execution_id: query_execution_id
58
+ )
59
+ rescue Aws::Athena::Errors::InvalidRequestException => e
60
+ unless e.message.start_with?("Query has not yet finished.")
61
+ raise e
62
+ end
63
+ if Time.now < stop_at
64
+ sleep(3)
65
+ retry
66
+ end
67
+ end
68
+
69
+ if resp && resp.result_set
70
+ column_info = resp.result_set.result_set_metadata.column_info
71
+ columns = column_info.map(&:name)
72
+ column_types = column_info.map(&:type)
73
+
74
+ untyped_rows = []
75
+
76
+ # paginated
77
+ resp.each do |page|
78
+ untyped_rows.concat page.result_set.rows.map { |r| r.data.map(&:var_char_value) }
79
+ end
80
+
81
+ utc = ActiveSupport::TimeZone['Etc/UTC']
82
+
83
+ rows = untyped_rows[1..-1] || []
84
+ rows = untyped_rows[0..-1] unless column_info.present?
85
+ column_types.each_with_index do |ct, i|
86
+ # TODO more column_types
87
+ case ct
88
+ when "timestamp", "timestamp with time zone"
89
+ rows.each do |row|
90
+ row[i] &&= utc.parse(row[i])
91
+ end
92
+ when "date"
93
+ rows.each do |row|
94
+ row[i] &&= Date.parse(row[i])
95
+ end
96
+ when "bigint"
97
+ rows.each do |row|
98
+ row[i] &&= row[i].to_i
99
+ end
100
+ when "double"
101
+ rows.each do |row|
102
+ row[i] &&= row[i].to_f
103
+ end
104
+ end
105
+ end
106
+ elsif resp
107
+ error = fetch_error(query_execution_id)
108
+ else
109
+ error = Blazer::TIMEOUT_MESSAGE
110
+ end
111
+ rescue Aws::Athena::Errors::InvalidRequestException => e
112
+ error = e.message
113
+ if error == "Query did not finish successfully. Final query state: FAILED"
114
+ error = fetch_error(query_execution_id)
115
+ end
116
+ end
117
+
118
+ [columns, rows, error]
119
+ end
120
+
121
+ def tables
122
+ glue.get_tables(database_name: database).table_list.map(&:name).sort
123
+ end
124
+
125
+ def schema
126
+ glue.get_tables(database_name: database).table_list.map { |t| {table: t.name, columns: t.storage_descriptor.columns.map { |c| {name: c.name, data_type: c.type} }} }
127
+ end
128
+
129
+ def preview_statement
130
+ "SELECT * FROM {table} LIMIT 10"
131
+ end
132
+
133
+ # https://docs.aws.amazon.com/athena/latest/ug/select.html#select-escaping
134
+ def quoting
135
+ :single_quote_escape
136
+ end
137
+
138
+ # https://docs.aws.amazon.com/athena/latest/ug/querying-with-prepared-statements.html
139
+ def parameter_binding
140
+ engine_version > 1 ? :positional : nil
141
+ end
142
+
143
+ private
144
+
145
+ def database
146
+ @database ||= settings["database"] || "default"
147
+ end
148
+
149
+ # note: this setting is experimental
150
+ # it does *not* need to be set to use engine version 2
151
+ # prepared statements must be manually deleted if enabled
152
+ def engine_version
153
+ @engine_version ||= (settings["engine_version"] || 1).to_i
154
+ end
155
+
156
+ def fetch_error(query_execution_id)
157
+ client.get_query_execution(
158
+ query_execution_id: query_execution_id
159
+ ).query_execution.status.state_change_reason
160
+ end
161
+
162
+ def client
163
+ @client ||= Aws::Athena::Client.new(**client_options)
164
+ end
165
+
166
+ def glue
167
+ @glue ||= Aws::Glue::Client.new(**client_options)
168
+ end
169
+
170
+ def client_options
171
+ @client_options ||= begin
172
+ options = {}
173
+ if settings["access_key_id"] || settings["secret_access_key"]
174
+ options[:credentials] = Aws::Credentials.new(settings["access_key_id"], settings["secret_access_key"])
175
+ end
176
+ options[:region] = settings["region"] if settings["region"]
177
+ options
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end