mirror of
https://github.com/apache/superset.git
synced 2026-07-09 00:05:36 +00:00
Compare commits
3 Commits
fix-databa
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0767674d3d | ||
|
|
d1b6a3d572 | ||
|
|
bc30677690 |
@@ -105,6 +105,15 @@ jest.mock('src/dashboard/containers/DashboardGrid', () => {
|
||||
MockDashboardGrid.displayName = 'MockDashboardGrid';
|
||||
return MockDashboardGrid;
|
||||
});
|
||||
// The real component renders null, so mock it with a visible marker to let
|
||||
// tests assert whether DashboardBuilder mounts it.
|
||||
jest.mock('src/dashboard/components/Header/HeadlessAutoRefresh', () => {
|
||||
const MockHeadlessAutoRefresh = () => (
|
||||
<div data-test="mock-headless-auto-refresh" />
|
||||
);
|
||||
MockHeadlessAutoRefresh.displayName = 'MockHeadlessAutoRefresh';
|
||||
return MockHeadlessAutoRefresh;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('DashboardBuilder', () => {
|
||||
@@ -183,6 +192,49 @@ describe('DashboardBuilder', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should mount HeadlessAutoRefresh when the header is hidden (?standalone=2)', () => {
|
||||
// Regression test for #25970: the auto-refresh timer lives in the header,
|
||||
// so hiding the header must swap in the headless driver — reverting the
|
||||
// conditional in DashboardBuilder would strand standalone dashboards with
|
||||
// no refresh timer at all.
|
||||
const originalHref = window.location.href;
|
||||
window.history.replaceState({}, '', '/?standalone=2');
|
||||
try {
|
||||
const { queryByTestId } = setup();
|
||||
expect(queryByTestId('mock-headless-auto-refresh')).toBeInTheDocument();
|
||||
expect(
|
||||
queryByTestId('dashboard-header-container'),
|
||||
).not.toBeInTheDocument();
|
||||
} finally {
|
||||
window.history.replaceState({}, '', originalHref);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not mount HeadlessAutoRefresh when the header is visible', () => {
|
||||
const { queryByTestId } = setup();
|
||||
expect(queryByTestId('dashboard-header-container')).toBeInTheDocument();
|
||||
expect(queryByTestId('mock-headless-auto-refresh')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not start any auto-refresh in report mode (?standalone=3)', () => {
|
||||
// Report mode drives one-shot screenshot renders (email reports,
|
||||
// thumbnails); a live refresh timer there could re-fetch charts
|
||||
// mid-capture, so neither the header nor the headless driver may mount.
|
||||
const originalHref = window.location.href;
|
||||
window.history.replaceState({}, '', '/?standalone=3');
|
||||
try {
|
||||
const { queryByTestId } = setup();
|
||||
expect(
|
||||
queryByTestId('dashboard-header-container'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
queryByTestId('mock-headless-auto-refresh'),
|
||||
).not.toBeInTheDocument();
|
||||
} finally {
|
||||
window.history.replaceState({}, '', originalHref);
|
||||
}
|
||||
});
|
||||
|
||||
test('should keep the DashboardHeader when standalone mode only hides nav (?standalone=1)', () => {
|
||||
// `?standalone=1` maps to DashboardStandaloneMode.HideNav, which only hides the
|
||||
// Flask-rendered global app menu (#app-menu) — it must NOT suppress the React-side
|
||||
|
||||
@@ -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';
|
||||
@@ -516,6 +517,10 @@ const DashboardBuilder = () => {
|
||||
() => (
|
||||
<>
|
||||
{!hideDashboardHeader && <DashboardHeader />}
|
||||
{/* Report mode is a one-shot screenshot render (reports, thumbnails),
|
||||
so it must never start a refresh timer that could re-fetch charts
|
||||
mid-capture. */}
|
||||
{hideDashboardHeader && !isReport && <HeadlessAutoRefresh />}
|
||||
{showFilterBar &&
|
||||
filterBarOrientation === FilterBarOrientation.Horizontal && (
|
||||
<FilterBar
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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,
|
||||
onRefresh: expect.any(Function),
|
||||
setRefreshFrequency: expect.any(Function),
|
||||
logEvent: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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 }),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 immuneSlices = useSelector(
|
||||
(state: RootState) =>
|
||||
state.dashboardInfo.metadata?.timed_refresh_immune_slices,
|
||||
);
|
||||
const timedRefreshImmuneSlices = useMemo(
|
||||
() => immuneSlices || [],
|
||||
[immuneSlices],
|
||||
);
|
||||
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;
|
||||
Reference in New Issue
Block a user