Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #35274 - Make columns on host index page selectable #9319

Merged
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(table: controller_name)
result = ""
Foreman::SelectableColumns::Storage.selected_by(User.current, table.to_s).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, table: controller_name)
result = ""
Foreman::SelectableColumns::Storage.selected_by(User.current, table.to_s).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.reduce([]) { |m, (k, v)| m << "#{k}=\"#{instance_exec(subject, &v)}\"" }
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
.join(' ')
.html_safe
end

private

def attributes(th_or_td)
th_or_td.except(:callback, :sortable, :default_sort, :attr_callbacks)
.reduce([]) { |m, (k, v)| m << "#{k}=\"#{v}\"" }
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
.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
28 changes: 28 additions & 0 deletions app/registries/foreman/selectable_columns/category.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Foreman
module SelectableColumns
class Category < Array
attr_reader :id, :label

def initialize(id, label, default: false)
@id = id
@label = label
@default = default
super(0)
end

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

# This instance is not meant to be updated after it was created by DSL
# Thus, saving once computed keys
def keys
@keys ||= map { |c| c[:key] }.sort
end

def default?
@default
end
end
end
end
44 changes: 44 additions & 0 deletions app/registries/foreman/selectable_columns/storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Foreman
module SelectableColumns
class Storage
include Singleton

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

def define(name, &block)
Foreman::Logging.logger('selectable columns').info _('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)
Foreman::Logging.logger('selectable columns').info _('Table %s is not defined, ignoring.') % name unless tables[name]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new logger will require a new entry in settings.yml.example

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think we need a new logger. This message is for devs only. Ideally, users shouldn't see those messages at all. I'll use app logger here.


tables[name].instance_eval(&block)
end

def defined_for(table)
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
tables[table].reduce({}) do |defined, category|
defined.update(category.label => category.map { |c| { c[:key] => c[:th][:label] } })
end
end

def selected_by(user, table)
selected_keys = user.table_preferences.find_by(name: table)&.columns&.sort
if selected_keys
tables[table].select { |category| (category.keys & selected_keys).any? }
.flatten
.select { |col| selected_keys.include?(col[:key]) }
else
tables[table].select { |category| category.default? }.flatten
end
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_sym, 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, 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
23 changes: 23 additions & 0 deletions config/initializers/foreman_register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,26 @@
partial: 'hosts/init_config_tab',
priority: 100
end

Foreman::SelectableColumns::Storage.define(:hosts) do
common_th_class = 'hidden-tablet hidden-xs'
common_td_class = common_th_class + ' ellipsis'
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
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
96 changes: 96 additions & 0 deletions developer_docs/how_to_create_a_plugin.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,102 @@ widget_name". The content of the widget should be in the path:
app/views/dashboard/_<widget_name>.html.erb
....

[[selectable-columns]]
=== Selectable columns

_Requires Foreman 3.4 or higher, set `requires_foreman '>= 3.4'` in engine.rb or
register.rb._

[[adding-selectable-columns]]
===== Adding selectable columns to index pages

Index pages always contain a table with a certain set of columns.
Since Foreman 3.4 some pages have this set extendable, e.g. hosts index page,
and provide users a way to select which columns are meant to be displayed. For
the list of available tables with selectable columns, please refer to
config/initializers/foreman_register.rb file.


If you want to add more selectable columns to an existing page in Foreman,
please the following syntax in
`Foreman::Plugin.register :foreman_plugin do ... end` block:

[source, ruby]
----
selectable_columns :table do
category :id, label: N_('Category name'), default: true do
column :key,
th: {
label: N_('Column name'),
sortable: true,
default_sort: 'DESC',
width: '10%',
class: 'hidden ca'
},
td: {
class: 'hidden ca',
attr_callbacks: {
title: ->(record) { record.value }
},
callback: ->(record) { record.value }
}
end
end
----

Where for `category`:

* `id`: [Symbol] (Required) Internal id of the category. Used to find an existing or define a new one.
* `label`: [String] (Optional) Translated string with category name. Required only for new categories.
* `default`: [Boolean] (Optional) Flag whether this category should be visible to users by default. Default: `false`.

Where for `column`:

* `key`: [Symbol] (Required) Record attribute's name. Used to make column sortable and selectable.
* `th`: [Hash] (Required) Column header definition. Please, see below for further information.
* `td`: [Hash] (Required) Column data definition. Please, see below for further information.

Where for `th`:

* `label`: [String] (Required) Translated string with column name to be shown.
* `sortable`: [Boolean] (Optional) Makes column sortable. Default: `false`.
* `default_sort`: [String] (Optional) Defines default sort order. Default: `'ASC'`

Where for `td`:

* `callback`: [Lambda] (Required) A callback that will be used to process data to be shown.
* `attr_callbacks`: [Hash] (Optional) Only use if you need to process values for HTML attributes dynamically.

You can also define HTML attributes for `<th>` and `<td>` tags. Some common are
listed below, but you can use your own.
_Note: all except `:callback`, `:sortable`, `:default_sort`, `:attr_callbacks`
are considered as HTML tag attributes.

* `width`: [String] (Optional) Width of the column.
* `class`: [String] (Optional) Space-separated list of CSS classes to use for the column.

[[creaing-selectable-columns]]
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
===== Creating selectable columns on index pages

If you want to not only extend existing pages, but to also have pages with
selectable columns in the plugin itself, you can use
`Foreman::SelectableColumns::Storage` directly. In this case everything the same
except you no longer need to use plugin's DSL, but the storage itself to define
your tables:

[source, ruby]
----
# in your initializer
Foreman::SelectableColumns::Storage.define(:table) do
category ...
end
----

To use your columns in views, please use

* `render_selected_column_ths(table: controller_name)`: Renders all selectable column <th> tags for table.
* `render_selected_column_tds(record, table: controller_name)`: Renders all selectable column <td> tags for table.

[[adding-a-pagelet]]
=== Adding a Pagelet

Expand Down