Skip to content

Commit da1c2b0

Browse files
committed
Fixes #39253 - Implement "Not Found" error handling for invalid Job Invocation IDs
1 parent c3cd96e commit da1c2b0

4 files changed

Lines changed: 131 additions & 6 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
import { PageSection, PageSectionVariants } from '@patternfly/react-core';
4+
import { translate as __ } from 'foremanReact/common/I18n';
5+
import { foremanUrl, visit } from 'foremanReact/common/helpers';
6+
import ResourceLoadFailedEmptyState from 'foremanReact/components/common/EmptyState/ResourceLoadFailedEmptyState';
7+
8+
const jobInvocationsIndexPath = '/job_invocations';
9+
10+
const JobInvocationEmptyState = ({
11+
jobInvocationId,
12+
httpStatus,
13+
errorMessage,
14+
}) => (
15+
<PageSection variant={PageSectionVariants.light}>
16+
<ResourceLoadFailedEmptyState
17+
resourceLabel={__('job invocation')}
18+
resourceId={jobInvocationId}
19+
httpStatus={httpStatus}
20+
errorMessage={errorMessage}
21+
viewPermissions={['view_job_invocations']}
22+
requiredPermissions={['view_job_invocations']}
23+
primaryAction={{
24+
label: __('Go to job invocations'),
25+
onClick: () => visit(foremanUrl(jobInvocationsIndexPath)),
26+
ouiaId: 'job-invocation-empty-state-go-to-job-invocations-button',
27+
}}
28+
secondaryActions={[
29+
{
30+
label: __('Create a new job invocation'),
31+
onClick: () => visit(foremanUrl('/job_invocations/new')),
32+
ouiaId: 'job-invocation-empty-state-create-new-job-invocation-button',
33+
},
34+
]}
35+
ouiaIdPrefix="job-invocation-empty-state"
36+
/>
37+
</PageSection>
38+
);
39+
40+
JobInvocationEmptyState.propTypes = {
41+
jobInvocationId: PropTypes.string.isRequired,
42+
httpStatus: PropTypes.number,
43+
errorMessage: PropTypes.string,
44+
};
45+
46+
JobInvocationEmptyState.defaultProps = {
47+
httpStatus: null,
48+
errorMessage: null,
49+
};
50+
51+
export default JobInvocationEmptyState;

webpack/JobInvocationDetail/__tests__/MainInformation.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ jest.spyOn(api, 'get');
3535
const originalToLocaleString = Date.prototype.toLocaleString;
3636
beforeAll(() => {
3737
// eslint-disable-next-line no-extend-native
38-
Date.prototype.toLocaleString = function (locale, options) {
39-
return originalToLocaleString.call(this, locale, { ...options, timeZone: 'UTC' });
38+
Date.prototype.toLocaleString = function(locale, options) {
39+
return originalToLocaleString.call(this, locale, {
40+
...options,
41+
timeZone: 'UTC',
42+
});
4043
};
4144
});
4245
afterAll(() => {

webpack/JobInvocationDetail/index.js

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@ import {
55
PageSectionVariants,
66
Skeleton,
77
} from '@patternfly/react-core';
8-
import React, { useEffect, useState } from 'react';
8+
import React, { useEffect, useMemo, useState } from 'react';
99
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
1010
import { useDispatch, useSelector } from 'react-redux';
1111
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
1212
import PropTypes from 'prop-types';
1313
import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
1414
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
1515
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
16+
import { STATUS as API_STATUS } from 'foremanReact/constants';
17+
import {
18+
selectAPIErrorMessage,
19+
selectAPIHttpStatus,
20+
selectAPIStatus,
21+
} from 'foremanReact/redux/API/APISelectors';
1622

1723
import { JobAdditionInfo } from './JobAdditionInfo';
1824
import JobInvocationHostTable from './JobInvocationHostTable';
1925
import JobInvocationOverview from './JobInvocationOverview';
2026
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
27+
import JobInvocationEmptyState from './JobInvocationEmptyState';
2128
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
2229
import { getJobInvocation, getTask } from './JobInvocationActions';
2330
import './JobInvocationDetail.scss';
@@ -51,9 +58,28 @@ const JobInvocationDetailPage = ({
5158
statusLabel === STATUS.SUCCEEDED ||
5259
statusLabel === STATUS.CANCELLED;
5360
const autoRefresh = task?.state === STATUS.PENDING || false;
54-
useAPI('get', currentPermissionsUrl, {
55-
key: CURRENT_PERMISSIONS,
56-
});
61+
const { status: permissionsApiStatus } = useAPI(
62+
'get',
63+
currentPermissionsUrl,
64+
{
65+
key: CURRENT_PERMISSIONS,
66+
}
67+
);
68+
const jobInvocationApiStatus = useSelector(state =>
69+
selectAPIStatus(state, JOB_INVOCATION_KEY)
70+
);
71+
const jobInvocationErrorMessage = useSelector(state =>
72+
selectAPIErrorMessage(state, JOB_INVOCATION_KEY)
73+
);
74+
const permissionsErrorMessage = useSelector(state =>
75+
selectAPIErrorMessage(state, CURRENT_PERMISSIONS)
76+
);
77+
const jobInvocationHttpStatus = useSelector(state =>
78+
selectAPIHttpStatus(state, JOB_INVOCATION_KEY)
79+
);
80+
const permissionsHttpStatus = useSelector(state =>
81+
selectAPIHttpStatus(state, CURRENT_PERMISSIONS)
82+
);
5783
const [selectedFilter, setSelectedFilter] = useState('');
5884

5985
const handleFilterChange = newFilter => {
@@ -88,6 +114,47 @@ const JobInvocationDetailPage = ({
88114
}
89115
}, [dispatch, taskId]);
90116

117+
const apiFailed =
118+
permissionsApiStatus === API_STATUS.ERROR ||
119+
jobInvocationApiStatus === API_STATUS.ERROR;
120+
121+
const backendErrorMessage = useMemo(() => {
122+
const parts = [];
123+
if (jobInvocationApiStatus === API_STATUS.ERROR) {
124+
const msg = jobInvocationErrorMessage;
125+
if (msg) {
126+
parts.push(msg);
127+
}
128+
}
129+
if (permissionsApiStatus === API_STATUS.ERROR) {
130+
const msg = permissionsErrorMessage;
131+
if (msg) {
132+
parts.push(msg);
133+
}
134+
}
135+
return parts.length ? parts.join('\n\n') : null;
136+
}, [
137+
jobInvocationApiStatus,
138+
permissionsApiStatus,
139+
jobInvocationErrorMessage,
140+
permissionsErrorMessage,
141+
]);
142+
143+
const httpStatus =
144+
jobInvocationApiStatus === API_STATUS.ERROR
145+
? jobInvocationHttpStatus
146+
: permissionsHttpStatus;
147+
148+
if (apiFailed) {
149+
return (
150+
<JobInvocationEmptyState
151+
jobInvocationId={id}
152+
httpStatus={httpStatus}
153+
errorMessage={backendErrorMessage}
154+
/>
155+
);
156+
}
157+
91158
const pageStatus =
92159
items.id === undefined
93160
? STATUS_UPPERCASE.PENDING
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export const foremanUrl = path => `foreman${path}`;
2+
3+
export const visit = jest.fn(url => {
4+
global.window.location.href = url;
5+
});

0 commit comments

Comments
 (0)