diff --git a/awx/ui_next/src/components/ContentError/ContentError.jsx b/awx/ui_next/src/components/ContentError/ContentError.jsx
index e3723dc48d..a94adc4ac9 100644
--- a/awx/ui_next/src/components/ContentError/ContentError.jsx
+++ b/awx/ui_next/src/components/ContentError/ContentError.jsx
@@ -1,6 +1,8 @@
import React from 'react';
-import { t } from '@lingui/macro';
import styled from 'styled-components';
+import { Link } from 'react-router-dom';
+import { bool, instanceOf } from 'prop-types';
+import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import {
Title,
@@ -9,30 +11,53 @@ import {
EmptyStateBody,
} from '@patternfly/react-core';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
-
+import { RootAPI } from '@api';
import ErrorDetail from '@components/ErrorDetail';
const EmptyState = styled(PFEmptyState)`
width: var(--pf-c-empty-state--m-lg--MaxWidth);
`;
-class ContentError extends React.Component {
- render() {
- const { error, i18n } = this.props;
- return (
-
-
- {i18n._(t`Something went wrong...`)}
-
- {i18n._(
- t`There was an error loading this content. Please reload the page.`
- )}
-
- {error && }
-
- );
- }
+async function logout() {
+ await RootAPI.logout();
+ window.location.replace('/#/login');
}
+function ContentError({ error, children, isNotFound, i18n }) {
+ if (error && error.response && error.response.status === 401) {
+ if (!error.response.headers['session-timeout']) {
+ logout();
+ return null;
+ }
+ }
+ const is404 =
+ isNotFound || (error && error.response && error.response.status === 404);
+ return (
+
+
+
+ {is404 ? i18n._(t`Not Found`) : i18n._(t`Something went wrong...`)}
+
+
+ {is404
+ ? i18n._(t`The page you requested could not be found.`)
+ : i18n._(
+ t`There was an error loading this content. Please reload the page.`
+ )}{' '}
+ {children || {i18n._(t`Back to Dashboard.`)}}
+
+ {error && }
+
+ );
+}
+ContentError.propTypes = {
+ error: instanceOf(Error),
+ isNotFound: bool,
+};
+ContentError.defaultProps = {
+ error: null,
+ isNotFound: false,
+};
+
export { ContentError as _ContentError };
export default withI18n()(ContentError);
diff --git a/awx/ui_next/src/index.jsx b/awx/ui_next/src/index.jsx
index dfec0c4c49..9fd10daf47 100644
--- a/awx/ui_next/src/index.jsx
+++ b/awx/ui_next/src/index.jsx
@@ -32,6 +32,7 @@ import License from '@screens/License';
import Teams from '@screens/Team';
import Templates from '@screens/Template';
import Users from '@screens/User';
+import NotFound from '@screens/NotFound';
import App from './App';
import RootProvider from './RootProvider';
@@ -224,8 +225,8 @@ export function main(render) {
],
},
]}
- render={({ routeGroups }) =>
- routeGroups
+ render={({ routeGroups }) => {
+ const routeList = routeGroups
.reduce(
(allRoutes, { routes }) => allRoutes.concat(routes),
[]
@@ -238,8 +239,16 @@ export function main(render) {
)}
/>
- ))
- }
+ ));
+ routeList.push(
+
+ );
+ return {routeList};
+ }}
/>
)}
/>
diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx
index 313f051cad..c3150ab15a 100644
--- a/awx/ui_next/src/screens/Job/Job.jsx
+++ b/awx/ui_next/src/screens/Job/Job.jsx
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
-import { Route, withRouter, Switch, Redirect } from 'react-router-dom';
+import { Route, withRouter, Switch, Redirect, Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
@@ -99,7 +99,14 @@ class Job extends Component {
return (
-
+
+ {contentError.response.status === 404 && (
+
+ {i18n._(`The page you requested could not be found.`)}{' '}
+ {i18n._(`View all Jobs.`)}
+
+ )}
+
);
@@ -139,6 +146,19 @@ class Job extends Component {
path="/jobs/:type/:id/output"
render={() => }
/>,
+ (
+
+
+ {i18n._(`View Job Details`)}
+
+
+ )}
+ />,
]}
diff --git a/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx b/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx
index 20ba8b1bc0..e2eeb57725 100644
--- a/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx
+++ b/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx
@@ -1,8 +1,13 @@
import React, { Component } from 'react';
-import { Redirect } from 'react-router-dom';
+import { Redirect, Link } from 'react-router-dom';
+import { PageSection, Card } from '@patternfly/react-core';
+import { withI18n } from '@lingui/react';
import { UnifiedJobsAPI } from '@api';
+import ContentError from '@components/ContentError';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
+const NOT_FOUND = 'not found';
+
class JobTypeRedirect extends Component {
static defaultProps = {
view: 'details',
@@ -12,8 +17,9 @@ class JobTypeRedirect extends Component {
super(props);
this.state = {
- hasError: false,
+ error: null,
job: null,
+ isLoading: true,
};
this.loadJob = this.loadJob.bind(this);
}
@@ -24,24 +30,44 @@ class JobTypeRedirect extends Component {
async loadJob() {
const { id } = this.props;
+ this.setState({ isLoading: true });
try {
const { data } = await UnifiedJobsAPI.read({ id });
+ const job = data.results[0];
this.setState({
- job: data.results[0],
+ job,
+ isLoading: false,
+ error: job ? null : NOT_FOUND,
+ });
+ } catch (error) {
+ this.setState({
+ error,
+ isLoading: false,
});
- } catch (err) {
- this.setState({ hasError: true });
}
}
render() {
- const { path, view } = this.props;
- const { hasError, job } = this.state;
+ const { path, view, i18n } = this.props;
+ const { error, job, isLoading } = this.state;
- if (hasError) {
- return Error
;
+ if (error) {
+ return (
+
+
+ {error === NOT_FOUND ? (
+
+ {i18n._(`View all Jobs`)}
+
+ ) : (
+
+ )}
+
+
+ );
}
- if (!job) {
+ if (isLoading) {
+ // TODO show loading state
return Loading...
;
}
const type = JOB_TYPE_URL_SEGMENTS[job.type];
@@ -49,4 +75,4 @@ class JobTypeRedirect extends Component {
}
}
-export default JobTypeRedirect;
+export default withI18n()(JobTypeRedirect);
diff --git a/awx/ui_next/src/screens/NotFound.jsx b/awx/ui_next/src/screens/NotFound.jsx
new file mode 100644
index 0000000000..f09a8fdba9
--- /dev/null
+++ b/awx/ui_next/src/screens/NotFound.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { PageSection, Card } from '@patternfly/react-core';
+import ContentError from '@components/ContentError';
+
+function NotFound() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default NotFound;
diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx
index 9727bdc411..69e08354dd 100644
--- a/awx/ui_next/src/screens/Organization/Organization.jsx
+++ b/awx/ui_next/src/screens/Organization/Organization.jsx
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-import { Switch, Route, withRouter, Redirect } from 'react-router-dom';
+import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
import {
Card,
CardHeader as PFCardHeader,
@@ -168,7 +168,16 @@ class Organization extends Component {
return (
-
+
+ {contentError.response.status === 404 && (
+
+ {i18n._(`Organization not found.`)}{' '}
+
+ {i18n._(`View all Organizations.`)}
+
+
+ )}
+
);
@@ -226,6 +235,20 @@ class Organization extends Component {
)}
/>
)}
+ (
+
+ {match.params.id && (
+
+ {i18n._(`View Organization Details`)}
+
+ )}
+
+ )}
+ />
+ ,
diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx
index 147bd9eec2..4f943126f5 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx
@@ -149,7 +149,7 @@ class OrganizationsList extends Component {
-
+
+ {contentError.response.status === 404 && (
+
+ {i18n._(`Template not found.`)}{' '}
+ {i18n._(`View all Templates.`)}
+
+ )}
+
);
@@ -92,8 +99,9 @@ class Template extends Component {
to="/templates/:templateType/:id/details"
exact
/>
- {template && (
+ {template && [
(
)}
- />
- )}
- {template && (
+ />,
}
- />
- )}
+ />,
+ (
+
+ {match.params.id && (
+
+ {i18n._(`View Template Details`)}
+
+ )}
+
+ )}
+ />,
+ ]}
diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
index dec5055c60..05d86493ae 100644
--- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
+++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx
@@ -175,7 +175,7 @@ class TemplatesList extends Component {