feat(network): help users orient themselves in the network panel

- add a network interfaces card to the network panel. The addresses
  users need to reach Ontime from other devices were only available
  in the sharing panel
- show the port along with the address, the bare IP is not enough to
  reach the server
- explain what each section does and how the client actions behave
- the client path was selectable text but could not be opened or
  copied. It now uses a copy tag with the full client URL, which also
  makes the client origin visible in setups with several interfaces

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrhcEgY8mqaxZi8veMPrrx
This commit is contained in:
Claude
2026-08-01 13:18:52 +00:00
parent c840add9c5
commit b5e1bccc70
5 changed files with 79 additions and 4 deletions
@@ -19,10 +19,12 @@ export default function InfoNif() {
// interfaces outside localhost wont have access
if (nif.name === 'localhost' && !isLocalhost) return null;
const address = linkToOtherHost(nif.address);
// we show the address as it would be typed in a browser, including the port
const label = new URL(address).host;
return (
<CopyTag key={nif.name} copyValue={address} onClick={() => handleClick(address)}>
{`${nif.name} - ${nif.address}`} <IoArrowUp className={style.goIcon} />
<CopyTag key={`${nif.name}-${nif.address}`} copyValue={address} onClick={() => handleClick(address)}>
{`${nif.name} - ${label}`} <IoArrowUp className={style.goIcon} />
</CopyTag>
);
})}
@@ -5,16 +5,18 @@ import Tag from '../../../../common/components/tag/Tag';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { usePing } from '../../../../common/hooks/useSocket';
import { sendSocket } from '../../../../common/utils/socket';
import { isDocker } from '../../../../externals';
import { isDocker, isOntimeCloud } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import ClientControlPanel from './client-control/ClientControlPanel';
import InfoNif from './NetworkInterfaces';
import LogExport from './NetworkLogExport';
/** ping values above this are flagged to the user (ms) */
const slowPingThreshold = 100;
export default function NetworkLogPanel({ location }: PanelBaseProps) {
const interfacesRef = useScrollIntoView<HTMLDivElement>('interfaces', location);
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
@@ -22,6 +24,21 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
<>
<Panel.Header>Network</Panel.Header>
{isDocker && <OntimeCloudStats />}
{!isOntimeCloud && (
<div ref={interfacesRef}>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Network interfaces</Panel.SubHeader>
<Panel.Divider />
<Panel.Paragraph>
Addresses which can be used to reach Ontime from other devices in the network. <br />
Click an address to open it in a new window, or use the icon to copy it to the clipboard.
</Panel.Paragraph>
<InfoNif />
</Panel.Card>
</Panel.Section>
</div>
)}
<div ref={logRef}>
<LogExport />
</div>
@@ -7,3 +7,12 @@
cursor: text;
user-select: text;
}
.muted {
color: $label-gray;
}
// align section notes with the table contents
.note {
padding: 0 2rem;
}
@@ -5,13 +5,42 @@ import { useState } from 'react';
import Button from '../../../../../common/components/buttons/Button';
import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal';
import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal';
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
import Tag from '../../../../../common/components/tag/Tag';
import Tooltip from '../../../../../common/components/tooltip/Tooltip';
import { setClientRemote } from '../../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../../common/stores/clientStore';
import { openLink } from '../../../../../common/utils/linkUtils';
import * as Panel from '../../../panel-utils/PanelUtils';
import style from './ClientControlPanel.module.scss';
/**
* Shows the path a client is showing
* The full URL is available for copying or opening in a new window
*/
function ClientPath({ origin, path }: { origin: string; path: string }) {
// clients report their path on connection, we may not have it yet
if (!path) {
return <span className={style.muted}>unknown</span>;
}
// origin is empty for clients which have not reported their location
if (!origin) {
return <span className={style.copiable}>{path}</span>;
}
const fullUrl = `${origin}${path}`;
return (
<Tooltip text={fullUrl} render={<span />}>
<CopyTag copyValue={fullUrl} size='small' onClick={() => openLink(fullUrl)}>
{path}
</CopyTag>
</Tooltip>
);
}
export default function ClientList() {
const id = useClientStore((store) => store.id);
const clients = useClientStore((store) => store.clients);
@@ -69,6 +98,12 @@ export default function ClientList() {
)}
<Panel.Section>
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
<div className={style.note}>
<Panel.Description>
Ontime views connected to this server. Identify shows a marker in the client screen until you turn it off,
redirect sends the client to a different view.
</Panel.Description>
</div>
<Panel.Table>
<thead>
<tr>
@@ -88,7 +123,9 @@ export default function ClientList() {
{isCurrent && <Tag>SELF</Tag>}
{name}
</Panel.InlineElements>
<td className={style.copiable}>{path}</td>
<td>
<ClientPath origin={client.origin} path={path} />
</td>
<Panel.InlineElements relation='inner' as='td'>
<Button
size='small'
@@ -127,6 +164,12 @@ export default function ClientList() {
<Panel.Divider />
<Panel.Section>
<Panel.Title>Other Clients ({otherClients.length})</Panel.Title>
<div className={style.note}>
<Panel.Description>
Connections which are not Ontime views, such as integrations and custom clients. These cannot be controlled
from Ontime.
</Panel.Description>
</div>
<Panel.Table>
<thead>
<tr>
@@ -67,6 +67,10 @@ const staticOptions = [
id: 'network',
label: 'Network',
secondary: [
{
id: 'network__interfaces',
label: 'Network interfaces',
},
{
id: 'network__log',
label: 'Event log',