diff --git a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx index fa06b7516f..aa34187d0c 100644 --- a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx +++ b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx @@ -12,6 +12,8 @@ import {HOUR_IN_SECONDS, SECOND_IN_MS} from '../../utils/constants'; import {useTypedSelector} from '../../utils/hooks'; import {isAxiosError} from '../../utils/response'; +import {useElapsedDuration} from './useElapsedDuration'; + import './QueryExecutionStatus.scss'; const b = cn('kv-query-execution-status'); @@ -29,38 +31,17 @@ export const QueryExecutionStatus = ({className, error, loading}: QueryExecution let textColor: TextProps['color']; const {startTime, endTime} = useTypedSelector(selectQueryDuration); - const [queryDuration, setQueryDuration] = React.useState( - startTime ? (endTime || Date.now()) - startTime : 0, - ); - - const isCancelled = isQueryCancelledError(error); - - const setDuration = React.useCallback(() => { - if (startTime) { - const actualEndTime = endTime || Date.now(); - setQueryDuration(actualEndTime - startTime); + const durationMs = useElapsedDuration({startTime, endTime, loading}); + const formattedDuration = React.useMemo(() => { + if (durationMs < 10 * SECOND_IN_MS) { + return duration(durationMs).format('mm:ss.SSS'); } - }, [endTime, startTime]); + return durationMs > HOUR_IN_SECONDS * SECOND_IN_MS + ? duration(durationMs).format('hh:mm:ss') + : duration(durationMs).format('mm:ss'); + }, [durationMs]); - React.useEffect(() => { - let timerId: ReturnType | undefined; - setDuration(); - - if (loading) { - timerId = setInterval(setDuration, SECOND_IN_MS); - } else { - clearInterval(timerId); - } - return () => { - clearInterval(timerId); - }; - }, [loading, setDuration]); - - const formattedQueryDuration = React.useMemo(() => { - return queryDuration > HOUR_IN_SECONDS * SECOND_IN_MS - ? duration(queryDuration).format('hh:mm:ss') - : duration(queryDuration).format('mm:ss'); - }, [queryDuration]); + const isCancelled = isQueryCancelledError(error); if (loading) { theme = 'info'; @@ -96,7 +77,7 @@ export const QueryExecutionStatus = ({className, error, loading}: QueryExecution size="m" className={b(null, className)} icon={icon} - value={formattedQueryDuration} + value={formattedDuration} > {label} diff --git a/src/components/QueryExecutionStatus/useElapsedDuration.ts b/src/components/QueryExecutionStatus/useElapsedDuration.ts new file mode 100644 index 0000000000..86a76d782f --- /dev/null +++ b/src/components/QueryExecutionStatus/useElapsedDuration.ts @@ -0,0 +1,63 @@ +import React from 'react'; + +import {SECOND_IN_MS} from '../../utils/constants'; + +const FAST_REFRESH_MS = 100; +const FAST_REFRESH_JITTER_MS = 20; +const TEN_SECONDS_IN_MS = 10 * SECOND_IN_MS; + +interface UseElapsedDurationParams { + startTime?: number; + endTime?: number; + loading?: boolean; +} + +export function useElapsedDuration({ + startTime, + endTime, + loading, +}: UseElapsedDurationParams): number { + const [durationMs, setDurationMs] = React.useState( + startTime ? (endTime || Date.now()) - startTime : 0, + ); + + const setDuration = React.useCallback(() => { + if (startTime) { + const actualEndTime = endTime || Date.now(); + setDurationMs(actualEndTime - startTime); + } + }, [endTime, startTime]); + + const timerRef = React.useRef | undefined>(undefined); + const cancelledRef = React.useRef(false); + + const tick = React.useCallback(() => { + if (cancelledRef.current) { + return; + } + setDuration(); + + if (!loading || !startTime) { + return; + } + + const actualEndTime = endTime || Date.now(); + const elapsedMs = actualEndTime - startTime; + const jitter = Math.floor((Math.random() * 2 - 1) * FAST_REFRESH_JITTER_MS); + const nextDelay = elapsedMs < TEN_SECONDS_IN_MS ? FAST_REFRESH_MS + jitter : SECOND_IN_MS; + timerRef.current = setTimeout(tick, nextDelay); + }, [setDuration, loading, startTime, endTime]); + + React.useEffect(() => { + cancelledRef.current = false; + tick(); + return () => { + cancelledRef.current = true; + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, [tick]); + + return durationMs; +}