From 8eec31178f02da8c225fb6039f1d13ea647fc5af Mon Sep 17 00:00:00 2001 From: Georgii Karataev Date: Mon, 20 Feb 2023 18:19:51 +0100 Subject: [PATCH 1/4] Implement the group detail reducer and add new API Implements https://issues.redhat.com/browse/ESSNTL-3726. --- src/components/InventoryGroups/utils/api.js | 8 ++++ src/store/action-types.js | 3 +- src/store/groupDetail.js | 45 +++++++++++++++++++++ src/store/inventory-actions.js | 7 +++- src/store/reducers.js | 4 +- 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/store/groupDetail.js diff --git a/src/components/InventoryGroups/utils/api.js b/src/components/InventoryGroups/utils/api.js index 4d21a62f8..69b3d3a34 100644 --- a/src/components/InventoryGroups/utils/api.js +++ b/src/components/InventoryGroups/utils/api.js @@ -25,6 +25,10 @@ export const validateGroupName = (name) => { .then((resp) => resp?.results.some((group) => group.name === name)); }; +export const getGroupDetail = (groupId) => { + return instance.get(`${INVENTORY_API_BASE}/groups/${groupId}`); +}; + getGroups.propTypes = { search: PropTypes.shape({ // eslint-disable-next-line camelcase @@ -35,3 +39,7 @@ getGroups.propTypes = { page: PropTypes.number }) }; + +getGroupDetail.propTypes = { + groupId: PropTypes.string.isRequired +}; diff --git a/src/store/action-types.js b/src/store/action-types.js index 42d8e4d71..fe2e271f7 100644 --- a/src/store/action-types.js +++ b/src/store/action-types.js @@ -35,7 +35,8 @@ export const asyncInventory = [ 'LOAD_TAGS', 'ALL_TAGS', 'OPERATING_SYSTEMS', - 'GROUPS' + 'GROUPS', + 'GROUP_DETAIL' ]; export const systemIssues = [ diff --git a/src/store/groupDetail.js b/src/store/groupDetail.js new file mode 100644 index 000000000..ef02d36e8 --- /dev/null +++ b/src/store/groupDetail.js @@ -0,0 +1,45 @@ + +import { applyReducerHash } from '@redhat-cloud-services/frontend-components-utilities/ReducerRegistry'; +import { ACTION_TYPES } from './action-types'; + +export const initialState = { + loading: false, + rejected: false, + fulfilled: false, + uninitialized: true, + error: null, + data: null +}; + +export default applyReducerHash( + { + [ACTION_TYPES.GROUP_DETAIL_PENDING]: (state) => { + return { + ...state, + loading: true, + uninitialized: false + }; + }, + [ACTION_TYPES.GROUP_DETAIL_FULFILLED]: (state, { payload }) => { + return { + ...state, + loading: false, + rejected: false, + uninitialized: false, + fulfilled: true, + data: payload + }; + }, + [ACTION_TYPES.GROUP_DETAIL_REJECTED]: (state, { payload }) => { + return { + ...state, + loading: false, + rejected: true, + uninitialized: false, + fulfilled: false, + error: payload + }; + } + }, + initialState +); diff --git a/src/store/inventory-actions.js b/src/store/inventory-actions.js index da4f07096..2a30bc72f 100644 --- a/src/store/inventory-actions.js +++ b/src/store/inventory-actions.js @@ -21,7 +21,7 @@ import { filtersReducer, getOperatingSystems } from '../api'; -import { getGroups } from '../components/InventoryGroups/utils/api'; +import { getGroupDetail, getGroups } from '../components/InventoryGroups/utils/api'; export const loadEntities = (items = [], { filters, ...config }, { showTags } = {}, getEntities = defaultGetEntities) => { const itemIds = items.reduce((acc, curr) => ( @@ -184,6 +184,11 @@ export const fetchGroups = (search, pagination) => ({ payload: getGroups(search, pagination) }); +export const fetchGroupDetail = (groupId) => ({ + type: ACTION_TYPES.GROUP_DETAIL, + payload: getGroupDetail(groupId) +}); + export const fetchOperatingSystems = (params = []) => ({ type: ACTION_TYPES.OPERATING_SYSTEMS, payload: getOperatingSystems(params) diff --git a/src/store/reducers.js b/src/store/reducers.js index cdfcdb0bb..8b3c08794 100644 --- a/src/store/reducers.js +++ b/src/store/reducers.js @@ -12,6 +12,7 @@ import { notificationsReducer } from '@redhat-cloud-services/frontend-components import entitiesReducer, { defaultState as entitiesDefault } from './entities'; import entityDetailsReducer, { entityDefaultState as entityDefault, updateEntity } from './entityDetails'; import groups from './groups'; +import groupDetail from './groupDetail'; export { entitiesReducer, entityDetailsReducer }; @@ -114,7 +115,8 @@ function onSetPagination(state, { payload }) { let reducers = { notifications: notificationsReducer, systemProfileStore, - groups + groups, + groupDetail }; export const tableReducer = applyReducerHash( From a0103979237f9fbd8d53697a9a1b881376af581b Mon Sep 17 00:00:00 2001 From: Georgii Karataev Date: Mon, 20 Feb 2023 18:20:42 +0100 Subject: [PATCH 2/4] Add /groups/%id route and the page component Implements https://issues.redhat.com/browse/ESSNTL-3726. --- src/Routes.js | 13 +++- .../InventoryGroupDetail/GroupDetailHeader.js | 35 +++++++++++ .../InventoryGroupDetail/GroupDetailInfo.js | 20 +++++++ .../GroupDetailSystems.js | 22 +++++++ .../InventoryGroupDetail.js | 60 +++++++++++++++++++ .../NoSystemsEmptyState.js | 42 +++++++++++++ src/components/InventoryGroupDetail/index.js | 3 + src/routes/InventoryGroupDetail.js | 3 + 8 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/components/InventoryGroupDetail/GroupDetailHeader.js create mode 100644 src/components/InventoryGroupDetail/GroupDetailInfo.js create mode 100644 src/components/InventoryGroupDetail/GroupDetailSystems.js create mode 100644 src/components/InventoryGroupDetail/InventoryGroupDetail.js create mode 100644 src/components/InventoryGroupDetail/NoSystemsEmptyState.js create mode 100644 src/components/InventoryGroupDetail/index.js create mode 100644 src/routes/InventoryGroupDetail.js diff --git a/src/Routes.js b/src/Routes.js index 39cd166a5..94c19011d 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -9,12 +9,14 @@ import useFeatureFlag from './Utilities/useFeatureFlag'; const InventoryTable = lazy(() => import('./routes/InventoryTable')); const InventoryDetail = lazy(() => import('./routes/InventoryDetail')); const InventoryGroups = lazy(() => import('./routes/InventoryGroups')); +const InventoryGroupDetail = lazy(() => import('./routes/InventoryGroupDetail')); export const routes = { table: '/', detail: '/:inventoryId', detailWithModal: '/:inventoryId/:modalId', - groups: '/groups' + groups: '/groups', + groupDetail: '/groups/:groupId' }; export const Routes = () => { @@ -51,6 +53,15 @@ export const Routes = () => { } rootClass="inventory" /> + ( + + + + + + )} rootClass='inventory' /> diff --git a/src/components/InventoryGroupDetail/GroupDetailHeader.js b/src/components/InventoryGroupDetail/GroupDetailHeader.js new file mode 100644 index 000000000..81b8326cc --- /dev/null +++ b/src/components/InventoryGroupDetail/GroupDetailHeader.js @@ -0,0 +1,35 @@ +import { Breadcrumb, BreadcrumbItem, Skeleton } from '@patternfly/react-core'; +import { + PageHeader, + PageHeaderTitle +} from '@redhat-cloud-services/frontend-components'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { Link, useParams } from 'react-router-dom'; +import { routes } from '../../Routes'; + +const GroupDetailHeader = () => { + const { uninitialized, loading, data } = useSelector((state) => state.groupDetail); + const { groupId } = useParams(); + + const nameOrId = uninitialized || loading ? ( + + ) : ( + // in case of error, render just id from URL + data?.name || groupId + ); + + return ( + + + + Groups + + {nameOrId} + + + + ); +}; + +export default GroupDetailHeader; diff --git a/src/components/InventoryGroupDetail/GroupDetailInfo.js b/src/components/InventoryGroupDetail/GroupDetailInfo.js new file mode 100644 index 000000000..01a935077 --- /dev/null +++ b/src/components/InventoryGroupDetail/GroupDetailInfo.js @@ -0,0 +1,20 @@ +import { EmptyState, EmptyStateBody, Spinner } from '@patternfly/react-core'; +import { InvalidObject } from '@redhat-cloud-services/frontend-components'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +const GroupDetailInfo = () => { + const { uninitialized, loading } = useSelector((state) => state.groupDetail); + + // TODO: implement according to mocks + + return ( + + + {uninitialized || loading ? : } + + + ); +}; + +export default GroupDetailInfo; diff --git a/src/components/InventoryGroupDetail/GroupDetailSystems.js b/src/components/InventoryGroupDetail/GroupDetailSystems.js new file mode 100644 index 000000000..e9470ce71 --- /dev/null +++ b/src/components/InventoryGroupDetail/GroupDetailSystems.js @@ -0,0 +1,22 @@ +import { EmptyState, EmptyStateBody, Spinner } from '@patternfly/react-core'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import NoSystemsEmptyState from './NoSystemsEmptyState'; + +const GroupDetailSystems = () => { + const { uninitialized, loading } = useSelector((state) => state.groupDetail); + + // TODO: integrate the inventory table + + return (uninitialized || loading ? + + + + + + : + + ); +}; + +export default GroupDetailSystems; diff --git a/src/components/InventoryGroupDetail/InventoryGroupDetail.js b/src/components/InventoryGroupDetail/InventoryGroupDetail.js new file mode 100644 index 000000000..fc18f6e43 --- /dev/null +++ b/src/components/InventoryGroupDetail/InventoryGroupDetail.js @@ -0,0 +1,60 @@ +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchGroupDetail } from '../../store/inventory-actions'; +import { PageSection, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import { useState } from 'react'; +import GroupDetailHeader from './GroupDetailHeader'; +import GroupDetailSystems from './GroupDetailSystems'; +import GroupDetailInfo from './GroupDetailInfo'; + +const InventoryGroupDetail = () => { + const dispatch = useDispatch(); + const { data } = useSelector((state) => state.groupDetail); + const chrome = useChrome(); + + const { groupId } = useParams(); + + useEffect(() => { + dispatch(fetchGroupDetail(groupId)); + }, []); + + useEffect(() => { + // if available, change ID to the group's name in the window title + chrome.updateDocumentTitle( + `${data?.name || groupId} - Inventory Groups | Red Hat Insights` + ); + }, [data]); + + const [activeTabKey, setActiveTabKey] = useState(0); + + return ( + + + + setActiveTabKey(value)} + aria-label="Group tabs" + role="region" + inset={{ default: 'insetMd' }} // add extra space before the first tab (according to mocks) + > + Systems} aria-label="Group systems tab"> + + + + + Group info} aria-label="Group info tab"> + + + + + + + + + ); +}; + +export default InventoryGroupDetail; diff --git a/src/components/InventoryGroupDetail/NoSystemsEmptyState.js b/src/components/InventoryGroupDetail/NoSystemsEmptyState.js new file mode 100644 index 000000000..decc11181 --- /dev/null +++ b/src/components/InventoryGroupDetail/NoSystemsEmptyState.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { + Button, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateSecondaryActions, + Title +} from '@patternfly/react-core'; +import { ExternalLinkAltIcon, PlusCircleIcon } from '@patternfly/react-icons'; + +import { global_palette_black_600 as globalPaletteBlack600 } from '@patternfly/react-tokens/dist/js/global_palette_black_600'; + +const NoSystemsEmptyState = () => { + return ( + + + + No systems added + + + To manage systems more effectively, add systems to the group. + + + + + + + );}; + +export default NoSystemsEmptyState; diff --git a/src/components/InventoryGroupDetail/index.js b/src/components/InventoryGroupDetail/index.js new file mode 100644 index 000000000..a3695ffa0 --- /dev/null +++ b/src/components/InventoryGroupDetail/index.js @@ -0,0 +1,3 @@ +import InventoryGroupDetail from './InventoryGroupDetail'; + +export default InventoryGroupDetail; diff --git a/src/routes/InventoryGroupDetail.js b/src/routes/InventoryGroupDetail.js new file mode 100644 index 000000000..235b9c98f --- /dev/null +++ b/src/routes/InventoryGroupDetail.js @@ -0,0 +1,3 @@ +import InventoryGroupDetail from '../components/InventoryGroupDetail'; + +export default InventoryGroupDetail; From 9b770f95294fc3643fd7bf0521b2e3b356fe49d0 Mon Sep 17 00:00:00 2001 From: Georgii Karataev Date: Tue, 21 Feb 2023 11:44:10 +0100 Subject: [PATCH 3/4] Implement tests for the page and header components Implements https://issues.redhat.com/browse/ESSNTL-3726. --- .../620f9ae75A8F6b83d78F3B55Af1c4b2C.json | 38 +++++++++++++ cypress/support/interceptors.js | 28 +++++++++ .../InventoryGroupDetail/GroupDetailHeader.js | 12 ++-- .../InventoryGroupDetail.cy.js | 57 +++++++++++++++++++ .../InventoryGroupDetail.js | 14 +++-- .../__tests__/GroupDetailHeader.test.js | 44 ++++++++++++++ .../__tests__/InventoryGroupDetail.test.js | 44 ++++++++++++++ src/components/InventoryGroupDetail/index.js | 10 +++- 8 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 cypress/fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json create mode 100644 src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js create mode 100644 src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js create mode 100644 src/components/InventoryGroupDetail/__tests__/InventoryGroupDetail.test.js diff --git a/cypress/fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json b/cypress/fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json new file mode 100644 index 000000000..e52cc1577 --- /dev/null +++ b/cypress/fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json @@ -0,0 +1,38 @@ +{ + "count": 1, + "page": 1, + "per_page": 50, + "results": [ + { + "name": "ea velit incididunt", + "updated_at": "1998-04-17T22:00:00.0Z", + "id": "620f9ae75A8F6b83d78F3B55Af1c4b2C", + "account": "irure ea exercitation adipisicing velit", + "org_id": "non", + "created_at": "1994-07-28T22:00:00.0Z", + "host_ids": [ + "eEfb7FAa-1f0b-Bde3-a4FF-fDdCd5faA764", + "bb7417faE7f9eCdacEDDd0Fcae9Cf4BB", + "2095BB72Aa6E1d2D1DC48bdecF1085eb", + "E645A3Fb42B77c4eBf9faEfCad8f6F5a", + "7fb4fa758Cbc0A861C2Cb21695aeA9d6", + "f6BE4AafA6bF543693Fa3F1fadFaA4E9", + "D608C33f-5e6D-BBEF-Cab5-0FFE8eB1bAc4", + "Dd4De9b6-7f3a-ED3d-2a84-D60e4Af2Cd9d", + "ae08E8dd-FFdB-cBC1-BA2A-d0aaC61C1B55", + "dcaD88bD4CeaaDC8bceD5d730ffba4cF", + "5a9F9CDAE74F3116a9c848Eeb1C65EA0", + "f53eDCBe3Fb957BE5dBec9322f030F94", + "c6fbBE38-30D7-D7e9-C8C7-72F4a61Bea9c", + "20E5EedA1e3f2aC2dCaADDCa4BFEED5B", + "adEcDf6ac4A999ccecdbCe7E9e01e23C", + "Bf0E2e8C-7e96-f0C8-9fea-1bD5fa9E18ce", + "6349ADdf-8d6B-bF1e-F5AE-13f0B7ca4acE", + "DBD5E149-C967-12b3-Cc6B-6c9220bA32e1", + "4EC19BbD-b556-ED9E-33E7-Eb5Be40AaeDe", + "CE05B39b3FdDad8dDE2b5DebB7CABe7D" + ] + } + ], + "total": 1 +} diff --git a/cypress/support/interceptors.js b/cypress/support/interceptors.js index 64f40d27b..55e7668f2 100644 --- a/cypress/support/interceptors.js +++ b/cypress/support/interceptors.js @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import { DEFAULT_ROW_COUNT } from '@redhat-cloud-services/frontend-components-utilities'; import fixtures from '../fixtures/groups.json'; +import groupDetailFixtures from '../fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json'; export const groupsInterceptors = { 'successful with some items': () => @@ -31,3 +32,30 @@ export const groupsInterceptors = { }).as('getGroups'); } }; + +export const groupDetailInterceptors = { + successful: () => + cy + .intercept('GET', '/api/inventory/v1/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C', groupDetailFixtures) + .as('getGroupDetail'), + empty: () => + cy + .intercept('GET', '/api/inventory/v1/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C', { statusCode: 404 }) + .as('getGroupDetail'), + 'failed with server error': () => { + Cypress.on('uncaught:exception', () => { + return false; + }); + cy.intercept('GET', '/api/inventory/v1/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C', { statusCode: 500 }).as( + 'getGroupDetail' + ); + }, + 'long responding': () => { + cy.intercept('GET', '/api/inventory/v1/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C', (req) => { + req.reply({ + body: groupDetailFixtures, + delay: 42000000 // milliseconds + }); + }).as('getGroupDetail'); + } +}; diff --git a/src/components/InventoryGroupDetail/GroupDetailHeader.js b/src/components/InventoryGroupDetail/GroupDetailHeader.js index 81b8326cc..9ef4e4830 100644 --- a/src/components/InventoryGroupDetail/GroupDetailHeader.js +++ b/src/components/InventoryGroupDetail/GroupDetailHeader.js @@ -5,18 +5,18 @@ import { } from '@redhat-cloud-services/frontend-components'; import React from 'react'; import { useSelector } from 'react-redux'; -import { Link, useParams } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { routes } from '../../Routes'; +import PropTypes from 'prop-types'; -const GroupDetailHeader = () => { +const GroupDetailHeader = ({ groupId }) => { const { uninitialized, loading, data } = useSelector((state) => state.groupDetail); - const { groupId } = useParams(); const nameOrId = uninitialized || loading ? ( ) : ( // in case of error, render just id from URL - data?.name || groupId + data?.results?.[0]?.name || groupId ); return ( @@ -32,4 +32,8 @@ const GroupDetailHeader = () => { ); }; +GroupDetailHeader.propTypes = { + groupId: PropTypes.string.isRequired +}; + export default GroupDetailHeader; diff --git a/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js b/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js new file mode 100644 index 000000000..e56e53713 --- /dev/null +++ b/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js @@ -0,0 +1,57 @@ +import { mount } from '@cypress/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import groupDetailFixtures from '../../../cypress/fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json'; +import { groupDetailInterceptors as interceptors } from '../../../cypress/support/interceptors'; +import { getStore } from '../../store'; +import InventoryGroupDetail from './InventoryGroupDetail'; + +const TEST_GROUP_ID = '620f9ae75A8F6b83d78F3B55Af1c4b2C'; + +const mountPage = () => + mount( + + + + + + ); + +before(() => { + cy.window().then( + (window) => + (window.insights = { + chrome: { + isProd: false, + auth: { + getUser: () => { + return Promise.resolve({}); + } + } + } + }) + ); +}); + +describe('group detail page', () => { + it('name from server is rendered in header and breadcrumb', () => { + interceptors.successful(); + mountPage(); + + cy.wait('@getGroupDetail'); + cy.get('h1').contains(groupDetailFixtures.results[0].name); + cy.get('[data-ouia-component-type="PF4/Breadcrumb"] li') + .last() + .should('have.text', groupDetailFixtures.results[0].name); + }); + + it('skeletons rendered while fetching data', () => { + interceptors['long responding'](); + mountPage(); + + cy.get('[data-ouia-component-type="PF4/Breadcrumb"] li').last().find('.pf-c-skeleton'); + cy.get('h1').find('.pf-c-skeleton'); + cy.get('.pf-c-empty-state').find('.pf-c-spinner'); + }); +}); diff --git a/src/components/InventoryGroupDetail/InventoryGroupDetail.js b/src/components/InventoryGroupDetail/InventoryGroupDetail.js index fc18f6e43..f9eb9dd69 100644 --- a/src/components/InventoryGroupDetail/InventoryGroupDetail.js +++ b/src/components/InventoryGroupDetail/InventoryGroupDetail.js @@ -1,5 +1,4 @@ import React, { useEffect } from 'react'; -import { useParams } from 'react-router-dom'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; import { useDispatch, useSelector } from 'react-redux'; import { fetchGroupDetail } from '../../store/inventory-actions'; @@ -8,21 +7,20 @@ import { useState } from 'react'; import GroupDetailHeader from './GroupDetailHeader'; import GroupDetailSystems from './GroupDetailSystems'; import GroupDetailInfo from './GroupDetailInfo'; +import PropTypes from 'prop-types'; -const InventoryGroupDetail = () => { +const InventoryGroupDetail = ({ groupId }) => { const dispatch = useDispatch(); const { data } = useSelector((state) => state.groupDetail); const chrome = useChrome(); - const { groupId } = useParams(); - useEffect(() => { dispatch(fetchGroupDetail(groupId)); }, []); useEffect(() => { // if available, change ID to the group's name in the window title - chrome.updateDocumentTitle( + chrome?.updateDocumentTitle?.( `${data?.name || groupId} - Inventory Groups | Red Hat Insights` ); }, [data]); @@ -31,7 +29,7 @@ const InventoryGroupDetail = () => { return ( - + { ); }; +InventoryGroupDetail.propTypes = { + groupId: PropTypes.string.isRequired +}; + export default InventoryGroupDetail; diff --git a/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js b/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js new file mode 100644 index 000000000..10a77cc0e --- /dev/null +++ b/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js @@ -0,0 +1,44 @@ +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import GroupDetailHeader from '../GroupDetailHeader'; + +jest.mock('react-redux', () => { + return { + ...jest.requireActual('react-redux'), + useSelector: () => ({ + uninitialized: false, + loading: false, + data: { + results: [ + { + name: 'group-name-1' + } + ] + } + }) + }; +}); + +describe('group detail header', () => { + let getByRole; + + beforeEach(() => { + const rendered = render( + + + + ); + getByRole = rendered.getByRole; + }); + + it('renders title and breadcrumbs', () => { + expect(getByRole('heading')).toBeInTheDocument(); + }); + + it('has breadcrumbs', () => { + expect(getByRole('navigation')).toHaveClass('pf-c-breadcrumb'); + expect(getByRole('navigation')).toHaveTextContent('group-name-1'); + }); +}); diff --git a/src/components/InventoryGroupDetail/__tests__/InventoryGroupDetail.test.js b/src/components/InventoryGroupDetail/__tests__/InventoryGroupDetail.test.js new file mode 100644 index 000000000..44f42fa57 --- /dev/null +++ b/src/components/InventoryGroupDetail/__tests__/InventoryGroupDetail.test.js @@ -0,0 +1,44 @@ +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import InventoryGroupDetail from '../InventoryGroupDetail'; + +jest.mock('react-redux', () => { + return { + ...jest.requireActual('react-redux'), + useSelector: () => ({ + uninitialized: false, + loading: false, + data: { + results: [ + { + name: 'group-name-1' + } + ] + } + }), + useDispatch: () => () => {} + }; +}); + +describe('group detail page component', () => { + let getByRole; + let container; + + beforeEach(() => { + const rendered = render( + + + + ); + getByRole = rendered.getByRole; + container = rendered.container; + }); + + it('renders two tabs', () => { + expect(getByRole('tablist')).toBeInTheDocument(); + expect(container.querySelectorAll('.pf-c-tabs__item')[0]).toHaveTextContent('Systems'); + expect(container.querySelectorAll('.pf-c-tabs__item')[1]).toHaveTextContent('Group info'); + }); +}); diff --git a/src/components/InventoryGroupDetail/index.js b/src/components/InventoryGroupDetail/index.js index a3695ffa0..91b334ef3 100644 --- a/src/components/InventoryGroupDetail/index.js +++ b/src/components/InventoryGroupDetail/index.js @@ -1,3 +1,11 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; import InventoryGroupDetail from './InventoryGroupDetail'; -export default InventoryGroupDetail; +const InventoryGroupDetailWrapper = () => { + const { groupId } = useParams(); + + return ; +}; + +export default InventoryGroupDetailWrapper; From f5e9447a93663fbc362a1c68f1e797ea4c2e7997 Mon Sep 17 00:00:00 2001 From: Georgii Karataev Date: Wed, 22 Feb 2023 12:02:37 +0100 Subject: [PATCH 4/4] Review small fixes --- cypress/support/interceptors.js | 2 +- src/Routes.js | 30 +++-------- .../InventoryGroupDetail.js | 51 ++++++++++++++----- src/components/LostPage.js | 13 +++++ 4 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 src/components/LostPage.js diff --git a/cypress/support/interceptors.js b/cypress/support/interceptors.js index 55e7668f2..0c3f8d2a2 100644 --- a/cypress/support/interceptors.js +++ b/cypress/support/interceptors.js @@ -16,7 +16,7 @@ export const groupsInterceptors = { }) .as('getGroups'), 'failed with server error': () => { - Cypress.on('uncaught:exception', (err, runnable) => { + Cypress.on('uncaught:exception', () => { return false; }); cy.intercept('GET', '/api/inventory/v1/groups*', { statusCode: 500 }).as( diff --git a/src/Routes.js b/src/Routes.js index 94c19011d..f2fa101cb 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -1,10 +1,9 @@ import { Route, Redirect, Switch } from 'react-router-dom'; import React, { lazy, Suspense, useMemo } from 'react'; -import { EmptyState, EmptyStateBody } from '@patternfly/react-core'; -import { InvalidObject } from '@redhat-cloud-services/frontend-components'; import { getSearchParams } from './constants'; import RenderWrapper from './Utilities/Wrapper'; import useFeatureFlag from './Utilities/useFeatureFlag'; +import LostPage from './components/LostPage'; const InventoryTable = lazy(() => import('./routes/InventoryTable')); const InventoryDetail = lazy(() => import('./routes/InventoryDetail')); @@ -40,28 +39,15 @@ export const Routes = () => { ( - - - - - - ) - } + component={groupsEnabled ? InventoryGroups : LostPage} + rootClass="inventory" + /> + - ( - - - - - - )} rootClass='inventory' /> diff --git a/src/components/InventoryGroupDetail/InventoryGroupDetail.js b/src/components/InventoryGroupDetail/InventoryGroupDetail.js index f9eb9dd69..816d81043 100644 --- a/src/components/InventoryGroupDetail/InventoryGroupDetail.js +++ b/src/components/InventoryGroupDetail/InventoryGroupDetail.js @@ -1,14 +1,22 @@ -import React, { useEffect } from 'react'; +import React, { lazy, Suspense, useEffect } from 'react'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; import { useDispatch, useSelector } from 'react-redux'; import { fetchGroupDetail } from '../../store/inventory-actions'; -import { PageSection, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import { + Bullseye, + PageSection, + Spinner, + Tab, + Tabs, + TabTitleText +} from '@patternfly/react-core'; import { useState } from 'react'; import GroupDetailHeader from './GroupDetailHeader'; import GroupDetailSystems from './GroupDetailSystems'; -import GroupDetailInfo from './GroupDetailInfo'; import PropTypes from 'prop-types'; +const GroupDetailInfo = lazy(() => import('./GroupDetailInfo')); + const InventoryGroupDetail = ({ groupId }) => { const dispatch = useDispatch(); const { data } = useSelector((state) => state.groupDetail); @@ -19,18 +27,20 @@ const InventoryGroupDetail = ({ groupId }) => { }, []); useEffect(() => { - // if available, change ID to the group's name in the window title + // if available, change ID to the group's name in the window title chrome?.updateDocumentTitle?.( - `${data?.name || groupId} - Inventory Groups | Red Hat Insights` + `${data?.name || groupId} - Inventory Groups | Red Hat Insights` ); }, [data]); const [activeTabKey, setActiveTabKey] = useState(0); + // TODO: append search parameter to identify the active tab + return ( - + setActiveTabKey(value)} @@ -38,18 +48,35 @@ const InventoryGroupDetail = ({ groupId }) => { role="region" inset={{ default: 'insetMd' }} // add extra space before the first tab (according to mocks) > - Systems} aria-label="Group systems tab"> + Systems} + aria-label="Group systems tab" + > - Group info} aria-label="Group info tab"> - - - + Group info} + aria-label="Group info tab" + > + {activeTabKey === 1 && ( // helps to lazy load the component + + + + + } + > + + + + )} - ); diff --git a/src/components/LostPage.js b/src/components/LostPage.js new file mode 100644 index 000000000..f2cdc6da3 --- /dev/null +++ b/src/components/LostPage.js @@ -0,0 +1,13 @@ +import { EmptyState, EmptyStateBody } from '@patternfly/react-core'; +import { InvalidObject } from '@redhat-cloud-services/frontend-components'; +import React from 'react'; + +const LostPage = () => ( + + + + + +); + +export default LostPage;