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 {