blazer 2.2.6

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

Potentially problematic release.


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

Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +310 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +1041 -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/Chart.js +14456 -0
  13. data/app/assets/javascripts/blazer/Sortable.js +1540 -0
  14. data/app/assets/javascripts/blazer/ace.js +6 -0
  15. data/app/assets/javascripts/blazer/ace/ace.js +21301 -0
  16. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1993 -0
  17. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -0
  18. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -0
  19. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -0
  20. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -0
  21. data/app/assets/javascripts/blazer/application.js +81 -0
  22. data/app/assets/javascripts/blazer/bootstrap.js +2377 -0
  23. data/app/assets/javascripts/blazer/chartkick.js +2214 -0
  24. data/app/assets/javascripts/blazer/daterangepicker.js +1653 -0
  25. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  26. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  27. data/app/assets/javascripts/blazer/jquery-ujs.js +555 -0
  28. data/app/assets/javascripts/blazer/jquery.js +10364 -0
  29. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  30. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1212 -0
  31. data/app/assets/javascripts/blazer/moment.js +3043 -0
  32. data/app/assets/javascripts/blazer/queries.js +110 -0
  33. data/app/assets/javascripts/blazer/routes.js +26 -0
  34. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  35. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  36. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  37. data/app/assets/javascripts/blazer/vue.js +10947 -0
  38. data/app/assets/stylesheets/blazer/application.css +234 -0
  39. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6756 -0
  40. data/app/assets/stylesheets/blazer/daterangepicker.css +269 -0
  41. data/app/assets/stylesheets/blazer/github.css +125 -0
  42. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  43. data/app/controllers/blazer/base_controller.rb +124 -0
  44. data/app/controllers/blazer/checks_controller.rb +56 -0
  45. data/app/controllers/blazer/dashboards_controller.rb +101 -0
  46. data/app/controllers/blazer/queries_controller.rb +347 -0
  47. data/app/helpers/blazer/base_helper.rb +43 -0
  48. data/app/mailers/blazer/check_mailer.rb +27 -0
  49. data/app/mailers/blazer/slack_notifier.rb +79 -0
  50. data/app/models/blazer/audit.rb +6 -0
  51. data/app/models/blazer/check.rb +104 -0
  52. data/app/models/blazer/connection.rb +5 -0
  53. data/app/models/blazer/dashboard.rb +17 -0
  54. data/app/models/blazer/dashboard_query.rb +9 -0
  55. data/app/models/blazer/query.rb +40 -0
  56. data/app/models/blazer/record.rb +5 -0
  57. data/app/views/blazer/_nav.html.erb +15 -0
  58. data/app/views/blazer/_variables.html.erb +124 -0
  59. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  60. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  61. data/app/views/blazer/checks/_form.html.erb +79 -0
  62. data/app/views/blazer/checks/edit.html.erb +3 -0
  63. data/app/views/blazer/checks/index.html.erb +69 -0
  64. data/app/views/blazer/checks/new.html.erb +3 -0
  65. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  66. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  67. data/app/views/blazer/dashboards/new.html.erb +3 -0
  68. data/app/views/blazer/dashboards/show.html.erb +51 -0
  69. data/app/views/blazer/queries/_form.html.erb +250 -0
  70. data/app/views/blazer/queries/docs.html.erb +131 -0
  71. data/app/views/blazer/queries/edit.html.erb +2 -0
  72. data/app/views/blazer/queries/home.html.erb +163 -0
  73. data/app/views/blazer/queries/new.html.erb +2 -0
  74. data/app/views/blazer/queries/run.html.erb +198 -0
  75. data/app/views/blazer/queries/schema.html.erb +55 -0
  76. data/app/views/blazer/queries/show.html.erb +75 -0
  77. data/app/views/layouts/blazer/application.html.erb +24 -0
  78. data/config/routes.rb +20 -0
  79. data/lib/blazer.rb +231 -0
  80. data/lib/blazer/adapters/athena_adapter.rb +129 -0
  81. data/lib/blazer/adapters/base_adapter.rb +53 -0
  82. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  83. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  84. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  85. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  86. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  87. data/lib/blazer/adapters/influxdb_adapter.rb +45 -0
  88. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  89. data/lib/blazer/adapters/neo4j_adapter.rb +47 -0
  90. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  91. data/lib/blazer/adapters/salesforce_adapter.rb +45 -0
  92. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  93. data/lib/blazer/adapters/soda_adapter.rb +96 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +221 -0
  95. data/lib/blazer/data_source.rb +195 -0
  96. data/lib/blazer/detect_anomalies.R +19 -0
  97. data/lib/blazer/engine.rb +43 -0
  98. data/lib/blazer/result.rb +218 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +18 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +22 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +73 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. metadata +231 -0
@@ -0,0 +1,43 @@
1
+ module Blazer
2
+ module BaseHelper
3
+ def blazer_title(title = nil)
4
+ if title
5
+ content_for(:title) { title }
6
+ else
7
+ content_for?(:title) ? content_for(:title) : nil
8
+ end
9
+ end
10
+
11
+ BLAZER_URL_REGEX = /\Ahttps?:\/\/[\S]+\z/
12
+ BLAZER_IMAGE_EXT = %w[png jpg jpeg gif]
13
+
14
+ def blazer_format_value(key, value)
15
+ if value.is_a?(Numeric) && !key.to_s.end_with?("id") && !key.to_s.start_with?("id")
16
+ number_with_delimiter(value)
17
+ elsif value.is_a?(String) && value =~ BLAZER_URL_REGEX
18
+ # see if image or link
19
+ if Blazer.images && (key.include?("image") || BLAZER_IMAGE_EXT.include?(value.split(".").last.split("?").first.try(:downcase)))
20
+ link_to value, target: "_blank" do
21
+ image_tag value, referrerpolicy: "no-referrer"
22
+ end
23
+ else
24
+ link_to value, value, target: "_blank"
25
+ end
26
+ else
27
+ value
28
+ end
29
+ end
30
+
31
+ def blazer_maps?
32
+ Blazer.mapbox_access_token.present?
33
+ end
34
+
35
+ def blazer_js_var(name, value)
36
+ "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
37
+ end
38
+
39
+ def blazer_series_name(k)
40
+ k.nil? ? "null" : k.to_s
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ module Blazer
2
+ class CheckMailer < ActionMailer::Base
3
+ include ActionView::Helpers::TextHelper
4
+
5
+ default from: Blazer.from_email if Blazer.from_email
6
+ layout false
7
+
8
+ def state_change(check, state, state_was, rows_count, error, columns, rows, column_types, check_type)
9
+ @check = check
10
+ @state = state
11
+ @state_was = state_was
12
+ @rows_count = rows_count
13
+ @error = error
14
+ @columns = columns
15
+ @rows = rows
16
+ @column_types = column_types
17
+ @check_type = check_type
18
+ mail to: check.emails, reply_to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
19
+ end
20
+
21
+ def failing_checks(email, checks)
22
+ @checks = checks
23
+ # add reply_to for mailing lists
24
+ mail to: email, reply_to: email, subject: "#{pluralize(checks.size, "Check")} Failing"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ require "net/http"
2
+
3
+ module Blazer
4
+ class SlackNotifier
5
+ def self.state_change(check, state, state_was, rows_count, error, check_type)
6
+ check.split_slack_channels.each do |channel|
7
+ text =
8
+ if error
9
+ error
10
+ elsif rows_count > 0 && check_type == "bad_data"
11
+ pluralize(rows_count, "row")
12
+ end
13
+
14
+ payload = {
15
+ channel: channel,
16
+ attachments: [
17
+ {
18
+ title: escape("Check #{state.titleize}: #{check.query.name}"),
19
+ title_link: query_url(check.query_id),
20
+ text: escape(text),
21
+ color: state == "passing" ? "good" : "danger"
22
+ }
23
+ ]
24
+ }
25
+
26
+ post(Blazer.slack_webhook_url, payload)
27
+ end
28
+ end
29
+
30
+ def self.failing_checks(channel, checks)
31
+ text =
32
+ checks.map do |check|
33
+ "<#{query_url(check.query_id)}|#{escape(check.query.name)}> #{escape(check.state)}"
34
+ end
35
+
36
+ payload = {
37
+ channel: channel,
38
+ attachments: [
39
+ {
40
+ title: escape("#{pluralize(checks.size, "Check")} Failing"),
41
+ text: text.join("\n"),
42
+ color: "warning"
43
+ }
44
+ ]
45
+ }
46
+
47
+ post(Blazer.slack_webhook_url, payload)
48
+ end
49
+
50
+ # https://api.slack.com/docs/message-formatting#how_to_escape_characters
51
+ # - Replace the ampersand, &, with &amp;
52
+ # - Replace the less-than sign, < with &lt;
53
+ # - Replace the greater-than sign, > with &gt;
54
+ # That's it. Don't HTML entity-encode the entire message.
55
+ def self.escape(str)
56
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") if str
57
+ end
58
+
59
+ def self.pluralize(*args)
60
+ ActionController::Base.helpers.pluralize(*args)
61
+ end
62
+
63
+ # checks shouldn't have variables, but in any case,
64
+ # avoid passing variable params to url helpers
65
+ # (known unsafe parameters are removed, but blacklist isn't ideal)
66
+ def self.query_url(id)
67
+ Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
68
+ end
69
+
70
+ def self.post(url, payload)
71
+ uri = URI.parse(url)
72
+ http = Net::HTTP.new(uri.host, uri.port)
73
+ http.use_ssl = true
74
+ http.open_timeout = 3
75
+ http.read_timeout = 5
76
+ http.post(uri.request_uri, payload.to_json)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ module Blazer
2
+ class Audit < Record
3
+ belongs_to :user, optional: true, class_name: Blazer.user_class.to_s
4
+ belongs_to :query, optional: true
5
+ end
6
+ end
@@ -0,0 +1,104 @@
1
+ module Blazer
2
+ class Check < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+ belongs_to :query
5
+
6
+ validates :query_id, presence: true
7
+ validate :validate_emails
8
+ validate :validate_variables, if: -> { query_id_changed? }
9
+
10
+ before_validation :set_state
11
+ before_validation :fix_emails
12
+
13
+ def split_emails
14
+ emails.to_s.downcase.split(",").map(&:strip)
15
+ end
16
+
17
+ def split_slack_channels
18
+ if Blazer.slack?
19
+ slack_channels.to_s.downcase.split(",").map(&:strip)
20
+ else
21
+ []
22
+ end
23
+ end
24
+
25
+ def update_state(result)
26
+ check_type =
27
+ if respond_to?(:check_type)
28
+ self.check_type
29
+ elsif respond_to?(:invert)
30
+ invert ? "missing_data" : "bad_data"
31
+ else
32
+ "bad_data"
33
+ end
34
+
35
+ message = result.error
36
+
37
+ self.state =
38
+ if result.timed_out?
39
+ "timed out"
40
+ elsif result.error
41
+ "error"
42
+ elsif check_type == "anomaly"
43
+ anomaly, message = result.detect_anomaly
44
+ if anomaly.nil?
45
+ "error"
46
+ elsif anomaly
47
+ "failing"
48
+ else
49
+ "passing"
50
+ end
51
+ elsif result.rows.any?
52
+ check_type == "missing_data" ? "passing" : "failing"
53
+ else
54
+ check_type == "missing_data" ? "failing" : "passing"
55
+ end
56
+
57
+ self.last_run_at = Time.now if respond_to?(:last_run_at=)
58
+ self.message = message if respond_to?(:message=)
59
+
60
+ if respond_to?(:timeouts=)
61
+ if result.timed_out?
62
+ self.timeouts += 1
63
+ self.state = "disabled" if timeouts >= 3
64
+ else
65
+ self.timeouts = 0
66
+ end
67
+ end
68
+
69
+ # do not notify on creation, except when not passing
70
+ if (state_was != "new" || state != "passing") && state != state_was
71
+ Blazer::CheckMailer.state_change(self, state, state_was, result.rows.size, message, result.columns, result.rows.first(10).as_json, result.column_types, check_type).deliver_now if emails.present?
72
+ Blazer::SlackNotifier.state_change(self, state, state_was, result.rows.size, message, check_type)
73
+ end
74
+ save! if changed?
75
+ end
76
+
77
+ private
78
+
79
+ def set_state
80
+ self.state ||= "new"
81
+ end
82
+
83
+ def fix_emails
84
+ # some people like doing ; instead of ,
85
+ # but we know what they mean, so let's fix it
86
+ # also, some people like to use whitespace
87
+ if emails.present?
88
+ self.emails = emails.strip.gsub(/[;\s]/, ",").gsub(/,+/, ", ")
89
+ end
90
+ end
91
+
92
+ def validate_emails
93
+ unless split_emails.all? { |e| e =~ /\A\S+@\S+\.\S+\z/ }
94
+ errors.add(:base, "Invalid emails")
95
+ end
96
+ end
97
+
98
+ def validate_variables
99
+ if query.variables.any?
100
+ errors.add(:base, "Query can't have variables")
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,5 @@
1
+ module Blazer
2
+ class Connection < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ module Blazer
2
+ class Dashboard < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+ has_many :dashboard_queries, dependent: :destroy
5
+ has_many :queries, through: :dashboard_queries
6
+
7
+ validates :name, presence: true
8
+
9
+ def variables
10
+ queries.flat_map { |q| q.variables }.uniq
11
+ end
12
+
13
+ def to_param
14
+ [id, name.gsub("'", "").parameterize].join("-")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module Blazer
2
+ class DashboardQuery < Record
3
+ belongs_to :dashboard
4
+ belongs_to :query
5
+
6
+ validates :dashboard_id, presence: true
7
+ validates :query_id, presence: true
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ module Blazer
2
+ class Query < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+ has_many :checks, dependent: :destroy
5
+ has_many :dashboard_queries, dependent: :destroy
6
+ has_many :dashboards, through: :dashboard_queries
7
+ has_many :audits
8
+
9
+ validates :statement, presence: true
10
+
11
+ scope :named, -> { where("blazer_queries.name <> ''") }
12
+
13
+ def to_param
14
+ [id, name].compact.join("-").gsub("'", "").parameterize
15
+ end
16
+
17
+ def friendly_name
18
+ name.to_s.sub(/\A[#\*]/, "").gsub(/\[.+\]/, "").strip
19
+ end
20
+
21
+ def viewable?(user)
22
+ if Blazer.query_viewable
23
+ Blazer.query_viewable.call(self, user)
24
+ else
25
+ true
26
+ end
27
+ end
28
+
29
+ def editable?(user)
30
+ editable = !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == try(:creator)
31
+ editable &&= viewable?(user)
32
+ editable &&= Blazer.query_editable.call(self, user) if Blazer.query_editable
33
+ editable
34
+ end
35
+
36
+ def variables
37
+ Blazer.extract_vars(statement)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module Blazer
2
+ class Record < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <div class="btn-group" style="vertical-align: top; margin-right: 5px;">
2
+ <%= link_to "Home", root_path, class: "btn btn-primary" %>
3
+ <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
4
+ <span class="caret"></span>
5
+ <span class="sr-only">Toggle Dropdown</span>
6
+ </button>
7
+ <ul class="dropdown-menu">
8
+ <li><%= link_to "Checks", checks_path %></li>
9
+ <li role="separator" class="divider"></li>
10
+ <li><%= link_to "New Query", new_query_path %></li>
11
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
12
+ <% check_params = @query ? {query_id: @query.id} : {} %>
13
+ <li><%= link_to "New Check", new_check_path(check_params) %></li>
14
+ </ul>
15
+ </div>
@@ -0,0 +1,124 @@
1
+ <% if @bind_vars.any? %>
2
+ <script>
3
+ <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
4
+ var now = moment.tz(timeZone)
5
+ var format = "YYYY-MM-DD"
6
+
7
+ function toDate(time) {
8
+ return moment.tz(time.format(format), timeZone)
9
+ }
10
+ </script>
11
+ <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
12
+ <% date_vars = ["start_time", "end_time"] %>
13
+ <% if (date_vars - @bind_vars).empty? %>
14
+ <% @bind_vars = @bind_vars - date_vars %>
15
+ <% else %>
16
+ <% date_vars = nil %>
17
+ <% end %>
18
+
19
+ <% @bind_vars.each_with_index do |var, i| %>
20
+ <%= label_tag var, var %>
21
+ <% if (data = @smart_vars[var]) %>
22
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
23
+ <script>
24
+ $("#<%= var %>").selectize({
25
+ create: true
26
+ });
27
+ </script>
28
+ <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
29
+ <%= hidden_field_tag var, params[var] %>
30
+
31
+ <div class="selectize-control single" style="width: 200px;">
32
+ <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
33
+ <span>Select a date</span>
34
+ </div>
35
+ </div>
36
+
37
+ <script>
38
+ (function() {
39
+ var input = $("#<%= var %>")
40
+ var datePicker = $("#<%= var %>-select")
41
+ datePicker.daterangepicker({
42
+ singleDatePicker: true,
43
+ locale: {format: format},
44
+ autoUpdateInput: false,
45
+ startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
46
+ })
47
+ // hack to start with empty date
48
+ datePicker.on("apply.daterangepicker", function(ev, picker) {
49
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
50
+ input.val(toDate(picker.startDate).utc().format())
51
+ submitIfCompleted($("#<%= var %>").closest("form"))
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
+ }
57
+ })()
58
+ </script>
59
+ <% else %>
60
+ <%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
61
+ <% end %>
62
+ <% end %>
63
+
64
+ <% if date_vars %>
65
+ <% date_vars.each do |var| %>
66
+ <%= hidden_field_tag var, params[var] %>
67
+ <% end %>
68
+
69
+ <%= label_tag nil, date_vars.join(" & ") %>
70
+ <div class="selectize-control single" style="width: 300px;">
71
+ <div id="reportrange" class="selectize-input" style="display: inline-block;">
72
+ <span>Select a time range</span>
73
+ </div>
74
+ </div>
75
+
76
+ <script>
77
+ function dateStr(daysAgo) {
78
+ return now.clone().subtract(daysAgo || 0, "days").format(format)
79
+ }
80
+
81
+ function setTimeInputs(start, end) {
82
+ $("#start_time").val(toDate(start).utc().format())
83
+ $("#end_time").val(toDate(end).endOf("day").utc().format())
84
+ }
85
+
86
+ $("#reportrange").daterangepicker(
87
+ {
88
+ ranges: {
89
+ "Today": [dateStr(), dateStr()],
90
+ "Last 7 Days": [dateStr(6), dateStr()],
91
+ "Last 30 Days": [dateStr(29), dateStr()]
92
+ },
93
+ locale: {
94
+ format: format
95
+ },
96
+ startDate: dateStr(29),
97
+ endDate: dateStr(),
98
+ opens: "right"
99
+ },
100
+ function(start, end) {
101
+ setTimeInputs(start, end)
102
+ submitIfCompleted($("#start_time").closest("form"))
103
+ }
104
+ ).on("apply.daterangepicker", function(ev, picker) {
105
+ setTimeInputs(picker.startDate, picker.endDate)
106
+ $("#reportrange span").html(toDate(picker.startDate).format("MMMM D, YYYY") + " - " + toDate(picker.endDate).format("MMMM D, YYYY"))
107
+ })
108
+
109
+ if ($("#start_time").val().length > 0) {
110
+ var picker = $("#reportrange").data("daterangepicker")
111
+ picker.setStartDate(moment.tz($("#start_time").val(), timeZone))
112
+ picker.setEndDate(moment.tz($("#end_time").val(), timeZone))
113
+ $("#reportrange").trigger("apply.daterangepicker", picker)
114
+ } else {
115
+ var picker = $("#reportrange").data("daterangepicker")
116
+ $("#reportrange").trigger("apply.daterangepicker", picker)
117
+ submitIfCompleted($("#start_time").closest("form"))
118
+ }
119
+ </script>
120
+ <% end %>
121
+
122
+ <input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
123
+ </form>
124
+ <% end %>