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:
Doug Lauder 2022-03-02 15:46:12 -05:00 committed by GitHub
parent 152c60400d
commit ec91f1c71b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1534 additions and 1533 deletions

View file

@ -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
}
}

View file

@ -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": [
{

View file

@ -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**')

View file

@ -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')(),
});
};

View file

@ -7,3 +7,5 @@ import 'cypress-real-events/support'
import './api_commands'
import './ui_commands'
import 'cypress-failed-log'

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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()) {

View file

@ -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
View 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}

View file

@ -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;
}
}

View file

@ -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>
)
}

View file

@ -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,

File diff suppressed because one or more lines are too long