feat: add connections page, rename dashboard page to overview

This commit is contained in:
2026-08-23 20:01:41 +08:00
parent ba65c43183
commit e3f575096b
11 changed files with 1416 additions and 34 deletions
+59
View File
@@ -0,0 +1,59 @@
import { useState, useEffect, useCallback } from 'react'
import type { NavTab } from '@/components/layout/app-sidebar'
const VALID_TABS: NavTab[] = [
'overview',
'connections',
'routes',
'topology',
'interfaces',
'settings',
]
function getTabFromHash(hash: string): NavTab {
const cleanHash = hash.replace(/^#\/?/, '').trim().toLowerCase()
if (!cleanHash || cleanHash === '') {
return 'overview'
}
// 向后兼容旧的 dashboard hash
if (cleanHash === 'dashboard') {
return 'overview'
}
const matchingTab = VALID_TABS.find((tab) => tab === cleanHash)
return matchingTab || 'overview'
}
export function useHashRoute() {
const [currentTab, setCurrentTabState] = useState<NavTab>(() =>
getTabFromHash(window.location.hash)
)
useEffect(() => {
const handleHashChange = () => {
const newTab = getTabFromHash(window.location.hash)
setCurrentTabState(newTab)
}
// If current hash is empty, initialize with #/overview
if (!window.location.hash || window.location.hash === '#' || window.location.hash === '#/') {
window.location.hash = '#/overview'
} else if (window.location.hash === '#/dashboard') {
window.location.hash = '#/overview'
}
window.addEventListener('hashchange', handleHashChange)
return () => {
window.removeEventListener('hashchange', handleHashChange)
}
}, [])
const navigate = useCallback((tab: NavTab) => {
window.location.hash = `#/${tab}`
setCurrentTabState(tab)
}, [])
return {
currentTab,
navigate,
}
}
+90 -6
View File
@@ -8,6 +8,12 @@ interface UseKlalbSSEResult {
error: string | null
reconnectAll: () => Promise<boolean>
refreshStatus: () => Promise<void>
addLink: (address: string) => Promise<{ success: boolean; message?: string }>
removeLink: (address: string) => Promise<{ success: boolean; message?: string }>
executeLinkAction: (
address: string,
action: 'reconnect' | 'disconnect' | 'remove'
) => Promise<{ success: boolean; message?: string }>
}
export function useKlalbSSE(): UseKlalbSSEResult {
@@ -20,14 +26,21 @@ export function useKlalbSSE(): UseKlalbSSEResult {
const refreshStatus = useCallback(async () => {
try {
const res = await fetch('/api/status')
if (res.ok) {
const data = (await res.json()) as SystemStatus
const [statusRes, linksRes] = await Promise.all([
fetch('/api/status'),
fetch('/api/links'),
])
if (statusRes.ok) {
const data = (await statusRes.json()) as SystemStatus
setStatus(data)
setError(null)
}
if (linksRes.ok) {
const linksData = await linksRes.json()
setLinks(Array.isArray(linksData) ? linksData : [])
}
setError(null)
} catch (err) {
console.warn('Failed to fetch status fallback:', err)
console.warn('Failed to fetch status/links fallback:', err)
}
}, [])
@@ -41,6 +54,74 @@ export function useKlalbSSE(): UseKlalbSSEResult {
}
}, [])
const addLink = useCallback(
async (address: string): Promise<{ success: boolean; message?: string }> => {
try {
const res = await fetch('/api/links', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: address.trim() }),
})
const data = await res.json()
if (res.ok && data.success) {
refreshStatus()
return { success: true }
}
return { success: false, message: data.error || '添加连接失败' }
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err)
return { success: false, message: msg }
}
},
[refreshStatus]
)
const removeLink = useCallback(
async (address: string): Promise<{ success: boolean; message?: string }> => {
try {
const res = await fetch(
`/api/links?address=${encodeURIComponent(address.trim())}`,
{ method: 'DELETE' }
)
const data = await res.json()
if (res.ok && data.success) {
refreshStatus()
return { success: true }
}
return { success: false, message: data.error || '移除连接失败' }
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err)
return { success: false, message: msg }
}
},
[refreshStatus]
)
const executeLinkAction = useCallback(
async (
address: string,
action: 'reconnect' | 'disconnect' | 'remove'
): Promise<{ success: boolean; message?: string }> => {
try {
const res = await fetch('/api/links/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, action }),
})
const data = await res.json()
if (res.ok && data.success) {
refreshStatus()
return { success: true }
}
return { success: false, message: data.error || `执行 ${action} 失败` }
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err)
return { success: false, message: msg }
}
},
[refreshStatus]
)
useEffect(() => {
let isUnmounted = false
@@ -65,7 +146,7 @@ export function useKlalbSSE(): UseKlalbSSEResult {
if (data.status) {
setStatus(data.status)
}
if (data.links) {
if (Array.isArray(data.links)) {
setLinks(data.links)
}
setIsConnected(true)
@@ -124,5 +205,8 @@ export function useKlalbSSE(): UseKlalbSSEResult {
error,
reconnectAll,
refreshStatus,
addLink,
removeLink,
executeLinkAction,
}
}