[MM-51453] - Bring back old log list if format is plain text (#23312)

* [MM-51453] - Bring back old log list if format is plain text

* i18n error

* linter

* remove redundant file

* linter again

* PR  comments

* i18n again

* Update tests

---------

Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MacBook-Pro.local>
This commit is contained in:
na 2023-05-08 22:53:09 +07:00 committed by GitHub
parent 8e6a5f6ffc
commit a1470c77ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 344 additions and 40 deletions

View File

@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';
import {getLogs} from 'mattermost-redux/actions/admin';
import {getLogs, getPlainLogs} from 'mattermost-redux/actions/admin';
import * as Selectors from 'mattermost-redux/selectors/entities/admin';
@ -15,8 +15,12 @@ import {GlobalState} from 'types/store';
import Logs from './logs';
function mapStateToProps(state: GlobalState) {
const config = Selectors.getConfig(state);
return {
logs: Selectors.getAllLogs(state),
plainLogs: Selectors.getPlainLogs(state),
isPlainLogs: config.LogSettings?.FileJson === false,
};
}
@ -24,6 +28,7 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators({
getLogs,
getPlainLogs,
}, dispatch),
};
}

View File

@ -9,14 +9,26 @@ import {ActionFunc} from 'mattermost-redux/types/actions';
import FormattedAdminHeader from 'components/widgets/admin_console/formatted_admin_header';
import {LogFilter, LogLevels, LogObject, LogServerNames} from '@mattermost/types/admin';
import {
LogFilter,
LogLevels,
LogObject,
LogServerNames,
} from '@mattermost/types/admin';
import LogList from './log_list';
import PlainLogList from './plain_log_list';
type Props = {
logs: LogObject[];
plainLogs: string[];
isPlainLogs: boolean;
actions: {
getLogs: (logFilter: LogFilter) => ActionFunc;
getPlainLogs: (
page?: number | undefined,
perPage?: number | undefined
) => ActionFunc;
};
};
@ -28,6 +40,9 @@ type State = {
logLevels: LogLevels;
search: string;
serverNames: LogServerNames;
page: number;
perPage: number;
loadingPlain: boolean;
};
export default class Logs extends React.PureComponent<Props, State> {
@ -41,13 +56,34 @@ export default class Logs extends React.PureComponent<Props, State> {
logLevels: [],
search: '',
serverNames: [],
page: 0,
perPage: 1000,
loadingPlain: true,
};
}
componentDidMount() {
this.reload();
if (this.props.isPlainLogs) {
this.reloadPlain();
} else {
this.reload();
}
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (this.state.page !== prevState.page && this.props.isPlainLogs) {
this.reloadPlain();
}
}
nextPage = () => {
this.setState({page: this.state.page + 1});
};
previousPage = () => {
this.setState({page: this.state.page - 1});
};
reload = async () => {
this.setState({loadingLogs: true});
await this.props.actions.getLogs({
@ -59,6 +95,15 @@ export default class Logs extends React.PureComponent<Props, State> {
this.setState({loadingLogs: false});
};
reloadPlain = async () => {
this.setState({loadingPlain: true});
await this.props.actions.getPlainLogs(
this.state.page,
this.state.perPage,
);
this.setState({loadingPlain: false});
};
onSearchChange = (search: string) => {
this.setState({search}, () => this.performSearch());
};
@ -72,11 +117,83 @@ export default class Logs extends React.PureComponent<Props, State> {
this.setState({filteredLogs});
}, 200);
onFiltersChange = ({dateFrom, dateTo, logLevels, serverNames}: LogFilter) => {
this.setState({dateFrom, dateTo, logLevels, serverNames}, () => this.reload());
onFiltersChange = ({
dateFrom,
dateTo,
logLevels,
serverNames,
}: LogFilter) => {
this.setState({dateFrom, dateTo, logLevels, serverNames}, () =>
this.reload(),
);
};
render() {
const content = this.props.isPlainLogs ? (
<>
<div className='banner'>
<div className='banner__content'>
<FormattedMessage
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.'
/>
</div>
</div>
<button
type='submit'
className='btn btn-primary'
onClick={this.reloadPlain}
>
<FormattedMessage
id='admin.logs.ReloadLogs'
defaultMessage='Reload Logs'
/>
</button>
<PlainLogList
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
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.'
/>
</div>
</div>
<button
type='submit'
className='btn btn-primary'
onClick={this.reload}
>
<FormattedMessage
id='admin.logs.ReloadLogs'
defaultMessage='Reload Logs'
/>
</button>
</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,
}}
/>
</>
);
return (
<div className='wrapper--admin'>
<FormattedAdminHeader
@ -86,39 +203,7 @@ export default class Logs extends React.PureComponent<Props, State> {
<div className='admin-console__wrapper'>
<div className='admin-logs-content admin-console__content'>
<div className='logs-banner'>
<div className='banner'>
<div className='banner__content'>
<FormattedMessage
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.'
/>
</div>
</div>
<button
type='submit'
className='btn btn-primary'
onClick={this.reload}
>
<FormattedMessage
id='admin.logs.ReloadLogs'
defaultMessage='ReloadLogs'
/>
</button>
</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,
}}
/>
{content}
</div>
</div>
</div>

View File

@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import LocalizedIcon from 'components/localized_icon';
import NextIcon from 'components/widgets/icons/fa_next_icon';
import {t} from 'utils/i18n';
const NEXT_BUTTON_TIMEOUT = 500;
type Props = {
logs: string[];
page: number;
perPage: number;
nextPage: () => void;
previousPage: () => void;
};
type State = {
nextDisabled: boolean;
};
export default class PlainLogList extends React.PureComponent<Props, State> {
private logPanel: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
super(props);
this.logPanel = React.createRef();
this.state = {
nextDisabled: false,
};
}
componentDidMount() {
// Scroll Down to get the latest logs
const node = this.logPanel.current;
if (node) {
node.scrollTop = node.scrollHeight;
}
}
componentDidUpdate() {
// Scroll Down to get the latest logs
const node = this.logPanel.current;
if (node) {
node.scrollTop = node.scrollHeight;
}
}
nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
this.setState({nextDisabled: true});
setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT);
this.props.nextPage();
};
previousPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
this.props.previousPage();
};
render() {
let content = null;
let nextButton;
let previousButton;
if (this.props.logs.length >= this.props.perPage) {
nextButton = (
<button
type='button'
className='btn btn-default filter-control filter-control__next pull-right'
onClick={this.nextPage}
disabled={this.state.nextDisabled}
>
<FormattedMessage
id='admin.logs.next'
defaultMessage='Next'
/>
<NextIcon additionalClassName='ml-2'/>
</button>
);
}
if (this.props.page > 0) {
previousButton = (
<button
type='button'
className='btn btn-default filter-control filter-control__prev'
onClick={this.previousPage}
>
<LocalizedIcon
className='fa fa-angle-left'
title={{id: t('generic_icons.previous'), defaultMessage: 'Previous Icon'}}
/>
<FormattedMessage
id='admin.logs.prev'
defaultMessage='Previous'
/>
</button>
);
}
content = [];
for (let i = 0; i < this.props.logs.length; i++) {
const style: React.CSSProperties = {
whiteSpace: 'nowrap',
fontFamily: 'monospace',
color: '',
};
if (this.props.logs[i].indexOf('[EROR]') > 0) {
style.color = 'red';
}
content.push(<br key={'br_' + i}/>);
content.push(
<span
key={'log_' + i}
style={style}
>
{this.props.logs[i]}
</span>,
);
}
return (
<div>
<div
tabIndex={-1}
ref={this.logPanel}
className='log__panel'
>
{content}
</div>
<div className='pt-3 pb-3 filter-controls'>
{previousButton}
{nextButton}
</div>
</div>
);
}
}

View File

@ -1405,7 +1405,9 @@
"admin.logs.Error": "Error",
"admin.logs.fullEvent": "Full log event",
"admin.logs.Info": "Info",
"admin.logs.next": "Next",
"admin.logs.options": "Options",
"admin.logs.prev": "Previous",
"admin.logs.ReloadLogs": "Reload Logs",
"admin.logs.showErrors": "Show last {n} errors",
"admin.logs.title": "Server Logs",

View File

@ -20,6 +20,7 @@ export default keyMirror({
DISABLE_PLUGIN_REQUEST: null,
RECEIVED_LOGS: null,
RECEIVED_PLAIN_LOGS: null,
RECEIVED_AUDITS: null,
RECEIVED_CONFIG: null,
RECEIVED_ENVIRONMENT_CONFIG: null,

View File

@ -37,6 +37,27 @@ describe('Actions.Admin', () => {
TestHelper.tearDown();
});
it('getPlainLogs', async () => {
nock(Client4.getBaseRoute()).
get('/logs').
query(true).
reply(200, [
'[2017/04/04 14:56:19 EDT] [INFO] Starting Server...',
'[2017/04/04 14:56:19 EDT] [INFO] Server is listening on :8065',
'[2017/04/04 15:01:48 EDT] [INFO] Stopping Server...',
'[2017/04/04 15:01:48 EDT] [INFO] Closing SqlStore',
]);
await Actions.getPlainLogs()(store.dispatch, store.getState);
const state = store.getState();
const logs = state.entities.admin.plainLogs;
expect(logs).toBeTruthy();
expect(Object.keys(logs).length > 0).toBeTruthy();
});
it('getAudits', async () => {
nock(Client4.getBaseRoute()).
get('/audits').

View File

@ -44,6 +44,17 @@ export function getLogs({serverNames = [], logLevels = [], dateFrom, dateTo}: Lo
});
}
export function getPlainLogs(page = 0, perPage: number = General.LOGS_PAGE_SIZE_DEFAULT): ActionFunc {
return bindClientFunc({
clientFunc: Client4.getPlainLogs,
onSuccess: [AdminTypes.RECEIVED_PLAIN_LOGS],
params: [
page,
perPage,
],
});
}
export function getAudits(page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc {
return bindClientFunc({
clientFunc: Client4.getAudits,

View File

@ -33,6 +33,19 @@ function logs(state: string[] = [], action: GenericAction) {
}
}
function plainLogs(state: string[] = [], action: GenericAction) {
switch (action.type) {
case AdminTypes.RECEIVED_PLAIN_LOGS: {
return action.data;
}
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
return state;
}
}
function audits(state: Record<string, Audit> = {}, action: GenericAction) {
switch (action.type) {
case AdminTypes.RECEIVED_AUDITS: {
@ -658,9 +671,12 @@ function dataRetentionCustomPoliciesCount(state = 0, action: GenericAction) {
export default combineReducers({
// array of strings each representing a log entry
// array of LogObjects each representing a log entry (JSON)
logs,
// array of strings each representing a log entry (legacy)
plainLogs,
// object where every key is an audit id and has an object with audit details
audits,

View File

@ -12,6 +12,11 @@ import {LogObject} from '@mattermost/types/admin';
export function getLogs(state: GlobalState) {
return state.entities.admin.logs;
}
export function getPlainLogs(state: GlobalState) {
return state.entities.admin.plainLogs;
}
export const getAllLogs = createSelector(
'getAllLogs',
getLogs,

View File

@ -96,6 +96,7 @@ const state: GlobalState = {
},
admin: {
logs: [],
plainLogs: [],
audits: {},
config: {},
environmentConfig: {},

View File

@ -154,7 +154,7 @@ const HEADER_USER_AGENT = 'User-Agent';
export const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
export const HEADER_X_VERSION_ID = 'X-Version-Id';
const LOGS_PER_PAGE_DEFAULT = 10000;
const AUTOCOMPLETE_LIMIT_DEFAULT = 25;
const PER_PAGE_DEFAULT = 60;
export const DEFAULT_LIMIT_BEFORE = 30;
@ -3023,6 +3023,13 @@ export default class Client4 {
);
};
getPlainLogs = (page = 0, perPage = LOGS_PER_PAGE_DEFAULT) => {
return this.doFetch<string[]>(
`${this.getBaseRoute()}/logs${buildQueryString({page, logs_per_page: perPage})}`,
{method: 'get'},
);
};
getAudits = (page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch<Audit[]>(
`${this.getBaseRoute()}/audits${buildQueryString({page, per_page: perPage})}`,

View File

@ -43,6 +43,7 @@ export type LogFilter = {
export type AdminState = {
logs: LogObject[];
plainLogs: string[];
audits: Record<string, Audit>;
config: Partial<AdminConfig>;
environmentConfig: Partial<EnvironmentConfig>;