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