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.
Этот коммит содержится в:
David Krauser
2024-12-06 10:39:36 -05:00
коммит произвёл GitHub
родитель 59b8532a89
Коммит 3224e0d3a2
16 изменённых файлов: 121 добавлений и 3 удалений

Просмотреть файл

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

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -25,11 +25,19 @@ describe('components/AboutBuildModal', () => {
let config: Partial<ClientConfig> = {};
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', () => {
<AboutBuildModal
config={config}
license={license}
socketStatus={socketStatus}
onExited={onExited}
/>,
state,
@@ -185,6 +201,7 @@ describe('components/AboutBuildModal', () => {
<AboutBuildModal
config={config}
license={license}
socketStatus={socketStatus}
onExited={jest.fn()}
/>,
state,
@@ -207,6 +224,7 @@ describe('components/AboutBuildModal', () => {
onExited,
config,
license,
socketStatus,
...props,
};

Просмотреть файл

@@ -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<Props, State> {
const mmversion: string | undefined = config.BuildNumber === 'dev' ? config.BuildNumber : config.Version;
let serverHostname;
if (!this.props.socketStatus.connected) {
serverHostname = (
<div>
<FormattedMessage
id='about.serverHostname'
defaultMessage='Hostname:'
/>
<Nbsp/>
<FormattedMessage
id='about.serverDisconnected'
defaultMessage='disconnected'
/>
</div>
);
} else if (this.props.socketStatus.serverHostname) {
serverHostname = (
<div>
<FormattedMessage
id='about.serverHostname'
defaultMessage='Hostname:'
/>
<Nbsp/>
{this.props.socketStatus.serverHostname}
</div>
);
} else {
serverHostname = (
<div>
<FormattedMessage
id='about.serverHostname'
defaultMessage='Hostname:'
/>
<Nbsp/>
<FormattedMessage
id='about.serverUnknown'
defaultMessage='server did not provide hostname'
/>
</div>
);
}
return (
<Modal
dialogClassName='a11y__modal about-modal'
@@ -246,6 +295,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
/>
{'\u00a0' + config.SQLDriverName}
</div>
{serverHostname}
</div>
{licensee}
</div>

Просмотреть файл

@@ -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),
};
}

Просмотреть файл

@@ -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,
}
}

Просмотреть файл

@@ -41,6 +41,7 @@ describe('ProductNoticesModal', () => {
connectionId: '',
lastConnectAt: 1599760193593,
lastDisconnectAt: 0,
serverHostname: '',
},
actions: {
getInProductNotices: jest.fn().mockResolvedValue({data: noticesData}),

Просмотреть файл

@@ -14,6 +14,9 @@
"about.licensed": "Licensed to:",
"about.notice": "Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> 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",

Просмотреть файл

@@ -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,

Просмотреть файл

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

Просмотреть файл

@@ -318,6 +318,7 @@ const state: GlobalState = {
lastConnectAt: 0,
lastDisconnectAt: 0,
connectionId: '',
serverHostname: '',
},
};
export default state;

Просмотреть файл

@@ -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,
}
}

Просмотреть файл

@@ -1903,6 +1903,7 @@
.about-modal__content {
display: block;
overflow-wrap: anywhere;
}
.about-modal__hash {

Просмотреть файл

@@ -57,6 +57,7 @@
display: flex;
flex-direction: row;
padding: 1em 0 3em;
overflow-wrap: anywhere;
}
.about-modal__copyright {

Просмотреть файл

@@ -68,6 +68,7 @@ export default class WebSocketClient {
private closeListeners = new Set<CloseListener>();
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,

Просмотреть файл

@@ -94,5 +94,6 @@ export type GlobalState = {
lastConnectAt: number;
lastDisconnectAt: number;
connectionId: string;
serverHostname: string;
};
};