Skip to content

Commit

Permalink
[ENH] Made authentication optional on the UI (#214)
Browse files Browse the repository at this point in the history
* Made authentication optional on UI

* Set default visiblity of `AuthDialog` to `false`

Renamed references of modal to dialog to avoid
confusion with image modal

* Updated `AuthDialog` component test

* Implemented an e2e test for auth dialog and profile menu functionality

* Updated `Navbar` component test

* Updated e2e-test workflow config file

* Addressed suggested changes from PR review
  • Loading branch information
rmanaem authored Jul 17, 2024
1 parent 4e98bbd commit 708adf5
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 34 deletions.
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: () => {},
};

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');
});
});
16 changes: 14 additions & 2 deletions cypress/component/Navbar.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
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'
);
// Check for the documentation and GitHub icon and links
cy.get("[data-cy='navbar'] a").eq(0).find('svg').should('be.visible');
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);
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

0 comments on commit 708adf5

Please sign in to comment.