Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import { StrictMode, useState } from 'react';
import { act, render, screen, waitFor } from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
import { storeWithState } from 'spec/fixtures/mockStore';
import mockState from 'spec/fixtures/mockState';
import { sliceId } from 'spec/fixtures/mockChartQueries';
import { ChartCustomizationType, NativeFilterType } from '@superset-ui/core';
import {
ChartCustomizationType,
NativeFilterType,
CategoricalColorNamespace,
} from '@superset-ui/core';
import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
import { applyDashboardLabelsColorOnLoad } from 'src/dashboard/actions/dashboardState';
import { dashboardInfoChanged } from 'src/dashboard/actions/dashboardInfo';
import { CHART_TYPE } from '../../util/componentTypes';
import DashboardContainer from './DashboardContainer';
import * as nativeFiltersActions from '../../actions/nativeFilters';
Expand All @@ -37,7 +45,7 @@ jest.mock('@visx/responsive', () => ({

jest.mock('src/dashboard/containers/DashboardGrid', () => ({
__esModule: true,
default: () => <div data-test="mock-dashboard-grid" />,
default: jest.fn(() => <div data-test="mock-dashboard-grid" />),
}));

// DashboardContainer dispatches these on mount, so unit tests stub them.
Expand Down Expand Up @@ -154,6 +162,12 @@ beforeEach(() => {

afterEach(() => {
setInScopeStatusMock.mockRestore();
jest
.mocked(applyDashboardLabelsColorOnLoad)
.mockImplementation(() => async () => {});
jest
.mocked(DashboardGrid)
.mockImplementation(() => <div data-test="mock-dashboard-grid" />);
});

test('calculates chartsInScope correctly for filters', async () => {
Expand Down Expand Up @@ -853,3 +867,128 @@ test('does not dispatch setInScopeStatusOfCustomizations when chart_customizatio
spy.mockRestore();
}
});

test.each([false, true])(
'applies custom label colors before a cached chart consumes its first color scale (StrictMode=%s)',
strict => {
const namespace = 'initial-label-colors';
CategoricalColorNamespace.getNamespace(namespace).resetColors();
jest
.mocked(applyDashboardLabelsColorOnLoad)
.mockImplementation(
jest.requireActual('src/dashboard/actions/dashboardState')
.applyDashboardLabelsColorOnLoad,
);
jest.mocked(DashboardGrid).mockImplementation(function CachedChart() {
const [color] = useState(() =>
CategoricalColorNamespace.getScale(undefined, namespace).getColor(
'20_Passed',
),
);
return <div data-test="cached-chart" data-color={color} />;
});

const initialState = createTestState({
dashboardInfo: {
...mockState.dashboardInfo,
metadata: {
...mockState.dashboardInfo.metadata,
color_namespace: namespace,
color_scheme: '',
label_colors: { '20_Passed': '#008000' },
},
},
});

const { unmount } = render(
strict ? (
<StrictMode>
<DashboardContainer />
</StrictMode>
) : (
<DashboardContainer />
),
{ useRedux: true, store: storeWithState(initialState) },
);

expect(screen.getByTestId('cached-chart')).toHaveAttribute(
'data-color',
'#008000',
);
unmount();
CategoricalColorNamespace.getNamespace(namespace).resetColors();
},
);

test.each([false, true])(
'initializes the next dashboard colors before mounting its cached charts (new namespace=%s)',
newNamespace => {
const namespace = 'navigation-label-colors';
let chartNamespace = namespace;
CategoricalColorNamespace.getNamespace(namespace).resetColors();
jest
.mocked(applyDashboardLabelsColorOnLoad)
.mockImplementation(
jest.requireActual('src/dashboard/actions/dashboardState')
.applyDashboardLabelsColorOnLoad,
);
jest.mocked(DashboardGrid).mockImplementation(function CachedChart() {
const [color] = useState(() =>
CategoricalColorNamespace.getScale(undefined, chartNamespace).getColor(
'20_Passed',
),
);
return <div data-test="cached-chart" data-color={color} />;
});
const metadata = {
...mockState.dashboardInfo.metadata,
color_namespace: namespace,
color_scheme: '',
label_colors: { '20_Passed': '#008000' },
};
const { store, unmount } = setupWithStore({
dashboardInfo: { ...mockState.dashboardInfo, metadata },
});
expect(screen.getByTestId('cached-chart')).toHaveAttribute(
'data-color',
'#008000',
);

chartNamespace = newNamespace ? 'next-dashboard-colors' : namespace;
act(() => {
store.dispatch(
dashboardInfoChanged({
id: mockState.dashboardInfo.id + 1,
metadata: {
...store.getState().dashboardInfo.metadata,
...metadata,
color_namespace: chartNamespace,
label_colors: { '20_Passed': '#ff0000' },
},
}),
);
});

expect(screen.getByTestId('cached-chart')).toHaveAttribute(
'data-color',
'#ff0000',
);
unmount();
CategoricalColorNamespace.getNamespace(namespace).resetColors();
CategoricalColorNamespace.getNamespace(chartNamespace).resetColors();
},
);

test('waits for dashboard hydration before mounting cached charts', () => {
jest.mocked(applyDashboardLabelsColorOnLoad).mockClear();
const { store } = setupWithStore({ dashboardInfo: {} });
expect(screen.queryByTestId('mock-dashboard-grid')).not.toBeInTheDocument();
expect(applyDashboardLabelsColorOnLoad).not.toHaveBeenCalled();

act(() => {
store.dispatch(dashboardInfoChanged({ id: mockState.dashboardInfo.id }));
});

expect(applyDashboardLabelsColorOnLoad).toHaveBeenCalled();
expect(screen.getByTestId('mock-dashboard-grid')).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,10 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {

const renderedChartIds = useRenderedChartIds();

const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
useState(false);
const [colorInitializedDashboardId, setColorInitializedDashboardId] =
useState<number | null>(null);
const dashboardLabelsColorInitiated =
colorInitializedDashboardId === dashboardInfo?.id;
const prevRenderedChartIds = useRef<number[]>([]);
const prevTabIndexRef = useRef<number>();
const prevFilterScopesRef = useRef<FilterScopeData[]>([]);
Expand Down Expand Up @@ -304,7 +306,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
if (dashboardInfo?.id && !dashboardLabelsColorInitiated) {
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
// apply labels color as dictated by stored metadata (if any)
setDashboardLabelsColorInitiated(true);
setColorInitializedDashboardId(dashboardInfo.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, just a note for the description: I think this line is the load-bearing half of the fix, and the summary currently reads as if the first-render race were the main story.

I ran both branches side by side. With DashboardContainer mounted and dashboardInfo.id changing in place, this branch dispatches applyDashboardLabelsColorOnLoad a second time with the new label_colors; on 61fffbe0c0 it is never dispatched again, because dashboardLabelsColorInitiated is already true. The effect cleanup has meanwhile run onBeforeUnload, which calls resetColors() on the namespace, so on master the second dashboard keeps palette colors for the rest of the session until a full reload. That matches the reporter's After refreshing or clicking legend, the colors become correct in #40708, and src/pages/Dashboard/index.tsx:25 renders DashboardPage with no key, so in-app navigation really does swap dashboardInfo.id under a mounted container.

For the first-render half I could not build a cold-load repro: hydrate.ts:174 seeds every chart from the initial chart state with queriesResponse: null, and DashboardPage.tsx:423 only mounts the container once dashboardInfo is populated, so no chart owns a query response at that first commit on a fresh page load. Am I missing a path there? Either way, calling the navigation case out in the description would make this easier to justify.

}

return () => {
Expand Down Expand Up @@ -379,7 +381,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {

return (
<div className="grid-container" data-test="grid-container" ref={parentRef}>
{renderParentSizeChildren({ width })}
{/* Defer the grid until hydration and color initialization complete,
before cached charts consume their color scales on first render. */}
{dashboardLabelsColorInitiated && renderParentSizeChildren({ width })}
</div>
);
};
Expand Down
Loading