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

[ENH] Made authentication optional on the UI #214

Merged
merged 7 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/e2e-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

- name: Create .env file
run: |
echo -e "NB_API_QUERY_URL=https://federate.neurobagel.org/\nNB_IS_FEDERATION_API=true" > .env
echo -e "NB_API_QUERY_URL=https://federate.neurobagel.org/\nNB_IS_FEDERATION_API=true\nNB_ENABLE_AUTH=true\nNB_QUERY_CLIENT_ID=mockclientid" > .env

- name: build
run: npm install && npm run build
Expand Down
6 changes: 3 additions & 3 deletions cypress/component/AuthDialog.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import { GoogleOAuthProvider } from '@react-oauth/google';
import AuthDialog from '../../src/components/AuthDialog';

const props = {
isLoggedIn: false,
onAuth: () => {},
onclose: () => {},
rmanaem marked this conversation as resolved.
Show resolved Hide resolved
};

describe('ContinuousField', () => {
it('Displays a MUI dialog with the title and "sing in with google" button', () => {
cy.mount(
<GoogleOAuthProvider clientId="mock-client-id">
{' '}
<AuthDialog isLoggedIn={props.isLoggedIn} onAuth={props.onAuth} />
<AuthDialog open onClose={props.onclose} onAuth={props.onAuth} />
</GoogleOAuthProvider>
);
cy.get('[data-cy="auth-dialog"]').should('be.visible');
cy.get('[data-cy="auth-dialog"]').should('contain', 'You must log in');
cy.get('[data-cy="auth-dialog"]').within(() => {
cy.contains('Google');
});
cy.get('[data-cy="close-auth-dialog-button"]').should('be.visible');
});
});
15 changes: 13 additions & 2 deletions cypress/component/Navbar.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import Navbar from '../../src/components/Navbar';

const props = {
isLoggedIn: true,
name: 'john doe',
profilePic: 'johndoe.png',
onLogout: () => {},
onLogin: () => {},
};

describe('Navbar', () => {
it('Displays a MUI Toolbar with logo, title, subtitle, documentation link, and GitHub link', () => {
cy.mount(<Navbar name={props.name} profilePic={props.profilePic} onLogout={props.onLogout} />);
cy.mount(
<Navbar
isLoggedIn={props.isLoggedIn}
name={props.name}
profilePic={props.profilePic}
onLogout={props.onLogout}
onLogin={props.onLogin}
/>
);
cy.get("[data-cy='navbar']").should('be.visible');
cy.get("[data-cy='navbar'] img").should('exist');
cy.get("[data-cy='navbar'] h5").should('contain', 'Neurobagel Query');
cy.get("[data-cy='navbar'] p").should(
'contain',
'Define and find cohorts at the subject level'
);
cy.get("[data-cy='navbar'] a").eq(0).find('svg').should('be.visible');
rmanaem marked this conversation as resolved.
Show resolved Hide resolved
cy.get("[data-cy='navbar'] a")
.should('contain', 'Documentation')
.eq(0)
.should('have.attr', 'href', 'https://neurobagel.org/query_tool/');
cy.get("[data-cy='navbar'] a").eq(1).find('svg').should('be.visible');
cy.get("[data-cy='navbar'] a")
Expand Down
8 changes: 4 additions & 4 deletions cypress/component/ResultContainer.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ResultContainer from '../../src/components/ResultContainer';
import { protectedResponse2 } from '../fixtures/mocked-responses';

describe('ResultContainer', () => {
it('Displays a set of Result Cards, select all checkbox, disabled download result buttons, summary stats, and how to get data modal button', () => {
it('Displays a set of Result Cards, select all checkbox, disabled download result buttons, summary stats, and how to get data dialog button', () => {
cy.mount(<ResultContainer response={protectedResponse2} />);
cy.get('[data-cy="card-https://someportal.org/datasets/ds0001"]').should('be.visible');
cy.get('[data-cy="card-https://someportal.org/datasets/ds0002"]').should('be.visible');
Expand All @@ -16,7 +16,7 @@ describe('ResultContainer', () => {
cy.get('[data-cy="dataset-level-download-results-button"]')
.should('be.visible')
.should('be.disabled');
cy.get('[data-cy="how-to-get-data-modal-button"]').should('be.visible');
cy.get('[data-cy="how-to-get-data-dialog-button"]').should('be.visible');
});
it('Selecting a dataset should enable the download result buttons', () => {
cy.mount(<ResultContainer response={protectedResponse2} />);
Expand Down Expand Up @@ -45,10 +45,10 @@ describe('ResultContainer', () => {
'not.be.checked'
);
});
it('Clicking the how to get data modal button should open the modal', () => {
it('Clicking the how to get data dialog button should open the dialog', () => {
cy.mount(<ResultContainer response={protectedResponse2} />);
cy.get('[data-cy="get-data-dialog"]').should('not.exist');
cy.get('[data-cy="how-to-get-data-modal-button"]').click();
cy.get('[data-cy="how-to-get-data-dialog-button"]').click();
cy.get('[data-cy="get-data-dialog"]').should('be.visible');
});
it('Shows no result view when result is empty', () => {
Expand Down
17 changes: 17 additions & 0 deletions cypress/e2e/Auth.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
describe('Authentication flow', () => {
it('Auth dialog is not visible by default and user is not logged in', () => {
cy.visit('/');
cy.get('[data-cy="auth-dialog"]').should('not.exist');
cy.get('.MuiAvatar-root').click();
cy.get('[data-cy="login-button"]').should('exist');
});
it('Auth dialog can be opened and closed', () => {
cy.visit('/');
cy.get('.MuiAvatar-root').click();
cy.get('[data-cy="login-button"]').click();
cy.get('[data-cy="auth-dialog"]').should('be.visible');
cy.get('[data-cy="close-auth-dialog-button"]').should('be.visible');
cy.get('[data-cy="close-auth-dialog-button"]').click();
cy.get('[data-cy="auth-dialog"]').should('not.exist');
});
});
16 changes: 14 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function App() {
const [searchParams, setSearchParams] = useSearchParams();

const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [openAuthDialog, setOpenAuthDialog] = useState(false);
const [name, setName] = useState<string>('');
const [profilePic, setProfilePic] = useState<string>('');
const [IDToken, setIDToken] = useState<string | undefined>('');
Expand Down Expand Up @@ -345,6 +346,7 @@ function App() {

function login(credential: string | undefined) {
setIsLoggedIn(true);
setOpenAuthDialog(false);
rmanaem marked this conversation as resolved.
Show resolved Hide resolved
const jwt: GoogleJWT = credential ? jwtDecode(credential) : ({} as GoogleJWT);
setIDToken(credential);
setName(jwt.given_name);
Expand All @@ -363,15 +365,25 @@ function App() {
<>
<div>
{enableAuth && (
<AuthDialog isLoggedIn={isLoggedIn} onAuth={(credential) => login(credential)} />
<AuthDialog
open={openAuthDialog}
onAuth={(credential) => login(credential)}
onClose={() => setOpenAuthDialog(false)}
/>
)}
</div>
<SnackbarProvider
autoHideDuration={6000}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
maxSnack={7}
/>
<Navbar name={name} profilePic={profilePic} onLogout={() => logout()} />
<Navbar
isLoggedIn={isLoggedIn}
name={name}
profilePic={profilePic}
onLogout={() => logout()}
onLogin={() => setOpenAuthDialog(true)}
/>
{showAlert() && (
<>
<Grow in={!alertDismissed}>
Expand Down
19 changes: 15 additions & 4 deletions src/components/AuthDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import { GoogleLogin } from '@react-oauth/google';

function AuthDialog({
isLoggedIn,
open,
onAuth,
onClose,
}: {
isLoggedIn: boolean;
open: boolean;
onAuth: (credential: string | undefined) => void;
onClose: () => void;
}) {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));

return (
<Dialog fullScreen={fullScreen} open={!isLoggedIn} data-cy="auth-dialog">
<DialogTitle>You must log in to a trusted identity provider in order to query!</DialogTitle>
<Dialog fullScreen={fullScreen} open={open} onClose={onClose} data-cy="auth-dialog">
<DialogTitle>
You must log in to a trusted identity provider in order to query all available nodes!
</DialogTitle>
<DialogContent>
<div className="flex flex-col items-center justify-center">
<GoogleLogin onSuccess={(response) => onAuth(response.credential)} />
</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose} data-cy="close-auth-dialog-button">
Close
</Button>
</DialogActions>
</Dialog>
);
}
Expand Down
48 changes: 34 additions & 14 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,27 @@ import {
Menu,
MenuItem,
ListItemIcon,
Tooltip,
} from '@mui/material';
import GitHubIcon from '@mui/icons-material/GitHub';
import GitHub from '@mui/icons-material/GitHub';
import Article from '@mui/icons-material/Article';
import Logout from '@mui/icons-material/Logout';
import Login from '@mui/icons-material/Login';
import Avatar from '@mui/material/Avatar';
import { enableAuth } from '../utils/constants';

function Navbar({
isLoggedIn,
name,
profilePic,
onLogout,
onLogin,
}: {
isLoggedIn: boolean;
name: string;
profilePic: string;
onLogout: () => void;
onLogin: () => void;
}) {
const [latestReleaseTag, setLatestReleaseTag] = useState('');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
Expand Down Expand Up @@ -66,11 +73,13 @@ function Navbar({
</div>
</div>
<div className="flex">
<IconButton size="small" href="https://neurobagel.org/query_tool/" target="_blank">
<Typography>Documentation</Typography>
</IconButton>
<Tooltip title="Documentation">
<IconButton size="small" href="https://neurobagel.org/query_tool/" target="_blank">
<Article />
</IconButton>
</Tooltip>
<IconButton href="https://github.com/neurobagel/react-query-tool/" target="_blank">
<GitHubIcon />
<GitHub />
</IconButton>
{enableAuth && (
<>
Expand All @@ -96,15 +105,26 @@ function Navbar({
<Avatar src={profilePic} alt={name} />
</MenuItem>
</div>
<MenuItem>
<Typography>Logged in as {name}</Typography>
</MenuItem>
<MenuItem onClick={onLogout}>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
{isLoggedIn ? (
<>
<MenuItem>
<Typography>Logged in as {name}</Typography>
</MenuItem>
<MenuItem onClick={onLogout}>
<ListItemIcon className="mr-[-8px]">
<Logout fontSize="small" />
</ListItemIcon>
Logout
</MenuItem>
</>
) : (
<MenuItem onClick={onLogin} data-cy="login-button">
<ListItemIcon className="mr-[-8px]">
<Login fontSize="small" />
</ListItemIcon>
Login
</MenuItem>
)}
</Menu>
</>
)}
Expand Down
8 changes: 4 additions & 4 deletions src/components/ResultContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import GetDataDialog from './GetDataDialog';

function ResultContainer({ response }: { response: QueryResponse | null }) {
const [download, setDownload] = useState<string[]>([]);
const [openModal, setOpenModal] = useState(false);
const [openDialog, setOpenDialog] = useState(false);
const selectAll: boolean = response
? response.responses.length === download.length &&
response.responses.every((r) => download.includes(r.dataset_uuid))
Expand Down Expand Up @@ -227,12 +227,12 @@ function ResultContainer({ response }: { response: QueryResponse | null }) {
<div className="col-span-1">
<Button
variant="contained"
data-cy="how-to-get-data-modal-button"
onClick={() => setOpenModal(true)}
data-cy="how-to-get-data-dialog-button"
onClick={() => setOpenDialog(true)}
>
How to get data
</Button>
<GetDataDialog open={openModal} onClose={() => setOpenModal(false)} />
<GetDataDialog open={openDialog} onClose={() => setOpenDialog(false)} />
</div>
<div className="col-span-3 space-x-2 justify-self-end">
<DownloadResultButton
Expand Down