system log + frontend update
This commit is contained in:
parent
716d623db4
commit
5da644acce
@ -17,10 +17,14 @@ namespace Core\API\Logs {
|
|||||||
use Core\API\LogsAPI;
|
use Core\API\LogsAPI;
|
||||||
use Core\API\Parameter\Parameter;
|
use Core\API\Parameter\Parameter;
|
||||||
use Core\API\Parameter\StringType;
|
use Core\API\Parameter\StringType;
|
||||||
|
use Core\API\Traits\Pagination;
|
||||||
use Core\Driver\Logger\Logger;
|
use Core\Driver\Logger\Logger;
|
||||||
use Core\Driver\SQL\Column\Column;
|
use Core\Driver\SQL\Column\Column;
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
|
use Core\Driver\SQL\Condition\CondAnd;
|
||||||
use Core\Driver\SQL\Condition\CondIn;
|
use Core\Driver\SQL\Condition\CondIn;
|
||||||
|
use Core\Driver\SQL\Condition\CondLike;
|
||||||
|
use Core\Driver\SQL\Condition\CondOr;
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\Objects\DatabaseEntity\Group;
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
@ -28,11 +32,15 @@ namespace Core\API\Logs {
|
|||||||
|
|
||||||
class Get extends LogsAPI {
|
class Get extends LogsAPI {
|
||||||
|
|
||||||
|
use Pagination;
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, [
|
$params = self::getPaginationParameters(['id', 'timestamp', "module", "severity"],
|
||||||
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true),
|
'timestamp', 'desc');
|
||||||
"severity" => new StringType("severity", 32, true, "debug")
|
$params["since"] = new Parameter("since", Parameter::TYPE_DATE_TIME, true);
|
||||||
]);
|
$params["severity"] = new StringType("severity", 32, true, "debug");
|
||||||
|
$params["query"] = new StringType("query", 64, true, null);
|
||||||
|
parent::__construct($context, $externalCall, $params);
|
||||||
$this->csrfTokenRequired = false;
|
$this->csrfTokenRequired = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +48,7 @@ namespace Core\API\Logs {
|
|||||||
$since = $this->getParam("since");
|
$since = $this->getParam("since");
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$severity = strtolower(trim($this->getParam("severity")));
|
$severity = strtolower(trim($this->getParam("severity")));
|
||||||
|
$query = $this->getParam("query");
|
||||||
$shownLogLevels = Logger::LOG_LEVELS;
|
$shownLogLevels = Logger::LOG_LEVELS;
|
||||||
|
|
||||||
$logLevel = array_search($severity, Logger::LOG_LEVELS, true);
|
$logLevel = array_search($severity, Logger::LOG_LEVELS, true);
|
||||||
@ -49,19 +58,23 @@ namespace Core\API\Logs {
|
|||||||
$shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel);
|
$shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$condition = new CondIn(new Column("severity"), $shownLogLevels);
|
||||||
$query = SystemLog::createBuilder($sql, false)
|
|
||||||
->orderBy("timestamp")
|
|
||||||
->descending();
|
|
||||||
|
|
||||||
if ($since !== null) {
|
if ($since !== null) {
|
||||||
$query->where(new Compare("timestamp", $since, ">="));
|
$condition = new CondAnd($condition, new Compare("timestamp", $since, ">="));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($logLevel > 0) {
|
if ($query) {
|
||||||
$query->where(new CondIn(new Column("severity"), $shownLogLevels));
|
$condition = new CondAnd($condition, new CondOr(
|
||||||
|
new CondLike(new Column("message"), "%$query%"),
|
||||||
|
new CondLike(new Column("module"), "%$query%"),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->initPagination($sql, SystemLog::class, $condition)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->createPaginationQuery($sql);
|
||||||
$logEntries = SystemLog::findBy($query);
|
$logEntries = SystemLog::findBy($query);
|
||||||
$this->success = $logEntries !== false;
|
$this->success = $logEntries !== false;
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
@ -97,7 +110,7 @@ namespace Core\API\Logs {
|
|||||||
if ($content && $date) {
|
if ($content && $date) {
|
||||||
|
|
||||||
// filter log date
|
// filter log date
|
||||||
if ($since !== null && datetimeDiff($date, $since) < 0) {
|
if ($since !== null && datetimeDiff($date, $since) > 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +148,6 @@ namespace Core\API\User {
|
|||||||
use ImagickException;
|
use ImagickException;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
use Core\Objects\DatabaseEntity\GpgKey;
|
use Core\Objects\DatabaseEntity\GpgKey;
|
||||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
|
||||||
use Core\Objects\DatabaseEntity\User;
|
use Core\Objects\DatabaseEntity\User;
|
||||||
|
|
||||||
class Create extends UserAPI {
|
class Create extends UserAPI {
|
||||||
|
4
cli.php
4
cli.php
@ -674,6 +674,8 @@ function onFrontend(array $argv): void {
|
|||||||
$moduleName = strtolower($argv[3]);
|
$moduleName = strtolower($argv[3]);
|
||||||
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
||||||
_exit("Module name should only be [a-zA-Z0-9_-]");
|
_exit("Module name should only be [a-zA-Z0-9_-]");
|
||||||
|
} else if (in_array($moduleName, ["_tmpl", "dist", "shared"])) {
|
||||||
|
_exit("Invalid module name");
|
||||||
}
|
}
|
||||||
|
|
||||||
$templatePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "_tmpl"]);
|
$templatePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "_tmpl"]);
|
||||||
@ -741,6 +743,8 @@ function onFrontend(array $argv): void {
|
|||||||
$moduleName = strtolower($argv[3]);
|
$moduleName = strtolower($argv[3]);
|
||||||
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
||||||
_exit("Module name should only be [a-zA-Z0-9_-]");
|
_exit("Module name should only be [a-zA-Z0-9_-]");
|
||||||
|
} else if (in_array($moduleName, ["_tmpl", "dist", "shared"])) {
|
||||||
|
_exit("This module cannot be removed");
|
||||||
}
|
}
|
||||||
|
|
||||||
$modulePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, $moduleName]);
|
$modulePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, $moduleName]);
|
||||||
|
@ -12,6 +12,7 @@ import './res/adminlte.min.css';
|
|||||||
|
|
||||||
// views
|
// views
|
||||||
import View404 from "./views/404";
|
import View404 from "./views/404";
|
||||||
|
import LogView from "./views/log-view";
|
||||||
const Overview = lazy(() => import('./views/overview'));
|
const Overview = lazy(() => import('./views/overview'));
|
||||||
const UserListView = lazy(() => import('./views/user/user-list'));
|
const UserListView = lazy(() => import('./views/user/user-list'));
|
||||||
const UserEditView = lazy(() => import('./views/user/user-edit'));
|
const UserEditView = lazy(() => import('./views/user/user-edit'));
|
||||||
@ -70,6 +71,7 @@ export default function AdminDashboard(props) {
|
|||||||
<Route path={"/admin/user/:userId"} element={<UserEditView {...controlObj} />}/>
|
<Route path={"/admin/user/:userId"} element={<UserEditView {...controlObj} />}/>
|
||||||
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
|
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
|
||||||
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
||||||
|
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
|
||||||
<Route path={"*"} element={<View404 />} />
|
<Route path={"*"} element={<View404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
142
react/admin-panel/src/views/log-view.js
Normal file
142
react/admin-panel/src/views/log-view.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import {LocaleContext} from "shared/locale";
|
||||||
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
|
import usePagination from "shared/hooks/pagination";
|
||||||
|
import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import {Chip, TextField} from "@mui/material";
|
||||||
|
import {DateTimePicker} from "@mui/x-date-pickers";
|
||||||
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
|
import {API_DATETIME_FORMAT_DAYJS} from "shared/constants";
|
||||||
|
|
||||||
|
export default function LogView(props) {
|
||||||
|
|
||||||
|
//
|
||||||
|
const LOG_LEVELS = ['debug', 'info', 'warning', 'error', 'severe'];
|
||||||
|
|
||||||
|
const api = props.api;
|
||||||
|
const showDialog = props.showDialog;
|
||||||
|
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const pagination = usePagination();
|
||||||
|
const [logEntries, setLogEntries] = useState([]);
|
||||||
|
|
||||||
|
// filters
|
||||||
|
const [logLevel, setLogLevel] = useState(2);
|
||||||
|
const [timestamp, setTimestamp] = useState(null);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [forceReload, setForceReload] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestModules(props.api, ["general"], currentLocale).then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
props.showDialog("Error fetching translations: " + data.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [currentLocale]);
|
||||||
|
|
||||||
|
const onFetchLogs = useCallback((page, count, orderBy, sortOrder) => {
|
||||||
|
api.fetchLogEntries(page, count, orderBy, sortOrder, LOG_LEVELS[logLevel], timestamp?.format(API_DATETIME_FORMAT_DAYJS), query).then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
setLogEntries(res.logs);
|
||||||
|
pagination.update(res.pagination);
|
||||||
|
} else {
|
||||||
|
showDialog(res.msg, "Error fetching log entries");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [api, showDialog, logLevel, timestamp, query]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: wait for user to finish typing before force reloading
|
||||||
|
setForceReload(forceReload + 1);
|
||||||
|
}, [query, timestamp, logLevel]);
|
||||||
|
|
||||||
|
const messageColumn = (() => {
|
||||||
|
let column = new DataColumn(L("message"), "message");
|
||||||
|
column.renderData = (L, entry) => {
|
||||||
|
return <pre>{entry.message}</pre>
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const columnDefinitions = [
|
||||||
|
new NumericColumn(L("general.id"), "id"),
|
||||||
|
new StringColumn(L("module"), "module"),
|
||||||
|
new StringColumn(L("severity"), "severity"),
|
||||||
|
new DateTimeColumn(L("timestamp"), "timestamp"),
|
||||||
|
messageColumn,
|
||||||
|
];
|
||||||
|
|
||||||
|
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"}>System Log</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">System Log</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"content overflow-auto"}>
|
||||||
|
<div className={"row p-2"}>
|
||||||
|
<div className={"col-2"}>
|
||||||
|
<div className={"form-group"}>
|
||||||
|
<label>{L("log.severity")}</label>
|
||||||
|
<select className={"form-control"} value={logLevel} onChange={e => setLogLevel(parseInt(e.target.value))}>
|
||||||
|
{LOG_LEVELS.map((value, index) =>
|
||||||
|
<option key={"option-" + value} value={index}>{value}</option>)
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"col-4"}>
|
||||||
|
<div className={"form-group"}>
|
||||||
|
<label>{L("log.timestamp")}</label>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<DateTimePicker className={"form-control"}
|
||||||
|
label="Select date time to filter..."
|
||||||
|
value={timestamp ? dayjs(timestamp) : null}
|
||||||
|
onChange={(newValue) => {console.log(newValue);
|
||||||
|
setTimestamp(newValue)
|
||||||
|
}}
|
||||||
|
slotProps={{ textField: { size: 'small' } }}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"col-6"}>
|
||||||
|
<div className={"form-group"}>
|
||||||
|
<label>{L("log.query")}</label>
|
||||||
|
<TextField
|
||||||
|
className={"form-control"}
|
||||||
|
placeholder={L("log.search_query") + "…"}
|
||||||
|
value={query}
|
||||||
|
onChange={e => setQuery(e.target.value)}
|
||||||
|
variant={"outlined"}
|
||||||
|
size={"small"}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"container-fluid"}>
|
||||||
|
<DataTable
|
||||||
|
data={logEntries}
|
||||||
|
pagination={pagination}
|
||||||
|
className={"table table-striped"}
|
||||||
|
fetchData={onFetchLogs}
|
||||||
|
forceReload={forceReload}
|
||||||
|
defaultSortColumn={3}
|
||||||
|
defaultSortOrder={"desc"}
|
||||||
|
placeholder={"No log entries to display"}
|
||||||
|
columns={columnDefinitions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
@ -32,23 +32,25 @@
|
|||||||
"maxParallelRequests": 1
|
"maxParallelRequests": 1
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@material-ui/core": "^4.12.4",
|
"@material-ui/core": "^4.12.4",
|
||||||
"@material-ui/icons": "^4.11.3",
|
"@material-ui/icons": "^4.11.3",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||||
"@mui/icons-material": "^5.11.0",
|
"@mui/icons-material": "^5.11.0",
|
||||||
"@mui/material": "^5.11.3",
|
"@mui/material": "^5.15.14",
|
||||||
|
"@mui/x-date-pickers": "^7.0.0",
|
||||||
"chart.js": "^4.0.1",
|
"chart.js": "^4.0.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"material-ui-color-picker": "^3.5.1",
|
"material-ui-color-picker": "^3.5.1",
|
||||||
"mini-css-extract-plugin": "^2.7.1",
|
"mini-css-extract-plugin": "^2.7.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.0.1",
|
"react-chartjs-2": "^5.0.1",
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.22.3",
|
||||||
"sprintf-js": "^1.1.2"
|
"sprintf-js": "^1.1.2"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
@ -360,4 +360,13 @@ export default class API {
|
|||||||
async downloadGPG(userId) {
|
async downloadGPG(userId) {
|
||||||
return this.apiCall("user/downloadGPG", { id: userId }, true);
|
return this.apiCall("user/downloadGPG", { id: userId }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Log API **/
|
||||||
|
async fetchLogEntries(pageNum = 1, count = 20, orderBy = 'id', sortOrder = 'asc',
|
||||||
|
severity = "debug", since = null, query = "") {
|
||||||
|
return this.apiCall("logs/get", {
|
||||||
|
page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder,
|
||||||
|
since: since, severity: severity, query: query
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
@ -1,6 +1,10 @@
|
|||||||
export const API_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
|
||||||
export const API_DATE_FORMAT = "yyyy-MM-dd";
|
export const API_DATE_FORMAT = "yyyy-MM-dd";
|
||||||
export const API_TIME_FORMAT = "HH:mm:ss";
|
export const API_TIME_FORMAT = "HH:mm:ss";
|
||||||
|
export const API_DATETIME_FORMAT = API_DATE_FORMAT + " " + API_TIME_FORMAT;
|
||||||
|
export const API_DATE_FORMAT_DAYJS = "YYYY-MM-DD";
|
||||||
|
export const API_TIME_FORMAT_DAYJS = "HH:mm:ss";
|
||||||
|
export const API_DATETIME_FORMAT_DAYJS = API_DATE_FORMAT_DAYJS + " " + API_TIME_FORMAT_DAYJS;
|
||||||
|
|
||||||
|
|
||||||
export const USER_GROUP_ADMIN = 1;
|
export const USER_GROUP_ADMIN = 1;
|
||||||
export const USER_GROUP_SUPPORT = 2;
|
export const USER_GROUP_SUPPORT = 2;
|
||||||
|
@ -16,6 +16,7 @@ export function DataTable(props) {
|
|||||||
columns, data, pagination,
|
columns, data, pagination,
|
||||||
fetchData, onClick, onFilter,
|
fetchData, onClick, onFilter,
|
||||||
defaultSortColumn, defaultSortOrder,
|
defaultSortColumn, defaultSortOrder,
|
||||||
|
forceReload,
|
||||||
title, ...other } = props;
|
title, ...other } = props;
|
||||||
|
|
||||||
const {translate: L} = useContext(LocaleContext);
|
const {translate: L} = useContext(LocaleContext);
|
||||||
@ -53,10 +54,10 @@ export function DataTable(props) {
|
|||||||
}
|
}
|
||||||
}, [pagination?.data?.pageSize, pagination?.data?.current]);
|
}, [pagination?.data?.pageSize, pagination?.data?.current]);
|
||||||
|
|
||||||
// sorting changed
|
// sorting changed or we forced an update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFetchData(true);
|
onFetchData(true);
|
||||||
}, [sortAscending, sortColumn]);
|
}, [sortAscending, sortColumn, forceReload]);
|
||||||
|
|
||||||
let headerRow = [];
|
let headerRow = [];
|
||||||
const onChangeSort = useCallback((index, column) => {
|
const onChangeSort = useCallback((index, column) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {format, parse, formatDistance as formatDistanceDateFns } from "date-fns";
|
import {format, parse, formatDistance as formatDistanceDateFns } from "date-fns";
|
||||||
import {API_DATE_FORMAT, API_DATETIME_FORMAT} from "./constants";
|
import {API_DATETIME_FORMAT} from "./constants";
|
||||||
|
|
||||||
function createDownload(name, data) {
|
function createDownload(name, data) {
|
||||||
const url = window.URL.createObjectURL(new Blob([data]));
|
const url = window.URL.createObjectURL(new Blob([data]));
|
||||||
|
6893
react/yarn.lock
6893
react/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user