fix: stabilize admin dashboard navigation
This commit is contained in:
@@ -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
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
setLoading(true)
|
if (!cachedDashboardStats) {
|
||||||
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user