rhea 0.0.1 → 0.1.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -2
  3. data/app/assets/javascripts/rhea/application.js +2 -0
  4. data/app/assets/javascripts/rhea/main.js +3 -0
  5. data/app/assets/javascripts/vendor/jquery-1.11.3.js +10351 -0
  6. data/app/assets/stylesheets/rhea/application.css.sass +1 -0
  7. data/app/assets/stylesheets/rhea/layout.css.sass +31 -0
  8. data/app/controllers/rhea/base_controller.rb +26 -0
  9. data/app/controllers/rhea/commands_controller.rb +168 -0
  10. data/app/controllers/rhea/events_controller.rb +8 -0
  11. data/app/controllers/rhea/nodes_controller.rb +7 -0
  12. data/app/controllers/rhea/system_services_controller.rb +7 -0
  13. data/app/helpers/rhea/helper.rb +42 -0
  14. data/app/views/rhea/command_types/_list.html.haml +5 -0
  15. data/app/views/rhea/commands/_form.html.haml +22 -0
  16. data/app/views/rhea/commands/_table.html.haml +51 -0
  17. data/app/views/rhea/commands/index.html.haml +51 -0
  18. data/app/views/rhea/errors/index.html.haml +2 -0
  19. data/app/views/rhea/events/index.html.haml +24 -0
  20. data/app/views/rhea/layouts/application.html.haml +35 -0
  21. data/app/views/rhea/nodes/index.html.haml +29 -0
  22. data/app/views/rhea/system_services/index.html.haml +2 -0
  23. data/config/routes.rb +21 -0
  24. data/docs/commands.gif +0 -0
  25. data/docs/commands.mov +0 -0
  26. data/docs/events.png +0 -0
  27. data/docs/logo.png +0 -0
  28. data/docs/nodes.png +0 -0
  29. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_All/_perform/an_existing_rc/returns_the_rc.yml +197 -0
  30. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Delete/_perform/an_existing_rc/deletes_the_rc.yml +163 -0
  31. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Export/_perform/an_existing_rc/returns_the_data.yml +197 -0
  32. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Get/_perform/an_existing_rc/gets_the_rc.yml +196 -0
  33. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Import/_perform/no_existing_rcs/creates_the_rcs.yml +197 -0
  34. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Redeploy/_perform/an_existing_rc/redeploys_the_rc.yml +424 -0
  35. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Reschedule/_perform/an_existing_rc/reschedules_the_rc.yml +421 -0
  36. data/fixtures/vcr_cassettes/Rhea_Kubernetes_Commands_Scale/_perform/no_existing_rc/creates_an_rc.yml +165 -0
  37. data/lib/rhea.rb +15 -1
  38. data/lib/rhea/command.rb +35 -0
  39. data/lib/rhea/command_type.rb +35 -0
  40. data/lib/rhea/engine.rb +16 -0
  41. data/lib/rhea/kubernetes.rb +5 -0
  42. data/lib/rhea/kubernetes/api.rb +15 -0
  43. data/lib/rhea/kubernetes/commands/all.rb +16 -0
  44. data/lib/rhea/kubernetes/commands/base.rb +28 -0
  45. data/lib/rhea/kubernetes/commands/delete.rb +18 -0
  46. data/lib/rhea/kubernetes/commands/export.rb +23 -0
  47. data/lib/rhea/kubernetes/commands/get.rb +18 -0
  48. data/lib/rhea/kubernetes/commands/import.rb +20 -0
  49. data/lib/rhea/kubernetes/commands/redeploy.rb +22 -0
  50. data/lib/rhea/kubernetes/commands/reschedule.rb +21 -0
  51. data/lib/rhea/kubernetes/commands/scale.rb +108 -0
  52. data/lib/rhea/kubernetes/configuration.rb +30 -0
  53. data/lib/rhea/kubernetes/events/recent.rb +23 -0
  54. data/lib/rhea/kubernetes/nodes/all.rb +45 -0
  55. data/lib/rhea/kubernetes/system_services.rb +22 -0
  56. data/lib/rhea/version.rb +1 -1
  57. data/rhea.gemspec +11 -3
  58. data/spec/lib/rhea/kubernetes/commands/all_spec.rb +29 -0
  59. data/spec/lib/rhea/kubernetes/commands/delete_spec.rb +22 -0
  60. data/spec/lib/rhea/kubernetes/commands/export_spec.rb +31 -0
  61. data/spec/lib/rhea/kubernetes/commands/get_spec.rb +28 -0
  62. data/spec/lib/rhea/kubernetes/commands/import_spec.rb +39 -0
  63. data/spec/lib/rhea/kubernetes/commands/redeploy_spec.rb +35 -0
  64. data/spec/lib/rhea/kubernetes/commands/reschedule_spec.rb +32 -0
  65. data/spec/lib/rhea/kubernetes/commands/scale_spec.rb +31 -0
  66. data/spec/spec_helper.rb +2 -0
  67. data/spec/support/kubernetes_spec_helper.rb +43 -0
  68. data/spec/support/vcr.rb +9 -0
  69. metadata +170 -11
@@ -0,0 +1 @@
1
+ @import layout
@@ -0,0 +1,31 @@
1
+ h1, h2, h3, h4, h5, h6
2
+ margin-top: 0
3
+
4
+ .batch-actions .btn-action
5
+ margin-left: 0
6
+ margin-right: 5px
7
+
8
+ .btn-action
9
+ margin-left: 5px
10
+
11
+ .btn-file
12
+ position: relative
13
+ overflow: hidden
14
+
15
+ .btn-file input[type=file]
16
+ position: absolute
17
+ top: 0
18
+ right: 0
19
+ min-width: 100%
20
+ min-height: 100%
21
+ font-size: 100px
22
+ text-align: right
23
+ filter: alpha(opacity=0)
24
+ opacity: 0
25
+ outline: none
26
+ background: white
27
+ cursor: inherit
28
+ display: block
29
+
30
+ .form-horizontal .control-label
31
+ padding-right: 0
@@ -0,0 +1,26 @@
1
+ module Rhea
2
+ class BaseController < ActionController::Base
3
+ protect_from_forgery
4
+
5
+ layout 'rhea/layouts/application'
6
+
7
+ helper Rhea::Helper
8
+
9
+ around_filter :rescue_exceptions
10
+
11
+ private
12
+
13
+ def rescue_exceptions
14
+ yield
15
+ rescue Errno::ECONNREFUSED
16
+ render_connection_exception
17
+ rescue Rhea::Kubernetes::ServerError => e
18
+ flash[:alert] = e.message
19
+ render_connection_exception
20
+ end
21
+
22
+ def render_connection_exception
23
+ render 'rhea/errors/index'
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,168 @@
1
+ module Rhea
2
+ class CommandsController < Rhea::BaseController
3
+ def index
4
+ @commands = Rhea::Kubernetes::Commands::All.new.perform
5
+ @default_image = Rhea.configuration.default_image
6
+ @images = (@commands.map(&:image) + [@default_image]).uniq.sort
7
+ params[:image] ||= @default_image
8
+ if params[:image].present?
9
+ @commands = @commands.select { |command| command.image == params[:image] }
10
+ end
11
+ end
12
+
13
+ def create
14
+ command_type_key = params[:command_type_key].presence
15
+ command_type_input = params[:command_type_input]
16
+ process_count = params[:process_count].presence.try(:to_i) || 1
17
+ image = params[:image].presence
18
+
19
+ redirect_to :back, flash: { error: 'Blank command_type_key' } and return if command_type_key.blank?
20
+ redirect_to :back, flash: { error: 'Blank process_count' } and return if process_count.blank?
21
+ redirect_to :back, flash: { error: 'Blank image' } and return if image.blank?
22
+
23
+ command_type = Rhea::CommandType.find(command_type_key)
24
+ command_expression = command_type.input_to_command_expression(command_type_input)
25
+
26
+ Rhea::Kubernetes::Commands::Scale.new(expression: command_expression, image: image, process_count: process_count).perform
27
+ wait_for_updates_to_persist
28
+ redirect_to params[:redirect_to], notice: 'Command created!'
29
+ end
30
+
31
+ def batch_update
32
+ case params[:batch_action]
33
+ when 'redeploy'
34
+ batch_redeploy
35
+ when 'reschedule'
36
+ batch_reschedule
37
+ when 'scale'
38
+ batch_scale
39
+ when 'stop'
40
+ batch_stop
41
+ else
42
+ redirect_to params[:redirect_to], notice: 'Invalid action!'
43
+ end
44
+ end
45
+
46
+ def delete
47
+ command_attributes = image_expression_to_command_attributes(params[:image_expression])
48
+ Rhea::Kubernetes::Commands::Delete.new(command_attributes).perform
49
+ flash[:notice] = "Command '#{command_attributes[:expression]}' deleted!"
50
+ redirect_to :back
51
+ end
52
+
53
+ def export
54
+ data = Rhea::Kubernetes::Commands::Export.new.perform
55
+ respond_to do |format|
56
+ format.json { send_data data.to_json, type: :json, disposition: 'attachment', filename: 'rhea.json' }
57
+ end
58
+ end
59
+
60
+ def import
61
+ data = ActiveSupport::JSON.decode(params[:data].read)
62
+ commands = data['commands']
63
+ if commands.nil?
64
+ flash[:alert] = 'Invalid file'
65
+ redirect_to :back
66
+ return
67
+ end
68
+
69
+ Rhea::Kubernetes::Commands::Import.new(data).perform
70
+ flash[:notice] = "Imported #{commands.length} commands!"
71
+ redirect_to :back
72
+ end
73
+
74
+ def redeploy
75
+ command_attributes = image_expression_to_command_attributes(params[:image_expression])
76
+ Rhea::Kubernetes::Commands::Redeploy.new(command_attributes).perform
77
+ flash[:notice] = "Command '#{command_attributes[:expression]}' redeployed!"
78
+ redirect_to :back
79
+ end
80
+
81
+ def reschedule
82
+ command_attributes = image_expression_to_command_attributes(params[:image_expression])
83
+ Rhea::Kubernetes::Commands::Reschedule.new(command_attributes).perform
84
+ flash[:notice] = "Command '#{command_attributes[:expression]}' rescheduled!"
85
+ redirect_to :back
86
+ end
87
+
88
+ def stop
89
+ command_attributes = image_expression_to_command_attributes(params[:image_expression])
90
+ Rhea::Kubernetes::Commands::Scale.new(command_attributes.merge(process_count: 0)).perform
91
+ flash[:notice] = "Command '#{command_attributes[:expression]}' stopped!"
92
+ redirect_to :back
93
+ end
94
+
95
+ private
96
+
97
+ def batch_redeploy
98
+ image_expressions = params[:batch_image_expressions]
99
+ redirect_to params[:redirect_to], notice: 'No commands were selected!' and return if image_expressions.blank?
100
+ threads = image_expressions.map do |image_expression|
101
+ command_attributes = image_expression_to_command_attributes(image_expression)
102
+ Thread.new do
103
+ Rhea::Kubernetes::Commands::Redeploy.new(command_attributes).perform
104
+ end
105
+ end
106
+ threads.map(&:join)
107
+ wait_for_updates_to_persist
108
+ redirect_to params[:redirect_to], notice: "Redeployed #{image_expressions.length} #{'command'.pluralize(image_expressions.length)}!"
109
+ end
110
+
111
+ def batch_reschedule
112
+ image_expressions = params[:batch_image_expressions]
113
+ redirect_to params[:redirect_to], notice: 'No commands were selected!' and return if image_expressions.blank?
114
+ threads = image_expressions.map do |image_expression|
115
+ command_attributes = image_expression_to_command_attributes(image_expression)
116
+ Thread.new do
117
+ Rhea::Kubernetes::Commands::Reschedule.new(command_attributes).perform
118
+ end
119
+ end
120
+ threads.map(&:join)
121
+ wait_for_updates_to_persist
122
+ redirect_to params[:redirect_to], notice: "Rescheduled #{image_expressions.length} #{'command'.pluralize(image_expressions.length)}!"
123
+ end
124
+
125
+ def batch_scale
126
+ image_expressions_process_counts = params[:image_expressions_process_counts]
127
+ scaled_commands_count = 0
128
+ image_expressions_process_counts.each do |image_expression, process_count|
129
+ next if process_count.blank?
130
+ process_count = process_count.to_i
131
+ command_attributes = image_expression_to_command_attributes(image_expression)
132
+ Rhea::Kubernetes::Commands::Scale.new(command_attributes.merge(process_count: process_count)).perform
133
+ scaled_commands_count += 1
134
+ end
135
+ wait_for_updates_to_persist
136
+ redirect_to params[:redirect_to], notice: "Scaled #{scaled_commands_count} #{'command'.pluralize(scaled_commands_count)}!"
137
+ end
138
+
139
+ def batch_stop
140
+ image_expressions = params[:batch_image_expressions]
141
+ redirect_to params[:redirect_to], notice: 'No commands were selected!' and return if image_expressions.blank?
142
+ threads = image_expressions.map do |image_expression|
143
+ command_attributes = image_expression_to_command_attributes(image_expression)
144
+ Thread.new do
145
+ Rhea::Kubernetes::Commands::Scale.new(command_attributes.merge(process_count: 0)).perform
146
+ end
147
+ end
148
+ threads.map(&:join)
149
+ wait_for_updates_to_persist
150
+ redirect_to params[:redirect_to], notice: "Stopped #{image_expressions.length} #{'command'.pluralize(image_expressions.length)}!"
151
+ end
152
+
153
+ # Sleep briefly to prevent user needing to refresh the page to see updated counts
154
+ def wait_for_updates_to_persist
155
+ sleep 0.2
156
+ end
157
+
158
+ # TODO: Use proper form submissions instead of string concatenation
159
+ def image_expression_to_command_attributes(image_expression)
160
+ image_expression = CGI.unescape(image_expression)
161
+ image, expression = image_expression.split(Rhea::Command::IMAGE_EXPRESSION_SEPARATOR, 2)
162
+ {
163
+ image: image,
164
+ expression: expression
165
+ }
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,8 @@
1
+ module Rhea
2
+ class EventsController < Rhea::BaseController
3
+ def index
4
+ events = Rhea::Kubernetes::Events::Recent.new.perform
5
+ @events = Kaminari.paginate_array(events).page(params[:page]).per(200)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Rhea
2
+ class NodesController < Rhea::BaseController
3
+ def index
4
+ @nodes = Rhea::Kubernetes::Nodes::All.new.perform
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Rhea
2
+ class SystemServicesController < Rhea::BaseController
3
+ def index
4
+ @service_names_urls = Rhea::Kubernetes::SystemServices.service_names_urls
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ module Rhea
2
+ module Helper
3
+ def app_name
4
+ 'Rhea'
5
+ end
6
+
7
+ def root_path
8
+ Rhea.root_path
9
+ end
10
+
11
+ def humanize_image(image)
12
+ image.split('/').last
13
+ end
14
+
15
+ ALERT_TYPES = [:success, :info, :warning, :danger] unless const_defined?(:ALERT_TYPES)
16
+
17
+ def bootstrap_flash(options = {})
18
+ flash_messages = []
19
+ flash.each do |type, message|
20
+ # Skip empty messages, e.g. for devise messages set to nothing in a locale file.
21
+ next if message.blank?
22
+
23
+ type = type.to_sym
24
+ type = :success if type == :notice
25
+ type = :danger if type == :alert
26
+ type = :danger if type == :error
27
+ next unless ALERT_TYPES.include?(type)
28
+
29
+ tag_class = options.extract!(:class)[:class]
30
+ tag_options = {
31
+ class: "alert fade in alert-#{type} #{tag_class}"
32
+ }.merge(options)
33
+
34
+ Array(message).each do |msg|
35
+ text = content_tag(:div, msg.html_safe, tag_options)
36
+ flash_messages << text if msg
37
+ end
38
+ end
39
+ flash_messages.join("\n").html_safe
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ %table.table.table-condensed
2
+ - Rhea::CommandType.all.each do |command_type|
3
+ %tr
4
+ %td= command_type.name
5
+ %td= command_type.displayed_format
@@ -0,0 +1,22 @@
1
+ = form_tag commands_path, method: 'POST', class: 'form-horizontal' do
2
+ = hidden_field_tag :redirect_to, request.fullpath
3
+ .form-group
4
+ = label_tag :image, 'Image', class: 'col-sm-3 control-label'
5
+ .col-sm-9
6
+ = text_field_tag :image, Rhea.configuration.default_image, class: 'form-control', style: 'width: 100%;', placeholder: Rhea.configuration.default_image
7
+ .form-group
8
+ = label_tag :command_type_key, 'Type', class: 'col-sm-3 control-label'
9
+ .col-sm-9
10
+ = select_tag :command_type_key, options_for_select(Rhea::CommandType.options_for_select, Rhea.configuration.default_command_type_key), class: 'form-control', style: 'width: 90px;', required: 'required'
11
+ .form-group
12
+ = label_tag :command_type_input, 'Input', class: 'col-sm-3 control-label'
13
+ .col-sm-9
14
+ = text_field_tag :command_type_input, nil, class: 'form-control', style: 'width: 100%;'
15
+ .form-group
16
+ = label_tag :process_count, 'Processes', class: 'col-sm-3 control-label'
17
+ .col-sm-9
18
+ = text_field_tag :process_count, 1, class: 'form-control', style: 'width: 50px;', placeholder: 1
19
+ .form-group
20
+ .col-sm-3
21
+ .col-sm-9
22
+ = submit_tag 'Create', class: 'btn btn-default'
@@ -0,0 +1,51 @@
1
+ %table.table.table-condensed.table-striped
2
+ %thead
3
+ %tr
4
+ %th
5
+ %input{type: 'checkbox', id: 'check-all'}
6
+ %th Image
7
+ %th Command
8
+ %th Processes
9
+ %th Desired Processes
10
+ %th Deployed
11
+ %th
12
+ %tbody
13
+ - commands.each do |command|
14
+ - escaped_image_expression = CGI.escape([command.image, command.expression].join(Rhea::Command::IMAGE_EXPRESSION_SEPARATOR))
15
+ %tr
16
+ %td= check_box_tag 'batch_image_expressions[]', escaped_image_expression, false, class: 'command-checkbox'
17
+ %td= humanize_image(command.image)
18
+ %td= command.expression
19
+ %td= command.process_count
20
+ %td
21
+ = text_field_tag "image_expressions_process_counts[#{escaped_image_expression}]", nil, class: 'form-control', style: 'width: 50px;'
22
+ %td
23
+ %small.text-muted
24
+ = time_ago_in_words(command.created_at.in_time_zone(Time.zone), include_seconds: true)
25
+ ago
26
+ %td{ style: 'white-space: nowrap;' }
27
+ = link_to redeploy_commands_path(image_expression: escaped_image_expression), title: 'Redeploy', class: 'btn btn-info btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
28
+ %span.glyphicon.glyphicon-cloud-download
29
+ = link_to reschedule_commands_path(image_expression: escaped_image_expression), title: 'Reschedule', class: 'btn btn-info btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
30
+ %span.glyphicon.glyphicon-fullscreen
31
+ - if command.process_count == 0
32
+ = link_to delete_commands_path(image_expression: escaped_image_expression), title: 'Delete', class: 'btn btn-danger btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
33
+ %span.glyphicon.glyphicon-remove
34
+ - else
35
+ = link_to stop_commands_path(image_expression: escaped_image_expression), title: 'Stop', class: 'btn btn-warning btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
36
+ %span.glyphicon.glyphicon-stop
37
+ %tr
38
+ %td{ colspan: 2 }
39
+ .batch-actions{ style: 'display: none;' }
40
+ = button_tag name: 'batch_action', value: 'redeploy', title: 'Redeploy', class: 'btn btn-info btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
41
+ %span.glyphicon.glyphicon-cloud-download
42
+ = button_tag name: 'batch_action', value: 'reschedule', title: 'Reschedule', class: 'btn btn-info btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
43
+ %span.glyphicon.glyphicon-fullscreen
44
+ = button_tag name: 'batch_action', value: 'stop', title: 'Stop', class: 'btn btn-warning btn-xs btn-action', 'data-toggle': 'tooltip', 'data-placement': 'bottom' do
45
+ %span.glyphicon.glyphicon-stop
46
+ %td
47
+ %td
48
+ %td
49
+ = button_tag 'Save', name: 'batch_action', value: 'scale', class: 'btn btn-default'
50
+ %td
51
+ %td
@@ -0,0 +1,51 @@
1
+ .row
2
+ .col-md-8
3
+ .pull-right
4
+ = form_tag commands_path, method: 'GET', class: 'form-inline' do
5
+ .form-group
6
+ = label_tag :image, 'Image:', style: 'margin-right: 5px; font-size: 12px; font-weight: normal;'
7
+ - image_options = [['All', nil]] + @images.map { |image| [humanize_image(image), image] }
8
+ = select_tag :image, options_for_select(image_options, params[:image].presence), class: 'form-control input-sm', onchange: "$(this).closest('form').submit();"
9
+ %h3 Commands
10
+ - if @commands.blank?
11
+ %p No commands found.
12
+ - else
13
+ = form_tag batch_update_commands_path, method: 'POST' do
14
+ = hidden_field_tag :redirect_to, request.fullpath
15
+ = render partial: 'rhea/commands/table', locals: { commands: @commands }
16
+ = link_to 'Export', export_commands_path(format: 'json'), class: 'btn btn-default'
17
+ = form_tag import_commands_path, method: 'PUT', multipart: true, style: 'display: inline-block;' do
18
+ %span.btn.btn-default.btn-file
19
+ Import
20
+ = file_field_tag :data, accept: 'application/json', onchange: "$(this).closest('form').submit();"
21
+ %br
22
+ %br
23
+
24
+ .col-md-4
25
+ .panel.panel-default
26
+ .panel-heading
27
+ %h4 New Command
28
+ .panel-body
29
+ = render partial: 'rhea/commands/form'
30
+
31
+ .panel.panel-default
32
+ .panel-heading
33
+ %h4 Command Types
34
+ .panel-body
35
+ = render partial: 'rhea/command_types/list'
36
+
37
+ :javascript
38
+ var checkboxes = $('.command-checkbox');
39
+ var batchActions = $('.batch-actions');
40
+
41
+ var toggleBatchActions = function() {
42
+ batchActions.toggle(checkboxes.filter(':checked').length > 0);
43
+ return true;
44
+ }
45
+
46
+ checkboxes.on('click', toggleBatchActions);
47
+
48
+ $('#check-all').change(function(){
49
+ $(".command-checkbox").prop('checked', $(this).prop("checked"));
50
+ toggleBatchActions()
51
+ })
@@ -0,0 +1,2 @@
1
+ %p
2
+ Sorry, there was an exception while trying to connect to the Kubernetes cluster.
@@ -0,0 +1,24 @@
1
+ %table.table.table-condensed.table-striped
2
+ %thead
3
+ %tr
4
+ %th Time
5
+ %th
6
+ %th Host
7
+ %th Message
8
+ %th Type
9
+ %th Resource Type
10
+ %th Resource ID
11
+ %tbody
12
+ - @events.each do |event|
13
+ %tr
14
+ %td{ style: 'white-space: nowrap;' }= event.created_at.in_time_zone(Time.zone)
15
+ %td{ style: 'white-space: nowrap;' }
16
+ = time_ago_in_words(event.created_at.in_time_zone(Time.zone), include_seconds: true)
17
+ ago
18
+ %td= event.hostname
19
+ %td= simple_format(event.message)
20
+ %td= event.type
21
+ %td= event.resource_type
22
+ %td= event.resource_id
23
+
24
+ = paginate @events