diff --git a/webpack/JobInvocationDetail/JobInvocationEmptyState.js b/webpack/JobInvocationDetail/JobInvocationEmptyState.js
new file mode 100644
index 000000000..9bc10919d
--- /dev/null
+++ b/webpack/JobInvocationDetail/JobInvocationEmptyState.js
@@ -0,0 +1,44 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { PageSection, PageSectionVariants } from '@patternfly/react-core';
+import { translate as __ } from 'foremanReact/common/I18n';
+import { foremanUrl, visit } from 'foremanReact/common/helpers';
+import ResourceLoadFailedEmptyState from 'foremanReact/components/common/EmptyState/ResourceLoadFailedEmptyState';
+
+const jobInvocationsIndexPath = '/job_invocations';
+
+const JobInvocationEmptyState = ({ jobInvocationId, errorMessage }) => (
+
+ visit(foremanUrl(jobInvocationsIndexPath)),
+ ouiaId: 'job-invocation-empty-state-go-to-job-invocations-button',
+ }}
+ secondaryActions={[
+ {
+ label: __('Create a new job invocation'),
+ onClick: () => visit(foremanUrl('/job_invocations/new')),
+ ouiaId: 'job-invocation-empty-state-create-new-job-invocation-button',
+ },
+ ]}
+ backButtonLabel={__('Return to the last page')}
+ ouiaIdPrefix="job-invocation-empty-state"
+ />
+
+);
+
+JobInvocationEmptyState.propTypes = {
+ jobInvocationId: PropTypes.string.isRequired,
+ errorMessage: PropTypes.string,
+};
+
+JobInvocationEmptyState.defaultProps = {
+ errorMessage: null,
+};
+
+export default JobInvocationEmptyState;
diff --git a/webpack/JobInvocationDetail/JobInvocationSelectors.js b/webpack/JobInvocationDetail/JobInvocationSelectors.js
index eca70541a..a7ce1f084 100644
--- a/webpack/JobInvocationDetail/JobInvocationSelectors.js
+++ b/webpack/JobInvocationDetail/JobInvocationSelectors.js
@@ -42,3 +42,38 @@ export const selectHasPermission = permissionRequired => state => {
)
: false;
};
+
+export const formatForemanApiError = apiFailureResponse => {
+ if (!apiFailureResponse) {
+ return null;
+ }
+ const { response, message } = apiFailureResponse;
+ const err = response?.data?.error;
+ if (err) {
+ if (Array.isArray(err.full_messages) && err.full_messages.length) {
+ return err.full_messages.join(' ');
+ }
+ if (typeof err.details === 'string' && err.details.trim()) {
+ return err.details.trim();
+ }
+ if (
+ Array.isArray(err.missing_permissions) &&
+ err.missing_permissions.length
+ ) {
+ return err.missing_permissions.join(', ');
+ }
+ if (typeof err.message === 'string' && err.message) {
+ return err.message;
+ }
+ if (typeof err === 'string') {
+ return err;
+ }
+ }
+ if (typeof response?.data?.message === 'string') {
+ return response.data.message;
+ }
+ if (message) {
+ return message;
+ }
+ return null;
+};
diff --git a/webpack/JobInvocationDetail/__tests__/MainInformation.test.js b/webpack/JobInvocationDetail/__tests__/MainInformation.test.js
index e3e62c0dc..47536f754 100644
--- a/webpack/JobInvocationDetail/__tests__/MainInformation.test.js
+++ b/webpack/JobInvocationDetail/__tests__/MainInformation.test.js
@@ -35,8 +35,11 @@ jest.spyOn(api, 'get');
const originalToLocaleString = Date.prototype.toLocaleString;
beforeAll(() => {
// eslint-disable-next-line no-extend-native
- Date.prototype.toLocaleString = function (locale, options) {
- return originalToLocaleString.call(this, locale, { ...options, timeZone: 'UTC' });
+ Date.prototype.toLocaleString = function(locale, options) {
+ return originalToLocaleString.call(this, locale, {
+ ...options,
+ timeZone: 'UTC',
+ });
};
});
afterAll(() => {
diff --git a/webpack/JobInvocationDetail/index.js b/webpack/JobInvocationDetail/index.js
index 6b266241b..5439d86e9 100644
--- a/webpack/JobInvocationDetail/index.js
+++ b/webpack/JobInvocationDetail/index.js
@@ -5,7 +5,7 @@ import {
PageSectionVariants,
Skeleton,
} from '@patternfly/react-core';
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { translate as __, documentLocale } from 'foremanReact/common/I18n';
import { useDispatch, useSelector } from 'react-redux';
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -13,11 +13,17 @@ import PropTypes from 'prop-types';
import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
+import { STATUS as API_STATUS } from 'foremanReact/constants';
+import {
+ selectAPIResponse,
+ selectAPIStatus,
+} from 'foremanReact/redux/API/APISelectors';
import { JobAdditionInfo } from './JobAdditionInfo';
import JobInvocationHostTable from './JobInvocationHostTable';
import JobInvocationOverview from './JobInvocationOverview';
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
+import JobInvocationEmptyState from './JobInvocationEmptyState';
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
import { getJobInvocation, getTask } from './JobInvocationActions';
import './JobInvocationDetail.scss';
@@ -29,7 +35,7 @@ import {
STATUS_UPPERCASE,
currentPermissionsUrl,
} from './JobInvocationConstants';
-import { selectItems } from './JobInvocationSelectors';
+import { formatForemanApiError, selectItems } from './JobInvocationSelectors';
const JobInvocationDetailPage = ({
match: {
@@ -51,9 +57,20 @@ const JobInvocationDetailPage = ({
statusLabel === STATUS.SUCCEEDED ||
statusLabel === STATUS.CANCELLED;
const autoRefresh = task?.state === STATUS.PENDING || false;
- useAPI('get', currentPermissionsUrl, {
+ const {
+ status: permissionsApiStatus,
+ response: permissionsApiResponse,
+ } = useAPI('get', currentPermissionsUrl, {
key: CURRENT_PERMISSIONS,
});
+ const jobInvocationApiStatus = useSelector(state =>
+ selectAPIStatus(state, JOB_INVOCATION_KEY)
+ );
+ const jobInvocationApiErrorPayload = useSelector(state =>
+ jobInvocationApiStatus === API_STATUS.ERROR
+ ? selectAPIResponse(state, JOB_INVOCATION_KEY)
+ : null
+ );
const [selectedFilter, setSelectedFilter] = useState('');
const handleFilterChange = newFilter => {
@@ -88,6 +105,41 @@ const JobInvocationDetailPage = ({
}
}, [dispatch, taskId]);
+ const apiFailed =
+ permissionsApiStatus === API_STATUS.ERROR ||
+ jobInvocationApiStatus === API_STATUS.ERROR;
+
+ const backendErrorMessage = useMemo(() => {
+ const parts = [];
+ if (jobInvocationApiStatus === API_STATUS.ERROR) {
+ const msg = formatForemanApiError(jobInvocationApiErrorPayload);
+ if (msg) {
+ parts.push(msg);
+ }
+ }
+ if (permissionsApiStatus === API_STATUS.ERROR) {
+ const msg = formatForemanApiError(permissionsApiResponse);
+ if (msg) {
+ parts.push(msg);
+ }
+ }
+ return parts.length ? parts.join('\n\n') : null;
+ }, [
+ jobInvocationApiStatus,
+ permissionsApiStatus,
+ jobInvocationApiErrorPayload,
+ permissionsApiResponse,
+ ]);
+
+ if (apiFailed) {
+ return (
+
+ );
+ }
+
const pageStatus =
items.id === undefined
? STATUS_UPPERCASE.PENDING
diff --git a/webpack/__mocks__/foremanReact/common/helpers.js b/webpack/__mocks__/foremanReact/common/helpers.js
index c0e38c44f..2939da4de 100644
--- a/webpack/__mocks__/foremanReact/common/helpers.js
+++ b/webpack/__mocks__/foremanReact/common/helpers.js
@@ -1 +1,5 @@
export const foremanUrl = path => `foreman${path}`;
+
+export const visit = jest.fn(url => {
+ global.window.location.href = url;
+});