admin -> src
This commit is contained in:
3
src/.babelrc
Normal file
3
src/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
||||
23
src/.gitignore
vendored
Normal file
23
src/.gitignore
vendored
Normal 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
68
src/README.md
Normal 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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
7493
src/dist/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14680
src/package-lock.json
generated
Normal file
14680
src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
src/package.json
Normal file
46
src/package.json
Normal 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
30
src/src/404.js
Normal 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
60
src/src/api.js
Normal 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
26
src/src/elements/alert.js
Normal 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>
|
||||
)
|
||||
}
|
||||
30
src/src/elements/dialog.js
Normal file
30
src/src/elements/dialog.js
Normal 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
24
src/src/elements/icon.js
Normal 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
7
src/src/global.js
Normal 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
90
src/src/header.js
Normal 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
12
src/src/include/adminlte.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/src/include/index.css
Normal file
6
src/src/include/index.css
Normal 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
97
src/src/index.js
Normal 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
112
src/src/sidebar.js
Normal 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
200
src/src/views/adduser.js
Normal 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"}/>
|
||||
Back
|
||||
</Link>
|
||||
{ this.state.isSubmitting
|
||||
? <button type={"submit"} className={"btn btn-primary mt-2"} disabled>Loading… <Icon icon={"circle-notch"} /></button>
|
||||
: <button type={"submit"} className={"btn btn-primary mt-2"}>Submit</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ReactTooltip/>
|
||||
</>;
|
||||
}
|
||||
|
||||
submitForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredFields = (this.state.sendInvite ?
|
||||
["username", "email"] :
|
||||
["username", "password", "confirmPassword"]);
|
||||
|
||||
let missingFields = [];
|
||||
for (const field of requiredFields) {
|
||||
if (!this.state[field]) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({title: "Missing input", message: "The following fields are missing: " + missingFields.join(", "), type: "warning"});
|
||||
this.setState({ ...this.state, errors: errors });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ ...this.state, isSubmitting: true });
|
||||
const username = this.state.username;
|
||||
const email = this.state.email || "";
|
||||
const password = this.state.password;
|
||||
const confirmPassword = this.state.confirmPassword;
|
||||
|
||||
if (this.state.sendInvite) {
|
||||
this.parent.api.inviteUser(username, email).then((res) => {
|
||||
let errors = this.state.errors.slice();
|
||||
if (!res.success) {
|
||||
errors.push({ title: "Error inviting User", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, isSubmitting: false });
|
||||
} else {
|
||||
errors.push({ title: "Success", message: "The invitation was successfully sent.", type: "success" });
|
||||
this.setState({ ...this.state, errors: errors, username: "", email: "", isSubmitting: false });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
if (this.state.password !== this.state.confirmPassword) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ title: "Error creating User", message: "The given passwords do not match", type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "", isSubmitting: false });
|
||||
return;
|
||||
}
|
||||
|
||||
this.parent.api.createUser(username, email, password, confirmPassword).then((res) => {
|
||||
let errors = this.state.errors.slice();
|
||||
if (!res.success) {
|
||||
errors.push({ title: "Error creating User", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "", isSubmitting: false });
|
||||
} else {
|
||||
errors.push({ title: "Success", message: "The user was successfully created.", type: "success" });
|
||||
this.setState({ ...this.state, errors: errors, username: "", email: "", password: "", confirmPassword: "", isSubmitting: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onCheckboxChange() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
sendInvite: !this.state.sendInvite,
|
||||
});
|
||||
}
|
||||
|
||||
onChangeInput(event) {
|
||||
const target = event.target;
|
||||
const value = target.value;
|
||||
const name = target.name;
|
||||
this.setState({ ...this.state, [name]: value });
|
||||
}
|
||||
}
|
||||
41
src/src/views/logs.js
Normal file
41
src/src/views/logs.js
Normal 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
193
src/src/views/overview.js
Normal 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
324
src/src/views/users.js
Normal 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… <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
18
src/webpack.config.js
Normal 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']
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user