localization, context, react stuff
This commit is contained in:
parent
a84a51018c
commit
cb75e22811
@ -25,7 +25,6 @@ class Account extends TemplateDocument {
|
||||
protected function loadParameters() {
|
||||
$settings = $this->getSettings();
|
||||
$templateName = $this->getTemplateName();
|
||||
$language = $this->getContext()->getLanguage();
|
||||
$this->parameters["view"] = ["success" => true];
|
||||
switch ($templateName) {
|
||||
|
||||
|
@ -14,5 +14,6 @@ class Admin extends TemplateDocument {
|
||||
$this->searchable = false;
|
||||
parent::__construct($router, $template, $params);
|
||||
$this->enableCSP();
|
||||
$this->addCSPWhitelist("/js/admin-panel/");
|
||||
}
|
||||
}
|
11
Core/Localization/de_DE/admin.php
Normal file
11
Core/Localization/de_DE/admin.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"dashboard" => "Dashboard",
|
||||
"visitor_statistics" => "Besucherstatistiken",
|
||||
"user_groups" => "Benutzer & Gruppen",
|
||||
"page_routes" => "Seiten & Routen",
|
||||
"settings" => "Einstellungen",
|
||||
"logs" => "Logs",
|
||||
"help" => "Hilfe",
|
||||
];
|
@ -9,4 +9,5 @@ return [
|
||||
"submit" => "Absenden",
|
||||
"language" => "Sprache",
|
||||
"loading" => "Laden",
|
||||
"logout" => "Ausloggen",
|
||||
];
|
11
Core/Localization/en_US/admin.php
Normal file
11
Core/Localization/en_US/admin.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"dashboard" => "Dashboard",
|
||||
"visitor_statistics" => "Visitor Statistics",
|
||||
"user_groups" => "User & Groups",
|
||||
"page_routes" => "Pages & Routes",
|
||||
"settings" => "Settings",
|
||||
"logs" => "Logs",
|
||||
"help" => "Help",
|
||||
];
|
@ -9,4 +9,5 @@ return [
|
||||
"submit" => "Submit",
|
||||
"language" => "Language",
|
||||
"loading" => "Loading",
|
||||
"logout" => "Logout",
|
||||
];
|
@ -7,7 +7,7 @@
|
||||
|
||||
{% block body %}
|
||||
<noscript>You need Javascript enabled to run this app</noscript>
|
||||
<div class="wrapper" type="module" id="admin-panel"></div>
|
||||
<div type="module" id="admin-panel"></div>
|
||||
<script src="/js/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
<link rel="stylesheet" href="/js/admin-panel/index.css" nonce="{{ site.csp.nonce }}"></link>
|
||||
{% endblock %}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
js/admin-panel/overview.01501b20.js
Normal file
2
js/admin-panel/overview.01501b20.js
Normal file
@ -0,0 +1,2 @@
|
||||
!function(){function e(e,t,n,r){Object.defineProperty(e,t,{get:n,set:r,enumerable:!0,configurable:!0})}var t=("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).parcelRequireeec4;t.register("aNQgM",(function(n,r){var o;o=n.exports,Object.defineProperty(o,"__esModule",{value:!0,configurable:!0}),e(n.exports,"default",(function(){return c}));var a=t("lBpE3");t("6cds3");var s=t("1dF7x"),i=t("2eEzJ");function c(e){for(var t=new Date,n=(0,i.default)(t),r=["#ff4444","#ffbb33","#00C851","#33b5e5"];r.length<n;)r=r.concat(r);var o=new Array(n).fill(0);Array.from(Array(n),(function(e,t){return t+1})),format(t,"MMMM");return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("div",{className:"content-header",children:(0,a.jsx)("div",{className:"container-fluid",children:(0,a.jsxs)("div",{className:"row mb-2",children:[(0,a.jsx)("div",{className:"col-sm-6",children:(0,a.jsx)("h1",{className:"m-0 text-dark",children:"Dashboard"})}),(0,a.jsx)("div",{className:"col-sm-6",children:(0,a.jsxs)("ol",{className:"breadcrumb float-sm-right",children:[(0,a.jsx)("li",{className:"breadcrumb-item",children:(0,a.jsx)(s.Link,{to:"/admin/dashboard",children:"Home"})}),(0,a.jsx)("li",{className:"breadcrumb-item active",children:"Dashboard"})]})})]})})}),(0,a.jsx)("section",{className:"content"})]})}})),t.register("2eEzJ",(function(n,r){e(n.exports,"default",(function(){return s}));var o=t("18SRp"),a=t("3QBsJ");function s(e){(0,a.default)(1,arguments);var t=(0,o.default)(e),n=t.getFullYear(),r=t.getMonth(),s=new Date(0);return s.setFullYear(n,r+1,0),s.setHours(0,0,0,0),s.getDate()}})),t.register("18SRp",(function(n,r){e(n.exports,"default",(function(){return s}));var o=t("3QBsJ");function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}function s(e){(0,o.default)(1,arguments);var t=Object.prototype.toString.call(e);return e instanceof Date||"object"===a(e)&&"[object Date]"===t?new Date(e.getTime()):"number"==typeof e||"[object Number]"===t?new Date(e):("string"!=typeof e&&"[object String]"!==t||"undefined"==typeof console||(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://github.com/date-fns/date-fns/blob/master/docs/upgradeGuide.md#string-arguments"),console.warn((new Error).stack)),new Date(NaN))}})),t.register("3QBsJ",(function(t,n){function r(e,t){if(t.length<e)throw new TypeError(e+" argument"+(e>1?"s":"")+" required, but only "+t.length+" present")}e(t.exports,"default",(function(){return r}))}))}();
|
||||
//# sourceMappingURL=overview.01501b20.js.map
|
1
js/admin-panel/overview.01501b20.js.map
Normal file
1
js/admin-panel/overview.01501b20.js.map
Normal file
File diff suppressed because one or more lines are too long
2
js/admin-panel/overview.e0e11fc2.js
Normal file
2
js/admin-panel/overview.e0e11fc2.js
Normal file
File diff suppressed because one or more lines are too long
1
js/admin-panel/overview.e0e11fc2.js.map
Normal file
1
js/admin-panel/overview.e0e11fc2.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -4,10 +4,22 @@ const {
|
||||
override,
|
||||
removeModuleScopePlugin,
|
||||
babelInclude,
|
||||
addWebpackModuleRule,
|
||||
} = require('customize-cra');
|
||||
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const addMiniCssExtractPlugin = config => {
|
||||
config.plugins.push(new MiniCssExtractPlugin());
|
||||
return config;
|
||||
}
|
||||
|
||||
module.exports = override(
|
||||
removeModuleScopePlugin(),
|
||||
addMiniCssExtractPlugin,
|
||||
addWebpackModuleRule({
|
||||
test: /\.css$/,
|
||||
use: [ MiniCssExtractPlugin.loader, 'css-loader' ]
|
||||
}),
|
||||
babelInclude([
|
||||
path.resolve(path.join(__dirname, 'src')),
|
||||
fs.realpathSync(path.join(__dirname, '../shared')),
|
||||
|
@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/fontawesome.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="admin-panel"></div>
|
||||
<div id="admin-panel"></div>
|
||||
</body>
|
||||
</html>
|
79
react/admin-panel/src/AdminDashboard.jsx
Normal file
79
react/admin-panel/src/AdminDashboard.jsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, {lazy, Suspense, useCallback, useState} from "react";
|
||||
import {BrowserRouter, Route, Routes} from "react-router-dom";
|
||||
import Header from "./elements/header";
|
||||
import Sidebar from "./elements/sidebar";
|
||||
import Dialog from "./elements/dialog";
|
||||
import Footer from "./elements/footer";
|
||||
import {useContext, useEffect} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
|
||||
// css
|
||||
import './res/adminlte.min.css';
|
||||
|
||||
// views
|
||||
const Overview = lazy(() => import('./views/overview'));
|
||||
|
||||
|
||||
export default function AdminDashboard(props) {
|
||||
|
||||
const api = props.api;
|
||||
const info = props.info;
|
||||
const [dialog, setDialog] = useState({show: false});
|
||||
|
||||
const {currentLocale, requestModules, translate: L} = useContext(LocaleContext);
|
||||
|
||||
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
|
||||
setDialog({ show: true, message: message, title: title, options: options, onOption: onOption });
|
||||
}, []);
|
||||
|
||||
const hideDialog = useCallback(() => {
|
||||
setDialog({show: false});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(api, ["general", "admin"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
alert(data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const controlObj = {
|
||||
...props,
|
||||
showDialog: showDialog,
|
||||
hideDialog: hideDialog
|
||||
};
|
||||
|
||||
return <BrowserRouter>
|
||||
<Header {...controlObj} />
|
||||
<Sidebar {...controlObj} />
|
||||
<div className={"wrapper"}>
|
||||
<div className={"content-wrapper p-2"}>
|
||||
<section className={"content"}>
|
||||
<Suspense fallback={<div>{L("general.loading")}... </div>}>
|
||||
<Routes>
|
||||
<Route path={"/admin/dashboard"} element={<Overview {...controlObj} />}/>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
{/*<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/edit/:userId"} render={(props) => {
|
||||
let newProps = {...props, ...this.controlObj};
|
||||
return <EditUser {...newProps} />
|
||||
}}/>
|
||||
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
|
||||
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
|
||||
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
|
||||
<Route path={"*"}><View404 /></Route>*/}
|
||||
<Dialog {...dialog}/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<Footer info={info} />
|
||||
</BrowserRouter>
|
||||
}
|
@ -1,102 +1,61 @@
|
||||
import React from 'react';
|
||||
import './res/adminlte.min.css';
|
||||
import './res/index.css';
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import API from "shared/api";
|
||||
import Icon from "shared/elements/icon";
|
||||
import {BrowserRouter, Routes} from "react-router-dom";
|
||||
import Dialog from "./elements/dialog";
|
||||
import Footer from "./elements/footer";
|
||||
import Header from "./elements/header";
|
||||
import Sidebar from "./elements/sidebar";
|
||||
import LoginForm from "./views/login";
|
||||
import {Alert} from "@material-ui/lab";
|
||||
import {Button} from "@material-ui/core";
|
||||
import { LocaleContext } from "shared/locale";
|
||||
import AdminDashboard from "./AdminDashboard";
|
||||
|
||||
const L = (key) => {
|
||||
return "<nope>";
|
||||
}
|
||||
export default function App() {
|
||||
|
||||
export default class AdminDashboard extends React.Component {
|
||||
const api = useMemo(() => new API(), []);
|
||||
const [user, setUser] = useState(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [info, setInfo] = useState({});
|
||||
const [error, setError] = useState(null);
|
||||
const {translate: L} = useContext(LocaleContext);
|
||||
|
||||
static contextType = LocaleContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = new API();
|
||||
this.state = {
|
||||
loaded: false,
|
||||
dialog: { onClose: () => this.hideDialog() },
|
||||
info: { },
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
}
|
||||
|
||||
showDialog(message, title, options=["Close"], onOption = null) {
|
||||
const props = { show: true, message: message, title: title, options: options, onOption: onOption };
|
||||
this.setState({ ...this.state, dialog: { ...this.state.dialog, ...props } });
|
||||
}
|
||||
|
||||
hideDialog() {
|
||||
this.setState({ ...this.state, dialog: { ...this.state.dialog, show: false } });
|
||||
}
|
||||
|
||||
onInit() {
|
||||
// return;
|
||||
this.setState({ ...this.state, loaded: false, error: null });
|
||||
this.api.getLanguageEntries("general").then(data => {
|
||||
const fetchUser = useCallback(() => {
|
||||
api.fetchUser().then(data => {
|
||||
if (data.success) {
|
||||
this.api.info().then(data => {
|
||||
setUser(data.user || null);
|
||||
setLoaded(true);
|
||||
} else {
|
||||
setError(data.msg);
|
||||
}
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const onInit = useCallback((force = false) => {
|
||||
if (loaded && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(false);
|
||||
setLoaded(false);
|
||||
api.getLanguageEntries("general").then(data => {
|
||||
if (data.success) {
|
||||
api.info().then(data => {
|
||||
if (data.success) {
|
||||
this.setState({...this.state, info: data.info })
|
||||
this.api.fetchUser().then(data => {
|
||||
if (data.success) {
|
||||
setInterval(this.onUpdate.bind(this), 60*1000);
|
||||
this.setState({...this.state, loaded: true});
|
||||
} else {
|
||||
this.setState({ ...this.state, error: data.msg })
|
||||
}
|
||||
});
|
||||
setInfo(data.info);
|
||||
fetchUser();
|
||||
} else {
|
||||
this.setState({ ...this.state, error: data.msg })
|
||||
setError(data.msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({ ...this.state, error: data.msg })
|
||||
setError(data.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [api, loaded, fetchUser]);
|
||||
|
||||
componentDidMount() {
|
||||
this.onInit();
|
||||
}
|
||||
useEffect(() => {
|
||||
onInit();
|
||||
}, []);
|
||||
|
||||
onLogin(username, password, rememberMe, callback) {
|
||||
this.setState({ ...this.state, error: "" });
|
||||
return this.api.login(username, password, rememberMe).then((res) => {
|
||||
if (res.success) {
|
||||
this.api.fetchUser().then(() => {
|
||||
this.setState({ ...this.state, user: res });
|
||||
callback(res);
|
||||
})
|
||||
} else {
|
||||
callback(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onLogout(callback) {
|
||||
this.api.logout().then(() => {
|
||||
this.api.loggedIn = false;
|
||||
this.setState({ ...this.state, user: { } })
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
onTotp2FA(code, callback) {
|
||||
/*
|
||||
const onTotp2FA = useCallback((code, callback) => {
|
||||
this.setState({ ...this.state, error: "" });
|
||||
return this.api.verifyTotp2FA(code).then((res) => {
|
||||
if (res.success) {
|
||||
@ -108,7 +67,7 @@ export default class AdminDashboard extends React.Component {
|
||||
callback(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [api]);
|
||||
|
||||
onKey2FA(credentialID, clientDataJson, authData, signature, callback) {
|
||||
this.setState({ ...this.state, error: "" });
|
||||
@ -124,62 +83,24 @@ export default class AdminDashboard extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
*/
|
||||
|
||||
if (!this.state.loaded) {
|
||||
if (this.state.error) {
|
||||
return <Alert severity={"error"} title={L("general.error_occurred")}>
|
||||
<div>{this.state.error}</div>
|
||||
<Button type={"button"} variant={"outlined"} onClick={() => this.onInit()}>
|
||||
Retry
|
||||
</Button>
|
||||
</Alert>
|
||||
} else {
|
||||
return <b>{L("general.loading")}… <Icon icon={"spinner"}/></b>
|
||||
}
|
||||
console.log(loaded, user, api.loggedIn);
|
||||
|
||||
if (!loaded) {
|
||||
if (error) {
|
||||
return <Alert severity={"error"} title={L("general.error_occurred")}>
|
||||
<div>{error}</div>
|
||||
<Button type={"button"} variant={"outlined"} onClick={() => onInit(true)}>
|
||||
Retry
|
||||
</Button>
|
||||
</Alert>
|
||||
} else {
|
||||
return <b>{L("general.loading")}… <Icon icon={"spinner"}/></b>
|
||||
}
|
||||
|
||||
this.controlObj = {
|
||||
showDialog: this.showDialog.bind(this),
|
||||
api: this.api,
|
||||
info: this.state.info,
|
||||
onLogout: this.onLogout.bind(this),
|
||||
onLogin: this.onLogin.bind(this),
|
||||
onTotp2FA: this.onTotp2FA.bind(this),
|
||||
onKey2FA: this.onKey2FA.bind(this),
|
||||
};
|
||||
|
||||
if (!this.api.loggedIn) {
|
||||
return <LoginForm {...this.controlObj}/>
|
||||
}
|
||||
|
||||
return <BrowserRouter>
|
||||
<Header {...this.controlObj} />
|
||||
<Sidebar {...this.controlObj} />
|
||||
<div className={"content-wrapper p-2"}>
|
||||
<section className={"content"}>
|
||||
<Routes>
|
||||
{/*<Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/user/edit/:userId"} render={(props) => {
|
||||
let newProps = {...props, ...this.controlObj};
|
||||
return <EditUser {...newProps} />
|
||||
}}/>
|
||||
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
|
||||
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
|
||||
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
|
||||
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
|
||||
<Route path={"*"}><View404 /></Route>*/}
|
||||
</Routes>
|
||||
<Dialog {...this.state.dialog}/>
|
||||
</section>
|
||||
</div>
|
||||
<Footer info={this.state.info} />
|
||||
</BrowserRouter>
|
||||
} else if (!user || !api.loggedIn) {
|
||||
return <LoginForm api={api} info={info} onLogin={fetchUser} />
|
||||
} else {
|
||||
return <AdminDashboard api={api} info={info} />
|
||||
}
|
||||
}
|
@ -4,8 +4,8 @@ export default function Footer(props) {
|
||||
|
||||
return (
|
||||
<footer className={"main-footer"}>
|
||||
Theme: <strong>Copyright © 2014-2019 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.0.3</strong>
|
||||
CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> {props.info.version}
|
||||
Theme: <strong>Copyright © 2014-2021 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.2.0</strong>
|
||||
Framework: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> {props.info.version}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
@ -29,7 +29,12 @@ export default function LanguageSelection(props) {
|
||||
let flags = [];
|
||||
if (languages === null) {
|
||||
api.getLanguages().then((res) => {
|
||||
setLanguages(res.languages);
|
||||
if (res.success) {
|
||||
setLanguages(res.languages);
|
||||
} else {
|
||||
setLanguages({});
|
||||
alert(res.msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (const language of Object.values(languages)) {
|
||||
|
@ -1,55 +1,51 @@
|
||||
import React from 'react';
|
||||
import React, {useCallback, useContext} from 'react';
|
||||
import {Link, NavLink} from "react-router-dom";
|
||||
import Icon from "shared/elements/icon";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
|
||||
export default function Sidebar(props) {
|
||||
|
||||
let parent = {
|
||||
showDialog: props.showDialog || function() {},
|
||||
api: props.api,
|
||||
};
|
||||
const api = props.api;
|
||||
const showDialog = props.showDialog;
|
||||
const {translate: L} = useContext(LocaleContext);
|
||||
|
||||
function onLogout() {
|
||||
parent.api.logout().then(obj => {
|
||||
const onLogout = useCallback(() => {
|
||||
api.logout().then(obj => {
|
||||
if (obj.success) {
|
||||
document.location = "/admin";
|
||||
} else {
|
||||
parent.showDialog("Error logging out: " + obj.msg, "Error logging out");
|
||||
showDialog("Error logging out: " + obj.msg, "Error logging out");
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [api, showDialog]);
|
||||
|
||||
const menuItems = {
|
||||
"dashboard": {
|
||||
"name": "Dashboard",
|
||||
"name": "admin.dashboard",
|
||||
"icon": "tachometer-alt"
|
||||
},
|
||||
"visitors": {
|
||||
"name": "Visitor Statistics",
|
||||
"name": "admin.visitor_statistics",
|
||||
"icon": "chart-bar",
|
||||
},
|
||||
"users": {
|
||||
"name": "Users & Groups",
|
||||
"name": "admin.user_groups",
|
||||
"icon": "users"
|
||||
},
|
||||
"pages": {
|
||||
"name": "Pages & Routes",
|
||||
"name": "admin.page_routes",
|
||||
"icon": "copy",
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
"name": "admin.settings",
|
||||
"icon": "tools"
|
||||
},
|
||||
"logs": {
|
||||
"name": "Logs & Notifications",
|
||||
"name": "admin.logs",
|
||||
"icon": "file-medical-alt"
|
||||
},
|
||||
"contact": {
|
||||
"name": "Contact Requests",
|
||||
"icon": "comments"
|
||||
},
|
||||
"help": {
|
||||
"name": "Help",
|
||||
"name": "admin.help",
|
||||
"icon": "question-circle"
|
||||
},
|
||||
};
|
||||
@ -61,8 +57,8 @@ export default function Sidebar(props) {
|
||||
|
||||
li.push(
|
||||
<li key={id} className={"nav-item"}>
|
||||
<NavLink to={"/admin/" + id} className={"nav-link"} activeClassName={"active"}>
|
||||
<Icon icon={obj.icon} className={"nav-icon"} /><p>{obj.name}{badge}</p>
|
||||
<NavLink to={"/admin/" + id} className={"nav-link"}>
|
||||
<Icon icon={obj.icon} className={"nav-icon"} /><p>{L(obj.name)}{badge}</p>
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
@ -71,7 +67,7 @@ export default function Sidebar(props) {
|
||||
li.push(<li className={"nav-item"} key={"logout"}>
|
||||
<a href={"#"} onClick={() => onLogout()} className={"nav-link"}>
|
||||
<Icon icon={"arrow-left"} className={"nav-icon"} />
|
||||
<p>Logout</p>
|
||||
<p>{L("general.logout")}</p>
|
||||
</a>
|
||||
</li>);
|
||||
|
||||
@ -96,7 +92,7 @@ export default function Sidebar(props) {
|
||||
<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">
|
||||
<a href="#" className="d-block">Logged in as: {parent.api.user.name}</a>
|
||||
<a href="#" className="d-block">Logged in as: {api.user.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={"mt-2"}>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import {createRoot} from "react-dom/client";
|
||||
import AdminDashboard from "./App";
|
||||
import App from "./App";
|
||||
import {LocaleProvider} from "shared/locale";
|
||||
|
||||
const root = createRoot(document.getElementById('admin-panel'));
|
||||
root.render(<LocaleProvider>
|
||||
<AdminDashboard />
|
||||
<App />
|
||||
</LocaleProvider>);
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,6 +0,0 @@
|
||||
.page-link { color: #222629; }
|
||||
.page-link:hover { color: black; }
|
||||
|
||||
.ReactCollapse--collapse {
|
||||
transition: height 500ms;
|
||||
}
|
@ -12,7 +12,6 @@ import {
|
||||
import {makeStyles} from '@material-ui/core/styles';
|
||||
import {Alert} from '@material-ui/lab';
|
||||
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {Navigate} from "react-router-dom";
|
||||
import ReplayIcon from '@material-ui/icons/Replay';
|
||||
import LanguageSelection from "../elements/language-selection";
|
||||
import {decodeText, encodeText, getParameter, removeParameter} from "shared/util";
|
||||
@ -85,10 +84,6 @@ export default function LoginForm(props) {
|
||||
|
||||
const {translate: L, currentLocale, requestModules} = useContext(LocaleContext);
|
||||
|
||||
const getNextUrl = () => {
|
||||
return getParameter("next") || "/admin";
|
||||
}
|
||||
|
||||
const onUpdateLocale = useCallback(() => {
|
||||
requestModules(api, ["general", "account"], currentLocale).then(data => {
|
||||
setLoaded(true);
|
||||
@ -107,17 +102,19 @@ export default function LoginForm(props) {
|
||||
setError("");
|
||||
setLoggingIn(true);
|
||||
removeParameter("success");
|
||||
props.onLogin(username, password, rememberMe, (res) => {
|
||||
api.login(username, password, rememberMe).then((res) => {
|
||||
set2FAState(0);
|
||||
setLoggingIn(false);
|
||||
setPassword("");
|
||||
if (!res.success) {
|
||||
setEmailConfirmed(res.emailConfirmed);
|
||||
setError(res.msg);
|
||||
} else {
|
||||
props.onLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isLoggingIn, password, props, rememberMe, username]);
|
||||
}, [api, isLoggingIn, password, props, rememberMe, username]);
|
||||
|
||||
const onSubmit2FA = useCallback(() => {
|
||||
setLoggingIn(true);
|
||||
@ -168,14 +165,7 @@ export default function LoginForm(props) {
|
||||
set2FAError(e.toString());
|
||||
});
|
||||
}
|
||||
}, [api.loggedIn, api.user, tfaState, props])
|
||||
|
||||
if (api.loggedIn) {
|
||||
if (!api.user["2fa"] || !api.user["2fa"].confirmed || api.user["2fa"].authenticated) {
|
||||
// Redirect by default takes only path names
|
||||
return <Navigate to={getNextUrl()}/>
|
||||
}
|
||||
}
|
||||
}, [api.loggedIn, api.user, tfaState, props]);
|
||||
|
||||
const createForm = () => {
|
||||
|
||||
|
119
react/admin-panel/src/views/overview.js
Normal file
119
react/admin-panel/src/views/overview.js
Normal file
@ -0,0 +1,119 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {format, getDaysInMonth} from "date-fns";
|
||||
|
||||
export default function Overview(props) {
|
||||
|
||||
const today = new Date();
|
||||
const numDays = getDaysInMonth(today);
|
||||
|
||||
let colors = [ '#ff4444', '#ffbb33', '#00C851', '#33b5e5' ];
|
||||
while (colors.length < numDays) {
|
||||
colors = colors.concat(colors);
|
||||
}
|
||||
|
||||
let data = new Array(numDays).fill(0);
|
||||
let visitorCount = 0;
|
||||
/*
|
||||
for (let date in this.state.visitors) {
|
||||
if (this.state.visitors.hasOwnProperty(date)) {
|
||||
let day = parseInt(date.split("/")[2]) - 1;
|
||||
if (day >= 0 && day < numDays) {
|
||||
let count = parseInt(this.state.visitors[date]);
|
||||
data[day] = count;
|
||||
visitorCount += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
let labels = Array.from(Array(numDays), (_, i) => i + 1);
|
||||
let chartOptions = {};
|
||||
let chartData = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Unique Visitors ' + format(today, "MMMM"),
|
||||
borderWidth: 1,
|
||||
data: data,
|
||||
backgroundColor: colors,
|
||||
}]
|
||||
};
|
||||
|
||||
/*
|
||||
let loadAvg = this.state.server.load_avg;
|
||||
if (Array.isArray(this.state.server.load_avg)) {
|
||||
loadAvg = this.state.server.load_avg.join(" ");
|
||||
}
|
||||
*/
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>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">Dashboard</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className={"content"}>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
|
||||
/*
|
||||
export default class Overview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
chartVisible : true,
|
||||
statusVisible : true,
|
||||
userCount: 0,
|
||||
notificationCount: 0,
|
||||
visitorsTotal: 0,
|
||||
visitors: { },
|
||||
server: { load_avg: ["Unknown"] },
|
||||
errors: []
|
||||
}
|
||||
}
|
||||
|
||||
removeError(i) {
|
||||
if (i >= 0 && i < this.state.errors.length) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.splice(i, 1);
|
||||
this.setState({...this.state, errors: errors});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parent.api.getStats().then((res) => {
|
||||
if(!res.success) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ message: res.msg, title: "Error fetching Stats" });
|
||||
this.setState({ ...this.state, errors: errors });
|
||||
} else {
|
||||
this.setState({
|
||||
...this.state,
|
||||
userCount: res.userCount,
|
||||
pageCount: res.pageCount,
|
||||
visitors: res.visitors,
|
||||
visitorsTotal: res.visitorsTotal,
|
||||
server: res.server
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
}
|
||||
}*/
|
@ -36,9 +36,13 @@
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"chart.js": "^4.0.1",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"mini-css-extract-plugin": "^2.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.0.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.3"
|
||||
},
|
||||
|
@ -53,7 +53,12 @@ export default class API {
|
||||
}
|
||||
|
||||
async logout() {
|
||||
return this.apiCall("user/logout");
|
||||
const res = await this.apiCall("user/logout");
|
||||
if (res.success) {
|
||||
this.loggedIn = false;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getUser(id) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import {createContext, useCallback, useState} from "react";
|
||||
|
||||
const LocaleContext = React.createContext(null);
|
||||
const LocaleContext = createContext(null);
|
||||
|
||||
function LocaleProvider(props) {
|
||||
|
||||
|
@ -3649,6 +3649,11 @@ char-regex@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e"
|
||||
integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==
|
||||
|
||||
chart.js@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.0.1.tgz#93d5d50ac222a5b3b6ac7488e82e1553ac031592"
|
||||
integrity sha512-5/8/9eBivwBZK81mKvmIwTb2Pmw4D/5h1RK9fBWZLLZ8mCJ+kfYNmV9rMrGoa5Hgy2/wVDBMLSUDudul2/9ihA==
|
||||
|
||||
check-types@^11.1.1:
|
||||
version "11.2.2"
|
||||
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.2.tgz#7afc0b6a860d686885062f2dba888ba5710335b4"
|
||||
@ -6885,7 +6890,7 @@ mimic-fn@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
mini-css-extract-plugin@^2.4.5:
|
||||
mini-css-extract-plugin@^2.4.5, mini-css-extract-plugin@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.1.tgz#ec924df783cff32ce6183fceb653028f70128643"
|
||||
integrity sha512-viOoaUFy+Z2w43VsGPbtfwFrr0tKwDctK9dUofG5MBViYhD1noGFUzzDIVw0KPwCGUP+c7zqLxm+acuQs7zLzw==
|
||||
@ -8165,6 +8170,16 @@ react-app-rewired@^2.2.1:
|
||||
dependencies:
|
||||
semver "^5.6.0"
|
||||
|
||||
react-chartjs-2@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz#7ef7dd57441903e8d34e1d06a1aead1095d0bca8"
|
||||
integrity sha512-u38C9OxynlNCBp+79grgXRs7DSJ9w8FuQ5/HO5FbYBbri8HSZW+9SWgjVshLkbXBfXnMGWakbHEtvN0nL2UG7Q==
|
||||
|
||||
react-collapse@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-collapse/-/react-collapse-5.1.1.tgz#a2fa08ef13f372141b02e6a7d49ef72427bcbc2b"
|
||||
integrity sha512-k6cd7csF1o9LBhQ4AGBIdxB60SUEUMQDAnL2z1YvYNr9KoKr+nDkhN6FK7uGaBd/rYrYfrMpzpmJEIeHRYogBw==
|
||||
|
||||
react-dev-utils@^12.0.1:
|
||||
version "12.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"
|
||||
|
Loading…
Reference in New Issue
Block a user