Skip to content

Commit

Permalink
Merge pull request #518 from IQSS/126-add-error-handling-mechanism-to…
Browse files Browse the repository at this point in the history
…-the-ui

126 add error handling mechanism to the UI
  • Loading branch information
ofahimIQSS authored Oct 15, 2024
2 parents ac7e2fc + a1bbdbe commit 72348db
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 12 deletions.
8 changes: 8 additions & 0 deletions public/locales/en/errorPage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"message": {
"heading": "Oops,",
"errorText": "something went wrong..."
},
"brandName": "Dataverse",
"backToHomepage": "Back to {{brandName}} Homepage"
}
34 changes: 22 additions & 12 deletions src/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { lazy, Suspense } from 'react'
import { RouteObject } from 'react-router-dom'
import { Route } from '../sections/Route.enum'
import { Layout } from '../sections/layout/Layout'
import { PageNotFound } from '../sections/page-not-found/PageNotFound'
import { ErrorPage } from '../sections/error-page/ErrorPage'
import { ProtectedRoute } from './ProtectedRoute'
import { AppLoader } from '../sections/shared/layout/app-loader/AppLoader'

Expand Down Expand Up @@ -70,47 +70,52 @@ export const routes: RouteObject[] = [
{
path: '/',
element: <Layout />,
errorElement: <PageNotFound />,
errorElement: <ErrorPage fullViewport />,
children: [
{
path: Route.HOME,
element: (
<Suspense fallback={<AppLoader />}>
<Homepage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.COLLECTIONS_BASE,
element: (
<Suspense fallback={<AppLoader />}>
<CollectionPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.COLLECTIONS,
element: (
<Suspense fallback={<AppLoader />}>
<CollectionPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.DATASETS,
element: (
<Suspense fallback={<AppLoader />}>
<DatasetPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.FILES,
element: (
<Suspense fallback={<AppLoader />}>
<FilePage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
// 🔐 Protected routes are only accessible to authenticated users
{
Expand All @@ -122,39 +127,44 @@ export const routes: RouteObject[] = [
<Suspense fallback={<AppLoader />}>
<CreateCollectionPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.CREATE_DATASET,
element: (
<Suspense fallback={<AppLoader />}>
<CreateDatasetPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.UPLOAD_DATASET_FILES,
element: (
<Suspense fallback={<AppLoader />}>
<UploadDatasetFilesPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.EDIT_DATASET_METADATA,
element: (
<Suspense fallback={<AppLoader />}>
<EditDatasetMetadataPage />
</Suspense>
)
),
errorElement: <ErrorPage />
},
{
path: Route.ACCOUNT,
element: (
<Suspense fallback={<AppLoader />}>
<AccountPage />
</Suspense>
)
),
errorElement: <ErrorPage />
}
]
}
Expand Down
34 changes: 34 additions & 0 deletions src/sections/error-page/ErrorPage.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@use 'sass:color';
@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module';
@import 'src/assets/variables';

.section-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: $main-container-available-height;

&.full-viewport {
min-height: 100vh;
}
}

.middle-errorMessage-wrapper {
display: flex;
flex-direction: column;
gap: 2rem;
align-items: center;
justify-content: center;
width: 100%;
padding-block: 2rem;
}

.icon-layout {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
width: 100%;
padding-block: 2rem;
}
39 changes: 39 additions & 0 deletions src/sections/error-page/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next'
import { useRouteError, Link } from 'react-router-dom'
import styles from './ErrorPage.module.scss'
import { useErrorLogger } from './useErrorLogger'
import { ExclamationCircle } from 'react-bootstrap-icons'
import { useTheme } from '@iqss/dataverse-design-system'
import cn from 'classnames'

interface AppLoaderProps {
fullViewport?: boolean
}

export function ErrorPage({ fullViewport = false }: AppLoaderProps) {
const { t } = useTranslation('errorPage')
const error = useRouteError()
useErrorLogger(error)
const theme = useTheme()

return (
<section
className={cn(styles['section-wrapper'], {
[styles['full-viewport']]: fullViewport
})}>
<div className={styles['middle-errorMessage-wrapper']}>
<div className={styles['icon-layout']}>
<ExclamationCircle color={theme.color.dangerColor} size={62} />
<div aria-label="error-page">
<h1>{t('message.heading')}</h1>
<h4>{t('message.errorText')}</h4>
</div>
</div>

<Link to="/" className="btn btn-secondary">
{t('backToHomepage', { brandName: t('brandName') })}
</Link>
</div>
</section>
)
}
21 changes: 21 additions & 0 deletions src/sections/error-page/useErrorLogger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback, useEffect } from 'react'

const loggedErrors = new Set<string>()

export function useErrorLogger(error: Error | unknown): (error: Error) => void {
const logErrorOnce = useCallback((err: Error & { data?: unknown }): void => {
const errorString = String(err.data)
if (!loggedErrors.has(errorString)) {
loggedErrors.add(errorString)
console.error('Error:', err)
}
}, [])

useEffect(() => {
if (error) {
logErrorOnce(error as Error)
}
}, [error, logErrorOnce])

return logErrorOnce
}
100 changes: 100 additions & 0 deletions tests/component/sections/error-page/useErrorLogger.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { renderHook } from '@testing-library/react'
import { useErrorLogger } from '../../../../src/sections/error-page/useErrorLogger'

interface RouterError {
status: number
statusText: string
internal: boolean
data: string
error: Error
}

describe('useErrorLogger', () => {
beforeEach(() => {
cy.window().then((win) => {
cy.stub(win.console, 'error').as('consoleError')
})
})

it('should only log the same errors once when multiple occur, and log different errors separately', () => {
const testError = {
status: 404,
statusText: 'Not Found',
internal: true,
data: 'Test Error: Not Found',
error: new Error('Test Error')
}

const newError = {
status: 500,
statusText: 'Internal Server Error',
internal: true,
data: 'Error: Another error occurred',
error: new Error('Another error occurred')
}

cy.window().then(() => {
const { rerender } = renderHook(
({ error }: { error: Error | RouterError | null }) => useErrorLogger(error),
{ initialProps: { error: testError } }
)
cy.get('@consoleError').should('have.been.calledOnceWith', 'Error:', testError)
cy.then(() => {
rerender({ error: testError })
})
cy.get('@consoleError').should('have.been.calledOnce')
cy.then(() => {
rerender({ error: newError })
})
cy.get('@consoleError').should('have.been.calledWith', 'Error:', newError)
})
})

it('should not log anything if no error is provided', () => {
cy.window().then(() => {
const { rerender } = renderHook(
({ error }: { error: Error | RouterError | null }) => useErrorLogger(error),
{ initialProps: { error: null } }
)
cy.get('@consoleError').should('not.have.been.called')
cy.then(() => {
rerender({ error: null })
})
cy.get('@consoleError').should('not.have.been.called')
})
})

it('should be used to log errors manually only once for the same error, and log different errors separately', () => {
const badRequestError = {
status: 400,
statusText: 'Bad Request',
internal: true,
data: 'Error: Manually logged error',
error: new Error('Manually logged error')
}

const newError = {
status: 500,
statusText: 'Internal Server Error',
internal: true,
data: 'Error: Another error occurred',
error: new Error('Another error occurred')
}

cy.window().then(() => {
const { result } = renderHook(() => useErrorLogger(null))
cy.then(() => {
return result.current(badRequestError.error)
})
cy.get('@consoleError').should('have.been.calledOnceWith', 'Error:', badRequestError.error)
cy.then(() => {
result.current(badRequestError.error)
})
cy.get('@consoleError').should('have.been.calledOnce')
cy.then(() => {
result.current(newError.error)
})
cy.get('@consoleError').should('have.been.calledWith', 'Error:', newError.error)
})
})
})

0 comments on commit 72348db

Please sign in to comment.