From bdf4c6ac30688e2f76f1e2e164e9e1438e255485 Mon Sep 17 00:00:00 2001 From: Georgy Karataev Date: Thu, 9 Mar 2023 15:29:55 +0100 Subject: [PATCH] feat(ESSNTL-3737, -3735): Rename and delete group (#1780) Implements https://issues.redhat.com/browse/ESSNTL-3737. Implements https://issues.redhat.com/browse/ESSNTL-3735. This makes it possible to rename or delete a group from the group details page. --- cypress/support/interceptors.js | 10 ++ src/components/GroupsTable/GroupsTable.cy.js | 15 ++- .../InventoryGroupDetail/GroupDetailHeader.js | 101 +++++++++++++++--- .../InventoryGroupDetail.cy.js | 37 ++++++- .../__tests__/GroupDetailHeader.test.js | 11 +- 5 files changed, 150 insertions(+), 24 deletions(-) diff --git a/cypress/support/interceptors.js b/cypress/support/interceptors.js index 97bdbbed2..902c6aa86 100644 --- a/cypress/support/interceptors.js +++ b/cypress/support/interceptors.js @@ -60,6 +60,16 @@ export const groupDetailInterceptors = { delay: 42000000 // milliseconds }); }).as('getGroupDetail'); + }, + 'patch successful': () => { + cy + .intercept('PATCH', '/api/inventory/v1/groups/*', { statusCode: 200 }) + .as('patchGroup'); + }, + 'delete successful': () => { + cy + .intercept('DELETE', '/api/inventory/v1/groups/*', { statusCode: 204 }) + .as('deleteGroup'); } }; diff --git a/src/components/GroupsTable/GroupsTable.cy.js b/src/components/GroupsTable/GroupsTable.cy.js index f13df86b1..4a0fba9a0 100644 --- a/src/components/GroupsTable/GroupsTable.cy.js +++ b/src/components/GroupsTable/GroupsTable.cy.js @@ -153,7 +153,7 @@ describe('sorting', () => { interceptors['successful with some items'](); mountTable(); - cy.wait('@getGroups'); // first initial call + cy.wait('@getGroups'); // first initial request }); const checkSorting = (label, order, dataField) => { @@ -187,7 +187,7 @@ describe('filtering', () => { interceptors['successful with some items'](); mountTable(); - cy.wait('@getGroups'); // first initial call + cy.wait('@getGroups'); // first initial request }); const applyNameFilter = () => @@ -201,16 +201,13 @@ describe('filtering', () => { }); it('sends correct request', () => { - applyNameFilter().then(() => { - cy.wait('@getGroups') - .its('request.url') - .should('include', 'hostname_or_id=lorem'); - }); + applyNameFilter(); + cy.wait('@getGroups').its('request.url').should('include', 'hostname_or_id=lorem'); }); it('can remove the chip or reset filters', () => { applyNameFilter(); - cy.wait('@getGroups'); + cy.wait('@getGroups').its('request.url').should('contain', 'hostname_or_id=lorem'); cy.get(CHIP_GROUP) .find(CHIP) .ouiaId('close', 'button') @@ -218,7 +215,7 @@ describe('filtering', () => { cy.get(CHIP_GROUP).find(CHIP).ouiaId('close', 'button'); }); cy.get('button').contains('Reset filters').click(); - cy.wait('@getGroups'); + cy.wait('@getGroups').its('request.url').should('not.contain', 'hostname_or_id'); cy.get(CHIP_GROUP).should('not.exist'); }); diff --git a/src/components/InventoryGroupDetail/GroupDetailHeader.js b/src/components/InventoryGroupDetail/GroupDetailHeader.js index 9ef4e4830..ec8886392 100644 --- a/src/components/InventoryGroupDetail/GroupDetailHeader.js +++ b/src/components/InventoryGroupDetail/GroupDetailHeader.js @@ -1,33 +1,108 @@ -import { Breadcrumb, BreadcrumbItem, Skeleton } from '@patternfly/react-core'; +import { + Breadcrumb, + BreadcrumbItem, + Dropdown, + DropdownItem, + DropdownToggle, + Flex, + FlexItem, + 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 } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useHistory } from 'react-router-dom'; import { routes } from '../../Routes'; import PropTypes from 'prop-types'; +import DeleteGroupModal from '../InventoryGroups/Modals/DeleteGroupModal'; +import RenameGroupModal from '../InventoryGroups/Modals/RenameGroupModal'; +import { fetchGroupDetail } from '../../store/inventory-actions'; const GroupDetailHeader = ({ groupId }) => { - const { uninitialized, loading, data } = useSelector((state) => state.groupDetail); - - const nameOrId = uninitialized || loading ? ( - - ) : ( - // in case of error, render just id from URL - data?.results?.[0]?.name || groupId + const dispatch = useDispatch(); + const { uninitialized, loading, data } = useSelector( + (state) => state.groupDetail ); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [renameModalOpen, setRenameModalOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + + const name = data?.results?.[0]?.name; + const title = + uninitialized || loading ? ( + + ) : ( + name || groupId // in case of error, render just id from URL + ); + + const history = useHistory(); + return ( + setRenameModalOpen(false)} + modalState={{ + id: groupId, + name: name || groupId + }} + reloadData={() => dispatch(fetchGroupDetail(groupId))} + /> + setDeleteModalOpen(false)} + modalState={{ + id: groupId, + name: name || groupId + }} + reloadData={() => history.push('/groups')} + /> Groups - {nameOrId} + {title} - + + + + + + setDropdownOpen(!dropdownOpen)} + autoFocus={false} + isOpen={dropdownOpen} + toggle={ + setDropdownOpen(isOpen)} + toggleVariant="secondary" + isDisabled={uninitialized || loading} + > + Group actions + + } + dropdownItems={[ + setRenameModalOpen(true)} + > + Rename + , + setDeleteModalOpen(true)} + > + Delete + + ]} + /> + + ); }; diff --git a/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js b/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js index b487e971b..ce6c71e54 100644 --- a/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js +++ b/src/components/InventoryGroupDetail/InventoryGroupDetail.cy.js @@ -1,9 +1,10 @@ import { mount } from '@cypress/react'; +import { DROPDOWN, DROPDOWN_ITEM, MODAL } from '@redhat-cloud-services/frontend-components-utilities'; 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 { groupDetailInterceptors as interceptors, groupsInterceptors } from '../../../cypress/support/interceptors'; import { getStore } from '../../store'; import InventoryGroupDetail from './InventoryGroupDetail'; @@ -59,4 +60,38 @@ describe('group detail page', () => { cy.get('h1').find('.pf-c-skeleton'); cy.get('.pf-c-empty-state').find('.pf-c-spinner'); }); + + it('can open rename group modal', () => { + interceptors.successful(); + interceptors['patch successful'](); + groupsInterceptors['successful with some items'](); // intercept modal validation requests + mountPage(); + + cy.wait('@getGroupDetail'); + + cy.get(DROPDOWN).click(); + cy.get(DROPDOWN_ITEM).contains('Rename').click(); + + cy.get(MODAL).find('input').type('1'); + cy.get(MODAL).find('button[type=submit]').click(); + + cy.wait('@patchGroup').its('request.body') + .should('deep.equal', { name: `${groupDetailFixtures.results[0].name}1` }); + cy.wait('@getGroupDetail'); // the page is refreshed after submition + }); + + it('can open delete group modal', () => { + interceptors.successful(); + interceptors['delete successful'](); + mountPage(); + cy.wait('@getGroupDetail'); + + cy.get(DROPDOWN).click(); + cy.get(DROPDOWN_ITEM).contains('Delete').click(); + + cy.get(`div[class="pf-c-check"]`).click(); + cy.get(`button[type="submit"]`).click(); + cy.wait('@deleteGroup').its('request.url') + .should('contain', groupDetailFixtures.results[0].id); + }); }); diff --git a/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js b/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js index 10a77cc0e..ecf4a1527 100644 --- a/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js +++ b/src/components/InventoryGroupDetail/__tests__/GroupDetailHeader.test.js @@ -3,6 +3,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import GroupDetailHeader from '../GroupDetailHeader'; +import { DROPDOWN } from '@redhat-cloud-services/frontend-components-utilities/CypressUtils/selectors'; jest.mock('react-redux', () => { return { @@ -17,12 +18,14 @@ jest.mock('react-redux', () => { } ] } - }) + }), + useDispatch: () => {} }; }); describe('group detail header', () => { let getByRole; + let container; beforeEach(() => { const rendered = render( @@ -31,6 +34,7 @@ describe('group detail header', () => { ); getByRole = rendered.getByRole; + container = rendered.container; }); it('renders title and breadcrumbs', () => { @@ -41,4 +45,9 @@ describe('group detail header', () => { expect(getByRole('navigation')).toHaveClass('pf-c-breadcrumb'); expect(getByRole('navigation')).toHaveTextContent('group-name-1'); }); + + it('renders the actions dropdown', () => { + expect(container.querySelector('#group-header-dropdown')).toHaveTextContent('Group actions'); + expect(container.querySelector(DROPDOWN)).toBeVisible(); + }); });