From d7e0fc264828a8c613b104155c5b6753b4852fa9 Mon Sep 17 00:00:00 2001 From: seungoh-lee Date: Wed, 25 Mar 2026 20:55:51 +0900 Subject: [PATCH 1/3] feat : refresh cooldwon add notification --- client/app/lib/hooks/useRefreshCooldown.js | 57 +++++++++++++++++++ .../dashboards/components/DashboardHeader.jsx | 37 ++++++++++-- client/app/pages/queries/QuerySource.jsx | 21 ++++++- client/app/pages/queries/QueryView.jsx | 14 ++++- .../GeneralSettings/FeatureFlagsSettings.jsx | 17 ++++++ .../settings/hooks/useOrganizationSettings.js | 1 + redash/handlers/authentication.py | 1 + redash/handlers/query_results.py | 6 ++ redash/settings/organization.py | 1 + 9 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 client/app/lib/hooks/useRefreshCooldown.js diff --git a/client/app/lib/hooks/useRefreshCooldown.js b/client/app/lib/hooks/useRefreshCooldown.js new file mode 100644 index 0000000000..934c161a1f --- /dev/null +++ b/client/app/lib/hooks/useRefreshCooldown.js @@ -0,0 +1,57 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import { currentUser, clientConfig } from "@/services/auth"; +import notification from "@/services/notification"; + +export default function useRefreshCooldown(retrievedAt) { + const cooldownSeconds = currentUser.isAdmin ? 0 : (clientConfig.nonAdminRefreshCooldown || 0); + const [remainingTime, setRemainingTime] = useState(0); + const timerRef = useRef(null); + + const calculateRemaining = useCallback(() => { + if (cooldownSeconds <= 0 || !retrievedAt) return 0; + const retrievedTime = new Date(retrievedAt).getTime(); + if (!retrievedTime) return 0; + const elapsed = Math.floor((Date.now() - retrievedTime) / 1000); + return Math.max(0, cooldownSeconds - elapsed); + }, [cooldownSeconds, retrievedAt]); + + useEffect(() => { + if (timerRef.current) clearInterval(timerRef.current); + + const remaining = calculateRemaining(); + setRemainingTime(remaining); + + if (remaining > 0) { + timerRef.current = setInterval(() => { + const r = calculateRemaining(); + setRemainingTime(r); + if (r <= 0) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }, 1000); + } + + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [calculateRemaining]); + + const isCooldownActive = remainingTime > 0; + + const notifyCooldown = useCallback(() => { + if (isCooldownActive) { + notification.warning( + `Refresh is on cooldown. Please wait ${remainingTime} seconds.`, + null, + { duration: 3 } + ); + } + }, [isCooldownActive, remainingTime]); + + return { + isCooldownActive, + remainingTime, + notifyCooldown, + }; +} diff --git a/client/app/pages/dashboards/components/DashboardHeader.jsx b/client/app/pages/dashboards/components/DashboardHeader.jsx index 86d7e12aee..95af44a89b 100644 --- a/client/app/pages/dashboards/components/DashboardHeader.jsx +++ b/client/app/pages/dashboards/components/DashboardHeader.jsx @@ -1,7 +1,7 @@ -import React from "react"; +import React, { useMemo } from "react"; import cx from "classnames"; import PropTypes from "prop-types"; -import { map, includes } from "lodash"; +import { map, includes, compact, max } from "lodash"; import Button from "antd/lib/button"; import Dropdown from "antd/lib/dropdown"; import Menu from "antd/lib/menu"; @@ -18,6 +18,7 @@ import { policy } from "@/services/policy"; import recordEvent from "@/services/recordEvent"; import { durationHumanize } from "@/lib/utils"; import { DashboardStatusEnum } from "../hooks/useDashboard"; +import useRefreshCooldown from "@/lib/hooks/useRefreshCooldown"; import "./DashboardHeader.less"; @@ -64,24 +65,48 @@ DashboardPageTitle.propTypes = { }; function RefreshButton({ dashboardConfiguration }) { - const { refreshRate, setRefreshRate, disableRefreshRate, refreshing, refreshDashboard } = dashboardConfiguration; + const { refreshRate, setRefreshRate, disableRefreshRate, refreshing, refreshDashboard, dashboard } = dashboardConfiguration; + + const latestRetrievedAt = useMemo(() => { + const timestamps = compact( + map(dashboard.widgets, (w) => { + const qr = w.getQueryResult(); + return qr ? qr.getUpdatedAt() : null; + }) + ); + return timestamps.length > 0 ? max(timestamps) : null; + }, [dashboard.widgets, refreshing]); // eslint-disable-line react-hooks/exhaustive-deps + + const { isCooldownActive, remainingTime, notifyCooldown } = useRefreshCooldown(latestRetrievedAt); const allowedIntervals = policy.getDashboardRefreshIntervals(); const refreshRateOptions = clientConfig.dashboardRefreshIntervals; + const handleRefresh = () => { + if (isCooldownActive) { + notifyCooldown(); + return; + } + refreshDashboard(); + }; const onRefreshRateSelected = ({ key }) => { const parsedRefreshRate = parseFloat(key); if (parsedRefreshRate) { setRefreshRate(parsedRefreshRate); - refreshDashboard(); + handleRefresh(); } else { disableRefreshRate(); } }; + const refreshLabel = isCooldownActive + ? `Refresh (${remainingTime}s)` + : refreshRate + ? durationHumanize(refreshRate) + : "Refresh"; return ( -