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;
|
||||
dateTo: string;
|
||||
filteredLogs: LogObject[];
|
||||
loadingLogs: boolean;
|
||||
loading: boolean;
|
||||
logLevels: LogLevels;
|
||||
search: string;
|
||||
serverNames: LogServerNames;
|
||||
page: number;
|
||||
perPage: number;
|
||||
loadingPlain: boolean;
|
||||
isPlainLogs: boolean;
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
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.'},
|
||||
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 = [
|
||||
messages.title,
|
||||
messages.bannerDesc,
|
||||
@@ -63,27 +65,23 @@ export default class Logs extends React.PureComponent<Props, State> {
|
||||
dateFrom: '',
|
||||
dateTo: '',
|
||||
filteredLogs: [],
|
||||
loadingLogs: true,
|
||||
loading: true,
|
||||
logLevels: [],
|
||||
search: '',
|
||||
serverNames: [],
|
||||
page: 0,
|
||||
perPage: 1000,
|
||||
loadingPlain: true,
|
||||
isPlainLogs: props.isPlainLogs,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.isPlainLogs) {
|
||||
this.reloadPlain();
|
||||
} else {
|
||||
this.reload();
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (this.state.page !== prevState.page && this.props.isPlainLogs) {
|
||||
this.reloadPlain();
|
||||
if (this.state.isPlainLogs && (this.state.page !== prevState.page || !this.props.plainLogs?.length)) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,23 +94,25 @@ export default class Logs extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
reload = async () => {
|
||||
this.setState({loadingLogs: true});
|
||||
await this.props.actions.getLogs({
|
||||
serverNames: this.state.serverNames,
|
||||
logLevels: this.state.logLevels,
|
||||
dateFrom: this.state.dateFrom,
|
||||
dateTo: this.state.dateTo,
|
||||
});
|
||||
this.setState({loadingLogs: false});
|
||||
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({
|
||||
serverNames: this.state.serverNames,
|
||||
logLevels: this.state.logLevels,
|
||||
dateFrom: this.state.dateFrom,
|
||||
dateTo: this.state.dateTo,
|
||||
});
|
||||
}
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
reloadPlain = async () => {
|
||||
this.setState({loadingPlain: true});
|
||||
await this.props.actions.getPlainLogs(
|
||||
this.state.page,
|
||||
this.state.perPage,
|
||||
);
|
||||
this.setState({loadingPlain: false});
|
||||
onLogFormatToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({isPlainLogs: event.target.value === 'plain'});
|
||||
};
|
||||
|
||||
onSearchChange = (search: string) => {
|
||||
@@ -140,89 +140,72 @@ export default class Logs extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const content = this.props.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
|
||||
logs={this.props.plainLogs}
|
||||
nextPage={this.nextPage}
|
||||
previousPage={this.previousPage}
|
||||
page={this.state.page}
|
||||
perPage={this.state.perPage}
|
||||
/>
|
||||
</>
|
||||
const list = this.state.isPlainLogs ? (
|
||||
<PlainLogList
|
||||
loading={this.state.loading}
|
||||
logs={this.props.plainLogs}
|
||||
nextPage={this.nextPage}
|
||||
previousPage={this.previousPage}
|
||||
page={this.state.page}
|
||||
perPage={this.state.perPage}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className='logs-banner'>
|
||||
<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.reload}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<LogList
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<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>
|
||||
@@ -230,7 +213,37 @@ export default class Logs extends React.PureComponent<Props, State> {
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper'>
|
||||
<div className='admin-logs-content admin-console__content'>
|
||||
{content}
|
||||
<div className='logs-banner'>
|
||||
<div className='banner'>
|
||||
<div className='banner__content'>
|
||||
<FormattedMessage {...messages.bannerDesc}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='banner-buttons'>
|
||||
{toggleLogFormat}
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
onClick={this.reload}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
{list}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,12 @@ import React from 'react';
|
||||
import {FormattedMessage, injectIntl, type WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import NextIcon from 'components/widgets/icons/fa_next_icon';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
const NEXT_BUTTON_TIMEOUT = 500;
|
||||
|
||||
interface Props extends WrappedComponentProps {
|
||||
loading: boolean;
|
||||
logs: string[];
|
||||
page: number;
|
||||
perPage: number;
|
||||
@@ -65,6 +67,14 @@ class PlainLogList extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<div className='log__panel'>
|
||||
<LoadingSpinner/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let content = null;
|
||||
let nextButton;
|
||||
let previousButton;
|
||||
|
||||
@@ -1493,6 +1493,9 @@
|
||||
"admin.logs.Error": "Error",
|
||||
"admin.logs.fullEvent": "Full log event",
|
||||
"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.options": "Options",
|
||||
"admin.logs.prev": "Previous",
|
||||
|
||||
@@ -186,8 +186,7 @@
|
||||
|
||||
&:focus:not(.Input) {
|
||||
border-color: #66afe9;
|
||||
box-shadow:
|
||||
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.75);
|
||||
outline: 0;
|
||||
}
|
||||
@@ -219,6 +218,12 @@
|
||||
border: variables.$border-gray;
|
||||
margin-top: 14px;
|
||||
background-color: white;
|
||||
|
||||
&:has(.LoadingSpinner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.admin {
|
||||
@@ -459,7 +464,7 @@
|
||||
|
||||
button,
|
||||
select {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
@@ -481,7 +486,8 @@
|
||||
height: 64px;
|
||||
align-items: 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;
|
||||
font-size: 3.2rem;
|
||||
text-decoration: none;
|
||||
@@ -492,7 +498,10 @@
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user