diff --git a/app/controllers/hosts_controller.rb b/app/controllers/hosts_controller.rb
index 1d92fe35b94..1d63bd4d237 100644
--- a/app/controllers/hosts_controller.rb
+++ b/app/controllers/hosts_controller.rb
@@ -56,7 +56,7 @@ def index(title = nil)
end
format.csv do
@hosts = search.preload(included_associations - [:host_statuses, :token])
- csv_response(@hosts)
+ csv_response(@hosts, csv_columns, csv_headers)
end
end
end
@@ -892,9 +892,28 @@ def host_attributes_for_templates(host)
end
def csv_columns
- Pagelets::Manager.pagelets_at("hosts/_list", 'hosts_table_column_header', filter: { selected: @selected_columns })
- .map { |pagelet| pagelet.opts[:export_data] || pagelet.opts[:export_key] || pagelet.opts[:key] }
- .flatten
+ csv_pagelets.map { |pagelet| pagelet.opts[:export_data] || pagelet.opts[:export_key] || pagelet.opts[:key] }.flatten
+ end
+
+ def csv_headers
+ csv_pagelets.map do |pagelet|
+ export_data = pagelet.opts[:export_data]
+
+ # Handle array of ExportDefinitions (like installable_updates)
+ if export_data.is_a?(Array)
+ export_data.map(&:label)
+ elsif export_data.is_a?(CsvExporter::ExportDefinition)
+ export_data.label
+ else
+ # Check for explicit export_label first, then pagelet label, then derive from export_key/key
+ # Use the same logic as CsvExporter::ExportDefinition.derive_label
+ pagelet.opts[:export_label] || pagelet.opts[:label] || (pagelet.opts[:export_key] || pagelet.opts[:key]).to_s.titleize.gsub('.', ' - ')
+ end
+ end.flatten
+ end
+
+ def csv_pagelets
+ @csv_pagelets ||= Pagelets::Manager.pagelets_at("hosts/_list", 'hosts_table_column_header', filter: { selected: @selected_columns })
end
def origin_intervals_query(compare_with)
diff --git a/config/initializers/foreman_register.rb b/config/initializers/foreman_register.rb
index 629454ad392..28ccf3e2490 100644
--- a/config/initializers/foreman_register.rb
+++ b/config/initializers/foreman_register.rb
@@ -21,6 +21,10 @@
add_pagelet :hosts_table_column_content, key: :owner, callback: ->(host) { host_owner_column(host) }, class: common_class
add_pagelet :hosts_table_column_header, key: :hostgroup, label: N_('Host group'), sortable: true, width: '15%', class: common_class
add_pagelet :hosts_table_column_content, key: :hostgroup, callback: ->(host) { label_with_link host.hostgroup, 23, @hostgroup_authorizer }, class: common_class
+ add_pagelet :hosts_table_column_header, key: :organization, label: N_('Organization'), sortable: true, width: '12%', export_key: 'organization.name', class: common_class
+ add_pagelet :hosts_table_column_content, key: :organization, callback: ->(host) { host.organization&.name }, class: common_class
+ add_pagelet :hosts_table_column_header, key: :location, label: N_('Location'), sortable: true, width: '12%', export_key: 'location.name', class: common_class
+ add_pagelet :hosts_table_column_content, key: :location, callback: ->(host) { host.location&.name }, class: common_class
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
add_pagelet :hosts_table_column_content, key: :boot_time, callback: ->(host) { date_time_unless_empty(host.reported_data&.boot_time) }, class: common_class
add_pagelet :hosts_table_column_header, key: :last_report, label: N_('Last report'), sortable: true, default_sort: 'DESC', width: '10%', class: common_class
diff --git a/config/routes.rb b/config/routes.rb
index 959f785f59f..45298419824 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -552,6 +552,7 @@
match 'host_statuses' => 'react#index', :via => :get
match 'new/hosts/auto_complete_search', :via => :get, :to => 'hosts#auto_complete_search', :as => "auto_complete_search_hosts_new"
+ match 'new/hosts.csv', :via => :get, :to => 'hosts#index', :defaults => { :format => 'csv' }, :as => :new_hosts_csv
constraints(id: /[^\/]+/) do
match 'new/hosts/:id' => 'react#index', :via => :get, :as => :host_details_page
end
diff --git a/test/controllers/hosts_controller_test.rb b/test/controllers/hosts_controller_test.rb
index 139e4c01c69..405bd72e4f4 100644
--- a/test/controllers/hosts_controller_test.rb
+++ b/test/controllers/hosts_controller_test.rb
@@ -51,6 +51,43 @@ def host_attributes(host)
end
end
+ test "csv_headers should use ExportDefinition labels for power_status" do
+ User.current.table_preferences.create(name: 'hosts', columns: ['power_status'])
+ get :index, params: { :format => 'csv' }, session: set_session_user
+ assert_response :success
+ buf = response.stream.instance_variable_get(:@buf)
+ header_line = buf.next
+ assert_includes header_line, "Power Status", "Header should use ExportDefinition label 'Power Status' not 'Power'"
+ end
+
+ test "csv_headers should derive headers from export_key when available" do
+ User.current.table_preferences.create(name: 'hosts', columns: ['os_title', 'boot_time'])
+ get :index, params: { :format => 'csv' }, session: set_session_user
+ assert_response :success
+ buf = response.stream.instance_variable_get(:@buf)
+ header_line = buf.next
+ assert_includes header_line, "Operatingsystem", "Header should be derived from export_key 'operatingsystem'"
+ assert_includes header_line, "Reported Data - Boot Time", "Header should format dotted keys with ' - '"
+ end
+
+ test "csv_columns should return columns from pagelets" do
+ User.current.table_preferences.create(name: 'hosts', columns: ['name', 'power_status'])
+ get :index, params: { :format => 'csv' }, session: set_session_user
+ assert_response :success
+ # Verify columns are properly extracted from pagelets
+ assert @controller.send(:csv_columns).any? { |col| col.is_a?(String) || col.is_a?(CsvExporter::ExportDefinition) }
+ end
+
+ test "csv_pagelets should be memoized" do
+ User.current.table_preferences.create(name: 'hosts', columns: ['name'])
+ get :index, params: { :format => 'csv' }, session: set_session_user
+
+ # Call csv_pagelets twice and verify it returns the same object (memoized)
+ pagelets1 = @controller.send(:csv_pagelets)
+ pagelets2 = @controller.send(:csv_pagelets)
+ assert_same pagelets1, pagelets2, "csv_pagelets should be memoized"
+ end
+
test "should include registered scope on index" do
# remember the previous state
old_scopes = HostsController.scopes_for(:index).dup
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js
index f468ae3cb92..b2856abcfb9 100644
--- a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js
@@ -465,6 +465,23 @@ const HostsIndex = () => {
selectedCount={selectedCount}
/>
+
+
+