Skip to content

Commit fcd128a

Browse files
sbernhardcodex
andcommitted
Fixes #39463 - show bulk actions error toasts correctly
Co-authored-by: Codex <codex@openai.com>
1 parent 34ba887 commit fcd128a

10 files changed

Lines changed: 358 additions & 61 deletions

File tree

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
TreeView,
1313
} from '@patternfly/react-core';
1414
import { addToast } from '../../../ToastsList/slice';
15+
import { buildBulkRequestBody, bulkErrorToastParams } from '../helpers';
1516
import { translate as __ } from '../../../../common/I18n';
1617
import { STATUS } from '../../../../constants';
1718
import {
@@ -32,7 +33,6 @@ import {
3233
MODAL_TYPES,
3334
} from './BulkAssignTaxonomyConstants';
3435
import TaxonomySelect from './TaxonomySelect';
35-
import { buildBulkRequestBody } from '../helpers';
3636

3737
export const BulkAssignOrganizationModal = props => (
3838
<BulkAssignTaxonomyModal modalType={MODAL_TYPES.ORGANIZATION} {...props} />
@@ -53,6 +53,9 @@ const BulkAssignTaxonomyModal = ({
5353
onSuccess: onSuccessCallback,
5454
}) => {
5555
const org = modalType === MODAL_TYPES.ORGANIZATION;
56+
const actionKey = org
57+
? BULK_ASSIGN_ORGANIZATION_KEY
58+
: BULK_ASSIGN_LOCATION_KEY;
5659
const taxType = org ? 'organization' : 'location';
5760
const dispatch = useDispatch();
5861
const [taxId, setTaxId] = useState('');
@@ -69,9 +72,7 @@ const BulkAssignTaxonomyModal = ({
6972
: selectAPIStatus(state, LOCATION_KEY)
7073
);
7174
const hostUpdateStatus = useSelector(state =>
72-
org
73-
? selectAPIStatus(state, BULK_ASSIGN_ORGANIZATION_KEY)
74-
: selectAPIStatus(state, BULK_ASSIGN_LOCATION_KEY)
75+
selectAPIStatus(state, actionKey)
7576
);
7677
const handleModalClose = () => {
7778
setTaxId('');
@@ -106,15 +107,8 @@ const BulkAssignTaxonomyModal = ({
106107
taxonomy.results.find(t => t.id === id)?.name;
107108

108109
const handleError = error => {
109-
const {
110-
response: {
111-
data: {
112-
error: { message },
113-
},
114-
},
115-
} = error;
116-
dispatch(addToast({ type: 'danger', message }));
117110
handleModalClose();
111+
dispatch(addToast(bulkErrorToastParams(error, actionKey)));
118112
};
119113

120114
const handleSuccess = response => {

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/buildHosts/BulkBuildHostModal.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@patternfly/react-core';
1313
import { addToast } from '../../../ToastsList/slice';
1414
import { translate as __ } from '../../../../common/I18n';
15-
import { buildBulkRequestBody, failedHostsToastParams } from '../helpers';
15+
import { buildBulkRequestBody, bulkErrorToastParams } from '../helpers';
1616
import { STATUS } from '../../../../constants';
1717
import { selectAPIStatus } from '../../../../redux/API/APISelectors';
1818
import { bulkBuildHosts, HOST_BUILD_KEY } from './actions';
@@ -37,13 +37,9 @@ const BulkBuildHostModal = ({
3737
closeModal();
3838
};
3939

40-
const handleError = ({ response }) => {
40+
const handleError = error => {
4141
handleModalClose();
42-
dispatch(
43-
addToast(
44-
failedHostsToastParams({ ...response.data.error, key: HOST_BUILD_KEY })
45-
)
46-
);
42+
dispatch(addToast(bulkErrorToastParams(error, HOST_BUILD_KEY)));
4743
};
4844
const handleSave = () => {
4945
const requestBody = buildBulkRequestBody({

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/changeOwner/BulkChangeOwnerModal.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
selectAPIStatus,
2929
selectAPIResponse,
3030
} from '../../../../redux/API/APISelectors';
31-
import { buildBulkRequestBody, failedHostsToastParams } from '../helpers';
31+
import { buildBulkRequestBody, bulkErrorToastParams } from '../helpers';
3232

3333
const BulkChangeOwnerModal = ({
3434
isOpen,
@@ -99,16 +99,9 @@ const BulkChangeOwnerModal = ({
9999
closeModal();
100100
};
101101

102-
const handleError = response => {
102+
const handleError = error => {
103103
handleModalClose();
104-
dispatch(
105-
addToast(
106-
failedHostsToastParams({
107-
...response.data.error,
108-
key: BULK_CHANGE_OWNER_KEY,
109-
})
110-
)
111-
);
104+
dispatch(addToast(bulkErrorToastParams(error, BULK_CHANGE_OWNER_KEY)));
112105
};
113106

114107
const handleSuccess = response => {

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/disassociate/BulkDisassociateModal.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { addToast } from '../../../ToastsList/slice';
1313
import { translate as __ } from '../../../../common/I18n';
1414
import { BULK_DISASSOCIATE_KEY, bulkDisassociate } from './actions';
15-
import { buildBulkRequestBody, failedHostsToastParams } from '../helpers';
15+
import { buildBulkRequestBody, bulkErrorToastParams } from '../helpers';
1616

1717
const BulkDisassociateModal = ({
1818
isOpen,
@@ -56,16 +56,9 @@ const BulkDisassociateModal = ({
5656
},
5757
];
5858

59-
const handleError = response => {
59+
const handleError = error => {
6060
closeModal();
61-
dispatch(
62-
addToast(
63-
failedHostsToastParams({
64-
...response.data.error,
65-
key: BULK_DISASSOCIATE_KEY,
66-
})
67-
)
68-
);
61+
dispatch(addToast(bulkErrorToastParams(error, BULK_DISASSOCIATE_KEY)));
6962
};
7063

7164
const handleSuccess = response => {

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/helpers.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,16 @@ export const failedHostsToastParams = ({
5252

5353
return toastParams;
5454
};
55+
56+
export const bulkErrorToastParams = (error, key) => {
57+
const fallback = error?.message || __('Unexpected error occurred.');
58+
const apiError = error?.response?.data?.error;
59+
const isObject = apiError && typeof apiError === 'object';
60+
const message = isObject ? apiError.message : apiError;
61+
62+
return failedHostsToastParams({
63+
message: message || fallback,
64+
failed_host_ids: isObject ? apiError.failed_host_ids : undefined,
65+
key,
66+
});
67+
};
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { buildBulkRequestBody, bulkErrorToastParams } from './helpers';
2+
3+
jest.mock('../../../common/I18n');
4+
5+
describe('BulkActions helpers', () => {
6+
describe('bulkErrorToastParams', () => {
7+
it('uses API error details from the response', () => {
8+
const toast = bulkErrorToastParams(
9+
{
10+
response: {
11+
data: {
12+
error: {
13+
message: 'Some hosts failed',
14+
failed_host_ids: [1, 2],
15+
},
16+
},
17+
},
18+
},
19+
'BULK_ACTION_KEY'
20+
);
21+
22+
expect(toast).toMatchObject({
23+
type: 'danger',
24+
message: 'Some hosts failed',
25+
});
26+
expect(toast.link.children).toBe('Failed hosts');
27+
expect(toast.link.href).toContain('/new/hosts');
28+
expect(toast.link.href).toContain('search=');
29+
});
30+
31+
it('uses the error message when no response exists', () => {
32+
const toast = bulkErrorToastParams(
33+
{ message: 'Network Error' },
34+
'BULK_ACTION_KEY'
35+
);
36+
37+
expect(toast).toMatchObject({
38+
type: 'danger',
39+
message: 'Network Error',
40+
});
41+
expect(toast.link).toBeUndefined();
42+
});
43+
44+
it('uses a response error string as the message', () => {
45+
const toast = bulkErrorToastParams(
46+
{
47+
response: {
48+
data: {
49+
error: 'Bulk action failed',
50+
},
51+
},
52+
},
53+
'BULK_ACTION_KEY'
54+
);
55+
56+
expect(toast).toMatchObject({
57+
type: 'danger',
58+
message: 'Bulk action failed',
59+
});
60+
expect(toast.link).toBeUndefined();
61+
});
62+
63+
it('falls back to a generic message when no usable message exists', () => {
64+
const toast = bulkErrorToastParams({}, 'BULK_ACTION_KEY');
65+
66+
expect(toast).toMatchObject({
67+
type: 'danger',
68+
message: 'Unexpected error occurred.',
69+
});
70+
expect(toast.link).toBeUndefined();
71+
});
72+
73+
it('keeps failed hosts link when response error has no message', () => {
74+
const toast = bulkErrorToastParams(
75+
{
76+
response: {
77+
data: {
78+
error: {
79+
failed_host_ids: [1, 2],
80+
},
81+
},
82+
},
83+
},
84+
'BULK_ACTION_KEY'
85+
);
86+
87+
expect(toast).toMatchObject({
88+
type: 'danger',
89+
message: 'Unexpected error occurred.',
90+
});
91+
expect(toast.link.children).toBe('Failed hosts');
92+
});
93+
});
94+
95+
describe('buildBulkRequestBody', () => {
96+
it('builds an included search from fetchBulkParams', () => {
97+
const body = buildBulkRequestBody({
98+
fetchBulkParams: () => 'id ^ (1,2,3)',
99+
reboot: true,
100+
});
101+
102+
expect(body).toEqual({
103+
included: {
104+
search: 'id ^ (1,2,3)',
105+
},
106+
reboot: true,
107+
});
108+
});
109+
110+
it('adds taxonomy params when present', () => {
111+
const body = buildBulkRequestBody({
112+
fetchBulkParams: () => 'id ^ (1,2,3)',
113+
organizationId: 1,
114+
locationId: 2,
115+
});
116+
117+
expect(body).toMatchObject({
118+
included: {
119+
search: 'id ^ (1,2,3)',
120+
},
121+
organization_id: 1,
122+
location_id: 2,
123+
});
124+
});
125+
126+
it('uses includedSearch instead of calling fetchBulkParams', () => {
127+
const fetchBulkParams = jest.fn();
128+
129+
const body = buildBulkRequestBody({
130+
fetchBulkParams,
131+
includedSearch: 'name ~ test',
132+
});
133+
134+
expect(body).toEqual({
135+
included: {
136+
search: 'name ~ test',
137+
},
138+
});
139+
expect(fetchBulkParams).not.toHaveBeenCalled();
140+
});
141+
});
142+
});

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/manageNotifications/BulkManageNotificationsModal.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { FormattedMessage } from 'react-intl';
1515
import { translate as __ } from '../../../../common/I18n';
1616
import { bulkManageNotifications } from './actions';
1717
import { BULK_MANAGE_NOTIFICATIONS_KEY } from './constants';
18-
import { failedHostsToastParams } from '../helpers';
18+
import { bulkErrorToastParams } from '../helpers';
1919
import { addToast } from '../../../ToastsList/slice';
2020

2121
const BulkManageNotificationsModal = ({
@@ -48,17 +48,9 @@ const BulkManageNotificationsModal = ({
4848
};
4949

5050
const handleError = error => {
51-
const apiError = error?.response?.data?.error;
52-
if (apiError) {
53-
dispatch(
54-
addToast(
55-
failedHostsToastParams({
56-
...apiError,
57-
key: BULK_MANAGE_NOTIFICATIONS_KEY,
58-
})
59-
)
60-
);
61-
}
51+
dispatch(
52+
addToast(bulkErrorToastParams(error, BULK_MANAGE_NOTIFICATIONS_KEY))
53+
);
6254
handleModalClose();
6355
};
6456

webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/powerState/BulkPowerStateModal.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import './BulkPowerStateModal.scss';
1919
import { HostsPowerRefreshContext } from '../../HostsPowerRefreshContext';
2020
import { POWER_STATES, BULK_POWER_STATE_KEY } from './constants';
2121
import { bulkChangePowerState } from './actions';
22-
import { buildBulkRequestBody, failedHostsToastParams } from '../helpers';
22+
import {
23+
buildBulkRequestBody,
24+
failedHostsToastParams,
25+
bulkErrorToastParams,
26+
} from '../helpers';
2327
import { addToast } from '../../../ToastsList/slice';
2428

2529
const BulkPowerStateModal = ({
@@ -61,8 +65,9 @@ const BulkPowerStateModal = ({
6165

6266
const handleError = error => {
6367
const apiError = error?.response?.data?.error;
68+
const isObject = apiError && typeof apiError === 'object';
6469

65-
if (apiError) {
70+
if (isObject) {
6671
let enhancedError = apiError;
6772

6873
if (apiError.failed_hosts && apiError.failed_hosts.length > 0) {
@@ -86,6 +91,8 @@ const BulkPowerStateModal = ({
8691
})
8792
)
8893
);
94+
} else {
95+
dispatch(addToast(bulkErrorToastParams(error, BULK_POWER_STATE_KEY)));
8996
}
9097

9198
cleanup();

0 commit comments

Comments
 (0)