Initial commit: TaskPool React panel
- React frontend with route-level code splitting - Backend rebranded from Baihu to TaskPool - DB brand migration script and local compatibility
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { MonitorDot } from 'lucide-react'
|
||||
import {
|
||||
activeInterconnectNodeId,
|
||||
api,
|
||||
setActiveInterconnectNodeId,
|
||||
} from '@/api'
|
||||
import { getNodes, type InterconnectNode } from '@/api/interconnect'
|
||||
import { useEventBus } from '@/hooks/useEventBus'
|
||||
import { SYSTEM_EVENTS } from '@/constants'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function NodeSwitcher() {
|
||||
const [nodes, setNodes] = useState<InterconnectNode[]>([])
|
||||
const [selected, setSelected] = useState(activeInterconnectNodeId || 'local')
|
||||
const [isMaster, setIsMaster] = useState(false)
|
||||
|
||||
const fetchNodes = useCallback(async () => {
|
||||
try {
|
||||
const res = await getNodes()
|
||||
setNodes(Array.isArray(res) ? res : [])
|
||||
} catch {
|
||||
setNodes([])
|
||||
}
|
||||
}, [])
|
||||
|
||||
const updateRoleState = useCallback(
|
||||
(role: string) => {
|
||||
const master = role === 'master'
|
||||
setIsMaster(master)
|
||||
if (master) {
|
||||
setSelected(activeInterconnectNodeId || 'local')
|
||||
fetchNodes()
|
||||
} else {
|
||||
setNodes([])
|
||||
setSelected('local')
|
||||
const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/)
|
||||
if (activeInterconnectNodeId && !traveling) {
|
||||
setActiveInterconnectNodeId('')
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchNodes],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
try {
|
||||
const role = await api.settings.get('interconnect', 'interconnect_role')
|
||||
updateRoleState(role || 'none')
|
||||
} catch {
|
||||
updateRoleState('none')
|
||||
}
|
||||
})()
|
||||
}, [updateRoleState])
|
||||
|
||||
useEventBus(SYSTEM_EVENTS.INTERCONNECT_ROLE_CHANGED, (role) => {
|
||||
updateRoleState(String(role || 'none'))
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMaster) return
|
||||
const t = window.setInterval(fetchNodes, 120000)
|
||||
const onVis = () => {
|
||||
if (document.visibilityState === 'visible') fetchNodes()
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
return () => {
|
||||
clearInterval(t)
|
||||
document.removeEventListener('visibilitychange', onVis)
|
||||
}
|
||||
}, [isMaster, fetchNodes])
|
||||
|
||||
if (!isMaster) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex h-8 max-w-[150px] items-center gap-1 rounded-md border px-2 text-[11px]',
|
||||
selected !== 'local'
|
||||
? 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400'
|
||||
: 'border-[var(--border)] bg-[var(--bg-secondary)] text-[var(--text-secondary)]',
|
||||
)}
|
||||
>
|
||||
<MonitorDot size={12} className="shrink-0" />
|
||||
<select
|
||||
className="max-w-[120px] truncate border-0 bg-transparent text-[11px] outline-none"
|
||||
value={selected}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
setSelected(val)
|
||||
if (val === 'local') {
|
||||
setActiveInterconnectNodeId('')
|
||||
} else {
|
||||
const name = nodes.find((n) => n.id === val)?.name || ''
|
||||
setActiveInterconnectNodeId(val, name)
|
||||
}
|
||||
window.location.reload()
|
||||
}}
|
||||
>
|
||||
<option value="local">本机节点</option>
|
||||
{nodes.map((n) => (
|
||||
<option key={n.id} value={n.id}>
|
||||
{n.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user