Files
hapi/web/src/components/MachineHealthIndicator.tsx
T
weishu 2e54d9fdff feat(web): replace machine tree level with filter chips in session list
Single-machine users no longer expand a redundant machine layer; with
multiple machines a chip filter bar (persisted, with hover health popup)
replaces the collapsible machine headers. Directory groups now render
top-level with machine-name suffixes when unfiltered. Also removes the
redundant session/project count header text.
2026-07-27 11:55:43 +08:00

141 lines
5.2 KiB
TypeScript

import { useEffect, useId, useRef, useState } from 'react'
import { HoverTooltip } from '@/components/HoverTooltip'
import {
MACHINE_HEALTH_BAR_FILL_CLASS,
type MachineHealthMetricPresentation,
type MachineHealthPresentation
} from '@/lib/machineHealth'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/lib/use-translation'
function TooltipMetricStat(props: {
metric: MachineHealthMetricPresentation
label: string
}) {
return (
<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(
'font-semibold tabular-nums',
props.metric.tone !== 'ok' ? 'text-[var(--app-fg)]' : 'text-[var(--app-fg)]/90'
)}
>
{props.metric.percent}%
</span>
<span
className="relative h-1.5 w-full overflow-hidden rounded-full bg-[var(--app-border)]/80"
aria-hidden="true"
>
<span
className={cn('block h-full rounded-full', MACHINE_HEALTH_BAR_FILL_CLASS[props.metric.tone])}
style={{ width: `${Math.max(4, Math.min(100, props.metric.percent))}%` }}
/>
</span>
</span>
)
}
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>
)
}
export function MachineHealthTooltipBody(props: {
presentation: MachineHealthPresentation
}) {
const { t } = useTranslation()
const { presentation } = props
const statusKey = `machine.health.status.${presentation.status}` as const
return (
<span className="block space-y-1.5">
<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="block space-y-1">
{presentation.metrics.map((metric) => (
<TooltipMetricStat
key={metric.id}
metric={metric}
label={t(`machine.health.metric.${metric.id}`, { n: metric.percent })}
/>
))}
{presentation.loadDetail ? (
<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}
</span>
</span>
) : null}
{presentation.uptimeDetail ? (
<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}
</span>
</span>
) : null}
</span>
</span>
)
}