Router bugfix, save and update route frontend
This commit is contained in:
153
react/admin-panel/src/views/route/route-edit.js
Normal file
153
react/admin-panel/src/views/route/route-edit.js
Normal 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>
|
||||
}
|
||||
105
react/admin-panel/src/views/route/route-form.js
Normal file
105
react/admin-panel/src/views/route/route-form.js
Normal 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;
|
||||
}
|
||||
220
react/admin-panel/src/views/route/route-list.js
Normal file
220
react/admin-panel/src/views/route/route-list.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {
|
||||
Paper,
|
||||
styled,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton, Button, Checkbox
|
||||
} from "@material-ui/core";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {Add, Cached, Delete, Edit, Refresh} from "@material-ui/icons";
|
||||
import Dialog from "shared/elements/dialog";
|
||||
|
||||
const RouteTableRow = styled(TableRow)((props) => ({
|
||||
"& td": {
|
||||
fontFamily: "monospace"
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
export default function RouteListView(props) {
|
||||
|
||||
// meta
|
||||
const api = props.api;
|
||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// data
|
||||
const [fetchRoutes, setFetchRoutes] = useState(true);
|
||||
const [routes, setRoutes] = useState({});
|
||||
|
||||
// ui
|
||||
const [dialogData, setDialogData] = useState({show: false});
|
||||
const [isGeneratingCache, setGeneratingCache] = useState(false);
|
||||
|
||||
const onFetchRoutes = useCallback((force = false) => {
|
||||
if (force || fetchRoutes) {
|
||||
setFetchRoutes(false);
|
||||
props.api.fetchRoutes().then(res => {
|
||||
if (!res.success) {
|
||||
props.showDialog(res.msg, "Error fetching routes");
|
||||
navigate("/admin/dashboard");
|
||||
} else {
|
||||
setRoutes(res.routes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [fetchRoutes]);
|
||||
|
||||
useEffect(() => {
|
||||
onFetchRoutes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog("Error fetching translations: " + data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const onToggleRoute = useCallback((id, active) => {
|
||||
if (active) {
|
||||
props.api.enableRoute(id).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog(data.msg, L("Error enabling route"));
|
||||
} else {
|
||||
setRoutes({...routes, [id]: { ...routes[id], active: true }});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
props.api.disableRoute(id).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog(data.msg, L("Error enabling route"));
|
||||
} else {
|
||||
setRoutes({...routes, [id]: { ...routes[id], active: false }});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [routes]);
|
||||
|
||||
const onDeleteRoute = useCallback(id => {
|
||||
props.api.deleteRoute(id).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog(data.msg, L("Error removing route"));
|
||||
} else {
|
||||
let newRoutes = { ...routes };
|
||||
delete newRoutes[id];
|
||||
setRoutes(newRoutes);
|
||||
}
|
||||
})
|
||||
}, [routes]);
|
||||
|
||||
const onRegenerateCache = useCallback(() => {
|
||||
if (!isGeneratingCache) {
|
||||
setGeneratingCache(true);
|
||||
props.api.regenerateRouterCache().then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog(data.msg, L("Error regenerating router cache"));
|
||||
setGeneratingCache(false);
|
||||
} else {
|
||||
setDialogData({
|
||||
open: true,
|
||||
title: L("general.success"),
|
||||
message: L("Router cache successfully regenerated"),
|
||||
onClose: () => setGeneratingCache(false)
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isGeneratingCache]);
|
||||
|
||||
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>Routes</h1>
|
||||
</div>
|
||||
<div className={"col-sm-6"}>
|
||||
<ol className={"breadcrumb float-sm-right"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active">Routes</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<div className={"col-6"} />
|
||||
<div className={"col-6 text-right"}>
|
||||
<div className={"form-group"}>
|
||||
<Button variant={"outlined"} color={"primary"} className={"m-1"} startIcon={<Refresh />} onClick={() => onFetchRoutes(true)}>
|
||||
{L("general.reload")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} className={"m-1"} startIcon={<Add />}
|
||||
disabled={!props.api.hasPermission("routes/add")}
|
||||
onClick={() => navigate("/admin/routes/new")} >
|
||||
{L("general.add")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} className={"m-1"} startIcon={<Cached />}
|
||||
disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache}
|
||||
onClick={onRegenerateCache} >
|
||||
{isGeneratingCache ? L("regenerating_cache") + "…" : L("regenerate_cache")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
||||
<Table stickyHeader size={"small"} className={"table-striped"}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{L("general.id")}</TableCell>
|
||||
<TableCell>{L("Route")}</TableCell>
|
||||
<TableCell>{L("Type")}</TableCell>
|
||||
<TableCell>{L("Target")}</TableCell>
|
||||
<TableCell>{L("Extra")}</TableCell>
|
||||
<TableCell align={"center"}>{L("Active")}</TableCell>
|
||||
<TableCell align={"center"}>{L("Exact")}</TableCell>
|
||||
<TableCell align={"center"}>{L("general.controls")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(routes).map(([id, route]) =>
|
||||
<RouteTableRow key={"route-" + id}>
|
||||
<TableCell>{route.id}</TableCell>
|
||||
<TableCell>{route.pattern}</TableCell>
|
||||
<TableCell>{route.type}</TableCell>
|
||||
<TableCell>{route.target}</TableCell>
|
||||
<TableCell>{route.extra}</TableCell>
|
||||
<TableCell align={"center"}>
|
||||
<Checkbox checked={route.active}
|
||||
size={"small"}
|
||||
disabled={!api.hasPermission(route.active ? "routes/disable" : "routes/enable")}
|
||||
onChange={(e) => onToggleRoute(route.id, e.target.checked)} />
|
||||
</TableCell>
|
||||
<TableCell align={"center"}><BoolCell checked={route.exact} /></TableCell>
|
||||
<TableCell align={"center"}>
|
||||
<IconButton size={"small"} title={L("general.edit")}
|
||||
disabled={!api.hasPermission("routes/add")}
|
||||
color={"primary"}
|
||||
onClick={() => navigate("/admin/routes/" + id)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
<IconButton size={"small"} title={L("general.delete")}
|
||||
disabled={!api.hasPermission("routes/remove")}
|
||||
color={"secondary"}
|
||||
onClick={() => setDialogData({
|
||||
open: true,
|
||||
title: L("Delete Route"),
|
||||
message: L("Do you really want to delete the following route?"),
|
||||
inputs: [
|
||||
{ type: "text", value: route.pattern, disabled: true}
|
||||
],
|
||||
options: [L("general.ok"), L("general.cancel")],
|
||||
onOption: btn => btn === 0 && onDeleteRoute(route.id)
|
||||
})}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</RouteTableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Dialog show={dialogData.open}
|
||||
onClose={() => { setDialogData({open: false}); dialogData.onClose && dialogData.onClose() }}
|
||||
title={dialogData.title}
|
||||
message={dialogData.message}
|
||||
onOption={dialogData.onOption}
|
||||
inputs={dialogData.inputs}
|
||||
options={[L("general.ok"), L("general.cancel")]} />
|
||||
</>
|
||||
}
|
||||
Reference in New Issue
Block a user