diff --git a/app/controllers/katello/api/v2/host_packages_controller.rb b/app/controllers/katello/api/v2/host_packages_controller.rb index 8f9b60faff2..c779e655080 100644 --- a/app/controllers/katello/api/v2/host_packages_controller.rb +++ b/app/controllers/katello/api/v2/host_packages_controller.rb @@ -44,7 +44,18 @@ def installed_packages add_scoped_search_description_for(Katello::InstalledPackage) def index validate_index_params! - collection = scoped_search(index_relation, :name, :asc, :resource_class => ::Katello::InstalledPackage) + options = { :resource_class => ::Katello::InstalledPackage } + + # Handle persistence sorting (normal scoped_search cannot handle this join for multiple hosts) + if params[:sort_by] == 'persistence' + options[:custom_sort] = lambda do |query| + query.joins(:host_installed_packages) + .where(katello_host_installed_packages: {host_id: @host.id}) + .order("katello_host_installed_packages.persistence #{sanitize_sort_order(params[:sort_order])}") + end + end + + collection = scoped_search(index_relation, :name, :asc, options) include_upgradable = ::Foreman::Cast.to_bool(params[:include_latest_upgradable]) # Present packages with persistence and (if requested) latest upgradable info @@ -145,5 +156,13 @@ def validate_index_params! fail _("Status must be one of: %s" % VERSION_STATUSES.join(', ')) end end + + def sanitize_sort_order(sort_order) + if sort_order.present? && ['asc', 'desc'].include?(sort_order.downcase) + sort_order.downcase + else + 'asc' + end + end end end diff --git a/app/models/katello/concerns/host_managed_extensions.rb b/app/models/katello/concerns/host_managed_extensions.rb index f24d315b1ab..ef8b20f0f8f 100644 --- a/app/models/katello/concerns/host_managed_extensions.rb +++ b/app/models/katello/concerns/host_managed_extensions.rb @@ -610,6 +610,10 @@ def yum_or_yum_transient content_facet&.yum_or_yum_transient || "yum" end + def contains_package_with_reported_persistence? + host_installed_packages.where.not(persistence: nil).exists? + end + protected def update_trace_status diff --git a/app/views/katello/api/v2/hosts/base.json.rabl b/app/views/katello/api/v2/hosts/base.json.rabl index 28108a81dc6..f02111fad47 100644 --- a/app/views/katello/api/v2/hosts/base.json.rabl +++ b/app/views/katello/api/v2/hosts/base.json.rabl @@ -26,4 +26,7 @@ if @facet } end end + node :contains_package_with_reported_persistence do + @resource.contains_package_with_reported_persistence? + end end diff --git a/test/controllers/api/v2/host_packages_controller_test.rb b/test/controllers/api/v2/host_packages_controller_test.rb index 4c6efe14e84..9ac96544a51 100644 --- a/test/controllers/api/v2/host_packages_controller_test.rb +++ b/test/controllers/api/v2/host_packages_controller_test.rb @@ -67,6 +67,22 @@ def test_index_includes_persistence assert_equal 'transient', package['persistence'] end + def test_index_sort_by_persistence + pkg1 = @host.installed_packages.first + pkg2 = @host.installed_packages.second + Katello::HostInstalledPackage.where(host: @host, installed_package: pkg1).update_all(persistence: 'transient') + Katello::HostInstalledPackage.where(host: @host, installed_package: pkg2).update_all(persistence: 'persistent') + + get :index, params: { :host_id => @host.id, :sort_by => 'persistence', :sort_order => 'asc' } + + assert_response :success + response_data = JSON.parse(response.body) + results = response_data['results'] + + assert_equal 'persistent', results.first['persistence'] + assert_equal 'transient', results.last['persistence'] + end + def test_view_permissions ::Host.any_instance.stubs(:check_host_registration).returns(true) diff --git a/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js b/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js index a1cd2ff273a..2b2b28b9368 100644 --- a/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js +++ b/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js @@ -1,5 +1,5 @@ import React, { useCallback, useState, useRef } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; import { ActionList, ActionListItem, @@ -22,7 +22,6 @@ import { FormattedMessage } from 'react-intl'; import { TableVariant, Thead, Tbody, Tr, Th, Td, TableText, ActionsColumn } from '@patternfly/react-table'; import PropTypes from 'prop-types'; import { translate as __ } from 'foremanReact/common/I18n'; -import { HOST_DETAILS_KEY } from 'foremanReact/components/HostDetails/consts'; import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors'; import { useSet, useBulkSelect, useUrlParams } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; import { useTableSort } from 'foremanReact/components/PF4/Helpers/useTableSort'; @@ -123,6 +122,11 @@ UpdateVersionsSelect.defaultProps = { upgradableVersionSelectOpen: null, }; +const formatPersistence = (persistence) => { + if (!persistence) return '—'; + return persistence.charAt(0).toUpperCase() + persistence.slice(1); +}; + export const PackagesTab = () => { const hostDetails = useSelector(state => selectAPIResponse(state, 'HOST_DETAILS')); const { @@ -174,17 +178,32 @@ export const PackagesTab = () => { const emptySearchTitle = __('No matching packages found'); const emptySearchBody = __('Try changing your search settings.'); const errorSearchTitle = __('Problem searching packages'); - const columnHeaders = [ + + const isBootCHost = hostIsImageMode({ hostDetails }); + const columnHeaders = isBootCHost ? [ __('Package'), + __('Persistence'), __('Status'), __('Installed version'), __('Upgradable to'), - ]; - - const COLUMNS_TO_SORT_PARAMS = { - [columnHeaders[0]]: 'nvra', - [columnHeaders[2]]: 'version', - }; + ] : + [ + __('Package'), + __('Status'), + __('Installed version'), + __('Upgradable to'), + ]; + + const COLUMNS_TO_SORT_PARAMS = isBootCHost ? + { + [columnHeaders[0]]: 'nvra', + [columnHeaders[1]]: 'persistence', + [columnHeaders[3]]: 'version', + } : + { + [columnHeaders[0]]: 'nvra', + [columnHeaders[2]]: 'version', + }; const { pfSortParams, apiSortParams, @@ -211,7 +230,6 @@ export const PackagesTab = () => { const { results, ...metadata } = response; const { error: errorSearchBody } = metadata; const status = useSelector(state => selectHostPackagesStatus(state)); - const dispatch = useDispatch(); const { selectOne, isSelected, @@ -286,17 +304,9 @@ export const PackagesTab = () => { isPolling: isInstallInProgress, } = useRexJobPolling(packageInstallAction, getHostDetails({ hostname })); - const refreshHostDetails = () => dispatch({ - type: 'API_GET', - payload: { - key: HOST_DETAILS_KEY, - url: `/api/hosts/${hostname}`, - }, - }); - const { triggerJobStart: triggerRecalculate, lastCompletedJob: lastCompletedRecalculate, - } = useRexJobPolling(() => runSubmanRepos(hostname, refreshHostDetails)); + } = useRexJobPolling(() => runSubmanRepos(hostname), getHostDetails({ hostname })); const handleRefreshApplicabilityClick = () => { setIsBulkActionOpen(false); @@ -486,7 +496,7 @@ export const PackagesTab = () => { return (