diff --git a/UPDATING.md b/UPDATING.md index 84893e9080d2..cf4f846715ae 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -28,6 +28,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [26328](https://github.com/apache/superset/issues/26328): Removes the deprecated Filter Box code and it's associated dependencies `react-select` and `array-move`. It also removes the `DeprecatedSelect` and `AsyncSelect` components that were exclusively used by filter boxes. Existing filter boxes will be automatically migrated to native filters. - [26330](https://github.com/apache/superset/issues/26330): Removes the deprecated `DASHBOARD_FILTERS_EXPERIMENTAL` feature flag. The previous value of the feature flag was `False` and now the feature is permanently removed. - [26344](https://github.com/apache/superset/issues/26344): Removes the deprecated `ENABLE_EXPLORE_JSON_CSRF_PROTECTION` feature flag. The previous value of the feature flag was `False` and now the feature is permanently removed. - [26345](https://github.com/apache/superset/issues/26345): Removes the deprecated `ENABLE_TEMPLATE_REMOVE_FILTERS` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled. diff --git a/docs/docs/miscellaneous/native-filter-migration.mdx b/docs/docs/miscellaneous/native-filter-migration.mdx deleted file mode 100644 index d8ffa8b32913..000000000000 --- a/docs/docs/miscellaneous/native-filter-migration.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Migrating from Legacy to Native Filters -sidebar_position: 5 -version: 1 ---- - -## - -The `superset native-filters` CLI command group—somewhat akin to an Alembic migration— -comprises of a number of sub-commands which allows administrators to upgrade/downgrade -existing dashboards which use the legacy filter-box charts—in combination with the -filter scopes/filter mapping—to use the native filter dashboard component. - -Even though both legacy and native filters can coexist the overall user experience (UX) -is substandard as the already convoluted filter space becomes overly complex. After -enabling the `DASHBOARD_NATIVE_FILTERS` it is strongly advised to run the migration ASAP to -ensure users are not exposed to the hybrid state. - -### Upgrading - -The - -``` -superset native-filters upgrade -``` - -command—which provides the option to target either specific dashboard(s) or all -dashboards—migrates the legacy filters to native filters. - -Specifically, the command performs the following: - -- Replaces every filter-box chart within the dashboard with a markdown element which -provides a link to the deprecated chart. This preserves the layout whilst simultaneously -providing context to help owners review/verify said change. -- Migrates the filter scopes/filter mappings to the native filter configuration. - -#### Quality Control - -Dashboard owners should: - -- Verify that the filter behavior is correct. -- Consolidate any conflicting/redundant filters—this previously may not have been -obvious given the embedded nature of the legacy filters and/or the non-optimal UX of the -legacy filter mapping (scopes and immunity). -- Rename the filters—which may not be uniquely named—to provide the necessary context -which previously was likely provided by both the location of the filter-box and the -corresponding filter-box title. - -Dashboard owners may: - -- Remove† the markdown elements from their dashboards and adjust the layout accordingly. - -† Note removing the markdown elements—which contain metadata relating to the replaced -chart—prevents the dashboard from being fully restored and thus this operation should -only be performed if it is evident that a downgrade is not necessary. - -### Downgrading - -Similarly the - -``` -superset native-filters downgrade -``` - -command reverses said migration, i.e., restores the dashboard to the previous state. - - -### Cleanup - -The ability to downgrade/reverse the migration requires temporary storage of the -dashboard metadata—relating to both positional composition and filter configuration. - -Once the upgrade has been verified it is recommended to run the - -``` -superset native-filters cleanup -``` - -command—which provides the option to target either specific dashboard(s) or all -dashboards. Note this operation is irreversible. - -Specifically, the command performs the following: - -- Removes the temporary dashboard metadata. -- Deletes the filter-box charts associated with the dashboard†. - -† Note the markdown elements will still remain however the link to the referenced filter-box -chart will no longer be valid. - -#### Quality Control - -Dashboard owners should: - -- Remove the markdown elements from their dashboards and adjust the layout accordingly. diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.filter.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.filter.test.ts deleted file mode 100644 index 6ae5d1e5d6fe..000000000000 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.filter.test.ts +++ /dev/null @@ -1,82 +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 { - isLegacyResponse, - parsePostForm, - getChartAliasesBySpec, - waitForChartLoad, -} from 'cypress/utils'; -import { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls'; -import { WORLD_HEALTH_CHARTS } from './utils'; - -describe.skip('Dashboard filter', () => { - before(() => { - cy.visit(WORLD_HEALTH_DASHBOARD); - }); - - it('should apply filter', () => { - WORLD_HEALTH_CHARTS.forEach(waitForChartLoad); - getChartAliasesBySpec( - WORLD_HEALTH_CHARTS.filter(({ viz }) => viz !== 'filter_box'), - ).then(nonFilterChartAliases => { - cy.get('.Select__placeholder:first').click(); - - // should show the filter indicator - cy.get('span[aria-label="filter"]:visible').should(nodes => { - expect(nodes.length).to.least(9); - }); - - cy.get('.Select__control:first input[type=text]').type('So', { - force: true, - delay: 100, - }); - - cy.get('.Select__menu').first().contains('South Asia').click(); - - // should still have all filter indicators - cy.get('span[aria-label="filter"]:visible').should(nodes => { - expect(nodes.length).to.least(9); - }); - - cy.get('.filter_box button').click({ force: true }); - cy.wait(nonFilterChartAliases).then(requests => { - requests.forEach(({ response, request }) => { - const responseBody = response?.body; - let requestFilter; - if (isLegacyResponse(responseBody)) { - const requestFormData = parsePostForm(request.body); - const requestParams = JSON.parse( - requestFormData.form_data as string, - ); - requestFilter = requestParams.extra_filters[0]; - } else { - requestFilter = request.body.queries[0].filters[0]; - } - expect(requestFilter).deep.eq({ - col: 'region', - op: 'IN', - val: ['South Asia'], - }); - }); - }); - }); - - // TODO add test with South Asia{enter} type action to select filter - }); -}); diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts index ba442e600ae6..208eb357534e 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts @@ -101,7 +101,7 @@ describe('Dashboard tabs', () => { cy.get('.Select__control').first().should('be.visible').click(); cy.get('.Select__control input[type=text]').first().focus().type('South'); cy.get('.Select__option').contains('South Asia').click(); - cy.get('.filter_box button:not(:disabled)').contains('Apply').click(); + cy.get('.filter button:not(:disabled)').contains('Apply').click(); // send new query from same tab cy.wait(treemapAlias).then(({ request }) => { @@ -149,7 +149,7 @@ describe('Dashboard tabs', () => { cy.get('.ant-tabs-tab').contains('row tab 1').click(); cy.get('.Select__clear-indicator').click(); - cy.get('.filter_box button:not(:disabled)').contains('Apply').click(); + cy.get('.filter button:not(:disabled)').contains('Apply').click(); // trigger 1 new query waitForChartLoad(TREEMAP); diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/filter_box.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/filter_box.test.js deleted file mode 100644 index a4ca5ddcf2ef..000000000000 --- a/superset-frontend/cypress-base/cypress/e2e/explore/filter_box.test.js +++ /dev/null @@ -1,38 +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 { FORM_DATA_DEFAULTS } from './visualizations/shared.helper'; - -describe('Edit FilterBox Chart', () => { - const VIZ_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'filter_box' }; - - function verify(formData) { - cy.visitChartByParams(formData); - cy.verifySliceSuccess({ waitAlias: '@getJson' }); - } - - beforeEach(() => { - cy.intercept('POST', '/superset/explore_json/**').as('getJson'); - }); - - it('should work with default date filter', () => { - verify(VIZ_DEFAULTS); - // Filter box should default to having a date filter with no filter selected - cy.get('div.filter_box').contains('No filter'); - }); -}); diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts b/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts index 1e13c7d7ed3f..3c7decd512c3 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/explore/link.test.ts @@ -78,7 +78,9 @@ describe('Test explore links', () => { cy.url().then(() => { cy.get('[data-test="query-save-button"]').click(); cy.get('[data-test="saveas-radio"]').check(); - cy.get('[data-test="new-chart-name"]').type(newChartName); + cy.get('[data-test="new-chart-name"]').type(newChartName, { + force: true, + }); cy.get('[data-test="btn-modal-save"]').click(); cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); cy.visitChartByName(newChartName); diff --git a/superset-frontend/cypress-base/cypress/support/directories.ts b/superset-frontend/cypress-base/cypress/support/directories.ts index b0eb024d2f48..8dcf739d14a8 100644 --- a/superset-frontend/cypress-base/cypress/support/directories.ts +++ b/superset-frontend/cypress-base/cypress/support/directories.ts @@ -657,7 +657,7 @@ export const dashboardView = { treeMapChartModal: { selectItem: '.Select_control', selectItemInput: '.Select__control input[type=text]', - applyButton: '.filter_box button:not(:disabled)', + applyButton: '.filter button:not(:disabled)', clearItemIcon: '.Select__clear-indicator', }, sliceThreeDots: '[aria-label="More Options"]', diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 6e9867ecb0a0..7b8073e9860a 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -59,7 +59,6 @@ "ace-builds": "^1.4.14", "ansi-regex": "^4.1.1", "antd": "4.10.3", - "array-move": "^2.2.1", "babel-plugin-typescript-to-proptypes": "^2.0.0", "bootstrap": "^3.4.1", "bootstrap-slider": "^10.0.0", @@ -124,7 +123,6 @@ "react-reverse-portal": "^2.1.1", "react-router-dom": "^5.3.4", "react-search-input": "^0.11.3", - "react-select": "^3.2.0", "react-sortable-hoc": "^2.0.0", "react-split": "^2.0.9", "react-syntax-highlighter": "^15.4.5", @@ -201,8 +199,8 @@ "@types/react-loadable": "^5.5.6", "@types/react-redux": "^7.1.10", "@types/react-router-dom": "^5.3.3", - "@types/react-select": "^3.0.19", "@types/react-table": "^7.0.19", + "@types/react-transition-group": "^4.4.10", "@types/react-ultimate-pagination": "^1.2.0", "@types/react-window": "^1.8.5", "@types/redux-localstorage": "^1.0.8", @@ -19787,17 +19785,6 @@ "@types/react-router": "*" } }, - "node_modules/@types/react-select": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.19.tgz", - "integrity": "sha512-d+6qtfFXZeIOAABlVL1e50RZn8ctOABE4tFDxM6KW4lKuXgTTgLVrSik5AX9XjBjV7N80FtS6GTN/WeoXL9Jww==", - "dev": true, - "dependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "@types/react-transition-group": "*" - } - }, "node_modules/@types/react-syntax-highlighter": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz", @@ -19823,9 +19810,9 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", - "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "dev": true, "dependencies": { "@types/react": "*" @@ -22895,17 +22882,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-move": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.1.tgz", - "integrity": "sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/array-tree-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", @@ -79812,17 +79788,6 @@ "@types/react-router": "*" } }, - "@types/react-select": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.19.tgz", - "integrity": "sha512-d+6qtfFXZeIOAABlVL1e50RZn8ctOABE4tFDxM6KW4lKuXgTTgLVrSik5AX9XjBjV7N80FtS6GTN/WeoXL9Jww==", - "dev": true, - "requires": { - "@types/react": "*", - "@types/react-dom": "*", - "@types/react-transition-group": "*" - } - }, "@types/react-syntax-highlighter": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz", @@ -79848,9 +79813,9 @@ } }, "@types/react-transition-group": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", - "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "dev": true, "requires": { "@types/react": "*" @@ -82292,11 +82257,6 @@ "is-string": "^1.0.7" } }, - "array-move": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.1.tgz", - "integrity": "sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ==" - }, "array-tree-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index cc243754fc59..8abd9dd95540 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -125,7 +125,6 @@ "ace-builds": "^1.4.14", "ansi-regex": "^4.1.1", "antd": "4.10.3", - "array-move": "^2.2.1", "babel-plugin-typescript-to-proptypes": "^2.0.0", "bootstrap": "^3.4.1", "bootstrap-slider": "^10.0.0", @@ -190,7 +189,6 @@ "react-reverse-portal": "^2.1.1", "react-router-dom": "^5.3.4", "react-search-input": "^0.11.3", - "react-select": "^3.2.0", "react-sortable-hoc": "^2.0.0", "react-split": "^2.0.9", "react-syntax-highlighter": "^15.4.5", @@ -267,8 +265,8 @@ "@types/react-loadable": "^5.5.6", "@types/react-redux": "^7.1.10", "@types/react-router-dom": "^5.3.3", - "@types/react-select": "^3.0.19", "@types/react-table": "^7.0.19", + "@types/react-transition-group": "^4.4.10", "@types/react-ultimate-pagination": "^1.2.0", "@types/react-window": "^1.8.5", "@types/redux-localstorage": "^1.0.8", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 9314f8d33f3b..8aa475a7f9bc 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -280,7 +280,6 @@ export type SelectControlType = | 'AdhocFilterControl' | 'FilterBoxItemControl'; -// via react-select/src/filters export interface FilterOption { label: string; value: string; 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 815a5df2f4e4..829440133a32 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 @@ -76,10 +76,6 @@ export interface ChartPropsConfig { annotationData?: AnnotationData; /** Datasource metadata */ datasource?: SnakeCaseDatasource; - /** - * Formerly called "filters", which was misleading because it is actually - * initial values of the filter_box and table vis - */ initialValues?: DataRecordFilters; /** Main configuration of the chart */ formData?: RawFormData; diff --git a/superset-frontend/packages/superset-ui-core/test/query/extractTimegrain.test.ts b/superset-frontend/packages/superset-ui-core/test/query/extractTimegrain.test.ts index d063e11a8f7c..384b09cabaf9 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/extractTimegrain.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/extractTimegrain.test.ts @@ -32,7 +32,7 @@ describe('extractTimegrain', () => { ).toEqual('P1D'); }); - it('should extract filter box time grain from form data', () => { + it('should extract filter time grain from form data', () => { expect( extractTimegrain({ ...baseFormData, diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx index c69b92346db8..27950a98b17e 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx @@ -29,9 +29,6 @@ import { AsyncAceEditorProps } from 'src/components/AsyncAceEditor'; const middlewares = [thunk]; const mockStore = configureStore(middlewares); -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx index 38dbbf65a05b..7fe7680f17f2 100644 --- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx @@ -34,9 +34,6 @@ import EstimateQueryCostButton, { const middlewares = [thunk]; const mockStore = configureStore(middlewares); -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx index 68a014ba52a0..69dcca42b5e4 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx @@ -32,9 +32,6 @@ import QueryLimitSelect, { const middlewares = [thunk]; const mockStore = configureStore(middlewares); -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx index 276f9b7d1967..354943908823 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx @@ -30,9 +30,6 @@ import RunQueryActionButton, { const middlewares = [thunk]; const mockStore = configureStore(middlewares); -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx index 6c231401c705..986dabc81489 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx @@ -42,9 +42,6 @@ import { } from 'src/SqlLab/actions/sqlLab'; import SqlEditorTabHeader from 'src/SqlLab/components/SqlEditorTabHeader'; -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx index fdf8fd3b53f5..3270ef1f5255 100644 --- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx +++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx @@ -33,9 +33,6 @@ import TemplateParamsEditor, { TemplateParamsEditorProps, } from 'src/SqlLab/components/TemplateParamsEditor'; -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/components/AsyncSelect/AsyncSelect.test.jsx b/superset-frontend/src/components/AsyncSelect/AsyncSelect.test.jsx deleted file mode 100644 index f3bce1271330..000000000000 --- a/superset-frontend/src/components/AsyncSelect/AsyncSelect.test.jsx +++ /dev/null @@ -1,150 +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 { shallow } from 'enzyme'; -import fetchMock from 'fetch-mock'; -import Select from 'src/components/DeprecatedSelect'; -import AsyncSelect from 'src/components/AsyncSelect'; - -describe('AsyncSelect', () => { - afterAll(fetchMock.reset); - afterEach(fetchMock.resetHistory); - - const dataEndpoint = '/chart/api/read'; - const dataGlob = 'glob:*/chart/api/read'; - fetchMock.get(dataGlob, []); - fetchMock.resetHistory(); - - const mockedProps = { - dataEndpoint, - onChange: () => {}, - placeholder: 'Select...', - mutator: () => [ - { value: 1, label: 'main' }, - { value: 2, label: 'another' }, - ], - valueRenderer: opt => opt.label, - }; - - it('is valid element', () => { - expect(React.isValidElement()).toBe(true); - }); - - it('has one select', () => { - const wrapper = shallow(); - expect(wrapper.find(Select)).toExist(); - }); - - it('calls onChange on select change', () => { - const onChangeSpy = jest.fn(); - const wrapper = shallow( - , - ); - - wrapper.find(Select).simulate('change', { value: 1 }); - expect(onChangeSpy.mock.calls).toHaveLength(1); - }); - - describe('auto select', () => { - it('should not call onChange if autoSelect=false', () => - new Promise(done => { - expect.assertions(2); - - const onChangeSpy = jest.fn(); - shallow(); - - setTimeout(() => { - expect(fetchMock.calls(dataGlob)).toHaveLength(1); - expect(onChangeSpy.mock.calls).toHaveLength(0); - done(); - }); - })); - - it('should auto select the first option if autoSelect=true', () => - new Promise(done => { - expect.assertions(3); - - const onChangeSpy = jest.fn(); - const wrapper = shallow( - , - ); - - setTimeout(() => { - expect(fetchMock.calls(dataGlob)).toHaveLength(1); - expect(onChangeSpy.mock.calls).toHaveLength(1); - expect(onChangeSpy).toBeCalledWith( - wrapper.instance().state.options[0], - ); - done(); - }); - })); - - it('should not auto select when value prop is set and autoSelect=true', () => - new Promise(done => { - expect.assertions(3); - - const onChangeSpy = jest.fn(); - const wrapper = shallow( - , - ); - - setTimeout(() => { - expect(fetchMock.calls(dataGlob)).toHaveLength(1); - expect(onChangeSpy.mock.calls).toHaveLength(0); - expect(wrapper.find(Select)).toExist(); - done(); - }); - })); - - it('should call onAsyncError if there is an error fetching options', () => { - expect.assertions(3); - - const errorEndpoint = 'async/error/'; - const errorGlob = 'glob:*async/error/'; - fetchMock.get(errorGlob, { throws: 'error' }); - - const onAsyncError = jest.fn(); - const wrapper = shallow( - , - ); - - return wrapper - .instance() - .fetchOptions() - .then(() => { - // Fails then retries thrice whenever fetching options, which happens twice: - // once on component mount and once when calling `fetchOptions` again - expect(fetchMock.calls(errorGlob)).toHaveLength(8); - expect(onAsyncError.mock.calls).toHaveLength(2); - expect(onAsyncError).toBeCalledWith('error'); - - return Promise.resolve(); - }); - }); - }); -}); diff --git a/superset-frontend/src/components/AsyncSelect/index.jsx b/superset-frontend/src/components/AsyncSelect/index.jsx deleted file mode 100644 index 69799eadeae4..000000000000 --- a/superset-frontend/src/components/AsyncSelect/index.jsx +++ /dev/null @@ -1,104 +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 PropTypes from 'prop-types'; -// TODO: refactor this with `import { AsyncSelect } from src/components/Select` -import { Select } from 'src/components/DeprecatedSelect'; -import { t, SupersetClient } from '@superset-ui/core'; -import { getClientErrorObject } from '../../utils/getClientErrorObject'; - -const propTypes = { - dataEndpoint: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - mutator: PropTypes.func.isRequired, - onAsyncError: PropTypes.func, - value: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.arrayOf(PropTypes.number), - ]), - valueRenderer: PropTypes.func, - placeholder: PropTypes.string, - autoSelect: PropTypes.bool, -}; - -const defaultProps = { - placeholder: t('Select ...'), - onAsyncError: () => {}, -}; - -class AsyncSelect extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - isLoading: false, - options: [], - }; - - this.onChange = this.onChange.bind(this); - } - - componentDidMount() { - this.fetchOptions(); - } - - onChange(option) { - this.props.onChange(option); - } - - fetchOptions() { - this.setState({ isLoading: true }); - const { mutator, dataEndpoint } = this.props; - - return SupersetClient.get({ endpoint: dataEndpoint }) - .then(({ json }) => { - const options = mutator ? mutator(json) : json; - - this.setState({ options, isLoading: false }); - - if (!this.props.value && this.props.autoSelect && options.length > 0) { - this.onChange(options[0]); - } - }) - .catch(response => - getClientErrorObject(response).then(error => { - this.props.onAsyncError(error.error || error.statusText || error); - this.setState({ isLoading: false }); - }), - ); - } - - render() { - return ( - {}} - options={OPTIONS} - placeholder="choose one" - width={600} - /> -
-

With no value

- {}} - options={OPTIONS} - placeholder="choose one or more values" - width={600} - value={[OPTIONS[0]]} - multi - /> - -); - -SelectGallery.args = { - value: '', - options: OPTIONS, -}; - -SelectGallery.story = { - parameters: { - knobs: { - disabled: true, - }, - }, -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const InteractiveSelect = (args: any) => { - const [{ value, multi, clearable, placeholder }, updateArgs] = useArgs(); - const onSelect = (selection: {}) => { - const { value }: { value?: any } = selection || {}; - if (multi) { - updateArgs({ value: selection }); - return; - } - updateArgs({ value }); - }; - - return ( - - ); - } - // for CreaTable - if (SelectComponent === WindowedCreatableSelect) { - restProps.getNewOptionData = (inputValue: string, label: string) => ({ - label: label || inputValue, - [valueKey]: inputValue, - isNew: true, - }); - } - - // handle forcing dropdown overflow - // use only when setting overflow:visible isn't possible on the container element - if (forceOverflow) { - Object.assign(restProps, { - closeMenuOnScroll: (e: Event) => { - // ensure menu is open - const menuIsOpen = (stateManager as BasicSelect)?.state - ?.menuIsOpen; - const target = e.target as HTMLElement; - return ( - menuIsOpen && - target && - !target.classList?.contains('Select__menu-list') - ); - }, - menuPosition: 'fixed', - }); - } - - // Make sure always return StateManager for the refs. - // To get the real `Select` component, keep tap into `obj.select`: - // - for normal trigger.parentNode} {...props} /> -))` - display: block; -`; - -const StyledNativeGraySelect = styled(Select)` - &.ant-select-single { - .ant-select-selector { - height: 36px; - padding: 0 11px; - background-color: ${({ theme }) => theme.colors.grayscale.light3}; - border: none; - - .ant-select-selection-search-input { - height: 100%; - } - - .ant-select-selection-item, - .ant-select-selection-placeholder { - line-height: 35px; - color: ${({ theme }) => theme.colors.grayscale.dark1}; - } - } - } -`; - -export const NativeSelect = Object.assign(StyledNativeSelect, { - Option: Select.Option, -}); - -export const NativeGraySelect = Object.assign(StyledNativeGraySelect, { - Option: Select.Option, -}); diff --git a/superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.jsx b/superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.jsx deleted file mode 100644 index bffa5428a60d..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.jsx +++ /dev/null @@ -1,104 +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 PropTypes from 'prop-types'; -import { Select } from 'src/components/DeprecatedSelect'; - -export default class OnPasteSelect extends React.Component { - constructor(props) { - super(props); - this.onPaste = this.onPaste.bind(this); - } - - onPaste(evt) { - if (!this.props.isMulti) { - return; - } - evt.preventDefault(); - const clipboard = evt.clipboardData.getData('Text'); - if (!clipboard) { - return; - } - const regex = `[${this.props.separator}]+`; - const values = clipboard.split(new RegExp(regex)).map(v => v.trim()); - const validator = this.props.isValidNewOption; - const selected = this.props.value || []; - const existingOptions = {}; - const existing = {}; - this.props.options.forEach(v => { - existingOptions[v[this.props.valueKey]] = 1; - }); - let options = []; - selected.forEach(v => { - options.push({ [this.props.labelKey]: v, [this.props.valueKey]: v }); - existing[v] = 1; - }); - options = options.concat( - values - .filter(v => { - const notExists = !existing[v]; - existing[v] = 1; - return ( - notExists && - (validator ? validator({ [this.props.labelKey]: v }) : !!v) - ); - }) - .map(v => { - const opt = { [this.props.labelKey]: v, [this.props.valueKey]: v }; - if (!existingOptions[v]) { - this.props.options.unshift(opt); - } - return opt; - }), - ); - if (options.length) { - if (this.props.onChange) { - this.props.onChange(options); - } - } - } - - render() { - const { selectWrap: SelectComponent, ...restProps } = this.props; - return ; - } -} - -OnPasteSelect.propTypes = { - separator: PropTypes.array, - selectWrap: PropTypes.elementType, - selectRef: PropTypes.func, - onChange: PropTypes.func.isRequired, - valueKey: PropTypes.string, - labelKey: PropTypes.string, - options: PropTypes.array, - isMulti: PropTypes.bool, - value: PropTypes.any, - isValidNewOption: PropTypes.func, - noResultsText: PropTypes.string, - forceOverflow: PropTypes.bool, -}; -OnPasteSelect.defaultProps = { - separator: [',', '\n', '\t', ';'], - selectWrap: Select, - valueKey: 'value', - labelKey: 'label', - options: [], - isMulti: false, -}; diff --git a/superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.test.jsx b/superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.test.jsx deleted file mode 100644 index 95d01cc28b9a..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.test.jsx +++ /dev/null @@ -1,216 +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. - */ -/* eslint-disable no-unused-expressions */ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; -import { - Select, - OnPasteSelect, - CreatableSelect, -} from 'src/components/DeprecatedSelect'; - -const defaultProps = { - onChange: sinon.spy(), - isMulti: true, - isValidNewOption: sinon.spy(s => !!s.label), - value: [], - options: [ - { value: 'United States', label: 'United States' }, - { value: 'China', label: 'China' }, - { value: 'India', label: 'India' }, - { value: 'Canada', label: 'Canada' }, - { value: 'Russian Federation', label: 'Russian Federation' }, - { value: 'Japan', label: 'Japan' }, - { value: 'Mexico', label: 'Mexico' }, - ], -}; - -const defaultEvt = { - preventDefault: sinon.spy(), - clipboardData: { - getData: sinon.spy(() => ' United States, China, India, Canada, '), - }, -}; - -describe('OnPasteSelect', () => { - let wrapper; - let props; - let evt; - let expected; - beforeEach(() => { - props = { ...defaultProps }; - wrapper = shallow(); - evt = { ...defaultEvt }; - }); - - it('renders the supplied selectWrap component', () => { - const select = wrapper.findWhere(x => x.type() === Select); - expect(select).toHaveLength(1); - }); - - it('renders custom selectWrap components', () => { - props.selectWrap = CreatableSelect; - wrapper = shallow(); - expect(wrapper.findWhere(x => x.type() === CreatableSelect)).toHaveLength( - 1, - ); - }); - - describe('onPaste', () => { - it('calls onChange with pasted comma separated values', () => { - wrapper.instance().onPaste(evt); - expected = props.options.slice(0, 4); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(5); - }); - - it('calls onChange with pasted new line separated values', () => { - evt.clipboardData.getData = sinon.spy( - () => 'United States\nChina\nRussian Federation\nIndia', - ); - wrapper.instance().onPaste(evt); - expected = [ - props.options[0], - props.options[1], - props.options[4], - props.options[2], - ]; - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(9); - }); - - it('calls onChange with pasted tab separated values', () => { - evt.clipboardData.getData = sinon.spy( - () => 'Russian Federation\tMexico\tIndia\tCanada', - ); - wrapper.instance().onPaste(evt); - expected = [ - props.options[4], - props.options[6], - props.options[2], - props.options[3], - ]; - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(13); - }); - - it('calls onChange without duplicate values and adds new comma separated values', () => { - evt.clipboardData.getData = sinon.spy( - () => 'China, China, China, China, Mexico, Mexico, Chi na, Mexico, ', - ); - expected = [ - props.options[1], - props.options[6], - { label: 'Chi na', value: 'Chi na' }, - ]; - wrapper.instance().onPaste(evt); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(17); - expect(props.options[0].value).toBe(expected[2].value); - props.options.splice(0, 1); - }); - - it('calls onChange without duplicate values and parses new line separated values', () => { - evt.clipboardData.getData = sinon.spy( - () => 'United States\nCanada\nMexico\nUnited States\nCanada', - ); - expected = [props.options[0], props.options[3], props.options[6]]; - wrapper.instance().onPaste(evt); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(20); - }); - - it('calls onChange without duplicate values and parses tab separated values', () => { - evt.clipboardData.getData = sinon.spy( - () => 'China\tIndia\tChina\tRussian Federation\tJapan\tJapan', - ); - expected = [ - props.options[1], - props.options[2], - props.options[4], - props.options[5], - ]; - wrapper.instance().onPaste(evt); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(24); - }); - - it('calls onChange with currently selected values and new comma separated values', () => { - props.value = ['United States', 'Canada', 'Mexico']; - evt.clipboardData.getData = sinon.spy( - () => 'United States, Canada, Japan, India', - ); - wrapper = shallow(); - expected = [ - props.options[0], - props.options[3], - props.options[6], - props.options[5], - props.options[2], - ]; - wrapper.instance().onPaste(evt); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(26); - }); - - it('calls onChange with currently selected values and new "new line" separated values', () => { - props.value = ['China', 'India', 'Japan']; - evt.clipboardData.getData = sinon.spy(() => 'Mexico\nJapan\nIndia'); - wrapper = shallow(); - expected = [ - props.options[1], - props.options[2], - props.options[5], - props.options[6], - ]; - wrapper.instance().onPaste(evt); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(27); - }); - - it('calls onChange with currently selected values and new tab separated values', () => { - props.value = ['United States', 'Canada', 'Mexico', 'Russian Federation']; - evt.clipboardData.getData = sinon.spy( - () => 'United States\tCanada\tJapan\tIndia', - ); - wrapper = shallow(); - expected = [ - props.options[0], - props.options[3], - props.options[6], - props.options[4], - props.options[5], - props.options[2], - ]; - wrapper.instance().onPaste(evt); - expect(props.onChange.calledWith(expected)).toBe(true); - expect(evt.preventDefault.called).toBe(true); - expect(props.isValidNewOption.callCount).toBe(29); - }); - }); -}); diff --git a/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/WindowedMenuList.tsx b/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/WindowedMenuList.tsx deleted file mode 100644 index f29466b33988..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/WindowedMenuList.tsx +++ /dev/null @@ -1,158 +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, { - useRef, - useEffect, - Component, - FunctionComponent, - ReactElement, - RefObject, -} from 'react'; -import { - ListChildComponentProps, - FixedSizeList as WindowedList, -} from 'react-window'; -import { - OptionTypeBase, - OptionProps, - MenuListComponentProps, -} from 'react-select'; -import { ThemeConfig } from '../styles'; - -export type WindowedMenuListProps = { - selectProps: { - windowListRef?: RefObject; - optionHeight?: number; - }; -}; - -/** - * MenuListComponentProps should always have `children` elements, as guaranteed - * by https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/Select.js#L1686-L1719 - * - * `children` may also be `Component>` if options are not - * provided (e.g., when async list is still loading, or no results), but that's - * not possible because this MenuList will only be rendered when - * optionsLength > windowThreshold. - * - * If may also be `Component>[]` but we are not supporting - * grouped options just yet. - */ - -type MenuListPropsChildren = - | Component>[] - | ReactElement[]; - -export type MenuListProps = - MenuListComponentProps & { - children: MenuListPropsChildren; - // theme is not present with built-in @types/react-select, but is actually - // available via CommonProps. - theme?: ThemeConfig; - className?: string; - } & WindowedMenuListProps; - -const DEFAULT_OPTION_HEIGHT = 30; - -/** - * Get the index of the last selected option. - */ -function getLastSelected(children: MenuListPropsChildren) { - return Array.isArray(children) - ? children.findIndex( - ({ props: { isFocused = false } = {} }) => isFocused, - ) || 0 - : -1; -} - -/** - * Calculate probable option height as set in theme configs - */ -function detectHeight({ spacing: { baseUnit, lineHeight } }: ThemeConfig) { - // Option item expects 2 * baseUnit for each of top and bottom padding. - return baseUnit * 4 + lineHeight; -} - -export default function WindowedMenuList({ - children, - ...props -}: MenuListProps) { - const { - maxHeight, - selectProps, - theme, - getStyles, - cx, - innerRef, - isMulti, - className, - } = props; - const { - // Expose react-window VariableSizeList instance and HTML elements - windowListRef: windowListRef_, - windowListInnerRef, - } = selectProps; - const defaultWindowListRef = useRef(null); - const windowListRef = windowListRef_ || defaultWindowListRef; - - // try get default option height from theme configs - let { optionHeight } = selectProps; - if (!optionHeight) { - optionHeight = theme ? detectHeight(theme) : DEFAULT_OPTION_HEIGHT; - } - - const itemCount = children.length; - const totalHeight = optionHeight * itemCount; - - const Row: FunctionComponent = ({ - data, - index, - style, - }) =>
{data[index]}
; - - useEffect(() => { - const lastSelected = getLastSelected(children); - if (windowListRef.current && lastSelected) { - windowListRef.current.scrollToItem(lastSelected); - } - }, [children, windowListRef]); - - return ( - - {Row} - - ); -} diff --git a/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/index.tsx b/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/index.tsx deleted file mode 100644 index 3770b542df41..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/index.tsx +++ /dev/null @@ -1,29 +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 Select from 'react-select'; -import Creatable from 'react-select/creatable'; -import AsyncCreatable from 'react-select/async-creatable'; -import windowed from './windowed'; - -export * from './windowed'; - -export const WindowedSelect = windowed(Select); -export const WindowedCreatableSelect = windowed(Creatable); -export const WindowedAsyncCreatableSelect = windowed(AsyncCreatable); -export default WindowedSelect; diff --git a/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/windowed.tsx b/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/windowed.tsx deleted file mode 100644 index a611cf36c96d..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/WindowedSelect/windowed.tsx +++ /dev/null @@ -1,84 +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, { - ComponentType, - FunctionComponent, - ReactElement, - forwardRef, -} from 'react'; -import Select, { - Props as SelectProps, - OptionTypeBase, - MenuListComponentProps, - components as defaultComponents, -} from 'react-select'; -import WindowedMenuList, { WindowedMenuListProps } from './WindowedMenuList'; - -const { MenuList: DefaultMenuList } = defaultComponents; - -export const DEFAULT_WINDOW_THRESHOLD = 100; - -export type WindowedSelectProps = - SelectProps & { - windowThreshold?: number; - } & WindowedMenuListProps['selectProps']; - -export type WindowedSelectComponentType = - FunctionComponent>; - -export function MenuList({ - children, - ...props -}: MenuListComponentProps & { - selectProps: WindowedSelectProps; -}) { - const { windowThreshold = DEFAULT_WINDOW_THRESHOLD } = props.selectProps; - if (Array.isArray(children) && children.length > windowThreshold) { - return ( - - {children as ReactElement[]} - - ); - } - return {children}; -} - -/** - * Add "windowThreshold" option to a react-select component, turn the options - * list into a virtualized list when appropriate. - * - * @param SelectComponent the React component to render Select - */ -export default function windowed( - SelectComponent: ComponentType>, -): WindowedSelectComponentType { - const WindowedSelect = forwardRef( - ( - props: WindowedSelectProps, - ref: React.RefObject>, - ) => { - const { components: components_ = {}, ...restProps } = props; - const components = { ...components_, MenuList }; - return ( - - ); - }, - ); - return WindowedSelect; -} diff --git a/superset-frontend/src/components/DeprecatedSelect/index.ts b/superset-frontend/src/components/DeprecatedSelect/index.ts deleted file mode 100644 index e8f30eb7e4cb..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/index.ts +++ /dev/null @@ -1,23 +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. - */ -export * from './DeprecatedSelect'; -export * from './styles'; -export { default } from './DeprecatedSelect'; -export { default as OnPasteSelect } from './OnPasteSelect'; -export { NativeSelect, NativeGraySelect } from './NativeSelect'; diff --git a/superset-frontend/src/components/DeprecatedSelect/styles.tsx b/superset-frontend/src/components/DeprecatedSelect/styles.tsx deleted file mode 100644 index f04cfbdba9ed..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/styles.tsx +++ /dev/null @@ -1,406 +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. - */ - -// Deprecated component -/* eslint-disable theme-colors/no-literal-colors */ - -import React, { CSSProperties, ComponentType, ReactNode } from 'react'; -import { SerializedStyles } from '@emotion/react'; -import { SupersetTheme, css } from '@superset-ui/core'; -import { - Styles, - Theme, - SelectComponentsConfig, - components as defaultComponents, - InputProps as ReactSelectInputProps, - Props as SelectProps, -} from 'react-select'; -import type { colors as reactSelectColors } from 'react-select/src/theme'; -import type { DeepNonNullable } from 'react-select/src/components'; -import { OptionType } from 'antd/lib/select'; -import { SupersetStyledSelectProps } from './DeprecatedSelect'; - -export const DEFAULT_CLASS_NAME = 'Select'; -export const DEFAULT_CLASS_NAME_PREFIX = 'Select'; - -type RecursivePartial = { - [P in keyof T]?: RecursivePartial; -}; - -const colors = (theme: SupersetTheme) => ({ - primary: theme.colors.success.base, - danger: theme.colors.error.base, - warning: theme.colors.warning.base, - indicator: theme.colors.info.base, - almostBlack: theme.colors.grayscale.dark1, - grayDark: theme.colors.grayscale.dark1, - grayLight: theme.colors.grayscale.light2, - gray: theme.colors.grayscale.light1, - grayBg: theme.colors.grayscale.light4, - grayBgDarker: theme.colors.grayscale.light3, - grayBgDarkest: theme.colors.grayscale.light2, - grayHeading: theme.colors.grayscale.light1, - menuHover: theme.colors.grayscale.light3, - lightest: theme.colors.grayscale.light5, - darkest: theme.colors.grayscale.dark2, - grayBorder: theme.colors.grayscale.light2, - grayBorderLight: theme.colors.grayscale.light3, - grayBorderDark: theme.colors.grayscale.light1, - textDefault: theme.colors.grayscale.dark1, - textDarkest: theme.colors.grayscale.dark2, - dangerLight: theme.colors.error.light1, -}); - -export type ThemeConfig = { - borderRadius: number; - // z-index for menu dropdown - // (the same as `@z-index-above-dashboard-charts + 1` in variables.less) - zIndex: number; - colors: { - // add known colors - [key in keyof typeof reactSelectColors]: string; - } & { - [key in keyof ReturnType]: string; - } & { - [key: string]: string; // any other colors - }; - spacing: Theme['spacing'] & { - // line height and font size must be pixels for easier computation - // of option item height in WindowedMenuList - lineHeight: number; - fontSize: number; - // other relative size must be string - minWidth: string; - }; -}; - -export type PartialThemeConfig = RecursivePartial; - -export const defaultTheme: (theme: SupersetTheme) => PartialThemeConfig = - theme => ({ - borderRadius: theme.borderRadius, - zIndex: 11, - colors: colors(theme), - spacing: { - baseUnit: 3, - menuGutter: 0, - controlHeight: 34, - lineHeight: 19, - fontSize: 14, - minWidth: '6.5em', - }, - weights: theme.typography.weights, - }); - -// let styles accept serialized CSS, too -type CSSStyles = CSSProperties | SerializedStyles; -type styleFnWithSerializedStyles = ( - base: CSSProperties, - state: any, -) => CSSStyles | CSSStyles[]; - -export type StylesConfig = { - [key in keyof Styles]: styleFnWithSerializedStyles; -}; -export type PartialStylesConfig = Partial; - -export const DEFAULT_STYLES: PartialStylesConfig = { - container: ( - provider, - { - theme: { - spacing: { minWidth }, - }, - }, - ) => [ - provider, - css` - min-width: ${minWidth}; - `, - ], - placeholder: provider => [ - provider, - css` - white-space: nowrap; - `, - ], - indicatorSeparator: () => css` - display: none; - `, - indicatorsContainer: provider => [ - provider, - css` - i { - width: 1em; - display: inline-block; - } - `, - ], - clearIndicator: provider => [ - provider, - css` - padding: 4px 0 4px 6px; - `, - ], - control: ( - provider, - { isFocused, menuIsOpen, theme: { borderRadius, colors } }, - ) => { - const isPseudoFocused = isFocused && !menuIsOpen; - let borderColor = colors.grayBorder; - if (isPseudoFocused || menuIsOpen) { - borderColor = colors.grayBorderDark; - } - return [ - provider, - css` - border-color: ${borderColor}; - box-shadow: ${isPseudoFocused - ? 'inset 0 1px 1px rgba(0,0,0,.075), 0 0 0 3px rgba(0,0,0,.1)' - : 'none'}; - border-radius: ${menuIsOpen - ? `${borderRadius}px ${borderRadius}px 0 0` - : `${borderRadius}px`}; - &:hover { - border-color: ${borderColor}; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); - } - flex-wrap: nowrap; - padding-left: 1px; - `, - ]; - }, - menu: (provider, { theme: { zIndex } }) => [ - provider, - css` - padding-bottom: 2em; - z-index: ${zIndex}; /* override at least multi-page pagination */ - width: auto; - min-width: 100%; - max-width: 80vw; - background: none; - box-shadow: none; - border: 0; - `, - ], - menuList: (provider, { theme: { borderRadius, colors } }) => [ - provider, - css` - background: ${colors.lightest}; - border-radius: 0 0 ${borderRadius}px ${borderRadius}px; - border: 1px solid ${colors.grayBorderDark}; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); - margin-top: -1px; - border-top-color: ${colors.grayBorderLight}; - min-width: 100%; - width: auto; - border-radius: 0 0 ${borderRadius}px ${borderRadius}px; - padding-top: 0; - padding-bottom: 0; - `, - ], - option: ( - provider, - { - isDisabled, - isFocused, - isSelected, - theme: { - colors, - spacing: { lineHeight, fontSize }, - weights, - }, - }, - ) => { - let color = colors.textDefault; - let backgroundColor = colors.lightest; - if (isFocused) { - backgroundColor = colors.grayBgDarker; - } else if (isDisabled) { - color = '#ccc'; - } - return [ - provider, - css` - cursor: pointer; - line-height: ${lineHeight}px; - font-size: ${fontSize}px; - background-color: ${backgroundColor}; - color: ${color}; - font-weight: ${isSelected ? weights.bold : weights.normal}; - white-space: nowrap; - &:hover:active { - background-color: ${colors.grayBg}; - } - `, - ]; - }, - valueContainer: ( - provider, - { - isMulti, - hasValue, - theme: { - spacing: { baseUnit }, - }, - }, - ) => [ - provider, - css` - padding-left: ${isMulti && hasValue ? 1 : baseUnit * 3}px; - `, - ], - multiValueLabel: ( - provider, - { - theme: { - spacing: { baseUnit }, - }, - }, - ) => ({ - ...provider, - paddingLeft: baseUnit * 1.2, - paddingRight: baseUnit * 1.2, - }), - input: (provider, { selectProps }) => [ - provider, - css` - margin-left: 0; - vertical-align: middle; - ${selectProps?.isMulti && selectProps?.value?.length - ? 'padding: 0 6px; width: 100%' - : 'padding: 0; flex: 1 1 auto;'}; - `, - ], - menuPortal: base => ({ - ...base, - zIndex: 1030, // must be same or higher of antd popover - }), -}; - -const INPUT_TAG_BASE_STYLES = { - background: 'none', - border: 'none', - outline: 'none', - padding: 0, -}; - -export type SelectComponentsType = Omit< - SelectComponentsConfig, - 'Input' -> & { - Input: ComponentType; -}; - -// react-select is missing selectProps from their props type -// so overwriting it here to avoid errors -export type InputProps = ReactSelectInputProps & { - placeholder?: ReactNode; - selectProps: SelectProps; - autoComplete?: string; - onPaste?: SupersetStyledSelectProps['onPaste']; - inputStyle?: object; -}; - -const { ClearIndicator, DropdownIndicator, Option, Input, SelectContainer } = - defaultComponents as Required>; - -export const DEFAULT_COMPONENTS: SelectComponentsType = { - SelectContainer: ({ children, ...props }) => { - const { - selectProps: { assistiveText }, - } = props; - return ( -
- {children} - {assistiveText && ( - ({ - marginLeft: 3, - fontSize: theme.typography.sizes.s, - color: theme.colors.grayscale.light1, - })} - > - {assistiveText} - - )} -
- ); - }, - Option: ({ children, innerProps, data, ...props }) => ( - - ), - ClearIndicator: props => ( - - × - - ), - DropdownIndicator: props => ( - - - - ), - Input: (props: InputProps) => { - const { getStyles } = props; - return ( - - ); - }, -}; - -export const VALUE_LABELED_STYLES: PartialStylesConfig = { - valueContainer: ( - provider, - { - getValue, - theme: { - spacing: { baseUnit }, - }, - isMulti, - }, - ) => ({ - ...provider, - paddingLeft: getValue().length > 0 ? 1 : baseUnit * 3, - overflow: isMulti && getValue().length > 0 ? 'visible' : 'hidden', - }), - // render single value as is they are multi-value - singleValue: (provider, props) => { - const { getStyles } = props; - return { - ...getStyles('multiValue', props), - '.metric-option': getStyles('multiValueLabel', props), - }; - }, -}; diff --git a/superset-frontend/src/components/DeprecatedSelect/utils.ts b/superset-frontend/src/components/DeprecatedSelect/utils.ts deleted file mode 100644 index a6590efebd36..000000000000 --- a/superset-frontend/src/components/DeprecatedSelect/utils.ts +++ /dev/null @@ -1,57 +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 { - OptionTypeBase, - ValueType, - OptionsType, - GroupedOptionsType, -} from 'react-select'; - -/** - * Find Option value that matches a possibly string value. - * - * Translate possible string values to `OptionType` objects, fallback to value - * itself if cannot be found in the options list. - * - * Always returns an array. - */ -export function findValue( - value: ValueType | string, - options: GroupedOptionsType | OptionsType = [], - valueKey = 'value', -): OptionType[] { - if (value === null || value === undefined || value === '') { - return []; - } - const isGroup = Array.isArray(options[0]?.options); - const flatOptions = isGroup - ? (options as GroupedOptionsType).flatMap(x => x.options || []) - : (options as OptionsType); - - const find = (val: OptionType) => { - const realVal = value?.hasOwnProperty(valueKey) ? val[valueKey] : val; - return ( - flatOptions.find(x => x === realVal || x[valueKey] === realVal) || val - ); - }; - - // If value is a single string, must return an Array so `cleanValue` won't be - // empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64 - return (Array.isArray(value) ? value : [value]).map(find); -} diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts index 8a8c57cb6234..31a9368a1ab4 100644 --- a/superset-frontend/src/components/ListView/utils.ts +++ b/superset-frontend/src/components/ListView/utils.ts @@ -35,7 +35,6 @@ import { import rison from 'rison'; import { isEqual } from 'lodash'; -import { PartialStylesConfig } from 'src/components/DeprecatedSelect'; import { FetchDataConfig, Filter, @@ -381,21 +380,3 @@ export function useListViewState({ query, }; } - -export const filterSelectStyles: PartialStylesConfig = { - container: (provider, { getValue }) => ({ - ...provider, - // dynamic width based on label string length - minWidth: `${Math.min( - 12, - Math.max(5, 3 + getValue()[0].label.length / 2), - )}em`, - }), - control: provider => ({ - ...provider, - borderWidth: 0, - boxShadow: 'none', - cursor: 'pointer', - backgroundColor: 'transparent', - }), -}; diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index d102af74833e..015a12cb968d 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -90,8 +90,6 @@ const getQueryCacheKey = (value: string, page: number, pageSize: number) => /** * This component is a customized version of the Antdesign 4.X Select component * https://ant.design/components/select/. - * The aim of the component was to combine all the instances of select components throughout the - * project under one and to remove the react-select component entirely. * This Select component provides an API that is tested against all the different use cases of Superset. * It limits and overrides the existing Antdesign API in order to keep their usage to the minimum * and to enforce simplification and standardization. diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx index 1e3bc73758cb..f4f9565abb8a 100644 --- a/superset-frontend/src/components/Select/Select.tsx +++ b/superset-frontend/src/components/Select/Select.tsx @@ -73,8 +73,6 @@ import { customTagRender } from './CustomTag'; /** * This component is a customized version of the Antdesign 4.X Select component * https://ant.design/components/select/. - * The aim of the component was to combine all the instances of select components throughout the - * project under one and to remove the react-select component entirely. * This Select component provides an API that is tested against all the different use cases of Superset. * It limits and overrides the existing Antdesign API in order to keep their usage to the minimum * and to enforce simplification and standardization. diff --git a/superset-frontend/src/dashboard/actions/dashboardFilters.js b/superset-frontend/src/dashboard/actions/dashboardFilters.js index b8f92b8df05e..0e5e5454c686 100644 --- a/superset-frontend/src/dashboard/actions/dashboardFilters.js +++ b/superset-frontend/src/dashboard/actions/dashboardFilters.js @@ -22,26 +22,6 @@ function isValidFilter(getState, chartId) { return getState().dashboardState.sliceIds.includes(chartId); } -export const ADD_FILTER = 'ADD_FILTER'; -export function addFilter(chartId, component, form_data) { - return (dispatch, getState) => { - if (isValidFilter(getState, chartId)) { - return dispatch({ type: ADD_FILTER, chartId, component, form_data }); - } - return getState().dashboardFilters; - }; -} - -export const REMOVE_FILTER = 'REMOVE_FILTER'; -export function removeFilter(chartId) { - return (dispatch, getState) => { - if (isValidFilter(getState, chartId)) { - return dispatch({ type: REMOVE_FILTER, chartId }); - } - return getState().dashboardFilters; - }; -} - export const CHANGE_FILTER = 'CHANGE_FILTER'; export function changeFilter(chartId, newSelectedValues, merge) { return (dispatch, getState) => { diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index b461275d8c69..fa3eeadf41d1 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -61,11 +61,7 @@ import { SAVE_CHART_CONFIG_COMPLETE, } from './dashboardInfo'; import { fetchDatasourceMetadata } from './datasources'; -import { - addFilter, - removeFilter, - updateDirectPathToFilter, -} from './dashboardFilters'; +import { updateDirectPathToFilter } from './dashboardFilters'; import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters'; import getOverwriteItems from '../util/getOverwriteItems'; @@ -554,7 +550,7 @@ export function showBuilderPane() { return { type: SHOW_BUILDER_PANE }; } -export function addSliceToDashboard(id, component) { +export function addSliceToDashboard(id) { return (dispatch, getState) => { const { sliceEntities } = getState(); const selectedSlice = sliceEntities.slices[id]; @@ -580,21 +576,12 @@ export function addSliceToDashboard(id, component) { dispatch(fetchDatasourceMetadata(form_data.datasource)), ]).then(() => { dispatch(addSlice(selectedSlice)); - - if (selectedSlice && selectedSlice.viz_type === 'filter_box') { - dispatch(addFilter(id, component, selectedSlice.form_data)); - } }); }; } export function removeSliceFromDashboard(id) { - return (dispatch, getState) => { - const sliceEntity = getState().sliceEntities.slices[id]; - if (sliceEntity && sliceEntity.viz_type === 'filter_box') { - dispatch(removeFilter(id)); - } - + return dispatch => { dispatch(removeSlice(id)); dispatch(removeChart(id)); getSharedLabelColor().removeSlice(id); diff --git a/superset-frontend/src/dashboard/actions/dashboardState.test.js b/superset-frontend/src/dashboard/actions/dashboardState.test.js index 1ef85f0b99ce..4e30c38fb1d4 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.test.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.test.js @@ -21,12 +21,10 @@ import { SupersetClient } from '@superset-ui/core'; import { waitFor } from '@testing-library/react'; import { - removeSliceFromDashboard, SAVE_DASHBOARD_STARTED, saveDashboardRequest, SET_OVERRIDE_CONFIRM, } from 'src/dashboard/actions/dashboardState'; -import { REMOVE_FILTER } from 'src/dashboard/actions/dashboardFilters'; import * as uiCore from '@superset-ui/core'; import { UPDATE_COMPONENTS_PARENTS_LIST } from 'src/dashboard/actions/dashboardLayout'; import { @@ -193,14 +191,4 @@ describe('dashboardState actions', () => { }); }); }); - - it('should dispatch removeFilter if a removed slice is a filter_box', () => { - const { getState, dispatch } = setup(mockState); - const thunk = removeSliceFromDashboard(filterId); - thunk(dispatch, getState); - - const removeFilter = dispatch.getCall(0).args[0]; - removeFilter(dispatch, getState); - expect(dispatch.getCall(3).args[0].type).toBe(REMOVE_FILTER); - }); }); diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 699c2041c892..930280a1f9bb 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -32,10 +32,6 @@ import { getCrossFiltersConfiguration, isCrossFiltersEnabled, } from 'src/dashboard/util/crossFilters'; -import { - DASHBOARD_FILTER_SCOPE_GLOBAL, - dashboardFilter, -} from 'src/dashboard/reducers/dashboardFilters'; import { DASHBOARD_HEADER_ID, GRID_DEFAULT_CHART_WIDTH, @@ -49,10 +45,8 @@ import { } from 'src/dashboard/util/componentTypes'; import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer'; import getEmptyLayout from 'src/dashboard/util/getEmptyLayout'; -import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata'; import getLocationHash from 'src/dashboard/util/getLocationHash'; import newComponentFactory from 'src/dashboard/util/newComponentFactory'; -import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox'; import { URL_PARAMS } from 'src/constants'; import { getUrlParam } from 'src/utils/urlUtils'; import { ResourceStatus } from 'src/hooks/apiResources/apiResources'; @@ -72,20 +66,10 @@ export const hydrateDashboard = const reservedUrlParams = extractUrlParams('reserved'); const editMode = reservedUrlParams.edit === 'true'; - let preselectFilters = {}; - charts.forEach(chart => { // eslint-disable-next-line no-param-reassign chart.slice_id = chart.form_data.slice_id; }); - try { - // allow request parameter overwrite dashboard metadata - preselectFilters = - getUrlParam(URL_PARAMS.preselectFilters) || - JSON.parse(metadata.default_filters); - } catch (e) { - // - } if (metadata?.shared_label_colors) { updateColorSchema(metadata, metadata?.shared_label_colors); @@ -117,8 +101,6 @@ export const hydrateDashboard = let newSlicesContainer; let newSlicesContainerWidth = 0; - const filterScopes = metadata?.filter_scopes || {}; - const chartQueries = {}; const dashboardFilters = {}; const slices = {}; @@ -189,57 +171,6 @@ export const hydrateDashboard = newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH; } - // build DashboardFilters for interactive filter features - if (slice.form_data.viz_type === 'filter_box') { - const configs = getFilterConfigsFromFormdata(slice.form_data); - let { columns } = configs; - const { labels } = configs; - if (preselectFilters[key]) { - Object.keys(columns).forEach(col => { - if (preselectFilters[key][col]) { - columns = { - ...columns, - [col]: preselectFilters[key][col], - }; - } - }); - } - - const scopesByChartId = Object.keys(columns).reduce((map, column) => { - const scopeSettings = { - ...filterScopes[key], - }; - const { scope, immune } = { - ...DASHBOARD_FILTER_SCOPE_GLOBAL, - ...scopeSettings[column], - }; - - return { - ...map, - [column]: { - scope, - immune, - }, - }; - }, {}); - - const componentId = chartIdToLayoutId[key]; - const directPathToFilter = (layout[componentId].parents || []).slice(); - directPathToFilter.push(componentId); - dashboardFilters[key] = { - ...dashboardFilter, - chartId: key, - componentId, - datasourceId: slice.form_data.datasource, - filterName: slice.slice_name, - directPathToFilter, - columns, - labels, - scopes: scopesByChartId, - isDateFilter: Object.keys(columns).includes(TIME_RANGE), - }; - } - // sync layout names with current slice names in case a slice was edited // in explore since the layout was updated. name updates go through layout for undo/redo // functionality and python updates slice names based on layout upon dashboard save diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.ts b/superset-frontend/src/dashboard/actions/sliceEntities.ts index 562d90657e78..9d85e57a6a59 100644 --- a/superset-frontend/src/dashboard/actions/sliceEntities.ts +++ b/superset-frontend/src/dashboard/actions/sliceEntities.ts @@ -17,13 +17,7 @@ * under the License. */ import rison from 'rison'; -import { - DatasourceType, - isFeatureEnabled, - FeatureFlag, - SupersetClient, - t, -} from '@superset-ui/core'; +import { DatasourceType, SupersetClient, t } from '@superset-ui/core'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { Dispatch } from 'redux'; @@ -114,14 +108,6 @@ export function fetchSlices( ? [{ col: 'slice_name', opr: 'chart_all_text', value: filter_value }] : []; - if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { - filters.push({ - col: 'viz_type', - opr: 'neq', - value: 'filter_box', - }); - } - if (userId) { filters.push({ col: 'owners', opr: 'rel_m_m', value: userId }); } diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx index 02a3a49971c3..179c03c996f1 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx @@ -47,12 +47,6 @@ jest.mock('src/dashboard/actions/dashboardState', () => ({ jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth'); // mock following dependant components to fix the prop warnings -jest.mock('src/components/DeprecatedSelect/WindowedSelect', () => () => ( -
-)); -jest.mock('src/components/DeprecatedSelect', () => () => ( -
-)); jest.mock('src/components/Select/Select', () => () => (
)); diff --git a/superset-frontend/src/dashboard/components/FilterBoxMigrationModal.tsx b/superset-frontend/src/dashboard/components/FilterBoxMigrationModal.tsx deleted file mode 100644 index d42b3254bec7..000000000000 --- a/superset-frontend/src/dashboard/components/FilterBoxMigrationModal.tsx +++ /dev/null @@ -1,95 +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, { FunctionComponent } from 'react'; -import { styled, t } from '@superset-ui/core'; - -import Modal from 'src/components/Modal'; -import Button from 'src/components/Button'; - -const StyledFilterBoxMigrationModal = styled(Modal)` - .modal-content { - height: 900px; - display: flex; - flex-direction: column; - align-items: stretch; - } - - .modal-header { - flex: 0 1 auto; - } - - .modal-body { - flex: 1 1 auto; - overflow: auto; - } - - .modal-footer { - flex: 0 1 auto; - } - - .ant-modal-body { - overflow: auto; - } -`; - -interface FilterBoxMigrationModalProps { - onHide: () => void; - onClickReview: () => void; - onClickSnooze: () => void; - show: boolean; - hideFooter: boolean; -} - -const FilterBoxMigrationModal: FunctionComponent = - ({ onClickReview, onClickSnooze, onHide, show, hideFooter = false }) => ( - - - - - - } - responsive - > -
- {t( - 'filter_box will be deprecated ' + - 'in a future version of Superset. ' + - 'Please replace filter_box by dashboard filter components.', - )} -
-
- ); - -export default FilterBoxMigrationModal; diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index 2d7b5c05bdf6..83099e54907f 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -183,12 +183,6 @@ test('Should "export to Excel"', async () => { expect(props.exportXLSX).toBeCalledWith(371); }); -test('Should not show "Download" if slice is filter box', () => { - const props = createProps('filter_box'); - renderWrapper(props); - expect(screen.queryByText('Download')).not.toBeInTheDocument(); -}); - test('Export full CSV is under featureflag', async () => { // @ts-ignore global.featureFlags = { diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 17d5bdc83e05..305bc3843451 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -489,7 +489,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => { )} - {props.slice.viz_type !== 'filter_box' && props.supersetCanCSV && ( + {props.supersetCanCSV && ( { {t('Export to Excel')} - {props.slice.viz_type !== 'filter_box' && - isFeatureEnabled(FeatureFlag.ALLOW_FULL_CSV_EXPORT) && + {isFeatureEnabled(FeatureFlag.ALLOW_FULL_CSV_EXPORT) && props.supersetCanCSV && isTable && ( <> diff --git a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx index 1b146148682b..3fc6775bdb50 100644 --- a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx +++ b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx @@ -30,7 +30,7 @@ import getKeyForFilterScopeTree from 'src/dashboard/util/getKeyForFilterScopeTre import getSelectedChartIdForFilterScopeTree from 'src/dashboard/util/getSelectedChartIdForFilterScopeTree'; import getFilterScopeFromNodesTree from 'src/dashboard/util/getFilterScopeFromNodesTree'; import getRevertedFilterScope from 'src/dashboard/util/getRevertedFilterScope'; -import { getChartIdsInFilterBoxScope } from 'src/dashboard/util/activeDashboardFilters'; +import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters'; import { getChartIdAndColumnFromFilterKey, getDashboardFilterKey, @@ -277,12 +277,6 @@ const ScopeSelector = styled.div` } .multi-edit-mode { - &.filter-scope-pane { - .rct-node.rct-node-leaf .filter-scope-type.filter_box { - display: none; - } - } - .filter-field-item { padding: 0 ${theme.gridUnit * 4}px 0 ${theme.gridUnit * 12}px; margin-left: ${theme.gridUnit * -12}px; @@ -367,9 +361,8 @@ export default class FilterScopeSelector extends React.PureComponent { selectedChartId: filterId, }); const expanded = getFilterScopeParentNodes(nodes, 1); - // force display filter_box chart as unchecked, but show checkbox as disabled const chartIdsInFilterScope = ( - getChartIdsInFilterBoxScope({ + getChartIdsInFilterScope({ filterScope: dashboardFilters[filterId].scopes[columnName], }) || [] ).filter(id => id !== filterId); diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index a99061c7071c..a60465391427 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -40,9 +40,6 @@ import SliceHeader from '../SliceHeader'; import MissingChart from '../MissingChart'; import { slicePropShape, chartPropShape } from '../../util/propShapes'; -import { isFilterBox } from '../../util/activeDashboardFilters'; -import getFilterValuesByFilterId from '../../util/getFilterValuesByFilterId'; - const propTypes = { id: PropTypes.number.isRequired, componentId: PropTypes.string.isRequired, @@ -100,7 +97,6 @@ const SHOULD_UPDATE_ON_PROP_CHANGES = Object.keys(propTypes).filter( prop => prop !== 'width' && prop !== 'height' && prop !== 'isComponentVisible', ); -const OVERFLOWABLE_VIZ_TYPES = new Set(['filter_box']); const DEFAULT_HEADER_HEIGHT = 22; const ChartWrapper = styled.div` @@ -421,13 +417,7 @@ class Chart extends React.Component { const cachedDttm = // eslint-disable-next-line camelcase queriesResponse?.map(({ cached_dttm }) => cached_dttm) || []; - const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice.viz_type); - const initialValues = isFilterBox(id) - ? getFilterValuesByFilterId({ - activeFilters: filters, - filterId: id, - }) - : {}; + const initialValues = {}; return ( )} - + {isLoading && ( - (type === TAB_TYPE || type === CHART_TYPE || type === DASHBOARD_ROOT_TYPE) && - (!charts || charts[meta?.chartId]?.form_data?.viz_type !== 'filter_box'); +export const isShowTypeInTree = ({ type }: LayoutItem) => + type === TAB_TYPE || type === CHART_TYPE || type === DASHBOARD_ROOT_TYPE; export const getNodeTitle = (node: LayoutItem) => node?.meta?.sliceNameOverride ?? @@ -51,7 +50,7 @@ export const buildTree = ( if ( node && treeItem && - isShowTypeInTree(node, charts) && + isShowTypeInTree(node) && node.type !== DASHBOARD_ROOT_TYPE && validNodes?.includes?.(node.id) ) { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index 9247538a5d44..a8bfafd8341b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts @@ -31,7 +31,7 @@ import { QueryFormColumn, } from '@superset-ui/core'; import { TIME_FILTER_MAP } from 'src/explore/constants'; -import { getChartIdsInFilterBoxScope } from 'src/dashboard/util/activeDashboardFilters'; +import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters'; import { ChartConfiguration, DashboardLayout, @@ -130,7 +130,7 @@ const selectIndicatorsForChartFromFilter = ( return Object.keys(filter.columns) .filter(column => - getChartIdsInFilterBoxScope({ + getChartIdsInFilterScope({ filterScope: filter.scopes[column], }).includes(chartId), ) diff --git a/superset-frontend/src/dashboard/containers/Dashboard.ts b/superset-frontend/src/dashboard/containers/Dashboard.ts index 5f9b29b95dd4..ba91d748dab4 100644 --- a/superset-frontend/src/dashboard/containers/Dashboard.ts +++ b/superset-frontend/src/dashboard/containers/Dashboard.ts @@ -54,11 +54,10 @@ function mapStateToProps(state: RootState) { dashboardInfo, dashboardState, datasources, - // filters prop: a map structure for all the active filter_box's values and scope in this dashboard, + // filters prop: a map structure for all the active filter's values and scope in this dashboard, // for each filter field. map key is [chartId_column] // When dashboard is first loaded into browser, // its value is from preselect_filters that dashboard owner saved in dashboard's meta data - // When user start interacting with dashboard, it will be user picked values from all filter_box activeFilters: { ...getActiveFilters(), ...getAllActiveFilters({ diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.js index d31af825717b..a971df7d6a40 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.js @@ -18,17 +18,13 @@ */ /* eslint-disable camelcase */ import { - ADD_FILTER, - REMOVE_FILTER, CHANGE_FILTER, UPDATE_DIRECT_PATH_TO_FILTER, UPDATE_LAYOUT_COMPONENTS, UPDATE_DASHBOARD_FILTERS_SCOPE, } from '../actions/dashboardFilters'; import { HYDRATE_DASHBOARD } from '../actions/hydrate'; -import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; import { DASHBOARD_ROOT_ID } from '../util/constants'; -import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata'; import { buildActiveFilters } from '../util/activeDashboardFilters'; import { getChartIdAndColumnFromFilterKey } from '../util/getDashboardFilterKey'; @@ -50,41 +46,10 @@ export const dashboardFilter = { scopes: {}, }; -const CHANGE_FILTER_VALUE_ACTIONS = [ADD_FILTER, REMOVE_FILTER, CHANGE_FILTER]; +const CHANGE_FILTER_VALUE_ACTIONS = [CHANGE_FILTER]; export default function dashboardFiltersReducer(dashboardFilters = {}, action) { const actionHandlers = { - [ADD_FILTER]() { - const { chartId, component, form_data } = action; - const { columns, labels } = getFilterConfigsFromFormdata(form_data); - const scopes = Object.keys(columns).reduce( - (map, column) => ({ - ...map, - [column]: DASHBOARD_FILTER_SCOPE_GLOBAL, - }), - {}, - ); - const directPathToFilter = component - ? (component.parents || []).slice().concat(component.id) - : []; - - const newFilter = { - ...dashboardFilter, - chartId, - componentId: component.id, - datasourceId: form_data.datasource, - filterName: component.meta.sliceName, - directPathToFilter, - columns, - labels, - scopes, - isInstantFilter: !!form_data.instant_filtering, - isDateFilter: Object.keys(columns).includes(TIME_RANGE), - }; - - return newFilter; - }, - [CHANGE_FILTER](state) { const { newSelectedValues, merge } = action; const updatedColumns = Object.keys(newSelectedValues).reduce( @@ -155,13 +120,6 @@ export default function dashboardFiltersReducer(dashboardFilters = {}, action) { buildActiveFilters({ dashboardFilters: updatedFilters }); return updatedFilters; } - if (action.type === REMOVE_FILTER) { - const { chartId } = action; - const { [chartId]: deletedFilter, ...updatedFilters } = dashboardFilters; - buildActiveFilters({ dashboardFilters: updatedFilters }); - - return updatedFilters; - } if (action.type === HYDRATE_DASHBOARD) { return action.data.dashboardFilters; } diff --git a/superset-frontend/src/dashboard/reducers/dashboardFilters.test.js b/superset-frontend/src/dashboard/reducers/dashboardFilters.test.js index 19527d2276ef..a629e631937c 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardFilters.test.js +++ b/superset-frontend/src/dashboard/reducers/dashboardFilters.test.js @@ -18,8 +18,6 @@ */ /* eslint-disable camelcase */ import { - ADD_FILTER, - REMOVE_FILTER, CHANGE_FILTER, UPDATE_DASHBOARD_FILTERS_SCOPE, } from 'src/dashboard/actions/dashboardFilters'; @@ -27,10 +25,7 @@ import dashboardFiltersReducer, { DASHBOARD_FILTER_SCOPE_GLOBAL, } from 'src/dashboard/reducers/dashboardFilters'; import * as activeDashboardFilters from 'src/dashboard/util/activeDashboardFilters'; -import { - emptyFilters, - dashboardFilters, -} from 'spec/fixtures/mockDashboardFilters'; +import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters'; import { sliceEntitiesForDashboard, filterId, @@ -44,35 +39,6 @@ describe('dashboardFilters reducer', () => { const directPathToFilter = (component.parents || []).slice(); directPathToFilter.push(component.id); - it('should add a new filter if it does not exist', () => { - expect( - dashboardFiltersReducer(emptyFilters, { - type: ADD_FILTER, - chartId: filterId, - component, - form_data, - }), - ).toEqual({ - [filterId]: { - chartId: filterId, - componentId: component.id, - directPathToFilter, - filterName: component.meta.sliceName, - isDateFilter: false, - isInstantFilter: !!form_data.instant_filtering, - columns: { - [column]: undefined, - }, - labels: { - [column]: column, - }, - scopes: { - [column]: DASHBOARD_FILTER_SCOPE_GLOBAL, - }, - }, - }); - }); - it('should overwrite a filter if merge is false', () => { expect( dashboardFiltersReducer(dashboardFilters, { @@ -139,15 +105,6 @@ describe('dashboardFilters reducer', () => { }); }); - it('should remove the filter if values are empty', () => { - expect( - dashboardFiltersReducer(dashboardFilters, { - type: REMOVE_FILTER, - chartId: filterId, - }), - ).toEqual({}); - }); - it('should buildActiveFilters on UPDATE_DASHBOARD_FILTERS_SCOPE', () => { const regionScope = { scope: ['TAB-1'], diff --git a/superset-frontend/src/dashboard/util/activeDashboardFilters.js b/superset-frontend/src/dashboard/util/activeDashboardFilters.js index 3369dc1c5be6..ef6fd7f5e1e2 100644 --- a/superset-frontend/src/dashboard/util/activeDashboardFilters.js +++ b/superset-frontend/src/dashboard/util/activeDashboardFilters.js @@ -25,7 +25,6 @@ import { import { CHART_TYPE } from './componentTypes'; import { DASHBOARD_FILTER_SCOPE_GLOBAL } from '../reducers/dashboardFilters'; -let allFilterBoxChartIds = []; let activeFilters = {}; let appliedFilterValuesByChart = {}; let allComponents = {}; @@ -35,13 +34,6 @@ export function getActiveFilters() { return activeFilters; } -// currently filter_box is a chart, -// when selecting filter scopes, they have to be out pulled out in a few places. -// after we make filter_box a dashboard build-in component, will not need this check anymore. -export function isFilterBox(chartId) { - return allFilterBoxChartIds.includes(chartId); -} - // this function is to find all filter values applied to a chart, // it goes through all active filters and their scopes. // return: { [column]: array of selected values } @@ -61,10 +53,10 @@ export function getAppliedFilterValues(chartId, filters) { return appliedFilterValuesByChart[chartId]; } -// Legacy - getChartIdsInFilterBoxScope is used only by -// components and functions related to filter box -// Please use src/dashboard/util/getChartIdsInFilterScope instead -export function getChartIdsInFilterBoxScope({ filterScope }) { +/** + * @deprecated Please use src/dashboard/util/getChartIdsInFilterScope instead + */ +export function getChartIdsInFilterScope({ filterScope }) { function traverse(chartIds = [], component = {}, immuneChartIds = []) { if (!component) { return; @@ -99,10 +91,6 @@ export function getChartIdsInFilterBoxScope({ filterScope }) { // values: array of selected values // scope: array of chartIds that applicable to the filter field. export function buildActiveFilters({ dashboardFilters = {}, components = {} }) { - allFilterBoxChartIds = Object.values(dashboardFilters).map( - filter => filter.chartId, - ); - // clear cache if (!isEmpty(components)) { allComponents = components; @@ -119,7 +107,7 @@ export function buildActiveFilters({ dashboardFilters = {}, components = {} }) { : columns[column] !== undefined ) { // remove filter itself - const scope = getChartIdsInFilterBoxScope({ + const scope = getChartIdsInFilterScope({ filterScope: scopes[column], }).filter(id => chartId !== id); diff --git a/superset-frontend/src/dashboard/util/getFilterScopeNodesTree.js b/superset-frontend/src/dashboard/util/getFilterScopeNodesTree.js index 92868f047e8c..aed133b1f399 100644 --- a/superset-frontend/src/dashboard/util/getFilterScopeNodesTree.js +++ b/superset-frontend/src/dashboard/util/getFilterScopeNodesTree.js @@ -51,12 +51,7 @@ function traverse({ return { ...chartNode, - children: filterFields.map(filterField => ({ - value: `${currentNode.meta.chartId}:${filterField}`, - label: `${chartNode.label}`, - type: 'filter_box', - showCheckbox: false, - })), + children: [], }; } diff --git a/superset-frontend/src/dashboard/util/getFilterValuesByFilterId.js b/superset-frontend/src/dashboard/util/getFilterValuesByFilterId.js deleted file mode 100644 index 75f9c705eb4a..000000000000 --- a/superset-frontend/src/dashboard/util/getFilterValuesByFilterId.js +++ /dev/null @@ -1,38 +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 { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey'; - -// input: { [id_column1]: values, [id_column2]: values } -// output: { column1: values, column2: values } -export default function getFilterValuesByFilterId({ - activeFilters = {}, - filterId, -}) { - return Object.entries(activeFilters).reduce((map, entry) => { - const [filterKey, { values }] = entry; - const { chartId, column } = getChartIdAndColumnFromFilterKey(filterKey); - if (chartId === filterId) { - return { - ...map, - [column]: values, - }; - } - return map; - }, {}); -} diff --git a/superset-frontend/src/dashboard/util/getRevertedFilterScope.ts b/superset-frontend/src/dashboard/util/getRevertedFilterScope.ts index 9dac0583aace..d873b80d488f 100644 --- a/superset-frontend/src/dashboard/util/getRevertedFilterScope.ts +++ b/superset-frontend/src/dashboard/util/getRevertedFilterScope.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey'; - interface FilterScopeMap { [key: string]: number[]; } @@ -44,19 +42,14 @@ export default function getRevertedFilterScope({ {}, ); - return filterFields.reduce((map, filterField) => { - const { chartId } = getChartIdAndColumnFromFilterKey(filterField); - // force display filter_box chart as unchecked, but show checkbox as disabled - const updatedCheckedIds = ( - checkedChartIdsByFilterField[filterField] || [] - ).filter(id => id !== chartId); - - return { + return filterFields.reduce( + (map, filterField) => ({ ...map, [filterField]: { ...filterScopeMap[filterField], - checked: updatedCheckedIds, + checked: checkedChartIdsByFilterField[filterField] || [], }, - }; - }, {}); + }), + {}, + ); } diff --git a/superset-frontend/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js b/superset-frontend/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js index cde72e35851a..ac9bc065017c 100644 --- a/superset-frontend/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js +++ b/superset-frontend/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js @@ -22,8 +22,6 @@ export default function getSelectedChartIdForFilterScopeTree({ activeFilterField, checkedFilterFields, }) { - // we don't apply filter on filter_box itself, so we will disable - // checkbox in filter scope selector. // this function returns chart id based on current filter scope selector local state: // 1. if in single-edit mode, return the chart id for selected filter field. // 2. if in multi-edit mode, if all filter fields are from same chart id, diff --git a/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js b/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js index 7cef9ae6dc52..a23958da0c4b 100644 --- a/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js +++ b/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js @@ -24,14 +24,7 @@ export default function childChartsDidLoad({ chartQueries, layout, id }) { let minQueryStartTime = Infinity; const didLoad = chartIds.every(chartId => { const query = chartQueries[chartId] || {}; - - // filterbox's don't re-render, don't use stale update time - if (query.form_data && query.form_data.viz_type !== 'filter_box') { - minQueryStartTime = Math.min( - query.chartUpdateStartTime, - minQueryStartTime, - ); - } + minQueryStartTime = Math.min(query.chartUpdateStartTime, minQueryStartTime); return ['stopped', 'failed', 'rendered'].indexOf(query.chartStatus) > -1; }); diff --git a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx index fdc31f78af52..96b0f21655d7 100644 --- a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx +++ b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.test.tsx @@ -24,9 +24,6 @@ import mockState from 'spec/fixtures/mockState'; import reducerIndex from 'spec/helpers/reducerIndex'; import { screen, render } from 'spec/helpers/testing-library'; import { initialState } from 'src/SqlLab/fixtures'; -import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters'; -import { dashboardWithFilter } from 'spec/fixtures/mockDashboardLayout'; -import { buildActiveFilters } from './activeDashboardFilters'; import useFilterFocusHighlightStyles from './useFilterFocusHighlightStyles'; const TestComponent = ({ chartId }: { chartId: number }) => { @@ -185,64 +182,4 @@ describe('useFilterFocusHighlightStyles', () => { const styles = getComputedStyle(container); expect(parseFloat(styles.opacity)).toBe(1); }); - - it('should return unfocused styles if chart is not inside filter box scope', async () => { - buildActiveFilters({ - dashboardFilters, - components: dashboardWithFilter, - }); - - const chartId = 18; - const store = createMockStore({ - dashboardState: { - focusedFilterField: { - chartId, - column: 'test', - }, - }, - dashboardFilters: { - [chartId]: { - scopes: { - column: {}, - }, - }, - }, - }); - renderWrapper(20, store); - - const container = screen.getByTestId('test-component'); - - const styles = getComputedStyle(container); - expect(parseFloat(styles.opacity)).toBe(0.3); - }); - - it('should return focused styles if chart is inside filter box scope', async () => { - buildActiveFilters({ - dashboardFilters, - components: dashboardWithFilter, - }); - - const chartId = 18; - const store = createMockStore({ - dashboardState: { - focusedFilterField: { - chartId, - column: 'test', - }, - }, - dashboardFilters: { - [chartId]: { - scopes: { - column: {}, - }, - }, - }, - }); - renderWrapper(chartId, store); - - const container = screen.getByTestId('test-component'); - - const styles = getComputedStyle(container); - expect(parseFloat(styles.opacity)).toBe(1); - }); }); diff --git a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts index 8be43490ad3a..f1f428240c16 100644 --- a/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts +++ b/superset-frontend/src/dashboard/util/useFilterFocusHighlightStyles.ts @@ -19,7 +19,7 @@ import { useTheme } from '@superset-ui/core'; import { useSelector } from 'react-redux'; -import { getChartIdsInFilterBoxScope } from 'src/dashboard/util/activeDashboardFilters'; +import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters'; import { DashboardState, RootState } from 'src/dashboard/types'; const selectFocusedFilterScope = ( @@ -78,7 +78,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => { } } else if ( chartId === focusedFilterScope?.chartId || - getChartIdsInFilterBoxScope({ + getChartIdsInFilterScope({ filterScope: focusedFilterScope?.scope, }).includes(chartId) ) { diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx index 984b389a5c9e..bc2d83a90a6d 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx @@ -430,31 +430,27 @@ const ExploreChartPanel = ({ className="panel panel-default chart-container" showSplite={showSplite} > - {vizType === 'filter_box' ? ( - panelBody - ) : ( - - {panelBody} - - - )} + + {panelBody} + + {showDatasetModal && ( { datasetName: props.datasource?.name, action: this.canOverwriteSlice() ? 'overwrite' : 'saveas', isLoading: false, - vizType: props.form_data?.viz_type, dashboard: undefined, }; this.onDashboardChange = this.onDashboardChange.bind(this); @@ -383,32 +379,27 @@ class SaveModal extends React.Component { /> )} - {!( - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - this.state.vizType === 'filter_box' - ) && ( - - - {t('Select')} - {t(' a dashboard OR ')} - {t('create')} - {t(' a new one')} -
- } - /> - - )} + + + {t('Select')} + {t(' a dashboard OR ')} + {t('create')} + {t(' a new one')} +
+ } + /> + {info && } {this.props.alert && ( { !this.state.newSliceName || !this.state.dashboard || (this.props.datasource?.type !== DatasourceType.Table && - !this.state.datasetName) || - (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - this.state.vizType === 'filter_box') + !this.state.datasetName) } onClick={() => this.saveOrOverwrite(true)} > diff --git a/superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.jsx b/superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.jsx deleted file mode 100644 index 4cf88645d752..000000000000 --- a/superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.jsx +++ /dev/null @@ -1,108 +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. - */ -/* eslint-disable no-unused-expressions */ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -import FilterBoxItemControl from 'src/explore/components/controls/FilterBoxItemControl'; -import FormRow from 'src/components/FormRow'; -import datasources from 'spec/fixtures/mockDatasource'; -import ControlPopover from '../ControlPopover/ControlPopover'; - -const defaultProps = { - label: 'some label', - datasource: datasources['7__table'], - onChange: sinon.spy(), -}; - -describe('FilterBoxItemControl', () => { - let wrapper; - let inst; - - const getWrapper = propOverrides => { - const props = { ...defaultProps, ...propOverrides }; - return shallow(); - }; - beforeEach(() => { - wrapper = getWrapper(); - inst = wrapper.instance(); - }); - - it('renders a Popover', () => { - expect(wrapper.find(ControlPopover)).toExist(); - }); - - it('renderForms does the job', () => { - const popover = shallow(inst.renderForm()); - expect(popover.find(FormRow)).toHaveLength(8); - expect(popover.find(FormRow).get(1).props.control.props.value).toEqual( - 'some label', - ); - }); - - it('convert type for single value filter_box', () => { - inst = getWrapper({ - datasource: { - columns: [ - { - column_name: 'SP_POP_TOTL', - description: null, - expression: null, - filterable: true, - groupby: true, - id: 312, - is_dttm: false, - type: 'FLOAT', - verbose_name: null, - }, - ], - metrics: [ - { - d3format: null, - description: null, - expression: 'sum("SP_POP_TOTL")', - id: 3, - metric_name: 'sum__SP_POP_TOTL', - verbose_name: null, - warning_text: null, - }, - ], - }, - }).instance(); - inst.setState({ - asc: true, - clearable: true, - column: 'SP_POP_TOTL', - defaultValue: 254454778, - metric: undefined, - multiple: false, - }); - inst.setState = sinon.spy(); - - inst.onControlChange('defaultValue', '1'); - expect(inst.setState.callCount).toBe(1); - expect(inst.setState.getCall(0).args[0]).toEqual({ defaultValue: 1 }); - - // user input is invalid for number type column - inst.onControlChange('defaultValue', 'abc'); - expect(inst.setState.callCount).toBe(2); - expect(inst.setState.getCall(1).args[0]).toEqual({ defaultValue: null }); - }); -}); diff --git a/superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.tsx b/superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.tsx deleted file mode 100644 index 4ad09e958cb7..000000000000 --- a/superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.tsx +++ /dev/null @@ -1,61 +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 { render, screen } from 'spec/helpers/testing-library'; -import userEvent from '@testing-library/user-event'; -import FilterBoxItemControl from '.'; - -const createProps = () => ({ - datasource: { - columns: [], - metrics: [], - }, - asc: true, - clearable: true, - multiple: true, - column: 'developer_type', - label: 'Developer Type', - metric: undefined, - searchAllOptions: false, - defaultValue: undefined, - onChange: jest.fn(), -}); - -test('Should render', () => { - const props = createProps(); - render(); - expect(screen.getByTestId('FilterBoxItemControl')).toBeInTheDocument(); - expect(screen.getByRole('button')).toBeInTheDocument(); -}); - -test('Should open modal', () => { - const props = createProps(); - render(); - userEvent.click(screen.getByRole('button')); - expect(screen.getByText('Filter configuration')).toBeInTheDocument(); - expect(screen.getByText('Column')).toBeInTheDocument(); - expect(screen.getByText('Label')).toBeInTheDocument(); - expect(screen.getByText('Default')).toBeInTheDocument(); - expect(screen.getByText('Sort metric')).toBeInTheDocument(); - expect(screen.getByText('Sort ascending')).toBeInTheDocument(); - expect(screen.getByText('Allow multiple selections')).toBeInTheDocument(); - expect(screen.getByText('Search all filter options')).toBeInTheDocument(); - expect(screen.getByText('Required')).toBeInTheDocument(); -}); diff --git a/superset-frontend/src/explore/components/controls/FilterBoxItemControl/index.jsx b/superset-frontend/src/explore/components/controls/FilterBoxItemControl/index.jsx deleted file mode 100644 index 4c8a367e3520..000000000000 --- a/superset-frontend/src/explore/components/controls/FilterBoxItemControl/index.jsx +++ /dev/null @@ -1,295 +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 PropTypes from 'prop-types'; -import { t } from '@superset-ui/core'; -import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; - -import FormRow from 'src/components/FormRow'; -import { Select } from 'src/components'; -import CheckboxControl from 'src/explore/components/controls/CheckboxControl'; -import TextControl from 'src/explore/components/controls/TextControl'; -import { FILTER_CONFIG_ATTRIBUTES } from 'src/explore/constants'; -import ControlPopover from '../ControlPopover/ControlPopover'; - -const INTEGRAL_TYPES = new Set([ - 'TINYINT', - 'SMALLINT', - 'INT', - 'INTEGER', - 'BIGINT', - 'LONG', -]); -const DECIMAL_TYPES = new Set([ - 'FLOAT', - 'DOUBLE', - 'REAL', - 'NUMERIC', - 'DECIMAL', - 'MONEY', -]); - -const propTypes = { - datasource: PropTypes.object.isRequired, - onChange: PropTypes.func, - asc: PropTypes.bool, - clearable: PropTypes.bool, - multiple: PropTypes.bool, - column: PropTypes.string, - label: PropTypes.string, - metric: PropTypes.string, - searchAllOptions: PropTypes.bool, - defaultValue: PropTypes.string, -}; - -const defaultProps = { - onChange: () => {}, - asc: true, - clearable: true, - multiple: true, - searchAllOptions: false, -}; - -const STYLE_WIDTH = { width: 350 }; - -export default class FilterBoxItemControl extends React.Component { - constructor(props) { - super(props); - const { - column, - metric, - asc, - clearable, - multiple, - searchAllOptions, - label, - defaultValue, - } = props; - this.state = { - column, - metric, - label, - asc, - clearable, - multiple, - searchAllOptions, - defaultValue, - }; - this.onChange = this.onChange.bind(this); - this.onControlChange = this.onControlChange.bind(this); - } - - onChange() { - this.props.onChange(this.state); - } - - onControlChange(attr, value) { - let typedValue = value; - const { column: selectedColumnName, multiple } = this.state; - if (value && !multiple && attr === FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE) { - // if single value filter_box, - // convert input value string to the column's data type - const { datasource } = this.props; - const selectedColumn = datasource.columns.find( - col => col.column_name === selectedColumnName, - ); - - if (selectedColumn && selectedColumn.type) { - const type = selectedColumn.type.toUpperCase(); - if (type === 'BOOLEAN') { - typedValue = value === 'true'; - } else if (INTEGRAL_TYPES.has(type)) { - typedValue = Number.isNaN(Number(value)) ? null : parseInt(value, 10); - } else if (DECIMAL_TYPES.has(type)) { - typedValue = Number.isNaN(Number(value)) ? null : parseFloat(value); - } - } - } - this.setState({ [attr]: typedValue }, this.onChange); - } - - setType() {} - - textSummary() { - return this.state.column || 'N/A'; - } - - renderForm() { - return ( -
- col.column_name !== this.state.column) - .map(col => ({ - value: col.column_name, - label: col.column_name, - })) - .concat([ - { value: this.state.column, label: this.state.column }, - ])} - onChange={v => this.onControlChange('column', v)} - /> - } - /> - this.onControlChange('label', v)} - /> - } - /> - - this.onControlChange(FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE, v) - } - /> - } - /> - m.metric_name !== this.state.metric) - .map(m => ({ - value: m.metric_name, - label: m.metric_name, - })) - .concat([ - { value: this.state.metric, label: this.state.metric }, - ])} - onChange={v => this.onControlChange('metric', v)} - /> - } - /> - this.onControlChange('asc', v)} - /> - } - /> - - this.onControlChange(FILTER_CONFIG_ATTRIBUTES.MULTIPLE, v) - } - /> - } - /> - - this.onControlChange( - FILTER_CONFIG_ATTRIBUTES.SEARCH_ALL_OPTIONS, - v, - ) - } - /> - } - /> - this.onControlChange('clearable', !v)} - /> - } - /> -
- ); - } - - renderPopover() { - return ( -
- {this.renderForm()} -
- ); - } - - render() { - return ( - - {this.textSummary()}{' '} - - - - - ); - } -} - -FilterBoxItemControl.propTypes = propTypes; -FilterBoxItemControl.defaultProps = defaultProps; diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx index 8934ca9cf031..347b339481de 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx @@ -77,7 +77,6 @@ const DEFAULT_ORDER = [ 'echarts_timeseries_scatter', 'pie', 'mixed_timeseries', - 'filter_box', 'dist_bar', 'area', 'bar', diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx index ec78b4267d87..bdfcf1e40a87 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx @@ -23,8 +23,6 @@ import { getChartMetadataRegistry, styled, SupersetTheme, - isFeatureEnabled, - FeatureFlag, } from '@superset-ui/core'; import { usePluginContext } from 'src/components/DynamicPlugins'; import Modal from 'src/components/Modal'; @@ -48,13 +46,6 @@ const bootstrapData = getBootstrapData(); const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || []; const metadataRegistry = getChartMetadataRegistry(); -if ( - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - !denyList.includes('filter_box') -) { - denyList.push('filter_box'); -} - export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; function VizSupportValidation({ vizType }: { vizType: string }) { diff --git a/superset-frontend/src/explore/components/controls/index.js b/superset-frontend/src/explore/components/controls/index.js index cba3c27f5569..a5d65f776837 100644 --- a/superset-frontend/src/explore/components/controls/index.js +++ b/superset-frontend/src/explore/components/controls/index.js @@ -38,7 +38,6 @@ import ViewportControl from './ViewportControl'; import VizTypeControl from './VizTypeControl'; import MetricsControl from './MetricControl/MetricsControl'; import AdhocFilterControl from './FilterControl/AdhocFilterControl'; -import FilterBoxItemControl from './FilterBoxItemControl'; import ConditionalFormattingControl from './ConditionalFormattingControl'; import ContourControl from './ContourControl'; import DndColumnSelectControl, { @@ -78,7 +77,6 @@ const controlMap = { VizTypeControl, MetricsControl, AdhocFilterControl, - FilterBoxItemControl, ConditionalFormattingControl, XAxisSortControl, ContourControl, diff --git a/superset-frontend/src/pages/ChartCreation/index.tsx b/superset-frontend/src/pages/ChartCreation/index.tsx index 7a1ff43dc7a6..f3a602dbc9d5 100644 --- a/superset-frontend/src/pages/ChartCreation/index.tsx +++ b/superset-frontend/src/pages/ChartCreation/index.tsx @@ -20,8 +20,6 @@ import React, { ReactNode } from 'react'; import rison from 'rison'; import querystring from 'query-string'; import { - isFeatureEnabled, - FeatureFlag, isDefined, JsonResponse, styled, @@ -64,13 +62,6 @@ const ELEMENTS_EXCEPT_VIZ_GALLERY = ESTIMATED_NAV_HEIGHT + 250; const bootstrapData = getBootstrapData(); const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || []; -if ( - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - !denyList.includes('filter_box') -) { - denyList.push('filter_box'); -} - const StyledContainer = styled.div` ${({ theme }) => ` flex: 1 1 auto; diff --git a/superset-frontend/src/utils/localStorageHelpers.ts b/superset-frontend/src/utils/localStorageHelpers.ts index beb5ed10147a..12fba5b94c98 100644 --- a/superset-frontend/src/utils/localStorageHelpers.ts +++ b/superset-frontend/src/utils/localStorageHelpers.ts @@ -30,7 +30,6 @@ export enum LocalStorageKeys { * TODO: Update all local storage keys to follow the new pattern. This is a breaking change, * and therefore should be done in a major release. */ - filter_box_transition_snoozed_at = 'filter_box_transition_snoozed_at', db = 'db', chart_split_sizes = 'chart_split_sizes', controls_width = 'controls_width', @@ -59,7 +58,6 @@ export enum LocalStorageKeys { } export type LocalStorageValues = { - filter_box_transition_snoozed_at: Record; db: object | null; chart_split_sizes: [number, number]; controls_width: number; diff --git a/superset-frontend/src/utils/urlUtils.ts b/superset-frontend/src/utils/urlUtils.ts index 07c8ad550074..91c0ae801cb8 100644 --- a/superset-frontend/src/utils/urlUtils.ts +++ b/superset-frontend/src/utils/urlUtils.ts @@ -172,7 +172,7 @@ export function getDashboardPermalink({ */ anchor?: string; }) { - // only encode filter box state if non-empty + // only encode filter state if non-empty return getPermalink(`/api/v1/dashboard/${dashboardId}/permalink`, { urlParams: getDashboardUrlParams(), dataMask, diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx deleted file mode 100644 index a2b2a9a28296..000000000000 --- a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx +++ /dev/null @@ -1,480 +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 PropTypes from 'prop-types'; -import { debounce } from 'lodash'; -import { max as d3Max } from 'd3-array'; -import { - AsyncCreatableSelect, - CreatableSelect, -} from 'src/components/DeprecatedSelect'; -import Button from 'src/components/Button'; -import { - css, - styled, - t, - SupersetClient, - ensureIsArray, - withTheme, -} from '@superset-ui/core'; -import { Global } from '@emotion/react'; - -import { - BOOL_FALSE_DISPLAY, - BOOL_TRUE_DISPLAY, - SLOW_DEBOUNCE, -} from 'src/constants'; -import { FormLabel } from 'src/components/Form'; -import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; -import ControlRow from 'src/explore/components/ControlRow'; -import Control from 'src/explore/components/Control'; -import { controls } from 'src/explore/controls'; -import { getExploreUrl } from 'src/explore/exploreUtils'; -import OnPasteSelect from 'src/components/DeprecatedSelect/OnPasteSelect'; -import { - FILTER_CONFIG_ATTRIBUTES, - FILTER_OPTIONS_LIMIT, - TIME_FILTER_LABELS, - TIME_FILTER_MAP, -} from 'src/explore/constants'; - -// a shortcut to a map key, used by many components -export const TIME_RANGE = TIME_FILTER_MAP.time_range; - -const propTypes = { - chartId: PropTypes.number.isRequired, - origSelectedValues: PropTypes.object, - datasource: PropTypes.object.isRequired, - instantFiltering: PropTypes.bool, - filtersFields: PropTypes.arrayOf( - PropTypes.shape({ - field: PropTypes.string, - label: PropTypes.string, - }), - ), - filtersChoices: PropTypes.objectOf( - PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - text: PropTypes.string, - filter: PropTypes.string, - metric: PropTypes.number, - }), - ), - ), - onChange: PropTypes.func, - onFilterMenuOpen: PropTypes.func, - onFilterMenuClose: PropTypes.func, - showDateFilter: PropTypes.bool, - showSqlaTimeGrain: PropTypes.bool, - showSqlaTimeColumn: PropTypes.bool, -}; -const defaultProps = { - origSelectedValues: {}, - onChange: () => {}, - onFilterMenuOpen: () => {}, - onFilterMenuClose: () => {}, - showDateFilter: false, - showSqlaTimeGrain: false, - showSqlaTimeColumn: false, - instantFiltering: false, -}; - -const StyledFilterContainer = styled.div` - ${({ theme }) => ` - display: flex; - flex-direction: column; - margin-bottom: ${theme.gridUnit * 2 + 2}px; - - &:last-child { - margin-bottom: 0; - } - - label { - display: flex; - font-weight: ${theme.typography.weights.bold}; - } - - .filter-badge-container { - width: 30px; - padding-right: ${theme.gridUnit * 2 + 2}px; - } - - .filter-badge-container + div { - width: 100%; - } - `} -`; - -/** - * @deprecated in version 3.0. - */ -class FilterBox extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - selectedValues: props.origSelectedValues, - // this flag is used by non-instant filter, to make the apply button enabled/disabled - hasChanged: false, - }; - this.debouncerCache = {}; - this.maxValueCache = {}; - this.changeFilter = this.changeFilter.bind(this); - this.onFilterMenuOpen = this.onFilterMenuOpen.bind(this); - this.onOpenDateFilterControl = this.onOpenDateFilterControl.bind(this); - this.onFilterMenuClose = this.onFilterMenuClose.bind(this); - } - - onFilterMenuOpen(column) { - return this.props.onFilterMenuOpen(this.props.chartId, column); - } - - onFilterMenuClose(column) { - return this.props.onFilterMenuClose(this.props.chartId, column); - } - - onOpenDateFilterControl() { - return this.onFilterMenuOpen(TIME_RANGE); - } - - onCloseDateFilterControl = () => this.onFilterMenuClose(TIME_RANGE); - - getControlData(controlName) { - const { selectedValues } = this.state; - const control = { - ...controls[controlName], // TODO: make these controls ('granularity_sqla', 'time_grain_sqla') accessible from getControlsForVizType. - name: controlName, - key: `control-${controlName}`, - value: selectedValues[TIME_FILTER_MAP[controlName]], - actions: { setControlValue: this.changeFilter }, - }; - const mapFunc = control.mapStateToProps; - return mapFunc ? { ...control, ...mapFunc(this.props) } : control; - } - - /** - * Get known max value of a column - */ - getKnownMax(key, choices) { - this.maxValueCache[key] = Math.max( - this.maxValueCache[key] || 0, - d3Max(choices || this.props.filtersChoices[key] || [], x => x.metric), - ); - return this.maxValueCache[key]; - } - - clickApply() { - const { selectedValues } = this.state; - this.setState({ hasChanged: false }, () => { - this.props.onChange(selectedValues, false); - }); - } - - changeFilter(filter, options) { - const fltr = TIME_FILTER_MAP[filter] || filter; - let vals = null; - if (options !== null) { - if (Array.isArray(options)) { - vals = options.map(opt => (typeof opt === 'string' ? opt : opt.value)); - } else if (Object.values(TIME_FILTER_MAP).includes(fltr)) { - vals = options.value ?? options; - } else { - // must use array member for legacy extra_filters's value - vals = ensureIsArray(options.value ?? options); - } - } - - this.setState( - prevState => ({ - selectedValues: { - ...prevState.selectedValues, - [fltr]: vals, - }, - hasChanged: true, - }), - () => { - if (this.props.instantFiltering) { - this.props.onChange({ [fltr]: vals }, false); - } - }, - ); - } - - /** - * Generate a debounce function that loads options for a specific column - */ - debounceLoadOptions(key) { - if (!(key in this.debouncerCache)) { - this.debouncerCache[key] = debounce((input, callback) => { - this.loadOptions(key, input).then(callback); - }, SLOW_DEBOUNCE); - } - return this.debouncerCache[key]; - } - - /** - * Transform select options, add bar background - */ - transformOptions(options, max) { - const maxValue = max === undefined ? d3Max(options, x => x.metric) : max; - return options.map(opt => { - const perc = Math.round((opt.metric / maxValue) * 100); - const color = 'lightgrey'; - const backgroundImage = `linear-gradient(to right, ${color}, ${color} ${perc}%, rgba(0,0,0,0) ${perc}%`; - const style = { backgroundImage }; - let label = opt.id; - if (label === true) { - label = BOOL_TRUE_DISPLAY; - } else if (label === false) { - label = BOOL_FALSE_DISPLAY; - } - return { value: opt.id, label, style }; - }); - } - - async loadOptions(key, inputValue = '') { - const input = inputValue.toLowerCase(); - const sortAsc = this.props.filtersFields.find(x => x.key === key).asc; - const formData = { - ...this.props.rawFormData, - adhoc_filters: inputValue - ? [ - { - clause: 'WHERE', - expressionType: 'SIMPLE', - subject: key, - operator: 'ILIKE', - comparator: `%${input}%`, - }, - ] - : null, - }; - - const { json } = await SupersetClient.get({ - url: getExploreUrl({ - formData, - endpointType: 'json', - method: 'GET', - }), - }); - const options = (json?.data?.[key] || []).filter(x => x.id); - if (!options || options.length === 0) { - return []; - } - if (input) { - // sort those starts with search query to front - options.sort((a, b) => { - const labelA = a.id.toLowerCase(); - const labelB = b.id.toLowerCase(); - const textOrder = labelB.startsWith(input) - labelA.startsWith(input); - return textOrder === 0 - ? (a.metric - b.metric) * (sortAsc ? 1 : -1) - : textOrder; - }); - } - return this.transformOptions(options, this.getKnownMax(key, options)); - } - - renderDateFilter() { - const { showDateFilter } = this.props; - const label = TIME_FILTER_LABELS.time_range; - if (showDateFilter) { - return ( -
-
- { - this.changeFilter(TIME_RANGE, newValue); - }} - onOpenDateFilterControl={this.onOpenDateFilterControl} - onCloseDateFilterControl={this.onCloseDateFilterControl} - value={this.state.selectedValues[TIME_RANGE] || 'No filter'} - endpoints={['inclusive', 'exclusive']} - /> -
-
- ); - } - return null; - } - - renderDatasourceFilters() { - const { showSqlaTimeGrain, showSqlaTimeColumn } = this.props; - const datasourceFilters = []; - const sqlaFilters = []; - if (showSqlaTimeGrain) sqlaFilters.push('time_grain_sqla'); - if (showSqlaTimeColumn) sqlaFilters.push('granularity_sqla'); - if (sqlaFilters.length) { - datasourceFilters.push( - ( - - ))} - />, - ); - } - return datasourceFilters; - } - - renderSelect(filterConfig) { - const { filtersChoices } = this.props; - const { selectedValues } = this.state; - this.debouncerCache = {}; - this.maxValueCache = {}; - - // Add created options to filtersChoices, even though it doesn't exist, - // or these options will exist in query sql but invisible to end user. - Object.keys(selectedValues) - .filter(key => key in filtersChoices) - .forEach(key => { - // empty values are ignored - if (!selectedValues[key]) { - return; - } - const choices = filtersChoices[key] || (filtersChoices[key] = []); - const choiceIds = new Set(choices.map(f => f.id)); - const selectedValuesForKey = Array.isArray(selectedValues[key]) - ? selectedValues[key] - : [selectedValues[key]]; - selectedValuesForKey - .filter(value => value !== null && !choiceIds.has(value)) - .forEach(value => { - choices.unshift({ - filter: key, - id: value, - text: value, - metric: 0, - }); - }); - }); - const { - key, - label, - [FILTER_CONFIG_ATTRIBUTES.MULTIPLE]: isMultiple, - [FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE]: defaultValue, - [FILTER_CONFIG_ATTRIBUTES.CLEARABLE]: isClearable, - [FILTER_CONFIG_ATTRIBUTES.SEARCH_ALL_OPTIONS]: searchAllOptions, - } = filterConfig; - const data = filtersChoices[key] || []; - let value = selectedValues[key] || null; - - // Assign default value if required - if (value === undefined && defaultValue) { - // multiple values are separated by semicolons - value = isMultiple ? defaultValue.split(';') : defaultValue; - } - - return ( - { - // avoid excessive re-renders - if (newValue !== value) { - this.changeFilter(key, newValue); - } - }} - // TODO try putting this back once react-select is upgraded - // onFocus={() => this.onFilterMenuOpen(key)} - onMenuOpen={() => this.onFilterMenuOpen(key)} - onBlur={() => this.onFilterMenuClose(key)} - onMenuClose={() => this.onFilterMenuClose(key)} - selectWrap={ - searchAllOptions && data.length >= FILTER_OPTIONS_LIMIT - ? AsyncCreatableSelect - : CreatableSelect - } - noResultsText={t('No results found')} - forceOverflow - /> - ); - } - - renderFilters() { - const { filtersFields = [] } = this.props; - return filtersFields.map(filterConfig => { - const { label, key } = filterConfig; - return ( - - {label} - {this.renderSelect(filterConfig)} - - ); - }); - } - - render() { - const { instantFiltering, width, height } = this.props; - const { zIndex, gridUnit } = this.props.theme; - return ( - <> - div:not(.alert) { - padding-top: 0; - } - - .filter_box { - padding: ${gridUnit * 2 + 2}px 0; - overflow: visible !important; - - &:hover { - z-index: ${zIndex.max}; - } - } - `} - /> -
- {this.renderDateFilter()} - {this.renderDatasourceFilters()} - {this.renderFilters()} - {!instantFiltering && ( - - )} -
- - ); - } -} - -FilterBox.propTypes = propTypes; -FilterBox.defaultProps = defaultProps; - -export default withTheme(FilterBox); diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.test.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.test.jsx deleted file mode 100644 index e37a4bf1c69d..000000000000 --- a/superset-frontend/src/visualizations/FilterBox/FilterBox.test.jsx +++ /dev/null @@ -1,87 +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 { styledMount as mount } from 'spec/helpers/theming'; -import FilterBox from 'src/visualizations/FilterBox/FilterBox'; -import SelectControl from 'src/explore/components/controls/SelectControl'; - -describe('FilterBox', () => { - it('should only add defined non-predefined options to filtersChoices', () => { - const wrapper = mount( - , - ); - const inst = wrapper.find('FilterBox').instance(); - // choose a predefined value - inst.setState({ selectedValues: { name: ['John'] } }); - expect(inst.props.filtersChoices.name.length).toEqual(2); - // reset selection - inst.setState({ selectedValues: { name: null } }); - expect(inst.props.filtersChoices.name.length).toEqual(2); - // Add a new name - inst.setState({ selectedValues: { name: 'James' } }); - expect(inst.props.filtersChoices.name.length).toEqual(3); - }); - - it('should support granularity_sqla options', () => { - const wrapper = mount( - , - ); - - expect(wrapper.find(SelectControl).props().choices).toEqual( - expect.arrayContaining([ - ['created_on', 'created_on'], - ['changed_on', 'changed_on'], - ]), - ); - }); -}); diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBoxChartPlugin.js b/superset-frontend/src/visualizations/FilterBox/FilterBoxChartPlugin.js deleted file mode 100644 index 774f7bdedffa..000000000000 --- a/superset-frontend/src/visualizations/FilterBox/FilterBoxChartPlugin.js +++ /dev/null @@ -1,52 +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 { t, ChartMetadata, ChartPlugin, ChartLabel } from '@superset-ui/core'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import example1 from './images/example1.jpg'; -import example2 from './images/example2.jpg'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Tools'), - label: ChartLabel.DEPRECATED, - name: t('Filter box (legacy)'), - description: - t(`Chart component that lets you add a custom filter UI in your dashboard. When added to dashboard, a filter box lets users specify specific values or ranges to filter charts by. The charts that each filter box is applied to can be fine tuned as well in the dashboard view. - - Note that this plugin is being replaced with the new Filters feature that lives in the dashboard view itself. It's easier to use and has more capabilities!`), - exampleGallery: [{ url: example1 }, { url: example2 }], - thumbnail, - useLegacyApi: true, - tags: [t('Legacy'), t('Deprecated')], -}); - -/** - * @deprecated in version 3.0. - */ -export default class FilterBoxChartPlugin extends ChartPlugin { - constructor() { - super({ - controlPanel, - metadata, - transformProps, - loadChart: () => import('./FilterBox'), - }); - } -} diff --git a/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx b/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx deleted file mode 100644 index 60bb9c83af9e..000000000000 --- a/superset-frontend/src/visualizations/FilterBox/controlPanel.jsx +++ /dev/null @@ -1,103 +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 { t } from '@superset-ui/core'; -import { sections } from '@superset-ui/chart-controls'; - -export default { - controlPanelSections: [ - sections.legacyTimeseriesTime, - { - label: t('Filters configuration'), - expanded: true, - controlSetRows: [ - [ - { - name: 'filter_configs', - config: { - type: 'CollectionControl', - label: t('Filters'), - description: t('Filter configuration for the filter box'), - validators: [], - controlName: 'FilterBoxItemControl', - mapStateToProps: ({ datasource }) => ({ datasource }), - }, - }, - ], - [
], - [ - { - name: 'date_filter', - config: { - type: 'CheckboxControl', - label: t('Date filter'), - default: true, - description: t('Whether to include a time filter'), - }, - }, - ], - [ - { - name: 'instant_filtering', - config: { - type: 'CheckboxControl', - label: t('Instant filtering'), - renderTrigger: true, - default: false, - description: t( - 'Check to apply filters instantly as they change instead of displaying [Apply] button', - ), - }, - }, - ], - [ - { - name: 'show_sqla_time_granularity', - config: { - type: 'CheckboxControl', - label: t('Show time grain dropdown'), - default: false, - description: t('Check to include time grain dropdown'), - }, - }, - ], - [ - { - name: 'show_sqla_time_column', - config: { - type: 'CheckboxControl', - label: t('Show time column'), - default: false, - description: t('Check to include time column dropdown'), - }, - }, - ], - ['adhoc_filters'], - ], - }, - ], - controlOverrides: { - adhoc_filters: { - label: t('Limit selector values'), - description: t( - 'These filters apply to the values available in the dropdowns', - ), - }, - }, -}; diff --git a/superset-frontend/src/visualizations/FilterBox/images/example1.jpg b/superset-frontend/src/visualizations/FilterBox/images/example1.jpg deleted file mode 100644 index cc109ee5aa38..000000000000 Binary files a/superset-frontend/src/visualizations/FilterBox/images/example1.jpg and /dev/null differ diff --git a/superset-frontend/src/visualizations/FilterBox/images/example2.jpg b/superset-frontend/src/visualizations/FilterBox/images/example2.jpg deleted file mode 100644 index 17912d52f72c..000000000000 Binary files a/superset-frontend/src/visualizations/FilterBox/images/example2.jpg and /dev/null differ diff --git a/superset-frontend/src/visualizations/FilterBox/images/thumbnail.png b/superset-frontend/src/visualizations/FilterBox/images/thumbnail.png deleted file mode 100644 index be08f687a5b7..000000000000 Binary files a/superset-frontend/src/visualizations/FilterBox/images/thumbnail.png and /dev/null differ diff --git a/superset-frontend/src/visualizations/FilterBox/images/thumbnailLarge.png b/superset-frontend/src/visualizations/FilterBox/images/thumbnailLarge.png deleted file mode 100644 index 209259c699a2..000000000000 Binary files a/superset-frontend/src/visualizations/FilterBox/images/thumbnailLarge.png and /dev/null differ diff --git a/superset-frontend/src/visualizations/FilterBox/transformProps.ts b/superset-frontend/src/visualizations/FilterBox/transformProps.ts deleted file mode 100644 index 59387edae1d7..000000000000 --- a/superset-frontend/src/visualizations/FilterBox/transformProps.ts +++ /dev/null @@ -1,74 +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 { FilterBoxChartProps } from './types'; - -const NOOP = () => {}; - -export default function transformProps(chartProps: FilterBoxChartProps) { - const { - datasource, - formData, - hooks, - initialValues, - queriesData, - rawDatasource = {}, - rawFormData, - width, - height, - } = chartProps; - const { - onAddFilter = NOOP, - onFilterMenuOpen = NOOP, - onFilterMenuClose = NOOP, - } = hooks; - const { - sliceId, - dateFilter, - instantFiltering, - showSqlaTimeColumn, - showSqlaTimeGranularity, - } = formData; - const { verboseMap = {} } = datasource; - const filterConfigs = formData.filterConfigs || []; - - const filtersFields = filterConfigs.map(flt => ({ - ...flt, - key: flt.column, - label: flt.label || verboseMap[flt.column] || flt.column, - })); - - return { - chartId: sliceId, - width, - height, - datasource: rawDatasource, - filtersChoices: queriesData[0].data, - filtersFields, - instantFiltering, - onChange: onAddFilter, - onFilterMenuOpen, - onFilterMenuClose, - origSelectedValues: initialValues || {}, - showDateFilter: dateFilter, - showSqlaTimeColumn, - showSqlaTimeGrain: showSqlaTimeGranularity, - // the original form data, needed for async select options - rawFormData, - }; -} diff --git a/superset-frontend/src/visualizations/FilterBox/types.ts b/superset-frontend/src/visualizations/FilterBox/types.ts deleted file mode 100644 index 316a29f9bbef..000000000000 --- a/superset-frontend/src/visualizations/FilterBox/types.ts +++ /dev/null @@ -1,29 +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 { ChartProps, Datasource } from '@superset-ui/core'; - -export interface FilterConfig { - column: string; - label: string; -} - -export type FilterBoxChartProps = ChartProps & { - datasource?: Datasource; - formData: ChartProps['formData'] & { filterConfigs: FilterConfig[] }; -}; diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js index a8cc847c8b4c..e96b528c9dea 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -76,7 +76,6 @@ import { } from 'src/filters/components'; import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table'; import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars'; -import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin'; import TimeTableChartPlugin from '../TimeTable'; export default class MainPreset extends Preset { @@ -98,7 +97,6 @@ export default class MainPreset extends Preset { new CountryMapChartPlugin().configure({ key: 'country_map' }), new DistBarChartPlugin().configure({ key: 'dist_bar' }), new EventFlowChartPlugin().configure({ key: 'event_flow' }), - new FilterBoxChartPlugin().configure({ key: 'filter_box' }), new EchartsFunnelChartPlugin().configure({ key: 'funnel' }), new EchartsTreemapChartPlugin().configure({ key: 'treemap_v2' }), new EchartsGaugeChartPlugin().configure({ key: 'gauge_chart' }), diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index 3399be3d608a..db512b074f14 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -247,7 +247,6 @@ const config = { 'redux', 'react-redux', 'react-hot-loader', - 'react-select', 'react-sortable-hoc', 'react-table', 'react-ace', diff --git a/superset/cli/native_filters.py b/superset/cli/native_filters.py deleted file mode 100644 index 75df428e381e..000000000000 --- a/superset/cli/native_filters.py +++ /dev/null @@ -1,398 +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 json -from copy import deepcopy -from textwrap import dedent - -import click -from click_option_group import optgroup, RequiredMutuallyExclusiveOptionGroup -from flask.cli import with_appcontext -from sqlalchemy import Column, ForeignKey, Integer, String, Table, Text -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -from superset import db, is_feature_enabled - -Base = declarative_base() - - -dashboard_slices = Table( - "dashboard_slices", - Base.metadata, - Column("id", Integer, primary_key=True), - Column("dashboard_id", Integer, ForeignKey("dashboards.id")), - Column("slice_id", Integer, ForeignKey("slices.id")), -) - - -slice_user = Table( - "slice_user", - Base.metadata, - Column("id", Integer, primary_key=True), - Column("slice_id", Integer, ForeignKey("slices.id")), -) - - -class Dashboard(Base): # type: ignore # pylint: disable=too-few-public-methods - __tablename__ = "dashboards" - - id = Column(Integer, primary_key=True) - json_metadata = Column(Text) - slices = relationship("Slice", secondary=dashboard_slices, backref="dashboards") - position_json = Column() - - def __repr__(self) -> str: - return f"Dashboard<{self.id}>" - - -class Slice(Base): # type: ignore # pylint: disable=too-few-public-methods - __tablename__ = "slices" - - id = Column(Integer, primary_key=True) - datasource_id = Column(Integer) - params = Column(Text) - slice_name = Column(String(250)) - viz_type = Column(String(250)) - - def __repr__(self) -> str: - return f"Slice<{self.id}>" - - -@click.group() -def native_filters() -> None: - """ - Perform native filter operations. - """ - - -@native_filters.command() -@with_appcontext -@optgroup.group( - "Grouped options", - cls=RequiredMutuallyExclusiveOptionGroup, -) -@optgroup.option( - "--all", - "all_", - default=False, - help="Upgrade all dashboards", - is_flag=True, -) -@optgroup.option( - "--id", - "dashboard_ids", - help="Upgrade the specific dashboard. Can be supplied multiple times.", - multiple=True, - type=int, -) -def upgrade( - all_: bool, # pylint: disable=unused-argument - dashboard_ids: tuple[int, ...], -) -> None: - """ - Upgrade legacy filter-box charts to native dashboard filters. - """ - - # pylint: disable=import-outside-toplevel - from superset.utils.dashboard_filter_scopes_converter import ( - convert_filter_scopes_to_native_filters, - ) - - if not is_feature_enabled("DASHBOARD_NATIVE_FILTERS"): - click.echo("The 'DASHBOARD_NATIVE_FILTERS' feature needs to be enabled.") - return - - # Mapping between the CHART- and MARKDOWN- IDs. - mapping = {} - - for dashboard in ( # pylint: disable=too-many-nested-blocks - db.session.query(Dashboard) - .filter(*[Dashboard.id.in_(dashboard_ids)] if dashboard_ids else []) - .all() - ): - click.echo(f"Upgrading {str(dashboard)}") - - try: - json_metadata = json.loads(dashboard.json_metadata or "{}") - position_json = json.loads(dashboard.position_json or "{}") - - if "native_filter_migration" in json_metadata: - click.echo(f"{dashboard} has already been upgraded") - continue - - # Save the native and legacy filter configurations for recovery purposes. - json_metadata["native_filter_migration"] = { - key: deepcopy(json_metadata[key]) - for key in ( - "default_filters", - "filter_scopes", - "native_filter_configuration", - ) - if key in json_metadata - } - - filter_boxes_by_id = { - slc.id: slc for slc in dashboard.slices if slc.viz_type == "filter_box" - } - - # Convert the legacy filter configurations to native filters. - native_filter_configuration = json_metadata.setdefault( - "native_filter_configuration", - [], - ) - - native_filter_configuration.extend( - convert_filter_scopes_to_native_filters( - json_metadata, - position_json, - filter_boxes=list(filter_boxes_by_id.values()), - ), - ) - - # Remove the legacy filter configuration. - for key in ["default_filters", "filter_scopes"]: - json_metadata.pop(key, None) - - # Replace the filter-box charts with markdown elements. - for key, value in list(position_json.items()): # Immutable iteration - if ( - isinstance(value, dict) - and value["type"] == "CHART" - and (meta := value.get("meta")) - and meta["chartId"] in filter_boxes_by_id - ): - slc = filter_boxes_by_id[meta["chartId"]] - mapping[key] = key.replace("CHART-", "MARKDOWN-") - - value["id"] = mapping[key] - value["type"] = "MARKDOWN" - - meta["code"] = dedent( - f""" - ⚠ The {slc.slice_name} - filter-box chart has been migrated to a native filter. - - This placeholder markdown element can be safely removed after - verifying that the native filter(s) have been correctly applied, - otherwise ask an admin to revert the migration. - """ - ) - - # Save the filter-box info for recovery purposes. - meta["native_filter_migration"] = { - key: meta.pop(key) - for key in ( - "chartId", - "sliceName", - "sliceNameOverride", - ) - if key in meta - } - - position_json[mapping[key]] = value - del position_json[key] - - # Replace the relevant CHART- references. - for value in position_json.values(): - if isinstance(value, dict): - for relation in ["children", "parents"]: - if relation in value: - for idx, key in enumerate(value[relation]): - if key in mapping: - value[relation][idx] = mapping[key] - - # Remove the filter-box charts from the dashboard/slice mapping - dashboard.slices = [ - slc for slc in dashboard.slices if slc.viz_type != "filter_box" - ] - - dashboard.json_metadata = json.dumps(json_metadata) - dashboard.position_json = json.dumps(position_json) - except Exception: # pylint: disable=broad-except - click.echo(f"Unable to upgrade {str(dashboard)}") - - db.session.commit() - db.session.close() - - -@native_filters.command() -@with_appcontext -@optgroup.group( - "Grouped options", - cls=RequiredMutuallyExclusiveOptionGroup, -) -@optgroup.option( - "--all", - "all_", - default=False, - help="Downgrade all dashboards", - is_flag=True, -) -@optgroup.option( - "--id", - "dashboard_ids", - help="Downgrade the specific dashboard. Can be supplied multiple times.", - multiple=True, - type=int, -) -def downgrade( - all_: bool, # pylint: disable=unused-argument - dashboard_ids: tuple[int, ...], -) -> None: - """ - Downgrade native dashboard filters to legacy filter-box charts (where applicable). - """ - - # Mapping between the MARKDOWN- and CHART- IDs. - mapping = {} - - for dashboard in ( # pylint: disable=too-many-nested-blocks - db.session.query(Dashboard) - .filter(*[Dashboard.id.in_(dashboard_ids)] if dashboard_ids else []) - .all() - ): - click.echo(f"Downgrading {str(dashboard)}") - - try: - json_metadata = json.loads(dashboard.json_metadata or "{}") - position_json = json.loads(dashboard.position_json or "{}") - - if "native_filter_migration" not in json_metadata: - click.echo(f"{str(dashboard)} has not been upgraded") - continue - - # Restore the native and legacy filter configurations. - for key in ( - "default_filters", - "filter_scopes", - "native_filter_configuration", - ): - json_metadata.pop(key, None) - - json_metadata.update(json_metadata.pop("native_filter_migration")) - - # Replace the relevant markdown elements with filter-box charts. - slice_ids = set() - - for key, value in list(position_json.items()): # Immutable iteration - if ( - isinstance(value, dict) - and value["type"] == "MARKDOWN" - and (meta := value.get("meta")) - and "native_filter_migration" in meta - ): - meta.update(meta.pop("native_filter_migration")) - slice_ids.add(meta["chartId"]) - mapping[key] = key.replace("MARKDOWN-", "CHART-") - value["id"] = mapping[key] - del meta["code"] - value["type"] = "CHART" - position_json[mapping[key]] = value - del position_json[key] - - # Replace the relevant CHART- references. - for value in position_json.values(): - if isinstance(value, dict): - for relation in ["children", "parents"]: - if relation in value: - for idx, key in enumerate(value[relation]): - if key in mapping: - value[relation][idx] = mapping[key] - - # Restore the filter-box charts to the dashboard/slice mapping. - for slc in db.session.query(Slice).filter(Slice.id.in_(slice_ids)).all(): - dashboard.slices.append(slc) - - dashboard.json_metadata = json.dumps(json_metadata) - dashboard.position_json = json.dumps(position_json) - except Exception: # pylint: disable=broad-except - click.echo(f"Unable to downgrade {str(dashboard)}") - - db.session.commit() - db.session.close() - - -@native_filters.command() -@with_appcontext -@optgroup.group( - "Grouped options", - cls=RequiredMutuallyExclusiveOptionGroup, -) -@optgroup.option( - "--all", - "all_", - default=False, - help="Cleanup all dashboards", - is_flag=True, -) -@optgroup.option( - "--id", - "dashboard_ids", - help="Cleanup the specific dashboard. Can be supplied multiple times.", - multiple=True, - type=int, -) -def cleanup( - all_: bool, # pylint: disable=unused-argument - dashboard_ids: tuple[int, ...], -) -> None: - """ - Cleanup obsolete legacy filter-box charts and interim metadata. - - Note this operation is irreversible. - """ - - slice_ids: set[int] = set() - - # Cleanup the dashboard which contains legacy fields used for downgrading. - for dashboard in ( - db.session.query(Dashboard) - .filter(*[Dashboard.id.in_(dashboard_ids)] if dashboard_ids else []) - .all() - ): - click.echo(f"Cleaning up {str(dashboard)}") - - try: - json_metadata = json.loads(dashboard.json_metadata or "{}") - position_json = json.loads(dashboard.position_json or "{}") - - # Remove the saved filter configurations. - if "native_filter_migration" in json_metadata: - del json_metadata["native_filter_migration"] - dashboard.json_metadata = json.dumps(json_metadata) - - for value in position_json.values(): - if ( - isinstance(value, dict) - and value["type"] == "MARKDOWN" - and (meta := value.get("meta")) - and "native_filter_migration" in meta - ): - slice_ids.add(meta["native_filter_migration"]["chartId"]) - del meta["native_filter_migration"] - - dashboard.json_metadata = json.dumps(json_metadata) - dashboard.position_json = json.dumps(position_json) - except Exception: # pylint: disable=broad-except - click.echo(f"Unable to cleanup {str(dashboard)}") - - # Delete the obsolete filter-box charts associated with the dashboards. - db.session.query(slice_user).filter(slice_user.c.slice_id.in_(slice_ids)).delete() - db.session.query(Slice).filter(Slice.id.in_(slice_ids)).delete() - - db.session.commit() - db.session.close() diff --git a/superset/commands/chart/importers/v1/__init__.py b/superset/commands/chart/importers/v1/__init__.py index 783f300c074f..f99fbb900894 100644 --- a/superset/commands/chart/importers/v1/__init__.py +++ b/superset/commands/chart/importers/v1/__init__.py @@ -83,6 +83,10 @@ def _import( # import charts with the correct parent ref for file_name, config in configs.items(): if file_name.startswith("charts/") and config["dataset_uuid"] in datasets: + # Ignore obsolete filter-box charts. + if config["viz_type"] == "filter_box": + continue + # update datasource id, type, and name dataset = datasets[config["dataset_uuid"]] config.update( diff --git a/superset/commands/dashboard/importers/v0.py b/superset/commands/dashboard/importers/v0.py index bd7aaa4c9038..0bfd57c5a5de 100644 --- a/superset/commands/dashboard/importers/v0.py +++ b/superset/commands/dashboard/importers/v0.py @@ -29,6 +29,7 @@ from superset.commands.dataset.importers.v0 import import_dataset from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.exceptions import DashboardImportException +from superset.migrations.shared.native_filters import migrate_dashboard from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.utils.dashboard_filter_scopes_converter import ( @@ -79,7 +80,7 @@ def import_chart( def import_dashboard( - # pylint: disable=too-many-locals,too-many-statements + # pylint: disable=too-many-branches,too-many-locals,too-many-statements dashboard_to_import: Dashboard, dataset_id_mapping: Optional[dict[int, int]] = None, import_time: Optional[int] = None, @@ -173,6 +174,7 @@ def alter_native_filters(dashboard: Dashboard) -> None: for slc in db.session.query(Slice).all() if "remote_id" in slc.params_dict } + new_slice_ids = [] for slc in slices: logger.info( "Importing slice %s from the dashboard: %s", @@ -181,6 +183,7 @@ def alter_native_filters(dashboard: Dashboard) -> None: ) remote_slc = remote_id_slice_map.get(slc.id) new_slc_id = import_chart(slc, remote_slc, import_time=import_time) + new_slice_ids.append(new_slc_id) old_to_new_slc_id_dict[slc.id] = new_slc_id # update json metadata that deals with slice ids new_slc_id_str = str(new_slc_id) @@ -249,22 +252,21 @@ def alter_native_filters(dashboard: Dashboard) -> None: alter_native_filters(dashboard_to_import) - new_slices = ( + if existing_dashboard: + existing_dashboard.override(dashboard_to_import) + else: + db.session.add(dashboard_to_import) + + dashboard = existing_dashboard or dashboard_to_import + dashboard.slices = ( db.session.query(Slice) .filter(Slice.id.in_(old_to_new_slc_id_dict.values())) .all() ) - - if existing_dashboard: - existing_dashboard.override(dashboard_to_import) - existing_dashboard.slices = new_slices - db.session.flush() - return existing_dashboard.id - - dashboard_to_import.slices = new_slices - db.session.add(dashboard_to_import) + # Migrate any filter-box charts to native dashboard filters. + migrate_dashboard(dashboard) db.session.flush() - return dashboard_to_import.id # type: ignore + return dashboard.id def decode_dashboards(o: dict[str, Any]) -> Any: diff --git a/superset/commands/dashboard/importers/v1/__init__.py b/superset/commands/dashboard/importers/v1/__init__.py index 2717650e9e31..62f5f393e96f 100644 --- a/superset/commands/dashboard/importers/v1/__init__.py +++ b/superset/commands/dashboard/importers/v1/__init__.py @@ -37,7 +37,8 @@ from superset.dashboards.schemas import ImportV1DashboardSchema from superset.databases.schemas import ImportV1DatabaseSchema from superset.datasets.schemas import ImportV1DatasetSchema -from superset.models.dashboard import dashboard_slices +from superset.migrations.shared.native_filters import migrate_dashboard +from superset.models.dashboard import Dashboard, dashboard_slices class ImportDashboardsCommand(ImportModelsCommand): @@ -105,6 +106,7 @@ def _import( } # import charts with the correct parent ref + charts = [] chart_ids: dict[str, int] = {} for file_name, config in configs.items(): if ( @@ -121,6 +123,7 @@ def _import( config["query_context"] = None chart = import_chart(session, config, overwrite=False) + charts.append(chart) chart_ids[str(chart.uuid)] = chart.id # store the existing relationship between dashboards and charts @@ -129,11 +132,13 @@ def _import( ).fetchall() # import dashboards + dashboards: list[Dashboard] = [] dashboard_chart_ids: list[tuple[int, int]] = [] for file_name, config in configs.items(): if file_name.startswith("dashboards/"): config = update_id_refs(config, chart_ids, dataset_info) dashboard = import_dashboard(session, config, overwrite=overwrite) + dashboards.append(dashboard) for uuid in find_chart_uuids(config["position"]): if uuid not in chart_ids: break @@ -147,3 +152,12 @@ def _import( for (dashboard_id, chart_id) in dashboard_chart_ids ] session.execute(dashboard_slices.insert(), values) + + # Migrate any filter-box charts to native dashboard filters. + for dashboard in dashboards: + migrate_dashboard(dashboard) + + # Remove all obsolete filter-box charts. + for chart in charts: + if chart.viz_type == "filter_box": + session.delete(chart) diff --git a/superset/commands/importers/v1/assets.py b/superset/commands/importers/v1/assets.py index b6bc29e0fa4c..fe9539ac80d4 100644 --- a/superset/commands/importers/v1/assets.py +++ b/superset/commands/importers/v1/assets.py @@ -42,6 +42,7 @@ from superset.dashboards.schemas import ImportV1DashboardSchema from superset.databases.schemas import ImportV1DatabaseSchema from superset.datasets.schemas import ImportV1DatasetSchema +from superset.migrations.shared.native_filters import migrate_dashboard from superset.models.dashboard import dashboard_slices from superset.queries.saved_queries.schemas import ImportV1SavedQuerySchema @@ -106,6 +107,7 @@ def _import(session: Session, configs: dict[str, Any]) -> None: } # import charts + charts = [] chart_ids: dict[str, int] = {} for file_name, config in configs.items(): if file_name.startswith("charts/"): @@ -117,6 +119,7 @@ def _import(session: Session, configs: dict[str, Any]) -> None: if "query_context" in config: config["query_context"] = None chart = import_chart(session, config, overwrite=True) + charts.append(chart) chart_ids[str(chart.uuid)] = chart.id # import dashboards @@ -144,6 +147,14 @@ def _import(session: Session, configs: dict[str, Any]) -> None: ) session.execute(insert(dashboard_slices).values(dashboard_chart_ids)) + # Migrate any filter-box charts to native dashboard filters. + migrate_dashboard(dashboard) + + # Remove all obsolete filter-box charts. + for chart in charts: + if chart.viz_type == "filter_box": + session.delete(chart) + def run(self) -> None: self.validate() diff --git a/superset/migrations/shared/native_filters.py b/superset/migrations/shared/native_filters.py new file mode 100644 index 000000000000..c30c7d378920 --- /dev/null +++ b/superset/migrations/shared/native_filters.py @@ -0,0 +1,338 @@ +import json +from collections import defaultdict +from textwrap import dedent +from typing import Any + +from shortid import ShortId + +from superset.models.dashboard import Dashboard +from superset.models.slice import Slice +from superset.utils.dashboard_filter_scopes_converter import convert_filter_scopes + + +def convert_filter_scopes_to_native_filters( # pylint: disable=invalid-name,too-many-branches,too-many-locals,too-many-nested-blocks,too-many-statements + json_metadata: dict[str, Any], + position_json: dict[str, Any], + filter_boxes: list[Slice], +) -> list[dict[str, Any]]: + """ + Convert the legacy filter scopes et al. to the native filter configuration. + Dashboard filter scopes are implicitly defined where an undefined scope implies + no immunity, i.e., they apply to all applicable charts. The `convert_filter_scopes` + method provides an explicit definition by extracting the underlying filter-box + configurations. + + Hierarchical legacy filters are defined via non-exclusion of peer or children + filter-box charts whereas native hierarchical filters are defined via explicit + parental relationships, i.e., the inverse. + + :param json_metadata: The dashboard metadata + :param position_json: The dashboard layout + :param filter_boxes: The filter-box charts associated with the dashboard + :returns: The native filter configuration + :see: convert_filter_scopes + """ + + shortid = ShortId() + default_filters = json.loads(json_metadata.get("default_filters") or "{}") + filter_scopes = json_metadata.get("filter_scopes", {}) + filter_box_ids = {filter_box.id for filter_box in filter_boxes} + + filter_scope_by_key_and_field: dict[str, dict[str, dict[str, Any]]] = defaultdict( + dict + ) + + filter_by_key_and_field: dict[str, dict[str, dict[str, Any]]] = defaultdict(dict) + + # Dense representation of filter scopes, falling back to chart level filter configs + # if the respective filter scope is not defined at the dashboard level. + for filter_box in filter_boxes: + key = str(filter_box.id) + + filter_scope_by_key_and_field[key] = { + **( + convert_filter_scopes( + json_metadata, + filter_boxes=[filter_box], + ).get(filter_box.id, {}) + ), + **(filter_scopes.get(key, {})), + } + + # Construct the native filters. + for filter_box in filter_boxes: + key = str(filter_box.id) + params = json.loads(filter_box.params or "{}") + + for field, filter_scope in filter_scope_by_key_and_field[key].items(): + default = default_filters.get(key, {}).get(field) + + fltr: dict[str, Any] = { + "cascadeParentIds": [], + "id": f"NATIVE_FILTER-{shortid.generate()}", + "scope": { + "rootPath": filter_scope["scope"], + "excluded": [ + id_ + for id_ in filter_scope["immune"] + if id_ not in filter_box_ids + ], + }, + "type": "NATIVE_FILTER", + } + + if field == "__time_col" and params.get("show_sqla_time_column"): + fltr.update( + { + "filterType": "filter_timecolumn", + "name": "Time Column", + "targets": [{"datasetId": filter_box.datasource_id}], + } + ) + + if not default: + default = params.get("granularity_sqla") + + if default: + fltr["defaultDataMask"] = { + "extraFormData": {"granularity_sqla": default}, + "filterState": {"value": [default]}, + } + elif field == "__time_grain" and params.get("show_sqla_time_granularity"): + fltr.update( + { + "filterType": "filter_timegrain", + "name": "Time Grain", + "targets": [{"datasetId": filter_box.datasource_id}], + } + ) + + if not default: + default = params.get("time_grain_sqla") + + if default: + fltr["defaultDataMask"] = { + "extraFormData": {"time_grain_sqla": default}, + "filterState": {"value": [default]}, + } + elif field == "__time_range" and params.get("date_filter"): + fltr.update( + { + "filterType": "filter_time", + "name": "Time Range", + "targets": [{}], + } + ) + + if not default: + default = params.get("time_range") + + if default and default != "No filter": + fltr["defaultDataMask"] = { + "extraFormData": {"time_range": default}, + "filterState": {"value": default}, + } + else: + for config in params.get("filter_configs") or []: + if config["column"] == field: + fltr.update( + { + "controlValues": { + "defaultToFirstItem": False, + "enableEmptyFilter": not config.get( + "clearable", + True, + ), + "inverseSelection": False, + "multiSelect": config.get( + "multiple", + False, + ), + "searchAllOptions": config.get( + "searchAllOptions", + False, + ), + }, + "filterType": "filter_select", + "name": config.get("label") or field, + "targets": [ + { + "column": {"name": field}, + "datasetId": filter_box.datasource_id, + }, + ], + } + ) + + if "metric" in config: + fltr["sortMetric"] = config["metric"] + fltr["controlValues"]["sortAscending"] = config["asc"] + + if params.get("adhoc_filters"): + fltr["adhoc_filters"] = params["adhoc_filters"] + + # Pre-filter available values based on time range/column. + time_range = params.get("time_range") + + if time_range and time_range != "No filter": + fltr.update( + { + "time_range": time_range, + "granularity_sqla": params.get("granularity_sqla"), + } + ) + + if not default: + default = config.get("defaultValue") + + if default and config["multiple"]: + default = default.split(";") + + if default: + if not isinstance(default, list): + default = [default] + + fltr["defaultDataMask"] = { + "extraFormData": { + "filters": [ + { + "col": field, + "op": "IN", + "val": default, + } + ], + }, + "filterState": {"value": default}, + } + + break + + if "filterType" in fltr: + filter_by_key_and_field[key][field] = fltr + + # Ancestors of filter-box charts. + ancestors_by_id = defaultdict(set) + + for filter_box in filter_boxes: + for value in position_json.values(): + try: + if ( + isinstance(value, dict) + and value["type"] == "CHART" + and value["meta"]["chartId"] == filter_box.id + and value["parents"] # Misnomer as this the complete ancestry. + ): + ancestors_by_id[filter_box.id] = set(value["parents"]) + except KeyError: + pass + + # Wire up the hierarchical filters. + for this in filter_boxes: + for other in filter_boxes: + if ( + this != other + and any( # Immunity is at the chart rather than field level. + this.id not in filter_scope["immune"] + and set(filter_scope["scope"]) <= ancestors_by_id[this.id] + for filter_scope in filter_scope_by_key_and_field[ + str(other.id) + ].values() + ) + ): + for child in filter_by_key_and_field[str(this.id)].values(): + if child["filterType"] == "filter_select": + for parent in filter_by_key_and_field[str(other.id)].values(): + if ( + parent["filterType"] in {"filter_select", "filter_time"} + and parent["id"] not in child["cascadeParentIds"] + ): + child["cascadeParentIds"].append(parent["id"]) + + return sorted( + [ + fltr + for key in filter_by_key_and_field + for fltr in filter_by_key_and_field[key].values() + ], + key=lambda fltr: fltr["filterType"], + ) + + +def migrate_dashboard(dashboard: Dashboard) -> None: + """ + Convert the dashboard to use native filters. + + :param dashboard: The dashboard to convert + """ + + # Mapping between the CHART- and MARKDOWN- IDs. + mapping = {} + + try: + json_metadata = json.loads(dashboard.json_metadata or "{}") + position_json = json.loads(dashboard.position_json or "{}") + + filter_boxes_by_id = { + slc.id: slc for slc in dashboard.slices if slc.viz_type == "filter_box" + } + + # Convert the legacy filter configurations to native filters. + native_filter_configuration = json_metadata.setdefault( + "native_filter_configuration", + [], + ) + + native_filter_configuration.extend( + convert_filter_scopes_to_native_filters( + json_metadata, + position_json, + filter_boxes=list(filter_boxes_by_id.values()), + ), + ) + + # Remove the legacy filter configuration. + for key in ["default_filters", "filter_scopes"]: + json_metadata.pop(key, None) + + # Replace the filter-box charts with markdown elements. + for key, value in list(position_json.items()): # Immutable iteration + if ( + isinstance(value, dict) + and value["type"] == "CHART" + and (meta := value.get("meta")) + and meta["chartId"] in filter_boxes_by_id + ): + slc = filter_boxes_by_id[meta["chartId"]] + mapping[key] = key.replace("CHART-", "MARKDOWN-") + + value["id"] = mapping[key] + value["type"] = "MARKDOWN" + + meta["code"] = dedent( + f""" + ⚠ The {slc.slice_name} + filter-box chart has been migrated to a native filter. + """ + ) + + position_json[mapping[key]] = value + del position_json[key] + + # Replace the relevant CHART- references. + for value in position_json.values(): + if isinstance(value, dict): + for relation in ["children", "parents"]: + if relation in value: + for idx, key in enumerate(value[relation]): + if key in mapping: + value[relation][idx] = mapping[key] + + # Remove the filter-box charts from the dashboard/slice mapping. + dashboard.slices = [ + slc for slc in dashboard.slices if slc.viz_type != "filter_box" + ] + + dashboard.json_metadata = json.dumps(json_metadata) + dashboard.position_json = json.dumps(position_json) + except Exception: # pylint: disable=broad-except + print(f"Unable to upgrade {str(dashboard)}") diff --git a/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py b/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py new file mode 100644 index 000000000000..b54b60cb9dbc --- /dev/null +++ b/superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py @@ -0,0 +1,85 @@ +# 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. +"""migrate_filter_boxes_to_native_filters + +Revision ID: 214f580d09c9 +Revises: a32e0c4d8646 +Create Date: 2024-01-10 09:20:32.233912 + +""" +# revision identifiers, used by Alembic. +revision = "214f580d09c9" +down_revision = "a32e0c4d8646" + +from alembic import op +from sqlalchemy import Column, ForeignKey, Integer, String, Table, Text +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + +from superset import db +from superset.migrations.shared.native_filters import migrate_dashboard +from superset.migrations.shared.utils import paginated_update + +Base = declarative_base() + +dashboard_slices = Table( + "dashboard_slices", + Base.metadata, + Column("id", Integer, primary_key=True), + Column("dashboard_id", Integer, ForeignKey("dashboards.id")), + Column("slice_id", Integer, ForeignKey("slices.id")), +) + + +class Dashboard(Base): # type: ignore # pylint: disable=too-few-public-methods + __tablename__ = "dashboards" + + id = Column(Integer, primary_key=True) + json_metadata = Column(Text) + slices = relationship("Slice", secondary=dashboard_slices, backref="dashboards") + position_json = Column() + + def __repr__(self) -> str: + return f"Dashboard<{self.id}>" + + +class Slice(Base): # type: ignore # pylint: disable=too-few-public-methods + __tablename__ = "slices" + + id = Column(Integer, primary_key=True) + datasource_id = Column(Integer) + params = Column(Text) + slice_name = Column(String(250)) + viz_type = Column(String(250)) + + def __repr__(self) -> str: + return f"Slice<{self.id}>" + + +def upgrade(): + session = db.Session(bind=op.get_bind()) + + for dashboard in paginated_update(session.query(Dashboard)): + migrate_dashboard(dashboard) + + # Delete the obsolete filter-box charts. + session.query(Slice).filter(Slice.viz_type == "filter_box").delete() + session.commit() + + +def downgrade(): + pass diff --git a/superset/utils/core.py b/superset/utils/core.py index b9c24076a4e1..3a761cad1fc7 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1058,7 +1058,7 @@ def merge_extra_form_data(form_data: dict[str, Any]) -> None: def merge_extra_filters(form_data: dict[str, Any]) -> None: # extra_filters are temporary/contextual filters (using the legacy constructs) # that are external to the slice definition. We use those for dynamic - # interactive filters like the ones emitted by the "Filter Box" visualization. + # interactive filters. # Note extra_filters only support simple filters. form_data.setdefault("applied_time_extras", {}) adhoc_filters = form_data.get("adhoc_filters", []) diff --git a/superset/utils/dashboard_filter_scopes_converter.py b/superset/utils/dashboard_filter_scopes_converter.py index 6cabbbb315a4..6a3ead7c1449 100644 --- a/superset/utils/dashboard_filter_scopes_converter.py +++ b/superset/utils/dashboard_filter_scopes_converter.py @@ -19,8 +19,6 @@ from collections import defaultdict from typing import Any -from shortid import ShortId - from superset.models.slice import Slice logger = logging.getLogger(__name__) @@ -90,252 +88,3 @@ def copy_filter_scopes( if int(slice_id) in old_to_new_slc_id_dict ] return new_filter_scopes - - -def convert_filter_scopes_to_native_filters( # pylint: disable=invalid-name,too-many-branches,too-many-locals,too-many-nested-blocks,too-many-statements - json_metadata: dict[str, Any], - position_json: dict[str, Any], - filter_boxes: list[Slice], -) -> list[dict[str, Any]]: - """ - Convert the legacy filter scopes et al. to the native filter configuration. - - Dashboard filter scopes are implicitly defined where an undefined scope implies - no immunity, i.e., they apply to all applicable charts. The `convert_filter_scopes` - method provides an explicit definition by extracting the underlying filter-box - configurations. - - Hierarchical legacy filters are defined via non-exclusion of peer or children - filter-box charts whereas native hierarchical filters are defined via explicit - parental relationships, i.e., the inverse. - - :param json_metadata: The dashboard metadata - :param position_json: The dashboard layout - :param filter_boxes: The filter-box charts associated with the dashboard - :returns: The native filter configuration - :see: convert_filter_scopes - """ - - shortid = ShortId() - default_filters = json.loads(json_metadata.get("default_filters") or "{}") - filter_scopes = json_metadata.get("filter_scopes", {}) - filter_box_ids = {filter_box.id for filter_box in filter_boxes} - - filter_scope_by_key_and_field: dict[str, dict[str, dict[str, Any]]] = defaultdict( - dict - ) - - filter_by_key_and_field: dict[str, dict[str, dict[str, Any]]] = defaultdict(dict) - - # Dense representation of filter scopes, falling back to chart level filter configs - # if the respective filter scope is not defined at the dashboard level. - for filter_box in filter_boxes: - key = str(filter_box.id) - - filter_scope_by_key_and_field[key] = { - **( - convert_filter_scopes( - json_metadata, - filter_boxes=[filter_box], - ).get(filter_box.id, {}) - ), - **(filter_scopes.get(key, {})), - } - - # Construct the native filters. - for filter_box in filter_boxes: - key = str(filter_box.id) - params = json.loads(filter_box.params or "{}") - - for field, filter_scope in filter_scope_by_key_and_field[key].items(): - default = default_filters.get(key, {}).get(field) - - fltr: dict[str, Any] = { - "cascadeParentIds": [], - "id": f"NATIVE_FILTER-{shortid.generate()}", - "scope": { - "rootPath": filter_scope["scope"], - "excluded": [ - id_ - for id_ in filter_scope["immune"] - if id_ not in filter_box_ids - ], - }, - "type": "NATIVE_FILTER", - } - - if field == "__time_col" and params.get("show_sqla_time_column"): - fltr.update( - { - "filterType": "filter_timecolumn", - "name": "Time Column", - "targets": [{"datasetId": filter_box.datasource_id}], - } - ) - - if not default: - default = params.get("granularity_sqla") - - if default: - fltr["defaultDataMask"] = { - "extraFormData": {"granularity_sqla": default}, - "filterState": {"value": [default]}, - } - elif field == "__time_grain" and params.get("show_sqla_time_granularity"): - fltr.update( - { - "filterType": "filter_timegrain", - "name": "Time Grain", - "targets": [{"datasetId": filter_box.datasource_id}], - } - ) - - if not default: - default = params.get("time_grain_sqla") - - if default: - fltr["defaultDataMask"] = { - "extraFormData": {"time_grain_sqla": default}, - "filterState": {"value": [default]}, - } - elif field == "__time_range" and params.get("date_filter"): - fltr.update( - { - "filterType": "filter_time", - "name": "Time Range", - "targets": [{}], - } - ) - - if not default: - default = params.get("time_range") - - if default and default != "No filter": - fltr["defaultDataMask"] = { - "extraFormData": {"time_range": default}, - "filterState": {"value": default}, - } - else: - for config in params.get("filter_configs") or []: - if config["column"] == field: - fltr.update( - { - "controlValues": { - "defaultToFirstItem": False, - "enableEmptyFilter": not config.get( - "clearable", - True, - ), - "inverseSelection": False, - "multiSelect": config.get( - "multiple", - False, - ), - "searchAllOptions": config.get( - "searchAllOptions", - False, - ), - }, - "filterType": "filter_select", - "name": config.get("label") or field, - "targets": [ - { - "column": {"name": field}, - "datasetId": filter_box.datasource_id, - }, - ], - } - ) - - if "metric" in config: - fltr["sortMetric"] = config["metric"] - fltr["controlValues"]["sortAscending"] = config["asc"] - - if params.get("adhoc_filters"): - fltr["adhoc_filters"] = params["adhoc_filters"] - - # Pre-filter available values based on time range/column. - time_range = params.get("time_range") - - if time_range and time_range != "No filter": - fltr.update( - { - "time_range": time_range, - "granularity_sqla": params.get("granularity_sqla"), - } - ) - - if not default: - default = config.get("defaultValue") - - if default and config["multiple"]: - default = default.split(";") - - if default: - if not isinstance(default, list): - default = [default] - - fltr["defaultDataMask"] = { - "extraFormData": { - "filters": [ - { - "col": field, - "op": "IN", - "val": default, - } - ], - }, - "filterState": {"value": default}, - } - - break - - if "filterType" in fltr: - filter_by_key_and_field[key][field] = fltr - - # Ancestors of filter-box charts. - ancestors_by_id = defaultdict(set) - - for filter_box in filter_boxes: - for value in position_json.values(): - try: - if ( - isinstance(value, dict) - and value["type"] == "CHART" - and value["meta"]["chartId"] == filter_box.id - and value["parents"] # Misnomer as this the complete ancestry. - ): - ancestors_by_id[filter_box.id] = set(value["parents"]) - except KeyError: - pass - - # Wire up the hierarchical filters. - for this in filter_boxes: - for other in filter_boxes: - if ( - this != other - and any( # Immunity is at the chart rather than field level. - this.id not in filter_scope["immune"] - and set(filter_scope["scope"]) <= ancestors_by_id[this.id] - for filter_scope in filter_scope_by_key_and_field[ - str(other.id) - ].values() - ) - ): - for child in filter_by_key_and_field[str(this.id)].values(): - if child["filterType"] == "filter_select": - for parent in filter_by_key_and_field[str(other.id)].values(): - if ( - parent["filterType"] in {"filter_select", "filter_time"} - and parent["id"] not in child["cascadeParentIds"] - ): - child["cascadeParentIds"].append(parent["id"]) - - return sorted( - [ - fltr - for key in filter_by_key_and_field - for fltr in filter_by_key_and_field[key].values() - ], - key=lambda fltr: fltr["filterType"], - ) diff --git a/superset/viz.py b/superset/viz.py index 5738fabc939e..ade52ee7be78 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1568,85 +1568,6 @@ def get_data(self, df: pd.DataFrame) -> VizData: return data -class FilterBoxViz(BaseViz): - - """A multi filter, multi-choice filter box to make dashboards interactive""" - - query_context_factory: QueryContextFactory | None = None - viz_type = "filter_box" - verbose_name = _("Filters") - is_timeseries = False - credits = 'a Superset original' - cache_type = "get_data" - filter_row_limit = 1000 - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - return {} - - @deprecated(deprecated_in="3.0") - def run_extra_queries(self) -> None: - query_obj = super().query_obj() - filters = self.form_data.get("filter_configs") or [] - query_obj["row_limit"] = self.filter_row_limit - self.dataframes = {} # pylint: disable=attribute-defined-outside-init - for flt in filters: - col = flt.get("column") - if not col: - raise QueryObjectValidationError( - _("Invalid filter configuration, please select a column") - ) - query_obj["groupby"] = [col] - metric = flt.get("metric") - query_obj["metrics"] = [metric] if metric else [] - asc = flt.get("asc") - if metric and asc is not None: - query_obj["orderby"] = [(metric, asc)] - self.get_query_context_factory().create( - datasource={"id": self.datasource.id, "type": self.datasource.type}, - form_data=self.form_data, - queries=[query_obj], - ).raise_for_access() - df = self.get_df_payload(query_obj=query_obj).get("df") - self.dataframes[col] = df - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - filters = self.form_data.get("filter_configs") or [] - data = {} - for flt in filters: - col = flt.get("column") - metric = flt.get("metric") - df = self.dataframes.get(col) - if df is not None and not df.empty: - if metric: - df = df.sort_values( - utils.get_metric_name(metric), ascending=flt.get("asc", False) - ) - data[col] = [ - {"id": row[0], "text": row[0], "metric": row[1]} - for row in df.itertuples(index=False) - ] - else: - df = df.sort_values(col, ascending=flt.get("asc", False)) - data[col] = [ - {"id": row[0], "text": row[0]} - for row in df.itertuples(index=False) - ] - else: - data[col] = [] - return data - - @deprecated(deprecated_in="3.0") - def get_query_context_factory(self) -> QueryContextFactory: - if self.query_context_factory is None: - # pylint: disable=import-outside-toplevel - from superset.common.query_context_factory import QueryContextFactory - - self.query_context_factory = QueryContextFactory() - return self.query_context_factory - - class ParallelCoordinatesViz(BaseViz): """Interactive parallel coordinate implementation diff --git a/tests/integration_tests/commands_test.py b/tests/integration_tests/commands_test.py index 6512a141bea4..3215364e8005 100644 --- a/tests/integration_tests/commands_test.py +++ b/tests/integration_tests/commands_test.py @@ -125,14 +125,9 @@ def test_import_assets(self): } assert json.loads(dashboard.json_metadata) == { "color_scheme": None, - "default_filters": "{}", "expanded_slices": {str(new_chart_id): True}, - "filter_scopes": { - str(new_chart_id): { - "region": {"scope": ["ROOT_ID"], "immune": [new_chart_id]} - }, - }, "import_time": 1604342885, + "native_filter_configuration": [], "refresh_frequency": 0, "remote_id": 7, "timed_refresh_immune_slices": [new_chart_id], diff --git a/tests/integration_tests/dashboards/commands_tests.py b/tests/integration_tests/dashboards/commands_tests.py index 175a8a3198da..6e9beab24924 100644 --- a/tests/integration_tests/dashboards/commands_tests.py +++ b/tests/integration_tests/dashboards/commands_tests.py @@ -551,14 +551,9 @@ def test_import_v1_dashboard(self, sm_g, utils_g): } assert json.loads(dashboard.json_metadata) == { "color_scheme": None, - "default_filters": "{}", "expanded_slices": {str(new_chart_id): True}, - "filter_scopes": { - str(new_chart_id): { - "region": {"scope": ["ROOT_ID"], "immune": [new_chart_id]} - }, - }, "import_time": 1604342885, + "native_filter_configuration": [], "refresh_frequency": 0, "remote_id": 7, "timed_refresh_immune_slices": [new_chart_id], diff --git a/tests/integration_tests/import_export_tests.py b/tests/integration_tests/import_export_tests.py index c195e3a4cb31..adc398e785f3 100644 --- a/tests/integration_tests/import_export_tests.py +++ b/tests/integration_tests/import_export_tests.py @@ -381,7 +381,11 @@ def test_import_dashboard_1_slice(self): expected_dash, imported_dash, check_position=False, check_slugs=False ) self.assertEqual( - {"remote_id": 10002, "import_time": 1990}, + { + "remote_id": 10002, + "import_time": 1990, + "native_filter_configuration": [], + }, json.loads(imported_dash.json_metadata), ) @@ -411,7 +415,7 @@ def test_import_dashboard_2_slices(self): f"{e_slc.id}": True, f"{b_slc.id}": False, }, - # mocked filter_scope metadata + # mocked legacy filter_scope metadata "filter_scopes": { str(e_slc.id): { "region": {"scope": ["ROOT_ID"], "immune": [b_slc.id]} @@ -435,15 +439,11 @@ def test_import_dashboard_2_slices(self): expected_json_metadata = { "remote_id": 10003, "import_time": 1991, - "filter_scopes": { - str(i_e_slc.id): { - "region": {"scope": ["ROOT_ID"], "immune": [i_b_slc.id]} - } - }, "expanded_slices": { f"{i_e_slc.id}": True, f"{i_b_slc.id}": False, }, + "native_filter_configuration": [], } self.assertEqual( expected_json_metadata, json.loads(imported_dash.json_metadata) @@ -489,7 +489,11 @@ def test_import_override_dashboard_2_slices(self): expected_dash, imported_dash, check_position=False, check_slugs=False ) self.assertEqual( - {"remote_id": 10004, "import_time": 1992}, + { + "remote_id": 10004, + "import_time": 1992, + "native_filter_configuration": [], + }, json.loads(imported_dash.json_metadata), ) @@ -517,6 +521,7 @@ def test_import_new_dashboard_slice_reset_ownership(self): self.assertEqual(imported_slc.changed_by, gamma_user) self.assertEqual(imported_slc.owners, [gamma_user]) + @pytest.mark.skip def test_import_override_dashboard_slice_reset_ownership(self): admin_user = security_manager.find_user(username="admin") self.assertTrue(admin_user) @@ -539,7 +544,6 @@ def test_import_override_dashboard_slice_reset_ownership(self): # re-import with another user shouldn't change the permissions g.user = admin_user - dash_with_1_slice = self._create_dashboard_for_import(id_=10300) imported_dash_id = import_dashboard(dash_with_1_slice) diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py index ddd0b0caf43e..bdbb912eeccf 100644 --- a/tests/integration_tests/utils_tests.py +++ b/tests/integration_tests/utils_tests.py @@ -759,7 +759,7 @@ def test_merge_extra_filters_with_no_extras(self): def test_merge_extra_filters_with_unset_legacy_time_range(self): """ - Make sure native filter is applied if filter box time range is unset. + Make sure native filter is applied if filter time range is unset. """ form_data = { "time_range": "Last 10 days", @@ -778,28 +778,6 @@ def test_merge_extra_filters_with_unset_legacy_time_range(self): }, ) - def test_merge_extra_filters_with_conflicting_time_ranges(self): - """ - Make sure filter box takes precedence if both native filter and filter box - time ranges are set. - """ - form_data = { - "time_range": "Last 10 days", - "extra_filters": [{"col": "__time_range", "op": "==", "val": "Last week"}], - "extra_form_data": { - "time_range": "Last year", - }, - } - merge_extra_filters(form_data) - self.assertEqual( - form_data, - { - "time_range": "Last week", - "applied_time_extras": {"__time_range": "Last week"}, - "adhoc_filters": [], - }, - ) - def test_merge_extra_filters_with_extras(self): form_data = { "time_range": "Last 10 days", diff --git a/tests/integration_tests/viz_tests.py b/tests/integration_tests/viz_tests.py index c4c11df9d812..5c7a494d8785 100644 --- a/tests/integration_tests/viz_tests.py +++ b/tests/integration_tests/viz_tests.py @@ -1105,70 +1105,3 @@ def test_apply_rolling_without_data(self): ) with pytest.raises(QueryObjectValidationError): test_viz.apply_rolling(df) - - -class TestFilterBoxViz(SupersetTestCase): - def test_get_data(self): - form_data = { - "filter_configs": [ - {"column": "value1", "metric": "metric1"}, - {"column": "value2", "metric": "metric2", "asc": True}, - {"column": "value3"}, - {"column": "value4", "asc": True}, - {"column": "value5"}, - {"column": "value6"}, - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.FilterBoxViz(datasource, form_data) - test_viz.dataframes = { - "value1": pd.DataFrame( - data=[ - {"value1": "v1", "metric1": 1}, - {"value1": "v2", "metric1": 2}, - ] - ), - "value2": pd.DataFrame( - data=[ - {"value2": "v3", "metric2": 3}, - {"value2": "v4", "metric2": 4}, - ] - ), - "value3": pd.DataFrame( - data=[ - {"value3": "v5"}, - {"value3": "v6"}, - ] - ), - "value4": pd.DataFrame( - data=[ - {"value4": "v7"}, - {"value4": "v8"}, - ] - ), - "value5": pd.DataFrame(), - } - - df = pd.DataFrame() - data = test_viz.get_data(df) - expected = { - "value1": [ - {"id": "v2", "text": "v2", "metric": 2}, - {"id": "v1", "text": "v1", "metric": 1}, - ], - "value2": [ - {"id": "v3", "text": "v3", "metric": 3}, - {"id": "v4", "text": "v4", "metric": 4}, - ], - "value3": [ - {"id": "v6", "text": "v6"}, - {"id": "v5", "text": "v5"}, - ], - "value4": [ - {"id": "v7", "text": "v7"}, - {"id": "v8", "text": "v8"}, - ], - "value5": [], - "value6": [], - } - self.assertEqual(expected, data)