diff --git a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx index f1c309e959..898eef3609 100644 --- a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx +++ b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx @@ -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 ( @@ -67,7 +61,17 @@ class ErrorDetail extends Component { {response?.config?.method.toUpperCase()} {response?.config?.url}{' '} {response?.status} - {message} + + {Array.isArray(message) ? ( + + ) : ( + message + )} + ); } diff --git a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx index b6499766dc..383c41e4ed 100644 --- a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx +++ b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx @@ -21,4 +21,25 @@ describe('ErrorDetail', () => { ); expect(wrapper).toHaveLength(1); }); + test('testing errors', () => { + const wrapper = mountWithContexts( + + ); + wrapper.find('Expandable').prop('onToggle')(); + wrapper.update(); + }); }); diff --git a/awx/ui_next/src/components/ErrorDetail/getErrorMessage.js b/awx/ui_next/src/components/ErrorDetail/getErrorMessage.js new file mode 100644 index 0000000000..ca6fbb23be --- /dev/null +++ b/awx/ui_next/src/components/ErrorDetail/getErrorMessage.js @@ -0,0 +1,15 @@ +export default function getErrorMessage(response) { + if (!response.data) { + return null; + } + if (typeof response.data === 'string') { + return response.data; + } + if (response.data.detail) { + return response.data.detail; + } + return Object.values(response.data).reduce( + (acc, currentValue) => acc.concat(currentValue), + [] + ); +} diff --git a/awx/ui_next/src/components/ErrorDetail/getErrorMessage.test.js b/awx/ui_next/src/components/ErrorDetail/getErrorMessage.test.js new file mode 100644 index 0000000000..c67728f00b --- /dev/null +++ b/awx/ui_next/src/components/ErrorDetail/getErrorMessage.test.js @@ -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', + ]); + }); +}); diff --git a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx index 3a490db6bc..78e8dc5504 100644 --- a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx @@ -6,9 +6,11 @@ import InventoryStep from './InventoryStep'; import CredentialsStep from './CredentialsStep'; import OtherPromptsStep from './OtherPromptsStep'; import PreviewStep from './PreviewStep'; -import { InventoriesAPI } from '@api'; +import { InventoriesAPI, CredentialsAPI, CredentialTypesAPI } from '@api'; jest.mock('@api/models/Inventories'); +jest.mock('@api/models/CredentialTypes'); +jest.mock('@api/models/Credentials'); let config; const resource = { @@ -25,6 +27,10 @@ describe('LaunchPrompt', () => { count: 1, }, }); + CredentialsAPI.read.mockResolvedValue({ + data: { results: [{ id: 1 }], count: 1 }, + }); + CredentialTypesAPI.loadAllTypes({ data: { results: [{ type: 'ssh' }] } }); config = { can_start_without_user_input: false, diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx index 6819581c93..19ae9a68c9 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx @@ -5,7 +5,7 @@ import { Button, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -function ToolbarAddButton({ linkTo, onClick, i18n }) { +function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) { if (!linkTo && !onClick) { throw new Error( 'ToolbarAddButton requires either `linkTo` or `onClick` prop' @@ -15,6 +15,7 @@ function ToolbarAddButton({ linkTo, onClick, i18n }) { return (