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 #38107 - Booted container images page #11269

Merged
merged 9 commits into from
Jan 22, 2025
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
jeremylenz marked this conversation as resolved.
Show resolved Hide resolved
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'),
Copy link
Member

Choose a reason for hiding this comment

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

Can we change this to Bootc - booted image name so it's also easier to figure out the serach query?

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed all of the references to image_name to bootc_booted_image like in the API.

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([]);
jeremylenz marked this conversation as resolved.
Show resolved Hide resolved
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
jeremylenz marked this conversation as resolved.
Show resolved Hide resolved
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
Loading