bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/connection-status/modal/component.jsx

573 lines
17 KiB
React
Raw Normal View History

2020-04-10 01:01:46 +08:00
import React, { PureComponent } from 'react';
2020-05-14 21:35:25 +08:00
import { FormattedTime, defineMessages, injectIntl } from 'react-intl';
2020-04-10 01:01:46 +08:00
import PropTypes from 'prop-types';
2020-05-14 21:35:25 +08:00
import UserAvatar from '/imports/ui/components/user-avatar/component';
import CommonIcon from '/imports/ui/components/common/icon/component';
import TooltipContainer from '/imports/ui/components/common/tooltip/container';
import Icon from '/imports/ui/components/connection-status/icon/component';
import { getHelp } from '../service';
import Styled from './styles';
2023-12-06 20:12:25 +08:00
import ConnectionStatusHelper from '../status-helper/component';
import Auth from '/imports/ui/services/auth';
import connectionStatus from '../../../core/graphql/singletons/connectionStatus';
import logger from '/imports/startup/client/logger';
2020-04-10 01:01:46 +08:00
const MIN_TIMEOUT = 3000;
2020-04-10 01:01:46 +08:00
const intlMessages = defineMessages({
ariaTitle: {
id: 'app.connection-status.ariaTitle',
description: 'Connection status aria title',
},
title: {
id: 'app.connection-status.title',
description: 'Connection status title',
},
description: {
id: 'app.connection-status.description',
description: 'Connection status description',
},
2020-05-14 21:35:25 +08:00
empty: {
id: 'app.connection-status.empty',
description: 'Connection status empty',
},
more: {
id: 'app.connection-status.more',
description: 'More about connectivity issues',
},
audioLabel: {
id: 'app.settings.audioTab.label',
description: 'Audio label',
},
videoLabel: {
id: 'app.settings.videoTab.label',
description: 'Video label',
},
copy: {
id: 'app.connection-status.copy',
description: 'Copy network data',
},
copied: {
id: 'app.connection-status.copied',
description: 'Copied network data',
},
offline: {
id: 'app.connection-status.offline',
description: 'Offline user',
},
dataSaving: {
id: 'app.settings.dataSavingTab.description',
description: 'Description of data saving',
},
webcam: {
id: 'app.settings.dataSavingTab.webcam',
description: 'Webcam data saving switch',
},
screenshare: {
id: 'app.settings.dataSavingTab.screenShare',
description: 'Screenshare data saving switch',
},
on: {
id: 'app.switch.onLabel',
description: 'label for toggle switch on state',
},
off: {
id: 'app.switch.offLabel',
description: 'label for toggle switch off state',
},
no: {
id: 'app.connection-status.no',
description: 'No to is using turn',
},
yes: {
id: 'app.connection-status.yes',
description: 'Yes to is using turn',
},
usingTurn: {
id: 'app.connection-status.usingTurn',
description: 'User is using turn server',
},
jitter: {
id: 'app.connection-status.jitter',
description: 'Jitter buffer in ms',
},
lostPackets: {
id: 'app.connection-status.lostPackets',
description: 'Number of lost packets',
},
2021-10-19 04:52:59 +08:00
audioUploadRate: {
id: 'app.connection-status.audioUploadRate',
description: 'Label for audio current upload rate',
},
audioDownloadRate: {
id: 'app.connection-status.audioDownloadRate',
description: 'Label for audio current download rate',
},
videoUploadRate: {
id: 'app.connection-status.videoUploadRate',
description: 'Label for video current upload rate',
},
videoDownloadRate: {
id: 'app.connection-status.videoDownloadRate',
description: 'Label for video current download rate',
},
connectionStats: {
id: 'app.connection-status.connectionStats',
description: 'Label for Connection Stats tab',
2021-10-19 04:52:59 +08:00
},
myLogs: {
id: 'app.connection-status.myLogs',
description: 'Label for My Logs tab',
2021-10-19 04:52:59 +08:00
},
sessionLogs: {
id: 'app.connection-status.sessionLogs',
description: 'Label for Session Logs tab',
},
next: {
id: 'app.connection-status.next',
description: 'Label for the next page of the connection stats tab',
},
prev: {
id: 'app.connection-status.prev',
description: 'Label for the previous page of the connection stats tab',
},
clientNotResponding: {
id: 'app.connection-status.clientNotRespondingWarning',
description: 'Text for Client not responding warning',
},
noEvent: {
id: 'app.connection-status.connectionStatusNoEvent',
description: 'Text for inform on status without event ot time of occurrence',
},
2020-04-10 01:01:46 +08:00
});
const propTypes = {
setModalIsOpen: PropTypes.func.isRequired,
2020-04-10 01:01:46 +08:00
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
startMonitoringNetwork: PropTypes.func.isRequired,
stopMonitoringNetwork: PropTypes.func.isRequired,
networkData: PropTypes.shape({
ready: PropTypes.bool,
audio: PropTypes.shape({
audioCurrentUploadRate: PropTypes.number,
audioCurrentDownloadRate: PropTypes.number,
jitter: PropTypes.number,
packetsLost: PropTypes.number,
transportStats: PropTypes.shape({
isUsingTurn: PropTypes.bool,
}),
}),
video: PropTypes.shape({
videoCurrentUploadRate: PropTypes.number,
videoCurrentDownloadRate: PropTypes.number,
}),
}),
2020-04-10 01:01:46 +08:00
};
const isConnectionStatusEmpty = (connectionStatusParam) => {
// Check if it's defined
if (!connectionStatusParam) return true;
// Check if it's an array
if (!Array.isArray(connectionStatusParam)) return true;
// Check if is empty
if (connectionStatusParam.length === 0) return true;
return false;
};
2020-04-10 01:01:46 +08:00
class ConnectionStatusComponent extends PureComponent {
constructor(props) {
super(props);
const { intl } = this.props;
this.help = getHelp();
this.state = {
selectedTab: 0,
copyButtonText: intl.formatMessage(intlMessages.copy),
};
this.setButtonMessage = this.setButtonMessage.bind(this);
2021-10-19 04:52:59 +08:00
this.audioUploadLabel = intl.formatMessage(intlMessages.audioUploadRate);
this.audioDownloadLabel = intl.formatMessage(intlMessages.audioDownloadRate);
this.videoUploadLabel = intl.formatMessage(intlMessages.videoUploadRate);
this.videoDownloadLabel = intl.formatMessage(intlMessages.videoDownloadRate);
this.handleSelectTab = this.handleSelectTab.bind(this);
}
async componentDidMount() {
const { startMonitoringNetwork } = this.props;
try {
await startMonitoringNetwork();
} catch (error) {
logger.warn({
logCode: 'stats_monitor_network_error',
extraInfo: {
errorMessage: error?.message,
errorStack: error?.stack,
},
}, 'Failed to start monitoring network');
}
}
componentWillUnmount() {
const { stopMonitoringNetwork } = this.props;
clearTimeout(this.copyNetworkDataTimeout);
stopMonitoringNetwork();
}
handleSelectTab(tab) {
this.setState({
selectedTab: tab,
});
}
setButtonMessage(msg) {
this.setState({
copyButtonText: msg,
});
}
/**
* Copy network data to clipboard
* @return {Promise} A Promise that is resolved after data is copied.
*
*
*/
async copyNetworkData() {
const { intl, networkData } = this.props;
if (!networkData?.ready) return;
this.setButtonMessage(intl.formatMessage(intlMessages.copied));
const data = JSON.stringify(networkData, null, 2);
await navigator.clipboard.writeText(data);
this.copyNetworkDataTimeout = setTimeout(() => {
this.setButtonMessage(intl.formatMessage(intlMessages.copy));
}, MIN_TIMEOUT);
}
renderEmpty() {
const { intl } = this.props;
return (
<Styled.Item last data-test="connectionStatusItemEmpty">
<Styled.Left>
<Styled.FullName>
<Styled.Text>
{intl.formatMessage(intlMessages.empty)}
</Styled.Text>
</Styled.FullName>
</Styled.Left>
</Styled.Item>
);
}
2020-05-14 21:35:25 +08:00
renderConnections() {
const {
connectionData,
intl,
} = this.props;
2020-05-14 21:35:25 +08:00
2021-10-19 04:52:59 +08:00
const { selectedTab } = this.state;
if (isConnectionStatusEmpty(connectionData) && selectedTab !== 1) return this.renderEmpty();
2020-05-14 21:35:25 +08:00
let connections = connectionData;
2023-01-11 20:27:31 +08:00
if (selectedTab === 1) {
connections = connections.filter((curr) => curr.user.userId === Auth.userID);
if (isConnectionStatusEmpty(connections)) {
connections = connectionStatus.getUserNetworkHistory();
}
2021-10-19 04:52:59 +08:00
if (isConnectionStatusEmpty(connections)) return this.renderEmpty();
}
return connections.map((conn, index) => {
const dateTime = new Date(conn.lastUnstableStatusAt);
2020-05-14 21:35:25 +08:00
return (
<Styled.Item
key={`${conn.user.name}-${conn.user.userId}`}
2021-10-19 04:52:59 +08:00
last={(index + 1) === connections.length}
data-test="connectionStatusItemUser"
2020-05-14 21:35:25 +08:00
>
<Styled.Left>
<Styled.Avatar>
2020-05-14 21:35:25 +08:00
<UserAvatar
you={conn.user.userId === Auth.userID}
avatar={conn.user.avatar}
moderator={conn.user.isModerator}
color={conn.user.color}
2020-05-14 21:35:25 +08:00
>
{conn.user.name.toLowerCase().slice(0, 2)}
2020-05-14 21:35:25 +08:00
</UserAvatar>
</Styled.Avatar>
<Styled.Name>
<Styled.Text
offline={!conn.user.currentlyInMeeting}
data-test={!conn.user.currentlyInMeeting ? 'offlineUser' : null}
>
{conn.user.name}
{!conn.user.currentlyInMeeting ? ` (${intl.formatMessage(intlMessages.offline)})` : null}
</Styled.Text>
</Styled.Name>
{
!conn.clientNotResponding ? (
<Styled.Status
aria-label={`${intl.formatMessage(intlMessages.title)} ${conn.lastUnstableStatus}`}
>
<Styled.Icon>
<Icon level={conn.lastUnstableStatus} />
</Styled.Icon>
</Styled.Status>
) : null
}
{conn.clientNotResponding && conn.user.currentlyInMeeting
? (
<Styled.ClientNotRespondingText>
{intl.formatMessage(intlMessages.clientNotResponding)}
</Styled.ClientNotRespondingText>
) : null}
</Styled.Left>
<Styled.Right>
<Styled.Time>
{
conn.lastUnstableStatusAt
? (
<time dateTime={dateTime}>
<FormattedTime value={dateTime} />
</time>
)
: (
<TooltipContainer
placement="top"
title={intl.formatMessage(intlMessages.noEvent)}
>
<CommonIcon iconName="close" rotate={false} />
</TooltipContainer>
)
}
</Styled.Time>
</Styled.Right>
</Styled.Item>
2020-05-14 21:35:25 +08:00
);
});
}
/**
* Render network data , containing information about current upload and
* download rates
* @return {Object} The component to be renderized.
*/
renderNetworkData() {
const { enableNetworkStats } = window.meetingClientSettings.public.app;
if (!enableNetworkStats) {
return null;
}
const {
2021-10-19 04:52:59 +08:00
audioUploadLabel,
audioDownloadLabel,
videoUploadLabel,
videoDownloadLabel,
} = this;
2023-12-06 20:12:25 +08:00
const { intl, setModalIsOpen, connectionData } = this.props;
const { networkData } = this.props;
const {
audioCurrentUploadRate,
audioCurrentDownloadRate,
jitter,
packetsLost,
transportStats,
} = networkData.audio;
const {
videoCurrentUploadRate,
videoCurrentDownloadRate,
} = networkData.video;
let isUsingTurn = '--';
if (transportStats) {
switch (transportStats.isUsingTurn) {
case true:
isUsingTurn = intl.formatMessage(intlMessages.yes);
break;
case false:
isUsingTurn = intl.formatMessage(intlMessages.no);
break;
default:
break;
}
}
return (
<Styled.NetworkDataContainer
data-test="networkDataContainer"
tabIndex={0}
>
<Styled.HelperWrapper>
<Styled.Helper>
2023-12-06 20:12:25 +08:00
<ConnectionStatusHelper
connectionData={connectionData}
closeModal={() => setModalIsOpen(false)}
/>
</Styled.Helper>
</Styled.HelperWrapper>
<Styled.NetworkDataContent>
2021-10-19 04:52:59 +08:00
<Styled.DataColumn>
<Styled.NetworkData>
<div>{`${audioUploadLabel}`}</div>
2021-10-19 04:52:59 +08:00
<div>{`${audioCurrentUploadRate}k ↑`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${videoUploadLabel}`}</div>
<div data-test="videoUploadRateData">{`${videoCurrentUploadRate}k ↑`}</div>
2021-10-19 04:52:59 +08:00
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.jitter)}`}</div>
2021-10-19 04:52:59 +08:00
<div>{`${jitter} ms`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.usingTurn)}`}</div>
2021-10-19 04:52:59 +08:00
<div>{`${isUsingTurn}`}</div>
</Styled.NetworkData>
</Styled.DataColumn>
<Styled.DataColumn>
<Styled.NetworkData>
<div>{`${audioDownloadLabel}`}</div>
2021-10-19 04:52:59 +08:00
<div>{`${audioCurrentDownloadRate}k ↓`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${videoDownloadLabel}`}</div>
2021-10-19 04:52:59 +08:00
<div>{`${videoCurrentDownloadRate}k ↓`}</div>
</Styled.NetworkData>
<Styled.NetworkData>
<div>{`${intl.formatMessage(intlMessages.lostPackets)}`}</div>
2021-10-19 04:52:59 +08:00
<div>{`${packetsLost}`}</div>
</Styled.NetworkData>
<Styled.NetworkData invisible>
<div>Content Hidden</div>
2021-10-19 04:52:59 +08:00
<div>0</div>
</Styled.NetworkData>
</Styled.DataColumn>
</Styled.NetworkDataContent>
</Styled.NetworkDataContainer>
);
}
/**
* Renders the clipboard's copy button, for network stats.
* @return {Object} - The component to be renderized
*/
renderCopyDataButton() {
const { networkData } = this.props;
const { enableCopyNetworkStatsButton } = window.meetingClientSettings.public.app;
if (!enableCopyNetworkStatsButton) {
return null;
}
const { copyButtonText } = this.state;
return (
<Styled.CopyContainer aria-live="polite">
<Styled.Copy
disabled={!networkData?.ready}
role="button"
data-test="copyStats"
// eslint-disable-next-line react/jsx-no-bind
onClick={this.copyNetworkData.bind(this)}
// eslint-disable-next-line react/jsx-no-bind
onKeyPress={this.copyNetworkData.bind(this)}
tabIndex={0}
>
{copyButtonText}
</Styled.Copy>
</Styled.CopyContainer>
);
}
2020-04-10 01:01:46 +08:00
render() {
const {
setModalIsOpen,
2020-04-10 01:01:46 +08:00
intl,
isModalOpen,
amIModerator,
2020-04-10 01:01:46 +08:00
} = this.props;
const { selectedTab } = this.state;
2020-04-10 01:01:46 +08:00
return (
<Styled.ConnectionStatusModal
priority="low"
onRequestClose={() => setModalIsOpen(false)}
setIsOpen={setModalIsOpen}
2020-04-10 01:01:46 +08:00
hideBorder
isOpen={isModalOpen}
2020-04-10 01:01:46 +08:00
contentLabel={intl.formatMessage(intlMessages.ariaTitle)}
2022-01-20 21:03:18 +08:00
data-test="connectionStatusModal"
2020-04-10 01:01:46 +08:00
>
<Styled.Container>
<Styled.Header>
<Styled.Title>
2020-04-10 01:01:46 +08:00
{intl.formatMessage(intlMessages.title)}
</Styled.Title>
</Styled.Header>
<Styled.ConnectionTabs
onSelect={this.handleSelectTab}
selectedIndex={selectedTab}
>
<Styled.ConnectionTabList>
<Styled.ConnectionTabSelector selectedClassName="is-selected">
2022-10-28 23:58:32 +08:00
<span id="connection-status-tab">{intl.formatMessage(intlMessages.title)}</span>
</Styled.ConnectionTabSelector>
<Styled.ConnectionTabSelector selectedClassName="is-selected">
2022-10-28 23:58:32 +08:00
<span id="my-logs-tab">{intl.formatMessage(intlMessages.myLogs)}</span>
</Styled.ConnectionTabSelector>
{amIModerator
2022-10-29 00:07:06 +08:00
&& (
<Styled.ConnectionTabSelector selectedClassName="is-selected">
<span id="session-logs-tab">{intl.formatMessage(intlMessages.sessionLogs)}</span>
</Styled.ConnectionTabSelector>
)}
</Styled.ConnectionTabList>
<Styled.ConnectionTabPanel selectedClassName="is-selected">
<div>
{this.renderNetworkData()}
{this.renderCopyDataButton()}
</div>
</Styled.ConnectionTabPanel>
<Styled.ConnectionTabPanel selectedClassName="is-selected">
2023-07-31 22:24:25 +08:00
<ul>{this.renderConnections()}</ul>
</Styled.ConnectionTabPanel>
{amIModerator
2022-10-29 00:07:06 +08:00
&& (
<Styled.ConnectionTabPanel selectedClassName="is-selected">
2023-07-31 22:24:25 +08:00
<ul>{this.renderConnections()}</ul>
2022-10-29 00:07:06 +08:00
</Styled.ConnectionTabPanel>
)}
</Styled.ConnectionTabs>
</Styled.Container>
</Styled.ConnectionStatusModal>
2020-04-10 01:01:46 +08:00
);
}
}
ConnectionStatusComponent.propTypes = propTypes;
export default injectIntl(ConnectionStatusComponent);