blazer 1.7.7 → 2.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +242 -33
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +621 -211
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +0 -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 +15658 -10011
  13. data/app/assets/javascripts/blazer/Sortable.js +3413 -848
  14. data/app/assets/javascripts/blazer/ace/ace.js +21294 -4
  15. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1991 -3
  16. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -1
  17. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -1
  18. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -1
  19. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -1
  20. data/app/assets/javascripts/blazer/application.js +5 -3
  21. data/app/assets/javascripts/blazer/bootstrap.js +842 -628
  22. data/app/assets/javascripts/blazer/chartkick.js +2015 -1244
  23. data/app/assets/javascripts/blazer/daterangepicker.js +372 -299
  24. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  25. data/app/assets/javascripts/blazer/{jquery_ujs.js → jquery-ujs.js} +161 -75
  26. data/app/assets/javascripts/blazer/jquery.js +10126 -9562
  27. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +321 -259
  28. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1546 -0
  29. data/app/assets/javascripts/blazer/moment.js +5085 -2460
  30. data/app/assets/javascripts/blazer/queries.js +18 -4
  31. data/app/assets/javascripts/blazer/routes.js +3 -0
  32. data/app/assets/javascripts/blazer/selectize.js +3828 -3604
  33. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  34. data/app/assets/javascripts/blazer/stupidtable.js +254 -87
  35. data/app/assets/javascripts/blazer/vue.js +11175 -6676
  36. data/app/assets/stylesheets/blazer/application.css +51 -6
  37. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  38. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  39. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +1337 -711
  40. data/app/assets/stylesheets/blazer/{daterangepicker-bs3.css → daterangepicker.css} +207 -172
  41. data/app/assets/stylesheets/blazer/{selectize.default.css → selectize.css} +26 -10
  42. data/app/controllers/blazer/base_controller.rb +73 -46
  43. data/app/controllers/blazer/checks_controller.rb +1 -1
  44. data/app/controllers/blazer/dashboards_controller.rb +7 -13
  45. data/app/controllers/blazer/queries_controller.rb +171 -51
  46. data/app/controllers/blazer/uploads_controller.rb +147 -0
  47. data/app/helpers/blazer/base_helper.rb +6 -16
  48. data/app/models/blazer/audit.rb +3 -3
  49. data/app/models/blazer/check.rb +31 -5
  50. data/app/models/blazer/dashboard.rb +6 -2
  51. data/app/models/blazer/dashboard_query.rb +1 -1
  52. data/app/models/blazer/query.rb +30 -4
  53. data/app/models/blazer/record.rb +5 -0
  54. data/app/models/blazer/upload.rb +11 -0
  55. data/app/models/blazer/uploads_connection.rb +7 -0
  56. data/app/views/blazer/_nav.html.erb +3 -1
  57. data/app/views/blazer/_variables.html.erb +48 -23
  58. data/app/views/blazer/check_mailer/failing_checks.html.erb +1 -0
  59. data/app/views/blazer/check_mailer/state_change.html.erb +1 -0
  60. data/app/views/blazer/checks/_form.html.erb +17 -9
  61. data/app/views/blazer/checks/edit.html.erb +2 -0
  62. data/app/views/blazer/checks/index.html.erb +37 -5
  63. data/app/views/blazer/checks/new.html.erb +2 -0
  64. data/app/views/blazer/dashboards/_form.html.erb +5 -5
  65. data/app/views/blazer/dashboards/edit.html.erb +2 -0
  66. data/app/views/blazer/dashboards/new.html.erb +2 -0
  67. data/app/views/blazer/dashboards/show.html.erb +13 -7
  68. data/app/views/blazer/queries/_caching.html.erb +16 -0
  69. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  70. data/app/views/blazer/queries/_form.html.erb +23 -13
  71. data/app/views/blazer/queries/docs.html.erb +137 -0
  72. data/app/views/blazer/queries/home.html.erb +21 -7
  73. data/app/views/blazer/queries/run.html.erb +64 -29
  74. data/app/views/blazer/queries/schema.html.erb +44 -7
  75. data/app/views/blazer/queries/show.html.erb +15 -8
  76. data/app/views/blazer/uploads/_form.html.erb +27 -0
  77. data/app/views/blazer/uploads/edit.html.erb +3 -0
  78. data/app/views/blazer/uploads/index.html.erb +55 -0
  79. data/app/views/blazer/uploads/new.html.erb +3 -0
  80. data/app/views/layouts/blazer/application.html.erb +10 -5
  81. data/config/routes.rb +10 -1
  82. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  83. data/lib/blazer/adapters/base_adapter.rb +24 -1
  84. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  85. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  86. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  87. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  88. data/lib/blazer/adapters/elasticsearch_adapter.rb +30 -18
  89. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  90. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  91. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  92. data/lib/blazer/adapters/mongodb_adapter.rb +5 -1
  93. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  94. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  95. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  96. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  97. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  98. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  99. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  100. data/lib/blazer/adapters/sql_adapter.rb +187 -20
  101. data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
  102. data/lib/blazer/data_source.rb +107 -30
  103. data/lib/blazer/engine.rb +21 -23
  104. data/lib/blazer/result.rb +95 -29
  105. data/lib/blazer/run_statement.rb +8 -4
  106. data/lib/blazer/run_statement_job.rb +8 -9
  107. data/lib/blazer/slack_notifier.rb +94 -0
  108. data/lib/blazer/statement.rb +75 -0
  109. data/lib/blazer/version.rb +1 -1
  110. data/lib/blazer.rb +154 -26
  111. data/lib/generators/blazer/install_generator.rb +7 -18
  112. data/lib/generators/blazer/templates/{config.yml → config.yml.tt} +26 -3
  113. data/lib/generators/blazer/templates/{install.rb → install.rb.tt} +6 -4
  114. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  115. data/lib/generators/blazer/uploads_generator.rb +18 -0
  116. data/lib/tasks/blazer.rake +11 -1
  117. data/licenses/LICENSE-ace.txt +24 -0
  118. data/licenses/LICENSE-bootstrap.txt +21 -0
  119. data/licenses/LICENSE-chart.js.txt +9 -0
  120. data/licenses/LICENSE-chartkick.js.txt +22 -0
  121. data/licenses/LICENSE-daterangepicker.txt +21 -0
  122. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  123. data/licenses/LICENSE-highlight.js.txt +29 -0
  124. data/licenses/LICENSE-jquery-ujs.txt +20 -0
  125. data/licenses/LICENSE-jquery.txt +20 -0
  126. data/licenses/LICENSE-moment-timezone.txt +20 -0
  127. data/licenses/LICENSE-moment.txt +22 -0
  128. data/licenses/LICENSE-selectize.txt +202 -0
  129. data/licenses/LICENSE-sortable.txt +21 -0
  130. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  131. data/licenses/LICENSE-stupidtable.txt +19 -0
  132. data/licenses/LICENSE-vue.txt +21 -0
  133. metadata +83 -53
  134. data/.gitignore +0 -14
  135. data/Gemfile +0 -4
  136. data/Rakefile +0 -1
  137. data/app/assets/javascripts/blazer/highlight.pack.js +0 -1
  138. data/app/assets/javascripts/blazer/moment-timezone.js +0 -1007
  139. data/blazer.gemspec +0 -26
@@ -0,0 +1,147 @@
1
+ module Blazer
2
+ class UploadsController < BaseController
3
+ before_action :ensure_uploads
4
+ before_action :set_upload, only: [:show, :edit, :update, :destroy]
5
+
6
+ def index
7
+ @uploads = Blazer::Upload.order(:table)
8
+ end
9
+
10
+ def new
11
+ @upload = Blazer::Upload.new
12
+ end
13
+
14
+ def create
15
+ @upload = Blazer::Upload.new(upload_params)
16
+ # use creator_id instead of creator
17
+ # since we setup association without checking if column exists
18
+ @upload.creator = blazer_user if @upload.respond_to?(:creator_id=) && blazer_user
19
+
20
+ success = params.require(:upload).key?(:file)
21
+ if success
22
+ Blazer::Upload.transaction do
23
+ success = @upload.save
24
+ if success
25
+ begin
26
+ update_file(@upload)
27
+ rescue CSV::MalformedCSVError, Blazer::UploadError => e
28
+ @upload.errors.add(:base, e.message)
29
+ success = false
30
+ raise ActiveRecord::Rollback
31
+ end
32
+ end
33
+ end
34
+ else
35
+ @upload.errors.add(:base, "File can't be blank")
36
+ end
37
+
38
+ if success
39
+ redirect_to upload_path(@upload)
40
+ else
41
+ render_errors @upload
42
+ end
43
+ end
44
+
45
+ def show
46
+ redirect_to new_query_path(upload_id: @upload.id)
47
+ end
48
+
49
+ def edit
50
+ end
51
+
52
+ def update
53
+ original_table = @upload.table
54
+ @upload.assign_attributes(upload_params)
55
+
56
+ success = nil
57
+ Blazer::Upload.transaction do
58
+ success = @upload.save
59
+ if success
60
+ if params.require(:upload).key?(:file)
61
+ begin
62
+ update_file(@upload, drop: original_table)
63
+ rescue CSV::MalformedCSVError, Blazer::UploadError => e
64
+ @upload.errors.add(:base, e.message)
65
+ success = false
66
+ raise ActiveRecord::Rollback
67
+ end
68
+ elsif @upload.table != original_table
69
+ Blazer.uploads_connection.execute("ALTER TABLE #{Blazer.uploads_table_name(original_table)} RENAME TO #{Blazer.uploads_connection.quote_table_name(@upload.table)}")
70
+ end
71
+ end
72
+ end
73
+
74
+ if success
75
+ redirect_to upload_path(@upload)
76
+ else
77
+ render_errors @upload
78
+ end
79
+ end
80
+
81
+ def destroy
82
+ Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{@upload.table_name}")
83
+ @upload.destroy
84
+ redirect_to uploads_path
85
+ end
86
+
87
+ private
88
+
89
+ def update_file(upload, drop: nil)
90
+ file = params.require(:upload).fetch(:file)
91
+ raise Blazer::UploadError, "File is not a CSV" if file.content_type != "text/csv"
92
+ raise Blazer::UploadError, "File is too large (maximum is 100MB)" if file.size > 100.megabytes
93
+
94
+ contents = file.read
95
+ rows = CSV.parse(contents, converters: %i[numeric date date_time])
96
+
97
+ # friendly column names
98
+ columns = rows.shift.map { |v| v.to_s.encode("UTF-8").gsub("%", " pct ").parameterize.gsub("-", "_") }
99
+ duplicate_column = columns.find { |c| columns.count(c) > 1 }
100
+ raise Blazer::UploadError, "Duplicate column name: #{duplicate_column}" if duplicate_column
101
+
102
+ column_types =
103
+ columns.size.times.map do |i|
104
+ values = rows.map { |r| r[i] }.uniq.compact
105
+ if values.all? { |v| v.is_a?(Integer) && v >= -9223372036854775808 && v <= 9223372036854775807 }
106
+ "bigint"
107
+ elsif values.all? { |v| v.is_a?(Numeric) }
108
+ "decimal"
109
+ elsif values.all? { |v| v.is_a?(DateTime) }
110
+ "timestamptz"
111
+ elsif values.all? { |v| v.is_a?(Date) }
112
+ "date"
113
+ else
114
+ "text"
115
+ end
116
+ end
117
+
118
+ begin
119
+ # maybe SET LOCAL statement_timeout = '30s'
120
+ # maybe regenerate CSV in Ruby to ensure consistent parsing
121
+ Blazer.uploads_connection.transaction do
122
+ Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{Blazer.uploads_table_name(drop)}") if drop
123
+ Blazer.uploads_connection.execute("CREATE TABLE #{upload.table_name} (#{columns.map.with_index { |c, i| "#{Blazer.uploads_connection.quote_column_name(c)} #{column_types[i]}" }.join(", ")})")
124
+ Blazer.uploads_connection.raw_connection.copy_data("COPY #{upload.table_name} FROM STDIN CSV HEADER") do
125
+ Blazer.uploads_connection.raw_connection.put_copy_data(contents)
126
+ end
127
+ end
128
+ rescue ActiveRecord::StatementInvalid => e
129
+ raise Blazer::UploadError, "Table already exists" if e.message.include?("PG::DuplicateTable")
130
+ raise e
131
+ end
132
+ end
133
+
134
+ def upload_params
135
+ params.require(:upload).except(:file).permit(:table, :description)
136
+ end
137
+
138
+ def set_upload
139
+ @upload = Blazer::Upload.find(params[:id])
140
+ end
141
+
142
+ # routes aren't added, but also check here
143
+ def ensure_uploads
144
+ render plain: "Uploads not enabled" unless Blazer.uploads?
145
+ end
146
+ end
147
+ end
@@ -12,9 +12,9 @@ module Blazer
12
12
  BLAZER_IMAGE_EXT = %w[png jpg jpeg gif]
13
13
 
14
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")
15
+ if value.is_a?(Numeric) && !key.to_s.end_with?("id") && !key.to_s.start_with?("id")
16
16
  number_with_delimiter(value)
17
- elsif value =~ BLAZER_URL_REGEX
17
+ elsif value.is_a?(String) && value =~ BLAZER_URL_REGEX
18
18
  # see if image or link
19
19
  if Blazer.images && (key.include?("image") || BLAZER_IMAGE_EXT.include?(value.split(".").last.split("?").first.try(:downcase)))
20
20
  link_to value, target: "_blank" do
@@ -29,25 +29,15 @@ module Blazer
29
29
  end
30
30
 
31
31
  def blazer_maps?
32
- ENV["MAPBOX_ACCESS_TOKEN"].present?
32
+ Blazer.mapbox_access_token.present?
33
33
  end
34
34
 
35
35
  def blazer_js_var(name, value)
36
- "var #{name} = #{blazer_json_escape(value.to_json)}".html_safe
36
+ "var #{name} = #{json_escape(value.to_json(root: false))};".html_safe
37
37
  end
38
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
39
+ def blazer_series_name(k)
40
+ k.nil? ? "null" : k.to_s
51
41
  end
52
42
  end
53
43
  end
@@ -1,6 +1,6 @@
1
1
  module Blazer
2
- class Audit < ActiveRecord::Base
3
- belongs_to :user, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s)
4
- belongs_to :query, Blazer::BELONGS_TO_OPTIONAL
2
+ class Audit < Record
3
+ belongs_to :user, optional: true, class_name: Blazer.user_class.to_s
4
+ belongs_to :query, optional: true
5
5
  end
6
6
  end
@@ -1,9 +1,11 @@
1
1
  module Blazer
2
- class Check < ActiveRecord::Base
3
- belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
2
+ class Check < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
4
  belongs_to :query
5
5
 
6
6
  validates :query_id, presence: true
7
+ validate :validate_emails
8
+ validate :validate_variables, if: -> { query_id_changed? }
7
9
 
8
10
  before_validation :set_state
9
11
  before_validation :fix_emails
@@ -12,6 +14,14 @@ module Blazer
12
14
  emails.to_s.downcase.split(",").map(&:strip)
13
15
  end
14
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
+
15
25
  def update_state(result)
16
26
  check_type =
17
27
  if respond_to?(:check_type)
@@ -57,8 +67,9 @@ module Blazer
57
67
  end
58
68
 
59
69
  # do not notify on creation, except when not passing
60
- if (state_was != "new" || state != "passing") && state != state_was && emails.present?
61
- 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
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)
62
73
  end
63
74
  save! if changed?
64
75
  end
@@ -72,7 +83,22 @@ module Blazer
72
83
  def fix_emails
73
84
  # some people like doing ; instead of ,
74
85
  # but we know what they mean, so let's fix it
75
- self.emails = emails.gsub(";", ",") if emails.present?
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
76
102
  end
77
103
  end
78
104
  end
@@ -1,11 +1,15 @@
1
1
  module Blazer
2
- class Dashboard < ActiveRecord::Base
3
- belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
2
+ class Dashboard < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
4
  has_many :dashboard_queries, dependent: :destroy
5
5
  has_many :queries, through: :dashboard_queries
6
6
 
7
7
  validates :name, presence: true
8
8
 
9
+ def variables
10
+ queries.flat_map { |q| q.variables }.uniq
11
+ end
12
+
9
13
  def to_param
10
14
  [id, name.gsub("'", "").parameterize].join("-")
11
15
  end
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- class DashboardQuery < ActiveRecord::Base
2
+ class DashboardQuery < Record
3
3
  belongs_to :dashboard
4
4
  belongs_to :query
5
5
 
@@ -1,6 +1,6 @@
1
1
  module Blazer
2
- class Query < ActiveRecord::Base
3
- belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
2
+ class Query < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
4
  has_many :checks, dependent: :destroy
5
5
  has_many :dashboard_queries, dependent: :destroy
6
6
  has_many :dashboards, through: :dashboard_queries
@@ -8,7 +8,8 @@ module Blazer
8
8
 
9
9
  validates :statement, presence: true
10
10
 
11
- scope :named, -> { where("blazer_queries.name <> ''") }
11
+ scope :active, -> { column_names.include?("status") ? where(status: ["active", nil]) : all }
12
+ scope :named, -> { where.not(name: "") }
12
13
 
13
14
  def to_param
14
15
  [id, name].compact.join("-").gsub("'", "").parameterize
@@ -18,10 +19,35 @@ module Blazer
18
19
  name.to_s.sub(/\A[#\*]/, "").gsub(/\[.+\]/, "").strip
19
20
  end
20
21
 
22
+ def viewable?(user)
23
+ if Blazer.query_viewable
24
+ Blazer.query_viewable.call(self, user)
25
+ else
26
+ true
27
+ end
28
+ end
29
+
21
30
  def editable?(user)
22
- editable = !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == creator
31
+ editable = !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == try(:creator)
32
+ editable &&= viewable?(user)
23
33
  editable &&= Blazer.query_editable.call(self, user) if Blazer.query_editable
24
34
  editable
25
35
  end
36
+
37
+ def variables
38
+ # don't require data_source to be loaded
39
+ variables = Statement.new(statement).variables
40
+ variables += ["cohort_period"] if cohort_analysis?
41
+ variables
42
+ end
43
+
44
+ def cohort_analysis?
45
+ # don't require data_source to be loaded
46
+ Statement.new(statement).cohort_analysis?
47
+ end
48
+
49
+ def statement_object
50
+ Statement.new(statement, data_source)
51
+ end
26
52
  end
27
53
  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,11 @@
1
+ module Blazer
2
+ class Upload < Record
3
+ belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
4
+
5
+ validates :table, presence: true, uniqueness: true, format: {with: /\A[a-z0-9_]+\z/, message: "can only contain lowercase letters, numbers, and underscores"}, length: {maximum: 63}
6
+
7
+ def table_name
8
+ Blazer.uploads_table_name(table)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Blazer
2
+ class UploadsConnection < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ establish_connection Blazer.settings["uploads"]["url"] if Blazer.uploads?
6
+ end
7
+ end
@@ -5,8 +5,10 @@
5
5
  <span class="sr-only">Toggle Dropdown</span>
6
6
  </button>
7
7
  <ul class="dropdown-menu">
8
- <li><%= link_to "Dashboards", dashboards_path %></li>
9
8
  <li><%= link_to "Checks", checks_path %></li>
9
+ <% if Blazer.uploads? %>
10
+ <li><%= link_to "Uploads", uploads_path %></li>
11
+ <% end %>
10
12
  <li role="separator" class="divider"></li>
11
13
  <li><%= link_to "New Query", new_query_path %></li>
12
14
  <li><%= link_to "New Dashboard", new_dashboard_path %></li>
@@ -1,5 +1,15 @@
1
1
  <% if @bind_vars.any? %>
2
- <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 10px;">
2
+ <% var_params = request.query_parameters %>
3
+ <script>
4
+ <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
5
+ var now = moment.tz(timeZone)
6
+ var format = "YYYY-MM-DD"
7
+
8
+ function toDate(time) {
9
+ return moment.tz(time.format(format), timeZone)
10
+ }
11
+ </script>
12
+ <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
3
13
  <% date_vars = ["start_time", "end_time"] %>
4
14
  <% if (date_vars - @bind_vars).empty? %>
5
15
  <% @bind_vars = @bind_vars - date_vars %>
@@ -10,30 +20,52 @@
10
20
  <% @bind_vars.each_with_index do |var, i| %>
11
21
  <%= label_tag var, var %>
12
22
  <% 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;" %>
23
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
14
24
  <script>
15
25
  $("#<%= var %>").selectize({
16
26
  create: true
17
27
  });
18
28
  </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});
29
+ <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
30
+ <%= hidden_field_tag var, var_params[var] %>
31
+
32
+ <div class="selectize-control single" style="width: 200px;">
33
+ <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
34
+ <span>Select a date</span>
35
+ </div>
36
+ </div>
37
+
38
+ <script>
39
+ (function() {
40
+ var input = $("#<%= var %>")
41
+ var datePicker = $("#<%= var %>-select")
42
+ datePicker.daterangepicker({
43
+ singleDatePicker: true,
44
+ locale: {format: format},
45
+ autoUpdateInput: false,
46
+ autoApply: true,
47
+ startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
48
+ })
24
49
  // 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 %>
50
+ datePicker.on("apply.daterangepicker", function(ev, picker) {
51
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
52
+ input.val(toDate(picker.startDate).utc().format())
53
+ submitIfCompleted($("#<%= var %>").closest("form"))
54
+ })
55
+ if (input.val().length > 0) {
56
+ var picker = datePicker.data("daterangepicker")
57
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
58
+ }
59
+ })()
60
+ </script>
61
+ <% else %>
62
+ <%= text_field_tag var, var_params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !var_params[var], class: "form-control" %>
31
63
  <% end %>
32
64
  <% end %>
33
65
 
34
66
  <% if date_vars %>
35
67
  <% date_vars.each do |var| %>
36
- <%= hidden_field_tag var, params[var] %>
68
+ <%= hidden_field_tag var, var_params[var] %>
37
69
  <% end %>
38
70
 
39
71
  <%= label_tag nil, date_vars.join(" & ") %>
@@ -44,18 +76,10 @@
44
76
  </div>
45
77
 
46
78
  <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
79
  function dateStr(daysAgo) {
52
80
  return now.clone().subtract(daysAgo || 0, "days").format(format)
53
81
  }
54
82
 
55
- function toDate(time) {
56
- return moment.tz(time.format(format), timeZone)
57
- }
58
-
59
83
  function setTimeInputs(start, end) {
60
84
  $("#start_time").val(toDate(start).utc().format())
61
85
  $("#end_time").val(toDate(end).endOf("day").utc().format())
@@ -73,7 +97,8 @@
73
97
  },
74
98
  startDate: dateStr(29),
75
99
  endDate: dateStr(),
76
- opens: "right"
100
+ opens: "right",
101
+ alwaysShowCalendars: true
77
102
  },
78
103
  function(start, end) {
79
104
  setTimeInputs(start, end)
@@ -1,5 +1,6 @@
1
1
  <ul>
2
2
  <% @checks.each do |check| %>
3
+ <%# check queries shouldn't have variables, but in any case, don't pass them to url helpers %>
3
4
  <li><%= link_to check.query.name, query_url(check.query_id) %> <%= check.state %></li>
4
5
  <% end %>
5
6
  </ul>
@@ -2,6 +2,7 @@
2
2
  <head>
3
3
  </head>
4
4
  <body style="font-family: 'Helvetica Neue', Arial, Helvetica; font-size: 14px; color: #333;">
5
+ <%# check queries shouldn't have variables, but in any case, don't pass them to url helpers %>
5
6
  <p><%= link_to "View", query_url(@check.query_id) %></p>
6
7
  <% if @error %>
7
8
  <p><%= @error %></p>
@@ -1,19 +1,19 @@
1
- <% unless @check.respond_to?(:invert) %>
2
- <p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
3
- <% end %>
1
+ <%= form_for @check, html: {class: "small-form"} do |f| %>
2
+ <% unless @check.respond_to?(:check_type) || @check.respond_to?(:invert) %>
3
+ <p class="text-muted">Checks are designed to identify bad data. A check fails if there are any results.</p>
4
+ <% end %>
4
5
 
5
- <% if @check.errors.any? %>
6
- <div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
7
- <% end %>
6
+ <% if @check.errors.any? %>
7
+ <div class="alert alert-danger"><%= @check.errors.full_messages.first %></div>
8
+ <% end %>
8
9
 
9
- <%= form_for @check do |f| %>
10
10
  <div class="form-group">
11
11
  <%= f.label :query_id, "Query" %>
12
12
  <div class="hide">
13
13
  <%= f.select :query_id, [], {include_blank: true} %>
14
14
  </div>
15
15
  <script>
16
- <%= blazer_js_var "queries", Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
16
+ <%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
17
17
  <%= blazer_js_var "items", [@check.query_id].compact %>
18
18
 
19
19
  $("#check_query_id").selectize({options: queries, items: items, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
@@ -60,7 +60,15 @@
60
60
  <%= f.label :emails %>
61
61
  <%= f.text_field :emails, placeholder: "Optional, comma separated", class: "form-control" %>
62
62
  </div>
63
- <p class="text-muted">Emails are sent when a check starts failing, and when it starts passing again.
63
+
64
+ <% if Blazer.slack? %>
65
+ <div class="form-group">
66
+ <%= f.label :slack_channels %>
67
+ <%= f.text_field :slack_channels, placeholder: "Optional, comma separated", class: "form-control" %>
68
+ </div>
69
+ <% end %>
70
+
71
+ <p class="text-muted">Emails <%= Blazer.slack? ? "and Slack notifications " : nil %>are sent when a check starts failing, and when it starts passing again.
64
72
  <p>
65
73
  <% if @check.persisted? %>
66
74
  <%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
@@ -1 +1,3 @@
1
+ <% blazer_title "Edit Check" %>
2
+
1
3
  <%= render partial: "form" %>
@@ -1,15 +1,35 @@
1
1
  <% blazer_title "Checks" %>
2
2
 
3
- <p style="float: right;"><%= link_to "New Check", new_check_path, class: "btn btn-info" %></p>
4
- <%= render partial: "blazer/nav" %>
3
+ <div id="header">
4
+ <div class="pull-right" style="line-height: 34px;">
5
+ <div class="btn-group">
6
+ <%= link_to "New Check", new_check_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
+ <% if Blazer.uploads? %>
14
+ <li><%= link_to "Uploads", uploads_path %></li>
15
+ <% end %>
16
+ <li role="separator" class="divider"></li>
17
+ <li><%= link_to "New Query", new_query_path %></li>
18
+ <li><%= link_to "New Dashboard", new_dashboard_path %></li>
19
+ </ul>
20
+ </div>
21
+ </div>
5
22
 
6
- <table class="table">
23
+ <input id="search" type="text" placeholder="Start typing a query or state" style="width: 300px; display: inline-block;" class="search form-control" />
24
+ </div>
25
+
26
+ <table id="checks" class="table">
7
27
  <thead>
8
28
  <tr>
9
29
  <th>Query</th>
10
30
  <th style="width: 10%;">State</th>
11
31
  <th style="width: 10%;">Run</th>
12
- <th style="width: 20%;">Emails</th>
32
+ <th style="width: 20%;">Notify</th>
13
33
  <th style="width: 15%;"></th>
14
34
  </tr>
15
35
  </thead>
@@ -19,7 +39,7 @@
19
39
  <td><%= link_to check.query.name, check.query %> <span class="text-muted"><%= check.try(:check_type).to_s.gsub("_", " ") %></span></td>
20
40
  <td>
21
41
  <% if check.state %>
22
- <small class="check-state <%= check.state.parameterize("_") %>"><%= check.state.upcase %></small>
42
+ <small class="check-state <%= check.state.parameterize.gsub("-", "_") %>"><%= check.state.upcase %></small>
23
43
  <% end %>
24
44
  </td>
25
45
  <td><%= check.schedule if check.respond_to?(:schedule) %></td>
@@ -28,6 +48,9 @@
28
48
  <% check.split_emails.each do |email| %>
29
49
  <li><%= email %></li>
30
50
  <% end %>
51
+ <% check.split_slack_channels.each do |channel| %>
52
+ <li><%= channel %></li>
53
+ <% end %>
31
54
  </ul>
32
55
  </td>
33
56
  <td style="text-align: right; padding: 1px;">
@@ -38,3 +61,12 @@
38
61
  <% end %>
39
62
  </tbody>
40
63
  </table>
64
+
65
+ <script>
66
+ $("#search").on("keyup", function() {
67
+ var value = $(this).val().toLowerCase()
68
+ $("#checks tbody tr").filter( function() {
69
+ $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
70
+ })
71
+ }).focus()
72
+ </script>
@@ -1 +1,3 @@
1
+ <% blazer_title "New Check" %>
2
+
1
3
  <%= render partial: "form" %>