Compare commits

...

1 Commits

Author SHA1 Message Date
Evan
bc30677690 fix(dashboard): apply auto-refresh interval in standalone mode
The dashboard auto-refresh timer is driven by the useHeaderAutoRefresh
hook, which only mounts inside DashboardHeader. When the header is hidden
(standalone HideNavAndTitle mode, ?standalone=1 reports, or hideTitle in
embedded dashboards) the header never renders, so the refresh interval
never starts and charts stop auto-refreshing.

Add a headless HeadlessAutoRefresh component that sources the same redux
state and runs the auto-refresh hook when the header is hidden, so the
configured refresh interval keeps working regardless of header visibility.
Exactly one instance of the timer runs: the header when visible, the
headless component otherwise.

Fixes #25970

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 02:04:49 -07:00
3 changed files with 186 additions and 1 deletions

View File

@@ -27,6 +27,7 @@ import { EmptyState, Loading } from '@superset-ui/core/components';
import { ErrorBoundary, BasicErrorAlert } from 'src/components';
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
import DashboardHeader from 'src/dashboard/components/Header';
import HeadlessAutoRefresh from 'src/dashboard/components/Header/HeadlessAutoRefresh';
import { Icons } from '@superset-ui/core/components/Icons';
import IconButton from 'src/dashboard/components/IconButton';
import { Droppable } from 'src/dashboard/components/dnd/DragDroppable';
@@ -515,7 +516,7 @@ const DashboardBuilder = () => {
const headerContent = useMemo(
() => (
<>
{!hideDashboardHeader && <DashboardHeader />}
{hideDashboardHeader ? <HeadlessAutoRefresh /> : <DashboardHeader />}
{showFilterBar &&
filterBarOrientation === FilterBarOrientation.Horizontal && (
<FilterBar

View File

@@ -0,0 +1,97 @@
/**
* 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 { render } from 'spec/helpers/testing-library';
import { AutoRefreshProvider } from 'src/dashboard/contexts/AutoRefreshContext';
import HeadlessAutoRefresh from './HeadlessAutoRefresh';
const mockUseHeaderAutoRefresh = jest.fn();
jest.mock('./useHeaderAutoRefresh', () => ({
useHeaderAutoRefresh: (props: unknown) => {
mockUseHeaderAutoRefresh(props);
return {
forceRefresh: jest.fn(),
handlePauseToggle: jest.fn(),
autoRefreshPauseOnInactiveTab: false,
setPauseOnInactiveTab: jest.fn(),
};
},
}));
const initialState = {
dashboardInfo: {
id: 42,
metadata: { timed_refresh_immune_slices: [7] },
common: { conf: { DASHBOARD_AUTO_REFRESH_MODE: 'fetch' } },
},
dashboardState: {
refreshFrequency: 30,
sliceIds: [1, 2, 3],
},
charts: {
1: { id: 1, chartUpdateStartTime: 0, chartUpdateEndTime: 0 },
2: { id: 2, chartUpdateStartTime: 0, chartUpdateEndTime: 0 },
},
};
beforeEach(() => {
mockUseHeaderAutoRefresh.mockClear();
});
test('drives the auto-refresh hook from redux state without a header', () => {
const { container } = render(
<AutoRefreshProvider>
<HeadlessAutoRefresh />
</AutoRefreshProvider>,
{ useRedux: true, initialState },
);
// The component renders nothing but must still start the refresh timer.
expect(container).toBeEmptyDOMElement();
expect(mockUseHeaderAutoRefresh).toHaveBeenCalledTimes(1);
expect(mockUseHeaderAutoRefresh).toHaveBeenCalledWith(
expect.objectContaining({
chartIds: [1, 2, 3],
dashboardId: 42,
refreshFrequency: 30,
timedRefreshImmuneSlices: [7],
autoRefreshMode: 'fetch',
isLoading: false,
}),
);
});
test('passes the redux refresh frequency through to the hook', () => {
render(
<AutoRefreshProvider>
<HeadlessAutoRefresh />
</AutoRefreshProvider>,
{
useRedux: true,
initialState: {
...initialState,
dashboardState: { ...initialState.dashboardState, refreshFrequency: 0 },
},
},
);
expect(mockUseHeaderAutoRefresh).toHaveBeenCalledWith(
expect.objectContaining({ refreshFrequency: 0 }),
);
});

View File

@@ -0,0 +1,87 @@
/**
* 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 { useMemo } from 'react';
import { bindActionCreators } from 'redux';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'src/dashboard/types';
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
import { onRefresh, setRefreshFrequency } from '../../actions/dashboardState';
import { logEvent } from '../../../logger/actions';
import { useHeaderAutoRefresh } from './useHeaderAutoRefresh';
/**
* Headless component that drives the dashboard auto-refresh timer when the
* dashboard header is not rendered (e.g. standalone mode or `hideTitle: true`
* in embedded dashboards). The auto-refresh logic lives in the header, so
* hiding the header would otherwise stop the refresh interval from ever
* starting. Rendering this component keeps the timer running independently of
* header visibility. It renders nothing.
*/
const HeadlessAutoRefresh = (): null => {
const dispatch = useDispatch();
const chartIds = useChartIds();
const dashboardId = useSelector((state: RootState) => state.dashboardInfo.id);
const refreshFrequency = useSelector(
(state: RootState) => state.dashboardState.refreshFrequency ?? 0,
);
const timedRefreshImmuneSlices = useSelector(
(state: RootState) =>
state.dashboardInfo.metadata?.timed_refresh_immune_slices || [],
);
const autoRefreshMode = useSelector(
(state: RootState) =>
state.dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_MODE,
);
const isLoading = useSelector((state: RootState) =>
Object.values(state.charts).some(chart => {
const start = chart.chartUpdateStartTime ?? 0;
const end = chart.chartUpdateEndTime ?? 0;
return start > end;
}),
);
const boundActionCreators = useMemo(
() =>
bindActionCreators(
{
onRefresh,
setRefreshFrequency,
logEvent,
},
dispatch,
),
[dispatch],
);
useHeaderAutoRefresh({
chartIds,
dashboardId,
refreshFrequency,
timedRefreshImmuneSlices,
autoRefreshMode,
isLoading,
onRefresh: boundActionCreators.onRefresh,
setRefreshFrequency: boundActionCreators.setRefreshFrequency,
logEvent: boundActionCreators.logEvent,
});
return null;
};
export default HeadlessAutoRefresh;