Router bugfix, save and update route frontend

This commit is contained in:
2024-03-29 13:33:29 +01:00
parent 90e7024a73
commit 80b5ac07d0
13 changed files with 352 additions and 136 deletions

View File

@@ -20,7 +20,8 @@ const GroupListView = lazy(() => import('./views/group/group-list'));
const EditGroupView = lazy(() => import('./views/group/group-edit'));
const LogView = lazy(() => import("./views/log-view"));
const AccessControlList = lazy(() => import("./views/access-control-list"));
const RouteListView = lazy(() => import("./views/routes"));
const RouteListView = lazy(() => import("./views/route/route-list"));
const RouteEditView = lazy(() => import("./views/route/route-edit"));
export default function AdminDashboard(props) {
@@ -81,6 +82,7 @@ export default function AdminDashboard(props) {
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
<Route path={"/admin/acl"} element={<AccessControlList {...controlObj} />}/>
<Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/>
<Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/>
<Route path={"*"} element={<View404 />} />
</Routes>
</Suspense>

View File

@@ -18,6 +18,10 @@ import {Add, Delete, Edit, Refresh} from "@material-ui/icons";
import {USER_GROUP_ADMIN} from "shared/constants";
import Dialog from "shared/elements/dialog";
const BorderedColumn = styled(TableCell)({
borderLeft: "1px dotted #666",
borderRight: "1px dotted #666",
});
export default function AccessControlList(props) {
@@ -203,11 +207,6 @@ export default function AccessControlList(props) {
return <>{rows}</>
}
const BorderedColumn = styled(TableCell)({
borderLeft: "1px dotted #666",
borderRight: "1px dotted #666",
});
return <>
<div className={"content-header"}>
<div className={"container-fluid"}>

View File

@@ -0,0 +1,153 @@
import {Link, useNavigate, useParams} from "react-router-dom";
import {useCallback, useContext, useEffect, useState} from "react";
import {LocaleContext} from "shared/locale";
import {
Box,
Button,
CircularProgress, styled,
} from "@material-ui/core";
import * as React from "react";
import RouteForm from "./route-form";
import {KeyboardArrowLeft, Save} from "@material-ui/icons";
import {TextField} from "@mui/material";
const ButtonBar = styled(Box)((props) => ({
"& > button": {
marginRight: props.theme.spacing(1)
}
}));
export default function RouteEditView(props) {
const {api, showDialog} = props;
const {routeId} = useParams();
const navigate = useNavigate();
const isNewRoute = routeId === "new";
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
// data
const [routeTest, setRouteTest] = useState("");
const [fetchRoute, setFetchRoute] = useState(!isNewRoute);
const [route, setRoute] = useState(isNewRoute ? {
pattern: "",
type: "",
target: "",
extra: "",
exact: true,
active: true
} : null);
// ui
const [routeTestResult, setRouteTestResult] = useState(false);
const [isSaving, setSaving] = useState(false);
useEffect(() => {
requestModules(props.api, ["general"], currentLocale).then(data => {
if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg);
}
});
}, [currentLocale]);
useEffect(() => {
if (routeTest?.trim()) {
props.api.testRoute(route.pattern, routeTest, route.exact).then(data => {
if (!data.success) {
props.showDialog("Error testing route: " + data.msg);
} else {
setRouteTestResult(data.match);
}
});
} else {
setRouteTestResult(false);
}
}, [routeTest]);
const onFetchRoute = useCallback((force = false) => {
if (!isNewRoute && (force || fetchRoute)) {
setFetchRoute(false);
api.getRoute(routeId).then((res) => {
if (!res.success) {
showDialog(res.msg, L("Error fetching route"));
navigate("/admin/routes");
} else {
setRoute(res.route);
}
});
}
}, [api, showDialog, fetchRoute, isNewRoute, routeId, route]);
const onSave = useCallback(() => {
if (!isSaving) {
setSaving(true);
let args = [route.pattern, route.type, route.target, route.extra, route.exact, route.active];
if (isNewRoute) {
api.addRoute(...args).then(res => {
setSaving(false);
if (res.success) {
navigate("/admin/routes/" + res.routeId);
} else {
showDialog(res.msg, L("Error saving route"));
}
});
} else {
args = [routeId, ...args];
api.updateRoute(...args).then(res => {
setSaving(false);
if (!res.success) {
showDialog(res.msg, L("Error saving route"));
}
});
}
}
}, [api, route, isSaving, isNewRoute, routeId]);
useEffect(() => {
if (!isNewRoute) {
onFetchRoute(true);
}
}, []);
if (route === null) {
return <CircularProgress/>
}
return <div className={"content-header"}>
<div className={"container-fluid"}>
<ol className={"breadcrumb"}>
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item active"><Link to={"/admin/routes"}>Routes</Link></li>
<li className="breadcrumb-item active">{isNewRoute ? "New" : "Edit"}</li>
</ol>
</div>
<div className={"content"}>
<div className={"container-fluid"}>
<h3>{L(isNewRoute ? "Create new Route" : "Edit Route")}</h3>
<div className={"col-sm-12 col-lg-6"}>
</div>
</div>
</div>
<RouteForm route={route} setRoute={setRoute} />
<ButtonBar mt={2}>
<Button startIcon={<KeyboardArrowLeft />}
variant={"outlined"}
onClick={() => navigate("/admin/routes")}>
{L("general.cancel")}
</Button>
<Button startIcon={<Save />} color={"primary"}
variant={"outlined"} disabled={isSaving}
onClick={onSave}>
{isSaving ? L("general.saving") + "…" : L("general.save")}
</Button>
</ButtonBar>
<Box mt={3}>
<h5>{L("Validate Route")}</h5>
<TextField value={routeTest} onChange={e => setRouteTest(e.target.value)}
variant={"outlined"} size={"small"} fullWidth={true}
placeholder={L("Enter a path to test the route…")} />
<pre>
Match: {JSON.stringify(routeTestResult)}
</pre>
</Box>
</div>
}

View File

@@ -0,0 +1,105 @@
import {Box, Checkbox, FormControl, FormControlLabel, FormGroup, Select, styled, TextField} from "@material-ui/core";
import * as React from "react";
import {useCallback, useContext} from "react";
import {LocaleContext} from "shared/locale";
const RouteFormControl = styled(FormControl)((props) => ({
"& > label": {
marginTop: 5
},
"& input, & textarea": {
fontFamily: "monospace",
}
}));
export default function RouteForm(props) {
const {route, setRoute} = props;
const {translate: L} = useContext(LocaleContext);
const onChangeRouteType = useCallback((type) => {
let newRoute = {...route, type: type };
if (newRoute.type === "dynamic" && !newRoute.extra) {
newRoute.extra = "[]";
} else if (newRoute.type === "static" && !newRoute.extra) {
newRoute.extra = 200;
}
setRoute(newRoute);
}, [route]);
const elements = [
<RouteFormControl key={"form-control-pattern"} fullWidth={true}>
<label htmlFor={"route-pattern"}>{L("Pattern")}</label>
<TextField id={"route-pattern"} variant={"outlined"} size={"small"}
value={route.pattern}
onChange={e => setRoute({...route, pattern: e.target.value})} />
</RouteFormControl>,
<FormGroup key={"form-control-exact"}>
<FormControlLabel label={L("Exact")} control={<Checkbox
checked={route.exact}
onChange={e => setRoute({...route, exact: e.target.checked})} />} />
</FormGroup>,
<FormGroup key={"form-control-active"}>
<FormControlLabel label={L("Active")} control={<Checkbox
checked={route.active}
onChange={e => setRoute({...route, active: e.target.checked})} />} />
</FormGroup>,
<RouteFormControl key={"form-control-type"} fullWidth={true} size={"small"}>
<label htmlFor={"route-type"}>{L("Type")}</label>
<Select value={route.type} variant={"outlined"} size={"small"} labelId={"route-type"}
onChange={e => onChangeRouteType(e.target.value)}>
<option value={""}>Select</option>
<option value={"dynamic"}>Dynamic</option>
<option value={"static"}>Static</option>
<option value={"redirect_permanently"}>Redirect Permanently</option>
<option value={"redirect_temporary"}>Redirect Temporary</option>
</Select>
</RouteFormControl>,
];
if (route.type) {
elements.push(
<RouteFormControl key={"form-control-target"} fullWidth={true}>
<label htmlFor={"route-target"}>{L("Target")}</label>
<TextField id={"route-target"} variant={"outlined"} size={"small"}
value={route.target}
onChange={e => setRoute({...route, target: e.target.value})}/>
</RouteFormControl>
);
if (route.type === "dynamic") {
let extraArgs;
try {
extraArgs = JSON.parse(route.extra)
} catch (e) {
extraArgs = null
}
elements.push(
<RouteFormControl key={"form-control-extra"} fullWidth={true}>
<label htmlFor={"route-extra"}>{L("Arguments")}</label>
<textarea id={"route-extra"}
value={route.extra}
onChange={e => setRoute({...route, extra: e.target.value})}/>
<i>{
extraArgs === null ?
"Invalid JSON-string" :
(typeof extraArgs !== "object" ?
"JSON must be Array or Object" : "JSON ok!")
}</i>
</RouteFormControl>
);
} else if (route.type === "static") {
elements.push(
<RouteFormControl key={"form-control-extra"} fullWidth={true}>
<label htmlFor={"route-extra"}>{L("Status Code")}</label>
<TextField id={"route-extra"} variant={"outlined"} size={"small"}
type={"number"} value={route.extra}
onChange={e => setRoute({...route, extra: parseInt(e.target.value) || 200})} />
</RouteFormControl>
);
}
}
return elements;
}

View File

@@ -13,9 +13,14 @@ import {
import {useCallback, useContext, useEffect, useState} from "react";
import {LocaleContext} from "shared/locale";
import {Add, Cached, Delete, Edit, Refresh} from "@material-ui/icons";
import {Quiz} from "@mui/icons-material";
import Dialog from "shared/elements/dialog";
const RouteTableRow = styled(TableRow)((props) => ({
"& td": {
fontFamily: "monospace"
}
}));
export default function RouteListView(props) {
@@ -109,12 +114,6 @@ export default function RouteListView(props) {
}
}, [isGeneratingCache]);
const RouteTableRow = styled(TableRow)((props) => ({
"& td": {
fontFamily: "monospace"
}
}));
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
return <>
@@ -204,11 +203,6 @@ export default function RouteListView(props) {
})}>
<Delete />
</IconButton>
<IconButton size={"small"} title={L("general.test")}
disabled={!api.hasPermission("routes/check")}
color={"primary"}>
<Quiz />
</IconButton>
</TableCell>
</RouteTableRow>
)}