Merge pull request #9563 from AlexSCorey/8769-WizardFailure

Fixes crashing wizard, and adds error handle on adding role

SUMMARY
This addresses #8769.  It also adds error handling if there is some sort of request error during the submit request.
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME

UI

AWX VERSION
ADDITIONAL INFORMATION

Reviewed-by: John Mitchell <None>
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-03-22 12:16:48 +00:00 committed by GitHub
commit 597435141d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 3 deletions

View File

@ -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();
}
};

View File

@ -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(
<AddResourceRole
onClose={() => {}}
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(
<AddResourceRole onClose={() => {}} 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(() => {

View File

@ -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 && (
<AlertModal
variant="error"
title={i18n._(t`Error!`)}
isOpen={submitError}
onClose={() => setSubmitError(null)}
>
{i18n._(t`Failed to assign roles properly`)}
<ErrorDetail error={submitError} />
</AlertModal>
)}
{deletionError && (
<AlertModal
isOpen={deletionError}