Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
40a9c5f
perf(explore): use useDeferredValue for TextControl inputs
rusackas Oct 2, 2026
809caad
fix(explore): clear pending TextControl edit on external value reset
rusackas Oct 2, 2026
5de9239
fix(explore): debounce axis-title-margin side effect against transien…
rusackas Oct 15, 2026
97c5250
test(explore): commit the title clear separately from the retype in m…
rusackas Oct 16, 2026
01daa45
fix(explore): read latest controls in axis-title margin timers; tight…
rusackas Oct 16, 2026
ae6d034
fix(explore): cancel pending axis-title margin adjustment when margin…
rusackas Oct 20, 2026
8d51019
test(explore): type store state in ExploreViewContainer test
rusackas Oct 20, 2026
abe3afa
test(explore): pin the axis-title margin timer guards and cover both …
rusackas Oct 21, 2026
5fdfb5f
test(explore): cover axis-title margin timer cleanup on unmount
rusackas Oct 22, 2026
3749a82
fix(explore): keep a trailing debounce on TextControl onChange
rusackas Oct 28, 2026
4a65d0c
Merge remote-tracking branch 'origin/master' into perf/text-control-u…
rusackas Oct 29, 2026
c5fcb03
Merge remote-tracking branch 'origin/master' into perf/text-control-u…
rusackas Oct 29, 2026
cbe065c
fix(explore): emit latest typed value on TextControl blur; test pendi…
rusackas Oct 29, 2026
0d09f61
fix(explore): drop stale queued TextControl emit on blur after extern…
rusackas Oct 29, 2026
05a28a1
fix(explore): skip superseded TextControl emit and pin blur/deferred-…
rusackas Oct 29, 2026
59b6e49
fix(explore): re-queue newest TextControl value when a superseded emi…
rusackas Oct 30, 2026
3944c66
Update superset-frontend/src/explore/components/ExploreViewContainer/…
rusackas Oct 30, 2026
937f12e
Merge remote-tracking branch 'origin/master' into HEAD
rusackas Oct 31, 2026
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 @@ -1296,20 +1296,16 @@ test('automatic axis title margin adjustment changes X axis margin when title is
exploreActions.setControlValue('x_axis_title', 'X Axis Label'),
);

// Wait a bit to ensure useEffect has run
// Should call setControlValue since margin is less than 30, once the
// debounced margin effect settles
await waitFor(() => {
expect(
screen.queryByTestId('query-error-tooltip-trigger'),
).not.toBeInTheDocument();
expect(setControlValueSpy).toHaveBeenCalledWith(
'x_axis_title_margin',
30,
undefined,
{ programmatic: true },
);
});

// Should call setControlValue since margin is less than 30
expect(setControlValueSpy).toHaveBeenCalledWith(
'x_axis_title_margin',
30,
undefined,
{ programmatic: true },
);
} finally {
getChartControlPanelRegistry().remove('table');
jest.restoreAllMocks();
Expand Down Expand Up @@ -1400,20 +1396,16 @@ test('automatic axis title margin adjustment changes Y axis margin when title is
exploreActions.setControlValue('y_axis_title', 'Y Axis Label'),
);

// Wait a bit to ensure useEffect has run
// Should call setControlValue since margin is less than 30, once the
// debounced margin effect settles
await waitFor(() => {
expect(
screen.queryByTestId('query-error-tooltip-trigger'),
).not.toBeInTheDocument();
expect(setControlValueSpy).toHaveBeenCalledWith(
'y_axis_title_margin',
30,
undefined,
{ programmatic: true },
);
});

// Should call setControlValue since margin is less than 30
expect(setControlValueSpy).toHaveBeenCalledWith(
'y_axis_title_margin',
30,
undefined,
{ programmatic: true },
);
} finally {
getChartControlPanelRegistry().remove('table');
jest.restoreAllMocks();
Expand Down Expand Up @@ -1733,6 +1725,285 @@ test('does not request datasource metadata when an unrelated control changes', (
}
});

type Axis = 'x' | 'y';

// Explore state for a Table chart whose `<axis>_axis_title` and
// `<axis>_axis_title_margin` controls start at the given values.
const axisTitleState = (axis: Axis, title: string, margin: number) => ({
...reduxState,
explore: {
...reduxState.explore,
form_data: {
datasource: '1__table',
viz_type: VizType.Table,
metrics: [],
},
controls: {
...reduxState.explore.controls,
[`${axis}_axis_title`]: { value: title },
[`${axis}_axis_title_margin`]: { value: margin },
},
},
});

// The arguments of a programmatic margin write made by the container itself,
// as opposed to the dispatches the tests make.
const programmaticMarginWrite = (axis: Axis) => [
`${axis}_axis_title_margin`,
expect.anything(),
undefined,
{ programmatic: true },
];

const controlValue = (store: Store, name: string) =>
(
store.getState() as unknown as {
explore: { controls: Record<string, { value: unknown }> };
}
).explore.controls[name].value;

const marginIn = (store: Store, axis: Axis) =>
controlValue(store, `${axis}_axis_title_margin`);

test.each<Axis>(['x', 'y'])(
'automatic %s axis title margin adjustment ignores a transient empty value from a quick clear-and-retype',
async axis => {
setupTableChartControlPanel();
try {
const setControlValueSpy = jest.spyOn(exploreActions, 'setControlValue');

// a custom margin above the default 30
const initialState = axisTitleState(axis, 'Axis Label', 75);
const store = createStore(initialState, reducerIndex);
renderWithRouter({ initialState, store: store as Store });

// Clear any calls from initial render
setControlValueSpy.mockClear();

// A transient axis title -- e.g. one cleared and then replaced, or one
// flushed on blur mid-edit -- can briefly pass an empty value through
// here before the retyped value settles. The margin effect must react
// to the settled value only, not this transient one, or it would zero
// out a custom margin and then bump it back to 30, losing the original
// 75.
// Commit the clear on its own so the margin effect actually sees the
// empty value, then retype inside the debounce window.
act(() => {
store.dispatch(
exploreActions.setControlValue(`${axis}_axis_title`, ''),
);
});
act(() => {
store.dispatch(
exploreActions.setControlValue(
`${axis}_axis_title`,
'Axis Label Redone',
),
);
});

// Give the debounced margin effect more than its window to settle.
await new Promise(resolve => setTimeout(resolve, 300));

expect(setControlValueSpy).not.toHaveBeenCalledWith(
...programmaticMarginWrite(axis),
);
expect(marginIn(store, axis)).toBe(75);
} finally {
getChartControlPanelRegistry().remove('table');
jest.restoreAllMocks();
}
},
);

test.each<Axis>(['x', 'y'])(
'automatic %s axis title margin adjustment yields to a margin picked inside the debounce window',
async axis => {
setupTableChartControlPanel();
try {
const setControlValueSpy = jest.spyOn(exploreActions, 'setControlValue');

const initialState = axisTitleState(axis, '', 0);
const store = createStore(initialState, reducerIndex);
renderWithRouter({ initialState, store: store as Store });
setControlValueSpy.mockClear();

act(() => {
store.dispatch(
exploreActions.setControlValue(`${axis}_axis_title`, 'Axis Label'),
);
});
// the user picks a margin before the adjustment timer fires
act(() => {
store.dispatch(
exploreActions.setControlValue(`${axis}_axis_title_margin`, 15),
);
});

await new Promise(resolve => setTimeout(resolve, 300));

expect(setControlValueSpy).not.toHaveBeenCalledWith(
...programmaticMarginWrite(axis),
);
expect(marginIn(store, axis)).toBe(15);
} finally {
getChartControlPanelRegistry().remove('table');
jest.restoreAllMocks();
}
},
);

test.each<Axis>(['x', 'y'])(
'automatic %s axis title margin adjustment is dropped when the viz type changes inside the debounce window',
async axis => {
setupTableChartControlPanel();
// A second viz with the same axis title and margin controls, so the
// title carries over and the margin stays 0 across the switch. Only the
// viz type snapshot then tells the pending adjustment it no longer
// applies; a viz whose margin differs is already caught by the margin
// snapshot.
getChartControlPanelRegistry().registerValue('fake_viz', {
controlPanelSections: [
{
label: 'Chart Options',
expanded: true,
controlSetRows: [
[
{
name: `${axis}_axis_title`,
config: { type: 'TextControl', label: 'Title', default: '' },
},
],
[
{
name: `${axis}_axis_title_margin`,
config: {
type: 'TextControl',
label: 'Margin',
isInt: true,
default: 0,
renderTrigger: true,
},
},
],
],
},
],
});
try {
const setControlValueSpy = jest.spyOn(exploreActions, 'setControlValue');

const initialState = axisTitleState(axis, '', 0);
const store = createStore(initialState, reducerIndex);
renderWithRouter({ initialState, store: store as Store });
setControlValueSpy.mockClear();

act(() => {
store.dispatch(
exploreActions.setControlValue(`${axis}_axis_title`, 'Axis Label'),
);
});
// switch viz before the adjustment timer fires
act(() => {
store.dispatch(exploreActions.setControlValue('viz_type', 'fake_viz'));
});
expect(controlValue(store, `${axis}_axis_title`)).toBe('Axis Label');
expect(marginIn(store, axis)).toBe(0);

await new Promise(resolve => setTimeout(resolve, 300));

// the adjustment was scheduled against the Table chart, not this one
expect(setControlValueSpy).not.toHaveBeenCalledWith(
...programmaticMarginWrite(axis),
);
expect(marginIn(store, axis)).toBe(0);
} finally {
getChartControlPanelRegistry().remove('table');
getChartControlPanelRegistry().remove('fake_viz');
jest.restoreAllMocks();
}
},
);

test.each<Axis>(['x', 'y'])(
'automatic %s axis title margin adjustment restarts its window on every title edit',
async axis => {
setupTableChartControlPanel();
jest.useFakeTimers();
try {
const setControlValueSpy = jest.spyOn(exploreActions, 'setControlValue');

const initialState = axisTitleState(axis, 'Axis Label', 75);
const store = createStore(initialState, reducerIndex);
renderWithRouter({ initialState, store: store as Store });
setControlValueSpy.mockClear();

const setTitle = (title: string) =>
act(() => {
store.dispatch(
exploreActions.setControlValue(`${axis}_axis_title`, title),
);
});

// Each edit must restart the window. If it did not, the timer from
// the first edit would fire at 250ms, see the transient empty title
// from the clear at 200ms, and zero the custom margin.
setTitle('Axis Label Edited');
act(() => jest.advanceTimersByTime(200));
setTitle('');
act(() => jest.advanceTimersByTime(100));
setTitle('Axis Label Redone');
// past the first timer's deadline and past the restarted window
act(() => jest.advanceTimersByTime(600));

expect(setControlValueSpy).not.toHaveBeenCalledWith(
...programmaticMarginWrite(axis),
);
expect(marginIn(store, axis)).toBe(75);
} finally {
jest.useRealTimers();
getChartControlPanelRegistry().remove('table');
jest.restoreAllMocks();
}
},
);

test.each<Axis>(['x', 'y'])(
'automatic %s axis title margin adjustment is cancelled when the container unmounts inside the debounce window',
async axis => {
setupTableChartControlPanel();
jest.useFakeTimers();
try {
const setControlValueSpy = jest.spyOn(exploreActions, 'setControlValue');

const initialState = axisTitleState(axis, '', 0);
const store = createStore(initialState, reducerIndex);
const { unmount } = renderWithRouter({
initialState,
store: store as Store,
});
setControlValueSpy.mockClear();

act(() => {
store.dispatch(
exploreActions.setControlValue(`${axis}_axis_title`, 'Axis Label'),
);
});
// navigate away before the adjustment timer fires
unmount();
act(() => jest.advanceTimersByTime(600));

expect(setControlValueSpy).not.toHaveBeenCalledWith(
...programmaticMarginWrite(axis),
);
expect(marginIn(store, axis)).toBe(0);
} finally {
jest.useRealTimers();
getChartControlPanelRegistry().remove('table');
jest.restoreAllMocks();
}
},
);
test('document.title falls back to the app name on unmount, not the title present at mount', async () => {
// A direct page load of an explore URL arrives with the chart name already
// in <title>, so restoring the title seen at mount leaves the stale name in
Expand Down
Loading
Loading