Router bugfix, save and update route frontend
This commit is contained in:
parent
90e7024a73
commit
80b5ac07d0
@ -9,6 +9,7 @@ namespace Core\API {
|
|||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
parent::__construct($context, $externalCall, $params);
|
parent::__construct($context, $externalCall, $params);
|
||||||
|
$this->loginRequired = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function fetchAPIKey(int $apiKeyId): ApiKey|bool {
|
protected function fetchAPIKey(int $apiKeyId): ApiKey|bool {
|
||||||
@ -41,7 +42,6 @@ namespace Core\API\ApiKey {
|
|||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array());
|
parent::__construct($context, $externalCall, array());
|
||||||
$this->apiKeyAllowed = false;
|
$this->apiKeyAllowed = false;
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -73,9 +73,7 @@ namespace Core\API\ApiKey {
|
|||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
$params = $this->getPaginationParameters(["token", "validUntil", "active"]);
|
$params = $this->getPaginationParameters(["token", "validUntil", "active"]);
|
||||||
$params["showActiveOnly"] = new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true);
|
$params["showActiveOnly"] = new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true);
|
||||||
|
|
||||||
parent::__construct($context, $externalCall, $params);
|
parent::__construct($context, $externalCall, $params);
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -119,7 +117,7 @@ namespace Core\API\ApiKey {
|
|||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, array(
|
||||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
$this->apiKeyAllowed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -147,7 +145,6 @@ namespace Core\API\ApiKey {
|
|||||||
parent::__construct($user, $externalCall, array(
|
parent::__construct($user, $externalCall, array(
|
||||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||||
));
|
));
|
||||||
$this->loginRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
@ -76,6 +76,7 @@ namespace Core\API\Routes {
|
|||||||
use Core\Objects\DatabaseEntity\Route;
|
use Core\Objects\DatabaseEntity\Route;
|
||||||
use Core\Objects\Router\ApiRoute;
|
use Core\Objects\Router\ApiRoute;
|
||||||
use Core\Objects\Router\Router;
|
use Core\Objects\Router\Router;
|
||||||
|
use Core\Objects\Router\StaticRoute;
|
||||||
|
|
||||||
class Fetch extends RoutesAPI {
|
class Fetch extends RoutesAPI {
|
||||||
|
|
||||||
@ -102,108 +103,40 @@ namespace Core\API\Routes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Save extends RoutesAPI {
|
class Get extends RoutesAPI {
|
||||||
|
|
||||||
private array $routes;
|
|
||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, [
|
||||||
'routes' => new Parameter('routes', Parameter::TYPE_ARRAY, false)
|
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||||
));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
if (!$this->validateRoutes()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$sql->startTransaction();
|
$routeId = $this->getParam("id");
|
||||||
|
|
||||||
// DELETE old rules;
|
$route = Route::find($sql, $routeId);
|
||||||
$this->success = ($sql->truncate("Route")->execute() !== FALSE);
|
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
$this->success = ($route !== FALSE);
|
||||||
// INSERT new routes
|
|
||||||
if ($this->success) {
|
|
||||||
$insertStatement = Route::getHandler($sql)->getInsertQuery($this->routes);
|
|
||||||
$this->success = ($insertStatement->execute() !== FALSE);
|
|
||||||
$this->lastError = $sql->getLastError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$sql->commit();
|
if ($route === null) {
|
||||||
return $this->regenerateCache();
|
return $this->createError("Route not found");
|
||||||
} else {
|
} else {
|
||||||
$sql->rollback();
|
$this->result["route"] = $route;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function validateRoutes(): bool {
|
|
||||||
|
|
||||||
$this->routes = array();
|
|
||||||
$keys = array(
|
|
||||||
"id" => Parameter::TYPE_INT,
|
|
||||||
"pattern" => [Parameter::TYPE_STRING, Parameter::TYPE_INT],
|
|
||||||
"type" => Parameter::TYPE_STRING,
|
|
||||||
"target" => Parameter::TYPE_STRING,
|
|
||||||
"extra" => Parameter::TYPE_STRING,
|
|
||||||
"active" => Parameter::TYPE_BOOLEAN,
|
|
||||||
"exact" => Parameter::TYPE_BOOLEAN,
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($this->getParam("routes") as $index => $route) {
|
|
||||||
foreach ($keys as $key => $expectedType) {
|
|
||||||
if (!array_key_exists($key, $route)) {
|
|
||||||
if ($key !== "id") { // id is optional
|
|
||||||
return $this->createError("Route $index missing key: $key");
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $route[$key];
|
|
||||||
$type = Parameter::parseType($value);
|
|
||||||
if (!is_array($expectedType)) {
|
|
||||||
$expectedType = [$expectedType];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array($type, $expectedType)) {
|
|
||||||
if (count($expectedType) > 0) {
|
|
||||||
$expectedTypeName = "expected: " . Parameter::names[$expectedType];
|
|
||||||
} else {
|
|
||||||
$expectedTypeName = "expected one of: " . implode(",", array_map(
|
|
||||||
function ($type) {
|
|
||||||
return Parameter::names[$type];
|
|
||||||
}, $expectedType));
|
|
||||||
}
|
|
||||||
$gotTypeName = Parameter::names[$type];
|
|
||||||
return $this->createError("Route $index has invalid value for key: $key, $expectedTypeName, got: $gotTypeName");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$type = $route["type"];
|
|
||||||
if (!isset(Route::ROUTE_TYPES[$type])) {
|
|
||||||
return $this->createError("Invalid type: $type");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($route["pattern"])) {
|
|
||||||
return $this->createError("Pattern cannot be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($route["target"])) {
|
|
||||||
return $this->createError("Target cannot be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->routes[] = $route;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDefaultACL(Insert $insert): void {
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to save the site routing", true);
|
$insert->addRow(
|
||||||
|
self::getEndpoint(),
|
||||||
|
[Group::ADMIN, Group::MODERATOR],
|
||||||
|
"Allows users to fetch a single route",
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +151,6 @@ namespace Core\API\Routes {
|
|||||||
"exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
|
"exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
|
||||||
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
|
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
|
||||||
));
|
));
|
||||||
$this->isPublic = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -237,6 +169,11 @@ namespace Core\API\Routes {
|
|||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$this->success = $route->save($sql) !== false;
|
$this->success = $route->save($sql) !== false;
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
|
if ($this->success) {
|
||||||
|
$this->result["routeId"] = $route->getId();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->success && $this->regenerateCache();
|
return $this->success && $this->regenerateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,7 +193,6 @@ namespace Core\API\Routes {
|
|||||||
"exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
|
"exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
|
||||||
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
|
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
|
||||||
));
|
));
|
||||||
$this->isPublic = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
@ -417,24 +353,17 @@ namespace Core\API\Routes {
|
|||||||
class Check extends RoutesAPI {
|
class Check extends RoutesAPI {
|
||||||
public function __construct(Context $context, bool $externalCall) {
|
public function __construct(Context $context, bool $externalCall) {
|
||||||
parent::__construct($context, $externalCall, [
|
parent::__construct($context, $externalCall, [
|
||||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
"pattern" => new StringType("pattern", 128),
|
||||||
"path" => new StringType("path")
|
"path" => new StringType("path"),
|
||||||
|
"exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN, true, true)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$routeId = $this->getParam("id");
|
|
||||||
$route = Route::find($sql, $routeId);
|
|
||||||
if ($route === false) {
|
|
||||||
$this->lastError = $sql->getLastError();
|
|
||||||
return false;
|
|
||||||
} else if ($route === null) {
|
|
||||||
return $this->createError("Route not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->success = true;
|
|
||||||
$path = $this->getParam("path");
|
$path = $this->getParam("path");
|
||||||
|
$pattern = $this->getParam("pattern");
|
||||||
|
$exact = $this->getParam("exact");
|
||||||
|
$route = new StaticRoute($pattern, $exact, "");
|
||||||
$this->result["match"] = $route->match($path);
|
$this->result["match"] = $route->match($path);
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
@ -442,7 +371,7 @@ namespace Core\API\Routes {
|
|||||||
public static function getDefaultACL(Insert $insert): void {
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
$insert->addRow("routes/check",
|
$insert->addRow("routes/check",
|
||||||
[Group::ADMIN, Group::MODERATOR],
|
[Group::ADMIN, Group::MODERATOR],
|
||||||
"Users with this permission can see, if a route is matched with the given path for debugging purposes",
|
"Users with this permission can see, if a route pattern is matched with the given path for debugging purposes",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
|||||||
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||||
use Core\Objects\Router\DocumentRoute;
|
use Core\Objects\Router\DocumentRoute;
|
||||||
use Core\Objects\Router\RedirectRoute;
|
use Core\Objects\Router\RedirectPermanentlyRoute;
|
||||||
|
use Core\Objects\Router\RedirectTemporaryRoute;
|
||||||
use Core\Objects\Router\Router;
|
use Core\Objects\Router\Router;
|
||||||
use Core\Objects\Router\StaticFileRoute;
|
use Core\Objects\Router\StaticFileRoute;
|
||||||
|
|
||||||
@ -23,8 +24,8 @@ abstract class Route extends DatabaseEntity {
|
|||||||
const TYPE_REDIRECT_PERMANENTLY = "redirect_permanently";
|
const TYPE_REDIRECT_PERMANENTLY = "redirect_permanently";
|
||||||
const TYPE_REDIRECT_TEMPORARY = "redirect_temporary";
|
const TYPE_REDIRECT_TEMPORARY = "redirect_temporary";
|
||||||
const ROUTE_TYPES = [
|
const ROUTE_TYPES = [
|
||||||
self::TYPE_REDIRECT_TEMPORARY => RedirectRoute::class,
|
self::TYPE_REDIRECT_TEMPORARY => RedirectTemporaryRoute::class,
|
||||||
self::TYPE_REDIRECT_PERMANENTLY => RedirectRoute::class,
|
self::TYPE_REDIRECT_PERMANENTLY => RedirectPermanentlyRoute::class,
|
||||||
self::TYPE_STATIC => StaticFileRoute::class,
|
self::TYPE_STATIC => StaticFileRoute::class,
|
||||||
self::TYPE_DYNAMIC => DocumentRoute::class
|
self::TYPE_DYNAMIC => DocumentRoute::class
|
||||||
];
|
];
|
||||||
@ -60,6 +61,10 @@ abstract class Route extends DatabaseEntity {
|
|||||||
return $this->active;
|
return $this->active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isExact(): bool {
|
||||||
|
return $this->exact;
|
||||||
|
}
|
||||||
|
|
||||||
private static function parseParamType(?string $type): ?int {
|
private static function parseParamType(?string $type): ?int {
|
||||||
if ($type === null || trim($type) === "") {
|
if ($type === null || trim($type) === "") {
|
||||||
return null;
|
return null;
|
||||||
|
@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
namespace Core\Objects\Router;
|
namespace Core\Objects\Router;
|
||||||
|
|
||||||
|
use Core\Driver\SQL\SQL;
|
||||||
|
|
||||||
class RedirectPermanentlyRoute extends RedirectRoute {
|
class RedirectPermanentlyRoute extends RedirectRoute {
|
||||||
|
|
||||||
|
const HTTP_STATUS_CODE = 308;
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $destination) {
|
public function __construct(string $pattern, bool $exact, string $destination) {
|
||||||
parent::__construct("redirect_permanently", $pattern, $exact, $destination, 308);
|
parent::__construct("redirect_permanently", $pattern, $exact, $destination, self::HTTP_STATUS_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postFetch(SQL $sql, array $row) {
|
||||||
|
parent::postFetch($sql, $row);
|
||||||
|
$this->code = self::HTTP_STATUS_CODE;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ use JetBrains\PhpStorm\Pure;
|
|||||||
class RedirectRoute extends Route {
|
class RedirectRoute extends Route {
|
||||||
|
|
||||||
#[Transient]
|
#[Transient]
|
||||||
private int $code;
|
protected int $code;
|
||||||
|
|
||||||
public function __construct(string $type, string $pattern, bool $exact, string $destination, int $code = 307) {
|
public function __construct(string $type, string $pattern, bool $exact, string $destination, int $code = 307) {
|
||||||
parent::__construct($type, $pattern, $destination, $exact);
|
parent::__construct($type, $pattern, $destination, $exact);
|
||||||
|
@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
namespace Core\Objects\Router;
|
namespace Core\Objects\Router;
|
||||||
|
|
||||||
|
use Core\Driver\SQL\SQL;
|
||||||
|
|
||||||
class RedirectTemporaryRoute extends RedirectRoute {
|
class RedirectTemporaryRoute extends RedirectRoute {
|
||||||
|
|
||||||
|
const HTTP_STATUS_CODE = 307;
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $destination) {
|
public function __construct(string $pattern, bool $exact, string $destination) {
|
||||||
parent::__construct("redirect_temporary", $pattern, $exact, $destination, 307);
|
parent::__construct("redirect_temporary", $pattern, $exact, $destination, self::HTTP_STATUS_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postFetch(SQL $sql, array $row) {
|
||||||
|
parent::postFetch($sql, $row);
|
||||||
|
$this->code = self::HTTP_STATUS_CODE;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,7 +14,7 @@ class StaticRoute extends Route {
|
|||||||
private int $code;
|
private int $code;
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
|
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
|
||||||
parent::__construct("static", $pattern, $exact);
|
parent::__construct("static", $pattern, "", $exact);
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
$this->code = $code;
|
$this->code = $code;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ const GroupListView = lazy(() => import('./views/group/group-list'));
|
|||||||
const EditGroupView = lazy(() => import('./views/group/group-edit'));
|
const EditGroupView = lazy(() => import('./views/group/group-edit'));
|
||||||
const LogView = lazy(() => import("./views/log-view"));
|
const LogView = lazy(() => import("./views/log-view"));
|
||||||
const AccessControlList = lazy(() => import("./views/access-control-list"));
|
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) {
|
export default function AdminDashboard(props) {
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ export default function AdminDashboard(props) {
|
|||||||
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
|
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
|
||||||
<Route path={"/admin/acl"} element={<AccessControlList {...controlObj} />}/>
|
<Route path={"/admin/acl"} element={<AccessControlList {...controlObj} />}/>
|
||||||
<Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/>
|
<Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/>
|
||||||
|
<Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/>
|
||||||
<Route path={"*"} element={<View404 />} />
|
<Route path={"*"} element={<View404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -18,6 +18,10 @@ import {Add, Delete, Edit, Refresh} from "@material-ui/icons";
|
|||||||
import {USER_GROUP_ADMIN} from "shared/constants";
|
import {USER_GROUP_ADMIN} from "shared/constants";
|
||||||
import Dialog from "shared/elements/dialog";
|
import Dialog from "shared/elements/dialog";
|
||||||
|
|
||||||
|
const BorderedColumn = styled(TableCell)({
|
||||||
|
borderLeft: "1px dotted #666",
|
||||||
|
borderRight: "1px dotted #666",
|
||||||
|
});
|
||||||
|
|
||||||
export default function AccessControlList(props) {
|
export default function AccessControlList(props) {
|
||||||
|
|
||||||
@ -203,11 +207,6 @@ export default function AccessControlList(props) {
|
|||||||
return <>{rows}</>
|
return <>{rows}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorderedColumn = styled(TableCell)({
|
|
||||||
borderLeft: "1px dotted #666",
|
|
||||||
borderRight: "1px dotted #666",
|
|
||||||
});
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"content-header"}>
|
<div className={"content-header"}>
|
||||||
<div className={"container-fluid"}>
|
<div className={"container-fluid"}>
|
||||||
|
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;
|
||||||
|
}
|
@ -13,9 +13,14 @@ import {
|
|||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {Add, Cached, Delete, Edit, Refresh} from "@material-ui/icons";
|
import {Add, Cached, Delete, Edit, Refresh} from "@material-ui/icons";
|
||||||
import {Quiz} from "@mui/icons-material";
|
|
||||||
import Dialog from "shared/elements/dialog";
|
import Dialog from "shared/elements/dialog";
|
||||||
|
|
||||||
|
const RouteTableRow = styled(TableRow)((props) => ({
|
||||||
|
"& td": {
|
||||||
|
fontFamily: "monospace"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
export default function RouteListView(props) {
|
export default function RouteListView(props) {
|
||||||
|
|
||||||
@ -109,12 +114,6 @@ export default function RouteListView(props) {
|
|||||||
}
|
}
|
||||||
}, [isGeneratingCache]);
|
}, [isGeneratingCache]);
|
||||||
|
|
||||||
const RouteTableRow = styled(TableRow)((props) => ({
|
|
||||||
"& td": {
|
|
||||||
fontFamily: "monospace"
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
|
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
@ -204,11 +203,6 @@ export default function RouteListView(props) {
|
|||||||
})}>
|
})}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size={"small"} title={L("general.test")}
|
|
||||||
disabled={!api.hasPermission("routes/check")}
|
|
||||||
color={"primary"}>
|
|
||||||
<Quiz />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</RouteTableRow>
|
</RouteTableRow>
|
||||||
)}
|
)}
|
@ -217,6 +217,10 @@ export default class API {
|
|||||||
return this.apiCall("routes/fetch");
|
return this.apiCall("routes/fetch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRoute(id) {
|
||||||
|
return this.apiCall("routes/get", { id: id });
|
||||||
|
}
|
||||||
|
|
||||||
async enableRoute(id) {
|
async enableRoute(id) {
|
||||||
return this.apiCall("routes/enable", { id: id });
|
return this.apiCall("routes/enable", { id: id });
|
||||||
}
|
}
|
||||||
@ -229,14 +233,22 @@ export default class API {
|
|||||||
return this.apiCall("routes/remove", { id: id });
|
return this.apiCall("routes/remove", { id: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRoutes(routes) {
|
|
||||||
return this.apiCall("routes/save", { routes: routes });
|
|
||||||
}
|
|
||||||
|
|
||||||
async regenerateRouterCache() {
|
async regenerateRouterCache() {
|
||||||
return this.apiCall("routes/generateCache");
|
return this.apiCall("routes/generateCache");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testRoute(pattern, path, exact = true) {
|
||||||
|
return this.apiCall("routes/check", { pattern: pattern, path: path, exact: exact });
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRoute(pattern, type, target, extra, exact, active) {
|
||||||
|
return this.apiCall("routes/add", { pattern, type, target, extra, exact, active });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRoute(id, pattern, type, target, extra, exact, active) {
|
||||||
|
return this.apiCall("routes/update", { id, pattern, type, target, extra, exact, active });
|
||||||
|
}
|
||||||
|
|
||||||
/** GroupAPI **/
|
/** GroupAPI **/
|
||||||
async createGroup(name, color) {
|
async createGroup(name, color) {
|
||||||
return this.apiCall("groups/create", { name: name, color: color });
|
return this.apiCall("groups/create", { name: name, color: color });
|
||||||
|
Loading…
Reference in New Issue
Block a user