From f7976254bb8403b7e875dd8d310132a84da20b9b Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Mon, 14 Apr 2025 11:38:48 +0200 Subject: [PATCH] MM-57097: Add a toggle to switch between plain and JSON logs format (#28806) --- .../admin_console/server_logs/logs.tsx | 231 +++++++++--------- .../server_logs/plain_log_list.tsx | 10 + webapp/channels/src/i18n/en.json | 3 + .../src/sass/routes/_admin-console.scss | 41 +++- 4 files changed, 171 insertions(+), 114 deletions(-) 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 d3d96b7381..5871f152bd 100644 --- a/webapp/channels/src/components/admin_console/server_logs/logs.tsx +++ b/webapp/channels/src/components/admin_console/server_logs/logs.tsx @@ -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 { 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 { }; 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) => { + this.setState({isPlainLogs: event.target.value === 'plain'}); }; onSearchChange = (search: string) => { @@ -140,89 +140,72 @@ export default class Logs extends React.PureComponent { }; render() { - const content = this.props.isPlainLogs ? ( - <> -
-
- -
-
-
- - - - -
- - + const list = this.state.isPlainLogs ? ( + ) : ( - <> -
-
-
- -
-
-
- - - - -
-
- - + ); + + let toggleLogFormat; + if (!this.props.isPlainLogs) { + toggleLogFormat = ( +
+ + + + + + +
+ ); + } + return (
@@ -230,7 +213,37 @@ export default class Logs extends React.PureComponent {
- {content} +
+
+
+ +
+
+
+ {toggleLogFormat} + + + + +
+
+ {list}
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 index dadb44f277..7e8da498d2 100644 --- 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 @@ -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 { }; render() { + if (this.props.loading) { + return ( +
+ +
+ ); + } + let content = null; let nextButton; let previousButton; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 984c3eb217..84e8f14ae7 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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", diff --git a/webapp/channels/src/sass/routes/_admin-console.scss b/webapp/channels/src/sass/routes/_admin-console.scss index a1965a08fd..a0da59f430 100644 --- a/webapp/channels/src/sass/routes/_admin-console.scss +++ b/webapp/channels/src/sass/routes/_admin-console.scss @@ -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; }