aboutsummaryrefslogtreecommitdiffstats
path: root/packages/renderer/src/components/errorPage/ErrorPage.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/renderer/src/components/errorPage/ErrorPage.tsx')
-rw-r--r--packages/renderer/src/components/errorPage/ErrorPage.tsx181
1 files changed, 181 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..dc01ddf
--- /dev/null
+++ b/packages/renderer/src/components/errorPage/ErrorPage.tsx
@@ -0,0 +1,181 @@
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
21import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
22import NoEncryptionIcon from '@mui/icons-material/NoEncryptionOutlined';
23import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
24import Box from '@mui/material/Box';
25import Button from '@mui/material/Button';
26import Typography from '@mui/material/Typography';
27import type { TFunction } from 'i18next';
28import { observer } from 'mobx-react-lite';
29import React from 'react';
30import { useTranslation } from 'react-i18next';
31
32import type Service from '../../stores/Service.js';
33
34import CertificateDetails from './CertificateDetails.js';
35
36interface ErrorDetails {
37 icon: JSX.Element;
38 title: string;
39 description: string;
40 errorCode: string | undefined;
41}
42
43function 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
126function ErrorPage({ service }: { service: Service }): JSX.Element | null {
127 const { t } = useTranslation(undefined);
128
129 if (!service.hasError) {
130 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
131 return null;
132 }
133
134 const {
135 settings: { name: serviceName },
136 state: { type: errorType },
137 } = service;
138 const { icon, title, description, errorCode } = formatError(service, t);
139
140 return (
141 <Box p={2}>
142 <Box component="section" maxWidth={800} mx="auto" my={{ md: '10vh' }}>
143 <Typography
144 aria-hidden
145 fontSize={{ xs: 48, md: 96 }}
146 lineHeight={1}
147 mb={2}
148 >
149 {icon}
150 </Typography>
151 <Typography
152 component="h1"
153 variant="h3"
154 fontSize={{ xs: '2rem', md: '3rem' }}
155 mb={2}
156 >
157 {title}
158 </Typography>
159 <Typography mb={4}>{description}</Typography>
160 {errorCode !== undefined && (
161 <Typography variant="body2" color="text.secondary" mb={2}>
162 {t('error.errorCode', { errorCode })}
163 </Typography>
164 )}
165 <Box display="flex" flexDirection="row" gap={1} mb={2}>
166 <Button onClick={() => service.goHome()} variant="contained">
167 {t('error.home', { serviceName })}
168 </Button>
169 {errorType !== 'certificateError' && (
170 <Button onClick={() => service.reload()} variant="outlined">
171 {t('error.reload')}
172 </Button>
173 )}
174 </Box>
175 <CertificateDetails service={service} />
176 </Box>
177 </Box>
178 );
179}
180
181export default observer(ErrorPage);