mirror of
https://github.com/rustfs/console.git
synced 2026-10-30 19:33:40 +08:00
fix: prevent duplicate running status requests (#175)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { ApiClient } from "@/lib/api-client"
|
||||
import { AwsClient } from "@/lib/aws4fetch"
|
||||
@@ -22,6 +22,11 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const pathnameRef = useRef(pathname)
|
||||
|
||||
useEffect(() => {
|
||||
pathnameRef.current = pathname
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !credentials?.AccessKeyId) {
|
||||
@@ -65,7 +70,7 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pathname === "/403/") return
|
||||
if (pathnameRef.current === "/403/") return
|
||||
router.replace("/403/")
|
||||
},
|
||||
})
|
||||
@@ -89,7 +94,6 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
|
||||
credentials?.SecretAccessKey,
|
||||
credentials?.SessionToken,
|
||||
logout,
|
||||
pathname,
|
||||
router,
|
||||
])
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from "react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useSystem } from "@/hooks/use-system"
|
||||
import { scheduleMicrotask } from "@/lib/schedule-microtask"
|
||||
import {
|
||||
normalizeDataUsageInfo,
|
||||
normalizeMetricsInfo,
|
||||
@@ -157,6 +158,7 @@ export function usePerformanceData() {
|
||||
}, [getDataUsageInfo, getStorageInfo, getSystemInfo, getSystemMetrics])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
mountedRef.current = true
|
||||
hasSystemDataRef.current = false
|
||||
setSystemInfo({})
|
||||
@@ -169,7 +171,9 @@ export function usePerformanceData() {
|
||||
setSourceErrors({})
|
||||
setLastUpdatedAt(null)
|
||||
setMetricsUpdatedAt(null)
|
||||
void refetch()
|
||||
scheduleMicrotask(() => {
|
||||
if (!cancelled) void refetch()
|
||||
})
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
if (document.visibilityState === "visible") void refetch()
|
||||
@@ -181,6 +185,7 @@ export function usePerformanceData() {
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
mountedRef.current = false
|
||||
requestVersionRef.current += 1
|
||||
abortControllerRef.current?.abort()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { hasConsoleScopes, type ConsolePolicy } from "@/lib/console-policy-parse
|
||||
import { hasConsoleCapability, type ConsoleCapability } from "@/lib/permission-capabilities"
|
||||
import type { PermissionResourceContext } from "@/lib/permission-resources"
|
||||
import { CONSOLE_SCOPES, PAGE_PERMISSIONS } from "@/lib/console-permissions"
|
||||
import { scheduleMicrotask } from "@/lib/schedule-microtask"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { useApiOptional } from "@/contexts/api-context"
|
||||
|
||||
@@ -130,8 +131,15 @@ export function PermissionsProvider({ children }: { children: React.ReactNode })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
if (api && isAuthenticated && !hasResolvedAdmin) {
|
||||
void fetchAdminStatus()
|
||||
scheduleMicrotask(() => {
|
||||
if (!cancelled) void fetchAdminStatus()
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [api, isAuthenticated, hasResolvedAdmin, fetchAdminStatus])
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from "node:fs"
|
||||
const rootLayout = fs.readFileSync("app/layout.tsx", "utf8")
|
||||
const dashboardLayout = fs.readFileSync("app/(dashboard)/layout.tsx", "utf8")
|
||||
const authContext = fs.readFileSync("contexts/auth-context.tsx", "utf8")
|
||||
const apiContext = fs.readFileSync("contexts/api-context.tsx", "utf8")
|
||||
const loginPage = fs.readFileSync("app/(auth)/auth/login/page.tsx", "utf8")
|
||||
const oidcCallbackPage = fs.readFileSync("app/(auth)/auth/oidc-callback/page.tsx", "utf8")
|
||||
const i18nProvider = fs.readFileSync("components/providers/i18n-provider.tsx", "utf8")
|
||||
@@ -33,6 +34,17 @@ test("dashboard layout owns dashboard providers before the auth guard", () => {
|
||||
assert.ok(dashboardLayout.indexOf("<PermissionsProvider>") < dashboardLayout.indexOf("<DashboardAuthGuard>"))
|
||||
})
|
||||
|
||||
test("dashboard navigation does not recreate the API client", () => {
|
||||
const apiSetupEffect = apiContext.match(
|
||||
/useEffect\(\(\) => \{\n if \(!isAuthenticated[\s\S]*?\n \}, \[([\s\S]*?)\]\)/,
|
||||
)
|
||||
|
||||
assert.ok(apiSetupEffect)
|
||||
assert.doesNotMatch(apiSetupEffect[1], /pathname/)
|
||||
assert.match(apiContext, /pathnameRef\.current = pathname/)
|
||||
assert.match(apiContext, /pathnameRef\.current === "\/403\/"/)
|
||||
})
|
||||
|
||||
test("auth routes do not depend on dashboard permission resolution", () => {
|
||||
assert.doesNotMatch(loginPage, /useFirstAccessibleDashboardRoute/)
|
||||
assert.doesNotMatch(oidcCallbackPage, /useFirstAccessibleDashboardRoute/)
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from "node:fs"
|
||||
|
||||
const hookSource = fs.readFileSync("hooks/use-performance-data.ts", "utf8")
|
||||
const systemSource = fs.readFileSync("hooks/use-system.ts", "utf8")
|
||||
const permissionsSource = fs.readFileSync("hooks/use-permissions.tsx", "utf8")
|
||||
const pageSource = fs.readFileSync("app/(dashboard)/status/page.tsx", "utf8")
|
||||
const serverSource = fs.readFileSync("app/(dashboard)/_components/performance-server-list.tsx", "utf8")
|
||||
const usageSource = fs.readFileSync("app/(dashboard)/_components/performance-usage-card.tsx", "utf8")
|
||||
@@ -26,6 +27,12 @@ test("performance refresh keeps partial data and rejects stale responses", () =>
|
||||
assert.ok((systemSource.match(/suppress403Redirect: true/g) ?? []).length >= 3)
|
||||
})
|
||||
|
||||
test("running status skips Strict Mode preview requests", () => {
|
||||
assert.match(hookSource, /scheduleMicrotask\(\(\) => \{\s*if \(!cancelled\) void refetch\(\)/)
|
||||
assert.match(hookSource, /return \(\) => \{\s*cancelled = true/)
|
||||
assert.match(permissionsSource, /scheduleMicrotask\(\(\) => \{\s*if \(!cancelled\) void fetchAdminStatus\(\)/)
|
||||
})
|
||||
|
||||
test("running status distinguishes unknown values and exposes recovery feedback", () => {
|
||||
assert.match(pageSource, /role="alert"/)
|
||||
assert.match(pageSource, /aria-busy=\{refreshing\}/)
|
||||
|
||||
Reference in New Issue
Block a user