@@ -4,10 +4,13 @@ import {
44 RiBookMarkLine ,
55 RiFeedbackLine ,
66 RiMoonLine ,
7+ RiRefreshLine ,
78 RiSunLine ,
89} from "react-icons/ri/" ;
910import { Outlet , Link as RouterLink } from "react-router-dom" ;
11+ import { useSWRConfig } from "swr" ;
1012import { GlobalContext } from "../../App" ;
13+ import { DASHBOARD_DATA_LOADED_EVENT } from "../../common/constants" ;
1114import { SearchTimezone } from "../../components/SearchComponent" ;
1215import Logo from "../../logo.svg" ;
1316import { MainNavContext , useMainNavState } from "./mainNavContext" ;
@@ -173,6 +176,7 @@ const MainNavBar = () => {
173176 </ Typography >
174177 ) ) }
175178 < Box sx = { { flexGrow : 1 } } > </ Box >
179+ < DashboardDataFreshness currentTimeZone = { currentTimeZone } />
176180 < Box sx = { { marginRight : 2 } } >
177181 { ! urlTheme && (
178182 < Tooltip title = { themeMode === "light" ? "Dark mode" : "Light mode" } >
@@ -223,6 +227,142 @@ const MainNavBar = () => {
223227 ) ;
224228} ;
225229
230+ const DashboardDataFreshness = ( {
231+ currentTimeZone,
232+ } : {
233+ currentTimeZone : string | undefined ;
234+ } ) => {
235+ const { mutate } = useSWRConfig ( ) ;
236+ const [ lastDataLoadTime , setLastDataLoadTime ] = React . useState < number > ( ) ;
237+ const [ now , setNow ] = React . useState ( Date . now ( ) ) ;
238+ const [ isRefreshingData , setIsRefreshingData ] = React . useState ( false ) ;
239+
240+ React . useEffect ( ( ) => {
241+ const handleDashboardDataLoaded = ( ) => {
242+ setLastDataLoadTime ( Date . now ( ) ) ;
243+ } ;
244+
245+ window . addEventListener (
246+ DASHBOARD_DATA_LOADED_EVENT ,
247+ handleDashboardDataLoaded ,
248+ ) ;
249+
250+ return ( ) => {
251+ window . removeEventListener (
252+ DASHBOARD_DATA_LOADED_EVENT ,
253+ handleDashboardDataLoaded ,
254+ ) ;
255+ } ;
256+ } , [ ] ) ;
257+
258+ React . useEffect ( ( ) => {
259+ const interval = window . setInterval ( ( ) => {
260+ setNow ( Date . now ( ) ) ;
261+ } , 5000 ) ;
262+
263+ return ( ) => {
264+ window . clearInterval ( interval ) ;
265+ } ;
266+ } , [ ] ) ;
267+
268+ const refreshDashboardData = async ( ) => {
269+ setIsRefreshingData ( true ) ;
270+ try {
271+ await mutate ( ( ) => true ) ;
272+ } finally {
273+ setIsRefreshingData ( false ) ;
274+ }
275+ } ;
276+
277+ return (
278+ < Box
279+ sx = { {
280+ alignItems : "center" ,
281+ display : "flex" ,
282+ flexShrink : 0 ,
283+ gap : 1 ,
284+ marginRight : 1 ,
285+ } }
286+ >
287+ < Tooltip
288+ title = {
289+ lastDataLoadTime
290+ ? `Last successful data load: ${ formatLastDataLoadTime (
291+ lastDataLoadTime ,
292+ currentTimeZone ,
293+ ) } `
294+ : "No dashboard data has loaded yet."
295+ }
296+ >
297+ < Typography
298+ color = "text.secondary"
299+ sx = { { whiteSpace : "nowrap" } }
300+ variant = "caption"
301+ >
302+ { formatFreshnessLabel ( lastDataLoadTime , now ) }
303+ </ Typography >
304+ </ Tooltip >
305+ < Tooltip title = "Refresh dashboard data" >
306+ < span >
307+ < IconButton
308+ aria-label = "Refresh dashboard data"
309+ disabled = { isRefreshingData }
310+ onClick = { refreshDashboardData }
311+ size = "large"
312+ sx = { ( theme ) => ( { color : theme . palette . text . secondary } ) }
313+ >
314+ < RiRefreshLine />
315+ </ IconButton >
316+ </ span >
317+ </ Tooltip >
318+ </ Box >
319+ ) ;
320+ } ;
321+
322+ export const formatLastDataLoadTime = (
323+ lastDataLoadTime : number ,
324+ currentTimeZone : string | undefined ,
325+ ) => {
326+ return new Date ( lastDataLoadTime ) . toLocaleString (
327+ undefined ,
328+ currentTimeZone ? { timeZone : currentTimeZone } : undefined ,
329+ ) ;
330+ } ;
331+
332+ export const formatFreshnessLabel = (
333+ lastDataLoadTime : number | undefined ,
334+ now : number ,
335+ ) => {
336+ if ( ! lastDataLoadTime ) {
337+ return "No data loaded" ;
338+ }
339+
340+ const ageMs = Math . max ( 0 , now - lastDataLoadTime ) ;
341+ const fiveSecondsMs = 5 * 1000 ;
342+ const minuteMs = 60 * 1000 ;
343+ const hourMs = 60 * minuteMs ;
344+ const dayMs = 24 * hourMs ;
345+
346+ if ( ageMs < fiveSecondsMs ) {
347+ return "Updated just now" ;
348+ }
349+
350+ if ( ageMs < minuteMs ) {
351+ const ageSeconds = Math . min ( 55 , Math . round ( ageMs / fiveSecondsMs ) * 5 ) ;
352+ return `Updated ${ ageSeconds } s ago` ;
353+ }
354+
355+ if ( ageMs < hourMs ) {
356+ return `Updated ${ Math . floor ( ageMs / minuteMs ) } m ago` ;
357+ }
358+
359+ if ( ageMs < dayMs ) {
360+ return `Updated ${ Math . floor ( ageMs / hourMs ) } h ago` ;
361+ }
362+
363+ return `Updated ${ Math . floor ( ageMs / dayMs ) } d ago` ;
364+ } ;
365+
226366const MainNavBreadcrumbs = ( ) => {
227367 const { mainNavPageHierarchy } = useContext ( MainNavContext ) ;
228368
0 commit comments