Error page (#2321)
* generic error page * redirect from errorboundary * clean compile * wip redirect from error boundry * error page displaying * ensure i18n strings are preserved * Updating error page UI * Update webapp/i18n/en.json Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * Update webapp/i18n/en.json Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * Update webapp/i18n/en.json Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * Update webapp/i18n/en.json Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * Update webapp/src/errors.ts Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * typo * remove background image * fix linter error * fix cypress errors * translation error page title * fix package lock * fix package.lock again Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
152c60400d
commit
ec91f1c71b
15 changed files with 1534 additions and 1533 deletions
|
@ -2,27 +2,43 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {Utils} from '../../../webapp/src/utils'
|
||||
|
||||
type Props = {}
|
||||
type State = {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
export default class ErrorBoundary<Props, State> extends React.Component {
|
||||
export default class ErrorBoundary extends React.Component {
|
||||
state = {hasError: false}
|
||||
propTypes = {children: PropTypes.node.isRequired}
|
||||
msg = 'Redirecting to error page...'
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
handleError = (): void => {
|
||||
const url = Utils.getBaseURL() + '/error?id=unknown'
|
||||
Utils.log('error boundary redirecting to ' + url)
|
||||
window.location.replace(url)
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(/*error: Error*/): State {
|
||||
return {hasError: true}
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.log(error, errorInfo)
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
Utils.logError(error + ': ' + errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return <span>Something went wrong.</span>
|
||||
this.handleError()
|
||||
return <span>{this.msg}</span>
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,9 @@
|
|||
],
|
||||
"react/no-string-refs": 2,
|
||||
"no-only-tests/no-only-tests": ["error", {"focus": ["only", "skip"]}],
|
||||
"max-nested-callbacks": ["error", {"max": 5}]
|
||||
"max-nested-callbacks": ["error", {"max": 5}],
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
@ -8,19 +8,21 @@ describe('Login actions', () => {
|
|||
|
||||
it('Can perform login/register actions', () => {
|
||||
// Redirects to login page
|
||||
cy.log('**Redirects to login page**')
|
||||
cy.log('**Redirects to error then login page**')
|
||||
cy.visit('/')
|
||||
cy.location('pathname').should('eq', '/error')
|
||||
cy.get('button').contains('Log in').click()
|
||||
cy.location('pathname').should('eq', '/login')
|
||||
cy.get('.LoginPage').contains('Log in')
|
||||
cy.get('#login-username').should('exist')
|
||||
cy.get('#login-password').should('exist')
|
||||
cy.get('button').contains('Log in')
|
||||
cy.get('a').contains('create an account')
|
||||
cy.get('a').contains('create an account', {matchCase: false})
|
||||
|
||||
// Can register a user
|
||||
cy.log('**Can register a user**')
|
||||
cy.visit('/login')
|
||||
cy.get('a').contains('create an account').click()
|
||||
cy.get('a').contains('create an account', {matchCase: false}).click()
|
||||
cy.location('pathname').should('eq', '/register')
|
||||
cy.get('.RegisterPage').contains('Sign up')
|
||||
cy.get('#login-email').type(email)
|
||||
|
@ -40,7 +42,7 @@ describe('Login actions', () => {
|
|||
// User should not be logged in automatically
|
||||
cy.log('**User should not be logged in automatically**')
|
||||
cy.visit('/')
|
||||
cy.location('pathname').should('eq', '/login')
|
||||
cy.location('pathname').should('eq', '/error')
|
||||
|
||||
// Can log in registered user
|
||||
cy.log('**Can log in registered user**')
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
|
@ -16,6 +18,10 @@
|
|||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
on('task', {
|
||||
failed: require('cypress-failed-log/src/failed')(),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -7,3 +7,5 @@ import 'cypress-real-events/support'
|
|||
|
||||
import './api_commands'
|
||||
import './ui_commands'
|
||||
|
||||
import 'cypress-failed-log'
|
||||
|
|
|
@ -254,10 +254,15 @@
|
|||
"calendar.week": "Week",
|
||||
"default-properties.badges": "Comments and Description",
|
||||
"default-properties.title": "Title",
|
||||
"error.no-workspace": "Your session may have expired or you may not have access to this workspace.",
|
||||
"error.relogin": "Log in again",
|
||||
"login.log-in-button": "Log in",
|
||||
"login.log-in-title": "Log in",
|
||||
"error.workspace-undefined": "Not a valid workspace.",
|
||||
"error.page.title": "Sorry, something went wrong",
|
||||
"error.not-logged-in": "Your session may have expired or you're not logged in.",
|
||||
"error.go-dashboard": "Go to the Dashboard",
|
||||
"error.go-login": "Log in",
|
||||
"error.unknown": "An error occurred.",
|
||||
"login.register-button": "or create an account if you don't have one",
|
||||
"register.login-button": "or log in if you already have an account",
|
||||
"OnboardingTour.OpenACard.Title": "Open a card",
|
||||
|
|
2412
webapp/package-lock.json
generated
2412
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -108,6 +108,8 @@
|
|||
"copy-webpack-plugin": "^8.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.0",
|
||||
"cypress": "^9.5.0",
|
||||
"cypress-failed-log": "^2.9.2",
|
||||
"cypress-real-events": "^1.6.0",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-import-resolver-webpack": "0.13.0",
|
||||
|
|
|
@ -125,7 +125,7 @@ const App = (props: Props): JSX.Element => {
|
|||
exact={true}
|
||||
render={() => {
|
||||
if (loggedIn === false) {
|
||||
return <Redirect to='/login'/>
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
|
||||
if (continueToWelcomeScreen()) {
|
||||
|
@ -164,7 +164,7 @@ const App = (props: Props): JSX.Element => {
|
|||
path='/board/:boardId?/:viewId?/:cardId?'
|
||||
render={({match: {params: {boardId, viewId, cardId}}}) => {
|
||||
if (loggedIn === false) {
|
||||
return <Redirect to='/login'/>
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
|
||||
if (continueToWelcomeScreen()) {
|
||||
|
@ -228,7 +228,7 @@ const App = (props: Props): JSX.Element => {
|
|||
const boardIdIsValidUUIDV4 = UUID_REGEX.test(boardId || '')
|
||||
|
||||
if (loggedIn === false) {
|
||||
return <Redirect to='/login'/>
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
|
||||
if (continueToWelcomeScreen()) {
|
||||
|
|
|
@ -80,11 +80,11 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||
const emojiPlugin = createEmojiPlugin()
|
||||
const markdownPlugin = createLiveMarkdownPlugin()
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const {EmojiSuggestions} = emojiPlugin
|
||||
// eslint-disable-next-line no-shadow
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const {MentionSuggestions} = mentionPlugin
|
||||
// eslint-disable-next-line no-shadow
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const plugins = [
|
||||
mentionPlugin,
|
||||
emojiPlugin,
|
||||
|
|
69
webapp/src/errors.ts
Normal file
69
webapp/src/errors.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
enum ErrorId {
|
||||
WorkspaceUndefined = 'workspace-undefined',
|
||||
NotLoggedIn = 'not-logged-in',
|
||||
}
|
||||
|
||||
type ErrorDef = {
|
||||
title: string
|
||||
|
||||
button1Enabled: boolean
|
||||
button1Text: string
|
||||
button1Redirect: string | (() => string)
|
||||
button1Fill: boolean
|
||||
|
||||
button2Enabled: boolean
|
||||
button2Text: string
|
||||
button2Redirect: string | (() => string)
|
||||
button2Fill: boolean
|
||||
}
|
||||
|
||||
function errorDefFromId(id: ErrorId | null): ErrorDef {
|
||||
const errDef = {
|
||||
title: '',
|
||||
button1Enabled: false,
|
||||
button1Text: '',
|
||||
button1Redirect: '',
|
||||
button1Fill: false,
|
||||
button2Enabled: false,
|
||||
button2Text: '',
|
||||
button2Redirect: '',
|
||||
button2Fill: false,
|
||||
}
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
switch (id) {
|
||||
case ErrorId.WorkspaceUndefined: {
|
||||
errDef.title = intl.formatMessage({id: 'error.workspace-undefined', defaultMessage: 'Not a valid workspace.'})
|
||||
errDef.button1Enabled = true
|
||||
errDef.button1Text = intl.formatMessage({id: 'error.go-dashboard', defaultMessage: 'Go to the Dashboard'})
|
||||
errDef.button1Redirect = '/dashboard'
|
||||
errDef.button1Fill = true
|
||||
break
|
||||
}
|
||||
case ErrorId.NotLoggedIn: {
|
||||
errDef.title = intl.formatMessage({id: 'error.not-logged-in', defaultMessage: 'Your session may have expired or you\'re not logged in. Log in again to access Boards.'})
|
||||
errDef.button1Enabled = true
|
||||
errDef.button1Text = intl.formatMessage({id: 'error.go-login', defaultMessage: 'Log in'})
|
||||
errDef.button1Redirect = '/login'
|
||||
errDef.button1Fill = true
|
||||
break
|
||||
}
|
||||
default: {
|
||||
errDef.title = intl.formatMessage({id: 'error.unknown', defaultMessage: 'An error occurred.'})
|
||||
errDef.button1Enabled = true
|
||||
errDef.button1Text = intl.formatMessage({id: 'error.go-dashboard', defaultMessage: 'Go to the Dashboard'})
|
||||
errDef.button1Redirect = '/dashboard'
|
||||
errDef.button1Fill = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return errDef
|
||||
}
|
||||
|
||||
export {ErrorId, ErrorDef, errorDefFromId}
|
|
@ -1,42 +1,28 @@
|
|||
.ErrorPage {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 15px;
|
||||
width: 450px;
|
||||
height: 400px;
|
||||
margin: 150px auto;
|
||||
padding: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
box-shadow: rgba(var(--center-channel-color-rgb), 0.1) 0 0 0 1px,
|
||||
rgba(var(--center-channel-color-rgb), 0.3) 0 4px 8px;
|
||||
justify-content: center;
|
||||
|
||||
@media screen and (max-width: 430px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
padding-top: 10px;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-size: 52px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
> .Button {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 38px;
|
||||
min-width: 250px;
|
||||
.subtitle {
|
||||
font-size: 20px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #900000;
|
||||
svg {
|
||||
margin: 56px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,67 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {useHistory, useLocation} from 'react-router-dom'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {useHistory} from 'react-router-dom'
|
||||
|
||||
import octoClient from '../octoClient'
|
||||
import ErrorIllustration from '../svg/error-illustration'
|
||||
|
||||
import Button from '../widgets/buttons/button'
|
||||
import './errorPage.scss'
|
||||
|
||||
import {errorDefFromId, ErrorId} from '../errors'
|
||||
|
||||
const ErrorPage = () => {
|
||||
const history = useHistory()
|
||||
const queryString = new URLSearchParams(useLocation().search)
|
||||
const errid = queryString.get('id')
|
||||
const errorDef = errorDefFromId(errid as ErrorId)
|
||||
|
||||
const handleButtonClick = useCallback((path: string | (()=>string)) => {
|
||||
let url = '/dashboard'
|
||||
if (typeof path === 'function') {
|
||||
url = path()
|
||||
} else if (path) {
|
||||
url = path as string
|
||||
}
|
||||
history.push(url)
|
||||
}, [history])
|
||||
|
||||
const makeButton = ((path: string | (()=>string), txt: string, fill: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
filled={fill}
|
||||
size='large'
|
||||
onClick={async () => {
|
||||
handleButtonClick(path)
|
||||
}}
|
||||
>
|
||||
{txt}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='ErrorPage'>
|
||||
<div className='title'>{'Error'}</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='error.no-workspace'
|
||||
defaultMessage='Your session may have expired or you may not have access to this workspace.'
|
||||
/>
|
||||
<div className='title'>
|
||||
<FormattedMessage
|
||||
id='error.page.title'
|
||||
defaultMessage={'Sorry, something went wrong'}
|
||||
/>
|
||||
</div>
|
||||
<div className='subtitle'>
|
||||
{errorDef.title}
|
||||
</div>
|
||||
<ErrorIllustration/>
|
||||
<br/>
|
||||
{
|
||||
(errorDef.button1Enabled ? makeButton(errorDef.button1Redirect, errorDef.button1Text, errorDef.button1Fill) : null)
|
||||
}
|
||||
{
|
||||
(errorDef.button2Enabled ? makeButton(errorDef.button2Redirect, errorDef.button2Text, errorDef.button2Fill) : null)
|
||||
}
|
||||
</div>
|
||||
<br/>
|
||||
<Button
|
||||
filled={true}
|
||||
onClick={async () => {
|
||||
await octoClient.logout()
|
||||
history.push('/login')
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='error.relogin'
|
||||
defaultMessage='Log in again'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export const initialLoad = createAsyncThunk(
|
|||
|
||||
// if no workspace, either bad id, or user doesn't have access
|
||||
if (workspace === undefined) {
|
||||
throw new Error('Workspace undefined')
|
||||
throw new Error('workspace-undefined')
|
||||
}
|
||||
return {
|
||||
workspace,
|
||||
|
|
390
webapp/src/svg/error-illustration.tsx
Normal file
390
webapp/src/svg/error-illustration.tsx
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue