Create User + Chart dependency
This commit is contained in:
parent
c369a6aec1
commit
373808879a
10
admin/dist/main.js
vendored
10
admin/dist/main.js
vendored
File diff suppressed because one or more lines are too long
96
admin/package-lock.json
generated
96
admin/package-lock.json
generated
@ -1349,6 +1349,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||
},
|
||||
"@reach/observe-rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.1.0.tgz",
|
||||
"integrity": "sha512-kE+jvoj/OyJV24C03VvLt5zclb9ArJi04wWXMMFwQvdZjdHoBlN4g0ZQFjyy/ejPF1Z/dpUD5dhRdBiUmIGZTA=="
|
||||
},
|
||||
"@sheerun/mutationobserver-shim": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
|
||||
@ -4240,6 +4245,80 @@
|
||||
"type": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"d3-array": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz",
|
||||
"integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw=="
|
||||
},
|
||||
"d3-color": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
|
||||
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
|
||||
},
|
||||
"d3-delaunay": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
|
||||
"integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
|
||||
"requires": {
|
||||
"delaunator": "4"
|
||||
}
|
||||
},
|
||||
"d3-format": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz",
|
||||
"integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw=="
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
|
||||
"integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
|
||||
"requires": {
|
||||
"d3-color": "1"
|
||||
}
|
||||
},
|
||||
"d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
|
||||
},
|
||||
"d3-scale": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz",
|
||||
"integrity": "sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==",
|
||||
"requires": {
|
||||
"d3-array": "1.2.0 - 2",
|
||||
"d3-format": "1",
|
||||
"d3-interpolate": "^1.2.0",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2"
|
||||
}
|
||||
},
|
||||
"d3-shape": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
|
||||
"requires": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"d3-time": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
|
||||
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
|
||||
},
|
||||
"d3-time-format": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz",
|
||||
"integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==",
|
||||
"requires": {
|
||||
"d3-time": "1"
|
||||
}
|
||||
},
|
||||
"d3-voronoi": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
|
||||
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
|
||||
},
|
||||
"damerau-levenshtein": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
|
||||
@ -4415,6 +4494,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"delaunator": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
|
||||
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@ -10442,6 +10526,18 @@
|
||||
"whatwg-fetch": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"react-charts": {
|
||||
"version": "2.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/react-charts/-/react-charts-2.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-iUspg9rnx7kD0H/wsK67HNUioOgKgJ8WRXr/Tk3EGP2qcFb9Vo7pjDk4oz1jH12TC+mqL+HFxNYraMkhWd6CUw==",
|
||||
"requires": {
|
||||
"@reach/observe-rect": "^1.1.0",
|
||||
"d3-delaunay": "^5.2.1",
|
||||
"d3-scale": "^3.2.1",
|
||||
"d3-shape": "^1.3.7",
|
||||
"d3-voronoi": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
|
||||
|
@ -7,13 +7,13 @@ import {Link} from "react-router-dom";
|
||||
export default function Header(props) {
|
||||
|
||||
const parent = {
|
||||
notifications: props.notifications || { },
|
||||
notifications: props.notifications || [ ],
|
||||
};
|
||||
|
||||
const [dropdownVisible, showDropdown] = useState(false);
|
||||
const mailIcon = <Icon icon={"envelope"} type={"fas"} />;
|
||||
|
||||
let notificationCount = Object.keys(parent.notifications).length;
|
||||
let notificationCount = parent.notifications.length;
|
||||
let notificationText = "No new notifications";
|
||||
|
||||
if(notificationCount === 1) {
|
||||
@ -23,8 +23,9 @@ export default function Header(props) {
|
||||
}
|
||||
|
||||
let notificationItems = [];
|
||||
for (let uid in parent.notifications) {
|
||||
const notification = parent.notifications[uid];
|
||||
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}>
|
||||
|
@ -24,10 +24,6 @@ class AdminDashboard extends React.Component {
|
||||
dialog: { onClose: () => this.hideDialog() },
|
||||
notifications: { }
|
||||
};
|
||||
this.controlObj = {
|
||||
showDialog: this.showDialog.bind(this),
|
||||
api: this.api
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
@ -71,16 +67,21 @@ class AdminDashboard extends React.Component {
|
||||
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} /></Route>
|
||||
<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={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
|
||||
<Route path={"*"}><View404 /></Route>
|
||||
</Switch>
|
||||
<Dialog {...this.state.dialog}/>
|
||||
|
@ -6,7 +6,8 @@ export default function Sidebar(props) {
|
||||
|
||||
let parent = {
|
||||
showDialog: props.showDialog || function() {},
|
||||
api: props.api
|
||||
api: props.api,
|
||||
notifications: props.notifications || [ ]
|
||||
};
|
||||
|
||||
function onLogout() {
|
||||
@ -28,6 +29,10 @@ export default function Sidebar(props) {
|
||||
"name": "Users & Groups",
|
||||
"icon": "users"
|
||||
},
|
||||
"pages": {
|
||||
"name": "Pages & Routes",
|
||||
"icon": "copy",
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
"icon": "tools"
|
||||
@ -42,7 +47,7 @@ export default function Sidebar(props) {
|
||||
},
|
||||
};
|
||||
|
||||
let numNotifications = Object.keys(props.notifications).length;
|
||||
let numNotifications = parent.notifications.length;
|
||||
if (numNotifications > 0) {
|
||||
if (numNotifications > 9) numNotifications = "9+";
|
||||
menuItems["logs"]["badge"] = { type: "warning", value: numNotifications };
|
||||
@ -88,15 +93,11 @@ export default function Sidebar(props) {
|
||||
<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%"}}>
|
||||
|
||||
{/* LOGGED IN AS */}
|
||||
<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>
|
||||
|
||||
{/* SIDEBAR */}
|
||||
<nav className={"mt-2"}>
|
||||
<ul className={"nav nav-pills nav-sidebar flex-column"} data-widget={"treeview"} role={"menu"} data-accordion={"false"}>
|
||||
{li}
|
||||
|
@ -146,7 +146,7 @@ export default class CreateUser extends React.Component {
|
||||
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: "error" });
|
||||
errors.push({ title: "Error inviting User", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors });
|
||||
} else {
|
||||
errors.push({ title: "Success", message: "The invitation was successfully sent.", type: "success" });
|
||||
@ -157,7 +157,7 @@ export default class CreateUser extends React.Component {
|
||||
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: "error" });
|
||||
errors.push({ title: "Error creating User", message: res.msg, type: "danger" });
|
||||
this.setState({ ...this.state, errors: errors, password: "", confirmPassword: "" });
|
||||
} else {
|
||||
errors.push({ title: "Success", message: "The user was successfully created.", type: "success" });
|
||||
|
@ -1,9 +1,130 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import Icon from "../elements/icon";
|
||||
|
||||
export default class Overview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parent = {
|
||||
showDialog: props.showDialog,
|
||||
notifications: props.notification,
|
||||
api: props.api,
|
||||
}
|
||||
}
|
||||
|
||||
fetchStats() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Overview</div>
|
||||
|
||||
let userCount = 0;
|
||||
let notificationCount = 0;
|
||||
let pageCount = 0;
|
||||
let visitorCount = 0;
|
||||
|
||||
|
||||
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"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className="small-box bg-info">
|
||||
<div className={"inner"}>
|
||||
<h3>{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>{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>{notificationCount}</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" data-card-widget="collapse">
|
||||
<Icon icon={"minus"} />
|
||||
</button>
|
||||
<button type="button" className="btn btn-tool" data-card-widget="remove">
|
||||
<Icon icon={"times"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<BarChart data={data} series={series} axes={axes} tooltip />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
}
|
@ -65,7 +65,7 @@ class Fetch extends Request {
|
||||
foreach($res as $row) {
|
||||
$id = $row["uid"];
|
||||
if (!isset($this->notifications[$id])) {
|
||||
$this->notifications[$id] = array(
|
||||
$this->notifications[] = array(
|
||||
"uid" => $id,
|
||||
"title" => $row["title"],
|
||||
"message" => $row["message"],
|
||||
|
@ -21,67 +21,58 @@ class Create extends Request {
|
||||
}
|
||||
|
||||
public function execute($values = array()) {
|
||||
if(!parent::execute($values)) {
|
||||
if (!parent::execute($values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$username = $this->getParam('username');
|
||||
$email = $this->getParam('email');
|
||||
|
||||
if(!$this->userExists($username, $email) || !$this->success) {
|
||||
if (!$this->userExists($username, $email) || !$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$password = $this->getParam('password');
|
||||
$confirmPassword = $this->getParam('confirmPassword');
|
||||
|
||||
if($password !== $confirmPassword) {
|
||||
if ($password !== $confirmPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
|
||||
$this->success = $this->createUser($username, $email, $password);
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function userExists($username, $email){
|
||||
private function userExists($username, $email) {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->select("User.name", "User.email")
|
||||
->from("User")
|
||||
->where(new Compare("User.name", $username), new Compare("User.email",$email))
|
||||
->where(new Compare("User.name", $username), new Compare("User.email", $email))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if($res !== 0) {
|
||||
$this->success = false;
|
||||
if (!empty($res)) {
|
||||
$row = $res[0];
|
||||
$message = "";
|
||||
if (strcmp($username,row['name']) != 0 && strcmp($email, row['email']) != 0) {
|
||||
$message = "Username and email are already taken";
|
||||
}else if (strcmp($username,row['name']) != 0) {
|
||||
$message = "Username is already taken";
|
||||
}else{
|
||||
$message = "Email is already taken";
|
||||
if (strcasecmp($username, $row['name']) === 0) {
|
||||
$this->lastError = "This username is already in use.";
|
||||
$this->success = false;
|
||||
} else if (strcasecmp($username, $row['email']) === 0) {
|
||||
$this->lastError = "This email address is already taken";
|
||||
$this->success = false;
|
||||
}
|
||||
$this->lastError = $message;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function createUser($username, $email, $password){
|
||||
private function createUser($username, $email, $password) {
|
||||
$sql = $this->user->getSQL();
|
||||
$salt = generateRandomString(16);
|
||||
$hash = hash('sha256', $password . $salt);
|
||||
$res = $sql->insert("User",array(
|
||||
$res = $sql->insert("User", array(
|
||||
'username' => $username,
|
||||
'password' => $hash,
|
||||
'salt' => $salt,
|
||||
'email' => $email
|
||||
))->execute();
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
@ -51,6 +51,12 @@ class CreateDatabase {
|
||||
->primaryKey("uid", "user_id")
|
||||
->foreignKey("user_id", "User", "uid", new CascadeStrategy());
|
||||
|
||||
$queries[] = $sql->createTable("UserInvitation")
|
||||
->addString("username",32)
|
||||
->addString("email",32)
|
||||
->addString("token",36)
|
||||
->addDateTime("valid_until");
|
||||
|
||||
$queries[] = $sql->createTable("UserToken")
|
||||
->addInt("user_id")
|
||||
->addString("token", 36)
|
||||
|
10
js/admin.min.js
vendored
10
js/admin.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user