Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
120 changes: 82 additions & 38 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ import { LoginPage } from './pages/LoginPage';
import { DatasetControls } from './pages/datasets/DatasetControls';
import { AuthCallback } from './pages/AuthCallback';
import { EnvironmentContextProvider } from './context/EnvironmentContext';
import { AuthProvider } from './context/AuthContext';
import { AuthProvider, useAuth, AUTH_TOKEN_STR } from './context/AuthContext';
import { AdminGuard } from './guards/AdminGuard';
import { LogoutPage } from './pages/LogoutPage';
import { CssBaseline, Box, styled } from '@mui/material';
import { useState } from 'react';
import { FC, ReactNode, useState } from 'react';
import { SideBar } from './components/SideBar';
import { ProjectProvider } from './context/ProjectContext';
import { ApolloClient, ApolloProvider, InMemoryCache, concat, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import {StudyProvider} from './context/Study';

const drawerWidth = 256;

const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
open?: boolean;
}>(({ theme, open }) => ({
Expand All @@ -46,50 +49,91 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
})
}));

function App() {
const [drawerOpen, setDrawerOpen] = useState(true);
const App: FC = () => {
const httpLink = createHttpLink({ uri: import.meta.env.VITE_GRAPHQL_ENDPOINT });
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem(AUTH_TOKEN_STR);
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
}
});

const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: concat(authLink, httpLink)
});

return (
<ThemeProvider>
<BrowserRouter>
<EnvironmentContextProvider>
<AuthProvider>
<CssBaseline />
<Box>
<NavBar drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</Box>
<Main open={drawerOpen}>
<Box sx={{ display: 'flex' }}>
<SideBar open={drawerOpen} drawerWidth={drawerWidth} />
<Box sx={{ flexGrow: 1, width: '90%' }}>
<Routes>
<Route path={'/'} element={<HomePage />} />
<Route path={'/callback'} element={<AuthCallback />} />
<Route path={'/loginpage'} element={<LoginPage />} />
<Route element={<AdminGuard />}>
<Route path={'/project/new'} element={<NewProject />} />
<Route path={'/project/controls'} element={<ProjectControl />} />
<Route path={'/project/permissions'} element={<ProjectUserPermissions />} />
<Route path={'/study/new'} element={<NewStudy />} />
<Route path={'/study/controls'} element={<StudyControl />} />
<Route path={'/study/permissions'} element={<StudyUserPermissions />} />
<Route path={'/study/tags'} element={<DownloadTags />} />
<Route path={'/successpage'} element={<SuccessPage />} />
<Route path={'/dataset/controls'} element={<DatasetControls />} />
<Route path={'/dataset/projectaccess'} element={<ProjectAccess />} />
<Route path={'/study/contribute'} element={<ContributePage />} />
<Route path={'/tagging'} element={<TagView />} />
<Route path={'/logoutpage'} element={<LogoutPage />} />
</Route>
</Routes>
</Box>
</Box>
</Main>
</AuthProvider>
<ApolloProvider client={apolloClient}>
<AuthProvider>
<CssBaseline />
<AppInternal />
</AuthProvider>
</ApolloProvider>
</EnvironmentContextProvider>
</BrowserRouter>
</ThemeProvider>
);
}

const AppInternal: FC = () => {
const [drawerOpen, setDrawerOpen] = useState(true);
const { authenticated } = useAuth();

const mainView: ReactNode = (
<ProjectProvider>
<StudyProvider>
<Box>
<NavBar drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</Box>
<Main open={drawerOpen}>
<Box sx={{ display: 'flex' }}>
<SideBar open={drawerOpen} drawerWidth={drawerWidth} />
<Box sx={{ flexGrow: 1, width: '90%' }}>
<MyRoutes />
</Box>
</Box>
</Main>
</StudyProvider>
</ProjectProvider>
);

return (<>{ authenticated ? mainView : <UnauthenticatedView /> }</>);
};

const UnauthenticatedView: FC = () => {
return <MyRoutes />;
};

const MyRoutes: FC = () => {
return (
<Routes>
<Route path={'/'} element={<HomePage />} />
<Route path={'/callback'} element={<AuthCallback />} />
<Route path={'/loginpage'} element={<LoginPage />} />
<Route element={<AdminGuard />}>
<Route path={'/project/new'} element={<NewProject />} />
<Route path={'/project/controls'} element={<ProjectControl />} />
<Route path={'/project/permissions'} element={<ProjectUserPermissions />} />
<Route path={'/study/new'} element={<NewStudy />} />
<Route path={'/study/controls'} element={<StudyControl />} />
<Route path={'/study/permissions'} element={<StudyUserPermissions />} />
<Route path={'/study/tags'} element={<DownloadTags />} />
<Route path={'/successpage'} element={<SuccessPage />} />
<Route path={'/dataset/controls'} element={<DatasetControls />} />
<Route path={'/dataset/projectaccess'} element={<ProjectAccess />} />
<Route path={'/study/contribute'} element={<ContributePage />} />
<Route path={'/tagging'} element={<TagView />} />
<Route path={'/logoutpage'} element={<LogoutPage />} />
</Route>
</Routes>
);
};

export default App;
102 changes: 54 additions & 48 deletions packages/client/src/components/Environment.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,63 @@
import { Box, Accordion, Button, Link } from '@mui/material';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { useContext } from 'react';
import { EnvironmentContext } from '../context/EnvironmentContext';
import { Select, MenuItem, FormControl, InputLabel, Stack, Paper, Typography } from '@mui/material';
import { useProject } from '../context/ProjectContext';
import { ProjectModel } from '../graphql/graphql';
import { useStudy } from '../context/Study';
import { Dispatch, SetStateAction, FC } from 'react';

export const Environment: React.FC = () => {
const { study } = useContext(EnvironmentContext);
const { project, updateProject } = useProject();
export const Environment: FC = () => {
const { project, projects, setProject } = useProject();
const { study, studies, setStudy } = useStudy();

const handleClick = (newValue: string) => {
const newProject: ProjectModel = { name: newValue } as any;
updateProject(newProject);
};
return (
<Paper sx={{ padding: 1 }}>
<Typography variant='body1' sx={{ paddingBottom: 1 }}>Environment</Typography>
<Stack sx={{ width: '100%' }} spacing={2}>
{/* Project Selection */}
<FieldSelector
value={project}
setValue={setProject}
label={'Project'}
options={projects}
getKey={(option) => option._id}
display={(option) => option.name}
/>
{/* Study Selection */}
<FieldSelector
value={study}
setValue={setStudy}
label={'Study'}
options={studies}
getKey={(option) => option._id}
display={(option) => option.name}
/>
</Stack>
</Paper>
);
};

interface FieldSelectorProps<T> {
value: T | null,
setValue: Dispatch<SetStateAction<T | null>>;
label: string;
options: T[];
getKey: (option: T) => string;
display: (option: T) => string;
}

const items = [
{
name: `Project: ${project?.name}`,
subitems: [{ title: 'Project name 1' }, { title: 'Project name 2' }]
},
{
name: `Study: ${study}`,
subitems: [{ title: 'Study name 1' }, { title: 'Study name 2' }]
function FieldSelector<T>(props: FieldSelectorProps<T>) {
const handleChange = (newValue: string | T) => {
if (typeof newValue == 'string') {
props.setValue(null);
return;
}
];
props.setValue(newValue);
};

return (
<Box>
{items?.map((item: any) => (
<Accordion key={item.name} disableGutters elevation={0} sx={{ '&:before': { display: 'none' } }}>
<AccordionSummary key={item.name} expandIcon={<ExpandMoreIcon />}>
<Link underline={'none'}>{item.name}</Link>
</AccordionSummary>
<AccordionDetails key={item.name}>
{item.subitems?.map((subitem: any) => (
<p key={subitem.title}>
<Button
sx={{
fontSize: '15px',
color: 'black'
}}
key={subitem.title}
onClick={() => handleClick(subitem)}
>
{subitem.title}
</Button>
</p>
))}
</AccordionDetails>
</Accordion>
))}
</Box>
<FormControl sx={{ minWidth: '200px' }}>
<InputLabel>{props.label}</InputLabel>
<Select value={props.value || ''} onChange={(event, _child) => handleChange(event.target.value)} renderValue={(option) => props.display(option)}>
{props.options.map((option) => <MenuItem value={option as any} key={props.getKey(option)}>{props.display(option)}</MenuItem>)}
</Select>
</FormControl>
);
};
8 changes: 5 additions & 3 deletions packages/client/src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { FC, ReactNode, useState } from 'react';
import { Collapse, Divider, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { ExpandMore, ExpandLess, School, Dataset, Work, Logout, GroupWork } from '@mui/icons-material';
import { useAuth } from '../context/AuthContext';
import {useNavigate} from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { Environment } from './Environment';

interface SideBarProps {
open: boolean;
Expand Down Expand Up @@ -71,16 +72,17 @@ export const SideBar: FC<SideBarProps> = ({ open, drawerWidth }) => {
boxSizing: 'border-box',
backgroundColor: '#103F68',
color: 'white',
paddingTop: 18,
paddingTop: 2,
mt: '64px'
}
}}
anchor='left'
open={open}
>
<List>
<List sx={{ paddingTop: '30px' }}>
{navItems.map((navItem) => <NavItem {...navItem} key={navItem.name} />)}
</List>
<Environment />
</Drawer>
);
};
Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createContext, FC, useContext, useEffect, useState, ReactNode } from 'r
import jwt_decode from 'jwt-decode';
import { useNavigate } from 'react-router-dom';

const AUTH_TOKEN_STR = 'token';
export const AUTH_TOKEN_STR = 'token';

export interface DecodedToken {
id: string;
Expand Down Expand Up @@ -63,7 +63,7 @@ export const AuthProvider: FC<AuthProviderProps> = (props) => {
// If not token present, redirect to login
if (!token) {
setUnautheticated();
navigate('/login');
navigate('/loginpage');
return;
}

Expand All @@ -74,7 +74,7 @@ export const AuthProvider: FC<AuthProviderProps> = (props) => {
// Handle expired token
if (currentTime > decodedToken.exp) {
setUnautheticated();
navigate('/login');
navigate('/loginpage');
return;
}

Expand Down
59 changes: 22 additions & 37 deletions packages/client/src/context/ProjectContext.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { createContext, FC, useContext, useEffect, useState } from 'react';
import { ProjectModel } from '../graphql/graphql';
import { useGetProjectLazyQuery } from '../graphql/project/project';
import { createTheme, ThemeProvider, useTheme } from '@mui/material';
import { useAuth } from '../context/AuthContext';
import { createContext, Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react';
import { Project } from '../graphql/graphql';
import { useGetProjectsQuery } from '../graphql/project/project';

export interface ProjectContextProps {
project?: ProjectModel;
updateProject: (updatedProject: ProjectModel) => void;
project: Project | null;
setProject: Dispatch<SetStateAction<Project | null>>;
projects: Project[];
updateProjectList: () => void;
}

const ProjectContext = createContext<ProjectContextProps>({} as ProjectContextProps);
Expand All @@ -15,42 +15,27 @@ export interface ProjectProviderProps {
children: React.ReactNode;
}

export const ProjectProvider: FC<ProjectProviderProps> = (props) => {
const [project, setProject] = useState<ProjectModel>();
const { decoded_token } = useAuth();
const [getProject] = useGetProjectLazyQuery();
const theme = useTheme();
const [projectTheme, setProjectTheme] = useState(theme);
export const ProjectProvider: FC<ProjectProviderProps> = ({ children }) => {
const [project, setProject] = useState<Project | null>(null);
const [projects, setProjects] = useState<Project[]>([]);

// Query for projects
const getProjectResults = useGetProjectsQuery();
useEffect(() => {
if (decoded_token?.projectId) {
getProject({ variables: { id: decoded_token.projectId } }).then((data: any) => {
if (data?.getProject) {
setProject(data.getProject as ProjectModel);
setProjectTheme(
createTheme({
...theme,
...data.getProject.muiTheme
})
);
}
});
if (getProjectResults.data) {
setProjects(getProjectResults.data.getProjects);
}
}, [decoded_token]);

const updateProject = (updatedProject: ProjectModel) => {
setProject(updatedProject);
};
}, [getProjectResults.data, getProjectResults.error]);

return (
<ProjectContext.Provider value={{ project, updateProject }}>
<ThemeProvider
theme={createTheme({
...projectTheme
})}
>
{props.children}
</ThemeProvider>
<ProjectContext.Provider value={{
project,
setProject,
projects,
updateProjectList: () => getProjectResults.refetch()
}}>
{children}
</ProjectContext.Provider>
);
};
Expand Down
Loading