Skip to content

Commit 236a8c6

Browse files
committed
Fixes #39256 - reviewer feedback added
1 parent 4ee0bf8 commit 236a8c6

3 files changed

Lines changed: 142 additions & 73 deletions

File tree

Lines changed: 61 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,85 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
33
import { useHistory } from 'react-router-dom';
4-
import { translate as __, sprintf } from 'foremanReact/common/I18n';
5-
import { foremanUrl } from 'foremanReact/common/helpers';
64
import {
7-
Bullseye,
85
Button,
9-
EmptyState,
10-
EmptyStateBody,
11-
EmptyStateIcon,
6+
EmptyStateVariant,
127
PageSection,
138
PageSectionVariants,
14-
EmptyStateVariant,
15-
EmptyStateHeader,
16-
EmptyStateActions,
17-
EmptyStateFooter,
189
} from '@patternfly/react-core';
1910
import { SearchIcon } from '@patternfly/react-icons';
11+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
12+
import { foremanUrl } from 'foremanReact/common/helpers';
13+
import EmptyStatePattern from 'foremanReact/components/common/EmptyState/EmptyStatePattern';
2014

2115
const jobInvocationsIndexPath = '/job_invocations';
2216

23-
const JobInvocationEmptyState = ({ jobInvocationId }) => {
17+
const JobInvocationEmptyState = ({ jobInvocationId, errorMessage = null }) => {
2418
const history = useHistory();
19+
20+
const descriptionContent = (
21+
<>
22+
<p>
23+
{sprintf(
24+
__(
25+
'There is no job invocation with id %s or there are access permissions needed. Opening this page requires the view_job_invocations permission. Please contact your administrator if this issue continues.'
26+
),
27+
jobInvocationId
28+
)}
29+
</p>
30+
{errorMessage ? (
31+
<p className="pf-v5-u-text-break" style={{ whiteSpace: 'pre-wrap' }}>
32+
{sprintf(__('The server returned: %s'), errorMessage)}
33+
</p>
34+
) : null}
35+
</>
36+
);
37+
2538
return (
2639
<PageSection variant={PageSectionVariants.light}>
27-
<Bullseye>
28-
<EmptyState
29-
ouiaId="job-invocation-empty-state"
30-
variant={EmptyStateVariant.lg}
31-
>
32-
<EmptyStateIcon icon={SearchIcon} />
33-
<EmptyStateHeader
34-
titleText={<>{sprintf(__('Job invocation not found'))}</>}
35-
headingLevel="h5"
36-
/>
37-
38-
<EmptyStateBody>
39-
{sprintf(
40-
__(
41-
'There is no job invocation with id %s or there are access permissions needed. Please contact your administrator if this issue continues.'
42-
),
43-
jobInvocationId
44-
)}
45-
</EmptyStateBody>
46-
<EmptyStateFooter>
47-
<EmptyStateActions>
48-
<Button
49-
ouiaId="job-invocation-empty-state-go-to-job-invocations-button"
50-
variant="primary"
51-
component="a"
52-
href={foremanUrl(jobInvocationsIndexPath)}
53-
isInline
54-
>
55-
{__('Go to job invocations')}
56-
</Button>
57-
</EmptyStateActions>
58-
<EmptyStateActions>
59-
<Button
60-
ouiaId="job-invocation-empty-state-create-new-job-invocation-button"
61-
variant="link"
62-
component="a"
63-
href={foremanUrl('/job_invocations/new')}
64-
>
65-
{__('Create a new job invocation')}
66-
</Button>
67-
<Button
68-
ouiaId="job-invocation-empty-state-return-to-last-page-button"
69-
variant="link"
70-
onClick={() => history.goBack()}
71-
>
72-
{__('Return to the last page')}
73-
</Button>
74-
</EmptyStateActions>
75-
</EmptyStateFooter>
76-
</EmptyState>
77-
</Bullseye>
40+
<EmptyStatePattern
41+
variant={EmptyStateVariant.lg}
42+
icon={<SearchIcon />}
43+
header={__('Job invocation not found')}
44+
description={descriptionContent}
45+
action={
46+
<Button
47+
ouiaId="job-invocation-empty-state-go-to-job-invocations-button"
48+
variant="primary"
49+
component="a"
50+
href={foremanUrl(jobInvocationsIndexPath)}
51+
isInline
52+
>
53+
{__('Go to job invocations')}
54+
</Button>
55+
}
56+
secondaryActions={
57+
<>
58+
<Button
59+
ouiaId="job-invocation-empty-state-create-new-job-invocation-button"
60+
variant="link"
61+
component="a"
62+
href={foremanUrl('/job_invocations/new')}
63+
>
64+
{__('Create a new job invocation')}
65+
</Button>
66+
<Button
67+
ouiaId="job-invocation-empty-state-return-to-last-page-button"
68+
variant="link"
69+
onClick={() => history.goBack()}
70+
>
71+
{__('Return to the last page')}
72+
</Button>
73+
</>
74+
}
75+
/>
7876
</PageSection>
7977
);
8078
};
8179

8280
JobInvocationEmptyState.propTypes = {
8381
jobInvocationId: PropTypes.string.isRequired,
82+
errorMessage: PropTypes.string,
8483
};
8584

8685
export default JobInvocationEmptyState;

webpack/JobInvocationDetail/JobInvocationSelectors.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,39 @@ export const selectHasPermission = permissionRequired => state => {
4242
)
4343
: false;
4444
};
45+
46+
export const formatForemanApiError = apiFailureResponse => {
47+
if (!apiFailureResponse) {
48+
return null;
49+
}
50+
const { response, message } = apiFailureResponse;
51+
const err = response?.data?.error;
52+
if (err) {
53+
if (Array.isArray(err.full_messages) && err.full_messages.length) {
54+
return err.full_messages.join(' ');
55+
}
56+
// API access_denied template exposes details + missing_permissions (see api/v2/errors/access_denied.json.rabl)
57+
if (typeof err.details === 'string' && err.details.trim()) {
58+
return err.details.trim();
59+
}
60+
if (
61+
Array.isArray(err.missing_permissions) &&
62+
err.missing_permissions.length
63+
) {
64+
return err.missing_permissions.join(', ');
65+
}
66+
if (typeof err.message === 'string' && err.message) {
67+
return err.message;
68+
}
69+
if (typeof err === 'string') {
70+
return err;
71+
}
72+
}
73+
if (typeof response?.data?.message === 'string') {
74+
return response.data.message;
75+
}
76+
if (message) {
77+
return message;
78+
}
79+
return null;
80+
};

webpack/JobInvocationDetail/index.js

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ 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';
@@ -14,7 +14,10 @@ import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
1414
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
1515
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
1616
import { STATUS as API_STATUS } from 'foremanReact/constants';
17-
import { selectAPIStatus } from 'foremanReact/redux/API/APISelectors';
17+
import {
18+
selectAPIResponse,
19+
selectAPIStatus,
20+
} from 'foremanReact/redux/API/APISelectors';
1821

1922
import { JobAdditionInfo } from './JobAdditionInfo';
2023
import JobInvocationHostTable from './JobInvocationHostTable';
@@ -32,7 +35,7 @@ import {
3235
STATUS_UPPERCASE,
3336
currentPermissionsUrl,
3437
} from './JobInvocationConstants';
35-
import { selectItems } from './JobInvocationSelectors';
38+
import { formatForemanApiError, selectItems } from './JobInvocationSelectors';
3639

3740
const JobInvocationDetailPage = ({
3841
match: {
@@ -54,16 +57,20 @@ const JobInvocationDetailPage = ({
5457
statusLabel === STATUS.SUCCEEDED ||
5558
statusLabel === STATUS.CANCELLED;
5659
const autoRefresh = task?.state === STATUS.PENDING || false;
57-
const { status: permissionsApiStatus } = useAPI(
58-
'get',
59-
currentPermissionsUrl,
60-
{
61-
key: CURRENT_PERMISSIONS,
62-
}
63-
);
60+
const {
61+
status: permissionsApiStatus,
62+
response: permissionsApiResponse,
63+
} = useAPI('get', currentPermissionsUrl, {
64+
key: CURRENT_PERMISSIONS,
65+
});
6466
const jobInvocationApiStatus = useSelector(state =>
6567
selectAPIStatus(state, JOB_INVOCATION_KEY)
6668
);
69+
const jobInvocationApiErrorPayload = useSelector(state =>
70+
jobInvocationApiStatus === API_STATUS.ERROR
71+
? selectAPIResponse(state, JOB_INVOCATION_KEY)
72+
: null
73+
);
6774
const [selectedFilter, setSelectedFilter] = useState('');
6875

6976
const handleFilterChange = newFilter => {
@@ -102,8 +109,35 @@ const JobInvocationDetailPage = ({
102109
permissionsApiStatus === API_STATUS.ERROR ||
103110
jobInvocationApiStatus === API_STATUS.ERROR;
104111

112+
const backendErrorMessage = useMemo(() => {
113+
const parts = [];
114+
if (jobInvocationApiStatus === API_STATUS.ERROR) {
115+
const msg = formatForemanApiError(jobInvocationApiErrorPayload);
116+
if (msg) {
117+
parts.push(msg);
118+
}
119+
}
120+
if (permissionsApiStatus === API_STATUS.ERROR) {
121+
const msg = formatForemanApiError(permissionsApiResponse);
122+
if (msg) {
123+
parts.push(msg);
124+
}
125+
}
126+
return parts.length ? parts.join('\n\n') : null;
127+
}, [
128+
jobInvocationApiStatus,
129+
permissionsApiStatus,
130+
jobInvocationApiErrorPayload,
131+
permissionsApiResponse,
132+
]);
133+
105134
if (apiFailed) {
106-
return <JobInvocationEmptyState jobInvocationId={id} />;
135+
return (
136+
<JobInvocationEmptyState
137+
jobInvocationId={id}
138+
errorMessage={backendErrorMessage}
139+
/>
140+
);
107141
}
108142

109143
const pageStatus =

0 commit comments

Comments
 (0)