Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<typeof DeviceSystemGlyph>['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,
Expand All @@ -55,8 +73,6 @@ function DeviceIcon({
switch (kind) {
case 'mobile':
return <Icon glyph={Smartphone} size={size} />;
case 'execution-host':
return <Icon glyph={Server} size={size} />;
case 'message-app': {
const chatApp = chatAppBrandFromIdentity(identity);
if (chatApp) {
Expand Down Expand Up @@ -385,6 +401,12 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
// the group names the same device the list does, rather than a
// second generic monitor.
<DeviceMark device={thisMachine} size="xs" />
) : 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.
<DeviceSystemGlyph device={HEADLESS_HOST_CLASS} size="xs" />
) : (
<DeviceIcon
identity={group.kind === 'message-app' ? attachedMessageAppIdentity : null}
Expand Down
Loading