From 5971a84e74e594a651f68ec2d7d3382d837bcc88 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 11 Mar 2021 11:50:17 -0500 Subject: [PATCH] fixes crashing wizard, and adds error handle on adding role --- .../components/AddRole/AddResourceRole.jsx | 16 ++- .../AddRole/AddResourceRole.test.jsx | 112 ++++++++++++++++++ .../ResourceAccessList/ResourceAccessList.jsx | 14 +++ 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index e339142b52..2b4fbccd9d 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -1,5 +1,6 @@ -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import SelectableCard from '../SelectableCard'; @@ -17,7 +18,9 @@ const readTeams = async queryParams => TeamsAPI.read(queryParams); const readTeamsOptions = async () => TeamsAPI.readOptions(); -function AddResourceRole({ onSave, onClose, roles, i18n, resource }) { +function AddResourceRole({ onSave, onClose, roles, i18n, resource, onError }) { + const history = useHistory(); + const [selectedResource, setSelectedResource] = useState(null); const [selectedResourceRows, setSelectedResourceRows] = useState([]); const [selectedRoleRows, setSelectedRoleRows] = useState([]); @@ -39,6 +42,12 @@ function AddResourceRole({ onSave, onClose, roles, i18n, resource }) { } }; + useEffect(() => { + if (currentStepId === 1 && maxEnabledStep > 1) { + history.push(history.location.pathname); + } + }, [currentStepId, history, maxEnabledStep]); + const handleRoleCheckboxClick = role => { const selectedIndex = selectedRoleRows.findIndex( selectedRow => selectedRow.id === role.id @@ -94,7 +103,8 @@ function AddResourceRole({ onSave, onClose, roles, i18n, resource }) { await Promise.all(roleRequests); onSave(); } catch (err) { - // TODO: handle this error + onError(err); + onClose(); } }; diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx index 264f7cdb28..5d995f9e09 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.test.jsx @@ -1,6 +1,7 @@ /* eslint-disable react/jsx-pascal-case */ import React from 'react'; import { shallow } from 'enzyme'; +import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; import { @@ -13,6 +14,11 @@ import { TeamsAPI, UsersAPI } from '../../api'; jest.mock('../../api/models/Teams'); jest.mock('../../api/models/Users'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + + useHistory: () => ({ push: jest.fn(), location: { pathname: {} } }), +})); // TODO: Once error handling is functional in // this component write tests for it @@ -111,6 +117,112 @@ describe('<_AddResourceRole />', () => { expect(UsersAPI.associateRole).toBeCalledWith(1, 1); }); + test('should call on error properly', async () => { + let wrapper; + const onError = jest.fn(); + UsersAPI.associateRole.mockRejectedValue( + new Error({ + response: { + config: { + method: 'post', + url: '/api/v2/users', + }, + data: 'An error occurred', + status: 403, + }, + }) + ); + act(() => { + wrapper = mountWithContexts( + {}} + onError={onError} + onSave={() => {}} + roles={roles} + />, + { context: { network: { handleHttpError: () => {} } } } + ); + }); + wrapper.update(); + + // Step 1 + const selectableCardWrapper = wrapper.find('SelectableCard'); + expect(selectableCardWrapper.length).toBe(2); + act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')()); + wrapper.update(); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + wrapper.update(); + + // Step 2 + await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + act(() => + wrapper.find('DataListCheck[name="foo"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('DataListCheck[name="foo"]').prop('checked')).toBe( + true + ); + act(() => wrapper.find('Button[type="submit"]').prop('onClick')()); + wrapper.update(); + + // Step 3 + act(() => + wrapper.find('Checkbox[aria-label="Admin"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('Checkbox[aria-label="Admin"]').prop('isChecked')).toBe( + true + ); + + // Save + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + expect(UsersAPI.associateRole).toBeCalledWith(1, 1); + expect(onError).toBeCalled(); + }); + + test('should should update history properly', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['organizations/2/access?resource.order_by=-username'], + }); + act(() => { + wrapper = mountWithContexts( + {}} onSave={() => {}} roles={roles} />, + { context: { router: { history } } } + ); + }); + wrapper.update(); + + // Step 1 + const selectableCardWrapper = wrapper.find('SelectableCard'); + expect(selectableCardWrapper.length).toBe(2); + act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')()); + wrapper.update(); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + wrapper.update(); + + // Step 2 + await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + act(() => + wrapper.find('DataListCheck[name="foo"]').invoke('onChange')(true) + ); + wrapper.update(); + expect(wrapper.find('DataListCheck[name="foo"]').prop('checked')).toBe( + true + ); + await act(async () => + wrapper.find('PFWizard').prop('onGoToStep')({ id: 1 }) + ); + wrapper.update(); + expect(history.location.pathname).toEqual('organizations/2/access'); + }); + test('should successfuly click user/team cards', async () => { let wrapper; act(() => { diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx index 1cc04c5fa3..f43a7fa8de 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx @@ -11,6 +11,7 @@ import { getQSConfig, parseQueryString } from '../../util/qs'; import useRequest, { useDeleteItems } from '../../util/useRequest'; import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal'; import ResourceAccessListItem from './ResourceAccessListItem'; +import ErrorDetail from '../ErrorDetail'; const QS_CONFIG = getQSConfig('access', { page: 1, @@ -19,6 +20,7 @@ const QS_CONFIG = getQSConfig('access', { }); function ResourceAccessList({ i18n, apiModel, resource }) { + const [submitError, setSubmitError] = useState(null); const [deletionRecord, setDeletionRecord] = useState(null); const [deletionRole, setDeletionRole] = useState(null); const [showAddModal, setShowAddModal] = useState(false); @@ -206,6 +208,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) { setShowAddModal(false); fetchAccessRecords(); }} + onError={err => setSubmitError(err)} roles={resource.summary_fields.object_roles} resource={resource} /> @@ -227,6 +230,17 @@ function ResourceAccessList({ i18n, apiModel, resource }) { }} /> )} + {submitError && ( + setSubmitError(null)} + > + {i18n._(t`Failed to assign roles properly`)} + + + )} {deletionError && (