Some more stuff

This commit is contained in:
Roman Hergenreder 2020-06-15 23:10:14 +02:00
parent 7124038b5b
commit 32df152ca7
14 changed files with 330 additions and 201 deletions

124
admin/dist/main.js vendored

File diff suppressed because one or more lines are too long

@ -1,7 +1,30 @@
import * as React from "react"; import * as React from "react";
import {Link} from "react-router-dom";
import Icon from "./elements/icon";
export default class View404 extends React.Component { export default class View404 extends React.Component {
render() { render() {
return <b>404 Not found</b> return <div className={"error-page"}>
<h2 className={"headline text-warning"}>404</h2>
<div className={"error-content"}>
<h3>
<Icon icon={"exclamation-triangle"} classes={"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>
} }
} }

@ -0,0 +1,18 @@
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";
return (
<div className={"alert alert-danger alert-dismissible"}>
<button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button>
<h5><Icon icon={"ban"} classes={"icon"} /> {title}</h5>
{message}
</div>
)
}

@ -14,7 +14,12 @@ export default function Icon(props) {
classes.push("fa-spin"); classes.push("fa-spin");
} }
let newProps = {...props, className: classes.join(" ") };
delete newProps["classes"];
delete newProps["type"];
delete newProps["icon"];
return ( return (
<i className={classes.join(" ")} /> <i {...newProps} />
); );
} }

@ -1,19 +1,15 @@
import * as React from "react"; import * as React from "react";
import Icon from "./icon"; import Icon from "./elements/icon";
import {useState} from "react"; import {useState} from "react";
import {getPeriodString} from "./global"; import {getPeriodString} from "./global";
import {Link} from "react-router-dom";
export default function Header(props) { export default function Header(props) {
const parent = { const parent = {
onChangeView: props.onChangeView || function() {},
notifications: props.notifications || { }, notifications: props.notifications || { },
}; };
function onChangeView(view) {
parent.onChangeView(view);
}
const [dropdownVisible, showDropdown] = useState(false); const [dropdownVisible, showDropdown] = useState(false);
const mailIcon = <Icon icon={"envelope"} type={"fas"} />; const mailIcon = <Icon icon={"envelope"} type={"fas"} />;
@ -31,11 +27,11 @@ export default function Header(props) {
const notification = parent.notifications[uid]; const notification = parent.notifications[uid];
const createdAt = getPeriodString(notification.created_at); const createdAt = getPeriodString(notification.created_at);
notificationItems.push( notificationItems.push(
<a href="#" className={"dropdown-item"} key={"notification-" + uid}> <Link to={"/admin/logs?notification=" + uid} className={"dropdown-item"} key={"notification-" + uid}>
{mailIcon} {mailIcon}
<span className={"ml-2"}>{notification.title}</span> <span className={"ml-2"}>{notification.title}</span>
<span className={"float-right text-muted text-sm"}>{createdAt}</span> <span className={"float-right text-muted text-sm"}>{createdAt}</span>
</a>); </Link>);
} }
return ( return (
@ -49,9 +45,9 @@ export default function Header(props) {
</a> </a>
</li> </li>
<li className={"nav-item d-none d-sm-inline-block"}> <li className={"nav-item d-none d-sm-inline-block"}>
<a href={"#"} onClick={() => onChangeView("dashboard")} className={"nav-link"}> <Link to={"/admin/dashboard"} className={"nav-link"}>
Home Home
</a> </Link>
</li> </li>
</ul> </ul>
@ -84,7 +80,7 @@ export default function Header(props) {
</span> </span>
{notificationItems} {notificationItems}
<div className={"dropdown-divider"} /> <div className={"dropdown-divider"} />
<a href={"#"} onClick={() => onChangeView("dashboard")} className={"dropdown-item dropdown-footer"}>See All Notifications</a> <Link to={"/admin/logs"} className={"dropdown-item dropdown-footer"}>See All Notifications</Link>
</div> </div>
</li> </li>
</ul> </ul>

@ -5,13 +5,13 @@ import './include/index.css';
import API from './api.js'; import API from './api.js';
import Header from './header.js'; import Header from './header.js';
import Sidebar from './sidebar.js'; import Sidebar from './sidebar.js';
import UserOverview from './users.js'; import UserOverview from './views/users.js';
import Overview from './overview.js' import Overview from './views/overview.js'
import Icon from "./icon"; import Icon from "./elements/icon";
import Dialog from "./dialog"; import Dialog from "./elements/dialog";
import {BrowserRouter as Router, Route} from 'react-router-dom' import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
import View404 from "./404"; import View404 from "./404";
import Switch from "react-router-dom/es/Switch"; import Logs from "./views/logs";
class AdminDashboard extends React.Component { class AdminDashboard extends React.Component {
@ -78,6 +78,7 @@ class AdminDashboard extends React.Component {
<Switch> <Switch>
<Route path={"/admin/dashboard"}><Overview {...this.controlObj} /></Route> <Route path={"/admin/dashboard"}><Overview {...this.controlObj} /></Route>
<Route path={"/admin/users"}><UserOverview {...this.controlObj} /></Route> <Route path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route> <Route path={"*"}><View404 /></Route>
</Switch> </Switch>
<Dialog {...this.state.dialog}/> <Dialog {...this.state.dialog}/>

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import Icon from "./icon"; import Icon from "./elements/icon";
import {Link, NavLink} from "react-router-dom"; import {Link, NavLink} from "react-router-dom";
export default function Sidebar(props) { export default function Sidebar(props) {
@ -32,6 +32,10 @@ export default function Sidebar(props) {
"name": "Settings", "name": "Settings",
"icon": "tools" "icon": "tools"
}, },
"logs": {
"name": "Logs & Notifications",
"icon": "file-medical-alt"
},
"help": { "help": {
"name": "Help", "name": "Help",
"icon": "question-circle" "icon": "question-circle"
@ -41,7 +45,7 @@ export default function Sidebar(props) {
let numNotifications = Object.keys(props.notifications).length; let numNotifications = Object.keys(props.notifications).length;
if (numNotifications > 0) { if (numNotifications > 0) {
if (numNotifications > 9) numNotifications = "9+"; if (numNotifications > 9) numNotifications = "9+";
menuItems["dashboard"]["badge"] = { type: "warning", value: numNotifications }; menuItems["logs"]["badge"] = { type: "warning", value: numNotifications };
} }
let li = []; let li = [];

41
admin/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>
</>;
}
}

@ -1,14 +1,16 @@
import * as React from "react"; import * as React from "react";
import Icon from "./icon"; import Icon from "../elements/icon";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {getPeriodString} from "./global"; import {getPeriodString} from "../global";
import Alert from "../elements/alert";
export default class UserOverview extends React.Component { export default class UserOverview extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.parent = { this.parent = {
showDialog: props.showDialog || function() {}, showDialog: props.showDialog || function () {
},
api: props.api, api: props.api,
}; };
this.state = { this.state = {
@ -16,12 +18,14 @@ export default class UserOverview extends React.Component {
users: { users: {
data: {}, data: {},
page: 1, page: 1,
pageCount: 1 pageCount: 1,
totalCount: 0,
}, },
groups: { groups: {
data: {}, data: {},
page: 1, page: 1,
pageCount: 1 pageCount: 1,
totalCount: 0,
}, },
errors: [] errors: []
}; };
@ -29,7 +33,7 @@ export default class UserOverview extends React.Component {
fetchGroups(page) { fetchGroups(page) {
page = page || this.state.groups.page; page = page || this.state.groups.page;
this.setState({ ...this.state, groups: { ...this.state.groups, data: { }, page: 1 } }); this.setState({...this.state, groups: {...this.state.groups, data: {}, page: 1, totalCount: 0}});
this.parent.api.fetchGroups(page).then((res) => { this.parent.api.fetchGroups(page).then((res) => {
if (res.success) { if (res.success) {
this.setState({ this.setState({
@ -37,13 +41,16 @@ export default class UserOverview extends React.Component {
groups: { groups: {
data: res.groups, data: res.groups,
pageCount: res.pageCount, pageCount: res.pageCount,
page: page page: page,
totalCount: res.totalCount,
} }
}); });
} else { } else {
let errors = this.state.errors.slice();
errors.push({title: "Error fetching groups", message: res.msg});
this.setState({ this.setState({
...this.state, ...this.state,
errors: this.state.errors.slice().push(res.msg) errors: errors
}); });
} }
if (!this.state.loaded) { if (!this.state.loaded) {
@ -54,7 +61,7 @@ export default class UserOverview extends React.Component {
fetchUsers(page) { fetchUsers(page) {
page = page || this.state.users.page; page = page || this.state.users.page;
this.setState({ ...this.state, users: { ...this.state.users, data: { }, pageCount: 1 } }); this.setState({...this.state, users: {...this.state.users, data: {}, pageCount: 1, totalCount: 0}});
this.parent.api.fetchUsers(page).then((res) => { this.parent.api.fetchUsers(page).then((res) => {
if (res.success) { if (res.success) {
this.setState({ this.setState({
@ -63,24 +70,35 @@ export default class UserOverview extends React.Component {
users: { users: {
data: res.users, data: res.users,
pageCount: res.pageCount, pageCount: res.pageCount,
page: page page: page,
totalCount: res.totalCount,
} }
}); });
} else { } else {
let errors = this.state.errors.slice();
errors.push({title: "Error fetching users", message: res.msg});
this.setState({ this.setState({
...this.state, ...this.state,
loaded: true, loaded: true,
errors: this.state.errors.slice().push(res.msg) errors: errors
}); });
} }
}); });
} }
componentDidMount() { componentDidMount() {
this.setState({ ...this.state, loaded: false }); this.setState({...this.state, loaded: false});
this.fetchGroups(1); 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() { render() {
if (!this.state.loaded) { if (!this.state.loaded) {
@ -89,6 +107,11 @@ export default class UserOverview extends React.Component {
</div> </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 <> return <>
<div className="content-header"> <div className="content-header">
<div className="container-fluid"> <div className="container-fluid">
@ -106,13 +129,14 @@ export default class UserOverview extends React.Component {
</div> </div>
</div> </div>
<div className={"content"}> <div className={"content"}>
{errors}
<div className={"content-fluid"}> <div className={"content-fluid"}>
<div className={"row"}> <div className={"row"}>
<div className={"col-lg-6"}> <div className={"col-lg-6"}>
{ this.createUserCard() } {this.createUserCard()}
</div> </div>
<div className={"col-lg-6"}> <div className={"col-lg-6"}>
{ this.createGroupCard() } {this.createGroupCard()}
</div> </div>
</div> </div>
</div> </div>
@ -135,7 +159,8 @@ export default class UserOverview extends React.Component {
if (user.groups.hasOwnProperty(groupId)) { if (user.groups.hasOwnProperty(groupId)) {
let groupName = user.groups[groupId]; let groupName = user.groups[groupId];
let color = (groupId === "1" ? "danger" : "secondary"); let color = (groupId === "1" ? "danger" : "secondary");
groups.push(<span key={"group-" + groupId} className={"mr-1 badge badge-" + color}>{groupName}</span>); groups.push(<span key={"group-" + groupId}
className={"mr-1 badge badge-" + color}>{groupName}</span>);
} }
} }
@ -156,11 +181,11 @@ export default class UserOverview extends React.Component {
for (let i = 1; i <= this.state.users.pageCount; i++) { for (let i = 1; i <= this.state.users.pageCount; i++) {
let active = (this.state.users.page === i ? " active" : ""); let active = (this.state.users.page === i ? " active" : "");
pages.push( pages.push(
<li key={"page-" + i} className={"page-item" + active}> <li key={"page-" + i} className={"page-item" + active}>
<a className={"page-link"} href={"#"} onClick={() => this.fetchUsers(i)}> <a className={"page-link"} href={"#"} onClick={() => this.fetchUsers(i)}>
{i} {i}
</a> </a>
</li> </li>
); );
} }
@ -176,31 +201,38 @@ export default class UserOverview extends React.Component {
<div className={"card-body table-responsive p-0"}> <div className={"card-body table-responsive p-0"}>
<table className={"table table-striped table-valign-middle"}> <table className={"table table-striped table-valign-middle"}>
<thead> <thead>
<tr> <tr>
<th>Username</th> <th>Username</th>
<th>Email</th> <th>Email</th>
<th>Groups</th> <th>Groups</th>
<th>Registered</th> <th>Registered</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ userRows } {userRows}
</tbody> </tbody>
</table> </table>
<nav aria-label={""}> <nav className={"row m-0"}>
<ul className={"pagination p-2 m-0 justify-content-end"}> <div className={"col-6 pl-3 pt-3 pb-3 text-muted"}>
<li className={"page-item" + previousDisabled}> Total: {this.state.users.totalCount}
<a className={"page-link"} href={"#"} onClick={() => this.fetchUsers(this.state.users.page - 1)}> </div>
Previous <div className={"col-6 p-0"}>
</a> <ul className={"pagination p-2 m-0 justify-content-end"}>
</li> <li className={"page-item" + previousDisabled}>
{ pages } <a className={"page-link"} href={"#"}
<li className={"page-item" + nextDisabled}> onClick={() => this.fetchUsers(this.state.users.page - 1)}>
<a className={"page-link"} href={"#"} onClick={() => this.fetchUsers(this.state.users.page + 1)}> Previous
Next </a>
</a> </li>
</li> {pages}
</ul> <li className={"page-item" + nextDisabled}>
<a className={"page-link"} href={"#"}
onClick={() => this.fetchUsers(this.state.users.page + 1)}>
Next
</a>
</li>
</ul>
</div>
</nav> </nav>
</div> </div>
</div>; </div>;
@ -256,23 +288,30 @@ export default class UserOverview extends React.Component {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ groupRows } {groupRows}
</tbody> </tbody>
</table> </table>
<nav aria-label={""}> <nav className={"row m-0"}>
<ul className={"pagination p-2 m-0 justify-content-end"}> <div className={"col-6 pl-3 pt-3 pb-3 text-muted"}>
<li className={"page-item" + previousDisabled}> Total: {this.state.groups.totalCount}
<a className={"page-link"} href={"#"} onClick={() => this.fetchGroups(this.state.groups.page - 1)}> </div>
Previous <div className={"col-6 p-0"}>
</a> <ul className={"pagination p-2 m-0 justify-content-end"}>
</li> <li className={"page-item" + previousDisabled}>
{ pages } <a className={"page-link"} href={"#"}
<li className={"page-item" + nextDisabled}> onClick={() => this.fetchGroups(this.state.groups.page - 1)}>
<a className={"page-link"} href={"#"} onClick={() => this.fetchGroups(this.state.groups.page + 1)}> Previous
Next </a>
</a> </li>
</li> {pages}
</ul> <li className={"page-item" + nextDisabled}>
<a className={"page-link"} href={"#"}
onClick={() => this.fetchGroups(this.state.groups.page + 1)}>
Next
</a>
</li>
</ul>
</div>
</nav> </nav>
</div> </div>
</div>; </div>;

@ -76,6 +76,7 @@ class Fetch extends Request {
); );
} }
$this->result["pageCount"] = intval(ceil($this->groupCount / Fetch::SELECT_SIZE)); $this->result["pageCount"] = intval(ceil($this->groupCount / Fetch::SELECT_SIZE));
$this->result["totalCount"] = $this->groupCount;
} }
return $this->success; return $this->success;

@ -87,6 +87,7 @@ class Fetch extends Request {
} }
} }
$this->result["pageCount"] = intval(ceil($this->userCount / Fetch::SELECT_SIZE)); $this->result["pageCount"] = intval(ceil($this->userCount / Fetch::SELECT_SIZE));
$this->result["totalCount"] = $this->userCount;
} }
return $this->success; return $this->success;

124
js/admin.min.js vendored

File diff suppressed because one or more lines are too long