From 9ed2326a20329d41abc8e0995b0ba6110379088f Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Wed, 25 Jan 2023 18:01:06 +0100 Subject: [PATCH] feat: Move cross filters to Dashboard (#22785) --- .../integration/chart_list/filter.test.ts | 94 ++---- .../dashboard/nativeFilters.test.ts | 6 +- .../integration/dashboard_list/filter.test.ts | 60 +--- .../cypress-base/cypress/utils/index.ts | 6 +- .../src/sections/echartsTimeSeriesQuery.tsx | 2 - .../ColumnConfigControl.tsx | 17 - .../ColumnConfigControl/constants.tsx | 13 - .../src/shared-controls/customControls.tsx | 19 -- .../src/chart/models/ChartProps.ts | 7 + .../src/BoxPlot/EchartsBoxPlot.tsx | 4 +- .../src/BoxPlot/controlPanel.ts | 2 - .../src/BoxPlot/transformProps.ts | 4 +- .../plugin-chart-echarts/src/BoxPlot/types.ts | 2 - .../src/Funnel/EchartsFunnel.tsx | 4 +- .../src/Funnel/controlPanel.tsx | 2 - .../src/Funnel/transformProps.ts | 4 +- .../plugin-chart-echarts/src/Funnel/types.ts | 2 - .../src/Gauge/EchartsGauge.tsx | 4 +- .../src/Gauge/controlPanel.tsx | 2 - .../src/Gauge/transformProps.ts | 15 +- .../plugin-chart-echarts/src/Gauge/types.ts | 2 - .../EchartsMixedTimeseries.tsx | 6 +- .../src/MixedTimeseries/controlPanel.tsx | 9 - .../src/MixedTimeseries/transformProps.ts | 6 +- .../src/MixedTimeseries/types.ts | 2 - .../src/Pie/EchartsPie.tsx | 4 +- .../src/Pie/controlPanel.tsx | 2 - .../src/Pie/transformProps.ts | 4 +- .../plugin-chart-echarts/src/Pie/types.ts | 2 - .../src/Radar/EchartsRadar.tsx | 4 +- .../src/Radar/controlPanel.tsx | 2 - .../src/Radar/transformProps.ts | 4 +- .../plugin-chart-echarts/src/Radar/types.ts | 2 - .../src/Sunburst/EchartsSunburst.tsx | 7 +- .../src/Sunburst/controlPanel.tsx | 2 - .../src/Sunburst/transformProps.ts | 4 +- .../src/Sunburst/types.ts | 2 - .../src/Timeseries/EchartsTimeseries.tsx | 7 +- .../src/Timeseries/constants.ts | 1 - .../src/Timeseries/transformProps.ts | 4 +- .../src/Timeseries/types.ts | 1 - .../src/Treemap/EchartsTreemap.tsx | 4 +- .../src/Treemap/controlPanel.tsx | 2 - .../src/Treemap/transformProps.ts | 4 +- .../plugin-chart-echarts/src/Treemap/types.ts | 2 - .../plugins/plugin-chart-echarts/src/types.ts | 2 +- .../test/BoxPlot/buildQuery.test.ts | 1 - .../test/MixedTimeseries/buildQuery.test.ts | 2 - .../src/plugin/controlPanel.tsx | 2 - .../plugin-chart-handlebars/src/types.ts | 1 - .../src/PivotTableChart.tsx | 10 +- .../src/plugin/controlPanel.tsx | 2 - .../src/plugin/transformProps.ts | 4 +- .../plugin-chart-pivot-table/src/types.ts | 2 +- .../test/plugin/transformProps.test.ts | 3 +- .../plugin-chart-table/src/TableChart.tsx | 28 +- .../plugin-chart-table/src/controlPanel.tsx | 3 - .../plugin-chart-table/src/transformProps.ts | 4 +- .../plugins/plugin-chart-table/src/types.ts | 3 +- .../spec/fixtures/mockDashboardState.js | 2 +- .../src/components/Chart/Chart.jsx | 1 + .../src/components/Chart/ChartRenderer.jsx | 7 +- .../src/components/Checkbox/Checkbox.tsx | 9 +- .../DropdownSelectableIcon/index.tsx | 73 ++++- .../src/dashboard/actions/dashboardInfo.ts | 54 +++- .../src/dashboard/actions/dashboardState.js | 7 +- .../src/dashboard/actions/hydrate.js | 5 + .../components/SliceHeader/index.tsx | 4 + .../components/SliceHeaderControls/index.tsx | 8 +- .../components/gridComponents/Chart.jsx | 4 + .../FilterBarOrientationSelect.test.tsx | 175 ---------- .../FilterBarOrientationSelect/index.tsx | 88 ------ .../FilterBarSettings.test.tsx | 299 ++++++++++++++++++ .../FilterBar/FilterBarSettings/index.tsx | 192 +++++++++++ .../nativeFilters/FilterBar/Header/index.tsx | 15 +- .../nativeFilters/FilterBar/Horizontal.tsx | 4 +- .../FilterBar/HorizontalFilterBar.test.tsx | 18 +- .../src/dashboard/containers/Chart.jsx | 1 + .../src/dashboard/reducers/dashboardInfo.js | 6 + superset-frontend/src/dashboard/types.ts | 2 + .../dashboard/util/crossFilters.ts} | 12 +- .../controlUtils/standardizedFormData.test.ts | 1 - .../src/utils/getClientErrorObject.ts | 31 ++ superset/dashboards/dao.py | 1 + superset/dashboards/schemas.py | 1 + .../integration_tests/dashboards/api_tests.py | 2 +- tests/unit_tests/fixtures/assets_configs.py | 1 + 87 files changed, 811 insertions(+), 637 deletions(-) delete mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx delete mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx rename superset-frontend/{packages/superset-ui-chart-controls/test/shared-controls/emitFilterControl.test.tsx => src/dashboard/util/crossFilters.ts} (72%) diff --git a/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts index b632969c6b79..acd11669bea1 100644 --- a/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts @@ -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'); - }); - - 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 allow filtering by "Created by" correctly', () => { + setFilter('Created by', 'alpha user'); + setFilter('Created by', 'admin user'); + }); - 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 allow filtering by "Chart type" correctly', () => { + setFilter('Chart type', 'Area Chart (legacy)'); + setFilter('Chart type', 'Bubble Chart'); + }); - 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 allow filtering by "Dataset" correctly', () => { + setFilter('Dataset', 'energy_usage'); + setFilter('Dataset', 'unicode_test'); + }); - 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'); }); }); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts index ded53e64eedd..b378388da314 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts @@ -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') diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts index ffd6626132bb..4654b3b5c263 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts @@ -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'); - }); - - 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 allow filtering by "Created by" correctly', () => { + setFilter('Created by', 'alpha user'); + setFilter('Created by', 'admin user'); + }); - 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'); }); }); diff --git a/superset-frontend/cypress-base/cypress/utils/index.ts b/superset-frontend/cypress-base/cypress/utils/index.ts index b685cc01e898..2f06efc22c25 100644 --- a/superset-frontend/cypress-base/cypress/utils/index.ts +++ b/superset-frontend/cypress-base/cypress/utils/index.ts @@ -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): Slice => ({ diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx index 296f8d9da820..cd58780d892d 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx @@ -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'], diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx index 55c560cb7935..548dd4ae4d7f 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx @@ -40,7 +40,6 @@ export type ColumnConfigControlProps = queryResponse?: ChartDataResponseResult; configFormLayout?: ColumnConfigFormLayout; appliedColumnNames?: string[]; - emitFilter: boolean; }; /** @@ -57,24 +56,8 @@ export default function ColumnConfigControl({ value, onChange, configFormLayout = DEFAULT_CONFIG_FORM_LAYOUT, - emitFilter, ...props }: ColumnConfigControlProps) { - 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[] = []; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx index a749e5a7cc0d..b940375aa9eb 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx @@ -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'), diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx index 5ece20ac0600..979912e58f1d 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx @@ -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: { diff --git a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts index 5e4f044942a6..e02aeca4f54d 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts @@ -144,6 +144,8 @@ export default class ChartProps { inContextMenu?: boolean; + emitCrossFilters?: boolean; + theme: SupersetTheme; constructor(config: ChartPropsConfig & { formData?: FormData } = {}) { @@ -164,6 +166,7 @@ export default class ChartProps { isRefreshing, inputRef, inContextMenu = false, + emitCrossFilters = false, theme, } = config; this.width = width; @@ -184,6 +187,7 @@ export default class ChartProps { 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, }), ); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx index 135d31317e22..4c18f7f7d620 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts index 95b8e6d966ee..f1db7534d086 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts @@ -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'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts index 337807e229a6..54a1cbfd205a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts index dcbc9da17aa9..6cdc57a26de0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts @@ -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, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx index c492500e8e9a..88ccae8ecc80 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx index f8acdcf6bef7..929069b2ed22 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx @@ -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', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts index 30b5ccbfd987..282bc42381ca 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts index 841722ce4bc5..15adbc208041 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts @@ -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 = diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx index 7ffb571b7948..8c4ca420d90b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx index 7ffa62fda0ff..1af718a989b1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx @@ -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', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts index d84078ad16eb..b2e31c3cc188 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts @@ -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 || [], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts index 9f2c08fd5f0b..02cda2db7f64 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts @@ -52,7 +52,6 @@ export type EchartsGaugeFormData = QueryFormData & { intervals: string; intervalColorIndices: string; valueFormatter: string; - emitFilter: boolean; }; export const DEFAULT_FORM_DATA: Partial = { @@ -76,7 +75,6 @@ export const DEFAULT_FORM_DATA: Partial = { intervals: '', intervalColorIndices: '', valueFormatter: '{value}', - emitFilter: false, }; export interface EchartsGaugeChartProps diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx index 1d1cd6547752..b532c7d9da77 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 6cbed6901bcd..c1a300778588 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -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}`, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index a39ca1d4d57e..98367daf9a84 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index a39e556cac6f..3ec9b2a4b6d3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -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 & ContextMenuTransformedProps & CrossFilterTransformedProps & { - emitFilterB: boolean; groupbyB: QueryFormColumn[]; labelMapB: Record; seriesBreakdown: number; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx index 6de4c8423dc4..9fecfeac0ef7 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx index c9f0f79d19ec..1a2d230b7497 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx @@ -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'], [ { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts index 426111708210..4db36e864e9d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts @@ -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, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts index 4b45751a23cb..d4acbb951710 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts @@ -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', }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx index 1291b69c1240..169240e8a45d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx index 32659bb20f61..3c320b05c4a0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx @@ -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', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts index 04c928036ca6..f185859f4eee 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts index 97e3c1a32a85..ca7cdbd2c2db 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts @@ -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, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx index 08f381b15d32..3dd8dc931aeb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx @@ -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 = { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx index 9c4c5ae5bc46..7ad3618a5ad9 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx @@ -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'], [ { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts index 4677e1af694c..9873d1dc324e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts @@ -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 || [], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts index fdd834c94e6d..91030fd7b5f0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts @@ -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 = { labelType: EchartsSunburstLabelType.Key, showLabels: false, dateFormat: 'smart_date', - emitFilter: false, }; export interface EchartsSunburstChartProps diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index 2ffdb0e87bdc..0cf7f3cf192c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -49,8 +49,9 @@ export default function EchartsTimeseries({ xValueFormatter, xAxis, refs, + emitCrossFilters, }: TimeseriesChartTransformedProps) { - const { emitFilter, stack } = formData; + const { stack } = formData; const echartRef = useRef(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 = { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts index 2590441ef67f..67a3b234164d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts @@ -57,7 +57,6 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = { zoomable: false, richTooltip: true, xAxisLabelRotation: 0, - emitFilter: false, groupby: [], showValue: false, onlyTotal: false, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index fa947b421b9f..eadded44a9ac 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index 82a204e38d7f..56527ebd63bf 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -80,7 +80,6 @@ export type EchartsTimeseriesFormData = QueryFormData & { zoomable: boolean; richTooltip: boolean; xAxisLabelRotation: number; - emitFilter: boolean; groupby: QueryFormColumn[]; showValue: boolean; onlyTotal: boolean; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx index 8559688939b0..0a2f01b4049c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx @@ -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; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx index befdce1b10dd..dbe2e97e6ee9 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx @@ -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, ], }, { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts index eb1b64ef3006..b220b55b3d3c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts @@ -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 || [], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts index 8d41b9d2e40b..1d4298838918 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts @@ -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 = { showLabels: true, showUpperLabels: true, dateFormat: 'smart_date', - emitFilter: false, }; export interface TreemapSeriesCallbackDataParams extends CallbackDataParams { treePathInfo?: TreePathInfo[]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts index 5bd56eb8ed0d..c24e2d84f46d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts @@ -130,12 +130,12 @@ export interface BaseTransformedProps { } export type CrossFilterTransformedProps = { - emitFilter: boolean; groupby: QueryFormColumn[]; labelMap: Record; setControlValue?: HandlerFunction; setDataMask: SetDataMaskHook; selectedValues: Record; + emitCrossFilters?: boolean; }; export type ContextMenuTransformedProps = { diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts index 304f5b7065cf..0d0f2f838990 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts @@ -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', diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts index 3796870fd883..066b796b5974 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts @@ -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 diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx index e34394b3043a..03904e54c83f 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx @@ -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, ], }, { diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts index 2a363059fa7d..741d3b982c48 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts @@ -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; diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx index 2063ab95dee6..a5feed70f21b 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx index 28d6361507fd..1b01e37a4c11 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx @@ -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'], [ { diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts index 546882274f0f..43d73e6193ea 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts @@ -80,6 +80,7 @@ export default function transformProps(chartProps: ChartProps) { 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) { rowTotals, valueFormat, dateFormat, - emitFilter, metricsLayout, conditionalFormatting, timeGrainSqla, @@ -157,7 +157,7 @@ export default function transformProps(chartProps: ChartProps) { colTotals, rowTotals, valueFormat, - emitFilter, + emitCrossFilters, setDataMask, selectedFilters, verboseMap, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts index 9c0523b582b6..24d78e2dcaec 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts @@ -65,7 +65,7 @@ interface PivotTableCustomizeProps { rowTotals: boolean; valueFormat: string; setDataMask: SetDataMaskHook; - emitFilter?: boolean; + emitCrossFilters?: boolean; selectedFilters?: SelectedFiltersType; verboseMap: JsonObject; columnFormats: JsonObject; diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts index 26c938e371bc..3edb4619afce 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts @@ -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: {}, }); }); diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index af490395f369..3c4c74af9e10 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -222,13 +222,13 @@ export default function TableChart( 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( const handleChange = useCallback( (filters: { [x: string]: DataRecordValue[] }) => { - if (!emitFilter) { + if (!emitCrossFilters) { return; } @@ -289,7 +289,7 @@ export default function TableChart( }, }); }, - [emitFilter, setDataMask], + [emitCrossFilters, setDataMask], ); // only take relevant page size options @@ -322,27 +322,21 @@ export default function TableChart( [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( getValueRange(key, alignPositiveNegative); let className = ''; - if (emitFilter) { + if (emitCrossFilters) { className += ' dt-is-filter'; } @@ -459,7 +453,7 @@ export default function TableChart( // 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( [ defaultAlignPN, defaultColorPN, - emitFilter, + emitCrossFilters, getValueRange, isActiveFilterValue, isRawRecords, diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index e7e6b3d9852f..35c5d3970a9e 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -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, }; }, }, diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts index bca48e63403c..2fae16c31c87 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts @@ -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, diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts index 1a6f06f4f886..3a591e8682ed 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts @@ -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; allow_rearrange_columns?: boolean; @@ -108,7 +107,7 @@ export interface TableChartTransformedProps { // 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; diff --git a/superset-frontend/spec/fixtures/mockDashboardState.js b/superset-frontend/spec/fixtures/mockDashboardState.js index b605443af6b0..0895ccf38695 100644 --- a/superset-frontend/spec/fixtures/mockDashboardState.js +++ b/superset-frontend/spec/fixtures/mockDashboardState.js @@ -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","":"#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","":"#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","":"#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","":"#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":{}}', }, }; diff --git a/superset-frontend/src/components/Chart/Chart.jsx b/superset-frontend/src/components/Chart/Chart.jsx index aac44a5186c5..7e9f6aa482e1 100644 --- a/superset-frontend/src/components/Chart/Chart.jsx +++ b/superset-frontend/src/components/Chart/Chart.jsx @@ -77,6 +77,7 @@ const propTypes = { postTransformProps: PropTypes.func, datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']), isInView: PropTypes.bool, + emitCrossFilters: PropTypes.bool, }; const BLANK = {}; diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index e1d3f7290adc..588e2b4e4dfb 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -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} /> diff --git a/superset-frontend/src/components/Checkbox/Checkbox.tsx b/superset-frontend/src/components/Checkbox/Checkbox.tsx index 7162929a967f..249fca01841e 100644 --- a/superset-frontend/src/components/Checkbox/Checkbox.tsx +++ b/superset-frontend/src/components/Checkbox/Checkbox.tsx @@ -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 ( {checked ? : } diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx index b6c5d89a2e4e..85c4284439f1 100644 --- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx +++ b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx @@ -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 { ref?: RefObject; 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, + ) => ( + + + {label} + {selectedKeys?.includes(key) && ( + + )} + + + ); const overlayMenu = useMemo( () => ( @@ -86,14 +126,19 @@ export default (props: DropDownSelectableProps) => { {info} )} - {menuItems.map(m => ( - - {m.label} - {selectedKeys?.includes(m.key) && ( - - )} - - ))} + {menuItems.map(m => + m.children?.length ? ( + + {m.children.map(s => menuItem(s.label, s.key))} + + ) : ( + menuItem(m.label, m.key, m.divider) + ), + )} ), [info, menuItems], diff --git a/superset-frontend/src/dashboard/actions/dashboardInfo.ts b/superset-frontend/src/dashboard/actions/dashboardInfo.ts index dbec0cd1cc26..bbc06d37b0ad 100644 --- a/superset-frontend/src/dashboard/actions/dashboardInfo.ts +++ b/superset-frontend/src/dashboard/actions/dashboardInfo.ts @@ -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.'); + const errorText = await getErrorText(errorObject, 'dashboard'); + dispatch(addDangerToast(errorText)); + throw errorObject; + } + }; +} - if (error) { - errorText = t( - 'Sorry, there was an error saving this dashboard: %s', - error, - ); +export function saveCrossFiltersSetting(crossFiltersEnabled: boolean) { + return async (dispatch: Dispatch, getState: () => RootState) => { + const { id, metadata } = getState().dashboardInfo; + const updateDashboard = makeApi< + Partial, + { result: Partial; 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 (typeof message === 'string' && message === 'Forbidden') { - errorText = t('You do not have permission to edit this dashboard'); + if (lastModifiedTime) { + dispatch(onSave(lastModifiedTime)); } + } catch (errorObject) { + const errorText = await getErrorText(errorObject, 'dashboard'); dispatch(addDangerToast(errorText)); throw errorObject; } diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 518dd7b5dc31..9f45b986fcdb 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -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, + ), }, }; diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index ed359a8cee10..efae72151ef9 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -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, diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx index 2c6e6d84ad2c..a57a7cd22038 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx @@ -169,6 +169,9 @@ const SliceHeader: FC = ({ const crossFilterValue = useSelector( state => state.dataMask[slice?.slice_id]?.filterState?.value, ); + const isCrossFiltersEnabled = useSelector( + ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled, + ); const indicator = useMemo( () => ({ @@ -289,6 +292,7 @@ const SliceHeader: FC = ({ chartStatus={chartStatus} formData={formData} exploreUrl={exploreUrl} + crossFiltersEnabled={isCrossFiltersEnabled} /> )} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index cccab1690a36..4037234f74fd 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -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 = ( {t('Cross-filter scoping')} diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 082088f1116c..506fefd436cc 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -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 ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx deleted file mode 100644 index 28a40aad057c..000000000000 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/FilterBarOrientationSelect.test.tsx +++ /dev/null @@ -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 = {}) => - render(, { - 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(); -}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx deleted file mode 100644 index 50e57fd86e0d..000000000000 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarOrientationSelect/index.tsx +++ /dev/null @@ -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( - ({ dashboardInfo }) => dashboardInfo.filterBarOrientation, - ); - const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] = - useState(filterBarOrientation); - - const toggleFilterBarOrientation = useCallback( - async ( - selection: Parameters< - Required>['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 ( - - } - menuItems={[ - { - key: FilterBarOrientation.VERTICAL, - label: t('Vertical (Left)'), - }, - { - key: FilterBarOrientation.HORIZONTAL, - label: t('Horizontal (Top)'), - }, - ]} - selectedKeys={[selectedFilterBarOrientation]} - /> - ); -}; - -export default FilterBarOrientationSelect; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx new file mode 100644 index 000000000000..344e891365a5 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx @@ -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 = {}) => + waitFor(() => + render(, { + 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(); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx new file mode 100644 index 000000000000..dda3b7e47ba0 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx @@ -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( + ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled, + ); + const filterBarOrientation = useSelector( + ({ dashboardInfo }) => dashboardInfo.filterBarOrientation, + ); + const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] = + useState(filterBarOrientation); + const isCrossFiltersFeatureEnabled = isFeatureEnabled( + FeatureFlag.DASHBOARD_CROSS_FILTERS, + ); + const shouldEnableCrossFilters = + !!isCrossFiltersEnabled && isCrossFiltersFeatureEnabled; + const [crossFiltersEnabled, setCrossFiltersEnabled] = useState( + shouldEnableCrossFilters, + ); + const canEdit = useSelector( + ({ 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>['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( + () => ( + + setCrossFiltersEnabled(checked || false)} + />{' '} + + {t('Enable cross-filtering')} + + + ), + [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 ( + + } + menuItems={menuItems} + selectedKeys={[selectedFilterBarOrientation]} + /> + ); +}; + +export default FilterBarSettings; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx index 023d86c9aeee..7cd4877d11e2 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx @@ -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 = ({ toggleFiltersBar }) => { const dashboardId = useSelector( ({ dashboardInfo }) => dashboardInfo.id, ); - const canSetHorizontalFilterBar = - canEdit && isFeatureEnabled(FeatureFlag.HORIZONTAL_FILTER_BAR); return ( {t('Filters')} - {canSetHorizontalFilterBar && } + = ({ ) : ( <> - {canEdit && } + {canEdit && ( ) => waitFor(() => render(, { 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( diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 337b5fc017b4..167ec0a04387 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -102,6 +102,7 @@ function mapStateToProps( setControlValue, filterboxMigrationState: dashboardState.filterboxMigrationState, datasetsStatus, + emitCrossFilters: !!dashboardInfo.crossFiltersEnabled, }; } diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js index 030fd6025056..3503c441b65c 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js +++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js @@ -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; } diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index e24356be6191..87dee0b05ab6 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -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; }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/shared-controls/emitFilterControl.test.tsx b/superset-frontend/src/dashboard/util/crossFilters.ts similarity index 72% rename from superset-frontend/packages/superset-ui-chart-controls/test/shared-controls/emitFilterControl.test.tsx rename to superset-frontend/src/dashboard/util/crossFilters.ts index 6070ccccfdee..061ed82634ac 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/shared-controls/emitFilterControl.test.tsx +++ b/superset-frontend/src/dashboard/util/crossFilters.ts @@ -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); diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts index 14e8975bd543..4f8de967a895 100644 --- a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts +++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.ts @@ -145,7 +145,6 @@ const tableVizStore = { value: true, }, show_totals: {}, - emit_filter: {}, table_timestamp_format: { value: 'smart_date', }, diff --git a/superset-frontend/src/utils/getClientErrorObject.ts b/superset-frontend/src/utils/getClientErrorObject.ts index 2706031e8266..de2a2c2beeec 100644 --- a/superset-frontend/src/utils/getClientErrorObject.ts +++ b/superset-frontend/src/utils/getClientErrorObject.ts @@ -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 diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index c6577f1b0eb9..3f0666266f9c 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -279,6 +279,7 @@ def set_dash_metadata( # pylint: disable=too-many-locals 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: diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index d3a944498096..f0d05445aae7 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -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() diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index e6ff9f583458..995e8784ffec 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -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, } diff --git a/tests/unit_tests/fixtures/assets_configs.py b/tests/unit_tests/fixtures/assets_configs.py index 14ff37cf6153..6e78d9e562d1 100644 --- a/tests/unit_tests/fixtures/assets_configs.py +++ b/tests/unit_tests/fixtures/assets_configs.py @@ -177,6 +177,7 @@ "show_native_filters": True, "color_scheme_domain": [], "shared_label_colors": {}, + "cross_filters_enabled": False, }, "version": "1.0.0", },