Removed AdminLTE, some minor improvements
This commit is contained in:
parent
163ef9fbfe
commit
fb353d1bc8
36
Core/API/TestRedis.class.php
Normal file
36
Core/API/TestRedis.class.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\API;
|
||||||
|
|
||||||
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
|
|
||||||
|
class TestRedis extends Request {
|
||||||
|
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _execute(): bool {
|
||||||
|
|
||||||
|
$settings = $this->context->getSettings();
|
||||||
|
if (!$settings->isRateLimitingEnabled()) {
|
||||||
|
return $this->createError("Rate Limiting is currently disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = $this->context->getRedis();
|
||||||
|
if ($connection === null || !$connection->isConnected()) {
|
||||||
|
return $this->createError("Redis connection failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDescription(): string {
|
||||||
|
return "Allows users to test the redis connection with the configured credentials.";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultPermittedGroups(): array {
|
||||||
|
return [Group::ADMIN];
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,7 @@ return [
|
|||||||
"active" => "Aktiv",
|
"active" => "Aktiv",
|
||||||
"group" => "Gruppe",
|
"group" => "Gruppe",
|
||||||
"no_members" => "Keine Mitglieder in dieser Gruppe",
|
"no_members" => "Keine Mitglieder in dieser Gruppe",
|
||||||
|
"user_list_placeholder" => "Keine Benutzer zum Anzeigen",
|
||||||
|
|
||||||
# profile picture
|
# profile picture
|
||||||
"change_picture" => "Profilbild ändern",
|
"change_picture" => "Profilbild ändern",
|
||||||
|
@ -83,4 +83,8 @@ return [
|
|||||||
"datefns_time_format_precise" => "HH:mm:ss",
|
"datefns_time_format_precise" => "HH:mm:ss",
|
||||||
"datefns_datetime_format" => "dd.MM.yyyy HH:mm",
|
"datefns_datetime_format" => "dd.MM.yyyy HH:mm",
|
||||||
"datefns_datetime_format_precise" => "dd.MM.yyyy HH:mm:ss",
|
"datefns_datetime_format_precise" => "dd.MM.yyyy HH:mm:ss",
|
||||||
|
|
||||||
|
# localization
|
||||||
|
"error_language_fetch" => "Fehler beim Holen der Sprachen",
|
||||||
|
"error_language_set" => "Fehler beim Setzen der Sprache",
|
||||||
];
|
];
|
@ -55,6 +55,9 @@ return [
|
|||||||
"redis_host" => "Redis Host",
|
"redis_host" => "Redis Host",
|
||||||
"redis_port" => "Redis Port",
|
"redis_port" => "Redis Port",
|
||||||
"redis_password" => "Redis Passwort",
|
"redis_password" => "Redis Passwort",
|
||||||
|
"redis_test" => "Verbindung testen",
|
||||||
|
"redis_test_error" => "Redis-Verbindung fehlgeschlagen, überprüfen Sie die Daten.",
|
||||||
|
"redis_test_success" => "Redis-Verbindung erfolgreich aufgebaut.",
|
||||||
|
|
||||||
# dialog
|
# dialog
|
||||||
"fetch_settings_error" => "Fehler beim Holen der Einstellungen",
|
"fetch_settings_error" => "Fehler beim Holen der Einstellungen",
|
||||||
|
@ -57,6 +57,7 @@ return [
|
|||||||
"group" => "Group",
|
"group" => "Group",
|
||||||
"no_members" => "No members in this group",
|
"no_members" => "No members in this group",
|
||||||
"edit_profile" => "Edit Profile",
|
"edit_profile" => "Edit Profile",
|
||||||
|
"user_list_placeholder" => "No users to display",
|
||||||
|
|
||||||
# profile picture
|
# profile picture
|
||||||
"change_picture" => "Change profile picture",
|
"change_picture" => "Change profile picture",
|
||||||
|
@ -83,4 +83,8 @@ return [
|
|||||||
"datefns_time_format_precise" => "pp",
|
"datefns_time_format_precise" => "pp",
|
||||||
"datefns_datetime_format" => "MM/dd/yyyy p",
|
"datefns_datetime_format" => "MM/dd/yyyy p",
|
||||||
"datefns_datetime_format_precise" => "MM/dd/yyyy pp",
|
"datefns_datetime_format_precise" => "MM/dd/yyyy pp",
|
||||||
|
|
||||||
|
# localization
|
||||||
|
"error_language_fetch" => "Error fetching languages",
|
||||||
|
"error_language_set" => "Error setting language",
|
||||||
];
|
];
|
@ -55,6 +55,9 @@ return [
|
|||||||
"redis_host" => "Redis host",
|
"redis_host" => "Redis host",
|
||||||
"redis_port" => "Redis port",
|
"redis_port" => "Redis port",
|
||||||
"redis_password" => "Redis password",
|
"redis_password" => "Redis password",
|
||||||
|
"redis_test" => "Test Connection",
|
||||||
|
"redis_test_error" => "Redis Connection failed, check your credentials.",
|
||||||
|
"redis_test_success" => "Redis Connection successfully established.",
|
||||||
|
|
||||||
# dialog
|
# dialog
|
||||||
"fetch_settings_error" => "Error fetching settings",
|
"fetch_settings_error" => "Error fetching settings",
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{{ site.name }} - {{ L("admin.title") }}</title>
|
<title>{{ site.name }} - {{ L("admin.title") }}</title>
|
||||||
<link rel="stylesheet" href="/css/fontawesome.min.css" nonce="{{ site.csp.nonce }}">
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
12
README.md
12
README.md
@ -51,6 +51,10 @@ The compiled dist files will be automatically moved to `/js`.
|
|||||||
|
|
||||||
To spawn a temporary development server, run:
|
To spawn a temporary development server, run:
|
||||||
```bash
|
```bash
|
||||||
|
php cli frontend dev <module>
|
||||||
|
```
|
||||||
|
or directly via yarn:
|
||||||
|
```bash
|
||||||
cd react
|
cd react
|
||||||
yarn workspace $project run dev
|
yarn workspace $project run dev
|
||||||
```
|
```
|
||||||
@ -97,7 +101,11 @@ An endpoint consists of two important functions:
|
|||||||
To create an API category containing multiple endpoints, a parent class inheriting from `Request`, e.g. `class MultipleAPI extends Request` is required.
|
To create an API category containing multiple endpoints, a parent class inheriting from `Request`, e.g. `class MultipleAPI extends Request` is required.
|
||||||
All endpoints inside this category then inherit from the `MultipleAPI` class.
|
All endpoints inside this category then inherit from the `MultipleAPI` class.
|
||||||
|
|
||||||
The classes must be present inside the [API](/Core/API) directory according to the other endpoints.
|
The classes must be present inside the [API](/Site/API) directory according to the other endpoints.
|
||||||
|
You can easily create new classes using the template command:
|
||||||
|
```bash
|
||||||
|
php cli.php api add
|
||||||
|
```
|
||||||
|
|
||||||
### Access Control
|
### Access Control
|
||||||
|
|
||||||
@ -355,7 +363,7 @@ php cli.php api <add> # interactive wizard
|
|||||||
│ ├── shared # shared source files, including API and localization
|
│ ├── shared # shared source files, including API and localization
|
||||||
│ ├── admin-panel # the admin panel source files
|
│ ├── admin-panel # the admin panel source files
|
||||||
│ ├── dist # compiler output
|
│ ├── dist # compiler output
|
||||||
├── [js/css/img/fonts/files/docs] # static web assets, files, licenses
|
├── [js/css/img/fonts/files] # static web assets, files
|
||||||
├── docker # docker configuration files and build scripts
|
├── docker # docker configuration files and build scripts
|
||||||
└── test # php unit & integraton test files
|
└── test # php unit & integraton test files
|
||||||
```
|
```
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014-2018 almasaeed2010
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,8 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="/css/fontawesome.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<div id="admin-panel"></div>
|
<div id="admin-panel"></div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -6,9 +6,6 @@ import Footer from "./elements/footer";
|
|||||||
import {useContext, useEffect} from "react";
|
import {useContext, useEffect} from "react";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
|
|
||||||
// css
|
|
||||||
import './res/adminlte.min.css';
|
|
||||||
|
|
||||||
// views
|
// views
|
||||||
import View404 from "./views/404";
|
import View404 from "./views/404";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@ -61,16 +58,8 @@ export default function AdminDashboard(props) {
|
|||||||
hideDialog: hideDialog
|
hideDialog: hideDialog
|
||||||
};
|
};
|
||||||
|
|
||||||
// add fixed-layout to body, I don't want to rewrite my base.twig template
|
|
||||||
if (!document.body.className.includes("layout-fixed")) {
|
|
||||||
document.body.className = clsx(document.body.className, "layout-fixed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return <BrowserRouter>
|
return <BrowserRouter>
|
||||||
<Sidebar {...controlObj} />
|
<Sidebar {...controlObj}>
|
||||||
<div className={"wrapper"}>
|
|
||||||
<div className={"content-wrapper p-2"}>
|
|
||||||
<section className={"content"}>
|
|
||||||
<Suspense fallback={<div>{L("general.loading")}... </div>}>
|
<Suspense fallback={<div>{L("general.loading")}... </div>}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={"/admin"} element={<Navigate to={"/admin/dashboard"} />}/>
|
<Route path={"/admin"} element={<Navigate to={"/admin/dashboard"} />}/>
|
||||||
@ -88,10 +77,8 @@ export default function AdminDashboard(props) {
|
|||||||
<Route path={"*"} element={<View404 />} />
|
<Route path={"*"} element={<View404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Dialog {...dialog}/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Footer info={info} />
|
<Footer info={info} />
|
||||||
|
</Sidebar>
|
||||||
|
<Dialog {...dialog}/>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
}
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||||
import API from "shared/api";
|
import API from "shared/api";
|
||||||
import Icon from "shared/elements/icon";
|
|
||||||
import LoginForm from "shared/views/login";
|
import LoginForm from "shared/views/login";
|
||||||
import {Alert} from "@mui/lab";
|
import {Alert} from "@mui/lab";
|
||||||
import {Button} from "@mui/material";
|
import {Button, CircularProgress} from "@mui/material";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import AdminDashboard from "./AdminDashboard";
|
import AdminDashboard from "./AdminDashboard";
|
||||||
|
|
||||||
@ -103,7 +102,7 @@ export default function App() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Alert>
|
</Alert>
|
||||||
} else {
|
} else {
|
||||||
return <b>{L("general.loading")}… <Icon icon={"spinner"}/></b>
|
return <b>{L("general.loading")}… <CircularProgress /></b>
|
||||||
}
|
}
|
||||||
} else if (!user || !api.loggedIn || (api.user.twoFactorToken?.confirmed && !api.user.twoFactorToken.authenticated)) {
|
} else if (!user || !api.loggedIn || (api.user.twoFactorToken?.confirmed && !api.user.twoFactorToken.authenticated)) {
|
||||||
return <LoginForm api={api} info={info} onLogin={fetchUser} onLogout={onLogout} />
|
return <LoginForm api={api} info={info} onLogin={fetchUser} onLogout={onLogout} />
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {Divider, styled} from "@mui/material";
|
||||||
|
|
||||||
|
const StyledFooter = styled("footer")((props) => ({
|
||||||
|
position: "fixed",
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingTop: props.theme.spacing(1),
|
||||||
|
paddingRight: props.theme.spacing(1),
|
||||||
|
paddingLeft: props.theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
export default function Footer(props) {
|
export default function Footer(props) {
|
||||||
|
|
||||||
return (
|
return <StyledFooter>
|
||||||
<footer className={"main-footer"}>
|
<Divider />
|
||||||
Theme: <strong>Copyright © 2014-2021 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.2.0</strong>
|
<b>Framework</b>: <a href={"https://git.romanh.de/Projekte/web-base"} target={"_blank"}>WebBase</a> Version {props.info.version}
|
||||||
Framework: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> {props.info.version}
|
</StyledFooter>
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import {Link} from "react-router-dom";
|
|
||||||
import Icon from "shared/elements/icon";
|
|
||||||
|
|
||||||
export default function Header(props) {
|
|
||||||
|
|
||||||
const parent = {
|
|
||||||
api: props.api
|
|
||||||
};
|
|
||||||
|
|
||||||
function onToggleSidebar() {
|
|
||||||
let classes = document.body.classList;
|
|
||||||
if (classes.contains("sidebar-collapse")) {
|
|
||||||
classes.remove("sidebar-collapse");
|
|
||||||
classes.add("sidebar-open");
|
|
||||||
} else {
|
|
||||||
classes.add("sidebar-collapse");
|
|
||||||
classes.remove("sidebar-add");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className={"main-header navbar navbar-expand navbar-white navbar-light"}>
|
|
||||||
|
|
||||||
{/*Left navbar links */}
|
|
||||||
<ul className={"navbar-nav"}>
|
|
||||||
<li className={"nav-item"}>
|
|
||||||
<a href={"#"} className={"nav-link"} role={"button"} onClick={onToggleSidebar}>
|
|
||||||
<Icon icon={"bars"}/>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className={"nav-item d-none d-sm-inline-block"}>
|
|
||||||
<Link to={"/admin/dashboard"} className={"nav-link"}>
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{/* SEARCH FORM */}
|
|
||||||
<form className={"form-inline ml-3"}>
|
|
||||||
<div className={"input-group input-group-sm"}>
|
|
||||||
<input className={"form-control form-control-navbar"} type={"search"} placeholder={"Search"} aria-label={"Search"} />
|
|
||||||
<div className={"input-group-append"}>
|
|
||||||
<button className={"btn btn-navbar"} type={"submit"}>
|
|
||||||
<Icon icon={"search"}/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Right navbar links */}
|
|
||||||
<ul className={"navbar-nav ml-auto"}>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,30 +1,107 @@
|
|||||||
import React, {useCallback, useContext} from 'react';
|
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||||
import {Link, NavLink} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import Icon from "shared/elements/icon";
|
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {styled} from "@mui/material";
|
import {
|
||||||
|
Box, CssBaseline, Divider,
|
||||||
|
IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
|
||||||
|
Select, Drawer,
|
||||||
|
styled, MenuItem, Menu,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Dropdown } from '@mui/base/Dropdown';
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
import ProfilePicture from "shared/elements/profile-picture";
|
import ProfilePicture from "shared/elements/profile-picture";
|
||||||
|
import {Dns, Groups, People, QueryStats, Security, Settings, Route, ArrowBack, Translate} from "@mui/icons-material";
|
||||||
|
import useCurrentPath from "shared/hooks/current-path";
|
||||||
|
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
const ProfileLink = styled(Link)((props) => ({
|
const ProfileLink = styled(Link)((props) => ({
|
||||||
"& > div": {
|
"& > div": {
|
||||||
padding: 3,
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
color: "inherit",
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginTop: props.theme.spacing(1),
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "35px auto",
|
||||||
|
"& > span": {
|
||||||
|
alignSelf: "center",
|
||||||
|
marginLeft: props.theme.spacing(1)
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
...theme.mixins.toolbar,
|
||||||
|
"& > button": {
|
||||||
|
display: 'flex',
|
||||||
|
marginLeft: "auto",
|
||||||
|
},
|
||||||
|
"& > img": {
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 30,
|
height: 30,
|
||||||
},
|
},
|
||||||
marginLeft: props.theme.spacing(1),
|
|
||||||
marginTop: props.theme.spacing(1),
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "45px auto",
|
|
||||||
"& > span": {
|
"& > span": {
|
||||||
alignSelf: "center"
|
marginLeft: theme.spacing(2),
|
||||||
|
fontSize: "1.5em",
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const openedMixin = (theme) => ({
|
||||||
|
width: drawerWidth,
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
overflowX: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
const closedMixin = (theme) => ({
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
overflowX: 'hidden',
|
||||||
|
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
width: `calc(${theme.spacing(8)} + 1px)`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledDrawer = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(
|
||||||
|
({ theme, open }) => ({
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
...(open && {
|
||||||
|
...openedMixin(theme),
|
||||||
|
'& .MuiDrawer-paper': openedMixin(theme),
|
||||||
|
}),
|
||||||
|
...(!open && {
|
||||||
|
...closedMixin(theme),
|
||||||
|
'& .MuiDrawer-paper': closedMixin(theme),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export default function Sidebar(props) {
|
export default function Sidebar(props) {
|
||||||
|
|
||||||
const api = props.api;
|
const api = props.api;
|
||||||
const showDialog = props.showDialog;
|
const showDialog = props.showDialog;
|
||||||
const {translate: L} = useContext(LocaleContext);
|
const {translate: L, currentLocale, setLanguageByCode} = useContext(LocaleContext);
|
||||||
|
const [languages, setLanguages] = useState(null);
|
||||||
|
const [fetchLanguages, setFetchLanguages] = useState(true);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(window.screen.width >= 1000);
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const currentPath = useCurrentPath();
|
||||||
|
|
||||||
const onLogout = useCallback(() => {
|
const onLogout = useCallback(() => {
|
||||||
api.logout().then(obj => {
|
api.logout().then(obj => {
|
||||||
@ -36,99 +113,156 @@ export default function Sidebar(props) {
|
|||||||
});
|
});
|
||||||
}, [api, showDialog]);
|
}, [api, showDialog]);
|
||||||
|
|
||||||
|
const onSetLanguage = useCallback((code) => {
|
||||||
|
setLanguageByCode(api, code).then((res) => {
|
||||||
|
if (!res.success) {
|
||||||
|
showDialog(res.msg, L("general.error_language_set"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [api, showDialog]);
|
||||||
|
|
||||||
|
const onFetchLanguages = useCallback((force = false) => {
|
||||||
|
if (force || fetchLanguages) {
|
||||||
|
setFetchLanguages(false);
|
||||||
|
api.getLanguages().then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
setLanguages(res.languages);
|
||||||
|
} else {
|
||||||
|
setLanguages({});
|
||||||
|
showDialog(res.msg, L("general.error_language_fetch"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [api, fetchLanguages, showDialog]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFetchLanguages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const menuItems = {
|
const menuItems = {
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"name": "admin.dashboard",
|
"name": "admin.dashboard",
|
||||||
"icon": "tachometer-alt"
|
"icon": <QueryStats />
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"name": "admin.users",
|
"name": "admin.users",
|
||||||
"icon": "users"
|
"icon": <People />
|
||||||
},
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"name": "admin.groups",
|
"name": "admin.groups",
|
||||||
"icon": "users-cog"
|
"icon": <Groups />
|
||||||
},
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
"name": "admin.page_routes",
|
"name": "admin.page_routes",
|
||||||
"icon": "copy",
|
"icon": <Route />
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "admin.settings",
|
"name": "admin.settings",
|
||||||
"icon": "tools"
|
"icon": <Settings />
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"name": "admin.acl",
|
"name": "admin.acl",
|
||||||
"icon": "door-open"
|
"icon": <Security />
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"name": "admin.logs",
|
"name": "admin.logs",
|
||||||
"icon": "file-medical-alt"
|
"icon": <Dns />
|
||||||
},
|
|
||||||
"help": {
|
|
||||||
"name": "admin.help",
|
|
||||||
"icon": "question-circle"
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let li = [];
|
const NavbarItem = (props) => <ListItem disablePadding sx={{ display: 'block' }}>
|
||||||
for (let id in menuItems) {
|
<ListItemButton onClick={props.onClick} selected={props.active} sx={{
|
||||||
let obj = menuItems[id];
|
minHeight: 48,
|
||||||
const badge = (obj.badge ? <span className={"right badge badge-" + obj.badge.type}>{obj.badge.value}</span> : <></>);
|
justifyContent: drawerOpen ? 'initial' : 'center',
|
||||||
|
px: 2.5,
|
||||||
|
}}>
|
||||||
|
<ListItemIcon sx={{
|
||||||
|
minWidth: 0,
|
||||||
|
mr: drawerOpen ? 2 : 'auto',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{props.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={L(props.name)} sx={{ display: drawerOpen ? "block" : "none" }} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
li.push(
|
let li = [];
|
||||||
<li key={id} className={"nav-item"}>
|
for (const [id, menuItem] of Object.entries(menuItems)) {
|
||||||
<NavLink to={"/admin/" + id} className={"nav-link"}>
|
const match= /^\/admin\/(.*)$/.exec(currentPath);
|
||||||
<Icon icon={obj.icon} className={"nav-icon"} /><p>{L(obj.name)}{badge}</p>
|
const active = match?.length >= 2 && match[1] === id;
|
||||||
</NavLink>
|
li.push(<NavbarItem key={id} {...menuItem} active={active} onClick={() => navigate(`/admin/${id}`)} />);
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li.push(<li className={"nav-item"} key={"logout"}>
|
li.push(<NavbarItem key={"logout"} name={"general.logout"} icon={<ArrowBack />} onClick={onLogout}/>);
|
||||||
<a href={"#"} onClick={() => onLogout()} className={"nav-link"}>
|
|
||||||
<Icon icon={"arrow-left"} className={"nav-icon"} />
|
|
||||||
<p>{L("general.logout")}</p>
|
|
||||||
</a>
|
|
||||||
</li>);
|
|
||||||
|
|
||||||
return <>
|
return <Box sx={{ display: 'flex' }}>
|
||||||
<aside className={"main-sidebar sidebar-dark-primary elevation-4"}>
|
<CssBaseline />
|
||||||
<Link href={"#"} className={"brand-link"} to={"/admin/dashboard"}>
|
<StyledDrawer variant={"permanent"} open={drawerOpen}>
|
||||||
<img src={"/img/icons/logo.png"} alt={"Logo"} className={"brand-image img-circle elevation-3"} style={{opacity: ".8"}} />
|
<DrawerHeader>
|
||||||
<span className={"brand-text font-weight-light ml-2"}>WebBase</span>
|
{drawerOpen && <>
|
||||||
</Link>
|
<img src={"/img/icons/logo.png"} alt={"Logo"} />
|
||||||
|
<span>WebBase</span>
|
||||||
<div className={"sidebar os-host os-theme-light os-host-overflow os-host-overflow-y os-host-resize-disabled os-host-scrollbar-horizontal-hidden os-host-transition"}>
|
</>}
|
||||||
{/* IDK what this is */}
|
<IconButton onClick={() => setDrawerOpen(!drawerOpen)}>
|
||||||
<div className={"os-resize-observer-host"}>
|
{drawerOpen ? <ChevronLeftIcon/> : <ChevronRightIcon/>}
|
||||||
<div className={"os-resize-observer observed"} style={{left: "0px", right: "auto"}}/>
|
</IconButton>
|
||||||
</div>
|
</DrawerHeader>
|
||||||
<div className={"os-size-auto-observer"} style={{height: "calc(100% + 1px)", float: "left"}}>
|
<Divider/>
|
||||||
<div className={"os-resize-observer observed"}/>
|
<ListItem sx={{display: 'block'}}>
|
||||||
</div>
|
<Box sx={{opacity: drawerOpen ? 1 : 0}}>{L("account.logged_in_as")}:</Box>
|
||||||
<div className={"os-content-glue"} style={{margin: "0px -8px"}}/>
|
|
||||||
<div className={"os-padding"}>
|
|
||||||
<div className={"os-viewport os-viewport-native-scrollbars-invisible"} style={{right: "0px", bottom: "0px"}}>
|
|
||||||
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
|
|
||||||
<div className="user-panel mt-3 pb-3 mb-3 d-flex">
|
|
||||||
<div className="info">
|
|
||||||
<div className={"d-block text-light"}>{L("account.logged_in_as")}:</div>
|
|
||||||
<ProfileLink to={"/admin/profile"}>
|
<ProfileLink to={"/admin/profile"}>
|
||||||
<ProfilePicture user={api.user} />
|
<ProfilePicture user={api.user}/>
|
||||||
<span>{api.user?.name || L("account.not_logged_in")}</span>
|
{drawerOpen && <span>{api.user?.name || L("account.not_logged_in")}</span>}
|
||||||
</ProfileLink>
|
</ProfileLink>
|
||||||
</div>
|
</ListItem>
|
||||||
</div>
|
<Divider/>
|
||||||
<nav className={"mt-2"}>
|
<List>
|
||||||
<ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}>
|
|
||||||
{li}
|
{li}
|
||||||
</ul>
|
</List>
|
||||||
</nav>
|
<Divider/>
|
||||||
</div>
|
<ListItem sx={{display: 'block'}}>
|
||||||
</div>
|
{ drawerOpen ?
|
||||||
</div>
|
<Select native value={currentLocale} size={"small"} fullWidth={true}
|
||||||
</div>
|
onChange={e => onSetLanguage(e.target.value)}>
|
||||||
</aside>
|
{Object.values(languages || {}).map(language =>
|
||||||
</>
|
<option key={language.code} value={language.code}>
|
||||||
|
{language.name}
|
||||||
|
</option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
: <ListItemButton sx={{
|
||||||
|
minHeight: 48,
|
||||||
|
justifyContent: 'center',
|
||||||
|
px: 2.5,
|
||||||
|
}}>
|
||||||
|
<Dropdown>
|
||||||
|
<ListItemIcon onClick={e => setAnchorEl(e.currentTarget)} sx={{
|
||||||
|
minWidth: 0,
|
||||||
|
mr: 'auto',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Translate />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Menu open={!!anchorEl}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
onClick={() => setAnchorEl(null)}
|
||||||
|
transformOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||||
|
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}>
|
||||||
|
{Object.values(languages || {}).map(language =>
|
||||||
|
<MenuItem key={language.code} onClick={() => onSetLanguage(language.code)}>
|
||||||
|
{language.name}
|
||||||
|
</MenuItem>)
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</ListItemButton>
|
||||||
|
}
|
||||||
|
</ListItem>
|
||||||
|
</StyledDrawer>
|
||||||
|
<Box component="main" sx={{flexGrow: 1, p: 1}}>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
|
31
react/admin-panel/src/elements/view-content.jsx
Normal file
31
react/admin-panel/src/elements/view-content.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {Box, Breadcrumbs, Grid, styled} from "@mui/material";
|
||||||
|
|
||||||
|
const StyledViewContent = styled(Box)((props) => ({
|
||||||
|
padding: props.theme.spacing(2),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const StyledNavigation = styled(Grid)((props) => ({
|
||||||
|
alignSelf: "end",
|
||||||
|
"& ol": {
|
||||||
|
justifyContent: "end",
|
||||||
|
margin: "auto"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function ViewContent(props) {
|
||||||
|
|
||||||
|
const {title, path, children, ...other} = props;
|
||||||
|
|
||||||
|
return <StyledViewContent {...other}>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
</Grid>
|
||||||
|
<StyledNavigation item xs={6}>
|
||||||
|
<Breadcrumbs>{path}</Breadcrumbs>
|
||||||
|
</StyledNavigation>
|
||||||
|
</Grid>
|
||||||
|
{children}
|
||||||
|
</StyledViewContent>
|
||||||
|
|
||||||
|
}
|
12
react/admin-panel/src/res/adminlte.min.css
vendored
12
react/admin-panel/src/res/adminlte.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -12,11 +12,14 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
IconButton, styled, FormGroup, FormLabel, Box
|
IconButton, styled, FormGroup, FormLabel, Box, Grid
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {Add, Delete, Edit, Refresh} from "@mui/icons-material";
|
import {Add, Delete, Edit, Refresh} from "@mui/icons-material";
|
||||||
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";
|
||||||
|
import ViewContent from "../elements/view-content";
|
||||||
|
import ButtonBar from "../elements/button-bar";
|
||||||
|
import TableBodyStriped from "shared/elements/table-body-striped";
|
||||||
|
|
||||||
const BorderedColumn = styled(TableCell)({
|
const BorderedColumn = styled(TableCell)({
|
||||||
borderLeft: "1px dotted #666",
|
borderLeft: "1px dotted #666",
|
||||||
@ -206,73 +209,68 @@ export default function AccessControlList(props) {
|
|||||||
return <>{rows}</>
|
return <>{rows}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <ViewContent title={L("permissions.title")} path={[
|
||||||
<div className={"content-header"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"container-fluid"}>
|
<span key={"permissions"}>{L("permissions.title_short")}</span>,
|
||||||
<div className={"row mb-2"}>
|
]}>
|
||||||
<div className={"col-sm-6"}>
|
<Grid container>
|
||||||
<h1 className={"m-0 text-dark"}>{L("permissions.title")}</h1>
|
<Grid item xs={6}>
|
||||||
</div>
|
<FormGroup>
|
||||||
<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">{L("permissions.title_short")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"row"}>
|
|
||||||
<FormGroup className={"col-6"}>
|
|
||||||
<FormLabel>{L("permissions.search")}</FormLabel>
|
<FormLabel>{L("permissions.search")}</FormLabel>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder={L("permissions.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"}
|
||||||
size={"small"} />
|
size={"small"}/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<div className={"col-6 text-right"}>
|
</Grid>
|
||||||
|
<Grid item xs={6} textAlign={"end"}>
|
||||||
<Box>
|
<Box>
|
||||||
<FormLabel>{L("general.controls")}</FormLabel>
|
<FormLabel>{L("general.controls")}</FormLabel>
|
||||||
</Box>
|
</Box>
|
||||||
<Box mb={2}>
|
<ButtonBar mb={2}>
|
||||||
<Button variant={"outlined"} color={"primary"} className={"mr-1"} size={"small"}
|
<Button variant={"outlined"} color={"primary"} size={"small"}
|
||||||
startIcon={<Refresh />} onClick={() => onFetchACL(true)}>
|
startIcon={<Refresh/>} onClick={() => onFetchACL(true)}>
|
||||||
{L("general.reload")}
|
{L("general.reload")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={"outlined"} startIcon={<Add />} size={"small"}
|
<Button variant={"outlined"} startIcon={<Add/>} size={"small"}
|
||||||
disabled={!props.api.hasGroup(USER_GROUP_ADMIN)}
|
disabled={!props.api.hasGroup(USER_GROUP_ADMIN)}
|
||||||
onClick={() => setDialogData({
|
onClick={() => setDialogData({
|
||||||
open: true,
|
open: true,
|
||||||
title: L("permissions.add_permission"),
|
title: L("permissions.add_permission"),
|
||||||
inputs: [
|
inputs: [
|
||||||
{ type: "label", value: L("permissions.method") + ":" },
|
{type: "label", value: L("permissions.method") + ":"},
|
||||||
{ type: "text", name: "method", value: "", placeholder: L("permissions.method") },
|
{type: "text", name: "method", value: "", placeholder: L("permissions.method")},
|
||||||
{ type: "label", value: L("permissions.description") + ":" },
|
{type: "label", value: L("permissions.description") + ":"},
|
||||||
{ type: "text", name: "description", maxLength: 128, placeholder: L("permissions.description") }
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "description",
|
||||||
|
maxLength: 128,
|
||||||
|
placeholder: L("permissions.description")
|
||||||
|
}
|
||||||
],
|
],
|
||||||
onOption: (option, inputData) => option === 0 ? onUpdatePermission(inputData, []) : true
|
onOption: (option, inputData) => option === 0 ? onUpdatePermission(inputData, []) : true
|
||||||
})} >
|
})}>
|
||||||
{L("general.add")}
|
{L("general.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</ButtonBar>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
||||||
<Table stickyHeader size={"small"} className={"table-striped"}>
|
<Table stickyHeader size={"small"}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{L("permissions.permission")}</TableCell>
|
<TableCell>{L("permissions.permission")}</TableCell>
|
||||||
<BorderedColumn align={"center"}><i>{L("permissions.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>)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBodyStriped>
|
||||||
<PermissionList />
|
<PermissionList/>
|
||||||
</TableBody>
|
</TableBodyStriped>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<Dialog show={dialogData.open}
|
<Dialog show={dialogData.open}
|
||||||
@ -281,6 +279,6 @@ export default function AccessControlList(props) {
|
|||||||
message={dialogData.message}
|
message={dialogData.message}
|
||||||
onOption={dialogData.onOption}
|
onOption={dialogData.onOption}
|
||||||
inputs={dialogData.inputs}
|
inputs={dialogData.inputs}
|
||||||
options={[L("general.ok"), L("general.cancel")]} />
|
options={[L("general.ok"), L("general.cancel")]}/>
|
||||||
</>
|
</ViewContent>
|
||||||
}
|
}
|
@ -8,10 +8,12 @@ import {DataTable, ControlsColumn, NumericColumn, StringColumn} from "shared/ele
|
|||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import usePagination from "shared/hooks/pagination";
|
import usePagination from "shared/hooks/pagination";
|
||||||
import Dialog from "shared/elements/dialog";
|
import Dialog from "shared/elements/dialog";
|
||||||
import {FormControl, FormGroup, FormLabel, TextField, Button, CircularProgress, Box} from "@mui/material";
|
import {FormControl, FormLabel, TextField, Button, CircularProgress, Box, Grid} from "@mui/material";
|
||||||
import {Add, Delete, KeyboardArrowLeft, Save} from "@mui/icons-material";
|
import {Add, Delete, KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||||
import {MuiColorInput} from "mui-color-input";
|
import {MuiColorInput} from "mui-color-input";
|
||||||
import ButtonBar from "../../elements/button-bar";
|
import ButtonBar from "../../elements/button-bar";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
import FormGroup from "../../elements/form-group";
|
||||||
|
|
||||||
const defaultGroupData = {
|
const defaultGroupData = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -175,28 +177,15 @@ export default function EditGroupView(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"content-header"}>
|
<ViewContent title={ isNewGroup ? L("account.new_group") : L("account.group") + ": " + group.name }
|
||||||
<div className={"container-fluid"}>
|
path={[
|
||||||
<div className={"row mb-2"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"col-sm-6"}>
|
<Link key={"group"} to={"/admin/groups"}>{L("account.group")}</Link>,
|
||||||
<h1 className={"m-0 text-dark"}>
|
<span key={"action"} >{isNewGroup ? L("general.new") : groupId}</span>,
|
||||||
{ isNewGroup ? L("account.new_group") : L("account.group") + ": " + group.name }
|
]}>
|
||||||
</h1>
|
<Grid container>
|
||||||
</div>
|
<Grid item xs={6}>
|
||||||
<div className={"col-sm-6"}>
|
<FormGroup>
|
||||||
<ol className={"breadcrumb float-sm-right"}>
|
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
|
||||||
<li className="breadcrumb-item active"><Link to={"/admin/groups"}>{L("account.group")}</Link></li>
|
|
||||||
<li className="breadcrumb-item active">{ isNewGroup ? L("general.new") : groupId }</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<div className={"row"}>
|
|
||||||
<div className={"col-4 pl-5 pr-5"}>
|
|
||||||
<FormGroup className={"my-2"}>
|
|
||||||
<FormLabel htmlFor={"name"}>
|
<FormLabel htmlFor={"name"}>
|
||||||
{L("account.group_name")}
|
{L("account.group_name")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@ -208,7 +197,7 @@ export default function EditGroupView(props) {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup className={"my-2"}>
|
<FormGroup>
|
||||||
<FormLabel htmlFor={"color"}>
|
<FormLabel htmlFor={"color"}>
|
||||||
{L("account.color")}
|
{L("account.color")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@ -249,17 +238,14 @@ export default function EditGroupView(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isNewGroup && api.hasPermission("groups/getMembers") ?
|
{!isNewGroup && api.hasPermission("groups/getMembers") ?
|
||||||
<Box m={3} className={"col-6"}>
|
<Box mt={3}>
|
||||||
<h4>{L("account.members")}</h4>
|
<h4>{L("account.members")}</h4>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={members}
|
data={members}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
defaultSortOrder={"asc"}
|
defaultSortOrder={"asc"}
|
||||||
defaultSortColumn={0}
|
defaultSortColumn={0}
|
||||||
className={"table table-striped"}
|
|
||||||
fetchData={onFetchMembers}
|
fetchData={onFetchMembers}
|
||||||
placeholder={L("account.no_members")}
|
placeholder={L("account.no_members")}
|
||||||
columns={[
|
columns={[
|
||||||
@ -298,7 +284,9 @@ export default function EditGroupView(props) {
|
|||||||
</Box>
|
</Box>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
</div>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ViewContent>
|
||||||
<Dialog show={dialogData.open}
|
<Dialog show={dialogData.open}
|
||||||
onClose={() => setDialogData({open: false})}
|
onClose={() => setDialogData({open: false})}
|
||||||
title={dialogData.title}
|
title={dialogData.title}
|
||||||
@ -307,5 +295,4 @@ export default function EditGroupView(props) {
|
|||||||
inputs={dialogData.inputs}
|
inputs={dialogData.inputs}
|
||||||
options={[L("general.ok"), L("general.cancel")]} />
|
options={[L("general.ok"), L("general.cancel")]} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import {LocaleContext} from "shared/locale";
|
|||||||
import {ControlsColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
import {ControlsColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||||
import {Add, Edit} from "@mui/icons-material";
|
import {Add, Edit} from "@mui/icons-material";
|
||||||
import usePagination from "shared/hooks/pagination";
|
import usePagination from "shared/hooks/pagination";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
|
||||||
|
|
||||||
export default function GroupListView(props) {
|
export default function GroupListView(props) {
|
||||||
@ -46,29 +47,15 @@ export default function GroupListView(props) {
|
|||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <>
|
return <ViewContent title={L("account.groups")} path={[
|
||||||
<div className={"content-header"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"container-fluid"}>
|
<span key={"groups"} >{L("account.groups")}</span>,
|
||||||
<div className={"row mb-2"}>
|
]}>
|
||||||
<div className={"col-sm-6"}>
|
|
||||||
<h1 className={"m-0 text-dark"}>{L("account.groups")}</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">{L("account.groups")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={groups}
|
data={groups}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
defaultSortOrder={"asc"}
|
defaultSortOrder={"asc"}
|
||||||
defaultSortColumn={0}
|
defaultSortColumn={0}
|
||||||
className={"table table-striped"}
|
|
||||||
fetchData={onFetchGroups}
|
fetchData={onFetchGroups}
|
||||||
placeholder={"No groups to display"}
|
placeholder={"No groups to display"}
|
||||||
columns={columnDefinitions}
|
columns={columnDefinitions}
|
||||||
@ -80,8 +67,5 @@ export default function GroupListView(props) {
|
|||||||
disabled: !api.hasPermission("groups/create"),
|
disabled: !api.hasPermission("groups/create"),
|
||||||
children: L("general.create_new")
|
children: L("general.create_new")
|
||||||
}]}/>
|
}]}/>
|
||||||
</div>
|
</ViewContent>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
@ -3,13 +3,14 @@ import {LocaleContext} from "shared/locale";
|
|||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import usePagination from "shared/hooks/pagination";
|
import usePagination from "shared/hooks/pagination";
|
||||||
import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
|
import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||||
import {Box, FormControl, FormGroup, FormLabel, IconButton, MenuItem, TextField} from "@mui/material";
|
import {Box, FormControl, FormGroup, FormLabel, Grid, IconButton, MenuItem, TextField} from "@mui/material";
|
||||||
import {DateTimePicker} from "@mui/x-date-pickers";
|
import {DateTimePicker} from "@mui/x-date-pickers";
|
||||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import {API_DATETIME_FORMAT} from "shared/constants";
|
import {API_DATETIME_FORMAT} from "shared/constants";
|
||||||
import {format, toDate} from "date-fns";
|
import {format, toDate} from "date-fns";
|
||||||
import {ExpandLess, ExpandMore} from "@mui/icons-material";
|
import {ExpandLess, ExpandMore} from "@mui/icons-material";
|
||||||
|
import ViewContent from "../elements/view-content";
|
||||||
|
|
||||||
export default function LogView(props) {
|
export default function LogView(props) {
|
||||||
|
|
||||||
@ -106,36 +107,27 @@ export default function LogView(props) {
|
|||||||
messageColumn,
|
messageColumn,
|
||||||
];
|
];
|
||||||
|
|
||||||
return <>
|
return <ViewContent title={L("logs.title")} path={[
|
||||||
<div className={"content-header"}>
|
<Link key={"dashboard"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"container-fluid"}>
|
<span key={"logs"}>{L("logs.title")}</span>
|
||||||
<div className={"row mb-2"}>
|
]}>
|
||||||
<div className={"col-sm-6"}>
|
<Grid container spacing={2}>
|
||||||
<h1 className={"m-0 text-dark"}>{L("logs.title")}</h1>
|
<Grid item xs={2}>
|
||||||
</div>
|
<FormGroup>
|
||||||
<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">{L("logs.title")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"content overflow-auto"}>
|
|
||||||
<div className={"row p-2"}>
|
|
||||||
<FormGroup className={"col-2"}>
|
|
||||||
<FormLabel>{L("logs.severity")}</FormLabel>
|
<FormLabel>{L("logs.severity")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TextField select variant={"outlined"} size={"small"} value={logLevel}
|
<TextField select variant={"outlined"} size={"small"} value={logLevel}
|
||||||
onChange={e => setLogLevel(parseInt(e.target.value))}
|
onChange={e => setLogLevel(parseInt(e.target.value))}
|
||||||
inputProps={{ size: "small" }}>
|
inputProps={{size: "small"}}>
|
||||||
{LOG_LEVELS.map((value, index) =>
|
{LOG_LEVELS.map((value, index) =>
|
||||||
<MenuItem key={"option-" + value} value={index}>{value}</MenuItem>)
|
<MenuItem key={"option-" + value} value={index}>{value}</MenuItem>)
|
||||||
}
|
}
|
||||||
</TextField>
|
</TextField>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className={"col-4"}>
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<FormGroup>
|
||||||
<FormLabel>{L("logs.timestamp")}</FormLabel>
|
<FormLabel>{L("logs.timestamp")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
@ -143,13 +135,15 @@ export default function LogView(props) {
|
|||||||
value={timestamp ? toDate(new Date()) : null}
|
value={timestamp ? toDate(new Date()) : null}
|
||||||
format={L("general.datefns_datetime_format_precise")}
|
format={L("general.datefns_datetime_format_precise")}
|
||||||
onChange={(newValue) => setTimestamp(newValue)}
|
onChange={(newValue) => setTimestamp(newValue)}
|
||||||
slotProps={{ textField: { size:'small' } }}
|
slotProps={{textField: {size: 'small'}}}
|
||||||
sx={{"& .MuiInputBase-input": { height: "23px", padding: 1 }}}
|
sx={{"& .MuiInputBase-input": {height: "23px", padding: 1}}}
|
||||||
/>
|
/>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className={"col-6"}>
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormGroup>
|
||||||
<FormLabel>{L("logs.search")}</FormLabel>
|
<FormLabel>{L("logs.search")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TextField
|
<TextField
|
||||||
@ -160,20 +154,16 @@ export default function LogView(props) {
|
|||||||
size={"small"}/>
|
size={"small"}/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</Grid>
|
||||||
<div className={"container-fluid"}>
|
</Grid>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={logEntries}
|
data={logEntries}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
className={"table table-striped"}
|
|
||||||
fetchData={onFetchLogs}
|
fetchData={onFetchLogs}
|
||||||
forceReload={forceReload}
|
forceReload={forceReload}
|
||||||
defaultSortColumn={3}
|
defaultSortColumn={3}
|
||||||
defaultSortOrder={"desc"}
|
defaultSortOrder={"desc"}
|
||||||
placeholder={L("logs.no_entries_placeholder")}
|
placeholder={L("logs.no_entries_placeholder")}
|
||||||
columns={columnDefinitions} />
|
columns={columnDefinitions}/>
|
||||||
</div>
|
</ViewContent>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
@ -13,26 +13,65 @@ import {
|
|||||||
LibraryBooks,
|
LibraryBooks,
|
||||||
People
|
People
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import {Box, CircularProgress, Paper, Table, TableBody, TableCell, TableRow} from "@mui/material";
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
styled,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow
|
||||||
|
} from "@mui/material";
|
||||||
|
import ViewContent from "../elements/view-content";
|
||||||
|
import {Alert} from "@mui/lab";
|
||||||
|
|
||||||
const StatBox = (props) => <div className={"col-lg-3 col-6"}>
|
const StyledStatBox = styled(Alert)((props) => ({
|
||||||
<div className={"small-box bg-" + props.color}>
|
position: "relative",
|
||||||
<div className={"inner"}>
|
padding: 0,
|
||||||
|
"& > div": {
|
||||||
|
padding: 0,
|
||||||
|
width: "100%",
|
||||||
|
"& a": {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
"& div:nth-of-type(1)": {
|
||||||
|
padding: props.theme.spacing(2),
|
||||||
|
},
|
||||||
|
"& div:nth-of-type(2) > svg": {
|
||||||
|
position: "absolute",
|
||||||
|
top: props.theme.spacing(1),
|
||||||
|
right: props.theme.spacing(1),
|
||||||
|
opacity: 0.6,
|
||||||
|
fontSize: "5em"
|
||||||
|
},
|
||||||
|
"& div:nth-of-type(3)": {
|
||||||
|
backdropFilter: "brightness(70%)",
|
||||||
|
textAlign: "right",
|
||||||
|
padding: props.theme.spacing(0.5),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StatBox = (props) => <StyledStatBox variant={"filled"} icon={false}
|
||||||
|
severity={props.color}>
|
||||||
|
<Box>
|
||||||
{!isNaN(props.count) ?
|
{!isNaN(props.count) ?
|
||||||
<>
|
<>
|
||||||
<h3>{props.count}</h3>
|
<h2>{props.count}</h2>
|
||||||
<p>{props.text}</p>
|
<p>{props.text}</p>
|
||||||
</> : <CircularProgress variant={"determinate"} />
|
</> : <CircularProgress variant={"determinate"} />
|
||||||
}
|
}
|
||||||
</div>
|
</Box>
|
||||||
<div className={"icon"}>
|
<Box>{props.icon}</Box>
|
||||||
{props.icon}
|
<Box>
|
||||||
</div>
|
<Link to={props.link}>
|
||||||
<Link to={props.link} className={"small-box-footer text-right p-1"}>
|
|
||||||
More info <ArrowCircleRight />
|
More info <ArrowCircleRight />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</StyledStatBox>
|
||||||
|
|
||||||
const StatusLine = (props) => {
|
const StatusLine = (props) => {
|
||||||
const {enabled, text, ...other} = props;
|
const {enabled, text, ...other} = props;
|
||||||
@ -84,46 +123,39 @@ export default function Overview(props) {
|
|||||||
loadAvg = loadAvg.map(v => sprintf("%.1f", v)).join(", ");
|
loadAvg = loadAvg.map(v => sprintf("%.1f", v)).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <ViewContent title={L("admin.dashboard")} path={[
|
||||||
<div className={"content-header"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>
|
||||||
<div className={"container-fluid"}>
|
]}>
|
||||||
<div className={"row mb-2"}>
|
<Grid container spacing={2}>
|
||||||
<div className={"col-sm-6"}>
|
<Grid item xs={3}>
|
||||||
<h1 className={"m-0 text-dark"}>{L("admin.dashboard")}</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">{L("admin.dashboard")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<section className={"content"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
<div className={"row"}>
|
|
||||||
<StatBox color={"info"} count={stats?.userCount}
|
<StatBox color={"info"} count={stats?.userCount}
|
||||||
text={L("admin.users_registered")}
|
text={L("admin.users_registered")}
|
||||||
icon={<People />}
|
icon={<People/>}
|
||||||
link={"/admin/users"} />
|
link={"/admin/users"}/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={3}>
|
||||||
<StatBox color={"success"} count={stats?.groupCount}
|
<StatBox color={"success"} count={stats?.groupCount}
|
||||||
text={L("admin.available_groups")}
|
text={L("admin.available_groups")}
|
||||||
icon={<Groups />}
|
icon={<Groups/>}
|
||||||
link={"/admin/groups"} />
|
link={"/admin/groups"}/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={3}>
|
||||||
<StatBox color={"warning"} count={stats?.pageCount}
|
<StatBox color={"warning"} count={stats?.pageCount}
|
||||||
text={L("admin.routes_defined")}
|
text={L("admin.routes_defined")}
|
||||||
icon={<LibraryBooks />}
|
icon={<LibraryBooks/>}
|
||||||
link={"/admin/routes"} />
|
link={"/admin/routes"}/>
|
||||||
<StatBox color={"danger"} count={stats?.errorCount}
|
</Grid>
|
||||||
|
<Grid item xs={3}>
|
||||||
|
<StatBox color={"error"} count={stats?.errorCount}
|
||||||
text={L("admin.error_count")}
|
text={L("admin.error_count")}
|
||||||
icon={<BugReport />}
|
icon={<BugReport />}
|
||||||
link={"/admin/logs"} />
|
link={"/admin/logs"}/>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
<Box m={2} p={2} component={Paper}>
|
<Box m={2} p={2} component={Paper}>
|
||||||
<h4>Server Stats</h4><hr />
|
<h4>Server Stats</h4>
|
||||||
{stats === null ? <CircularProgress /> :
|
<Divider />
|
||||||
|
{stats === null ? <CircularProgress/> :
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -174,6 +206,5 @@ export default function Overview(props) {
|
|||||||
</Table>
|
</Table>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
</section>
|
</ViewContent>
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export default function ChangePasswordBox(props) {
|
|||||||
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</SpacedFormGroup>
|
</SpacedFormGroup>
|
||||||
<Box className={"w-50"}>
|
<Box sx={{width: "30%", minWidth: 300}}>
|
||||||
<PasswordStrength password={changePassword.new} minLength={6} />
|
<PasswordStrength password={changePassword.new} minLength={6} />
|
||||||
</Box>
|
</Box>
|
||||||
</CollapseBox>
|
</CollapseBox>
|
||||||
|
@ -18,6 +18,7 @@ import ChangePasswordBox from "./change-password-box";
|
|||||||
import GpgBox from "./gpg-box";
|
import GpgBox from "./gpg-box";
|
||||||
import MultiFactorBox from "./mfa-box";
|
import MultiFactorBox from "./mfa-box";
|
||||||
import EditProfilePicture from "./edit-picture";
|
import EditProfilePicture from "./edit-picture";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
|
||||||
export default function ProfileView(props) {
|
export default function ProfileView(props) {
|
||||||
|
|
||||||
@ -78,24 +79,10 @@ export default function ProfileView(props) {
|
|||||||
}, [profile, changePassword, api, showDialog, isSaving]);
|
}, [profile, changePassword, api, showDialog, isSaving]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"content-header"}>
|
<ViewContent title={L("account.edit_profile")} path={[
|
||||||
<div className={"container-fluid"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"row mb-2"}>
|
<span key={"profile"}>{L("account.profile")}</span>
|
||||||
<div className={"col-sm-6"}>
|
]}>
|
||||||
<h1 className={"m-0 text-dark"}>
|
|
||||||
{L("account.edit_profile")}
|
|
||||||
</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">{L("account.profile")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Box>
|
|
||||||
<Box display={"grid"} gridTemplateColumns={"300px auto"}>
|
<Box display={"grid"} gridTemplateColumns={"300px auto"}>
|
||||||
<EditProfilePicture api={api} showDialog={showDialog} setProfile={setProfile}
|
<EditProfilePicture api={api} showDialog={showDialog} setProfile={setProfile}
|
||||||
profile={profile} setDialogData={setDialogData} />
|
profile={profile} setDialogData={setDialogData} />
|
||||||
@ -154,8 +141,7 @@ export default function ProfileView(props) {
|
|||||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</ViewContent>
|
||||||
|
|
||||||
<Dialog show={dialogData.show}
|
<Dialog show={dialogData.show}
|
||||||
title={dialogData.title}
|
title={dialogData.title}
|
||||||
message={dialogData.message}
|
message={dialogData.message}
|
||||||
|
@ -11,6 +11,7 @@ import * as React from "react";
|
|||||||
import RouteForm from "./route-form";
|
import RouteForm from "./route-form";
|
||||||
import {KeyboardArrowLeft, Save} from "@mui/icons-material";
|
import {KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||||
import ButtonBar from "../../elements/button-bar";
|
import ButtonBar from "../../elements/button-bar";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
|
||||||
const MonoSpaceTextField = styled(TextField)((props) => ({
|
const MonoSpaceTextField = styled(TextField)((props) => ({
|
||||||
"& input": {
|
"& input": {
|
||||||
@ -113,19 +114,12 @@ export default function RouteEditView(props) {
|
|||||||
return <CircularProgress/>
|
return <CircularProgress/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={"content-header"}>
|
return <ViewContent title={L(isNewRoute ? "routes.create_route_title" : "routes.edit_route_title")}
|
||||||
<div className={"container-fluid"}>
|
path={[
|
||||||
<ol className={"breadcrumb"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
<Link key={"routes"} to={"/admin/routes"}>{L("routes.title")}</Link>,
|
||||||
<li className="breadcrumb-item active"><Link to={"/admin/routes"}>{L("routes.title")}</Link></li>
|
<span key={"action"}>{isNewRoute ? L("general.new") : L("general.edit")}</span>,
|
||||||
<li className="breadcrumb-item active">{isNewRoute ? L("general.new") : L("general.edit")}</li>
|
]}>
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
<h3>{L(isNewRoute ? "routes.create_route_title" : "routes.edit_route_title")}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<RouteForm route={route} setRoute={setRoute} />
|
<RouteForm route={route} setRoute={setRoute} />
|
||||||
<ButtonBar mt={2}>
|
<ButtonBar mt={2}>
|
||||||
<Button startIcon={<KeyboardArrowLeft />}
|
<Button startIcon={<KeyboardArrowLeft />}
|
||||||
@ -150,5 +144,5 @@ export default function RouteEditView(props) {
|
|||||||
Match: {JSON.stringify(routeTestResult)}
|
Match: {JSON.stringify(routeTestResult)}
|
||||||
</pre>
|
</pre>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</ViewContent>
|
||||||
}
|
}
|
@ -9,12 +9,15 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Button,
|
Button,
|
||||||
IconButton, Checkbox
|
IconButton, Checkbox, Box
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
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 "@mui/icons-material";
|
import {Add, Cached, Delete, Edit, Refresh} from "@mui/icons-material";
|
||||||
import Dialog from "shared/elements/dialog";
|
import Dialog from "shared/elements/dialog";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
import ButtonBar from "../../elements/button-bar";
|
||||||
|
import TableBodyStriped from "shared/elements/table-body-striped";
|
||||||
|
|
||||||
const RouteTableRow = styled(TableRow)((props) => ({
|
const RouteTableRow = styled(TableRow)((props) => ({
|
||||||
"& td": {
|
"& td": {
|
||||||
@ -22,7 +25,6 @@ const RouteTableRow = styled(TableRow)((props) => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
export default function RouteListView(props) {
|
export default function RouteListView(props) {
|
||||||
|
|
||||||
// meta
|
// meta
|
||||||
@ -118,44 +120,28 @@ export default function RouteListView(props) {
|
|||||||
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
|
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={"content-header"}>
|
<ViewContent title={L("routes.title")} path={[
|
||||||
<div className={"container-fluid"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"row mb-2"}>
|
<span key={"routes"}>{L("routes.title")}</span>
|
||||||
<div className={"col-sm-6"}>
|
]}>
|
||||||
<h1 className={"m-0 text-dark"}>{L("routes.title")}</h1>
|
<ButtonBar mb={1}>
|
||||||
</div>
|
<Button variant={"outlined"} color={"primary"} size={"small"}
|
||||||
<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">{L("routes.title")}</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"} size={"small"}
|
|
||||||
startIcon={<Refresh />} onClick={() => onFetchRoutes(true)}>
|
startIcon={<Refresh />} onClick={() => onFetchRoutes(true)}>
|
||||||
{L("general.reload")}
|
{L("general.reload")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={"outlined"} className={"m-1"} startIcon={<Add />} size={"small"}
|
<Button variant={"outlined"} startIcon={<Add />} size={"small"}
|
||||||
disabled={!props.api.hasPermission("routes/add")}
|
disabled={!props.api.hasPermission("routes/add")}
|
||||||
onClick={() => navigate("/admin/routes/new")} >
|
onClick={() => navigate("/admin/routes/new")} >
|
||||||
{L("general.add")}
|
{L("general.add")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={"outlined"} className={"m-1"} startIcon={<Cached />} size={"small"}
|
<Button variant={"outlined"} startIcon={<Cached />} size={"small"}
|
||||||
disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache}
|
disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache}
|
||||||
onClick={onRegenerateCache} >
|
onClick={onRegenerateCache} >
|
||||||
{isGeneratingCache ? L("routes.regenerating_cache") + "…" : L("routes.regenerate_cache")}
|
{isGeneratingCache ? L("routes.regenerating_cache") + "…" : L("routes.regenerate_cache")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ButtonBar>
|
||||||
</div>
|
<TableContainer component={Paper} sx={{overflowX: "initial"}}>
|
||||||
</div>
|
<Table stickyHeader size={"small"}>
|
||||||
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
|
||||||
<Table stickyHeader size={"small"} className={"table-striped"}>
|
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{L("general.id")}</TableCell>
|
<TableCell>{L("general.id")}</TableCell>
|
||||||
@ -168,7 +154,7 @@ export default function RouteListView(props) {
|
|||||||
<TableCell align={"center"}>{L("general.controls")}</TableCell>
|
<TableCell align={"center"}>{L("general.controls")}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBodyStriped>
|
||||||
{Object.entries(routes).map(([id, route]) =>
|
{Object.entries(routes).map(([id, route]) =>
|
||||||
<RouteTableRow key={"route-" + id}>
|
<RouteTableRow key={"route-" + id}>
|
||||||
<TableCell>{route.id}</TableCell>
|
<TableCell>{route.id}</TableCell>
|
||||||
@ -208,11 +194,15 @@ export default function RouteListView(props) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</RouteTableRow>
|
</RouteTableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBodyStriped>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
</ViewContent>
|
||||||
<Dialog show={dialogData.open}
|
<Dialog show={dialogData.open}
|
||||||
onClose={() => { setDialogData({open: false}); dialogData.onClose && dialogData.onClose() }}
|
onClose={() => {
|
||||||
|
setDialogData({open: false});
|
||||||
|
dialogData.onClose && dialogData.onClose()
|
||||||
|
}}
|
||||||
title={dialogData.title}
|
title={dialogData.title}
|
||||||
message={dialogData.message}
|
message={dialogData.message}
|
||||||
onOption={dialogData.onOption}
|
onOption={dialogData.onOption}
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
Delete,
|
Delete, DownloadDone,
|
||||||
LibraryBooks,
|
LibraryBooks,
|
||||||
Mail,
|
Mail,
|
||||||
RestartAlt,
|
RestartAlt,
|
||||||
@ -33,6 +33,7 @@ import SettingsNumberInput from "./input-number";
|
|||||||
import SettingsPasswordInput from "./input-password";
|
import SettingsPasswordInput from "./input-password";
|
||||||
import SettingsTextInput from "./input-text";
|
import SettingsTextInput from "./input-text";
|
||||||
import SettingsSelection from "./input-selection";
|
import SettingsSelection from "./input-selection";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
|
||||||
export default function SettingsView(props) {
|
export default function SettingsView(props) {
|
||||||
|
|
||||||
@ -197,6 +198,16 @@ export default function SettingsView(props) {
|
|||||||
}
|
}
|
||||||
}, [api, showDialog, testMailAddress, isSending]);
|
}, [api, showDialog, testMailAddress, isSending]);
|
||||||
|
|
||||||
|
const onTestRedis = useCallback(data => {
|
||||||
|
api.testRedis().then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
showDialog(data.msg, L("settings.redis_test_error"));
|
||||||
|
} else {
|
||||||
|
showDialog(L("settings.redis_test_success"), L("general.success"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [api, showDialog]);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
setFetchSettings(true);
|
setFetchSettings(true);
|
||||||
setNewKey("");
|
setNewKey("");
|
||||||
@ -266,7 +277,8 @@ export default function SettingsView(props) {
|
|||||||
<Button startIcon={isSending ? <CircularProgress size={14} /> : <Send />}
|
<Button startIcon={isSending ? <CircularProgress size={14} /> : <Send />}
|
||||||
variant={"outlined"} onClick={onSendTestMail}
|
variant={"outlined"} onClick={onSendTestMail}
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
disabled={!settings.mail_enabled || isSending || !api.hasPermission("mail/test")}>
|
disabled={!settings.mail_enabled || isSending || !api.hasPermission("mail/test") || hasChanged}
|
||||||
|
title={hasChanged ? L("general.unsaved_changes") : ""}>
|
||||||
{isSending ? L("general.sending") + "…" : L("general.send")}
|
{isSending ? L("general.sending") + "…" : L("general.send")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -300,6 +312,12 @@ export default function SettingsView(props) {
|
|||||||
renderTextInput("redis_host", !settings.rate_limiting_enabled),
|
renderTextInput("redis_host", !settings.rate_limiting_enabled),
|
||||||
renderNumberInput("redis_port", 1, 65535, !settings.rate_limiting_enabled),
|
renderNumberInput("redis_port", 1, 65535, !settings.rate_limiting_enabled),
|
||||||
renderPasswordInput("redis_password", !settings.rate_limiting_enabled),
|
renderPasswordInput("redis_password", !settings.rate_limiting_enabled),
|
||||||
|
<Button startIcon={<DownloadDone />}
|
||||||
|
variant={"outlined"} onClick={onTestRedis}
|
||||||
|
disabled={!settings.rate_limiting_enabled || !api.hasPermission("testRedis") || hasChanged}
|
||||||
|
title={hasChanged ? L("general.unsaved_changes") : ""}>
|
||||||
|
{L("settings.redis_test")}
|
||||||
|
</Button>
|
||||||
];
|
];
|
||||||
} else if (selectedTab === "uncategorized") {
|
} else if (selectedTab === "uncategorized") {
|
||||||
return <TableContainer component={Paper}>
|
return <TableContainer component={Paper}>
|
||||||
@ -357,23 +375,10 @@ export default function SettingsView(props) {
|
|||||||
return <CircularProgress />
|
return <CircularProgress />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <ViewContent title={L("settings.title")} path={[
|
||||||
<div className={"content-header"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"container-fluid"}>
|
<span key={"settings"}>{L("settings.title")}</span>,
|
||||||
<div className={"row mb-2"}>
|
]}>
|
||||||
<div className={"col-sm-6"}>
|
|
||||||
<h1 className={"m-0 text-dark"}>{L("settings.title")}</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">{L("settings.title")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)} component={Paper}>
|
<Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)} component={Paper}>
|
||||||
<Tab value={"general"} label={L("settings.general")}
|
<Tab value={"general"} label={L("settings.general")}
|
||||||
icon={<SettingsApplications />} iconPosition={"start"} />
|
icon={<SettingsApplications />} iconPosition={"start"} />
|
||||||
@ -407,7 +412,5 @@ export default function SettingsView(props) {
|
|||||||
{L("general.reset")}
|
{L("general.reset")}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
</div>
|
</ViewContent>
|
||||||
</>
|
|
||||||
|
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ import {useCallback, useContext, useEffect, useState} from "react";
|
|||||||
import {CircularProgress} from "@mui/material";
|
import {CircularProgress} from "@mui/material";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
|
||||||
export default function UserEditView(props) {
|
export default function UserEditView(props) {
|
||||||
|
|
||||||
@ -56,34 +56,11 @@ export default function UserEditView(props) {
|
|||||||
return <CircularProgress />
|
return <CircularProgress />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={"content-header"}>
|
return <ViewContent title={L(isNewUser ? "account.new_user" : "account.edit_user")} path={[
|
||||||
<div className={"container-fluid"}>
|
<Link key={"dashboard"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<ol className={"breadcrumb"}>
|
<Link key={"users"} to={"/admin/users"}>User</Link>,
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
<span key={"action"}>{isNewUser ? "New" : "Edit"}</span>
|
||||||
<li className="breadcrumb-item active"><Link to={"/admin/users"}>User</Link></li>
|
]}>
|
||||||
<li className="breadcrumb-item active">{ isNewUser ? "New" : "Edit" }</li>
|
|
||||||
</ol>
|
</ViewContent>
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
<h3>{L(isNewUser ? "Create new User" : "Edit User")}</h3>
|
|
||||||
<div className={"col-sm-12 col-lg-6"}>
|
|
||||||
<div className={"row"}>
|
|
||||||
<div className={"col-sm-6 form-group"}>
|
|
||||||
<label htmlFor={"username"}>{L("account.username")}</label>
|
|
||||||
<input type={"text"} className={"form-control"} placeholder={L("account.username")}
|
|
||||||
name={"username"} id={"username"} maxLength={32} value={user.name}
|
|
||||||
onChange={(e) => setUser({...user, name: e.target.value})}/>
|
|
||||||
</div>
|
|
||||||
<div className={"col-sm-6 form-group"}>
|
|
||||||
<label htmlFor={"fullName"}>{L("account.full_name")}</label>
|
|
||||||
<input type={"text"} className={"form-control"} placeholder={L("account.full_name")}
|
|
||||||
name={"fullName"} id={"fullName"} maxLength={32} value={user.fullName}
|
|
||||||
onChange={(e) => setUser({...user, fullName: e.target.value})}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ import {
|
|||||||
import {Chip} from "@mui/material";
|
import {Chip} from "@mui/material";
|
||||||
import {Edit, Add} from "@mui/icons-material";
|
import {Edit, Add} from "@mui/icons-material";
|
||||||
import usePagination from "shared/hooks/pagination";
|
import usePagination from "shared/hooks/pagination";
|
||||||
|
import ViewContent from "../../elements/view-content";
|
||||||
|
|
||||||
export default function UserListView(props) {
|
export default function UserListView(props) {
|
||||||
|
|
||||||
@ -69,33 +69,17 @@ export default function UserListView(props) {
|
|||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <>
|
return <ViewContent title={L("account.users")} path={[
|
||||||
<div className={"content-header"}>
|
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||||
<div className={"container-fluid"}>
|
<span key={"users"}>{L("account.users")}</span>
|
||||||
<div className={"row mb-2"}>
|
]}>
|
||||||
<div className={"col-sm-6"}>
|
|
||||||
<h1 className={"m-0 text-dark"}>{L("account.users")}</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">{L("account.users")}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={users}
|
data={users}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
defaultSortOrder={"asc"}
|
defaultSortOrder={"asc"}
|
||||||
defaultSortColumn={0}
|
defaultSortColumn={0}
|
||||||
className={"table table-striped"}
|
|
||||||
fetchData={onFetchUsers}
|
fetchData={onFetchUsers}
|
||||||
placeholder={"No users to display"}
|
placeholder={L("account.user_list_placeholder")}
|
||||||
columns={columnDefinitions}
|
columns={columnDefinitions}
|
||||||
buttons={[{
|
buttons={[{
|
||||||
key: "btn-create",
|
key: "btn-create",
|
||||||
@ -105,7 +89,5 @@ export default function UserListView(props) {
|
|||||||
disabled: !api.hasPermission("user/create") && !api.hasPermission("user/invite"),
|
disabled: !api.hasPermission("user/create") && !api.hasPermission("user/invite"),
|
||||||
onClick: () => navigate("/admin/user/new")
|
onClick: () => navigate("/admin/user/new")
|
||||||
}]}/>
|
}]}/>
|
||||||
</div>
|
</ViewContent>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
}
|
@ -22,8 +22,8 @@
|
|||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.20.5",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.19.0",
|
"@babel/plugin-transform-react-jsx": "^7.19.0",
|
||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "^9.0.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
|
||||||
"customize-cra": "^1.0.0",
|
"customize-cra": "^1.0.0",
|
||||||
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"parcel": "^2.8.0",
|
"parcel": "^2.8.0",
|
||||||
"react-app-rewired": "^2.2.1",
|
"react-app-rewired": "^2.2.1",
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1"
|
||||||
@ -36,6 +36,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/base": "^5.0.0-beta.40",
|
||||||
"@mui/icons-material": "^5.11.0",
|
"@mui/icons-material": "^5.11.0",
|
||||||
"@mui/lab": "^5.0.0-alpha.170",
|
"@mui/lab": "^5.0.0-alpha.170",
|
||||||
"@mui/material": "^5.15.14",
|
"@mui/material": "^5.15.14",
|
||||||
|
@ -425,4 +425,9 @@ export default class API {
|
|||||||
since: since, severity: severity, query: query
|
since: since, severity: severity, query: query
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Redis **/
|
||||||
|
async testRedis() {
|
||||||
|
return this.apiCall("testRedis");
|
||||||
|
}
|
||||||
};
|
};
|
@ -6,6 +6,7 @@ import {Box, Button, Select, TextField, Table, TableBody, TableCell, TableHead,
|
|||||||
import {formatDate, formatDateTime} from "../util";
|
import {formatDate, formatDateTime} from "../util";
|
||||||
import {isNumber} from "chart.js/helpers";
|
import {isNumber} from "chart.js/helpers";
|
||||||
import {ArrowUpward, ArrowDownward, Refresh} from "@mui/icons-material";
|
import {ArrowUpward, ArrowDownward, Refresh} from "@mui/icons-material";
|
||||||
|
import TableBodyStriped from "./table-body-striped";
|
||||||
|
|
||||||
|
|
||||||
export function DataTable(props) {
|
export function DataTable(props) {
|
||||||
@ -135,9 +136,9 @@ export function DataTable(props) {
|
|||||||
{ headerRow }
|
{ headerRow }
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBodyStriped>
|
||||||
{ rows }
|
{ rows }
|
||||||
</TableBody>
|
</TableBodyStriped>
|
||||||
</Table>
|
</Table>
|
||||||
{pagination && pagination.renderPagination(L, numRows)}
|
{pagination && pagination.renderPagination(L, numRows)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Icon(props) {
|
|
||||||
|
|
||||||
let classes = props.className || [];
|
|
||||||
classes = Array.isArray(classes) ? classes : classes.toString().split(" ");
|
|
||||||
let type = props.type || "fas";
|
|
||||||
let icon = props.icon;
|
|
||||||
|
|
||||||
classes.push(type);
|
|
||||||
classes.push("fa-" + icon);
|
|
||||||
|
|
||||||
if (icon === "spinner" || icon === "circle-notch") {
|
|
||||||
classes.push("fa-spin");
|
|
||||||
}
|
|
||||||
|
|
||||||
let newProps = {...props, className: classes.join(" ") };
|
|
||||||
delete newProps["type"];
|
|
||||||
delete newProps["icon"];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<i {...newProps} />
|
|
||||||
);
|
|
||||||
}
|
|
12
react/shared/elements/table-body-striped.jsx
Normal file
12
react/shared/elements/table-body-striped.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {styled, TableBody} from "@mui/material";
|
||||||
|
|
||||||
|
const TableBodyStriped = styled(TableBody)(({ theme }) => ({
|
||||||
|
'& tr:nth-of-type(odd)': {
|
||||||
|
backgroundColor: theme.palette.grey[0],
|
||||||
|
},
|
||||||
|
'& tr:nth-of-type(even)': {
|
||||||
|
backgroundColor: theme.palette.grey[100],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default TableBodyStriped;
|
Loading…
Reference in New Issue
Block a user