Localization for routes and permissions, added compression for language/getEntries

This commit is contained in:
Roman 2024-03-29 15:20:45 +01:00
parent 12b8a0b386
commit 0e3d27fa10
15 changed files with 219 additions and 73 deletions

@ -115,7 +115,8 @@ namespace Core\API\Language {
public function __construct(Context $context, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [ parent::__construct($context, $externalCall, [
"code" => new StringType("code", 5, true, NULL), "code" => new StringType("code", 5, true, NULL),
"modules" => new ArrayType("modules", Parameter::TYPE_STRING, true, false) "modules" => new ArrayType("modules", Parameter::TYPE_STRING, true, false),
"compression" => new StringType("compression", -1, true, NULL, ["gzip", "zlib"])
]); ]);
$this->loginRequired = false; $this->loginRequired = false;
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
@ -156,7 +157,23 @@ namespace Core\API\Language {
} }
$this->result["code"] = $code; $this->result["code"] = $code;
$this->result["entries"] = $entries;
$compression = $this->getParam("compression");
if ($compression) {
switch ($compression) {
case "gzip":
$this->result["compressed"] = base64_encode(gzencode(json_encode($entries), 9));
break;
case "zlib":
$this->result["compressed"] = base64_encode(gzcompress(json_encode($entries), 9, ZLIB_ENCODING_DEFLATE));
break;
default:
http_response_code(400);
return $this->createError("Invalid compression method: $compression");
}
} else {
$this->result["entries"] = $entries;
}
return true; return true;
} }
} }

@ -19,6 +19,7 @@ return [
"cancel" => "Abbrechen", "cancel" => "Abbrechen",
"confirm" => "Bestätigen", "confirm" => "Bestätigen",
"add" => "Hinzufügen", "add" => "Hinzufügen",
"select" => "Auswählen",
"ok" => "OK", "ok" => "OK",
"id" => "ID", "id" => "ID",
"user" => "Benutzer", "user" => "Benutzer",

@ -0,0 +1,20 @@
<?php
return [
"title" => "Berechtigungen",
"title_short" => "ACL",
"query" => "Suchanfrage",
"add_permission" => "Berechtigung hinzufügen",
"permission" => "Berechtigung",
"everyone" => "Alle",
"method" => "Methode",
"description" => "Beschreibung",
# dialog
"delete_permission_confirm" => "Diese Berechtigung wirklich löschen?",
"edit_permission" => "Berechtigung bearbeiten",
"update_permission_error" => "Fehler beim Aktualisieren der Berechtigung",
"delete_permission_error" => "Fehler beim Löschen der Berechtigung",
"fetch_permission_error" => "Fehler beim Holen der Berechtigungen",
"fetch_group_error" => "Fehler beim Holen der Gruppen",
];

@ -0,0 +1,41 @@
<?php
return [
"title" => "Routen",
"regenerating_cache" => "Lade Cache neu",
"regenerate_cache" => "Cache neuladen",
# table
"route" => "Route",
"type" => "Typ",
"target" => "Ziel",
"extra" => "Extra",
"active" => "Aktiv",
"exact" => "Exakt",
# form
"edit_route_title" => "Route bearbeiten",
"create_route_title" => "Neue Route erstellen",
"pattern" => "Pattern",
"arguments" => "Argumente",
"status_code" => "Status Code",
"json_ok" => "JSON ok!",
"json_err" => "Ungültiger JSON-string!",
"json_not_object" => "Das JSON muss ein Array oder Objekt sein!",
"validate_route" => "Route validieren",
"validate_route_placeholder" => "Einen Pfad eingeben um die Route zu testen",
# data
"type_dynamic" => "Dynamisch",
"type_static" => "Statisch",
"type_redirect_permanently" => "Dauerhaft weiterleiten",
"type_redirect_temporary" => "Temporär weiterleiten",
# dialogs
"fetch_routes_error" => "Fehler beim Holen der Routen",
"enable_route_error" => "Fehler beim Aktivieren der Route",
"disable_route_error" => "Fehler beim Deaktivieren der Route",
"remove_route_error" => "Fehler beim Entfernen der Route",
"regenerate_router_cache_error" => "Fehler beim Erzeugen des Router Caches",
"regenerate_router_cache_success" => "Router Cache erfolgreich erzeugt",
];

@ -30,6 +30,7 @@ return [
"cancel" => "Cancel", "cancel" => "Cancel",
"confirm" => "Confirm", "confirm" => "Confirm",
"add" => "Add", "add" => "Add",
"select" => "Select",
"close" => "Close", "close" => "Close",
"ok" => "OK", "ok" => "OK",
"id" => "ID", "id" => "ID",

@ -0,0 +1,20 @@
<?php
return [
"title" => "Permissions",
"title_short" => "ACL",
"query" => "Search query",
"add_permission" => "Add permission",
"permission" => "Permission",
"everyone" => "Everyone",
"method" => "Method",
"description" => "Description",
# dialog
"delete_permission_confirm" => "Do you really want to delete this permission?",
"edit_permission" => "Edit Permission",
"update_permission_error" => "Error updating permission",
"delete_permission_error" => "Error deleting permission",
"fetch_permission_error" => "Error fetching permissions",
"fetch_group_error" => "Error fetching groups",
];

@ -0,0 +1,41 @@
<?php
return [
"title" => "Routes",
"regenerating_cache" => "Regenerating Cache",
"regenerate_cache" => "Regenerate Cache",
# table
"route" => "Route",
"type" => "Type",
"target" => "Target",
"extra" => "Extra",
"active" => "Active",
"exact" => "Exact",
# form
"edit_route_title" => "Edit Route",
"create_route_title" => "Create new Route",
"pattern" => "Pattern",
"arguments" => "Arguments",
"status_code" => "Status Code",
"json_ok" => "JSON ok!",
"json_err" => "Invalid JSON-string!",
"json_not_object" => "JSON must be Array or Object!",
"validate_route" => "Validate Route",
"validate_route_placeholder" => "Enter a path to test the route",
# data
"type_dynamic" => "Dynamic",
"type_static" => "Static",
"type_redirect_permanently" => "Redirect permanently",
"type_redirect_temporary" => "Redirect temporary",
# dialogs
"fetch_routes_error" => "Error fetching routes",
"enable_route_error" => "Error enabling route",
"disable_route_error" => "Error disabling route",
"remove_route_error" => "Error removing route",
"regenerate_router_cache_error" => "Error regenerating router cache",
"regenerate_router_cache_success" => "Router cache successfully regenerated",
];

@ -45,13 +45,13 @@ export default function AccessControlList(props) {
setFetchACL(false); setFetchACL(false);
props.api.fetchGroups().then(res => { props.api.fetchGroups().then(res => {
if (!res.success) { if (!res.success) {
props.showDialog(res.msg, "Error fetching groups"); props.showDialog(res.msg, L("permissions.fetch_group_error"));
navigate("/admin/dashboard"); navigate("/admin/dashboard");
} else { } else {
setGroups(res.groups); setGroups(res.groups);
props.api.fetchPermissions().then(res => { props.api.fetchPermissions().then(res => {
if (!res.success) { if (!res.success) {
props.showDialog(res.msg, "Error fetching permissions"); props.showDialog(res.msg, L("permissions.fetch_permission_error"));
navigate("/admin/dashboard"); navigate("/admin/dashboard");
} else { } else {
setACL(res.permissions); setACL(res.permissions);
@ -67,7 +67,7 @@ export default function AccessControlList(props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
requestModules(props.api, ["general"], currentLocale).then(data => { requestModules(props.api, ["general", "permissions"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg); props.showDialog("Error fetching translations: " + data.msg);
} }
@ -103,7 +103,7 @@ export default function AccessControlList(props) {
setACL(newACL); setACL(newACL);
props.api.fetchUser(); props.api.fetchUser();
} else { } else {
props.showDialog("Error updating permission: " + data.msg); props.showDialog(data.msg, L("permissions.update_permission_error"));
} }
}); });
} }
@ -116,7 +116,7 @@ export default function AccessControlList(props) {
setACL(newACL); setACL(newACL);
props.api.fetchUser(); props.api.fetchUser();
} else { } else {
props.showDialog("Error deleting permission: " + data.msg); props.showDialog(data.msg, L("permissions.delete_permission_error"));
} }
}) })
}, [acl]); }, [acl]);
@ -130,7 +130,7 @@ export default function AccessControlList(props) {
setACL(newACL); setACL(newACL);
props.api.fetchUser(); props.api.fetchUser();
} else { } else {
props.showDialog("Error updating permission: " + data.msg); props.showDialog(data.msg, L("permissions.update_permission_error"));
} }
}) })
}, [acl]); }, [acl]);
@ -162,11 +162,11 @@ export default function AccessControlList(props) {
disabled={isRestricted(permission.method)} disabled={isRestricted(permission.method)}
onClick={() => setDialogData({ onClick={() => setDialogData({
open: true, open: true,
title: L("Edit permission"), title: L("permissions.edit_permission"),
inputs: [ inputs: [
{ type: "label", value: L("general.method") + ":" }, { type: "label", value: L("permissions.method") + ":" },
{ type: "text", name: "method", value: permission.method, disabled: true }, { type: "text", name: "method", value: permission.method, disabled: true },
{ type: "label", value: L("general.description") + ":" }, { type: "label", value: L("permissions.description") + ":" },
{ type: "text", name: "description", value: permission.description, maxLength: 128 } { type: "text", name: "description", value: permission.description, maxLength: 128 }
], ],
onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, permission.groups) onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, permission.groups)
@ -177,8 +177,8 @@ export default function AccessControlList(props) {
disabled={isRestricted(permission.method)} disabled={isRestricted(permission.method)}
onClick={() => setDialogData({ onClick={() => setDialogData({
open: true, open: true,
title: L("Do you really want to delete this permission?"), title: L("permissions.delete_permission_confirm"),
message: "Method: " + permission.method, message: L("permissions.method") + ": " + permission.method,
onOption: (option) => option === 0 && onDeletePermission(permission.method) onOption: (option) => option === 0 && onDeletePermission(permission.method)
})} > })} >
<Delete /> <Delete />
@ -212,12 +212,12 @@ export default function AccessControlList(props) {
<div className={"container-fluid"}> <div className={"container-fluid"}>
<div className={"row mb-2"}> <div className={"row mb-2"}>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<h1 className={"m-0 text-dark"}>Access Control List</h1> <h1 className={"m-0 text-dark"}>{L("permissions.title")}</h1>
</div> </div>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<ol className={"breadcrumb float-sm-right"}> <ol className={"breadcrumb float-sm-right"}>
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li> <li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item active">ACL</li> <li className="breadcrumb-item active">{L("permissions.title_short")}</li>
</ol> </ol>
</div> </div>
</div> </div>
@ -226,10 +226,10 @@ export default function AccessControlList(props) {
<div className={"row"}> <div className={"row"}>
<div className={"col-6"}> <div className={"col-6"}>
<div className={"form-group"}> <div className={"form-group"}>
<label>{L("query")}</label> <label>{L("permissions.query")}</label>
<TextField <TextField
className={"form-control"} className={"form-control"}
placeholder={L("search_query") + "…"} placeholder={L("permissions.query") + "…"}
value={query} value={query}
onChange={e => setQuery(e.target.value)} onChange={e => setQuery(e.target.value)}
variant={"outlined"} variant={"outlined"}
@ -245,7 +245,7 @@ export default function AccessControlList(props) {
<Button variant={"outlined"} className={"m-1"} startIcon={<Add />} disabled={!props.api.hasGroup(USER_GROUP_ADMIN)} <Button variant={"outlined"} className={"m-1"} startIcon={<Add />} disabled={!props.api.hasGroup(USER_GROUP_ADMIN)}
onClick={() => setDialogData({ onClick={() => setDialogData({
open: true, open: true,
title: L("Add permission"), title: L("permissions.add_permission"),
inputs: [ inputs: [
{ type: "label", value: L("general.method") + ":" }, { type: "label", value: L("general.method") + ":" },
{ type: "text", name: "method", value: "", placeholder: L("general.method") }, { type: "text", name: "method", value: "", placeholder: L("general.method") },
@ -263,8 +263,8 @@ export default function AccessControlList(props) {
<Table stickyHeader size={"small"} className={"table-striped"}> <Table stickyHeader size={"small"} className={"table-striped"}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>{L("permission")}</TableCell> <TableCell>{L("permissions.permission")}</TableCell>
<BorderedColumn align={"center"}><i>{L("everyone")}</i></BorderedColumn> <BorderedColumn align={"center"}><i>{L("permissions.everyone")}</i></BorderedColumn>
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}> { groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>
{group.name} {group.name}
</TableCell>) } </TableCell>) }

@ -48,7 +48,7 @@ export default function RouteEditView(props) {
const [isSaving, setSaving] = useState(false); const [isSaving, setSaving] = useState(false);
useEffect(() => { useEffect(() => {
requestModules(props.api, ["general"], currentLocale).then(data => { requestModules(props.api, ["general", "routes"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg); props.showDialog("Error fetching translations: " + data.msg);
} }
@ -122,15 +122,13 @@ export default function RouteEditView(props) {
<div className={"container-fluid"}> <div className={"container-fluid"}>
<ol className={"breadcrumb"}> <ol className={"breadcrumb"}>
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li> <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"><Link to={"/admin/routes"}>{L("routes.title")}</Link></li>
<li className="breadcrumb-item active">{isNewRoute ? "New" : "Edit"}</li> <li className="breadcrumb-item active">{isNewRoute ? L("general.new") : L("general.edit")}</li>
</ol> </ol>
</div> </div>
<div className={"content"}> <div className={"content"}>
<div className={"container-fluid"}> <div className={"container-fluid"}>
<h3>{L(isNewRoute ? "Create new Route" : "Edit Route")}</h3> <h3>{L(isNewRoute ? "routes.create_route_title" : "routes.edit_route_title")}</h3>
<div className={"col-sm-12 col-lg-6"}>
</div>
</div> </div>
</div> </div>
<RouteForm route={route} setRoute={setRoute} /> <RouteForm route={route} setRoute={setRoute} />
@ -147,10 +145,10 @@ export default function RouteEditView(props) {
</Button> </Button>
</ButtonBar> </ButtonBar>
<Box mt={3}> <Box mt={3}>
<h5>{L("Validate Route")}</h5> <h5>{L("routes.validate_route")}</h5>
<MonoSpaceTextField value={routeTest} onChange={e => setRouteTest(e.target.value)} <MonoSpaceTextField value={routeTest} onChange={e => setRouteTest(e.target.value)}
variant={"outlined"} size={"small"} fullWidth={true} variant={"outlined"} size={"small"} fullWidth={true}
placeholder={L("Enter a path to test the route…")} /> placeholder={L("routes.validate_route_placeholder") + "…"} />
<pre> <pre>
Match: {JSON.stringify(routeTestResult)} Match: {JSON.stringify(routeTestResult)}
</pre> </pre>

@ -1,4 +1,4 @@
import {Box, Checkbox, FormControl, FormControlLabel, FormGroup, Select, styled, TextField} from "@material-ui/core"; import {Checkbox, FormControl, FormControlLabel, Select, styled, TextField} from "@material-ui/core";
import * as React from "react"; import * as React from "react";
import {useCallback, useContext, useEffect, useRef} from "react"; import {useCallback, useContext, useEffect, useRef} from "react";
import {LocaleContext} from "shared/locale"; import {LocaleContext} from "shared/locale";
@ -38,30 +38,30 @@ export default function RouteForm(props) {
const elements = [ const elements = [
<RouteFormControl key={"form-control-pattern"} fullWidth={true}> <RouteFormControl key={"form-control-pattern"} fullWidth={true}>
<label htmlFor={"route-pattern"}>{L("Pattern")}</label> <label htmlFor={"route-pattern"}>{L("routes.pattern")}</label>
<TextField id={"route-pattern"} variant={"outlined"} size={"small"} <TextField id={"route-pattern"} variant={"outlined"} size={"small"}
value={route.pattern} value={route.pattern}
onChange={e => setRoute({...route, pattern: e.target.value})} /> onChange={e => setRoute({...route, pattern: e.target.value})} />
</RouteFormControl>, </RouteFormControl>,
<FormGroup key={"form-control-exact"}> <RouteFormControl key={"form-control-exact"}>
<FormControlLabel label={L("Exact")} control={<Checkbox <FormControlLabel label={L("routes.exact")} control={<Checkbox
checked={route.exact} checked={route.exact}
onChange={e => setRoute({...route, exact: e.target.checked})} />} /> onChange={e => setRoute({...route, exact: e.target.checked})} />} />
</FormGroup>, </RouteFormControl>,
<FormGroup key={"form-control-active"}> <RouteFormControl key={"form-control-active"}>
<FormControlLabel label={L("Active")} control={<Checkbox <FormControlLabel label={L("routes.active")} control={<Checkbox
checked={route.active} checked={route.active}
onChange={e => setRoute({...route, active: e.target.checked})} />} /> onChange={e => setRoute({...route, active: e.target.checked})} />} />
</FormGroup>, </RouteFormControl>,
<RouteFormControl key={"form-control-type"} fullWidth={true} size={"small"}> <RouteFormControl key={"form-control-type"} fullWidth={true} size={"small"}>
<label htmlFor={"route-type"}>{L("Type")}</label> <label htmlFor={"route-type"}>{L("routes.type")}</label>
<Select value={route.type} variant={"outlined"} size={"small"} labelId={"route-type"} <Select value={route.type} variant={"outlined"} size={"small"} labelId={"route-type"}
onChange={e => onChangeRouteType(e.target.value)} native> onChange={e => onChangeRouteType(e.target.value)} native>
<option value={""}>Select</option> <option value={""}>{L("general.select")}</option>
<option value={"dynamic"}>Dynamic</option> <option value={"dynamic"}>{L("routes.type_dynamic")}</option>
<option value={"static"}>Static</option> <option value={"static"}>{L("routes.type_static")}</option>
<option value={"redirect_permanently"}>Redirect Permanently</option> <option value={"redirect_permanently"}>{L("routes.type_redirect_permanently")}</option>
<option value={"redirect_temporary"}>Redirect Temporary</option> <option value={"redirect_temporary"}>{L("routes.type_redirect_temporary")}</option>
</Select> </Select>
</RouteFormControl>, </RouteFormControl>,
]; ];
@ -77,7 +77,7 @@ export default function RouteForm(props) {
if (route.type) { if (route.type) {
elements.push( elements.push(
<RouteFormControl key={"form-control-target"} fullWidth={true}> <RouteFormControl key={"form-control-target"} fullWidth={true}>
<label htmlFor={"route-target"}>{L("Target")}</label> <label htmlFor={"route-target"}>{L("routes.target")}</label>
<TextField id={"route-target"} variant={"outlined"} size={"small"} <TextField id={"route-target"} variant={"outlined"} size={"small"}
value={route.target} value={route.target}
onChange={e => setRoute({...route, target: e.target.value})}/> onChange={e => setRoute({...route, target: e.target.value})}/>
@ -95,23 +95,22 @@ export default function RouteForm(props) {
} }
elements.push( elements.push(
<RouteFormControl key={"form-control-extra"} fullWidth={true}> <RouteFormControl key={"form-control-extra"} fullWidth={true}>
<label htmlFor={"route-extra"}>{L("Arguments")}</label> <label htmlFor={"route-extra"}>{L("routes.arguments")}</label>
<textarea id={"route-extra"} <textarea id={"route-extra"}
ref={extraRef} ref={extraRef}
value={extraArgs ?? route.extra} value={extraArgs ?? route.extra}
onChange={e => setRoute({...route, extra: minifyJson(e.target.value)})}/> onChange={e => setRoute({...route, extra: minifyJson(e.target.value)})}/>
<i>{ <i>{
extraArgs === null ? extraArgs === null ?
"Invalid JSON-string" : L("routes.json_err") :
(type !== "object" ? (type !== "object" ? L("routes.json_not_obj") : L("routes.json_ok"))
"JSON must be Array or Object" : "JSON ok!")
}</i> }</i>
</RouteFormControl> </RouteFormControl>
); );
} else if (route.type === "static") { } else if (route.type === "static") {
elements.push( elements.push(
<RouteFormControl key={"form-control-extra"} fullWidth={true}> <RouteFormControl key={"form-control-extra"} fullWidth={true}>
<label htmlFor={"route-extra"}>{L("Status Code")}</label> <label htmlFor={"route-extra"}>{L("routes.status_code")}</label>
<TextField id={"route-extra"} variant={"outlined"} size={"small"} <TextField id={"route-extra"} variant={"outlined"} size={"small"}
type={"number"} value={route.extra} type={"number"} value={route.extra}
onChange={e => setRoute({...route, extra: parseInt(e.target.value) || 200})} /> onChange={e => setRoute({...route, extra: parseInt(e.target.value) || 200})} />

@ -42,7 +42,7 @@ export default function RouteListView(props) {
setFetchRoutes(false); setFetchRoutes(false);
props.api.fetchRoutes().then(res => { props.api.fetchRoutes().then(res => {
if (!res.success) { if (!res.success) {
props.showDialog(res.msg, "Error fetching routes"); props.showDialog(res.msg, L("routes.fetch_routes_error"));
navigate("/admin/dashboard"); navigate("/admin/dashboard");
} else { } else {
setRoutes(res.routes); setRoutes(res.routes);
@ -56,7 +56,7 @@ export default function RouteListView(props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
requestModules(props.api, ["general"], currentLocale).then(data => { requestModules(props.api, ["general", "routes"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg); props.showDialog("Error fetching translations: " + data.msg);
} }
@ -67,7 +67,7 @@ export default function RouteListView(props) {
if (active) { if (active) {
props.api.enableRoute(id).then(data => { props.api.enableRoute(id).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog(data.msg, L("Error enabling route")); props.showDialog(data.msg, L("routes.enable_route_error"));
} else { } else {
setRoutes({...routes, [id]: { ...routes[id], active: true }}); setRoutes({...routes, [id]: { ...routes[id], active: true }});
} }
@ -75,7 +75,7 @@ export default function RouteListView(props) {
} else { } else {
props.api.disableRoute(id).then(data => { props.api.disableRoute(id).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog(data.msg, L("Error enabling route")); props.showDialog(data.msg, L("routes.disable_route_error"));
} else { } else {
setRoutes({...routes, [id]: { ...routes[id], active: false }}); setRoutes({...routes, [id]: { ...routes[id], active: false }});
} }
@ -86,7 +86,7 @@ export default function RouteListView(props) {
const onDeleteRoute = useCallback(id => { const onDeleteRoute = useCallback(id => {
props.api.deleteRoute(id).then(data => { props.api.deleteRoute(id).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog(data.msg, L("Error removing route")); props.showDialog(data.msg, L("routes.remove_route_error"));
} else { } else {
let newRoutes = { ...routes }; let newRoutes = { ...routes };
delete newRoutes[id]; delete newRoutes[id];
@ -100,13 +100,13 @@ export default function RouteListView(props) {
setGeneratingCache(true); setGeneratingCache(true);
props.api.regenerateRouterCache().then(data => { props.api.regenerateRouterCache().then(data => {
if (!data.success) { if (!data.success) {
props.showDialog(data.msg, L("Error regenerating router cache")); props.showDialog(data.msg, L("routes.regenerate_router_cache_error"));
setGeneratingCache(false); setGeneratingCache(false);
} else { } else {
setDialogData({ setDialogData({
open: true, open: true,
title: L("general.success"), title: L("general.success"),
message: L("Router cache successfully regenerated"), message: L("routes.regenerate_router_cache_success"),
onClose: () => setGeneratingCache(false) onClose: () => setGeneratingCache(false)
}) })
} }
@ -121,12 +121,12 @@ export default function RouteListView(props) {
<div className={"container-fluid"}> <div className={"container-fluid"}>
<div className={"row mb-2"}> <div className={"row mb-2"}>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<h1 className={"m-0 text-dark"}>Routes</h1> <h1 className={"m-0 text-dark"}>{L("routes.title")}</h1>
</div> </div>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<ol className={"breadcrumb float-sm-right"}> <ol className={"breadcrumb float-sm-right"}>
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li> <li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item active">Routes</li> <li className="breadcrumb-item active">{L("routes.title")}</li>
</ol> </ol>
</div> </div>
</div> </div>
@ -147,7 +147,7 @@ export default function RouteListView(props) {
<Button variant={"outlined"} className={"m-1"} startIcon={<Cached />} <Button variant={"outlined"} className={"m-1"} startIcon={<Cached />}
disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache} disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache}
onClick={onRegenerateCache} > onClick={onRegenerateCache} >
{isGeneratingCache ? L("regenerating_cache") + "…" : L("regenerate_cache")} {isGeneratingCache ? L("routes.regenerating_cache") + "…" : L("routes.regenerate_cache")}
</Button> </Button>
</div> </div>
</div> </div>
@ -157,12 +157,12 @@ export default function RouteListView(props) {
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>{L("general.id")}</TableCell> <TableCell>{L("general.id")}</TableCell>
<TableCell>{L("Route")}</TableCell> <TableCell>{L("routes.route")}</TableCell>
<TableCell>{L("Type")}</TableCell> <TableCell>{L("routes.type")}</TableCell>
<TableCell>{L("Target")}</TableCell> <TableCell>{L("routes.target")}</TableCell>
<TableCell>{L("Extra")}</TableCell> <TableCell>{L("routes.extra")}</TableCell>
<TableCell align={"center"}>{L("Active")}</TableCell> <TableCell align={"center"}>{L("routes.active")}</TableCell>
<TableCell align={"center"}>{L("Exact")}</TableCell> <TableCell align={"center"}>{L("routes.exact")}</TableCell>
<TableCell align={"center"}>{L("general.controls")}</TableCell> <TableCell align={"center"}>{L("general.controls")}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>

@ -45,6 +45,7 @@
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"material-ui-color-picker": "^3.5.1", "material-ui-color-picker": "^3.5.1",
"mini-css-extract-plugin": "^2.7.1", "mini-css-extract-plugin": "^2.7.1",
"pako": "^2.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.0.1", "react-chartjs-2": "^5.0.1",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",

@ -308,12 +308,12 @@ export default class API {
return res; return res;
} }
async getLanguageEntries(modules, code=null, useCache=false) { async getLanguageEntries(modules, code=null, compression=null) {
if (!Array.isArray(modules)) { if (!Array.isArray(modules)) {
modules = [modules]; modules = [modules];
} }
return this.apiCall("language/getEntries", {code: code, modules: modules}); return this.apiCall("language/getEntries", {code: code, modules: modules, compression: compression});
} }
/** ApiKeyAPI **/ /** ApiKeyAPI **/

@ -1,7 +1,8 @@
import React, {useReducer} from 'react'; import React, {useReducer} from 'react';
import {createContext, useCallback, useState} from "react"; import {createContext, useCallback, useState} from "react";
import { enUS as dateFnsEN, de as dateFnsDE } from 'date-fns/locale'; import { enUS as dateFnsEN, de as dateFnsDE } from 'date-fns/locale';
import {getCookie, getParameter} from "./util"; import {encodeText, getCookie, getParameter} from "./util";
import pako from "pako";
const LocaleContext = createContext(null); const LocaleContext = createContext(null);
@ -109,7 +110,13 @@ function LocaleProvider(props) {
} }
if (modules.length > 0) { if (modules.length > 0) {
let data = await api.apiCall("language/getEntries", { code: code, modules: modules }); let compression = "zlib";
let data = await api.getLanguageEntries(modules, code, compression);
if (compression && data.success) {
data.entries = JSON.parse(pako.inflate(encodeText(atob(data.compressed)), { to: 'string' }));
}
if (useCache) { if (useCache) {
if (data && data.success) { if (data && data.success) {

@ -4858,11 +4858,6 @@ date-fns@^2.29.3:
dependencies: dependencies:
"@babel/runtime" "^7.21.0" "@babel/runtime" "^7.21.0"
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
debug@2.6.9, debug@^2.6.0: debug@2.6.9, debug@^2.6.0:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -8392,6 +8387,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pako@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
param-case@^3.0.4: param-case@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"