diff --git a/webapp/channels/src/components/admin_console/server_logs/index.ts b/webapp/channels/src/components/admin_console/server_logs/index.ts index e633e4974a..c00088a22d 100644 --- a/webapp/channels/src/components/admin_console/server_logs/index.ts +++ b/webapp/channels/src/components/admin_console/server_logs/index.ts @@ -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) { return { actions: bindActionCreators({ getLogs, + getPlainLogs, }, dispatch), }; } diff --git a/webapp/channels/src/components/admin_console/server_logs/logs.tsx b/webapp/channels/src/components/admin_console/server_logs/logs.tsx index 0edbb270fc..959ecbc131 100644 --- a/webapp/channels/src/components/admin_console/server_logs/logs.tsx +++ b/webapp/channels/src/components/admin_console/server_logs/logs.tsx @@ -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 { @@ -41,13 +56,34 @@ export default class Logs extends React.PureComponent { 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 { 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 { 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 ? ( + <> +
+
+ +
+
+ + + + ) : ( + <> +
+
+
+ +
+
+ +
+ + + ); return (
{
-
-
-
- -
-
- -
- + {content}
diff --git a/webapp/channels/src/components/admin_console/server_logs/plain_log_list.tsx b/webapp/channels/src/components/admin_console/server_logs/plain_log_list.tsx new file mode 100644 index 0000000000..57fcb95b38 --- /dev/null +++ b/webapp/channels/src/components/admin_console/server_logs/plain_log_list.tsx @@ -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 { + private logPanel: React.RefObject; + + 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) => { + e.preventDefault(); + + this.setState({nextDisabled: true}); + setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT); + + this.props.nextPage(); + }; + + previousPage = (e: React.MouseEvent) => { + e.preventDefault(); + + this.props.previousPage(); + }; + + render() { + let content = null; + let nextButton; + let previousButton; + + if (this.props.logs.length >= this.props.perPage) { + nextButton = ( + + ); + } + + if (this.props.page > 0) { + previousButton = ( + + ); + } + + 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(
); + content.push( + + {this.props.logs[i]} + , + ); + } + return ( +
+
+ {content} +
+
+ {previousButton} + {nextButton} +
+
+ ); + } +} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 91fbdb3a7f..a7f29fd777 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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", diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/admin.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/admin.ts index d73555d207..40e77ec7d5 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/admin.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/admin.ts @@ -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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/admin.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/admin.test.ts index a63f41cd3f..2bf512b8cf 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/admin.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/admin.test.ts @@ -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'). diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts index 70ace0e293..a062258a52 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts @@ -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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/admin.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/admin.ts index 3d3ffd58f8..23b36230e2 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/admin.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/admin.ts @@ -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 = {}, 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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/admin.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/admin.ts index 5488eea75b..ec3b45b5a5 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/admin.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/admin.ts @@ -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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts index 05a6bc904d..c06991c245 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts @@ -96,6 +96,7 @@ const state: GlobalState = { }, admin: { logs: [], + plainLogs: [], audits: {}, config: {}, environmentConfig: {}, diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index ba9d713775..da6dafb44c 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -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( + `${this.getBaseRoute()}/logs${buildQueryString({page, logs_per_page: perPage})}`, + {method: 'get'}, + ); + }; + getAudits = (page = 0, perPage = PER_PAGE_DEFAULT) => { return this.doFetch( `${this.getBaseRoute()}/audits${buildQueryString({page, per_page: perPage})}`, diff --git a/webapp/platform/types/src/admin.ts b/webapp/platform/types/src/admin.ts index 94434b9504..2e15030fad 100644 --- a/webapp/platform/types/src/admin.ts +++ b/webapp/platform/types/src/admin.ts @@ -43,6 +43,7 @@ export type LogFilter = { export type AdminState = { logs: LogObject[]; + plainLogs: string[]; audits: Record; config: Partial; environmentConfig: Partial;