MM-57097: Add a toggle to switch between plain and JSON logs format (#28806)

Этот коммит содержится в:
Nicolas Le Cam
2025-04-14 11:38:48 +02:00
коммит произвёл GitHub
родитель ae046fb34e
Коммит f7976254bb
4 изменённых файлов: 171 добавлений и 114 удалений

Просмотреть файл

@@ -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;
}