You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by en...@apache.org on 2025/11/13 21:14:07 UTC

(superset) branch master updated: fix(dashboard): refresh tabs as they load when dashboard is refreshed (#35265)

This is an automated email from the ASF dual-hosted git repository.

enzomartellucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 74a590cb76 fix(dashboard): refresh tabs as they load when dashboard is refreshed (#35265)
74a590cb76 is described below

commit 74a590cb76f52b21c1c655f30daaf1771539e997
Author: Mehmet Salih Yavuz <sa...@proton.me>
AuthorDate: Fri Nov 14 00:14:01 2025 +0300

    fix(dashboard): refresh tabs as they load when dashboard is refreshed (#35265)
---
 .../components/gridComponents/Tab/Tab.jsx          | 46 +++++++++-
 .../components/gridComponents/Tab/Tab.test.tsx     | 97 +++++++++++++++++++++-
 .../src/dashboard/reducers/dashboardState.js       | 20 ++++-
 .../src/dashboard/util/charts/useAllChartIds.ts    | 29 +++++++
 .../src/dashboard/util/getChartIdsFromComponent.ts | 44 ++++++++++
 5 files changed, 232 insertions(+), 4 deletions(-)

diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab/Tab.jsx
index 87011ae4c7..c9240a4526 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 3ff5f28797..a5072293ad 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(() => <div data-test="DashboardComponent" />),
 );
@@ -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(<Tab {...props} />, {
+    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(<Tab {...props} isComponentVisible />);
+
+  // 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(<Tab {...props} />, {
+    useRedux: true,
+    useDnd: true,
+    initialState,
+  });
+
+  // Make tab visible - should NOT trigger refresh since tabActivationTime > lastRefreshTime
+  rerender(<Tab {...props} isComponentVisible />);
+
+  // 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 9336d56388..e24f875022 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 0000000000..dc6a48fb45
--- /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 0000000000..507550c76b
--- /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;
+}