From 3224e0d3a229d006b98eef02ffee6d8618ed757b Mon Sep 17 00:00:00 2001 From: David Krauser Date: Fri, 6 Dec 2024 10:39:36 -0500 Subject: [PATCH] MM-61991 Show server hostname in about modal (#29413) This introduces a new entry in the `Main Menu -> About` modal with the hostname of the currently connected websocket. This will be used to aid debugging issues in clustered environments by showing which node in the cluster is servicing requests for a particular websocket. This information is only visible in self-managed instances. It will not be visible on cloud instances. --- server/channels/app/platform/web_conn.go | 10 ++++ .../src/actions/websocket_actions.jsx | 8 +++ .../about_build_modal.test.tsx | 24 +++++++-- .../about_build_modal/about_build_modal.tsx | 50 +++++++++++++++++++ .../src/components/about_build_modal/index.ts | 3 ++ ...post_body_additional_content.test.tsx.snap | 2 + .../product_notices.test.tsx | 1 + webapp/channels/src/i18n/en.json | 3 ++ .../src/action_types/general.ts | 1 + .../src/reducers/websocket.ts | 9 ++++ .../src/store/initial_state.ts | 1 + .../__snapshots__/pluggable.test.tsx.snap | 4 ++ .../channels/src/sass/responsive/_mobile.scss | 1 + .../src/sass/routes/_about-modal.scss | 1 + webapp/platform/client/src/websocket.ts | 5 ++ webapp/platform/types/src/store.ts | 1 + 16 files changed, 121 insertions(+), 3 deletions(-) diff --git a/server/channels/app/platform/web_conn.go b/server/channels/app/platform/web_conn.go index 1090b7fa720..c92856eecd6 100644 --- a/server/channels/app/platform/web_conn.go +++ b/server/channels/app/platform/web_conn.go @@ -11,6 +11,7 @@ import ( "fmt" "net" "net/http" + "os" "slices" "strconv" "strings" @@ -776,6 +777,15 @@ func (wc *WebConn) createHelloMessage() *model.WebSocketEvent { wc.Platform.ClientConfigHash(), ee)) msg.Add("connection_id", wc.connectionID.Load()) + + hostname, err := os.Hostname() + if err != nil { + wc.Platform.logger.Error("Could not get hostname", mlog.Err(err)) + // return without the hostname in the message + return msg + } + + msg.Add("server_hostname", hostname) return msg } diff --git a/webapp/channels/src/actions/websocket_actions.jsx b/webapp/channels/src/actions/websocket_actions.jsx index 57e3ff5fd03..b54586c5c7f 100644 --- a/webapp/channels/src/actions/websocket_actions.jsx +++ b/webapp/channels/src/actions/websocket_actions.jsx @@ -1313,6 +1313,7 @@ export function handleStatusChangedEvent(msg) { function handleHelloEvent(msg) { dispatch(setServerVersion(msg.data.server_version)); dispatch(setConnectionId(msg.data.connection_id)); + dispatch(setServerHostname(msg.data.server_hostname)); } function handleReactionAddedEvent(msg) { @@ -1333,6 +1334,13 @@ function setConnectionId(connectionId) { }; } +function setServerHostname(serverHostname) { + return { + type: GeneralTypes.SET_SERVER_HOSTNAME, + payload: {serverHostname}, + }; +} + function handleAddEmoji(msg) { const data = JSON.parse(msg.data.emoji); diff --git a/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx b/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx index 5827c3b1a06..ab039f7284c 100644 --- a/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx +++ b/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx @@ -25,11 +25,19 @@ describe('components/AboutBuildModal', () => { let config: Partial = {}; let license: ClientLicense = {}; + let socketStatus = { + connected: false, + serverHostname: '', + }; afterEach(() => { global.Date = RealDate; config = {}; license = {}; + socketStatus = { + connected: false, + serverHostname: '', + }; }); beforeEach(() => { @@ -51,10 +59,14 @@ describe('components/AboutBuildModal', () => { IsLicensed: 'true', Company: 'Mattermost Inc', }; + socketStatus = { + connected: true, + serverHostname: 'mock.localhost', + }; }); test('should match snapshot for enterprise edition', () => { - renderAboutBuildModal({config, license}); + renderAboutBuildModal({config, license, socketStatus}); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.0'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77'); expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123456'); @@ -62,6 +74,7 @@ describe('components/AboutBuildModal', () => { expect(screen.getByText('Modern communication from behind your firewall.')).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'mattermost.com'})).toHaveAttribute('href', 'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid='); expect(screen.getByText('EE Build Hash: 0123456789abcdef', {exact: false})).toBeInTheDocument(); + expect(screen.queryByText('Hostname: mock.localhost', {exact: false})).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt'); expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt'); @@ -75,7 +88,7 @@ describe('components/AboutBuildModal', () => { BuildHashEnterprise: '', }; - renderAboutBuildModal({config: teamConfig, license: {}}); + renderAboutBuildModal({config: teamConfig, license: {}, socketStatus: {connected: false}}); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.0'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77'); expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123456'); @@ -83,6 +96,7 @@ describe('components/AboutBuildModal', () => { expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid='); expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument(); + expect(screen.queryByText('Hostname: disconnected', {exact: false})).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt'); expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt'); @@ -123,7 +137,7 @@ describe('components/AboutBuildModal', () => { BuildNumber: 'dev', }; - renderAboutBuildModal({config: sameBuildConfig, license: {}}); + renderAboutBuildModal({config: sameBuildConfig, license: {}, socketStatus: {connected: true}}); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: dev'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77'); @@ -132,6 +146,7 @@ describe('components/AboutBuildModal', () => { expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid='); expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument(); + expect(screen.queryByText('Hostname: server did not provide hostname', {exact: false})).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt'); expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt'); @@ -158,6 +173,7 @@ describe('components/AboutBuildModal', () => { , state, @@ -185,6 +201,7 @@ describe('components/AboutBuildModal', () => { , state, @@ -207,6 +224,7 @@ describe('components/AboutBuildModal', () => { onExited, config, license, + socketStatus, ...props, }; diff --git a/webapp/channels/src/components/about_build_modal/about_build_modal.tsx b/webapp/channels/src/components/about_build_modal/about_build_modal.tsx index 5d5bb754955..57aab101921 100644 --- a/webapp/channels/src/components/about_build_modal/about_build_modal.tsx +++ b/webapp/channels/src/components/about_build_modal/about_build_modal.tsx @@ -15,6 +15,11 @@ import {AboutLinks} from 'utils/constants'; import AboutBuildModalCloud from './about_build_modal_cloud/about_build_modal_cloud'; +type SocketStatus = { + connected: boolean; + serverHostname: string | undefined; +} + type Props = { /** @@ -31,6 +36,8 @@ type Props = { * Global license object */ license: ClientLicense; + + socketStatus: SocketStatus; }; type State = { @@ -182,6 +189,48 @@ export default class AboutBuildModal extends React.PureComponent { const mmversion: string | undefined = config.BuildNumber === 'dev' ? config.BuildNumber : config.Version; + let serverHostname; + if (!this.props.socketStatus.connected) { + serverHostname = ( +
+ + + +
+ ); + } else if (this.props.socketStatus.serverHostname) { + serverHostname = ( +
+ + + {this.props.socketStatus.serverHostname} +
+ ); + } else { + serverHostname = ( +
+ + + +
+ ); + } + return ( { /> {'\u00a0' + config.SQLDriverName} + {serverHostname} {licensee} diff --git a/webapp/channels/src/components/about_build_modal/index.ts b/webapp/channels/src/components/about_build_modal/index.ts index 71664588cdd..5defc4e8039 100644 --- a/webapp/channels/src/components/about_build_modal/index.ts +++ b/webapp/channels/src/components/about_build_modal/index.ts @@ -5,6 +5,8 @@ import {connect} from 'react-redux'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getSocketStatus} from 'selectors/views/websocket'; + import type {GlobalState} from 'types/store'; import AboutBuildModal from './about_build_modal'; @@ -13,6 +15,7 @@ function mapStateToProps(state: GlobalState) { return { config: getConfig(state), license: getLicense(state), + socketStatus: getSocketStatus(state), }; } diff --git a/webapp/channels/src/components/post_view/post_body_additional_content/__snapshots__/post_body_additional_content.test.tsx.snap b/webapp/channels/src/components/post_view/post_body_additional_content/__snapshots__/post_body_additional_content.test.tsx.snap index 7afaaccb562..05e879c4793 100644 --- a/webapp/channels/src/components/post_view/post_body_additional_content/__snapshots__/post_body_additional_content.test.tsx.snap +++ b/webapp/channels/src/components/post_view/post_body_additional_content/__snapshots__/post_body_additional_content.test.tsx.snap @@ -116,6 +116,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c "reconnectListeners": Set {}, "responseCallbacks": Object {}, "responseSequence": 1, + "serverHostname": "", "serverSequence": 0, } } @@ -163,6 +164,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c "reconnectListeners": Set {}, "responseCallbacks": Object {}, "responseSequence": 1, + "serverHostname": "", "serverSequence": 0, } } diff --git a/webapp/channels/src/components/product_notices_modal/product_notices.test.tsx b/webapp/channels/src/components/product_notices_modal/product_notices.test.tsx index 611560c3736..7226925fc6c 100644 --- a/webapp/channels/src/components/product_notices_modal/product_notices.test.tsx +++ b/webapp/channels/src/components/product_notices_modal/product_notices.test.tsx @@ -41,6 +41,7 @@ describe('ProductNoticesModal', () => { connectionId: '', lastConnectAt: 1599760193593, lastDisconnectAt: 0, + serverHostname: '', }, actions: { getInProductNotices: jest.fn().mockResolvedValue({data: noticesData}), diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index aab13955a47..2638e4396c4 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -14,6 +14,9 @@ "about.licensed": "Licensed to:", "about.notice": "Mattermost is made possible by the open source software used in our server, desktop and mobile apps.", "about.privacy": "Privacy Policy", + "about.serverDisconnected": "disconnected", + "about.serverHostname": "Hostname:", + "about.serverUnknown": "server did not provide hostname", "about.teamEditionLearn": "Join the Mattermost community at ", "about.teamEditionSt": "All your team communication in one place, instantly searchable and accessible anywhere.", "about.teamEditiont0": "Team Edition", diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/general.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/general.ts index 7ec5d71b254..c94d5d6beb1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/general.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/general.ts @@ -21,6 +21,7 @@ export default keyMirror({ WEBSOCKET_FAILURE: null, WEBSOCKET_CLOSED: null, SET_CONNECTION_ID: null, + SET_SERVER_HOSTNAME: null, SET_CONFIG_AND_LICENSE: null, diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/websocket.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/websocket.ts index 33786531bd3..3d2012dc5eb 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/websocket.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/websocket.ts @@ -11,6 +11,7 @@ function getInitialState() { lastConnectAt: 0, lastDisconnectAt: 0, connectionId: '', + serverHostname: '', }; } @@ -26,6 +27,7 @@ export default function reducer(state = getInitialState(), action: AnyAction) { ...state, connected: false, lastDisconnectAt: action.timestamp, + serverHostname: '', }; } @@ -44,5 +46,12 @@ export default function reducer(state = getInitialState(), action: AnyAction) { }; } + if (action.type === GeneralTypes.SET_SERVER_HOSTNAME) { + return { + ...state, + serverHostname: action.payload.serverHostname, + }; + } + return state; } 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 a00cfd8744e..e6e63a2d965 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 @@ -318,6 +318,7 @@ const state: GlobalState = { lastConnectAt: 0, lastDisconnectAt: 0, connectionId: '', + serverHostname: '', }, }; export default state; diff --git a/webapp/channels/src/plugins/pluggable/__snapshots__/pluggable.test.tsx.snap b/webapp/channels/src/plugins/pluggable/__snapshots__/pluggable.test.tsx.snap index 651bf5faa5c..cce8b71c8a7 100644 --- a/webapp/channels/src/plugins/pluggable/__snapshots__/pluggable.test.tsx.snap +++ b/webapp/channels/src/plugins/pluggable/__snapshots__/pluggable.test.tsx.snap @@ -124,6 +124,7 @@ exports[`plugins/Pluggable should match snapshot with extended component 1`] = ` "reconnectListeners": Set {}, "responseCallbacks": Object {}, "responseSequence": 1, + "serverHostname": "", "serverSequence": 0, } } @@ -262,6 +263,7 @@ exports[`plugins/Pluggable should match snapshot with extended component with pl "reconnectListeners": Set {}, "responseCallbacks": Object {}, "responseSequence": 1, + "serverHostname": "", "serverSequence": 0, } } @@ -534,6 +536,7 @@ exports[`plugins/Pluggable should match snapshot with null pluggableId 1`] = ` "reconnectListeners": Set {}, "responseCallbacks": Object {}, "responseSequence": 1, + "serverHostname": "", "serverSequence": 0, } } @@ -673,6 +676,7 @@ exports[`plugins/Pluggable should match snapshot with valid pluggableId 1`] = ` "reconnectListeners": Set {}, "responseCallbacks": Object {}, "responseSequence": 1, + "serverHostname": "", "serverSequence": 0, } } diff --git a/webapp/channels/src/sass/responsive/_mobile.scss b/webapp/channels/src/sass/responsive/_mobile.scss index a41f92576f3..48f0a79772e 100644 --- a/webapp/channels/src/sass/responsive/_mobile.scss +++ b/webapp/channels/src/sass/responsive/_mobile.scss @@ -1903,6 +1903,7 @@ .about-modal__content { display: block; + overflow-wrap: anywhere; } .about-modal__hash { diff --git a/webapp/channels/src/sass/routes/_about-modal.scss b/webapp/channels/src/sass/routes/_about-modal.scss index b9c00c841ed..4eae3be0f7f 100644 --- a/webapp/channels/src/sass/routes/_about-modal.scss +++ b/webapp/channels/src/sass/routes/_about-modal.scss @@ -57,6 +57,7 @@ display: flex; flex-direction: row; padding: 1em 0 3em; + overflow-wrap: anywhere; } .about-modal__copyright { diff --git a/webapp/platform/client/src/websocket.ts b/webapp/platform/client/src/websocket.ts index 8b97f7abe8e..bd478f291bb 100644 --- a/webapp/platform/client/src/websocket.ts +++ b/webapp/platform/client/src/websocket.ts @@ -68,6 +68,7 @@ export default class WebSocketClient { private closeListeners = new Set(); private connectionId: string | null; + private serverHostname: string | null; private postedAck: boolean; constructor() { @@ -78,6 +79,7 @@ export default class WebSocketClient { this.connectFailCount = 0; this.responseCallbacks = {}; this.connectionId = ''; + this.serverHostname = ''; this.postedAck = false; } @@ -210,6 +212,9 @@ export default class WebSocketClient { // If it's a fresh connection, we have to set the connectionId regardless. // And if it's an existing connection, setting it again is harmless, and keeps the code simple. this.connectionId = msg.data.connection_id; + + // Also update the server hostname + this.serverHostname = msg.data.server_hostname; } // Now we check for sequence number, and if it does not match, diff --git a/webapp/platform/types/src/store.ts b/webapp/platform/types/src/store.ts index ac313d12447..25d9f7eb2f3 100644 --- a/webapp/platform/types/src/store.ts +++ b/webapp/platform/types/src/store.ts @@ -94,5 +94,6 @@ export type GlobalState = { lastConnectAt: number; lastDisconnectAt: number; connectionId: string; + serverHostname: string; }; };