mirror of
https://github.com/ellmau/adf-obdd.git
synced 2025-12-20 09:39:38 +01:00
* 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>
248 lines
7.5 KiB
TypeScript
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;
|