Client remote control (#930)

---------

Co-authored-by: kellhogs <fabianpos99@gmail.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>
Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
Alex Christoffer Rasmussen
2024-06-15 12:26:41 +02:00
committed by GitHub
parent 70e8fbf327
commit a15eb0db4c
33 changed files with 868 additions and 169 deletions
@@ -3,10 +3,11 @@ import { ErrorBoundary } from '@sentry/react';
import { useKeyDown } from '../../common/hooks/useKeyDown';
import AboutPanel from './panel/about-panel/AboutPanel';
import ClientControlPanel from './panel/client-control-panel/ClientControlPanel';
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
import GeneralPanel from './panel/general-panel/GeneralPanel';
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
import LogPanel from './panel/log-panel/LogPanel';
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
import ProjectPanel from './panel/project-panel/ProjectPanel';
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
import SourcesPanel from './panel/sources-panel/SourcesPanel';
@@ -30,8 +31,9 @@ export default function AppSettings() {
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
{panel === 'sources' && <SourcesPanel />}
{panel === 'integrations' && <IntegrationsPanel location={location} />}
{panel === 'client_control' && <ClientControlPanel />}
{panel === 'about' && <AboutPanel />}
{panel === 'log' && <LogPanel />}
{panel === 'network' && <NetworkLogPanel location={location} />}
{panel === 'shutdown' && <ShutdownPanel />}
</PanelContent>
</ErrorBoundary>
@@ -73,12 +73,12 @@ $inner-padding: 1rem;
top: 0;
z-index: 3;
box-shadow: 0 1px $white-10;
background-color: $gray-1350;
}
th {
font-weight: 400;
color: $gray-400;
background-color: $gray-1350;
white-space: nowrap;
text-transform: capitalize;
}
@@ -0,0 +1,24 @@
.fullWidth {
width: 100%;
}
.actionButtons {
display: flex;
gap: $element-spacing;
}
.halfWidth {
width: 50%;
white-space: nowrap;
}
.pathList {
width: 100%;
}
.badgeList {
width: 100%;
* {
margin-right: $element-spacing;
}
}
@@ -0,0 +1,15 @@
import * as Panel from '../PanelUtils';
import ClientList from './ClientList';
export default function ClientControlPanel() {
return (
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Manage clients</Panel.SubHeader>
<Panel.Divider />
<ClientList />
</Panel.Card>
</Panel.Section>
);
}
@@ -0,0 +1,147 @@
import { useState } from 'react';
import { Badge, Button, useDisclosure } from '@chakra-ui/react';
import { RedirectClientModal } from '../../../../common/components/client-modal/RedirectClientModal';
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
import { setClientRemote } from '../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../common/stores/clientStore';
import * as Panel from '../PanelUtils';
import style from './ClientControlPanel.module.scss';
export default function ClientList() {
const id = useClientStore((store) => store.id);
const clients = useClientStore((store) => store.clients);
const { isOpen: isOpenRedirect, onOpen: onOpenRedirect, onClose: onCloseRedirect } = useDisclosure();
const { isOpen: isOpenRename, onOpen: onOpenRename, onClose: onCloseRename } = useDisclosure();
const { setIdentify } = setClientRemote;
const [targetId, setTargetId] = useState('');
const openRename = (targetId: string) => {
setTargetId(targetId);
onOpenRename();
};
const openRedirect = (targetId: string) => {
setTargetId(targetId);
onOpenRedirect();
};
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
const otherClients = Object.entries(clients).filter(([_, { type }]) => type !== 'ontime');
const targetClient = clients[targetId];
return (
<>
{isOpenRedirect && (
<RedirectClientModal
id={targetId}
name={targetClient?.name}
path={targetClient?.path}
isOpen={isOpenRedirect}
onClose={onCloseRedirect}
/>
)}
{isOpenRename && (
<RenameClientModal id={targetId} name={targetClient?.name} isOpen={isOpenRename} onClose={onCloseRename} />
)}
<Panel.Section>
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
<Panel.Table>
<thead>
<tr>
<td className={style.fullWidth}>Client Name (Connection ID)</td>
<td className={style.fullWidth}>Path</td>
<td />
</tr>
</thead>
<tbody>
{ontimeClients.map(([key, client]) => {
const { identify, name, path } = client;
const isCurrent = id === key;
return (
<tr key={key}>
<td className={style.badgeList}>
<Badge variant='outline' size='xs'>
{key}
</Badge>
{isCurrent && (
<Badge variant='outline' colorScheme='yellow' size='xs'>
self
</Badge>
)}
{name}
</td>
{isCurrent ? <td /> : <td className={style.pathList}>{path}</td>}
<td className={style.actionButtons}>
<Button
size='xs'
className={`${identify ? style.blink : ''}`}
isDisabled={isCurrent}
variant={identify ? 'ontime-filled' : 'ontime-subtle'}
data-testid={isCurrent ? '' : 'not-self-identify'}
onClick={() => {
setIdentify({ target: key, identify: !identify });
}}
>
Identify
</Button>
<Button
size='xs'
variant='ontime-subtle'
data-testid={isCurrent ? '' : 'not-self-rename'}
onClick={() => openRename(key)}
>
Rename
</Button>
<Button
size='xs'
variant='ontime-subtle'
isDisabled={isCurrent}
data-testid={isCurrent ? '' : 'not-self-redirect'}
onClick={() => openRedirect(key)}
>
Redirect
</Button>
</td>
</tr>
);
})}
</tbody>
</Panel.Table>
</Panel.Section>
<Panel.Divider />
<Panel.Section>
<Panel.Title>Other Clients ({otherClients.length})</Panel.Title>
<Panel.Table>
<thead>
<tr>
<td className={style.halfWidth}>Client Name (Connection ID)</td>
<td className={style.halfWidth}>Client type</td>
</tr>
</thead>
<tbody>
{otherClients.map(([key, client]) => {
const { name, type } = client;
return (
<tr key={key}>
<td className={style.badgeList}>
<Badge variant='outline' size='sx'>
{key}
</Badge>
{name}
</td>
<td>{type}</td>
</tr>
);
})}
</tbody>
</Panel.Table>
</Panel.Section>
</>
);
}
@@ -1,17 +0,0 @@
import * as Panel from '../PanelUtils';
import LogExport from './LogExport';
import InfoNif from './NetworkInterfaces';
export default function LogPanel() {
return (
<>
<Panel.Header>Log</Panel.Header>
<Panel.Section>
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
</Panel.Section>
<InfoNif />
<LogExport />
</>
);
}
@@ -6,7 +6,7 @@ import { handleLinks } from '../../../../common/utils/linkUtils';
import Log from '../../../log/Log';
import * as Panel from '../PanelUtils';
import style from './LogExport.module.scss';
import style from './NetworkLogExport.module.scss';
export default function LogExport() {
const extract = (event: MouseEvent) => {
@@ -0,0 +1,28 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { PanelBaseProps } from '../../settingsStore';
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
import * as Panel from '../PanelUtils';
import InfoNif from './NetworkInterfaces';
import LogExport from './NetworkLogExport';
export default function NetworkLogPanel({ location }: PanelBaseProps) {
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
return (
<>
<Panel.Header>Network</Panel.Header>
<Panel.Section>
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
</Panel.Section>
<InfoNif />
<div ref={logRef}>
<LogExport />
</div>
<div ref={clientsRef}>
<ClientControlPanel />
</div>
</>
);
}
@@ -50,7 +50,21 @@ export const settingPanels: Readonly<SettingsOption[]> = [
{ id: 'integrations__http', label: 'HTTP settings' },
],
},
{ id: 'log', label: 'Log', split: true },
{
id: 'network',
label: 'Network',
split: true,
secondary: [
{
id: 'network__log',
label: 'Application log',
},
{
id: 'network__clients',
label: 'Manage clients',
},
],
},
{
id: 'about',
label: 'About',