diff options
Diffstat (limited to 'packages/renderer/src/components/errorPage/ErrorPage.tsx')
-rw-r--r-- | packages/renderer/src/components/errorPage/ErrorPage.tsx | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/packages/renderer/src/components/errorPage/ErrorPage.tsx b/packages/renderer/src/components/errorPage/ErrorPage.tsx new file mode 100644 index 0000000..571059a --- /dev/null +++ b/packages/renderer/src/components/errorPage/ErrorPage.tsx | |||
@@ -0,0 +1,180 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; | ||
22 | import NoEncryptionIcon from '@mui/icons-material/NoEncryptionOutlined'; | ||
23 | import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; | ||
24 | import Box from '@mui/material/Box'; | ||
25 | import Button from '@mui/material/Button'; | ||
26 | import Typography from '@mui/material/Typography'; | ||
27 | import type { TFunction } from 'i18next'; | ||
28 | import { observer } from 'mobx-react-lite'; | ||
29 | import React from 'react'; | ||
30 | import { useTranslation } from 'react-i18next'; | ||
31 | |||
32 | import type Service from '../../stores/Service'; | ||
33 | |||
34 | import CertificateDetails from './CertificateDetails'; | ||
35 | |||
36 | interface ErrorDetails { | ||
37 | icon: JSX.Element; | ||
38 | title: string; | ||
39 | description: string; | ||
40 | errorCode: string | undefined; | ||
41 | } | ||
42 | |||
43 | function formatError(service: Service, t: TFunction): ErrorDetails { | ||
44 | const { | ||
45 | settings: { name: serviceName }, | ||
46 | state, | ||
47 | } = service; | ||
48 | const { type } = state; | ||
49 | switch (type) { | ||
50 | case 'failed': { | ||
51 | const { errorCode, errorDesc } = state; | ||
52 | const errorCodeStr = errorCode.toString(10); | ||
53 | return { | ||
54 | icon: <ErrorOutlineIcon fontSize="inherit" />, | ||
55 | title: t( | ||
56 | [ | ||
57 | `error.failed.${errorCodeStr}.title`, | ||
58 | 'error.failed.unspecific.title', | ||
59 | ], | ||
60 | { serviceName }, | ||
61 | ), | ||
62 | description: t( | ||
63 | [ | ||
64 | `error.failed.${errorCodeStr}.description`, | ||
65 | 'error.failed.unspecific.description', | ||
66 | ], | ||
67 | { serviceName }, | ||
68 | ), | ||
69 | errorCode: | ||
70 | errorDesc.length > 0 | ||
71 | ? `${errorDesc} (${errorCodeStr})` | ||
72 | : errorCodeStr, | ||
73 | }; | ||
74 | } | ||
75 | case 'certificateError': { | ||
76 | const { errorCode } = state; | ||
77 | // Avoid i18next namespace separators in the error code. | ||
78 | const errorCodeSafe = errorCode.replaceAll(':', '_'); | ||
79 | return { | ||
80 | icon: <NoEncryptionIcon fontSize="inherit" />, | ||
81 | title: t( | ||
82 | [ | ||
83 | `error.certificateError.${errorCodeSafe}.title`, | ||
84 | 'error.certificateError.unspecific.title', | ||
85 | ], | ||
86 | { serviceName }, | ||
87 | ), | ||
88 | description: t( | ||
89 | [ | ||
90 | `error.certificateError.${errorCodeSafe}.description`, | ||
91 | 'error.certificateError.unspecific.description', | ||
92 | ], | ||
93 | { serviceName }, | ||
94 | ), | ||
95 | errorCode, | ||
96 | }; | ||
97 | } | ||
98 | case 'crashed': { | ||
99 | const { reason, exitCode } = state; | ||
100 | return { | ||
101 | icon: <SentimentVeryDissatisfiedIcon fontSize="inherit" />, | ||
102 | title: t( | ||
103 | [`error.crashed.${reason}.title`, 'error.crashed.unspecific.title'], | ||
104 | { serviceName }, | ||
105 | ), | ||
106 | description: t( | ||
107 | [ | ||
108 | `error.crashed.${reason}.description`, | ||
109 | 'error.crashed.unspecific.description', | ||
110 | ], | ||
111 | { serviceName }, | ||
112 | ), | ||
113 | errorCode: `${reason} (${exitCode})`, | ||
114 | }; | ||
115 | } | ||
116 | default: | ||
117 | return { | ||
118 | icon: <ErrorOutlineIcon fontSize="inherit" />, | ||
119 | title: t('error.unknown.title', { serviceName }), | ||
120 | description: t('error.unknown.description', { serviceName }), | ||
121 | errorCode: undefined, | ||
122 | }; | ||
123 | } | ||
124 | } | ||
125 | |||
126 | function ErrorPage({ | ||
127 | service, | ||
128 | }: { | ||
129 | service: Service | undefined; | ||
130 | }): JSX.Element | null { | ||
131 | const { t } = useTranslation(undefined); | ||
132 | |||
133 | if (service === undefined || !service.hasError) { | ||
134 | // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. | ||
135 | return null; | ||
136 | } | ||
137 | |||
138 | const { | ||
139 | settings: { name: serviceName }, | ||
140 | state: { type: errorType }, | ||
141 | } = service; | ||
142 | const { icon, title, description, errorCode } = formatError(service, t); | ||
143 | |||
144 | return ( | ||
145 | <Box p={2}> | ||
146 | <Box component="section" maxWidth={800} mx="auto" my={{ md: '10vh' }}> | ||
147 | <Typography | ||
148 | aria-hidden | ||
149 | fontSize={{ xs: 48, md: 96 }} | ||
150 | lineHeight={1} | ||
151 | mb={2} | ||
152 | > | ||
153 | {icon} | ||
154 | </Typography> | ||
155 | <Typography component="h1" variant="h3" mb={2}> | ||
156 | {title} | ||
157 | </Typography> | ||
158 | <Typography mb={4}>{description}</Typography> | ||
159 | {errorCode !== undefined && ( | ||
160 | <Typography variant="body2" color="text.secondary" mb={2}> | ||
161 | {t('error.errorCode', { errorCode })} | ||
162 | </Typography> | ||
163 | )} | ||
164 | <Box display="flex" flexDirection="row" gap={1} mb={2}> | ||
165 | <Button onClick={() => service.goHome()} variant="contained"> | ||
166 | {t('error.home', { serviceName })} | ||
167 | </Button> | ||
168 | {errorType !== 'certificateError' && ( | ||
169 | <Button onClick={() => service.reload()} variant="outlined"> | ||
170 | {t('error.reload')} | ||
171 | </Button> | ||
172 | )} | ||
173 | </Box> | ||
174 | <CertificateDetails service={service} /> | ||
175 | </Box> | ||
176 | </Box> | ||
177 | ); | ||
178 | } | ||
179 | |||
180 | export default observer(ErrorPage); | ||