From c440d98fadaf5ee1859bd8c4a01036fb71f1753a Mon Sep 17 00:00:00 2001 From: simcha90 <56388545+simcha90@users.noreply.github.com> Date: Mon, 8 Feb 2021 13:26:58 +0200 Subject: [PATCH] refactor(native-filters): refactor code of native filters (#12889) * refactor(native-filters): refactor code of native filters * refactor: update refactor dependencies * refactor: update refactor dependencies * lint: fix lint * fix: merge with master * chore: fix selector --- .../spec/fixtures/mockNativeFilters.ts | 6 +- .../nativeFilters/FilterBar_spec.tsx | 2 +- .../FilterConfigurationLink_spec.tsx | 2 +- .../nativeFilters/FilterScope_spec.tsx | 4 +- .../nativeFilters/NativeFiltersModal_spec.tsx | 2 +- .../src/dashboard/actions/nativeFilters.ts | 4 +- .../dashboard/components/DashboardBuilder.jsx | 2 +- .../components/FiltersBadge/selectors.ts | 5 +- .../FilterBar/CascadeFilterControl.tsx | 81 +++++ .../{ => FilterBar}/CascadePopover.tsx | 7 +- .../{ => FilterBar}/FilterBar.tsx | 256 +-------------- .../FilterConfigurationLink.tsx | 5 +- .../nativeFilters/FilterBar/FilterControl.tsx | 68 ++++ .../nativeFilters/FilterBar/FilterValue.tsx | 151 +++++++++ .../nativeFilters/FilterBar/state.ts | 69 +++++ .../nativeFilters/FilterBar/types.ts | 37 +++ .../nativeFilters/FilterBar/utils.ts | 53 ++++ .../CancelConfirmationAlert.tsx | 0 .../{ => FilterConfigModal}/ColumnSelect.tsx | 0 .../FilterConfigForm.tsx | 11 +- .../FilterConfigModal.tsx | 5 +- .../{ => FilterConfigModal}/FilterScope.tsx | 12 +- .../{ => FilterConfigModal}/FiltersList.tsx | 2 +- .../{ => FilterConfigModal}/ScopingTree.tsx | 5 +- .../nativeFilters/FilterConfigModal/state.ts | 128 ++++++++ .../nativeFilters/FilterConfigModal/types.ts | 70 +++++ .../nativeFilters/FilterConfigModal/utils.ts | 194 ++++++++++++ .../components/nativeFilters/state.ts | 164 +--------- .../components/nativeFilters/types.ts | 92 +----- .../components/nativeFilters/utils.ts | 293 +++--------------- .../src/dashboard/reducers/nativeFilters.ts | 11 +- .../src/dashboard/reducers/types.ts | 23 ++ .../util/activeDashboardNativeFilters.ts | 3 +- .../charts/getFormDataWithExtraFilters.ts | 2 +- 34 files changed, 968 insertions(+), 801 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterBar}/CascadePopover.tsx (96%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterBar}/FilterBar.tsx (55%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterBar}/FilterConfigurationLink.tsx (92%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/CancelConfirmationAlert.tsx (100%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/ColumnSelect.tsx (100%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/FilterConfigForm.tsx (98%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/FilterConfigModal.tsx (99%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/FilterScope.tsx (92%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/FiltersList.tsx (97%) rename superset-frontend/src/dashboard/components/nativeFilters/{ => FilterConfigModal}/ScopingTree.tsx (94%) create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/state.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/types.ts create mode 100644 superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/utils.ts diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts b/superset-frontend/spec/fixtures/mockNativeFilters.ts index 24872d8a6..8addc87b2 100644 --- a/superset-frontend/spec/fixtures/mockNativeFilters.ts +++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts @@ -16,10 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { - FilterType, - NativeFiltersState, -} from 'src/dashboard/components/nativeFilters/types'; +import { FilterType } from 'src/dashboard/components/nativeFilters/types'; +import { NativeFiltersState } from 'src/dashboard/reducers/types'; export const nativeFilters: NativeFiltersState = { filters: { diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx index 7e6e9b04a..97959f371 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { styledMount as mount } from 'spec/helpers/theming'; import { Provider } from 'react-redux'; -import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar'; +import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar/FilterBar'; import Button from 'src/components/Button'; import { mockStore } from 'spec/fixtures/mockStore'; diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterConfigurationLink_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterConfigurationLink_spec.tsx index 1879f1622..5382ec5cf 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterConfigurationLink_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterConfigurationLink_spec.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { styledMount as mount } from 'spec/helpers/theming'; import { Provider } from 'react-redux'; -import FilterConfigurationLink from 'src/dashboard/components/nativeFilters/FilterConfigurationLink'; +import FilterConfigurationLink from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink'; import { mockStore } from 'spec/fixtures/mockStore'; describe('FilterConfigurationButton', () => { diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterScope_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterScope_spec.tsx index 2d3eb3e07..d1c89499b 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterScope_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterScope_spec.tsx @@ -21,8 +21,8 @@ import { Provider } from 'react-redux'; import { render, screen, fireEvent } from 'spec/helpers/testing-library'; import { mockStoreWithChartsInTabsAndRoot } from 'spec/fixtures/mockStore'; import { Form, FormInstance } from 'src/common/components'; -import { NativeFiltersForm } from 'src/dashboard/components/nativeFilters/types'; -import FilterConfigForm from 'src/dashboard/components/nativeFilters/FilterConfigForm'; +import { NativeFiltersForm } from 'src/dashboard/components/nativeFilters/FilterConfigModal/types'; +import FilterConfigForm from 'src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm'; describe('FilterScope', () => { const save = jest.fn(); diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx index a3329c3fc..8432288c2 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx @@ -22,7 +22,7 @@ import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; import { Provider } from 'react-redux'; import Alert from 'react-bootstrap/lib/Alert'; -import { FilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterConfigModal'; +import { FilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigModal'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import { mockStore } from 'spec/fixtures/mockStore'; diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index a327ebaea..09a2e2c0a 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -20,12 +20,12 @@ import { ExtraFormData, makeApi } from '@superset-ui/core'; import { Dispatch } from 'redux'; import { - CurrentFilterState, Filter, FilterConfiguration, - SelectedValues, } from 'src/dashboard/components/nativeFilters/types'; import { dashboardInfoChanged } from './dashboardInfo'; +import { CurrentFilterState } from '../reducers/types'; +import { SelectedValues } from '../components/nativeFilters/FilterConfigModal/types'; export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN'; export interface SetFilterConfigBegin { diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index f14c30172..2e3b3e238 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -47,7 +47,7 @@ import { DASHBOARD_ROOT_ID, DASHBOARD_ROOT_DEPTH, } from '../util/constants'; -import FilterBar from './nativeFilters/FilterBar'; +import FilterBar from './nativeFilters/FilterBar/FilterBar'; import { StickyVerticalBar } from './StickyVerticalBar'; const TABS_HEIGHT = 47; diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts index 4a4779aaa..5b04e249b 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts +++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts @@ -18,10 +18,7 @@ */ import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters'; import { TIME_FILTER_MAP } from '../../../visualizations/FilterBox/FilterBox'; -import { - NativeFiltersState, - FilterState as NativeFilterState, -} from '../nativeFilters/types'; +import { NativeFiltersState, NativeFilterState } from '../../reducers/types'; export enum IndicatorStatus { Unset = 'UNSET', diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx new file mode 100644 index 000000000..0a2f32c01 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilterControl.tsx @@ -0,0 +1,81 @@ +/** + * 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 { ExtraFormData, styled } from '@superset-ui/core'; +import Icon from 'src/components/Icon'; +import { CurrentFilterState } from 'src/dashboard/reducers/types'; +import FilterControl from './FilterControl'; +import { Filter } from '../types'; +import { CascadeFilter } from './types'; + +interface CascadeFilterControlProps { + filter: CascadeFilter; + directPathToChild?: string[]; + onFilterSelectionChange: ( + filter: Filter, + extraFormData: ExtraFormData, + currentState: CurrentFilterState, + ) => void; +} + +const StyledCascadeChildrenList = styled.ul` + list-style-type: none; + & > * { + list-style-type: none; + } +`; + +const StyledFilterControlBox = styled.div` + display: flex; +`; + +const StyledCaretIcon = styled(Icon)` + margin-top: ${({ theme }) => -theme.gridUnit}px; +`; + +const CascadeFilterControl: React.FC = ({ + filter, + directPathToChild, + onFilterSelectionChange, +}) => ( + <> + + + + + + + {filter.cascadeChildren?.map(childFilter => ( +
  • + +
  • + ))} +
    + +); + +export default CascadeFilterControl; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/CascadePopover.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx similarity index 96% rename from superset-frontend/src/dashboard/components/nativeFilters/CascadePopover.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx index b131ba6ad..b5a9432b0 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/CascadePopover.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadePopover.tsx @@ -21,9 +21,12 @@ import { ExtraFormData, styled, t } from '@superset-ui/core'; import Popover from 'src/common/components/Popover'; import Icon from 'src/components/Icon'; import { Pill } from 'src/dashboard/components/FiltersBadge/Styles'; -import { CascadeFilterControl, FilterControl } from './FilterBar'; -import { Filter, CascadeFilter, CurrentFilterState } from './types'; +import { CurrentFilterState } from 'src/dashboard/reducers/types'; import { useFilterState } from './state'; +import FilterControl from './FilterControl'; +import CascadeFilterControl from './CascadeFilterControl'; +import { CascadeFilter } from './types'; +import { Filter } from '../types'; interface CascadePopoverProps { filter: CascadeFilter; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx similarity index 55% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 9f1f46bc9..ed1df92cc 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -16,36 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import { - QueryFormData, - styled, - SuperChart, - t, - ExtraFormData, -} from '@superset-ui/core'; -import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { styled, t, ExtraFormData } from '@superset-ui/core'; +import React, { useState, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import cx from 'classnames'; import Button from 'src/components/Button'; import Icon from 'src/components/Icon'; -import { getChartDataRequest } from 'src/chart/chartAction'; -import { areObjectsEqual } from 'src/reduxUtils'; -import Loading from 'src/components/Loading'; -import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; +import { CurrentFilterState } from 'src/dashboard/reducers/types'; import FilterConfigurationLink from './FilterConfigurationLink'; -import { - useCascadingFilters, - useFilterConfiguration, - useFilters, - useFilterState, - useSetExtraFormData, -} from './state'; -import { Filter, CascadeFilter, CurrentFilterState } from './types'; -import { - buildCascadeFiltersTree, - getFormData, - mapParentFiltersToChildren, -} from './utils'; +import { useFilters, useSetExtraFormData } from './state'; +import { useFilterConfiguration } from '../state'; +import { Filter } from '../types'; +import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils'; import CascadePopover from './CascadePopover'; const barWidth = `250px`; @@ -57,10 +39,6 @@ const BarWrapper = styled.div` } `; -const FilterItem = styled.div` - padding-bottom: 10px; -`; - const Bar = styled.div` position: absolute; top: 0; @@ -157,232 +135,12 @@ const FilterControls = styled.div` padding: ${({ theme }) => theme.gridUnit * 4}px; `; -const StyledCascadeChildrenList = styled.ul` - list-style-type: none; - & > * { - list-style-type: none; - } -`; - -const StyledFilterControlTitle = styled.h4` - width: 100%; - font-size: ${({ theme }) => theme.typography.sizes.s}px; - color: ${({ theme }) => theme.colors.grayscale.dark1}; - margin: 0; - overflow-wrap: break-word; -`; - -const StyledFilterControlTitleBox = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - margin-bottom: ${({ theme }) => theme.gridUnit}px; -`; - -const StyledFilterControlContainer = styled.div` - width: 100%; -`; - -const StyledFilterControlBox = styled.div` - display: flex; -`; - -const StyledCaretIcon = styled(Icon)` - margin-top: ${({ theme }) => -theme.gridUnit}px; -`; - -const StyledLoadingBox = styled.div` - position: relative; - height: ${({ theme }) => theme.gridUnit * 8}px; - margin-bottom: ${({ theme }) => theme.gridUnit * 6}px; -`; - -interface FilterProps { - filter: Filter; - icon?: React.ReactElement; - directPathToChild?: string[]; - onFilterSelectionChange: ( - filter: Filter, - extraFormData: ExtraFormData, - currentState: CurrentFilterState, - ) => void; -} - interface FiltersBarProps { filtersOpen: boolean; toggleFiltersBar: any; directPathToChild?: string[]; } -const FilterValue: React.FC = ({ - filter, - directPathToChild, - onFilterSelectionChange, -}) => { - const { - id, - allowsMultipleValues, - inverseSelection, - targets, - defaultValue, - filterType, - } = filter; - const cascadingFilters = useCascadingFilters(id); - const filterState = useFilterState(id); - const [loading, setLoading] = useState(true); - const [state, setState] = useState([]); - const [error, setError] = useState(false); - const [formData, setFormData] = useState>({}); - const inputRef = useRef(null); - const [target] = targets; - const { datasetId = 18, column } = target; - const { name: groupby } = column; - const currentValue = filterState.currentState?.value; - useEffect(() => { - const newFormData = getFormData({ - datasetId, - cascadingFilters, - groupby, - allowsMultipleValues, - defaultValue, - currentValue, - inverseSelection, - }); - if (!areObjectsEqual(formData || {}, newFormData)) { - setFormData(newFormData); - getChartDataRequest({ - formData: newFormData, - force: false, - requestParams: { dashboardId: 0 }, - }) - .then(response => { - setState(response.result); - setError(false); - setLoading(false); - }) - .catch(() => { - setError(true); - setLoading(false); - }); - } - }, [cascadingFilters, datasetId, groupby, defaultValue, currentValue]); - - useEffect(() => { - if (directPathToChild?.[0] === filter.id) { - // wait for Cascade Popover to open - const timeout = setTimeout(() => { - inputRef?.current?.focus(); - }, 200); - return () => clearTimeout(timeout); - } - return undefined; - }, [inputRef, directPathToChild, filter.id]); - - const setExtraFormData = ({ - extraFormData, - currentState, - }: { - extraFormData: ExtraFormData; - currentState: CurrentFilterState; - }) => onFilterSelectionChange(filter, extraFormData, currentState); - - if (loading) { - return ( - - - - ); - } - - if (error) { - return ( - - ); - } - - return ( - - - - ); -}; - -export const FilterControl: React.FC = ({ - filter, - icon, - onFilterSelectionChange, - directPathToChild, -}) => { - const { name = '' } = filter; - return ( - - - - {name} - -
    {icon}
    -
    - -
    - ); -}; - -interface CascadeFilterControlProps { - filter: CascadeFilter; - directPathToChild?: string[]; - onFilterSelectionChange: ( - filter: Filter, - extraFormData: ExtraFormData, - currentState: CurrentFilterState, - ) => void; -} - -export const CascadeFilterControl: React.FC = ({ - filter, - directPathToChild, - onFilterSelectionChange, -}) => ( - <> - - - - - - - {filter.cascadeChildren?.map(childFilter => ( -
  • - -
  • - ))} -
    - -); - const FilterBar: React.FC = ({ filtersOpen, toggleFiltersBar, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigurationLink.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink.tsx similarity index 92% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterConfigurationLink.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink.tsx index bcd27005a..2e0b8bd8b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigurationLink.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink.tsx @@ -18,10 +18,9 @@ */ import React, { useState } from 'react'; import { useDispatch } from 'react-redux'; -// import shortid from 'shortid'; import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters'; -import { FilterConfigModal } from './FilterConfigModal'; -import { FilterConfiguration } from './types'; +import { FilterConfigModal } from '../FilterConfigModal/FilterConfigModal'; +import { FilterConfiguration } from '../types'; export interface FCBProps { createNewOnOpen?: boolean; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx new file mode 100644 index 000000000..157f50e09 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControl.tsx @@ -0,0 +1,68 @@ +/** + * 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 { styled } from '@superset-ui/core'; +import FilterValue from './FilterValue'; +import { FilterProps } from './types'; + +const StyledFilterControlTitle = styled.h4` + width: 100%; + font-size: ${({ theme }) => theme.typography.sizes.s}px; + color: ${({ theme }) => theme.colors.grayscale.dark1}; + margin: 0; + overflow-wrap: break-word; +`; + +const StyledFilterControlTitleBox = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: ${({ theme }) => theme.gridUnit}px; +`; + +const StyledFilterControlContainer = styled.div` + width: 100%; +`; + +const FilterControl: React.FC = ({ + filter, + icon, + onFilterSelectionChange, + directPathToChild, +}) => { + const { name = '' } = filter; + return ( + + + + {name} + +
    {icon}
    +
    + +
    + ); +}; + +export default FilterControl; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx new file mode 100644 index 000000000..6d25f874e --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx @@ -0,0 +1,151 @@ +/** + * 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, { useEffect, useRef, useState } from 'react'; +import { + ExtraFormData, + QueryFormData, + styled, + SuperChart, + t, +} from '@superset-ui/core'; +import { areObjectsEqual } from 'src/reduxUtils'; +import { getChartDataRequest } from 'src/chart/chartAction'; +import Loading from 'src/components/Loading'; +import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; +import { CurrentFilterState } from 'src/dashboard/reducers/types'; +import { FilterProps } from './types'; +import { getFormData } from '../utils'; +import { useCascadingFilters, useFilterState } from './state'; + +const StyledLoadingBox = styled.div` + position: relative; + height: ${({ theme }) => theme.gridUnit * 8}px; + margin-bottom: ${({ theme }) => theme.gridUnit * 6}px; +`; + +const FilterItem = styled.div` + padding-bottom: 10px; +`; + +const FilterValue: React.FC = ({ + filter, + directPathToChild, + onFilterSelectionChange, +}) => { + const { + id, + allowsMultipleValues, + inverseSelection, + targets, + defaultValue, + filterType, + } = filter; + const cascadingFilters = useCascadingFilters(id); + const filterState = useFilterState(id); + const [loading, setLoading] = useState(true); + const [state, setState] = useState([]); + const [error, setError] = useState(false); + const [formData, setFormData] = useState>({}); + const inputRef = useRef(null); + const [target] = targets; + const { datasetId = 18, column } = target; + const { name: groupby } = column; + const currentValue = filterState.currentState?.value; + useEffect(() => { + const newFormData = getFormData({ + datasetId, + cascadingFilters, + groupby, + allowsMultipleValues, + defaultValue, + currentValue, + inverseSelection, + }); + if (!areObjectsEqual(formData || {}, newFormData)) { + setFormData(newFormData); + getChartDataRequest({ + formData: newFormData, + force: false, + requestParams: { dashboardId: 0 }, + }) + .then(response => { + setState(response.result); + setError(false); + setLoading(false); + }) + .catch(() => { + setError(true); + setLoading(false); + }); + } + }, [cascadingFilters, datasetId, groupby, defaultValue, currentValue]); + + useEffect(() => { + if (directPathToChild?.[0] === filter.id) { + // wait for Cascade Popover to open + const timeout = setTimeout(() => { + inputRef?.current?.focus(); + }, 200); + return () => clearTimeout(timeout); + } + return undefined; + }, [inputRef, directPathToChild, filter.id]); + + const setExtraFormData = ({ + extraFormData, + currentState, + }: { + extraFormData: ExtraFormData; + currentState: CurrentFilterState; + }) => onFilterSelectionChange(filter, extraFormData, currentState); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + ); + } + + return ( + + + + ); +}; + +export default FilterValue; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts new file mode 100644 index 000000000..af3c473b6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/state.ts @@ -0,0 +1,69 @@ +/** + * 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 { useDispatch, useSelector } from 'react-redux'; +import { useCallback } from 'react'; +import { ExtraFormData } from '@superset-ui/core'; +import { setExtraFormData } from 'src/dashboard/actions/nativeFilters'; +import { getInitialFilterState } from 'src/dashboard/reducers/nativeFilters'; +import { + CurrentFilterState, + NativeFilterState, + NativeFiltersState, +} from 'src/dashboard/reducers/types'; +import { mergeExtraFormData } from '../utils'; + +export function useFilters() { + return useSelector( + state => state.nativeFilters.filters, + ); +} + +export function useSetExtraFormData() { + const dispatch = useDispatch(); + return useCallback( + ( + id: string, + extraFormData: ExtraFormData, + currentState: CurrentFilterState, + ) => dispatch(setExtraFormData(id, extraFormData, currentState)), + [dispatch], + ); +} + +export function useCascadingFilters(id: string) { + const nativeFilters = useSelector( + state => state.nativeFilters, + ); + const { filters, filtersState } = nativeFilters; + const filter = filters[id]; + const cascadeParentIds = filter?.cascadeParentIds ?? []; + let cascadedFilters = {}; + cascadeParentIds.forEach(parentId => { + const parentState = filtersState[parentId] || {}; + const { extraFormData: parentExtra = {} } = parentState; + cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra); + }); + return cascadedFilters; +} + +export function useFilterState(id: string) { + return useSelector( + state => state.nativeFilters.filtersState[id] || getInitialFilterState(id), + ); +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts new file mode 100644 index 000000000..b0d8a2760 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/types.ts @@ -0,0 +1,37 @@ +/** + * 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 { ExtraFormData } from '@superset-ui/core'; +import { CurrentFilterState } from 'src/dashboard/reducers/types'; +import { Filter } from '../types'; + +export interface FilterProps { + filter: Filter; + icon?: React.ReactElement; + directPathToChild?: string[]; + onFilterSelectionChange: ( + filter: Filter, + extraFormData: ExtraFormData, + currentState: CurrentFilterState, + ) => void; +} + +export interface CascadeFilter extends Filter { + cascadeChildren: CascadeFilter[]; +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts new file mode 100644 index 000000000..4052cd931 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/utils.ts @@ -0,0 +1,53 @@ +import { Filter } from '../types'; +import { CascadeFilter } from './types'; + +/** + * 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 function mapParentFiltersToChildren( + filters: Filter[], +): { [id: string]: Filter[] } { + const cascadeChildren = {}; + filters.forEach(filter => { + const [parentId] = filter.cascadeParentIds || []; + if (parentId) { + if (!cascadeChildren[parentId]) { + cascadeChildren[parentId] = []; + } + cascadeChildren[parentId].push(filter); + } + }); + return cascadeChildren; +} + +export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { + const cascadeChildren = mapParentFiltersToChildren(filters); + + const getCascadeFilter = (filter: Filter): CascadeFilter => { + const children = cascadeChildren[filter.id] || []; + return { + ...filter, + cascadeChildren: children.map(getCascadeFilter), + }; + }; + + return filters + .filter(filter => !filter.cascadeParentIds?.length) + .map(getCascadeFilter); +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/CancelConfirmationAlert.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/CancelConfirmationAlert.tsx similarity index 100% rename from superset-frontend/src/dashboard/components/nativeFilters/CancelConfirmationAlert.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/CancelConfirmationAlert.tsx diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ColumnSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/ColumnSelect.tsx similarity index 100% rename from superset-frontend/src/dashboard/components/nativeFilters/ColumnSelect.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/ColumnSelect.tsx diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx similarity index 98% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterConfigForm.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx index 60119751d..181f440bb 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx @@ -31,15 +31,12 @@ import SupersetResourceSelect from 'src/components/SupersetResourceSelect'; import { addDangerToast } from 'src/messageToasts/actions'; import { ClientErrorObject } from 'src/utils/getClientErrorObject'; import { ColumnSelect } from './ColumnSelect'; -import { Filter, FilterType, NativeFiltersForm } from './types'; +import { NativeFiltersForm } from './types'; import FilterScope from './FilterScope'; -import { - FilterTypeNames, - getFormData, - setFilterFieldValues, - useForceUpdate, -} from './utils'; +import { FilterTypeNames, setFilterFieldValues, useForceUpdate } from './utils'; import { useBackendFormUpdate } from './state'; +import { getFormData } from '../utils'; +import { Filter, FilterType } from '../types'; type DatasetSelectValue = { value: number; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigModal.tsx similarity index 99% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigModal.tsx index 10dfc5a6e..8e7b4f3d6 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigModal.tsx @@ -28,10 +28,11 @@ import Button from 'src/components/Button'; import { LineEditableTabs } from 'src/common/components/Tabs'; import { usePrevious } from 'src/common/hooks/usePrevious'; import ErrorBoundary from 'src/components/ErrorBoundary'; -import { useFilterConfigMap, useFilterConfiguration } from './state'; +import { useFilterConfigMap, useFilterConfiguration } from '../state'; import FilterConfigForm from './FilterConfigForm'; -import { FilterConfiguration, NativeFiltersForm } from './types'; +import { NativeFiltersForm } from './types'; import { CancelConfirmationAlert } from './CancelConfirmationAlert'; +import { FilterConfiguration } from '../types'; // how long to show the "undo" button when removing a filter const REMOVAL_DELAY_SECS = 5; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterScope.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterScope.tsx similarity index 92% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterScope.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterScope.tsx index 75a438287..e3c94e8be 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterScope.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterScope.tsx @@ -20,16 +20,12 @@ import React, { FC } from 'react'; import { t, styled } from '@superset-ui/core'; import { Radio } from 'src/common/components/Radio'; -import { - Form, - Typography, - Space, - FormInstance, -} from '../../../common/components'; -import { Filter, NativeFiltersForm, Scoping } from './types'; +import { Form, Typography, Space, FormInstance } from 'src/common/components'; +import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; +import { NativeFiltersForm, Scoping } from './types'; import ScopingTree from './ScopingTree'; -import { DASHBOARD_ROOT_ID } from '../../util/constants'; import { isScopingAll, setFilterFieldValues, useForceUpdate } from './utils'; +import { Filter } from '../types'; type FilterScopeProps = { filterId: string; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersList.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FiltersList.tsx similarity index 97% rename from superset-frontend/src/dashboard/components/nativeFilters/FiltersList.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FiltersList.tsx index f85e31b0a..09a84f691 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersList.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FiltersList.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { styled } from '@superset-ui/core'; import { Button } from 'src/common/components'; import Icon from 'src/components/Icon'; -import { useFilterConfiguration } from './state'; +import { useFilterConfiguration } from '../state'; interface Args { filter: any; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ScopingTree.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/ScopingTree.tsx similarity index 94% rename from superset-frontend/src/dashboard/components/nativeFilters/ScopingTree.tsx rename to superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/ScopingTree.tsx index 467159c63..7e7d471a9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/ScopingTree.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/ScopingTree.tsx @@ -19,15 +19,16 @@ import React, { FC, useMemo, useState } from 'react'; import { FormInstance, Tree } from 'src/common/components'; +import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; import { useFilterScopeTree } from './state'; -import { DASHBOARD_ROOT_ID } from '../../util/constants'; import { findFilterScope, getTreeCheckedItems, setFilterFieldValues, useForceUpdate, } from './utils'; -import { NativeFiltersForm, Scope } from './types'; +import { NativeFiltersForm } from './types'; +import { Scope } from '../types'; type ScopingTreeProps = { form: FormInstance; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/state.ts new file mode 100644 index 000000000..d17a58774 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/state.ts @@ -0,0 +1,128 @@ +/** + * 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 { useEffect, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { t } from '@superset-ui/core'; +import { Charts, Layout, RootState } from 'src/dashboard/types'; +import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; +import { + CHART_TYPE, + DASHBOARD_ROOT_TYPE, +} from 'src/dashboard/util/componentTypes'; +import { FormInstance } from 'antd/lib/form'; +import { getChartDataRequest } from 'src/chart/chartAction'; +import { NativeFilterState } from 'src/dashboard/reducers/types'; +import { NativeFiltersForm, TreeItem } from './types'; +import { buildTree, setFilterFieldValues, useForceUpdate } from './utils'; +import { Filter } from '../types'; +import { getFormData } from '../utils'; + +export function useFiltersState() { + return useSelector( + state => state.nativeFilters.filtersState, + ); +} + +export function useFilterScopeTree(): { + treeData: [TreeItem]; + layout: Layout; +} { + const layout = useSelector( + ({ dashboardLayout: { present } }) => present, + ); + + const charts = useSelector(({ charts }) => charts); + const tree = { + children: [], + key: DASHBOARD_ROOT_ID, + type: DASHBOARD_ROOT_TYPE, + title: t('All panels'), + }; + + // We need to get only nodes that have charts as children or grandchildren + const validNodes = useMemo( + () => + Object.values(layout).reduce((acc, cur) => { + if (cur?.type === CHART_TYPE) { + return [...new Set([...acc, ...cur?.parents, cur.id])]; + } + return acc; + }, []), + [layout], + ); + + useMemo(() => { + buildTree(layout[DASHBOARD_ROOT_ID], tree, layout, charts, validNodes); + }, [charts, layout, tree]); + + return { treeData: [tree], layout }; +} + +// When some fields in form changed we need re-fetch data for Filter defaultValue +export const useBackendFormUpdate = ( + form: FormInstance, + filterId: string, + filterToEdit?: Filter, +) => { + const forceUpdate = useForceUpdate(); + const formFilter = (form.getFieldValue('filters') || {})[filterId]; + useEffect(() => { + let resolvedDefaultValue: any = null; + // No need to check data set change because it cascading update column + // So check that column exists is enough + if (!formFilter?.column) { + setFilterFieldValues(form, filterId, { + defaultValueQueriesData: [], + defaultValue: resolvedDefaultValue, + }); + return; + } + const formData = getFormData({ + datasetId: formFilter?.dataset?.value, + groupby: formFilter?.column, + allowsMultipleValues: formFilter?.allowsMultipleValues, + defaultValue: formFilter?.defaultValue, + inverseSelection: formFilter?.inverseSelection, + }); + getChartDataRequest({ + formData, + force: false, + requestParams: { dashboardId: 0 }, + }).then(response => { + if ( + filterToEdit?.filterType === formFilter?.filterType && + filterToEdit?.targets[0].datasetId === formFilter?.dataset?.value && + formFilter?.column === filterToEdit?.targets[0]?.column?.name && + filterToEdit?.allowsMultipleValues === formFilter?.allowsMultipleValues + ) { + resolvedDefaultValue = filterToEdit?.defaultValue; + } + setFilterFieldValues(form, filterId, { + defaultValueQueriesData: response.result, + defaultValue: resolvedDefaultValue, + }); + forceUpdate(); + }); + }, [ + formFilter?.filterType, + formFilter?.column, + formFilter?.dataset?.value, + filterId, + ]); +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/types.ts new file mode 100644 index 000000000..a18dcfaf6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/types.ts @@ -0,0 +1,70 @@ +/** + * 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 { QueryObjectFilterClause } from '@superset-ui/core'; +import { Column, FilterType, Scope } from '../types'; + +export enum Scoping { + all, + specific, +} + +// Using to pass setState React callbacks directly to And components +export type AntCallback = (value1?: any, value2?: any) => void; + +interface NativeFiltersFormItem { + scope: Scope; + name: string; + filterType: FilterType; + dataset: { + value: number; + label: string; + }; + column: string; + defaultValue: any; + parentFilter: { + value: string; + label: string; + }; + inverseSelection: boolean; + isInstant: boolean; + allowsMultipleValues: boolean; + isRequired: boolean; +} + +export interface NativeFiltersForm { + filters: Record; +} + +export type SelectedValues = string[] | null; + +export type AllFilterState = { + column: Column; + datasetId: number; + datasource: string; + id: string; + selectedValues: SelectedValues; + filterClause?: QueryObjectFilterClause; +}; + +/** UI Ant tree type */ +export type TreeItem = { + children: TreeItem[]; + key: string; + title: string; +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/utils.ts new file mode 100644 index 000000000..b1740b0c6 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/utils.ts @@ -0,0 +1,194 @@ +/** + * 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 } from '@superset-ui/core'; +import { Charts, Layout, LayoutItem } from 'src/dashboard/types'; +import { + CHART_TYPE, + DASHBOARD_ROOT_TYPE, + TAB_TYPE, +} from 'src/dashboard/util/componentTypes'; +import { FormInstance } from 'antd/lib/form'; +import React from 'react'; +import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; +import { TreeItem } from './types'; +import { FilterType, Scope } from '../types'; + +export const useForceUpdate = () => { + const [, updateState] = React.useState({}); + return React.useCallback(() => updateState({}), []); +}; + +export const isShowTypeInTree = ({ type, meta }: LayoutItem, charts?: Charts) => + (type === TAB_TYPE || type === CHART_TYPE || type === DASHBOARD_ROOT_TYPE) && + (!charts || charts[meta?.chartId]?.formData?.viz_type !== 'filter_box'); + +export const buildTree = ( + node: LayoutItem, + treeItem: TreeItem, + layout: Layout, + charts: Charts, + validNodes: string[], +) => { + let itemToPass: TreeItem = treeItem; + if ( + isShowTypeInTree(node, charts) && + node.type !== DASHBOARD_ROOT_TYPE && + validNodes.includes(node.id) + ) { + const currentTreeItem = { + key: node.id, + title: node.meta.sliceName || node.meta.text || node.id.toString(), + children: [], + }; + treeItem.children.push(currentTreeItem); + itemToPass = currentTreeItem; + } + node.children.forEach(child => + buildTree(layout[child], itemToPass, layout, charts, validNodes), + ); +}; + +const addInvisibleParents = (layout: Layout, item: string) => [ + ...(layout[item]?.children || []), + ...Object.values(layout) + .filter( + val => + val.parents && + val.parents[val.parents.length - 1] === item && + !isShowTypeInTree(layout[val.parents[val.parents.length - 1]]), + ) + .map(({ id }) => id), +]; + +// Generate checked options for Ant tree from redux scope +const checkTreeItem = ( + checkedItems: string[], + layout: Layout, + items: string[], + excluded: number[], +) => { + items.forEach(item => { + checkTreeItem( + checkedItems, + layout, + addInvisibleParents(layout, item), + excluded, + ); + if ( + layout[item]?.type === CHART_TYPE && + !excluded.includes(layout[item]?.meta.chartId) + ) { + checkedItems.push(item); + } + }); +}; + +export const getTreeCheckedItems = (scope: Scope, layout: Layout) => { + const checkedItems: string[] = []; + checkTreeItem(checkedItems, layout, [...scope.rootPath], [...scope.excluded]); + return [...new Set(checkedItems)]; +}; + +// Looking for first common parent for selected charts/tabs/tab +export const findFilterScope = ( + checkedKeys: string[], + layout: Layout, +): Scope => { + if (!checkedKeys.length) { + return { + rootPath: [], + excluded: [], + }; + } + + // Get arrays of parents for selected charts + const checkedItemParents = checkedKeys + .filter(item => layout[item]?.type === CHART_TYPE) + .map(key => { + const parents = [DASHBOARD_ROOT_ID, ...(layout[key]?.parents || [])]; + return parents.filter(parent => isShowTypeInTree(layout[parent])); + }); + // Sort arrays of parents to get first shortest array of parents, + // that means on it's level of parents located common parent, from this place parents start be different + checkedItemParents.sort((p1, p2) => p1.length - p2.length); + const rootPath = checkedItemParents.map( + parents => parents[checkedItemParents[0].length - 1], + ); + + const excluded: number[] = []; + const isExcluded = (parent: string, item: string) => + rootPath.includes(parent) && !checkedKeys.includes(item); + // looking for charts to be excluded: iterate over all charts + // and looking for charts that have one of their parents in `rootPath` and not in selected items + Object.entries(layout).forEach(([key, value]) => { + if ( + value.type === CHART_TYPE && + [DASHBOARD_ROOT_ID, ...value.parents]?.find(parent => + isExcluded(parent, key), + ) + ) { + excluded.push(value.meta.chartId); + } + }); + + return { + rootPath: [...new Set(rootPath)], + excluded, + }; +}; + +export const FilterTypeNames = { + [FilterType.filter_select]: t('Select'), + [FilterType.filter_range]: t('Range'), +}; + +export const setFilterFieldValues = ( + form: FormInstance, + filterId: string, + values: object, +) => { + const formFilters = form.getFieldValue('filters'); + form.setFieldsValue({ + filters: { + ...formFilters, + [filterId]: { + ...formFilters[filterId], + ...values, + }, + }, + }); +}; + +export const isScopingAll = (scope: Scope) => + !scope || (scope.rootPath[0] === DASHBOARD_ROOT_ID && !scope.excluded.length); + +type AppendFormData = { + filters: { + val?: number | string | null; + }[]; +}; + +export const extractDefaultValue = { + [FilterType.filter_select]: (appendFormData: AppendFormData) => + appendFormData.filters?.[0]?.val, + [FilterType.filter_range]: (appendFormData: AppendFormData) => ({ + min: appendFormData.filters?.[0].val, + max: appendFormData.filters?.[1].val, + }), +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/state.ts index 16ffa622e..d07b979f0 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/state.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/state.ts @@ -16,35 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { useCallback, useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { setExtraFormData } from 'src/dashboard/actions/nativeFilters'; -import { getInitialFilterState } from 'src/dashboard/reducers/nativeFilters'; -import { ExtraFormData, t } from '@superset-ui/core'; -import { Charts, Layout, RootState } from 'src/dashboard/types'; -import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants'; -import { - CHART_TYPE, - DASHBOARD_ROOT_TYPE, -} from 'src/dashboard/util/componentTypes'; -import { FormInstance } from 'antd/lib/form'; -import { getChartDataRequest } from 'src/chart/chartAction'; -import { - CurrentFilterState, - Filter, - FilterConfiguration, - FilterState, - NativeFiltersForm, - NativeFiltersState, - TreeItem, -} from './types'; -import { - buildTree, - getFormData, - mergeExtraFormData, - setFilterFieldValues, - useForceUpdate, -} from './utils'; +import { useSelector } from 'react-redux'; +import { useMemo } from 'react'; +import { Filter, FilterConfiguration } from './types'; const defaultFilterConfiguration: Filter[] = []; @@ -71,135 +45,3 @@ export function useFilterConfigMap() { [filterConfig], ); } - -export function useFilterState(id: string) { - return useSelector( - state => state.nativeFilters.filtersState[id] || getInitialFilterState(id), - ); -} - -export function useFiltersState() { - return useSelector( - state => state.nativeFilters.filtersState, - ); -} - -export function useFilters() { - return useSelector(state => state.nativeFilters.filters); -} - -export function useSetExtraFormData() { - const dispatch = useDispatch(); - return useCallback( - ( - id: string, - extraFormData: ExtraFormData, - currentState: CurrentFilterState, - ) => dispatch(setExtraFormData(id, extraFormData, currentState)), - [dispatch], - ); -} - -export function useFilterScopeTree(): { - treeData: [TreeItem]; - layout: Layout; -} { - const layout = useSelector( - ({ dashboardLayout: { present } }) => present, - ); - - const charts = useSelector(({ charts }) => charts); - const tree = { - children: [], - key: DASHBOARD_ROOT_ID, - type: DASHBOARD_ROOT_TYPE, - title: t('All panels'), - }; - - // We need to get only nodes that have charts as children or grandchildren - const validNodes = useMemo( - () => - Object.values(layout).reduce((acc, cur) => { - if (cur?.type === CHART_TYPE) { - return [...new Set([...acc, ...cur?.parents, cur.id])]; - } - return acc; - }, []), - [layout], - ); - - useMemo(() => { - buildTree(layout[DASHBOARD_ROOT_ID], tree, layout, charts, validNodes); - }, [charts, layout, tree]); - - return { treeData: [tree], layout }; -} - -export function useCascadingFilters(id: string) { - const nativeFilters = useSelector( - state => state.nativeFilters, - ); - const { filters, filtersState } = nativeFilters; - const filter = filters[id]; - const cascadeParentIds = filter?.cascadeParentIds ?? []; - let cascadedFilters = {}; - cascadeParentIds.forEach(parentId => { - const parentState = filtersState[parentId] || {}; - const { extraFormData: parentExtra = {} } = parentState; - cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra); - }); - return cascadedFilters; -} - -// When some fields in form changed we need re-fetch data for Filter defaultValue -export const useBackendFormUpdate = ( - form: FormInstance, - filterId: string, - filterToEdit?: Filter, -) => { - const forceUpdate = useForceUpdate(); - const formFilter = (form.getFieldValue('filters') || {})[filterId]; - useEffect(() => { - let resolvedDefaultValue: any = null; - // No need to check data set change because it cascading update column - // So check that column exists is enough - if (!formFilter?.column) { - setFilterFieldValues(form, filterId, { - defaultValueQueriesData: [], - defaultValue: resolvedDefaultValue, - }); - return; - } - const formData = getFormData({ - datasetId: formFilter?.dataset?.value, - groupby: formFilter?.column, - allowsMultipleValues: formFilter?.allowsMultipleValues, - defaultValue: formFilter?.defaultValue, - inverseSelection: formFilter?.inverseSelection, - }); - getChartDataRequest({ - formData, - force: false, - requestParams: { dashboardId: 0 }, - }).then(response => { - if ( - filterToEdit?.filterType === formFilter?.filterType && - filterToEdit?.targets[0].datasetId === formFilter?.dataset?.value && - formFilter?.column === filterToEdit?.targets[0]?.column?.name && - filterToEdit?.allowsMultipleValues === formFilter?.allowsMultipleValues - ) { - resolvedDefaultValue = filterToEdit?.defaultValue; - } - setFilterFieldValues(form, filterId, { - defaultValueQueriesData: response.result, - defaultValue: resolvedDefaultValue, - }); - forceUpdate(); - }); - }, [ - formFilter?.filterType, - formFilter?.column, - formFilter?.dataset?.value, - filterId, - ]); -}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/types.ts index ac90309a4..ecce3a6ba 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts @@ -16,43 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { - ExtraFormData, - JsonObject, - QueryObjectFilterClause, -} from '@superset-ui/core'; - -export enum Scoping { - all, - specific, -} - -// Using to pass setState React callbacks directly to And components -export type AntCallback = (value1?: any, value2?: any) => void; - -interface NativeFiltersFormItem { - scope: Scope; - name: string; - filterType: FilterType; - dataset: { - value: number; - label: string; - }; - column: string; - defaultValue: any; - parentFilter: { - value: string; - label: string; - }; - inverseSelection: boolean; - isInstant: boolean; - allowsMultipleValues: boolean; - isRequired: boolean; -} - -export interface NativeFiltersForm { - filters: Record; -} export interface Column { name: string; @@ -64,6 +27,11 @@ export interface Scope { excluded: number[]; } +export enum FilterType { + filter_select = 'filter_select', + filter_range = 'filter_range', +} + /** The target of a filter is the datasource/column being filtered */ export interface Target { datasetId: number; @@ -74,15 +42,6 @@ export interface Target { // clarityColumns?: Column[]; } -export enum FilterType { - filter_select = 'filter_select', - filter_range = 'filter_range', -} - -/** - * This is a filter configuration object, stored in the dashboard's json metadata. - * The values here do not reflect the current state of the filter. - */ export interface Filter { allowsMultipleValues: boolean; cascadeParentIds: string[]; @@ -100,45 +59,4 @@ export interface Filter { targets: [Target]; } -export interface CascadeFilter extends Filter { - cascadeChildren: CascadeFilter[]; -} - export type FilterConfiguration = Filter[]; - -export type SelectedValues = string[] | null; -export type CurrentFilterState = JsonObject & { - value: any; -}; - -/** Current state of the filter, stored in `nativeFilters` in redux */ -export type FilterState = { - id: string; // ties this filter state to the config object - extraFormData?: ExtraFormData; - currentState?: CurrentFilterState; -}; - -export type AllFilterState = { - column: Column; - datasetId: number; - datasource: string; - id: string; - selectedValues: SelectedValues; - filterClause?: QueryObjectFilterClause; -}; - -/** UI Ant tree type */ -export type TreeItem = { - children: TreeItem[]; - key: string; - title: string; -}; - -export type NativeFiltersState = { - filters: { - [filterId: string]: Filter; - }; - filtersState: { - [filterId: string]: FilterState; - }; -}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts index 3257ef0b4..b0815b632 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts @@ -16,153 +16,46 @@ * specific language governing permissions and limitations * under the License. */ -import { - ExtraFormData, - QueryFormData, - QueryObject, - t, -} from '@superset-ui/core'; -import { Charts, Layout, LayoutItem } from 'src/dashboard/types'; -import { - CHART_TYPE, - DASHBOARD_ROOT_TYPE, - TAB_TYPE, -} from 'src/dashboard/util/componentTypes'; -import { FormInstance } from 'antd/lib/form'; -import React, { RefObject } from 'react'; -import { - CascadeFilter, - Filter, - FilterType, - NativeFiltersState, - Scope, - TreeItem, -} from './types'; -import { DASHBOARD_ROOT_ID } from '../../util/constants'; +import { ExtraFormData, QueryFormData, QueryObject } from '@superset-ui/core'; +import { RefObject } from 'react'; +import { Filter } from './types'; +import { NativeFiltersState } from '../../reducers/types'; -export const useForceUpdate = () => { - const [, updateState] = React.useState({}); - return React.useCallback(() => updateState({}), []); -}; - -export const isShowTypeInTree = ({ type, meta }: LayoutItem, charts?: Charts) => - (type === TAB_TYPE || type === CHART_TYPE || type === DASHBOARD_ROOT_TYPE) && - (!charts || charts[meta?.chartId]?.formData?.viz_type !== 'filter_box'); - -export const buildTree = ( - node: LayoutItem, - treeItem: TreeItem, - layout: Layout, - charts: Charts, - validNodes: string[], -) => { - let itemToPass: TreeItem = treeItem; - if ( - isShowTypeInTree(node, charts) && - node.type !== DASHBOARD_ROOT_TYPE && - validNodes.includes(node.id) - ) { - const currentTreeItem = { - key: node.id, - title: node.meta.sliceName || node.meta.text || node.id.toString(), - children: [], - }; - treeItem.children.push(currentTreeItem); - itemToPass = currentTreeItem; - } - node.children.forEach(child => - buildTree(layout[child], itemToPass, layout, charts, validNodes), - ); -}; - -const addInvisibleParents = (layout: Layout, item: string) => [ - ...(layout[item]?.children || []), - ...Object.values(layout) - .filter( - val => - val.parents && - val.parents[val.parents.length - 1] === item && - !isShowTypeInTree(layout[val.parents[val.parents.length - 1]]), - ) - .map(({ id }) => id), -]; - -// Generate checked options for Ant tree from redux scope -const checkTreeItem = ( - checkedItems: string[], - layout: Layout, - items: string[], - excluded: number[], -) => { - items.forEach(item => { - checkTreeItem( - checkedItems, - layout, - addInvisibleParents(layout, item), - excluded, - ); - if ( - layout[item]?.type === CHART_TYPE && - !excluded.includes(layout[item]?.meta.chartId) - ) { - checkedItems.push(item); - } - }); -}; - -export const getTreeCheckedItems = (scope: Scope, layout: Layout) => { - const checkedItems: string[] = []; - checkTreeItem(checkedItems, layout, [...scope.rootPath], [...scope.excluded]); - return [...new Set(checkedItems)]; -}; - -// Looking for first common parent for selected charts/tabs/tab -export const findFilterScope = ( - checkedKeys: string[], - layout: Layout, -): Scope => { - if (!checkedKeys.length) { - return { - rootPath: [], - excluded: [], - }; - } - - // Get arrays of parents for selected charts - const checkedItemParents = checkedKeys - .filter(item => layout[item]?.type === CHART_TYPE) - .map(key => { - const parents = [DASHBOARD_ROOT_ID, ...(layout[key]?.parents || [])]; - return parents.filter(parent => isShowTypeInTree(layout[parent])); - }); - // Sort arrays of parents to get first shortest array of parents, - // that means on it's level of parents located common parent, from this place parents start be different - checkedItemParents.sort((p1, p2) => p1.length - p2.length); - const rootPath = checkedItemParents.map( - parents => parents[checkedItemParents[0].length - 1], - ); - - const excluded: number[] = []; - const isExcluded = (parent: string, item: string) => - rootPath.includes(parent) && !checkedKeys.includes(item); - // looking for charts to be excluded: iterate over all charts - // and looking for charts that have one of their parents in `rootPath` and not in selected items - Object.entries(layout).forEach(([key, value]) => { - if ( - value.type === CHART_TYPE && - [DASHBOARD_ROOT_ID, ...value.parents]?.find(parent => - isExcluded(parent, key), - ) - ) { - excluded.push(value.meta.chartId); - } - }); - - return { - rootPath: [...new Set(rootPath)], - excluded, - }; -}; +export const getFormData = ({ + datasetId = 18, + cascadingFilters = {}, + groupby, + allowsMultipleValues = false, + defaultValue, + currentValue, + inverseSelection, + inputRef, +}: Partial & { + datasetId?: number; + inputRef?: RefObject; + cascadingFilters?: object; + groupby: string; +}): Partial => ({ + adhoc_filters: [], + datasource: `${datasetId}__table`, + extra_filters: [], + extra_form_data: cascadingFilters, + granularity_sqla: 'ds', + groupby: [groupby], + inverseSelection, + metrics: ['count'], + multiSelect: allowsMultipleValues, + row_limit: 10000, + showSearch: true, + currentValue, + time_range: 'No filter', + time_range_endpoints: ['inclusive', 'exclusive'], + url_params: {}, + viz_type: 'filter_select', + // TODO: need process per filter type after will be decided approach + defaultValue, + inputRef, +}); export function mergeExtraFormData( originalExtra: ExtraFormData, @@ -211,111 +104,3 @@ export function getExtraFormData( }); return extraFormData; } - -export function mapParentFiltersToChildren( - filters: Filter[], -): { [id: string]: Filter[] } { - const cascadeChildren = {}; - filters.forEach(filter => { - const [parentId] = filter.cascadeParentIds || []; - if (parentId) { - if (!cascadeChildren[parentId]) { - cascadeChildren[parentId] = []; - } - cascadeChildren[parentId].push(filter); - } - }); - return cascadeChildren; -} - -export function buildCascadeFiltersTree(filters: Filter[]): CascadeFilter[] { - const cascadeChildren = mapParentFiltersToChildren(filters); - - const getCascadeFilter = (filter: Filter): CascadeFilter => { - const children = cascadeChildren[filter.id] || []; - return { - ...filter, - cascadeChildren: children.map(getCascadeFilter), - }; - }; - - return filters - .filter(filter => !filter.cascadeParentIds?.length) - .map(getCascadeFilter); -} - -export const FilterTypeNames = { - [FilterType.filter_select]: t('Select'), - [FilterType.filter_range]: t('Range'), -}; - -export const setFilterFieldValues = ( - form: FormInstance, - filterId: string, - values: object, -) => { - const formFilters = form.getFieldValue('filters'); - form.setFieldsValue({ - filters: { - ...formFilters, - [filterId]: { - ...formFilters[filterId], - ...values, - }, - }, - }); -}; - -export const isScopingAll = (scope: Scope) => - !scope || (scope.rootPath[0] === DASHBOARD_ROOT_ID && !scope.excluded.length); - -export const getFormData = ({ - datasetId = 18, - cascadingFilters = {}, - groupby, - allowsMultipleValues = false, - defaultValue, - currentValue, - inverseSelection, - inputRef, -}: Partial & { - datasetId?: number; - inputRef?: RefObject; - cascadingFilters?: object; - groupby: string; -}): Partial => ({ - adhoc_filters: [], - datasource: `${datasetId}__table`, - extra_filters: [], - extra_form_data: cascadingFilters, - granularity_sqla: 'ds', - groupby: [groupby], - inverseSelection, - metrics: ['count'], - multiSelect: allowsMultipleValues, - row_limit: 10000, - showSearch: true, - currentValue, - time_range: 'No filter', - time_range_endpoints: ['inclusive', 'exclusive'], - url_params: {}, - viz_type: 'filter_select', - // TODO: need process per filter type after will be decided approach - defaultValue, - inputRef, -}); - -type AppendFormData = { - filters: { - val?: number | string | null; - }[]; -}; - -export const extractDefaultValue = { - [FilterType.filter_select]: (appendFormData: AppendFormData) => - appendFormData.filters?.[0]?.val, - [FilterType.filter_range]: (appendFormData: AppendFormData) => ({ - min: appendFormData.filters?.[0].val, - max: appendFormData.filters?.[1].val, - }), -}; diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index 56e34b941..f0cdd8ace 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -21,13 +21,10 @@ import { AnyFilterAction, SET_FILTER_CONFIG_COMPLETE, } from 'src/dashboard/actions/nativeFilters'; -import { - FilterConfiguration, - FilterState, - NativeFiltersState, -} from 'src/dashboard/components/nativeFilters/types'; +import { NativeFiltersState, NativeFilterState } from './types'; +import { FilterConfiguration } from '../components/nativeFilters/types'; -export function getInitialFilterState(id: string): FilterState { +export function getInitialFilterState(id: string): NativeFilterState { return { id, extraFormData: {}, @@ -36,7 +33,7 @@ export function getInitialFilterState(id: string): FilterState { export function getInitialState( filterConfig: FilterConfiguration, - prevFiltersState: { [filterId: string]: FilterState }, + prevFiltersState: { [filterId: string]: NativeFilterState }, ): NativeFiltersState { const filters = {}; const filtersState = {}; diff --git a/superset-frontend/src/dashboard/reducers/types.ts b/superset-frontend/src/dashboard/reducers/types.ts index f3f7c2145..0f8b06793 100644 --- a/superset-frontend/src/dashboard/reducers/types.ts +++ b/superset-frontend/src/dashboard/reducers/types.ts @@ -18,6 +18,8 @@ */ import componentTypes from 'src/dashboard/util/componentTypes'; +import { ExtraFormData, JsonObject } from '@superset-ui/core'; +import { Filter } from '../components/nativeFilters/types'; export enum Scoping { all, @@ -43,6 +45,11 @@ export type RootState = { /** State of dashboardLayout in redux */ export type Layout = { [key: string]: LayoutItem }; +/** State of nativeFilters currentState */ +export type CurrentFilterState = JsonObject & { + value: any; +}; + /** State of charts in redux */ export type Charts = { [key: number]: Chart }; @@ -64,3 +71,19 @@ export type LayoutItem = { width: number; }; }; + +/** Current state of the filter, stored in `nativeFilters` in redux */ +export type NativeFilterState = { + id: string; // ties this filter state to the config object + extraFormData?: ExtraFormData; + currentState?: CurrentFilterState; +}; + +export type NativeFiltersState = { + filters: { + [filterId: string]: Filter; + }; + filtersState: { + [filterId: string]: NativeFilterState; + }; +}; diff --git a/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts b/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts index e14316422..66128ea9c 100644 --- a/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts +++ b/superset-frontend/src/dashboard/util/activeDashboardNativeFilters.ts @@ -17,8 +17,9 @@ * under the License. */ import { CHART_TYPE } from './componentTypes'; -import { NativeFiltersState, Scope } from '../components/nativeFilters/types'; +import { Scope } from '../components/nativeFilters/types'; import { ActiveFilters, LayoutItem } from '../types'; +import { NativeFiltersState } from '../reducers/types'; // Looking for affected chart scopes and values export const findAffectedCharts = ({ diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index 6d7e56b49..47aada1e1 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -22,10 +22,10 @@ import { DataRecordFilters, } from '@superset-ui/core'; import { ChartQueryPayload, LayoutItem } from 'src/dashboard/types'; -import { NativeFiltersState } from 'src/dashboard/components/nativeFilters/types'; import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils'; import getEffectiveExtraFilters from './getEffectiveExtraFilters'; import { getActiveNativeFilters } from '../activeDashboardNativeFilters'; +import { NativeFiltersState } from '../../reducers/types'; // We cache formData objects so that our connected container components don't always trigger // render cascades. we cannot leverage the reselect library because our cache size is >1