feat: Move cross filters to Dashboard (#22785)
This commit is contained in:
parent
262c947273
commit
9ed2326a20
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
|
|||
zoomable: false,
|
||||
richTooltip: true,
|
||||
xAxisLabelRotation: 0,
|
||||
emitFilter: false,
|
||||
groupby: [],
|
||||
showValue: false,
|
||||
onlyTotal: false,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
|||
zoomable: boolean;
|
||||
richTooltip: boolean;
|
||||
xAxisLabelRotation: number;
|
||||
emitFilter: boolean;
|
||||
groupby: QueryFormColumn[];
|
||||
showValue: boolean;
|
||||
onlyTotal: boolean;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ interface PivotTableCustomizeProps {
|
|||
rowTotals: boolean;
|
||||
valueFormat: string;
|
||||
setDataMask: SetDataMaskHook;
|
||||
emitFilter?: boolean;
|
||||
emitCrossFilters?: boolean;
|
||||
selectedFilters?: SelectedFiltersType;
|
||||
verboseMap: JsonObject;
|
||||
columnFormats: JsonObject;
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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":{}}',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ const propTypes = {
|
|||
postTransformProps: PropTypes.func,
|
||||
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
||||
isInView: PropTypes.bool,
|
||||
emitCrossFilters: PropTypes.bool,
|
||||
};
|
||||
|
||||
const BLANK = {};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ function mapStateToProps(
|
|||
setControlValue,
|
||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||
datasetsStatus,
|
||||
emitCrossFilters: !!dashboardInfo.crossFiltersEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -145,7 +145,6 @@ const tableVizStore = {
|
|||
value: true,
|
||||
},
|
||||
show_totals: {},
|
||||
emit_filter: {},
|
||||
table_timestamp_format: {
|
||||
value: 'smart_date',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue