Status + More dependencies

This commit is contained in:
Roman Hergenreder 2020-06-19 13:13:13 +02:00
parent c7b8301db1
commit 077fe68914
14 changed files with 753 additions and 16 deletions

@ -0,0 +1,53 @@
<?php
namespace Api\Routes;
use \Api\Request;
use \Driver\SQL\Condition\Compare;
class Fetch extends Request {
private array $notifications;
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array());
$this->loginRequired = true;
$this->csrfTokenRequired = true;
}
public function execute($values = array()) {
if(!parent::execute($values)) {
return false;
}
$sql = $this->user->getSQL();
$res = $sql
->select("uid", "request", "action", "target", "extra", "active")
->from("Route")
->orderBy("uid")
->ascending()
->execute();
$this->lastError = $sql->getLastError();
$this->success = ($res !== FALSE);
if ($this->success) {
$routes = array();
foreach($res as $row) {
$routes[] = array(
"uid" => intval($row["uid"]),
"request" => $row["request"],
"action" => $row["action"],
"target" => $row["target"],
"extra" => $row["extra"],
"active" => intval($row["active"]),
);
}
$this->result["routes"] = $routes;
}
return $this->success;
}
}

@ -3,6 +3,7 @@
namespace Api; namespace Api;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondBool;
class Stats extends Request { class Stats extends Request {
@ -16,14 +17,21 @@ class Stats extends Request {
private function getUserCount() { private function getUserCount() {
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$res = $sql->select($sql->count())->from("User")->execute(); $res = $sql->select($sql->count())->from("User")->execute();
$this->success = ($res !== FALSE); $this->success = $this->success && ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return ($this->success ? $res[0]["count"] : 0); return ($this->success ? $res[0]["count"] : 0);
} }
private function getPageCount() { private function getPageCount() {
return 0; $sql = $this->user->getSQL();
$res = $sql->select($sql->count())->from("Route")
->where(new CondBool("active"))
->execute();
$this->success = $this->success && ($res !== FALSE);
$this->lastError = $sql->getLastError();
return ($this->success ? $res[0]["count"] : 0);
} }
private function getVisitorStatistics() { private function getVisitorStatistics() {
@ -43,7 +51,7 @@ class Stats extends Request {
->ascending() ->ascending()
->execute(); ->execute();
$this->success = ($res !== FALSE); $this->success = $this->success && ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
$visitors = array(); $visitors = array();
@ -72,6 +80,13 @@ class Stats extends Request {
$this->result["userCount"] = $userCount; $this->result["userCount"] = $userCount;
$this->result["pageCount"] = $pageCount; $this->result["pageCount"] = $pageCount;
$this->result["visitors"] = $visitorStatistics; $this->result["visitors"] = $visitorStatistics;
$this->result["server"] = array(
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
"memory_usage" => memory_get_usage(),
"load_avg" => sys_getloadavg(),
"database" => $this->user->getSQL()->getStatus(),
"mail" => $this->user->getConfiguration()->getMail() !== NULL
);
} }
return $this->success; return $this->success;

@ -119,6 +119,18 @@ class CreateDatabase {
->addString("cookie", 26) ->addString("cookie", 26)
->unique("month", "cookie"); ->unique("month", "cookie");
$queries[] = $sql->createTable("Route")
->addSerial("uid")
->addString("request", 128)
->addEnum("action", array("redirect_temporary", "redirect_permanently", "static", "dynamic"))
->addString("target", 128)
->addString("extra", 64, true)
->addBool("active", true)
->primaryKey("uid");
$queries[] = $sql->insert("Route", array("request", "action", "target"))
->addRow("/admin(/.*)?", "dynamic", "\\Core\\Documents\\AdminDashboard");
return $queries; return $queries;
} }
} }

@ -300,4 +300,7 @@ class MySQL extends SQL {
return new Keyword("NOW()"); return new Keyword("NOW()");
} }
public function getStatus() {
return mysqli_stat($this->connection);
}
} }

@ -290,4 +290,15 @@ class PostgreSQL extends SQL {
public function currentTimestamp() { public function currentTimestamp() {
return new Keyword("CURRENT_TIMESTAMP"); return new Keyword("CURRENT_TIMESTAMP");
} }
public function getStatus() {
$version = pg_version($this->connection)["client"] ?? "??";
$status = pg_connection_status($this->connection);
static $statusTexts = array(
PGSQL_CONNECTION_OK => "PGSQL_CONNECTION_OK",
PGSQL_CONNECTION_BAD => "PGSQL_CONNECTION_BAD",
);
return ($statusTexts[$status] ?? "Unknown") . " (v$version)";
}
} }

@ -369,4 +369,6 @@ abstract class SQL {
return $sql; return $sql;
} }
public abstract function getStatus();
} }

249
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

168
src/package-lock.json generated

@ -1129,6 +1129,87 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
}, },
"@emotion/cache": {
"version": "10.0.29",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz",
"integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==",
"requires": {
"@emotion/sheet": "0.9.4",
"@emotion/stylis": "0.8.5",
"@emotion/utils": "0.11.3",
"@emotion/weak-memoize": "0.2.5"
}
},
"@emotion/core": {
"version": "10.0.28",
"resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.28.tgz",
"integrity": "sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==",
"requires": {
"@babel/runtime": "^7.5.5",
"@emotion/cache": "^10.0.27",
"@emotion/css": "^10.0.27",
"@emotion/serialize": "^0.11.15",
"@emotion/sheet": "0.9.4",
"@emotion/utils": "0.11.3"
}
},
"@emotion/css": {
"version": "10.0.27",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz",
"integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==",
"requires": {
"@emotion/serialize": "^0.11.15",
"@emotion/utils": "0.11.3",
"babel-plugin-emotion": "^10.0.27"
}
},
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"@emotion/serialize": {
"version": "0.11.16",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz",
"integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==",
"requires": {
"@emotion/hash": "0.8.0",
"@emotion/memoize": "0.7.4",
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
}
},
"@emotion/sheet": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz",
"integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA=="
},
"@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@emotion/utils": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz",
"integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw=="
},
"@emotion/weak-memoize": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
"@hapi/address": { "@hapi/address": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@ -2550,6 +2631,30 @@
"object.assign": "^4.1.0" "object.assign": "^4.1.0"
} }
}, },
"babel-plugin-emotion": {
"version": "10.0.33",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz",
"integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/hash": "0.8.0",
"@emotion/memoize": "0.7.4",
"@emotion/serialize": "^0.11.16",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^1.0.5",
"find-root": "^1.1.0",
"source-map": "^0.5.7"
},
"dependencies": {
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
}
}
},
"babel-plugin-istanbul": { "babel-plugin-istanbul": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz",
@ -2668,6 +2773,11 @@
"resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz",
"integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==" "integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA=="
}, },
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-syntax-object-rest-spread": { "babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@ -4579,6 +4689,15 @@
"utila": "~0.4" "utila": "~0.4"
} }
}, },
"dom-helpers": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^2.6.7"
}
},
"dom-serializer": { "dom-serializer": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@ -5769,6 +5888,11 @@
"pkg-dir": "^3.0.0" "pkg-dir": "^3.0.0"
} }
}, },
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": { "find-up": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@ -8099,6 +8223,11 @@
"p-is-promise": "^2.0.0" "p-is-promise": "^2.0.0"
} }
}, },
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memory-fs": { "memory-fs": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -9257,6 +9386,11 @@
"ts-pnp": "^1.1.6" "ts-pnp": "^1.1.6"
} }
}, },
"porn": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/porn/-/porn-0.0.1.tgz",
"integrity": "sha1-76quRf+/Bpku7+IFPtjjKOJhxRI="
},
"portfinder": { "portfinder": {
"version": "1.0.26", "version": "1.0.26",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
@ -10739,6 +10873,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==" "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
}, },
"react-input-autosize": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
"integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -11002,6 +11144,21 @@
} }
} }
}, },
"react-select": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.0.tgz",
"integrity": "sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/cache": "^10.0.9",
"@emotion/core": "^10.0.9",
"@emotion/css": "^10.0.9",
"memoize-one": "^5.0.0",
"prop-types": "^15.6.0",
"react-input-autosize": "^2.2.2",
"react-transition-group": "^4.3.0"
}
},
"react-tooltip": { "react-tooltip": {
"version": "4.2.7", "version": "4.2.7",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.7.tgz", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.7.tgz",
@ -11018,6 +11175,17 @@
} }
} }
}, },
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"read-pkg": { "read-pkg": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",

@ -8,12 +8,14 @@
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"moment": "^2.26.0", "moment": "^2.26.0",
"porn": "0.0.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-chartjs-2": "^2.9.0", "react-chartjs-2": "^2.9.0",
"react-collapse": "^5.0.1", "react-collapse": "^5.0.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-select": "^3.1.0",
"react-tooltip": "^4.2.7" "react-tooltip": "^4.2.7"
}, },
"scripts": { "scripts": {

@ -57,4 +57,8 @@ export default class API {
async getStats() { async getStats() {
return this.apiCall("stats"); return this.apiCall("stats");
} }
async getRoutes() {
return this.apiCall("routes/fetch");
}
}; };

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

@ -13,6 +13,7 @@ import Dialog from "./elements/dialog";
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom' import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
import View404 from "./404"; import View404 from "./404";
import Logs from "./views/logs"; import Logs from "./views/logs";
import PageOverview from "./views/pages";
class AdminDashboard extends React.Component { class AdminDashboard extends React.Component {
@ -82,6 +83,7 @@ class AdminDashboard extends React.Component {
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route> <Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route exact={true} path={"/admin/users/adduser"}><CreateUser {...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} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route> <Route path={"*"}><View404 /></Route>
</Switch> </Switch>
<Dialog {...this.state.dialog}/> <Dialog {...this.state.dialog}/>

@ -5,6 +5,7 @@ import { Bar } from 'react-chartjs-2';
import {Collapse} from 'react-collapse'; import {Collapse} from 'react-collapse';
import moment from 'moment'; import moment from 'moment';
import Alert from "../elements/alert"; import Alert from "../elements/alert";
import humanReadableSize from "../global";
export default class Overview extends React.Component { export default class Overview extends React.Component {
@ -18,9 +19,11 @@ export default class Overview extends React.Component {
this.state = { this.state = {
chartVisible : true, chartVisible : true,
statusVisible : true,
userCount: 0, userCount: 0,
notificationCount: 0, notificationCount: 0,
visitors: { }, visitors: { },
server: { load_avg: ["Unknown"] },
errors: [] errors: []
} }
} }
@ -45,6 +48,7 @@ export default class Overview extends React.Component {
userCount: res.userCount, userCount: res.userCount,
pageCount: res.pageCount, pageCount: res.pageCount,
visitors: res.visitors, visitors: res.visitors,
server: res.server
}); });
} }
}); });
@ -172,20 +176,40 @@ export default class Overview extends React.Component {
<Collapse isOpened={this.state.chartVisible}> <Collapse isOpened={this.state.chartVisible}>
<div className="card-body"> <div className="card-body">
<div className="chart"> <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} /> <Bar data={chartData} options={chartOptions} />
</div> </div>
</div> </div>
</Collapse> </Collapse>
</div> </div>
</div> </div>
<div className="col-lg-6 col-12">
<div className="card card-warning">
<div className="card-header">
<h3 className="card-title">Server Status</h3>
<div className="card-tools">
<button type="button" className={"btn btn-tool"} onClick={(e) => {
e.preventDefault();
this.setState({ ...this.state, statusVisible: !this.state.statusVisible });
}}>
<Icon icon={"minus"} />
</button>
</div>
</div>
<Collapse isOpened={this.state.statusVisible}>
<div className="card-body">
<ul className={"list-unstyled"}>
<li><b>Server</b>: {this.state.server.server}</li>
<li><b>Memory Usage</b>: {humanReadableSize(this.state.server.memory_usage)}</li>
<li><b>Load Average</b>: { this.state.server.load_avg.join(" ") }</li>
<li><b>Database</b>: { this.state.server.database }</li>
<li><b>Mail</b>: { this.state.server.mail === true
? <span>OK<Icon icon={""} className={"ml-2"}/></span>
: <Link to={"/admin/settings"}>Not configured</Link>}</li>
</ul>
</div>
</Collapse>
</div>
</div>
</div> </div>
</section> </section>
</> </>

183
src/src/views/pages.js Normal file

@ -0,0 +1,183 @@
import * as React from "react";
import Alert from "../elements/alert";
import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import ReactTooltip from "react-tooltip";
import Select from 'react-select';
export default class PageOverview extends React.Component {
constructor(props) {
super(props);
this.parent = {
api: props.api
};
this.state = {
routes: [],
errors: []
};
this.options = {
"redirect_temporary": "Redirect Temporary",
"redirect_permanently": "Redirect Permanently",
"static": "Serve Static",
"dynamic": "Load Dynamic",
};
}
buildOption(key) {
if (typeof key === 'object' && key.hasOwnProperty("key") && key.hasOwnProperty("label")) {
return key;
} else {
return { value: key, label: this.options[key] };
}
}
removeError(i) {
if (i >= 0 && i < this.state.errors.length) {
let errors = this.state.errors.slice();
errors.splice(i, 1);
this.setState({...this.state, errors: errors});
}
}
componentDidMount() {
this.parent.api.getRoutes().then((res) => {
if (res.success) {
this.setState({...this.state, routes: res.routes});
} else {
let errors = this.state.errors.slice();
errors.push({ title: "Error fetching routes", message: res.msg });
this.setState({...this.state, errors: errors});
}
});
}
render() {
let errors = [];
let rows = [];
for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError(i)} {...this.state.errors[i]}/>)
}
let options = [];
for (let key in Object.keys(this.options)) {
options.push(this.buildOption(key));
}
for (let i = 0; i < this.state.routes.length; i++) {
let route = this.state.routes[i];
rows.push(
<tr key={"route-" + i}>
<td className={"align-middle"}>{route.request}</td>
<td className={"text-center"}>
<Select options={options} value={this.buildOption(route.action)} onChange={(selectedOption) => this.changeAction(i, selectedOption)} />
</td>
<td className={"align-middle"}>{route.target}</td>
<td className={"align-middle"}>{route.extra}</td>
<td className={"text-center"}>
<input
type={"checkbox"}
checked={route.active === 1}
onChange={(e) => this.changeActive(i, e)} />
</td>
</tr>
);
}
return <>
<div className={"content-header"}>
<div className={"container-fluid"}>
<div className={"row mb-2"}>
<div className="col-sm-6">
<h1 className="m-0 text-dark">Routes & Pages</h1>
</div>
<div className="col-sm-6">
<ol className="breadcrumb float-sm-right">
<li className="breadcrumb-item"><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item active">Pages</li>
</ol>
</div>
</div>
</div>
</div>
<div className={"content"}>
{errors}
<div className={"content-fluid"}>
<div className={"row"}>
<div className={"col-lg-8 col-12"}>
<table className={"table"}>
<thead className={"thead-dark"}>
<tr>
<td>
Request&nbsp;
<Icon icon={"question-circle"} style={{"color": "#0069d9"}}
data-tip={"The request, the user is making. Can also be interpreted as a regular expression."}
data-type={"info"} data-place={"bottom"} data-effect={"solid"}/>
</td>
<td style={{minWidth: "250px"}}>
Action&nbsp;
<Icon icon={"question-circle"} style={{"color": "#0069d9"}}
data-tip={"The action to be taken"}
data-type={"info"} data-place={"bottom"} data-effect={"solid"}/>
</td>
<td>
Target&nbsp;
<Icon icon={"question-circle"} style={{"color": "#0069d9"}}
data-tip={"Any URL if action is redirect or static. Path to a class inheriting from Document, " +
"if dynamic is chosen"}
data-type={"info"} data-place={"bottom"} data-effect={"solid"}/>
</td>
<td>
Extra&nbsp;
<Icon icon={"question-circle"} style={{"color": "#0069d9"}}
data-tip={"If action is dynamic, a view name can be entered here, otherwise leave empty."}
data-type={"info"} data-place={"bottom"} data-effect={"solid"}/>
</td>
<td className={"text-center"}>
Active&nbsp;
<Icon icon={"question-circle"} style={{"color": "#0069d9"}}
data-tip={"True, if the route is currently active."}
data-type={"info"} data-place={"bottom"} data-effect={"solid"}/>
</td>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
</div>
</div>
</div>
<ReactTooltip/>
</>
}
changeAction(index, selectedOption) {
if (index < 0 || index >= this.state.routes.length)
return;
let routes = this.state.routes.slice();
routes[index].action = selectedOption;
this.setState({
...this.state,
routes: routes
});
}
changeActive(index, e) {
if (index < 0 || index >= this.state.routes.length)
return;
let routes = this.state.routes.slice();
routes[index].active = e.target.checked ? 1 : 0;
this.setState({
...this.state,
routes: routes
});
}
}