admin -> src

This commit is contained in:
2020-06-18 15:20:50 +02:00
parent 63fcba9dd9
commit f91567186e
23 changed files with 35 additions and 17 deletions

3
src/.babelrc Normal file
View File

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

23
src/.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*

68
src/README.md Normal file
View File

@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

7493
src/dist/main.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

14680
src/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

46
src/package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "admin",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"chart.js": "^2.9.3",
"moment": "^2.26.0",
"react": "^16.13.1",
"react-chartjs-2": "^2.9.0",
"react-collapse": "^5.0.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-tooltip": "^4.2.7"
},
"scripts": {
"build": "webpack --mode development && cp 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"
}
}

30
src/src/404.js Normal file
View File

@@ -0,0 +1,30 @@
import * as React from "react";
import {Link} from "react-router-dom";
import Icon from "./elements/icon";
export default class View404 extends React.Component {
render() {
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>
}
}

60
src/src/api.js Normal file
View File

@@ -0,0 +1,60 @@
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)
});
return await response.json();
}
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 logout() {
return this.apiCall("user/logout");
}
async getNotifications() {
return this.apiCall("notifications/fetch");
}
async fetchUsers(pageNum = 1) {
return this.apiCall("user/fetch", { page: pageNum });
}
async fetchGroups(pageNum = 1) {
return this.apiCall("groups/fetch", { page: pageNum });
}
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");
}
};

26
src/src/elements/alert.js Normal file
View File

@@ -0,0 +1,26 @@
import Icon from "./icon";
import React from "react";
export default function Alert(props) {
const onClose = props.onClose || function() { };
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"}>
<button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button>
<h5><Icon icon={icon} className={"icon"} /> {title}</h5>
{message}
</div>
)
}

View File

@@ -0,0 +1,30 @@
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() { };
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 justify-content-between">
<button type="button" className="btn btn-default" data-dismiss="modal" onClick={() => onClose()}>Close</button>
</div>
</div>
</div>
</div>
);
}

24
src/src/elements/icon.js Normal file
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} />
);
}

7
src/src/global.js Normal file
View File

@@ -0,0 +1,7 @@
import moment from 'moment';
function getPeriodString(date) {
return moment(date).fromNow();
}
export { getPeriodString };

90
src/src/header.js Normal file
View File

@@ -0,0 +1,90 @@
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>);
}
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"} data-widget={"pushmenu"} role={"button"}>
<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"}>
{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
src/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;
}

97
src/src/index.js Normal file
View File

@@ -0,0 +1,97 @@
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";
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) {
const props = { show: true, message: message, title: title };
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),
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 exact={true} path={"/admin/users/adduser"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route>
</Switch>
<Dialog {...this.state.dialog}/>
</section>
</div>
</Router>
}
}
ReactDOM.render(
<AdminDashboard />,
document.getElementById('root')
);

112
src/src/sidebar.js Normal file
View File

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

200
src/src/views/adduser.js Normal file
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 });
}
}

41
src/src/views/logs.js Normal file
View File

@@ -0,0 +1,41 @@
import * as React from "react";
import {Link} from "react-router-dom";
export default class Logs extends React.Component {
constructor(props) {
super(props);
}
render() {
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={"content-fluid"}>
<div className={"row"}>
<div className={"col-lg-6"}>
</div>
<div className={"col-lg-6"}>
</div>
</div>
</div>
</div>
</>;
}
}

193
src/src/views/overview.js Normal file
View File

@@ -0,0 +1,193 @@
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";
export default class Overview extends React.Component {
constructor(props) {
super(props);
this.parent = {
showDialog: props.showDialog,
api: props.api,
};
this.state = {
chartVisible : true,
userCount: 0,
notificationCount: 0,
visitors: { },
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,
});
}
});
}
render() {
const colors = [
'#ff4444', '#ffbb33', '#00C851', '#33b5e5',
'#ff4444', '#ffbb33', '#00C851', '#33b5e5',
'#ff4444', '#ffbb33', '#00C851', '#33b5e5'
];
let data = new Array(12).fill(0);
let visitorCount = 0;
for (let date in this.state.visitors) {
let month = parseInt(date) % 100 - 1;
if (month >= 0 && month < 12) {
let count = parseInt(this.state.visitors[date]);
data[month] = count;
visitorCount += count;
}
}
let chartOptions = {};
let chartData = {
labels: moment.monthsShort(),
datasets: [{
label: 'Unique Visitors ' + moment().year(),
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]}/>)
}
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>{visitorCount}</h3>
<p>Unique Visitors</p>
</div>
<div className="icon">
<Icon icon={"chart-line"} />
</div>
<Link to={"/admin/statistics"} 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 year</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">
<div className="chartjs-size-monitor">
<div className="chartjs-size-monitor-expand">
<div/>
</div>
<div className="chartjs-size-monitor-shrink">
<div/>
</div>
</div>
<Bar data={chartData} options={chartOptions} />
</div>
</div>
</Collapse>
</div>
</div>
</div>
</section>
</>
}
}

324
src/src/views/users.js Normal file
View File

@@ -0,0 +1,324 @@
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";
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: []
};
}
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).then((res) => {
if (res.success) {
this.setState({
...this.state,
groups: {
data: res.groups,
pageCount: res.pageCount,
page: page,
totalCount: res.totalCount,
}
});
} 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).then((res) => {
if (res.success) {
this.setState({
...this.state,
loaded: true,
users: {
data: res.users,
pageCount: res.pageCount,
page: page,
totalCount: res.totalCount,
}
});
} 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>
</div>
</>;
}
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 groups = [];
for (let groupId in user.groups) {
if (user.groups.hasOwnProperty(groupId)) {
let groupName = user.groups[groupId];
let color = (groupId === "1" ? "danger" : "secondary");
groups.push(<span key={"group-" + groupId}
className={"mr-1 badge badge-" + color}>{groupName}</span>);
}
}
userRows.push(
<tr key={"user-" + uid}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{groups}</td>
<td>{getPeriodString(user.registered_at)}</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={() => 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/users/adduser"} >
<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>
</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>{group.memberCount}</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={() => 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/users/addgroup"} >
<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>Members</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>;
}
}

18
src/webpack.config.js Normal file
View File

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