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
This commit is contained in:
simcha90 2021-02-08 13:26:58 +02:00 committed by GitHub
parent 50fa10054f
commit c440d98fad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 968 additions and 801 deletions

View File

@ -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: {

View File

@ -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';

View File

@ -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', () => {

View File

@ -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();

View File

@ -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';

View File

@ -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 {

View File

@ -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;

View File

@ -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',

View File

@ -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<CascadeFilterControlProps> = ({
filter,
directPathToChild,
onFilterSelectionChange,
}) => (
<>
<StyledFilterControlBox>
<StyledCaretIcon name="caret-down" />
<FilterControl
filter={filter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
</StyledFilterControlBox>
<StyledCascadeChildrenList>
{filter.cascadeChildren?.map(childFilter => (
<li key={childFilter.id}>
<CascadeFilterControl
filter={childFilter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
</li>
))}
</StyledCascadeChildrenList>
</>
);
export default CascadeFilterControl;

View File

@ -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;

View File

@ -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<FilterProps> = ({
filter,
directPathToChild,
onFilterSelectionChange,
}) => {
const {
id,
allowsMultipleValues,
inverseSelection,
targets,
defaultValue,
filterType,
} = filter;
const cascadingFilters = useCascadingFilters(id);
const filterState = useFilterState(id);
const [loading, setLoading] = useState<boolean>(true);
const [state, setState] = useState([]);
const [error, setError] = useState<boolean>(false);
const [formData, setFormData] = useState<Partial<QueryFormData>>({});
const inputRef = useRef<HTMLInputElement>(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 (
<StyledLoadingBox>
<Loading />
</StyledLoadingBox>
);
}
if (error) {
return (
<BasicErrorAlert
title={t('Cannot load filter')}
body={t('Check configuration')}
level="error"
/>
);
}
return (
<FilterItem data-test="form-item-value">
<SuperChart
height={20}
width={220}
formData={formData}
queriesData={state}
chartType={filterType}
// @ts-ignore (update superset-ui)
hooks={{ setExtraFormData }}
/>
</FilterItem>
);
};
export const FilterControl: React.FC<FilterProps> = ({
filter,
icon,
onFilterSelectionChange,
directPathToChild,
}) => {
const { name = '<undefined>' } = filter;
return (
<StyledFilterControlContainer>
<StyledFilterControlTitleBox>
<StyledFilterControlTitle data-test="filter-control-name">
{name}
</StyledFilterControlTitle>
<div data-test="filter-icon">{icon}</div>
</StyledFilterControlTitleBox>
<FilterValue
filter={filter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
</StyledFilterControlContainer>
);
};
interface CascadeFilterControlProps {
filter: CascadeFilter;
directPathToChild?: string[];
onFilterSelectionChange: (
filter: Filter,
extraFormData: ExtraFormData,
currentState: CurrentFilterState,
) => void;
}
export const CascadeFilterControl: React.FC<CascadeFilterControlProps> = ({
filter,
directPathToChild,
onFilterSelectionChange,
}) => (
<>
<StyledFilterControlBox>
<StyledCaretIcon name="caret-down" />
<FilterControl
filter={filter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
</StyledFilterControlBox>
<StyledCascadeChildrenList>
{filter.cascadeChildren?.map(childFilter => (
<li key={childFilter.id}>
<CascadeFilterControl
filter={childFilter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
</li>
))}
</StyledCascadeChildrenList>
</>
);
const FilterBar: React.FC<FiltersBarProps> = ({
filtersOpen,
toggleFiltersBar,

View File

@ -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;

View File

@ -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<FilterProps> = ({
filter,
icon,
onFilterSelectionChange,
directPathToChild,
}) => {
const { name = '<undefined>' } = filter;
return (
<StyledFilterControlContainer>
<StyledFilterControlTitleBox>
<StyledFilterControlTitle data-test="filter-control-name">
{name}
</StyledFilterControlTitle>
<div data-test="filter-icon">{icon}</div>
</StyledFilterControlTitleBox>
<FilterValue
filter={filter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
</StyledFilterControlContainer>
);
};
export default FilterControl;

View File

@ -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<FilterProps> = ({
filter,
directPathToChild,
onFilterSelectionChange,
}) => {
const {
id,
allowsMultipleValues,
inverseSelection,
targets,
defaultValue,
filterType,
} = filter;
const cascadingFilters = useCascadingFilters(id);
const filterState = useFilterState(id);
const [loading, setLoading] = useState<boolean>(true);
const [state, setState] = useState([]);
const [error, setError] = useState<boolean>(false);
const [formData, setFormData] = useState<Partial<QueryFormData>>({});
const inputRef = useRef<HTMLInputElement>(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 (
<StyledLoadingBox>
<Loading />
</StyledLoadingBox>
);
}
if (error) {
return (
<BasicErrorAlert
title={t('Cannot load filter')}
body={t('Check configuration')}
level="error"
/>
);
}
return (
<FilterItem data-test="form-item-value">
<SuperChart
height={20}
width={220}
formData={formData}
queriesData={state}
chartType={filterType}
// @ts-ignore (update superset-ui)
hooks={{ setExtraFormData }}
/>
</FilterItem>
);
};
export default FilterValue;

View File

@ -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<any, NativeFilterState>(
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<any, NativeFiltersState>(
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<any, NativeFilterState>(
state => state.nativeFilters.filtersState[id] || getInitialFilterState(id),
);
}

View File

@ -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[];
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<NativeFiltersForm>;

View File

@ -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<any, NativeFilterState>(
state => state.nativeFilters.filtersState,
);
}
export function useFilterScopeTree(): {
treeData: [TreeItem];
layout: Layout;
} {
const layout = useSelector<RootState, Layout>(
({ dashboardLayout: { present } }) => present,
);
const charts = useSelector<RootState, Charts>(({ 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<string[]>((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<NativeFiltersForm>,
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,
]);
};

View File

@ -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<string, NativeFiltersFormItem>;
}
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;
};

View File

@ -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,
}),
};

View File

@ -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<any, FilterState>(
state => state.nativeFilters.filtersState[id] || getInitialFilterState(id),
);
}
export function useFiltersState() {
return useSelector<any, FilterState>(
state => state.nativeFilters.filtersState,
);
}
export function useFilters() {
return useSelector<any, FilterState>(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<RootState, Layout>(
({ dashboardLayout: { present } }) => present,
);
const charts = useSelector<RootState, Charts>(({ 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<string[]>((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<any, NativeFiltersState>(
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<NativeFiltersForm>,
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,
]);
};

View File

@ -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<string, NativeFiltersFormItem>;
}
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;
};
};

View File

@ -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<Filter> & {
datasetId?: number;
inputRef?: RefObject<HTMLInputElement>;
cascadingFilters?: object;
groupby: string;
}): Partial<QueryFormData> => ({
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<Filter> & {
datasetId?: number;
inputRef?: RefObject<HTMLInputElement>;
cascadingFilters?: object;
groupby: string;
}): Partial<QueryFormData> => ({
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,
}),
};

View File

@ -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 = {};

View File

@ -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;
};
};

View File

@ -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 = ({

View File

@ -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