[web] Update socket diagnostics: 2 columns, copy to clipboard (#23581)

* Use 2 columns for socket diagnostics

* Fixup time so it's never negative

* Add "Copy to clipboard" feature

GitOrigin-RevId: ba7bcc672287c2697afadd904ade315b4a39f954
This commit is contained in:
Antoine Clausse 2025-02-14 13:33:43 +01:00 committed by Copybot
parent 5d42e51d3e
commit ed3442ada6

View file

@ -7,9 +7,10 @@ import {
DiagnosticItem,
ErrorAlert,
} from './diagnostic-component'
import { Container } from 'react-bootstrap-5'
import { Col, Container, Row } from 'react-bootstrap-5'
import MaterialIcon from '@/shared/components/material-icon'
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
type NetworkInformation = {
downlink: number
@ -19,33 +20,38 @@ type NetworkInformation = {
type: string
}
const NavigatorInfo = () => {
const navigatorInfo = (): string[] => {
if (!('connection' in navigator)) {
return <div>Network Information API not supported</div>
return ['Network Information API not supported']
}
const connection = navigator.connection as NetworkInformation
return (
<>
<div>Downlink: {connection.downlink} Mbps</div>
<div>Effective Type: {connection.effectiveType}</div>
<div>Round Trip Time: {connection.rtt} ms</div>
<div>Save Data: {connection.saveData ? 'Enabled' : 'Disabled'}</div>
<div>Platform: {navigator.platform}</div>
{/* @ts-ignore */}
<div>Device Memory: {navigator.deviceMemory}</div>
<div>Hardware Concurrency: {navigator.hardwareConcurrency}</div>
</>
)
return [
`Downlink: ${connection.downlink} Mbps`,
`Effective Type: ${connection.effectiveType}`,
`Round Trip Time: ${connection.rtt} ms`,
`Save Data: ${connection.saveData ? 'Enabled' : 'Disabled'}`,
`Platform: ${navigator.platform}`,
// @ts-ignore
`Device Memory: ${navigator.deviceMemory}`,
`Hardware Concurrency: ${navigator.hardwareConcurrency}`,
]
}
const useCurrentTime = () => {
const [time, setTime] = React.useState(new Date())
const [, setTime] = React.useState(new Date())
useEffect(() => {
const interval = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(interval)
}, [])
return time
}
type DiagnosticProps = {
icon: string
label: string
text: string[]
type?: 'success' | 'danger'
}
export const SocketDiagnostics = () => {
@ -58,7 +64,9 @@ export const SocketDiagnostics = () => {
autoping,
setAutoping,
} = useSocketManager()
const now = useCurrentTime()
useCurrentTime()
const now = new Date()
const getConnectionState = (): ConnectionStatus => {
if (socketState.connected) return 'connected'
@ -74,12 +82,108 @@ export const SocketDiagnostics = () => {
!!debugInfo.unansweredSince &&
now.getTime() - debugInfo.unansweredSince >= 3000
const diagnosticProps: DiagnosticProps[] = [
{
icon: 'network_ping',
label: 'Ping Count',
text: [
`${debugInfo.received} / ${debugInfo.sent}`,
lastReceivedS !== null ? `Last received ${lastReceivedS}s ago` : null,
].filter(Boolean) as string[],
type: isLate === null ? undefined : isLate ? 'danger' : 'success',
},
{
icon: 'schedule',
label: 'Latency',
text: [
debugInfo.latency
? `${debugInfo.latency} ms\nMax: ${debugInfo.maxLatency} ms`
: '-',
],
type: debugInfo.latency && debugInfo.latency < 450 ? 'success' : 'danger',
},
{
icon: 'difference',
label: 'Clock Delta',
text: [
debugInfo.clockDelta === null
? '-'
: `${Math.round(debugInfo.clockDelta / 1000)}s`,
],
type:
debugInfo.clockDelta !== null && Math.abs(debugInfo.clockDelta) < 1500
? 'success'
: 'danger',
},
{
icon: 'signal_cellular_alt',
label: 'Online',
text: [debugInfo.onLine?.toString() ?? '-'],
type: debugInfo.onLine ? 'success' : 'danger',
},
{
icon: 'schedule',
label: 'Current time',
text: [now.toUTCString()],
},
{
icon: 'hourglass',
label: 'Connection time',
text: [
debugInfo.client?.connectedAt
? `${new Date(debugInfo.client.connectedAt).toUTCString()} (${Math.round(
(Date.now() - debugInfo.client.connectedAt) / 1000
)}s)`
: '-',
],
},
{
icon: 'local_shipping',
label: 'Transport',
text: [socket?.socket.transport?.name ?? '-'],
},
{
icon: 'badge',
label: 'Client Public ID',
text: [debugInfo.client?.publicId ?? '-'],
},
{
icon: 'pin',
label: 'IP Address',
text: [debugInfo.client?.remoteIp ?? '-'],
},
{
icon: 'web',
label: 'User agent',
text: [debugInfo.client?.userAgent ?? '-'],
},
{
icon: 'directions_boat',
label: 'Navigator info',
text: navigatorInfo(),
},
]
const diagnosticItems = diagnosticProps.map(item => (
<DiagnosticItem
key={item.label}
icon={item.icon}
label={item.label}
value={item.text.map((t, i) => (
<div key={i}>{t}</div>
))}
type={item.type}
/>
))
const cutAtIndex = 7
const leftItems = diagnosticItems.slice(0, cutAtIndex)
const rightItems = diagnosticItems.slice(cutAtIndex)
return (
<Container>
<h1>Socket Diagnostics</h1>
<ConnectionBadge state={getConnectionState()} />
<div className="d-flex gap-2 mt-3">
<div className="d-flex flex-wrap gap-4 mt-3 align-items-center">
<ActionButton
label="Reconnect"
icon="refresh"
@ -91,132 +195,35 @@ export const SocketDiagnostics = () => {
onClick={disconnectSocket}
disabled={!socketState.connected}
/>
<OLFormCheckbox
label="Auto ping"
id="autoping"
checked={autoping}
onChange={e => setAutoping(e.target.checked)}
/>
</div>
{socketState.lastError && <ErrorAlert message={socketState.lastError} />}
<div className="card p-4 mt-3">
<h3 className="text-lg">
<MaterialIcon type="speed" /> Connection Stats
</h3>
<div className="space-y-2">
<OLFormCheckbox
label="Auto ping"
id="autoping"
checked={autoping}
onChange={e => setAutoping(e.target.checked)}
/>
<DiagnosticItem
icon="network_ping"
label="Ping Count"
value={
<>
{debugInfo.received} / {debugInfo.sent}
{lastReceivedS !== null && (
<>
<br />
Last received {lastReceivedS}s ago
</>
)}
</>
}
type={isLate === null ? undefined : isLate ? 'danger' : 'success'}
/>
<DiagnosticItem
icon="schedule"
label="Latency"
value={
debugInfo.latency ? (
<>
{debugInfo.latency} ms
<br />
Max: {debugInfo.maxLatency} ms
</>
) : (
'-'
)
}
type={
debugInfo.latency
? debugInfo.latency < 450
? 'success'
: 'danger'
: undefined
}
/>
<DiagnosticItem
icon="difference"
label="Clock Delta"
value={
debugInfo.clockDelta === null
? '-'
: `${Math.round(debugInfo.clockDelta / 1000)}s`
}
type={
debugInfo.clockDelta !== null
? Math.abs(debugInfo.clockDelta) < 1500
? 'success'
: 'danger'
: undefined
}
/>
<DiagnosticItem
icon="signal_cellular_alt"
label="Online"
value={debugInfo.onLine?.toString() ?? '-'}
type={debugInfo.onLine ? 'success' : 'danger'}
/>
<DiagnosticItem
icon="schedule"
label="Current time"
value={now.toUTCString()}
/>
<DiagnosticItem
icon="hourglass"
label="Connection time"
value={
debugInfo.client?.connectedAt ? (
<>
{new Date(debugInfo.client.connectedAt).toUTCString()} (
{Math.round(
(Date.now() - debugInfo.client.connectedAt) / 1000
)}
s)
</>
) : (
'-'
)
}
/>
<DiagnosticItem
icon="local_shipping"
label="Transport"
value={socket?.socket.transport?.name ?? '-'}
/>
<DiagnosticItem
icon="badge"
label="Client Public ID"
value={debugInfo.client?.publicId ?? '-'}
/>
<DiagnosticItem
icon="pin"
label="IP Address"
value={debugInfo.client?.remoteIp ?? '-'}
/>
<DiagnosticItem
icon="web"
label="User agent"
value={debugInfo.client?.userAgent ?? '-'}
/>
<DiagnosticItem
icon="directions_boat"
label="Navigator info"
value={<NavigatorInfo />}
/>
<div className="d-flex flex-wrap gap-4 row-gap-1 justify-content-between align-items-center">
<h3 className="text-lg">
<MaterialIcon type="speed" /> Connection Stats
</h3>
<div className="ms-auto">
<CopyToClipboard
content={diagnosticProps
.map(item => [`${item.label}:`, ...item.text].join('\n'))
.join('\n\n')}
tooltipId="copy-debug-info"
kind="text"
/>
</div>
</div>
<Row>
<Col md={6}>{leftItems}</Col>
<Col md={6}>{rightItems}</Col>
</Row>
</div>
</Container>
)