Skip to content

Commit

Permalink
Fixes #38107 - Booted container images page (#11269)
Browse files Browse the repository at this point in the history
* Fixes #38107 - add booted container images page

* Refs #38107 - use Table on bootc images page

* Refs #38107 - add digests + hosts columns to bootc containers page

* Refs #38107 - Make booted container images rows expandable

* Refs #38107 - sorting on booted container images page

* Refs #38107 - fix bootc bookmarks + adjust table format

* Refs #38107 - fix all rows expanding issue

* Refs #38107 - add test for BootedContainerImagesPage

* Refs #38107 - fix empty state, fix page caps, do more testing
  • Loading branch information
ianballou authored Jan 22, 2025
1 parent 527df0a commit 775218f
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Katello
module Concerns
module BookmarkControllerValidatorExtensions
extend ActiveSupport::Concern

def valid_controllers_list
@valid_controllers_list ||= (["dashboard", "common_parameters", "/katello/api/v2/host_bootc_images"] +
ActiveRecord::Base.connection.tables.map(&:to_s) +
Permission.resources.map(&:tableize)).uniq
end
end
end
end
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
match '/alternate_content_sources' => 'react#index', :via => [:get]
match '/alternate_content_sources/*page' => 'react#index', :via => [:get]

match '/booted_container_images' => 'react#index', :via => [:get]

Katello::RepositoryTypeManager.generic_ui_content_types(false).each do |type|
get "/#{type.pluralize}", to: redirect("/content/#{type.pluralize}")
get "/#{type.pluralize}/:page", to: redirect("/content/#{type.pluralize}/%{page}")
Expand Down
3 changes: 3 additions & 0 deletions lib/katello/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ class Engine < ::Rails::Engine
::HttpProxy.include Katello::Concerns::HttpProxyExtensions
ForemanTasks::RecurringLogic.include Katello::Concerns::RecurringLogicExtensions

# Validator extensions
::BookmarkControllerValidator.singleton_class.send :prepend, Katello::Concerns::BookmarkControllerValidatorExtensions

#Controller extensions
::HostsController.include Katello::Concerns::HostsControllerExtensions
::SmartProxiesController.include Katello::Concerns::SmartProxiesControllerExtensions
Expand Down
9 changes: 9 additions & 0 deletions lib/katello/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@
:engine => Katello::Engine,
:turbolinks => false

menu :top_menu,
:booted_container_images,
:caption => N_('Booted Container Images'),
:url_hash => {:controller => 'katello/api/v2/host_bootc_images',
:action => 'bootc_images'},
:url => '/booted_container_images',
:engine => Katello::Engine,
:turbolinks => false

divider :top_menu, :caption => N_('Lifecycle'), :parent => :content_menu

menu :top_menu,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/prefer-default-export
export const useForemanSettings = () => ({ perPage: 20 });
export const useForemanVersion = () => 'nightly';
export const useForemanHostsPageUrl = () => '/new/hosts';
5 changes: 5 additions & 0 deletions webpack/containers/Application/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ContentDetails from '../../scenes/Content/Details';
import withHeader from './withHeaders';
import ChangeContentSource from '../../scenes/Hosts/ChangeContentSource';
import AlternateContentSource from '../../scenes/AlternateContentSources';
import BootedContainerImages from '../../scenes/BootedContainerImages';

// eslint-disable-next-line import/prefer-default-export
export const links = [
Expand Down Expand Up @@ -85,4 +86,8 @@ export const links = [
component: WithOrganization(withHeader(AlternateContentSource, { title: __('Alternate Content Sources') })),
exact: false,
},
{
path: 'booted_container_images',
component: WithOrganization(withHeader(BootedContainerImages, { title: __('Booted container images') })),
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { foremanApi } from '../../services/api';

const BOOTED_CONTAINER_IMAGES_KEY = 'BOOTED_CONTAINER_IMAGES';
export const BOOTED_CONTAINER_IMAGES_API_PATH = foremanApi.getApiUrl('/hosts/bootc_images');
export default BOOTED_CONTAINER_IMAGES_KEY;
242 changes: 242 additions & 0 deletions webpack/scenes/BootedContainerImages/BootedContainerImagesPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import React from 'react';
import { TableComposable, Thead, Th, Tbody, Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
import {
useSetParamsAndApiAndSearch,
useTableIndexAPIResponse,
} from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks';
import {
useUrlParams,
useSet,
} from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
import {
getColumnHelpers,
} from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
import {
useTableSort,
} from 'foremanReact/components/PF4/Helpers/useTableSort';
import Pagination from 'foremanReact/components/Pagination';
import EmptyPage from 'foremanReact/routes/common/EmptyPage';
import { translate as __ } from 'foremanReact/common/I18n';
import { useForemanHostsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
import BOOTED_CONTAINER_IMAGES_KEY, { BOOTED_CONTAINER_IMAGES_API_PATH } from './BootedContainerImagesConstants';

const BootedContainerImagesPage = () => {
const foremanHostsPageUrl = useForemanHostsPageUrl();
const columns = {
bootc_booted_image: {
title: __('Image name'),
isSorted: true,
},
digest: {
title: __('Image digests'),
wrapper: ({ digests }) => digests.length,
},
hosts: {
title: __('Hosts'),
wrapper: ({ bootc_booted_image: bootcBootedImage, digests }) => (
<a href={`${foremanHostsPageUrl}?search=bootc_booted_image%20=%20${bootcBootedImage}`}>{digests.reduce((total, digest) => total + digest.host_count, 0)}</a>
),
},
};

const {
searchParam: urlSearchQuery = '',
page: urlPage,
per_page: urlPerPage,
} = useUrlParams();
const defaultParams = { search: urlSearchQuery };
if (urlPage) defaultParams.page = Number(urlPage);
if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
const apiOptions = { key: BOOTED_CONTAINER_IMAGES_KEY };

const response = useTableIndexAPIResponse({
apiUrl: BOOTED_CONTAINER_IMAGES_API_PATH,
apiOptions,
defaultParams,
});
const columnsToSortParams = {};
Object.keys(columns).forEach((key) => {
if (columns[key].isSorted) {
columnsToSortParams[columns[key].title] = key;
}
});
const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
defaultParams,
apiOptions,
setAPIOptions: response.setAPIOptions,
});
const onSort = (_event, index, direction) => {
setParamsAndAPI({
...params,
order: `${Object.keys(columns)[index]} ${direction}`,
});
};
const { pfSortParams } = useTableSort({
allColumns: Object.keys(columns).map(k => columns[k].title),
columnsToSortParams,
onSort,
});
const expandedImages = useSet([]);
const imageIsExpanded = bootcBootedImage => expandedImages.has(bootcBootedImage);
const STATUS = {
PENDING: 'PENDING',
RESOLVED: 'RESOLVED',
ERROR: 'ERROR',
};

const {
response: {
results = [],
subtotal,
message: errorMessage,
},
status = STATUS.PENDING,
} = response;

const [columnNamesKeys, keysToColumnNames] = getColumnHelpers(columns);
const onPagination = (newPagination) => {
setParamsAndAPI({ ...params, ...newPagination });
};
const getColumnWidth = (key) => {
if (key === 'bootc_booted_image') return 40;
if (key === 'digest') return 15;
return 45;
};

return (
<TableIndexPage
apiUrl={BOOTED_CONTAINER_IMAGES_API_PATH}
apiOptions={apiOptions}
header={__('Booted container images')}
createable={false}
isDeleteable={false}
controller="/katello/api/v2/host_bootc_images"
>
<>
<TableComposable variant="compact" ouiaId="booted-containers-table" isStriped>
<Thead>
<Tr ouiaId="table-header">
<>
<Th />
{columnNamesKeys.map(k => (
<Th
width={getColumnWidth(k)}
key={k}
sort={
Object.values(columnsToSortParams).includes(k) &&
pfSortParams(keysToColumnNames[k])
}
>
{keysToColumnNames[k]}
</Th>
))}
</>
</Tr>
</Thead>
<Tbody>
{status === STATUS.PENDING && results.length === 0 && (
<Tr ouiaId="table-loading">
<Td colSpan={100}>
<EmptyPage
message={{
type: 'loading',
text: __('Loading...'),
}}
/>
</Td>
</Tr>
)}
{!(status === STATUS.PENDING) &&
results.length === 0 &&
!errorMessage && (
<Tr ouiaId="table-empty">
<Td colSpan={100}>
<EmptyPage
message={{
type: 'empty',
}}
/>
</Td>
</Tr>
)}
{errorMessage && (
<Tr ouiaId="table-error">
<Td colSpan={100}>
<EmptyPage message={{ type: 'error', text: errorMessage }} />
</Td>
</Tr>
)}
</Tbody>
{results?.map((result, rowIndex) => {
const { bootc_booted_image: bootcBootedImage, digests } = result;
const isExpanded = imageIsExpanded(bootcBootedImage);
return (
<Tbody key={`bootable-container-images-body-${bootcBootedImage}`} isExpanded={isExpanded}>
<Tr key={bootcBootedImage} ouiaId={`table-row-${rowIndex}`}>
<>
<Td
expand={digests.length > 0 && {
rowIndex,
isExpanded,
onToggle: (_event, _rInx, isOpen) =>
expandedImages.onToggle(isOpen, bootcBootedImage),
expandId: `booted-containers-expander-${bootcBootedImage}`,
}}
/>
{columnNamesKeys.map(k => (
<Td
key={`${bootcBootedImage}-${keysToColumnNames[k]}`}
dataLabel={keysToColumnNames[k]}
>
{columns[k].wrapper ? columns[k].wrapper(result) : result[k]}
</Td>
))}
</>
</Tr>
{digests ?
<Tr isExpanded={isExpanded} ouiaId={`table-row-outer-expandable-${rowIndex}`}>
<Td />
<Td colSpan={3}>
<ExpandableRowContent>
<TableComposable variant="compact" isStriped ouiaId={`table-composable-expanded-${rowIndex}`}>
<Thead>
<Tr ouiaId={`table-row-inner-expandable-${rowIndex}`}>
<Th width={55}>{__('Image digest')}</Th>
<Th width={45}>{__('Hosts')}</Th>
</Tr>
</Thead>
<Tbody>
{digests.map((digest, index) => (
<Tr key={digest.bootc_booted_digest} ouiaId={`table-row-expandable-content-${index}`}>
<Td>{digest.bootc_booted_digest}</Td>
<Td>
<a href={`${foremanHostsPageUrl}?search=bootc_booted_digest%20=%20${digest.bootc_booted_digest}`}>{digest.host_count}</a>
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
</ExpandableRowContent>
</Td>
</Tr> : null}
</Tbody>
);
})}
</TableComposable>
{results.length > 0 && !errorMessage &&
<Pagination
key="table-bottom-pagination"
page={params.page}
perPage={params.perPage}
itemCount={subtotal}
onChange={onPagination}
updateParamsByUrl
/>
}
</>
</TableIndexPage>
);
};

export default BootedContainerImagesPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Immutable from 'seamless-immutable';

const bootedContainerImagesResponse = Immutable({
total: 2,
page: 1,
per_page: 20,
subtotal: 2,
results: [
{
bootc_booted_image: 'quay.io/centos-bootc/centos-bootc:stream10',
digests: [
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd572',
host_count: 1,
},
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd573',
host_count: 2,
},
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd574',
host_count: 3,
},
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd575',
host_count: 4,
},
],
},
{
bootc_booted_image: 'quay.io/centos-bootc/centos-bootc:stream9',
digests: [
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd576',
host_count: 6,
},
],
},
],
});

export default bootedContainerImagesResponse;
Loading

0 comments on commit 775218f

Please sign in to comment.