/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { FC, useMemo, useRef, useEffect, useState } from 'react'; import { css, useTheme } from '@apache-superset/core/theme'; import { AutoRefreshStatus } from '../../types/autoRefresh'; export interface StatusIndicatorDotProps { /** Current status to display */ status: AutoRefreshStatus; /** Size of the dot in pixels */ size?: number; } /** * Status indicator configuration mapping. * * - Green dot: Refreshed on schedule * - Blue dot: Fetching data or waiting for first refresh * - Yellow/warning dot: Delayed * - Red dot: Error * - White dot: Paused */ interface StatusConfig { color: string; needsBorder: boolean; outlineColor?: string; } export const getStatusConfig = ( theme: ReturnType, status: AutoRefreshStatus, ): StatusConfig => { switch (status) { case AutoRefreshStatus.Success: return { color: theme.colorSuccess, needsBorder: false, }; case AutoRefreshStatus.Idle: return { color: theme.colorInfo, needsBorder: false, }; case AutoRefreshStatus.Fetching: return { color: theme.colorInfo, needsBorder: false, }; case AutoRefreshStatus.Delayed: return { color: theme.colorWarning, needsBorder: false, }; case AutoRefreshStatus.Error: return { color: theme.colorError, needsBorder: false, }; case AutoRefreshStatus.Paused: return { color: theme.colorBgContainer, needsBorder: true, outlineColor: 'currentColor', }; default: return { color: theme.colorTextSecondary, needsBorder: false, }; } }; /** * A colored dot indicator that shows the auto-refresh status. * * Uses CSS transitions to prevent flickering between states. * The color change is animated smoothly rather than instantly. */ export const StatusIndicatorDot: FC = ({ status, size = 10, }) => { const theme = useTheme(); // Debounce rapid status changes to prevent flickering const [displayStatus, setDisplayStatus] = useState(status); const timerRef = useRef | null>(null); useEffect(() => { // Clear any pending timer if (timerRef.current) { clearTimeout(timerRef.current); } // For fetching state, update immediately to show user something is happening if (status === AutoRefreshStatus.Fetching) { setDisplayStatus(status); } else { // For other states, debounce to prevent flickering timerRef.current = setTimeout(() => { setDisplayStatus(status); }, 100); } return () => { if (timerRef.current) { clearTimeout(timerRef.current); } }; }, [status]); const statusConfig = useMemo( () => getStatusConfig(theme, displayStatus), [theme, displayStatus], ); const dotStyles = useMemo( () => css` display: inline-flex; align-items: center; justify-content: center; width: ${size}px; height: ${size}px; border-radius: 50%; color: ${theme.colorTextSecondary}; background-color: ${statusConfig.color}; transition: background-color ${theme.motionDurationMid} ease-in-out, border-color ${theme.motionDurationMid} ease-in-out; border: ${statusConfig.needsBorder ? '1px solid' : 'none'}; border-color: ${statusConfig.needsBorder ? statusConfig.outlineColor : 'transparent'}; box-shadow: ${statusConfig.needsBorder ? 'none' : `0 0 0 2px ${theme.colorBgContainer}`}; margin-left: ${theme.marginXS}px; margin-right: ${theme.marginXS}px; cursor: help; `, [statusConfig, size, theme], ); return ( ); }; export default StatusIndicatorDot;