diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js b/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js
new file mode 100644
index 00000000000..8fff3593e6b
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/RowSelectTd.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Td } from '@patternfly/react-table';
+
+export const RowSelectTd = ({ rowData, selectOne, isSelected }) => (
+
{
+ selectOne(isSelecting, rowData.id, rowData);
+ },
+ isSelected: isSelected(rowData.id),
+ disable: false,
+ }}
+ />
+);
+
+RowSelectTd.propTypes = {
+ rowData: PropTypes.object.isRequired,
+ selectOne: PropTypes.func.isRequired,
+ isSelected: PropTypes.func.isRequired,
+};
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js
index dbf072568af..e36441ff417 100644
--- a/webpack/assets/javascripts/react_app/components/HostsIndex/index.js
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/index.js
@@ -1,8 +1,7 @@
-import React, { createContext } from 'react';
+import React, { createContext, useState } from 'react';
import { useHistory, Link } from 'react-router-dom';
-import PropTypes from 'prop-types';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
-import { Td } from '@patternfly/react-table';
+import { Tr, Td, ActionsColumn } from '@patternfly/react-table';
import {
ToolbarItem,
Dropdown,
@@ -15,15 +14,18 @@ import {
SplitItem,
} from '@patternfly/react-core';
import { UndoIcon } from '@patternfly/react-icons';
+import { Table } from '../PF4/TableIndexPage/Table/Table';
import { translate as __ } from '../../common/I18n';
import TableIndexPage from '../PF4/TableIndexPage/TableIndexPage';
import { ActionKebab } from './ActionKebab';
import { HOSTS_API_PATH, API_REQUEST_KEY } from '../../routes/Hosts/constants';
import { selectKebabItems } from './Selectors';
-import { useAPI } from '../../common/hooks/API/APIHooks';
import { useBulkSelect } from '../PF4/TableIndexPage/Table/TableHooks';
import SelectAllCheckbox from '../PF4/TableIndexPage/Table/SelectAllCheckbox';
-import { getPageStats } from '../PF4/TableIndexPage/Table/helpers';
+import {
+ getColumnHelpers,
+ getPageStats,
+} from '../PF4/TableIndexPage/Table/helpers';
import { deleteHost } from '../HostDetails/ActionsBar/actions';
import { useForemanSettings } from '../../Root/Context/ForemanContext';
import { getURIsearch } from '../../common/urlHelpers';
@@ -32,6 +34,12 @@ import { foremanUrl } from '../../common/helpers';
import Slot from '../common/Slot';
import forceSingleton from '../../common/forceSingleton';
import './index.scss';
+import { STATUS } from '../../constants';
+import { RowSelectTd } from './RowSelectTd';
+import {
+ useSetParamsAndApiAndSearch,
+ useTableIndexAPIResponse,
+} from '../PF4/TableIndexPage/Table/TableIndexHooks';
export const ForemanHostsIndexActionsBarContext = forceSingleton(
'ForemanHostsIndexActionsBarContext',
@@ -46,34 +54,42 @@ const HostsIndex = () => {
isSorted: true,
},
};
-
+ const [columnNamesKeys, keysToColumnNames] = getColumnHelpers(columns);
const history = useHistory();
const { location: { search: historySearch } = {} } = history || {};
const urlParams = new URLSearchParams(historySearch);
const urlParamsSearch = urlParams.get('search') || '';
const searchFromUrl = urlParamsSearch || getURIsearch();
const initialSearchQuery = apiSearchQuery || searchFromUrl || '';
-
const defaultParams = { search: initialSearchQuery };
+ const apiOptions = { key: API_REQUEST_KEY };
+ const response = useTableIndexAPIResponse({
+ apiUrl: HOSTS_API_PATH,
+ apiOptions,
+ defaultParams,
+ });
- const response = useAPI('get', `${HOSTS_API_PATH}?include_permissions=true`, {
- key: API_REQUEST_KEY,
- params: defaultParams,
+ const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
+ defaultParams,
+ apiOptions,
+ setAPIOptions: response.setAPIOptions,
});
const {
response: {
search: apiSearchQuery,
results,
- subtotal,
total,
per_page: perPage,
page,
+ subtotal,
+ message: errorMessage,
},
+ status = STATUS.PENDING,
+ setAPIOptions,
} = response;
const { pageRowCount } = getPageStats({ total, page, perPage });
-
const {
fetchBulkParams,
updateSearchQuery,
@@ -112,22 +128,6 @@ const HostsIndex = () => {
);
- const RowSelectTd = ({ rowData }) => (
- | {
- selectOne(isSelecting, rowData.id, rowData);
- },
- isSelected: isSelected(rowData.id),
- disable: false,
- }}
- />
- );
-
- RowSelectTd.propTypes = {
- rowData: PropTypes.object.isRequired,
- };
const dispatch = useDispatch();
const { destroyVmOnHostDelete } = useForemanSettings();
const deleteHostHandler = ({ hostName, computeId }) =>
@@ -153,8 +153,9 @@ const HostsIndex = () => {
{__('Delete')}
,
];
+
const registeredItems = useSelector(selectKebabItems, shallowEqual);
- const customToolbarItems = (
+ const pluginToolbarItems = (
@@ -166,7 +167,7 @@ const HostsIndex = () => {
id,
name: hostName,
compute_id: computeId,
- canDelete,
+ can_delete: canDelete,
}) => [
{
title: __('Delete'),
@@ -175,7 +176,7 @@ const HostsIndex = () => {
},
];
- const [legacyUIKebabOpen, setLegacyUIKebabOpen] = React.useState(false);
+ const [legacyUIKebabOpen, setLegacyUIKebabOpen] = useState(false);
const legacyUIKebab = (
{
return (
+ >
+
+ setAPIOptions({
+ ...apiOptions,
+ params: { search: searchFromUrl },
+ })
+ }
+ columns={columns}
+ errorMessage={
+ status === STATUS.ERROR && errorMessage ? errorMessage : null
+ }
+ isPending={status === STATUS.PENDING}
+ >
+ {results?.map((result, rowIndex) => {
+ const rowActions = rowKebabItems(result);
+ return (
+
+ {}
+ {columnNamesKeys.map(k => (
+
+ {columns[k].wrapper ? columns[k].wrapper(result) : result[k]}
+ |
+ ))}
+
+ {rowActions.length ? (
+
+ ) : null}
+ |
+
+ );
+ })}
+
+
);
};
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js
index 69bb76da6a6..4954550162c 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js
@@ -15,6 +15,7 @@ import { useTableSort } from '../../Helpers/useTableSort';
import Pagination from '../../../Pagination';
import { DeleteModal } from './DeleteModal';
import EmptyPage from '../../../../routes/common/EmptyPage';
+import { getColumnHelpers } from './helpers';
export const Table = ({
columns,
@@ -31,6 +32,7 @@ export const Table = ({
isEmbedded,
showCheckboxes,
rowSelectTd,
+ children,
}) => {
const columnsToSortParams = {};
Object.keys(columns).forEach(key => {
@@ -38,10 +40,7 @@ export const Table = ({
columnsToSortParams[columns[key].title] = key;
}
});
- const columnNames = {};
- Object.keys(columns).forEach(key => {
- columnNames[key] = columns[key].title;
- });
+ const [columnNamesKeys, keysToColumnNames] = getColumnHelpers(columns);
const onSort = (_event, index, direction) => {
setParams({
...params,
@@ -71,7 +70,6 @@ export const Table = ({
},
...((getActions && getActions({ id, name, canDelete, ...item })) ?? []),
].filter(Boolean);
- const columnNamesKeys = Object.keys(columns);
const RowSelectTd = rowSelectTd;
return (
<>
@@ -91,10 +89,10 @@ export const Table = ({
key={k}
sort={
Object.values(columnsToSortParams).includes(k) &&
- pfSortParams(columnNames[k])
+ pfSortParams(keysToColumnNames[k])
}
>
- {columnNames[k]}
+ {keysToColumnNames[k]}
))}
@@ -126,26 +124,27 @@ export const Table = ({
|
)}
- {results.map((result, rowIndex) => {
- const rowActions = actions(result);
- return (
-
- {showCheckboxes && }
- {columnNamesKeys.map(k => (
-
- {columns[k].wrapper
- ? columns[k].wrapper(result)
- : result[k]}
+ {children ||
+ results.map((result, rowIndex) => {
+ const rowActions = actions(result);
+ return (
+ |
+ {showCheckboxes && }
+ {columnNamesKeys.map(k => (
+
+ {columns[k].wrapper
+ ? columns[k].wrapper(result)
+ : result[k]}
+ |
+ ))}
+
+ {rowActions.length ? (
+
+ ) : null}
|
- ))}
-
- {rowActions.length ? (
-
- ) : null}
- |
-
- );
- })}
+
+ );
+ })}
{results.length > 0 && !errorMessage && (
@@ -162,6 +161,7 @@ export const Table = ({
};
Table.propTypes = {
+ children: PropTypes.node,
columns: PropTypes.object.isRequired,
params: PropTypes.shape({
page: PropTypes.number,
@@ -183,6 +183,7 @@ Table.propTypes = {
};
Table.defaultProps = {
+ children: null,
errorMessage: null,
isDeleteable: false,
itemCount: 0,
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js
new file mode 100644
index 00000000000..dbab54c1c66
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/TableIndexHooks.js
@@ -0,0 +1,79 @@
+import { useState } from 'react';
+import URI from 'urijs';
+import { useHistory } from 'react-router-dom';
+import { useAPI } from '../../../../common/hooks/API/APIHooks';
+
+/**
+
+A hook that encapsulates the logic for fetching the API response for TableIndexPage and HostsIndexPage
+@param {Object}{replacementResponse} - If included, skip the API request and use this response instead
+@param {string}{apiUrl} - url for the API to make requests to
+@param {Object}{apiOptions} - options object. Should include { key: HOSTS_API_KEY }; see APIRequest.js for more details
+@param {Object}{defaultParams} - 'params' object to send to useAPI
+@return {Object} - returns the API response
+
+*/
+
+export const useTableIndexAPIResponse = ({
+ replacementResponse,
+ apiUrl,
+ apiOptions = {},
+ defaultParams = {},
+}) => {
+ let response = useAPI(
+ replacementResponse ? null : 'get',
+ apiUrl.includes('include_permissions')
+ ? apiUrl
+ : `${apiUrl}?include_permissions=true`,
+ {
+ ...apiOptions,
+ params: defaultParams,
+ }
+ );
+
+ if (replacementResponse) {
+ response = replacementResponse;
+ }
+
+ return response;
+};
+
+/**
+A hook that stores the 'params' state and returns the setParamsAndAPI and setSearch functions for TableIndexPage and HostsIndexPage
+@param {Object}{defaultParams} - initial state value for params
+@param {Object}{apiOptions} - options object. Should include { key: HOSTS_API_KEY }; see APIRequest.js for more details
+@param {Function}{setAPIOptions} - Pass in the setAPIOptions function returned from useAPI.
+@param {Function}{updateSearchQuery} - Pass in the updateSearchQuery function returned from useBulkSelect.
+@return {Object} - returns the setParamsAndAPI and setSearch functions, and current params
+*/
+export const useSetParamsAndApiAndSearch = ({
+ defaultParams,
+ apiOptions,
+ setAPIOptions,
+ updateSearchQuery,
+}) => {
+ const [params, setParams] = useState(defaultParams);
+ const history = useHistory();
+ const setParamsAndAPI = newParams => {
+ // add url edit params to the new params
+ const uri = new URI();
+ uri.setSearch(newParams);
+ history.push({ search: uri.search() });
+ setParams(newParams);
+ setAPIOptions({ ...apiOptions, params: newParams });
+ };
+
+ const setSearch = newSearch => {
+ const uri = new URI();
+ uri.setSearch(newSearch);
+ updateSearchQuery(newSearch.search);
+ history.push({ search: uri.search() });
+ setParamsAndAPI({ ...params, ...newSearch });
+ };
+
+ return {
+ setParamsAndAPI,
+ setSearch,
+ params,
+ };
+};
diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/helpers.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/helpers.js
index 84d6a6a9cb1..8d7ef805e7a 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/helpers.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/helpers.js
@@ -17,3 +17,17 @@ export const getPageStats = ({ total, page, perPage }) => {
lastPage,
};
};
+
+/**
+ * Assembles column data into various forms needed
+ * @param {Object} columns - Object with column sort params as keys and column objects as values. Column objects must have a title key
+ * @returns {Array} - an array of column sort params and a map of keys to column names
+ */
+export const getColumnHelpers = columns => {
+ const columnNamesKeys = Object.keys(columns);
+ const keysToColumnNames = {};
+ columnNamesKeys.forEach(key => {
+ keysToColumnNames[key] = columns[key].title;
+ });
+ return [columnNamesKeys, keysToColumnNames];
+};
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 a8b6e02a8b9..34df0423b76 100644
--- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
+++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js
@@ -1,9 +1,8 @@
/* eslint-disable max-lines */
-import React, { useState, useMemo } from 'react';
+import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { QuestionCircleIcon } from '@patternfly/react-icons';
import { useHistory } from 'react-router-dom';
-import URI from 'urijs';
import {
Spinner,
Toolbar,
@@ -23,7 +22,6 @@ import {
helpURL,
getURIsearch,
} from '../../../common/urlHelpers';
-import { useAPI } from '../../../common/hooks/API/APIHooks';
import { translate as __ } from '../../../common/I18n';
import { noop } from '../../../common/helpers';
import Pagination from '../../Pagination';
@@ -34,14 +32,19 @@ import Head from '../../Head';
import { ActionButtons } from './ActionButtons';
import './TableIndexPage.scss';
import { Table } from './Table/Table';
+import {
+ useSetParamsAndApiAndSearch,
+ useTableIndexAPIResponse,
+} from './Table/TableIndexHooks';
/**
-A page component that displays a table with data fetched from an API. It provides search and filtering functionality, and the ability to create new entries and export data.
-@param {Object}{apiOptions} - options object for API requests
+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.
+@param {Object}{apiOptions} - options object for API requests. See APIRequest.js for more details
@param {string}{apiUrl} - url for the API to make requests to
@param {React.Component} {beforeToolbarComponent} - a component to be rendered before the toolbar
@param {Object} {breadcrumbOptions} - props to send to the breadcrumb bar
-@param {Object}{columns} - an object of objects representing the columns to be displayed in the table, keys should be the same as in the api response
+@param {React.ReactNode} {children} - optional children to be rendered inside the page instead of the table
+@param {Object}{columns} - Not needed when passing children. An object of objects representing the columns to be displayed in the table, keys should be the same as in the api response
@param {string} columns[].title - the title of the column, translated
@param {function} columns[].wrapper - a function that returns a React component to be rendered in the column
@param {boolean} columns[].isSorted - whether or not the column is sorted
@@ -59,7 +62,12 @@ A page component that displays a table with data fetched from an API. It provide
@param {string}{header} - header node; default is {headerText}
@param {boolean} {isDeleteable} - whether or not entries can be deleted
@param {boolean} {searchable} - whether or not the table can be searched
-@param {React.ReactNode} {children} - optional children to be rendered inside the page instead of the table
+@param {React.ReactNode} {selectionToolbar} - Pass in the SelectAll toolbar, if desired
+@param {Object} {replacementResponse} - If included, skip the API request and use this response instead
+@param {boolean} {showCheckboxes} - Not needed when passing children. Whether or not to show selection checkboxes in the first column.
+@param {function} {rowSelectTd} - Not needed when passing children. A function that takes a single result object and returns a React component to be rendered in the first column.
+@param {function} {rowKebabItems} - Not needed when passing children. A function that takes a single result object and returns an array of kebab items to be displayed in the last column
+@param {function} {updateSearchQuery} - Pass in the updateSearchQuery function returned from useBulkSelect.
*/
const TableIndexPage = ({
@@ -67,6 +75,7 @@ const TableIndexPage = ({
apiUrl,
beforeToolbarComponent,
breadcrumbOptions,
+ children,
columns,
controller,
creatable,
@@ -82,7 +91,6 @@ const TableIndexPage = ({
header,
isDeleteable,
searchable,
- children,
selectionToolbar,
replacementResponse,
showCheckboxes,
@@ -104,21 +112,12 @@ const TableIndexPage = ({
if (urlPerPage) {
defaultParams.per_page = parseInt(urlPerPage, 10);
}
- const [params, setParams] = useState(defaultParams);
- let response = useAPI(
- replacementResponse ? null : 'get',
- apiUrl.includes('include_permissions')
- ? apiUrl
- : `${apiUrl}?include_permissions=true`,
- {
- ...apiOptions,
- params: defaultParams,
- }
- );
-
- if (replacementResponse) {
- response = replacementResponse;
- }
+ const response = useTableIndexAPIResponse({
+ replacementResponse,
+ apiUrl,
+ apiOptions,
+ defaultParams,
+ });
const {
response: {
@@ -145,22 +144,13 @@ const TableIndexPage = ({
);
const searchProps = customSearchProps || memoDefaultSearchProps;
searchProps.autocomplete.searchQuery = search;
- const setParamsAndAPI = newParams => {
- // add url edit params to the new params
- const uri = new URI();
- uri.setSearch(newParams);
- history.push({ search: uri.search() });
- setParams(newParams);
- setAPIOptions({ ...apiOptions, params: newParams });
- };
- const setSearch = newSearch => {
- const uri = new URI();
- uri.setSearch(newSearch);
- updateSearchQuery(newSearch.search);
- history.push({ search: uri.search() });
- setParamsAndAPI({ ...params, ...newSearch });
- };
+ const { setParamsAndAPI, setSearch, params } = useSetParamsAndApiAndSearch({
+ defaultParams,
+ apiOptions,
+ setAPIOptions,
+ updateSearchQuery,
+ });
const onSearch = newSearch => {
if (newSearch !== apiSearchQuery) {