mirror of
https://github.com/ansible/awx.git
synced 2026-01-15 11:50:42 -03:30
Adds error handling and validation.
Also adresses small PR issues
This commit is contained in:
parent
ed3b6385f1
commit
a95632c349
@ -9,6 +9,7 @@ import {
|
||||
CardBody as PFCardBody,
|
||||
Expandable as PFExpandable,
|
||||
} from '@patternfly/react-core';
|
||||
import getErrorMessage from './getErrorMessage';
|
||||
|
||||
const Card = styled(PFCard)`
|
||||
background-color: var(--pf-global--BackgroundColor--200);
|
||||
@ -52,14 +53,7 @@ class ErrorDetail extends Component {
|
||||
renderNetworkError() {
|
||||
const { error } = this.props;
|
||||
const { response } = error;
|
||||
|
||||
let message = '';
|
||||
if (response?.data) {
|
||||
message =
|
||||
typeof response.data === 'string'
|
||||
? response.data
|
||||
: response.data?.detail;
|
||||
}
|
||||
const message = getErrorMessage(response);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@ -67,7 +61,17 @@ class ErrorDetail extends Component {
|
||||
{response?.config?.method.toUpperCase()} {response?.config?.url}{' '}
|
||||
<strong>{response?.status}</strong>
|
||||
</CardBody>
|
||||
<CardBody>{message}</CardBody>
|
||||
<CardBody>
|
||||
{Array.isArray(message) ? (
|
||||
<ul>
|
||||
{message.map(m => (
|
||||
<li key={m}>{m}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
message
|
||||
)}
|
||||
</CardBody>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,4 +21,26 @@ describe('ErrorDetail', () => {
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
test('testing errors', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ErrorDetail
|
||||
error={
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'patch',
|
||||
},
|
||||
data: {
|
||||
project: ['project error'],
|
||||
inventory: ['inventory error'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
wrapper.find('Expandable').prop('onToggle')();
|
||||
wrapper.update();
|
||||
// console.log(wrapper.find('ErrorDetail').prop('error'));
|
||||
});
|
||||
});
|
||||
|
||||
15
awx/ui_next/src/components/ErrorDetail/getErrorMessage.js
Normal file
15
awx/ui_next/src/components/ErrorDetail/getErrorMessage.js
Normal file
@ -0,0 +1,15 @@
|
||||
export default function getErrorMessage(response) {
|
||||
if (typeof response.data === 'string') {
|
||||
return response.data;
|
||||
}
|
||||
if (!response.data) {
|
||||
return null;
|
||||
}
|
||||
if (response.data.detail) {
|
||||
return response.data.detail;
|
||||
}
|
||||
return Object.values(response.data).reduce(
|
||||
(acc, currentValue) => acc.concat(currentValue),
|
||||
[]
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import getErrorMessage from './getErrorMessage';
|
||||
|
||||
describe('getErrorMessage', () => {
|
||||
test('should return data string', () => {
|
||||
const response = {
|
||||
data: 'error response',
|
||||
};
|
||||
expect(getErrorMessage(response)).toEqual('error response');
|
||||
});
|
||||
test('should return detail string', () => {
|
||||
const response = {
|
||||
data: {
|
||||
detail: 'detail string',
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(response)).toEqual('detail string');
|
||||
});
|
||||
test('should return an array of strings', () => {
|
||||
const response = {
|
||||
data: {
|
||||
project: ['project error response'],
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(response)).toEqual(['project error response']);
|
||||
});
|
||||
test('should consolidate error messages from multiple keys into an array', () => {
|
||||
const response = {
|
||||
data: {
|
||||
project: ['project error response'],
|
||||
inventory: ['inventory error response'],
|
||||
organization: ['org error response'],
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(response)).toEqual([
|
||||
'project error response',
|
||||
'inventory error response',
|
||||
'org error response',
|
||||
]);
|
||||
});
|
||||
test('should handle no response.data', () => {
|
||||
const response = {};
|
||||
expect(getErrorMessage(response)).toEqual(null);
|
||||
});
|
||||
test('should consolidate multiple error messages from multiple keys into an array', () => {
|
||||
const response = {
|
||||
data: {
|
||||
project: ['project error response'],
|
||||
inventory: [
|
||||
'inventory error response',
|
||||
'another inventory error response',
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(getErrorMessage(response)).toEqual([
|
||||
'project error response',
|
||||
'inventory error response',
|
||||
'another inventory error response',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -322,6 +322,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
className="pf-c-backdrop"
|
||||
>
|
||||
<FocusTrap
|
||||
_createFocusTrap={[Function]}
|
||||
active={true}
|
||||
className="pf-l-bullseye"
|
||||
focusTrapOptions={
|
||||
@ -330,6 +331,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
}
|
||||
}
|
||||
paused={false}
|
||||
tag="div"
|
||||
>
|
||||
<div
|
||||
className="pf-l-bullseye"
|
||||
|
||||
@ -22,7 +22,7 @@ function SurveyList({
|
||||
toggleSurvey,
|
||||
updateSurvey,
|
||||
deleteSurvey,
|
||||
canAddAndEditSurvey,
|
||||
canEdit,
|
||||
i18n,
|
||||
}) {
|
||||
const questions = survey?.spec || [];
|
||||
@ -98,7 +98,7 @@ function SurveyList({
|
||||
onSelect={() => handleSelect(question)}
|
||||
onMoveUp={moveUp}
|
||||
onMoveDown={moveDown}
|
||||
canAddAndEditSurvey={canAddAndEditSurvey}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
))}
|
||||
{isPreviewModalOpen && (
|
||||
@ -171,7 +171,7 @@ function SurveyList({
|
||||
surveyEnabled={surveyEnabled}
|
||||
onToggleSurvey={toggleSurvey}
|
||||
isDeleteDisabled={selected?.length === 0}
|
||||
canAddAndEditSurvey={canAddAndEditSurvey}
|
||||
canEdit={canEdit}
|
||||
onToggleDeleteModal={() => setIsDeleteModalOpen(true)}
|
||||
/>
|
||||
{content}
|
||||
|
||||
@ -54,11 +54,7 @@ describe('<SurveyList />', () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SurveyList
|
||||
survey={surveyData}
|
||||
deleteSurvey={deleteSurvey}
|
||||
canAddAndEditSurvey
|
||||
/>
|
||||
<SurveyList survey={surveyData} deleteSurvey={deleteSurvey} canEdit />
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
@ -4,7 +4,8 @@ import { withI18n } from '@lingui/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Button as _Button,
|
||||
Chip as _Chip,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
DataListAction as _DataListAction,
|
||||
DataListCheck,
|
||||
DataListItemCells,
|
||||
@ -29,11 +30,8 @@ const Button = styled(_Button)`
|
||||
padding-left: 0;
|
||||
`;
|
||||
const Required = styled.span`
|
||||
color: red;
|
||||
margin-left: 5px;
|
||||
`;
|
||||
const Chip = styled(_Chip)`
|
||||
margin-right: 5px;
|
||||
color: var(--pf-global--danger-color--100);
|
||||
margin-left: var(--pf-global--spacer--xs);
|
||||
`;
|
||||
|
||||
const Label = styled.b`
|
||||
@ -41,7 +39,7 @@ const Label = styled.b`
|
||||
`;
|
||||
|
||||
function SurveyListItem({
|
||||
canAddAndEditSurvey,
|
||||
canEdit,
|
||||
question,
|
||||
i18n,
|
||||
isLast,
|
||||
@ -67,7 +65,7 @@ function SurveyListItem({
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`move up`)}
|
||||
isDisabled={isFirst || !canAddAndEditSurvey}
|
||||
isDisabled={isFirst || !canEdit}
|
||||
onClick={() => onMoveUp(question)}
|
||||
>
|
||||
<CaretUpIcon />
|
||||
@ -77,7 +75,7 @@ function SurveyListItem({
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`move down`)}
|
||||
isDisabled={isLast || !canAddAndEditSurvey}
|
||||
isDisabled={isLast || !canEdit}
|
||||
onClick={() => onMoveDown(question)}
|
||||
>
|
||||
<CaretDownIcon />
|
||||
@ -86,7 +84,7 @@ function SurveyListItem({
|
||||
</Stack>
|
||||
</DataListAction>
|
||||
<DataListCheck
|
||||
isDisabled={!canAddAndEditSurvey}
|
||||
isDisabled={!canEdit}
|
||||
checked={isChecked}
|
||||
onChange={onSelect}
|
||||
aria-labelledby="survey check"
|
||||
@ -120,12 +118,15 @@ function SurveyListItem({
|
||||
<span>{i18n._(t`encrypted`).toUpperCase()}</span>
|
||||
)}
|
||||
{[question.type].includes('multiselect') &&
|
||||
question.default.length > 0 &&
|
||||
question.default.split('\n').map(chip => (
|
||||
<Chip key={chip} isReadOnly>
|
||||
{chip}
|
||||
</Chip>
|
||||
))}
|
||||
question.default.length > 0 && (
|
||||
<ChipGroup numChips={5}>
|
||||
{question.default.split('\n').map(chip => (
|
||||
<Chip key={chip} isReadOnly>
|
||||
{chip}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
)}
|
||||
{![question.type].includes('password') &&
|
||||
![question.type].includes('multiselect') && (
|
||||
<span>{question.default}</span>
|
||||
|
||||
@ -96,9 +96,10 @@ describe('<SurveyListItem />', () => {
|
||||
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('Chip').length).toBe(9);
|
||||
expect(wrapper.find('Chip').length).toBe(6);
|
||||
wrapper
|
||||
.find('Chip')
|
||||
.filter(chip => chip.prop('isOverFlowChip') !== true)
|
||||
.map(chip => expect(chip.prop('isReadOnly')).toBe(true));
|
||||
});
|
||||
test('items that are no required should have no an asterisk', () => {
|
||||
|
||||
@ -13,7 +13,13 @@ import FormField, {
|
||||
FieldTooltip,
|
||||
} from '@components/FormField';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import { required, noWhiteSpace, combine } from '@util/validators';
|
||||
import {
|
||||
required,
|
||||
noWhiteSpace,
|
||||
combine,
|
||||
maxLength,
|
||||
defaultIsNotAvailable,
|
||||
} from '@util/validators';
|
||||
|
||||
function AnswerTypeField({ i18n }) {
|
||||
const [field] = useField({
|
||||
@ -156,6 +162,7 @@ function SurveyQuestionForm({
|
||||
<FormField
|
||||
id="question-default"
|
||||
name="default"
|
||||
validate={maxLength(formik.values.max, i18n)}
|
||||
type={formik.values.type === 'text' ? 'text' : 'number'}
|
||||
label={i18n._(t`Default answer`)}
|
||||
/>
|
||||
@ -191,6 +198,7 @@ function SurveyQuestionForm({
|
||||
<FormField
|
||||
id="question-default"
|
||||
name="default"
|
||||
validate={defaultIsNotAvailable(formik.values.choices, i18n)}
|
||||
type={
|
||||
formik.values.type === 'multiplechoice'
|
||||
? 'text'
|
||||
|
||||
@ -18,7 +18,7 @@ const DataToolbar = styled(_DataToolbar)`
|
||||
`;
|
||||
|
||||
function SurveyToolbar({
|
||||
canAddAndEditSurvey,
|
||||
canEdit,
|
||||
isAllSelected,
|
||||
onSelectAll,
|
||||
i18n,
|
||||
@ -27,14 +27,14 @@ function SurveyToolbar({
|
||||
isDeleteDisabled,
|
||||
onToggleDeleteModal,
|
||||
}) {
|
||||
isDeleteDisabled = !canAddAndEditSurvey || isDeleteDisabled;
|
||||
isDeleteDisabled = !canEdit || isDeleteDisabled;
|
||||
const match = useRouteMatch();
|
||||
return (
|
||||
<DataToolbar id="survey-toolbar">
|
||||
<DataToolbarContent>
|
||||
<DataToolbarItem>
|
||||
<Checkbox
|
||||
isDisabled={!canAddAndEditSurvey}
|
||||
isDisabled={!canEdit}
|
||||
isChecked={isAllSelected}
|
||||
onChange={isChecked => {
|
||||
onSelectAll(isChecked);
|
||||
@ -50,14 +50,14 @@ function SurveyToolbar({
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
isChecked={surveyEnabled}
|
||||
isDisabled={!canAddAndEditSurvey}
|
||||
isDisabled={!canEdit}
|
||||
onChange={() => onToggleSurvey(!surveyEnabled)}
|
||||
/>
|
||||
</DataToolbarItem>
|
||||
<DataToolbarGroup>
|
||||
<DataToolbarItem>
|
||||
<ToolbarAddButton
|
||||
isDisabled={!canAddAndEditSurvey}
|
||||
isDisabled={!canEdit}
|
||||
linkTo={`${match.url}/add`}
|
||||
/>
|
||||
</DataToolbarItem>
|
||||
|
||||
@ -36,7 +36,7 @@ describe('<SurveyToolbar />', () => {
|
||||
isAllSelected
|
||||
onToggleDeleteModal={jest.fn()}
|
||||
onToggleSurvey={jest.fn()}
|
||||
canAddAndEditSurvey
|
||||
canEdit
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -96,7 +96,7 @@ describe('<SurveyToolbar />', () => {
|
||||
isAllSelected
|
||||
onToggleDelete={jest.fn()}
|
||||
onToggleSurvey={jest.fn()}
|
||||
canAddAndEditSurvey={false}
|
||||
canEdit={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@ -205,7 +205,7 @@ function Template({ i18n, me, setBreadcrumb }) {
|
||||
<Route path="/templates/:templateType/:id/survey">
|
||||
<TemplateSurvey
|
||||
template={template}
|
||||
canAddAndEditSurvey={canAddAndEditSurvey}
|
||||
canEdit={canAddAndEditSurvey}
|
||||
/>
|
||||
</Route>
|
||||
)}
|
||||
|
||||
@ -9,7 +9,7 @@ import ErrorDetail from '@components/ErrorDetail';
|
||||
import useRequest, { useDismissableError } from '@util/useRequest';
|
||||
import { SurveyList, SurveyQuestionAdd, SurveyQuestionEdit } from './Survey';
|
||||
|
||||
function TemplateSurvey({ template, canAddAndEditSurvey, i18n }) {
|
||||
function TemplateSurvey({ template, canEdit, i18n }) {
|
||||
const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled);
|
||||
|
||||
const { templateType } = useParams();
|
||||
@ -85,7 +85,7 @@ function TemplateSurvey({ template, canAddAndEditSurvey, i18n }) {
|
||||
return (
|
||||
<>
|
||||
<Switch>
|
||||
{canAddAndEditSurvey && (
|
||||
{canEdit && (
|
||||
<Route path="/templates/:templateType/:id/survey/add">
|
||||
<SurveyQuestionAdd
|
||||
survey={survey}
|
||||
@ -93,7 +93,7 @@ function TemplateSurvey({ template, canAddAndEditSurvey, i18n }) {
|
||||
/>
|
||||
</Route>
|
||||
)}
|
||||
{canAddAndEditSurvey && (
|
||||
{canEdit && (
|
||||
<Route path="/templates/:templateType/:id/survey/edit/:variable">
|
||||
<SurveyQuestionEdit
|
||||
survey={survey}
|
||||
@ -109,7 +109,7 @@ function TemplateSurvey({ template, canAddAndEditSurvey, i18n }) {
|
||||
toggleSurvey={toggleSurvey}
|
||||
updateSurvey={updateSurveySpec}
|
||||
deleteSurvey={deleteSurvey}
|
||||
canAddAndEditSurvey={canAddAndEditSurvey}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
@ -274,7 +274,7 @@ class WorkflowJobTemplate extends Component {
|
||||
<Route path="/templates/:templateType/:id/survey">
|
||||
<TemplateSurvey
|
||||
template={template}
|
||||
canAddAndEditSurvey={canAddAndEditSurvey}
|
||||
canEdit={canAddAndEditSurvey}
|
||||
/>
|
||||
</Route>
|
||||
)}
|
||||
|
||||
@ -48,6 +48,7 @@ describe('<WorkflowJobTemplate/>', () => {
|
||||
{ name: 'Label 3', id: 3 },
|
||||
],
|
||||
},
|
||||
user_capabilities: {},
|
||||
},
|
||||
related: {
|
||||
webhook_key: '/api/v2/workflow_job_templates/57/webhook_key/',
|
||||
|
||||
@ -36,6 +36,17 @@ export function minMaxValue(min, max, i18n) {
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultIsNotAvailable(choices, i18n) {
|
||||
return defaultValue => {
|
||||
if (!choices.includes(defaultValue)) {
|
||||
return i18n._(
|
||||
t`Default choice must be answered from the choices listed.`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function requiredEmail(i18n) {
|
||||
return value => {
|
||||
if (!value) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user