Skip to content

Commit 9e6a0c2

Browse files
committed
Fixes #38941 - Implement CSV exports on the hosts overview
1 parent c34fe0e commit 9e6a0c2

File tree

5 files changed

+77
-4
lines changed

5 files changed

+77
-4
lines changed

app/controllers/hosts_controller.rb

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def index(title = nil)
5656
end
5757
format.csv do
5858
@hosts = search.preload(included_associations - [:host_statuses, :token])
59-
csv_response(@hosts)
59+
csv_response(@hosts, csv_columns, csv_headers)
6060
end
6161
end
6262
end
@@ -892,9 +892,29 @@ def host_attributes_for_templates(host)
892892
end
893893

894894
def csv_columns
895-
Pagelets::Manager.pagelets_at("hosts/_list", 'hosts_table_column_header', filter: { selected: @selected_columns })
896-
.map { |pagelet| pagelet.opts[:export_data] || pagelet.opts[:export_key] || pagelet.opts[:key] }
897-
.flatten
895+
csv_pagelets.map { |pagelet| pagelet.opts[:export_data] || pagelet.opts[:export_key] || pagelet.opts[:key] }.flatten
896+
end
897+
898+
def csv_headers
899+
csv_pagelets.map do |pagelet|
900+
export_data = pagelet.opts[:export_data]
901+
902+
# Handle array of ExportDefinitions (like installable_updates)
903+
if export_data.is_a?(Array)
904+
export_data.map(&:label)
905+
elsif export_data.is_a?(CsvExporter::ExportDefinition)
906+
export_data.label
907+
else
908+
# Check for explicit export_label first, then derive from export_key/key
909+
# Use the same logic as CsvExporter::ExportDefinition.derive_label
910+
key = export_data || pagelet.opts[:export_key] || pagelet.opts[:key]
911+
pagelet.opts[:export_label] || key.to_s.titleize.gsub('.', ' - ')
912+
end
913+
end.flatten
914+
end
915+
916+
def csv_pagelets
917+
@csv_pagelets ||= Pagelets::Manager.pagelets_at("hosts/_list", 'hosts_table_column_header', filter: { selected: @selected_columns })
898918
end
899919

900920
def origin_intervals_query(compare_with)

config/initializers/foreman_register.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
add_pagelet :hosts_table_column_content, key: :owner, callback: ->(host) { host_owner_column(host) }, class: common_class
2222
add_pagelet :hosts_table_column_header, key: :hostgroup, label: N_('Host group'), sortable: true, width: '15%', class: common_class
2323
add_pagelet :hosts_table_column_content, key: :hostgroup, callback: ->(host) { label_with_link host.hostgroup, 23, @hostgroup_authorizer }, class: common_class
24+
add_pagelet :hosts_table_column_header, key: :organization, label: N_('Organization'), sortable: true, width: '12%', class: common_class
25+
add_pagelet :hosts_table_column_content, key: :organization, callback: ->(host) { host.organization&.name }, class: common_class
26+
add_pagelet :hosts_table_column_header, key: :location, label: N_('Location'), sortable: true, width: '12%', class: common_class
27+
add_pagelet :hosts_table_column_content, key: :location, callback: ->(host) { host.location&.name }, class: common_class
2428
add_pagelet :hosts_table_column_header, key: :boot_time, label: N_('Boot time'), sortable: true, width: '10%', export_key: 'reported_data.boot_time', class: common_class
2529
add_pagelet :hosts_table_column_content, key: :boot_time, callback: ->(host) { date_time_unless_empty(host.reported_data&.boot_time) }, class: common_class
2630
add_pagelet :hosts_table_column_header, key: :last_report, label: N_('Last report'), sortable: true, default_sort: 'DESC', width: '10%', class: common_class

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@
552552

553553
match 'host_statuses' => 'react#index', :via => :get
554554
match 'new/hosts/auto_complete_search', :via => :get, :to => 'hosts#auto_complete_search', :as => "auto_complete_search_hosts_new"
555+
match 'new/hosts.csv', :via => :get, :to => 'hosts#index', :defaults => { :format => 'csv' }, :as => :new_hosts_csv
555556
constraints(id: /[^\/]+/) do
556557
match 'new/hosts/:id' => 'react#index', :via => :get, :as => :host_details_page
557558
end

test/controllers/hosts_controller_test.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,43 @@ def host_attributes(host)
5151
end
5252
end
5353

54+
test "csv_headers should use ExportDefinition labels for power_status" do
55+
User.current.table_preferences.create(name: 'hosts', columns: ['power_status'])
56+
get :index, params: { :format => 'csv' }, session: set_session_user
57+
assert_response :success
58+
buf = response.stream.instance_variable_get(:@buf)
59+
header_line = buf.next
60+
assert_includes header_line, "Power Status", "Header should use ExportDefinition label 'Power Status' not 'Power'"
61+
end
62+
63+
test "csv_headers should derive headers from export_key when available" do
64+
User.current.table_preferences.create(name: 'hosts', columns: ['os_title', 'boot_time'])
65+
get :index, params: { :format => 'csv' }, session: set_session_user
66+
assert_response :success
67+
buf = response.stream.instance_variable_get(:@buf)
68+
header_line = buf.next
69+
assert_includes header_line, "Operatingsystem", "Header should be derived from export_key 'operatingsystem'"
70+
assert_includes header_line, "Reported Data - Boot Time", "Header should format dotted keys with ' - '"
71+
end
72+
73+
test "csv_columns should return columns from pagelets" do
74+
User.current.table_preferences.create(name: 'hosts', columns: ['name', 'power_status'])
75+
get :index, params: { :format => 'csv' }, session: set_session_user
76+
assert_response :success
77+
# Verify columns are properly extracted from pagelets
78+
assert @controller.send(:csv_columns).any? { |col| col.is_a?(String) || col.is_a?(CsvExporter::ExportDefinition) }
79+
end
80+
81+
test "csv_pagelets should be memoized" do
82+
User.current.table_preferences.create(name: 'hosts', columns: ['name'])
83+
get :index, params: { :format => 'csv' }, session: set_session_user
84+
85+
# Call csv_pagelets twice and verify it returns the same object (memoized)
86+
pagelets1 = @controller.send(:csv_pagelets)
87+
pagelets2 = @controller.send(:csv_pagelets)
88+
assert_same pagelets1, pagelets2, "csv_pagelets should be memoized"
89+
end
90+
5491
test "should include registered scope on index" do
5592
# remember the previous state
5693
old_scopes = HostsController.scopes_for(:index).dup

webpack/assets/javascripts/react_app/components/HostsIndex/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,17 @@ const HostsIndex = () => {
465465
selectedCount={selectedCount}
466466
/>
467467
</SplitItem>
468+
<SplitItem>
469+
<Button
470+
component="a"
471+
ouiaId="export-hosts-button"
472+
href={foremanUrl(`${hostsIndexUrl}.csv`)}
473+
variant="secondary"
474+
isDisabled={false}
475+
>
476+
{__('Export')}
477+
</Button>
478+
</SplitItem>
468479
<SplitItem>
469480
<Button
470481
component="a"

0 commit comments

Comments
 (0)