1
0
mirror of https://github.com/ellmau/adf-obdd.git synced 2025-12-20 09:39:38 +01:00
Stefan Ellmauthaler c9278cf5ce
milestone/frontend (#63)
* Introduce separate server package

* Implement basic visualization of solve response

* Make fetch endpoint depend on environment

* Introduce features flag for localhost cors support

* Serve static files from './assets' directory

* Add Dockerfile as example for server with frontend

* Support multiple solving strategies

* Support stable model semantics with nogoods

* Introduce custom node type for nicer layout

* Support more options and multiple models

* Use standard example for adfs on the frontend

* Use unoptimised hybrid step for better presentation

* Upgrade frontend dependencies

* Animate graph changes

* Experiment with timeout on API endpoints

* Relax CORS restrictions for local development

* Add API for adding/deleting users; login; logout

* Add API for uploading and solving adf problems

* Add API for getting and updating user

* Return early for parse and solve; Add Adf GET

* Add Delete and Index endpoints for ADFs

* Add basic UI for user endpoints

* Enforce username and password to be set on login

* Show colored snackbars

* Allow file upload for ADF; fix some server bugs

* Implement ADF Add Form and Overview

* Add Detail View for ADF problems

* Add docker-compose file for mongodb (development)

* Add mongodb (DEV) data directory to dockerignore

* Let unknown routes be handled by frontend

* Add legal information page to frontend

* Change G6 Graph layout slightly

* Add missing doc comments to lib

* Update legal information regarding cookies

* Add project logos to frontend

* Add help texts to frontend

* Move DoubleLabeledGraph from lib to server

* Give example for custom Adf datastructure in docs

* Update README and Project Website

* Update devskim.yml

* Add READMEs for frontend and server

---------

Co-authored-by: monsterkrampe <monsterkrampe@users.noreply.github.com>
2023-05-04 17:10:38 +02:00

248 lines
7.5 KiB
TypeScript

import React, {
useState, useCallback, useContext, useEffect, useRef,
} from 'react';
import {
AlertColor,
Alert,
AppBar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Link,
TextField,
Toolbar,
} from '@mui/material';
import LoadingContext from './loading-context';
import SnackbarContext from './snackbar-context';
enum UserFormType {
Login = 'Login',
Register = 'Register',
Update = 'Update',
}
interface UserFormProps {
formType: UserFormType | null;
close: (message?: string, severity?: AlertColor) => void;
username?: string;
}
function UserForm({ username: propUsername, formType, close }: UserFormProps) {
const { setLoading } = useContext(LoadingContext);
const [username, setUsername] = useState<string>(propUsername || '');
const [password, setPassword] = useState<string>('');
const [errorOccurred, setError] = useState<boolean>(false);
const submitHandler = useCallback(
(del: boolean) => {
setLoading(true);
setError(false);
let method; let
endpoint;
if (del) {
method = 'DELETE';
endpoint = '/users/delete';
} else {
switch (formType) {
case UserFormType.Login:
method = 'POST';
endpoint = '/users/login';
break;
case UserFormType.Register:
method = 'POST';
endpoint = '/users/register';
break;
case UserFormType.Update:
method = 'PUT';
endpoint = '/users/update';
break;
default:
// NOTE: the value is not null when the dialog is open
break;
}
}
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}${endpoint}`, {
method,
credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: !del ? JSON.stringify({ username, password }) : undefined,
})
.then((res) => {
switch (res.status) {
case 200:
close(`Action '${del ? 'Delete' : formType}' successful!`, 'success');
break;
default:
setError(true);
break;
}
})
.finally(() => setLoading(false));
},
[username, password, formType],
);
return (
<form onSubmit={(e) => { e.preventDefault(); submitHandler(false); }}>
<DialogTitle>{formType}</DialogTitle>
<DialogContent>
<TextField
variant="standard"
type="text"
label="Username"
value={username}
onChange={(event) => { setUsername(event.target.value); }}
/>
<br />
<TextField
variant="standard"
type="password"
label="Password"
value={password}
onChange={(event) => { setPassword(event.target.value); }}
/>
{errorOccurred
&& <Alert severity="error">Check your inputs!</Alert>}
</DialogContent>
<DialogActions>
<Button type="button" onClick={() => close()}>Cancel</Button>
<Button type="submit" variant="contained" color="success">{formType}</Button>
{formType === UserFormType.Update
// TODO: add another confirm dialog here
&& (
<Button
type="button"
variant="outlined"
color="error"
onClick={() => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure that you want to delete your account?')) {
submitHandler(true);
}
}}
>
Delete Account
</Button>
)}
</DialogActions>
</form>
);
}
UserForm.defaultProps = { username: undefined };
function Footer() {
const { status: snackbarInfo, setStatus: setSnackbarInfo } = useContext(SnackbarContext);
const [username, setUsername] = useState<string>();
const [tempUser, setTempUser] = useState<boolean>();
const [dialogTypeOpen, setDialogTypeOpen] = useState<UserFormType | null>(null);
const isFirstRender = useRef(true);
const logout = useCallback(() => {
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/users/logout`, {
method: 'DELETE',
credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin',
headers: {
'Content-Type': 'application/json',
},
})
.then((res) => {
switch (res.status) {
case 200:
setSnackbarInfo({ message: 'Logout successful!', severity: 'success', potentialUserChange: true });
setUsername(undefined);
break;
default:
setSnackbarInfo({ message: 'An error occurred while trying to log out.', severity: 'error', potentialUserChange: false });
break;
}
});
}, [setSnackbarInfo]);
useEffect(() => {
// TODO: having the info if the user may have changed on the snackbar info
// is a bit lazy and unclean; be better!
if (isFirstRender.current || snackbarInfo?.potentialUserChange) {
isFirstRender.current = false;
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/users/info`, {
method: 'GET',
credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin',
headers: {
'Content-Type': 'application/json',
},
})
.then((res) => {
switch (res.status) {
case 200:
res.json().then(({ username: user, temp }) => {
setUsername(user);
setTempUser(temp);
});
break;
default:
setUsername(undefined);
break;
}
});
}
}, [snackbarInfo?.potentialUserChange]);
return (
<>
<AppBar position="fixed" sx={{ top: 'auto', bottom: 0 }}>
<Toolbar sx={{ justifyContent: 'center', alignItems: 'center' }}>
<Box sx={{ flexGrow: 1 }}>
{username ? (
<>
<span>
Logged in as:
{' '}
{username}
{' '}
{tempUser ? '(Temporary User. Edit to set a password!)' : undefined}
</span>
<Button color="inherit" onClick={() => setDialogTypeOpen(UserFormType.Update)}>Edit</Button>
{!tempUser && <Button color="inherit" onClick={() => logout()}>Logout</Button>}
</>
) : (
<>
<Button color="inherit" onClick={() => setDialogTypeOpen(UserFormType.Login)}>Login</Button>
<Button color="inherit" onClick={() => setDialogTypeOpen(UserFormType.Register)}>Register</Button>
</>
)}
</Box>
<Link color="inherit" href="/legal.html" target="_blank" sx={{ fontSize: '0.8rem' }}>
Legal Information (Impressum and Data Protection Regulation)
</Link>
</Toolbar>
</AppBar>
<Dialog open={!!dialogTypeOpen} onClose={() => setDialogTypeOpen(null)}>
<UserForm
formType={dialogTypeOpen}
close={(message, severity) => {
setDialogTypeOpen(null);
setSnackbarInfo((!!message && !!severity)
? { message, severity, potentialUserChange: true }
: undefined);
}}
username={dialogTypeOpen === UserFormType.Update ? username : undefined}
/>
</Dialog>
</>
);
}
export default Footer;