Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect } from 'react-router-dom';
import { translate as __, sprintf } from '../../../common/I18n';
import { foremanUrl } from '../../../common/helpers';
import { translate as __ } from '../../../common/I18n';
import { visit, foremanUrl } from '../../../common/helpers';
import { ResourceLoadFailedEmptyState } from '../../common/EmptyState';
import {
useForemanHostsPageUrl,
useForemanSettings,
} from '../../../Root/Context/ForemanContext';

const RedirectToEmptyHostPage = ({ hostname }) => (
<Redirect
to={{
pathname: '/page-not-found',
state: {
back: true,
header: __('No host found'),
body: sprintf(
__(
`The host %s does not exist or there are access permissions needed. Please contact your administrator if this issue continues.`
),
hostname
),
action: {
title: __('All hosts'),
url: foremanUrl('/hosts'),
const HostDetailsEmptyState = ({ hostname, httpStatus, errorMessage }) => {
const { displayNewHostsPage } = useForemanSettings();
const hostsIndexUrl = useForemanHostsPageUrl();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from an AI review yool: useForemanHostsPageUrl() can return /hosts (a Rails route) when displayNewHostsPage is false. Using history.push on that URL would navigate inside the SPA to a non-existent React route, rendering nothing. The old code used foremanUrl() for full-page navigation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like we check for displayNewHostsPage setting elsewhere, so I'd do the same approach here


const hostsIndexAction = displayNewHostsPage
? { url: hostsIndexUrl }
: { onClick: () => visit(foremanUrl(hostsIndexUrl)) };

return (
<ResourceLoadFailedEmptyState
resourceLabel={__('host')}
resourceId={hostname}
httpStatus={httpStatus}
errorMessage={errorMessage}
viewPermissions={['view_hosts']}
primaryAction={{
label: __('Back to all hosts'),
...hostsIndexAction,
ouiaId: 'host-details-empty-state-all-hosts',
}}
requiredPermissions={['view_hosts', 'create_hosts']}
secondaryActions={[
{
label: __('Create a new host'),
onClick: () => visit(foremanUrl('/hosts/new')),
ouiaId: 'host-details-empty-state-create-host',
},
secondayActions: [
{
title: __('Create a host'),
url: foremanUrl('/hosts/new'),
},
],
},
}}
/>
);
]}
ouiaIdPrefix="host-details-empty-state"
/>
);
};

RedirectToEmptyHostPage.propTypes = {
HostDetailsEmptyState.propTypes = {
hostname: PropTypes.string.isRequired,
httpStatus: PropTypes.number,
errorMessage: PropTypes.string,
};

HostDetailsEmptyState.defaultProps = {
httpStatus: null,
errorMessage: null,
};

export default RedirectToEmptyHostPage;
export default HostDetailsEmptyState;
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,24 @@ import {
import { selectIsCollapsed } from '../Layout/LayoutSelectors';
import ActionsBar from './ActionsBar';
import { registerCoreTabs } from './Tabs';
import { HOST_DETAILS_API_OPTIONS, TABS_SLOT_ID } from './consts';
import {
HOST_DETAILS_API_OPTIONS,
HOST_DETAILS_KEY,
TABS_SLOT_ID,
} from './consts';

import { translate as __, sprintf } from '../../common/I18n';
import HostGlobalStatus from './Status/GlobalStatus';
import SkeletonLoader from '../common/SkeletonLoader';
import { STATUS } from '../../constants';
import './HostDetails.scss';
import { useAPI } from '../../common/hooks/API/APIHooks';
import {
selectAPIErrorMessage,
selectAPIHttpStatus,
} from '../../redux/API/APISelectors';
import TabRouter from './Tabs/TabRouter';
import RedirectToEmptyHostPage from './EmptyState';
import HostDetailsEmptyState from './EmptyState';
import BreadcrumbBar from '../BreadcrumbBar';
import { CardExpansionContextWrapper } from './CardExpansionContext';
import Head from '../Head';
Expand All @@ -60,6 +68,12 @@ const HostDetails = ({
`/api/hosts/${id}?show_hidden_parameters=true`,
HOST_DETAILS_API_OPTIONS
);
const httpStatus = useSelector(state =>
selectAPIHttpStatus(state, HOST_DETAILS_KEY)
);
const errorMessage = useSelector(state =>
selectAPIErrorMessage(state, HOST_DETAILS_KEY)
);
const isNavCollapsed = useSelector(selectIsCollapsed);
const hostsIndexUrl = useForemanHostsPageUrl();
const tabs = useSelector(
Expand Down Expand Up @@ -91,7 +105,14 @@ const HostDetails = ({
tab => !slotMetadata?.[tab]?.hideTab?.({ hostDetails: response })
) ?? [];

if (status === STATUS.ERROR) return <RedirectToEmptyHostPage hostname={id} />;
if (status === STATUS.ERROR)
return (
<HostDetailsEmptyState
hostname={id}
httpStatus={httpStatus}
errorMessage={errorMessage}
/>
);
return (
<>
<Head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const DefaultEmptyState = props => {
documentation,
action,
secondaryActions,
variant,
} = props;

const dispatch = useDispatch();
Expand Down Expand Up @@ -55,6 +56,7 @@ const DefaultEmptyState = props => {
documentation={documentation}
action={ActionButton}
secondaryActions={SecondaryButton}
variant={variant}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,31 @@ export const defaultEmptyStatePropTypes = {
action: PropTypes.shape(actionButtonPropTypes),
secondaryActions: PropTypes.arrayOf(PropTypes.shape(actionButtonPropTypes)),
};

export const footerActionPropTypes = {
label: PropTypes.node.isRequired,
onClick: PropTypes.func,
url: PropTypes.string,
ouiaId: PropTypes.string,
};

export const resourceLoadFailedEmptyStatePropTypes = {
resourceLabel: PropTypes.string.isRequired,
resourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
header: PropTypes.string,
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
errorMessage: PropTypes.string,
/** HTTP status from the failed load request (e.g. axios error.response.status). */
httpStatus: PropTypes.number,
/** Permissions required to view the resource; used with usePermissions and httpStatus. */
viewPermissions: PropTypes.arrayOf(PropTypes.string),
/** Optional page-level permissions shown as documentation when load did not fail for access. */
requiredPermissions: PropTypes.arrayOf(PropTypes.string),
primaryAction: PropTypes.shape(footerActionPropTypes).isRequired,
secondaryActions: PropTypes.arrayOf(PropTypes.shape(footerActionPropTypes)),
showBackButton: PropTypes.bool,
backButtonLabel: PropTypes.string,
icon: PropTypes.node,
variant: PropTypes.oneOf(['xs', 'sm', 'lg', 'xl', 'full']),
ouiaIdPrefix: PropTypes.string,
};
Loading
Loading