mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): keep machine names visible and health tooltips touchable (#1049)
* fix(web): improve machine health sidebar UX Keep machine names visible, align health metrics, and make nested health tooltips usable with touch and keyboard input. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): preserve tooltip focus reveal groups Keep the unnamed group used by existing focus reveal classes while retaining named hover groups for nested machine health tooltips. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): add exec* timestamps to ToolCard test fixture Unblocks typecheck after #1036 made execStartedAt/execCompletedAt required on ChatToolCall; fixture was missing both fields. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align machine health status with meters Right-align the capacity status with the utilization meter edge to balance the tooltip header without shortening the bars. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): restore machine health disclosure semantics Expose the machine group's expanded state on its toggle and describe the health trigger with the tooltip body for assistive technologies. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
HAPI
Debian
parent
f1b5ed5e5d
commit
77f94ef738
@@ -1,4 +1,4 @@
|
||||
import { useId, type ReactNode } from 'react'
|
||||
import { useId, type ReactNode, type Ref } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** Tailwind classes that reveal the bubble when a named parent row has :focus-visible. */
|
||||
@@ -39,10 +39,14 @@ export function HoverTooltip(props: {
|
||||
revealOnParentFocusClass?: string
|
||||
/** Optional classes for the tooltip panel (e.g. wider popover). */
|
||||
tooltipClassName?: string
|
||||
open?: boolean
|
||||
containerRef?: Ref<HTMLSpanElement>
|
||||
hoverGroup?: 'default' | 'help'
|
||||
}) {
|
||||
const side = props.side ?? 'bottom'
|
||||
const align = props.align ?? 'center'
|
||||
const spansRow = align === 'row'
|
||||
const isHelpGroup = props.hoverGroup === 'help'
|
||||
|
||||
const alignClasses = spansRow
|
||||
? 'left-1 right-1 w-auto'
|
||||
@@ -51,7 +55,15 @@ export function HoverTooltip(props: {
|
||||
: 'left-1/2 -translate-x-1/2'
|
||||
|
||||
return (
|
||||
<span className={cn(spansRow ? 'static' : 'relative', 'inline-flex group', props.className)}>
|
||||
<span
|
||||
ref={props.containerRef}
|
||||
className={cn(
|
||||
spansRow ? 'static' : 'relative',
|
||||
'inline-flex group',
|
||||
isHelpGroup ? 'group/help-tooltip' : 'group/hover-tooltip',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex">
|
||||
{props.target}
|
||||
</span>
|
||||
@@ -66,8 +78,11 @@ export function HoverTooltip(props: {
|
||||
side === 'top' ? 'bottom-full mb-1' : 'top-full mt-1',
|
||||
alignClasses,
|
||||
'opacity-0 invisible',
|
||||
'group-hover:opacity-100 group-hover:visible',
|
||||
isHelpGroup
|
||||
? 'group-hover/help-tooltip:opacity-100 group-hover/help-tooltip:visible'
|
||||
: 'group-hover/hover-tooltip:opacity-100 group-hover/hover-tooltip:visible',
|
||||
props.revealOnParentFocusClass,
|
||||
props.open && 'opacity-100 visible',
|
||||
props.tooltipClassName,
|
||||
'transition-opacity duration-100'
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { Machine } from '@/types/api'
|
||||
import { MachineGroupHeader } from './MachineGroupHeader'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
@@ -23,14 +23,15 @@ const machine: Machine = {
|
||||
}
|
||||
|
||||
describe('MachineGroupHeader', () => {
|
||||
it('renders a single-row machine tile with os label and compact health', () => {
|
||||
it('renders a single-row machine tile with machine name and compact health', () => {
|
||||
const onToggle = vi.fn()
|
||||
render(
|
||||
<I18nProvider>
|
||||
<MachineGroupHeader
|
||||
label="Teemo"
|
||||
sessionCount={4}
|
||||
collapsed={false}
|
||||
onToggle={() => {}}
|
||||
onToggle={onToggle}
|
||||
machine={machine}
|
||||
healthPresentation={{
|
||||
metrics: [
|
||||
@@ -44,13 +45,21 @@ describe('MachineGroupHeader', () => {
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Teemo/i })).toBeTruthy()
|
||||
expect(screen.getByText('Windows')).toBeTruthy()
|
||||
const machineButton = screen.getByRole('button', { name: /Teemo/i })
|
||||
expect(machineButton.getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.click(machineButton)
|
||||
expect(onToggle).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByText('Windows')).toBeNull()
|
||||
expect(screen.getByText('(4)')).toBeTruthy()
|
||||
expect(screen.getByLabelText(/CPU 12 percent; RAM 88 percent/i)).toBeTruthy()
|
||||
|
||||
const healthButton = screen.getByRole('button', { name: /CPU 12 percent; RAM 88 percent/i })
|
||||
fireEvent.click(healthButton)
|
||||
expect(healthButton.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(onToggle).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('shows compact uptime in the machine meta row', () => {
|
||||
it('keeps uptime in the health tooltip instead of replacing the machine name', () => {
|
||||
render(
|
||||
<I18nProvider>
|
||||
<MachineGroupHeader
|
||||
@@ -75,6 +84,7 @@ describe('MachineGroupHeader', () => {
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByTitle('Linux · up 1h 54m')).toBeTruthy()
|
||||
expect(screen.getByTitle('proxmox')).toBeTruthy()
|
||||
expect(screen.getByText('1h 54m')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { useId } from 'react'
|
||||
import type { Machine } from '@/types/api'
|
||||
import { MACHINE_ROW_TOOLTIP_FOCUS_CLASS } from '@/components/HoverTooltip'
|
||||
import { MachineHealthIndicator } from '@/components/MachineHealthIndicator'
|
||||
import {
|
||||
getMachineHost,
|
||||
getMachinePlatform,
|
||||
resolveMachineOsLabel,
|
||||
shouldShowMachineHostSubtitle,
|
||||
type MachineHealthPresentation,
|
||||
} from '@/lib/machineHealth'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
function MachineIcon(props: { className?: string }) {
|
||||
return (
|
||||
@@ -56,16 +49,6 @@ function ChevronIcon(props: { className?: string; collapsed?: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
function formatOsLabel(
|
||||
osLabel: ReturnType<typeof resolveMachineOsLabel>,
|
||||
t: (key: string) => string
|
||||
): string {
|
||||
if (osLabel.kind === 'raw') {
|
||||
return osLabel.value
|
||||
}
|
||||
return t(osLabel.key)
|
||||
}
|
||||
|
||||
export function MachineGroupHeader(props: {
|
||||
label: string
|
||||
sessionCount: number
|
||||
@@ -74,60 +57,39 @@ export function MachineGroupHeader(props: {
|
||||
machine?: Machine
|
||||
healthPresentation: MachineHealthPresentation | null
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const healthTooltipId = useId()
|
||||
const platform = getMachinePlatform(props.machine)
|
||||
const host = getMachineHost(props.machine)
|
||||
const osLabel = resolveMachineOsLabel(platform)
|
||||
const osText = formatOsLabel(osLabel, t)
|
||||
const showHost = shouldShowMachineHostSubtitle(props.label, host)
|
||||
const uptimeText = props.healthPresentation?.uptimeDetail
|
||||
const metaParts = [osText]
|
||||
if (showHost && host) {
|
||||
metaParts.push(host)
|
||||
}
|
||||
if (uptimeText) {
|
||||
metaParts.push(t('machine.health.uptimeCompact', { value: uptimeText }))
|
||||
}
|
||||
const machineMeta = metaParts.join(' · ')
|
||||
const hasHealth = props.healthPresentation && props.healthPresentation.metrics.length > 0
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onToggle}
|
||||
aria-describedby={hasHealth ? healthTooltipId : undefined}
|
||||
<div
|
||||
className={cn(
|
||||
'group/machine-row relative flex w-full min-w-0 items-center gap-2 px-1 py-1.5 text-left rounded-lg select-none',
|
||||
'border border-[var(--app-border)] bg-[var(--app-subtle-bg)]/70',
|
||||
'transition-colors hover:bg-[var(--app-subtle-bg)]',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]'
|
||||
'transition-colors hover:bg-[var(--app-subtle-bg)]'
|
||||
)}
|
||||
>
|
||||
<ChevronIcon className="h-4 w-4 shrink-0 text-[var(--app-hint)]" collapsed={props.collapsed} />
|
||||
<MachineIcon className="h-4 w-4 shrink-0 text-[var(--app-link)]/80" />
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-[var(--app-fg)]">
|
||||
{props.label}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 max-w-[8rem] shrink truncate text-[11px] text-[var(--app-hint)]"
|
||||
title={machineMeta}
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onToggle}
|
||||
aria-expanded={!props.collapsed}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 rounded-md text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
|
||||
>
|
||||
{machineMeta}
|
||||
</span>
|
||||
<ChevronIcon className="h-4 w-4 shrink-0 text-[var(--app-hint)]" collapsed={props.collapsed} />
|
||||
<MachineIcon className="h-4 w-4 shrink-0 text-[var(--app-link)]/80" />
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-[var(--app-fg)]" title={props.label}>
|
||||
{props.label}
|
||||
</span>
|
||||
</button>
|
||||
{hasHealth ? (
|
||||
<MachineHealthIndicator
|
||||
presentation={props.healthPresentation!}
|
||||
layout="inline"
|
||||
compact
|
||||
className="shrink-0"
|
||||
tooltipId={healthTooltipId}
|
||||
revealOnParentFocusClass={MACHINE_ROW_TOOLTIP_FOCUS_CLASS}
|
||||
/>
|
||||
) : null}
|
||||
<span className="ml-auto shrink-0 text-[11px] tabular-nums text-[var(--app-hint)]">
|
||||
({props.sessionCount})
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { MachineHealthIndicator } from './MachineHealthIndicator'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
@@ -22,10 +22,30 @@ describe('MachineHealthIndicator', () => {
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByText('CPU')).toBeTruthy()
|
||||
expect(screen.getByText('RAM')).toBeTruthy()
|
||||
expect(screen.getByText('CPU across all 6 cores')).toBeTruthy()
|
||||
expect(screen.getByLabelText(/CPU 72/i)).toBeTruthy()
|
||||
expect(screen.getAllByText('CPU')).toHaveLength(2)
|
||||
expect(screen.getAllByText('RAM')).toHaveLength(2)
|
||||
expect(screen.getByLabelText('Updated every ~20s from the runner on this machine')).toBeTruthy()
|
||||
const healthButton = screen.getByRole('button', { name: /CPU 72/i })
|
||||
|
||||
fireEvent.click(healthButton)
|
||||
expect(healthButton.getAttribute('aria-expanded')).toBe('true')
|
||||
|
||||
fireEvent.click(healthButton)
|
||||
expect(healthButton.getAttribute('aria-expanded')).toBe('false')
|
||||
|
||||
fireEvent.click(healthButton)
|
||||
fireEvent.pointerDown(document.body)
|
||||
expect(healthButton.getAttribute('aria-expanded')).toBe('false')
|
||||
|
||||
fireEvent.click(healthButton)
|
||||
fireEvent.keyDown(healthButton, { key: 'Escape' })
|
||||
expect(healthButton.getAttribute('aria-expanded')).toBe('false')
|
||||
|
||||
const helpButton = screen.getByRole('button', { name: 'Updated every ~20s from the runner on this machine' })
|
||||
fireEvent.click(helpButton)
|
||||
expect(helpButton.getAttribute('aria-expanded')).toBe('true')
|
||||
fireEvent.click(helpButton)
|
||||
expect(helpButton.getAttribute('aria-expanded')).toBe('false')
|
||||
})
|
||||
|
||||
it('renders inline percent labels', () => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useId } from 'react'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { HoverTooltip } from '@/components/HoverTooltip'
|
||||
import {
|
||||
MACHINE_HEALTH_BAR_FILL_CLASS,
|
||||
MACHINE_HEALTH_CHIP_CLASS,
|
||||
getCpuMetricTooltipLabel,
|
||||
type MachineHealthMetricPresentation,
|
||||
type MachineHealthPresentation
|
||||
} from '@/lib/machineHealth'
|
||||
@@ -51,7 +50,7 @@ function TooltipMetricStat(props: {
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<span className="inline-flex min-w-[7.5rem] items-center gap-2 whitespace-nowrap">
|
||||
<span className="grid grid-cols-[3.75rem_2.25rem_minmax(3.5rem,1fr)] items-center gap-x-1 whitespace-nowrap">
|
||||
<span className="text-[var(--app-hint)]">{props.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -62,7 +61,7 @@ function TooltipMetricStat(props: {
|
||||
{props.metric.percent}%
|
||||
</span>
|
||||
<span
|
||||
className="relative h-1.5 w-14 overflow-hidden rounded-full bg-[var(--app-border)]/80"
|
||||
className="relative h-1.5 w-full overflow-hidden rounded-full bg-[var(--app-border)]/80"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
@@ -74,6 +73,64 @@ function TooltipMetricStat(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function MachineHealthHint() {
|
||||
const { t } = useTranslation()
|
||||
const tooltipId = useId()
|
||||
const [clickOpen, setClickOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLSpanElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!clickOpen) return
|
||||
|
||||
const closeOnOutsidePointer = (event: PointerEvent) => {
|
||||
if (!containerRef.current?.contains(event.target as Node)) {
|
||||
setClickOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('pointerdown', closeOnOutsidePointer)
|
||||
return () => document.removeEventListener('pointerdown', closeOnOutsidePointer)
|
||||
}, [clickOpen])
|
||||
|
||||
const target = (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-3.5 w-3.5 items-center justify-center rounded-full border border-current text-[9px] font-semibold leading-none text-[var(--app-hint)]"
|
||||
aria-label={t('machine.health.tooltip.hint')}
|
||||
aria-expanded={clickOpen}
|
||||
aria-controls={tooltipId}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
if (clickOpen) {
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
setClickOpen((open) => !open)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setClickOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<HoverTooltip
|
||||
id={tooltipId}
|
||||
target={target}
|
||||
side="bottom"
|
||||
align="start"
|
||||
open={clickOpen}
|
||||
containerRef={containerRef}
|
||||
hoverGroup="help"
|
||||
tooltipClassName="pointer-events-auto w-56"
|
||||
>
|
||||
{t('machine.health.tooltip.hint')}
|
||||
</HoverTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function MachineHealthTooltipBody(props: {
|
||||
presentation: MachineHealthPresentation
|
||||
}) {
|
||||
@@ -83,22 +140,23 @@ function MachineHealthTooltipBody(props: {
|
||||
|
||||
return (
|
||||
<span className="block space-y-1.5">
|
||||
<span className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-0.5">
|
||||
<span className="font-medium">{t('machine.health.tooltip.title')}</span>
|
||||
<span className="text-[var(--app-fg)]">{t(statusKey)}</span>
|
||||
<span className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
||||
<span className="inline-flex items-center gap-1 font-medium">
|
||||
{t('machine.health.tooltip.title')}
|
||||
<MachineHealthHint />
|
||||
</span>
|
||||
<span className="text-right text-[var(--app-fg)]">{t(statusKey)}</span>
|
||||
</span>
|
||||
<span className="flex flex-wrap items-center gap-x-5 gap-y-1">
|
||||
<span className="block space-y-1">
|
||||
{presentation.metrics.map((metric) => (
|
||||
<TooltipMetricStat
|
||||
key={metric.id}
|
||||
metric={metric}
|
||||
label={metric.id === 'cpu'
|
||||
? getCpuMetricTooltipLabel(presentation.cpuCount, t)
|
||||
: t(`machine.health.metric.${metric.id}`, { n: metric.percent })}
|
||||
label={t(`machine.health.metric.${metric.id}`, { n: metric.percent })}
|
||||
/>
|
||||
))}
|
||||
{presentation.loadDetail ? (
|
||||
<span className="inline-flex min-w-[7.5rem] items-center gap-2 whitespace-nowrap text-[var(--app-hint)]">
|
||||
<span className="grid grid-cols-[3.75rem_1fr] items-center gap-x-1 whitespace-nowrap text-[var(--app-hint)]">
|
||||
<span>{t('machine.health.tooltip.loadShort')}</span>
|
||||
<span className="font-semibold tabular-nums text-[var(--app-fg)]">
|
||||
{presentation.loadDetail}
|
||||
@@ -106,7 +164,7 @@ function MachineHealthTooltipBody(props: {
|
||||
</span>
|
||||
) : null}
|
||||
{presentation.uptimeDetail ? (
|
||||
<span className="inline-flex min-w-[7.5rem] items-center gap-2 whitespace-nowrap text-[var(--app-hint)]">
|
||||
<span className="grid grid-cols-[3.75rem_1fr] items-center gap-x-1 whitespace-nowrap text-[var(--app-hint)]">
|
||||
<span>{t('machine.health.tooltip.uptimeShort')}</span>
|
||||
<span className="font-semibold tabular-nums text-[var(--app-fg)]">
|
||||
{presentation.uptimeDetail}
|
||||
@@ -114,9 +172,6 @@ function MachineHealthTooltipBody(props: {
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="block text-[11px] leading-snug text-[var(--app-hint)]">
|
||||
{t('machine.health.tooltip.hint')}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -133,6 +188,20 @@ export function MachineHealthIndicator(props: {
|
||||
const generatedTooltipId = useId()
|
||||
const tooltipId = props.tooltipId ?? generatedTooltipId
|
||||
const { presentation, layout = 'stack', compact = false } = props
|
||||
const [clickOpen, setClickOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLSpanElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!clickOpen) return
|
||||
|
||||
const closeOnOutsidePointer = (event: PointerEvent) => {
|
||||
if (!containerRef.current?.contains(event.target as Node)) {
|
||||
setClickOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('pointerdown', closeOnOutsidePointer)
|
||||
return () => document.removeEventListener('pointerdown', closeOnOutsidePointer)
|
||||
}, [clickOpen])
|
||||
|
||||
const ariaLabel = presentation.metrics.length > 0
|
||||
? presentation.metrics
|
||||
@@ -141,7 +210,8 @@ export function MachineHealthIndicator(props: {
|
||||
: t('machine.health.aria.unknown')
|
||||
|
||||
const chip = (
|
||||
<span
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex rounded-md border',
|
||||
compact ? 'flex-row flex-nowrap items-center gap-x-1.5 px-1 py-0.5' : layout === 'inline'
|
||||
@@ -151,6 +221,21 @@ export function MachineHealthIndicator(props: {
|
||||
props.className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
aria-describedby={tooltipId}
|
||||
aria-expanded={clickOpen}
|
||||
aria-controls={tooltipId}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
if (clickOpen) {
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
setClickOpen((open) => !open)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setClickOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{presentation.metrics.map((metric) => (
|
||||
<HealthMeterBar
|
||||
@@ -162,7 +247,7 @@ export function MachineHealthIndicator(props: {
|
||||
compact={compact}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -172,8 +257,10 @@ export function MachineHealthIndicator(props: {
|
||||
side="bottom"
|
||||
align="end"
|
||||
className="shrink-0"
|
||||
tooltipClassName="px-3 py-2 min-w-[16rem]"
|
||||
tooltipClassName="pointer-events-auto before:absolute before:inset-x-0 before:-top-1 before:h-1 before:content-[''] px-3 py-2 min-w-[16rem]"
|
||||
revealOnParentFocusClass={props.revealOnParentFocusClass}
|
||||
open={clickOpen}
|
||||
containerRef={containerRef}
|
||||
>
|
||||
<MachineHealthTooltipBody presentation={presentation} />
|
||||
</HoverTooltip>
|
||||
|
||||
@@ -269,14 +269,13 @@ export default {
|
||||
'machine.health.status.elevated': 'Elevated — new agents may run slower',
|
||||
'machine.health.status.high': 'High pressure — avoid spawning more here',
|
||||
'machine.health.status.unknown': 'Metrics unavailable',
|
||||
'machine.health.metric.cpu': 'CPU across all cores',
|
||||
'machine.health.metric.cpuWithCount': 'CPU across all {n} cores',
|
||||
'machine.health.metric.ram': 'RAM in use',
|
||||
'machine.health.metric.cpu': 'CPU',
|
||||
'machine.health.metric.ram': 'RAM',
|
||||
'machine.health.tooltip.load': 'Run queue (1 min): {value}',
|
||||
'machine.health.tooltip.loadShort': 'Load (1m)',
|
||||
'machine.health.tooltip.uptimeShort': 'Uptime',
|
||||
'machine.health.uptimeCompact': 'up {value}',
|
||||
'machine.health.tooltip.hint': 'Updated every ~20s from the runner on this machine.',
|
||||
'machine.health.tooltip.hint': 'Updated every ~20s from the runner on this machine',
|
||||
'machine.health.aria.cpu': 'CPU {n} percent',
|
||||
'machine.health.aria.ram': 'RAM {n} percent',
|
||||
'machine.health.aria.unknown': 'Machine health unavailable',
|
||||
|
||||
@@ -273,14 +273,13 @@ export default {
|
||||
'machine.health.status.elevated': '偏高 — 新代理可能变慢',
|
||||
'machine.health.status.high': '高压 — 避免在此继续启动',
|
||||
'machine.health.status.unknown': '指标不可用',
|
||||
'machine.health.metric.cpu': '全部核心的 CPU',
|
||||
'machine.health.metric.cpuWithCount': '全部 {n} 个核心的 CPU',
|
||||
'machine.health.metric.cpu': 'CPU占用',
|
||||
'machine.health.metric.ram': '内存占用',
|
||||
'machine.health.tooltip.load': '运行队列 (1 分钟): {value}',
|
||||
'machine.health.tooltip.loadShort': '负载 (1 分钟)',
|
||||
'machine.health.tooltip.uptimeShort': '运行时间',
|
||||
'machine.health.uptimeCompact': '已运行 {value}',
|
||||
'machine.health.tooltip.hint': '约每 20 秒由该机器上的 runner 更新。',
|
||||
'machine.health.tooltip.hint': '约每 20 秒由该机器上的 runner 更新',
|
||||
'machine.health.aria.cpu': 'CPU {n}%',
|
||||
'machine.health.aria.ram': '内存 {n}%',
|
||||
'machine.health.aria.unknown': '机器健康数据不可用',
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
presentMachineHealth,
|
||||
resolveMachineOsLabel,
|
||||
shouldShowMachineHostSubtitle,
|
||||
getCpuMetricTooltipLabel,
|
||||
} from './machineHealth'
|
||||
|
||||
describe('presentMachineHealth', () => {
|
||||
@@ -75,22 +74,6 @@ describe('resolveMachineOsLabel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCpuMetricTooltipLabel', () => {
|
||||
const t = (key: string, params?: Record<string, string | number>) => {
|
||||
if (key === 'machine.health.metric.cpuWithCount') {
|
||||
return `CPU across all ${params?.n} cores`
|
||||
}
|
||||
return 'CPU across all cores'
|
||||
}
|
||||
|
||||
it('includes core count when known', () => {
|
||||
expect(getCpuMetricTooltipLabel(6, t)).toBe('CPU across all 6 cores')
|
||||
})
|
||||
|
||||
it('falls back when core count is missing', () => {
|
||||
expect(getCpuMetricTooltipLabel(undefined, t)).toBe('CPU across all cores')
|
||||
})
|
||||
})
|
||||
describe('shouldShowMachineHostSubtitle', () => {
|
||||
it('hides host when it matches the display label', () => {
|
||||
expect(shouldShowMachineHostSubtitle('Teemo', 'Teemo')).toBe(false)
|
||||
|
||||
@@ -140,16 +140,6 @@ export function presentMachineHealth(
|
||||
}
|
||||
}
|
||||
|
||||
export function getCpuMetricTooltipLabel(
|
||||
cpuCount: number | undefined,
|
||||
t: (key: string, params?: Record<string, string | number>) => string
|
||||
): string {
|
||||
if (cpuCount !== undefined && cpuCount > 0) {
|
||||
return t('machine.health.metric.cpuWithCount', { n: cpuCount })
|
||||
}
|
||||
return t('machine.health.metric.cpu')
|
||||
}
|
||||
|
||||
export function getMachinePlatform(machine: Machine | null | undefined): string | null {
|
||||
return machine?.metadata?.platform ?? null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user