mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
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.
This commit is contained in:
parent
59b8532a89
commit
3224e0d3a2
16 changed files with 121 additions and 3 deletions
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue