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\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\Traits\Pagination;
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondAnd;
|
||||
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\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
@ -28,11 +32,15 @@ namespace Core\API\Logs {
|
||||
|
||||
class Get extends LogsAPI {
|
||||
|
||||
use Pagination;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true),
|
||||
"severity" => new StringType("severity", 32, true, "debug")
|
||||
]);
|
||||
$params = self::getPaginationParameters(['id', 'timestamp', "module", "severity"],
|
||||
'timestamp', 'desc');
|
||||
$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;
|
||||
}
|
||||
|
||||
@ -40,6 +48,7 @@ namespace Core\API\Logs {
|
||||
$since = $this->getParam("since");
|
||||
$sql = $this->context->getSQL();
|
||||
$severity = strtolower(trim($this->getParam("severity")));
|
||||
$query = $this->getParam("query");
|
||||
$shownLogLevels = Logger::LOG_LEVELS;
|
||||
|
||||
$logLevel = array_search($severity, Logger::LOG_LEVELS, true);
|
||||
@ -49,19 +58,23 @@ namespace Core\API\Logs {
|
||||
$shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel);
|
||||
}
|
||||
|
||||
|
||||
$query = SystemLog::createBuilder($sql, false)
|
||||
->orderBy("timestamp")
|
||||
->descending();
|
||||
|
||||
$condition = new CondIn(new Column("severity"), $shownLogLevels);
|
||||
if ($since !== null) {
|
||||
$query->where(new Compare("timestamp", $since, ">="));
|
||||
$condition = new CondAnd($condition, new Compare("timestamp", $since, ">="));
|
||||
}
|
||||
|
||||
if ($logLevel > 0) {
|
||||
$query->where(new CondIn(new Column("severity"), $shownLogLevels));
|
||||
if ($query) {
|
||||
$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);
|
||||
$this->success = $logEntries !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
@ -97,7 +110,7 @@ namespace Core\API\Logs {
|
||||
if ($content && $date) {
|
||||
|
||||
// filter log date
|
||||
if ($since !== null && datetimeDiff($date, $since) < 0) {
|
||||
if ($since !== null && datetimeDiff($date, $since) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -148,7 +148,6 @@ namespace Core\API\User {
|
||||
use ImagickException;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\GpgKey;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
class Create extends UserAPI {
|
||||
|
4
cli.php
4
cli.php
@ -674,6 +674,8 @@ function onFrontend(array $argv): void {
|
||||
$moduleName = strtolower($argv[3]);
|
||||
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
||||
_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"]);
|
||||
@ -741,6 +743,8 @@ function onFrontend(array $argv): void {
|
||||
$moduleName = strtolower($argv[3]);
|
||||
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
||||
_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]);
|
||||
|
@ -12,6 +12,7 @@ import './res/adminlte.min.css';
|
||||
|
||||
// views
|
||||
import View404 from "./views/404";
|
||||
import LogView from "./views/log-view";
|
||||
const Overview = lazy(() => import('./views/overview'));
|
||||
const UserListView = lazy(() => import('./views/user/user-list'));
|
||||
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/groups"} element={<GroupListView {...controlObj} />}/>
|
||||
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
||||
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
|
||||
<Route path={"*"} element={<View404 />} />
|
||||
</Routes>
|
||||
</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
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@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",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"material-ui-color-picker": "^3.5.1",
|
||||
"mini-css-extract-plugin": "^2.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.0.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.6.2",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"browserslist": {
|
||||
@ -63,4 +65,4 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -360,4 +360,13 @@ export default class API {
|
||||
async downloadGPG(userId) {
|
||||
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_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_SUPPORT = 2;
|
||||
|
@ -16,6 +16,7 @@ export function DataTable(props) {
|
||||
columns, data, pagination,
|
||||
fetchData, onClick, onFilter,
|
||||
defaultSortColumn, defaultSortOrder,
|
||||
forceReload,
|
||||
title, ...other } = props;
|
||||
|
||||
const {translate: L} = useContext(LocaleContext);
|
||||
@ -53,10 +54,10 @@ export function DataTable(props) {
|
||||
}
|
||||
}, [pagination?.data?.pageSize, pagination?.data?.current]);
|
||||
|
||||
// sorting changed
|
||||
// sorting changed or we forced an update
|
||||
useEffect(() => {
|
||||
onFetchData(true);
|
||||
}, [sortAscending, sortColumn]);
|
||||
}, [sortAscending, sortColumn, forceReload]);
|
||||
|
||||
let headerRow = [];
|
||||
const onChangeSort = useCallback((index, column) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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) {
|
||||
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