Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

221: Add basic error boundaries #238

Merged
merged 18 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@
"framer-motion": "^11.9.0",
"jotai": "^2.10.0",
"js-cookie": "^3.0.5",
"mantine-form-zod-resolver": "^1.1.0",
"jwt-decode": "^4.0.0",
"mantine-form-zod-resolver": "^1.1.0",
"prettier": "^3.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-icons": "^5.3.0",
"react-router-dom": "^6.26.2",
"recharts": "^2.13.3",
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const Layout = () => {
}}
>
<DefaultNavbar />
<Box
<Flex
style={{
// flex 1 to fill the entire available space
flex: '1',
padding: '2rem 15%',
}}
>
<Outlet />
</Box>
</Flex>
<Footer />
</Flex>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Outlet } from 'react-router-dom';
import Navbar from './navbar/Navbar.tsx';
import DashboardFooter from './footer/DashboardFooter';
import { Box, Flex } from '@mantine/core';
import { ScrollToTop } from '../components/ScrollToTop';
import { ScrollToTop } from './ScrollToTop.tsx';

const dashboardFlex = {
flex: 'auto',
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/components/fallbacks/ProfilePictureFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Box, Image } from '@mantine/core';
import React from 'react';

export const ProfilePictureFallback = ({ error }) => {
console.error('ProfilePictureFallback:', error.message);
return (
<Box
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Image
src={'https://placehold.co/250x250?text=PfP'}
alt={`defaulting to placeholder: ${error.message}`}
w={'250px'}
h={'250px'}
fit="contain"
radius="50%"
referrerPolicy="no-referrer"
style={{ objectFit: 'cover', display: 'block' }}
/>
</Box>
);
};
17 changes: 17 additions & 0 deletions frontend/src/components/fallbacks/RateLimitStatusFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HoverCard } from '@mantine/core';
import { IconBrandReddit } from '@tabler/icons-react';
import React from 'react';

export const RateLimitStatusFallback = ({ error }) => {
console.error('RateLimitStatusFallback:', error.message);
return (
<HoverCard>
<HoverCard.Target>
<IconBrandReddit size={24} />
</HoverCard.Target>
<HoverCard.Dropdown>
Error occurred while fetching rate limit status: {error.message}
</HoverCard.Dropdown>
</HoverCard>
);
};
11 changes: 11 additions & 0 deletions frontend/src/components/fallbacks/UserMenuFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { logout } from '../../utility/logout.ts';
import { useNavigate } from 'react-router-dom';

export const UserMenuFallback = ({ error, resetErrorBoundary }) => {
resetErrorBoundary();
console.error('UserMenuFallback:', error.message);
return (
// TODO: will have to get more info how to handle UserMenu from the team
<></>
);
};
6 changes: 2 additions & 4 deletions frontend/src/components/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ const RightNavItems = () => {
*/
const Navbar = () => {
return (
<Card
style={{ margin: 0, borderRadius: 0, marginBottom: 0 }}
>
<Card style={{ margin: 0, borderRadius: 0, marginBottom: 0 }}>
<nav>
<Group justify='space-between'>
<Group justify="space-between">
<LeftNavItems />
<RightNavItems />
</Group>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/navbar/RateLimitStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import React, { useEffect } from 'react';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import duration from 'dayjs/plugin/duration';
import { ErrorBoundary } from 'react-error-boundary';
import { RateLimitStatusFallback } from '../fallbacks/RateLimitStatusFallback.tsx';

dayjs.extend(duration);
dayjs.extend(relativeTime);
Expand Down Expand Up @@ -125,4 +127,10 @@ const RateLimitStatus = () => {
);
};

export default RateLimitStatus;
export default function () {
return (
<ErrorBoundary FallbackComponent={RateLimitStatusFallback}>
<RateLimitStatus />
</ErrorBoundary>
);
}
90 changes: 48 additions & 42 deletions frontend/src/components/navbar/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import Cookies from 'js-cookie';
import { useNavigate } from 'react-router-dom';
import { Avatar, Menu } from '@mantine/core';
import authFetch from '../../rmoods/client/authFetch.ts';
import { useEffect, useState } from 'react';
import { jwtDecode } from 'jwt-decode';
import { JwtClaims } from '../../rmoods/jwt.ts';
import { changeDefaultGoogleProfilePictureSize } from '../../utility/changeDefaultGoogleProfilePictureSize.ts';
import { useQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
import { UserMenuFallback } from '../fallbacks/UserMenuFallback.tsx';
import { logout } from '../../utility/logout.ts';

/**
* User interface representing the user data.
Expand All @@ -15,56 +18,47 @@ interface User {
picture: string;
}

/**
* Fetches the user data from the server.
* @returns {Promise<User>} The user data.
*/
const fetchUserData = async (): Promise<User> => {
const token = Cookies.get('RMOODS_JWT');
if (!token) {
throw new Error('No JWT token found');
}

const data = jwtDecode<JwtClaims>(token);
const id = data.userInfo.id;
const response = await authFetch(`http://localhost:8001/api/user?id=${id}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}

return response.json();
};

/**
* UserMenu component that displays the user menu.
* @returns {JSX.Element} The UserMenu component.
*/
const UserMenu = () => {
const navigate = useNavigate();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const token = Cookies.get('RMOODS_JWT');
if (!token) {
navigate('/login');
return;
}

let isMounted = true;
try {
const data = jwtDecode<JwtClaims>(token);
const id = data.userInfo.id;
const { data, error } = useQuery<User, Error>({
queryKey: ['userData'],
queryFn: fetchUserData,
});

authFetch('http://localhost:8001/api/user?id=' + id).then((response) => {
response.json().then((data) => {
if (isMounted) {
setUser(data);
setLoading(false);
}
});
});
} catch (error) {
console.error('JWT token could not be decoded.', error);
if (isMounted) {
setLoading(false);
}
}
return () => {
isMounted = false;
};
}, [navigate]);

const handleLogout = () => {
console.log('Logging out');
Cookies.remove('RMOODS_JWT');
navigate('/login');
};
if (error) {
throw new Error('Failed to fetch user data');
}

const size = 45;
const resizedPicture = user
? changeDefaultGoogleProfilePictureSize(user.picture, size)
const resizedPicture = data?.picture
? changeDefaultGoogleProfilePictureSize(data.picture, size)
: '';

return (
<Menu id="user-dropdown">
<Menu.Target>
Expand All @@ -80,10 +74,22 @@ const UserMenu = () => {
<Menu.Item onClick={() => navigate('/user')}>Profile</Menu.Item>
<Menu.Item onClick={() => navigate('/dashboard')}>Dashboard</Menu.Item>
<Menu.Item onClick={() => navigate('/settings')}>Settings</Menu.Item>
<Menu.Item onClick={() => handleLogout()}>Log out</Menu.Item>
<Menu.Item onClick={() => logout(navigate)}>Log out</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};

export default UserMenu;
export default function () {
const navigate = useNavigate();
return (
<ErrorBoundary
FallbackComponent={UserMenuFallback}
onReset={() => {
logout(navigate);
}}
>
<UserMenu />
</ErrorBoundary>
);
}
6 changes: 5 additions & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import './index.css';
import { RouterProvider } from 'react-router-dom';
import router from './router.tsx';
import Providers from './providers/Providers.tsx';
import { ErrorBoundary } from 'react-error-boundary';
import { MainFallback } from './routes/fallbacks/MainFallback.tsx';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<Providers>
<RouterProvider router={router} />
<ErrorBoundary FallbackComponent={MainFallback}>
<RouterProvider router={router} />
</ErrorBoundary>
</Providers>
</StrictMode>
);
14 changes: 7 additions & 7 deletions frontend/src/rmoods/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ export const DataSourceSchema = z.object({
const GoogleUserInfoSchema = z.object({
sub: z.string(),
name: z.string(),
given_name: z.string(),
family_name: z.string().nullable(),
givenName: z.string(),
familyName: z.string().nullable(),
picture: z.string().url(),
email: z.string().email(),
email_verified: z.boolean(),
emailVerified: z.boolean(),
});

const LanguageResponseSchema = z.object({
Expand All @@ -67,7 +67,7 @@ const LanguageResponseSchema = z.object({
});

const NlpMetadataSchema = z.object({
generated_in: z.number(),
generatedIn: z.number(),
});

const NlpAnalysisSchema = z.object({
Expand All @@ -76,9 +76,9 @@ const NlpAnalysisSchema = z.object({
});

const ReportMetadataSchema = z.object({
created_at: z.number(),
user_info: GoogleUserInfoSchema,
is_public: z.boolean(),
createdAt: z.number(),
userInfo: GoogleUserInfoSchema,
isPublic: z.boolean(),
});

const ReportResponseSchema = z.object({
Expand Down
1 change: 0 additions & 1 deletion frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const router = createBrowserRouter([
path: 'about',
element: <About />,
},

{
path: 'login',
element: <Login />,
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/routes/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Section from './Section';
import { Box } from '@mantine/core';
import { ErrorBoundary } from 'react-error-boundary';
import { PageFallback } from '../fallbacks/PageFallback.tsx';

/**
* About page, contains information about the project and the team.
Expand Down Expand Up @@ -41,4 +43,10 @@ const About = () => {
);
};

export default About;
export default function () {
return (
<ErrorBoundary FallbackComponent={PageFallback}>
<About />
</ErrorBoundary>
);
}
Loading
Loading