MM-57097: Add a toggle to switch between plain and JSON logs format (#28806)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ae046fb34e
Коммит
f7976254bb
@@ -37,20 +37,22 @@ type State = {
|
|||||||
dateFrom: string;
|
dateFrom: string;
|
||||||
dateTo: string;
|
dateTo: string;
|
||||||
filteredLogs: LogObject[];
|
filteredLogs: LogObject[];
|
||||||
loadingLogs: boolean;
|
loading: boolean;
|
||||||
logLevels: LogLevels;
|
logLevels: LogLevels;
|
||||||
search: string;
|
search: string;
|
||||||
serverNames: LogServerNames;
|
serverNames: LogServerNames;
|
||||||
page: number;
|
page: number;
|
||||||
perPage: number;
|
perPage: number;
|
||||||
loadingPlain: boolean;
|
isPlainLogs: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: {id: 'admin.logs.title', defaultMessage: 'Server Logs'},
|
title: {id: 'admin.logs.title', defaultMessage: 'Server Logs'},
|
||||||
bannerDesc: {id: 'admin.logs.bannerDesc', defaultMessage: 'To look up users by User ID or Token ID, go to User Management > Users and paste the ID into the search filter.'},
|
bannerDesc: {id: 'admin.logs.bannerDesc', defaultMessage: 'To look up users by User ID or Token ID, go to User Management > Users and paste the ID into the search filter.'},
|
||||||
|
logFormatTitle: {id: 'admin.logs.logFormatTitle', defaultMessage: 'Log Format:'},
|
||||||
|
logFormatJson: {id: 'admin.logs.logFormatJson', defaultMessage: 'JSON'},
|
||||||
|
logFormatPlain: {id: 'admin.logs.logFormatPlain', defaultMessage: 'Plain text'},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const searchableStrings = [
|
export const searchableStrings = [
|
||||||
messages.title,
|
messages.title,
|
||||||
messages.bannerDesc,
|
messages.bannerDesc,
|
||||||
@@ -63,27 +65,23 @@ export default class Logs extends React.PureComponent<Props, State> {
|
|||||||
dateFrom: '',
|
dateFrom: '',
|
||||||
dateTo: '',
|
dateTo: '',
|
||||||
filteredLogs: [],
|
filteredLogs: [],
|
||||||
loadingLogs: true,
|
loading: true,
|
||||||
logLevels: [],
|
logLevels: [],
|
||||||
search: '',
|
search: '',
|
||||||
serverNames: [],
|
serverNames: [],
|
||||||
page: 0,
|
page: 0,
|
||||||
perPage: 1000,
|
perPage: 1000,
|
||||||
loadingPlain: true,
|
isPlainLogs: props.isPlainLogs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.isPlainLogs) {
|
|
||||||
this.reloadPlain();
|
|
||||||
} else {
|
|
||||||
this.reload();
|
this.reload();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
if (this.state.page !== prevState.page && this.props.isPlainLogs) {
|
if (this.state.isPlainLogs && (this.state.page !== prevState.page || !this.props.plainLogs?.length)) {
|
||||||
this.reloadPlain();
|
this.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,23 +94,25 @@ export default class Logs extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reload = async () => {
|
reload = async () => {
|
||||||
this.setState({loadingLogs: true});
|
this.setState({loading: true});
|
||||||
|
if (this.state.isPlainLogs) {
|
||||||
|
await this.props.actions.getPlainLogs(
|
||||||
|
this.state.page,
|
||||||
|
this.state.perPage,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
await this.props.actions.getLogs({
|
await this.props.actions.getLogs({
|
||||||
serverNames: this.state.serverNames,
|
serverNames: this.state.serverNames,
|
||||||
logLevels: this.state.logLevels,
|
logLevels: this.state.logLevels,
|
||||||
dateFrom: this.state.dateFrom,
|
dateFrom: this.state.dateFrom,
|
||||||
dateTo: this.state.dateTo,
|
dateTo: this.state.dateTo,
|
||||||
});
|
});
|
||||||
this.setState({loadingLogs: false});
|
}
|
||||||
|
this.setState({loading: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
reloadPlain = async () => {
|
onLogFormatToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({loadingPlain: true});
|
this.setState({isPlainLogs: event.target.value === 'plain'});
|
||||||
await this.props.actions.getPlainLogs(
|
|
||||||
this.state.page,
|
|
||||||
this.state.perPage,
|
|
||||||
);
|
|
||||||
this.setState({loadingPlain: false});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchChange = (search: string) => {
|
onSearchChange = (search: string) => {
|
||||||
@@ -140,45 +140,79 @@ export default class Logs extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = this.props.isPlainLogs ? (
|
const list = this.state.isPlainLogs ? (
|
||||||
<>
|
|
||||||
<div className='banner'>
|
|
||||||
<div className='banner__content'>
|
|
||||||
<FormattedMessage {...messages.bannerDesc}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='banner-buttons'>
|
|
||||||
<button
|
|
||||||
type='submit'
|
|
||||||
className='btn btn-primary'
|
|
||||||
onClick={this.reloadPlain}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='admin.logs.ReloadLogs'
|
|
||||||
defaultMessage='Reload Logs'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<ExternalLink
|
|
||||||
location='download_logs'
|
|
||||||
className='btn btn-primary'
|
|
||||||
href={Client4.getUrl() + '/api/v4/logs/download'}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='admin.logs.DownloadLogs'
|
|
||||||
defaultMessage='Download Logs'
|
|
||||||
/>
|
|
||||||
</ExternalLink>
|
|
||||||
</div>
|
|
||||||
<PlainLogList
|
<PlainLogList
|
||||||
|
loading={this.state.loading}
|
||||||
logs={this.props.plainLogs}
|
logs={this.props.plainLogs}
|
||||||
nextPage={this.nextPage}
|
nextPage={this.nextPage}
|
||||||
previousPage={this.previousPage}
|
previousPage={this.previousPage}
|
||||||
page={this.state.page}
|
page={this.state.page}
|
||||||
perPage={this.state.perPage}
|
perPage={this.state.perPage}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<LogList
|
||||||
|
loading={this.state.loading}
|
||||||
|
logs={this.state.search ? this.state.filteredLogs : this.props.logs}
|
||||||
|
onSearchChange={this.onSearchChange}
|
||||||
|
search={this.state.search}
|
||||||
|
onFiltersChange={this.onFiltersChange}
|
||||||
|
filters={{
|
||||||
|
dateFrom: this.state.dateFrom,
|
||||||
|
dateTo: this.state.dateTo,
|
||||||
|
logLevels: this.state.logLevels,
|
||||||
|
serverNames: this.state.serverNames,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
let toggleLogFormat;
|
||||||
|
if (!this.props.isPlainLogs) {
|
||||||
|
toggleLogFormat = (
|
||||||
|
<div
|
||||||
|
className='banner-buttons__log-format'
|
||||||
|
id='admin.logs.LogFormat'
|
||||||
|
role='radiogroup'
|
||||||
|
aria-labelledby='admin.logs.LogFormat.legend'
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
id='admin.logs.LogFormat.legend'
|
||||||
|
>
|
||||||
|
<FormattedMessage {...messages.logFormatTitle}/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
id='admin.logs.LogFormat.json'
|
||||||
|
name='log-format'
|
||||||
|
value='json'
|
||||||
|
checked={!this.state.isPlainLogs}
|
||||||
|
onChange={this.onLogFormatToggle}
|
||||||
|
/>
|
||||||
|
<FormattedMessage {...messages.logFormatJson}/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
id='admin.logs.LogFormat.plain'
|
||||||
|
name='log-format'
|
||||||
|
value='plain'
|
||||||
|
checked={this.state.isPlainLogs}
|
||||||
|
onChange={this.onLogFormatToggle}
|
||||||
|
/>
|
||||||
|
<FormattedMessage {...messages.logFormatPlain}/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='wrapper--admin'>
|
||||||
|
<AdminHeader>
|
||||||
|
<FormattedMessage {...messages.title}/>
|
||||||
|
</AdminHeader>
|
||||||
|
<div className='admin-console__wrapper'>
|
||||||
|
<div className='admin-logs-content admin-console__content'>
|
||||||
<div className='logs-banner'>
|
<div className='logs-banner'>
|
||||||
<div className='banner'>
|
<div className='banner'>
|
||||||
<div className='banner__content'>
|
<div className='banner__content'>
|
||||||
@@ -186,6 +220,7 @@ export default class Logs extends React.PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='banner-buttons'>
|
<div className='banner-buttons'>
|
||||||
|
{toggleLogFormat}
|
||||||
<button
|
<button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
@@ -208,29 +243,7 @@ export default class Logs extends React.PureComponent<Props, State> {
|
|||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LogList
|
{list}
|
||||||
loading={this.state.loadingLogs}
|
|
||||||
logs={this.state.search ? this.state.filteredLogs : this.props.logs}
|
|
||||||
onSearchChange={this.onSearchChange}
|
|
||||||
search={this.state.search}
|
|
||||||
onFiltersChange={this.onFiltersChange}
|
|
||||||
filters={{
|
|
||||||
dateFrom: this.state.dateFrom,
|
|
||||||
dateTo: this.state.dateTo,
|
|
||||||
logLevels: this.state.logLevels,
|
|
||||||
serverNames: this.state.serverNames,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className='wrapper--admin'>
|
|
||||||
<AdminHeader>
|
|
||||||
<FormattedMessage {...messages.title}/>
|
|
||||||
</AdminHeader>
|
|
||||||
<div className='admin-console__wrapper'>
|
|
||||||
<div className='admin-logs-content admin-console__content'>
|
|
||||||
{content}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import React from 'react';
|
|||||||
import {FormattedMessage, injectIntl, type WrappedComponentProps} from 'react-intl';
|
import {FormattedMessage, injectIntl, type WrappedComponentProps} from 'react-intl';
|
||||||
|
|
||||||
import NextIcon from 'components/widgets/icons/fa_next_icon';
|
import NextIcon from 'components/widgets/icons/fa_next_icon';
|
||||||
|
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||||
|
|
||||||
const NEXT_BUTTON_TIMEOUT = 500;
|
const NEXT_BUTTON_TIMEOUT = 500;
|
||||||
|
|
||||||
interface Props extends WrappedComponentProps {
|
interface Props extends WrappedComponentProps {
|
||||||
|
loading: boolean;
|
||||||
logs: string[];
|
logs: string[];
|
||||||
page: number;
|
page: number;
|
||||||
perPage: number;
|
perPage: number;
|
||||||
@@ -65,6 +67,14 @@ class PlainLogList extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.props.loading) {
|
||||||
|
return (
|
||||||
|
<div className='log__panel'>
|
||||||
|
<LoadingSpinner/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
let nextButton;
|
let nextButton;
|
||||||
let previousButton;
|
let previousButton;
|
||||||
|
|||||||
@@ -1493,6 +1493,9 @@
|
|||||||
"admin.logs.Error": "Error",
|
"admin.logs.Error": "Error",
|
||||||
"admin.logs.fullEvent": "Full log event",
|
"admin.logs.fullEvent": "Full log event",
|
||||||
"admin.logs.Info": "Info",
|
"admin.logs.Info": "Info",
|
||||||
|
"admin.logs.logFormatJson": "JSON",
|
||||||
|
"admin.logs.logFormatPlain": "Plain text",
|
||||||
|
"admin.logs.logFormatTitle": "Log display format:",
|
||||||
"admin.logs.next": "Next",
|
"admin.logs.next": "Next",
|
||||||
"admin.logs.options": "Options",
|
"admin.logs.options": "Options",
|
||||||
"admin.logs.prev": "Previous",
|
"admin.logs.prev": "Previous",
|
||||||
|
|||||||
@@ -186,8 +186,7 @@
|
|||||||
|
|
||||||
&:focus:not(.Input) {
|
&:focus:not(.Input) {
|
||||||
border-color: #66afe9;
|
border-color: #66afe9;
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||||
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
|
||||||
0 0 8px rgba(102, 175, 233, 0.75);
|
0 0 8px rgba(102, 175, 233, 0.75);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
@@ -219,6 +218,12 @@
|
|||||||
border: variables.$border-gray;
|
border: variables.$border-gray;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
|
&:has(.LoadingSpinner) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.admin {
|
&.admin {
|
||||||
@@ -459,7 +464,7 @@
|
|||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select {
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: "Open Sans", sans-serif;
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +486,8 @@
|
|||||||
height: 64px;
|
height: 64px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-right: 1px solid rgba(var(--center-channel-color-rgb), 0.12);
|
border-right: 1px solid
|
||||||
|
rgba(var(--center-channel-color-rgb), 0.12);
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
font-size: 3.2rem;
|
font-size: 3.2rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -492,7 +498,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(var(--center-channel-color-rgb), 0.04);
|
background-color: rgba(
|
||||||
|
var(--center-channel-color-rgb),
|
||||||
|
0.04
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1126,6 +1135,28 @@
|
|||||||
max-width: 920px;
|
max-width: 920px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner-buttons {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.banner-buttons__log-format {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 28px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user