diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx
index 87011ae4c73c..c9240a4526ac 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Fragment, useCallback, memo } from 'react';
+import { Fragment, useCallback, memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
@@ -24,7 +24,8 @@ import { t } from '@superset-ui/core';
import { styled } from '@apache-superset/core/ui';
import { EditableTitle, EmptyState } from '@superset-ui/core/components';
-import { setEditMode } from 'src/dashboard/actions/dashboardState';
+import { setEditMode, onRefresh } from 'src/dashboard/actions/dashboardState';
+import getChartIdsFromComponent from 'src/dashboard/util/getChartIdsFromComponent';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import AnchorLink from 'src/dashboard/components/AnchorLink';
import {
@@ -37,6 +38,9 @@ import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
export const RENDER_TAB = 'RENDER_TAB';
export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
+// Delay before refreshing charts to ensure they are fully mounted
+const CHART_MOUNT_DELAY = 100;
+
const propTypes = {
dashboardId: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
@@ -64,6 +68,7 @@ const propTypes = {
handleComponentDrop: PropTypes.func.isRequired,
updateComponents: PropTypes.func.isRequired,
setDirectPathToChild: PropTypes.func.isRequired,
+ isComponentVisible: PropTypes.bool,
};
const defaultProps = {
@@ -114,6 +119,43 @@ const renderDraggableContent = dropProps =>
const Tab = props => {
const dispatch = useDispatch();
const canEdit = useSelector(state => state.dashboardInfo.dash_edit_perm);
+ const dashboardLayout = useSelector(state => state.dashboardLayout.present);
+ const lastRefreshTime = useSelector(
+ state => state.dashboardState.lastRefreshTime,
+ );
+ const tabActivationTime = useSelector(
+ state => state.dashboardState.tabActivationTimes?.[props.id] || 0,
+ );
+ const dashboardInfo = useSelector(state => state.dashboardInfo);
+
+ useEffect(() => {
+ if (props.renderType === RENDER_TAB_CONTENT && props.isComponentVisible) {
+ if (
+ lastRefreshTime &&
+ tabActivationTime &&
+ lastRefreshTime > tabActivationTime
+ ) {
+ const chartIds = getChartIdsFromComponent(props.id, dashboardLayout);
+ if (chartIds.length > 0) {
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ dispatch(onRefresh(chartIds, true, 0, dashboardInfo.id));
+ }, CHART_MOUNT_DELAY);
+ });
+ }
+ }
+ }
+ }, [
+ props.isComponentVisible,
+ props.renderType,
+ props.id,
+ lastRefreshTime,
+ tabActivationTime,
+ dashboardLayout,
+ dashboardInfo.id,
+ dispatch,
+ ]);
+
const handleChangeTab = useCallback(
({ pathToTabIndex }) => {
props.setDirectPathToChild(pathToTabIndex);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx
index 3ff5f2879783..a5072293ada2 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.test.tsx
@@ -26,11 +26,15 @@ import {
} from 'spec/helpers/testing-library';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import { EditableTitle } from '@superset-ui/core/components';
-import { setEditMode } from 'src/dashboard/actions/dashboardState';
+import { setEditMode, onRefresh } from 'src/dashboard/actions/dashboardState';
import Tab from './Tab';
import Markdown from '../Markdown';
+jest.mock('src/dashboard/util/getChartIdsFromComponent', () =>
+ jest.fn(() => []),
+);
+
jest.mock('src/dashboard/containers/DashboardComponent', () =>
jest.fn(() =>
),
);
@@ -66,6 +70,9 @@ jest.mock('src/dashboard/actions/dashboardState', () => ({
setEditMode: jest.fn(() => ({
type: 'SET_EDIT_MODE',
})),
+ onRefresh: jest.fn(() => ({
+ type: 'ON_REFRESH',
+ })),
}));
const createProps = () => ({
@@ -445,3 +452,91 @@ test('AnchorLink does not render in embedded mode', () => {
expect(screen.queryByTestId('anchor-link')).not.toBeInTheDocument();
});
+
+test('Should refresh charts when tab becomes active after dashboard refresh', async () => {
+ jest.clearAllMocks();
+ const getChartIdsFromComponent = require('src/dashboard/util/getChartIdsFromComponent');
+ getChartIdsFromComponent.mockReturnValue([101, 102]);
+
+ const props = createProps();
+ props.renderType = 'RENDER_TAB_CONTENT';
+ props.isComponentVisible = false;
+
+ const initialState = {
+ dashboardState: {
+ lastRefreshTime: Date.now() - 5000, // Dashboard was refreshed 5 seconds ago
+ tabActivationTimes: {
+ 'TAB-YT6eNksV-': Date.now() - 10000, // Tab was activated 10 seconds ago (before refresh)
+ },
+ },
+ dashboardInfo: {
+ id: 23,
+ dash_edit_perm: true,
+ },
+ };
+
+ const { rerender } = render(, {
+ useRedux: true,
+ useDnd: true,
+ initialState,
+ });
+
+ // onRefresh should not be called when tab is not visible
+ expect(onRefresh).not.toHaveBeenCalled();
+
+ // Make tab visible - this should trigger refresh since lastRefreshTime > tabActivationTime
+ rerender();
+
+ // Wait for the refresh to be triggered after the delay
+ await waitFor(
+ () => {
+ expect(onRefresh).toHaveBeenCalled();
+ },
+ { timeout: 500 },
+ );
+
+ expect(onRefresh).toHaveBeenCalledWith(
+ [101, 102], // Chart IDs from the tab
+ true, // Force refresh
+ 0, // Interval
+ 23, // Dashboard ID
+ );
+});
+
+test('Should not refresh charts when tab becomes active if no dashboard refresh occurred', async () => {
+ jest.clearAllMocks();
+ const getChartIdsFromComponent = require('src/dashboard/util/getChartIdsFromComponent');
+ getChartIdsFromComponent.mockReturnValue([101]);
+
+ const props = createProps();
+ props.renderType = 'RENDER_TAB_CONTENT';
+ props.isComponentVisible = false;
+
+ const currentTime = Date.now();
+ const initialState = {
+ dashboardState: {
+ lastRefreshTime: currentTime - 10000, // Dashboard was refreshed 10 seconds ago
+ tabActivationTimes: {
+ 'TAB-YT6eNksV-': currentTime - 5000, // Tab was activated 5 seconds ago (after refresh)
+ },
+ },
+ dashboardInfo: {
+ id: 23,
+ dash_edit_perm: true,
+ },
+ };
+
+ const { rerender } = render(, {
+ useRedux: true,
+ useDnd: true,
+ initialState,
+ });
+
+ // Make tab visible - should NOT trigger refresh since tabActivationTime > lastRefreshTime
+ rerender();
+
+ // Wait a bit to ensure no refresh is triggered
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ expect(onRefresh).not.toHaveBeenCalled();
+});
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js
index 9336d5638826..e24f875022f6 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.js
@@ -59,7 +59,17 @@ import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export default function dashboardStateReducer(state = {}, action) {
const actionHandlers = {
[HYDRATE_DASHBOARD]() {
- return { ...state, ...action.data.dashboardState };
+ const hydratedState = { ...state, ...action.data.dashboardState };
+ // Initialize tab activation times for initially active tabs
+ if (hydratedState.activeTabs && hydratedState.activeTabs.length > 0) {
+ const now = Date.now();
+ hydratedState.tabActivationTimes =
+ hydratedState.tabActivationTimes || {};
+ hydratedState.activeTabs.forEach(tabId => {
+ hydratedState.tabActivationTimes[tabId] = now;
+ });
+ }
+ return hydratedState;
},
[ADD_SLICE]() {
const updatedSliceIds = new Set(state.sliceIds);
@@ -181,6 +191,7 @@ export default function dashboardStateReducer(state = {}, action) {
return {
...state,
isRefreshing: true,
+ lastRefreshTime: Date.now(),
};
},
[ON_FILTERS_REFRESH]() {
@@ -216,10 +227,17 @@ export default function dashboardStateReducer(state = {}, action) {
.difference(new Set(action.activeTabs))
.union(new Set(action.inactiveTabs));
+ // Track when each tab was last activated
+ const tabActivationTimes = { ...state.tabActivationTimes };
+ action.activeTabs.forEach(tabId => {
+ tabActivationTimes[tabId] = Date.now();
+ });
+
return {
...state,
inactiveTabs: Array.from(newInactiveTabs),
activeTabs: Array.from(newActiveTabs.union(new Set(action.activeTabs))),
+ tabActivationTimes,
};
},
[SET_ACTIVE_TABS]() {
diff --git a/superset-frontend/src/dashboard/util/charts/useAllChartIds.ts b/superset-frontend/src/dashboard/util/charts/useAllChartIds.ts
new file mode 100644
index 000000000000..dc6a48fb4592
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/charts/useAllChartIds.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 { useSelector } from 'react-redux';
+import { useMemo } from 'react';
+import { RootState } from 'src/dashboard/types';
+import getChartIdsFromLayout from '../getChartIdsFromLayout';
+
+export const useAllChartIds = () => {
+ const layout = useSelector(
+ (state: RootState) => state.dashboardLayout.present,
+ );
+ return useMemo(() => getChartIdsFromLayout(layout), [layout]);
+};
diff --git a/superset-frontend/src/dashboard/util/getChartIdsFromComponent.ts b/superset-frontend/src/dashboard/util/getChartIdsFromComponent.ts
new file mode 100644
index 000000000000..507550c76bc7
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/getChartIdsFromComponent.ts
@@ -0,0 +1,44 @@
+/**
+ * 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 { CHART_TYPE } from './componentTypes';
+import type { DashboardLayout } from '../types';
+
+export default function getChartIdsFromComponent(
+ componentId: string,
+ layout: DashboardLayout,
+): number[] {
+ const chartIds: number[] = [];
+ const component = layout[componentId];
+
+ if (!component) return chartIds;
+
+ // If this component is a chart, add its ID
+ if (component.type === CHART_TYPE && component.meta?.chartId) {
+ chartIds.push(component.meta.chartId);
+ }
+
+ // Recursively check children
+ if (component.children) {
+ component.children.forEach((childId: string) => {
+ chartIds.push(...getChartIdsFromComponent(childId, layout));
+ });
+ }
+
+ return chartIds;
+}