feat: Move cross filters to Dashboard (#22785)

This commit is contained in:
Geido 2023-01-25 18:01:06 +01:00 committed by GitHub
parent 262c947273
commit 9ed2326a20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 814 additions and 640 deletions

View File

@ -23,91 +23,35 @@ import { setFilter } from '../explore/utils';
describe('Charts filters', () => {
before(() => {
cy.visit(CHART_LIST);
setGridMode('card');
});
beforeEach(() => {
clearAllInputs();
});
describe('card-view', () => {
before(() => {
setGridMode('card');
});
xit('should filter by owners correctly', () => {
setFilter('Owner', 'alpha user');
cy.getBySel('styled-card').should('not.exist');
setFilter('Owner', 'admin user');
cy.getBySel('styled-card').should('exist');
});
xit('should filter by created by correctly', () => {
setFilter('Created by', 'alpha user');
cy.getBySel('styled-card').should('not.exist');
setFilter('Created by', 'admin user');
cy.getBySel('styled-card').should('exist');
});
it('should filter by viz type correctly', () => {
setFilter('Chart type', 'Area Chart (legacy)');
cy.getBySel('styled-card').should('have.length', 3);
setFilter('Chart type', 'Bubble Chart');
cy.getBySel('styled-card').should('have.length', 2);
});
it('should filter by datasource correctly', () => {
setFilter('Dataset', 'energy_usage');
cy.getBySel('styled-card').should('have.length', 3);
setFilter('Dataset', 'unicode_test');
cy.getBySel('styled-card').should('have.length', 1);
});
it('should filter by dashboards correctly', () => {
setFilter('Dashboards', 'Unicode Test');
cy.getBySel('styled-card').should('have.length', 1);
setFilter('Dashboards', 'Tabbed Dashboard');
cy.getBySel('styled-card').should('have.length', 9);
});
it('should allow filtering by "Owner"', () => {
setFilter('Owner', 'alpha user');
setFilter('Owner', 'admin user');
});
describe('list-view', () => {
before(() => {
setGridMode('list');
});
it('should allow filtering by "Created by" correctly', () => {
setFilter('Created by', 'alpha user');
setFilter('Created by', 'admin user');
});
xit('should filter by owners correctly', () => {
setFilter('Owner', 'alpha user');
cy.getBySel('table-row').should('not.exist');
setFilter('Owner', 'admin user');
cy.getBySel('table-row').should('exist');
});
it('should allow filtering by "Chart type" correctly', () => {
setFilter('Chart type', 'Area Chart (legacy)');
setFilter('Chart type', 'Bubble Chart');
});
xit('should filter by created by correctly', () => {
setFilter('Created by', 'alpha user');
cy.getBySel('table-row').should('not.exist');
setFilter('Created by', 'admin user');
cy.getBySel('table-row').should('exist');
});
it('should allow filtering by "Dataset" correctly', () => {
setFilter('Dataset', 'energy_usage');
setFilter('Dataset', 'unicode_test');
});
it('should filter by viz type correctly', () => {
setFilter('Chart type', 'Area Chart (legacy)');
cy.getBySel('table-row').should('have.length', 3);
setFilter('Chart type', 'Bubble Chart');
cy.getBySel('table-row').should('have.length', 2);
});
it('should filter by datasource correctly', () => {
setFilter('Dataset', 'energy_usage');
cy.getBySel('table-row').should('have.length', 3);
setFilter('Dataset', 'unicode_test');
cy.getBySel('table-row').should('have.length', 1);
});
it('should filter by dashboards correctly', () => {
setFilter('Dashboards', 'Unicode Test');
cy.getBySel('table-row').should('have.length', 1);
setFilter('Dashboards', 'Tabbed Dashboard');
cy.getBySel('table-row').should('have.length', 9);
});
it('should allow filtering by "Dashboards" correctly', () => {
setFilter('Dashboards', 'Unicode Test');
setFilter('Dashboards', 'Tabbed Dashboard');
});
});

View File

@ -128,6 +128,7 @@ function prepareDashboardFilters(
label_colors: {},
shared_label_colors: {},
color_scheme_domain: [],
cross_filters_enabled: false,
positions: {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
@ -203,9 +204,10 @@ function openVerticalFilterBar() {
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
cy.getBySel('filterbar-orientation-icon').click();
cy.wait(250);
cy.getBySel('dropdown-selectable-info')
cy.getBySel('dropdown-selectable-icon-submenu')
.contains('Orientation of filter bar')
.should('exist');
.should('exist')
.trigger('mouseover');
if (orientation === 'vertical') {
cy.get('.ant-dropdown-menu-item-selected')

View File

@ -23,63 +23,25 @@ import { setFilter } from '../dashboard/utils';
describe('Dashboards filters', () => {
before(() => {
cy.visit(DASHBOARD_LIST);
setGridMode('card');
});
beforeEach(() => {
clearAllInputs();
});
describe('card-view', () => {
before(() => {
setGridMode('card');
});
xit('should filter by owners correctly', () => {
setFilter('Owner', 'alpha user');
cy.getBySel('styled-card').should('not.exist');
setFilter('Owner', 'admin user');
cy.getBySel('styled-card').should('exist');
});
xit('should filter by created by correctly', () => {
setFilter('Created by', 'alpha user');
cy.getBySel('styled-card').should('not.exist');
setFilter('Created by', 'admin user');
cy.getBySel('styled-card').should('exist');
});
it('should filter by published correctly', () => {
setFilter('Status', 'Published');
cy.getBySel('styled-card').should('have.length', 3);
setFilter('Status', 'Draft');
cy.getBySel('styled-card').should('have.length', 2);
});
it('should allow filtering by "Owner" correctly', () => {
setFilter('Owner', 'alpha user');
setFilter('Owner', 'admin user');
});
describe('list-view', () => {
before(() => {
setGridMode('list');
});
it('should allow filtering by "Created by" correctly', () => {
setFilter('Created by', 'alpha user');
setFilter('Created by', 'admin user');
});
xit('should filter by owners correctly', () => {
setFilter('Owner', 'alpha user');
cy.getBySel('table-row').should('not.exist');
setFilter('Owner', 'admin user');
cy.getBySel('table-row').should('exist');
});
xit('should filter by created by correctly', () => {
setFilter('Created by', 'alpha user');
cy.getBySel('table-row').should('not.exist');
setFilter('Created by', 'admin user');
cy.getBySel('table-row').should('exist');
});
it('should filter by published correctly', () => {
setFilter('Status', 'Published');
cy.getBySel('table-row').should('have.length', 3);
setFilter('Status', 'Draft');
cy.getBySel('table-row').should('have.length', 2);
});
it('should allow filtering by "Status" correctly', () => {
setFilter('Status', 'Published');
setFilter('Status', 'Draft');
});
});

View File

@ -34,7 +34,11 @@ export function toggleBulkSelect() {
}
export function clearAllInputs() {
cy.get('[aria-label="close-circle"]').click({ multiple: true, force: true });
cy.get('body').then($body => {
if ($body.find('.ant-select-clear').length) {
cy.get('.ant-select-clear').click({ multiple: true, force: true });
}
});
}
const toSlicelike = ($chart: JQuery<HTMLElement>): Slice => ({

View File

@ -20,7 +20,6 @@ import { hasGenericChartAxes, t } from '@superset-ui/core';
import { ControlPanelSectionConfig, ControlSetRow } from '../types';
import {
contributionModeControl,
emitFilterControl,
xAxisSortControl,
xAxisSortAscControl,
} from '../shared-controls';
@ -30,7 +29,6 @@ const controlsWithoutXAxis: ControlSetRow[] = [
['groupby'],
[contributionModeControl],
['adhoc_filters'],
emitFilterControl,
['limit'],
['timeseries_limit_metric'],
['order_desc'],

View File

@ -40,7 +40,6 @@ export type ColumnConfigControlProps<T extends ColumnConfig> =
queryResponse?: ChartDataResponseResult;
configFormLayout?: ColumnConfigFormLayout;
appliedColumnNames?: string[];
emitFilter: boolean;
};
/**
@ -57,24 +56,8 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
value,
onChange,
configFormLayout = DEFAULT_CONFIG_FORM_LAYOUT,
emitFilter,
...props
}: ColumnConfigControlProps<T>) {
if (emitFilter) {
Object.values(configFormLayout).forEach(array_of_array => {
if (!array_of_array.some(arr => arr.includes('emitTarget'))) {
array_of_array.push(['emitTarget']);
}
});
} else {
Object.values(configFormLayout).forEach(array_of_array => {
const index = array_of_array.findIndex(arr => arr.includes('emitTarget'));
if (index > -1) {
array_of_array.splice(index, 1);
}
});
}
const { colnames: _colnames, coltypes: _coltypes } = queryResponse || {};
let colnames: string[] = [];
let coltypes: GenericDataType[] = [];

View File

@ -35,7 +35,6 @@ export type SharedColumnConfigProp =
| 'colorPositiveNegative'
| 'columnWidth'
| 'fractionDigits'
| 'emitTarget'
| 'd3NumberFormat'
| 'd3SmallNumberFormat'
| 'd3TimeFormat'
@ -43,17 +42,6 @@ export type SharedColumnConfigProp =
| 'truncateLongCells'
| 'showCellBars';
const emitTarget: ControlFormItemSpec<'Input'> = {
controlType: 'Input',
label: t('Emit Target'),
description: t(
'If you wish to specify a different target column than the original column, it can be entered here',
),
defaultValue: '',
debounceDelay: 500,
validators: undefined,
};
const d3NumberFormat: ControlFormItemSpec<'Select'> = {
controlType: 'Select',
label: t('D3 format'),
@ -156,7 +144,6 @@ const truncateLongCells: ControlFormItemSpec<'Checkbox'> = {
*/
export const SHARED_COLUMN_CONFIG_PROPS = {
d3NumberFormat,
emitTarget,
d3SmallNumberFormat: {
...d3NumberFormat,
label: t('Small number format'),

View File

@ -20,12 +20,10 @@
import {
ContributionType,
ensureIsArray,
FeatureFlag,
getColumnLabel,
getMetricLabel,
isDefined,
isEqualArray,
isFeatureEnabled,
QueryFormColumn,
QueryFormMetric,
t,
@ -33,23 +31,6 @@ import {
import { ControlPanelState, ControlState, ControlStateMapping } from '../types';
import { isTemporalColumn } from '../utils';
export const emitFilterControl = isFeatureEnabled(
FeatureFlag.DASHBOARD_CROSS_FILTERS,
)
? [
{
name: 'emit_filter',
config: {
type: 'CheckboxControl',
label: t('Enable dashboard cross filters'),
default: false,
renderTrigger: true,
description: t('Enable dashboard cross filters'),
},
},
]
: [];
export const contributionModeControl = {
name: 'contributionMode',
config: {

View File

@ -144,6 +144,8 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
inContextMenu?: boolean;
emitCrossFilters?: boolean;
theme: SupersetTheme;
constructor(config: ChartPropsConfig & { formData?: FormData } = {}) {
@ -164,6 +166,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
isRefreshing,
inputRef,
inContextMenu = false,
emitCrossFilters = false,
theme,
} = config;
this.width = width;
@ -184,6 +187,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
this.isRefreshing = isRefreshing;
this.inputRef = inputRef;
this.inContextMenu = inContextMenu;
this.emitCrossFilters = emitCrossFilters;
this.theme = theme;
}
}
@ -207,6 +211,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
input => input.isRefreshing,
input => input.inputRef,
input => input.inContextMenu,
input => input.emitCrossFilters,
input => input.theme,
(
annotationData,
@ -225,6 +230,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
isRefreshing,
inputRef,
inContextMenu,
emitCrossFilters,
theme,
) =>
new ChartProps({
@ -244,6 +250,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
isRefreshing,
inputRef,
inContextMenu,
emitCrossFilters,
theme,
}),
);

View File

@ -30,12 +30,12 @@ export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
labelMap,
groupby,
selectedValues,
formData,
refs,
emitCrossFilters,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -28,7 +28,6 @@ import {
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
sections,
emitFilterControl,
ControlPanelConfig,
getStandardizedControls,
ControlState,
@ -77,7 +76,6 @@ const config: ControlPanelConfig = {
['groupby'],
['metrics'],
['adhoc_filters'],
emitFilterControl,
['series_limit'],
['series_limit_metric'],
[

View File

@ -53,6 +53,7 @@ export default function transformProps(
filterState,
queriesData,
inContextMenu,
emitCrossFilters,
} = chartProps;
const { data = [] } = queriesData[0];
const { setDataMask = () => {}, onContextMenu } = hooks;
@ -64,7 +65,6 @@ export default function transformProps(
numberFormat,
dateFormat,
xTicksLayout,
emitFilter,
legendOrientation = 'top',
xAxisTitle,
yAxisTitle,
@ -291,7 +291,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
emitCrossFilters,
labelMap,
groupby,
selectedValues,

View File

@ -30,7 +30,6 @@ export type BoxPlotQueryFormData = QueryFormData & {
numberFormat?: string;
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
xTickLayout?: BoxPlotFormXTickLayout;
emitFilter: boolean;
} & TitleFormData;
export type BoxPlotFormDataWhiskerOptions =
@ -48,7 +47,6 @@ export type BoxPlotFormXTickLayout =
// @ts-ignore
export const DEFAULT_FORM_DATA: BoxPlotQueryFormData = {
emitFilter: false,
...DEFAULT_TITLE_FORM_DATA,
};

View File

@ -30,12 +30,12 @@ export default function EchartsFunnel(props: FunnelChartTransformedProps) {
labelMap,
groupby,
selectedValues,
formData,
emitCrossFilters,
refs,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -24,7 +24,6 @@ import {
sections,
sharedControls,
ControlStateMapping,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA, EchartsFunnelLabelTypeType } from './types';
@ -45,7 +44,6 @@ const config: ControlPanelConfig = {
['groupby'],
['metric'],
['adhoc_filters'],
emitFilterControl,
[
{
name: 'row_limit',

View File

@ -92,6 +92,7 @@ export default function transformProps(
width,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const data: DataRecord[] = queriesData[0].data || [];
@ -110,7 +111,6 @@ export default function transformProps(
numberFormat,
showLabels,
showLegend,
emitFilter,
sliceId,
}: EchartsFunnelFormData = {
...DEFAULT_LEGEND_FORM_DATA,
@ -238,7 +238,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
emitCrossFilters,
labelMap,
groupby,
selectedValues,

View File

@ -40,7 +40,6 @@ export type EchartsFunnelFormData = QueryFormData &
gap: number;
sort: 'descending' | 'ascending' | 'none' | undefined;
orient: 'vertical' | 'horizontal' | undefined;
emitFilter: boolean;
};
export enum EchartsFunnelLabelTypeType {
@ -70,7 +69,6 @@ export const DEFAULT_FORM_DATA: EchartsFunnelFormData = {
sort: 'descending',
orient: 'vertical',
gap: 0,
emitFilter: false,
};
export type FunnelChartTransformedProps =

View File

@ -30,12 +30,12 @@ export default function EchartsGauge(props: GaugeChartTransformedProps) {
labelMap,
groupby,
selectedValues,
formData: { emitFilter },
emitCrossFilters,
refs,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -23,7 +23,6 @@ import {
ControlPanelConfig,
D3_FORMAT_OPTIONS,
sections,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
@ -46,7 +45,6 @@ const config: ControlPanelConfig = {
],
['metric'],
['adhoc_filters'],
emitFilterControl,
[
{
name: 'row_limit',

View File

@ -91,8 +91,16 @@ const calculateMax = (data: GaugeDataItemOption[]) =>
export default function transformProps(
chartProps: EchartsGaugeChartProps,
): GaugeChartTransformedProps {
const { width, height, formData, queriesData, hooks, filterState, theme } =
chartProps;
const {
width,
height,
formData,
queriesData,
hooks,
filterState,
theme,
emitCrossFilters,
} = chartProps;
const gaugeSeriesOptions = defaultGaugeSeriesOption(theme);
@ -117,7 +125,6 @@ export default function transformProps(
intervals,
intervalColorIndices,
valueFormatter,
emitFilter,
sliceId,
}: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData };
const refs: Refs = {};
@ -327,7 +334,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
emitCrossFilters,
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues: filterState.selectedValues || [],

View File

@ -52,7 +52,6 @@ export type EchartsGaugeFormData = QueryFormData & {
intervals: string;
intervalColorIndices: string;
valueFormatter: string;
emitFilter: boolean;
};
export const DEFAULT_FORM_DATA: Partial<EchartsGaugeFormData> = {
@ -76,7 +75,6 @@ export const DEFAULT_FORM_DATA: Partial<EchartsGaugeFormData> = {
intervals: '',
intervalColorIndices: '',
valueFormatter: '{value}',
emitFilter: false,
};
export interface EchartsGaugeChartProps

View File

@ -39,6 +39,7 @@ export default function EchartsMixedTimeseries({
groupbyB,
selectedValues,
formData,
emitCrossFilters,
seriesBreakdown,
onContextMenu,
xValueFormatter,
@ -52,10 +53,7 @@ export default function EchartsMixedTimeseries({
const handleChange = useCallback(
(values: string[], seriesIndex: number) => {
const emitFilter = isFirstQuery(seriesIndex)
? formData.emitFilter
: formData.emitFilterB;
if (!emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -24,7 +24,6 @@ import {
ControlPanelSectionConfig,
ControlSetRow,
CustomControlItem,
emitFilterControl,
getStandardizedControls,
sections,
sharedControls,
@ -79,14 +78,6 @@ function createQuerySection(
config: sharedControls.adhoc_filters,
},
],
emitFilterControl.length > 0
? [
{
...emitFilterControl[0],
name: `emit_filter${controlSuffix}`,
},
]
: [],
[
{
name: `limit${controlSuffix}`,

View File

@ -95,6 +95,7 @@ export default function transformProps(
datasource,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const { verboseMap = {} } = datasource;
const { label_map: labelMap } =
@ -144,8 +145,6 @@ export default function transformProps(
xAxisLabelRotation,
groupby,
groupbyB,
emitFilter,
emitFilterB,
xAxis: xAxisOrig,
xAxisTitle,
yAxisTitle,
@ -504,8 +503,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
emitFilterB,
emitCrossFilters,
labelMap,
labelMapB,
groupby,

View File

@ -86,7 +86,6 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
yAxisIndexB?: number;
groupby: QueryFormColumn[];
groupbyB: QueryFormColumn[];
emitFilter: boolean;
} & LegendFormData &
TitleFormData;
@ -143,7 +142,6 @@ export type EchartsMixedTimeseriesChartTransformedProps =
BaseTransformedProps<EchartsMixedTimeseriesFormData> &
ContextMenuTransformedProps &
CrossFilterTransformedProps & {
emitFilterB: boolean;
groupbyB: QueryFormColumn[];
labelMapB: Record<string, string[]>;
seriesBreakdown: number;

View File

@ -30,12 +30,12 @@ export default function EchartsPie(props: PieChartTransformedProps) {
labelMap,
groupby,
selectedValues,
formData,
refs,
emitCrossFilters,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -25,7 +25,6 @@ import {
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
sections,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
@ -52,7 +51,6 @@ const config: ControlPanelConfig = {
['groupby'],
['metric'],
['adhoc_filters'],
emitFilterControl,
['row_limit'],
[
{

View File

@ -144,6 +144,7 @@ export default function transformProps(
width,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const { data = [] } = queriesData[0];
const coltypeMapping = getColtypesMapping(queriesData[0]);
@ -166,7 +167,6 @@ export default function transformProps(
showLabels,
showLegend,
showLabelsThreshold,
emitFilter,
sliceId,
showTotal,
}: EchartsPieFormData = {
@ -339,11 +339,11 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
labelMap,
groupby,
selectedValues,
onContextMenu,
refs,
emitCrossFilters,
};
}

View File

@ -45,7 +45,6 @@ export type EchartsPieFormData = QueryFormData &
numberFormat: string;
dateFormat: string;
showLabelsThreshold: number;
emitFilter: boolean;
};
export enum EchartsPieLabelType {
@ -77,7 +76,6 @@ export const DEFAULT_FORM_DATA: EchartsPieFormData = {
showLabels: true,
labelsOutside: true,
showLabelsThreshold: 5,
emitFilter: false,
dateFormat: 'smart_date',
};

View File

@ -30,12 +30,12 @@ export default function EchartsRadar(props: RadarChartTransformedProps) {
labelMap,
groupby,
selectedValues,
formData,
emitCrossFilters,
refs,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -31,7 +31,6 @@ import {
D3_TIME_FORMAT_OPTIONS,
sections,
sharedControls,
emitFilterControl,
ControlFormItemSpec,
getStandardizedControls,
} from '@superset-ui/chart-controls';
@ -68,7 +67,6 @@ const config: ControlPanelConfig = {
['metrics'],
['timeseries_limit_metric'],
['adhoc_filters'],
emitFilterControl,
[
{
name: 'row_limit',

View File

@ -80,6 +80,7 @@ export default function transformProps(
width,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const refs: Refs = {};
const { data = [] } = queriesData[0];
@ -101,7 +102,6 @@ export default function transformProps(
isCircle,
columnConfig,
sliceId,
emitFilter,
}: EchartsRadarFormData = {
...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_RADAR_FORM_DATA,
@ -252,7 +252,7 @@ export default function transformProps(
width,
height,
echartOptions,
emitFilter,
emitCrossFilters,
setDataMask,
labelMap: Object.fromEntries(columnsLabelMap),
groupby,

View File

@ -50,7 +50,6 @@ export type EchartsRadarFormData = QueryFormData &
isCircle: boolean;
numberFormat: string;
dateFormat: string;
emitFilter: boolean;
};
export enum EchartsRadarLabelType {
@ -73,7 +72,6 @@ export const DEFAULT_FORM_DATA: EchartsRadarFormData = {
legendType: LegendType.Scroll,
numberFormat: 'SMART_NUMBER',
showLabels: true,
emitFilter: false,
dateFormat: 'smart_date',
isCircle: false,
};

View File

@ -38,13 +38,14 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
formData,
onContextMenu,
refs,
emitCrossFilters,
} = props;
const { emitFilter, columns } = formData;
const { columns } = formData;
const handleChange = useCallback(
(values: string[]) => {
if (!emitFilter) {
if (!emitCrossFilters) {
return;
}
@ -75,7 +76,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
},
});
},
[emitFilter, setDataMask, columns, labelMap],
[emitCrossFilters, setDataMask, columns, labelMap],
);
const eventHandlers: EventHandlers = {

View File

@ -24,7 +24,6 @@ import {
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
emitFilterControl,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
@ -43,7 +42,6 @@ const config: ControlPanelConfig = {
['metric'],
['secondary_metric'],
['adhoc_filters'],
emitFilterControl,
['row_limit'],
[
{

View File

@ -163,6 +163,7 @@ export default function transformProps(
width,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const { data = [] } = queriesData[0];
const coltypeMapping = getColtypesMapping(queriesData[0]);
@ -180,7 +181,6 @@ export default function transformProps(
showLabelsThreshold,
showTotal,
sliceId,
emitFilter,
} = formData;
const refs: Refs = {};
const numberFormatter = getNumberFormatter(numberFormat);
@ -352,7 +352,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
emitCrossFilters,
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues: filterState.selectedValues || [],

View File

@ -36,7 +36,6 @@ export type EchartsSunburstFormData = QueryFormData & {
secondaryMetric?: QueryFormMetric;
colorScheme?: string;
linearColorScheme?: string;
emitFilter: boolean;
};
export enum EchartsSunburstLabelType {
@ -51,7 +50,6 @@ export const DEFAULT_FORM_DATA: Partial<EchartsSunburstFormData> = {
labelType: EchartsSunburstLabelType.Key,
showLabels: false,
dateFormat: 'smart_date',
emitFilter: false,
};
export interface EchartsSunburstChartProps

View File

@ -49,8 +49,9 @@ export default function EchartsTimeseries({
xValueFormatter,
xAxis,
refs,
emitCrossFilters,
}: TimeseriesChartTransformedProps) {
const { emitFilter, stack } = formData;
const { stack } = formData;
const echartRef = useRef<EchartsHandler | null>(null);
// eslint-disable-next-line no-param-reassign
refs.echartRef = echartRef;
@ -109,7 +110,7 @@ export default function EchartsTimeseries({
const handleChange = useCallback(
(values: string[]) => {
if (!emitFilter) {
if (!emitCrossFilters) {
return;
}
const groupbyValues = values.map(value => labelMap[value]);
@ -140,7 +141,7 @@ export default function EchartsTimeseries({
},
});
},
[groupby, labelMap, setDataMask, emitFilter],
[groupby, labelMap, setDataMask, emitCrossFilters],
);
const eventHandlers: EventHandlers = {

View File

@ -57,7 +57,6 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
zoomable: false,
richTooltip: true,
xAxisLabelRotation: 0,
emitFilter: false,
groupby: [],
showValue: false,
onlyTotal: false,

View File

@ -99,6 +99,7 @@ export default function transformProps(
datasource,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const { verboseMap = {} } = datasource;
const [queryData] = queriesData;
@ -134,7 +135,6 @@ export default function transformProps(
richTooltip,
xAxis: xAxisOrig,
xAxisLabelRotation,
emitFilter,
groupby,
showValue,
onlyTotal,
@ -448,7 +448,7 @@ export default function transformProps(
return {
echartOptions,
emitFilter,
emitCrossFilters,
formData,
groupby,
height,

View File

@ -80,7 +80,6 @@ export type EchartsTimeseriesFormData = QueryFormData & {
zoomable: boolean;
richTooltip: boolean;
xAxisLabelRotation: number;
emitFilter: boolean;
groupby: QueryFormColumn[];
showValue: boolean;
onlyTotal: boolean;

View File

@ -29,7 +29,7 @@ import { TreemapTransformedProps } from './types';
export default function EchartsTreemap({
echartOptions,
formData,
emitCrossFilters,
groupby,
height,
labelMap,
@ -41,7 +41,7 @@ export default function EchartsTreemap({
}: TreemapTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
if (!emitCrossFilters) {
return;
}

View File

@ -24,7 +24,6 @@ import {
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
sections,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
@ -55,7 +54,6 @@ const config: ControlPanelConfig = {
},
],
['adhoc_filters'],
emitFilterControl,
],
},
{

View File

@ -117,6 +117,7 @@ export default function transformProps(
filterState,
theme,
inContextMenu,
emitCrossFilters,
} = chartProps;
const { data = [] } = queriesData[0];
const { setDataMask = () => {}, onContextMenu } = hooks;
@ -133,7 +134,6 @@ export default function transformProps(
showLabels,
showUpperLabels,
dashboardId,
emitFilter,
sliceId,
}: EchartsTreemapFormData = {
...DEFAULT_TREEMAP_FORM_DATA,
@ -288,7 +288,7 @@ export default function transformProps(
height,
echartOptions,
setDataMask,
emitFilter,
emitCrossFilters,
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues: filterState.selectedValues || [],

View File

@ -43,7 +43,6 @@ export type EchartsTreemapFormData = QueryFormData & {
numberFormat: string;
dateFormat: string;
dashboardId?: number;
emitFilter: boolean;
};
export enum EchartsTreemapLabelType {
@ -66,7 +65,6 @@ export const DEFAULT_FORM_DATA: Partial<EchartsTreemapFormData> = {
showLabels: true,
showUpperLabels: true,
dateFormat: 'smart_date',
emitFilter: false,
};
export interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
treePathInfo?: TreePathInfo[];

View File

@ -130,12 +130,12 @@ export interface BaseTransformedProps<F> {
}
export type CrossFilterTransformedProps = {
emitFilter: boolean;
groupby: QueryFormColumn[];
labelMap: Record<string, string[]>;
setControlValue?: HandlerFunction;
setDataMask: SetDataMaskHook;
selectedValues: Record<number, string>;
emitCrossFilters?: boolean;
};
export type ContextMenuTransformedProps = {

View File

@ -27,7 +27,6 @@ import { BoxPlotQueryFormData } from '../../src/BoxPlot/types';
describe('BoxPlot buildQuery', () => {
const formData: BoxPlotQueryFormData = {
...DEFAULT_TITLE_FORM_DATA,
emitFilter: false,
columns: [],
datasource: '5__table',
granularity_sqla: 'ds',

View File

@ -46,7 +46,6 @@ const formDataMixedChart = {
row_limit: 10,
timeseries_limit_metric: 'count',
order_desc: true,
emit_filter: true,
truncate_metric: true,
show_empty_columns: true,
// -- query b
@ -63,7 +62,6 @@ const formDataMixedChart = {
row_limit_b: 100,
timeseries_limit_metric_b: undefined,
order_desc_b: false,
emit_filter_b: undefined,
truncate_metric_b: true,
show_empty_columns_b: true,
// chart configs

View File

@ -18,7 +18,6 @@
*/
import {
ControlPanelConfig,
emitFilterControl,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
@ -65,7 +64,6 @@ const config: ControlPanelConfig = {
[includeTimeControlSetItem],
[showTotalsControlSetItem],
['adhoc_filters'],
emitFilterControl,
],
},
{

View File

@ -51,7 +51,6 @@ export type HandlebarsQueryFormData = QueryFormData &
all_columns?: QueryFormMetric[] | null;
order_desc?: boolean;
table_timestamp_format?: string;
emit_filter?: boolean;
granularitySqla?: string;
time_grain_sqla?: TimeGranularity;
column_config?: Record<string, ColumnConfig>;

View File

@ -135,7 +135,7 @@ export default function PivotTableChart(props: PivotTableProps) {
colTotals,
rowTotals,
valueFormat,
emitFilter,
emitCrossFilters,
setDataMask,
selectedFilters,
verboseMap,
@ -287,7 +287,7 @@ export default function PivotTableChart(props: PivotTableProps) {
isSubtotal: boolean,
isGrandTotal: boolean,
) => {
if (isSubtotal || isGrandTotal || !emitFilter) {
if (isSubtotal || isGrandTotal || !emitCrossFilters) {
return;
}
@ -327,7 +327,7 @@ export default function PivotTableChart(props: PivotTableProps) {
}
handleChange(updatedFilters);
},
[emitFilter, selectedFilters, handleChange],
[emitCrossFilters, selectedFilters, handleChange],
);
const tableOptions = useMemo(
@ -336,7 +336,7 @@ export default function PivotTableChart(props: PivotTableProps) {
clickColumnHeaderCallback: toggleFilter,
colTotals,
rowTotals,
highlightHeaderCellsOnHover: emitFilter,
highlightHeaderCellsOnHover: emitCrossFilters,
highlightedHeaderCells: selectedFilters,
omittedHighlightHeaderGroups: [METRIC_KEY],
cellColorFormatters: { [METRIC_KEY]: metricColorFormatters },
@ -345,7 +345,7 @@ export default function PivotTableChart(props: PivotTableProps) {
[
colTotals,
dateFormatters,
emitFilter,
emitCrossFilters,
metricColorFormatters,
rowTotals,
selectedFilters,

View File

@ -32,7 +32,6 @@ import {
D3_TIME_FORMAT_OPTIONS,
sections,
sharedControls,
emitFilterControl,
Dataset,
getStandardizedControls,
} from '@superset-ui/chart-controls';
@ -127,7 +126,6 @@ const config: ControlPanelConfig = {
},
],
['adhoc_filters'],
emitFilterControl,
['series_limit'],
[
{

View File

@ -80,6 +80,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
hooks: { setDataMask = () => {}, onContextMenu },
filterState,
datasource: { verboseMap = {}, columnFormats = {} },
emitCrossFilters,
} = chartProps;
const { data, colnames, coltypes } = queriesData[0];
const {
@ -98,7 +99,6 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
rowTotals,
valueFormat,
dateFormat,
emitFilter,
metricsLayout,
conditionalFormatting,
timeGrainSqla,
@ -157,7 +157,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
colTotals,
rowTotals,
valueFormat,
emitFilter,
emitCrossFilters,
setDataMask,
selectedFilters,
verboseMap,

View File

@ -65,7 +65,7 @@ interface PivotTableCustomizeProps {
rowTotals: boolean;
valueFormat: string;
setDataMask: SetDataMaskHook;
emitFilter?: boolean;
emitCrossFilters?: boolean;
selectedFilters?: SelectedFiltersType;
verboseMap: JsonObject;
columnFormats: JsonObject;

View File

@ -38,7 +38,6 @@ describe('PivotTableChart transformProps', () => {
colTotals: true,
rowTotals: true,
valueFormat: 'SMART_NUMBER',
emitFilter: false,
metricsLayout: MetricsLayoutEnum.COLUMNS,
viz_type: '',
datasource: '',
@ -83,13 +82,13 @@ describe('PivotTableChart transformProps', () => {
rowTotals: true,
valueFormat: 'SMART_NUMBER',
data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }],
emitFilter: false,
setDataMask,
selectedFilters: {},
verboseMap: {},
metricsLayout: MetricsLayoutEnum.COLUMNS,
metricColorFormatters: [],
dateFormatters: {},
emitCrossFilters: false,
columnFormats: {},
});
});

View File

@ -222,13 +222,13 @@ export default function TableChart<D extends DataRecord = DataRecord>(
serverPaginationData,
setDataMask,
showCellBars = true,
emitFilter = false,
sortDesc = false,
filters,
sticky = true, // whether to use sticky header
columnColorFormatters,
allowRearrangeColumns = false,
onContextMenu,
emitCrossFilters,
} = props;
const timestampFormatter = useCallback(
value => getTimeFormatterForGranularity(timeGrain)(value),
@ -243,7 +243,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const handleChange = useCallback(
(filters: { [x: string]: DataRecordValue[] }) => {
if (!emitFilter) {
if (!emitCrossFilters) {
return;
}
@ -289,7 +289,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
},
});
},
[emitFilter, setDataMask],
[emitCrossFilters, setDataMask],
);
// only take relevant page size options
@ -322,27 +322,21 @@ export default function TableChart<D extends DataRecord = DataRecord>(
[filters],
);
function getEmitTarget(col: string) {
const meta = columnsMeta?.find(x => x.key === col);
return meta?.config?.emitTarget || col;
}
const toggleFilter = useCallback(
function toggleFilter(key: string, val: DataRecordValue) {
let updatedFilters = { ...(filters || {}) };
const target = getEmitTarget(key);
if (filters && isActiveFilterValue(target, val)) {
if (filters && isActiveFilterValue(key, val)) {
updatedFilters = {};
} else {
updatedFilters = {
[target]: [val],
[key]: [val],
};
}
if (
Array.isArray(updatedFilters[target]) &&
updatedFilters[target].length === 0
Array.isArray(updatedFilters[key]) &&
updatedFilters[key].length === 0
) {
delete updatedFilters[target];
delete updatedFilters[key];
}
handleChange(updatedFilters);
},
@ -396,7 +390,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
getValueRange(key, alignPositiveNegative);
let className = '';
if (emitFilter) {
if (emitCrossFilters) {
className += ' dt-is-filter';
}
@ -459,7 +453,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// show raw number in title in case of numeric values
title: typeof value === 'number' ? String(value) : undefined,
onClick:
emitFilter && !valueRange
emitCrossFilters && !valueRange
? () => toggleFilter(key, value)
: undefined,
className: [
@ -567,7 +561,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
[
defaultAlignPN,
defaultColorPN,
emitFilter,
emitCrossFilters,
getValueRange,
isActiveFilterValue,
isRawRecords,

View File

@ -44,7 +44,6 @@ import {
sharedControls,
ControlPanelState,
ControlState,
emitFilterControl,
Dataset,
ColumnMeta,
defineSavedMetrics,
@ -369,7 +368,6 @@ const config: ControlPanelConfig = {
},
},
],
emitFilterControl,
],
},
{
@ -488,7 +486,6 @@ const config: ControlPanelConfig = {
queryResponse: chart?.queriesResponse?.[0] as
| ChartDataResponseResult
| undefined,
emitFilter: explore?.controls?.table_filter?.value,
};
},
},

View File

@ -209,6 +209,7 @@ const transformProps = (
setDataMask = () => {},
onContextMenu,
},
emitCrossFilters,
} = chartProps;
const {
@ -217,7 +218,6 @@ const transformProps = (
show_cell_bars: showCellBars = true,
include_search: includeSearch = false,
page_length: pageLength,
emit_filter: emitFilter,
server_pagination: serverPagination = false,
server_page_length: serverPageLength = 10,
order_desc: sortDesc = false,
@ -273,7 +273,7 @@ const transformProps = (
? serverPageLength
: getPageSize(pageLength, data.length, columns.length),
filters: filterState.filters,
emitFilter,
emitCrossFilters,
onChangeFilter,
columnColorFormatters,
timeGrain,

View File

@ -69,7 +69,6 @@ export type TableChartFormData = QueryFormData & {
order_desc?: boolean;
show_cell_bars?: boolean;
table_timestamp_format?: string;
emit_filter?: boolean;
time_grain_sqla?: TimeGranularity;
column_config?: Record<string, ColumnConfig>;
allow_rearrange_columns?: boolean;
@ -108,7 +107,7 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
// These are dashboard filters, don't be confused with in-chart search filter
// enabled by `includeSearch`
filters?: DataRecordFilters;
emitFilter?: boolean;
emitCrossFilters?: boolean;
onChangeFilter?: ChartProps['hooks']['onAddFilter'];
columnColorFormatters?: ColorFormatters;
allowRearrangeColumns?: boolean;

View File

@ -113,6 +113,6 @@ export const overwriteConfirmMetadata = {
slug: null,
owners: [],
json_metadata:
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"show_native_filters":true,"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"filter_scopes":{},"chart_configuration":{},"positions":{}}',
'{"timed_refresh_immune_slices":[],"expanded_slices":{},"refresh_frequency":0,"default_filters":"{}","color_scheme":"supersetColors","label_colors":{"0":"#FCC700","1":"#A868B7","15":"#3CCCCB","30":"#A38F79","45":"#8FD3E4","age":"#1FA8C9","Yes,":"#1FA8C9","Female":"#454E7C","Prefer":"#5AC189","No,":"#FF7F44","Male":"#666666","Prefer not to say":"#E04355","Ph.D.":"#FCC700","associate\'s degree":"#A868B7","bachelor\'s degree":"#3CCCCB","high school diploma or equivalent (GED)":"#A38F79","master\'s degree (non-professional)":"#8FD3E4","no high school (secondary school)":"#A1A6BD","professional degree (MBA, MD, JD, etc.)":"#ACE1C4","some college credit, no degree":"#FEC0A1","some high school":"#B2B2B2","trade, technical, or vocational training":"#EFA1AA","No, not an ethnic minority":"#1FA8C9","Yes, an ethnic minority":"#454E7C","<NULL>":"#5AC189","Yes":"#FF7F44","No":"#666666","last_yr_income":"#E04355","More":"#A1A6BD","Less":"#ACE1C4","I":"#FEC0A1","expected_earn":"#B2B2B2","Yes: Willing To":"#EFA1AA","No: Not Willing to":"#FDE380","No Answer":"#D3B3DA","In an Office (with Other Developers)":"#9EE5E5","No Preference":"#D1C6BC","From Home":"#1FA8C9"},"show_native_filters":true,"color_scheme_domain":["#1FA8C9","#454E7C","#5AC189","#FF7F44","#666666","#E04355","#FCC700","#A868B7","#3CCCCB","#A38F79","#8FD3E4","#A1A6BD","#ACE1C4","#FEC0A1","#B2B2B2","#EFA1AA","#FDE380","#D3B3DA","#9EE5E5","#D1C6BC"],"shared_label_colors":{"Male":"#5ac19e","Female":"#1f86c9","<NULL>":"#5AC189","Prefer not to say":"#47457c","No Answer":"#e05043","Yes, an ethnic minority":"#666666","No, not an ethnic minority":"#ffa444","age":"#1FA8C9"},"cross_filters_enabled":false,"filter_scopes":{},"chart_configuration":{},"positions":{}}',
},
};

View File

@ -77,6 +77,7 @@ const propTypes = {
postTransformProps: PropTypes.func,
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
isInView: PropTypes.bool,
emitCrossFilters: PropTypes.bool,
};
const BLANK = {};

View File

@ -62,6 +62,7 @@ const propTypes = {
ownState: PropTypes.object,
postTransformProps: PropTypes.func,
source: PropTypes.oneOf([ChartSource.Dashboard, ChartSource.Explore]),
emitCrossFilters: PropTypes.bool,
};
const BLANK = {};
@ -142,7 +143,8 @@ class ChartRenderer extends React.Component {
nextProps.sharedLabelColors !== this.props.sharedLabelColors ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
nextProps.formData.stack !== this.props.formData.stack ||
nextProps.cacheBusterProp !== this.props.cacheBusterProp
nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
nextProps.emitCrossFilters !== this.props.emitCrossFilters
);
}
return false;
@ -223,7 +225,7 @@ class ChartRenderer extends React.Component {
}
render() {
const { chartAlert, chartStatus, chartId } = this.props;
const { chartAlert, chartStatus, chartId, emitCrossFilters } = this.props;
// Skip chart rendering
if (chartStatus === 'loading' || !!chartAlert || chartStatus === null) {
@ -341,6 +343,7 @@ class ChartRenderer extends React.Component {
onRenderFailure={this.handleRenderFailure}
noResults={noResultsComponent}
postTransformProps={postTransformProps}
emitCrossFilters={emitCrossFilters}
{...drillToDetailProps}
/>
</div>

View File

@ -24,6 +24,7 @@ export interface CheckboxProps {
checked: boolean;
onChange: (val?: boolean) => void;
style?: React.CSSProperties;
className?: string;
}
const Styles = styled.span`
@ -33,7 +34,12 @@ const Styles = styled.span`
}
`;
export default function Checkbox({ checked, onChange, style }: CheckboxProps) {
export default function Checkbox({
checked,
onChange,
style,
className,
}: CheckboxProps) {
return (
<Styles
style={style}
@ -44,6 +50,7 @@ export default function Checkbox({ checked, onChange, style }: CheckboxProps) {
tabIndex={0}
aria-checked={checked}
aria-label="Checkbox"
className={className || ''}
>
{checked ? <CheckboxChecked /> : <CheckboxUnchecked />}
</Styles>

View File

@ -23,11 +23,20 @@ import { DropdownButton } from 'src/components/DropdownButton';
import { DropdownButtonProps } from 'antd/lib/dropdown';
import { Menu, MenuProps } from 'src/components/Menu';
const { SubMenu } = Menu;
type SubMenuItemProps = { key: string; label: string | React.ReactNode };
export interface DropDownSelectableProps extends Pick<MenuProps, 'onSelect'> {
ref?: RefObject<HTMLDivElement>;
icon: React.ReactNode;
info?: string;
menuItems: { key: string; label: React.ReactNode }[];
menuItems: {
key: string;
label: string | React.ReactNode;
children?: SubMenuItemProps[];
divider?: boolean;
}[];
selectedKeys?: string[];
}
@ -67,17 +76,48 @@ const StyledMenu = styled(Menu)`
color: ${theme.colors.grayscale.dark1};
background-color: ${theme.colors.primary.light5};
}
.ant-dropdown-menu-item > span.anticon {
float: right;
margin-right: 0;
font-size: ${theme.typography.sizes.xl}px;
}
`}
`;
const StyleMenuItem = styled(Menu.Item)<{ divider?: boolean }>`
display: flex;
justify-content: space-between;
> span {
width: 100%;
}
border-bottom: ${({ divider, theme }) =>
divider ? `1px solid ${theme.colors.grayscale.light3};` : 'none;'};
`;
const StyleSubmenuItem = styled.div`
display: flex;
justify-content: space-between;
> span {
width: 100%;
}
`;
export default (props: DropDownSelectableProps) => {
const theme = useTheme();
const { icon, info, menuItems, selectedKeys, onSelect } = props;
const menuItem = (
label: string | React.ReactNode,
key: string,
divider?: boolean,
) => (
<StyleMenuItem key={key} divider={divider}>
<StyleSubmenuItem>
<span>{label}</span>
{selectedKeys?.includes(key) && (
<Icons.Check
iconColor={theme.colors.primary.base}
className="tick-menu-item"
iconSize="xl"
/>
)}
</StyleSubmenuItem>
</StyleMenuItem>
);
const overlayMenu = useMemo(
() => (
<StyledMenu selectedKeys={selectedKeys} onSelect={onSelect} selectable>
@ -86,14 +126,19 @@ export default (props: DropDownSelectableProps) => {
{info}
</div>
)}
{menuItems.map(m => (
<Menu.Item key={m.key}>
{m.label}
{selectedKeys?.includes(m.key) && (
<Icons.Check iconColor={theme.colors.primary.base} />
)}
</Menu.Item>
))}
{menuItems.map(m =>
m.children?.length ? (
<SubMenu
title={m.label}
key={m.key}
data-test="dropdown-selectable-icon-submenu"
>
{m.children.map(s => menuItem(s.label, s.key))}
</SubMenu>
) : (
menuItem(m.label, m.key, m.divider)
),
)}
</StyledMenu>
),
[info, menuItems],

View File

@ -17,9 +17,9 @@
* under the License.
*/
import { Dispatch } from 'redux';
import { makeApi, CategoricalColorNamespace, t } from '@superset-ui/core';
import { makeApi, CategoricalColorNamespace } from '@superset-ui/core';
import { isString } from 'lodash';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { getErrorText } from 'src/utils/getClientErrorObject';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import {
DashboardInfo,
@ -131,6 +131,15 @@ export function setFilterBarOrientation(
return { type: SET_FILTER_BAR_ORIENTATION, filterBarOrientation };
}
export const SET_CROSS_FILTERS_ENABLED = 'SET_CROSS_FILTERS_ENABLED';
export interface SetCrossFiltersEnabled {
type: typeof SET_CROSS_FILTERS_ENABLED;
crossFiltersEnabled: boolean;
}
export function setCrossFiltersEnabled(crossFiltersEnabled: boolean) {
return { type: SET_CROSS_FILTERS_ENABLED, crossFiltersEnabled };
}
export function saveFilterBarOrientation(orientation: FilterBarOrientation) {
return async (dispatch: Dispatch, getState: () => RootState) => {
const { id, metadata } = getState().dashboardInfo;
@ -160,18 +169,41 @@ export function saveFilterBarOrientation(orientation: FilterBarOrientation) {
dispatch(onSave(lastModifiedTime));
}
} catch (errorObject) {
const { error, message } = await getClientErrorObject(errorObject);
let errorText = t('Sorry, an unknown error occurred.');
if (error) {
errorText = t(
'Sorry, there was an error saving this dashboard: %s',
error,
);
}
if (typeof message === 'string' && message === 'Forbidden') {
errorText = t('You do not have permission to edit this dashboard');
}
const errorText = await getErrorText(errorObject, 'dashboard');
dispatch(addDangerToast(errorText));
throw errorObject;
}
};
}
export function saveCrossFiltersSetting(crossFiltersEnabled: boolean) {
return async (dispatch: Dispatch, getState: () => RootState) => {
const { id, metadata } = getState().dashboardInfo;
const updateDashboard = makeApi<
Partial<DashboardInfo>,
{ result: Partial<DashboardInfo>; last_modified_time: number }
>({
method: 'PUT',
endpoint: `/api/v1/dashboard/${id}`,
});
try {
const response = await updateDashboard({
json_metadata: JSON.stringify({
...metadata,
cross_filters_enabled: crossFiltersEnabled,
}),
});
const updatedDashboard = response.result;
const lastModifiedTime = response.last_modified_time;
if (updatedDashboard.json_metadata) {
const metadata = JSON.parse(updatedDashboard.json_metadata);
dispatch(setCrossFiltersEnabled(metadata.cross_filters_enabled));
}
if (lastModifiedTime) {
dispatch(onSave(lastModifiedTime));
}
} catch (errorObject) {
const errorText = await getErrorText(errorObject, 'dashboard');
dispatch(addDangerToast(errorText));
throw errorObject;
}

View File

@ -36,6 +36,7 @@ import {
SAVE_TYPE_OVERWRITE,
SAVE_TYPE_OVERWRITE_CONFIRMED,
} from 'src/dashboard/util/constants';
import { isCrossFiltersEnabled } from 'src/dashboard/util/crossFilters';
import {
addSuccessToast,
addWarningToast,
@ -242,7 +243,7 @@ export function saveDashboardRequest(data, id, saveType) {
} = data;
const hasId = item => item.id !== undefined;
const metadataCrossFiltersEnabled = data.metadata?.cross_filters_enabled;
// making sure the data is what the backend expects
const cleanedData = {
...data,
@ -267,6 +268,10 @@ export function saveDashboardRequest(data, id, saveType) {
refresh_frequency: data.metadata?.refresh_frequency || 0,
timed_refresh_immune_slices:
data.metadata?.timed_refresh_immune_slices || [],
// cross-filters should be enabled by default
cross_filters_enabled: isCrossFiltersEnabled(
metadataCrossFiltersEnabled,
),
},
};

View File

@ -26,6 +26,7 @@ import { applyDefaultFormData } from 'src/explore/store';
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { findPermission } from 'src/utils/findPermission';
import { canUserEditDashboard } from 'src/dashboard/util/permissionUtils';
import { isCrossFiltersEnabled } from 'src/dashboard/util/crossFilters';
import {
DASHBOARD_FILTER_SCOPE_GLOBAL,
dashboardFilter,
@ -394,6 +395,9 @@ export const hydrateDashboard =
const { roles } = user;
const canEdit = canUserEditDashboard(dashboard, user);
const crossFiltersEnabled = isCrossFiltersEnabled(
metadata.cross_filters_enabled,
);
return dispatch({
type: HYDRATE_DASHBOARD,
@ -433,6 +437,7 @@ export const hydrateDashboard =
(isFeatureEnabled(FeatureFlag.HORIZONTAL_FILTER_BAR) &&
metadata.filter_bar_orientation) ||
FilterBarOrientation.VERTICAL,
crossFiltersEnabled,
},
dataMask,
dashboardFilters,

View File

@ -169,6 +169,9 @@ const SliceHeader: FC<SliceHeaderProps> = ({
const crossFilterValue = useSelector<RootState, any>(
state => state.dataMask[slice?.slice_id]?.filterState?.value,
);
const isCrossFiltersEnabled = useSelector<RootState, boolean>(
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
);
const indicator = useMemo(
() => ({
@ -289,6 +292,7 @@ const SliceHeader: FC<SliceHeaderProps> = ({
chartStatus={chartStatus}
formData={formData}
exploreUrl={exploreUrl}
crossFiltersEnabled={isCrossFiltersEnabled}
/>
)}
</>

View File

@ -120,7 +120,6 @@ export interface SliceHeaderControlsProps {
slice_name: string;
slice_id: number;
slice_description: string;
form_data?: { emit_filter?: boolean };
datasource: string;
};
@ -151,6 +150,8 @@ export interface SliceHeaderControlsProps {
supersetCanShare?: boolean;
supersetCanCSV?: boolean;
sliceCanEdit?: boolean;
crossFiltersEnabled?: boolean;
}
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps &
RouteComponentProps;
@ -345,6 +346,7 @@ class SliceHeaderControls extends React.PureComponent<
addDangerToast = () => {},
supersetCanShare = false,
isCached = [],
crossFiltersEnabled,
} = this.props;
const crossFilterItems = getChartMetadataRegistry().items;
const isTable = slice.viz_type === 'table';
@ -354,7 +356,6 @@ class SliceHeaderControls extends React.PureComponent<
value.behaviors?.includes(Behavior.INTERACTIVE_CHART),
)
.find(([key]) => key === slice.viz_type);
const canEmitCrossFilter = slice.form_data?.emit_filter;
const cachedWhen = (cachedDttm || []).map(itemCachedDttm =>
moment.utc(itemCachedDttm).fromNow(),
@ -381,6 +382,7 @@ class SliceHeaderControls extends React.PureComponent<
const fullscreenLabel = isFullSize
? t('Exit fullscreen')
: t('Enter fullscreen');
const menu = (
<Menu
onClick={this.handleMenuClick}
@ -477,7 +479,7 @@ class SliceHeaderControls extends React.PureComponent<
{isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
isCrossFilter &&
canEmitCrossFilter && (
crossFiltersEnabled && (
<>
<Menu.Item key={MENU_KEYS.CROSS_FILTER_SCOPING}>
{t('Cross-filter scoping')}

View File

@ -97,6 +97,7 @@ const propTypes = {
postTransformProps: PropTypes.func,
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
isInView: PropTypes.bool,
emitCrossFilters: PropTypes.bool,
};
const defaultProps = {
@ -408,6 +409,7 @@ class Chart extends React.Component {
postTransformProps,
datasetsStatus,
isInView,
emitCrossFilters,
logEvent,
} = this.props;
@ -438,6 +440,7 @@ class Chart extends React.Component {
filterId: id,
})
: {};
return (
<SliceContainer
className="chart-slice"
@ -540,6 +543,7 @@ class Chart extends React.Component {
postTransformProps={postTransformProps}
datasetsStatus={datasetsStatus}
isInView={isInView}
emitCrossFilters={emitCrossFilters}
/>
</ChartWrapper>
</SliceContainer>

View File

@ -1,175 +0,0 @@
/**
* 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 React from 'react';
import fetchMock from 'fetch-mock';
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen, within } from 'spec/helpers/testing-library';
import { DashboardInfo, FilterBarOrientation } from 'src/dashboard/types';
import * as mockedMessageActions from 'src/components/MessageToasts/actions';
import FilterBarOrientationSelect from '.';
const initialState: { dashboardInfo: DashboardInfo } = {
dashboardInfo: {
id: 1,
userId: '1',
metadata: {
native_filter_configuration: {},
show_native_filters: true,
chart_configuration: {},
color_scheme: '',
color_namespace: '',
color_scheme_domain: [],
label_colors: {},
shared_label_colors: {},
},
json_metadata: '',
dash_edit_perm: true,
filterBarOrientation: FilterBarOrientation.VERTICAL,
common: {
conf: {},
flash_messages: [],
},
},
};
const setup = (dashboardInfoOverride: Partial<DashboardInfo> = {}) =>
render(<FilterBarOrientationSelect />, {
useRedux: true,
initialState: {
...initialState,
dashboardInfo: {
...initialState.dashboardInfo,
...dashboardInfoOverride,
},
},
});
test('Dropdown trigger renders', () => {
setup();
expect(screen.getByLabelText('gear')).toBeVisible();
});
test('Popover opens with "Vertical" selected', async () => {
setup();
userEvent.click(screen.getByLabelText('gear'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[0]).getByLabelText('check'),
).toBeInTheDocument();
});
test('Popover opens with "Horizontal" selected', async () => {
setup({ filterBarOrientation: FilterBarOrientation.HORIZONTAL });
userEvent.click(screen.getByLabelText('gear'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).getByLabelText('check'),
).toBeInTheDocument();
});
test('On selection change, send request and update checked value', async () => {
fetchMock.reset();
fetchMock.put('glob:*/api/v1/dashboard/1', {
result: {
json_metadata: JSON.stringify({
...initialState.dashboardInfo.metadata,
filter_bar_orientation: 'HORIZONTAL',
}),
},
});
setup();
userEvent.click(screen.getByLabelText('gear'));
expect(
within(screen.getAllByRole('menuitem')[0]).getByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).queryByLabelText('check'),
).not.toBeInTheDocument();
userEvent.click(await screen.findByText('Horizontal (Top)'));
// 1st check - checkmark appears immediately after click
expect(
await within(screen.getAllByRole('menuitem')[1]).findByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[0]).queryByLabelText('check'),
).not.toBeInTheDocument();
// successful query
await waitFor(() =>
expect(fetchMock.lastCall()?.[1]?.body).toEqual(
JSON.stringify({
json_metadata: JSON.stringify({
...initialState.dashboardInfo.metadata,
filter_bar_orientation: 'HORIZONTAL',
}),
}),
),
);
// 2nd check - checkmark stays after successful query
expect(
await within(screen.getAllByRole('menuitem')[1]).findByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[0]).queryByLabelText('check'),
).not.toBeInTheDocument();
fetchMock.reset();
});
test('On failed request, restore previous selection', async () => {
fetchMock.reset();
fetchMock.put('glob:*/api/v1/dashboard/1', 400);
const dangerToastSpy = jest.spyOn(mockedMessageActions, 'addDangerToast');
setup();
userEvent.click(screen.getByLabelText('gear'));
expect(
within(screen.getAllByRole('menuitem')[0]).getByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).queryByLabelText('check'),
).not.toBeInTheDocument();
userEvent.click(await screen.findByText('Horizontal (Top)'));
// checkmark gets rolled back to the original selection after successful query
expect(
await within(screen.getAllByRole('menuitem')[0]).findByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).queryByLabelText('check'),
).not.toBeInTheDocument();
expect(dangerToastSpy).toHaveBeenCalledWith(
'Sorry, there was an error saving this dashboard: Unknown Error',
);
fetchMock.reset();
});

View File

@ -1,88 +0,0 @@
/**
* 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 React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { t, useTheme } from '@superset-ui/core';
import { MenuProps } from 'src/components/Menu';
import { FilterBarOrientation, RootState } from 'src/dashboard/types';
import { saveFilterBarOrientation } from 'src/dashboard/actions/dashboardInfo';
import Icons from 'src/components/Icons';
import DropdownSelectableIcon from 'src/components/DropdownSelectableIcon';
const FilterBarOrientationSelect = () => {
const dispatch = useDispatch();
const theme = useTheme();
const filterBarOrientation = useSelector<RootState, FilterBarOrientation>(
({ dashboardInfo }) => dashboardInfo.filterBarOrientation,
);
const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] =
useState(filterBarOrientation);
const toggleFilterBarOrientation = useCallback(
async (
selection: Parameters<
Required<Pick<MenuProps, 'onSelect'>>['onSelect']
>[0],
) => {
const selectedKey = selection.key as FilterBarOrientation;
if (selectedKey !== filterBarOrientation) {
// set displayed selection in local state for immediate visual response after clicking
setSelectedFilterBarOrientation(selectedKey);
try {
// save selection in Redux and backend
await dispatch(
saveFilterBarOrientation(selection.key as FilterBarOrientation),
);
} catch {
// revert local state in case of error when saving
setSelectedFilterBarOrientation(filterBarOrientation);
}
}
},
[dispatch, filterBarOrientation],
);
return (
<DropdownSelectableIcon
onSelect={toggleFilterBarOrientation}
info={t('Orientation of filter bar')}
icon={
<Icons.Gear
name="gear"
iconColor={theme.colors.grayscale.base}
data-test="filterbar-orientation-icon"
/>
}
menuItems={[
{
key: FilterBarOrientation.VERTICAL,
label: t('Vertical (Left)'),
},
{
key: FilterBarOrientation.HORIZONTAL,
label: t('Horizontal (Top)'),
},
]}
selectedKeys={[selectedFilterBarOrientation]}
/>
);
};
export default FilterBarOrientationSelect;

View File

@ -0,0 +1,299 @@
/**
* 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 React from 'react';
import fetchMock from 'fetch-mock';
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen, within } from 'spec/helpers/testing-library';
import { DashboardInfo, FilterBarOrientation } from 'src/dashboard/types';
import * as mockedMessageActions from 'src/components/MessageToasts/actions';
import { FeatureFlag } from '@superset-ui/core';
import FilterBarSettings from '.';
const initialState: { dashboardInfo: DashboardInfo } = {
dashboardInfo: {
id: 1,
userId: '1',
metadata: {
native_filter_configuration: {},
show_native_filters: true,
chart_configuration: {},
color_scheme: '',
color_namespace: '',
color_scheme_domain: [],
label_colors: {},
shared_label_colors: {},
cross_filters_enabled: false,
},
json_metadata: '',
dash_edit_perm: true,
filterBarOrientation: FilterBarOrientation.VERTICAL,
common: {
conf: {},
flash_messages: [],
},
crossFiltersEnabled: true,
},
};
const setup = (dashboardInfoOverride: Partial<DashboardInfo> = {}) =>
waitFor(() =>
render(<FilterBarSettings />, {
useRedux: true,
initialState: {
...initialState,
dashboardInfo: {
...initialState.dashboardInfo,
...dashboardInfoOverride,
},
},
}),
);
test('Dropdown trigger renders with FF HORIZONTAL_FILTER_BAR on', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
await setup();
expect(screen.getByLabelText('gear')).toBeVisible();
});
test('Dropdown trigger does not render with FF HORIZONTAL_FILTER_BAR off', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: false,
};
await setup();
expect(screen.queryByLabelText('gear')).not.toBeInTheDocument();
});
test('Dropdown trigger renders with dashboard edit permissions', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
await setup({
dash_edit_perm: true,
});
expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument();
});
test('Dropdown trigger does not render without dashboard edit permissions', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
await setup({
dash_edit_perm: false,
});
expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument();
});
test('Dropdown trigger renders with FF DASHBOARD_CROSS_FILTERS on', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_CROSS_FILTERS]: true,
};
await setup();
expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument();
});
test('Dropdown trigger does not render with FF DASHBOARD_CROSS_FILTERS off', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_CROSS_FILTERS]: false,
};
await setup();
expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument();
});
test('Popover shows cross-filtering option on by default', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_CROSS_FILTERS]: true,
};
await setup();
userEvent.click(screen.getByLabelText('gear'));
expect(screen.getByText('Enable cross-filtering')).toBeInTheDocument();
expect(screen.getByRole('checkbox')).toBeChecked();
});
test('Can enable/disable cross-filtering', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_CROSS_FILTERS]: true,
};
fetchMock.reset();
fetchMock.put('glob:*/api/v1/dashboard/1', {
result: {},
});
await setup();
userEvent.click(screen.getByLabelText('gear'));
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked();
userEvent.click(checkbox);
userEvent.click(screen.getByLabelText('gear'));
expect(checkbox).not.toBeChecked();
});
test('Popover opens with "Vertical" selected', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
await setup();
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).getByLabelText('check'),
).toBeInTheDocument();
});
test('Popover opens with "Horizontal" selected', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
await setup({ filterBarOrientation: FilterBarOrientation.HORIZONTAL });
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[2]).getByLabelText('check'),
).toBeInTheDocument();
});
test('On selection change, send request and update checked value', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
fetchMock.reset();
fetchMock.put('glob:*/api/v1/dashboard/1', {
result: {
json_metadata: JSON.stringify({
...initialState.dashboardInfo.metadata,
filter_bar_orientation: 'HORIZONTAL',
}),
},
});
await setup();
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).getByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[2]).queryByLabelText('check'),
).not.toBeInTheDocument();
userEvent.click(await screen.findByText('Horizontal (Top)'));
// 1st check - checkmark appears immediately after click
expect(
await within(screen.getAllByRole('menuitem')[2]).findByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).queryByLabelText('check'),
).not.toBeInTheDocument();
// successful query
await waitFor(() =>
expect(fetchMock.lastCall()?.[1]?.body).toEqual(
JSON.stringify({
json_metadata: JSON.stringify({
...initialState.dashboardInfo.metadata,
filter_bar_orientation: 'HORIZONTAL',
}),
}),
),
);
// 2nd check - checkmark stays after successful query
expect(
await within(screen.getAllByRole('menuitem')[2]).findByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).queryByLabelText('check'),
).not.toBeInTheDocument();
fetchMock.reset();
});
test('On failed request, restore previous selection', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.HORIZONTAL_FILTER_BAR]: true,
};
fetchMock.reset();
fetchMock.put('glob:*/api/v1/dashboard/1', 400);
const dangerToastSpy = jest.spyOn(mockedMessageActions, 'addDangerToast');
await setup();
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[1]).getByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[2]).queryByLabelText('check'),
).not.toBeInTheDocument();
userEvent.click(await screen.findByText('Horizontal (Top)'));
await waitFor(() => {
expect(dangerToastSpy).toHaveBeenCalledWith(
'Sorry, there was an error saving this dashboard: Unknown Error',
);
});
userEvent.click(screen.getByLabelText('gear'));
userEvent.hover(screen.getByText('Orientation of filter bar'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
// checkmark gets rolled back to the original selection after successful query
expect(
await within(screen.getAllByRole('menuitem')[1]).findByLabelText('check'),
).toBeInTheDocument();
expect(
within(screen.getAllByRole('menuitem')[2]).queryByLabelText('check'),
).not.toBeInTheDocument();
fetchMock.reset();
});

View File

@ -0,0 +1,192 @@
/**
* 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 React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
FeatureFlag,
isFeatureEnabled,
styled,
t,
useTheme,
} from '@superset-ui/core';
import { MenuProps } from 'src/components/Menu';
import { FilterBarOrientation, RootState } from 'src/dashboard/types';
import {
saveFilterBarOrientation,
saveCrossFiltersSetting,
} from 'src/dashboard/actions/dashboardInfo';
import Icons from 'src/components/Icons';
import DropdownSelectableIcon, {
DropDownSelectableProps,
} from 'src/components/DropdownSelectableIcon';
import Checkbox from 'src/components/Checkbox';
import { clearDataMaskState } from 'src/dataMask/actions';
type SelectedKey = FilterBarOrientation | string | number;
const StyledMenuLabel = styled.span`
display: flex;
align-items: center;
justify-content: space-between;
.enable-cross-filters-text {
padding-left: ${({ theme }) => `${theme.gridUnit * 2}px`};
}
`;
const StyledCheckbox = styled(Checkbox)`
${({ theme }) => `
&,
svg {
display: inline-block;
width: ${theme.gridUnit * 4}px;
height: ${theme.gridUnit * 4}px;
}
`}
`;
const FilterBarSettings = () => {
const dispatch = useDispatch();
const theme = useTheme();
const isCrossFiltersEnabled = useSelector<RootState, boolean>(
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
);
const filterBarOrientation = useSelector<RootState, FilterBarOrientation>(
({ dashboardInfo }) => dashboardInfo.filterBarOrientation,
);
const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] =
useState(filterBarOrientation);
const isCrossFiltersFeatureEnabled = isFeatureEnabled(
FeatureFlag.DASHBOARD_CROSS_FILTERS,
);
const shouldEnableCrossFilters =
!!isCrossFiltersEnabled && isCrossFiltersFeatureEnabled;
const [crossFiltersEnabled, setCrossFiltersEnabled] = useState<boolean>(
shouldEnableCrossFilters,
);
const canEdit = useSelector<RootState, boolean>(
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);
const canSetHorizontalFilterBar =
canEdit && isFeatureEnabled(FeatureFlag.HORIZONTAL_FILTER_BAR);
const crossFiltersMenuKey = 'cross-filters-menu-key';
const isOrientation = (o: SelectedKey): o is FilterBarOrientation =>
o === FilterBarOrientation.VERTICAL ||
o === FilterBarOrientation.HORIZONTAL;
const updateCrossFiltersSetting = useCallback(
async isEnabled => {
if (!isEnabled) {
dispatch(clearDataMaskState());
}
await dispatch(saveCrossFiltersSetting(isEnabled));
},
[dispatch, crossFiltersEnabled],
);
const changeFilterBarSettings = useCallback(
async (
selection: Parameters<
Required<Pick<MenuProps, 'onSelect'>>['onSelect']
>[0],
) => {
const selectedKey: SelectedKey = selection.key;
if (selectedKey === crossFiltersMenuKey) {
setCrossFiltersEnabled(!crossFiltersEnabled);
updateCrossFiltersSetting(!crossFiltersEnabled);
return;
}
if (isOrientation(selectedKey) && selectedKey !== filterBarOrientation) {
// set displayed selection in local state for immediate visual response after clicking
setSelectedFilterBarOrientation(selectedKey as FilterBarOrientation);
try {
// save selection in Redux and backend
await dispatch(
saveFilterBarOrientation(selection.key as FilterBarOrientation),
);
} catch {
// revert local state in case of error when saving
setSelectedFilterBarOrientation(filterBarOrientation);
}
}
},
[dispatch, crossFiltersEnabled, filterBarOrientation],
);
const crossFiltersMenuItem = useMemo(
() => (
<StyledMenuLabel>
<StyledCheckbox
className="enable-cross-filters"
checked={crossFiltersEnabled}
onChange={checked => setCrossFiltersEnabled(checked || false)}
/>{' '}
<span className="enable-cross-filters-text">
{t('Enable cross-filtering')}
</span>
</StyledMenuLabel>
),
[crossFiltersEnabled],
);
const menuItems: DropDownSelectableProps['menuItems'] = [];
if (isCrossFiltersFeatureEnabled) {
menuItems.unshift({
key: crossFiltersMenuKey,
label: crossFiltersMenuItem,
divider: canSetHorizontalFilterBar,
});
}
if (canSetHorizontalFilterBar) {
menuItems.push({
key: 'placement',
label: t('Orientation of filter bar'),
children: [
{
key: FilterBarOrientation.VERTICAL,
label: t('Vertical (Left)'),
},
{
key: FilterBarOrientation.HORIZONTAL,
label: t('Horizontal (Top)'),
},
],
});
}
if (!menuItems.length) {
return null;
}
return (
<DropdownSelectableIcon
onSelect={changeFilterBarSettings}
icon={
<Icons.Gear
name="gear"
iconColor={theme.colors.grayscale.base}
data-test="filterbar-orientation-icon"
/>
}
menuItems={menuItems}
selectedKeys={[selectedFilterBarOrientation]}
/>
);
};
export default FilterBarSettings;

View File

@ -17,14 +17,7 @@
* under the License.
*/
/* eslint-disable no-param-reassign */
import {
css,
FeatureFlag,
isFeatureEnabled,
styled,
t,
useTheme,
} from '@superset-ui/core';
import { css, styled, t, useTheme } from '@superset-ui/core';
import React, { FC, useMemo } from 'react';
import Icons from 'src/components/Icons';
import Button from 'src/components/Button';
@ -33,7 +26,7 @@ import FilterConfigurationLink from 'src/dashboard/components/nativeFilters/Filt
import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state';
import { RootState } from 'src/dashboard/types';
import { getFilterBarTestId } from '../utils';
import FilterBarOrientationSelect from '../FilterBarOrientationSelect';
import FilterBarSettings from '../FilterBarSettings';
const TitleArea = styled.div`
${({ theme }) => css`
@ -109,14 +102,12 @@ const Header: FC<HeaderProps> = ({ toggleFiltersBar }) => {
const dashboardId = useSelector<RootState, number>(
({ dashboardInfo }) => dashboardInfo.id,
);
const canSetHorizontalFilterBar =
canEdit && isFeatureEnabled(FeatureFlag.HORIZONTAL_FILTER_BAR);
return (
<Wrapper>
<TitleArea>
<span>{t('Filters')}</span>
{canSetHorizontalFilterBar && <FilterBarOrientationSelect />}
<FilterBarSettings />
<HeaderButton
{...getFilterBarTestId('collapse-button')}
buttonStyle="link"

View File

@ -24,7 +24,7 @@ import Loading from 'src/components/Loading';
import FilterControls from './FilterControls/FilterControls';
import { getFilterBarTestId } from './utils';
import { HorizontalBarProps } from './types';
import FilterBarOrientationSelect from './FilterBarOrientationSelect';
import FilterBarSettings from './FilterBarSettings';
import FilterConfigurationLink from './FilterConfigurationLink';
const HorizontalBar = styled.div`
@ -105,7 +105,7 @@ const HorizontalFilterBar: React.FC<HorizontalBarProps> = ({
<Loading position="inline-centered" />
) : (
<>
{canEdit && <FilterBarOrientationSelect />}
<FilterBarSettings />
{canEdit && (
<FiltersLinkContainer hasFilters={hasFilters}>
<FilterConfigurationLink

View File

@ -36,6 +36,11 @@ const renderWrapper = (overrideProps?: Record<string, any>) =>
waitFor(() =>
render(<HorizontalBar {...defaultProps} {...overrideProps} />, {
useRedux: true,
initialState: {
dashboardInfo: {
dash_edit_perm: true,
},
},
}),
);
@ -65,19 +70,6 @@ test('should render the empty message', async () => {
).toBeInTheDocument();
});
test('should render the gear icon', async () => {
await renderWrapper();
expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument();
});
test('should not render the gear icon', async () => {
await renderWrapper({
canEdit: false,
});
expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument();
});
test('should not render the loading icon', async () => {
await renderWrapper();
expect(

View File

@ -102,6 +102,7 @@ function mapStateToProps(
setControlValue,
filterboxMigrationState: dashboardState.filterboxMigrationState,
datasetsStatus,
emitCrossFilters: !!dashboardInfo.crossFiltersEnabled,
};
}

View File

@ -20,6 +20,7 @@
import {
DASHBOARD_INFO_UPDATED,
SET_FILTER_BAR_ORIENTATION,
SET_CROSS_FILTERS_ENABLED,
} from '../actions/dashboardInfo';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
@ -43,6 +44,11 @@ export default function dashboardStateReducer(state = {}, action) {
...state,
filterBarOrientation: action.filterBarOrientation,
};
case SET_CROSS_FILTERS_ENABLED:
return {
...state,
crossFiltersEnabled: action.crossFiltersEnabled,
};
default:
return state;
}

View File

@ -108,7 +108,9 @@ export type DashboardInfo = {
color_scheme_domain: string[];
label_colors: JsonObject;
shared_label_colors: JsonObject;
cross_filters_enabled: boolean;
};
crossFiltersEnabled: boolean;
filterBarOrientation: FilterBarOrientation;
};

View File

@ -17,10 +17,10 @@
* under the License.
*/
import { emitFilterControl } from '@superset-ui/chart-controls';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
describe('isFeatureFlagEnabled', () => {
it('returns empty array for unset feature flag', () => {
expect(emitFilterControl).toHaveLength(0);
});
});
export const isCrossFiltersEnabled = (
metadataCrossFiltersEnabled: boolean | undefined,
): boolean =>
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
(metadataCrossFiltersEnabled === undefined || metadataCrossFiltersEnabled);

View File

@ -145,7 +145,6 @@ const tableVizStore = {
value: true,
},
show_totals: {},
emit_filter: {},
table_timestamp_format: {
value: 'smart_date',
},

View File

@ -41,6 +41,14 @@ interface TimeoutError {
timeout: number;
}
type ErrorType =
| SupersetClientResponse
| TimeoutError
| { response: Response }
| string;
type ErrorTextSource = 'dashboard' | 'chart' | 'query' | 'dataset' | 'database';
export function parseErrorJson(responseObject: JsonObject): ClientErrorObject {
let error = { ...responseObject };
// Backwards compatibility for old error renderers with the new error object
@ -78,6 +86,29 @@ export function parseErrorJson(responseObject: JsonObject): ClientErrorObject {
return { ...error, error: error.error }; // explicit ClientErrorObject
}
/*
* Utility to get standardized error text for generic update failures
*/
export async function getErrorText(
errorObject: ErrorType,
source: ErrorTextSource,
) {
const { error, message } = await getClientErrorObject(errorObject);
let errorText = t('Sorry, an unknown error occurred.');
if (error) {
errorText = t(
'Sorry, there was an error saving this %s: %s',
source,
error,
);
}
if (typeof message === 'string' && message === 'Forbidden') {
errorText = t('You do not have permission to edit this %s', source);
}
return errorText;
}
export function getClientErrorObject(
response:
| SupersetClientResponse

View File

@ -279,6 +279,7 @@ class DashboardDAO(BaseDAO):
md["label_colors"] = data.get("label_colors", {})
md["shared_label_colors"] = data.get("shared_label_colors", {})
md["color_scheme_domain"] = data.get("color_scheme_domain", [])
md["cross_filters_enabled"] = data.get("cross_filters_enabled", True)
dashboard.json_metadata = json.dumps(md)
if commit:

View File

@ -130,6 +130,7 @@ class DashboardJSONMetadataSchema(Schema):
label_colors = fields.Dict()
shared_label_colors = fields.Dict()
color_scheme_domain = fields.List(fields.Str())
cross_filters_enabled = fields.Boolean(default=True)
# used for v0 import/export
import_time = fields.Integer()
remote_id = fields.Integer()

View File

@ -72,7 +72,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
"slug": "slug1_changed",
"position_json": '{"b": "B"}',
"css": "css_changed",
"json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}, "shared_label_colors": {}, "color_scheme_domain": []}',
"json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}, "shared_label_colors": {}, "color_scheme_domain": [], "cross_filters_enabled": false}',
"published": False,
}

View File

@ -177,6 +177,7 @@ dashboards_config_1: Dict[str, Any] = {
"show_native_filters": True,
"color_scheme_domain": [],
"shared_label_colors": {},
"cross_filters_enabled": False,
},
"version": "1.0.0",
},