src -> adminPanel

This commit is contained in:
2021-01-07 14:55:35 +01:00
parent 9730c97683
commit 810f51bdcc
29 changed files with 0 additions and 0 deletions

3
adminPanel/.babelrc Normal file
View File

@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

23
adminPanel/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# 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*

15045
adminPanel/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

51
adminPanel/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"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": "^3.4.1",
"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"
},
"eslintConfig": {
"extends": "react-app"
},
"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"
}
}

37
adminPanel/src/404.js Normal file
View File

@@ -0,0 +1,37 @@
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>
)
}

121
adminPanel/src/api.js Normal file
View File

@@ -0,0 +1,121 @@
import 'babel-polyfill';
export default class API {
constructor() {
this.loggedIn = false;
this.user = { };
}
csrfToken() {
return this.loggedIn ? this.user.session.csrf_token : null;
}
async apiCall(method, params) {
params = params || { };
params.csrf_token = this.csrfToken();
let response = await fetch("/api/" + method, {
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(params)
});
let res = await response.json();
if (!res.success && res.msg === "You are not logged in.") {
document.location.reload();
}
return res;
}
async fetchUser() {
let response = await fetch("/api/user/info");
let data = await response.json();
this.user = data["user"];
this.loggedIn = data["loggedIn"];
return data && data.success && data.loggedIn;
}
async editUser(id, username, email, password, groups, confirmed) {
return this.apiCall("user/edit", { id: id, username: username, email: email, password: password, groups: groups, confirmed: confirmed });
}
async logout() {
return this.apiCall("user/logout");
}
async getNotifications(onlyNew = true) {
return this.apiCall("notifications/fetch", { new: onlyNew });
}
async markNotificationsSeen() {
return this.apiCall("notifications/seen");
}
async getUser(id) {
return this.apiCall("user/get", { id: id });
}
async deleteUser(id) {
return this.apiCall("user/delete", { id: id });
}
async fetchUsers(pageNum = 1, count = 20) {
return this.apiCall("user/fetch", { page: pageNum, count: count });
}
async fetchGroups(pageNum = 1, count = 20) {
return this.apiCall("groups/fetch", { page: pageNum, count: count });
}
async inviteUser(username, email) {
return this.apiCall("user/invite", { username: username, email: email });
}
async createUser(username, email, password, confirmPassword) {
return this.apiCall("user/create", { username: username, email: email, password: password, confirmPassword: confirmPassword });
}
async getStats() {
return this.apiCall("stats");
}
async getRoutes() {
return this.apiCall("routes/fetch");
}
async saveRoutes(routes) {
return this.apiCall("routes/save", { routes: routes });
}
async createGroup(name, color) {
return this.apiCall("groups/create", { name: name, color: color });
}
async deleteGroup(id) {
return this.apiCall("groups/delete", { uid: id });
}
async getSettings(key = "") {
return this.apiCall("settings/get", { key: key });
}
async saveSettings(settings) {
return this.apiCall("settings/set", { settings: settings });
}
async sendTestMail(receiver) {
return this.apiCall("mail/test", { receiver: receiver });
}
async fetchPermissions() {
return this.apiCall("permission/fetch");
}
async savePermissions(permissions) {
return this.apiCall("permission/save", { permissions: permissions });
}
async getVisitors(type, date) {
return this.apiCall("visitors/stats", { type: type, date: date });
}
};

View File

@@ -0,0 +1,25 @@
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>
)
}

View File

@@ -0,0 +1,45 @@
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>
);
}

View File

@@ -0,0 +1,24 @@
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} />
);
}

11
adminPanel/src/footer.js Normal file
View File

@@ -0,0 +1,11 @@
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>&nbsp;
CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> 1.0.3
</footer>
)
}

26
adminPanel/src/global.js Normal file
View File

@@ -0,0 +1,26 @@
import moment from 'moment';
function getPeriodString(date) {
return moment(date).fromNow();
}
export default function humanReadableSize(bytes, dp = 1) {
const thresh = 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10**dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
export { getPeriodString };

102
adminPanel/src/header.js Normal file
View File

@@ -0,0 +1,102 @@
import * as React from "react";
import Icon from "./elements/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 uid = notification.uid;
const createdAt = getPeriodString(notification["created_at"]);
notificationItems.push(
<Link to={"/admin/logs?notification=" + uid} className={"dropdown-item"} key={"notification-" + uid}>
{mailIcon}
<span className={"ml-2"}>{notification.title}</span>
<span className={"float-right text-muted text-sm"}>{createdAt}</span>
</Link>);
}
function onToggleSidebar() {
console.log(document.body.classList);
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>
)
}

12
adminPanel/src/include/adminlte.min.css vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
.page-link { color: #222629; }
.page-link:hover { color: black; }
.ReactCollapse--collapse {
transition: height 500ms;
}

View File

File diff suppressed because one or more lines are too long

117
adminPanel/src/index.js Normal file
View File

@@ -0,0 +1,117 @@
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 './header.js';
import Sidebar from './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 "./404";
import Logs from "./views/logs";
import PageOverview from "./views/pages";
import HelpPage from "./views/help";
import Footer from "./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";
class AdminDashboard extends React.Component {
constructor(props) {
super(props);
this.api = new API();
this.state = {
loaded: false,
dialog: { onClose: () => this.hideDialog() },
notifications: [ ]
};
}
onUpdate() {
this.fetchNotifications();
}
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 });
}
});
}
componentDidMount() {
this.api.fetchUser().then(Success => {
if (!Success) {
document.location = "/admin";
} else {
this.fetchNotifications();
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} />
<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 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')
);

116
adminPanel/src/sidebar.js Normal file
View File

@@ -0,0 +1,116 @@
import React from 'react';
import Icon from "./elements/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 || [ ]
};
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"
},
"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 };
}
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>
)
}

View File

@@ -0,0 +1,133 @@
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"}/>
&nbsp;Back
</Link>
{this.state.isSubmitting
?
<button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading&nbsp;<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});
}
});
}
}

View File

@@ -0,0 +1,200 @@
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"}/>
&nbsp;Back
</Link>
{ this.state.isSubmitting
? <button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading&nbsp;<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 });
}
}

View File

@@ -0,0 +1,322 @@
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&nbsp;
<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"}/>
&nbsp;Back
</Link>
{ this.state.isSaving
? <button type={"submit"} className={"btn btn-primary mt-2 mr-2"} disabled>Saving&nbsp;<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&nbsp;<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>
</>
}
}

View File

@@ -0,0 +1,141 @@
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"}>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>
</>
)
}

View File

@@ -0,0 +1,133 @@
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.uid}>
<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>
</>;
}
}

View File

@@ -0,0 +1,247 @@
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>
</>
}
}

View File

@@ -0,0 +1,274 @@
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>
<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&nbsp;
<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&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"The action to be taken"}
data-type={"info"} data-place={"bottom"}/>
</th>
<th>
Target&nbsp;
<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&nbsp;
<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&nbsp;
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
data-tip={"True, if the route is currently active."}
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"}/>&nbsp;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&nbsp;<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&nbsp;<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
});
}
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 });
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);
}
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});
}
});
}
}

View File

@@ -0,0 +1,262 @@
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"}/>
&nbsp;Back
</Link>
<button className={"btn btn-info ml-2"} onClick={() => this.onAddPermission()} disabled={this.state.isResetting || this.state.isSaving}>
<Icon icon={"plus"}/>&nbsp;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&nbsp;<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&nbsp;<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 })
}
};

View File

@@ -0,0 +1,743 @@
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 { EditorState, ContentState, convertToRaw } from 'draft-js'
import { Editor } from 'react-draft-wysiwyg'
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import sanitizeHtml from 'sanitize-html'
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"]
},
messages: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
editor: EditorState.createEmpty(),
isEditing: null,
keys: ["message_confirm_email", "message_accept_invite", "message_reset_password"]
},
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",
"jwt_secret"
];
}
isDefaultKey(key) {
key = key.trim();
return this.state.general.keys.includes(key)
|| this.state.mail.keys.includes(key)
|| this.state.messages.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&nbsp;<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&nbsp;<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&nbsp;<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>
</>
}
getMessagesForm() {
const editor = <Editor
editorState={this.state.messages.editor}
onEditorStateChange={this.onEditorStateChange.bind(this)}
/>;
let messageTemplates = {
"message_confirm_email": "Confirm E-Mail Message",
"message_accept_invite": "Accept Invitation Message",
"message_reset_password": "Reset Password Message",
};
let formGroups = [];
for (let key in messageTemplates) {
let title = messageTemplates[key];
if (this.state.messages.isEditing === key) {
formGroups.push(
<div className={"form-group"} key={"group-" + key}>
<label htmlFor={key}>
{ title }
<ReactTooltip id={"tooltip-" + key} />
<Icon icon={"times"} className={"ml-2 text-danger"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(false)} data-type={"error"}
data-tip={"Discard Changes"} data-place={"top"} data-effect={"solid"}
data-for={"tooltip-" + key}
/>
<Icon icon={"check"} className={"ml-2 text-success"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(true)} data-type={"success"}
data-tip={"Save Changes"} data-place={"top"} data-effect={"solid"}
data-for={"tooltip-" + key}
/>
</label>
{ editor }
</div>
);
} else {
formGroups.push(
<div className={"form-group"} key={"group-" + key}>
<ReactTooltip id={"tooltip-" + key} />
<label htmlFor={key}>
{ title }
<Icon icon={"pencil-alt"} className={"ml-2"} style={{cursor: "pointer"}}
onClick={() => this.openEditor(key)} data-type={"info"}
data-tip={"Edit Template"} data-place={"top"} data-effect={"solid"}
data-for={"tooltip-" + key}
/>
</label>
<div className={"p-2 text-black"} style={{backgroundColor: "#d2d6de"}} dangerouslySetInnerHTML={{ __html: sanitizeHtml(this.state.settings[key] ?? "") }} />
</div>
);
}
}
return formGroups;
}
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"}>
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()},
"messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()},
"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 />
</>
}
onEditorStateChange(editorState) {
this.setState({
...this.state,
messages: {
...this.state.messages,
editor: editorState
}
});
};
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;
} else if (category === "messages") {
categoryUpdated.isEditing = null;
}
}
newState.settings = newSettings;
newState[category] = categoryUpdated;
this.setState(newState);
}
});
}
onSave(category) {
this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
if (category === "messages" && this.state.messages.isEditing) {
this.closeEditor(true, () => this.onSave(category));
}
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}});
console.log(this.state.mail);
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});
});
}
closeEditor(save, callback = null) {
if (this.state.messages.isEditing) {
const key = this.state.messages.isEditing;
let newState = { ...this.state, messages: {...this.state.messages, isEditing: null }};
if (save) {
newState.settings = {
...this.state.settings,
[key]: draftToHtml(convertToRaw(this.state.messages.editor.getCurrentContent())),
};
}
callback = callback || function () { };
this.setState(newState, callback);
}
}
openEditor(message) {
this.closeEditor(true);
const contentBlock = htmlToDraft(this.state.settings[message] ?? "");
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
this.setState({
...this.state,
messages: {
...this.state.messages,
isEditing: message,
editor: editorState
}
});
}
}
}

View File

@@ -0,0 +1,418 @@
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&nbsp;<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 uid in this.state.users.data) {
if (!this.state.users.data.hasOwnProperty(uid)) {
continue;
}
let user = this.state.users.data[uid];
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-" + uid}>
<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/" + uid} 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>&nbsp;</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 uid in this.state.groups.data) {
if (!this.state.groups.data.hasOwnProperty(uid)) {
continue;
}
let group = this.state.groups.data[uid];
groupRows.push(
<tr key={"group-" + uid}>
<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, uid)} 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>&nbsp;</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, uid) {
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(uid).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();
}
});
}
});
}
}

View File

@@ -0,0 +1,208 @@
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>
</>
}
}

View File

@@ -0,0 +1,17 @@
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};