Skip to content

Commit f2fc53e

Browse files
Feat: gbfs validator error grouping + error details (#1480)
* add sentry to web-app * Sentry trace rate parsing safety Co-authored-by: Copilot <[email protected]> * co pilot pr comments * deployment settings * update variable script to allow optional variables * error grouping and error detail * gbfs validator: error grouping and detail * errorDetailDialog refactor * ui of error dialog * tooltip adjustment * mobile support styling * accessibility fixes * styling touchup * updated test data * lint * co-pilot pr comments * improved UI to help find error details * focus fix * strict null checks for auth header builder * allow empty username and password * styling adjustment --------- Co-authored-by: Copilot <[email protected]>
1 parent 7ba634e commit f2fc53e

File tree

11 files changed

+1588
-327
lines changed

11 files changed

+1588
-327
lines changed

web-app/src/app/context/GbfsAuthProvider.tsx

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface GbfsAuthContextValue {
2121
auth: GbfsAuthDetails;
2222
setAuth: (details: GbfsAuthDetails) => void;
2323
clearAuth: () => void;
24+
buildAuthHeaders: () => Promise<Record<string, string> | undefined>;
2425
}
2526

2627
const GbfsAuthContext = createContext<GbfsAuthContextValue | undefined>(
@@ -44,7 +45,75 @@ export function GbfsAuthProvider({
4445
setAuthState(defaultAuth);
4546
};
4647

47-
const value = useMemo(() => ({ auth, setAuth, clearAuth }), [auth]);
48+
const buildAuthHeaders = async (): Promise<
49+
Record<string, string> | undefined
50+
> => {
51+
if (
52+
auth?.authType === AuthTypeEnum.BASIC &&
53+
// The RFC 7617 describes HTTP Basic Authentication doesn't prohibit the use of an empty username or password
54+
(('username' in auth &&
55+
auth.username != null &&
56+
auth.username.trim() !== '') ||
57+
('password' in auth &&
58+
auth.password != null &&
59+
auth.password.trim() !== ''))
60+
) {
61+
try {
62+
const token = btoa(`${auth.username ?? ''}:${auth.password ?? ''}`);
63+
return { Authorization: `Basic ${token}` };
64+
} catch {
65+
return undefined;
66+
}
67+
}
68+
if (
69+
auth?.authType === AuthTypeEnum.BEARER &&
70+
'token' in auth &&
71+
auth.token != null &&
72+
auth.token.trim() !== ''
73+
) {
74+
return { Authorization: `Bearer ${auth.token}` };
75+
}
76+
if (
77+
auth?.authType === AuthTypeEnum.OAUTH &&
78+
'clientId' in auth &&
79+
'clientSecret' in auth &&
80+
'tokenUrl' in auth &&
81+
auth.clientId != null &&
82+
auth.clientId.trim() !== '' &&
83+
auth.clientSecret != null &&
84+
auth.clientSecret.trim() !== '' &&
85+
auth.tokenUrl != null &&
86+
auth.tokenUrl.trim() !== ''
87+
) {
88+
const tokenResp = await fetch(auth.tokenUrl, {
89+
method: 'POST',
90+
headers: {
91+
'Content-Type': 'application/x-www-form-urlencoded',
92+
Authorization: `Basic ${btoa(
93+
`${auth.clientId}:${auth.clientSecret}`,
94+
)}`,
95+
},
96+
body: 'grant_type=client_credentials',
97+
credentials: 'omit',
98+
});
99+
if (!tokenResp.ok) {
100+
return undefined;
101+
}
102+
const tokenJson = await tokenResp.json();
103+
const accessToken = tokenJson?.access_token ?? tokenJson?.token;
104+
if (accessToken == null) {
105+
return undefined;
106+
}
107+
const tokenType = tokenJson?.token_type ?? 'Bearer';
108+
return { Authorization: `${tokenType} ${accessToken}` };
109+
}
110+
return undefined;
111+
};
112+
113+
const value = useMemo(
114+
() => ({ auth, setAuth, clearAuth, buildAuthHeaders }),
115+
[auth, setAuth, clearAuth, buildAuthHeaders],
116+
);
48117

49118
return (
50119
<GbfsAuthContext.Provider value={value}>

web-app/src/app/screens/GbfsValidator/ValidationReport.styles.ts

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type Theme,
77
} from '@mui/material';
88
import { type CSSProperties } from 'react';
9+
import { fontFamily } from '../../Theme';
910

1011
export const gbfsValidatorHeroBg = {
1112
backgroundColor: '#43e0ff',
@@ -67,9 +68,6 @@ export const ValidationElementCardStyles = (
6768
bgcolor: 'background.default',
6869
transition: 'box-shadow 0.3s ease',
6970
mt: index === 0 ? 0.5 : 0,
70-
'&:focus': {
71-
boxShadow: `0 0 0 3px ${theme.palette.primary.main}`,
72-
},
7371
});
7472

7573
export const AlertErrorBoxStyles = (
@@ -89,11 +87,16 @@ export const AlertErrorBoxStyles = (
8987
});
9088

9189
export const ValidationErrorPathStyles = (theme: Theme): CSSProperties => ({
92-
padding: theme.spacing(0.5),
9390
background: theme.palette.background.paper,
9491
width: '100%',
9592
overflowX: 'auto',
9693
fontSize: '0.875em',
94+
position: 'relative',
95+
display: 'inline-flex',
96+
justifyContent: 'space-between',
97+
alignItems: 'center',
98+
padding: theme.spacing(0.5, 1.25),
99+
borderRadius: theme.spacing(1.5),
97100
});
98101

99102
export const ContentTitle = styled(Typography)(({ theme }) => ({
@@ -103,3 +106,127 @@ export const ContentTitle = styled(Typography)(({ theme }) => ({
103106
lineHeight: '48px',
104107
fontWeight: 500,
105108
}));
109+
110+
export const dialogTitleSx: SxProps<Theme> = () => ({
111+
display: 'flex',
112+
alignItems: 'center',
113+
justifyContent: 'space-between',
114+
});
115+
116+
export const highlightedPreSx: SxProps<Theme> = (theme: Theme) => ({
117+
m: 0,
118+
p: 1,
119+
borderRadius: 1,
120+
backgroundColor: theme.palette.action.hover,
121+
maxHeight: 300,
122+
overflow: 'auto',
123+
whiteSpace: 'pre-wrap',
124+
wordBreak: 'break-word',
125+
fontFamily: 'monospace',
126+
});
127+
128+
export const highlightedContainerSx: SxProps<Theme> = (theme: Theme) => ({
129+
border: '1px solid',
130+
borderColor: theme.palette.divider,
131+
borderRadius: 2,
132+
overflow: 'hidden',
133+
});
134+
135+
export const highlightedTitleSx: SxProps<Theme> = (theme: Theme) => ({
136+
width: '100%',
137+
backgroundColor: theme.palette.background.default,
138+
borderBottom: `1px solid ${theme.palette.divider}`,
139+
p: 1,
140+
px: 1,
141+
color: theme.palette.text.primary,
142+
display: 'flex',
143+
alignItems: 'center',
144+
gap: 2,
145+
});
146+
147+
export const highlightedInnerSx: SxProps<Theme> = (theme: Theme) => ({
148+
m: 0,
149+
p: 1,
150+
borderRadius: 1,
151+
backgroundColor: theme.palette.action.hover,
152+
maxHeight: 300,
153+
overflow: 'auto',
154+
fontFamily: 'monospace',
155+
});
156+
157+
export const entryRowSx = (
158+
theme: Theme,
159+
isHitProp: boolean,
160+
): SxProps<Theme> => ({
161+
display: 'flex',
162+
gap: 1,
163+
alignItems: 'flex-start',
164+
px: 0.5,
165+
borderLeft: isHitProp ? '3px solid' : undefined,
166+
borderColor: isHitProp ? theme.palette.error.main : undefined,
167+
backgroundColor: isHitProp ? 'rgba(244,67,54,0.08)' : undefined,
168+
borderRadius: 0.5,
169+
});
170+
171+
export const keyTypographySx = (
172+
theme: Theme,
173+
isHitProp: boolean,
174+
): SxProps<Theme> => ({
175+
fontFamily: 'inherit',
176+
fontWeight: isHitProp ? 700 : 400,
177+
color: isHitProp ? theme.palette.error.main : 'inherit',
178+
});
179+
180+
export const listItemSx = (
181+
theme: Theme,
182+
isOffender: boolean,
183+
): SxProps<Theme> => ({
184+
backgroundColor: isOffender ? 'rgba(244,67,54,0.08)' : '',
185+
borderLeft: isOffender ? '3px solid' : '',
186+
borderColor: isOffender ? theme.palette.error.main : '',
187+
pl: isOffender ? 1 : 0,
188+
borderRadius: 0.5,
189+
wordBreak: 'break-word',
190+
});
191+
192+
export const valueTypographySx: SxProps<Theme> = (theme: Theme) => ({
193+
fontFamily: 'inherit',
194+
whiteSpace: 'pre-wrap',
195+
wordBreak: 'break-word',
196+
});
197+
198+
export const outlinePreSx: SxProps<Theme> = (theme: Theme) => ({
199+
m: 0,
200+
p: 1,
201+
borderRadius: 1,
202+
backgroundColor: theme.palette.action.hover,
203+
maxHeight: 300,
204+
overflow: 'auto',
205+
whiteSpace: 'pre-wrap',
206+
wordBreak: 'break-word',
207+
outline: `2px solid ${theme.palette.error.main}`,
208+
outlineOffset: '-2px',
209+
});
210+
211+
export const rowButtonOutlineErrorSx: SxProps<Theme> = (theme: Theme) => ({
212+
display: 'inline-flex',
213+
alignItems: 'center',
214+
gap: theme.spacing(0.5),
215+
opacity: 0,
216+
pointerEvents: 'none',
217+
transition: 'opacity 120ms, transform 120ms',
218+
whiteSpace: 'nowrap',
219+
px: theme.spacing(1),
220+
py: theme.spacing(0.5),
221+
borderRadius: '5px',
222+
border: `1px solid ${theme.palette.error.main}`,
223+
color: theme.palette.error.main,
224+
fontSize: '0.8125rem',
225+
fontFamily: fontFamily.secondary,
226+
fontWeight: 500,
227+
background: 'transparent',
228+
'&:hover': {
229+
backgroundColor: theme.palette.error.light,
230+
color: theme.palette.error.contrastText,
231+
},
232+
});

0 commit comments

Comments
 (0)