diff --git a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.test.tsx b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.test.tsx index 887d0c101d..b56c0a6b3f 100644 --- a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.test.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.test.tsx @@ -391,6 +391,35 @@ describe('device status card', () => { expect(attachedDesktop()?.querySelector('svg')).not.toBeNull(); }); + it('draws a group of headless hosts with the mark the list draws for them', () => { + const footer = () => element('nav-footer-device-status'); + const headlessGroup = () => footer() + .querySelector('[data-openbitfun-device-kind="execution-host"]'); + + // A dispatch job running on another host is what puts that group in the + // footer, and the group names a class rather than one device: it has no row + // to read a system from. + state.overview = overview({ + localDeviceName: 'Workstation', + localDeviceOs: 'macOS', + dispatchJobs: [{ + id: 'job-1', + state: 'running', + target: { kind: 'device', id: 'peer-2', name: 'Build host' }, + }], + }); + render(); + + const drawnPath = (scope: Element | null | undefined) => ( + scope?.querySelector('svg[data-system="server"] path')?.getAttribute('d') + ); + const listRow = element('nav-device-status-connected-devices') + .querySelector('[data-openbitfun-device-kind="execution-host"]'); + // One drawing, whatever slot it lands in: the group is not a second glyph. + expect(drawnPath(headlessGroup())).not.toBeNull(); + expect(drawnPath(headlessGroup())).toBe(drawnPath(listRow)); + }); + it('marks a device name with the system that device runs', () => { state.overview = overview({ localDeviceName: 'Workstation', diff --git a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx index e64d0b3ede..110f385954 100644 --- a/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/DeviceStatusControl.tsx @@ -1,7 +1,7 @@ import { useDeviceDirectory, resolveDeviceName, isDeviceControllable, deviceClientVersion } from '@/infrastructure/account/deviceDirectory'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { subscribeOverlayInteraction, createOverlayPortal, OverflowText, Button, Card, CardBody, CardFooter, CardHeader, Icon, IconButton, ScrollArea, type IconSize } from '@openbitfun/ui'; -import { ChevronLeft, ChevronRight, MessageCircle, Monitor, Server, Smartphone, Undo2 } from 'lucide-react'; +import { ChevronLeft, ChevronRight, MessageCircle, Monitor, Smartphone, Undo2 } from 'lucide-react'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost'; import { useAnchoredPopoverPosition } from '@/shared/utils/useAnchoredPopoverPosition'; @@ -43,6 +43,24 @@ function chatAppBrandFromIdentity(identity: string | null | undefined): ChatAppB return null; } +/** + * The class of headless hosts the footer names as one group. A group is not a + * device, so it carries the facts its mark is resolved from and nothing else: + * the kind already decides that mark, and the name, system and host kind a row + * would answer with do not exist at this level. + */ +const HEADLESS_HOST_CLASS: React.ComponentProps['device'] = { + kind: 'execution-host', + name: '', + os: null, + hostKind: null, +}; + +/** + * The kinds that are not a system: a phone, a chat app, and the neutral monitor + * for a kind this build cannot place. A desktop and a headless host are the + * machines the device marks draw, so they never reach this switch. + */ function DeviceIcon({ identity, kind, @@ -55,8 +73,6 @@ function DeviceIcon({ switch (kind) { case 'mobile': return ; - case 'execution-host': - return ; case 'message-app': { const chatApp = chatAppBrandFromIdentity(identity); if (chatApp) { @@ -385,6 +401,12 @@ const DeviceStatusControl: React.FC = ({ // the group names the same device the list does, rather than a // second generic monitor. + ) : group.kind === 'execution-host' ? ( + // A group of headless hosts is a class, not one device, so there + // is no row to read a system from: it draws the mark the list + // draws for each of them, the way the desktop group does, rather + // than a second server glyph of its own. + ) : (