1
0
mirror of https://github.com/ellmau/adf-obdd.git synced 2025-12-20 09:39:38 +01:00

Compare commits

..

6 Commits

Author SHA1 Message Date
monsterkrampe
c8ab6db93d
Update Dockerfile 2023-04-14 15:44:15 +02:00
monsterkrampe
0fc75c8456
Add mongodb (DEV) data directory to dockerignore 2023-04-14 14:23:46 +02:00
monsterkrampe
7a64818e89
Add docker-compose file for mongodb (development) 2023-04-14 14:22:41 +02:00
monsterkrampe
21203f7ae8
Add Detail View for ADF problems 2023-04-14 14:19:47 +02:00
monsterkrampe
a965f75e4b
Implement ADF Add Form and Overview 2023-04-14 09:03:01 +02:00
monsterkrampe
9012f0ee23
Allow file upload for ADF; fix some server bugs 2023-04-14 09:01:39 +02:00
12 changed files with 587 additions and 363 deletions

View File

@ -1,4 +1,5 @@
frontend/node_modules frontend/node_modules
frontend/.parcel-cache frontend/.parcel-cache
server/mongodb-data
target/debug target/debug

View File

@ -1,5 +1,5 @@
# 1. BUILD-CONTAINER: Frontend # 1. BUILD-CONTAINER: Frontend
FROM node:gallium-alpine FROM node:hydrogen-alpine
WORKDIR /root WORKDIR /root
@ -30,5 +30,7 @@ WORKDIR /root
COPY --from=0 /root/dist /root/assets COPY --from=0 /root/dist /root/assets
COPY --from=1 /root/target/release/adf-bdd-server /root/server COPY --from=1 /root/target/release/adf-bdd-server /root/server
EXPOSE 8080
ENTRYPOINT ["./server"] ENTRYPOINT ["./server"]

View File

@ -1,14 +1,33 @@
import React from 'react'; import React, {
useState, useContext, useEffect, useCallback, useRef,
} from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Alert,
AlertColor,
Button,
Chip,
Container,
Paper,
Pagination,
Skeleton,
Stack,
Tabs,
Tab,
TextField,
Typography,
} from '@mui/material';
import {AlertColor} from '@mui/material'; import GraphG6, { GraphProps } from './graph-g6';
import LoadingContext from './loading-context';
import { GraphProps } from './graph-g6'; import SnackbarContext from './snackbar-context';
export type Parsing = 'Naive' | 'Hybrid'; export type Parsing = 'Naive' | 'Hybrid';
export type StrategySnakeCase = 'parse_only' | 'ground' | 'complete' | 'stable' | 'stable_counting_a' | 'stable_counting_b' | 'stable_nogood'; export type StrategySnakeCase = 'parse_only' | 'ground' | 'complete' | 'stable' | 'stable_counting_a' | 'stable_counting_b' | 'stable_nogood';
export type StrategyCamelCase = 'ParseOnly' | 'Ground' | 'Complete' | 'Stable' | 'StableCountingA' | 'StableCountingB' | 'StableNogood'; export type StrategyCamelCase = 'ParseOnly' | 'Ground' | 'Complete' | 'Stable' | 'StableCountingA' | 'StableCountingB' | 'StableNogood';
export const STRATEGIES_WITHOUT_PARSE: StrategyCamelCase[] = ['Ground', 'Complete', 'Stable', 'StableCountingA', 'StableCountingB', 'StableNogood'];
export interface AcAndGraph { export interface AcAndGraph {
ac: string[], ac: string[],
@ -36,7 +55,8 @@ export interface AdfProblemInfo {
name: string, name: string,
code: string, code: string,
parsing_used: Parsing, parsing_used: Parsing,
acs_per_strategy: { [key in StrategySnakeCase]: AcsWithGraphsOpt }, // NOTE: the keys are really only strategies // NOTE: the keys are really only strategies
acs_per_strategy: { [key in StrategySnakeCase]: AcsWithGraphsOpt },
running_tasks: Task[], running_tasks: Task[],
} }
@ -49,6 +69,8 @@ export function acsWithGraphOptToColor(status: AcsWithGraphsOpt, running: boolea
case 'None': return 'info'; case 'None': return 'info';
case 'Error': return 'error'; case 'Error': return 'error';
case 'Some': return 'success'; case 'Some': return 'success';
default:
throw new Error('Unknown type union variant (cannot occur)');
} }
} }
@ -61,12 +83,258 @@ export function acsWithGraphOptToText(status: AcsWithGraphsOpt, running: boolean
case 'None': return 'Not attempted'; case 'None': return 'Not attempted';
case 'Error': return 'Failed'; case 'Error': return 'Failed';
case 'Some': return 'Done'; case 'Some': return 'Done';
default:
throw new Error('Unknown type union variant (cannot occur)');
} }
} }
function AdfDetails() { function AdfDetails() {
const { adfName } = useParams();
const navigate = useNavigate();
const { setLoading } = useContext(LoadingContext);
const { status: snackbarInfo, setStatus: setSnackbarInfo } = useContext(SnackbarContext);
const [problem, setProblem] = useState<AdfProblemInfo>();
const [tab, setTab] = useState<StrategySnakeCase>('parse_only');
const [solutionIndex, setSolutionIndex] = useState<number>(0);
const isFirstRender = useRef(true);
const fetchProblem = useCallback(
() => {
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/${adfName}`, {
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((resProblem) => {
setProblem(resProblem);
});
break;
default:
navigate('/');
break;
}
});
},
[setProblem],
);
const solveHandler = useCallback(
(strategy: StrategyCamelCase) => {
setLoading(true);
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/${adfName}/solve`, {
method: 'PUT',
credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ strategy }),
})
.then((res) => {
switch (res.status) {
case 200:
setSnackbarInfo({ message: 'Solving problem now...', severity: 'success', potentialUserChange: false });
fetchProblem();
break;
default:
setSnackbarInfo({ message: 'Something went wrong tying to solve the problem.', severity: 'error', potentialUserChange: false });
break;
}
})
.finally(() => setLoading(false));
},
[adfName],
);
const deleteHandler = useCallback(
() => {
setLoading(true);
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/${adfName}`, {
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: 'ADF Problem deleted.', severity: 'success', potentialUserChange: false });
navigate('/');
break;
default:
break;
}
})
.finally(() => setLoading(false));
},
[adfName],
);
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;
fetchProblem();
}
},
[snackbarInfo?.potentialUserChange],
);
useEffect(
() => {
// if there is a running task, fetch problems again after 20 seconds
let timeout: ReturnType<typeof setTimeout>;
if (problem && problem.running_tasks.length > 0) {
timeout = setTimeout(() => fetchProblem(), 20000);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
},
[problem],
);
const acsOpt = problem?.acs_per_strategy[tab];
const acsContent = acsOpt?.type === 'Some' ? acsOpt.content : undefined;
const tabCamelCase: StrategyCamelCase = tab.replace(/^([a-z])/, (_, p1) => p1.toUpperCase()).replace(/_([a-z])/g, (_, p1) => `${p1.toUpperCase()}`) as StrategyCamelCase;
return ( return (
<div>Details</div> <>
<Typography variant="h3" component="h1" align="center" gutterBottom>
ADF-BDD.DEV
</Typography>
<Container sx={{ marginBottom: 4 }}>
{problem ? (
<>
<Paper elevation={8} sx={{ padding: 2, marginBottom: 2 }}>
<Stack direction="row" justifyContent="space-between" sx={{ marginBottom: 1 }}>
<Button
variant="outlined"
color="info"
onClick={() => { navigate('/'); }}
>
Back
</Button>
<Typography variant="h4" component="h2" align="center" gutterBottom>
{problem.name}
</Typography>
<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 this ADF problem?')) {
deleteHandler();
}
}}
>
Delete
</Button>
</Stack>
<TextField
name="code"
label="Code"
helperText="Click here to copy!"
multiline
maxRows={5}
fullWidth
variant="filled"
value={problem.code.trim()}
disabled
sx={{ cursor: 'pointer' }}
onClick={() => { navigator.clipboard.writeText(problem.code); setSnackbarInfo({ message: 'Code copied to clipboard!', severity: 'info', potentialUserChange: false }); }}
/>
</Paper>
<Tabs
value={tab}
onChange={(_e, newTab) => { setTab(newTab); setSolutionIndex(0); }}
variant="scrollable"
scrollButtons="auto"
>
<Tab wrapped value="parse_only" label={<Chip color={acsWithGraphOptToColor(problem.acs_per_strategy.parse_only, problem.running_tasks.some((t: Task) => t.type === 'Parse'))} label={`${problem.parsing_used} Parsing`} sx={{ cursor: 'inherit' }} />} />
{STRATEGIES_WITHOUT_PARSE.map((strategy) => {
const spaced = strategy.replace(/([A-Za-z])([A-Z])/g, '$1 $2');
const snakeCase = strategy.replace(/^([A-Z])/, (_, p1) => p1.toLowerCase()).replace(/([A-Z])/g, (_, p1) => `_${p1.toLowerCase()}`) as StrategySnakeCase;
const status = problem.acs_per_strategy[snakeCase];
const running = problem.running_tasks.some((t: Task) => t.type === 'Solve' && t.content === strategy);
const color = acsWithGraphOptToColor(status, running);
return <Tab key={strategy} wrapped value={snakeCase} label={<Chip color={color} label={spaced} sx={{ cursor: 'inherit' }} />} />;
})}
</Tabs>
{acsContent && acsContent.length > 1 && (
<>
Models:
<br />
<Pagination variant="outlined" shape="rounded" count={acsContent.length} page={solutionIndex + 1} onChange={(_e, newIdx) => setSolutionIndex(newIdx - 1)} />
</>
)}
<Paper elevation={3} square sx={{ padding: 2, marginTop: 4, marginBottom: 4 }}>
{problem.running_tasks.some((t: Task) => (tab === 'parse_only' && t.type === 'Parse') || (t.type === 'Solve' && t.content === tabCamelCase)) ? (
<Alert severity="warning">Working hard to solve the problem right now...</Alert>
) : (
<>
{acsContent && acsContent.length > 0 && (
<GraphG6 graph={acsContent[solutionIndex].graph} />
)}
{acsContent && acsContent.length === 0 && (
<Alert severity="info">The problem has no models for this strategy.</Alert>
)}
{!acsContent && acsOpt?.type === 'Error' && (
<Alert severity="error">
An error occurred:
{acsOpt.content}
</Alert>
)}
{!acsContent && acsOpt?.type === 'None' && (
<>
<Alert severity="info" sx={{ marginBottom: 1 }}>This strategy was not attempted yet.</Alert>
<Button
variant="contained"
size="large"
color="warning"
onClick={() => {
solveHandler(tabCamelCase);
}}
>
Solve now!
</Button>
</>
)}
</>
)}
</Paper>
</>
) : (
<>
<Paper elevation={8} sx={{ padding: 2, marginBottom: 8 }}>
<Skeleton variant="text" width="50%" sx={{ fontSize: '2.125rem', margin: 'auto' }} />
<Skeleton variant="rounded" width="100%" height={200} />
</Paper>
<Skeleton variant="rectangular" width="100%" height={500} />
</>
)}
</Container>
</>
); );
} }

View File

@ -3,16 +3,12 @@ import React, {
} from 'react'; } from 'react';
import { import {
Backdrop,
Button, Button,
CircularProgress,
CssBaseline,
Container, Container,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
FormLabel, FormLabel,
Link, Link,
Pagination,
Paper, Paper,
Radio, Radio,
RadioGroup, RadioGroup,
@ -54,7 +50,10 @@ function AdfNewForm({fetchProblems}: { fetchProblems: () => void; }) {
const formData = new FormData(); const formData = new FormData();
if (isFileUpload && fileRef.current) { if (isFileUpload && fileRef.current) {
formData.append('file', fileRef.current.files![0]); const file = fileRef.current.files?.[0];
if (file) {
formData.append('file', file);
}
} else { } else {
formData.append('code', code); formData.append('code', code);
} }
@ -110,7 +109,7 @@ function AdfNewForm({fetchProblems}: { fetchProblems: () => void; }) {
{isFileUpload ? ( {isFileUpload ? (
<Stack direction="row" justifyContent="center"> <Stack direction="row" justifyContent="center">
<Button component="label"> <Button component="label">
{(!!filename && fileRef?.current?.files?.[0]) ? `File '${filename.split(/[\\\/]/).pop()}' selected! (Click to change)` : 'Upload File'} {(!!filename && fileRef?.current?.files?.[0]) ? `File '${filename.split(/[\\/]/).pop()}' selected! (Click to change)` : 'Upload File'}
<input hidden type="file" onChange={(event) => { setFilename(event.target.value); }} ref={fileRef} /> <input hidden type="file" onChange={(event) => { setFilename(event.target.value); }} ref={fileRef} />
</Button> </Button>
</Stack> </Stack>
@ -147,8 +146,8 @@ function AdfNewForm({fetchProblems}: { fetchProblems: () => void; }) {
value={parsing} value={parsing}
onChange={(e) => setParsing(((e.target as HTMLInputElement).value) as Parsing)} onChange={(e) => setParsing(((e.target as HTMLInputElement).value) as Parsing)}
> >
<FormControlLabel value={'Naive'} control={<Radio />} label="Naive" /> <FormControlLabel value="Naive" control={<Radio />} label="Naive" />
<FormControlLabel value={'Hybrid'} control={<Radio />} label="Hybrid" /> <FormControlLabel value="Hybrid" control={<Radio />} label="Hybrid" />
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
<TextField <TextField

View File

@ -1,21 +1,15 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, {
useRef, useState, useCallback, useEffect, useContext,
} from 'react';
import {
useNavigate,
} from 'react-router-dom';
import { import {
Backdrop,
Button,
Chip, Chip,
CircularProgress,
CssBaseline,
Container, Container,
FormControl,
FormControlLabel,
FormLabel,
Link,
Pagination,
Paper, Paper,
Radio,
RadioGroup,
Stack,
TableContainer, TableContainer,
Table, Table,
TableHead, TableHead,
@ -23,18 +17,29 @@ import {
TableCell, TableCell,
TableBody, TableBody,
Typography, Typography,
TextField,
ToggleButtonGroup,
ToggleButton,
} from '@mui/material'; } from '@mui/material';
import AdfNewForm from './adf-new-form'; import AdfNewForm from './adf-new-form';
import {AdfProblemInfo, StrategySnakeCase, StrategyCamelCase, Task, acsWithGraphOptToColor, acsWithGraphOptToText} from './adf-details'; import {
AdfProblemInfo,
StrategySnakeCase,
STRATEGIES_WITHOUT_PARSE,
Task,
acsWithGraphOptToColor,
acsWithGraphOptToText,
} from './adf-details';
import SnackbarContext from './snackbar-context';
function AdfOverview() { function AdfOverview() {
const { status: snackbarInfo } = useContext(SnackbarContext);
const [problems, setProblems] = useState<AdfProblemInfo[]>([]); const [problems, setProblems] = useState<AdfProblemInfo[]>([]);
const navigate = useNavigate();
const isFirstRender = useRef(true);
const fetchProblems = useCallback( const fetchProblems = useCallback(
() => { () => {
fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/`, { fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/`, {
@ -47,10 +52,13 @@ function AdfOverview() {
.then((res) => { .then((res) => {
switch (res.status) { switch (res.status) {
case 200: case 200:
res.json().then((problems) => { res.json().then((resProblems) => {
setProblems(problems); setProblems(resProblems);
}); });
break; break;
case 401:
setProblems([]);
break;
default: default:
break; break;
} }
@ -60,18 +68,42 @@ function AdfOverview() {
); );
useEffect( useEffect(
() => { fetchProblems(); }, () => {
[], // 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;
fetchProblems();
}
},
[snackbarInfo?.potentialUserChange],
); );
// TODO set timeout for refetching if there are running problems useEffect(
() => {
// if there is a running task, fetch problems again after 20 seconds
let timeout: ReturnType<typeof setTimeout>;
if (problems.some((p) => p.running_tasks.length > 0)) {
timeout = setTimeout(() => fetchProblems(), 20000);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
},
[problems],
);
return ( return (
<> <>
<Typography variant="h3" component="h1" align="center" gutterBottom> <Typography variant="h3" component="h1" align="center" gutterBottom>
ADF-BDD.DEV ADF-BDD.DEV
</Typography> </Typography>
{problems.length > 0 && {problems.length > 0
&& (
<Container sx={{ marginBottom: 4 }}> <Container sx={{ marginBottom: 4 }}>
<Paper elevation={8} sx={{ padding: 2 }}> <Paper elevation={8} sx={{ padding: 2 }}>
<Typography variant="h4" component="h2" align="center" gutterBottom> <Typography variant="h4" component="h2" align="center" gutterBottom>
@ -95,7 +127,8 @@ function AdfOverview() {
{problems.map((problem) => ( {problems.map((problem) => (
<TableRow <TableRow
key={problem.name} key={problem.name}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }} onClick={() => { navigate(`/${problem.name}`); }}
sx={{ '&:last-child td, &:last-child th': { border: 0 }, cursor: 'pointer' }}
> >
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{problem.name} {problem.name}
@ -108,18 +141,18 @@ function AdfOverview() {
const color = acsWithGraphOptToColor(status, running); const color = acsWithGraphOptToColor(status, running);
const text = acsWithGraphOptToText(status, running); const text = acsWithGraphOptToText(status, running);
return <TableCell align="center"><Chip color={color} label={`${text} (${problem.parsing_used} Parsing)`} /></TableCell>; return <TableCell align="center"><Chip color={color} label={`${text} (${problem.parsing_used} Parsing)`} sx={{ cursor: 'inherit' }} /></TableCell>;
})() })()
} }
{ {
(['Ground', 'Complete', 'Stable', 'StableCountingA', 'StableCountingB', 'StableNogood'] as StrategyCamelCase[]).map((strategy) => { STRATEGIES_WITHOUT_PARSE.map((strategy) => {
const status = problem.acs_per_strategy[strategy.replace(/^([A-Z])/, (_, p1) => p1.toLowerCase()).replace(/([A-Z])/g, (_, p1) => `_${p1.toLowerCase()}`) as StrategySnakeCase]; const status = problem.acs_per_strategy[strategy.replace(/^([A-Z])/, (_, p1) => p1.toLowerCase()).replace(/([A-Z])/g, (_, p1) => `_${p1.toLowerCase()}`) as StrategySnakeCase];
const running = problem.running_tasks.some((t: Task) => t.type === 'Solve' && t.content === strategy); const running = problem.running_tasks.some((t: Task) => t.type === 'Solve' && t.content === strategy);
const color = acsWithGraphOptToColor(status, running); const color = acsWithGraphOptToColor(status, running);
const text = acsWithGraphOptToText(status, running); const text = acsWithGraphOptToText(status, running);
return <TableCell align="center"><Chip color={color} label={text} /></TableCell>; return <TableCell key={strategy} align="center"><Chip color={color} label={text} sx={{ cursor: 'inherit' }} /></TableCell>;
}) })
} }
</TableRow> </TableRow>
@ -129,7 +162,7 @@ function AdfOverview() {
</TableContainer> </TableContainer>
</Paper> </Paper>
</Container> </Container>
} )}
<AdfNewForm fetchProblems={fetchProblems} /> <AdfNewForm fetchProblems={fetchProblems} />
</> </>
); );

View File

@ -1,4 +1,4 @@
import * as React from 'react'; import React, { useState, useMemo } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
@ -7,33 +7,18 @@ import {
Alert, Alert,
AlertColor, AlertColor,
Backdrop, Backdrop,
Button,
CircularProgress, CircularProgress,
CssBaseline, CssBaseline,
Container,
FormControl,
FormControlLabel,
FormLabel,
Link,
Pagination,
Paper,
Radio,
RadioGroup,
Snackbar, Snackbar,
Typography,
TextField,
useMediaQuery, useMediaQuery,
} from '@mui/material'; } from '@mui/material';
import LoadingContext from './loading-context'; import LoadingContext from './loading-context';
import SnackbarContext from './snackbar-context'; import SnackbarContext from './snackbar-context';
import GraphG6, { GraphProps } from './graph-g6';
import Footer from './footer'; import Footer from './footer';
import AdfOverview from './adf-overview'; import AdfOverview from './adf-overview';
import AdfDetails from './adf-details'; import AdfDetails from './adf-details';
const { useState, useCallback, useMemo } = React;
const browserRouter = createBrowserRouter([ const browserRouter = createBrowserRouter([
{ {
path: '/', path: '/',
@ -49,8 +34,7 @@ function App() {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const theme = useMemo( const theme = useMemo(
() => () => createTheme({
createTheme({
palette: { palette: {
mode: prefersDarkMode ? 'dark' : 'light', mode: prefersDarkMode ? 'dark' : 'light',
}, },
@ -66,7 +50,10 @@ function App() {
severity: AlertColor, severity: AlertColor,
potentialUserChange: boolean, potentialUserChange: boolean,
} | undefined>(); } | undefined>();
const snackbarContext = useMemo(() => ({ status: snackbarInfo, setStatus: setSnackbarInfo }), [snackbarInfo, setSnackbarInfo]); const snackbarContext = useMemo(
() => ({ status: snackbarInfo, setStatus: setSnackbarInfo }),
[snackbarInfo, setSnackbarInfo],
);
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
@ -96,113 +83,4 @@ function App() {
); );
} }
// function BACKUP() {
// const [code, setCode] = useState(placeholder);
// const [parsing, setParsing] = useState(Parsing.Naive);
// const [graphs, setGraphs] = useState<GraphProps[]>();
// const [graphIndex, setGraphIndex] = useState(0);
// const submitHandler = useCallback(
// (strategy: Strategy) => {
// setLoading(true);
// fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/solve`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ code, strategy, parsing }),
// })
// .then((res) => res.json())
// .then((data) => {
// setGraphs(data);
// setGraphIndex(0);
// })
// .finally(() => setLoading(false));
// // TODO: error handling
// },
// [code, parsing],
// );
// return (<>
// <Typography variant="h2" component="h1" align="center" gutterBottom>
// Solve your ADF Problem with OBDDs!
// </Typography>
// <Container>
// <TextField
// name="code"
// label="Put your code here:"
// helperText={(
// <>
// For more info on the syntax, have a
// look
// {' '}
// <Link href="https://github.com/ellmau/adf-obdd" target="_blank" rel="noreferrer">here</Link>
// .
// </>
// )}
// multiline
// fullWidth
// variant="filled"
// value={code}
// onChange={(event) => { setCode(event.target.value); }}
// />
// </Container>
// <Container sx={{ marginTop: 2, marginBottom: 2 }}>
// <FormControl>
// <FormLabel id="parsing-radio-group">Parsing Strategy</FormLabel>
// <RadioGroup
// row
// aria-labelledby="parsing-radio-group"
// name="parsing"
// value={parsing}
// onChange={(e) => setParsing(((e.target as HTMLInputElement).value) as Parsing)}
// >
// <FormControlLabel value={Parsing.Naive} control={<Radio />} label="Naive" />
// <FormControlLabel value={Parsing.Hybrid} control={<Radio />} label="Hybrid" />
// </RadioGroup>
// </FormControl>
// <br />
// <br />
// <Button variant="outlined" onClick={() => submitHandler(Strategy.ParseOnly)}>Parse only</Button>
// {' '}
// <Button variant="outlined" onClick={() => submitHandler(Strategy.Ground)}>Grounded Model</Button>
// {' '}
// <Button variant="outlined" onClick={() => submitHandler(Strategy.Complete)}>Complete Models</Button>
// {' '}
// <Button variant="outlined" onClick={() => submitHandler(Strategy.Stable)}>Stable Models (naive heuristics)</Button>
// {' '}
// <Button disabled={parsing !== Parsing.Hybrid} variant="outlined" onClick={() => submitHandler(Strategy.StableCountingA)}>Stable Models (counting heuristic A)</Button>
// {' '}
// <Button disabled={parsing !== Parsing.Hybrid} variant="outlined" onClick={() => submitHandler(Strategy.StableCountingB)}>Stable Models (counting heuristic B)</Button>
// {' '}
// <Button variant="outlined" onClick={() => submitHandler(Strategy.StableNogood)}>Stable Models using nogoods (Simple Heuristic)</Button>
// </Container>
// {graphs
// && (
// <Container sx={{ marginTop: 4, marginBottom: 4 }}>
// {graphs.length > 1
// && (
// <>
// Models:
// <br />
// <Pagination variant="outlined" shape="rounded" count={graphs.length} page={graphIndex + 1} onChange={(e, value) => setGraphIndex(value - 1)} />
// </>
// )}
// {graphs.length > 0
// && (
// <Paper elevation={3} square sx={{ marginTop: 4, marginBottom: 4 }}>
// <GraphG6 graph={graphs[graphIndex]} />
// </Paper>
// )}
// {graphs.length === 0
// && <>No models!</>}
// </Container>
// )}
// </>
// );
// }
export default App; export default App;

View File

@ -11,7 +11,6 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Snackbar,
TextField, TextField,
Toolbar, Toolbar,
} from '@mui/material'; } from '@mui/material';
@ -78,7 +77,7 @@ function UserForm({ username: propUsername, formType, close }: UserFormProps) {
.then((res) => { .then((res) => {
switch (res.status) { switch (res.status) {
case 200: case 200:
close(`Action '${formType}' successful!`, 'success'); close(`Action '${del ? 'Delete' : formType}' successful!`, 'success');
break; break;
default: default:
setError(true); setError(true);
@ -114,13 +113,14 @@ function UserForm({ username: propUsername, formType, close }: UserFormProps) {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button type="button" onClick={() => close()}>Cancel</Button> <Button type="button" onClick={() => close()}>Cancel</Button>
<Button type="submit">{formType}</Button> <Button type="submit" variant="contained" color="success">{formType}</Button>
{formType === UserFormType.Update {formType === UserFormType.Update
// TODO: add another confirm dialog here // TODO: add another confirm dialog here
&& ( && (
<Button <Button
type="button" type="button"
variant="outlined" variant="outlined"
color="error"
onClick={() => { onClick={() => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm('Are you sure that you want to delete your account?')) { if (window.confirm('Are you sure that you want to delete your account?')) {
@ -157,7 +157,7 @@ function Footer() {
.then((res) => { .then((res) => {
switch (res.status) { switch (res.status) {
case 200: case 200:
setSnackbarInfo({ message: 'Logout successful!', severity: 'success', potentialUserChange: false }); setSnackbarInfo({ message: 'Logout successful!', severity: 'success', potentialUserChange: true });
setUsername(undefined); setUsername(undefined);
break; break;
default: default:
@ -168,7 +168,8 @@ function Footer() {
}, [setSnackbarInfo]); }, [setSnackbarInfo]);
useEffect(() => { useEffect(() => {
// TODO: having the info if the user may have changed on the snackbar info is a bit lazy and unclean; be better! // 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) { if (isFirstRender.current || snackbarInfo?.potentialUserChange) {
isFirstRender.current = false; isFirstRender.current = false;
@ -224,7 +225,9 @@ function Footer() {
formType={dialogTypeOpen} formType={dialogTypeOpen}
close={(message, severity) => { close={(message, severity) => {
setDialogTypeOpen(null); setDialogTypeOpen(null);
setSnackbarInfo((!!message && !!severity) ? { message, severity, potentialUserChange: true } : undefined); setSnackbarInfo((!!message && !!severity)
? { message, severity, potentialUserChange: true }
: undefined);
}} }}
username={dialogTypeOpen === UserFormType.Update ? username : undefined} username={dialogTypeOpen === UserFormType.Update ? username : undefined}
/> />

View File

@ -183,7 +183,7 @@ function GraphG6(props: Props) {
graphRef.current = new Graph({ graphRef.current = new Graph({
container: ref.current!, container: ref.current!,
width: 1200, width: 1200,
height: 600, height: 400,
fitView: true, fitView: true,
modes: { modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'], default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
@ -368,7 +368,9 @@ function GraphG6(props: Props) {
<div style={{ padding: 4 }}> <div style={{ padding: 4 }}>
<span style={{ color: '#ed6c02', marginRight: 8 }}>lo edge (condition is false)</span> <span style={{ color: '#ed6c02', marginRight: 8 }}>lo edge (condition is false)</span>
{' '} {' '}
<span style={{ color: '#1976d2' }}>hi edge (condition is true)</span> <span style={{ color: '#1976d2', marginRight: 8 }}>hi edge (condition is true)</span>
{' '}
Click nodes to hightlight paths! (You can also drag and zoom.)
</div> </div>
</> </>
); );

24
server/docker-compose.yml Normal file
View File

@ -0,0 +1,24 @@
version: '3.1'
services:
mongo:
image: mongo
restart: always
ports:
- 27017:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
volumes:
- ./mongodb-data:/data/db
mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: example
ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/

View File

@ -28,13 +28,13 @@ use crate::user::{username_exists, User};
type Ac = Vec<Term>; type Ac = Vec<Term>;
type AcDb = Vec<String>; type AcDb = Vec<String>;
#[derive(Copy, Clone, Deserialize, Serialize)] #[derive(Copy, Clone, Debug, Deserialize, Serialize)]
pub(crate) enum Parsing { pub(crate) enum Parsing {
Naive, Naive,
Hybrid, Hybrid,
} }
#[derive(Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub(crate) enum Strategy { pub(crate) enum Strategy {
Ground, Ground,
Complete, Complete,
@ -209,6 +209,7 @@ impl TryFrom<AddAdfProblemBodyMultipart> for AddAdfProblemBodyPlain {
.file .file
.map(|f| std::io::read_to_string(f.file).expect("TempFile should be readable")) .map(|f| std::io::read_to_string(f.file).expect("TempFile should be readable"))
.or_else(|| source.code.map(|c| c.into_inner())) .or_else(|| source.code.map(|c| c.into_inner()))
.and_then(|code| (!code.is_empty()).then_some(code))
.ok_or("Either a file or the code has to be provided.")?, .ok_or("Either a file or the code has to be provided.")?,
parsing: source.parsing.into_inner(), parsing: source.parsing.into_inner(),
}) })
@ -388,9 +389,10 @@ async fn add_adf_problem(
std::thread::sleep(Duration::from_secs(20)); std::thread::sleep(Duration::from_secs(20));
let parser = AdfParser::default(); let parser = AdfParser::default();
parser.parse()(&adf_problem_input.code) let parse_result = parser.parse()(&adf_problem_input.code)
.map_err(|_| "ADF could not be parsed, double check your input!")?; .map_err(|_| "ADF could not be parsed, double check your input!");
let result = parse_result.map(|_| {
let lib_adf = match adf_problem_input.parsing { let lib_adf = match adf_problem_input.parsing {
Parsing::Naive => Adf::from_parser(&parser), Parsing::Naive => Adf::from_parser(&parser),
Parsing::Hybrid => { Parsing::Hybrid => {
@ -404,13 +406,16 @@ async fn add_adf_problem(
graph: lib_adf.into_double_labeled_graph(None), graph: lib_adf.into_double_labeled_graph(None),
}; };
(SimplifiedAdf::from_lib_adf(lib_adf), ac_and_graph)
});
app_state app_state
.currently_running .currently_running
.lock() .lock()
.unwrap() .unwrap()
.remove(&running_info); .remove(&running_info);
Ok::<_, &str>((SimplifiedAdf::from_lib_adf(lib_adf), ac_and_graph)) result
}), }),
); );
@ -510,24 +515,30 @@ async fn solve_adf_problem(
Strategy::StableNogood => adf_problem.acs_per_strategy.stable_nogood.is_some(), Strategy::StableNogood => adf_problem.acs_per_strategy.stable_nogood.is_some(),
}; };
// NOTE: we could also return the result here instead of throwing an error but I think the canonical way should just be to call the get endpoint for the problem.
if has_been_solved {
return HttpResponse::Conflict()
.body("The ADF problem has already been solved with this strategy. You can just get the solution from the problem data directly.");
}
let username_clone = username.clone(); let username_clone = username.clone();
let problem_name_clone = problem_name.clone(); let problem_name_clone = problem_name.clone();
let acs_and_graphs_fut = timeout(
COMPUTE_TIME,
spawn_blocking(move || {
let running_info = RunningInfo { let running_info = RunningInfo {
username: username_clone, username: username_clone,
adf_name: problem_name_clone, adf_name: problem_name_clone,
task: Task::Solve(adf_problem_input.strategy), task: Task::Solve(adf_problem_input.strategy),
}; };
// NOTE: we could also return the result here instead of throwing an error but I think the canonical way should just be to call the get endpoint for the problem.
if has_been_solved
|| app_state
.currently_running
.lock()
.unwrap()
.contains(&running_info)
{
return HttpResponse::Conflict()
.body("The ADF problem has already been solved with this strategy. You can just get the solution from the problem data directly.");
}
let acs_and_graphs_fut = timeout(
COMPUTE_TIME,
spawn_blocking(move || {
app_state app_state
.currently_running .currently_running
.lock() .lock()

View File

@ -17,14 +17,14 @@ pub(crate) const DB_NAME: &str = "adf-obdd";
pub(crate) const USER_COLL: &str = "users"; pub(crate) const USER_COLL: &str = "users";
pub(crate) const ADF_COLL: &str = "adf-problems"; pub(crate) const ADF_COLL: &str = "adf-problems";
#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(tag = "type", content = "content")] #[serde(tag = "type", content = "content")]
pub(crate) enum Task { pub(crate) enum Task {
Parse, Parse,
Solve(Strategy), Solve(Strategy),
} }
#[derive(Clone, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct RunningInfo { pub(crate) struct RunningInfo {
pub(crate) username: String, pub(crate) username: String,
pub(crate) adf_name: String, pub(crate) adf_name: String,

View File

@ -43,6 +43,12 @@ async fn main() -> std::io::Result<()> {
// cookie secret ket // cookie secret ket
let secret_key = Key::generate(); let secret_key = Key::generate();
// needs to be set outside of httpserver closure to only create it once!
let app_data = web::Data::new(AppState {
mongodb_client: client.clone(),
currently_running: Mutex::new(HashSet::new()),
});
HttpServer::new(move || { HttpServer::new(move || {
let app = App::new(); let app = App::new();
@ -62,10 +68,7 @@ async fn main() -> std::io::Result<()> {
#[cfg(not(feature = "cors_for_local_development"))] #[cfg(not(feature = "cors_for_local_development"))]
let cookie_secure = true; let cookie_secure = true;
app.app_data(web::Data::new(AppState { app.app_data(app_data.clone())
mongodb_client: client.clone(),
currently_running: Mutex::new(HashSet::new()),
}))
.wrap(IdentityMiddleware::default()) .wrap(IdentityMiddleware::default())
.wrap( .wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())