Skip to content

Commit b937878

Browse files
authored
Merge pull request #1406 from dcoric/denis-coric/fix-1392-linked
feat: enhance error handling in git-push and repo services
2 parents 6713757 + 2c2b093 commit b937878

File tree

18 files changed

+1600
-226
lines changed

18 files changed

+1600
-226
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import React, { Component, ErrorInfo, PropsWithChildren, ReactNode, useState } from 'react';
2+
import Paper from '@material-ui/core/Paper';
3+
import Typography from '@material-ui/core/Typography';
4+
import Button from '@material-ui/core/Button';
5+
import Collapse from '@material-ui/core/Collapse';
6+
import { makeStyles } from '@material-ui/core/styles';
7+
8+
const IS_DEV = process.env.NODE_ENV !== 'production';
9+
10+
const useStyles = makeStyles((theme) => ({
11+
wrapper: {
12+
display: 'flex',
13+
alignItems: 'center',
14+
justifyContent: 'center',
15+
height: '100%',
16+
minHeight: '60vh',
17+
padding: theme.spacing(2),
18+
},
19+
root: {
20+
padding: theme.spacing(4),
21+
borderLeft: `4px solid ${theme.palette.error.main}`,
22+
maxWidth: 560,
23+
width: '100%',
24+
},
25+
title: {
26+
color: theme.palette.error.main,
27+
marginBottom: theme.spacing(1),
28+
},
29+
message: {
30+
marginBottom: theme.spacing(2),
31+
color: theme.palette.text.secondary,
32+
},
33+
hint: {
34+
marginBottom: theme.spacing(2),
35+
color: theme.palette.text.secondary,
36+
fontStyle: 'italic',
37+
},
38+
actions: {
39+
display: 'flex',
40+
gap: theme.spacing(1),
41+
alignItems: 'center',
42+
marginBottom: theme.spacing(1),
43+
},
44+
stack: {
45+
marginTop: theme.spacing(2),
46+
padding: theme.spacing(2),
47+
backgroundColor: theme.palette.grey[100],
48+
borderRadius: theme.shape.borderRadius,
49+
overflowX: 'auto',
50+
fontSize: '0.75rem',
51+
fontFamily: 'monospace',
52+
whiteSpace: 'pre-wrap',
53+
wordBreak: 'break-word',
54+
},
55+
devBadge: {
56+
display: 'inline-block',
57+
marginBottom: theme.spacing(2),
58+
padding: '2px 8px',
59+
backgroundColor: theme.palette.warning.main,
60+
color: theme.palette.warning.contrastText,
61+
borderRadius: theme.shape.borderRadius,
62+
fontSize: '0.7rem',
63+
fontWeight: 700,
64+
letterSpacing: '0.05em',
65+
textTransform: 'uppercase',
66+
},
67+
}));
68+
69+
const ProdFallback = ({ reset }: { reset: () => void }) => {
70+
const classes = useStyles();
71+
return (
72+
<div className={classes.wrapper}>
73+
<Paper className={classes.root} role='alert' elevation={0} variant='outlined'>
74+
<Typography variant='h6' className={classes.title}>
75+
Something went wrong
76+
</Typography>
77+
<Typography variant='body2' className={classes.message}>
78+
An unexpected error occurred. Please try again — if the problem persists, contact your
79+
administrator.
80+
</Typography>
81+
<div className={classes.actions}>
82+
<Button variant='outlined' size='small' color='primary' onClick={reset}>
83+
Retry
84+
</Button>
85+
<Button size='small' onClick={() => window.location.reload()}>
86+
Reload page
87+
</Button>
88+
</div>
89+
</Paper>
90+
</div>
91+
);
92+
};
93+
94+
const DevFallback = ({
95+
error,
96+
name,
97+
reset,
98+
}: {
99+
error: Error;
100+
name?: string;
101+
reset: () => void;
102+
}) => {
103+
const classes = useStyles();
104+
const [showDetails, setShowDetails] = useState(false);
105+
const context = name ? ` in ${name}` : '';
106+
107+
return (
108+
<div className={classes.wrapper}>
109+
<Paper className={classes.root} role='alert' elevation={0} variant='outlined'>
110+
<div className={classes.devBadge}>dev</div>
111+
<Typography variant='h6' className={classes.title}>
112+
Something went wrong{context}
113+
</Typography>
114+
<Typography variant='body2' className={classes.message}>
115+
{error.message}
116+
</Typography>
117+
<div className={classes.actions}>
118+
<Button variant='outlined' size='small' color='primary' onClick={reset}>
119+
Retry
120+
</Button>
121+
{error.stack && (
122+
<Button size='small' onClick={() => setShowDetails((v) => !v)}>
123+
{showDetails ? 'Hide stack trace' : 'Show stack trace'}
124+
</Button>
125+
)}
126+
</div>
127+
{error.stack && (
128+
<Collapse in={showDetails}>
129+
<pre className={classes.stack}>{error.stack}</pre>
130+
</Collapse>
131+
)}
132+
</Paper>
133+
</div>
134+
);
135+
};
136+
137+
type Props = PropsWithChildren<{
138+
name?: string;
139+
fallback?: (error: Error, reset: () => void) => ReactNode;
140+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
141+
}>;
142+
143+
type State = { error: Error | undefined };
144+
145+
export class ErrorBoundary extends Component<Props, State> {
146+
state: State = { error: undefined };
147+
148+
static getDerivedStateFromError(error: Error): State {
149+
return { error };
150+
}
151+
152+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
153+
this.props.onError?.(error, errorInfo);
154+
if (IS_DEV) {
155+
console.error('ErrorBoundary caught:', error, errorInfo);
156+
}
157+
}
158+
159+
reset = () => this.setState({ error: undefined });
160+
161+
render() {
162+
const { error } = this.state;
163+
const { children, fallback, name } = this.props;
164+
165+
if (error) {
166+
if (fallback) return fallback(error, this.reset);
167+
return IS_DEV ? (
168+
<DevFallback error={error} name={name} reset={this.reset} />
169+
) : (
170+
<ProdFallback reset={this.reset} />
171+
);
172+
}
173+
174+
return children;
175+
}
176+
}

src/ui/layouts/Dashboard.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { UserContext } from '../context';
1313
import { getUser } from '../services/user';
1414
import { Route as RouteType } from '../types';
1515
import { PublicUser } from '../../db/types';
16+
import { ErrorBoundary } from '../components/ErrorBoundary/ErrorBoundary';
1617

1718
interface DashboardProps {
1819
[key: string]: any;
@@ -95,16 +96,18 @@ const Dashboard: React.FC<DashboardProps> = ({ ...rest }) => {
9596
/>
9697
<div className={classes.mainPanel} ref={mainPanel}>
9798
<Navbar routes={routes} handleDrawerToggle={handleDrawerToggle} {...rest} />
98-
{isMapRoute() ? (
99-
<div className={classes.map}>{switchRoutes}</div>
100-
) : (
101-
<>
102-
<div className={classes.content}>
103-
<div className={classes.container}>{switchRoutes}</div>
104-
</div>
105-
<Footer />
106-
</>
107-
)}
99+
<ErrorBoundary name='Dashboard'>
100+
{isMapRoute() ? (
101+
<div className={classes.map}>{switchRoutes}</div>
102+
) : (
103+
<>
104+
<div className={classes.content}>
105+
<div className={classes.container}>{switchRoutes}</div>
106+
</div>
107+
<Footer />
108+
</>
109+
)}
110+
</ErrorBoundary>
108111
</div>
109112
</div>
110113
</UserContext.Provider>

src/ui/services/auth.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ interface AxiosConfig {
1111
};
1212
}
1313

14+
const IS_DEV = process.env.NODE_ENV !== 'production';
15+
1416
/**
1517
* Gets the current user's information
1618
*/
@@ -20,10 +22,17 @@ export const getUserInfo = async (): Promise<PublicUser | null> => {
2022
const response = await fetch(`${baseUrl}/api/auth/profile`, {
2123
credentials: 'include', // Sends cookies
2224
});
23-
if (!response.ok) throw new Error(`Failed to fetch user info: ${response.statusText}`);
25+
if (!response.ok) {
26+
if (response.status === 401) {
27+
return null;
28+
}
29+
throw new Error(`Failed to fetch user info: ${response.statusText}`);
30+
}
2431
return await response.json();
2532
} catch (error) {
26-
console.error('Error fetching user info:', error);
33+
if (IS_DEV) {
34+
console.warn('Error fetching user info:', error);
35+
}
2736
return null;
2837
}
2938
};

src/ui/services/errors.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export interface ServiceResult<T = void> {
2+
success: boolean;
3+
status?: number;
4+
message?: string;
5+
data?: T;
6+
}
7+
8+
export const getServiceError = (
9+
error: any,
10+
fallbackMessage: string,
11+
): { status?: number; message: string } => {
12+
const status = error?.response?.status;
13+
const responseMessage = error?.response?.data?.message;
14+
const message =
15+
typeof responseMessage === 'string' && responseMessage.trim().length > 0
16+
? responseMessage
17+
: status
18+
? `Unknown error occurred, response code: ${status}`
19+
: error?.message || fallbackMessage;
20+
return { status, message };
21+
};
22+
23+
export const formatErrorMessage = (
24+
prefix: string,
25+
status: number | undefined,
26+
message: string,
27+
): string => `${prefix}: ${status ? `${status} ` : ''}${message}`;
28+
29+
export const errorResult = <T = void>(error: any, fallbackMessage: string): ServiceResult<T> => {
30+
const { status, message } = getServiceError(error, fallbackMessage);
31+
return { success: false, status, message };
32+
};
33+
34+
export const successResult = <T = void>(data?: T): ServiceResult<T> => ({
35+
success: true,
36+
...(data !== undefined && { data }),
37+
});

0 commit comments

Comments
 (0)