removed old admin panel
This commit is contained in:
parent
e7283310f0
commit
ae5210ec57
@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["react"],
|
||||
}
|
23
react/adminPanel.old/.gitignore
vendored
23
react/adminPanel.old/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1 +0,0 @@
|
||||
DENY FROM ALL
|
5
react/adminPanel.old/.idea/.gitignore
vendored
5
react/adminPanel.old/.idea/.gitignore
vendored
@ -1,5 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/adminPanel.iml" filepath="$PROJECT_DIR$/.idea/adminPanel.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
40134
react/adminPanel.old/package-lock.json
generated
40134
react/adminPanel.old/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"chart.js": "^2.9.3",
|
||||
"draft-js": "^0.11.6",
|
||||
"draftjs-to-html": "^0.9.1",
|
||||
"html-to-draftjs": "latest",
|
||||
"moment": "^2.26.0",
|
||||
"rc-color-picker": "^1.2.6",
|
||||
"react": "^16.13.1",
|
||||
"react-chartjs-2": "^2.9.0",
|
||||
"react-collapse": "^5.0.1",
|
||||
"react-datepicker": "^3.0.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-draft-wysiwyg": "^1.14.5",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-select": "^3.1.0",
|
||||
"react-tooltip": "^4.2.7",
|
||||
"sanitize-html": "^1.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --mode production && mv dist/main.js ../js/admin.min.js",
|
||||
"debug": "react-scripts start"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/preset-react": "^7.10.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/css/fontawesome.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
@ -1,25 +0,0 @@
|
||||
import Icon from "./icon";
|
||||
import React from "react";
|
||||
|
||||
export default function Alert(props) {
|
||||
|
||||
const onClose = props.onClose || null;
|
||||
const title = props.title || "Untitled Alert";
|
||||
const message = props.message || "Alert message";
|
||||
const type = props.type || "danger";
|
||||
|
||||
let icon = "ban";
|
||||
if (type === "warning") {
|
||||
icon = "exclamation-triangle";
|
||||
} else if(type === "success") {
|
||||
icon = "check";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"alert alert-" + type + " alert-dismissible"}>
|
||||
{onClose ? <button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> : null}
|
||||
<h5><Icon icon={icon} className={"icon"} /> {title}</h5>
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Dialog(props) {
|
||||
|
||||
const show = props.show;
|
||||
const classes = "modal fade" + (show ? " show" : "");
|
||||
const style = { paddingRight: "12px", display: (show ? "block" : "none") };
|
||||
const onClose = props.onClose || function() { };
|
||||
const onOption = props.onOption || function() { };
|
||||
const options = props.options || ["Close"];
|
||||
|
||||
let buttons = [];
|
||||
for (let name of options) {
|
||||
let type = "default";
|
||||
if (name === "Yes") type = "warning";
|
||||
else if(name === "No") type = "danger";
|
||||
|
||||
buttons.push(
|
||||
<button type="button" key={"button-" + name} className={"btn btn-" + type} data-dismiss={"modal"} onClick={() => { onClose(); onOption(name); }}>
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes} id="modal-default" style={style} aria-modal="true" onClick={() => onClose()}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">{props.title}</h4>
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close" onClick={() => onClose()}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>{props.message}</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{ buttons }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Footer() {
|
||||
|
||||
return (
|
||||
<footer className={"main-footer"}>
|
||||
Theme: <strong>Copyright © 2014-2019 <a href={"http://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> 1.2.6
|
||||
</footer>
|
||||
)
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
import * as React from "react";
|
||||
import Icon from "./icon";
|
||||
import {useState} from "react";
|
||||
import {getPeriodString} from "../global";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
export default function Header(props) {
|
||||
|
||||
const parent = {
|
||||
notifications: props.notifications || [ ],
|
||||
};
|
||||
|
||||
const [dropdownVisible, showDropdown] = useState(false);
|
||||
const mailIcon = <Icon icon={"envelope"} type={"fas"} />;
|
||||
|
||||
let notificationCount = parent.notifications.length;
|
||||
let notificationText = "No new notifications";
|
||||
|
||||
if(notificationCount === 1) {
|
||||
notificationText = "1 new notification";
|
||||
} else if(notificationCount > 1) {
|
||||
notificationText = notificationCount + " new notification";
|
||||
}
|
||||
|
||||
let notificationItems = [];
|
||||
for (let i = 0; i < parent.notifications.length; i++) {
|
||||
const notification = parent.notifications[i];
|
||||
const id = notification.id;
|
||||
const createdAt = getPeriodString(notification["created_at"]);
|
||||
notificationItems.push(
|
||||
<Link to={"/admin/logs?notification=" + id} className={"dropdown-item"} key={"notification-" + id}>
|
||||
{mailIcon}
|
||||
<span className={"ml-2"}>{notification.title}</span>
|
||||
<span className={"float-right text-muted text-sm"}>{createdAt}</span>
|
||||
</Link>);
|
||||
}
|
||||
|
||||
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"}>
|
||||
|
||||
{/* Notifications Dropdown Menu */}
|
||||
<li className={"nav-item dropdown"} onClick={() => showDropdown(!dropdownVisible)}>
|
||||
<a href={"#"} className={"nav-link"} data-toggle={"dropdown"}>
|
||||
<Icon icon={"bell"} type={"far"} />
|
||||
<span className={"badge badge-warning navbar-badge"} style={{display: (notificationCount > 0 ? "block" : "none")}}>
|
||||
{notificationCount}
|
||||
</span>
|
||||
</a>
|
||||
<div className={"dropdown-menu dropdown-menu-lg dropdown-menu-right " + (dropdownVisible ? " show" : "")}>
|
||||
<span className={"dropdown-item dropdown-header"}>
|
||||
{notificationText}
|
||||
</span>
|
||||
{notificationItems}
|
||||
<div className={"dropdown-divider"} />
|
||||
<Link to={"/admin/logs"} className={"dropdown-item dropdown-footer"}>See All Notifications</Link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import * as 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} />
|
||||
);
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
import React from 'react';
|
||||
import Icon from "./icon";
|
||||
import {Link, NavLink} from "react-router-dom";
|
||||
|
||||
export default function Sidebar(props) {
|
||||
|
||||
let parent = {
|
||||
showDialog: props.showDialog || function() {},
|
||||
api: props.api,
|
||||
notifications: props.notifications || [ ],
|
||||
contactRequests: props.contactRequests || [ ]
|
||||
};
|
||||
|
||||
function onLogout() {
|
||||
parent.api.logout().then(obj => {
|
||||
if (obj.success) {
|
||||
document.location = "/admin";
|
||||
} else {
|
||||
parent.showDialog("Error logging out: " + obj.msg, "Error logging out");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const menuItems = {
|
||||
"dashboard": {
|
||||
"name": "Dashboard",
|
||||
"icon": "tachometer-alt"
|
||||
},
|
||||
"visitors": {
|
||||
"name": "Visitor Statistics",
|
||||
"icon": "chart-bar",
|
||||
},
|
||||
"users": {
|
||||
"name": "Users & Groups",
|
||||
"icon": "users"
|
||||
},
|
||||
"pages": {
|
||||
"name": "Pages & Routes",
|
||||
"icon": "copy",
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
"icon": "tools"
|
||||
},
|
||||
"logs": {
|
||||
"name": "Logs & Notifications",
|
||||
"icon": "file-medical-alt"
|
||||
},
|
||||
"contact": {
|
||||
"name": "Contact Requests",
|
||||
"icon": "comments"
|
||||
},
|
||||
"help": {
|
||||
"name": "Help",
|
||||
"icon": "question-circle"
|
||||
},
|
||||
};
|
||||
|
||||
let numNotifications = parent.notifications.length;
|
||||
if (numNotifications > 0) {
|
||||
if (numNotifications > 9) numNotifications = "9+";
|
||||
menuItems["logs"]["badge"] = { type: "warning", value: numNotifications };
|
||||
}
|
||||
|
||||
console.log("sidebar", parent.contactRequests);
|
||||
let numUnreadContactMessages = 0;
|
||||
for (const contactRequest of parent.contactRequests) {
|
||||
numUnreadContactMessages += contactRequest.unread;
|
||||
}
|
||||
if (numUnreadContactMessages > 0) {
|
||||
menuItems["contact"]["badge"] = { type: "info", value: numUnreadContactMessages };
|
||||
}
|
||||
|
||||
let li = [];
|
||||
for (let id in menuItems) {
|
||||
let obj = menuItems[id];
|
||||
const badge = (obj.badge ? <span className={"right badge badge-" + obj.badge.type}>{obj.badge.value}</span> : <></>);
|
||||
|
||||
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>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
</a>
|
||||
</li>);
|
||||
|
||||
return (
|
||||
<aside className={"main-sidebar sidebar-dark-primary elevation-4"}>
|
||||
<Link href={"#"} className={"brand-link"} to={"/admin/dashboard"}>
|
||||
<img src={"/img/icons/logo.png"} alt={"Logo"} className={"brand-image img-circle elevation-3"} style={{opacity: ".8"}} />
|
||||
<span className={"brand-text font-weight-light ml-2"}>WebBase</span>
|
||||
</Link>
|
||||
|
||||
<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 */}
|
||||
<div className={"os-resize-observer-host"}>
|
||||
<div className={"os-resize-observer observed"} style={{left: "0px", right: "auto"}}/>
|
||||
</div>
|
||||
<div className={"os-size-auto-observer"} style={{height: "calc(100% + 1px)", float: "left"}}>
|
||||
<div className={"os-resize-observer observed"}/>
|
||||
</div>
|
||||
<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">
|
||||
<a href="#" className="d-block">Logged in as: {parent.api.user.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={"mt-2"}>
|
||||
<ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}>
|
||||
{li}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
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;
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './include/adminlte.min.css';
|
||||
import './include/index.css';
|
||||
import API from './api.js';
|
||||
import Header from './elements/header.js';
|
||||
import Sidebar from './elements/sidebar.js';
|
||||
import UserOverview from './views/users.js';
|
||||
import Overview from './views/overview.js'
|
||||
import CreateUser from "./views/adduser";
|
||||
import Icon from "./elements/icon";
|
||||
import Dialog from "./elements/dialog";
|
||||
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
|
||||
import View404 from "./views/404";
|
||||
import Logs from "./views/logs";
|
||||
import PageOverview from "./views/pages";
|
||||
import HelpPage from "./views/help";
|
||||
import Footer from "./elements/footer";
|
||||
import EditUser from "./views/edituser";
|
||||
import CreateGroup from "./views/addgroup";
|
||||
import Settings from "./views/settings";
|
||||
import PermissionSettings from "./views/permissions";
|
||||
import Visitors from "./views/visitors";
|
||||
import ContactRequestOverview from "./views/contact";
|
||||
|
||||
class AdminDashboard extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = new API();
|
||||
this.state = {
|
||||
loaded: false,
|
||||
dialog: { onClose: () => this.hideDialog() },
|
||||
notifications: [ ],
|
||||
contactRequests: [ ]
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
this.fetchNotifications();
|
||||
this.fetchContactRequests();
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
|
||||
fetchNotifications() {
|
||||
this.api.getNotifications().then((res) => {
|
||||
if (!res.success) {
|
||||
this.showDialog("Error fetching notifications: " + res.msg, "Error fetching notifications");
|
||||
} else {
|
||||
this.setState({...this.state, notifications: res.notifications });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchContactRequests() {
|
||||
this.api.fetchContactRequests().then((res) => {
|
||||
if (!res.success) {
|
||||
this.showDialog("Error fetching contact requests: " + res.msg, "Error fetching contact requests");
|
||||
} else {
|
||||
this.setState({...this.state, contactRequests: res.contactRequests });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.api.fetchUser().then(Success => {
|
||||
if (!Success) {
|
||||
document.location = "/admin";
|
||||
} else {
|
||||
this.fetchNotifications();
|
||||
this.fetchContactRequests();
|
||||
setInterval(this.onUpdate.bind(this), 60*1000);
|
||||
this.setState({...this.state, loaded: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (!this.state.loaded) {
|
||||
return <b>Loading… <Icon icon={"spinner"} /></b>
|
||||
}
|
||||
|
||||
this.controlObj = {
|
||||
showDialog: this.showDialog.bind(this),
|
||||
fetchNotifications: this.fetchNotifications.bind(this),
|
||||
api: this.api
|
||||
};
|
||||
|
||||
return <Router>
|
||||
<Header {...this.controlObj} notifications={this.state.notifications} />
|
||||
<Sidebar {...this.controlObj} notifications={this.state.notifications} contactRequests={this.state.contactRequests}/>
|
||||
<div className={"content-wrapper p-2"}>
|
||||
<section className={"content"}>
|
||||
<Switch>
|
||||
<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>
|
||||
</Switch>
|
||||
<Dialog {...this.state.dialog}/>
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<AdminDashboard />,
|
||||
document.getElementById('root')
|
||||
);
|
@ -1,37 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {Link, useLocation, useHistory} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
|
||||
export default function View404() {
|
||||
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
if (location.pathname === "/admin" || location.pathname === "/admin/") {
|
||||
history.push("/admin/dashboard");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"error-page"}>
|
||||
<h2 className={"headline text-warning"}>404</h2>
|
||||
<div className={"error-content"}>
|
||||
<h3>
|
||||
<Icon icon={"exclamation-triangle"} className={"text-warning"}/> Oops! Page not found.
|
||||
</h3>
|
||||
<p>
|
||||
We could not find the page you were looking for.
|
||||
Meanwhile, you may <Link to={"/admin/dashboard"}>return to dashboard</Link> or try using the search form.
|
||||
</p>
|
||||
<form className={"search-form"} onSubmit={(e) => e.preventDefault()}>
|
||||
<div className={"input-group"}>
|
||||
<input type={"text"} name={"search"} className={"form-control"} placeholder={"Search"} />
|
||||
<div className={"input-group-append"}>
|
||||
<button type="submit" name="submit" className={"btn btn-warning"}>
|
||||
<Icon icon={"search"}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
import Alert from "../elements/alert";
|
||||
import {Link} from "react-router-dom";
|
||||
import * as React from "react";
|
||||
import Icon from "../elements/icon";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import 'rc-color-picker/assets/index.css';
|
||||
import ColorPicker from 'rc-color-picker';
|
||||
|
||||
export default class CreateGroup extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
alerts: [],
|
||||
isSubmitting: false,
|
||||
name: "",
|
||||
color: "#0F0"
|
||||
};
|
||||
|
||||
this.parent = {
|
||||
api: props.api,
|
||||
};
|
||||
}
|
||||
|
||||
removeAlert(i) {
|
||||
if (i >= 0 && i < this.state.alerts.length) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.splice(i, 1);
|
||||
this.setState({...this.state, alerts: alerts});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let alerts = [];
|
||||
for (let i = 0; i < this.state.alerts.length; i++) {
|
||||
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
|
||||
}
|
||||
|
||||
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">Create a new group</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"><Link to={"/admin/users"}>Users</Link></li>
|
||||
<li className="breadcrumb-item active">Add User</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 pl-5 pr-5"}>
|
||||
{alerts}
|
||||
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"name"}>Group Name</label>
|
||||
<input type={"text"} className={"form-control"} placeholder={"Name"}
|
||||
name={"name"} id={"name"} maxLength={32} value={this.state.name}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"color"}>Color</label>
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text" style={{ padding: "0.35rem 0.4rem 0 0.4rem" }}>
|
||||
<ColorPicker
|
||||
color={this.state.color}
|
||||
alpha={100}
|
||||
name={"color"}
|
||||
onChange={this.onChangeColor.bind(this)}
|
||||
placement="topLeft">
|
||||
<span className="rc-color-picker-trigger"/>
|
||||
</ColorPicker>
|
||||
</span>
|
||||
<input type={"text"} className={"form-control float-right"} readOnly={true}
|
||||
value={this.state.color}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
|
||||
<Icon icon={"arrow-left"}/>
|
||||
Back
|
||||
</Link>
|
||||
{this.state.isSubmitting
|
||||
?
|
||||
<button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading… <Icon
|
||||
icon={"circle-notch"}/></button>
|
||||
: <button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip/>
|
||||
</>;
|
||||
}
|
||||
|
||||
onChangeColor(e) {
|
||||
this.setState({...this.state, color: e.color});
|
||||
}
|
||||
|
||||
onChangeInput(event) {
|
||||
const target = event.target;
|
||||
const value = target.value;
|
||||
const name = target.name;
|
||||
this.setState({...this.state, [name]: value});
|
||||
}
|
||||
|
||||
submitForm(e) {
|
||||
e.preventDefault();
|
||||
const name = this.state.name;
|
||||
const color = this.state.color;
|
||||
this.setState({...this.state, isSubmitting: true});
|
||||
this.parent.api.createGroup(name, color).then((res) => {
|
||||
let alerts = this.state.alerts.slice();
|
||||
if (res.success) {
|
||||
alerts.push({message: "Group was successfully created", title: "Success!", type: "success"});
|
||||
this.setState({...this.state, name: "", color: "", alerts: alerts, isSubmitting: false});
|
||||
} else {
|
||||
alerts.push({message: res.msg, title: "Error creating Group", type: "danger"});
|
||||
this.setState({...this.state, alerts: alerts, isSubmitting: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Alert from "../elements/alert";
|
||||
import Icon from "../elements/icon";
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
import {Collapse} from "react-collapse/lib/Collapse";
|
||||
|
||||
export default class CreateUser extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.parent = {
|
||||
showDialog: props.showDialog || function () { },
|
||||
api: props.api,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
sendInvite: true,
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
isSubmitting: false
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let errors = [];
|
||||
for (let i = 0; i < this.state.errors.length; i++) {
|
||||
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
|
||||
}
|
||||
|
||||
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">Create a new user</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"><Link to={"/admin/users"}>Users</Link></li>
|
||||
<li className="breadcrumb-item active">Add User</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 pl-5 pr-5"}>
|
||||
{errors}
|
||||
<form role={"form"} onSubmit={(e) => this.submitForm(e)}>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"username"}>Username</label>
|
||||
<input type={"text"} className={"form-control"} placeholder={"Enter username"}
|
||||
name={"username"} id={"username"} maxLength={32} value={this.state.username}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"email"}>E-Mail</label>
|
||||
<input type={"email"} className={"form-control"} placeholder={"E-Mail address"}
|
||||
id={"email"} name={"email"} maxLength={64} value={this.state.email}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
<div className={"form-check"}>
|
||||
<input type={"checkbox"} className={"form-check-input"}
|
||||
onChange={() => this.onCheckboxChange()}
|
||||
id={"sendInvite"} name={"sendInvite"} defaultChecked={this.state.sendInvite}/>
|
||||
<label className={"form-check-label"} htmlFor={"sendInvite"}>
|
||||
Send Invitation
|
||||
<Icon icon={"question-circle"} className={"ml-2"} style={{"color": "#0069d9"}}
|
||||
data-tip={"The user will receive an invitation token via email and can choose the password on his own."}
|
||||
data-type={"info"} data-place={"right"} data-effect={"solid"}/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Collapse isOpened={!this.state.sendInvite}>
|
||||
<div className={"mt-2"}>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"password"}>Password</label>
|
||||
<input type={"password"} className={"form-control"} placeholder={"Password"}
|
||||
id={"password"} name={"password"} value={this.state.password}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"confirmPassword"}>Confirm Password</label>
|
||||
<input type={"password"} className={"form-control"} placeholder={"Confirm Password"}
|
||||
id={"confirmPassword"} name={"confirmPassword"} value={this.state.confirmPassword}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
|
||||
<Icon icon={"arrow-left"}/>
|
||||
Back
|
||||
</Link>
|
||||
{ this.state.isSubmitting
|
||||
? <button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading… <Icon icon={"circle-notch"} /></button>
|
||||
: <button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip/>
|
||||
</>;
|
||||
}
|
||||
|
||||
submitForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredFields = (this.state.sendInvite ?
|
||||
["username", "email"] :
|
||||
["username", "password", "confirmPassword"]);
|
||||
|
||||
let missingFields = [];
|
||||
for (const field of requiredFields) {
|
||||
if (!this.state[field]) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({title: "Missing input", message: "The following fields are missing: " + missingFields.join(", "), type: "warning"});
|
||||
this.setState({ ...this.state, errors: errors });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ ...this.state, isSubmitting: true });
|
||||
const username = this.state.username;
|
||||
const email = this.state.email || "";
|
||||
const password = this.state.password;
|
||||
const confirmPassword = this.state.confirmPassword;
|
||||
|
||||
if (this.state.sendInvite) {
|
||||
this.parent.api.inviteUser(username, email).then((res) => {
|
||||
let errors = this.state.errors.slice();
|
||||
if (!res.success) {
|
||||
errors.push({ title: "Error inviting User", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, isSubmitting: false });
|
||||
} else {
|
||||
errors.push({ title: "Success", message: "The invitation was successfully sent.", type: "success" });
|
||||
this.setState({ ...this.state, errors: errors, username: "", email: "", isSubmitting: false });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
if (this.state.password !== this.state.confirmPassword) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ title: "Error creating User", message: "The given passwords do not match", type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "", isSubmitting: false });
|
||||
return;
|
||||
}
|
||||
|
||||
this.parent.api.createUser(username, email, password, confirmPassword).then((res) => {
|
||||
let errors = this.state.errors.slice();
|
||||
if (!res.success) {
|
||||
errors.push({ title: "Error creating User", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "", isSubmitting: false });
|
||||
} else {
|
||||
errors.push({ title: "Success", message: "The user was successfully created.", type: "success" });
|
||||
this.setState({ ...this.state, errors: errors, username: "", email: "", password: "", confirmPassword: "", isSubmitting: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onCheckboxChange() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
sendInvite: !this.state.sendInvite,
|
||||
});
|
||||
}
|
||||
|
||||
onChangeInput(event) {
|
||||
const target = event.target;
|
||||
const value = target.value;
|
||||
const name = target.name;
|
||||
this.setState({ ...this.state, [name]: value });
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Alert from "../elements/alert";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function ContactRequestOverview(props) {
|
||||
|
||||
let parent = {
|
||||
api: props.api,
|
||||
contactRequests: props.contactRequests || [ ]
|
||||
};
|
||||
|
||||
let [errors, setErrors] = useState([]);
|
||||
|
||||
function removeError(i) {
|
||||
if (i >= 0 && i < errors.length) {
|
||||
let newErrors = errors.slice();
|
||||
newErrors.splice(i, 1);
|
||||
setErrors(newErrors);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("contact site", parent.contactRequests);
|
||||
|
||||
let errorElements = [];
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
errorElements.push(<Alert key={"error-" + i} onClose={() => removeError(i)} {...errors[i]}/>)
|
||||
}
|
||||
|
||||
let chats = [];
|
||||
for (let i = 0; i < parent.contactRequests.length; i++) {
|
||||
const req = parent.contactRequests[i];
|
||||
chats.push(<div key={"contact-request-" + i}>
|
||||
From: { req.from_name } - { req.from_email }
|
||||
Unread messages: { req.unread }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">Contact Requests</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">Contact</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
{errorElements}
|
||||
{chats}
|
||||
</div>
|
||||
<ReactTooltip />
|
||||
</>;
|
||||
}
|
@ -1,322 +0,0 @@
|
||||
import * as React from "react";
|
||||
import Icon from "../elements/icon";
|
||||
import Alert from "../elements/alert";
|
||||
import {Link} from "react-router-dom";
|
||||
import "../include/select2.min.css";
|
||||
|
||||
export default class EditUser extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parent = {
|
||||
api: props.api,
|
||||
showDialog: props.showDialog,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
user: {},
|
||||
alerts: [],
|
||||
fetchError: null,
|
||||
loaded: false,
|
||||
isSaving: false,
|
||||
isDeleting: false,
|
||||
groups: { },
|
||||
searchString: "",
|
||||
searchActive: false
|
||||
};
|
||||
|
||||
this.searchBox = React.createRef();
|
||||
}
|
||||
|
||||
removeAlert(i) {
|
||||
if (i >= 0 && i < this.state.alerts.length) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.splice(i, 1);
|
||||
this.setState({...this.state, alerts: alerts});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parent.api.getUser(this.props.match.params["userId"]).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({ ...this.state, user: {
|
||||
name: res.user.name,
|
||||
email: res.user.email || "",
|
||||
groups: res.user.groups,
|
||||
password: "",
|
||||
confirmed: res.user.confirmed
|
||||
}
|
||||
});
|
||||
this.parent.api.fetchGroups(1, 50).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({ ...this.state, groups: res.groups, loaded: true });
|
||||
} else {
|
||||
this.setState({ ...this.state, fetchError: res.msg, loaded: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({ ...this.state, fetchError: res.msg, loaded: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeInput(event) {
|
||||
const target = event.target;
|
||||
let value = target.value;
|
||||
const name = target.name;
|
||||
|
||||
if (target.type === "checkbox") {
|
||||
value = !!target.checked;
|
||||
}
|
||||
|
||||
if (name === "search") {
|
||||
this.setState({...this.state, searchString: value});
|
||||
} else {
|
||||
this.setState({ ...this.state, user: { ...this.state.user, [name]: value } });
|
||||
}
|
||||
}
|
||||
|
||||
onToggleSearch(e) {
|
||||
e.stopPropagation();
|
||||
this.setState({ ...this.state, searchActive: !this.state.searchActive });
|
||||
this.searchBox.current.focus();
|
||||
}
|
||||
|
||||
onSubmitForm(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const id = this.props.match.params["userId"];
|
||||
const username = this.state.user["name"];
|
||||
const email = this.state.user["email"];
|
||||
let password = this.state.user["password"].length > 0 ? this.state.user["password"] : null;
|
||||
let groups = Object.keys(this.state.user.groups);
|
||||
let confirmed = this.state.user["confirmed"];
|
||||
|
||||
this.setState({ ...this.state, isSaving: true});
|
||||
this.parent.api.editUser(id, username, email, password, groups, confirmed).then((res) => {
|
||||
let alerts = this.state.alerts.slice();
|
||||
|
||||
if (res.success) {
|
||||
alerts.push({ title: "Success", message: "User was successfully updated.", type: "success" });
|
||||
this.setState({ ...this.state, isSaving: false, alerts: alerts, user: { ...this.state.user, password: "" } });
|
||||
} else {
|
||||
alerts.push({ title: "Error updating user", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, isSaving: false, alerts: alerts, user: { ...this.state.user, password: "" } });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteUser(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const id = this.props.match.params["userId"];
|
||||
|
||||
this.parent.showDialog("Are you sure you want to delete this user permanently?", "Delete User?", ["Yes", "No"], (btn) => {
|
||||
if (btn === "Yes") {
|
||||
this.parent.api.deleteUser(id).then((res) => {
|
||||
if (res.success) {
|
||||
this.props.history.push("/admin/users");
|
||||
} else {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.push({ title: "Error deleting user", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, isSaving: false, alerts: alerts, user: { ...this.state.user, password: "" } });
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveGroup(event, groupId) {
|
||||
event.stopPropagation();
|
||||
if (this.state.user.groups.hasOwnProperty(groupId)) {
|
||||
let groups = { ...this.state.user.groups };
|
||||
delete groups[groupId];
|
||||
this.setState({ ...this.state, user: { ...this.state.user, groups: groups }});
|
||||
}
|
||||
}
|
||||
|
||||
onAddGroup(event, groupId) {
|
||||
event.stopPropagation();
|
||||
if (!this.state.user.groups.hasOwnProperty(groupId)) {
|
||||
let groups = { ...this.state.user.groups, [groupId]: { ...this.state.groups[groupId] } };
|
||||
this.setState({ ...this.state, user: { ...this.state.user, groups: groups }, searchActive: false, searchString: "" });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.loaded) {
|
||||
return <h2 className={"text-center"}>
|
||||
Loading…<br/>
|
||||
<Icon icon={"spinner"} className={"mt-3 text-muted fa-2x"}/>
|
||||
</h2>
|
||||
}
|
||||
|
||||
let alerts = [];
|
||||
let form = null;
|
||||
if(this.state.fetchError) {
|
||||
alerts.push(
|
||||
<Alert key={"error-fetch"} title={"Error fetching data"} type={"danger"} message={
|
||||
<div>{this.state.fetchError}<br/>You can meanwhile return to the
|
||||
<Link to={"/admin/users"}>user overview</Link>
|
||||
</div>
|
||||
}/>
|
||||
)
|
||||
} else {
|
||||
|
||||
for (let i = 0; i < this.state.alerts.length; i++) {
|
||||
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
|
||||
}
|
||||
|
||||
let possibleOptions = [];
|
||||
let renderedOptions = [];
|
||||
for (let groupId in this.state.groups) {
|
||||
if (this.state.groups.hasOwnProperty(groupId)) {
|
||||
let groupName = this.state.groups[groupId].name;
|
||||
let groupColor = this.state.groups[groupId].color;
|
||||
if (this.state.user.groups.hasOwnProperty(groupId)) {
|
||||
renderedOptions.push(
|
||||
<li className={"select2-selection__choice"} key={"group-" + groupId} title={groupName} style={{backgroundColor: groupColor}}>
|
||||
<span className="select2-selection__choice__remove" role="presentation"
|
||||
onClick={(e) => this.onRemoveGroup(e, groupId)}>
|
||||
×
|
||||
</span>
|
||||
{groupName}
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
if (this.state.searchString.length === 0 || groupName.toLowerCase().includes(this.state.searchString.toLowerCase())) {
|
||||
possibleOptions.push(
|
||||
<li className={"select2-results__option"} role={"option"} key={"group-" + groupId} aria-selected={false}
|
||||
onClick={(e) => this.onAddGroup(e, groupId)}>
|
||||
{groupName}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let searchWidth = "100%";
|
||||
let placeholder = "Select Groups";
|
||||
let searchVisible = (this.state.searchString.length > 0 || this.state.searchActive) ? "block" : "none";
|
||||
if (renderedOptions.length > 0) {
|
||||
searchWidth = (0.75 + this.state.searchString.length * 0.75) + "em";
|
||||
placeholder = "";
|
||||
}
|
||||
|
||||
if (this.state.searchString.length > 0 && possibleOptions.length === 0) {
|
||||
possibleOptions.push(
|
||||
<li className={"select2-results__option"} role={"option"} key={"group-notfound"} aria-selected={true}>
|
||||
Group not found
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
form = <form role={"form"} onSubmit={(e) => e.preventDefault()}>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"username"}>Username</label>
|
||||
<input type={"text"} className={"form-control"} placeholder={"Enter username"}
|
||||
name={"username"} id={"username"} maxLength={32} value={this.state.user.name}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"email"}>E-Mail</label>
|
||||
<input type={"email"} className={"form-control"} placeholder={"E-Mail address"}
|
||||
id={"email"} name={"email"} maxLength={64} value={this.state.user.email}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"password"}>Password</label>
|
||||
<input type={"password"} className={"form-control"} placeholder={"(unchanged)"}
|
||||
id={"password"} name={"password"} value={this.state.user.password}
|
||||
onChange={this.onChangeInput.bind(this)}/>
|
||||
</div>
|
||||
|
||||
<div className={"form-group position-relative"}>
|
||||
<label>Groups</label>
|
||||
<span className={"select2 select2-container select2-container--default select2-container--below"}
|
||||
dir={"ltr"} style={{width: "100%"}} >
|
||||
<span className="selection">
|
||||
<span className={"select2-selection select2-selection--multiple"} role={"combobox"} aria-haspopup={"true"}
|
||||
aria-expanded={false} aria-disabled={false} onClick={this.onToggleSearch.bind(this)}>
|
||||
<ul className={"select2-selection__rendered"}>
|
||||
{renderedOptions}
|
||||
<li className={"select2-search select2-search--inline"}>
|
||||
<input className={"select2-search__field"} type={"search"} tabIndex={0}
|
||||
autoComplete={"off"} autoCorrect={"off"} autoCapitalize={"none"} spellCheck={false}
|
||||
role={"searchbox"} aria-autocomplete={"list"} placeholder={placeholder}
|
||||
name={"search"} style={{width: searchWidth}} value={this.state.searchString}
|
||||
onChange={this.onChangeInput.bind(this)} ref={this.searchBox} />
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
<span className="dropdown-wrapper" aria-hidden="true"/>
|
||||
</span>
|
||||
<span className={"select2-container select2-container--default select2-container--open"}
|
||||
style={{position: "absolute", bottom: 0, left: 0, width: "100%", display: searchVisible}}>
|
||||
<span className={"select2-dropdown select2-dropdown--below"} dir={"ltr"}>
|
||||
<span className={"select2-results"}>
|
||||
<ul className={"select2-results__options"} role={"listbox"}
|
||||
aria-multiselectable={true} aria-expanded={true} aria-hidden={false}>
|
||||
{possibleOptions}
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={"form-check"}>
|
||||
<input type={"checkbox"} className={"form-check-input"}
|
||||
onChange={this.onChangeInput.bind(this)}
|
||||
id={"confirmed"} name={"confirmed"} checked={this.state.user.confirmed}/>
|
||||
<label className={"form-check-label"} htmlFor={"confirmed"}>
|
||||
Confirmed
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Link to={"/admin/users"} className={"btn btn-info mt-2 mr-2"}>
|
||||
<Icon icon={"arrow-left"}/>
|
||||
Back
|
||||
</Link>
|
||||
{ this.state.isSaving
|
||||
? <button type={"submit"} className={"btn btn-primary mt-2 mr-2"} disabled>Saving… <Icon icon={"circle-notch"} /></button>
|
||||
: <button type={"submit"} className={"btn btn-primary mt-2 mr-2"} onClick={this.onSubmitForm.bind(this)}>Save</button>
|
||||
}
|
||||
{ this.state.isDeleting
|
||||
? <button type={"submit"} className={"btn btn-danger mt-2"} disabled>Deleting… <Icon icon={"circle-notch"} /></button>
|
||||
: <button type={"submit"} className={"btn btn-danger mt-2"} onClick={this.onDeleteUser.bind(this)}>Delete</button>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
|
||||
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">Edit User</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"><Link to={"/admin/users"}>Users</Link></li>
|
||||
<li className="breadcrumb-item active">Add User</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 pl-5 pr-5"}>
|
||||
{alerts}
|
||||
{form}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
import React, {useState} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
import {Collapse} from "react-collapse";
|
||||
|
||||
export default function HelpPage() {
|
||||
|
||||
const [firstStepsCollapsed, collapseFirstSteps] = useState(false);
|
||||
const [faqCollapsed, collapseFaq] = useState(false);
|
||||
const [aboutCollapsed, collapseAbout] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1>WebBase Help & Information</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"}>Help</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="content">
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row"}>
|
||||
<div className="col-12 col-md-8 col-lg-4">
|
||||
<p>
|
||||
WebBase is a php framework to simplify user management, pages and routing.
|
||||
It can easily be modified and extended by writing document classes and the database
|
||||
can be accessed through the available abstracted scheme. It also includes
|
||||
a REST API with access control, parameter type checking and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<div className={"col-12 col-lg-6"}>
|
||||
<div className={"card"}>
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<Icon icon={"walking"} className={"mr-1"}/>
|
||||
First Steps
|
||||
</h3>
|
||||
<div className={"card-tools"}>
|
||||
<button type={"button"} className={"btn btn-tool"} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
collapseFirstSteps(!firstStepsCollapsed);
|
||||
}}>
|
||||
<Icon icon={"minus"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse isOpened={!firstStepsCollapsed}>
|
||||
<div className="card-body">
|
||||
<ol>
|
||||
<li>Customize <Link to={"/admin/settings"}>website settings</Link></li>
|
||||
<li>Manage users and groups on <Link to={"/admin/users"}>this page</Link></li>
|
||||
<li><Link to={"/admin/pages"}>Create routes</Link> for your website</li>
|
||||
<li>For dynamic pages:
|
||||
<ol>
|
||||
<li>Create a document class in <b>/Core/Documents</b> according to the other classes</li>
|
||||
<li>Create a view class in <b>/Core/Views</b> for every view you have</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>For static pages:
|
||||
<ul>
|
||||
<li>Create html files in <b>/static</b></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-12 col-lg-6"}>
|
||||
<div className={"card"}>
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<Icon icon={"question-circle"} className={"mr-1"}/>
|
||||
FAQ
|
||||
</h3>
|
||||
<div className={"card-tools"}>
|
||||
<button type={"button"} className={"btn btn-tool"} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
collapseFaq(!faqCollapsed);
|
||||
}}>
|
||||
<Icon icon={"minus"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse isOpened={!faqCollapsed}>
|
||||
<div className="card-body">
|
||||
Nobody asked questions so far…
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-12 col-lg-6"}>
|
||||
<div className={"card"}>
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<Icon icon={"address-card"} className={"mr-1"}/>
|
||||
About
|
||||
</h3>
|
||||
<div className={"card-tools"}>
|
||||
<button type={"button"} className={"btn btn-tool"} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
collapseAbout(!aboutCollapsed);
|
||||
}}>
|
||||
<Icon icon={"minus"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse isOpened={!aboutCollapsed}>
|
||||
<div className="card-body">
|
||||
<b>Project Lead & Main Developer</b>
|
||||
<ul className={"list-unstyled"}>
|
||||
<li><small><Icon icon={"address-card"} className={"mr-1"}/>Roman Hergenreder</small></li>
|
||||
<li>
|
||||
<small><Icon icon={"globe"} className={"mr-1"}/>
|
||||
<a href={"https://romanh.de/"} target={"_blank"} rel={"noopener"}>
|
||||
https://romanh.de/
|
||||
</a>
|
||||
</small>
|
||||
</li>
|
||||
<li><small><Icon icon={"envelope"} className={"mr-1"}/><a href={"mailto:webmaster@romanh.de"}>webmaster@romanh.de</a></small></li>
|
||||
</ul>
|
||||
|
||||
<b className={"mt-2"}>Backend Developer</b>
|
||||
<ul className={"list-unstyled"}>
|
||||
<li><small><Icon icon={"address-card"} className={"mr-1"}/>Leon Krause</small></li>
|
||||
</ul>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
import moment from 'moment';
|
||||
|
||||
export default class Logs extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
alerts: [],
|
||||
notifications: []
|
||||
};
|
||||
|
||||
this.parent = {
|
||||
api : props.api,
|
||||
fetchNotifications: props.fetchNotifications,
|
||||
}
|
||||
}
|
||||
|
||||
removeError(i) {
|
||||
if (i >= 0 && i < this.state.alerts.length) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.splice(i, 1);
|
||||
this.setState({...this.state, alerts: alerts});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parent.api.getNotifications(false).then((res) => {
|
||||
if (!res.success) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.push({ message: res.msg, title: "Error fetching Notifications" });
|
||||
this.setState({ ...this.state, alerts: alerts });
|
||||
} else {
|
||||
this.setState({ ...this.state, notifications: res.notifications });
|
||||
}
|
||||
|
||||
this.parent.api.markNotificationsSeen().then((res) => {
|
||||
if (!res.success) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.push({ message: res.msg, title: "Error fetching Notifications" });
|
||||
this.setState({ ...this.state, alerts: alerts });
|
||||
}
|
||||
|
||||
this.parent.fetchNotifications();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const colors = ["red", "green", "blue", "purple", "maroon"];
|
||||
|
||||
let dates = { };
|
||||
for (let notification of this.state.notifications) {
|
||||
let day = moment(notification["created_at"]).format('ll');
|
||||
if (!dates.hasOwnProperty(day)) {
|
||||
dates[day] = [];
|
||||
}
|
||||
|
||||
let icon = "bell";
|
||||
if (notification.type === "message") {
|
||||
icon = "envelope";
|
||||
} else if(notification.type === "warning") {
|
||||
icon = "exclamation-triangle";
|
||||
}
|
||||
|
||||
dates[day].push({ ...notification, icon: icon, timestamp: notification["created_at"] });
|
||||
}
|
||||
|
||||
let elements = [];
|
||||
for (let date in dates) {
|
||||
let color = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
elements.push(
|
||||
<div className={"time-label"} key={"time-label-" + date}>
|
||||
<span className={"bg-" + color}>{date}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
for (let event of dates[date]) {
|
||||
let timeString = moment(event.timestamp).fromNow();
|
||||
elements.push(
|
||||
<div key={"time-entry-" + event.id}>
|
||||
<Icon icon={event.icon} className={"bg-" + color}/>
|
||||
<div className="timeline-item">
|
||||
<span className="time"><Icon icon={"clock"}/> {timeString}</span>
|
||||
<h3 className="timeline-header">{event.title}</h3>
|
||||
<div className="timeline-body">{event.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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">Logs & Notifications</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">Logs</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-8 col-12"}>
|
||||
<div className="timeline">
|
||||
<div className={"time-label"}>
|
||||
<span className={"bg-blue"}>Today</span>
|
||||
</div>
|
||||
{elements}
|
||||
<div>
|
||||
<Icon icon={"clock"} className={"bg-gray"}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import {Collapse} from 'react-collapse';
|
||||
import moment from 'moment';
|
||||
import Alert from "../elements/alert";
|
||||
import humanReadableSize from "../global";
|
||||
|
||||
export default class Overview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.parent = {
|
||||
showDialog: props.showDialog,
|
||||
api: props.api,
|
||||
};
|
||||
|
||||
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() {
|
||||
|
||||
const numDays = moment().daysInMonth();
|
||||
|
||||
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 ' + moment().format("MMMM"),
|
||||
borderWidth: 1,
|
||||
data: data,
|
||||
backgroundColor: colors,
|
||||
}]
|
||||
};
|
||||
|
||||
let errors = [];
|
||||
for (let i = 0; i < this.state.errors.length; i++) {
|
||||
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
|
||||
}
|
||||
|
||||
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"}>
|
||||
<div className={"container-fluid"}>
|
||||
{errors}
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className="small-box bg-info">
|
||||
<div className={"inner"}>
|
||||
<h3>{this.state.userCount}</h3>
|
||||
<p>Users registered</p>
|
||||
</div>
|
||||
<div className="icon">
|
||||
<Icon icon={"users"} />
|
||||
</div>
|
||||
<Link to={"/admin/users"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className={"small-box bg-success"}>
|
||||
<div className={"inner"}>
|
||||
<h3>{this.state.pageCount}</h3>
|
||||
<p>Routes & Pages</p>
|
||||
</div>
|
||||
<div className="icon">
|
||||
<Icon icon={"copy"} />
|
||||
</div>
|
||||
<Link to={"/admin/pages"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className={"small-box bg-warning"}>
|
||||
<div className={"inner"}>
|
||||
<h3>{this.props.notifications.length}</h3>
|
||||
<p>new Notifications</p>
|
||||
</div>
|
||||
<div className={"icon"}>
|
||||
<Icon icon={"bell"} />
|
||||
</div>
|
||||
<Link to={"/admin/logs"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className={"small-box bg-danger"}>
|
||||
<div className={"inner"}>
|
||||
<h3>{this.state.visitorsTotal}</h3>
|
||||
<p>Unique Visitors</p>
|
||||
</div>
|
||||
<div className="icon">
|
||||
<Icon icon={"chart-line"} />
|
||||
</div>
|
||||
<Link to={"/admin/visitors"} className="small-box-footer">More info <Icon icon={"arrow-circle-right"}/></Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-lg-6 col-12">
|
||||
<div className="card card-info">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Unique Visitors this month</h3>
|
||||
<div className="card-tools">
|
||||
<button type="button" className={"btn btn-tool"} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ ...this.state, chartVisible: !this.state.chartVisible });
|
||||
}}>
|
||||
<Icon icon={"minus"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse isOpened={this.state.chartVisible}>
|
||||
<div className="card-body">
|
||||
<div className="chart">
|
||||
<Bar data={chartData} options={chartOptions} />
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6 col-12">
|
||||
<div className="card card-warning">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Server Status</h3>
|
||||
<div className="card-tools">
|
||||
<button type="button" className={"btn btn-tool"} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ ...this.state, statusVisible: !this.state.statusVisible });
|
||||
}}>
|
||||
<Icon icon={"minus"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse isOpened={this.state.statusVisible}>
|
||||
<div className="card-body">
|
||||
<ul className={"list-unstyled"}>
|
||||
<li><b>Version</b>: {this.state.server.version}</li>
|
||||
<li><b>Server</b>: {this.state.server.server}</li>
|
||||
<li><b>Memory Usage</b>: {humanReadableSize(this.state.server["memory_usage"])}</li>
|
||||
<li><b>Load Average</b>: { loadAvg }</li>
|
||||
<li><b>Database</b>: { this.state.server["database"] }</li>
|
||||
<li><b>Mail</b>: { this.state.server["mail"] === true
|
||||
? <span>
|
||||
OK
|
||||
<Icon icon={"check-circle"} className={"ml-2 text-success"}/>
|
||||
</span>
|
||||
: <span>
|
||||
<Link to={"/admin/settings"}>Not configured</Link>
|
||||
<Icon icon={"times-circle"} className={"ml-2 text-danger"}/>
|
||||
</span>}
|
||||
</li>
|
||||
<li>
|
||||
<b>Google reCaptcha</b>: { this.state.server["reCaptcha"] === true
|
||||
? <span>
|
||||
OK
|
||||
<Icon icon={"check-circle"} className={"ml-2 text-success"}/>
|
||||
</span>
|
||||
: <span>
|
||||
<Link to={"/admin/settings"}>Not configured</Link>
|
||||
<Icon icon={"times-circle"} className={"ml-2 text-danger"}/>
|
||||
</span>}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
}
|
@ -1,291 +0,0 @@
|
||||
import * as React from "react";
|
||||
import Alert from "../elements/alert";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import Select from 'react-select';
|
||||
|
||||
export default class PageOverview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parent = {
|
||||
api: props.api
|
||||
};
|
||||
|
||||
this.state = {
|
||||
routes: [],
|
||||
errors: [],
|
||||
isResetting: false,
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
this.optionMap = {
|
||||
"redirect_temporary": "Redirect Temporary",
|
||||
"redirect_permanently": "Redirect Permanently",
|
||||
"static": "Serve Static",
|
||||
"dynamic": "Load Dynamic",
|
||||
};
|
||||
|
||||
this.options = [];
|
||||
for (let key in this.optionMap) {
|
||||
this.options.push(this.buildOption(key));
|
||||
}
|
||||
}
|
||||
|
||||
buildOption(key) {
|
||||
if (typeof key === 'object' && key.hasOwnProperty("key") && key.hasOwnProperty("label")) {
|
||||
return key;
|
||||
} else if (typeof key === 'string') {
|
||||
return { value: key, label: this.optionMap[key] };
|
||||
} else {
|
||||
return this.options[key];
|
||||
}
|
||||
}
|
||||
|
||||
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.fetchRoutes()
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let errors = [];
|
||||
let rows = [];
|
||||
|
||||
for (let i = 0; i < this.state.errors.length; i++) {
|
||||
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
|
||||
}
|
||||
|
||||
const inputStyle = { fontFamily: "Courier", paddingTop: "14px" };
|
||||
|
||||
for (let i = 0; i < this.state.routes.length; i++) {
|
||||
let route = this.state.routes[i];
|
||||
rows.push(
|
||||
<tr key={"route-" + i}>
|
||||
<td className={"align-middle"}>
|
||||
<input type={"text"} maxLength={128} style={inputStyle} className={"form-control"}
|
||||
value={route.request} onChange={(e) => this.changeRequest(i, e)} />
|
||||
</td>
|
||||
<td className={"text-center"}>
|
||||
<Select options={this.options} value={this.buildOption(route.action)} onChange={(selectedOption) => this.changeAction(i, selectedOption)} />
|
||||
</td>
|
||||
<td className={"align-middle"}>
|
||||
<input type={"text"} maxLength={128} style={inputStyle} className={"form-control"}
|
||||
value={route.target} onChange={(e) => this.changeTarget(i, e)} />
|
||||
</td>
|
||||
<td className={"align-middle"}>
|
||||
<input type={"text"} maxLength={64} style={inputStyle} className={"form-control"}
|
||||
value={route.extra} onChange={(e) => this.changeExtra(i, e)} />
|
||||
</td>
|
||||
<td className={"text-center"}>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={route.active === 1}
|
||||
onChange={(e) => this.changeActive(i, e)} />
|
||||
</td>
|
||||
<td className={"text-center"}>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={route.exact === 1}
|
||||
onChange={(e) => this.changeExact(i, e)} />
|
||||
</td>
|
||||
<td>
|
||||
<ReactTooltip id={"delete-" + i} />
|
||||
<Icon icon={"trash"} style={{color: "red", cursor: "pointer"}}
|
||||
data-tip={"Click to delete this route"}
|
||||
data-type={"warning"} data-place={"right"}
|
||||
data-for={"delete-" + i} data-effect={"solid"}
|
||||
onClick={() => this.removeRoute(i)}/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className="col-sm-6">
|
||||
<h1 className="m-0 text-dark">Routes & Pages</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">Pages</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
{errors}
|
||||
<div className={"content-fluid"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-10 col-12"}>
|
||||
<table className={"table"}>
|
||||
<thead className={"thead-dark"}>
|
||||
<tr>
|
||||
<th>
|
||||
Request
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"The request, the user is making. Can also be interpreted as a regular expression."}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th style={{minWidth: "200px"}}>
|
||||
Action
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"The action to be taken"}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th>
|
||||
Target
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"Any URL if action is redirect or static. Path to a class inheriting from Document, " +
|
||||
"if dynamic is chosen"}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th>
|
||||
Extra
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"If action is dynamic, a view name can be entered here, otherwise leave empty."}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th className={"text-center"}>
|
||||
Active
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"True, if the route is currently active."}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th className={"text-center"}>
|
||||
Exact
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"True, if the URL must match exactly."}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button className={"btn btn-info"} onClick={() => this.onAddRoute()} disabled={this.state.isResetting || this.state.isSaving}>
|
||||
<Icon icon={"plus"}/> Add new Route
|
||||
</button>
|
||||
<button className={"btn btn-secondary ml-2"} onClick={() => this.onResetRoutes()} disabled={this.state.isResetting || this.state.isSaving}>
|
||||
{ this.state.isResetting ? <span>Resetting <Icon icon={"circle-notch"}/></span> : "Reset" }
|
||||
</button>
|
||||
<button className={"btn btn-success ml-2"} onClick={() => this.onSaveRoutes()} disabled={this.state.isResetting || this.state.isSaving}>
|
||||
{ this.state.isSaving ? <span>Saving <Icon icon={"circle-notch"}/></span> : "Save" }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip data-effect={"solid"}/>
|
||||
</>
|
||||
}
|
||||
|
||||
onResetRoutes() {
|
||||
this.setState({ ...this.state, isResetting: true });
|
||||
this.fetchRoutes();
|
||||
}
|
||||
|
||||
onSaveRoutes() {
|
||||
this.setState({ ...this.state, isSaving: true });
|
||||
|
||||
let routes = [];
|
||||
for (let i = 0; i < this.state.routes.length; i++) {
|
||||
let route = this.state.routes[i];
|
||||
routes.push({
|
||||
request: route.request,
|
||||
action: typeof route.action === 'object' ? route.action.value : route.action,
|
||||
target: route.target,
|
||||
extra: route.extra ?? "",
|
||||
active: route.active === 1,
|
||||
exact: route.exact === 1,
|
||||
});
|
||||
}
|
||||
|
||||
this.parent.api.saveRoutes(routes).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({...this.state, isSaving: false});
|
||||
} else {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ title: "Error saving routes", message: res.msg });
|
||||
this.setState({...this.state, errors: errors, isSaving: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changeRoute(index, key, value) {
|
||||
if (index < 0 || index >= this.state.routes.length)
|
||||
return;
|
||||
|
||||
let routes = this.state.routes.slice();
|
||||
routes[index][key] = value;
|
||||
this.setState({ ...this.state, routes: routes });
|
||||
}
|
||||
|
||||
removeRoute(index) {
|
||||
if (index < 0 || index >= this.state.routes.length)
|
||||
return;
|
||||
let routes = this.state.routes.slice();
|
||||
routes.splice(index, 1);
|
||||
this.setState({ ...this.state, routes: routes });
|
||||
}
|
||||
|
||||
|
||||
onAddRoute() {
|
||||
let routes = this.state.routes.slice();
|
||||
routes.push({ request: "", action: "dynamic", target: "", extra: "", active: 1, exact: 1 });
|
||||
this.setState({ ...this.state, routes: routes });
|
||||
}
|
||||
|
||||
changeAction(index, selectedOption) {
|
||||
this.changeRoute(index, "action", selectedOption);
|
||||
}
|
||||
|
||||
changeActive(index, e) {
|
||||
this.changeRoute(index, "active", e.target.checked ? 1 : 0);
|
||||
}
|
||||
|
||||
changeExact(index, e) {
|
||||
this.changeRoute(index, "exact", e.target.checked ? 1 : 0);
|
||||
}
|
||||
|
||||
changeRequest(index, e) {
|
||||
this.changeRoute(index, "request", e.target.value);
|
||||
}
|
||||
|
||||
changeTarget(index, e) {
|
||||
this.changeRoute(index, "target", e.target.value);
|
||||
}
|
||||
|
||||
changeExtra(index, e) {
|
||||
this.changeRoute(index, "extra", e.target.value);
|
||||
}
|
||||
|
||||
fetchRoutes() {
|
||||
this.parent.api.getRoutes().then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({...this.state, routes: res.routes, isResetting: false});
|
||||
ReactTooltip.rebuild();
|
||||
} else {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ title: "Error fetching routes", message: res.msg });
|
||||
this.setState({...this.state, errors: errors, isResetting: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
import Alert from "../elements/alert";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
export default class PermissionSettings extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
alerts: [],
|
||||
permissions: [],
|
||||
groups: {},
|
||||
isSaving: false,
|
||||
isResetting: false
|
||||
};
|
||||
|
||||
this.parent = {
|
||||
api: props.api
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchPermissions()
|
||||
}
|
||||
|
||||
fetchPermissions() {
|
||||
this.parent.api.fetchPermissions().then((res) => {
|
||||
if (!res.success) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.push({ message: res.msg, title: "Error fetching permissions" });
|
||||
this.setState({...this.state, alerts: alerts, isResetting: false});
|
||||
} else {
|
||||
this.setState({...this.state, groups: res.groups, permissions: res.permissions, isResetting: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeAlert(i) {
|
||||
if (i >= 0 && i < this.state.alerts.length) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.splice(i, 1);
|
||||
this.setState({...this.state, alerts: alerts});
|
||||
}
|
||||
}
|
||||
|
||||
onChangeMethod(e, index) {
|
||||
if (index < 0 || index >= this.state.permissions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = e.target.value;
|
||||
let newPermissions = this.state.permissions.slice();
|
||||
newPermissions[index].method = value;
|
||||
this.setState({ ...this.state, permissions: newPermissions })
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let alerts = [];
|
||||
for (let i = 0; i < this.state.alerts.length; i++) {
|
||||
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeAlert(i)} {...this.state.alerts[i]}/>)
|
||||
}
|
||||
|
||||
let th = [];
|
||||
th.push(<th key={"th-method"}>Method</th>);
|
||||
th.push(<th key={"th-everyone"} className={"text-center"}>Everyone</th>);
|
||||
|
||||
for (let groupId in this.state.groups) {
|
||||
if (this.state.groups.hasOwnProperty(groupId)) {
|
||||
let groupName = this.state.groups[groupId].name;
|
||||
let groupColor = this.state.groups[groupId].color;
|
||||
th.push(
|
||||
<th key={"th-" + groupId} className={"text-center"}>
|
||||
<span key={"group-" + groupId} className={"badge text-white"} style={{backgroundColor: groupColor}}>
|
||||
{groupName}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let tr = [];
|
||||
for (let i = 0; i < this.state.permissions.length; i++) {
|
||||
let permission = this.state.permissions[i];
|
||||
let td = [];
|
||||
|
||||
if (permission.description) {
|
||||
td.push(
|
||||
<td>
|
||||
<ReactTooltip id={"tooltip-" + i} />
|
||||
{ permission.method }
|
||||
<Icon icon={"info-circle"} className={"text-info float-right"}
|
||||
data-tip={permission.description} data-place={"right"} data-type={"info"}
|
||||
data-effect={"solid"} data-for={"tooltip-" + i} />
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
td.push(
|
||||
<td>
|
||||
<ReactTooltip id={"tooltip-" + i} />
|
||||
<input type={"text"} maxLength={32} value={this.state.permissions[i].method}
|
||||
onChange={(e) => this.onChangeMethod(e, i)} />
|
||||
<Icon icon={"trash"} className={"text-danger float-right"}
|
||||
data-tip={"Delete"} data-place={"right"} data-type={"error"}
|
||||
data-effect={"solid"} data-for={"tooltip-" + i}
|
||||
onClick={() => this.onDeletePermission(i)} style={{cursor: "pointer"}} />
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
td.push(
|
||||
<td key={"td-everyone"} className={"text-center"}>
|
||||
<input type={"checkbox"} checked={this.state.permissions[i].groups.length === 0}
|
||||
onChange={(e) => this.onChangePermission(e, i)}/>
|
||||
</td>
|
||||
);
|
||||
|
||||
for (let groupId in this.state.groups) {
|
||||
if (this.state.groups.hasOwnProperty(groupId)) {
|
||||
groupId = parseInt(groupId);
|
||||
td.push(
|
||||
<td key={"td-" + groupId} className={"text-center"}>
|
||||
<input type={"checkbox"} checked={this.state.permissions[i].groups.includes(groupId)}
|
||||
onChange={(e) => this.onChangePermission(e, i, groupId)}/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tr.push(<tr key={"permission-" + i}>{td}</tr>);
|
||||
}
|
||||
|
||||
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">API Access Control</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"><Link to={"/admin/users"}>Users</Link></li>
|
||||
<li className="breadcrumb-item active">Permissions</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6 pl-5 pr-5"}>
|
||||
{alerts}
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<table className={"table table-bordered table-hover dataTable dtr-inline"}>
|
||||
<thead>
|
||||
<tr role={"row"}>
|
||||
{th}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tr}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className={"mt-2"}>
|
||||
<Link to={"/admin/users"} className={"btn btn-primary"}>
|
||||
<Icon icon={"arrow-left"}/>
|
||||
Back
|
||||
</Link>
|
||||
<button className={"btn btn-info ml-2"} onClick={() => this.onAddPermission()} disabled={this.state.isResetting || this.state.isSaving}>
|
||||
<Icon icon={"plus"}/> Add new Permission
|
||||
</button>
|
||||
<button className={"btn btn-secondary ml-2"} onClick={() => this.onResetPermissions()} disabled={this.state.isResetting || this.state.isSaving}>
|
||||
{ this.state.isResetting ? <span>Resetting <Icon icon={"circle-notch"}/></span> : "Reset" }
|
||||
</button>
|
||||
<button className={"btn btn-success ml-2"} onClick={() => this.onSavePermissions()} disabled={this.state.isResetting || this.state.isSaving}>
|
||||
{ this.state.isSaving ? <span>Saving <Icon icon={"circle-notch"}/></span> : "Save" }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
onAddPermission() {
|
||||
let newPermissions = this.state.permissions.slice();
|
||||
newPermissions.push({ method: "", groups: [], description: null });
|
||||
this.setState({ ...this.state, permissions: newPermissions })
|
||||
}
|
||||
|
||||
onResetPermissions() {
|
||||
this.setState({ ...this.state, isResetting: true });
|
||||
this.fetchPermissions();
|
||||
}
|
||||
|
||||
onSavePermissions() {
|
||||
this.setState({ ...this.state, isSaving: true });
|
||||
|
||||
let permissions = [];
|
||||
for (let i = 0; i < this.state.permissions.length; i++) {
|
||||
let permission = this.state.permissions[i];
|
||||
permissions.push({ method: permission.method, groups: permission.groups });
|
||||
}
|
||||
|
||||
this.parent.api.savePermissions(permissions).then((res) => {
|
||||
if (!res.success) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.push({ message: res.msg, title: "Error saving permissions" });
|
||||
this.setState({...this.state, alerts: alerts, isSaving: false});
|
||||
} else {
|
||||
this.setState({...this.state, isSaving: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDeletePermission(index) {
|
||||
if (index < 0 || index >= this.state.permissions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newPermissions = this.state.permissions.slice();
|
||||
newPermissions.splice(index, 1);
|
||||
this.setState({ ...this.state, permissions: newPermissions })
|
||||
}
|
||||
|
||||
onChangePermission(event, index, group = null) {
|
||||
if (index < 0 || index >= this.state.permissions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isChecked = event.target.checked;
|
||||
let newPermissions = this.state.permissions.slice();
|
||||
if (group === null) {
|
||||
if (isChecked) {
|
||||
newPermissions[index].groups = [];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (isChecked && !newPermissions[index].groups.includes(group)) {
|
||||
newPermissions[index].groups.push(group);
|
||||
} else if(!isChecked) {
|
||||
let indexOf = newPermissions[index].groups.indexOf(group);
|
||||
if (indexOf !== -1) {
|
||||
newPermissions[index].groups.splice(indexOf, 1);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ ...this.state, permissions: newPermissions })
|
||||
}
|
||||
};
|
@ -1,619 +0,0 @@
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Alert from "../elements/alert";
|
||||
import {Collapse} from "react-collapse/lib/Collapse";
|
||||
import Icon from "../elements/icon";
|
||||
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
export default class Settings extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
settings: {},
|
||||
general: {
|
||||
alerts: [],
|
||||
isOpen: true,
|
||||
isSaving: false,
|
||||
isResetting: false,
|
||||
keys: ["site_name", "base_url", "user_registration_enabled"]
|
||||
},
|
||||
mail: {
|
||||
alerts: [],
|
||||
isOpen: true,
|
||||
isSaving: false,
|
||||
isResetting: false,
|
||||
isSending: false,
|
||||
test_email: "",
|
||||
unsavedMailSettings: false,
|
||||
keys: ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"]
|
||||
},
|
||||
recaptcha: {
|
||||
alerts: [],
|
||||
isOpen: true,
|
||||
isSaving: false,
|
||||
isResetting: false,
|
||||
keys: ["recaptcha_enabled", "recaptcha_public_key", "recaptcha_private_key"]
|
||||
},
|
||||
uncategorised: {
|
||||
alerts: [],
|
||||
isOpen: true,
|
||||
isSaving: false,
|
||||
isResetting: false,
|
||||
settings: []
|
||||
},
|
||||
};
|
||||
|
||||
this.parent = {
|
||||
api: props.api,
|
||||
showDialog: props.showDialog
|
||||
};
|
||||
|
||||
this.hiddenKeys = [
|
||||
"recaptcha_private_key",
|
||||
"mail_password"
|
||||
];
|
||||
}
|
||||
|
||||
isDefaultKey(key) {
|
||||
key = key.trim();
|
||||
return this.state.general.keys.includes(key)
|
||||
|| this.state.mail.keys.includes(key)
|
||||
|| this.state.recaptcha.keys.includes(key)
|
||||
|| this.hiddenKeys.includes(key);
|
||||
}
|
||||
|
||||
getUncategorisedValues(res) {
|
||||
let uncategorised = [];
|
||||
for(let key in res.settings) {
|
||||
if (res.settings.hasOwnProperty(key) && !this.isDefaultKey(key)) {
|
||||
uncategorised.push({key: key, value: res.settings[key]});
|
||||
}
|
||||
}
|
||||
|
||||
return uncategorised;
|
||||
}
|
||||
|
||||
onDeleteUncategorisedProp(index) {
|
||||
if (index < 0 || index >= this.state.uncategorised.settings.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let props = this.state.uncategorised.settings.slice();
|
||||
props.splice(index, 1);
|
||||
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
|
||||
}
|
||||
|
||||
onChangeUncategorisedValue(event, index, isKey) {
|
||||
if (index < 0 || index >= this.state.uncategorised.settings.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let props = this.state.uncategorised.settings.slice();
|
||||
if (isKey) {
|
||||
props[index].key = event.target.value;
|
||||
} else {
|
||||
props[index].value = event.target.value;
|
||||
}
|
||||
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
|
||||
}
|
||||
|
||||
onAddUncategorisedProperty() {
|
||||
let props = this.state.uncategorised.settings.slice();
|
||||
props.push({key: "", value: ""});
|
||||
this.setState({ ...this.state, uncategorised: { ...this.state.uncategorised, settings: props }});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parent.api.getSettings().then((res) => {
|
||||
if (res.success) {
|
||||
let newState = {
|
||||
...this.state,
|
||||
settings: res.settings,
|
||||
uncategorised: { ...this.state.uncategorised, settings: this.getUncategorisedValues(res) }
|
||||
};
|
||||
|
||||
this.setState(newState);
|
||||
} else {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({title: "Error fetching settings", message: res.msg});
|
||||
this.setState({...this.state, errors: errors });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeError(i, category = null) {
|
||||
if (category) {
|
||||
if (i >= 0 && i < this.state[category].alerts.length) {
|
||||
let alerts = this.state[category].alerts.slice();
|
||||
alerts.splice(i, 1);
|
||||
this.setState({...this.state, [category]: {...this.state[category], alerts: alerts}});
|
||||
}
|
||||
} else {
|
||||
if (i >= 0 && i < this.state.errors.length) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.splice(i, 1);
|
||||
this.setState({...this.state, errors: errors});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleCollapse(category) {
|
||||
this.setState({...this.state, [category]: {...this.state[category], isOpen: !this.state[category].isOpen}});
|
||||
}
|
||||
|
||||
createCard(category, color, icon, title, content) {
|
||||
|
||||
let alerts = [];
|
||||
for (let i = 0; i < this.state[category].alerts.length; i++) {
|
||||
alerts.push(<Alert key={"alert-" + i}
|
||||
onClose={() => this.removeError(i, category)} {...this.state[category].alerts[i]}/>)
|
||||
}
|
||||
|
||||
return <div className={"card card-" + color} key={"card-" + category}>
|
||||
<div className={"card-header"} style={{cursor: "pointer"}}
|
||||
onClick={() => this.toggleCollapse(category)}>
|
||||
<h4 className={"card-title"}>
|
||||
<Icon className={"mr-2"} icon={icon} type={icon==="google"?"fab":"fas"} />
|
||||
{title}
|
||||
</h4>
|
||||
<div className={"card-tools"}>
|
||||
<span className={"btn btn-tool btn-sm"}>
|
||||
<Icon icon={this.state[category].isOpen ? "angle-up" : "angle-down"}/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse isOpened={this.state[category].isOpen}>
|
||||
<div className={"card-body"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-12 col-lg-6"}>
|
||||
{alerts}
|
||||
{content}
|
||||
<div>
|
||||
<button className={"btn btn-secondary"} onClick={() => this.onReset(category)}
|
||||
disabled={this.state[category].isResetting || this.state[category].isSaving}>
|
||||
{this.state[category].isResetting ?
|
||||
<span>Resetting <Icon icon={"circle-notch"}/></span> : "Reset"}
|
||||
</button>
|
||||
<button className={"btn btn-success ml-2"} onClick={() => this.onSave(category)}
|
||||
disabled={this.state[category].isResetting || this.state[category].isSaving}>
|
||||
{this.state[category].isSaving ?
|
||||
<span>Saving <Icon icon={"circle-notch"}/></span> : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
|
||||
createGeneralForm() {
|
||||
return <>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"site_name"}>Site Name</label>
|
||||
<input type={"text"} className={"form-control"}
|
||||
value={this.state.settings["site_name"] ?? ""}
|
||||
placeholder={"Enter a title"} name={"site_name"} id={"site_name"}
|
||||
onChange={this.onChangeValue.bind(this)}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"base_url"}>Base URL</label>
|
||||
<input type={"text"} className={"form-control"}
|
||||
value={this.state.settings["base_url"] ?? ""}
|
||||
placeholder={"Enter a url"} name={"base_url"} id={"base_url"}
|
||||
onChange={this.onChangeValue.bind(this)}/>
|
||||
</div>
|
||||
<div className={"form-group"}>
|
||||
<label htmlFor={"user_registration_enabled"}>User Registration</label>
|
||||
<div className={"form-check"}>
|
||||
<input type={"checkbox"} className={"form-check-input"}
|
||||
name={"user_registration_enabled"}
|
||||
id={"user_registration_enabled"}
|
||||
checked={(this.state.settings["user_registration_enabled"] ?? "0") === "1"}
|
||||
onChange={this.onChangeValue.bind(this)}/>
|
||||
<label className={"form-check-label"}
|
||||
htmlFor={"user_registration_enabled"}>
|
||||
Allow anyone to register an account
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
createMailForm() {
|
||||
return <>
|
||||
<div className={"form-group mt-2"}>
|
||||
<div className={"form-check"}>
|
||||
<input type={"checkbox"} className={"form-check-input"}
|
||||
name={"mail_enabled"} id={"mail_enabled"}
|
||||
checked={(this.state.settings["mail_enabled"] ?? "0") === "1"}
|
||||
onChange={this.onChangeValue.bind(this)}/>
|
||||
<label className={"form-check-label"} htmlFor={"mail_enabled"}>
|
||||
Enable E-Mail service
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr className={"m-3"}/>
|
||||
<label htmlFor={"mail_username"}>Username</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>
|
||||
<Icon icon={"hashtag"}/>
|
||||
</span>
|
||||
</div>
|
||||
<input type={"text"} className={"form-control"}
|
||||
value={this.state.settings["mail_username"] ?? ""}
|
||||
placeholder={"Enter a username"} name={"mail_username"}
|
||||
id={"mail_username"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
<label htmlFor={"mail_password"} className={"mt-2"}>Password</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>
|
||||
<Icon icon={"key"}/>
|
||||
</span>
|
||||
</div>
|
||||
<input type={"password"} className={"form-control"}
|
||||
value={this.state.settings["mail_password"] ?? ""}
|
||||
placeholder={"(unchanged)"} name={"mail_password"}
|
||||
id={"mail_password"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
<label htmlFor={"mail_from"} className={"mt-2"}>Sender Email Address</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>@</span>
|
||||
</div>
|
||||
<input type={"email"} className={"form-control"}
|
||||
value={this.state.settings["mail_from"] ?? ""}
|
||||
placeholder={"Enter a email address"} name={"mail_from"}
|
||||
id={"mail_from"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<div className={"col-6"}>
|
||||
<label htmlFor={"mail_host"} className={"mt-2"}>SMTP Host</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>
|
||||
<Icon icon={"project-diagram"}/>
|
||||
</span>
|
||||
</div>
|
||||
<input type={"text"} className={"form-control"}
|
||||
value={this.state.settings["mail_host"] ?? ""}
|
||||
placeholder={"e.g. smtp.example.com"} name={"mail_host"}
|
||||
id={"mail_host"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-6"}>
|
||||
<label htmlFor={"mail_port"} className={"mt-2"}>SMTP Port</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>
|
||||
<Icon icon={"project-diagram"}/>
|
||||
</span>
|
||||
</div>
|
||||
<input type={"number"} className={"form-control"}
|
||||
value={parseInt(this.state.settings["mail_port"] ?? "25")}
|
||||
placeholder={"smtp port"} name={"mail_port"}
|
||||
id={"mail_port"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"mt-3"}>
|
||||
<label htmlFor={"mail_from"} className={"mt-2"}>Send Test E-Mail</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>@</span>
|
||||
</div>
|
||||
<input type={"email"} className={"form-control"}
|
||||
value={this.state.mail.test_email}
|
||||
placeholder={"Enter a email address"}
|
||||
onChange={(e) => this.setState({
|
||||
...this.state,
|
||||
mail: {...this.state.mail, test_email: e.target.value},
|
||||
})}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
<div className={"form-group form-inline mt-3"}>
|
||||
<button className={"btn btn-info col-2"}
|
||||
onClick={() => this.onSendTestMail()}
|
||||
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1" || this.state.mail.isSending}>
|
||||
{this.state.mail.isSending ?
|
||||
<span>Sending <Icon icon={"circle-notch"}/></span> : "Send Mail"}
|
||||
</button>
|
||||
|
||||
<div className={"col-10"}>
|
||||
{this.state.mail.unsavedMailSettings ?
|
||||
<span className={"text-red"}>You need to save your mail settings first.</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
getRecaptchaForm() {
|
||||
return <>
|
||||
<div className={"form-group mt-2"}>
|
||||
<div className={"form-check"}>
|
||||
<input type={"checkbox"} className={"form-check-input"}
|
||||
name={"recaptcha_enabled"} id={"recaptcha_enabled"}
|
||||
checked={(this.state.settings["recaptcha_enabled"] ?? "0") === "1"}
|
||||
onChange={this.onChangeValue.bind(this)}/>
|
||||
<label className={"form-check-label"} htmlFor={"recaptcha_enabled"}>
|
||||
Enable Google's reCaptcha
|
||||
<span className={"ml-2"}>
|
||||
(<a href={"https://www.google.com/recaptcha/intro/v3.html"} target={"_blank"} rel={"noopener noreferrer"}>
|
||||
More Info
|
||||
<sup><small><Icon icon={"external-link-alt"} className={"ml-1"}/></small></sup>
|
||||
</a>)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr className={"m-2"}/>
|
||||
<label htmlFor={"recaptcha_public_key"} className={"mt-2"}>reCaptcha Site Key</label>
|
||||
<div className={"input-group"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>
|
||||
<Icon icon={"unlock"}/>
|
||||
</span>
|
||||
</div>
|
||||
<input type={"text"} className={"form-control"}
|
||||
value={this.state.settings["recaptcha_public_key"] ?? ""}
|
||||
placeholder={"Enter site key"} name={"recaptcha_public_key"}
|
||||
id={"recaptcha_public_key"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["recaptcha_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
<label htmlFor={"recaptcha_private_key"} className={"mt-2"}>reCaptcha Secret Key</label>
|
||||
<div className={"input-group mb-3"}>
|
||||
<div className={"input-group-prepend"}>
|
||||
<span className={"input-group-text"}>
|
||||
<Icon icon={"lock"}/>
|
||||
</span>
|
||||
</div>
|
||||
<input type={"password"} className={"form-control"}
|
||||
value={this.state.settings["recaptcha_private_key"] ?? ""}
|
||||
placeholder={"(unchanged)"} name={"recaptcha_private_key"}
|
||||
id={"mail_password"} onChange={this.onChangeValue.bind(this)}
|
||||
disabled={(this.state.settings["recaptcha_enabled"] ?? "0") !== "1"}/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
getUncategorizedForm() {
|
||||
let tr = [];
|
||||
|
||||
for(let i = 0; i < this.state.uncategorised.settings.length; i++) {
|
||||
let key = this.state.uncategorised.settings[i].key;
|
||||
let value = this.state.uncategorised.settings[i].value;
|
||||
tr.push(
|
||||
<tr key={"uncategorised-" + i} className={(i % 2 === 0) ? "even" : "odd"}>
|
||||
<td>
|
||||
<input className={"form-control"} type={"text"} value={key} maxLength={32} placeholder={"Key"}
|
||||
onChange={(e) => this.onChangeUncategorisedValue(e, i, true)} />
|
||||
</td>
|
||||
<td>
|
||||
<input className={"form-control"} type={"text"} value={value} placeholder={"value"}
|
||||
onChange={(e) => this.onChangeUncategorisedValue(e, i, false)} />
|
||||
</td>
|
||||
<td className={"text-center align-middle"}>
|
||||
<ReactTooltip id={"tooltip-uncategorised-" + i} />
|
||||
<Icon icon={"trash"} className={"text-danger"} style={{cursor: "pointer"}}
|
||||
onClick={() => this.onDeleteUncategorisedProp(i)} data-type={"error"}
|
||||
data-tip={"Delete property"} data-place={"right"} data-effect={"solid"}
|
||||
data-for={"tooltip-uncategorised-" + i}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<table className={"table table-bordered table-hover dataTable dtr-inline"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<th className={"text-center"}><Icon icon={"tools"}/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tr}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={"mt-2 mb-3"}>
|
||||
<button className={"btn btn-info"} onClick={() => this.onAddUncategorisedProperty()} >
|
||||
<Icon icon={"plus"} className={"mr-2"} /> Add property
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let errors = [];
|
||||
for (let i = 0; i < this.state.errors.length; i++) {
|
||||
errors.push(<Alert key={"error-" + i}
|
||||
onClose={() => this.removeError("errors", i)} {...this.state.errors[i]}/>)
|
||||
}
|
||||
|
||||
const categories = {
|
||||
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
|
||||
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
|
||||
"recaptcha": {color: "danger", icon: "google", title: "Google reCaptcha", content: this.getRecaptchaForm()},
|
||||
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
|
||||
};
|
||||
|
||||
let cards = [];
|
||||
for (let name in categories) {
|
||||
let category = categories[name];
|
||||
cards.push(this.createCard(name, category.color, category.icon, category.title, category.content));
|
||||
}
|
||||
|
||||
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">Settings</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">Settings</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
{errors}
|
||||
<div>
|
||||
{cards}
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip />
|
||||
</>
|
||||
}
|
||||
|
||||
onChangeValue(event) {
|
||||
const target = event.target;
|
||||
const name = target.name;
|
||||
const type = target.type;
|
||||
let value = target.value;
|
||||
|
||||
if (type === "checkbox") {
|
||||
value = event.target.checked ? "1" : "0";
|
||||
}
|
||||
|
||||
let changedMailSettings = false;
|
||||
if (this.state.mail.keys.includes(name)) {
|
||||
changedMailSettings = true;
|
||||
}
|
||||
|
||||
let newState = {...this.state, settings: {...this.state.settings, [name]: value}};
|
||||
if (changedMailSettings) {
|
||||
newState.mail = {...this.state.mail, unsavedMailSettings: true};
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
onReset(category) {
|
||||
this.setState({...this.state, [category]: {...this.state[category], isResetting: true}});
|
||||
|
||||
this.parent.api.getSettings().then((res) => {
|
||||
if (!res.success) {
|
||||
let alerts = this.state[category].alerts.slice();
|
||||
alerts.push({title: "Error fetching settings", message: res.msg});
|
||||
this.setState({
|
||||
...this.state,
|
||||
[category]: {...this.state[category], alerts: alerts, isResetting: false}
|
||||
});
|
||||
} else {
|
||||
let newState = { ...this.state };
|
||||
let categoryUpdated = {...this.state[category], isResetting: false};
|
||||
let newSettings = {...this.state.settings};
|
||||
|
||||
if (category === "uncategorised") {
|
||||
categoryUpdated.settings = this.getUncategorisedValues(res);
|
||||
for (let key in res.settings) {
|
||||
if (res.settings.hasOwnProperty(key) && !this.isDefaultKey(key)) {
|
||||
newSettings[key] = res.settings[key] ?? "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let key of this.state[category].keys) {
|
||||
newSettings[key] = res.settings[key] ?? "";
|
||||
}
|
||||
|
||||
if (category === "mail") {
|
||||
categoryUpdated.unsavedMailSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
newState.settings = newSettings;
|
||||
newState[category] = categoryUpdated;
|
||||
this.setState(newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSave(category) {
|
||||
this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
|
||||
|
||||
let values = {};
|
||||
if (category === "uncategorised") {
|
||||
for (let prop of this.state.uncategorised.settings) {
|
||||
if (prop.key) {
|
||||
values[prop.key] = prop.value;
|
||||
if (this.isDefaultKey(prop.key)) {
|
||||
this.parent.showDialog("You cannot use this key as property key: " + prop.key, "System specific key");
|
||||
this.setState({...this.state, [category]: {...this.state[category], isSaving: false}});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let key in this.state.settings) {
|
||||
if (this.state.settings.hasOwnProperty(key) && !this.isDefaultKey(key) && !values.hasOwnProperty(key)) {
|
||||
values[key] = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let key of this.state[category].keys) {
|
||||
if (this.hiddenKeys.includes(key) && !this.state.settings[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
values[key] = this.state.settings[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.parent.api.saveSettings(values).then((res) => {
|
||||
let alerts = this.state[category].alerts.slice();
|
||||
let categoryUpdated = {...this.state[category], isSaving: false};
|
||||
|
||||
if (!res.success) {
|
||||
alerts.push({title: "Error saving settings", message: res.msg});
|
||||
} else {
|
||||
alerts.push({title: "Success", message: "Settings were successfully saved.", type: "success"});
|
||||
if (category === "mail") categoryUpdated.unsavedMailSettings = false;
|
||||
this.setState({...this.state, [category]: categoryUpdated});
|
||||
}
|
||||
|
||||
categoryUpdated.alerts = alerts;
|
||||
this.setState({...this.state, [category]: categoryUpdated});
|
||||
});
|
||||
}
|
||||
|
||||
onSendTestMail() {
|
||||
this.setState({...this.state, mail: {...this.state.mail, isSending: true}});
|
||||
|
||||
this.parent.api.sendTestMail(this.state.mail.test_email).then((res) => {
|
||||
let alerts = this.state.mail.alerts.slice();
|
||||
let newState = {...this.state.mail, isSending: false};
|
||||
if (!res.success) {
|
||||
alerts.push({title: "Error sending email", message: res.msg});
|
||||
} else {
|
||||
alerts.push({
|
||||
title: "Success!",
|
||||
message: "E-Mail was successfully sent, check your inbox.",
|
||||
type: "success"
|
||||
});
|
||||
newState.test_email = "";
|
||||
}
|
||||
|
||||
newState.alerts = alerts;
|
||||
this.setState({...this.state, mail: newState});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,418 +0,0 @@
|
||||
import * as React from "react";
|
||||
import Icon from "../elements/icon";
|
||||
import {Link} from "react-router-dom";
|
||||
import {getPeriodString} from "../global";
|
||||
import Alert from "../elements/alert";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
const TABLE_SIZE = 10;
|
||||
|
||||
export default class UserOverview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parent = {
|
||||
showDialog: props.showDialog || function () { },
|
||||
api: props.api,
|
||||
};
|
||||
this.state = {
|
||||
loaded: false,
|
||||
users: {
|
||||
data: {},
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
totalCount: 0,
|
||||
},
|
||||
groups: {
|
||||
data: {},
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
totalCount: 0,
|
||||
},
|
||||
errors: [],
|
||||
rowCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
fetchGroups(page) {
|
||||
page = page || this.state.groups.page;
|
||||
this.setState({...this.state, groups: {...this.state.groups, data: {}, page: 1, totalCount: 0}});
|
||||
this.parent.api.fetchGroups(page, TABLE_SIZE).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
groups: {
|
||||
data: res.groups,
|
||||
pageCount: res.pageCount,
|
||||
page: page,
|
||||
totalCount: res.totalCount,
|
||||
},
|
||||
rowCount: Math.max(this.state.rowCount, Object.keys(res.groups).length)
|
||||
});
|
||||
} else {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({title: "Error fetching groups", message: res.msg});
|
||||
this.setState({
|
||||
...this.state,
|
||||
errors: errors
|
||||
});
|
||||
}
|
||||
if (!this.state.loaded) {
|
||||
this.fetchUsers(1)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchUsers(page) {
|
||||
page = page || this.state.users.page;
|
||||
this.setState({...this.state, users: {...this.state.users, data: {}, pageCount: 1, totalCount: 0}});
|
||||
this.parent.api.fetchUsers(page, TABLE_SIZE).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loaded: true,
|
||||
users: {
|
||||
data: res.users,
|
||||
pageCount: res.pageCount,
|
||||
page: page,
|
||||
totalCount: res.totalCount,
|
||||
},
|
||||
rowCount: Math.max(this.state.rowCount, Object.keys(res.users).length)
|
||||
});
|
||||
} else {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({title: "Error fetching users", message: res.msg});
|
||||
this.setState({
|
||||
...this.state,
|
||||
loaded: true,
|
||||
errors: errors
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({...this.state, loaded: false});
|
||||
this.fetchGroups(1);
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (!this.state.loaded) {
|
||||
return <div className={"text-center mt-4"}>
|
||||
<h3>Loading… <Icon icon={"spinner"}/></h3>
|
||||
</div>
|
||||
}
|
||||
|
||||
let errors = [];
|
||||
for (let i = 0; i < this.state.errors.length; i++) {
|
||||
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
|
||||
}
|
||||
|
||||
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">Users & 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">Users</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
{errors}
|
||||
<div className={"content-fluid"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-6"}>
|
||||
{this.createUserCard()}
|
||||
</div>
|
||||
<div className={"col-lg-6"}>
|
||||
{this.createGroupCard()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<div className={"col-12"}>
|
||||
<Link to={"/admin/user/permissions"} className={"btn btn-primary"}>
|
||||
<Icon icon={"user-check"} className={"mr-2"}/>
|
||||
Edit Permissions
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip />
|
||||
</>;
|
||||
}
|
||||
|
||||
createUserCard() {
|
||||
|
||||
let userRows = [];
|
||||
for (let id in this.state.users.data) {
|
||||
if (!this.state.users.data.hasOwnProperty(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = this.state.users.data[id];
|
||||
let confirmedIcon = <Icon icon={user["confirmed"] ? "check" : "times"}/>;
|
||||
|
||||
let groups = [];
|
||||
|
||||
for (let groupId in user.groups) {
|
||||
if (user.groups.hasOwnProperty(groupId)) {
|
||||
let groupName = user.groups[groupId].name;
|
||||
let groupColor = user.groups[groupId].color;
|
||||
groups.push(
|
||||
<span key={"group-" + groupId} className={"mr-1 badge text-white"} style={{backgroundColor: groupColor}}>
|
||||
{groupName}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
userRows.push(
|
||||
<tr key={"user-" + id}>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{groups}</td>
|
||||
<td>
|
||||
<span data-effect={"solid"}
|
||||
data-tip={user["registered_at"]}
|
||||
data-place={"bottom"}>
|
||||
{getPeriodString(user["registered_at"])}
|
||||
</span>
|
||||
</td>
|
||||
<td className={"text-center"}>{confirmedIcon}</td>
|
||||
<td>
|
||||
<Link to={"/admin/user/edit/" + id} className={"text-reset"}>
|
||||
<Icon icon={"pencil-alt"} data-effect={"solid"}
|
||||
data-tip={"Modify user details & group membership"}
|
||||
data-type={"info"} data-place={"right"}/>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
while(userRows.length < this.state.rowCount) {
|
||||
userRows.push(
|
||||
<tr key={"empty-row-" + userRows.length}>
|
||||
<td> </td>
|
||||
<td/>
|
||||
<td/>
|
||||
<td/>
|
||||
<td/>
|
||||
<td/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
let pages = [];
|
||||
let previousDisabled = (this.state.users.page === 1 ? " disabled" : "");
|
||||
let nextDisabled = (this.state.users.page >= this.state.users.pageCount ? " disabled" : "");
|
||||
|
||||
for (let i = 1; i <= this.state.users.pageCount; i++) {
|
||||
let active = (this.state.users.page === i ? " active" : "");
|
||||
pages.push(
|
||||
<li key={"page-" + i} className={"page-item" + active}>
|
||||
<a className={"page-link"} href={"#"} onClick={() => { if (this.state.users.page !== i) this.fetchUsers(i) }}>
|
||||
{i}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={"card"}>
|
||||
<div className={"card-header border-0"}>
|
||||
<h3 className={"card-title"}>Users</h3>
|
||||
<div className={"card-tools"}>
|
||||
<Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/user/add"} >
|
||||
<Icon icon={"plus"}/>
|
||||
</Link>
|
||||
<a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchUsers()}>
|
||||
<Icon icon={"sync"}/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"card-body table-responsive p-0"}>
|
||||
<table className={"table table-striped table-valign-middle"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Groups</th>
|
||||
<th>Registered</th>
|
||||
<th className={"text-center"}>Confirmed</th>
|
||||
<th><Icon icon={"tools"} /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userRows}
|
||||
</tbody>
|
||||
</table>
|
||||
<nav className={"row m-0"}>
|
||||
<div className={"col-6 pl-3 pt-3 pb-3 text-muted"}>
|
||||
Total: {this.state.users.totalCount}
|
||||
</div>
|
||||
<div className={"col-6 p-0"}>
|
||||
<ul className={"pagination p-2 m-0 justify-content-end"}>
|
||||
<li className={"page-item" + previousDisabled}>
|
||||
<a className={"page-link"} href={"#"}
|
||||
onClick={() => this.fetchUsers(this.state.users.page - 1)}>
|
||||
Previous
|
||||
</a>
|
||||
</li>
|
||||
{pages}
|
||||
<li className={"page-item" + nextDisabled}>
|
||||
<a className={"page-link"} href={"#"}
|
||||
onClick={() => this.fetchUsers(this.state.users.page + 1)}>
|
||||
Next
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
createGroupCard() {
|
||||
let groupRows = [];
|
||||
for (let id in this.state.groups.data) {
|
||||
if (!this.state.groups.data.hasOwnProperty(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let group = this.state.groups.data[id];
|
||||
|
||||
groupRows.push(
|
||||
<tr key={"group-" + id}>
|
||||
<td>{group.name}</td>
|
||||
<td className={"text-center"}>{group["memberCount"]}</td>
|
||||
<td>
|
||||
<span className={"badge text-white mr-1"} style={{backgroundColor: group.color, fontFamily: "monospace"}}>
|
||||
{group.color}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<Icon icon={"trash"} style={{color: "red", cursor: "pointer"}}
|
||||
onClick={(e) => this.onDeleteGroup(e, id)} data-effect={"solid"}
|
||||
data-tip={"Delete"} data-type={"error"}
|
||||
data-place={"bottom"}/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
while(groupRows.length < this.state.rowCount) {
|
||||
groupRows.push(
|
||||
<tr key={"empty-row-" + groupRows.length}>
|
||||
<td> </td>
|
||||
<td/>
|
||||
<td/>
|
||||
<td/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
let pages = [];
|
||||
let previousDisabled = (this.state.groups.page === 1 ? " disabled" : "");
|
||||
let nextDisabled = (this.state.groups.page >= this.state.groups.pageCount ? " disabled" : "");
|
||||
|
||||
for (let i = 1; i <= this.state.groups.pageCount; i++) {
|
||||
let active = (this.state.groups.page === i ? " active" : "");
|
||||
pages.push(
|
||||
<li key={"page-" + i} className={"page-item" + active}>
|
||||
<a className={"page-link"} href={"#"} onClick={() => { if (this.state.groups.page !== i) this.fetchGroups(i) }}>
|
||||
{i}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={"card"}>
|
||||
<div className={"card-header border-0"}>
|
||||
<h3 className={"card-title"}>Groups</h3>
|
||||
<div className={"card-tools"}>
|
||||
<Link href={"#"} className={"btn btn-tool btn-sm"} to={"/admin/group/add"} >
|
||||
<Icon icon={"plus"}/>
|
||||
</Link>
|
||||
<a href={"#"} className={"btn btn-tool btn-sm"} onClick={() => this.fetchGroups()}>
|
||||
<Icon icon={"sync"}/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"card-body table-responsive p-0"}>
|
||||
<table className={"table table-striped table-valign-middle"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th className={"text-center"}>Members</th>
|
||||
<th>Color</th>
|
||||
<th><Icon icon={"tools"} /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupRows}
|
||||
</tbody>
|
||||
</table>
|
||||
<nav className={"row m-0"}>
|
||||
<div className={"col-6 pl-3 pt-3 pb-3 text-muted"}>
|
||||
Total: {this.state.groups.totalCount}
|
||||
</div>
|
||||
<div className={"col-6 p-0"}>
|
||||
<ul className={"pagination p-2 m-0 justify-content-end"}>
|
||||
<li className={"page-item" + previousDisabled}>
|
||||
<a className={"page-link"} href={"#"}
|
||||
onClick={() => this.fetchGroups(this.state.groups.page - 1)}>
|
||||
Previous
|
||||
</a>
|
||||
</li>
|
||||
{pages}
|
||||
<li className={"page-item" + nextDisabled}>
|
||||
<a className={"page-link"} href={"#"}
|
||||
onClick={() => this.fetchGroups(this.state.groups.page + 1)}>
|
||||
Next
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
onDeleteGroup(e, id) {
|
||||
e.stopPropagation();
|
||||
this.parent.showDialog("Are you really sure you want to delete this group?", "Delete Group?", ["Yes", "No"], (btn) => {
|
||||
if (btn === "Yes") {
|
||||
this.parent.api.deleteGroup(id).then((res) => {
|
||||
if (!res.success) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({title: "Error deleting group", message: res.msg});
|
||||
this.setState({
|
||||
...this.state,
|
||||
errors: errors
|
||||
});
|
||||
} else {
|
||||
this.setState({ ...this.state, loaded: false });
|
||||
this.fetchGroups();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
import {Link} from "react-router-dom";
|
||||
import * as React from "react";
|
||||
import Alert from "../elements/alert";
|
||||
import moment from 'moment'
|
||||
import {Bar} from "react-chartjs-2";
|
||||
import DatePicker from "react-datepicker";
|
||||
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
|
||||
export default class Visitors extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
alerts: [],
|
||||
date: new Date(),
|
||||
type: 'monthly',
|
||||
visitors: { }
|
||||
};
|
||||
|
||||
this.parent = {
|
||||
api: props.api,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchData(this.state.type, this.state.date);
|
||||
}
|
||||
|
||||
fetchData(type, date) {
|
||||
this.setState({...this.state, type: type, date: date });
|
||||
this.parent.api.getVisitors(type, moment(date).format("YYYY-MM-DD")).then((res) => {
|
||||
if(!res.success) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.push({ message: res.msg, title: "Error fetching Visitor Statistics" });
|
||||
this.setState({ ...this.state, alerts: alerts });
|
||||
} else {
|
||||
this.setState({
|
||||
...this.state,
|
||||
visitors: res.visitors
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeError(i) {
|
||||
if (i >= 0 && i < this.state.alerts.length) {
|
||||
let alerts = this.state.alerts.slice();
|
||||
alerts.splice(i, 1);
|
||||
this.setState({...this.state, alerts: alerts});
|
||||
}
|
||||
}
|
||||
|
||||
showData(type) {
|
||||
if (type === this.state.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchData(type, this.state.date);
|
||||
}
|
||||
|
||||
createLabels() {
|
||||
if (this.state.type === 'weekly') {
|
||||
return moment.weekdays();
|
||||
} else if(this.state.type === 'monthly') {
|
||||
const numDays = moment().daysInMonth();
|
||||
return Array.from(Array(numDays), (_, i) => i + 1);
|
||||
} else if(this.state.type === 'yearly') {
|
||||
return moment.monthsShort();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
createTitle() {
|
||||
if (this.state.type === 'weekly') {
|
||||
return "Week " + moment(this.state.date).week();
|
||||
} else if(this.state.type === 'monthly') {
|
||||
return moment(this.state.date).format('MMMM');
|
||||
} else if(this.state.type === 'yearly') {
|
||||
return moment(this.state.date).format('YYYY');
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
fillData(data = []) {
|
||||
|
||||
for (let date in this.state.visitors) {
|
||||
if (!this.state.visitors.hasOwnProperty(date)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts = date.split("/");
|
||||
let count = parseInt(this.state.visitors[date]);
|
||||
|
||||
if (this.state.type === 'weekly') {
|
||||
let day = moment(date).day();
|
||||
if (day >= 0 && day < 7) {
|
||||
data[day] = count;
|
||||
}
|
||||
} else if(this.state.type === 'monthly') {
|
||||
let day = parseInt(parts[2]) - 1;
|
||||
if (day >= 0 && day < data.length) {
|
||||
data[day] = count;
|
||||
}
|
||||
} else if(this.state.type === 'yearly') {
|
||||
let month = parseInt(parts[1]) - 1;
|
||||
if (month >= 0 && month < 12) {
|
||||
data[month] = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(date) {
|
||||
this.fetchData(this.state.type, date);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let alerts = [];
|
||||
for (let i = 0; i < this.state.alerts.length; i++) {
|
||||
alerts.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.alerts[i]}/>)
|
||||
}
|
||||
|
||||
const viewTypes = ["Weekly", "Monthly", "Yearly"];
|
||||
let viewOptions = [];
|
||||
for (let type of viewTypes) {
|
||||
let isActive = this.state.type === type.toLowerCase();
|
||||
viewOptions.push(
|
||||
<label key={"option-" + type.toLowerCase()} className={"btn btn-secondary" + (isActive ? " active" : "")}>
|
||||
<input type="radio" autoComplete="off" defaultChecked={isActive} onClick={() => this.showData(type.toLowerCase())} />
|
||||
{type}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
const labels = this.createLabels();
|
||||
let data = new Array(labels.length).fill(0);
|
||||
this.fillData(data);
|
||||
|
||||
let colors = [ '#ff4444', '#ffbb33', '#00C851', '#33b5e5' ];
|
||||
const title = this.createTitle();
|
||||
|
||||
while (colors.length < labels.length) {
|
||||
colors = colors.concat(colors);
|
||||
}
|
||||
|
||||
let chartOptions = {};
|
||||
let chartData = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Unique Visitors ' + title,
|
||||
borderWidth: 1,
|
||||
data: data,
|
||||
backgroundColor: colors
|
||||
}],
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
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"}>Visitor Statistics</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">Visitors</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
{alerts}
|
||||
<div className={"row"}>
|
||||
<div className={"col-4"}>
|
||||
<p className={"mb-1 lead"}>Show data…</p>
|
||||
<div className="btn-group btn-group-toggle" data-toggle="buttons">
|
||||
{viewOptions}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"col-4"}>
|
||||
<p className={"mb-1 lead"}>Select date…</p>
|
||||
<DatePicker className={"text-center"} selected={this.state.date} onChange={(d) => this.handleChange(d)}
|
||||
showMonthYearPicker={this.state.type === "monthly"}
|
||||
showYearPicker={this.state.type === "yearly"} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<div className={"col-12"}>
|
||||
<div className="chart p-3">
|
||||
<Bar data={chartData} options={chartOptions} height={100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user