sql-jarvis 2.1.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,7 @@
1
1
  module Blazer
2
2
  class Query < Record
3
3
  serialize :assignee_ids, Array
4
+ serialize :team_ids, Array
4
5
 
5
6
  belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
6
7
  has_many :checks, dependent: :destroy
@@ -8,6 +9,7 @@ module Blazer
8
9
  has_many :dashboards, through: :dashboard_queries
9
10
  has_many :audits
10
11
 
12
+ before_validation :statement_format
11
13
  validates :statement, presence: true
12
14
 
13
15
  scope :named, -> { where("blazer_queries.name <> ''") }
@@ -42,5 +44,12 @@ module Blazer
42
44
  def variables
43
45
  Blazer.extract_vars(statement)
44
46
  end
47
+
48
+ private
49
+
50
+ def statement_format
51
+ self.statement.gsub!(/\n/, '')
52
+ end
53
+
45
54
  end
46
55
  end
@@ -3,10 +3,8 @@
3
3
  #{blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name}
4
4
  var now = moment.tz(timeZone)
5
5
  var format = "YYYY-MM-DD"
6
+ function toDate(time) { return moment.tz(time.format(format), timeZone) }
6
7
 
7
- function toDate(time) {
8
- return moment.tz(time.format(format), timeZone)
9
- }
10
8
  %form#bind.form-inline{:action => action, :method => "get", :style => "margin-bottom: 15px;"}
11
9
  - date_vars = ["start_time", "end_time"]
12
10
  - if (date_vars - @bind_vars).empty?
@@ -16,7 +14,7 @@
16
14
  - @bind_vars.each_with_index do |var, i|
17
15
  = label_tag var, var
18
16
  - if (data = @smart_vars[var])
19
- = select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;", multiple: true
17
+ = select_tag var, options_for_select(data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;", multiple: true
20
18
  :javascript
21
19
  $("##{var}").selectize({
22
20
  create: true
@@ -100,3 +98,4 @@
100
98
  submitIfCompleted($("#start_time").closest("form"))
101
99
  }
102
100
  %input.btn.btn-success{:style => "vertical-align: top;", :type => "submit", :value => "Run"}/
101
+ %hr
@@ -0,0 +1,52 @@
1
+ - values = rows.first
2
+ - chart_id = SecureRandom.hex
3
+ - column_types = result.column_types
4
+ - chart_type = result.chart_type
5
+ - chart_options = { id: chart_id, height: '80vh' }
6
+
7
+ - if ["line", "line2"].include?(chart_type)
8
+ - chart_options.merge!(min: nil)
9
+ - if chart_type == "scatter"
10
+ - chart_options.merge!(library: {tooltips: {intersect: false}})
11
+ - elsif ["bar", "bar2"].include?(chart_type)
12
+ - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
13
+ - elsif chart_type != "pie"
14
+ - if column_types.size == 2 || forecast
15
+ - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
16
+ - else
17
+ - chart_options.merge!(library: {tooltips: {intersect: false}})
18
+ - series_library = {}
19
+ - target_index = columns.index { |k| k.downcase == "target" }
20
+ - if target_index
21
+ - series_library[target_index - 1] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"}
22
+ - if forecast
23
+ - color = "#54a3ee"
24
+ - series_library[1] = {borderDash: [8], borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color}
25
+ - elsif chart_type == "line"
26
+ - 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]} }
27
+ = line_chart chart_data, chart_options
28
+ - elsif chart_type == "line2"
29
+ = line_chart rows.group_by { |r| v = r[1]; (boom[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
30
+ - elsif chart_type == "pie"
31
+ = pie_chart rows.map { |r| [(boom[columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, chart_options
32
+ - elsif chart_type == "bar"
33
+ = column_chart (values.size - 1).times.map { |i| name = columns[i + 1]; {name: blazer_series_name(name), data: rows.first(20).map { |r| [(boom[columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, chart_options
34
+ - elsif chart_type == "bar2"
35
+ - first_20 = rows.group_by { |r| r[0] }.values.first(20).flatten(1)
36
+ - labels = first_20.map { |r| r[0] }.uniq
37
+ - series = first_20.map { |r| r[1] }.uniq
38
+ - labels.each do |l|
39
+ - series.each do |s|
40
+ - first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s }
41
+ = column_chart first_20.group_by { |r| v = r[1]; (boom[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]; [(boom[columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, chart_options
42
+ - elsif chart_type == "scatter"
43
+ = scatter_chart rows, xtitle: columns[0], ytitle: columns[1], **chart_options
44
+ - elsif only_chart
45
+ - if rows.size == 1 && rows.first.size == 1
46
+ - v = rows.first.first
47
+ - if v.is_a?(String) && v == ""
48
+ .text-muted empty string
49
+ - else
50
+ %p{style: "font-size: 160px;"}= blazer_format_value(columns.first, v)
51
+ - else
52
+ - @no_chart = true
@@ -7,17 +7,24 @@
7
7
  .form-group
8
8
  = f.hidden_field :statement
9
9
  #editor-container
10
- #editor{":style" => "{ height: editorHeight }"}= @query.statement
11
- .form-group.text-right{:style => "margin-bottom: 8px;"}
12
- .pull-left{:style => "margin-top: 8px;"}
10
+ #editor{style: "{ height: editorHeight }"}= @query.statement.to_s.gsub("\n", '')
11
+ .form-group.text-right{style: "margin-bottom: 8px;"}
12
+ .pull-left{style: "margin-top: 8px;"}
13
13
  = link_to "Back", :back
14
- %a{":href" => "schemaPath", :style => "margin-left: 40px;", :target => "_blank"} Schema
14
+ = link_to 'Schema', 'schemaPath', style: 'margin-left: 40px;', target: :blank
15
+ - if Blazer.integration
16
+ = link_to 'Integration', '#integration-session', style: 'margin-left: 40px;', data: { toggle: :collapse }
15
17
  = f.select :data_source, Blazer.data_sources.values.select { |ds| q = @query.dup; q.data_source = ds.id; q.editable?(blazer_user) }.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size <= 1), style: "width: 140px;"
16
- #tables{:style => "display: inline-block; width: 250px; margin-right: 10px;"}
17
- %select#table_names{:placeholder => "Preview table", :style => "width: 240px;"}
18
- %a.btn.btn-info{:style => "vertical-align: top; width: 70px;", "v-if" => "!running", "v-on:click" => "run"} Run
19
- %a.btn.btn-danger{:style => "vertical-align: top; width: 70px;", "v-if" => "running", "v-on:click" => "cancel"} Cancel
20
- %hr/
18
+ #tables{style: "display: inline-block; width: 250px; margin-right: 10px;"}
19
+ %select#table_names{placeholder: "Preview table", style: "width: 240px;"}
20
+ %a.btn.btn-info{style: "vertical-align: top; width: 70px;", "v-if" => "!running", "v-on:click" => "run"} Run
21
+ %a.btn.btn-danger{style: "vertical-align: top; width: 70px;", "v-if" => "running", "v-on:click" => "cancel"} Cancel
22
+ - if Blazer.integration
23
+ #integration-session.collapse
24
+ %hr
25
+ = f.hidden_field :integration
26
+ #integration-editor{style: 'height: 200px'}
27
+ %hr
21
28
  = render partial: "tips"
22
29
  .col-xs-4
23
30
  .form-group
@@ -30,6 +37,10 @@
30
37
  .form-group
31
38
  = f.label :assignees
32
39
  = f.collection_select :assignee_ids, @assignees, :first, :last, {}, { placeholder: "Assignees", style: "height: 80px;", class: "form-control", multiple: true }
40
+ - if @teams.any?
41
+ .form-group
42
+ = f.label :teams
43
+ = f.collection_select :team_ids, @teams, :first, :last, {}, { placeholder: "Teams", style: "height: 80px;", class: "form-control", multiple: true }
33
44
  .form-group.text-right
34
45
  = f.submit "For Enter Press", class: "hide"
35
46
  - if @query.persisted?
@@ -48,6 +59,7 @@
48
59
  #results
49
60
  %p.text-muted{"v-if" => "running"} Loading...
50
61
  #results-html{":class" => "{ 'query-error': error }", "v-if" => "!running"}
62
+
51
63
  :javascript
52
64
  #{blazer_js_var "params", variable_params}
53
65
  #{blazer_js_var "previewStatement", Hash[Blazer.data_sources.map { |k, v| [k, (v.preview_statement rescue "")] }]}
@@ -77,7 +89,11 @@
77
89
  this.error = false
78
90
  cancelAllQueries()
79
91
 
80
- var data = $.extend({}, params, {statement: this.getSQL(), data_source: $("#query_data_source").val()})
92
+ var data = $.extend({}, params, {
93
+ statement: this.getSQL(),
94
+ integration: $('#integration').val(),
95
+ data_source: $("#query_data_source").val()
96
+ })
81
97
 
82
98
  var _this = this
83
99
 
@@ -121,9 +137,25 @@
121
137
  selectize.refreshOptions(false)
122
138
  })
123
139
  },
140
+ showIntegrationEditor: function() {
141
+ integrationEditor = ace.edit("integration-editor")
142
+ integrationEditor.setTheme("ace/theme/twilight")
143
+ integrationEditor.getSession().setMode("ace/mode/ruby")
144
+ integrationEditor.setOptions({
145
+ fontSize: 12,
146
+ minLines: 10,
147
+ enableSnippets: false,
148
+ highlightActiveLine: false,
149
+ enableLiveAutocompletion: true,
150
+ enableBasicAutocompletion: true
151
+ })
152
+ integrationEditor.renderer.setShowGutter(true)
153
+ integrationEditor.renderer.setPrintMarginColumn(false)
154
+ integrationEditor.renderer.setPadding(10)
155
+ integrationEditor.getSession().setUseWrapMode(true)
156
+ },
124
157
  showEditor: function() {
125
158
  var _this = this
126
-
127
159
  editor = ace.edit("editor")
128
160
  editor.setTheme("ace/theme/twilight")
129
161
  editor.getSession().setMode("ace/mode/sql")
@@ -278,6 +310,7 @@
278
310
  })
279
311
 
280
312
  this.showEditor()
313
+ this.showIntegrationEditor()
281
314
  }
282
315
  })
283
316
 
@@ -292,7 +325,7 @@
292
325
  }
293
326
 
294
327
  $(document).ready(function() {
295
- $('#query_assignee_ids').select2();
328
+ $('#query_assignee_ids, #query_team_ids').select2();
296
329
  setInterval(function() {
297
330
  var lastVersion = $('#query_statement').val();
298
331
  if (localStorage.getItem('lastVersion') !== lastVersion) {
@@ -20,32 +20,12 @@
20
20
  - if @forecast_error
21
21
  .alert.alert-danger= @forecast_error
22
22
  - if @rows.any?
23
- - values = @rows.first
24
- - chart_id = SecureRandom.hex
25
- - column_types = @result.column_types
26
- - chart_type = @result.chart_type
27
- - chart_options = {id: chart_id}
28
- - if ["line", "line2"].include?(chart_type)
29
- - chart_options.merge!(min: nil)
30
- - if chart_type == "scatter"
31
- - chart_options.merge!(library: {tooltips: {intersect: false}})
32
- - elsif ["bar", "bar2"].include?(chart_type)
33
- - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
34
- - elsif chart_type != "pie"
35
- - if column_types.size == 2 || @forecast
36
- - chart_options.merge!(library: {tooltips: {intersect: false, axis: 'x'}})
37
- - else
38
- - chart_options.merge!(library: {tooltips: {intersect: false}})
39
- - series_library = {}
40
- - target_index = @columns.index { |k| k.downcase == "target" }
41
- - if target_index
42
- - series_library[target_index - 1] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"}
43
- - if @forecast
44
- - color = "#54a3ee"
45
- - series_library[1] = {borderDash: [8], borderColor: color, pointBackgroundColor: color, backgroundColor: color, pointHoverBackgroundColor: color}
23
+ #main-chart
24
+ = render 'chart', rows: @rows, columns: @columns, result: @result, forecast: @forecast, only_chart: @only_chart, boom: @boom
46
25
  - if blazer_maps? && @markers.any?
47
- #map{style: "height: #{@only_chart ? 300 : 500}px;"}
26
+ #map{style: "height: 500px; width: 100%"}
48
27
  :javascript
28
+ $('#map').html('');
49
29
  L.mapbox.accessToken = '#{Blazer.mapbox_access_token}';
50
30
  var map = L.mapbox.map('map', 'ankane.ioo8nki0');
51
31
  #{blazer_js_var "markers", @markers}
@@ -71,34 +51,12 @@
71
51
  }
72
52
  featureLayer.setGeoJSON(geojson);
73
53
  map.fitBounds(featureLayer.getBounds());
74
- - elsif chart_type == "line"
75
- - 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]} }
76
- = line_chart chart_data, chart_options
77
- - elsif chart_type == "line2"
78
- = line_chart @rows.group_by { |r| v = r[1]; (@boom[@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
79
- - elsif chart_type == "pie"
80
- = pie_chart @rows.map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, chart_options
81
- - elsif chart_type == "bar"
82
- = column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, chart_options
83
- - elsif chart_type == "bar2"
84
- - first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1)
85
- - labels = first_20.map { |r| r[0] }.uniq
86
- - series = first_20.map { |r| r[1] }.uniq
87
- - labels.each do |l|
88
- - series.each do |s|
89
- - first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s }
90
- = column_chart first_20.group_by { |r| v = r[1]; (@boom[@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]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, chart_options
91
- - elsif chart_type == "scatter"
92
- = scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], **chart_options
93
- - elsif @only_chart
94
- - if @rows.size == 1 && @rows.first.size == 1
95
- - v = @rows.first.first
96
- - if v.is_a?(String) && v == ""
97
- .text-muted empty string
98
- - else
99
- %p{style: "font-size: 160px;"}= blazer_format_value(@columns.first, v)
100
- - else
101
- - @no_chart = true
54
+ map.setZoom(5)._onResize();
55
+ - elsif @no_chart
56
+ :javascript
57
+ $('#chart-tab').hide();
58
+ $('#table-tab').click();
59
+
102
60
  - unless @only_chart && !@no_chart
103
61
  - header_width = 100 / @columns.size.to_f
104
62
  .results-container
@@ -110,7 +68,7 @@
110
68
  %code= @rows[0][0]
111
69
  - else
112
70
  %p.text-muted{style: "margin-bottom: 10px; margin-top: 15px"}
113
- = pluralize(@rows.size, "row")
71
+ = "#{@rows.size} rows" if @rows.size > 1
114
72
  - @checks.select(&:state).each do |check|
115
73
  ·
116
74
  %small{:class => "check-state #{check.state.parameterize.gsub("-", "_")}"}= link_to check.state.upcase, edit_check_path(check)
@@ -157,3 +115,7 @@
157
115
  .text-muted= v2
158
116
  - else
159
117
  %p.text-muted.text-center{style: "margin-top: 15px"} No rows
118
+
119
+ :javascript
120
+ $('#main-chart').appendTo('#chart');
121
+ $('#map').appendTo('#chart');
@@ -4,7 +4,7 @@
4
4
  .row{:style => "padding-top: 13px;"}
5
5
  .col-sm-8
6
6
  = render partial: "blazer/nav"
7
- %h3{:style => "line-height: 34px; display: inline; margin-left: 5px;"}
7
+ %h4{style: "line-height: 34px; display: inline; margin-left: 5px;"}
8
8
  = @query.name
9
9
  .col-sm-4.text-right
10
10
  = link_to "Edit", edit_query_path(@query, variable_params), class: "btn btn-default", disabled: !@query.editable?(blazer_user)
@@ -20,31 +20,54 @@
20
20
  %li= message
21
21
  - if @query.description.present?
22
22
  %p= @query.description
23
+
23
24
  = render partial: "blazer/variables", locals: {action: query_path(@query)}
24
- %pre#code
25
- %code= @statement
26
- - if @success
27
- #results
28
- %p.text-muted Loading...
29
- :javascript
30
- function showRun(data) {
31
- $("#results").html(data);
32
- $("#results table").stupidtable().stickyTableHeaders({
33
- fixedOffset: 60,
34
- });
35
- }
36
25
 
37
- function showError(message) {
38
- $("#results").addClass("query-error").html(message);
39
- }
26
+ %ul.nav.nav-tabs
27
+ %li#chart-tab.active= link_to 'Chart', '#chart', data: { toggle: :tab }
28
+ %li= link_to 'Table', '#table', data: { toggle: :tab }, id: 'table-tab'
29
+ %li= link_to 'Query', '#query', data: { toggle: :tab }
30
+ - if @query.integration.present?
31
+ %li= link_to 'Integration', '#integration', data: { toggle: :tab }
32
+
33
+ .tab-content
34
+ #chart.tab-pane.fade.in.active
35
+ #query.tab-pane.fade
36
+ %pre#code
37
+ %code= @statement.gsub("\r", "\n")
38
+ - if @query.integration.present?
39
+ #integration.tab-pane.fade
40
+ = @query.integration
41
+ #table.tab-pane.fade
42
+ - if @success
43
+ #results
44
+ %p.text-muted Loading...
45
+ :javascript
46
+ function showRun(data) {
47
+ $("#results").html(data);
48
+ $("#results table").stupidtable().stickyTableHeaders({
49
+ fixedOffset: 60,
50
+ });
51
+ }
52
+
53
+ function showError(message) {
54
+ $("#results").addClass("query-error").html(message);
55
+ }
56
+
57
+ #{blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id, data_source: @query.data_source)}
40
58
 
41
- #{blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id, data_source: @query.data_source)}
59
+ runQuery(data, showRun, showError)
60
+ - unless %w(mongodb).include?(Blazer.data_sources[@query.data_source].adapter)
61
+ :javascript
62
+ // do not highlight really long queries
63
+ // this can lead to performance issues
64
+ if ($("code").text().length < 10000) {
65
+ hljs.highlightBlock(document.getElementById("code"));
66
+ }
42
67
 
43
- runQuery(data, showRun, showError)
44
- - unless %w(mongodb).include?(Blazer.data_sources[@query.data_source].adapter)
45
- :javascript
46
- // do not highlight really long queries
47
- // this can lead to performance issues
48
- if ($("code").text().length < 10000) {
49
- hljs.highlightBlock(document.getElementById("code"));
50
- }
68
+ :javascript
69
+ $('ul.nav.nav-tabs li a').click(function(event) {
70
+ localStorage.setItem('currentQueryTab', $(event.currentTarget).attr('href'));
71
+ });
72
+ var currentQueryTab = localStorage.getItem('currentQueryTab');
73
+ $("ul.nav.nav-tabs li a[href='" + currentQueryTab + "']").click();
@@ -15,5 +15,5 @@
15
15
  = javascript_include_tag "https://api.mapbox.com/mapbox.js/v3.1.1/mapbox.js", integrity: "sha384-j81LqvtvYigFzGSUgAoFijpvoq4yGoCJSOXI9DFaUEpenR029MBE3E/X5Gr+WdO0", crossorigin: "anonymous"
16
16
  = csrf_meta_tags
17
17
  %body
18
- .container
18
+ .container-fluid
19
19
  = yield
@@ -12,6 +12,7 @@ require "blazer/data_source"
12
12
  require "blazer/result"
13
13
  require "blazer/excel_parser"
14
14
  require "blazer/run_statement"
15
+ require "blazer/run_integration"
15
16
 
16
17
  # adapters
17
18
  require "blazer/adapters/base_adapter"
@@ -49,9 +50,11 @@ module Blazer
49
50
  attr_accessor :check_schedules
50
51
  attr_accessor :mapbox_access_token
51
52
  attr_accessor :assignees
53
+ attr_accessor :teams
52
54
  attr_accessor :anomaly_checks
53
55
  attr_accessor :forecasting
54
56
  attr_accessor :async
57
+ attr_accessor :integration
55
58
  attr_accessor :images
56
59
  attr_accessor :query_viewable
57
60
  attr_accessor :query_editable
@@ -65,6 +68,7 @@ module Blazer
65
68
  self.check_schedules = ["5 minutes", "1 hour", "1 day"]
66
69
  self.mapbox_access_token = nil
67
70
  self.assignees = []
71
+ self.teams = []
68
72
  self.host = nil
69
73
  self.preview_rows_number = 365
70
74
  self.anomaly_checks = false
@@ -89,6 +93,11 @@ module Blazer
89
93
  @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
90
94
  end
91
95
 
96
+ def self.integration
97
+ return @integration if defined?(@integration)
98
+ @integration = settings.key?("integration") ? settings["integration"] : false
99
+ end
100
+
92
101
  def self.user_class
93
102
  if !defined?(@user_class)
94
103
  @user_class = settings.key?("user_class") ? settings["user_class"] : (User.name rescue nil)
@@ -52,11 +52,11 @@ module Blazer
52
52
 
53
53
  def preview_statement
54
54
  if postgresql?
55
- "SELECT * FROM \"{table}\" LIMIT 10"
55
+ "SELECT * FROM \"{table}\" LIMIT 30"
56
56
  elsif sqlserver?
57
- "SELECT TOP (10) * FROM {table}"
57
+ "SELECT TOP (30) * FROM {table}"
58
58
  else
59
- "SELECT *\nFROM {table}\nORDER BY id DESC\nLIMIT 10"
59
+ "SELECT *\nFROM {table}\nORDER BY id DESC\nLIMIT 30"
60
60
  end
61
61
  end
62
62