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:
parent
50fa10054f
commit
c440d98fad
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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,
|
||||
]);
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
|
@ -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,
|
||||
]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue