rhea 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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