mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
fix(hub,web): stop machines from showing raw id prefixes as names
Hub: getOrCreateMachine now merges incoming machine-owned metadata over the stored row (first-write-wins previously kept rows registered without a host name nameless forever; hub-only fields like displayName survive). Web: session-list machine labels are cached in localStorage so machines whose row is gone or whose query has not loaded yet keep their last known name instead of flickering to the 8-char id prefix.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import type { Machine } from '@/types/api'
|
||||
import { getMachineTitle, useMachineLabels } from './useMachineLabels'
|
||||
|
||||
function makeMachine(id: string, metadata: Machine['metadata']): Machine {
|
||||
return {
|
||||
id,
|
||||
namespace: 'default',
|
||||
seq: 1,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
active: true,
|
||||
activeAt: 0,
|
||||
metadata,
|
||||
metadataVersion: 1,
|
||||
runnerState: null,
|
||||
runnerStateVersion: 0,
|
||||
}
|
||||
}
|
||||
|
||||
describe('getMachineTitle', () => {
|
||||
it('prefers displayName, then host, then the id prefix', () => {
|
||||
expect(getMachineTitle(makeMachine('abcdef123456', { displayName: 'Work', host: 'mac' } as Machine['metadata']))).toBe('Work')
|
||||
expect(getMachineTitle(makeMachine('abcdef123456', { host: 'mac' } as Machine['metadata']))).toBe('mac')
|
||||
expect(getMachineTitle(makeMachine('abcdef123456', null))).toBe('abcdef12')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMachineLabels', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
it('returns live titles and caches them', () => {
|
||||
const machines = [makeMachine('machine-1', { host: 'MacBook Pro' } as Machine['metadata'])]
|
||||
const { result } = renderHook(() => useMachineLabels(machines))
|
||||
|
||||
expect(result.current['machine-1']).toBe('MacBook Pro')
|
||||
expect(JSON.parse(window.localStorage.getItem('hapi-machine-labels')!)).toEqual({ 'machine-1': 'MacBook Pro' })
|
||||
})
|
||||
|
||||
it('keeps the cached label when the machine is absent from the list', () => {
|
||||
window.localStorage.setItem('hapi-machine-labels', JSON.stringify({ 'gone-machine': 'MacBook Pro' }))
|
||||
const { result } = renderHook(() => useMachineLabels([]))
|
||||
|
||||
expect(result.current['gone-machine']).toBe('MacBook Pro')
|
||||
})
|
||||
|
||||
it('prefers the live title over a stale cached one', () => {
|
||||
window.localStorage.setItem('hapi-machine-labels', JSON.stringify({ 'machine-1': 'old-name' }))
|
||||
const machines = [makeMachine('machine-1', { host: 'new-name' } as Machine['metadata'])]
|
||||
const { result } = renderHook(() => useMachineLabels(machines))
|
||||
|
||||
expect(result.current['machine-1']).toBe('new-name')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import type { Machine } from '@/types/api'
|
||||
|
||||
export function getMachineTitle(machine: Machine): string {
|
||||
if (machine.metadata?.displayName) return machine.metadata.displayName
|
||||
if (machine.metadata?.host) return machine.metadata.host
|
||||
return machine.id.slice(0, 8)
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'hapi-machine-labels'
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
}
|
||||
|
||||
function readCachedLabels(): Record<string, string> {
|
||||
if (!isBrowser()) return {}
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return {}
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return {}
|
||||
const labels: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
labels[key] = value
|
||||
}
|
||||
}
|
||||
return labels
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function writeCachedLabels(labels: Record<string, string>): void {
|
||||
if (!isBrowser()) return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(labels))
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Machine id → display label for the session list. Live data from the
|
||||
* machines query wins, but labels are also cached in localStorage so a
|
||||
* machine whose row is gone (reinstalled CLI, stale sessions) or whose
|
||||
* query has not loaded yet keeps its last known name instead of falling
|
||||
* back to a raw id prefix.
|
||||
*/
|
||||
export function useMachineLabels(machines: Machine[]): Record<string, string> {
|
||||
const labels = useMemo(() => {
|
||||
const merged = readCachedLabels()
|
||||
for (const machine of machines) {
|
||||
merged[machine.id] = getMachineTitle(machine)
|
||||
}
|
||||
return merged
|
||||
}, [machines])
|
||||
|
||||
useEffect(() => {
|
||||
writeCachedLabels(labels)
|
||||
}, [labels])
|
||||
|
||||
return labels
|
||||
}
|
||||
Reference in New Issue
Block a user