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.map(m => (
+ - {m}
+ ))}
+
+ ) : (
+ 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 (