Compare commits

...

2 Commits

Author SHA1 Message Date
Joe Li
dc0de0e248 fix(embedded): respect show_filters param in standalone report mode
standalone=3 (Report mode) unconditionally hid the filter bar, breaking
embedded dashboards that set dashboardUIConfig.filters.visible = true.
Now show_filters=true overrides the Report mode hiding while preserving
the default behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:51:35 -07:00
Joe Li
4b7283e3b8 fix(pivot-table): guard convertToNumberIfNumeric against non-string Date values
When temporal columns are used in pivot table Rows/Columns, Date objects
can reach convertToNumberIfNumeric at runtime despite the string[] type
declaration (due to any-typed boundaries in the data pipeline). Calling
.trim() on a Date object throws TypeError: t.trim is not a function,
crashing the chart entirely.

Widen the parameter type to unknown and add a type guard that returns
non-string values unchanged, letting the date formatter receive them
directly. Also update makeColPivotSettings test helper to accept unknown
and add regression tests for the Date object passthrough paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:51:49 -07:00
4 changed files with 165 additions and 6 deletions

View File

@@ -271,7 +271,10 @@ function sortHierarchicalObject(
return result;
}
function convertToNumberIfNumeric(value: string): string | number {
function convertToNumberIfNumeric(value: unknown): unknown {
if (typeof value !== 'string') {
return value;
}
const n = Number(value);
return value.trim() !== '' && !Number.isNaN(n) ? n : value;
}

View File

@@ -1052,7 +1052,7 @@ test('renderTableRow uses active header surface for adaptive contrast', () => {
});
function makeColPivotSettings(
value: string,
value: unknown,
): Parameters<TableRenderer['renderColHeaderRow']>[2] {
return {
rowAttrs: [],
@@ -1146,3 +1146,45 @@ test.each([
expect(formatter).toHaveBeenCalledWith(expected);
},
);
test('col header date formatter does not throw when value is a Date object', () => {
const dateValue = new Date(1700000000000);
const formatter = jest.fn().mockReturnValue('formatted');
tableRenderer = new TableRenderer({
...mockProps,
cols: ['event_time'],
tableOptions: {
...mockProps.tableOptions,
dateFormatters: { event_time: formatter },
},
});
expect(() =>
tableRenderer.renderColHeaderRow(
'event_time',
0,
makeColPivotSettings(dateValue),
),
).not.toThrow();
expect(formatter).toHaveBeenCalledWith(dateValue);
});
test('row header date formatter does not throw when value is a Date object', () => {
const dateValue = new Date(1700000000000);
const formatter = jest.fn().mockReturnValue('formatted');
tableRenderer = new TableRenderer({
...mockProps,
rows: ['event_time'],
tableOptions: {
...mockProps.tableOptions,
dateFormatters: { event_time: formatter },
},
});
expect(() =>
tableRenderer.renderTableRow(
[dateValue as unknown as string],
0,
makeRowPivotSettings(),
),
).not.toThrow();
expect(formatter).toHaveBeenCalledWith(dateValue);
});

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
/// <reference types="@emotion/jest" />
import fetchMock from 'fetch-mock';
import {
fireEvent,
@@ -42,6 +43,7 @@ import {
import { storeWithState } from 'spec/fixtures/mockStore';
import mockState from 'spec/fixtures/mockState';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import * as urlUtils from 'src/utils/urlUtils';
import * as useNativeFiltersModule from './state';
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
@@ -456,6 +458,116 @@ describe('DashboardBuilder', () => {
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
});
test('should hide filter panel in standalone=3 report mode', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId } = setup();
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).toHaveStyleRule('display', 'none');
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
test('should show filter panel in standalone=3 when show_filters=true', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
if (param.name === 'show_filters') return true;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId } = setup();
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).not.toHaveStyleRule('display', 'none');
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
test('should hide filter panel in standalone=3 when show_filters=false', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
if (param.name === 'show_filters') return false;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId } = setup();
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).toHaveStyleRule('display', 'none');
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
test('should show filters but hide tab navigation in report mode with show_filters=true on tabbed dashboard', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
if (param.name === 'show_filters') return true;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId, queryByRole } = setup({
dashboardLayout: undoableDashboardLayoutWithTabs,
});
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).not.toHaveStyleRule('display', 'none');
expect(queryByRole('tablist')).not.toBeInTheDocument();
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
});
test('should render ParentSize wrapper with height 100% for tabs', async () => {

View File

@@ -414,6 +414,8 @@ const DashboardBuilder = () => {
: undefined;
const standaloneMode = getUrlParam(URL_PARAMS.standalone);
const isReport = standaloneMode === DashboardStandaloneMode.Report;
const showFiltersUrlParam = getUrlParam(URL_PARAMS.showFilters);
const hideFilterBar = isReport && showFiltersUrlParam !== true;
const hideDashboardHeader =
uiConfig.hideTitle ||
standaloneMode === DashboardStandaloneMode.HideNavAndTitle ||
@@ -511,12 +513,12 @@ const DashboardBuilder = () => {
filterBarOrientation === FilterBarOrientation.Horizontal && (
<FilterBar
orientation={FilterBarOrientation.Horizontal}
hidden={isReport}
hidden={hideFilterBar}
/>
)}
</>
),
[hideDashboardHeader, showFilterBar, filterBarOrientation, isReport],
[hideDashboardHeader, showFilterBar, filterBarOrientation, hideFilterBar],
);
const renderDraggableContent = useCallback(
@@ -578,7 +580,7 @@ const DashboardBuilder = () => {
return (
<FiltersPanel
width={filterBarWidth}
hidden={isReport}
hidden={hideFilterBar}
data-test="dashboard-filters-panel"
>
<StickyPanel ref={containerRef} width={filterBarWidth}>
@@ -603,7 +605,7 @@ const DashboardBuilder = () => {
toggleDashboardFiltersOpen,
filterBarHeight,
filterBarOffset,
isReport,
hideFilterBar,
],
);