railsblazer 2.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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.github/ISSUE_TEMPLATE.md +0 -0
  4. data/.gitignore +14 -0
  5. data/CHANGELOG.md +247 -0
  6. data/CONTRIBUTING.md +42 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +855 -0
  10. data/Rakefile +1 -0
  11. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  12. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  13. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  14. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  15. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  16. data/app/assets/javascripts/blazer/Chart.js +14145 -0
  17. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  18. data/app/assets/javascripts/blazer/ace.js +6 -0
  19. data/app/assets/javascripts/blazer/ace/ace.js +11 -0
  20. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
  21. data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
  22. data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
  23. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
  24. data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
  25. data/app/assets/javascripts/blazer/application.js +79 -0
  26. data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
  27. data/app/assets/javascripts/blazer/chartkick.js +1693 -0
  28. data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
  29. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  30. data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
  31. data/app/assets/javascripts/blazer/jquery.js +10308 -0
  32. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
  33. data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
  34. data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
  35. data/app/assets/javascripts/blazer/moment.js +3043 -0
  36. data/app/assets/javascripts/blazer/queries.js +110 -0
  37. data/app/assets/javascripts/blazer/routes.js +23 -0
  38. data/app/assets/javascripts/blazer/selectize.js +3667 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +114 -0
  40. data/app/assets/javascripts/blazer/vue.js +7515 -0
  41. data/app/assets/stylesheets/blazer/application.css +198 -0
  42. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
  43. data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
  44. data/app/assets/stylesheets/blazer/github.css +125 -0
  45. data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
  46. data/app/controllers/blazer/base_controller.rb +113 -0
  47. data/app/controllers/blazer/checks_controller.rb +56 -0
  48. data/app/controllers/blazer/dashboards_controller.rb +105 -0
  49. data/app/controllers/blazer/queries_controller.rb +337 -0
  50. data/app/helpers/blazer/base_helper.rb +57 -0
  51. data/app/mailers/blazer/check_mailer.rb +27 -0
  52. data/app/mailers/blazer/slack_notifier.rb +76 -0
  53. data/app/models/blazer/audit.rb +6 -0
  54. data/app/models/blazer/check.rb +104 -0
  55. data/app/models/blazer/connection.rb +5 -0
  56. data/app/models/blazer/dashboard.rb +13 -0
  57. data/app/models/blazer/dashboard_query.rb +9 -0
  58. data/app/models/blazer/query.rb +40 -0
  59. data/app/models/blazer/record.rb +5 -0
  60. data/app/views/blazer/_nav.html.erb +16 -0
  61. data/app/views/blazer/_variables.html.erb +102 -0
  62. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  63. data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
  64. data/app/views/blazer/checks/_form.html.erb +79 -0
  65. data/app/views/blazer/checks/edit.html.erb +1 -0
  66. data/app/views/blazer/checks/index.html.erb +43 -0
  67. data/app/views/blazer/checks/new.html.erb +1 -0
  68. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  69. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  70. data/app/views/blazer/dashboards/new.html.erb +1 -0
  71. data/app/views/blazer/dashboards/show.html.erb +47 -0
  72. data/app/views/blazer/queries/_form.html.erb +240 -0
  73. data/app/views/blazer/queries/edit.html.erb +2 -0
  74. data/app/views/blazer/queries/home.html.erb +152 -0
  75. data/app/views/blazer/queries/new.html.erb +2 -0
  76. data/app/views/blazer/queries/run.html.erb +165 -0
  77. data/app/views/blazer/queries/schema.html.erb +20 -0
  78. data/app/views/blazer/queries/show.html.erb +73 -0
  79. data/app/views/layouts/blazer/application.html.erb +24 -0
  80. data/blazer-0.0.1.gem +0 -0
  81. data/blazer.gemspec +27 -0
  82. data/config/routes.rb +16 -0
  83. data/lib/blazer.rb +223 -0
  84. data/lib/blazer/adapters/athena_adapter.rb +128 -0
  85. data/lib/blazer/adapters/base_adapter.rb +53 -0
  86. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  87. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  88. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  89. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  90. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  91. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  92. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  93. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +182 -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 +30 -0
  98. data/lib/blazer/result.rb +170 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +21 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +39 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +62 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. data/railsblazer-0.0.1.gem +0 -0
  107. metadata +234 -0
@@ -0,0 +1,57 @@
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?(Integer) && !key.to_s.end_with?("id") && !key.to_s.start_with?("id")
16
+ number_with_delimiter(value)
17
+ elsif 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
+ ENV["MAPBOX_ACCESS_TOKEN"].present?
33
+ end
34
+
35
+ def blazer_js_var(name, value)
36
+ "var #{name} = #{blazer_json_escape(value.to_json(root: false))};".html_safe
37
+ end
38
+
39
+ JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
40
+ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
41
+
42
+ # Prior to version 4.1 of rails double quotes were inadventently removed in json_escape.
43
+ # This adds the correct json_escape functionality to rails versions < 4.1
44
+ def blazer_json_escape(s)
45
+ if Rails::VERSION::STRING < "4.1"
46
+ result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
47
+ s.html_safe? ? result.html_safe : result
48
+ else
49
+ json_escape(s)
50
+ end
51
+ end
52
+
53
+ def blazer_series_name(k)
54
+ k.nil? ? "null" : k.to_s
55
+ end
56
+ end
57
+ 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,76 @@
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
+ def self.query_url(id)
64
+ Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
65
+ end
66
+
67
+ def self.post(url, payload)
68
+ uri = URI.parse(url)
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ http.use_ssl = true
71
+ http.open_timeout = 3
72
+ http.read_timeout = 5
73
+ http.post(uri.request_uri, payload.to_json)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,6 @@
1
+ module Blazer
2
+ class Audit < Record
3
+ belongs_to :user, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s)
4
+ belongs_to :query, Blazer::BELONGS_TO_OPTIONAL
5
+ end
6
+ end
@@ -0,0 +1,104 @@
1
+ module Blazer
2
+ class Check < Record
3
+ belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(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 && emails.present?
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
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,13 @@
1
+ module Blazer
2
+ class Dashboard < Record
3
+ belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(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 to_param
10
+ [id, name.gsub("'", "").parameterize].join("-")
11
+ end
12
+ end
13
+ 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, Blazer::BELONGS_TO_OPTIONAL.merge(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,16 @@
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 "Dashboards", dashboards_path %></li>
9
+ <li><%= link_to "Checks", checks_path %></li>
10
+ <li role="separator" class="divider"></li>
11
+ <li><%= link_to "New Query", new_query_path %></li>
12
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
13
+ <% check_params = @query ? {query_id: @query.id} : {} %>
14
+ <li><%= link_to "New Check", new_check_path(check_params) %></li>
15
+ </ul>
16
+ </div>
@@ -0,0 +1,102 @@
1
+ <% if @bind_vars.any? %>
2
+ <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 10px;">
3
+ <% date_vars = ["start_time", "end_time"] %>
4
+ <% if (date_vars - @bind_vars).empty? %>
5
+ <% @bind_vars = @bind_vars - date_vars %>
6
+ <% else %>
7
+ <% date_vars = nil %>
8
+ <% end %>
9
+
10
+ <% @bind_vars.each_with_index do |var, i| %>
11
+ <%= label_tag var, var %>
12
+ <% if (data = @smart_vars[var]) %>
13
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
14
+ <script>
15
+ $("#<%= var %>").selectize({
16
+ create: true
17
+ });
18
+ </script>
19
+ <% else %>
20
+ <%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
21
+ <% if var.end_with?("_at") %>
22
+ <script>
23
+ $("#<%= var %>").daterangepicker({singleDatePicker: true, locale: {format: "YYYY-MM-DD"}, autoUpdateInput: false});
24
+ // hack to start with empty date
25
+ $("#<%= var %>").on("apply.daterangepicker", function(ev, picker) {
26
+ $(this).val(picker.startDate.format("YYYY-MM-DD"));
27
+ $(this).change();
28
+ });
29
+ </script>
30
+ <% end %>
31
+ <% end %>
32
+ <% end %>
33
+
34
+ <% if date_vars %>
35
+ <% date_vars.each do |var| %>
36
+ <%= hidden_field_tag var, params[var] %>
37
+ <% end %>
38
+
39
+ <%= label_tag nil, date_vars.join(" & ") %>
40
+ <div class="selectize-control single" style="width: 300px;">
41
+ <div id="reportrange" class="selectize-input" style="display: inline-block;">
42
+ <span>Select a time range</span>
43
+ </div>
44
+ </div>
45
+
46
+ <script>
47
+ <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
48
+ var format = "YYYY-MM-DD"
49
+ var now = moment.tz(timeZone)
50
+
51
+ function dateStr(daysAgo) {
52
+ return now.clone().subtract(daysAgo || 0, "days").format(format)
53
+ }
54
+
55
+ function toDate(time) {
56
+ return moment.tz(time.format(format), timeZone)
57
+ }
58
+
59
+ function setTimeInputs(start, end) {
60
+ $("#start_time").val(toDate(start).utc().format())
61
+ $("#end_time").val(toDate(end).endOf("day").utc().format())
62
+ }
63
+
64
+ $("#reportrange").daterangepicker(
65
+ {
66
+ ranges: {
67
+ "Today": [dateStr(), dateStr()],
68
+ "Last 7 Days": [dateStr(6), dateStr()],
69
+ "Last 30 Days": [dateStr(29), dateStr()]
70
+ },
71
+ locale: {
72
+ format: format
73
+ },
74
+ startDate: dateStr(29),
75
+ endDate: dateStr(),
76
+ opens: "right"
77
+ },
78
+ function(start, end) {
79
+ setTimeInputs(start, end)
80
+ submitIfCompleted($("#start_time").closest("form"))
81
+ }
82
+ ).on("apply.daterangepicker", function(ev, picker) {
83
+ setTimeInputs(picker.startDate, picker.endDate)
84
+ $("#reportrange span").html(toDate(picker.startDate).format("MMMM D, YYYY") + " - " + toDate(picker.endDate).format("MMMM D, YYYY"))
85
+ })
86
+
87
+ if ($("#start_time").val().length > 0) {
88
+ var picker = $("#reportrange").data("daterangepicker")
89
+ picker.setStartDate(moment.tz($("#start_time").val(), timeZone))
90
+ picker.setEndDate(moment.tz($("#end_time").val(), timeZone))
91
+ $("#reportrange").trigger("apply.daterangepicker", picker)
92
+ } else {
93
+ var picker = $("#reportrange").data("daterangepicker")
94
+ $("#reportrange").trigger("apply.daterangepicker", picker)
95
+ submitIfCompleted($("#start_time").closest("form"))
96
+ }
97
+ </script>
98
+ <% end %>
99
+
100
+ <input type="submit" class="btn btn-success" value="Run" style="vertical-align: top;" />
101
+ </form>
102
+ <% end %>