diff --git a/packages/k8s-ui/src/components/resources/ResourcesView.tsx b/packages/k8s-ui/src/components/resources/ResourcesView.tsx index 5dec7cf72..b922bb7b6 100644 --- a/packages/k8s-ui/src/components/resources/ResourcesView.tsx +++ b/packages/k8s-ui/src/components/resources/ResourcesView.tsx @@ -1,6 +1,7 @@ import React, { useState, useMemo, useEffect, useCallback, useRef, useContext } from 'react' import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso' import { useRefreshAnimation } from '../../hooks/useRefreshAnimation' +import { useNow } from '../../hooks/useNow' import { PaneLoader } from '../ui/PaneLoader' import type { TopPodMetrics, TopNodeMetrics } from '../../types' import { @@ -1749,6 +1750,26 @@ function getInitialFiltersFromURL() { // Sort state type type SortDirection = 'asc' | 'desc' | null +/** + * "Updated Xs" / "Updated 1m" badge in the toolbar. + * + * Lives in its own component so the 1Hz `useNow` tick re-renders + * only this tiny label, NOT the entire ResourcesView (which is + * ~4000 lines and contains a virtualized table). Without this + * boundary, every visible row's React render would run once per + * second just to advance one label by 1s. + */ +function LastUpdatedLabel({ lastUpdated }: { lastUpdated: Date }) { + // Read the ticking clock here so only this subtree re-renders. + useNow(1000) + return ( +
+ + Updated {formatAge(lastUpdated.toISOString())} +
+ ) +} + export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange, apiResources: apiResourcesProp, @@ -2624,12 +2645,25 @@ export function ResourcesView({ const [refetch, isRefreshAnimating, refreshPhase] = useRefreshAnimation(() => refetchFn?.()) - // Track last updated time + // Track last updated time. + // + // React Query bumps `dataUpdatedAt` every time it records a successful + // fetch — even when the response is byte-identical to what's already + // cached and structural sharing returns the same `data` reference. + // Mounting / focusing windows / a sibling subscriber issuing the same + // queryKey can all trigger a no-op refetch. Resetting the user-visible + // "Updated Xs" timer on those events is misleading: it suggests fresh + // data arrived when nothing actually changed, and the user reads the + // "<1s" jump as evidence that opening a filter drawer triggered a real + // network round-trip. Gate the bump on data-reference change so we only + // bump when the cache actually mutated. + const lastDataRef = useRef(undefined) useEffect(() => { - if (dataUpdatedAt) { + if (dataUpdatedAt && resources !== lastDataRef.current) { + lastDataRef.current = resources setLastUpdated(new Date(dataUpdatedAt)) } - }, [dataUpdatedAt]) + }, [dataUpdatedAt, resources]) // Derive counts — prefer lightweight resourceCounts prop over full query data const counts = useMemo(() => { @@ -3468,12 +3502,7 @@ export function ResourcesView({ )} - {lastUpdated && ( -
- - Updated {formatAge(lastUpdated.toISOString())} -
- )} + {lastUpdated && } {/* Column picker */}