diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
index 85482b432e..d9dedd85ac 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
@@ -9,10 +9,6 @@ import {
ToolbarContent,
ToolbarGroup,
ToolbarItem,
- PageSection,
- PageSectionVariants,
- TextContent,
- Text,
Icon,
PaginationVariant,
} from '@patternfly/react-core';
@@ -27,9 +23,8 @@ import { translate as __ } from '../../../common/I18n';
import { noop } from '../../../common/helpers';
import Pagination from '../../Pagination';
import { getControllerSearchProps, STATUS } from '../../../constants';
-import BreadcrumbBar from '../../BreadcrumbBar';
+import PageLayout from '../../../routes/common/PageLayout/PageLayout';
import SearchBar from '../../SearchBar';
-import Head from '../../Head';
import { ActionButtons } from './ActionButtons';
import './TableIndexPage.scss';
import { Table } from './Table/Table';
@@ -37,6 +32,7 @@ import {
useSetParamsAndApiAndSearch,
useTableIndexAPIResponse,
} from './Table/TableIndexHooks';
+
/**
A page component that displays a table with data fetched from the API. It provides search and filtering functionality, and the ability to create new entries and export data.
@@ -62,7 +58,6 @@ A page component that displays a table with data fetched from the API. It provid
@param {boolean} {exportable} - whether or not to show export button
@param {boolean} {hasHelpPage} - whether or not to show documentation button
@param {React.ReactNode}{customHeader} - a custom header to be rendered instead of the default header
-@param {string}{headerText} - DEPRECATED - the header text for the page
@param {string}{header} -the header text for the page and the title
@param {boolean} {isDeleteable} - whether or not entries can be deleted
@param {boolean} {searchable} - whether or not the table can be searched
@@ -101,7 +96,6 @@ const TableIndexPage = ({
exportable,
hasHelpPage,
customHeader,
- headerText,
header,
isDeleteable,
searchable,
@@ -224,89 +218,66 @@ const TableIndexPage = ({
...customActionButtons,
].filter(item => item);
- header = headerText || header;
- return (
-
-
-
{header}
-
- {breadcrumbOptions && (
-
-
-
- )}
-
- {customHeader || (
-
-
- {header}
-
-
- )}
-
- {beforeToolbarComponent}
-
-
-
- {searchable && (
-
- {selectionToolbar}
-
-
-
- {status === STATUS.PENDING && (
-
-
-
- )}
-
- )}
- {(customToolbarItems || actionButtons.length > 0) && (
-
- {actionButtons.length > 0 && (
-
-
-
- )}
- {customToolbarItems && customToolbarItems}
-
- )}
- {total > 0 && (
-
+
+ {searchable && (
+
+ {selectionToolbar}
+
+
+
+ {status === STATUS.PENDING && (
+
+
+
+ )}
+
+ )}
+ {(customToolbarItems || actionButtons.length > 0) && (
+
+ {actionButtons.length > 0 && (
+
+
+
)}
-
-
-
-
+ )}
+ {total > 0 && (
+
+ )}
+
+
+ );
+
+ return (
+
);
};
@@ -399,7 +370,6 @@ TableIndexPage.propTypes = {
replacementResponse: PropTypes.object,
exportable: PropTypes.bool,
hasHelpPage: PropTypes.bool,
- headerText: PropTypes.string,
header: PropTypes.string,
customHeader: PropTypes.node,
isDeleteable: PropTypes.bool,
@@ -442,7 +412,6 @@ TableIndexPage.defaultProps = {
exportable: false,
hasHelpPage: false,
header: '',
- headerText: '',
customHeader: undefined,
isDeleteable: false,
searchable: true,
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss
index cfc0317984..fd7cefb6b3 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss
@@ -1,10 +1,8 @@
-#foreman-page {
- .table-title-section {
- padding-bottom: 0;
- }
- .table-toolbar-section {
- padding-top: 16px;
- padding-bottom: 16px;
+.table-index-page {
+ .page-toolbar-section {
+ padding-top: var(--pf-v5-global--spacer--md);
+ padding-bottom: var(--pf-v5-global--spacer--md);
+
.table-toolbar {
padding: 0;
.pf-v5-c-toolbar__content {
@@ -15,7 +13,7 @@
display: block;
}
.table-toolbar-actions {
- padding-left: 16px;
+ padding-left: var(--pf-v5-global--spacer--md);
}
}
.pf-v5-c-toolbar__group {
@@ -25,7 +23,7 @@
}
}
}
- .table-section {
+ .page-content-section {
padding-top: 0;
padding-left: 0;
padding-right: 0;
diff --git a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap
index df284245f6..a5432cbc40 100644
--- a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap
+++ b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPage.test.js.snap
@@ -5,6 +5,8 @@ exports[`AuditsPage rendering render audits page 1`] = `
beforeToolbarComponent={null}
breadcrumbOptions={null}
customBreadcrumbs={null}
+ customHeader={null}
+ customToolbar={null}
header="Audits"
isLoading={false}
onSearch={[Function]}
@@ -79,6 +81,8 @@ exports[`AuditsPage rendering render audits page w/empty audits 1`] = `
beforeToolbarComponent={null}
breadcrumbOptions={null}
customBreadcrumbs={null}
+ customHeader={null}
+ customToolbar={null}
header="Audits"
isLoading={false}
onSearch={[Function]}
@@ -155,6 +159,8 @@ exports[`AuditsPage rendering render audits page w/error 1`] = `
beforeToolbarComponent={null}
breadcrumbOptions={null}
customBreadcrumbs={null}
+ customHeader={null}
+ customToolbar={null}
header="Audits"
isLoading={false}
onSearch={[Function]}
@@ -231,6 +237,8 @@ exports[`AuditsPage rendering render loading audits page 1`] = `
beforeToolbarComponent={null}
breadcrumbOptions={null}
customBreadcrumbs={null}
+ customHeader={null}
+ customToolbar={null}
header="Audits"
isLoading={false}
onSearch={[Function]}
diff --git a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js
index 96f7046c3b..44f7bc8d34 100644
--- a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js
+++ b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.js
@@ -24,13 +24,15 @@ const PageLayout = ({
customBreadcrumbs,
breadcrumbOptions,
toolbarButtons,
+ customToolbar,
header,
+ customHeader,
beforeToolbarComponent,
isLoading,
pageSectionType,
children,
}) => {
- const title = (
+ const titleSectionBody = customHeader ?? (
{header}
@@ -38,6 +40,14 @@ const PageLayout = ({
);
+ const showStandaloneTitleSection = searchable || !toolbarButtons;
+
+ const toolbarSectionShowsDefaultToolbar =
+ !customToolbar && (searchable || isLoading || toolbarButtons);
+
+ const showToolbarSection =
+ toolbarSectionShowsDefaultToolbar || Boolean(customToolbar);
+
return (
<>
@@ -53,51 +63,55 @@ const PageLayout = ({
)}
- {(searchable || !toolbarButtons) && (
+ {showStandaloneTitleSection && (
- {title}
+ {titleSectionBody}
)}
- {(searchable ||
- beforeToolbarComponent ||
- isLoading ||
- toolbarButtons) && (
+ {beforeToolbarComponent}
+
+ {showToolbarSection && (
- {beforeToolbarComponent}
-
-
-
- {!searchable && toolbarButtons && title}
- {searchable && (
-
- )}
-
- {isLoading && (
-
-
+ {customToolbar || (
+
+
+
+ {!searchable && toolbarButtons && titleSectionBody}
+ {searchable && (
+
+ )}
- )}
-
- {toolbarButtons}
-
-
-
+ {isLoading && (
+
+
+
+ )}
+
+ {toolbarButtons}
+
+
+
+ )}
)}
-
+
{children}
>
@@ -107,7 +121,6 @@ const PageLayout = ({
PageLayout.propTypes = {
children: PropTypes.node.isRequired,
searchable: PropTypes.bool.isRequired,
- header: PropTypes.string,
searchProps: PropTypes.shape({
autocomplete: PropTypes.shape({
results: PropTypes.array,
@@ -147,22 +160,28 @@ PageLayout.propTypes = {
),
}),
toolbarButtons: PropTypes.node,
+ customToolbar: PropTypes.node,
+ header: PropTypes.string,
+ customHeader: PropTypes.node,
onSearch: PropTypes.func,
searchQuery: PropTypes.string,
beforeToolbarComponent: PropTypes.node,
isLoading: PropTypes.bool,
pageSectionType: PropTypes.string,
+ className: PropTypes.string,
};
PageLayout.defaultProps = {
searchProps: {},
header: '',
+ customHeader: null,
searchQuery: '',
customBreadcrumbs: null,
toolbarButtons: null,
+ customToolbar: null,
breadcrumbOptions: null,
isLoading: false,
- onSearch: searchQuery => changeQuery({ search: searchQuery.trim(), page: 1 }),
+ onSearch: newSearch => changeQuery({ search: newSearch.trim(), page: 1 }),
beforeToolbarComponent: null,
pageSectionType: 'default',
};
diff --git a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js
index ca99fc63c1..b855d696df 100644
--- a/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js
+++ b/webpack/assets/javascripts/react_app/routes/common/PageLayout/PageLayout.test.js
@@ -4,6 +4,7 @@ import { screen } from '@testing-library/react';
import PageLayout from './PageLayout';
import '@testing-library/jest-dom';
+import { breadcrumbBar } from '../../../components/BreadcrumbBar/BreadcrumbBar.fixtures';
import { pageLayoutMock } from './PageLayout.fixtures';
import { rtlHelpers } from '../../../common/rtlTestHelpers';
@@ -11,7 +12,7 @@ const { renderWithStore } = rtlHelpers;
jest.unmock('react-helmet');
describe('PageLayout', () => {
- it('should render header text', () => {
+ it('renders string header as title when searchable is false (no SearchBar)', () => {
const header = 'My Header';
const { getByText } = renderWithStore(
@@ -30,7 +31,35 @@ describe('PageLayout', () => {
expect(screen.queryAllByLabelText('Search')).toHaveLength(0);
});
- it('should have Search', () => {
+ it('renders customHeader node inside title breadcrumb region', () => {
+ function CompositeHeader() {
+ return (
+
+ Scoped Hosts view
+
+ );
+ }
+
+ renderWithStore(
+
+ }
+ searchable={false}
+ >
+ Content
+
+
+ );
+
+ const composite = screen.getByTestId('composite-header');
+ expect(composite).toBeInTheDocument();
+ expect(composite.closest('#page-title')).toBeInTheDocument();
+ expect(screen.getByText('Hosts')).toBeInTheDocument();
+ });
+
+ it('renders SearchBar in toolbar when searchable is true', () => {
const onSearchMock = jest.fn();
const { getByLabelText } = renderWithStore(
@@ -47,7 +76,7 @@ describe('PageLayout', () => {
expect(getByLabelText('Search')).toBeInTheDocument();
});
- it('should render custom breadcrumbs', () => {
+ it('renders customBreadcrumbs when provided', () => {
const customBreadcrumbs = test Breadcrumbs
;
const { getByText } = renderWithStore(
@@ -64,7 +93,45 @@ describe('PageLayout', () => {
expect(breadcrumbsElement).toBeInTheDocument();
});
- it('should render toolbar buttons', () => {
+ it('renders BreadcrumbBar from breadcrumbOptions', () => {
+ renderWithStore(
+
+
+ Content
+
+
+ );
+ expect(screen.getByText('root')).toBeInTheDocument();
+ });
+
+ it('renders beforeToolbarComponent between title and toolbar sections', () => {
+ renderWithStore(
+
+ TB}
+ beforeToolbarComponent={
+ Before toolbar
+ }
+ >
+ Content
+
+
+ );
+ expect(screen.getByTestId('before-toolbar')).toHaveTextContent(
+ 'Before toolbar'
+ );
+ expect(screen.getByText('TB')).toBeInTheDocument();
+ });
+
+ it('renders toolbarButtons in toolbar', () => {
const toolbarButtons = test Button ;
const { getByText } = renderWithStore(
@@ -81,7 +148,7 @@ describe('PageLayout', () => {
expect(buttonElement).toBeInTheDocument();
});
- it('should render content', () => {
+ it('renders children in main content PageSection', () => {
const { getByText } = renderWithStore(
@@ -93,7 +160,7 @@ describe('PageLayout', () => {
expect(contentElement).toBeInTheDocument();
});
- it('should show spinner when isLoading is true', () => {
+ it('shows toolbar spinner when isLoading is true', () => {
const { container } = renderWithStore(
@@ -104,7 +171,7 @@ describe('PageLayout', () => {
expect(container.querySelector('#toolbar-spinner')).toBeInTheDocument();
});
- it('should not show spinner when isLoading is false', () => {
+ it('does not render toolbar spinner when isLoading is false', () => {
const { container } = renderWithStore(
@@ -114,4 +181,24 @@ describe('PageLayout', () => {
);
expect(container.querySelector('#toolbar-spinner')).toBeNull();
});
+
+ it('renders customToolbar instead of built-in toolbar (skips SearchBar)', () => {
+ renderWithStore(
+
+ Custom }
+ header="Index"
+ >
+ Content
+
+
+ );
+ expect(screen.getByTestId('custom-toolbar-slot')).toHaveTextContent(
+ 'Custom'
+ );
+ expect(screen.queryByLabelText('Search input')).not.toBeInTheDocument();
+ });
});