Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationEmptyState.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<PageSection variant={PageSectionVariants.light}>
<ResourceLoadFailedEmptyState
resourceLabel={__('job invocation')}
resourceId={jobInvocationId}
errorMessage={errorMessage}
requiredPermissions={['view_job_invocations']}
primaryAction={{
label: __('Go to job invocations'),
onClick: () => 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"
/>
</PageSection>
);

JobInvocationEmptyState.propTypes = {
jobInvocationId: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
};

JobInvocationEmptyState.defaultProps = {
errorMessage: null,
};

export default JobInvocationEmptyState;
35 changes: 35 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
7 changes: 5 additions & 2 deletions webpack/JobInvocationDetail/__tests__/MainInformation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
58 changes: 55 additions & 3 deletions webpack/JobInvocationDetail/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ 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';
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';
Expand All @@ -29,7 +35,7 @@ import {
STATUS_UPPERCASE,
currentPermissionsUrl,
} from './JobInvocationConstants';
import { selectItems } from './JobInvocationSelectors';
import { formatForemanApiError, selectItems } from './JobInvocationSelectors';

const JobInvocationDetailPage = ({
match: {
Expand All @@ -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 => {
Expand Down Expand Up @@ -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 (
<JobInvocationEmptyState
jobInvocationId={id}
errorMessage={backendErrorMessage}
/>
);
}

const pageStatus =
items.id === undefined
? STATUS_UPPERCASE.PENDING
Expand Down
4 changes: 4 additions & 0 deletions webpack/__mocks__/foremanReact/common/helpers.js
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export const foremanUrl = path => `foreman${path}`;

export const visit = jest.fn(url => {
global.window.location.href = url;
});
Loading