finery 3.0.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.
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