fix: stabilize admin dashboard navigation

This commit is contained in:
linkong
2026-03-31 18:09:52 +08:00
parent e903723877
commit 6f01dfb590
8 changed files with 51 additions and 18 deletions

View File

@@ -1 +1 @@
0.22.3 0.22.4

View File

@@ -7,6 +7,26 @@ This project follows the repository versioning rule:
- `feature` -> `+0.1.0` - `feature` -> `+0.1.0`
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## 0.22.4
Released: 2026-03-31
### Highlights
- Fixed the admin dashboard navigation so the dashboard no longer appears to hard-reload when operators click the current “仪表盘” entry.
- Smoothed dashboard revisits by caching the last stats snapshot, reducing visible loading flashes even when the page is remounted by navigation flow.
### Improved
- Improved route ownership in [App.tsx](/home/ray/dev/linkong/planet/frontend/src/App.tsx) so the dashboard lives only at `/admin`, while `/` no longer opens the admin dashboard directly.
- Improved sidebar navigation behavior in [AppLayout.tsx](/home/ray/dev/linkong/planet/frontend/src/components/AppLayout/AppLayout.tsx) by replacing direct link rendering with guarded programmatic navigation that ignores clicks on the already-active route.
- Improved dashboard data reuse in [Dashboard.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Dashboard/Dashboard.tsx) by caching the latest stats payload and reusing it on remount before the next refresh cycle completes.
### Fixed
- Fixed the dashboard menu entry switching between `/` and `/admin`, which caused the dashboard route to remount and replay its initial data-fetch/WebSocket bootstrap.
- Fixed the most visible “reload” symptom on repeated dashboard clicks by preventing no-op navigation and avoiding an empty loading state when cached dashboard stats are available.
## 0.22.3 ## 0.22.3
Released: 2026-03-31 Released: 2026-03-31

View File

@@ -1,12 +1,12 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.22.3", "version": "0.22.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.22.3", "version": "0.22.4",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"antd": "^5.12.5", "antd": "^5.12.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.22.3", "version": "0.22.4",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",

View File

@@ -19,15 +19,15 @@ function App() {
return ( return (
<Routes> <Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/admin" element={<Dashboard />} /> <Route path="/admin" element={<Dashboard />} />
<Route path="/" element={<Navigate to="/earth" replace />} />
<Route path="/earth" element={<Earth />} /> <Route path="/earth" element={<Earth />} />
<Route path="/users" element={<Users />} /> <Route path="/users" element={<Users />} />
<Route path="/datasources" element={<DataSources />} /> <Route path="/datasources" element={<DataSources />} />
<Route path="/data" element={<DataList />} /> <Route path="/data" element={<DataList />} />
<Route path="/bgp" element={<BGP />} /> <Route path="/bgp" element={<BGP />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/admin" replace />} />
</Routes> </Routes>
) )
} }

View File

@@ -10,7 +10,7 @@ import {
MenuUnfoldOutlined, MenuUnfoldOutlined,
MenuFoldOutlined, MenuFoldOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { Link, useLocation } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import packageJson from '../../../package.json' import packageJson from '../../../package.json'
@@ -23,18 +23,19 @@ interface AppLayoutProps {
function AppLayout({ children }: AppLayoutProps) { function AppLayout({ children }: AppLayoutProps) {
const location = useLocation() const location = useLocation()
const navigate = useNavigate()
const { user, logout } = useAuthStore() const { user, logout } = useAuthStore()
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false)
const showBanner = true const showBanner = true
const appVersion = `v${packageJson.version}` const appVersion = `v${packageJson.version}`
const menuItems = [ const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/"></Link> }, { key: '/admin', icon: <DashboardOutlined />, label: '仪表盘' },
{ key: '/datasources', icon: <DatabaseOutlined />, label: <Link to="/datasources"></Link> }, { key: '/datasources', icon: <DatabaseOutlined />, label: '数据源' },
{ key: '/data', icon: <BarChartOutlined />, label: <Link to="/data"></Link> }, { key: '/data', icon: <BarChartOutlined />, label: '采集数据' },
{ key: '/bgp', icon: <DeploymentUnitOutlined />, label: <Link to="/bgp">BGP观测</Link> }, { key: '/bgp', icon: <DeploymentUnitOutlined />, label: 'BGP观测' },
{ key: '/users', icon: <UserOutlined />, label: <Link to="/users"></Link> }, { key: '/users', icon: <UserOutlined />, label: '用户管理' },
{ key: '/settings', icon: <SettingOutlined />, label: <Link to="/settings"></Link> }, { key: '/settings', icon: <SettingOutlined />, label: '系统配置' },
] ]
return ( return (
@@ -68,6 +69,11 @@ function AppLayout({ children }: AppLayoutProps) {
mode="inline" mode="inline"
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
items={menuItems} items={menuItems}
onClick={({ key }) => {
if (key !== location.pathname) {
navigate(key)
}
}}
/> />
</div> </div>

View File

@@ -90,6 +90,8 @@ const RESTART_GUIDE_LINES: Record<RestartAction, string[]> = {
], ],
} }
let cachedDashboardStats: Stats | null = null
function getRestartConfirmMessage(action: RestartAction): string { function getRestartConfirmMessage(action: RestartAction): string {
if (action === 'restart-database') { if (action === 'restart-database') {
return '将重启 PostgreSQL 和 Redis页面通常保持在线但相关请求可能短暂波动。' return '将重启 PostgreSQL 和 Redis页面通常保持在线但相关请求可能短暂波动。'
@@ -102,8 +104,8 @@ function getRestartConfirmMessage(action: RestartAction): string {
function Dashboard() { function Dashboard() {
const { token, clearAuth, user } = useAuthStore() const { token, clearAuth, user } = useAuthStore()
const [stats, setStats] = useState<Stats | null>(null) const [stats, setStats] = useState<Stats | null>(cachedDashboardStats)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(cachedDashboardStats === null)
const [wsConnected, setWsConnected] = useState(false) const [wsConnected, setWsConnected] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [restartModalOpen, setRestartModalOpen] = useState(false) const [restartModalOpen, setRestartModalOpen] = useState(false)
@@ -122,7 +124,9 @@ function Dashboard() {
const fetchStats = async () => { const fetchStats = async () => {
try { try {
if (!cachedDashboardStats) {
setLoading(true) setLoading(true)
}
const res = await fetch('/api/v1/dashboard/stats', { const res = await fetch('/api/v1/dashboard/stats', {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) })
@@ -132,6 +136,7 @@ function Dashboard() {
return return
} }
const data = await res.json() const data = await res.json()
cachedDashboardStats = data
setStats(data) setStats(data)
setError(null) setError(null)
} catch (err) { } catch (err) {
@@ -164,7 +169,9 @@ function Dashboard() {
try { try {
const msg = JSON.parse(event.data) const msg = JSON.parse(event.data)
if (msg.type === 'data_frame' && msg.channel === 'dashboard') { if (msg.type === 'data_frame' && msg.channel === 'dashboard') {
setStats(msg.payload?.stats as Stats) const nextStats = msg.payload?.stats as Stats
cachedDashboardStats = nextStats
setStats(nextStats)
} }
} catch (e) { } catch (e) {
console.error('Parse WS message error:', e) console.error('Parse WS message error:', e)

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]] [[package]]
name = "planet" name = "planet"
version = "0.22.2" version = "0.22.4"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },