From 93ac77a35eba41e4dc2b6287731084c2dd5e8eca Mon Sep 17 00:00:00 2001 From: MariaAga Date: Mon, 18 Sep 2023 17:47:49 +0200 Subject: [PATCH] Fixes #32748 - add pf4 table template --- .../components/ModelsTable/ModelsTable.js | 42 --- .../ModelsTable/ModelsTable.test.js | 30 --- .../ModelsTable/ModelsTableSchema.js | 57 ---- .../__snapshots__/ModelsTable.test.js.snap | 129 --------- .../react_app/components/ModelsTable/index.js | 24 -- .../components/PF4/Bookmarks/Bookmarks.js | 1 + .../components/PF4/Helpers/useTableSort.js | 18 +- .../PF4/TableIndexPage/Table/DeleteModal.js | 78 ++++++ .../PF4/TableIndexPage/Table/Table.js | 159 +++++++++++ .../PF4/TableIndexPage/Table/Table.test.js | 184 +++++++++++++ .../PF4/TableIndexPage/TableIndexPage.js | 183 +++++++++---- .../PF4/TableIndexPage/TableIndexPage.scss | 16 +- .../PF4/TableIndexPage/TableIndexPage.test.js | 2 - .../components/SearchBar/SearchBar.scss | 3 + .../common/table/schemaHelpers/column.js | 2 +- .../table/schemaHelpers/sortableColumn.js | 2 +- .../react_app/components/componentRegistry.js | 2 - .../react_app/redux/reducers/index.js | 2 - .../react_app/routes/FiltersForm/constants.js | 13 - .../routes/Models/ModelsPage/ModelsPage.js | 53 ---- .../Models/ModelsPage/ModelsPageActions.js | 45 ---- .../Models/ModelsPage/ModelsPageHelpers.js | 29 --- .../Models/ModelsPage/ModelsPageSelectors.js | 62 ----- .../__tests__/ModelsPage.fixtures.js | 75 ------ .../ModelsPage/__tests__/ModelsPage.test.js | 49 ---- .../__tests__/ModelsPageHelpers.test.js | 32 --- .../__tests__/ModelsPageSelectors.test.js | 42 --- .../__snapshots__/ModelsPage.test.js.snap | 246 ------------------ .../ModelsPageSelectors.test.js.snap | 50 ---- .../ModelsPage/components/ModelDeleteModal.js | 44 ---- .../components/ModelDeleteModal.test.js | 16 -- .../components/ModelsPageContent.js | 40 --- .../ModelDeleteModal.test.js.snap | 23 -- .../routes/Models/ModelsPage/index.js | 69 ++--- .../react_app/routes/Models/constants.js | 9 +- .../routes/common/EmptyPage/index.js | 4 +- 36 files changed, 628 insertions(+), 1207 deletions(-) delete mode 100644 webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.js delete mode 100644 webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.test.js delete mode 100644 webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTableSchema.js delete mode 100644 webpack/assets/javascripts/react_app/components/ModelsTable/__snapshots__/ModelsTable.test.js.snap delete mode 100644 webpack/assets/javascripts/react_app/components/ModelsTable/index.js create mode 100644 webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/DeleteModal.js create mode 100644 webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js create mode 100644 webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.test.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPage.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageActions.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageHelpers.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageSelectors.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.fixtures.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.test.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageHelpers.test.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageSelectors.test.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPage.test.js.snap delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPageSelectors.test.js.snap delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.test.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelsPageContent.js delete mode 100644 webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/__snapshots__/ModelDeleteModal.test.js.snap diff --git a/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.js b/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.js deleted file mode 100644 index b5fc5e228956..000000000000 --- a/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Table } from '../common/table'; -import createModelsTableSchema from './ModelsTableSchema'; - -const ModelsTable = ({ - getTableItems, - sortBy, - sortOrder, - results, - onDeleteClick, - id, -}) => ( - -); - -ModelsTable.propTypes = { - results: PropTypes.array.isRequired, - getTableItems: PropTypes.func.isRequired, - onDeleteClick: PropTypes.func.isRequired, - sortBy: PropTypes.string, - sortOrder: PropTypes.string, - id: PropTypes.string, -}; - -ModelsTable.defaultProps = { - sortBy: '', - sortOrder: '', - id: undefined, -}; - -export default ModelsTable; diff --git a/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.test.js b/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.test.js deleted file mode 100644 index 27b92d1bea62..000000000000 --- a/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTable.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; -import ModelsTable from './ModelsTable'; - -const results = [ - { - info: null, - created_at: '2018-03-26 09:54:21 +0300', - updated_at: '2018-03-26 09:54:21 +0300', - vendor_class: null, - hardware_model: null, - id: 29, - name: 'X8SIL', - can_edit: true, - can_delete: true, - hosts_count: 1, - }, -]; - -const fixtures = { - 'should render ModelsTable': { - getTableItems: () => {}, - onDeleteClick: () => {}, - results, - }, -}; - -describe('ModelsTable', () => { - describe('rendering', () => - testComponentSnapshotsWithFixtures(ModelsTable, fixtures)); -}); diff --git a/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTableSchema.js b/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTableSchema.js deleted file mode 100644 index 7545de5e8353..000000000000 --- a/webpack/assets/javascripts/react_app/components/ModelsTable/ModelsTableSchema.js +++ /dev/null @@ -1,57 +0,0 @@ -import { translate as __ } from '../../common/I18n'; -import { - column, - sortableColumn, - headerFormatterWithProps, - cellFormatterWithProps, - nameCellFormatter, - hostsCountCellFormatter, - deleteActionCellFormatter, - cellFormatter, -} from '../common/table'; - -const sortControllerFactory = (apiCall, sortBy, sortOrder) => ({ - apply: (by, order) => { - apiCall({ sort: { by, order } }); - }, - property: sortBy, - order: sortOrder, -}); - -/** - * Generate a table schema to the Hardware Models page. - * @param {Function} apiCall a Redux async action that fetches and stores table data in Redux. - * See ModelsTableActions. - * @param {String} by by which column the table is sorted. - * If none then set it to undefined/null. - * @param {String} order in what order to sort a column. If none then set it to undefined/null. - * Otherwise, 'ASC' for ascending and 'DESC' for descending - * @return {Array} - */ -const createModelsTableSchema = (apiCall, by, order, onDeleteClick) => { - const sortController = sortControllerFactory(apiCall, by, order); - - return [ - sortableColumn('name', __('Name'), 4, sortController, [ - nameCellFormatter('models'), - ]), - sortableColumn('vendorClass', __('Vendor class'), 3, sortController), - sortableColumn('hardwareModel', __('Hardware model'), 3, sortController), - column( - 'hostsCount', - __('Hosts'), - [headerFormatterWithProps], - [hostsCountCellFormatter('model'), cellFormatterWithProps], - { className: 'col-md-1' }, - { align: 'right' } - ), - column( - 'actions', - __('Actions'), - [headerFormatterWithProps], - [deleteActionCellFormatter(onDeleteClick), cellFormatter] - ), - ]; -}; - -export default createModelsTableSchema; diff --git a/webpack/assets/javascripts/react_app/components/ModelsTable/__snapshots__/ModelsTable.test.js.snap b/webpack/assets/javascripts/react_app/components/ModelsTable/__snapshots__/ModelsTable.test.js.snap deleted file mode 100644 index e3451cb797e0..000000000000 --- a/webpack/assets/javascripts/react_app/components/ModelsTable/__snapshots__/ModelsTable.test.js.snap +++ /dev/null @@ -1,129 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ModelsTable rendering should render ModelsTable 1`] = ` -
-`; diff --git a/webpack/assets/javascripts/react_app/components/ModelsTable/index.js b/webpack/assets/javascripts/react_app/components/ModelsTable/index.js deleted file mode 100644 index 19ae236d5c88..000000000000 --- a/webpack/assets/javascripts/react_app/components/ModelsTable/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import ModelsTable from './ModelsTable'; -import { MODEL_DELETE_MODAL_ID } from '../../routes/Models/constants'; -import { useForemanModal } from '../ForemanModal/ForemanModalHooks'; - -const WrappedModelsTable = props => { - const { setModalOpen } = useForemanModal({ id: MODEL_DELETE_MODAL_ID }); - const { setToDelete, ...rest } = props; - - const onDeleteClick = rowData => { - setToDelete(rowData); - setModalOpen(); - }; - - return ; -}; - -WrappedModelsTable.propTypes = { - setToDelete: PropTypes.func.isRequired, -}; - -export default WrappedModelsTable; diff --git a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js index 19dbff71f15c..9eb46bb91f39 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js +++ b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/Bookmarks.js @@ -72,6 +72,7 @@ const Bookmarks = ({ searchQuery={searchQuery} /> setIsDropdownOpen(false)} diff --git a/webpack/assets/javascripts/react_app/components/PF4/Helpers/useTableSort.js b/webpack/assets/javascripts/react_app/components/PF4/Helpers/useTableSort.js index 096315ff1324..2093518d0cea 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/Helpers/useTableSort.js +++ b/webpack/assets/javascripts/react_app/components/PF4/Helpers/useTableSort.js @@ -9,7 +9,16 @@ export const useTableSort = ({ }) => { const translatedInitialSortColumnName = initialSortColumnName ? __(initialSortColumnName) - : allColumns[0]; + : Object.keys(columnsToSortParams)[0]; + + const [activeSortColumn, setActiveSortColumn] = useState( + translatedInitialSortColumnName + ); + const [activeSortDirection, setActiveSortDirection] = useState('asc'); + + if (Object.keys(columnsToSortParams).length === 0) { + return {}; + } if ( !Object.keys(columnsToSortParams).includes(translatedInitialSortColumnName) ) { @@ -17,11 +26,6 @@ export const useTableSort = ({ `translatedInitialSortColumnName '${translatedInitialSortColumnName}' must also be defined in columnsToSortParams` ); } - const [activeSortColumn, setActiveSortColumn] = useState( - translatedInitialSortColumnName - ); - const [activeSortDirection, setActiveSortDirection] = useState('asc'); - if (!allColumns.includes(activeSortColumn)) { setActiveSortColumn(translatedInitialSortColumnName); } @@ -30,7 +34,7 @@ export const useTableSort = ({ const onSort = (_event, index, direction) => { setActiveSortColumn(allColumns?.[index]); setActiveSortDirection(direction); - _onSort(_event, index, direction); + _onSort && _onSort(_event, index, direction); }; // Patternfly sort params to pass to the + + {columnNamesKeys.map(k => ( + + ))} + + + + {!errorMessage && results.length === 0 && ( + + + + )} + {errorMessage && ( + + + + )} + {results.map((result, rowIndex) => ( + + {columnNamesKeys.map(k => ( + + ))} + + + ))} + + + {results.length > 0 && !errorMessage && ( + + )} + + ); +}; + +Table.propTypes = { + columns: PropTypes.object.isRequired, + params: PropTypes.shape({ + page: PropTypes.number, + perPage: PropTypes.number, + order: PropTypes.string, + }).isRequired, + errorMessage: PropTypes.string, + getActions: PropTypes.func, + isDeleteable: PropTypes.bool, + itemCount: PropTypes.number, + refreshData: PropTypes.func.isRequired, + results: PropTypes.array, + setParams: PropTypes.func.isRequired, + url: PropTypes.string.isRequired, +}; + +Table.defaultProps = { + errorMessage: null, + isDeleteable: false, + itemCount: 0, + getActions: null, + results: [], +}; diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.test.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.test.js new file mode 100644 index 000000000000..6db7b5ded9cf --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.test.js @@ -0,0 +1,184 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent, screen, render, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { Table } from './Table'; + +const mockStore = configureMockStore([thunk]); +const store = mockStore({}); +const columns = { + name: { title: 'Name' }, + email: { title: 'Email' }, + role: { title: 'Role' }, +}; + +const results = [ + { id: 1, name: 'John Doe', email: 'johndoe@example.com', role: 'Admin' }, + { id: 2, name: 'Jane Smith', email: 'janesmith@example.com', role: 'User' }, +]; + +const setParams = jest.fn(); +const refreshData = jest.fn(); + +describe('Table', () => { + test('renders column names and result data', () => { + render( + +
component. diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/DeleteModal.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/DeleteModal.js new file mode 100644 index 000000000000..89d839ab3813 --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/DeleteModal.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Button, Modal } from '@patternfly/react-core'; + +import { sprintf, translate as __ } from '../../../../common/I18n'; +import { APIActions } from '../../../../redux/API'; + +export const DeleteModal = ({ + isModalOpen, + setIsModalOpen, + url, + selectedItem, + refreshData, +}) => { + const { name, id } = selectedItem; + const dispatch = useDispatch(); + const onSubmit = () => { + dispatch( + APIActions.delete({ + url: `${url}/${id}`, + key: 'DELETE_MODAL', + handleSuccess: () => { + setIsModalOpen(false); + refreshData(); + }, + successToast: () => sprintf(__('%s was successfully deleted'), name), + errorToast: ({ message }) => message, + }) + ); + }; + return ( + setIsModalOpen(false)} + appendTo={() => document.getElementsByTagName('table')[0]} + actions={[ + , + , + ]} + > + {sprintf(__('You are about to delete %s. Are you sure?'), name)} + + ); +}; + +DeleteModal.propTypes = { + isModalOpen: PropTypes.bool.isRequired, + setIsModalOpen: PropTypes.func.isRequired, + url: PropTypes.string.isRequired, + selectedItem: PropTypes.shape({ + name: PropTypes.string, + id: PropTypes.number, + }), + refreshData: PropTypes.func.isRequired, +}; + +DeleteModal.defaultProps = { + selectedItem: {}, +}; 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 new file mode 100644 index 000000000000..f2f167d9a4ed --- /dev/null +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/Table/Table.js @@ -0,0 +1,159 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { + TableComposable, + Thead, + Tr, + Th, + Tbody, + Td, + ActionsColumn, +} from '@patternfly/react-table'; +import { translate as __ } from '../../../../common/I18n'; +import { useTableSort } from '../../Helpers/useTableSort'; +import Pagination from '../../../Pagination'; +import { DeleteModal } from './DeleteModal'; +import EmptyPage from '../../../../routes/common/EmptyPage'; + +export const Table = ({ + columns, + errorMessage, + getActions, + isDeleteable, + itemCount, + params, + refreshData, + results, + setParams, + url, +}) => { + const columnsToSortParams = {}; + Object.keys(columns).forEach(key => { + if (columns[key].isSorted) { + columnsToSortParams[columns[key].title] = key; + } + }); + const columnNames = {}; + Object.keys(columns).forEach(key => { + columnNames[key] = columns[key].title; + }); + const onSort = (_event, index, direction) => { + setParams({ + ...params, + order: `${Object.keys(columns)[index]} ${direction}`, + }); + }; + const onPagination = newPagination => { + setParams(newPagination); + }; + const { pfSortParams } = useTableSort({ + allColumns: Object.keys(columns).map(k => columns[k].title), + columnsToSortParams, + onSort, + }); + const [selectedItem, setSelectedItem] = useState({}); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const onDeleteClick = ({ id, name }) => { + setSelectedItem({ id, name }); + setDeleteModalOpen(true); + }; + const actions = ({ can_delete: canDelete, id, name, ...item }) => + [ + isDeleteable && { + title: __('Delete'), + onClick: () => onDeleteClick({ id, name }), + isDisabled: !canDelete, + }, + getActions && getActions({ id, name, ...item }), + ].filter(Boolean); + const columnNamesKeys = Object.keys(columns); + return ( + <> + + +
+ {columnNames[k]} +
+ +
+ +
+ {columns[k].wrapper ? columns[k].wrapper(result) : result[k]} + + {actions ? : null} +
+ + ); + + // Check that column names are displayed + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Email')).toBeInTheDocument(); + expect(screen.getByText('Role')).toBeInTheDocument(); + + // Check that result data is displayed + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('johndoe@example.com')).toBeInTheDocument(); + expect(screen.getByText('Admin')).toBeInTheDocument(); + expect(screen.getByText('Jane Smith')).toBeInTheDocument(); + expect(screen.getByText('janesmith@example.com')).toBeInTheDocument(); + expect(screen.getByText('User')).toBeInTheDocument(); + }); + + test('calls setParams with sort order when column header is clicked', async () => { + render( + +
+ + ); + fireEvent.click(screen.getByRole('button', { name: 'Name' })); + expect(setParams).toHaveBeenCalledWith({ + order: 'name desc', + page: 1, + perPage: 10, + }); + fireEvent.click(screen.getByRole('button', { name: 'Name' })); + expect(setParams).toHaveBeenCalledWith({ + order: 'name asc', + page: 1, + perPage: 10, + }); + }); + + test('shows delete modal when delete button is clicked', () => { + const onDeleteClick = jest.fn(); + const resultWithDeleteButton = { ...results[0], can_delete: true }; + + render( + +
+ + ); + + fireEvent.click(screen.getByLabelText('Actions')); + fireEvent.click(screen.getByText('Delete')); + expect( + screen.getByText('You are about to delete John Doe. Are you sure?') + ).toBeInTheDocument(); + fireEvent.click(screen.getByText('Delete')); + }); + + test('disables delete button when item cannot be deleted', () => { + const resultWithDeleteButton = { ...results[0], can_delete: false }; + + render( + +
+ + ); + fireEvent.click(screen.getByLabelText('Actions')); + expect(screen.getByText('Delete')).toHaveClass('pf-m-disabled'); + }); + + test('no actions button when there are no actions', () => { + const resultWithDeleteButton = { ...results[0], can_delete: true }; + + render( + +
+ + ); + expect(screen.queryAllByText('Actions')).toHaveLength(0); + }); + + test('show error and not the table on error', () => { + render( + +
+ + ); + expect(screen.queryAllByText('John')).toHaveLength(0); + expect(screen.queryAllByText('items')).toHaveLength(0); + expect(screen.queryAllByText('Error test')).toHaveLength(1); + }); + test('show empty state', () => { + render( + +
+ + ); + expect(screen.queryAllByText('items')).toHaveLength(0); + expect(screen.queryAllByText('No Results')).toHaveLength(2); + }); +}); 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 2aa8920cf6c5..5ec4d7c1c644 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.js @@ -1,8 +1,7 @@ -import React, { useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { QuestionCircleIcon } from '@patternfly/react-icons'; import { useHistory } from 'react-router-dom'; -import { isEqual } from 'lodash'; import URI from 'urijs'; import { @@ -20,9 +19,7 @@ import { createURL, exportURL, helpURL, - changeQuery, getURIsearch, - getURI, } from '../../../common/urlHelpers'; import { translate as __ } from '../../../common/I18n'; @@ -33,24 +30,54 @@ import SearchBar from '../../SearchBar'; import Head from '../../Head'; import { ActionButtons } from './ActionButtons'; import './TableIndexPage.scss'; +import { Table } from './Table/Table'; + +/** + +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 +@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 {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 +@param {string}{controller} - the name of the controller for the API +@param {boolean} {creatable} - whether or not to show create button +@param {Array} {customActionButtons} - an array of custom action buttons to be displayed in the toolbar +@param {function} {customCreateAction} - a custom action for the create new button +@param {string} {customExportURL} - a custom URL for the export button +@param {string} {customHelpURL} - a custom URL for the documentation button +@param {Object} {customSearchProps} custom search props to send to the search bar +@param {Array} {cutsomToolbarItems} - an array of custom toolbar items to be displayed +@param {boolean} {exportable} - whether or not to show export button +@param {boolean} {hasHelpPage} - whether or not to show documentation button +@param {string}{header} - the header text for the page +@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 +*/ const TableIndexPage = ({ - apiUrl, apiOptions, - header, - breadcrumbOptions, + apiUrl, beforeToolbarComponent, + breadcrumbOptions, + columns, controller, - searchable, - exportable, creatable, - hasHelpPage, - customSearchProps, - customExportURL, + customActionButtons, customCreateAction, + customExportURL, customHelpURL, - customActionButtons, + customSearchProps, cutsomToolbarItems, + exportable, + hasHelpPage, + header, + isDeleteable, + searchable, children, }) => { const history = useHistory(); @@ -58,11 +85,36 @@ const TableIndexPage = ({ const urlParams = new URLSearchParams(historySearch); const urlParamsSearch = urlParams.get('search') || ''; const search = urlParamsSearch || getURIsearch(); + const defaultParams = { search: search || '' }; + const urlPage = urlParams.get('page'); + const urlPerPage = urlParams.get('per_page'); + if (urlPage) { + defaultParams.page = parseInt(urlPage, 10); + } + if (urlPerPage) { + defaultParams.per_page = parseInt(urlPerPage, 10); + } + const [params, setParams] = useState(defaultParams); const { - response: { search: apiSearchQuery, can_create: canCreate }, + response: { + search: apiSearchQuery, + can_create: canCreate, + results, + subtotal, + message: errorMessage, + }, status = STATUS.PENDING, setAPIOptions, - } = useAPI('get', apiUrl, { ...apiOptions, params: { search } }); + } = useAPI( + 'get', + apiUrl.includes('include_permissions') + ? apiUrl + : `${apiUrl}?include_permissions=true`, + { + ...apiOptions, + params: defaultParams, + } + ); const memoDefaultSearchProps = useMemo( () => getControllerSearchProps(controller), @@ -70,20 +122,28 @@ 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() }); + newParams = { ...newParams, perPage: newParams.per_page }; + setParams(newParams); + setAPIOptions({ ...apiOptions, params: newParams }); + }; + + const setSearch = newSearch => { + const uri = new URI(); + uri.setSearch(newSearch); + history.push({ search: uri.search() }); + setParamsAndAPI({ ...params, ...newSearch }); + }; + const onSearch = newSearch => { - const params = { search: newSearch, page: 1 }; - if (history) { - const uri = new URI(); - uri.removeSearch('search'); - uri.addSearch(params); - history.push({ search: uri.search() }); - } else { - const uri = new URI(getURI()); - uri.removeSearch('search'); - uri.addSearch(params); - changeQuery(uri.search()); + if (newSearch !== apiSearchQuery) { + setSearch({ search: newSearch, page: 1 }); } - if (!isEqual(newSearch, search)) setAPIOptions({ params }); }; const actionButtons = [ @@ -107,7 +167,7 @@ const TableIndexPage = ({ ].filter(item => item); return ( -
+
{header} @@ -129,7 +189,7 @@ const TableIndexPage = ({ {searchable && ( - + )} + {/* todo add flex grow 1 to make search bigger */} {actionButtons.length > 0 && ( - + @@ -156,15 +217,35 @@ const TableIndexPage = ({ - {children} + + {children || ( +
+ setAPIOptions({ + ...apiOptions, + params: { search }, + }) + } + columns={columns} + errorMessage={ + status === STATUS.ERROR && errorMessage ? errorMessage : null + } + /> + )} + ); }; TableIndexPage.propTypes = { - apiUrl: PropTypes.string.isRequired, - header: PropTypes.string, apiOptions: PropTypes.object, + apiUrl: PropTypes.string.isRequired, breadcrumbOptions: PropTypes.shape({ isSwitchable: PropTypes.bool, resource: PropTypes.shape({ @@ -190,36 +271,42 @@ TableIndexPage.propTypes = { ), }), beforeToolbarComponent: PropTypes.node, + columns: PropTypes.object, controller: PropTypes.string, - searchable: PropTypes.bool, - exportable: PropTypes.bool, creatable: PropTypes.bool, - hasHelpPage: PropTypes.bool, - customSearchProps: PropTypes.object, - customExportURL: PropTypes.string, + customActionButtons: PropTypes.array, customCreateAction: PropTypes.func, + customExportURL: PropTypes.string, customHelpURL: PropTypes.string, - customActionButtons: PropTypes.array, - children: PropTypes.node.isRequired, + customSearchProps: PropTypes.object, cutsomToolbarItems: PropTypes.node, + exportable: PropTypes.bool, + hasHelpPage: PropTypes.bool, + header: PropTypes.string, + isDeleteable: PropTypes.bool, + searchable: PropTypes.bool, + children: PropTypes.node, }; TableIndexPage.defaultProps = { - header: '', apiOptions: null, - breadcrumbOptions: null, beforeToolbarComponent: null, + breadcrumbOptions: null, + columns: null, + children: null, controller: '', - searchable: true, - exportable: false, creatable: true, - hasHelpPage: false, - customSearchProps: null, - customExportURL: '', + customActionButtons: [], customCreateAction: null, + customExportURL: '', customHelpURL: '', - customActionButtons: [], + customSearchProps: null, cutsomToolbarItems: null, + exportable: false, + hasHelpPage: false, + header: '', + isDeleteable: false, + searchable: true, }; export default TableIndexPage; 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 6b07052dcbdc..a96ebf073026 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.scss @@ -1,8 +1,20 @@ -.foreman-page { +#foreman-page { .table-toolbar { padding: 0; - .pf-c-toolbar__content{ + .pf-c-toolbar__content { padding: 0; } + .toolbar-search { + flex-grow: 1; + } + .autocomplete-search { + max-width: 600px; + } + } + .pf-c-toolbar__group { + flex-grow: 1; + &.pf-m-align-right{ + justify-content: flex-end; + } } } diff --git a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.test.js b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.test.js index dd6f8c06204c..ce306c97b3c2 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.test.js +++ b/webpack/assets/javascripts/react_app/components/PF4/TableIndexPage/TableIndexPage.test.js @@ -3,8 +3,6 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; import { fireEvent, screen, render, act } from '@testing-library/react'; -// import * as api from 'foremanReact/redux/API'; -// import * as routerSelectors from 'foremanReact/routes/RouterSelector'; import TableIndexPage from './TableIndexPage'; import { breadcrumbBar } from '../../../components/BreadcrumbBar/BreadcrumbBar.fixtures'; import '@testing-library/jest-dom'; diff --git a/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss b/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss index 4378ccf40daf..5754da7f48a6 100644 --- a/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss +++ b/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss @@ -12,4 +12,7 @@ .foreman-search-bar { display: flex; + .pf-c-dropdown.pf-m-align-right { + width: unset; + } } diff --git a/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/column.js b/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/column.js index 5715f70ca91a..3f419155110c 100644 --- a/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/column.js +++ b/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/column.js @@ -1,7 +1,7 @@ /** * Generate a column for a patternfly-3 table. * See more in http://patternfly-react.surge.sh/patternfly-3/ - * See an example: components ModelsTableSchema + * See an example: components SettingsTableSchema.js * @param {String} property the property name of the table. * @param {String} label the column label. * @param {Array} headFormat array of functions that format the header. Read more about format diff --git a/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/sortableColumn.js b/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/sortableColumn.js index d4480fe26c89..ea67c13b7e3a 100644 --- a/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/sortableColumn.js +++ b/webpack/assets/javascripts/react_app/components/common/table/schemaHelpers/sortableColumn.js @@ -9,7 +9,7 @@ import { column } from './column'; /** * Generate a sortable column for a patternfly-3 table. * See more in http://patternfly-react.surge.sh/patternfly-3/ - * See an example: ModelsTableSchema + * See an example: SettingsTableSchema.js * @param {String} property the property name of the table. * @param {String} label the column label. * @param {Number} mdWidth column size on medium devices. Note: using bootstrap diff --git a/webpack/assets/javascripts/react_app/components/componentRegistry.js b/webpack/assets/javascripts/react_app/components/componentRegistry.js index 9061afd68df3..361aac8672ec 100644 --- a/webpack/assets/javascripts/react_app/components/componentRegistry.js +++ b/webpack/assets/javascripts/react_app/components/componentRegistry.js @@ -28,7 +28,6 @@ import ChartBox from './ChartBox/ChartBox'; import ConfigReports from './ConfigReports/ConfigReports'; import DiffModal from './ConfigReports/DiffModal'; import { WrapperFactory } from './wrapperFactory'; -import ModelsTable from './ModelsTable'; import TemplateGenerator from './TemplateGenerator'; import Editor from './Editor'; import LoginPage from './LoginPage'; @@ -173,7 +172,6 @@ const coreComponets = [ }, { name: 'FormField', type: FormField }, { name: 'InputFactory', type: InputFactory }, - { name: 'ModelsTable', type: ModelsTable }, { name: 'Editor', type: Editor }, // Report templates diff --git a/webpack/assets/javascripts/react_app/redux/reducers/index.js b/webpack/assets/javascripts/react_app/redux/reducers/index.js index 6d7f58599a77..3638e043a51b 100644 --- a/webpack/assets/javascripts/react_app/redux/reducers/index.js +++ b/webpack/assets/javascripts/react_app/redux/reducers/index.js @@ -16,7 +16,6 @@ import { reducers as intervalReducers } from '../middlewares/IntervalMiddleware' import { reducers as bookmarksPF4Reducers } from '../../components/PF4/Bookmarks'; import { reducers as modalReducers } from '../../components/ForemanModal'; import { reducers as apiReducer } from '../API'; -import { reducers as modelsPageReducers } from '../../routes/Models/ModelsPage'; import { reducers as settingRecordsReducers } from '../../components/SettingRecords'; import { reducers as personalAccessTokensReducers } from '../../components/users/PersonalAccessTokens'; import { reducers as confirmModalReducers } from '../../components/ConfirmModal'; @@ -45,7 +44,6 @@ export function combineReducersAsync(asyncReducers) { ...fillReducers, ...auditsPageReducers, ...modalReducers, - ...modelsPageReducers, // Middlewares ...intervalReducers, diff --git a/webpack/assets/javascripts/react_app/routes/FiltersForm/constants.js b/webpack/assets/javascripts/react_app/routes/FiltersForm/constants.js index ada2aabce89b..1cb7fa92c6d2 100644 --- a/webpack/assets/javascripts/react_app/routes/FiltersForm/constants.js +++ b/webpack/assets/javascripts/react_app/routes/FiltersForm/constants.js @@ -1,15 +1,2 @@ -import { getControllerSearchProps } from '../../constants'; - -export const MODELS_PAGE_DATA_RESOLVED = 'MODELS_PAGE_DATA_RESOLVED'; -export const MODELS_PAGE_DATA_FAILED = 'MODELS_PAGE_DATA_FAILED'; -export const MODELS_PAGE_HIDE_LOADING = 'MODELS_PAGE_HIDE_LOADING'; -export const MODELS_PAGE_SHOW_LOADING = 'MODELS_PAGE_SHOW_LOADING'; -export const MODELS_PAGE_CLEAR_ERROR = 'MODELS_PAGE_CLEAR_ERROR'; - -export const MODELS_SEARCH_PROPS = getControllerSearchProps('models'); -export const MODELS_API_PATH = '/api/models?include_permissions=true'; -export const MODEL_DELETE_MODAL_ID = 'modelDeleteModal'; -export const API_REQUEST_KEY = 'MODELS'; - export const FILTERS_PATH_NEW = '/filters/new'; export const FILTERS_PATH_EDIT = '/filters/:id/edit'; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPage.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPage.js deleted file mode 100644 index 36d581556fe5..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPage.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { translate as __ } from '../../../common/I18n'; -import TableIndexPage from '../../../components/PF4/TableIndexPage/TableIndexPage'; -import ModelsPageContent from './components/ModelsPageContent'; -import { MODELS_API_PATH, API_REQUEST_KEY } from '../constants'; - -const ModelsPage = ({ - fetchAndPush, - isLoading, - hasData, - models, - sort, - hasError, - itemCount, - message, -}) => ( - - - -); - -ModelsPage.propTypes = { - fetchAndPush: PropTypes.func.isRequired, - isLoading: PropTypes.bool.isRequired, - hasData: PropTypes.bool.isRequired, - models: PropTypes.array.isRequired, - sort: PropTypes.object.isRequired, - hasError: PropTypes.bool.isRequired, - itemCount: PropTypes.number.isRequired, - message: PropTypes.object, -}; - -ModelsPage.defaultProps = { - message: {}, -}; - -export default ModelsPage; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageActions.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageActions.js deleted file mode 100644 index 7cb6a391e23f..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageActions.js +++ /dev/null @@ -1,45 +0,0 @@ -import URI from 'urijs'; -import history from '../../../history'; -import { get } from '../../../redux/API'; -import { buildQuery } from './ModelsPageHelpers'; - -import { MODELS_API_PATH, MODELS_PATH, API_REQUEST_KEY } from '../constants'; - -import { stringifyParams, getParams } from '../../../common/urlHelpers'; - -export const initializeModels = () => dispatch => { - const params = getParams(); - dispatch(fetchModels(params)); - if (!history.action === 'POP') { - history.replace({ - pathname: MODELS_PATH, - search: stringifyParams(params), - }); - } -}; - -export const fetchModels = ( - { page, perPage, searchQuery, sort }, - url = MODELS_API_PATH -) => { - const sortString = - sort && Object.keys(sort).length > 0 ? `${sort.by} ${sort.order}` : ''; - - const uriWithPrams = new URI(url); - uriWithPrams.setSearch({ - page, - per_page: perPage, - search: searchQuery, - order: sortString, - }); - return get({ key: API_REQUEST_KEY, url: uriWithPrams }); -}; - -export const fetchAndPush = (params = {}) => (dispatch, getState) => { - const query = buildQuery(params, getState()); - dispatch(fetchModels(query)); - history.push({ - pathname: MODELS_PATH, - search: stringifyParams(query), - }); -}; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageHelpers.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageHelpers.js deleted file mode 100644 index 9fe3489ec010..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageHelpers.js +++ /dev/null @@ -1,29 +0,0 @@ -import { snakeCase } from 'lodash'; -import { compose } from 'redux'; -import { - selectSort, - selectPage, - selectPerPage, - selectSearch, -} from './ModelsPageSelectors'; - -export const buildQuery = (query, state) => { - const querySort = pickSort(query, state); - - return { - page: query.page || selectPage(state), - perPage: query.per_page || selectPerPage(state), - searchQuery: - query.searchQuery === undefined ? selectSearch(state) : query.searchQuery, - ...(querySort && { sort: querySort }), - }; -}; - -export const pickSort = (query, state) => - checkSort(query.sort) - ? transformSort(query.sort) - : checkSort(compose(transformSort, selectSort)(state)); - -const checkSort = sort => (sort && sort.by && sort.order ? sort : undefined); - -const transformSort = sort => ({ ...sort, by: snakeCase(sort.by) }); diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageSelectors.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageSelectors.js deleted file mode 100644 index 54d2cfc8d03c..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/ModelsPageSelectors.js +++ /dev/null @@ -1,62 +0,0 @@ -import { camelCase } from 'lodash'; -import Immutable from 'seamless-immutable'; - -import { API_REQUEST_KEY } from '../constants'; -import { STATUS } from '../../../constants'; -import { deepPropsToCamelCase } from '../../../common/helpers'; - -export const response = { - results: [], - page: 0, - perPage: 0, - search: '', - sort: {}, - subtotal: 0, - message: {}, -}; - -const emptyState = Immutable({ - payload: null, - response, - status: null, -}); - -export const selectModelsPageData = state => - deepPropsToCamelCase(state.API[API_REQUEST_KEY]) || emptyState; - -const selectModelsPageResponse = state => - selectModelsPageData(state).response || Immutable(response); - -export const selectIsLoading = state => { - const { status } = selectModelsPageData(state); - return !status || status === STATUS.PENDING; -}; - -const selectModelsPageStatus = state => selectModelsPageData(state).status; - -export const selectHasError = state => - selectModelsPageStatus(state) === STATUS.ERROR; - -export const selectModels = state => selectModelsPageResponse(state).results; - -export const selectHasData = state => { - const status = selectModelsPageStatus(state); - const results = selectModels(state); - - return status === STATUS.RESOLVED && results && results.length > 0; -}; - -export const selectPage = state => selectModelsPageResponse(state).page; -export const selectPerPage = state => selectModelsPageResponse(state).perPage; -export const selectSearch = state => selectModelsPageResponse(state).search; - -export const selectSort = state => { - const sort = selectModelsPageResponse(state).sort || Immutable({}); - if (sort.by && sort.order) { - return { ...sort, by: camelCase(sort.by) }; - } - return sort; -}; - -export const selectSubtotal = state => selectModelsPageResponse(state).subtotal; -export const selectMessage = state => selectModelsPageResponse(state).message; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.fixtures.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.fixtures.js deleted file mode 100644 index 3f0addb5d019..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.fixtures.js +++ /dev/null @@ -1,75 +0,0 @@ -import { API_REQUEST_KEY } from '../../constants'; - -export const pickedQuery = { by: 'default_name', order: 'ASC' }; - -const searchString = 'name=foo'; - -const stateSearch = { search: searchString }; - -const querySearch = { searchQuery: searchString }; - -export const querySort = { - sort: { - by: 'defaultName', - order: 'ASC', - }, -}; - -const pageParams = { - page: 5, - perPage: 42, -}; - -const stateParams = { - ...pageParams, - ...stateSearch, - ...querySort, -}; - -export const resultParams = { - ...pageParams, - ...querySearch, - sort: pickedQuery, -}; - -export const queryParams = { - page: 5, - per_page: 42, - ...querySearch, - ...querySort, -}; - -export const stateFactory = state => ({ - API: { - [API_REQUEST_KEY]: { - response: { - ...stateParams, - ...state, - }, - }, - }, -}); - -export const propsFactory = (state = {}) => ({ - ...stateParams, - ...state, -}); - -export const models = [ - { - id: 1, - name: 'my-hw-model', - canEdit: true, - canDelete: true, - hostsCount: 5, - vendorClass: 'custom', - }, - { - id: 2, - name: 'your-hw-model', - canEdit: false, - canDelete: false, - hostsCount: 4, - vendorClass: 'B+', - }, -]; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.test.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.test.js deleted file mode 100644 index 9b2d8d0d12b6..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPage.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; -import ModelsPage from '../ModelsPage'; -import { propsFactory, models } from './ModelsPage.fixtures'; - -const props = { - fetchAndPush: () => {}, - models, - itemCount: models.length, -}; - -const fixtures = { - 'should render when loading': propsFactory({ - ...props, - isLoading: true, - hasData: false, - hasError: false, - toasts: [], - }), - 'should render with no data': propsFactory({ - ...props, - isLoading: false, - hasData: false, - hasError: false, - toasts: [], - }), - 'should render with error': propsFactory({ - isLoading: false, - hasData: false, - hasError: true, - message: { - type: 'error', - text: 'this is error', - }, - ...props, - toasts: [], - }), - 'should render with models': propsFactory({ - ...props, - isLoading: false, - hasError: false, - hasData: true, - toasts: [], - }), -}; - -describe('ModelsPage', () => { - describe('redering', () => - testComponentSnapshotsWithFixtures(ModelsPage, fixtures)); -}); diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageHelpers.test.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageHelpers.test.js deleted file mode 100644 index f5aac7484ed1..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageHelpers.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import { pickSort, buildQuery } from '../ModelsPageHelpers'; - -import { - querySort, - pickedQuery, - queryParams, - resultParams, - stateParams, - stateFactory, -} from './ModelsPage.fixtures'; - -describe('pickSort', () => { - it('should pick sort from query', () => { - expect(pickSort(querySort, {})).toStrictEqual(pickedQuery); - }); - - it('should pick sort from state', () => { - const state = stateFactory({ sort: pickedQuery }); - expect(pickSort({}, state)).toStrictEqual(pickedQuery); - }); -}); - -describe('buildQuery', () => { - it('should return params from query if present', () => { - expect(buildQuery(queryParams, {})).toStrictEqual(resultParams); - }); - - it('should return params from state', () => { - const state = stateFactory(stateParams); - expect(buildQuery({}, state)).toStrictEqual(resultParams); - }); -}); diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageSelectors.test.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageSelectors.test.js deleted file mode 100644 index 55a812fa3aa5..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/ModelsPageSelectors.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import { testSelectorsSnapshotWithFixtures } from '@theforeman/test'; - -import { - selectModels, - selectPage, - selectPerPage, - selectSearch, - selectSort, - selectHasData, - selectHasError, - selectIsLoading, - selectSubtotal, - selectMessage, -} from '../ModelsPageSelectors'; - -import { stateFactory, models } from './ModelsPage.fixtures'; - -const state = stateFactory({ - results: models, - sort: { by: 'name', order: 'DESC' }, - page: 1, - perPage: 1, - search: 'name ~ foo', - subtotal: 42, - message: { type: 'error', text: 'This is error' }, -}); - -const fixtures = { - 'should return models': () => selectModels(state), - 'should return page': () => selectPage(state), - 'should return perPage': () => selectPerPage(state), - 'should return search': () => selectSearch(state), - 'should return sort': () => selectSort(state), - 'should return hasData': () => selectHasData(state), - 'should return hasError': () => selectHasError(state), - 'should return isLoading': () => selectIsLoading(state), - 'should return subtotal': () => selectSubtotal(state), - 'should return message': () => selectMessage(state), -}; - -describe('ModelsPage selectors', () => - testSelectorsSnapshotWithFixtures(fixtures)); diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPage.test.js.snap b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPage.test.js.snap deleted file mode 100644 index 2f4e11016437..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPage.test.js.snap +++ /dev/null @@ -1,246 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ModelsPage redering should render when loading 1`] = ` - - - -`; - -exports[`ModelsPage redering should render with error 1`] = ` - - - -`; - -exports[`ModelsPage redering should render with models 1`] = ` - - - -`; - -exports[`ModelsPage redering should render with no data 1`] = ` - - - -`; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPageSelectors.test.js.snap b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPageSelectors.test.js.snap deleted file mode 100644 index ba0240448ed1..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/__tests__/__snapshots__/ModelsPageSelectors.test.js.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ModelsPage selectors should return hasData 1`] = `false`; - -exports[`ModelsPage selectors should return hasError 1`] = `false`; - -exports[`ModelsPage selectors should return isLoading 1`] = `true`; - -exports[`ModelsPage selectors should return message 1`] = ` -Object { - "text": "This is error", - "type": "error", -} -`; - -exports[`ModelsPage selectors should return models 1`] = ` -Array [ - Object { - "canDelete": true, - "canEdit": true, - "hostsCount": 5, - "id": 1, - "name": "my-hw-model", - "vendorClass": "custom", - }, - Object { - "canDelete": false, - "canEdit": false, - "hostsCount": 4, - "id": 2, - "name": "your-hw-model", - "vendorClass": "B+", - }, -] -`; - -exports[`ModelsPage selectors should return page 1`] = `1`; - -exports[`ModelsPage selectors should return perPage 1`] = `1`; - -exports[`ModelsPage selectors should return search 1`] = `"name ~ foo"`; - -exports[`ModelsPage selectors should return sort 1`] = ` -Object { - "by": "name", - "order": "DESC", -} -`; - -exports[`ModelsPage selectors should return subtotal 1`] = `42`; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.js deleted file mode 100644 index f502c0035cf3..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { sprintf, translate as __ } from '../../../../common/I18n'; -import ForemanModal from '../../../../components/ForemanModal'; -import { MODEL_DELETE_MODAL_ID } from '../../constants'; - -const ModelDeleteModal = props => { - const { id, name } = props.toDelete; - - return ( - - {sprintf(__('You are about to delete %s. Are you sure?'), name)} - - - ); -}; - -ModelDeleteModal.propTypes = { - toDelete: PropTypes.object, - fetchAndPush: PropTypes.func.isRequired, -}; - -ModelDeleteModal.defaultProps = { - toDelete: {}, -}; - -export default ModelDeleteModal; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.test.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.test.js deleted file mode 100644 index 81fda86cc112..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelDeleteModal.test.js +++ /dev/null @@ -1,16 +0,0 @@ -import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; -import ModelDeleteModal from './ModelDeleteModal'; - -const fixtures = { - 'should render a modal': { - fetchAndPush: jest.fn(), - toDelete: { - name: 'HW model to delete', - id: 5, - }, - }, -}; - -describe('ModelDeleteModal', () => { - testComponentSnapshotsWithFixtures(ModelDeleteModal, fixtures); -}); diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelsPageContent.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelsPageContent.js deleted file mode 100644 index 51d62136d61e..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/ModelsPageContent.js +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; - -import ModelsTable from '../../../../components/ModelsTable'; -import Pagination from '../../../../components/Pagination'; - -import ModelDeleteModal from './ModelDeleteModal'; -import LoadingPage from '../../../common/LoadingPage'; -import { withRenderHandler } from '../../../../common/HOC'; - -const ModelsPageContent = ({ models, sort, fetchAndPush, itemCount }) => { - const [toDelete, setToDelete] = useState({}); - - return ( - - - - - - ); -}; - -ModelsPageContent.propTypes = { - models: PropTypes.array.isRequired, - sort: PropTypes.object.isRequired, - fetchAndPush: PropTypes.func.isRequired, - itemCount: PropTypes.number.isRequired, -}; - -export default withRenderHandler({ - Component: ModelsPageContent, - LoadingComponent: LoadingPage, -}); diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/__snapshots__/ModelDeleteModal.test.js.snap b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/__snapshots__/ModelDeleteModal.test.js.snap deleted file mode 100644 index 68fd6ed41358..000000000000 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/components/__snapshots__/ModelDeleteModal.test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ModelDeleteModal should render a modal 1`] = ` - - You are about to delete HW model to delete. Are you sure? - - -`; diff --git a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/index.js b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/index.js index c5cbe6e3d0c3..a10faae2ae50 100644 --- a/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/index.js +++ b/webpack/assets/javascripts/react_app/routes/Models/ModelsPage/index.js @@ -1,34 +1,41 @@ -import { connect } from 'react-redux'; -import { compose, bindActionCreators } from 'redux'; +import React from 'react'; -import ModelsPage from './ModelsPage'; -import * as actions from './ModelsPageActions'; +import { translate as __ } from '../../../common/I18n'; +import TableIndexPage from '../../../components/PF4/TableIndexPage/TableIndexPage'; +import { MODELS_API_PATH, API_REQUEST_KEY } from '../constants'; -import { callOnPopState } from '../../../common/HOC'; +const ModelsPage = () => { + const columns = { + name: { + title: __('Name'), + wrapper: ({ can_edit: canEdit, id, name }) => + canEdit ? ( + {name} + ) : ( + {name} + ), + isSorted: true, + }, + vendor_class: { + title: __('Vendor class'), + }, + hardware_model: { + title: __('Hardware model'), + }, + hosts_count: { + title: __('Hosts'), + }, + }; + return ( + + ); +}; -import { - selectModels, - selectSort, - selectHasData, - selectHasError, - selectIsLoading, - selectSubtotal, - selectMessage, -} from './ModelsPageSelectors'; - -const mapStateToProps = state => ({ - models: selectModels(state), - sort: selectSort(state), - isLoading: selectIsLoading(state), - hasData: selectHasData(state), - hasError: selectHasError(state), - itemCount: selectSubtotal(state), - message: selectMessage(state), -}); - -const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch); - -export default compose( - connect(mapStateToProps, mapDispatchToProps), - callOnPopState(({ initializeModels }) => initializeModels()) -)(ModelsPage); +export default ModelsPage; diff --git a/webpack/assets/javascripts/react_app/routes/Models/constants.js b/webpack/assets/javascripts/react_app/routes/Models/constants.js index 6b282199a3e3..6fd52ccb4f70 100644 --- a/webpack/assets/javascripts/react_app/routes/Models/constants.js +++ b/webpack/assets/javascripts/react_app/routes/Models/constants.js @@ -1,10 +1,3 @@ -export const MODELS_PAGE_DATA_RESOLVED = 'MODELS_PAGE_DATA_RESOLVED'; -export const MODELS_PAGE_DATA_FAILED = 'MODELS_PAGE_DATA_FAILED'; -export const MODELS_PAGE_HIDE_LOADING = 'MODELS_PAGE_HIDE_LOADING'; -export const MODELS_PAGE_SHOW_LOADING = 'MODELS_PAGE_SHOW_LOADING'; -export const MODELS_PAGE_CLEAR_ERROR = 'MODELS_PAGE_CLEAR_ERROR'; - -export const MODELS_API_PATH = '/api/models?include_permissions=true'; +export const MODELS_API_PATH = '/api/models'; export const MODELS_PATH = '/models'; -export const MODEL_DELETE_MODAL_ID = 'modelDeleteModal'; export const API_REQUEST_KEY = 'MODELS'; diff --git a/webpack/assets/javascripts/react_app/routes/common/EmptyPage/index.js b/webpack/assets/javascripts/react_app/routes/common/EmptyPage/index.js index 5e643f3428e6..06ed42586a9d 100644 --- a/webpack/assets/javascripts/react_app/routes/common/EmptyPage/index.js +++ b/webpack/assets/javascripts/react_app/routes/common/EmptyPage/index.js @@ -20,10 +20,10 @@ EmptyPage.propTypes = { }; EmptyPage.defaultProps = { - message: PropTypes.shape({ + message: { type: 'empty', text: 'No Results', - }), + }, }; export default EmptyPage;