Skip to content

Commit

Permalink
Fixes #35274 - Make columns on host index page selectable (#9319)
Browse files Browse the repository at this point in the history
  • Loading branch information
ofedoren authored Aug 3, 2022
1 parent 5df0fc3 commit b1b46b6
Show file tree
Hide file tree
Showing 17 changed files with 585 additions and 16 deletions.
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base
before_action :session_expiry, :update_activity_time, :unless => proc { |c| c.remote_user_provided? || c.api_request? }
before_action :set_taxonomy, :require_mail, :check_empty_taxonomy
before_action :authorize
before_action :welcome, :only => :index, :unless => :api_request?
before_action :welcome, :find_selected_columns, :only => :index, :unless => :api_request?
prepend_before_action :allow_webpack, if: -> { Rails.configuration.webpack.dev_server.enabled }
around_action :set_timezone

Expand Down
4 changes: 4 additions & 0 deletions app/controllers/concerns/application_shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,8 @@ def find_session_taxonomy(taxonomy, user)
end
determined_taxonomy
end

def find_selected_columns
@selected_columns = Foreman::SelectableColumns::Storage.selected_by(User.current, controller_name)
end
end
2 changes: 2 additions & 0 deletions app/controllers/hosts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class HostsController < ApplicationController
before_action :set_host_type, :only => [:update]
before_action :find_multiple, :only => MULTIPLE_ACTIONS
before_action :validate_power_action, :only => :update_multiple_power_state
# index action is already included in ApplicationController
before_action(:only => SEARCHABLE_ACTIONS.without('index')) { find_selected_columns }

helper :hosts, :reports, :interfaces

Expand Down
52 changes: 52 additions & 0 deletions app/helpers/selectable_columns_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module SelectableColumnsHelper
def render_selected_column_ths
result = ""
@selected_columns.each do |column|
result += render(
'common/selectable_column_th',
attributes: attributes(column[:th]),
th_content: th_content(column),
callback: column[:th][:callback]
)
end
result.html_safe
end

def render_selected_column_tds(record)
result = ""
@selected_columns.each do |column|
result += render(
'common/selectable_column_td',
attributes: attributes(column[:td]),
attr_callbacks: column[:td][:attr_callbacks],
subject: record,
callback: column[:td][:callback]
)
end
result.html_safe
end

def attr_from_callbacks(callbacks, subject)
return unless callbacks

callbacks.map { |(k, v)| "#{k}=\"#{instance_exec(subject, &v)}\"" }
.join(' ')
.html_safe
end

private

def attributes(th_or_td)
th_or_td.except(:callback, :sortable, :default_sort, :attr_callbacks, :label)
.map { |(k, v)| "#{k}=\"#{v}\"" }
.join(' ')
.html_safe
end

def th_content(col)
return if col[:th][:callback]
return col[:th][:label] unless col[:th][:sortable]

sort col[:key], as: col[:th][:label], default: col[:th][:default_sort] || 'ASC'
end
end
4 changes: 4 additions & 0 deletions app/registries/foreman/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,10 @@ def settings(&block)
SettingManager.define(id, &block)
end

def selectable_columns(table, &block)
Foreman::SelectableColumns::Storage.register(table, &block)
end

def security_block(name, &block)
@security_block = name
instance_eval(&block)
Expand Down
63 changes: 63 additions & 0 deletions app/registries/foreman/selectable_columns/category.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Foreman
module SelectableColumns
class Category < Array
attr_reader :id, :label

def initialize(id, label, table, default: false)
@id = id
@label = label
@table = table
@default = default
@columns_to_use = HashWithIndifferentAccess.new
super(0)
end

def column(key, th:, td:)
self << { key: key.to_s, th: th, td: td }.with_indifferent_access
end

def use_column(key, from:)
return if from.to_s == @id

@columns_to_use[from] ||= []
@columns_to_use[from] << key.to_s
end

def common_th_class
'hidden-tablet hidden-xs'
end

def common_td_class
common_th_class + ' ellipsis'
end

# This instance can be updated after it was created by DSL, but
# only at the initialization time.
# This method is only for actual runtime usage,
# after the Storage is fully defined/updated.
# Thus, saving once computed result.
def keys
@keys ||= columns.map { |c| c[:key] }.sort
end

def default?
@default
end

def columns
self + foreign_columns
end

private

# This is meant for actual runtime usage,
# after the categories are fully defined.
# Thus, saving once computed result.
def foreign_columns
@foreign_columns ||= @columns_to_use.keys.map do |category_id|
@table.find { |cat| cat.id == category_id }&.select { |col| @columns_to_use[category_id].include?(col[:key]) }
end.flatten.compact
end
end
end
end
57 changes: 57 additions & 0 deletions app/registries/foreman/selectable_columns/storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module Foreman
module SelectableColumns
class Storage
include Singleton

class << self
def tables
@tables ||= ActiveSupport::HashWithIndifferentAccess.new
end

def define(name, &block)
return Foreman::Logging.logger('app').warn _('Table %s is already defined, ignoring.') % name if tables[name]

table = SelectableColumns::Table.new(name)
table.instance_eval(&block)
tables[name] = table
end

def register(name, &block)
return Foreman::Logging.logger('app').warn _('Table %s is not defined, ignoring.') % name unless tables[name]

tables[name].instance_eval(&block)
end

# This is for UI data mostly
def defined_for(table)
return Foreman::Logging.logger('app').warn _('Table %s is not defined, ignoring.') % table unless tables[table]

tables[table].map do |category|
{
id: category.id,
name: category.label,
columns: category.columns.map { |c| { id: c[:key], name: c[:th][:label] } },
}
end
end

def selected_by(user, table)
return unless tables[table]

selected_keys = user.table_preferences.find_by(name: table)&.columns&.sort
result = if selected_keys
tables[table].select { |category| (category.keys & selected_keys).any? }
.map(&:columns)
.flatten
.select { |col| selected_keys.include?(col[:key]) }
else
tables[table].select { |category| category.default? }
.map(&:columns)
.flatten
end
result.uniq
end
end
end
end
end
28 changes: 28 additions & 0 deletions app/registries/foreman/selectable_columns/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Foreman
module SelectableColumns
class Table < Array
attr_reader :name

def initialize(name)
@name = name
super(0)
end

def category(id, label: _('General'), default: false, &block)
category = find_or_create(id.to_s, label, default)
category.instance_eval(&block)
end

private

def find_or_create(id, label, default)
category = find { |c| c.id == id }
unless category
category = Category.new(id, label, self, default: default)
self << category
end
category
end
end
end
end
1 change: 1 addition & 0 deletions app/views/common/_selectable_column_td.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<td <%= attributes %> <%= attr_from_callbacks(attr_callbacks, subject) %>><%= instance_exec(subject, &callback) %></td>
1 change: 1 addition & 0 deletions app/views/common/_selectable_column_th.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<th <%= attributes %>><%= th_content ? th_content : instance_eval(callback) %></th>
17 changes: 2 additions & 15 deletions app/views/hosts/_list.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,8 @@
<% if power_status_visible? %>
<th class="ca" width="80px"><%= _('Power') %></th>
<% end %>
<th width="25%"><%= sort :name, :as => _('Name') %></th>
<th class="hidden-xs" width="17%"><%= sort :os_title, :as => _("Operating system") %></th>
<th class="hidden-tablet hidden-xs" width="10%"><%= sort :model, :as => _('Model') %></th>
<th class="hidden-tablet hidden-xs" width="8%"><%= sort :owner, :as => _('Owner') %></th>
<th class="hidden-tablet hidden-xs" width="15%"><%= sort :hostgroup, :as => _("Host group") %></th>
<th class="hidden-tablet hidden-xs" width="10%"><%= sort :last_report, :as => _('Last report'), :default => 'DESC' %></th>
<%= render_selected_column_ths %>
<%= render_pagelets_for(:hosts_table_column_header) %>
<th class="hidden-tablet hidden-xs" width="7%"><%= sort :comment, :as => _('Comment') %></th>
<th width="100px"><%= _('Actions') %></th>
</tr>
</thead>
Expand All @@ -29,15 +23,8 @@
<%= react_component('PowerStatus', id: host.id, url: power_api_host_path(host)) %>
</td>
<% end %>
<td class="ellipsis"><%= name_column(host) %>
</td>
<td class="hidden-xs ellipsis"><%= (icon(host.operatingsystem, :size => "16x16") + " #{host.operatingsystem.to_label}").html_safe if host.operatingsystem %></td>
<td class="hidden-tablet hidden-xs ellipsis"><%= host.compute_resource_or_model %></td>
<td class="hidden-tablet hidden-xs ellipsis"><%= host_owner_column(host) %></td>
<td class="hidden-tablet hidden-xs"><%= label_with_link host.hostgroup, 23, @hostgroup_authorizer %></td>
<td class="hidden-tablet hidden-xs ellipsis"><%= last_report_column(host) %></td>
<%= render_selected_column_tds(host) %>
<%= render_pagelets_for(:hosts_table_column_content, :subject => host) %>
<td class="hidden-tablet hidden-xs ca" title="<%= host.comment&.truncate(255) %>"><%= icon_text('comment', '') unless host.comment.empty? %></td>
<td>
<%= action_buttons(
display_link_if_authorized(_("Edit"), hash_for_edit_host_path(:id => host).merge(:auth_object => host, :authorizer => authorizer)),
Expand Down
21 changes: 21 additions & 0 deletions config/initializers/foreman_register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,24 @@
partial: 'hosts/init_config_tab',
priority: 100
end

Foreman::SelectableColumns::Storage.define(:hosts) do
category :general, default: true do
column :name, th: { label: _('Name'), sortable: true, width: '25%' },
td: { class: 'ellipsis', callback: ->(host) { name_column(host) } }
column :os_title, th: { label: _('Operating system'), sortable: true, width: '17%', class: 'hidden-xs' },
td: { class: 'hidden-xs ellipsis', callback: ->(host) { (icon(host.operatingsystem, size: "16x16") + " #{host.operatingsystem.to_label}").html_safe if host.operatingsystem } }
column :model, th: { label: _('Model'), sortable: true, width: '10%', class: common_th_class },
td: { class: common_td_class, callback: ->(host) { host.compute_resource_or_model } }
column :owner, th: { label: _('Owner'), sortable: true, width: '8%', class: common_th_class },
td: { class: common_td_class, callback: ->(host) { host_owner_column(host) } }
column :hostgroup, th: { label: _('Host group'), sortable: true, width: '15%', class: common_th_class },
td: { class: common_th_class, callback: ->(host) { label_with_link host.hostgroup, 23, @hostgroup_authorizer } }
column :last_report, th: { label: _('Last report'), sortable: true, default_sort: 'DESC', width: '10%', class: common_th_class },
td: { class: common_td_class, callback: ->(host) { last_report_column(host) } }
column :comment, th: { label: _('Comment'), sortable: true, width: '7%', class: common_th_class },
td: { class: common_th_class + ' ca',
attr_callbacks: { title: ->(host) { host.comment&.truncate(255) } },
callback: ->(host) { icon_text('comment', '') unless host.comment.empty? } }
end
end
Loading

0 comments on commit b1b46b6

Please sign in to comment.