mirror of
https://github.com/apache/superset.git
synced 2026-09-10 01:04:25 +00:00
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
/* eslint-disable camelcase */
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
import {
|
|
ControlPanelConfig,
|
|
ControlPanelsContainerProps,
|
|
ControlState,
|
|
CustomControlItem,
|
|
isCustomControlItem,
|
|
} from '@superset-ui/chart-controls';
|
|
import { QueryMode } from '@superset-ui/core';
|
|
import config from '../src/controlPanel';
|
|
|
|
type VisibilityFn = (
|
|
props: ControlPanelsContainerProps,
|
|
control?: ControlState,
|
|
) => boolean;
|
|
|
|
function isControlWithVisibility(
|
|
controlItem: unknown,
|
|
): controlItem is CustomControlItem & {
|
|
config: Required<CustomControlItem['config']> & { visibility: VisibilityFn };
|
|
} {
|
|
return (
|
|
typeof controlItem === 'object' &&
|
|
controlItem !== null &&
|
|
'name' in controlItem &&
|
|
'config' in controlItem &&
|
|
typeof (controlItem as CustomControlItem).config?.visibility === 'function'
|
|
);
|
|
}
|
|
|
|
function getVisibility(
|
|
panel: ControlPanelConfig,
|
|
controlName: string,
|
|
): VisibilityFn {
|
|
const item = (panel.controlPanelSections || [])
|
|
.flatMap(section => section?.controlSetRows || [])
|
|
.flat()
|
|
.find(c => isControlWithVisibility(c) && c.name === controlName);
|
|
|
|
if (!isControlWithVisibility(item)) {
|
|
throw new Error(`Control "${controlName}" with visibility not found`);
|
|
}
|
|
return item.config.visibility;
|
|
}
|
|
|
|
function mkProps(
|
|
groupbyValue: string[],
|
|
options = [
|
|
{ column_name: 'ORDERDATE', is_dttm: true },
|
|
{ column_name: 'some_other_col', is_dttm: false },
|
|
],
|
|
): ControlPanelsContainerProps {
|
|
return {
|
|
controls: {
|
|
groupby: { value: groupbyValue, options },
|
|
},
|
|
} as unknown as ControlPanelsContainerProps;
|
|
}
|
|
|
|
function withControls(
|
|
props: ControlPanelsContainerProps,
|
|
controls: Record<string, unknown>,
|
|
): ControlPanelsContainerProps {
|
|
return {
|
|
...props,
|
|
controls: { ...props.controls, ...controls },
|
|
} as unknown as ControlPanelsContainerProps;
|
|
}
|
|
|
|
test('time_grain_sqla visibility should be case-insensitive', () => {
|
|
const vis = getVisibility(config, 'time_grain_sqla');
|
|
const controlState = {} as ControlState;
|
|
|
|
expect(vis(mkProps(['orderdate']), controlState)).toBe(true);
|
|
expect(vis(mkProps(['ORDERDATE']), controlState)).toBe(true);
|
|
expect(vis(mkProps(['some_other_col']), controlState)).toBe(false);
|
|
});
|
|
|
|
test('time_grain_sqla is hidden in raw records mode', () => {
|
|
const vis = getVisibility(config, 'time_grain_sqla');
|
|
const controlState = {} as ControlState;
|
|
const temporalGroupby = mkProps(['ORDERDATE']);
|
|
|
|
expect(
|
|
vis(
|
|
withControls(temporalGroupby, {
|
|
query_mode: { value: QueryMode.Aggregate },
|
|
}),
|
|
controlState,
|
|
),
|
|
).toBe(true);
|
|
|
|
// `groupby` is kept when the query mode switches to raw records, both in the
|
|
// control state and in a saved chart's form data, so a temporal dimension on
|
|
// its own must not bring the control back.
|
|
expect(
|
|
vis(
|
|
withControls(temporalGroupby, { query_mode: { value: QueryMode.Raw } }),
|
|
controlState,
|
|
),
|
|
).toBe(false);
|
|
|
|
// Charts saved before the query mode control existed are inferred as raw
|
|
// records from their columns.
|
|
expect(
|
|
vis(
|
|
withControls(temporalGroupby, { all_columns: { value: ['name'] } }),
|
|
controlState,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test('time_grain_sqla is hidden in raw records mode for an adhoc dimension', () => {
|
|
const vis = getVisibility(config, 'time_grain_sqla');
|
|
const controlState = {} as ControlState;
|
|
|
|
// An adhoc column reports temporal without the lookup, so it needs the guard too.
|
|
const adhocGroupby = withControls(mkProps([]), {
|
|
groupby: {
|
|
value: [{ sqlExpression: 'ds', label: 'ds', expressionType: 'SQL' }],
|
|
options: [],
|
|
},
|
|
});
|
|
|
|
expect(
|
|
vis(
|
|
withControls(adhocGroupby, {
|
|
query_mode: { value: QueryMode.Aggregate },
|
|
}),
|
|
controlState,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
vis(
|
|
withControls(adhocGroupby, { query_mode: { value: QueryMode.Raw } }),
|
|
controlState,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test('show_totals renders in the customize tab atop visual formatting', () => {
|
|
const visualFormatting = config.controlPanelSections.find(
|
|
section => section?.label === 'Visual formatting',
|
|
);
|
|
expect(visualFormatting).toBeDefined();
|
|
|
|
const [firstRow] = visualFormatting!.controlSetRows;
|
|
const firstControl = firstRow[0] as CustomControlItem;
|
|
expect(firstControl.name).toBe('show_totals');
|
|
// renderTrigger keeps the whole section classified into the customize tab
|
|
// and must not regress; without it the section moves to the data tab.
|
|
expect(firstControl.config.renderTrigger).toBe(true);
|
|
// No visibility gate: the summary checkbox must render in raw records
|
|
// mode as well as aggregate mode.
|
|
expect(firstControl.config.visibility).toBeUndefined();
|
|
});
|
|
|
|
test('every Visual formatting control is a renderTrigger', () => {
|
|
// A non-renderTrigger control in this section would silently drag the
|
|
// whole section's classification from the customize tab to the data tab.
|
|
const visualFormatting = config.controlPanelSections.find(
|
|
section => section?.label === 'Visual formatting',
|
|
);
|
|
expect(visualFormatting).toBeDefined();
|
|
|
|
const controls = visualFormatting!.controlSetRows
|
|
.flat()
|
|
.filter(isCustomControlItem);
|
|
expect(controls.length).toBeGreaterThan(0);
|
|
|
|
controls.forEach(control => {
|
|
expect(control.config.renderTrigger).toBe(true);
|
|
});
|
|
});
|
|
|
|
function findControl(
|
|
panel: ControlPanelConfig,
|
|
controlName: string,
|
|
): CustomControlItem {
|
|
const item = (panel.controlPanelSections || [])
|
|
.flatMap(section => section?.controlSetRows || [])
|
|
.flat()
|
|
.find(c => isCustomControlItem(c) && c.name === controlName);
|
|
|
|
if (!item || !isCustomControlItem(item)) {
|
|
throw new Error(`Control "${controlName}" not found`);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
test('allow_rearrange_columns defaults to false, matching v1, and hides while time_compare is set', () => {
|
|
const control = findControl(config, 'allow_rearrange_columns');
|
|
expect(control.config.type).toBe('CheckboxControl');
|
|
expect(control.config.default).toBe(false);
|
|
expect(control.config.renderTrigger).toBe(true);
|
|
|
|
const vis = control.config.visibility as VisibilityFn;
|
|
expect(
|
|
vis({
|
|
controls: { time_compare: { value: [] } },
|
|
} as unknown as ControlPanelsContainerProps),
|
|
).toBe(true);
|
|
expect(
|
|
vis({
|
|
controls: { time_compare: { value: ['1 year ago'] } },
|
|
} as unknown as ControlPanelsContainerProps),
|
|
).toBe(false);
|
|
});
|
|
|
|
test('allow_render_html defaults to true, matching v1, and has no visibility gate', () => {
|
|
const control = findControl(config, 'allow_render_html');
|
|
expect(control.config.type).toBe('CheckboxControl');
|
|
expect(control.config.default).toBe(true);
|
|
expect(control.config.renderTrigger).toBe(true);
|
|
expect(control.config.visibility).toBeUndefined();
|
|
});
|