From d9a3c3e1dd23eca113a799bb6a2fa4abddd4a76b Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:54:53 -0300 Subject: [PATCH 01/11] refactor: Removes the Filter Box code (#26328) Co-authored-by: John Bodley --- UPDATING.md | 1 + .../miscellaneous/native-filter-migration.mdx | 94 ---- .../e2e/dashboard/_skip.filter.test.ts | 82 --- .../cypress/e2e/dashboard/tabs.test.ts | 4 +- .../cypress/e2e/explore/filter_box.test.js | 38 -- .../cypress/e2e/explore/link.test.ts | 4 +- .../cypress/support/directories.ts | 2 +- superset-frontend/package-lock.json | 54 +- superset-frontend/package.json | 4 +- .../superset-ui-chart-controls/src/types.ts | 1 - .../src/chart/models/ChartProps.ts | 4 - .../test/query/extractTimegrain.test.ts | 2 +- .../AceEditorWrapper.test.tsx | 3 - .../EstimateQueryCostButton.test.tsx | 3 - .../QueryLimitSelect.test.tsx | 3 - .../RunQueryActionButton.test.tsx | 3 - .../SqlEditorTabHeader.test.tsx | 3 - .../TemplateParamsEditor.test.tsx | 3 - .../AsyncSelect/AsyncSelect.test.jsx | 150 ------ .../src/components/AsyncSelect/index.jsx | 104 ---- .../DeprecatedSelect.stories.tsx | 143 ------ .../DeprecatedSelect/DeprecatedSelect.tsx | 324 ------------ .../DeprecatedSelect/NativeSelect.tsx | 61 --- .../DeprecatedSelect/OnPasteSelect.jsx | 104 ---- .../DeprecatedSelect/OnPasteSelect.test.jsx | 216 -------- .../WindowedSelect/WindowedMenuList.tsx | 158 ------ .../DeprecatedSelect/WindowedSelect/index.tsx | 29 -- .../WindowedSelect/windowed.tsx | 84 --- .../src/components/DeprecatedSelect/index.ts | 23 - .../components/DeprecatedSelect/styles.tsx | 406 --------------- .../src/components/DeprecatedSelect/utils.ts | 57 --- .../src/components/ListView/utils.ts | 19 - .../src/components/Select/AsyncSelect.tsx | 2 - .../src/components/Select/Select.tsx | 2 - .../src/dashboard/actions/dashboardFilters.js | 20 - .../src/dashboard/actions/dashboardState.js | 19 +- .../dashboard/actions/dashboardState.test.js | 12 - .../src/dashboard/actions/hydrate.js | 69 --- .../src/dashboard/actions/sliceEntities.ts | 16 +- .../DashboardBuilder.test.tsx | 6 - .../components/FilterBoxMigrationModal.tsx | 95 ---- .../SliceHeaderControls.test.tsx | 6 - .../components/SliceHeaderControls/index.tsx | 5 +- .../filterscope/FilterScopeSelector.jsx | 11 +- .../components/gridComponents/Chart.jsx | 19 +- .../FilterScope/utils.test.ts | 57 --- .../FiltersConfigForm/FilterScope/utils.ts | 7 +- .../components/nativeFilters/selectors.ts | 4 +- .../src/dashboard/containers/Dashboard.ts | 3 +- .../dashboard/reducers/dashboardFilters.js | 44 +- .../reducers/dashboardFilters.test.js | 45 +- .../dashboard/util/activeDashboardFilters.js | 22 +- .../dashboard/util/getFilterScopeNodesTree.js | 7 +- .../util/getFilterValuesByFilterId.js | 38 -- .../dashboard/util/getRevertedFilterScope.ts | 19 +- .../getSelectedChartIdForFilterScopeTree.js | 2 - .../util/logging/childChartsDidLoad.js | 9 +- .../useFilterFocusHighlightStyles.test.tsx | 63 --- .../util/useFilterFocusHighlightStyles.ts | 4 +- .../components/ExploreChartPanel/index.jsx | 46 +- .../src/explore/components/SaveModal.tsx | 55 +- .../FilterBoxItemControl.test.jsx | 108 ---- .../FilterBoxItemControl.test.tsx | 61 --- .../controls/FilterBoxItemControl/index.jsx | 295 ----------- .../VizTypeControl/VizTypeGallery.tsx | 1 - .../controls/VizTypeControl/index.tsx | 9 - .../src/explore/components/controls/index.js | 2 - .../src/pages/ChartCreation/index.tsx | 9 - .../src/utils/localStorageHelpers.ts | 2 - superset-frontend/src/utils/urlUtils.ts | 2 +- .../visualizations/FilterBox/FilterBox.jsx | 480 ------------------ .../FilterBox/FilterBox.test.jsx | 87 ---- .../FilterBox/FilterBoxChartPlugin.js | 52 -- .../visualizations/FilterBox/controlPanel.jsx | 103 ---- .../FilterBox/images/example1.jpg | Bin 10921 -> 0 bytes .../FilterBox/images/example2.jpg | Bin 16969 -> 0 bytes .../FilterBox/images/thumbnail.png | Bin 8550 -> 0 bytes .../FilterBox/images/thumbnailLarge.png | Bin 49653 -> 0 bytes .../FilterBox/transformProps.ts | 74 --- .../src/visualizations/FilterBox/types.ts | 29 -- .../src/visualizations/presets/MainPreset.js | 2 - superset-frontend/webpack.config.js | 1 - superset/cli/native_filters.py | 398 --------------- .../commands/chart/importers/v1/__init__.py | 4 + superset/commands/dashboard/importers/v0.py | 26 +- .../dashboard/importers/v1/__init__.py | 16 +- superset/commands/importers/v1/assets.py | 11 + superset/migrations/shared/native_filters.py | 338 ++++++++++++ ..._migrate_filter_boxes_to_native_filters.py | 85 ++++ superset/utils/core.py | 2 +- .../dashboard_filter_scopes_converter.py | 251 --------- superset/viz.py | 79 --- tests/integration_tests/commands_test.py | 7 +- .../dashboards/commands_tests.py | 7 +- .../integration_tests/import_export_tests.py | 22 +- tests/integration_tests/utils_tests.py | 24 +- tests/integration_tests/viz_tests.py | 67 --- 97 files changed, 577 insertions(+), 4970 deletions(-) delete mode 100644 docs/docs/miscellaneous/native-filter-migration.mdx delete mode 100644 superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.filter.test.ts delete mode 100644 superset-frontend/cypress-base/cypress/e2e/explore/filter_box.test.js delete mode 100644 superset-frontend/src/components/AsyncSelect/AsyncSelect.test.jsx delete mode 100644 superset-frontend/src/components/AsyncSelect/index.jsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/DeprecatedSelect.stories.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/DeprecatedSelect.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/NativeSelect.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.jsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/OnPasteSelect.test.jsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/WindowedSelect/WindowedMenuList.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/WindowedSelect/index.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/WindowedSelect/windowed.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/index.ts delete mode 100644 superset-frontend/src/components/DeprecatedSelect/styles.tsx delete mode 100644 superset-frontend/src/components/DeprecatedSelect/utils.ts delete mode 100644 superset-frontend/src/dashboard/components/FilterBoxMigrationModal.tsx delete mode 100644 superset-frontend/src/dashboard/util/getFilterValuesByFilterId.js delete mode 100644 superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.jsx delete mode 100644 superset-frontend/src/explore/components/controls/FilterBoxItemControl/FilterBoxItemControl.test.tsx delete mode 100644 superset-frontend/src/explore/components/controls/FilterBoxItemControl/index.jsx delete mode 100644 superset-frontend/src/visualizations/FilterBox/FilterBox.jsx delete mode 100644 superset-frontend/src/visualizations/FilterBox/FilterBox.test.jsx delete mode 100644 superset-frontend/src/visualizations/FilterBox/FilterBoxChartPlugin.js delete mode 100644 superset-frontend/src/visualizations/FilterBox/controlPanel.jsx delete mode 100644 superset-frontend/src/visualizations/FilterBox/images/example1.jpg delete mode 100644 superset-frontend/src/visualizations/FilterBox/images/example2.jpg delete mode 100644 superset-frontend/src/visualizations/FilterBox/images/thumbnail.png delete mode 100644 superset-frontend/src/visualizations/FilterBox/images/thumbnailLarge.png delete mode 100644 superset-frontend/src/visualizations/FilterBox/transformProps.ts delete mode 100644 superset-frontend/src/visualizations/FilterBox/types.ts delete mode 100644 superset/cli/native_filters.py create mode 100644 superset/migrations/shared/native_filters.py create mode 100644 superset/migrations/versions/2024-01-18_15-20_214f580d09c9_migrate_filter_boxes_to_native_filters.py diff --git a/UPDATING.md b/UPDATING.md index 84893e9080d2e..cf4f846715aea 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 d8ffa8b329133..0000000000000 --- 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 6ae5d1e5d6fe9..0000000000000 --- 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 ba442e600ae60..208eb357534e9 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 a4ca5ddcf2ef9..0000000000000 --- 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 1e13c7d7ed3fb..3c7decd512c3a 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 b0eb024d2f483..8dcf739d14a85 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 6e9867ecb0a05..7b8073e9860a0 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 cc243754fc598..8abd9dd955401 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 9314f8d33f3b9..8aa475a7f9bc8 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 815a5df2f4e4d..829440133a32c 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 d063e11a8f7c8..384b09cabaf98 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 c69b92346db8a..27950a98b17e9 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 38dbbf65a05b2..7fe7680f17f26 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 68a014ba52a05..69dcca42b5e46 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 276f9b7d19675..3549439088233 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 6c231401c7053..986dabc814899 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 fdf8fd3b53f57..3270ef1f52551 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 f3bce12713308..0000000000000 --- 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 69799eadeae46..0000000000000 --- 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 bffa5428a60df..0000000000000 --- 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 95d01cc28b9a6..0000000000000 --- 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 f29466b33988c..0000000000000 --- 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 3770b542df412..0000000000000 --- 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 a611cf36c96db..0000000000000 --- 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 e8f30eb7e4cbd..0000000000000 --- 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 f04cfbdba9eda..0000000000000 --- 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 a6590efebd36d..0000000000000 --- 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 8a8c57cb6234e..31a9368a1ab41 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 d102af74833ee..015a12cb968de 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 1e3bc73758cb1..f4f9565abb8a7 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 b8f92b8df05e9..0e5e5454c6865 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 b461275d8c69d..fa3eeadf41d1c 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 1ef85f0b99ce0..4e30c38fb1d4f 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 699c2041c892b..930280a1f9bb1 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 562d90657e780..9d85e57a6a59b 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 02a3a49971c3e..179c03c996f10 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 d42b3254bec79..0000000000000 --- 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 2d7b5c05bdf64..83099e54907f8 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 17d5bdc83e05c..305bc38434510 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 1b146148682bc..3fc6775bdb507 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 a99061c7071c6..a604653914279 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 9247538a5d44a..a8bfafd8341bc 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 5f9b29b95dd46..ba91d748dab4a 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 d31af825717bd..a971df7d6a408 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 19527d2276efa..a629e631937cd 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 3369dc1c5be62..ef6fd7f5e1e25 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 92868f047e8c6..aed133b1f3991 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 75f9c705eb4a5..0000000000000 --- 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 9dac0583aace0..d873b80d488f1 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 cde72e35851a2..ac9bc065017c5 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 7cef9ae6dc521..a23958da0c4b8 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 fdc31f78af52e..96b0f21655d7f 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 8be43490ad3aa..f1f428240c169 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 984b389a5c9e9..bc2d83a90a6d1 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 4cf88645d7520..0000000000000 --- 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 4ad09e958cb76..0000000000000 --- 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 4c8a367e3520b..0000000000000 --- 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 8934ca9cf0318..347b339481dee 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 ec78b4267d870..bdfcf1e40a87d 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 cba3c27f5569d..a5d65f7768375 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 7a1ff43dc7a62..f3a602dbc9d59 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 beb5ed10147ab..12fba5b94c986 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 07c8ad550074d..91c0ae801cb85 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 a2b2a9a282963..0000000000000 --- 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 e37a4bf1c69d7..0000000000000 --- 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 774f7bdedffae..0000000000000 --- 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 60bb9c83af9eb..0000000000000 --- 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 cc109ee5aa38724a0aa7b2274a739a485b520859..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10921 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 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 17912d52f72cf29106303524e866766398587dea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16969 zcmeHu2T)YY*6s!rkRU;VWFK^d;f6Hx$m8O-d}&!tH0jqnXc;Y-Me?M)!l2aZ>_bl6WDox{DG3H5`cq) z14M&A0J{KKDtcI30)Vt;n1S|&obBs_73?B0A*3wDL?_h#liXY|F?v98ShuZzjO)jG66mT!LLks zh4=~~ArT<~0TC$?5itpv2(DZuBPF@|tNd$_U$_6d3;dH15)l6C@qd@F-vJ6j;1I_F z4~G@NrNF_Xz`?cvEZ|HpgIxK=n14$+xOkT?n^%YIO64E|QvUvW&((0wPjkAlZo4bdnS71>3͢v+m_#frE-kODuB~ru?(H8O z9vz>cPS1YPg##|(zo)+w_B*;LK)P_jHMm6Zi!K~ocQE5oT)KQy2%l0;i{Po#HCEv_ zgjDh|8D&4Nu!(3Rsh_qvss*tYA;6yP8LYx=%iz9jBLBZq-#( zk2zvbP;QSP6#C8XNXk1xZEM$!e3K z#K8NvvpoeFuMNHel!}(U=W-}*DPGTnv*U%5>;w*2)zD6fK$b@n9gPa52;YnQF|kbg z%+IiZ$8!vv!OzQUo&Ok90beQ2&9hb%zcRLZDaPGCMuD{J@%&j}#~HKr2%x1%_1}{9 z1(%?gO+6CZW=Tgr$Id)K?qtc#bak{yWlfsg!vzSrTnZkk%riUtdxfp(I@YU|Y{s+~ zm~K>^*=^XpvuEUc&Ut->ukAkcbi*HCcGqWANkSxFCjnu$7H>0GE*N%A+g?X`w4y=6 zP$jL8;qyLa!U-*G$b_tBMA4Nh^e$cT0Ha`!P7}O5FSL;k1yhex8m_QyU>%SqrbiWE z0o8LxbA*1PP!!K~CV_il3;3NXnjOs?29;iGXx2n5a42G%ezMt*1>S360m2+Ca6y7t zyErSy0!vg&R8_u?GpD6J~B!9}Vu*q-O(T)SFgiUZ`vzXHY|TGS2ikNCiUkzJ!7F}6SOC?^-OA;1 ze)1D8-%wHNr#uW(uT4`e#B|pl$M0QT(j6#uaUXI)Z97z$3U9Bxds3}d?j%+xTxbuo zh<%TDJqe}sST}pzj~FdKidMba;B@uv7c+*G=8II?uu{?B@(_b<3ZTBSoU+f^p`|G1 z3(nx{Sp%+d$O07{3uL62U;(%u6#Xm;r6`C6)-A^&yQAq?;CiNz)>~FDo3kB+V_qz# zM57{6LV~R^Jh#)(_4F)-CPcxOaL`9VlWG+I^$lwA?r1E~A(IV{!LY14Xc<6%JD~FK*T1U{ z16^hwGVj0wWCs%5MUdZ-mW82E!vbe?mHp>OR#>2od&LwBK=#S31(%_3Pg6eVvHX6- ze_F!VC0HQ+l%OJUAJUBO?cyc-{g7B7NE-`aLhPL}7lv41FYJy!v>zRaP&;K=z*w$O zzx}5n|7lk+7Q)vsa@!o}C?pmTc^%gLPh6 zvY{b8)0tnfz`xiRS%0m@f8T15EtslmenhJmCJF7IMk9o+rU~WR2LgAMd3N9||F9vU z!D1nMWWg0P@`?ttXJfn}y6ZUuLU;$oLB9*OE4K)qh1{gJY~lMLsB-?TNjYW7CMVjrRzQBk=mOU4+a*7T(7uHO%*@89;?e` zeae2*gseX3O}vca>ocy_RDFZJ)}!KbWh-A-v4m81iMrLmho6*P_R((=ogBF@Hw(;n zI7jl12^^mL-fKP%y_vGLf92T$xkSQ}-Z?k_0SZh47hr z4y~{|PuZ8XZzvzGR95qx+s6WdC+#E|RgTN1imiO5!Ph?>M0qRo|E! ziqkFSj8|9Qv7fLf*GN@eK!y2VIBImGk2o{Kc$BEL6oLOrrv8B$Hm9hJ3c>=HDpS3i zkB?zqqlLzb)}$@6F0Z9wLJ9$Cb~mGTuj0hxJ0;+8)^+_rB_hxI7ZU?^Dx@|_inn*t zZb?sB>Lzf7J&vby=M}sf_UduqFcr=L#2+K7Tw|XEgvu}b>!8}lMpw1`dNyPWbEj?0B=*K?t|ofpXD}6C%#?p0YFbq3 zzWl~GL~Ag_lj%ySIv1}0Z44b2=#&5@IeeJ0oe9+J2xMGZPX#_m@BG*J;)NcGqEw(Tr_@9#3R@@=QR(N??Ob_u%emVOCy z)680*nJGnc=+IZ7TW@4{X@oUQ&7-6wr%H}dFiI@&zADa}8~!+w_*q;%AKah!2{}2~ zY(Sg+><-gW`eQ5ydnM5MRZtv94q^eKvu2E+SU;L^7P^P{frPdfi?o>IS86brDR3s{ ztncO%XRy5$YPWEb7M4}IdwSV_`;&gk1ZL`Pd#E(tg0HhW3M!(yNNRzh_?bdH^GwP( zl~;3ilQr7YjwoL9{LT8$c2S;K$9M@}2DEGQ6u)Y>~N1u)spcCVdp5z#bqE7h1 z9xDC)%^M)vS4az$b&RM-(uBMDD9px}AZtsYCi0_q4GS=3 zVW^MZqS3bLs5|wb`c}Er$aK+oXsXkmMut4v33tt#@yOB}skl`_v(KAi%upvpfNQYN z9;sIyK^-#07|e6^-VVfy3?Zd{ks}&KX+y7`+FBeLSkj-R$X7%NHyDyI4q6IbHl$9J zc^o)fAe^GFp=Q34d7(d>_I+L3xYg0V_9v)sz^gam3+4vU{x(og3ry+9NGYRvf0CV| z#vw>&Pb|=G`#Q{Q$aKQjEOEcI<~sCW5K*JZ{8#7ySkk|^@sAt){}DGb8ksh}-O`UU zh+zm(74wGaF$dAzRxT^<;y#YsY2G}9$NP5J3GvsfG1`>1UneTa!nwB&#IUshsWqF-sub-Zps4GM=F`u2XT?SYZzyibVy-)JT%DO)j5|_2RE|QuJqD1M)L>%SWvZ z-4`E2zok7s-e5Q6dTPm7GHQMgC5412plI`K61;Gy6|D+Pb0!0n^wgdu3>fn{EyxkF z!5&?Gq~<(uHSo8SU_nNJ){cI41uTGT`!Oc(zMH1JEFaUa-|V7TBj4h5y`u9^QY9)8aQquZ(EK@hI1h zJU1HNt&pKVKI`+9(5?CEv8uup|M3T_BjE4WsE?>AwpyggKkKiw-?uLw;=%P^m8zqe z7ljCp(PFi>~V6i1O{UDvQYk6P3qO_rYl{fANebA{Y*X$ zGI97NoA-^Q53ktN4?(>v8-GF?CiW(wHs;hGjz5s7EX}vgUb%@-=VBwdTmxfu>SfX= ziIFA4q9r9o{6t?V1crG&eR^^{hl}AxWwo9)=36cDO8Hie>5Y$#>zz4oV5~kqCuR9? z(9?teUGLsw@kQ}~`fVSHi*%n`$VSx?(F+!hINciCK-oP0Y;*fm%cPa(p1KOc0q6H1 zO$Aq-jAe;XRtxZufobK189c=Si+U&apLvtYHk?mC(GcRRbhdd}=H8k-^Sk>?m@`~D z^4o>}L=NvwgIYSd-$YWL9IR=IefAAd-bd|CXh!n(%i%4`Ry;5bm zocN=Z&>H3y(FzatxpV2P`JG^PlzOnyUf^4NdD^v`-yKsR5;C5}8dM${YI|+(#8Zl` ztLUpzoub6Y_Ghd@9+E3*y&<@SD>)yQZ`9RY`qSI%e z>ot1~*_vC!ob$}lsv*3E0v^i#%_F#sX0V&7OQc1y37*3KO_+e_@vAT0WC-3?>&2fa zzT(wLEYOl1oD@d4y*!#Rq<_vlyQ*Rlawl_Et(+x6kx}a4J&SD}ig?}|3y>gQmZKz^ zhF>02idKzjTWg9n)WK$-%h%~fy~fw@8SV96P!2}x-Auv+VboK2)X484|$XSuSL+6LR z^H_kC^9^a1P4s42CnkBQn^i=ar?+Z`sD0nTF!GqkQzz~1yll;)y5mCPI{_cXLL(b@ z@}Kmr#pP3zm4yn75{VQ%pSn)*o=m-`Ho-NBXfH~iUkh?5bxC%uEj=U?-pX#wW=oEP&mTxxgY3^%I8g>A3CXpBW%3R9~g+7`U2Nh~foJt?i{ znz?d?`$*mQ75u^M^Lu)&EzzusF$nDGE7y3&V)Zdk50UAl6idnl^L8Datq_9@*#=Dq_ zmKx#M*D|J!)F{)AnT3YSRg<}&MDki&3&=YOX0X8HBAhEHa=;rkCuCi-M4AvYYf3YM zvG%Neg;aIxneEq<2ipg3J8eVybn7Y`Iy!o%qVUrVl&Pn)xl35MNxlpgn72huqKnI| z!j;eO`N*gIpr`o^VG zubAtGXpNxgwtllwe0*CH$(Wys{_RIHS1eHYvjc@~KJj z6e{TRC_B9(Dp4Y#Hxz{apHCkRw&RvdYapXk_rzvX1b0P`hD+=n9=d6@QqEi?wIu9y zxzPx>6>(70WZPj>?!me{mv8jv~O!v&HFN_S<)nDSY>NXWwf4ExrfIBNEu($^V{m_o$Tr4nNWPSz| z6ZIqW&|wJ23$Ioz@R?}fGj167XWV>~mu+hKoFSW=qq#q#6~Df{@r07j_!A)6E~jtX z-6-HUB;q5z7t!}F+rtdhOKFWBB{7#3UUz&_p~h>??y;yVa+^|}O;KQC|MS*LB5&*k zq-3ew3kw)e9aw$F0v6NI0b@|fZM*@(Xcy>js;-Nq=N=QPi=e50W6wHSBXR!RHazKO zN(;F{ff=9P74MH*VW+_ls!b=q-;A=*v|&MNHe7|`Hy6D;;~zaq&zYP{@lJ^^j~(+o z;yygCOn)V7+{%~q?Dg$Dx=WP&c8SKuYi^C_kj*A9oAaqP7x*1P_{fRlmUq+{u?@>f z3k%vI>K`kOK5-Z#-aJ^S=r-Fqi{kJR@w?JuhZMa%_2T$X&PJ+^4B6Kf5|-Q%R9_g3 zSDrj3zjyY~CBv3ltb9UnSMta>2@6b|upH8iKz2hRs2P@jti05^2O5d~vE{GJAdmh( zVhd^Jqo0+P>T(~DN)t*@itCVgka!Z<8-D(!FtI5{8;Zwnc>cEzweDS}0<$Nyt$$KS z5j9g}GCOT^b{x)`mq*H`t1dYuYSfU~@mwWWE%XJDCGsIwCYI~tgj1W0d^u1(>Pp;d z+*xc<8GWCIL*8IZpFKejb1d2Z;L)ueG0rMXErv{YEq`Uqyv!{wi;U86qw&_BWzs@F zwH!NbEfsNIWpd#}PU6n8w_c5DnKB(-U)wTxl5+W>$0SIWP}d7Jtr%Cy=VN>>Q%v&Q z%c0F)d=5{m!&E7vYiuD)SU%^ZG)J~|?ulh6S% z+wU&_zWe$wT=!R>|06s9;?Ey%xBo4F?3vdEy9;J_wreFHU?lHBjX=Xd;rdB${ss+< zB*SQtD*DX;B@U+Y+21vQe$WK9%vJ~;2mL+kp@s70htN~P&AFpP_!%n?u!IGy>ryT5 zqj*7WSYZ!8Z{b3r`OI`S>za{*Ezh*{tMURBt`IpGkL=Fyu7SJxb-_l;Zt=h`s$Wo9 z2Fbj1he@S*umE-4lDu;%#q)5FAt7RW%bVA?qA9)gb5VKC;<94MzKpERSH5}bqxRVq zwQs$-QmDdo$6rS`^Qd&(h-|AVr9iVrwL)m>MHYGUi`7emigzvte}&1qrwa#JS*y)| zmTS7yx0~V)Xfbh=Ak6osDnT4sF~fDT;b;c;Y?0V@a?kYem6j{Fg_#?9xohew4OMdn-iW=~m{TaO`U-r4P})>p-_ z3lPz<2T*dzrwa!^4bi^7ya;bjpVVn?BrSK`zFa*nkezVRQBzy(Eoxlz!dl#@K7z!~ z;zL_c0Ws;^>=Unk0L?Rqp})x9NJqqq4P`r=oaKyt-h5a0jMqeEg~DB&T7~g2KVoZl zYHu7vfF^D7KCd$eA!G;^coStTE;&&1T0}ulii@@N&2TKmQQjlY21W z`=zVe3yhbomAkqO7_UwHXxHs!`3P6VsaA^DPMOHN5>~uhsI3kmoohJfongJdkMo{R z6Ia5gt}2iTzSTTm2$g|;+sO@kzCT1)DV6`UJgQfiJ+HaGNQ|lq>Ej>a-$|;Y!RW`H_4X3X~_08R=DWu6{airIkvp+7f>dNL34Wv z!kt|ZPo|glTeRsMe@Xd4Q?`yYRbSjTHQKa}gcGkP>It>3Jez$HGTGes@N*%bR$GVM z%wTF6W;`+351wB-F}8HBeLP?-?v+v*V2x}{x;&C;6?WzMH1k6Xi`4%2!F0?ZkIK1% zn42W^=8QsKE+;58zBPr1VdlHYRXA!4gN6zTvo4!Wu6|w~Xk>C9J{U(17aokrjwis8kYY+i&kFj= zxzf*cst2-x^P|bU*SGtM61_4it6`o@f&8_ik{@|8eESAM^h{o>(9d1=yXg_v4k7cA z`(is*YZ~7hnO|UNEEX}`L&v{k8RS4p`J^-fV3YEnmXE72slG@--*k4eU^AE_``Su^ zFE;8hCe`w;yG=k-t!m9ygt3P)w*5|L@%FWa)ue?K(`WqRceeG7(u@m(C4{@LCz~Wb zSAJHu^UNTKh?r8PlQZ+11EuC>DxBC4j&$dNP#N!(ZdgIv=8)pjj^AfF_sGIre6OiH z(xT!X1hoGcljVTde=7%BsXdZcE9?{cO-|e*s?#x6GQ2DXki2e#FyAibOvyI>UNtt$ zlde{!KPpHqP%#xAx7K@w+(@ik=p&&){cF(&*SGc9V%C!(TPwMcKp&~RpZfal$x$I& zAu0Cx8}9l>V%^661#DKZk9V{wu4u2Qk^YctxQ-~J*k*MA4|@WW5#Nu zLW*2TA1yUi6=zPFZz4?Vy9;3m2LZdMwJEFN9f+8YsuBs$_gyWdLC(5p#q%dNX4G&R zm>=yhVsor(PO}jfsF!9Pq*G3%_NwY*vKGC=3k%BSV0{ObLsN@+iJ+qkN_Fvr2M0yV z4Rs?%kcys~w9PwNsr!s9sREqW`|#2JgqN6EQPc}?Dx0eDL_I{1v+)n=e45&v(IR)v z7S+f*L$|1{6!|77Ls&$vzL)Ehxl<44hceZ_<(<=j^lP9Kp~A2xsj0R-$0$ArG2*Fn zUn%1Ve!rn}1mSX7<;TyGEZm-q{$5tp)A|zgEV$Di%6fYoN3$8s8BU8#_sv-ZraBC$ zpM+hyCsDOXL(TC`Sasf|BsTclp?Ng#5eWL;1piX{{zPUpNAoi&V`vDD$O0p&*G?OApHoE9PC5Hi+ooM;CkS~imEc`~zjzxwe-=(fhfTu|EGIEB%oqt$bc zh{h~msO^ij$&IfXqR~nR!l|3YE%VXY+;oz3H)e9C#UE)rcQ>~hg{&Qb9*pl2AmFR; zO$QAR>Aj$F9GC9&_upc1N`Qtx;sXF@K_9a+-d z=`%YAy_Z;9f}ZPiJYh~c_SL~aMM1{}9u#z&P9ABTqCrbPk`%Q4Z#&6KUwrZT<*{!$ zN#-@rsIzB!xRcz}-tCvr!wS#Vdd)1Op_uiwL8#){p1=1q+3;owNb(%|z z+83CvGC{RN)-=4mL+zKZMK_wH827MPMN{8!`*CA6GU6C~J)qxqO{o=P0VzK(rvBeP zp5-?@s$5GnugraCGb@mCPS&bzEqg^RcU0_t`Fn)Y*zw}xeVtVY%ZbNiiI<{9W@}l3 z(ivS!-;?DVPfq@%H_OR7<@bD=k6`c))qXBWjC>$m8z83#aS03{%&;k` zVL({hpy)Zb{ZfA-5ua^ZH6CtGW*7iktpMI`AKPn+2cxO*fw6Ho8~0 zo5vf7m9k;Re&%0fo-H1GUu+I~>4r}d zLY^n$-M&ch2lwob;3JxBjXJkh56w}k8MDbYJu2olmUS5h-< zX9>p-K~IEDf-J}Akp{_+&jvP#gh&`IJzq>p?!kiagFY&TV4{>Cx^HmYq@%KA|LELh z!BgOUW9Tq#n||2o+<`Qi&P8Bu!#L*Vv~r;bzXj^eWi+K(=K@Wf@mE^CXH(D2e81w+ z$CG_+k-+VaNo^j0Tg^?@wjX~~0Pe{jOZY(+S3@_oeuvtvxJB&-LDfCT zSl-_a*uMeC14S1s82P)f3tvG@Xqdkw7C<PlmzM4ItQ63Jlj*gvxmWn%z%6GM9gc5LG}!;MRAo zjE}gJYm7rhF zkrZbS*{hdSjgzO!^Rs$5l-vw--}UP{QHhcEx4?f4M9c|ne{Od?zm4LV>o?!3%g;=b zt_ZPzv}s_sbfE1hFheh2wf;O(SntN?ElnkWddOpU0sNkUO)0WnFT|QHZ*9>DwUOY2`UrM-xuNqD9g)%u2;}SW!|oSs6VCNBvp5cF<<+6FtPXHqJR^rSmY&5 ztK(6puUDIFQQCCIG5X%g(*TzAwCdGC95+ulmD4wJcNZ%=XywXkdg-R^u~;=`q@jXd zJuP?pNwSY%uAD1((qe$21VP;Oi8be?n@Y+V@U6}Tg0GR(u(6EfI~*~k8RL$N@M_^8 z$}0|fPeluwxPR!s7NUPbS{Uw?zIC!7qxAv{+^a25R$Z*2r7?==O>NOucHob0=hNoR z+qoxC<=_0}j;P>U(?j$TT67bAL{_;{iYEJczpDHW6xVOCcCD2%>N85rGOSI?xsYS_ zi@gK?tn!m?hls13L|@FED{jUoGRES=ewc@vvG4Ua&ym~EAC)A&aJDG0Nzm)Ha5=zN z^G?cO2)zH1cg3HuZ(f2tdz)4OVhP6Dm`odVj7cw9hMeIMnS>tQ)=5Uw8Stvi;Nu(_b#^R10DkzXws;(EL75>8n?8F1s1sC9%T`G@ckG8dXk>6-zw_$ z^yL+jbv$ElWm^Yk=$9gzzZ}ky_zm3sfT4HXw!bQ6YSwQ26?wGfG>s0IX>F*8Rgu;4 zk9hT%^O2kHj>kHN8qFFA!ugz(q|A}Z2tn2G`^Kn6Z>aDP+sA?S)w?@PYFAbJ<-V)y z%+m(DrqheVou0ZN;!Iopez-DxfZ2YG*X0ubc_XNcTG#!<^y4S7lEt<*h{!(hgAY^i ztq(I7oUt!i@JsgE2SY@1crQ+7+oM&_)jhhJ=dD6^D%GT9AdmNGVyt|h78uXu^d#O_ ze_4JyaGgfdaOm~-o6%R59=}OFCqsmsE53agX)k11-Z^EH@d%4!gNnLNehZkI%8lyLQUSK|VD`v+(R{HnM2TVI zPnl{zF7I7Ml#=iR%Ulj{xS})HfNloAFrSGD7A>6TDb+1c;8o0&# zNc)j>amnr()Cih%Z1i*Z41_+?=glmY;*|?zWSzc?y#lkl%CRSAYWDI7Bm)c#qtB+O z{2EN5-|3(EHU1q>j{meuy81<_ScJZ2`S49-T8emazS)$Kv8Na4z(e-+0BtOpRDgf5;*M(dOO#F*DYX32)||V&{!A5=ISfo=H3?@Z?#^ zn{!!|d2^y(d8=cAsfL9!(@X{Itaat!`(Acg8&xlFbXd6xE?MGgpGctL@L-931$bH@ zIn|ps>71AoG+EtQV+({}S1>Bf$JN5q*VU6brC2Fp(bH$HyeL7uT-0WQ;E5*-9TTa$ zZZC@9JB*oPv|iQM{fqJ!6OXrAwC&wh`!tsgSl^J7ZdV5ftJ=Mf^`?0g zm1q1v6DaQlMOL`7pbS_hAjrt+-I==n^LvxaYZs0)Z8GKKk(CH7MJ7I`hkwe~`$a9+ zq}fxnDv`WJ;JS1;AHG~Qx5<0qC4U(&-Ln-R<2@w^$>z3YmHn$df0HC}K&1mOFpcmt zY3U!SR<2XyP@Z)L1GfcX$8rp|nlr zR2F;UB~|v8W4kvv2;)51NuoWj&(m*y`3r>p1<3!5aU!H*l*ujSCr3Y>u{eorQgqZ% z1@~vdBKvC^-$G`kPhVxW)%^6o&0_=xN&E_ws1IL+qN60{j=`u;#bU2*Puf{1ZNJTr;$#xnU6F|=9&|~~sWxwlFtk;1a2TFC}w4S!m!?j``AzQ=WGyav~9n|mS z>d-^?ooNXXp;MJMkMB7Qh<;xvgYnMzh{L!Z;wQ2JbK$1_ZqNnbBN>ejH3wpopz=jT=26Gikk)Tbj7#Wk&mrd6HTOWDa2Uykfp#F9j+`7YJPdK8@N zdqcm^?QT@2?6dYK%s}V2Ps~>n)l%Wj;7Zs*xAQRJHb;nk!C)}rQhEXlOle%ujQ*?$ z`!iuRf4Wcim=9g9?Fp-Goo=kA_zp>z%E=Epw5@g+@EsI==Aaw8KMJE1hXumGk4XI9 zVgXsmDZK`U2e$s1gbby7#)3wH-aTswssOT}zYaYBMQ2Pl7|LAe7Y1|>*3;#_q~)|# f(~Z;?-{I@Icibt7=G6wRwZAO3{{u1}cJhA#Pfe$` 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 be08f687a5b7a7ca4ff56204b5f358c90ae39898..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8550 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 diff --git a/superset-frontend/src/visualizations/FilterBox/transformProps.ts b/superset-frontend/src/visualizations/FilterBox/transformProps.ts deleted file mode 100644 index 59387edae1d70..0000000000000 --- 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 316a29f9bbef6..0000000000000 --- 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 a8cc847c8b4c9..e96b528c9dea2 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 3399be3d608a2..db512b074f149 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 75df428e381e8..0000000000000 --- 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 783f300c074f7..f99fbb900894b 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 bd7aaa4c90381..0bfd57c5a5dec 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 2717650e9e31b..62f5f393e96f9 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 b6bc29e0fa4c9..fe9539ac80d49 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 0000000000000..c30c7d378920f --- /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 0000000000000..b54b60cb9dbcf --- /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 b9c24076a4e12..3a761cad1fc73 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 6cabbbb315a4b..6a3ead7c1449d 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 5738fabc939eb..ade52ee7be78d 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 6512a141bea44..3215364e80056 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 175a8a3198da6..6e9beab249248 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 c195e3a4cb31e..adc398e785f3a 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 ddd0b0caf43ef..bdbb912eeccfe 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 c4c11df9d8120..5c7a494d87854 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) From a84d86fe264f6b3b1ea51545e31acbbc559c1805 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:39:04 -0300 Subject: [PATCH 02/11] refactor: Removes the deprecated redirect endpoint (#26377) --- RESOURCES/STANDARD_ROLES.md | 1 - UPDATING.md | 1 + .../components/gridComponents/Tabs.test.jsx | 3 - superset/initialization/__init__.py | 2 - ...01-19_10-03_e863403c0c50_drop_url_table.py | 45 +++++++++++ superset/models/core.py | 8 -- superset/views/__init__.py | 1 - superset/views/redirects.py | 74 ------------------- superset/views/utils.py | 17 +---- tests/integration_tests/core_tests.py | 11 --- .../integration_tests/tags/commands_tests.py | 1 + 11 files changed, 48 insertions(+), 116 deletions(-) create mode 100644 superset/migrations/versions/2024-01-19_10-03_e863403c0c50_drop_url_table.py delete mode 100644 superset/views/redirects.py diff --git a/RESOURCES/STANDARD_ROLES.md b/RESOURCES/STANDARD_ROLES.md index 51fcc6479b6c4..e1734314f4d50 100644 --- a/RESOURCES/STANDARD_ROLES.md +++ b/RESOURCES/STANDARD_ROLES.md @@ -57,7 +57,6 @@ | can external metadata on Datasource | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O | | can save on Datasource | :heavy_check_mark: | :heavy_check_mark: | O | O | | can get on Datasource | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O | -| can shortner on R | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O | | can my queries on SqlLab | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | can log on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O | | can schemas access for csv upload on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O | diff --git a/UPDATING.md b/UPDATING.md index cf4f846715aea..b239866221e36 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -41,6 +41,7 @@ assists people when migrating to a new version. - [26636](https://github.com/apache/superset/issues/26636): Sets the `DASHBOARD_VIRTUALIZATION` feature flag to `True` by default. This feature was introduced by [21438](https://github.com/apache/superset/pull/21438) and will enable virtualization when rendering a dashboard's charts in an attempt to reduce the number of elements (DOM nodes) rendered at once. This is especially useful for large dashboards. - [26637](https://github.com/apache/superset/issues/26637): Sets the `DRILL_BY` feature flag to `True` by default given that the feature has been tested for a while and reached a stable state. - [26462](https://github.com/apache/superset/issues/26462): Removes the Profile feature given that it's not actively maintained and not widely used. +- [26377](https://github.com/apache/superset/pull/26377): Removes the deprecated Redirect API that supported short URLs used before the permalink feature. ### Potential Downtime diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx index 8a4f5117187c0..359e58b5a8d93 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx @@ -25,7 +25,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { LineEditableTabs } from 'src/components/Tabs'; import { AntdModal } from 'src/components'; -import fetchMock from 'fetch-mock'; import { styledMount as mount } from 'spec/helpers/theming'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; @@ -40,8 +39,6 @@ import { nativeFilters } from 'spec/fixtures/mockNativeFilters'; import { initialState } from 'src/SqlLab/fixtures'; describe('Tabs', () => { - fetchMock.post('glob:*/r/shortener/', {}); - const props = { id: 'TABS_ID', parentId: DASHBOARD_ROOT_ID, diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 9da84c1fa323f..807f430ee44ab 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -183,7 +183,6 @@ def init_views(self) -> None: from superset.views.key_value import KV from superset.views.log.api import LogRestApi from superset.views.log.views import LogModelView - from superset.views.redirects import R from superset.views.sql_lab.views import ( SavedQueryView, SavedQueryViewApi, @@ -309,7 +308,6 @@ def init_views(self) -> None: appbuilder.add_view_no_menu(ExploreView) appbuilder.add_view_no_menu(ExplorePermalinkView) appbuilder.add_view_no_menu(KV) - appbuilder.add_view_no_menu(R) appbuilder.add_view_no_menu(SavedQueryView) appbuilder.add_view_no_menu(SavedQueryViewApi) appbuilder.add_view_no_menu(SliceAsync) diff --git a/superset/migrations/versions/2024-01-19_10-03_e863403c0c50_drop_url_table.py b/superset/migrations/versions/2024-01-19_10-03_e863403c0c50_drop_url_table.py new file mode 100644 index 0000000000000..49b320f13f6a9 --- /dev/null +++ b/superset/migrations/versions/2024-01-19_10-03_e863403c0c50_drop_url_table.py @@ -0,0 +1,45 @@ +# 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. +"""drop_url_table + +Revision ID: e863403c0c50 +Revises: 214f580d09c9 +Create Date: 2023-12-28 16:03:31.691033 + +""" + +# revision identifiers, used by Alembic. +revision = "e863403c0c50" +down_revision = "214f580d09c9" + +from importlib import import_module + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +module = import_module("superset.migrations.versions.2016-01-13_20-24_8e80a26a31db_") + + +def upgrade(): + module.downgrade() + + +def downgrade(): + module.upgrade() + op.alter_column("url", "changed_on", existing_type=sa.DATETIME(), nullable=True) + op.alter_column("url", "created_on", existing_type=sa.DATETIME(), nullable=True) diff --git a/superset/models/core.py b/superset/models/core.py index eece661ec5143..a8d0cdeb517ba 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -90,14 +90,6 @@ DB_CONNECTION_MUTATOR = config["DB_CONNECTION_MUTATOR"] -class Url(Model, AuditMixinNullable): - """Used for the short url feature""" - - __tablename__ = "url" - id = Column(Integer, primary_key=True) - url = Column(Text) - - class KeyValue(Model): # pylint: disable=too-few-public-methods """Used for any type of key-value store""" diff --git a/superset/views/__init__.py b/superset/views/__init__.py index 1b8d1b8f09567..838e92aca231e 100644 --- a/superset/views/__init__.py +++ b/superset/views/__init__.py @@ -22,7 +22,6 @@ css_templates, dynamic_plugins, health, - redirects, sql_lab, tags, ) diff --git a/superset/views/redirects.py b/superset/views/redirects.py deleted file mode 100644 index 93a4339403109..0000000000000 --- a/superset/views/redirects.py +++ /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 logging -from typing import Optional - -from flask import flash -from flask_appbuilder import expose -from werkzeug.utils import redirect - -from superset import db, event_logger -from superset.models import core as models -from superset.superset_typing import FlaskResponse -from superset.views.base import BaseSupersetView - -logger = logging.getLogger(__name__) - - -class R(BaseSupersetView): # pylint: disable=invalid-name - - """used for short urls""" - - @staticmethod - def _validate_explore_url(url: str) -> Optional[str]: - if url.startswith("//superset/explore/p/"): - return url - - if url.startswith("//superset/explore"): - return "/" + url[10:] # Remove /superset from old Explore URLs - - if url.startswith("//explore"): - return url - - return None - - @staticmethod - def _validate_dashboard_url(url: str) -> Optional[str]: - if url.startswith("//superset/dashboard/"): - return url - - return None - - @event_logger.log_this - @expose("/") - def index(self, url_id: int) -> FlaskResponse: - url = db.session.query(models.Url).get(url_id) - if url and url.url: - explore_url = self._validate_explore_url(url.url) - if explore_url: - if explore_url.startswith("//explore/?"): - explore_url = f"//explore/?r={url_id}" - return redirect(explore_url[1:]) - - dashboard_url = self._validate_dashboard_url(url.url) - if dashboard_url: - return redirect(dashboard_url[1:]) - - return redirect("/") - - flash("URL to nowhere...", "danger") - return redirect("/") diff --git a/superset/views/utils.py b/superset/views/utils.py index 574fedb66b7d0..bf1099a6aa42a 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -19,7 +19,6 @@ from collections import defaultdict from functools import wraps from typing import Any, Callable, DefaultDict, Optional, Union -from urllib import parse import msgpack import pyarrow as pa @@ -31,7 +30,6 @@ from sqlalchemy.exc import NoResultFound from werkzeug.wrappers.response import Response -import superset.models.core as models from superset import app, dataframe, db, result_set, viz from superset.common.db_query_status import QueryStatus from superset.daos.datasource import DatasourceDAO @@ -145,7 +143,7 @@ def loads_request_json(request_json_data: str) -> dict[Any, Any]: return {} -def get_form_data( # pylint: disable=too-many-locals +def get_form_data( slice_id: Optional[int] = None, use_slice_data: bool = False, initial_form_data: Optional[dict[str, Any]] = None, @@ -185,19 +183,6 @@ def get_form_data( # pylint: disable=too-many-locals json_data = form_data["queries"][0] if "queries" in form_data else {} form_data.update(json_data) - if has_request_context(): - url_id = request.args.get("r") - if url_id: - saved_url = db.session.query(models.Url).filter_by(id=url_id).first() - if saved_url: - url_str = parse.unquote_plus( - saved_url.url.split("?")[1][10:], encoding="utf-8" - ) - url_form_data = loads_request_json(url_str) - # allow form_date in request override saved url - url_form_data.update(form_data) - form_data = url_form_data - form_data = {k: v for k, v in form_data.items() if k not in REJECTED_FORM_DATA_KEYS} # When a slice_id is present, load from DB and override diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 526a5592e9637..9e1a9ad11c825 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -413,17 +413,6 @@ def test_cache_logging(self): db.session.delete(ck) app.config["STORE_CACHE_KEYS_IN_METADATA_DB"] = store_cache_keys - def test_redirect_invalid(self): - model_url = models.Url(url="hhttp://invalid.com") - db.session.add(model_url) - db.session.commit() - - self.login(username="admin") - response = self.client.get(f"/r/{model_url.id}") - assert response.headers["Location"] == "/" - db.session.delete(model_url) - db.session.commit() - @with_feature_flags(KV_STORE=False) def test_kv_disabled(self): self.login(username="admin") diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py index 48abfd31b4128..83762f8f6e876 100644 --- a/tests/integration_tests/tags/commands_tests.py +++ b/tests/integration_tests/tags/commands_tests.py @@ -112,6 +112,7 @@ def test_delete_tags_command(self): TaggedObject.object_id == example_dashboard.id, Tag.type == TagType.custom, ) + .order_by(Tag.name) .all() ) assert example_tags == [tag.name for tag in created_tags] From 1010294f2b5459c3a9456b76b2aaf27f42059457 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 19 Jan 2024 13:31:56 -0300 Subject: [PATCH 03/11] chore: Updates the Release Process link in the issue template (#26677) --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index b79acd184d826..40e5d97f27dfa 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -38,7 +38,7 @@ body: value: | ### Environment - Please specify your environment. If your environment does not match the alternatives, you need to upgrade your environment before submitting the issue as it may have already been fixed. For additional information about the releases, check out the [release page](https://github.com/apache/superset/wiki). + Please specify your environment. If your environment does not match the alternatives, you need to upgrade your environment before submitting the issue as it may have already been fixed. For additional information about the releases, see [Release Process](https://github.com/apache/superset/wiki/Release-Process). - type: dropdown id: superset-version attributes: From 649ff4dd616fe1e7839444e0408b07cb16b18d12 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:00:51 -0300 Subject: [PATCH 04/11] fix: Revert "buld(deps): bump swagger-ui-react from 4.1.3 to 5.11.0 in docs (#26552) (#26679) --- docs/package.json | 2 +- docs/yarn.lock | 1415 +++++++++++---------------------------------- 2 files changed, 340 insertions(+), 1077 deletions(-) diff --git a/docs/package.json b/docs/package.json index 5ac908c3af90c..747063d5bad09 100644 --- a/docs/package.json +++ b/docs/package.json @@ -41,7 +41,7 @@ "react-dom": "^17.0.1", "react-github-btn": "^1.2.0", "stream": "^0.0.2", - "swagger-ui-react": "^5.11.0", + "swagger-ui-react": "^4.1.3", "url-loader": "^4.1.1" }, "devDependencies": { diff --git a/docs/yarn.lock b/docs/yarn.lock index 7fc5b3393270c..60b67abfac173 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2455,15 +2455,23 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime-corejs3@^7.18.6", "@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.23.7": - version "7.23.8" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.23.8.tgz#b8aa3d47570bdd08fed77fdfd69542118af0df26" - integrity sha512-2ZzmcDugdm0/YQKFVYsXiwUN7USPX8PM7cytpb4PFl87fM+qYPSvTZX//8tyeJB1j0YDmafBJEbl5f8NfLyuKw== +"@babel/runtime-corejs3@^7.11.2", "@babel/runtime-corejs3@^7.16.3": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.9.tgz#3d02d0161f0fbf3ada8e88159375af97690f4055" + integrity sha512-WxYHHUWF2uZ7Hp1K+D1xQgbgkGUfA+5UPOegEXGt2Y5SMog/rYCVaifLZDbw8UkNXozEqqrZTy6bglL7xTaCOw== dependencies: - core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" + core-js-pure "^3.20.2" + regenerator-runtime "^0.13.4" + +"@babel/runtime-corejs3@^7.18.6": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.21.0.tgz#6e4939d9d9789ff63e2dc58e88f13a3913a24eba" + integrity sha512-TDD4UJzos3JJtM+tHX+w2Uc+KWj7GV+VKKFdMVd2Rx8sdA19hcc3P3AHFYd5LVOw+pYuSd5lICC3gm52B6Rwxw== + dependencies: + core-js-pure "^3.25.1" + regenerator-runtime "^0.13.11" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.15.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.16.3" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz" integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== @@ -2570,10 +2578,10 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@braintree/sanitize-url@=7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.0.0.tgz#8899d8e68a1b3f6933d4ad57a263fd3cf1d34d8a" - integrity sha512-GMu2OJiTd1HSe74bbJYQnVvELANpYiGFZELyyTM1CR0sdv5ReQAcJ/c/8pIrPab3lO11+D+EpuGLUxqz+y832g== +"@braintree/sanitize-url@^5.0.2": + version "5.0.2" + resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-5.0.2.tgz" + integrity sha512-NBEJlHWrhQucLhZGHtSxM2loSaNUMajC7KOYJLyfcdW/6goVoff2HoYI3bz8YCDN0wKGbxtUL0gx2dvHpvnWlw== "@colors/colors@1.5.0": version "1.5.0" @@ -3156,11 +3164,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@fastify/busboy@^2.0.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" - integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== - "@hapi/hoek@^9.0.0": version "9.2.1" resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz" @@ -3578,401 +3581,6 @@ "@svgr/plugin-jsx" "^6.2.1" "@svgr/plugin-svgo" "^6.2.0" -"@swagger-api/apidom-ast@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ast/-/apidom-ast-0.92.0.tgz#58faf1bbc88fa161cabe40fa16bd837f3570fd28" - integrity sha512-j9vuKaYZP3mAGXUcKeWIkSToxPPCBLJcLEfjSEh14P0n6NRJp7Yg19SA+IwHdIvOAfJonuebj/lhPOMjzd6P1g== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-error" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - unraw "^3.0.0" - -"@swagger-api/apidom-core@>=0.90.0 <1.0.0", "@swagger-api/apidom-core@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-core/-/apidom-core-0.92.0.tgz#44bb5d58f0a551ec7529617df10a23093eac2a06" - integrity sha512-PK1zlS0UCcE5dIPtSy8/+oWfXAVf7b/iM3LRaPgaFGF5b8qa6S/zmROTh10Yjug9v9Vnuq8opEhyHkGyl+WdSA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-ast" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@types/ramda" "~0.29.6" - minim "~0.23.8" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - short-unique-id "^5.0.2" - stampit "^4.3.2" - -"@swagger-api/apidom-error@>=0.90.0 <1.0.0", "@swagger-api/apidom-error@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-error/-/apidom-error-0.92.0.tgz#a5e93e98f689cf346b9d3d12ea31e20bb67376b1" - integrity sha512-wo7xCvTpWr5Lpt/ly1L4bhZ6W7grgtAg7SK/d8FNZR85zPJXM4FPMpcRtKktfWJ/RikQJT/g5DjI33iTqB6z/w== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - -"@swagger-api/apidom-json-pointer@>=0.90.0 <1.0.0", "@swagger-api/apidom-json-pointer@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.92.0.tgz#68188d7e1ae2988b9a8c0107e7201b9545a77764" - integrity sha512-VmZ1EXE7BWX+ndeeh9t1uFRql5jbPRmAcglUfdtu3jlg6fOqXzzgx9qFpRz9GhpMHWEGFm1ymd8tMAa1CvgcHw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-ns-api-design-systems@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-0.92.0.tgz#b99dc79b96a9b444e20ccb45aec49b0aa16e8996" - integrity sha512-wXEXhw0wDQIPTUqff953h44oQZr29DcoAzZfROWlGtOLItGDDMjhfIYiRg1406mXA4N7d5d0vNi9V/HXkxItQw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-1" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-asyncapi-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-0.92.0.tgz#872fc6ef5548bacb88b3c3550ac7350e12830331" - integrity sha512-FmJLT3GqzT4HK7Mwh54cXZ4PZt58yKVtJAKWKJ0dg2/Gim0AKJWf6t6B3Z9ZFUiKyehbqP4K7gSM7qGL0tKe2Q== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-json-schema-draft-7" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-json-schema-draft-4@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.92.0.tgz#11c6fede3eef005efe8b5c9d61001d24916b7633" - integrity sha512-7s2EKjCQwRXbK4Y4AGpVkyn1AANCxOUFSHebo1h2katyVeAopV0LJmbXH5yQedTltV0k3BIjnd7hS+7dI846Pw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-ast" "^0.92.0" - "@swagger-api/apidom-core" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-json-schema-draft-6@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-0.92.0.tgz#59c37e8064c72b5e88939d71b6abd6a8312a94c4" - integrity sha512-zur80x04jesXVzlU9sLZhW4giO9RfOouI7L/H8v2wUlcBvjaPBn1tIqrURw2VEHKAcJORhTRusQCR21vnFot2g== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@swagger-api/apidom-ns-json-schema-draft-4" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-json-schema-draft-7@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-0.92.0.tgz#7d833b1b8b968aa16c8313a8594ed8b309758d3f" - integrity sha512-DSY7lY98XHnc0wg0V38ZmBPs5HWuRuSb6G+n5Z+qs5RRodh1x5BrTIY6M0Yk3oJVbbEoFGmF0VlTe6vHf44pbw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@swagger-api/apidom-ns-json-schema-draft-6" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-openapi-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-0.92.0.tgz#b61cc851f21fc14fcc58fef15a0bea0ab57b87e6" - integrity sha512-OJlSTvPzK+zqzd2xXeWkF50z08Wlpygc98eVzZjYI0Af8mz7x6R5T9BCP5p6ZlQoO9OTvk4gfv7ViWXCdamObg== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@swagger-api/apidom-ns-json-schema-draft-4" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-openapi-3-0@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.92.0.tgz#f8e9a62cc06e758a7d3b8cc8b030c28f5457281a" - integrity sha512-VGha4RRnoeoAZBWLGy37YsBzwICM3ZFNyCk2Dwpaqfg9zFN+E6BL2CtIbkxvFkMdwaMURmDItiQsw28pF0tOgQ== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@swagger-api/apidom-ns-json-schema-draft-4" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-openapi-3-1@>=0.90.0 <1.0.0", "@swagger-api/apidom-ns-openapi-3-1@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.92.0.tgz#3a393c9c56672471233198079a1e15354629dfdd" - integrity sha512-xZD+JxifYhDoTjn76K2ZT3xNoXBQChaKfSkJr4l5Xh9Guuk0IcsPTUDRpuytuZZXVez0O401XFoUso/mZRTjkA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-ast" "^0.92.0" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-0" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-ns-workflows-1@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-workflows-1/-/apidom-ns-workflows-1-0.92.0.tgz#8ee3d51bd0014fcf8dfc0b58ba03d97b8427b9c3" - integrity sha512-gl1dF+SrRHK4lLiwaK4PMjL9A5z28cW9xiMWCxRyppX/I2bVTVVOfgdAyqLWsFA0gopmITWesJxohRumG35fTw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-1" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - -"@swagger-api/apidom-parser-adapter-api-design-systems-json@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-0.92.0.tgz#9437cae4c06dc8933345830ff1d055eaea314cda" - integrity sha512-i07FeLdNobWzHT9LnfsdOix+XrlZN/KnQL1RODPzxWk7i7ya2e4uc3JemyHh4Tnv04G8JV32SQqtzOtMteJsdA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-api-design-systems" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-api-design-systems-yaml@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-0.92.0.tgz#72d97d3cef6d30c30a7de61dbdd67321b1913d59" - integrity sha512-bbjFkU0D4zqaZnd8/m1Kyx2UuHpri8ZxLdT1TiXqHweSfRQcNt4VYt0bjWBnnGGBMkHElgYbX5ov6kHvPf3wJg== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-api-design-systems" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-asyncapi-json-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-0.92.0.tgz#4996bc69e9f7e17a9f4d1b8316429d5373043918" - integrity sha512-Q7gudmGA5TUGbbr0QYNQkndktP91C0WE7uDDS2IwCBtHroRDiMPFCjzE9dsjIST5WnP+LUXmxG1Bv0NLTWcSUg== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-asyncapi-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-0.92.0.tgz#29eef57f3632570cdd4d8e8ba0b22fc8e046c44c" - integrity sha512-V5/VdDj0aeOKp+3AtvPSz2b0HosJfYkHPjNvPU5eafLSzqzMIR/evYq5BvKWoJL1IvLdjoEPqDVVaEZluHZTew== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-asyncapi-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-json@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-0.92.0.tgz#52af0254d3c27f601d6bee5af7f6e78c2b15f939" - integrity sha512-KA1Nn6FN0zTA5JhRazwYN9voTDlmExID7Jwz6GXmY826OXqeT4Yl0Egyo1aLYrfT0S73vhC4LVqpdORWLGdZtg== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-ast" "^0.92.0" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - tree-sitter "=0.20.4" - tree-sitter-json "=0.20.1" - web-tree-sitter "=0.20.3" - -"@swagger-api/apidom-parser-adapter-openapi-json-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-0.92.0.tgz#ccaac1c6129f284aad40d6ff75a2559cbd3bcd73" - integrity sha512-8OlvjcvI/GuOFJJxN+Mc4tJSo9UWuJdzQtQOtO4k3QwWwS28hGvRTjQ5PpsXAVZoLJMAbDuRdREYD9qeIKvM2g== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-openapi-json-3-0@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-0.92.0.tgz#7115ba14ee1fd303ae0d40f6ee813c14bcd69819" - integrity sha512-kzE4COaNobKIUjGsdqqXgO/LruaQHs2kTzOzHPUTR1TH1ZlB2t8MTV+6LJzGNG3IB3QSfZDd7KBEYWklsCTyTA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-0" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-openapi-json-3-1@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-0.92.0.tgz#f72ef7ddd55bd692d856e056795ee338ce0769e4" - integrity sha512-4gkIXfKGwEKZQ6+kxp4EdFBlAc7Kjq8GAgaC7ilGTSSxIaz5hBHBOJoe3cXWpQ/WlXiOyNCy7WdbuKRpUDKIdg== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-openapi-yaml-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-0.92.0.tgz#66449fcfc680ccd59c9d1023f407529b27f7e247" - integrity sha512-TIY9cytYhA3yUf+5PcwsH9UjzKy5V4nGUtK6n5RvcL4btaGQA2LUB5CiV/1nSvYLNjYjGxhtB3haZDbHe3/gyw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-0.92.0.tgz#233ea2e347943c98cb0961ed025f16f6fe8f40f4" - integrity sha512-AUwtAxeautYtiwifNCmv6Kjs7ksptRFxcQ3sgLv2bP3f9t5jzcI9NhmgJNdbRfohHYaHMwTuUESrfsTdBgKlAA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-0" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-0.92.0.tgz#96a4a4f3baeaf2349043ba8bb2b4b5a22717a26b" - integrity sha512-gMR4zUZ/RrjVJVr6DnqwsCsnlplGXJk6O9UKbkoBsiom81dkcHx68BmWA2oM2lYVGKx+G8WVmVDo2EJaZvZYGg== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-workflows-json-1@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-workflows-json-1/-/apidom-parser-adapter-workflows-json-1-0.92.0.tgz#e31454a5b5a6ec38281a8aa4aeaf430b8fd61db7" - integrity sha512-tyLiSxEKeU6mhClFjNxrTQJA2aSgfEF7LJ/ZcJgvREsvyk6ns3op9wN2SXw4UmD+657IgN0aUPihh92aEXKovA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-workflows-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-workflows-yaml-1@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-workflows-yaml-1/-/apidom-parser-adapter-workflows-yaml-1-0.92.0.tgz#c4bde8ebf67f1a7bde3998145830dfa6b81ce245" - integrity sha512-0Nr+5oAocuw3SZXcO8WEqnU7GGWP7O6GrsFafD6KLBL05v3I0erPfmnWQjWh6jBeXv8r5W69WEQItzES0DBJjA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-ns-workflows-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.0.0" - -"@swagger-api/apidom-parser-adapter-yaml-1-2@^0.92.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-0.92.0.tgz#52cf595aa07289a4eadd1850e58d796e0ff0c59e" - integrity sha512-cFLqlhehMuY5WRdU1780Vno6iWpjMlr7CfOOloZW1rKf2lvojn0c4eDsyfWFaB2DgE+Xd4CWl55McuaPZMngsw== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-ast" "^0.92.0" - "@swagger-api/apidom-core" "^0.92.0" - "@swagger-api/apidom-error" "^0.92.0" - "@types/ramda" "~0.29.6" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - tree-sitter "=0.20.4" - tree-sitter-yaml "=0.5.0" - web-tree-sitter "=0.20.3" - -"@swagger-api/apidom-reference@>=0.90.0 <1.0.0": - version "0.92.0" - resolved "https://registry.yarnpkg.com/@swagger-api/apidom-reference/-/apidom-reference-0.92.0.tgz#11054a13e438bf15736200c6826cae845c12e465" - integrity sha512-G/qJBTpXCdwPsc5dqPjX+vAfhvtnhIFqnKtEZ71wnEvF7TpIxdeZKKfqpg+Zxi7MSuZD/Gpkr4J/eP0lO0fAdA== - dependencies: - "@babel/runtime-corejs3" "^7.20.7" - "@swagger-api/apidom-core" "^0.92.0" - "@types/ramda" "~0.29.6" - axios "^1.4.0" - minimatch "^7.4.3" - process "^0.11.10" - ramda "~0.29.1" - ramda-adjunct "^4.1.1" - stampit "^4.3.2" - optionalDependencies: - "@swagger-api/apidom-error" "^0.92.0" - "@swagger-api/apidom-json-pointer" "^0.92.0" - "@swagger-api/apidom-ns-asyncapi-2" "^0.92.0" - "@swagger-api/apidom-ns-openapi-2" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-0" "^0.92.0" - "@swagger-api/apidom-ns-openapi-3-1" "^0.92.0" - "@swagger-api/apidom-ns-workflows-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-api-design-systems-json" "^0.92.0" - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml" "^0.92.0" - "@swagger-api/apidom-parser-adapter-asyncapi-json-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-json" "^0.92.0" - "@swagger-api/apidom-parser-adapter-openapi-json-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-openapi-json-3-0" "^0.92.0" - "@swagger-api/apidom-parser-adapter-openapi-json-3-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-openapi-yaml-2" "^0.92.0" - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0" "^0.92.0" - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-workflows-json-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-workflows-yaml-1" "^0.92.0" - "@swagger-api/apidom-parser-adapter-yaml-1-2" "^0.92.0" - "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz" @@ -4077,6 +3685,14 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^6.0.0": version "6.0.0" resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.0.0.tgz" @@ -4170,18 +3786,21 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== -"@types/ramda@~0.29.6": - version "0.29.9" - resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.29.9.tgz#a4c1a9d056249268ffe16c2f6d198e42c2fc994a" - integrity sha512-X3yEG6tQCWBcUAql+RPC/O1Hm9BSU+MXu2wJnCETuAgUlrEDwTA1kIOdEEE4YXDtf0zfQLHa9CCE7WYp9kqPIQ== - dependencies: - types-ramda "^0.29.6" - "@types/range-parser@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/react-redux@^7.1.20": + version "7.1.20" + resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.20.tgz" + integrity sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-router-config@*": version "5.0.3" resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.3.tgz" @@ -4287,11 +3906,6 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== -"@types/use-sync-external-store@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" - integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== - "@types/ws@^8.5.1": version "8.5.4" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" @@ -4442,11 +4056,6 @@ resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -"@yarnpkg/lockfile@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -4714,11 +4323,6 @@ async-validator@^4.0.2: resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-4.0.7.tgz#034a0fd2103a6b2ebf010da75183bec299247afe" integrity sha512-Pj2IR7u8hmUEDOwB++su6baaRi+QvsgajuFB9j95foM1N2gy5HM4z60hfusIO0fBPG5uLAEl6yCJr1jNSVugEQ== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" @@ -4750,15 +4354,6 @@ axios@^0.25.0: dependencies: follow-redirects "^1.14.7" -axios@^1.4.0: - version "1.6.5" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8" - integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg== - dependencies: - follow-redirects "^1.15.4" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - babel-loader@^8.2.5: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" @@ -4904,15 +4499,6 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -4982,13 +4568,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -5006,19 +4585,16 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4 node-releases "^2.0.8" update-browserslist-db "^1.0.10" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -5058,15 +4634,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -call-bind@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" - integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== - dependencies: - function-bind "^1.1.2" - get-intrinsic "^1.2.1" - set-function-length "^1.1.1" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -5202,11 +4769,6 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" @@ -5222,15 +4784,10 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== -ci-info@^3.7.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== +classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== clean-css@^5.1.5: version "5.2.2" @@ -5356,13 +4913,6 @@ combine-promises@^1.1.0: resolved "https://registry.npmjs.org/combine-promises/-/combine-promises-1.1.0.tgz" integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg== -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - comma-separated-tokens@^1.0.0: version "1.0.8" resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz" @@ -5479,10 +5029,10 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookie@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@~0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== copy-anything@^2.0.1: version "2.0.6" @@ -5496,20 +5046,13 @@ copy-text-to-clipboard@^3.0.1: resolved "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz" integrity sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q== -copy-to-clipboard@^3.2.0: +copy-to-clipboard@^3, copy-to-clipboard@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== dependencies: toggle-selection "^1.0.6" -copy-to-clipboard@^3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" - integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== - dependencies: - toggle-selection "^1.0.6" - copy-webpack-plugin@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" @@ -5537,10 +5080,15 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.5" -core-js-pure@^3.30.2: - version "3.35.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.35.0.tgz#4660033304a050215ae82e476bd2513a419fbb34" - integrity sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew== +core-js-pure@^3.20.2: + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51" + integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== + +core-js-pure@^3.25.1: + version "3.29.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.29.1.tgz#1be6ca2b8772f6b4df7fc4621743286e676c6162" + integrity sha512-4En6zYVi0i0XlXHVz/bi6l1XDjCqkKRq765NXuX+SnaIatlE96Odt5lMLjdxUiNI1v9OXI5DSLWYPlmTfkTktg== core-js@^3.23.3: version "3.29.1" @@ -5574,7 +5122,7 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" -cross-fetch@^3.0.4: +cross-fetch@^3.0.4, cross-fetch@^3.1.4: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== @@ -5782,6 +5330,14 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz" integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + date-fns@2.x: version "2.28.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" @@ -5820,14 +5376,7 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@0.6.0, deep-extend@^0.6.0: +deep-extend@0.6.0, deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -5837,11 +5386,6 @@ deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -deepmerge@~4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - default-gateway@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" @@ -5854,15 +5398,6 @@ defer-to-connect@^1.0.1: resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== -define-data-property@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" - integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== - dependencies: - get-intrinsic "^1.2.1" - gopd "^1.0.1" - has-property-descriptors "^1.0.0" - define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" @@ -5889,11 +5424,6 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -5916,11 +5446,6 @@ detab@2.0.4: dependencies: repeat-string "^1.5.4" -detect-libc@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" - integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== - detect-node@^2.0.4: version "2.1.0" resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" @@ -6033,10 +5558,10 @@ domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@=3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae" - integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w== +dompurify@=2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz" + integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== domutils@^1.7.0: version "1.7.0" @@ -6079,11 +5604,6 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" -drange@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8" - integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA== - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" @@ -6148,7 +5668,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -6232,6 +5752,42 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.53" + resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.3" + next-tick "~1.0.0" + +es6-iterator@^2.0.3, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" @@ -6310,6 +5866,14 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -6335,11 +5899,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - express@^4.17.3: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -6377,6 +5936,13 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.6.0" + resolved "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz" + integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== + dependencies: + type "^2.5.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" @@ -6567,13 +6133,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-yarn-workspace-root@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" - integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== - dependencies: - micromatch "^4.0.2" - flux@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/flux/-/flux-4.0.2.tgz" @@ -6587,11 +6146,6 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.7: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== -follow-redirects@^1.15.4: - version "1.15.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" - integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== - fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e" @@ -6611,20 +6165,24 @@ fork-ts-checker-webpack-plugin@^6.5.0: semver "^7.3.2" tapable "^1.0.0" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" +form-data-encoder@^1.4.3: + version "1.7.1" + resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz" + integrity sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg== format@^0.2.0: version "0.2.2" resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formdata-node@^4.0.0: + version "4.3.1" + resolved "https://registry.npmjs.org/formdata-node/-/formdata-node-4.3.1.tgz" + integrity sha512-8xKSa9et4zb+yziWsD/bI+EYjdg1z2p9EpKr+o+Yk12F/wP66bmDdvjj2ZXd2K/MJlR3HBzWnuV7f82jzHRqCA== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.1" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6640,11 +6198,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - fs-extra@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -6684,11 +6237,6 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -6712,16 +6260,6 @@ get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" - integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== - dependencies: - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" @@ -6759,11 +6297,6 @@ github-buttons@^2.8.0: resolved "https://registry.yarnpkg.com/github-buttons/-/github-buttons-2.21.1.tgz#9e55eb83b70c9149a21c235db2e971c53d4d98a2" integrity sha512-n9bCQ8sj+5oX1YH5NeyWGbAclRDtHEhMBzqw2ctsWpdEHOwVgfruRu0VIVy01Ah10dd/iFajMHYU71L7IBWBOw== -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== - github-slugger@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-1.4.0.tgz" @@ -6863,13 +6396,6 @@ globby@^13.1.1: merge2 "^1.4.1" slash "^4.0.0" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" @@ -6887,11 +6413,6 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.11: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" @@ -6934,18 +6455,6 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" - integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== - dependencies: - get-intrinsic "^1.2.2" - -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" @@ -6975,13 +6484,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== - dependencies: - function-bind "^1.1.2" - hast-to-hyperscript@^9.0.0: version "9.0.1" resolved "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz" @@ -7072,7 +6574,7 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7241,7 +6743,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7314,7 +6816,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7458,6 +6960,14 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-dom@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-dom/-/is-dom-1.1.0.tgz" + integrity sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ== + dependencies: + is-object "^1.0.1" + is-window "^1.0.2" + is-extendable@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" @@ -7525,6 +7035,11 @@ is-obj@^2.0.0: resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-object@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz" + integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== + is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz" @@ -7552,10 +7067,10 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== is-regex@^1.1.4: version "1.1.4" @@ -7621,12 +7136,17 @@ is-whitespace-character@^1.0.0: resolved "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz" integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== +is-window@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz" + integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= + is-word-character@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz" integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== -is-wsl@^2.1.1, is-wsl@^2.2.0: +is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -7643,11 +7163,6 @@ isarray@0.0.1: resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -7760,16 +7275,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-stable-stringify@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454" - integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg== - dependencies: - call-bind "^1.0.5" - isarray "^2.0.5" - jsonify "^0.0.1" - object-keys "^1.1.1" - json2mq@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" @@ -7796,11 +7301,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" - integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== - keyv@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" @@ -7813,13 +7313,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -klaw-sync@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" - integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== - dependencies: - graceful-fs "^4.1.11" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" @@ -7942,7 +7435,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.15.0, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7993,6 +7486,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz" + integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= + dependencies: + es5-ext "~0.10.2" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -8085,6 +7585,20 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== +memoizee@^0.4.15: + version "0.4.15" + resolved "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz" + integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== + dependencies: + d "^1.0.1" + es5-ext "^0.10.53" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -8143,7 +7657,7 @@ mime-types@2.1.18: dependencies: mime-db "~1.33.0" -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -8170,11 +7684,6 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - mini-css-extract-plugin@^2.6.1: version "2.7.3" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.3.tgz#794aa4d598bf178a66b2a35fe287c3df3eac394e" @@ -8182,13 +7691,6 @@ mini-css-extract-plugin@^2.6.1: dependencies: schema-utils "^4.0.0" -minim@~0.23.8: - version "0.23.8" - resolved "https://registry.yarnpkg.com/minim/-/minim-0.23.8.tgz#a529837afe1654f119dfb68ce7487dd8d4866b9c" - integrity sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww== - dependencies: - lodash "^4.15.0" - minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" @@ -8208,28 +7710,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@^7.4.3: - version "7.4.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" - integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -minimist@^1.2.3, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -8265,21 +7750,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" -nan@^2.14.0, nan@^2.17.0, nan@^2.18.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== - nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - needle@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/needle/-/needle-3.2.0.tgz#07d240ebcabfd65c76c03afae7f6defe6469df44" @@ -8299,6 +7774,16 @@ neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@1, next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +next-tick@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + no-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" @@ -8307,21 +7792,9 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-abi@^3.3.0: - version "3.54.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.54.0.tgz#f6386f7548817acac6434c6cba02999c9aebcc69" - integrity sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA== - dependencies: - semver "^7.3.5" - -node-abort-controller@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" - integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== - -node-domexception@^1.0.0: +node-domexception@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== node-emoji@^1.10.0: @@ -8331,14 +7804,6 @@ node-emoji@^1.10.0: dependencies: lodash "^4.17.21" -node-fetch-commonjs@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch-commonjs/-/node-fetch-commonjs-3.3.2.tgz#0dd0fd4c4a314c5234f496ff7b5d9ce5a6c8feaa" - integrity sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -8488,14 +7953,6 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^7.4.2: - version "7.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" - integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== - dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" - open@^8.0.9, open@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" @@ -8510,11 +7967,6 @@ opener@^1.5.2: resolved "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" @@ -8665,27 +8117,6 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" -patch-package@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" - integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== - dependencies: - "@yarnpkg/lockfile" "^1.1.0" - chalk "^4.1.2" - ci-info "^3.7.0" - cross-spawn "^7.0.3" - find-yarn-workspace-root "^2.0.0" - fs-extra "^9.0.0" - json-stable-stringify "^1.0.2" - klaw-sync "^6.0.0" - minimist "^1.2.6" - open "^7.4.2" - rimraf "^2.6.3" - semver "^7.5.3" - slash "^2.0.0" - tmp "^0.0.33" - yaml "^2.2.2" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" @@ -9075,24 +8506,6 @@ postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" -prebuild-install@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== - dependencies: - detect-libc "^2.0.0" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^3.3.0" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^4.0.0" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" @@ -9121,26 +8534,16 @@ prism-react-renderer@^1.3.5: resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085" integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg== -prismjs@^1.27.0, prismjs@^1.28.0: +prismjs@^1.28.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== -prismjs@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" - integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - promise@^7.1.1: version "7.3.1" resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" @@ -9156,14 +8559,14 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== +prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== dependencies: loose-envify "^1.4.0" object-assign "^4.1.1" - react-is "^16.13.1" + react-is "^16.8.1" property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" @@ -9180,11 +8583,6 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -9198,6 +8596,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + punycode@^1.3.2: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" @@ -9225,19 +8628,17 @@ q@^1.1.2: resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.11.0: +qs@6.11.0, qs@^6.9.4: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" -qs@^6.10.2: - version "6.11.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" - integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== - dependencies: - side-channel "^1.0.4" +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= querystringify@^2.1.1: version "2.2.0" @@ -9256,24 +8657,6 @@ queue@6.0.2: dependencies: inherits "~2.0.3" -ramda-adjunct@^4.0.0, ramda-adjunct@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ramda-adjunct/-/ramda-adjunct-4.1.1.tgz#085ca9a7bf19857378eff648f9852b15136dc66f" - integrity sha512-BnCGsZybQZMDGram9y7RiryoRHS5uwx8YeGuUeDKuZuvK38XO6JJfmK85BwRWAKFA6pZ5nZBO/HBFtExVaf31w== - -ramda@~0.29.1: - version "0.29.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.29.1.tgz#408a6165b9555b7ba2fc62555804b6c5a2eca196" - integrity sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA== - -randexp@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738" - integrity sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w== - dependencies: - drange "^1.0.2" - ret "^0.2.0" - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -9689,7 +9072,7 @@ rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.1: rc-resize-observer "^1.0.0" rc-util "^5.0.7" -rc@^1.2.7, rc@^1.2.8: +rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -9709,21 +9092,21 @@ react-base16-styling@^0.6.0: lodash.flow "^3.3.0" pure-color "^1.2.0" -react-copy-to-clipboard@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" - integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A== +react-copy-to-clipboard@5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz" + integrity sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ== dependencies: - copy-to-clipboard "^3.3.1" - prop-types "^15.8.1" + copy-to-clipboard "^3" + prop-types "^15.5.8" -react-debounce-input@=3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/react-debounce-input/-/react-debounce-input-3.3.0.tgz#85e3ebcaa41f2016e50613134a1ec9fe3cdb422e" - integrity sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA== +react-debounce-input@=3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.2.4.tgz" + integrity sha512-fX70bNj0fLEYO2Zcvuh7eh9wOUQ29GIx6r8IxIJlc0i0mpUH++9ax0BhfAYfzndADli3RAMROrZQ014J01owrg== dependencies: lodash.debounce "^4" - prop-types "^15.8.1" + prop-types "^15.7.2" react-dev-utils@^12.0.1: version "12.0.1" @@ -9815,16 +9198,25 @@ react-immutable-pure-component@^2.2.0: resolved "https://registry.npmjs.org/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz" integrity sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A== -react-inspector@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d" - integrity sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ== +react-inspector@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz" + integrity sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg== + dependencies: + "@babel/runtime" "^7.0.0" + is-dom "^1.0.0" + prop-types "^15.0.0" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-json-view@^1.21.3: version "1.21.3" resolved "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz" @@ -9847,13 +9239,17 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" -react-redux@^9.0.4: - version "9.1.0" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.0.tgz#46a46d4cfed4e534ce5452bb39ba18e1d98a8197" - integrity sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ== +react-redux@^7.2.4: + version "7.2.6" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz" + integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== dependencies: - "@types/use-sync-external-store" "^0.0.3" - use-sync-external-store "^1.0.0" + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" react-router-config@^5.1.1: version "5.1.1" @@ -9890,16 +9286,16 @@ react-router@5.3.4, react-router@^5.3.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-syntax-highlighter@^15.5.0: - version "15.5.0" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" - integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg== +react-syntax-highlighter@^15.4.5: + version "15.4.5" + resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.4.5.tgz" + integrity sha512-RC90KQTxZ/b7+9iE6s9nmiFLFjWswUcfULi4GwVzdFVKVMQySkJWBuOmJFfjwjMVCo0IUUuJrWebNKyviKpwLQ== dependencies: "@babel/runtime" "^7.3.1" highlight.js "^10.4.1" lowlight "^1.17.0" - prismjs "^1.27.0" - refractor "^3.6.0" + prismjs "^1.28.0" + refractor "^3.2.0" react-textarea-autosize@^8.3.2: version "8.3.3" @@ -9940,15 +9336,6 @@ readable-stream@^3.0.6: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -9980,19 +9367,21 @@ redux-immutable@^4.0.0: resolved "https://registry.npmjs.org/redux-immutable/-/redux-immutable-4.0.0.tgz" integrity sha1-Ohoy32Y2ZGK2NpHw4dw15HK7yfM= -redux@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" - integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== +redux@^4.0.0, redux@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz" + integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + dependencies: + "@babel/runtime" "^7.9.2" -refractor@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" - integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== +refractor@^3.2.0: + version "3.5.0" + resolved "https://registry.npmjs.org/refractor/-/refractor-3.5.0.tgz" + integrity sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg== dependencies: hastscript "^6.0.0" parse-entities "^2.0.0" - prismjs "~1.27.0" + prismjs "~1.28.0" regenerate-unicode-properties@^10.1.0: version "10.1.0" @@ -10023,11 +9412,6 @@ regenerator-runtime@^0.13.4: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz" @@ -10200,10 +9584,10 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.0.tgz#c479139ab9dd91be4d9c764a7f3868210ef8cd21" - integrity sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg== +reselect@^4.0.0: + version "4.1.5" + resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz" + integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: version "1.5.1" @@ -10244,11 +9628,6 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" -ret@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" - integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== - retry@^0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" @@ -10259,13 +9638,6 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -10416,7 +9788,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -10500,17 +9872,6 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" -set-function-length@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" - integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== - dependencies: - define-data-property "^1.1.1" - function-bind "^1.1.2" - get-intrinsic "^1.2.2" - gopd "^1.0.1" - has-property-descriptors "^1.0.1" - setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" @@ -10572,11 +9933,6 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -short-unique-id@^5.0.2: - version "5.0.3" - resolved "https://registry.yarnpkg.com/short-unique-id/-/short-unique-id-5.0.3.tgz#bc6975dc5e8b296960ff5ac91ddabbc7ddb693d9" - integrity sha512-yhniEILouC0s4lpH0h7rJsfylZdca10W9mDJRAFh3EpcSUanCHGb0R7kcFOIUCZYSAPo0PUD5ZxWQdW0T4xaug== - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -10591,20 +9947,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz" integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" - integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== - dependencies: - decompress-response "^6.0.0" - once "^1.3.1" - simple-concat "^1.0.0" - sirv@^1.0.7: version "1.0.18" resolved "https://registry.npmjs.org/sirv/-/sirv-1.0.18.tgz" @@ -10629,11 +9971,6 @@ sitemap@^7.1.1: arg "^5.0.0" sax "^1.2.4" -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -10719,11 +10056,6 @@ stable@^0.1.8: resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stampit@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stampit/-/stampit-4.3.2.tgz#cfd3f607dd628a161ce6305621597994b4d56573" - integrity sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA== - state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz" @@ -10925,64 +10257,60 @@ svgo@^2.5.0, svgo@^2.7.0: picocolors "^1.0.0" stable "^0.1.8" -swagger-client@^3.25.0: - version "3.25.0" - resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.25.0.tgz#c59b181bed7172475d275487e6ab8365bd3f06ec" - integrity sha512-p143zWkIhgyh2E5+3HPFMlCw3WkV9RbX9HyftfBdiccCbOlmHdcJC0XEJZxcm+ZA+80DORs0F30/mzk7sx4iwA== - dependencies: - "@babel/runtime-corejs3" "^7.22.15" - "@swagger-api/apidom-core" ">=0.90.0 <1.0.0" - "@swagger-api/apidom-error" ">=0.90.0 <1.0.0" - "@swagger-api/apidom-json-pointer" ">=0.90.0 <1.0.0" - "@swagger-api/apidom-ns-openapi-3-1" ">=0.90.0 <1.0.0" - "@swagger-api/apidom-reference" ">=0.90.0 <1.0.0" - cookie "~0.6.0" - deepmerge "~4.3.0" +swagger-client@^3.17.0: + version "3.17.0" + resolved "https://registry.npmjs.org/swagger-client/-/swagger-client-3.17.0.tgz" + integrity sha512-d8DOEME49wTXm+uT+lBAjJ5D6IDjEHdbkqa7MbcslR2c+oHIhi13ObwleVWGfr89MPkWgBl6RBq9VUHmrBJRbg== + dependencies: + "@babel/runtime-corejs3" "^7.11.2" + btoa "^1.2.1" + cookie "~0.4.1" + cross-fetch "^3.1.4" + deep-extend "~0.6.0" fast-json-patch "^3.0.0-1" - is-plain-object "^5.0.0" + form-data-encoder "^1.4.3" + formdata-node "^4.0.0" js-yaml "^4.1.0" - node-abort-controller "^3.1.1" - node-fetch-commonjs "^3.3.1" - qs "^6.10.2" + lodash "^4.17.21" + qs "^6.9.4" traverse "~0.6.6" - undici "^5.24.0" + url "~0.11.0" -swagger-ui-react@^5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.11.0.tgz#ed50f61ce7aa2c322b3a2435f2aa38396668431d" - integrity sha512-iqc5/Z8nvqOdjU2LuWYbREnDmKj5gndZSESTH9dXfymlzLc2NoPQmXZAw02U8kFgHyciX0yDMp3oaCw1zBdPSA== +swagger-ui-react@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-4.1.3.tgz#a722ecbe54ef237fa9080447a7c708c4c72d846a" + integrity sha512-o1AoXUTNH40cxWus0QOeWQ8x9tSIEmrLBrOgAOHDnvWJ1qyjT8PjgHjPbUVjMbja18coyuaAAeUdyLKvLGmlDA== dependencies: - "@babel/runtime-corejs3" "^7.23.7" - "@braintree/sanitize-url" "=7.0.0" + "@babel/runtime-corejs3" "^7.16.3" + "@braintree/sanitize-url" "^5.0.2" base64-js "^1.5.1" - classnames "^2.5.1" + classnames "^2.3.1" css.escape "1.5.1" deep-extend "0.6.0" - dompurify "=3.0.6" + dompurify "=2.3.3" ieee754 "^1.2.1" immutable "^3.x.x" js-file-download "^0.4.12" js-yaml "=4.1.0" lodash "^4.17.21" - patch-package "^8.0.0" - prop-types "^15.8.1" - randexp "^0.5.3" + memoizee "^0.4.15" + prop-types "^15.7.2" randombytes "^2.1.0" - react-copy-to-clipboard "5.1.0" - react-debounce-input "=3.3.0" + react-copy-to-clipboard "5.0.4" + react-debounce-input "=3.2.4" react-immutable-proptypes "2.2.0" react-immutable-pure-component "^2.2.0" - react-inspector "^6.0.1" - react-redux "^9.0.4" - react-syntax-highlighter "^15.5.0" - redux "^5.0.0" + react-inspector "^5.1.1" + react-redux "^7.2.4" + react-syntax-highlighter "^15.4.5" + redux "^4.1.2" redux-immutable "^4.0.0" remarkable "^2.0.1" - reselect "^5.0.1" + reselect "^4.0.0" serialize-error "^8.1.0" sha.js "^2.4.11" - swagger-client "^3.25.0" - url-parse "^1.5.10" + swagger-client "^3.17.0" + url-parse "^1.5.3" xml "=1.0.1" xml-but-prettier "^1.0.1" zenscroll "^4.0.2" @@ -10997,27 +10325,6 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.3: version "5.3.7" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" @@ -11059,6 +10366,14 @@ thunky@^1.0.2: resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +timers-ext@^0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz" + integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + dependencies: + es5-ext "~0.10.46" + next-tick "1" + tiny-invariant@^1.0.2: version "1.2.0" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz" @@ -11069,13 +10384,6 @@ tiny-warning@^1.0.0: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" @@ -11118,28 +10426,6 @@ traverse@~0.6.6: resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= -tree-sitter-json@=0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/tree-sitter-json/-/tree-sitter-json-0.20.1.tgz#d1fe6c59571dd3a987ebb3f5aeef404f37b3a453" - integrity sha512-482hf7J+aBwhksSw8yWaqI8nyP1DrSwnS4IMBShsnkFWD3SE8oalHnsEik59fEVi3orcTCUtMzSjZx+0Tpa6Vw== - dependencies: - nan "^2.18.0" - -tree-sitter-yaml@=0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/tree-sitter-yaml/-/tree-sitter-yaml-0.5.0.tgz#c617ba72837399d8105ec10cdb4c360e1ed76076" - integrity sha512-POJ4ZNXXSWIG/W4Rjuyg36MkUD4d769YRUGKRqN+sVaj/VCo6Dh6Pkssn1Rtewd5kybx+jT1BWMyWN0CijXnMA== - dependencies: - nan "^2.14.0" - -tree-sitter@=0.20.4: - version "0.20.4" - resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.4.tgz#7d9d4f769fc05342ef43e5559f7ff34b0fc48327" - integrity sha512-rjfR5dc4knG3jnJNN/giJ9WOoN1zL/kZyrS0ILh+eqq8RNcIbiXA63JsMEgluug0aNvfQvK4BfCErN1vIzvKog== - dependencies: - nan "^2.17.0" - prebuild-install "^7.1.1" - trim-trailing-lines@^1.0.0: version "1.1.4" resolved "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz" @@ -11160,11 +10446,6 @@ ts-essentials@^2.0.3: resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz" integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w== -ts-toolbelt@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5" - integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w== - tslib@^1.9.3: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" @@ -11175,13 +10456,6 @@ tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" @@ -11200,6 +10474,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.npmjs.org/type/-/type-1.2.0.tgz" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/type/-/type-2.5.0.tgz" + integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" @@ -11207,13 +10491,6 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -types-ramda@^0.29.6: - version "0.29.6" - resolved "https://registry.yarnpkg.com/types-ramda/-/types-ramda-0.29.6.tgz#a1d2a3c15a48e27d35832d7194d93369975f1427" - integrity sha512-VJoOk1uYNh9ZguGd3eZvqkdhD4hTGtnjRBUx5Zc0U9ftmnCgiWcSj/lsahzKunbiwRje1MxxNkEy1UdcXRCpYw== - dependencies: - ts-toolbelt "^9.6.0" - typescript@^5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" @@ -11234,13 +10511,6 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" -undici@^5.24.0: - version "5.28.2" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91" - integrity sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w== - dependencies: - "@fastify/busboy" "^2.0.0" - unherit@^1.0.4: version "1.1.3" resolved "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz" @@ -11381,11 +10651,6 @@ unquote@~1.1.1: resolved "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz" integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= -unraw@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unraw/-/unraw-3.0.0.tgz#73443ed70d2ab09ccbac2b00525602d5991fbbe3" - integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg== - update-browserslist-db@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -11437,7 +10702,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.5.10: +url-parse@^1.5.3: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -11445,6 +10710,14 @@ url-parse@^1.5.10: querystringify "^2.1.1" requires-port "^1.0.0" +url@~0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + use-composed-ref@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz" @@ -11464,7 +10737,7 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" -use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: +use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -11568,15 +10841,10 @@ web-namespaces@^1.0.0: resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== -web-streams-polyfill@^3.0.3: - version "3.3.2" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz#32e26522e05128203a7de59519be3c648004343b" - integrity sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ== - -web-tree-sitter@=0.20.3: - version "0.20.3" - resolved "https://registry.yarnpkg.com/web-tree-sitter/-/web-tree-sitter-0.20.3.tgz#3dd17b283ad63b1d8c07c5ea814f0fefb2b1f776" - integrity sha512-zKGJW9r23y3BcJusbgvnOH2OYAW40MXAOi9bi3Gcc7T4Gms9WWgXF8m6adsJWpGJEhgOzCrfiz1IzKowJWrtYw== +web-streams-polyfill@4.0.0-beta.1: + version "4.0.0-beta.1" + resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz" + integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ== webidl-conversions@^3.0.0: version "3.0.1" @@ -11880,11 +11148,6 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.2.2: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" From f63e66be01cd0b0aee3ac3245ec677e2e5d93c78 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:21:14 -0300 Subject: [PATCH 05/11] refactor: Removes the deprecated VERSIONED_EXPORT feature flag (#26347) --- RESOURCES/FEATURE_FLAGS.md | 1 - UPDATING.md | 1 + .../src/utils/featureFlags.ts | 1 - .../src/features/charts/ChartCard.tsx | 3 +- .../src/pages/ChartList/index.tsx | 31 +- .../src/pages/DashboardList/index.tsx | 31 +- .../src/pages/DatabaseList/index.tsx | 5 +- .../src/pages/DatasetList/index.tsx | 33 +- .../src/pages/SavedQueryList/index.tsx | 5 +- superset/cli/importexport.py | 661 +++++++++--------- superset/config.py | 1 - superset/dashboards/api.py | 67 +- superset/datasets/api.py | 69 +- superset/initialization/__init__.py | 15 - superset/views/core.py | 51 -- tests/integration_tests/cli_tests.py | 195 ------ .../integration_tests/dashboards/api_tests.py | 8 +- tests/integration_tests/security_tests.py | 1 - 18 files changed, 427 insertions(+), 752 deletions(-) diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index 6ce8c1c1ee2dc..ef05ab98db5c5 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -88,4 +88,3 @@ These features flags currently default to True and **will be removed in a future - ENABLE_JAVASCRIPT_CONTROLS - GENERIC_CHART_AXES - KV_STORE -- VERSIONED_EXPORT diff --git a/UPDATING.md b/UPDATING.md index b239866221e36..f1f2566736817 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -28,6 +28,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [26347](https://github.com/apache/superset/issues/26347): Removes the deprecated `VERSIONED_EXPORT` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled. - [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. diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 6d5935f04eac6..ba9d0d6680fd8 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -57,7 +57,6 @@ export enum FeatureFlag { TAGGING_SYSTEM = 'TAGGING_SYSTEM', THUMBNAILS = 'THUMBNAILS', USE_ANALAGOUS_COLORS = 'USE_ANALAGOUS_COLORS', - VERSIONED_EXPORT = 'VERSIONED_EXPORT', } export type ScheduleQueriesProps = { JSONSCHEMA: { diff --git a/superset-frontend/src/features/charts/ChartCard.tsx b/superset-frontend/src/features/charts/ChartCard.tsx index 352f34d3590e8..38a46eb89873e 100644 --- a/superset-frontend/src/features/charts/ChartCard.tsx +++ b/superset-frontend/src/features/charts/ChartCard.tsx @@ -67,8 +67,7 @@ export default function ChartCard({ const history = useHistory(); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); - const canExport = - hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + const canExport = hasPerm('can_export'); const theme = useTheme(); const menu = ( diff --git a/superset-frontend/src/pages/ChartList/index.tsx b/superset-frontend/src/pages/ChartList/index.tsx index 508019666111c..bfd945ea189df 100644 --- a/superset-frontend/src/pages/ChartList/index.tsx +++ b/superset-frontend/src/pages/ChartList/index.tsx @@ -234,8 +234,7 @@ function ChartList(props: ChartListProps) { const canCreate = hasPerm('can_write'); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); - const canExport = - hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + const canExport = hasPerm('can_export'); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const handleBulkChartExport = (chartsToExport: Chart[]) => { const ids = chartsToExport.map(({ id }) => id); @@ -777,21 +776,19 @@ function ChartList(props: ChartListProps) { }, }); - if (isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT)) { - subMenuButtons.push({ - name: ( - - - - ), - buttonStyle: 'link', - onClick: openChartImportModal, - }); - } + subMenuButtons.push({ + name: ( + + + + ), + buttonStyle: 'link', + onClick: openChartImportModal, + }); } return ( diff --git a/superset-frontend/src/pages/DashboardList/index.tsx b/superset-frontend/src/pages/DashboardList/index.tsx index e82b70185991e..76b34c6652b2b 100644 --- a/superset-frontend/src/pages/DashboardList/index.tsx +++ b/superset-frontend/src/pages/DashboardList/index.tsx @@ -183,8 +183,7 @@ function DashboardList(props: DashboardListProps) { const canCreate = hasPerm('can_write'); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); - const canExport = - hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + const canExport = hasPerm('can_export'); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; @@ -659,21 +658,19 @@ function DashboardList(props: DashboardListProps) { }, }); - if (isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT)) { - subMenuButtons.push({ - name: ( - - - - ), - buttonStyle: 'link', - onClick: openDashboardImportModal, - }); - } + subMenuButtons.push({ + name: ( + + + + ), + buttonStyle: 'link', + onClick: openDashboardImportModal, + }); } return ( <> diff --git a/superset-frontend/src/pages/DatabaseList/index.tsx b/superset-frontend/src/pages/DatabaseList/index.tsx index 8c98392aca93e..b30786941ea53 100644 --- a/superset-frontend/src/pages/DatabaseList/index.tsx +++ b/superset-frontend/src/pages/DatabaseList/index.tsx @@ -17,8 +17,6 @@ * under the License. */ import { - isFeatureEnabled, - FeatureFlag, getExtensionsRegistry, styled, SupersetClient, @@ -216,8 +214,7 @@ function DatabaseList({ const canCreate = hasPerm('can_write'); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); - const canExport = - hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + const canExport = hasPerm('can_export'); const { canUploadCSV, canUploadColumnar, canUploadExcel } = uploadUserPerms( roles, diff --git a/superset-frontend/src/pages/DatasetList/index.tsx b/superset-frontend/src/pages/DatasetList/index.tsx index 5aef13dbb6101..2ec521ad45057 100644 --- a/superset-frontend/src/pages/DatasetList/index.tsx +++ b/superset-frontend/src/pages/DatasetList/index.tsx @@ -17,8 +17,6 @@ * under the License. */ import { - isFeatureEnabled, - FeatureFlag, getExtensionsRegistry, styled, SupersetClient, @@ -207,8 +205,7 @@ const DatasetList: FunctionComponent = ({ const canDelete = hasPerm('can_write'); const canCreate = hasPerm('can_write'); const canDuplicate = hasPerm('can_duplicate'); - const canExport = - hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + const canExport = hasPerm('can_export'); const initialSort = SORT_BY; @@ -654,21 +651,19 @@ const DatasetList: FunctionComponent = ({ buttonStyle: 'primary', }); - if (isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT)) { - buttonArr.push({ - name: ( - - - - ), - buttonStyle: 'link', - onClick: openDatasetImportModal, - }); - } + buttonArr.push({ + name: ( + + + + ), + buttonStyle: 'link', + onClick: openDatasetImportModal, + }); } menuData.buttons = buttonArr; diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx b/superset-frontend/src/pages/SavedQueryList/index.tsx index d48ffef8c90c3..958107aa5d253 100644 --- a/superset-frontend/src/pages/SavedQueryList/index.tsx +++ b/superset-frontend/src/pages/SavedQueryList/index.tsx @@ -159,8 +159,7 @@ function SavedQueryList({ const canCreate = hasPerm('can_write'); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); - const canExport = - hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + const canExport = hasPerm('can_export'); const handleSavedQueryPreview = useCallback( (id: number) => { @@ -204,7 +203,7 @@ function SavedQueryList({ buttonStyle: 'primary', }); - if (canCreate && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT)) { + if (canCreate) { subMenuButtons.push({ name: ( None: ) -if feature_flags.get("VERSIONED_EXPORT"): +@click.command() +@with_appcontext +@click.option( + "--dashboard-file", + "-f", + help="Specify the file to export to", +) +def export_dashboards(dashboard_file: Optional[str] = None) -> None: + """Export dashboards to ZIP file""" + # pylint: disable=import-outside-toplevel + from superset.commands.dashboard.export import ExportDashboardsCommand + from superset.models.dashboard import Dashboard + + g.user = security_manager.find_user(username="admin") + + dashboard_ids = [id_ for (id_,) in db.session.query(Dashboard.id).all()] + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + root = f"dashboard_export_{timestamp}" + dashboard_file = dashboard_file or f"{root}.zip" + + try: + with ZipFile(dashboard_file, "w") as bundle: + for file_name, file_content in ExportDashboardsCommand(dashboard_ids).run(): + with bundle.open(f"{root}/{file_name}", "w") as fp: + fp.write(file_content.encode()) + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when exporting the dashboards, please check " + "the exception traceback in the log" + ) + sys.exit(1) - @click.command() - @with_appcontext - @click.option( - "--dashboard-file", - "-f", - help="Specify the file to export to", - ) - def export_dashboards(dashboard_file: Optional[str] = None) -> None: - """Export dashboards to ZIP file""" - # pylint: disable=import-outside-toplevel - from superset.commands.dashboard.export import ExportDashboardsCommand - from superset.models.dashboard import Dashboard - - g.user = security_manager.find_user(username="admin") - - dashboard_ids = [id_ for (id_,) in db.session.query(Dashboard.id).all()] - timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") - root = f"dashboard_export_{timestamp}" - dashboard_file = dashboard_file or f"{root}.zip" - - try: - with ZipFile(dashboard_file, "w") as bundle: - for file_name, file_content in ExportDashboardsCommand( - dashboard_ids - ).run(): - with bundle.open(f"{root}/{file_name}", "w") as fp: - fp.write(file_content.encode()) - except Exception: # pylint: disable=broad-except - logger.exception( - "There was an error when exporting the dashboards, please check " - "the exception traceback in the log" - ) - sys.exit(1) - - @click.command() - @with_appcontext - @click.option( - "--datasource-file", - "-f", - help="Specify the file to export to", - ) - def export_datasources(datasource_file: Optional[str] = None) -> None: - """Export datasources to ZIP file""" - # pylint: disable=import-outside-toplevel - from superset.commands.dataset.export import ExportDatasetsCommand - from superset.connectors.sqla.models import SqlaTable - - g.user = security_manager.find_user(username="admin") - - dataset_ids = [id_ for (id_,) in db.session.query(SqlaTable.id).all()] - timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") - root = f"dataset_export_{timestamp}" - datasource_file = datasource_file or f"{root}.zip" - - try: - with ZipFile(datasource_file, "w") as bundle: - for file_name, file_content in ExportDatasetsCommand(dataset_ids).run(): - with bundle.open(f"{root}/{file_name}", "w") as fp: - fp.write(file_content.encode()) - except Exception: # pylint: disable=broad-except - logger.exception( - "There was an error when exporting the datasets, please check " - "the exception traceback in the log" - ) - sys.exit(1) - - @click.command() - @with_appcontext - @click.option( - "--path", - "-p", - help="Path to a single ZIP file", - ) - @click.option( - "--username", - "-u", - default=None, - help="Specify the user name to assign dashboards to", - ) - def import_dashboards(path: str, username: Optional[str]) -> None: - """Import dashboards from ZIP file""" - # pylint: disable=import-outside-toplevel - from superset.commands.dashboard.importers.dispatcher import ( - ImportDashboardsCommand, + +@click.command() +@with_appcontext +@click.option( + "--datasource-file", + "-f", + help="Specify the file to export to", +) +def export_datasources(datasource_file: Optional[str] = None) -> None: + """Export datasources to ZIP file""" + # pylint: disable=import-outside-toplevel + from superset.commands.dataset.export import ExportDatasetsCommand + from superset.connectors.sqla.models import SqlaTable + + g.user = security_manager.find_user(username="admin") + + dataset_ids = [id_ for (id_,) in db.session.query(SqlaTable.id).all()] + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + root = f"dataset_export_{timestamp}" + datasource_file = datasource_file or f"{root}.zip" + + try: + with ZipFile(datasource_file, "w") as bundle: + for file_name, file_content in ExportDatasetsCommand(dataset_ids).run(): + with bundle.open(f"{root}/{file_name}", "w") as fp: + fp.write(file_content.encode()) + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when exporting the datasets, please check " + "the exception traceback in the log" ) - from superset.commands.importers.v1.utils import get_contents_from_bundle - - if username is not None: - g.user = security_manager.find_user(username=username) - if is_zipfile(path): - with ZipFile(path) as bundle: - contents = get_contents_from_bundle(bundle) - else: - with open(path) as file: - contents = {path: file.read()} - try: - ImportDashboardsCommand(contents, overwrite=True).run() - except Exception: # pylint: disable=broad-except - logger.exception( - "There was an error when importing the dashboards(s), please check " - "the exception traceback in the log" - ) - sys.exit(1) - - @click.command() - @with_appcontext - @click.option( - "--path", - "-p", - help="Path to a single ZIP file", - ) - def import_datasources(path: str) -> None: - """Import datasources from ZIP file""" - # pylint: disable=import-outside-toplevel - from superset.commands.dataset.importers.dispatcher import ImportDatasetsCommand - from superset.commands.importers.v1.utils import get_contents_from_bundle - - if is_zipfile(path): - with ZipFile(path) as bundle: - contents = get_contents_from_bundle(bundle) - else: - with open(path) as file: - contents = {path: file.read()} - try: - ImportDatasetsCommand(contents, overwrite=True).run() - except Exception: # pylint: disable=broad-except - logger.exception( - "There was an error when importing the dataset(s), please check the " - "exception traceback in the log" - ) - sys.exit(1) - -else: - - @click.command() - @with_appcontext - @click.option( - "--dashboard-file", - "-f", - default=None, - help="Specify the file to export to", - ) - @click.option( - "--print_stdout", - "-p", - is_flag=True, - default=False, - help="Print JSON to stdout", - ) - def export_dashboards( - dashboard_file: Optional[str], print_stdout: bool = False - ) -> None: - """Export dashboards to JSON""" - # pylint: disable=import-outside-toplevel - from superset.utils import dashboard_import_export - - data = dashboard_import_export.export_dashboards(db.session) - if print_stdout or not dashboard_file: - print(data) - if dashboard_file: - logger.info("Exporting dashboards to %s", dashboard_file) - with open(dashboard_file, "w") as data_stream: - data_stream.write(data) - - @click.command() - @with_appcontext - @click.option( - "--datasource-file", - "-f", - default=None, - help="Specify the file to export to", - ) - @click.option( - "--print_stdout", - "-p", - is_flag=True, - default=False, - help="Print YAML to stdout", - ) - @click.option( - "--back-references", - "-b", - is_flag=True, - default=False, - help="Include parent back references", - ) - @click.option( - "--include-defaults", - "-d", - is_flag=True, - default=False, - help="Include fields containing defaults", - ) - def export_datasources( - datasource_file: Optional[str], - print_stdout: bool = False, - back_references: bool = False, - include_defaults: bool = False, - ) -> None: - """Export datasources to YAML""" - # pylint: disable=import-outside-toplevel - from superset.utils import dict_import_export - - data = dict_import_export.export_to_dict( - session=db.session, - recursive=True, - back_references=back_references, - include_defaults=include_defaults, + sys.exit(1) + + +@click.command() +@with_appcontext +@click.option( + "--path", + "-p", + help="Path to a single ZIP file", +) +@click.option( + "--username", + "-u", + default=None, + help="Specify the user name to assign dashboards to", +) +def import_dashboards(path: str, username: Optional[str]) -> None: + """Import dashboards from ZIP file""" + # pylint: disable=import-outside-toplevel + from superset.commands.dashboard.importers.dispatcher import ImportDashboardsCommand + from superset.commands.importers.v1.utils import get_contents_from_bundle + + if username is not None: + g.user = security_manager.find_user(username=username) + if is_zipfile(path): + with ZipFile(path) as bundle: + contents = get_contents_from_bundle(bundle) + else: + with open(path) as file: + contents = {path: file.read()} + try: + ImportDashboardsCommand(contents, overwrite=True).run() + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when importing the dashboards(s), please check " + "the exception traceback in the log" ) - if print_stdout or not datasource_file: - yaml.safe_dump(data, sys.stdout, default_flow_style=False) - if datasource_file: - logger.info("Exporting datasources to %s", datasource_file) - with open(datasource_file, "w") as data_stream: - yaml.safe_dump(data, data_stream, default_flow_style=False) - - @click.command() - @with_appcontext - @click.option( - "--path", - "-p", - help="Path to a single JSON file or path containing multiple JSON " - "files to import (*.json)", - ) - @click.option( - "--recursive", - "-r", - is_flag=True, - default=False, - help="recursively search the path for json files", - ) - @click.option( - "--username", - "-u", - default=None, - help="Specify the user name to assign dashboards to", - ) - def import_dashboards(path: str, recursive: bool, username: str) -> None: - """Import dashboards from JSON file""" - # pylint: disable=import-outside-toplevel - from superset.commands.dashboard.importers.v0 import ImportDashboardsCommand - - path_object = Path(path) - files: list[Path] = [] - if path_object.is_file(): - files.append(path_object) - elif path_object.exists() and not recursive: - files.extend(path_object.glob("*.json")) - elif path_object.exists() and recursive: - files.extend(path_object.rglob("*.json")) - if username is not None: - g.user = security_manager.find_user(username=username) - contents = {} - for path_ in files: - with open(path_) as file: - contents[path_.name] = file.read() - try: - ImportDashboardsCommand(contents).run() - except Exception: # pylint: disable=broad-except - logger.exception("Error when importing dashboard") - sys.exit(1) - - @click.command() - @with_appcontext - @click.option( - "--path", - "-p", - help="Path to a single YAML file or path containing multiple YAML " - "files to import (*.yaml or *.yml)", - ) - @click.option( - "--sync", - "-s", - "sync", - default="", - help="comma separated list of element types to synchronize " - 'e.g. "metrics,columns" deletes metrics and columns in the DB ' - "that are not specified in the YAML file", - ) - @click.option( - "--recursive", - "-r", - is_flag=True, - default=False, - help="recursively search the path for yaml files", - ) - def import_datasources(path: str, sync: str, recursive: bool) -> None: - """Import datasources from YAML""" - # pylint: disable=import-outside-toplevel - from superset.commands.dataset.importers.v0 import ImportDatasetsCommand - - sync_array = sync.split(",") - sync_columns = "columns" in sync_array - sync_metrics = "metrics" in sync_array - - path_object = Path(path) - files: list[Path] = [] - if path_object.is_file(): - files.append(path_object) - elif path_object.exists() and not recursive: - files.extend(path_object.glob("*.yaml")) - files.extend(path_object.glob("*.yml")) - elif path_object.exists() and recursive: - files.extend(path_object.rglob("*.yaml")) - files.extend(path_object.rglob("*.yml")) - contents = {} - for path_ in files: - with open(path_) as file: - contents[path_.name] = file.read() - try: - ImportDatasetsCommand( - contents, sync_columns=sync_columns, sync_metrics=sync_metrics - ).run() - except Exception: # pylint: disable=broad-except - logger.exception("Error when importing dataset") - sys.exit(1) - - @click.command() - @with_appcontext - @click.option( - "--back-references", - "-b", - is_flag=True, - default=False, - help="Include parent back references", - ) - def export_datasource_schema(back_references: bool) -> None: - """Export datasource YAML schema to stdout""" - # pylint: disable=import-outside-toplevel - from superset.utils import dict_import_export + sys.exit(1) + + +@click.command() +@with_appcontext +@click.option( + "--path", + "-p", + help="Path to a single ZIP file", +) +def import_datasources(path: str) -> None: + """Import datasources from ZIP file""" + # pylint: disable=import-outside-toplevel + from superset.commands.dataset.importers.dispatcher import ImportDatasetsCommand + from superset.commands.importers.v1.utils import get_contents_from_bundle + + if is_zipfile(path): + with ZipFile(path) as bundle: + contents = get_contents_from_bundle(bundle) + else: + with open(path) as file: + contents = {path: file.read()} + try: + ImportDatasetsCommand(contents, overwrite=True).run() + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when importing the dataset(s), please check the " + "exception traceback in the log" + ) + sys.exit(1) + + +@click.command() +@with_appcontext +@click.option( + "--dashboard-file", + "-f", + default=None, + help="Specify the file to export to", +) +@click.option( + "--print_stdout", + "-p", + is_flag=True, + default=False, + help="Print JSON to stdout", +) +def legacy_export_dashboards( + dashboard_file: Optional[str], print_stdout: bool = False +) -> None: + """Export dashboards to JSON""" + # pylint: disable=import-outside-toplevel + from superset.utils import dashboard_import_export + + data = dashboard_import_export.export_dashboards(db.session) + if print_stdout or not dashboard_file: + print(data) + if dashboard_file: + logger.info("Exporting dashboards to %s", dashboard_file) + with open(dashboard_file, "w") as data_stream: + data_stream.write(data) + + +@click.command() +@with_appcontext +@click.option( + "--datasource-file", + "-f", + default=None, + help="Specify the file to export to", +) +@click.option( + "--print_stdout", + "-p", + is_flag=True, + default=False, + help="Print YAML to stdout", +) +@click.option( + "--back-references", + "-b", + is_flag=True, + default=False, + help="Include parent back references", +) +@click.option( + "--include-defaults", + "-d", + is_flag=True, + default=False, + help="Include fields containing defaults", +) +def legacy_export_datasources( + datasource_file: Optional[str], + print_stdout: bool = False, + back_references: bool = False, + include_defaults: bool = False, +) -> None: + """Export datasources to YAML""" + # pylint: disable=import-outside-toplevel + from superset.utils import dict_import_export - data = dict_import_export.export_schema_to_dict(back_references=back_references) + data = dict_import_export.export_to_dict( + session=db.session, + recursive=True, + back_references=back_references, + include_defaults=include_defaults, + ) + if print_stdout or not datasource_file: yaml.safe_dump(data, sys.stdout, default_flow_style=False) + if datasource_file: + logger.info("Exporting datasources to %s", datasource_file) + with open(datasource_file, "w") as data_stream: + yaml.safe_dump(data, data_stream, default_flow_style=False) + + +@click.command() +@with_appcontext +@click.option( + "--path", + "-p", + help="Path to a single JSON file or path containing multiple JSON " + "files to import (*.json)", +) +@click.option( + "--recursive", + "-r", + is_flag=True, + default=False, + help="recursively search the path for json files", +) +@click.option( + "--username", + "-u", + default=None, + help="Specify the user name to assign dashboards to", +) +def legacy_import_dashboards(path: str, recursive: bool, username: str) -> None: + """Import dashboards from JSON file""" + # pylint: disable=import-outside-toplevel + from superset.commands.dashboard.importers.v0 import ImportDashboardsCommand + + path_object = Path(path) + files: list[Path] = [] + if path_object.is_file(): + files.append(path_object) + elif path_object.exists() and not recursive: + files.extend(path_object.glob("*.json")) + elif path_object.exists() and recursive: + files.extend(path_object.rglob("*.json")) + if username is not None: + g.user = security_manager.find_user(username=username) + contents = {} + for path_ in files: + with open(path_) as file: + contents[path_.name] = file.read() + try: + ImportDashboardsCommand(contents).run() + except Exception: # pylint: disable=broad-except + logger.exception("Error when importing dashboard") + sys.exit(1) + + +@click.command() +@with_appcontext +@click.option( + "--path", + "-p", + help="Path to a single YAML file or path containing multiple YAML " + "files to import (*.yaml or *.yml)", +) +@click.option( + "--sync", + "-s", + "sync", + default="", + help="comma separated list of element types to synchronize " + 'e.g. "metrics,columns" deletes metrics and columns in the DB ' + "that are not specified in the YAML file", +) +@click.option( + "--recursive", + "-r", + is_flag=True, + default=False, + help="recursively search the path for yaml files", +) +def legacy_import_datasources(path: str, sync: str, recursive: bool) -> None: + """Import datasources from YAML""" + # pylint: disable=import-outside-toplevel + from superset.commands.dataset.importers.v0 import ImportDatasetsCommand + + sync_array = sync.split(",") + sync_columns = "columns" in sync_array + sync_metrics = "metrics" in sync_array + + path_object = Path(path) + files: list[Path] = [] + if path_object.is_file(): + files.append(path_object) + elif path_object.exists() and not recursive: + files.extend(path_object.glob("*.yaml")) + files.extend(path_object.glob("*.yml")) + elif path_object.exists() and recursive: + files.extend(path_object.rglob("*.yaml")) + files.extend(path_object.rglob("*.yml")) + contents = {} + for path_ in files: + with open(path_) as file: + contents[path_.name] = file.read() + try: + ImportDatasetsCommand( + contents, sync_columns=sync_columns, sync_metrics=sync_metrics + ).run() + except Exception: # pylint: disable=broad-except + logger.exception("Error when importing dataset") + sys.exit(1) + + +@click.command() +@with_appcontext +@click.option( + "--back-references", + "-b", + is_flag=True, + default=False, + help="Include parent back references", +) +def legacy_export_datasource_schema(back_references: bool) -> None: + """Export datasource YAML schema to stdout""" + # pylint: disable=import-outside-toplevel + from superset.utils import dict_import_export + + data = dict_import_export.export_schema_to_dict(back_references=back_references) + yaml.safe_dump(data, sys.stdout, default_flow_style=False) diff --git a/superset/config.py b/superset/config.py index 474cb0b99b880..4d0eda4b2f048 100644 --- a/superset/config.py +++ b/superset/config.py @@ -432,7 +432,6 @@ class D3Format(TypedDict, total=False): "DASHBOARD_CROSS_FILTERS": True, # deprecated "DASHBOARD_VIRTUALIZATION": True, "GLOBAL_ASYNC_QUERIES": False, - "VERSIONED_EXPORT": True, # deprecated "EMBEDDED_SUPERSET": False, # Enables Alerts and reports new implementation "ALERT_REPORTS": False, diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 783543e45de8b..e719a27fc743b 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -23,7 +23,7 @@ from typing import Any, Callable, cast, Optional from zipfile import is_zipfile, ZipFile -from flask import make_response, redirect, request, Response, send_file, url_for +from flask import redirect, request, Response, send_file, url_for from flask_appbuilder import permission_name from flask_appbuilder.api import expose, protect, rison, safe from flask_appbuilder.hooks import before_request @@ -85,7 +85,6 @@ from superset.tasks.utils import get_current_user from superset.utils.screenshots import DashboardScreenshot from superset.utils.urls import get_url_path -from superset.views.base import generate_download_headers from superset.views.base_api import ( BaseSupersetModelRestApi, RelatedFieldFilter, @@ -714,7 +713,7 @@ def bulk_delete(self, **kwargs: Any) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.export", log_to_statsd=False, ) - def export(self, **kwargs: Any) -> Response: # pylint: disable=too-many-locals + def export(self, **kwargs: Any) -> Response: """Download multiple dashboards as YAML files. --- get: @@ -745,50 +744,32 @@ def export(self, **kwargs: Any) -> Response: # pylint: disable=too-many-locals $ref: '#/components/responses/500' """ requested_ids = kwargs["rison"] - token = request.args.get("token") - if is_feature_enabled("VERSIONED_EXPORT"): - timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") - root = f"dashboard_export_{timestamp}" - filename = f"{root}.zip" + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + root = f"dashboard_export_{timestamp}" + filename = f"{root}.zip" - buf = BytesIO() - with ZipFile(buf, "w") as bundle: - try: - for file_name, file_content in ExportDashboardsCommand( - requested_ids - ).run(): - with bundle.open(f"{root}/{file_name}", "w") as fp: - fp.write(file_content.encode()) - except DashboardNotFoundError: - return self.response_404() - buf.seek(0) + buf = BytesIO() + with ZipFile(buf, "w") as bundle: + try: + for file_name, file_content in ExportDashboardsCommand( + requested_ids + ).run(): + with bundle.open(f"{root}/{file_name}", "w") as fp: + fp.write(file_content.encode()) + except DashboardNotFoundError: + return self.response_404() + buf.seek(0) - response = send_file( - buf, - mimetype="application/zip", - as_attachment=True, - download_name=filename, - ) - if token: - response.set_cookie(token, "done", max_age=600) - return response - - query = self.datamodel.session.query(Dashboard).filter( - Dashboard.id.in_(requested_ids) + response = send_file( + buf, + mimetype="application/zip", + as_attachment=True, + download_name=filename, ) - query = self._base_filters.apply_all(query) - ids = {item.id for item in query.all()} - if not ids: - return self.response_404() - export = Dashboard.export_dashboards(ids) - resp = make_response(export, 200) - resp.headers["Content-Disposition"] = generate_download_headers("json")[ - "Content-Disposition" - ] - if token: - resp.set_cookie(token, "done", max_age=600) - return resp + if token := request.args.get("token"): + response.set_cookie(token, "done", max_age=600) + return response @expose("//thumbnail//", methods=("GET",)) @protect() diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 809074e10ddb2..8cc1b2df6301d 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -22,14 +22,13 @@ from typing import Any from zipfile import is_zipfile, ZipFile -import yaml from flask import request, Response, send_file from flask_appbuilder.api import expose, protect, rison, safe from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_babel import ngettext from marshmallow import ValidationError -from superset import event_logger, is_feature_enabled +from superset import event_logger from superset.commands.dataset.create import CreateDatasetCommand from superset.commands.dataset.delete import DeleteDatasetCommand from superset.commands.dataset.duplicate import DuplicateDatasetCommand @@ -68,7 +67,7 @@ openapi_spec_methods_override, ) from superset.utils.core import parse_boolean_string -from superset.views.base import DatasourceFilter, generate_download_headers +from superset.views.base import DatasourceFilter from superset.views.base_api import ( BaseSupersetModelRestApi, RelatedFieldFilter, @@ -489,7 +488,7 @@ def delete(self, pk: int) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.export", log_to_statsd=False, ) - def export(self, **kwargs: Any) -> Response: # pylint: disable=too-many-locals + def export(self, **kwargs: Any) -> Response: """Download multiple datasets as YAML files. --- get: @@ -519,49 +518,31 @@ def export(self, **kwargs: Any) -> Response: # pylint: disable=too-many-locals """ requested_ids = kwargs["rison"] - if is_feature_enabled("VERSIONED_EXPORT"): - token = request.args.get("token") - timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") - root = f"dataset_export_{timestamp}" - filename = f"{root}.zip" + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + root = f"dataset_export_{timestamp}" + filename = f"{root}.zip" - buf = BytesIO() - with ZipFile(buf, "w") as bundle: - try: - for file_name, file_content in ExportDatasetsCommand( - requested_ids - ).run(): - with bundle.open(f"{root}/{file_name}", "w") as fp: - fp.write(file_content.encode()) - except DatasetNotFoundError: - return self.response_404() - buf.seek(0) + buf = BytesIO() + with ZipFile(buf, "w") as bundle: + try: + for file_name, file_content in ExportDatasetsCommand( + requested_ids + ).run(): + with bundle.open(f"{root}/{file_name}", "w") as fp: + fp.write(file_content.encode()) + except DatasetNotFoundError: + return self.response_404() + buf.seek(0) - response = send_file( - buf, - mimetype="application/zip", - as_attachment=True, - download_name=filename, - ) - if token: - response.set_cookie(token, "done", max_age=600) - return response - - query = self.datamodel.session.query(SqlaTable).filter( - SqlaTable.id.in_(requested_ids) - ) - query = self._base_filters.apply_all(query) - items = query.all() - ids = [item.id for item in items] - if len(ids) != len(requested_ids): - return self.response_404() - - data = [t.export_to_dict() for t in items] - return Response( - yaml.safe_dump(data), - headers=generate_download_headers("yaml"), - mimetype="application/text", + response = send_file( + buf, + mimetype="application/zip", + as_attachment=True, + download_name=filename, ) + if token := request.args.get("token"): + response.set_cookie(token, "done", max_age=600) + return response @expose("/duplicate", methods=("POST",)) @protect() diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 807f430ee44ab..2e5f5a716a6f8 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -118,7 +118,6 @@ def init_views(self) -> None: # the global Flask app # # pylint: disable=import-outside-toplevel,too-many-locals,too-many-statements - from superset import security_manager from superset.advanced_data_type.api import AdvancedDataTypeRestApi from superset.annotation_layers.annotations.api import AnnotationRestApi from superset.annotation_layers.api import AnnotationLayerRestApi @@ -327,20 +326,6 @@ def init_views(self) -> None: # # Add links # - appbuilder.add_link( - "Import Dashboards", - label=__("Import Dashboards"), - href="/superset/import_dashboards/", - icon="fa-cloud-upload", - category="Manage", - category_label=__("Manage"), - category_icon="fa-wrench", - cond=lambda: ( - security_manager.can_access("can_import_dashboards", "Superset") - and not feature_flag_manager.is_feature_enabled("VERSIONED_EXPORT") - ), - ) - appbuilder.add_link( "SQL Editor", label=__("SQL Lab"), diff --git a/superset/views/core.py b/superset/views/core.py index 330d517a37d4c..307cd08c6414e 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -47,7 +47,6 @@ from superset.async_events.async_query_manager import AsyncQueryTokenException from superset.commands.chart.exceptions import ChartNotFoundError from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand -from superset.commands.dashboard.importers.v0 import ImportDashboardsCommand from superset.commands.dashboard.permalink.get import GetDashboardPermalinkCommand from superset.commands.dataset.exceptions import DatasetNotFoundError from superset.commands.explore.form_data.create import CreateFormDataCommand @@ -61,7 +60,6 @@ from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError from superset.exceptions import ( CacheLoadError, - DatabaseNotFound, SupersetException, SupersetSecurityException, ) @@ -345,55 +343,6 @@ def explore_json( except SupersetException as ex: return json_error_response(utils.error_msg_from_exception(ex), 400) - @has_access - @event_logger.log_this - @expose( - "/import_dashboards/", - methods=( - "GET", - "POST", - ), - ) - def import_dashboards(self) -> FlaskResponse: - """Overrides the dashboards using json instances from the file.""" - import_file = request.files.get("file") - if request.method == "POST" and import_file: - success = False - database_id = request.form.get("db_id") - try: - ImportDashboardsCommand( - {import_file.filename: import_file.read()}, database_id - ).run() - success = True - except DatabaseNotFound as ex: - logger.exception(ex) - flash( - _( - "Cannot import dashboard: %(db_error)s.\n" - "Make sure to create the database before " - "importing the dashboard.", - db_error=ex, - ), - "danger", - ) - except Exception as ex: # pylint: disable=broad-except - logger.exception(ex) - flash( - _( - "An unknown error occurred. " - "Please contact your Superset administrator" - ), - "danger", - ) - if success: - flash("Dashboard(s) have been imported", "success") - return redirect("/dashboard/list/") - - databases = db.session.query(Database).all() - return self.render_template( - "superset/import_dashboards.html", databases=databases - ) - @staticmethod def get_redirect_url() -> str: """Assembles the redirect URL to the new endpoint. It also replaces diff --git a/tests/integration_tests/cli_tests.py b/tests/integration_tests/cli_tests.py index 55557ab32deac..2441809b0da64 100644 --- a/tests/integration_tests/cli_tests.py +++ b/tests/integration_tests/cli_tests.py @@ -16,7 +16,6 @@ # under the License. import importlib -import json import logging from pathlib import Path from unittest import mock @@ -49,69 +48,7 @@ def assert_cli_fails_properly(response, caplog): assert caplog.records[-1].levelname == "ERROR" -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True -) -@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") -def test_export_dashboards_original(app_context, fs): - """ - Test that a JSON file is exported. - """ - # pylint: disable=reimported, redefined-outer-name - import superset.cli.importexport # noqa: F811 - - # reload to define export_dashboards correctly based on the - # feature flags - importlib.reload(superset.cli.importexport) - - runner = app.test_cli_runner() - response = runner.invoke( - superset.cli.importexport.export_dashboards, ("-f", "dashboards.json") - ) - - assert response.exit_code == 0 - assert Path("dashboards.json").exists() - - # check that file is valid JSON - with open("dashboards.json") as fp: - contents = fp.read() - json.loads(contents) - - -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True -) -@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") -def test_export_datasources_original(app_context, fs): - """ - Test that a YAML file is exported. - """ - # pylint: disable=reimported, redefined-outer-name - import superset.cli.importexport # noqa: F811 - - # reload to define export_dashboards correctly based on the - # feature flags - importlib.reload(superset.cli.importexport) - - runner = app.test_cli_runner() - response = runner.invoke( - superset.cli.importexport.export_datasources, ("-f", "datasources.yaml") - ) - - assert response.exit_code == 0 - - assert Path("datasources.yaml").exists() - - # check that file is valid JSON - with open("datasources.yaml") as fp: - contents = fp.read() - yaml.safe_load(contents) - - @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) def test_export_dashboards_versioned_export(app_context, fs): """ Test that a ZIP file is exported. @@ -133,9 +70,6 @@ def test_export_dashboards_versioned_export(app_context, fs): assert is_zipfile("dashboard_export_20210101T000000.zip") -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) @mock.patch( "superset.commands.dashboard.export.ExportDashboardsCommand.run", side_effect=Exception(), @@ -163,9 +97,6 @@ def test_failing_export_dashboards_versioned_export( @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) def test_export_datasources_versioned_export(app_context, fs): """ Test that a ZIP file is exported. @@ -187,9 +118,6 @@ def test_export_datasources_versioned_export(app_context, fs): assert is_zipfile("dataset_export_20210101T000000.zip") -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) @mock.patch( "superset.commands.dashboard.export.ExportDatasetsCommand.run", side_effect=Exception(), @@ -214,9 +142,6 @@ def test_failing_export_datasources_versioned_export( assert_cli_fails_properly(response, caplog) -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) @mock.patch("superset.commands.dashboard.importers.dispatcher.ImportDashboardsCommand") def test_import_dashboards_versioned_export(import_dashboards_command, app_context, fs): """ @@ -257,9 +182,6 @@ def test_import_dashboards_versioned_export(import_dashboards_command, app_conte import_dashboards_command.assert_called_with(expected_contents, overwrite=True) -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) @mock.patch( "superset.commands.dashboard.importers.dispatcher.ImportDashboardsCommand.run", side_effect=Exception(), @@ -301,9 +223,6 @@ def test_failing_import_dashboards_versioned_export( assert_cli_fails_properly(response, caplog) -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) @mock.patch("superset.commands.dataset.importers.dispatcher.ImportDatasetsCommand") def test_import_datasets_versioned_export(import_datasets_command, app_context, fs): """ @@ -344,120 +263,6 @@ def test_import_datasets_versioned_export(import_datasets_command, app_context, import_datasets_command.assert_called_with(expected_contents, overwrite=True) -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True -) -@mock.patch("superset.commands.dataset.importers.v0.ImportDatasetsCommand") -def test_import_datasets_sync_argument_columns_metrics( - import_datasets_command, app_context, fs -): - """ - Test that the --sync command line argument syncs dataset in superset - with YAML file. Using both columns and metrics with the --sync flag - """ - # pylint: disable=reimported, redefined-outer-name - import superset.cli.importexport # noqa: F811 - - # reload to define export_datasets correctly based on the - # feature flags - importlib.reload(superset.cli.importexport) - - # write YAML file - with open("dataset.yaml", "w") as fp: - fp.write("hello: world") - - runner = app.test_cli_runner() - response = runner.invoke( - superset.cli.importexport.import_datasources, - ["-p", "dataset.yaml", "-s", "metrics,columns"], - ) - - assert response.exit_code == 0 - expected_contents = {"dataset.yaml": "hello: world"} - import_datasets_command.assert_called_with( - expected_contents, - sync_columns=True, - sync_metrics=True, - ) - - -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True -) -@mock.patch("superset.commands.dataset.importers.v0.ImportDatasetsCommand") -def test_import_datasets_sync_argument_columns( - import_datasets_command, app_context, fs -): - """ - Test that the --sync command line argument syncs dataset in superset - with YAML file. Using only columns with the --sync flag - """ - # pylint: disable=reimported, redefined-outer-name - import superset.cli.importexport # noqa: F811 - - # reload to define export_datasets correctly based on the - # feature flags - importlib.reload(superset.cli.importexport) - - # write YAML file - with open("dataset.yaml", "w") as fp: - fp.write("hello: world") - - runner = app.test_cli_runner() - response = runner.invoke( - superset.cli.importexport.import_datasources, - ["-p", "dataset.yaml", "-s", "columns"], - ) - - assert response.exit_code == 0 - expected_contents = {"dataset.yaml": "hello: world"} - import_datasets_command.assert_called_with( - expected_contents, - sync_columns=True, - sync_metrics=False, - ) - - -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True -) -@mock.patch("superset.commands.dataset.importers.v0.ImportDatasetsCommand") -def test_import_datasets_sync_argument_metrics( - import_datasets_command, app_context, fs -): - """ - Test that the --sync command line argument syncs dataset in superset - with YAML file. Using only metrics with the --sync flag - """ - # pylint: disable=reimported, redefined-outer-name - import superset.cli.importexport # noqa: F811 - - # reload to define export_datasets correctly based on the - # feature flags - importlib.reload(superset.cli.importexport) - - # write YAML file - with open("dataset.yaml", "w") as fp: - fp.write("hello: world") - - runner = app.test_cli_runner() - response = runner.invoke( - superset.cli.importexport.import_datasources, - ["-p", "dataset.yaml", "-s", "metrics"], - ) - - assert response.exit_code == 0 - expected_contents = {"dataset.yaml": "hello: world"} - import_datasets_command.assert_called_with( - expected_contents, - sync_columns=False, - sync_metrics=True, - ) - - -@mock.patch.dict( - "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": True}, clear=True -) @mock.patch( "superset.commands.dataset.importers.dispatcher.ImportDatasetsCommand.run", side_effect=Exception(), diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index 6b435da997fa8..d809880bf7df5 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -36,7 +36,6 @@ from superset.reports.models import ReportSchedule, ReportScheduleType from superset.models.slice import Slice from superset.utils.core import backend, override_user -from superset.views.base import generate_download_headers from tests.integration_tests.conftest import with_feature_flags from tests.integration_tests.base_api_tests import ApiOwnersTestCaseMixin @@ -1652,11 +1651,6 @@ def test_update_dashboard_not_owned(self): db.session.delete(user_alpha2) db.session.commit() - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": False}, - clear=True, - ) @pytest.mark.usefixtures( "load_world_bank_dashboard_with_slices", "load_birth_names_dashboard_with_slices", @@ -1671,8 +1665,8 @@ def test_export(self): uri = f"api/v1/dashboard/export/?q={prison.dumps(dashboards_ids)}" rv = self.get_assert_metric(uri, "export") - headers = generate_download_headers("json")["Content-Disposition"] + headers = f"attachment; filename=dashboard_export_20220101T000000.zip" assert rv.status_code == 200 assert rv.headers["Content-Disposition"] == headers diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py index c9d2a41057ba3..395aebf29c61f 100644 --- a/tests/integration_tests/security_tests.py +++ b/tests/integration_tests/security_tests.py @@ -1348,7 +1348,6 @@ def assert_can_alpha(self, perm_set): self.assert_can_all("CssTemplate", perm_set) self.assert_can_all("Dataset", perm_set) self.assert_can_read("Database", perm_set) - self.assertIn(("can_import_dashboards", "Superset"), perm_set) self.assertIn(("can_this_form_post", "CsvToDatabaseView"), perm_set) self.assertIn(("can_this_form_get", "CsvToDatabaseView"), perm_set) self.assert_can_menu("Manage", perm_set) From 3acda145f1d15bb93db2dcbaf1f283b4b9840e8c Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 19 Jan 2024 12:46:44 -0700 Subject: [PATCH 06/11] fix: Revert "build(deps): bump @mdx-js/react from 1.6.22 to 3.0.0 in /docs" (#26682) --- docs/package.json | 2 +- docs/yarn.lock | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/package.json b/docs/package.json index 747063d5bad09..9bcde81fa72a8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -25,7 +25,7 @@ "@docusaurus/preset-classic": "^2.4.3", "@emotion/core": "^10.1.1", "@emotion/styled": "^10.0.27", - "@mdx-js/react": "^3.0.0", + "@mdx-js/react": "^1.6.22", "@saucelabs/theme-github-codeblock": "^0.1.1", "@superset-ui/style": "^0.14.23", "@svgr/webpack": "^5.5.0", diff --git a/docs/yarn.lock b/docs/yarn.lock index 60b67abfac173..3a666ab6619d9 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3286,13 +3286,6 @@ resolved "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.22.tgz" integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg== -"@mdx-js/react@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.0.tgz#eaccaa8d6a7736b19080aff5a70448a7ba692271" - integrity sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ== - dependencies: - "@types/mdx" "^2.0.0" - "@mdx-js/util@1.6.22": version "1.6.22" resolved "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz" @@ -3736,11 +3729,6 @@ dependencies: "@types/unist" "*" -"@types/mdx@^2.0.0": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.10.tgz#0d7b57fb1d83e27656156e4ee0dfba96532930e4" - integrity sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg== - "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" From 69b57016b3b48b35e20537f07b71c6151e72265b Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:07:38 -0300 Subject: [PATCH 07/11] refactor: Removes the deprecated DASHBOARD_NATIVE_FILTERS feature flag (#26329) --- RESOURCES/FEATURE_FLAGS.md | 1 - UPDATING.md | 1 + .../src/utils/featureFlags.ts | 1 - .../src/plugin/controls/pagination.tsx | 32 +++++----- .../plugin-chart-table/src/controlPanel.tsx | 31 ++++------ .../DashboardBuilder.test.tsx | 44 +++++--------- .../DashboardBuilder/DashboardContainer.tsx | 7 +-- .../components/DashboardBuilder/state.ts | 4 +- .../FiltersBadge/FiltersBadge.test.tsx | 4 -- .../HeaderActionsDropdown.test.tsx | 60 +++---------------- .../Header/HeaderActionsDropdown/index.jsx | 27 +++------ .../FilterBar/FilterBar.test.tsx | 11 +--- .../FilterControls/FilterControls.tsx | 31 +++++----- .../nativeFilters/FilterBar/Header/index.tsx | 11 +--- .../nativeFilters/FilterBar/Horizontal.tsx | 12 +--- .../FilterBar/HorizontalFilterBar.test.tsx | 8 +-- .../nativeFilters/FilterBar/Vertical.tsx | 11 +--- .../components/nativeFilters/selectors.ts | 51 ++++++++-------- .../components/nativeFilters/utils.test.ts | 36 +---------- superset-frontend/src/dataMask/actions.ts | 14 +---- superset/config.py | 1 - .../integration_tests/superset_test_config.py | 1 - 22 files changed, 110 insertions(+), 289 deletions(-) diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index ef05ab98db5c5..d782c10cbec79 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -84,7 +84,6 @@ These features flags currently default to True and **will be removed in a future [//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY" - DASHBOARD_CROSS_FILTERS -- DASHBOARD_NATIVE_FILTERS - ENABLE_JAVASCRIPT_CONTROLS - GENERIC_CHART_AXES - KV_STORE diff --git a/UPDATING.md b/UPDATING.md index f1f2566736817..804b7cdd24f34 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -43,6 +43,7 @@ assists people when migrating to a new version. - [26637](https://github.com/apache/superset/issues/26637): Sets the `DRILL_BY` feature flag to `True` by default given that the feature has been tested for a while and reached a stable state. - [26462](https://github.com/apache/superset/issues/26462): Removes the Profile feature given that it's not actively maintained and not widely used. - [26377](https://github.com/apache/superset/pull/26377): Removes the deprecated Redirect API that supported short URLs used before the permalink feature. +- [26329](https://github.com/apache/superset/issues/26329): Removes the deprecated `DASHBOARD_NATIVE_FILTERS` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled. ### Potential Downtime diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index ba9d0d6680fd8..f993e451f3bf3 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -29,7 +29,6 @@ export enum FeatureFlag { CONFIRM_DASHBOARD_DIFF = 'CONFIRM_DASHBOARD_DIFF', /** @deprecated */ DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS', - DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS', DASHBOARD_VIRTUALIZATION = 'DASHBOARD_VIRTUALIZATION', DASHBOARD_RBAC = 'DASHBOARD_RBAC', DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT', diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx index bf4c1207174d1..97a8206a1133b 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/pagination.tsx @@ -21,26 +21,22 @@ import { ControlSetItem, ControlSetRow, } from '@superset-ui/chart-controls'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { PAGE_SIZE_OPTIONS } from '../../consts'; -export const serverPaginationControlSetRow: ControlSetRow = - isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) || - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) - ? [ - { - name: 'server_pagination', - config: { - type: 'CheckboxControl', - label: t('Server pagination'), - description: t( - 'Enable server side pagination of results (experimental feature)', - ), - default: false, - }, - }, - ] - : []; +export const serverPaginationControlSetRow: ControlSetRow = [ + { + name: 'server_pagination', + config: { + type: 'CheckboxControl', + label: t('Server pagination'), + description: t( + 'Enable server side pagination of results (experimental feature)', + ), + default: false, + }, + }, +]; export const serverPageLengthControlSetItem: ControlSetItem = { name: 'server_page_length', diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index a59ade460a5fc..c3fe7282e698f 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -21,11 +21,9 @@ import React from 'react'; import { ChartDataResponseResult, ensureIsArray, - FeatureFlag, GenericDataType, hasGenericChartAxes, isAdhocColumn, - isFeatureEnabled, isPhysicalColumn, QueryFormColumn, QueryMode, @@ -290,22 +288,19 @@ const config: ControlPanelConfig = { }, }, ], - isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) || - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) - ? [ - { - name: 'server_pagination', - config: { - type: 'CheckboxControl', - label: t('Server pagination'), - description: t( - 'Enable server side pagination of results (experimental feature)', - ), - default: false, - }, - }, - ] - : [], + [ + { + name: 'server_pagination', + config: { + type: 'CheckboxControl', + label: t('Server pagination'), + description: t( + 'Enable server side pagination of results (experimental feature)', + ), + default: false, + }, + }, + ], [ { name: 'row_limit', diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx index 179c03c996f10..08cfed16f622d 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import fetchMock from 'fetch-mock'; import { render } from 'spec/helpers/testing-library'; import { fireEvent, within } from '@testing-library/react'; -import * as uiCore from '@superset-ui/core'; import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder'; import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth'; import { @@ -247,35 +246,20 @@ describe('DashboardBuilder', () => { expect(await findByAltText('Loading...')).toBeVisible(); }); - describe('when nativeFiltersEnabled', () => { - let isFeatureEnabledMock: jest.MockInstance; - beforeAll(() => { - isFeatureEnabledMock = jest - .spyOn(uiCore, 'isFeatureEnabled') - .mockImplementation( - flag => flag === uiCore.FeatureFlag.DASHBOARD_NATIVE_FILTERS, - ); - }); - - afterAll(() => { - isFeatureEnabledMock.mockRestore(); - }); - - it('should set FilterBar width by useStoredSidebarWidth', () => { - const expectedValue = 200; - const setter = jest.fn(); - (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [ - expectedValue, - setter, - ]); - const { getByTestId } = setup({ - dashboardInfo: { - ...mockState.dashboardInfo, - dash_edit_perm: true, - }, - }); - const filterbar = getByTestId('dashboard-filters-panel'); - expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`); + it('should set FilterBar width by useStoredSidebarWidth', () => { + const expectedValue = 200; + const setter = jest.fn(); + (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [ + expectedValue, + setter, + ]); + const { getByTestId } = setup({ + dashboardInfo: { + ...mockState.dashboardInfo, + dash_edit_perm: true, + }, }); + const filterbar = getByTestId('dashboard-filters-panel'); + expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`); }); }); diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx index f3f214468e833..5f03eeaf1a8fa 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx @@ -21,11 +21,9 @@ import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { - FeatureFlag, Filter, Filters, getCategoricalSchemeRegistry, - isFeatureEnabled, SupersetClient, useComponentDidUpdate, } from '@superset-ui/core'; @@ -104,10 +102,7 @@ const DashboardContainer: FC = ({ topLevelTabs }) => { }, [dashboardLayout, directPathToChild]); useEffect(() => { - if ( - !isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) || - nativeFilterScopes.length === 0 - ) { + if (nativeFilterScopes.length === 0) { return; } const scopes = nativeFilterScopes.map(filterScope => { diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts index 1936e331c3db2..9b8af1dda30f5 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts @@ -17,7 +17,6 @@ * under the License. */ import { useSelector } from 'react-redux'; -import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; import { useCallback, useEffect, useState } from 'react'; import { URL_PARAMS } from 'src/constants'; import { getUrlParam } from 'src/utils/urlUtils'; @@ -42,8 +41,7 @@ export const useNativeFilters = () => { ); const nativeFiltersEnabled = - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - (canEdit || (!canEdit && filterValues.length !== 0)); + canEdit || (!canEdit && filterValues.length !== 0); const requiredFirstFilter = filterValues.filter( filter => filter.requiredFirst, diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/FiltersBadge.test.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/FiltersBadge.test.tsx index 058c5329b6ba0..c5e0d0df9c37b 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/FiltersBadge.test.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/FiltersBadge.test.tsx @@ -133,10 +133,6 @@ describe('FiltersBadge', () => { }); it('shows the indicator when filters have been applied', () => { - // @ts-ignore - global.featureFlags = { - [SupersetUI.FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true, - }; const store = getMockStoreWithNativeFilters(); // start with basic dashboard state, dispatch an event to simulate query completion store.dispatch({ diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index deea296d4c04f..cefdbafa73d48 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -24,12 +24,8 @@ import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import { HeaderDropdownProps } from 'src/dashboard/components/Header/types'; import injectCustomCss from 'src/dashboard/util/injectCustomCss'; -import { FeatureFlag } from '@superset-ui/core'; -import * as uiCore from '@superset-ui/core'; import HeaderActionsDropdown from '.'; -let isFeatureEnabledMock: jest.MockInstance; - const createProps = () => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), @@ -135,7 +131,7 @@ test('should render the menu items', async () => { test('should render the menu items in edit mode', async () => { setup(editModeOnProps); - expect(screen.getAllByRole('menuitem')).toHaveLength(5); + expect(screen.getAllByRole('menuitem')).toHaveLength(4); expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); expect(screen.getByText('Edit properties')).toBeInTheDocument(); expect(screen.getByText('Edit CSS')).toBeInTheDocument(); @@ -150,56 +146,14 @@ test('should render the menu items in Embedded mode', async () => { expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); }); -describe('with native filters feature flag disabled', () => { - beforeAll(() => { - isFeatureEnabledMock = jest - .spyOn(uiCore, 'isFeatureEnabled') - .mockImplementation( - (featureFlag: FeatureFlag) => - featureFlag !== FeatureFlag.DASHBOARD_NATIVE_FILTERS, - ); - }); - - afterAll(() => { - // @ts-ignore - isFeatureEnabledMock.restore(); - }); - - it('should render filter mapping in edit mode if explicit filter scopes undefined', async () => { - setup(editModeOnProps); - expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); - }); - - it('should render filter mapping in edit mode if explicit filter scopes defined', async () => { - setup(editModeOnWithFilterScopesProps); - expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); - }); +test('should not render filter mapping in edit mode if explicit filter scopes undefined', async () => { + setup(editModeOnProps); + expect(screen.queryByText('Set filter mapping')).not.toBeInTheDocument(); }); -describe('with native filters feature flag enabled', () => { - beforeAll(() => { - isFeatureEnabledMock = jest - .spyOn(uiCore, 'isFeatureEnabled') - .mockImplementation( - (featureFlag: FeatureFlag) => - featureFlag === FeatureFlag.DASHBOARD_NATIVE_FILTERS, - ); - }); - - afterAll(() => { - // @ts-ignore - isFeatureEnabledMock.restore(); - }); - - it('should not render filter mapping in edit mode if explicit filter scopes undefined', async () => { - setup(editModeOnProps); - expect(screen.queryByText('Set filter mapping')).not.toBeInTheDocument(); - }); - - it('should render filter mapping in edit mode if explicit filter scopes defined', async () => { - setup(editModeOnWithFilterScopesProps); - expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); - }); +test('should render filter mapping in edit mode if explicit filter scopes defined', async () => { + setup(editModeOnWithFilterScopesProps); + expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); }); test('should show the share actions', async () => { diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx index b0d3fc251e9d9..f1a3f59039bd4 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx @@ -19,12 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { isEmpty } from 'lodash'; -import { - isFeatureEnabled, - FeatureFlag, - SupersetClient, - t, -} from '@superset-ui/core'; +import { SupersetClient, t } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; import { URL_PARAMS } from 'src/constants'; import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; @@ -361,18 +356,14 @@ class HeaderActionsDropdown extends React.PureComponent { ) ) : null} - {editMode && - !( - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - isEmpty(dashboardInfo?.metadata?.filter_scopes) - ) && ( - - - - )} + {editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && ( + + + + )} (FILTERS_CONFIG_MODAL_TEST_ID, true); const FILTER_NAME = 'Time filter 1'; -// @ts-ignore -global.featureFlags = { - [FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true, -}; - const addFilterFlow = async () => { // open filter config modal userEvent.click(screen.getByTestId(getTestId('collapsable'))); @@ -291,10 +286,6 @@ describe('FilterBar', () => { }); it('create filter and apply it flow', async () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true, - }; renderWrapper(openedBarProps, stateWithoutNativeFilters); expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx index c74d1b08a967e..629ec75cb000c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -212,23 +212,20 @@ const FilterControls: FC = ({ selectedCrossFilters.at(-1), ), })); - if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { - const nativeFiltersInScope = filtersInScope.map((filter, index) => ({ - id: filter.id, - element: ( -
- {renderer(filter, index)} -
- ), - })); - return [...crossFilters, ...nativeFiltersInScope]; - } - return [...crossFilters]; + const nativeFiltersInScope = filtersInScope.map((filter, index) => ({ + id: filter.id, + element: ( +
+ {renderer(filter, index)} +
+ ), + })); + return [...crossFilters, ...nativeFiltersInScope]; }, [filtersInScope, renderer, rendererCrossFilter, selectedCrossFilters]); const renderHorizontalContent = () => ( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx index d839cd4d39404..7cd4877d11e2c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx @@ -17,14 +17,7 @@ * under the License. */ /* eslint-disable no-param-reassign */ -import { - FeatureFlag, - css, - isFeatureEnabled, - styled, - t, - useTheme, -} from '@superset-ui/core'; +import { css, styled, t, useTheme } from '@superset-ui/core'; import React, { FC, useMemo } from 'react'; import Icons from 'src/components/Icons'; import Button from 'src/components/Button'; @@ -124,7 +117,7 @@ const Header: FC = ({ toggleFiltersBar }) => { - {canEdit && isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && ( + {canEdit && ( = ({ : []; const hasFilters = filterValues.length > 0 || selectedCrossFilters.length > 0; - const actionsElement = useMemo( - () => - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) ? actions : null, - [actions], - ); - return ( @@ -143,7 +137,7 @@ const HorizontalFilterBar: React.FC = ({ ) : ( <> - {canEdit && isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && ( + {canEdit && ( = ({ onFilterSelectionChange={onSelectionChange} /> )} - {actionsElement} + {actions} )} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx index efad04a0388d7..09625151a2c2d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/HorizontalFilterBar.test.tsx @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { FeatureFlag, NativeFilterType } from '@superset-ui/core'; +import { NativeFilterType } from '@superset-ui/core'; import React from 'react'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import HorizontalBar from './Horizontal'; @@ -32,11 +31,6 @@ const defaultProps = { onSelectionChange: jest.fn(), }; -// @ts-ignore -global.featureFlags = { - [FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true, -}; - const renderWrapper = (overrideProps?: Record) => waitFor(() => render(, { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index b2da460502979..998c460dfc0f2 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -196,12 +196,6 @@ const VerticalFilterBar: React.FC = ({ [], ); - const actionsElement = useMemo( - () => - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) ? actions : null, - [actions], - ); - return ( = ({
<> {crossFilters} - {isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - filterControls} + {filterControls}
)} - {actionsElement} + {actions}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index a8bfafd8341bc..ae8a2f7a4df8e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts @@ -358,34 +358,31 @@ export const selectNativeIndicatorsForChart = ( return cachedNativeIndicatorsForChart[chartId]; } - let nativeFilterIndicators: any = []; - if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { - nativeFilterIndicators = - nativeFilters && - Object.values(nativeFilters) - .filter( - nativeFilter => - nativeFilter.type === NativeFilterType.NATIVE_FILTER && - nativeFilter.chartsInScope?.includes(chartId), - ) - .map(nativeFilter => { - const column = nativeFilter.targets?.[0]?.column?.name; - const filterState = dataMask[nativeFilter.id]?.filterState; - const label = extractLabel(filterState); - return { + const nativeFilterIndicators = + nativeFilters && + Object.values(nativeFilters) + .filter( + nativeFilter => + nativeFilter.type === NativeFilterType.NATIVE_FILTER && + nativeFilter.chartsInScope?.includes(chartId), + ) + .map(nativeFilter => { + const column = nativeFilter.targets?.[0]?.column?.name; + const filterState = dataMask[nativeFilter.id]?.filterState; + const label = extractLabel(filterState); + return { + column, + name: nativeFilter.name, + path: [nativeFilter.id], + status: getStatus({ + label, column, - name: nativeFilter.name, - path: [nativeFilter.id], - status: getStatus({ - label, - column, - rejectedColumns, - appliedColumns, - }), - value: label, - }; - }); - } + rejectedColumns, + appliedColumns, + }), + value: label, + }; + }); let crossFilterIndicators: any = []; if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts index 0388565595ff4..be636bf17a4af 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts @@ -55,41 +55,7 @@ describe('nativeFilterGate', () => { }); }); - describe('with only native filters feature flag enabled', () => { - beforeAll(() => { - isFeatureEnabledMock = jest - .spyOn(uiCore, 'isFeatureEnabled') - .mockImplementation( - (featureFlag: FeatureFlag) => - featureFlag === FeatureFlag.DASHBOARD_NATIVE_FILTERS, - ); - }); - - afterAll(() => { - // @ts-ignore - isFeatureEnabledMock.restore(); - }); - - it('should return true for regular chart', () => { - expect(nativeFilterGate([])).toEqual(true); - }); - - it('should return true for cross filter chart', () => { - expect(nativeFilterGate([Behavior.INTERACTIVE_CHART])).toEqual(true); - }); - - it('should return false for native filter chart with cross filter support', () => { - expect( - nativeFilterGate([Behavior.NATIVE_FILTER, Behavior.INTERACTIVE_CHART]), - ).toEqual(false); - }); - - it('should return false for native filter behavior', () => { - expect(nativeFilterGate([Behavior.NATIVE_FILTER])).toEqual(false); - }); - }); - - describe('with native filters and experimental feature flag enabled', () => { + describe('with cross filters and experimental feature flag enabled', () => { beforeAll(() => { isFeatureEnabledMock = jest .spyOn(uiCore, 'isFeatureEnabled') diff --git a/superset-frontend/src/dataMask/actions.ts b/superset-frontend/src/dataMask/actions.ts index 7c41703a10bfb..cb7ed393d732c 100644 --- a/superset-frontend/src/dataMask/actions.ts +++ b/superset-frontend/src/dataMask/actions.ts @@ -16,13 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { - DataMask, - isFeatureEnabled, - FeatureFlag, - FilterConfiguration, - Filters, -} from '@superset-ui/core'; +import { DataMask, FilterConfiguration, Filters } from '@superset-ui/core'; import { getInitialDataMask } from './reducer'; export const CLEAR_DATA_MASK_STATE = 'CLEAR_DATA_MASK_STATE'; @@ -73,14 +67,10 @@ export function updateDataMask( filterId: string | number, dataMask: DataMask, ): UpdateDataMask { - // Only apply data mask if one of the relevant features is enabled - const isFeatureFlagActive = - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) || - isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS); return { type: UPDATE_DATA_MASK, filterId, - dataMask: isFeatureFlagActive ? dataMask : {}, + dataMask, }; } diff --git a/superset/config.py b/superset/config.py index 4d0eda4b2f048..1d6409deb8ac0 100644 --- a/superset/config.py +++ b/superset/config.py @@ -428,7 +428,6 @@ class D3Format(TypedDict, total=False): "LISTVIEWS_DEFAULT_CARD_VIEW": False, # When True, this escapes HTML (rather than rendering it) in Markdown components "ESCAPE_MARKDOWN_HTML": False, - "DASHBOARD_NATIVE_FILTERS": True, # deprecated "DASHBOARD_CROSS_FILTERS": True, # deprecated "DASHBOARD_VIRTUALIZATION": True, "GLOBAL_ASYNC_QUERIES": False, diff --git a/tests/integration_tests/superset_test_config.py b/tests/integration_tests/superset_test_config.py index 89287be663e55..2aeeb3004e184 100644 --- a/tests/integration_tests/superset_test_config.py +++ b/tests/integration_tests/superset_test_config.py @@ -69,7 +69,6 @@ "SHARE_QUERIES_VIA_KV_STORE": True, "ENABLE_TEMPLATE_PROCESSING": True, "ALERT_REPORTS": True, - "DASHBOARD_NATIVE_FILTERS": True, "DRILL_TO_DETAIL": True, "DRILL_BY": True, "HORIZONTAL_FILTER_BAR": True, From effd73f2cc5bd4afbc830ae98feca568c773e4a5 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Sat, 20 Jan 2024 00:00:43 +0100 Subject: [PATCH 08/11] fix: Catch ImportErrors for Google SDKs (#25550) --- superset/db_engine_specs/bigquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/db_engine_specs/bigquery.py b/superset/db_engine_specs/bigquery.py index f3b9c486e98e5..8e7ed0bf7d061 100644 --- a/superset/db_engine_specs/bigquery.py +++ b/superset/db_engine_specs/bigquery.py @@ -52,7 +52,7 @@ from google.oauth2 import service_account dependencies_installed = True -except ModuleNotFoundError: +except ImportError: dependencies_installed = False try: From 12838038449d4c4efdad749a2fe8f6d936e4375e Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Fri, 19 Jan 2024 15:12:54 -0800 Subject: [PATCH 09/11] chore: add unique constraint to tagged_objects (#26654) --- superset/daos/tag.py | 21 ++++- superset/migrations/__init__.py | 7 ++ superset/migrations/migration_utils.py | 46 +++++++++ ...e3017c6_tagged_object_unique_constraint.py | 89 ++++++++++++++++++ ...18_12-12_15a2c68a2e6b_merging_two_heads.py | 38 ++++++++ .../2024-01-19_08-42_1cf8e4344e2b_merging.py | 38 ++++++++ superset/tags/models.py | 93 +++++++++++++++---- tests/integration_tests/tags/api_tests.py | 22 +++-- .../integration_tests/tags/commands_tests.py | 12 +-- 9 files changed, 335 insertions(+), 31 deletions(-) create mode 100644 superset/migrations/migration_utils.py create mode 100644 superset/migrations/versions/2024-01-17_13-09_96164e3017c6_tagged_object_unique_constraint.py create mode 100644 superset/migrations/versions/2024-01-18_12-12_15a2c68a2e6b_merging_two_heads.py create mode 100644 superset/migrations/versions/2024-01-19_08-42_1cf8e4344e2b_merging.py diff --git a/superset/daos/tag.py b/superset/daos/tag.py index e4aa89181644d..46a1d2538f16a 100644 --- a/superset/daos/tag.py +++ b/superset/daos/tag.py @@ -51,14 +51,29 @@ def create_custom_tagged_objects( object_type: ObjectType, object_id: int, tag_names: list[str] ) -> None: tagged_objects = [] - for name in tag_names: + + # striping and de-dupping + clean_tag_names: set[str] = {tag.strip() for tag in tag_names} + + for name in clean_tag_names: type_ = TagType.custom - tag_name = name.strip() - tag = TagDAO.get_by_name(tag_name, type_) + tag = TagDAO.get_by_name(name, type_) tagged_objects.append( TaggedObject(object_id=object_id, object_type=object_type, tag=tag) ) + # Check if the association already exists + existing_tagged_object = ( + db.session.query(TaggedObject) + .filter_by(object_id=object_id, object_type=object_type, tag=tag) + .first() + ) + + if not existing_tagged_object: + tagged_objects.append( + TaggedObject(object_id=object_id, object_type=object_type, tag=tag) + ) + db.session.add_all(tagged_objects) db.session.commit() diff --git a/superset/migrations/__init__.py b/superset/migrations/__init__.py index 13a83393a9124..b083f44bb43e7 100644 --- a/superset/migrations/__init__.py +++ b/superset/migrations/__init__.py @@ -14,3 +14,10 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + +import os +import sys + +# hack to be able to import / reuse migration_utils.py in revisions +module_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(module_dir) diff --git a/superset/migrations/migration_utils.py b/superset/migrations/migration_utils.py new file mode 100644 index 0000000000000..c754669a1af69 --- /dev/null +++ b/superset/migrations/migration_utils.py @@ -0,0 +1,46 @@ +# 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. + +from alembic.operations import BatchOperations, Operations + +naming_convention = { + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", +} + + +def create_unique_constraint( + op: Operations, index_id: str, table_name: str, uix_columns: list[str] +) -> None: + with op.batch_alter_table( + table_name, naming_convention=naming_convention + ) as batch_op: + batch_op.create_unique_constraint(index_id, uix_columns) + + +def drop_unique_constraint(op: Operations, index_id: str, table_name: str) -> None: + dialect = op.get_bind().dialect.name + + with op.batch_alter_table( + table_name, naming_convention=naming_convention + ) as batch_op: + if dialect == "mysql": + # MySQL requires specifying the type of constraint + batch_op.drop_constraint(index_id, type_="unique") + else: + # For other databases, a standard drop_constraint call is sufficient + batch_op.drop_constraint(index_id) diff --git a/superset/migrations/versions/2024-01-17_13-09_96164e3017c6_tagged_object_unique_constraint.py b/superset/migrations/versions/2024-01-17_13-09_96164e3017c6_tagged_object_unique_constraint.py new file mode 100644 index 0000000000000..0b67ad5024f75 --- /dev/null +++ b/superset/migrations/versions/2024-01-17_13-09_96164e3017c6_tagged_object_unique_constraint.py @@ -0,0 +1,89 @@ +# 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 enum + +import migration_utils as utils +import sqlalchemy as sa +from alembic import op +from sqlalchemy import Column, Enum, Integer, MetaData, Table +from sqlalchemy.sql import and_, func, select + +# revision identifiers, used by Alembic. +revision = "96164e3017c6" +down_revision = "59a1450b3c10" + + +class ObjectType(enum.Enum): + # pylint: disable=invalid-name + query = 1 + chart = 2 + dashboard = 3 + dataset = 4 + + +# Define the tagged_object table structure +metadata = MetaData() +tagged_object_table = Table( + "tagged_object", + metadata, + Column("id", Integer, primary_key=True), + Column("tag_id", Integer), + Column("object_id", Integer), + Column("object_type", Enum(ObjectType)), # Replace ObjectType with your Enum +) + +index_id = "uix_tagged_object" +table_name = "tagged_object" +uix_columns = ["tag_id", "object_id", "object_type"] + + +def upgrade(): + bind = op.get_bind() # Get the database connection bind + + # Reflect the current database state to get existing tables + metadata.reflect(bind=bind) + + # Delete duplicates if any + min_id_subquery = ( + select( + [ + func.min(tagged_object_table.c.id).label("min_id"), + tagged_object_table.c.tag_id, + tagged_object_table.c.object_id, + tagged_object_table.c.object_type, + ] + ) + .group_by( + tagged_object_table.c.tag_id, + tagged_object_table.c.object_id, + tagged_object_table.c.object_type, + ) + .alias("min_ids") + ) + + delete_query = tagged_object_table.delete().where( + tagged_object_table.c.id.notin_(select([min_id_subquery.c.min_id])) + ) + + bind.execute(delete_query) + + # Create unique constraint + utils.create_unique_constraint(op, index_id, table_name, uix_columns) + + +def downgrade(): + utils.drop_unique_constraint(op, index_id, table_name) diff --git a/superset/migrations/versions/2024-01-18_12-12_15a2c68a2e6b_merging_two_heads.py b/superset/migrations/versions/2024-01-18_12-12_15a2c68a2e6b_merging_two_heads.py new file mode 100644 index 0000000000000..7904d9298df47 --- /dev/null +++ b/superset/migrations/versions/2024-01-18_12-12_15a2c68a2e6b_merging_two_heads.py @@ -0,0 +1,38 @@ +# 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. +"""merging two heads + +Revision ID: 15a2c68a2e6b +Revises: ('96164e3017c6', 'a32e0c4d8646') +Create Date: 2024-01-18 12:12:52.174742 + +""" + +# revision identifiers, used by Alembic. +revision = "15a2c68a2e6b" +down_revision = ("96164e3017c6", "a32e0c4d8646") + +import sqlalchemy as sa +from alembic import op + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/migrations/versions/2024-01-19_08-42_1cf8e4344e2b_merging.py b/superset/migrations/versions/2024-01-19_08-42_1cf8e4344e2b_merging.py new file mode 100644 index 0000000000000..9ac2a9b24ff5d --- /dev/null +++ b/superset/migrations/versions/2024-01-19_08-42_1cf8e4344e2b_merging.py @@ -0,0 +1,38 @@ +# 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. +"""merging + +Revision ID: 1cf8e4344e2b +Revises: ('e863403c0c50', '15a2c68a2e6b') +Create Date: 2024-01-19 08:42:37.694192 + +""" + +# revision identifiers, used by Alembic. +revision = "1cf8e4344e2b" +down_revision = ("e863403c0c50", "15a2c68a2e6b") + +import sqlalchemy as sa +from alembic import op + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/tags/models.py b/superset/tags/models.py index bae4417507bd4..1e8ca7de1a332 100644 --- a/superset/tags/models.py +++ b/superset/tags/models.py @@ -21,10 +21,21 @@ from flask import escape from flask_appbuilder import Model -from sqlalchemy import Column, Enum, ForeignKey, Integer, orm, String, Table, Text +from sqlalchemy import ( + Column, + Enum, + exists, + ForeignKey, + Integer, + orm, + String, + Table, + Text, +) from sqlalchemy.engine.base import Connection from sqlalchemy.orm import relationship, sessionmaker from sqlalchemy.orm.mapper import Mapper +from sqlalchemy.schema import UniqueConstraint from superset import security_manager from superset.models.helpers import AuditMixinNullable @@ -110,6 +121,14 @@ class TaggedObject(Model, AuditMixinNullable): object_type = Column(Enum(ObjectType)) tag = relationship("Tag", back_populates="objects", overlaps="tags") + __table_args__ = ( + UniqueConstraint( + "tag_id", "object_id", "object_type", name="uix_tagged_object" + ), + ) + + def __str__(self) -> str: + return f"" def get_tag(name: str, session: orm.Session, type_: TagType) -> Tag: @@ -138,7 +157,7 @@ def get_object_type(class_name: str) -> ObjectType: class ObjectUpdater: - object_type: str | None = None + object_type: str = "default" @classmethod def get_owners_ids( @@ -146,6 +165,19 @@ def get_owners_ids( ) -> list[int]: raise NotImplementedError("Subclass should implement `get_owners_ids`") + @classmethod + def get_owner_tag_ids( + cls, + session: orm.Session, + target: Dashboard | FavStar | Slice | Query | SqlaTable, + ) -> set[int]: + tag_ids = set() + for owner_id in cls.get_owners_ids(target): + name = f"owner:{owner_id}" + tag = get_tag(name, session, TagType.owner) + tag_ids.add(tag.id) + return tag_ids + @classmethod def _add_owners( cls, @@ -153,10 +185,28 @@ def _add_owners( target: Dashboard | FavStar | Slice | Query | SqlaTable, ) -> None: for owner_id in cls.get_owners_ids(target): - name = f"owner:{owner_id}" + name: str = f"owner:{owner_id}" tag = get_tag(name, session, TagType.owner) + cls.add_tag_object_if_not_tagged( + session, tag_id=tag.id, object_id=target.id, object_type=cls.object_type + ) + + @classmethod + def add_tag_object_if_not_tagged( + cls, session: orm.Session, tag_id: int, object_id: int, object_type: str + ) -> None: + # Check if the object is already tagged + exists_query = exists().where( + TaggedObject.tag_id == tag_id, + TaggedObject.object_id == object_id, + TaggedObject.object_type == object_type, + ) + already_tagged = session.query(exists_query).scalar() + + # Add TaggedObject to the session if it isn't already tagged + if not already_tagged: tagged_object = TaggedObject( - tag_id=tag.id, object_id=target.id, object_type=cls.object_type + tag_id=tag_id, object_id=object_id, object_type=object_type ) session.add(tagged_object) @@ -173,10 +223,9 @@ def after_insert( # add `type:` tags tag = get_tag(f"type:{cls.object_type}", session, TagType.type) - tagged_object = TaggedObject( - tag_id=tag.id, object_id=target.id, object_type=cls.object_type + cls.add_tag_object_if_not_tagged( + session, tag_id=tag.id, object_id=target.id, object_type=cls.object_type ) - session.add(tagged_object) session.commit() @classmethod @@ -187,23 +236,35 @@ def after_update( target: Dashboard | FavStar | Slice | Query | SqlaTable, ) -> None: with Session(bind=connection) as session: - # delete current `owner:` tags - query = ( - session.query(TaggedObject.id) + # Fetch current owner tags + existing_tags = ( + session.query(TaggedObject) .join(Tag) .filter( TaggedObject.object_type == cls.object_type, TaggedObject.object_id == target.id, Tag.type == TagType.owner, ) + .all() ) - ids = [row[0] for row in query] - session.query(TaggedObject).filter(TaggedObject.id.in_(ids)).delete( - synchronize_session=False - ) + existing_owner_tag_ids = {tag.tag_id for tag in existing_tags} - # add `owner:` tags - cls._add_owners(session, target) + # Determine new owner IDs + new_owner_tag_ids = cls.get_owner_tag_ids(session, target) + + # Add missing tags + for owner_tag_id in new_owner_tag_ids - existing_owner_tag_ids: + tagged_object = TaggedObject( + tag_id=owner_tag_id, + object_id=target.id, + object_type=cls.object_type, + ) + session.add(tagged_object) + + # Remove unnecessary tags + for tag in existing_tags: + if tag.tag_id not in new_owner_tag_ids: + session.delete(tag) session.commit() @classmethod diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py index 863288a3e73ec..d79261c2a3022 100644 --- a/tests/integration_tests/tags/api_tests.py +++ b/tests/integration_tests/tags/api_tests.py @@ -577,15 +577,25 @@ def test_post_bulk_tag(self): result = TagDAO.get_tagged_objects_for_tags(tags, ["chart"]) assert len(result) == 1 - tagged_objects = db.session.query(TaggedObject).filter( - TaggedObject.object_id == dashboard.id, - TaggedObject.object_type == ObjectType.dashboard, + tagged_objects = ( + db.session.query(TaggedObject) + .join(Tag) + .filter( + TaggedObject.object_id == dashboard.id, + TaggedObject.object_type == ObjectType.dashboard, + Tag.type == TagType.custom, + ) ) assert tagged_objects.count() == 2 - tagged_objects = db.session.query(TaggedObject).filter( - TaggedObject.object_id == chart.id, - TaggedObject.object_type == ObjectType.chart, + tagged_objects = ( + db.session.query(TaggedObject) + .join(Tag) + .filter( + TaggedObject.object_id == chart.id, + TaggedObject.object_type == ObjectType.chart, + Tag.type == TagType.custom, + ) ) assert tagged_objects.count() == 2 diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py index 83762f8f6e876..3644c076e6a3d 100644 --- a/tests/integration_tests/tags/commands_tests.py +++ b/tests/integration_tests/tags/commands_tests.py @@ -63,7 +63,7 @@ def test_create_custom_tag_command(self): example_dashboard = ( db.session.query(Dashboard).filter_by(slug="world_health").one() ) - example_tags = ["create custom tag example 1", "create custom tag example 2"] + example_tags = {"create custom tag example 1", "create custom tag example 2"} command = CreateCustomTagCommand( ObjectType.dashboard.value, example_dashboard.id, example_tags ) @@ -78,7 +78,7 @@ def test_create_custom_tag_command(self): ) .all() ) - assert example_tags == [tag.name for tag in created_tags] + assert example_tags == {tag.name for tag in created_tags} # cleanup tags = db.session.query(Tag).filter(Tag.name.in_(example_tags)) @@ -99,7 +99,7 @@ def test_delete_tags_command(self): .filter_by(dashboard_title="World Bank's Data") .one() ) - example_tags = ["create custom tag example 1", "create custom tag example 2"] + example_tags = {"create custom tag example 1", "create custom tag example 2"} command = CreateCustomTagCommand( ObjectType.dashboard.value, example_dashboard.id, example_tags ) @@ -115,7 +115,7 @@ def test_delete_tags_command(self): .order_by(Tag.name) .all() ) - assert example_tags == [tag.name for tag in created_tags] + assert example_tags == {tag.name for tag in created_tags} command = DeleteTagsCommand(example_tags) command.run() @@ -132,7 +132,7 @@ def test_delete_tags_command(self): example_dashboard = ( db.session.query(Dashboard).filter_by(slug="world_health").one() ) - example_tags = ["create custom tag example 1", "create custom tag example 2"] + example_tags = {"create custom tag example 1", "create custom tag example 2"} command = CreateCustomTagCommand( ObjectType.dashboard.value, example_dashboard.id, example_tags ) @@ -152,7 +152,7 @@ def test_delete_tags_command(self): command = DeleteTaggedObjectCommand( object_type=ObjectType.dashboard.value, object_id=example_dashboard.id, - tag=example_tags[0], + tag=list(example_tags)[0], ) command.run() tagged_objects = ( From e86d4d3c92b91da0aa09bc215908c23f261b4f9a Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Fri, 19 Jan 2024 15:13:18 -0800 Subject: [PATCH 10/11] fix(import): only import FORMULA annotations (#26652) --- superset/cli/importexport.py | 3 +- superset/commands/chart/importers/v1/utils.py | 18 ++++++++++ .../charts/commands_tests.py | 1 + tests/integration_tests/cli_tests.py | 12 ++++--- .../fixtures/importexport.py | 35 +++++++++++++++++++ .../commands/importers/v1/import_test.py | 19 ++++++++++ 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/superset/cli/importexport.py b/superset/cli/importexport.py index 3d4f87dd26bf8..fc6a9ad3c4682 100755 --- a/superset/cli/importexport.py +++ b/superset/cli/importexport.py @@ -130,12 +130,13 @@ def export_datasources(datasource_file: Optional[str] = None) -> None: @click.option( "--path", "-p", + required=True, help="Path to a single ZIP file", ) @click.option( "--username", "-u", - default=None, + required=True, help="Specify the user name to assign dashboards to", ) def import_dashboards(path: str, username: Optional[str]) -> None: diff --git a/superset/commands/chart/importers/v1/utils.py b/superset/commands/chart/importers/v1/utils.py index d27b631f97fde..f905f8cc3ad4f 100644 --- a/superset/commands/chart/importers/v1/utils.py +++ b/superset/commands/chart/importers/v1/utils.py @@ -28,6 +28,22 @@ from superset.migrations.shared.migrate_viz import processors from superset.migrations.shared.migrate_viz.base import MigrateViz from superset.models.slice import Slice +from superset.utils.core import AnnotationType + + +def filter_chart_annotations(chart_config: dict[str, Any]) -> None: + """ + Mutating the chart's config params to keep only the annotations of + type FORMULA. + TODO: + handle annotation dependencies on either other charts or + annotation layers objects. + """ + params = chart_config.get("params", {}) + als = params.get("annotation_layers", []) + params["annotation_layers"] = [ + al for al in als if al.get("annotationType") == AnnotationType.FORMULA + ] def import_chart( @@ -47,6 +63,8 @@ def import_chart( "Chart doesn't exist and user doesn't have permission to create charts" ) + filter_chart_annotations(config) + # TODO (betodealmeida): move this logic to import_from_dict config["params"] = json.dumps(config["params"]) diff --git a/tests/integration_tests/charts/commands_tests.py b/tests/integration_tests/charts/commands_tests.py index 87c7823ae5ab8..b6adf197f5751 100644 --- a/tests/integration_tests/charts/commands_tests.py +++ b/tests/integration_tests/charts/commands_tests.py @@ -190,6 +190,7 @@ def test_import_v1_chart(self, sm_g, utils_g): ) dataset = chart.datasource assert json.loads(chart.params) == { + "annotation_layers": [], "color_picker": {"a": 1, "b": 135, "g": 122, "r": 0}, "datasource": dataset.uid, "js_columns": ["color"], diff --git a/tests/integration_tests/cli_tests.py b/tests/integration_tests/cli_tests.py index 2441809b0da64..1b2c5f8b4eeb1 100644 --- a/tests/integration_tests/cli_tests.py +++ b/tests/integration_tests/cli_tests.py @@ -160,7 +160,8 @@ def test_import_dashboards_versioned_export(import_dashboards_command, app_conte runner = app.test_cli_runner() response = runner.invoke( - superset.cli.importexport.import_dashboards, ("-p", "dashboards.json") + superset.cli.importexport.import_dashboards, + ("-p", "dashboards.json", "-u", "admin"), ) assert response.exit_code == 0 @@ -174,7 +175,8 @@ def test_import_dashboards_versioned_export(import_dashboards_command, app_conte runner = app.test_cli_runner() response = runner.invoke( - superset.cli.importexport.import_dashboards, ("-p", "dashboards.zip") + superset.cli.importexport.import_dashboards, + ("-p", "dashboards.zip", "-u", "admin"), ) assert response.exit_code == 0 @@ -205,7 +207,8 @@ def test_failing_import_dashboards_versioned_export( runner = app.test_cli_runner() response = runner.invoke( - superset.cli.importexport.import_dashboards, ("-p", "dashboards.json") + superset.cli.importexport.import_dashboards, + ("-p", "dashboards.json", "-u", "admin"), ) assert_cli_fails_properly(response, caplog) @@ -217,7 +220,8 @@ def test_failing_import_dashboards_versioned_export( runner = app.test_cli_runner() response = runner.invoke( - superset.cli.importexport.import_dashboards, ("-p", "dashboards.zip") + superset.cli.importexport.import_dashboards, + ("-p", "dashboards.zip", "-u", "admin"), ) assert_cli_fails_properly(response, caplog) diff --git a/tests/integration_tests/fixtures/importexport.py b/tests/integration_tests/fixtures/importexport.py index 8fa61539dcc4b..5096c272335bc 100644 --- a/tests/integration_tests/fixtures/importexport.py +++ b/tests/integration_tests/fixtures/importexport.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from copy import deepcopy from typing import Any # example V0 import/export format @@ -575,6 +576,40 @@ "version": "1.0.0", "dataset_uuid": "10808100-158b-42c4-842e-f32b99d88dfb", } +chart_config_with_mixed_annotations: dict[str, Any] = deepcopy(chart_config) +chart_config_with_mixed_annotations["params"]["annotation_layers"] = [ + { + "name": "Formula test layer", + "annotationType": "FORMULA", + "color": None, + "descriptionColumns": [], + "hideLine": False, + "opacity": "", + "overrides": {"time_range": None}, + "show": True, + "showLabel": False, + "showMarkers": False, + "style": "solid", + "value": "100000", + "width": 1, + }, + { + "name": "Native layer to be removed on import", + "annotationType": "EVENT", + "sourceType": "NATIVE", + "color": None, + "opacity": "", + "style": "solid", + "width": 1, + "showMarkers": False, + "hideLine": False, + "value": 2, + "overrides": {"time_range": None}, + "show": True, + "showLabel": False, + "descriptionColumns": [], + }, +] dashboard_config: dict[str, Any] = { "dashboard_title": "Test dash", diff --git a/tests/unit_tests/charts/commands/importers/v1/import_test.py b/tests/unit_tests/charts/commands/importers/v1/import_test.py index f0d142644df25..903b7468baf21 100644 --- a/tests/unit_tests/charts/commands/importers/v1/import_test.py +++ b/tests/unit_tests/charts/commands/importers/v1/import_test.py @@ -108,3 +108,22 @@ def test_import_chart_without_permission( str(excinfo.value) == "Chart doesn't exist and user doesn't have permission to create charts" ) + + +def test_filter_chart_annotations(mocker: MockFixture, session: Session) -> None: + """ + Test importing a chart. + """ + from superset import security_manager + from superset.commands.chart.importers.v1.utils import filter_chart_annotations + from tests.integration_tests.fixtures.importexport import ( + chart_config_with_mixed_annotations, + ) + + config = copy.deepcopy(chart_config_with_mixed_annotations) + filter_chart_annotations(config) + params = config["params"] + annotation_layers = params["annotation_layers"] + + assert len(annotation_layers) == 1 + assert all([al["annotationType"] == "FORMULA" for al in annotation_layers]) From d34874cf2bbd6385d2ca6f38856d3d10d6fd745b Mon Sep 17 00:00:00 2001 From: Denis Krivenko Date: Sun, 21 Jan 2024 18:43:17 +0100 Subject: [PATCH 11/11] feat(helm): Upgrade default Superset version to 3.1.0 (#26707) --- helm/superset/Chart.yaml | 4 ++-- helm/superset/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml index cbca942569349..019b2a5159f21 100644 --- a/helm/superset/Chart.yaml +++ b/helm/superset/Chart.yaml @@ -15,7 +15,7 @@ # limitations under the License. # apiVersion: v2 -appVersion: "3.0.1" +appVersion: "3.1.0" description: Apache Superset is a modern, enterprise-ready business intelligence web application name: superset icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x @@ -29,7 +29,7 @@ maintainers: - name: craig-rueda email: craig@craigrueda.com url: https://github.com/craig-rueda -version: 0.11.2 +version: 0.12.0 dependencies: - name: postgresql version: 12.1.6 diff --git a/helm/superset/README.md b/helm/superset/README.md index 1eaf4928c158a..acb8f10716d63 100644 --- a/helm/superset/README.md +++ b/helm/superset/README.md @@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs # superset -![Version: 0.11.2](https://img.shields.io/badge/Version-0.11.2-informational?style=flat-square) +![Version: 0.12.0](https://img.shields.io/badge/Version-0.12.0-informational?style=flat-square) Apache Superset is a modern, enterprise-ready business intelligence web application