mirror of
https://github.com/ellmau/adf-obdd.git
synced 2025-12-20 09:39:38 +01:00
Compare commits
4 Commits
46ad9eb3fa
...
da8e79147b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da8e79147b | ||
|
|
b7fd34ee48 | ||
|
|
257bd5cdfc | ||
|
|
e353e01c1d |
@ -5,7 +5,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"airbnb"
|
"airbnb",
|
||||||
|
"airbnb-typescript",
|
||||||
],
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
@ -13,7 +14,8 @@ module.exports = {
|
|||||||
"jsx": true
|
"jsx": true
|
||||||
},
|
},
|
||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
"sourceType": "module"
|
"sourceType": "module",
|
||||||
|
"project": "tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react",
|
"react",
|
||||||
|
|||||||
@ -4,16 +4,19 @@
|
|||||||
"source": "src/index.html",
|
"source": "src/index.html",
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"check": "tsc --noEmit && eslint ./src",
|
||||||
"start": "parcel",
|
"start": "parcel",
|
||||||
"build": "parcel build"
|
"build": "parcel build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.15.11",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-plugin-import": "^2.27.4",
|
"eslint-plugin-import": "^2.27.4",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-react": "^7.32.0",
|
"eslint-plugin-react": "^7.32.0",
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import '@fontsource/roboto/400.css';
|
|||||||
import '@fontsource/roboto/500.css';
|
import '@fontsource/roboto/500.css';
|
||||||
import '@fontsource/roboto/700.css';
|
import '@fontsource/roboto/700.css';
|
||||||
|
|
||||||
import App from './components/app.tsx';
|
import App from './components/app';
|
||||||
|
|
||||||
const container = document.getElementById('app');
|
const container = document.getElementById('app');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container!);
|
||||||
root.render(<App />);
|
root.render(<App />);
|
||||||
|
|||||||
@ -19,9 +19,11 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import GraphG6 from './graph-g6.tsx';
|
import LoadingContext from './loading-context';
|
||||||
|
import GraphG6, { GraphProps } from './graph-g6';
|
||||||
|
import Footer from './footer';
|
||||||
|
|
||||||
const { useState, useCallback } = React;
|
const { useState, useCallback, useMemo } = React;
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@ -57,7 +59,7 @@ function App() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [code, setCode] = useState(placeholder);
|
const [code, setCode] = useState(placeholder);
|
||||||
const [parsing, setParsing] = useState(Parsing.Naive);
|
const [parsing, setParsing] = useState(Parsing.Naive);
|
||||||
const [graphs, setGraphs] = useState();
|
const [graphs, setGraphs] = useState<GraphProps[]>();
|
||||||
const [graphIndex, setGraphIndex] = useState(0);
|
const [graphIndex, setGraphIndex] = useState(0);
|
||||||
|
|
||||||
const submitHandler = useCallback(
|
const submitHandler = useCallback(
|
||||||
@ -82,8 +84,11 @@ function App() {
|
|||||||
[code, parsing],
|
[code, parsing],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadingContext = useMemo(() => ({ loading, setLoading }), [loading, setLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
<LoadingContext.Provider value={loadingContext}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<main>
|
<main>
|
||||||
<Typography variant="h2" component="h1" align="center" gutterBottom>
|
<Typography variant="h2" component="h1" align="center" gutterBottom>
|
||||||
@ -118,7 +123,7 @@ function App() {
|
|||||||
aria-labelledby="parsing-radio-group"
|
aria-labelledby="parsing-radio-group"
|
||||||
name="parsing"
|
name="parsing"
|
||||||
value={parsing}
|
value={parsing}
|
||||||
onChange={(e) => setParsing((e.target as HTMLInputElement).value)}
|
onChange={(e) => setParsing(((e.target as HTMLInputElement).value) as Parsing)}
|
||||||
>
|
>
|
||||||
<FormControlLabel value={Parsing.Naive} control={<Radio />} label="Naive" />
|
<FormControlLabel value={Parsing.Naive} control={<Radio />} label="Naive" />
|
||||||
<FormControlLabel value={Parsing.Hybrid} control={<Radio />} label="Hybrid" />
|
<FormControlLabel value={Parsing.Hybrid} control={<Radio />} label="Hybrid" />
|
||||||
@ -163,12 +168,14 @@ function App() {
|
|||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
<Footer />
|
||||||
|
|
||||||
<Backdrop
|
<Backdrop
|
||||||
open={loading}
|
open={loading}
|
||||||
>
|
>
|
||||||
<CircularProgress color="inherit" />
|
<CircularProgress color="inherit" />
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
|
</LoadingContext.Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
241
frontend/src/components/footer.tsx
Normal file
241
frontend/src/components/footer.tsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import React, {
|
||||||
|
useState, useCallback, useContext, useEffect,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertColor,
|
||||||
|
Alert,
|
||||||
|
AppBar,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Snackbar,
|
||||||
|
TextField,
|
||||||
|
Toolbar,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import LoadingContext from './loading-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 '${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">{formType}</Button>
|
||||||
|
{formType === UserFormType.Update
|
||||||
|
// TODO: add another confirm dialog here
|
||||||
|
&& (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outlined"
|
||||||
|
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 [username, setUsername] = useState<string>();
|
||||||
|
const [tempUser, setTempUser] = useState<boolean>();
|
||||||
|
const [dialogTypeOpen, setDialogTypeOpen] = useState<UserFormType | null>(null);
|
||||||
|
const [snackbarInfo, setSnackbarInfo] = useState<{
|
||||||
|
message: string,
|
||||||
|
severity: AlertColor,
|
||||||
|
} | undefined>();
|
||||||
|
|
||||||
|
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' });
|
||||||
|
setUsername(undefined);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setSnackbarInfo({ message: 'An error occurred while trying to log out.', severity: 'error' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [setSnackbarInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Intuition: If the dialog was just closed (or on first render).
|
||||||
|
if (!dialogTypeOpen) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dialogTypeOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppBar position="fixed" sx={{ top: 'auto', bottom: 0 }}>
|
||||||
|
<Toolbar sx={{ justifyContent: 'center' }}>
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<Dialog open={!!dialogTypeOpen} onClose={() => setDialogTypeOpen(null)}>
|
||||||
|
<UserForm
|
||||||
|
formType={dialogTypeOpen}
|
||||||
|
close={(message, severity) => {
|
||||||
|
setDialogTypeOpen(null);
|
||||||
|
setSnackbarInfo((!!message && !!severity) ? { message, severity } : undefined);
|
||||||
|
}}
|
||||||
|
username={dialogTypeOpen === UserFormType.Update ? username : undefined}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
<Snackbar
|
||||||
|
open={!!snackbarInfo}
|
||||||
|
autoHideDuration={10000}
|
||||||
|
onClose={() => setSnackbarInfo(undefined)}
|
||||||
|
>
|
||||||
|
<Alert severity={snackbarInfo?.severity}>{snackbarInfo?.message}</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import G6 from '@antv/g6';
|
import G6, { Graph } from '@antv/g6';
|
||||||
|
|
||||||
G6.registerNode('nodeWithFlag', {
|
G6.registerNode('nodeWithFlag', {
|
||||||
draw(cfg, group) {
|
draw(cfg, group) {
|
||||||
const mainWidth = Math.max(30, 5 * cfg.mainLabel.length + 10);
|
const mainWidth = Math.max(30, 5 * (cfg!.mainLabel as string).length + 10);
|
||||||
const mainHeight = 30;
|
const mainHeight = 30;
|
||||||
|
|
||||||
const keyShape = group.addShape('rect', {
|
const keyShape = group!.addShape('rect', {
|
||||||
attrs: {
|
attrs: {
|
||||||
width: mainWidth,
|
width: mainWidth,
|
||||||
height: mainHeight,
|
height: mainHeight,
|
||||||
@ -20,13 +20,13 @@ G6.registerNode('nodeWithFlag', {
|
|||||||
draggable: true,
|
draggable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
group.addShape('text', {
|
group!.addShape('text', {
|
||||||
attrs: {
|
attrs: {
|
||||||
x: mainWidth / 2,
|
x: mainWidth / 2,
|
||||||
y: mainHeight / 2,
|
y: mainHeight / 2,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
textBaseline: 'middle',
|
textBaseline: 'middle',
|
||||||
text: cfg.mainLabel,
|
text: cfg!.mainLabel,
|
||||||
fill: '#212121',
|
fill: '#212121',
|
||||||
fontFamily: 'Roboto',
|
fontFamily: 'Roboto',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@ -37,14 +37,14 @@ G6.registerNode('nodeWithFlag', {
|
|||||||
draggable: true,
|
draggable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cfg.subLabel) {
|
if (cfg!.subLabel) {
|
||||||
const subWidth = 5 * cfg.subLabel.length + 4;
|
const subWidth = 5 * (cfg!.subLabel as string).length + 4;
|
||||||
const subHeight = 20;
|
const subHeight = 20;
|
||||||
|
|
||||||
const subRectX = mainWidth - 4;
|
const subRectX = mainWidth - 4;
|
||||||
const subRectY = -subHeight + 4;
|
const subRectY = -subHeight + 4;
|
||||||
|
|
||||||
group.addShape('rect', {
|
group!.addShape('rect', {
|
||||||
attrs: {
|
attrs: {
|
||||||
x: subRectX,
|
x: subRectX,
|
||||||
y: subRectY,
|
y: subRectY,
|
||||||
@ -59,13 +59,13 @@ G6.registerNode('nodeWithFlag', {
|
|||||||
draggable: true,
|
draggable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
group.addShape('text', {
|
group!.addShape('text', {
|
||||||
attrs: {
|
attrs: {
|
||||||
x: subRectX + subWidth / 2,
|
x: subRectX + subWidth / 2,
|
||||||
y: subRectY + subHeight / 2,
|
y: subRectY + subHeight / 2,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
textBaseline: 'middle',
|
textBaseline: 'middle',
|
||||||
text: cfg.subLabel,
|
text: cfg!.subLabel,
|
||||||
fill: '#212121',
|
fill: '#212121',
|
||||||
fontFamily: 'Roboto',
|
fontFamily: 'Roboto',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@ -95,6 +95,7 @@ G6.registerNode('nodeWithFlag', {
|
|||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
setState(name, value, item) {
|
setState(name, value, item) {
|
||||||
|
if (!item) { return; }
|
||||||
const group = item.getContainer();
|
const group = item.getContainer();
|
||||||
const mainShape = group.get('children')[0]; // Find the first graphics shape of the node. It is determined by the order of being added
|
const mainShape = group.get('children')[0]; // Find the first graphics shape of the node. It is determined by the order of being added
|
||||||
const subShape = group.get('children')[2];
|
const subShape = group.get('children')[2];
|
||||||
@ -131,11 +132,11 @@ G6.registerNode('nodeWithFlag', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GraphProps {
|
export interface GraphProps {
|
||||||
lo_edges: [number, number][],
|
lo_edges: [string, string][],
|
||||||
hi_edges: [number, number][],
|
hi_edges: [string, string][],
|
||||||
node_labels: { [key: number]: string },
|
node_labels: { [key: string]: string },
|
||||||
tree_root_labels: { [key: number]: string[] },
|
tree_root_labels: { [key: string]: string[] },
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodesAndEdgesFromGraphProps(graphProps: GraphProps) {
|
function nodesAndEdgesFromGraphProps(graphProps: GraphProps) {
|
||||||
@ -174,24 +175,26 @@ function GraphG6(props: Props) {
|
|||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
const graphRef = useRef();
|
const graphRef = useRef<Graph>();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (!graphRef.current) {
|
if (!graphRef.current) {
|
||||||
graphRef.current = new G6.Graph({
|
graphRef.current = new Graph({
|
||||||
container: ref.current,
|
container: ref.current!,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 600,
|
height: 600,
|
||||||
fitView: true,
|
fitView: true,
|
||||||
|
modes: {
|
||||||
|
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'dagre',
|
||||||
rankdir: 'TB',
|
rankdir: 'TB',
|
||||||
align: 'DR',
|
align: 'DR',
|
||||||
nodesep: 100,
|
nodesep: 100,
|
||||||
ranksep: 100,
|
ranksep: 100,
|
||||||
modes: {
|
|
||||||
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
|
|
||||||
},
|
},
|
||||||
layout: { type: 'dagre' },
|
|
||||||
// defaultNode: {
|
// defaultNode: {
|
||||||
// anchorPoints: [[0.5, 0], [0, 0.5], [1, 0.5], [0.5, 1]],
|
// anchorPoints: [[0.5, 0], [0, 0.5], [1, 0.5], [0.5, 1]],
|
||||||
// type: 'rect',
|
// type: 'rect',
|
||||||
@ -239,13 +242,13 @@ function GraphG6(props: Props) {
|
|||||||
|
|
||||||
// Mouse enter a node
|
// Mouse enter a node
|
||||||
graph.on('node:mouseenter', (e) => {
|
graph.on('node:mouseenter', (e) => {
|
||||||
const nodeItem = e.item; // Get the target item
|
const nodeItem = e.item!; // Get the target item
|
||||||
graph.setItemState(nodeItem, 'hover', true); // Set the state 'hover' of the item to be true
|
graph.setItemState(nodeItem, 'hover', true); // Set the state 'hover' of the item to be true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mouse leave a node
|
// Mouse leave a node
|
||||||
graph.on('node:mouseleave', (e) => {
|
graph.on('node:mouseleave', (e) => {
|
||||||
const nodeItem = e.item; // Get the target item
|
const nodeItem = e.item!; // Get the target item
|
||||||
graph.setItemState(nodeItem, 'hover', false); // Set the state 'hover' of the item to be false
|
graph.setItemState(nodeItem, 'hover', false); // Set the state 'hover' of the item to be false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -254,11 +257,11 @@ function GraphG6(props: Props) {
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
const graph = graphRef.current;
|
const graph = graphRef.current!;
|
||||||
|
|
||||||
// Click a node
|
// Click a node
|
||||||
graph.on('node:click', (e) => {
|
graph.on('node:click', (e) => {
|
||||||
const nodeItem = e.item; // et the clicked item
|
const nodeItem = e.item!; // et the clicked item
|
||||||
|
|
||||||
let onlyRemoveStates = false;
|
let onlyRemoveStates = false;
|
||||||
if (nodeItem.hasState('highlight')) {
|
if (nodeItem.hasState('highlight')) {
|
||||||
@ -290,12 +293,12 @@ function GraphG6(props: Props) {
|
|||||||
graph.setItemState(edge, 'lowlight', true);
|
graph.setItemState(edge, 'lowlight', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const relevantNodeIds = [];
|
const relevantNodeIds: string[] = [];
|
||||||
const relevantLoEdges = [];
|
const relevantLoEdges: [string, string][] = [];
|
||||||
const relevantHiEdges = [];
|
const relevantHiEdges: [string, string][] = [];
|
||||||
let newNodeIds = [nodeItem.getModel().id];
|
let newNodeIds: string[] = [nodeItem.getModel().id!];
|
||||||
let newLoEdges = [];
|
let newLoEdges: [string, string][] = [];
|
||||||
let newHiEdges = [];
|
let newHiEdges: [string, string][] = [];
|
||||||
|
|
||||||
while (newNodeIds.length > 0 || newLoEdges.length > 0 || newHiEdges.length > 0) {
|
while (newNodeIds.length > 0 || newLoEdges.length > 0 || newHiEdges.length > 0) {
|
||||||
relevantNodeIds.push(...newNodeIds);
|
relevantNodeIds.push(...newNodeIds);
|
||||||
@ -347,7 +350,7 @@ function GraphG6(props: Props) {
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
const graph = graphRef.current;
|
const graph = graphRef.current!;
|
||||||
|
|
||||||
const { nodes, edges } = nodesAndEdgesFromGraphProps(graphProps);
|
const { nodes, edges } = nodesAndEdgesFromGraphProps(graphProps);
|
||||||
|
|
||||||
|
|||||||
13
frontend/src/components/loading-context.ts
Normal file
13
frontend/src/components/loading-context.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
interface ILoadingContext {
|
||||||
|
loading: boolean;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingContext = createContext<ILoadingContext>({
|
||||||
|
loading: false,
|
||||||
|
setLoading: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoadingContext;
|
||||||
103
frontend/tsconfig.json
Normal file
103
frontend/tsconfig.json
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
"jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1323,6 +1323,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||||
|
|
||||||
|
"@types/node@^18.15.11":
|
||||||
|
version "18.15.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
|
||||||
|
integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
@ -2204,6 +2209,13 @@ eslint-config-airbnb-base@^15.0.0:
|
|||||||
object.entries "^1.1.5"
|
object.entries "^1.1.5"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
|
eslint-config-airbnb-typescript@^17.0.0:
|
||||||
|
version "17.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz#360dbcf810b26bbcf2ff716198465775f1c49a07"
|
||||||
|
integrity sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==
|
||||||
|
dependencies:
|
||||||
|
eslint-config-airbnb-base "^15.0.0"
|
||||||
|
|
||||||
eslint-config-airbnb@^19.0.4:
|
eslint-config-airbnb@^19.0.4:
|
||||||
version "19.0.4"
|
version "19.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3"
|
resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3"
|
||||||
|
|||||||
@ -51,6 +51,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.allowed_origin("http://localhost:1234")
|
.allowed_origin("http://localhost:1234")
|
||||||
.allow_any_method()
|
.allow_any_method()
|
||||||
.allow_any_header()
|
.allow_any_header()
|
||||||
|
.supports_credentials()
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
|
|
||||||
#[cfg(feature = "cors_for_local_development")]
|
#[cfg(feature = "cors_for_local_development")]
|
||||||
|
|||||||
@ -56,6 +56,11 @@ pub(crate) async fn username_exists(user_coll: &mongodb::Collection<User>, usern
|
|||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(app_state: web::Data<AppState>, user: web::Json<UserPayload>) -> impl Responder {
|
async fn register(app_state: web::Data<AppState>, user: web::Json<UserPayload>) -> impl Responder {
|
||||||
let mut user: UserPayload = user.into_inner();
|
let mut user: UserPayload = user.into_inner();
|
||||||
|
|
||||||
|
if user.username.is_empty() || user.password.is_empty() {
|
||||||
|
return HttpResponse::BadRequest().body("Username and Password need to be set!");
|
||||||
|
}
|
||||||
|
|
||||||
let user_coll = app_state
|
let user_coll = app_state
|
||||||
.mongodb_client
|
.mongodb_client
|
||||||
.database(DB_NAME)
|
.database(DB_NAME)
|
||||||
@ -115,9 +120,6 @@ async fn delete_account(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||||
Ok(DeleteResult {
|
|
||||||
deleted_count: 0, ..
|
|
||||||
}) => HttpResponse::InternalServerError().body("Account could not be deleted."),
|
|
||||||
Ok(DeleteResult {
|
Ok(DeleteResult {
|
||||||
deleted_count: _, ..
|
deleted_count: _, ..
|
||||||
}) => {
|
}) => {
|
||||||
@ -153,6 +155,11 @@ async fn login(
|
|||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let username = &user_data.username;
|
let username = &user_data.username;
|
||||||
let pw = &user_data.password;
|
let pw = &user_data.password;
|
||||||
|
|
||||||
|
if username.is_empty() || pw.is_empty() {
|
||||||
|
return HttpResponse::BadRequest().body("Username and Password need to be set!");
|
||||||
|
}
|
||||||
|
|
||||||
let user_coll: mongodb::Collection<User> = app_state
|
let user_coll: mongodb::Collection<User> = app_state
|
||||||
.mongodb_client
|
.mongodb_client
|
||||||
.database(DB_NAME)
|
.database(DB_NAME)
|
||||||
@ -163,7 +170,7 @@ async fn login(
|
|||||||
{
|
{
|
||||||
Ok(Some(user)) => {
|
Ok(Some(user)) => {
|
||||||
let stored_password = match &user.password {
|
let stored_password = match &user.password {
|
||||||
None => return HttpResponse::BadRequest().body("Invalid email or password"), // NOTE: login as tremporary user is not allowed
|
None => return HttpResponse::BadRequest().body("Invalid username or password"), // NOTE: login as tremporary user is not allowed
|
||||||
Some(password) => password,
|
Some(password) => password,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -264,6 +271,11 @@ async fn update_user(
|
|||||||
user: web::Json<UserPayload>,
|
user: web::Json<UserPayload>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let mut user: UserPayload = user.into_inner();
|
let mut user: UserPayload = user.into_inner();
|
||||||
|
|
||||||
|
if user.username.is_empty() || user.password.is_empty() {
|
||||||
|
return HttpResponse::BadRequest().body("Username and Password need to be set!");
|
||||||
|
}
|
||||||
|
|
||||||
let user_coll = app_state
|
let user_coll = app_state
|
||||||
.mongodb_client
|
.mongodb_client
|
||||||
.database(DB_NAME)
|
.database(DB_NAME)
|
||||||
@ -324,10 +336,6 @@ async fn update_user(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||||
Ok(UpdateResult {
|
|
||||||
modified_count: 0, ..
|
|
||||||
}) => HttpResponse::InternalServerError()
|
|
||||||
.body("Account could not be updated."),
|
|
||||||
Ok(UpdateResult {
|
Ok(UpdateResult {
|
||||||
modified_count: _, ..
|
modified_count: _, ..
|
||||||
}) => HttpResponse::Ok().json(UserInfo {
|
}) => HttpResponse::Ok().json(UserInfo {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user