feat(dashboard): Refactor FiltersBadge (#23286)

This commit is contained in:
Kamil Gabryjelski 2023-03-10 20:58:24 +01:00 committed by GitHub
parent 6311b40329
commit c2b282ac71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 248 additions and 535 deletions

View File

@ -378,10 +378,13 @@ describe('Horizontal FilterBar', () => {
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
openMoreFilters();
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.getBySel('slice-header').within(() => {
cy.get('.filter-counts').click();
cy.get('.filter-counts').trigger('mouseover');
});
cy.get('.filterStatusPopover').contains('test_8').click();
cy.get('.filterStatusPopover').contains('test_9').click();
cy.getBySel('dropdown-content').should('be.visible');
cy.get('.ant-select-focused').should('be.visible');
});

View File

@ -86,7 +86,7 @@ const createProps = () => ({
onHighlightFilterSource: jest.fn(),
});
test('Should render "appliedCrossFilterIndicators"', () => {
test('Should render "appliedCrossFilterIndicators"', async () => {
const props = createProps();
props.appliedIndicators = [];
props.incompatibleIndicators = [];
@ -99,8 +99,10 @@ test('Should render "appliedCrossFilterIndicators"', () => {
{ useRedux: true },
);
userEvent.click(screen.getByTestId('details-panel-content'));
expect(screen.getByText('Applied Cross Filters (1)')).toBeInTheDocument();
userEvent.hover(screen.getByTestId('details-panel-content'));
expect(
await screen.findByText('Applied cross-filters (1)'),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Clinical Stage' }),
).toBeInTheDocument();
@ -118,7 +120,7 @@ test('Should render "appliedCrossFilterIndicators"', () => {
]);
});
test('Should render "appliedIndicators"', () => {
test('Should render "appliedIndicators"', async () => {
const props = createProps();
props.appliedCrossFilterIndicators = [];
props.incompatibleIndicators = [];
@ -131,8 +133,8 @@ test('Should render "appliedIndicators"', () => {
{ useRedux: true },
);
userEvent.click(screen.getByTestId('details-panel-content'));
expect(screen.getByText('Applied Filters (1)')).toBeInTheDocument();
userEvent.hover(screen.getByTestId('details-panel-content'));
expect(await screen.findByText('Applied filters (1)')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Country' })).toBeInTheDocument();
expect(props.onHighlightFilterSource).toBeCalledTimes(0);
@ -148,72 +150,6 @@ test('Should render "appliedIndicators"', () => {
]);
});
test('Should render "incompatibleIndicators"', () => {
const props = createProps();
props.appliedCrossFilterIndicators = [];
props.appliedIndicators = [];
props.unsetIndicators = [];
render(
<DetailsPanel {...props}>
<div data-test="details-panel-content">Content</div>
</DetailsPanel>,
{ useRedux: true },
);
userEvent.click(screen.getByTestId('details-panel-content'));
expect(screen.getByText('Incompatible Filters (1)')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Vaccine Approach Copy' }),
).toBeInTheDocument();
expect(props.onHighlightFilterSource).toBeCalledTimes(0);
userEvent.click(
screen.getByRole('button', { name: 'Vaccine Approach Copy' }),
);
expect(props.onHighlightFilterSource).toBeCalledTimes(1);
expect(props.onHighlightFilterSource).toBeCalledWith([
'ROOT_ID',
'TABS-wUKya7eQ0Zz',
'TAB-BCIJF4NvgQq',
'ROW-xSeNAspgww',
'CHART-eirDduqb1Aa',
'LABEL-product_category_copy',
]);
});
test('Should render "unsetIndicators"', () => {
const props = createProps();
props.appliedCrossFilterIndicators = [];
props.appliedIndicators = [];
props.incompatibleIndicators = [];
render(
<DetailsPanel {...props}>
<div data-test="details-panel-content">Content</div>
</DetailsPanel>,
{ useRedux: true },
);
userEvent.click(screen.getByTestId('details-panel-content'));
expect(screen.getByText('Unset Filters (1)')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Vaccine Approach' }),
).toBeInTheDocument();
expect(props.onHighlightFilterSource).toBeCalledTimes(0);
userEvent.click(screen.getByRole('button', { name: 'Vaccine Approach' }));
expect(props.onHighlightFilterSource).toBeCalledTimes(1);
expect(props.onHighlightFilterSource).toBeCalledWith([
'ROOT_ID',
'TABS-wUKya7eQ0Z',
'TAB-BCIJF4NvgQ',
'ROW-xSeNAspgw',
'CHART-eirDduqb1A',
'LABEL-product_category',
]);
});
test('Should render empty', () => {
const props = createProps();
props.appliedCrossFilterIndicators = [];

View File

@ -19,31 +19,21 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Global, css } from '@emotion/react';
import { t, useTheme } from '@superset-ui/core';
import { t } from '@superset-ui/core';
import Popover from 'src/components/Popover';
import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons';
import {
Indent,
Panel,
Reset,
Title,
FiltersContainer,
FiltersDetailsContainer,
Separator,
SectionName,
} from 'src/dashboard/components/FiltersBadge/Styles';
import { Indicator } from 'src/dashboard/components/nativeFilters/selectors';
import FilterIndicator from 'src/dashboard/components/FiltersBadge/FilterIndicator';
import { RootState } from 'src/dashboard/types';
const iconReset = css`
span {
line-height: 0;
}
`;
export interface DetailsPanelProps {
appliedCrossFilterIndicators: Indicator[];
appliedIndicators: Indicator[];
incompatibleIndicators: Indicator[];
unsetIndicators: Indicator[];
onHighlightFilterSource: (path: string[]) => void;
children: JSX.Element;
}
@ -51,13 +41,10 @@ export interface DetailsPanelProps {
const DetailsPanelPopover = ({
appliedCrossFilterIndicators = [],
appliedIndicators = [],
incompatibleIndicators = [],
unsetIndicators = [],
onHighlightFilterSource,
children,
}: DetailsPanelProps) => {
const [visible, setVisible] = useState(false);
const theme = useTheme();
const activeTabs = useSelector<RootState>(
state => state.dashboardState?.activeTabs,
);
@ -76,57 +63,22 @@ const DetailsPanelPopover = ({
setVisible(false);
}, [activeTabs]);
const getDefaultActivePanel = () => {
const result = [];
if (appliedCrossFilterIndicators.length) {
result.push('appliedCrossFilters');
}
if (appliedIndicators.length) {
result.push('applied');
}
if (incompatibleIndicators.length) {
result.push('incompatible');
}
if (result.length) {
return result;
}
return ['unset'];
};
const [activePanels, setActivePanels] = useState<string[]>(() => [
...getDefaultActivePanel(),
]);
function handlePopoverStatus(isOpen: boolean) {
setVisible(isOpen);
// every time the popover opens, make sure the most relevant panel is active
if (isOpen) {
setActivePanels(getDefaultActivePanel());
}
}
function handleActivePanelChange(panels: string | string[]) {
// need to convert to an array so that handlePopoverStatus will work
if (typeof panels === 'string') {
setActivePanels([panels]);
} else {
setActivePanels(panels);
}
}
const indicatorKey = (indicator: Indicator): string =>
`${indicator.column} - ${indicator.name}`;
const content = (
<Panel>
<FiltersDetailsContainer>
<Global
styles={css`
styles={theme => css`
.filterStatusPopover {
.ant-popover-inner {
background-color: ${theme.colors.grayscale.dark2}cc;
.ant-popover-inner-content {
padding-top: 0;
padding-bottom: 0;
padding: ${theme.gridUnit * 2}px;
}
}
&.ant-popover-placement-bottom,
@ -168,110 +120,47 @@ const DetailsPanelPopover = ({
}
`}
/>
<Reset>
<Collapse
ghost
light
activeKey={activePanels}
onChange={handleActivePanelChange}
>
{appliedCrossFilterIndicators.length ? (
<Collapse.Panel
key="appliedCrossFilters"
header={
<Title bold color={theme.colors.primary.light1}>
<Icons.CursorTarget
css={{ fill: theme.colors.primary.light1 }}
iconSize="xl"
/>
{t(
'Applied Cross Filters (%d)',
appliedCrossFilterIndicators.length,
)}
</Title>
}
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{appliedCrossFilterIndicators.map(indicator => (
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
/>
))}
</Indent>
</Collapse.Panel>
) : null}
{appliedIndicators.length ? (
<Collapse.Panel
key="applied"
header={
<Title bold color={theme.colors.success.base}>
<Icons.CheckCircleFilled css={iconReset} iconSize="m" />{' '}
{t('Applied Filters (%d)', appliedIndicators.length)}
</Title>
}
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{appliedIndicators.map(indicator => (
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
/>
))}
</Indent>
</Collapse.Panel>
) : null}
{incompatibleIndicators.length ? (
<Collapse.Panel
key="incompatible"
header={
<Title bold color={theme.colors.alert.base}>
<Icons.ExclamationCircleFilled css={iconReset} iconSize="m" />{' '}
{t(
'Incompatible Filters (%d)',
incompatibleIndicators.length,
)}
</Title>
}
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{incompatibleIndicators.map(indicator => (
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
/>
))}
</Indent>
</Collapse.Panel>
) : null}
{unsetIndicators.length ? (
<Collapse.Panel
key="unset"
header={
<Title bold color={theme.colors.grayscale.light1}>
<Icons.MinusCircleFilled css={iconReset} iconSize="m" />{' '}
{t('Unset Filters (%d)', unsetIndicators.length)}
</Title>
}
disabled={!unsetIndicators.length}
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{unsetIndicators.map(indicator => (
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
/>
))}
</Indent>
</Collapse.Panel>
) : null}
</Collapse>
</Reset>
</Panel>
<div>
{appliedCrossFilterIndicators.length ? (
<div>
<SectionName>
{t(
'Applied cross-filters (%d)',
appliedCrossFilterIndicators.length,
)}
</SectionName>
<FiltersContainer>
{appliedCrossFilterIndicators.map(indicator => (
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
/>
))}
</FiltersContainer>
</div>
) : null}
{appliedCrossFilterIndicators.length && appliedIndicators.length ? (
<Separator />
) : null}
{appliedIndicators.length ? (
<div>
<SectionName>
{t('Applied filters (%d)', appliedIndicators.length)}
</SectionName>
<FiltersContainer>
{appliedIndicators.map(indicator => (
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
/>
))}
</FiltersContainer>
</div>
) : null}
</div>
</FiltersDetailsContainer>
);
return (
@ -281,7 +170,7 @@ const DetailsPanelPopover = ({
visible={visible}
onVisibleChange={handlePopoverStatus}
placement="bottomRight"
trigger="click"
trigger="hover"
>
{children}
</Popover>

View File

@ -22,47 +22,48 @@ import { css } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { getFilterValueForDisplay } from 'src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils';
import {
FilterIndicatorText,
FilterValue,
Item,
ItemIcon,
Title,
FilterItem,
FilterName,
} from 'src/dashboard/components/FiltersBadge/Styles';
import { Indicator } from 'src/dashboard/components/nativeFilters/selectors';
export interface IndicatorProps {
indicator: Indicator;
onClick?: (path: string[]) => void;
text?: string;
}
const FilterIndicator: FC<IndicatorProps> = ({
indicator: { column, name, value, path = [] },
onClick = () => {},
text,
onClick,
}) => {
const resultValue = getFilterValueForDisplay(value);
return (
<>
<Item onClick={() => onClick([...path, `LABEL-${column}`])}>
<Title bold>
<ItemIcon>
<Icons.SearchOutlined
iconSize="m"
css={css`
span {
vertical-align: 0;
}
`}
/>
</ItemIcon>
<FilterItem
onClick={
onClick ? () => onClick([...path, `LABEL-${column}`]) : undefined
}
>
{onClick && (
<i>
<Icons.SearchOutlined
iconSize="m"
css={css`
span {
vertical-align: 0;
}
`}
/>
</i>
)}
<div>
<FilterName>
{name}
{resultValue ? ': ' : ''}
</Title>
</FilterName>
<FilterValue>{resultValue}</FilterValue>
</Item>
{text && <FilterIndicatorText>{text}</FilterIndicatorText>}
</>
</div>
</FilterItem>
);
};

View File

@ -36,7 +36,6 @@ import {
import { sliceId } from 'spec/fixtures/mockChartQueries';
import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters';
import { dashboardWithFilter } from 'spec/fixtures/mockDashboardLayout';
import Icons from 'src/components/Icons';
import { FeatureFlag } from 'src/featureFlags';
const defaultStore = getMockStoreWithFilters();
@ -111,36 +110,6 @@ describe('FiltersBadge', () => {
);
expect(wrapper.find('WarningFilled')).not.toExist();
});
it("shows a warning when there's a rejected filter", () => {
const store = getMockStoreWithFilters();
// start with basic dashboard state, dispatch an event to simulate query completion
store.dispatch({
type: CHART_UPDATE_SUCCEEDED,
key: sliceId,
queriesResponse: [
{
status: 'success',
applied_filters: [],
rejected_filters: [
{ column: 'region', reason: 'not_in_datasource' },
],
},
],
dashboardFilters,
});
store.dispatch({ type: CHART_RENDERING_SUCCEEDED, key: sliceId });
const wrapper = setup(store);
expect(wrapper.find('DetailsPanelPopover')).toExist();
expect(wrapper.find('[data-test="applied-filter-count"]')).toHaveText(
'0',
);
expect(
wrapper.find('[data-test="incompatible-filter-count"]'),
).toHaveText('1');
// to look at the shape of the wrapper use:
expect(wrapper.find(Icons.AlertSolid)).toExist();
});
});
describe('for native filters', () => {
@ -189,37 +158,5 @@ describe('FiltersBadge', () => {
);
expect(wrapper.find('WarningFilled')).not.toExist();
});
it("shows a warning when there's a rejected filter", () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
};
const store = getMockStoreWithNativeFilters();
// start with basic dashboard state, dispatch an event to simulate query completion
store.dispatch({
type: CHART_UPDATE_SUCCEEDED,
key: sliceId,
queriesResponse: [
{
status: 'success',
applied_filters: [],
rejected_filters: [
{ column: 'region', reason: 'not_in_datasource' },
],
},
],
});
store.dispatch({ type: CHART_RENDERING_SUCCEEDED, key: sliceId });
const wrapper = setup(store);
expect(wrapper.find('DetailsPanelPopover')).toExist();
expect(wrapper.find('[data-test="applied-filter-count"]')).toHaveText(
'0',
);
expect(
wrapper.find('[data-test="incompatible-filter-count"]'),
).toHaveText('1');
expect(wrapper.find(Icons.AlertSolid)).toExist();
});
});
});

View File

@ -20,7 +20,7 @@ import { css, styled } from '@superset-ui/core';
export const Pill = styled.div`
${({ theme }) => css`
display: inline-block;
display: flex;
color: ${theme.colors.grayscale.light5};
background: ${theme.colors.grayscale.base};
border-radius: 1em;
@ -36,7 +36,6 @@ export const Pill = styled.div`
svg {
position: relative;
top: -2px;
color: ${theme.colors.grayscale.light5};
width: 1em;
height: 1em;
@ -55,64 +54,27 @@ export const Pill = styled.div`
background: ${theme.colors.primary.dark1};
}
}
`}
`;
&.has-incompatible-filters {
color: ${theme.colors.grayscale.dark2};
background: ${theme.colors.alert.base};
&:hover {
background: ${theme.colors.alert.dark1};
}
svg {
color: ${theme.colors.grayscale.dark2};
}
}
&.filters-inactive {
color: ${theme.colors.grayscale.light5};
background: ${theme.colors.grayscale.light1};
padding: ${theme.gridUnit}px;
text-align: center;
height: 22px;
width: 22px;
&:hover {
background: ${theme.colors.grayscale.base};
}
export const SectionName = styled.span`
${({ theme }) => css`
font-weight: ${theme.typography.weights.bold};
`}
`;
export const FilterName = styled.span`
${({ theme }) => css`
padding-right: ${theme.gridUnit}px;
font-style: italic;
& > * {
margin-right: ${theme.gridUnit}px;
}
`}
`;
export interface TitleProps {
bold?: boolean;
color?: string;
}
export const Title = styled.span<TitleProps>`
position: relative;
margin-right: ${({ theme }) => theme.gridUnit}px;
font-weight: ${({ bold, theme }) => {
if (bold) return theme.typography.weights.bold;
return 'auto';
}};
color: ${({ color, theme }) => color || theme.colors.grayscale.light5};
display: flex;
align-items: center;
& > * {
margin-right: ${({ theme }) => theme.gridUnit}px;
}
`;
export const ItemIcon = styled.i`
position: absolute;
top: 50%;
transform: translateY(-50%);
left: -${({ theme }) => theme.gridUnit * 5}px;
`;
export const Item = styled.button`
export const FilterItem = styled.button`
cursor: pointer;
display: flex;
flex-wrap: wrap;
text-align: left;
padding: 0;
border: none;
@ -134,34 +96,36 @@ export const Item = styled.button`
}
`;
export const Reset = styled.div`
margin: 0 -${({ theme }) => theme.gridUnit * 4}px;
`;
export const Indent = styled.div`
padding-left: ${({ theme }) => theme.gridUnit * 6}px;
margin: -${({ theme }) => theme.gridUnit * 3}px 0;
`;
export const Panel = styled.div`
min-width: 200px;
max-width: 300px;
overflow-x: hidden;
`;
export const FilterValue = styled.div`
max-width: 100%;
flex-grow: 1;
overflow: auto;
color: ${({ theme }) => theme.colors.grayscale.light5};
`;
export const FilterIndicatorText = styled.div`
${({ theme }) => `
padding-top: ${theme.gridUnit * 3}px;
max-width: 100%;
flex-grow: 1;
overflow: auto;
color: ${theme.colors.grayscale.light5};
export const FiltersContainer = styled.div`
${({ theme }) => css`
margin-top: ${theme.gridUnit}px;
&:not(:last-child) {
padding-bottom: ${theme.gridUnit * 3}px;
}
`}
`;
export const FiltersDetailsContainer = styled.div`
${({ theme }) => css`
min-width: 200px;
max-width: 300px;
overflow-x: hidden;
color: ${theme.colors.grayscale.light5};
`}
`;
export const FilterValue = styled.span`
max-width: 100%;
flex-grow: 1;
overflow: auto;
`;
export const Separator = styled.div`
${({ theme }) => css`
width: 100%;
height: 1px;
background-color: ${theme.colors.grayscale.light1};
margin: ${theme.gridUnit * 4}px 0;
`}
`;

View File

@ -211,66 +211,27 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
),
[indicators],
);
const unsetIndicators = useMemo(
() =>
indicators.filter(
indicator => indicator.status === IndicatorStatus.Unset,
),
[indicators],
);
const incompatibleIndicators = useMemo(
() =>
indicators.filter(
indicator => indicator.status === IndicatorStatus.Incompatible,
),
[indicators],
);
if (
!appliedCrossFilterIndicators.length &&
!appliedIndicators.length &&
!incompatibleIndicators.length &&
!unsetIndicators.length
) {
if (!appliedCrossFilterIndicators.length && !appliedIndicators.length) {
return null;
}
const isInactive =
!appliedCrossFilterIndicators.length &&
!appliedIndicators.length &&
!incompatibleIndicators.length;
return (
<DetailsPanelPopover
appliedCrossFilterIndicators={appliedCrossFilterIndicators}
appliedIndicators={appliedIndicators}
unsetIndicators={unsetIndicators}
incompatibleIndicators={incompatibleIndicators}
onHighlightFilterSource={onHighlightFilterSource}
>
<Pill
className={cx(
'filter-counts',
!!incompatibleIndicators.length && 'has-incompatible-filters',
!!appliedCrossFilterIndicators.length && 'has-cross-filters',
isInactive && 'filters-inactive',
)}
>
<Icons.Filter iconSize="m" />
{!isInactive && (
<span data-test="applied-filter-count">
{appliedIndicators.length + appliedCrossFilterIndicators.length}
</span>
)}
{incompatibleIndicators.length ? (
<>
{' '}
<Icons.AlertSolid />
<span data-test="incompatible-filter-count">
{incompatibleIndicators.length}
</span>
</>
) : null}
<span data-test="applied-filter-count">
{appliedIndicators.length + appliedCrossFilterIndicators.length}
</span>
</Pill>
</DetailsPanelPopover>
);

View File

@ -21,11 +21,10 @@ import React, {
ReactNode,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { css, styled, t } from '@superset-ui/core';
import { css, styled, SupersetTheme, t } from '@superset-ui/core';
import { useUiConfig } from 'src/components/UiConfigContext';
import { Tooltip } from 'src/components/Tooltip';
import { useDispatch, useSelector } from 'react-redux';
@ -36,10 +35,10 @@ import SliceHeaderControls, {
import FiltersBadge from 'src/dashboard/components/FiltersBadge';
import Icons from 'src/components/Icons';
import { RootState } from 'src/dashboard/types';
import FilterIndicator from 'src/dashboard/components/FiltersBadge/FilterIndicator';
import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip';
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
import { clearDataMask } from 'src/dataMask/actions';
import { getFilterValueForDisplay } from '../nativeFilters/FilterBar/FilterSets/utils';
type SliceHeaderProps = SliceHeaderControlsProps & {
innerRef?: string;
@ -173,14 +172,6 @@ const SliceHeader: FC<SliceHeaderProps> = ({
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
);
const indicator = useMemo(
() => ({
value: crossFilterValue,
name: t('Emitted values'),
}),
[crossFilterValue],
);
const canExplore = !editMode && supersetCanExplore;
useEffect(() => {
@ -251,10 +242,19 @@ const SliceHeader: FC<SliceHeaderProps> = ({
<Tooltip
placement="top"
title={
<FilterIndicator
indicator={indicator}
text={t('Click to clear emitted filters')}
/>
<div>
<span>{t('Emitted values: ')}</span>
<span>{getFilterValueForDisplay(crossFilterValue)}</span>
<div
css={(theme: SupersetTheme) =>
css`
margin-top: ${theme.gridUnit * 2}px;
`
}
>
{t('Click to clear emitted filters')}
</div>
</div>
}
>
<CrossFilterIcon

View File

@ -18,9 +18,9 @@
*/
import React from 'react';
import { DataMaskStateWithId } from '@superset-ui/core';
import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { DashboardInfo, DashboardLayout, RootState } from 'src/dashboard/types';
import { DashboardLayout, RootState } from 'src/dashboard/types';
import crossFiltersSelector from './selectors';
import VerticalCollapse from './VerticalCollapse';
@ -28,15 +28,15 @@ const CrossFiltersVertical = () => {
const dataMask = useSelector<RootState, DataMaskStateWithId>(
state => state.dataMask,
);
const dashboardInfo = useSelector<RootState, DashboardInfo>(
state => state.dashboardInfo,
const chartConfiguration = useSelector<RootState, JsonObject>(
state => state.dashboardInfo.metadata?.chart_configuration,
);
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
);
const selectedCrossFilters = crossFiltersSelector({
dataMask,
dashboardInfo,
chartConfiguration,
dashboardLayout,
});

View File

@ -17,37 +17,35 @@
* under the License.
*/
import { DataMaskStateWithId } from '@superset-ui/core';
import { DashboardInfo, DashboardLayout } from 'src/dashboard/types';
import { CrossFilterIndicator, selectChartCrossFilters } from '../../selectors';
import { DataMaskStateWithId, isDefined, JsonObject } from '@superset-ui/core';
import { DashboardLayout } from 'src/dashboard/types';
import { CrossFilterIndicator, getCrossFilterIndicator } from '../../selectors';
export const crossFiltersSelector = (props: {
dataMask: DataMaskStateWithId;
dashboardInfo: DashboardInfo;
chartConfiguration: JsonObject;
dashboardLayout: DashboardLayout;
}): CrossFilterIndicator[] => {
const { dataMask, dashboardInfo, dashboardLayout } = props;
const chartConfiguration = dashboardInfo.metadata?.chart_configuration;
const { dataMask, chartConfiguration, dashboardLayout } = props;
const chartsIds = Object.keys(chartConfiguration);
const shouldFilterEmitters = true;
let selectedCrossFilters: CrossFilterIndicator[] = [];
for (let i = 0; i < chartsIds.length; i += 1) {
const chartId = Number(chartsIds[i]);
const crossFilters = selectChartCrossFilters(
dataMask,
chartId,
dashboardLayout,
chartConfiguration,
shouldFilterEmitters,
);
selectedCrossFilters = [
...selectedCrossFilters,
...(crossFilters as CrossFilterIndicator[]),
];
}
return selectedCrossFilters;
return chartsIds
.map(chartId => {
const id = Number(chartId);
const filterIndicator = getCrossFilterIndicator(
id,
dataMask[id],
dashboardLayout,
);
if (
isDefined(filterIndicator.column) &&
isDefined(filterIndicator.value)
) {
return { ...filterIndicator, emitterId: id };
}
return null;
})
.filter(Boolean) as CrossFilterIndicator[];
};
export default crossFiltersSelector;

View File

@ -35,6 +35,7 @@ import {
isFeatureEnabled,
FeatureFlag,
isNativeFilterWithDataMask,
JsonObject,
} from '@superset-ui/core';
import {
createHtmlPortalNode,
@ -47,7 +48,6 @@ import {
useSelectFiltersInScope,
} from 'src/dashboard/components/nativeFilters/state';
import {
DashboardInfo,
DashboardLayout,
FilterBarOrientation,
RootState,
@ -87,8 +87,8 @@ const FilterControls: FC<FilterControlsProps> = ({
const dataMask = useSelector<RootState, DataMaskStateWithId>(
state => state.dataMask,
);
const dashboardInfo = useSelector<RootState, DashboardInfo>(
state => state.dashboardInfo,
const chartConfiguration = useSelector<RootState, JsonObject>(
state => state.dashboardInfo.metadata?.chart_configuration,
);
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
@ -101,11 +101,11 @@ const FilterControls: FC<FilterControlsProps> = ({
isCrossFiltersEnabled
? crossFiltersSelector({
dataMask,
dashboardInfo,
chartConfiguration,
dashboardLayout,
})
: [],
[dashboardInfo, dashboardLayout, dataMask, isCrossFiltersEnabled],
[chartConfiguration, dashboardLayout, dataMask, isCrossFiltersEnabled],
);
const { filterControlFactory, filtersWithValues } = useFilterControlFactory(
dataMaskSelected,

View File

@ -22,12 +22,13 @@ import {
DataMaskStateWithId,
FeatureFlag,
isFeatureEnabled,
JsonObject,
styled,
t,
} from '@superset-ui/core';
import Icons from 'src/components/Icons';
import Loading from 'src/components/Loading';
import { DashboardInfo, DashboardLayout, RootState } from 'src/dashboard/types';
import { DashboardLayout, RootState } from 'src/dashboard/types';
import { useSelector } from 'react-redux';
import FilterControls from './FilterControls/FilterControls';
import { getFilterBarTestId } from './utils';
@ -107,8 +108,8 @@ const HorizontalFilterBar: React.FC<HorizontalBarProps> = ({
const dataMask = useSelector<RootState, DataMaskStateWithId>(
state => state.dataMask,
);
const dashboardInfo = useSelector<RootState, DashboardInfo>(
state => state.dashboardInfo,
const chartConfiguration = useSelector<RootState, JsonObject>(
state => state.dashboardInfo.metadata?.chart_configuration,
);
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
@ -119,7 +120,7 @@ const HorizontalFilterBar: React.FC<HorizontalBarProps> = ({
const selectedCrossFilters = isCrossFiltersEnabled
? crossFiltersSelector({
dataMask,
dashboardInfo,
chartConfiguration,
dashboardLayout,
})
: [];

View File

@ -17,6 +17,7 @@
* under the License.
*/
import {
DataMask,
DataMaskStateWithId,
DataMaskType,
ensureIsArray,
@ -32,7 +33,7 @@ import {
import { TIME_FILTER_MAP } from 'src/explore/constants';
import { getChartIdsInFilterBoxScope } from 'src/dashboard/util/activeDashboardFilters';
import { ChartConfiguration } from 'src/dashboard/reducers/types';
import { Layout } from 'src/dashboard/types';
import { DashboardLayout, Layout } from 'src/dashboard/types';
import { areObjectsEqual } from 'src/reduxUtils';
export enum IndicatorStatus {
@ -61,7 +62,7 @@ type Filter = {
datasourceId: string;
};
const extractLabel = (filter?: FilterState): string | null => {
export const extractLabel = (filter?: FilterState): string | null => {
if (filter?.label && !filter?.label?.includes(undefined)) {
return filter.label;
}
@ -162,6 +163,36 @@ export type Indicator = {
export type CrossFilterIndicator = Indicator & { emitterId: number };
export const getCrossFilterIndicator = (
chartId: number,
dataMask: DataMask,
dashboardLayout: DashboardLayout,
) => {
const filterState = dataMask?.filterState;
const filters = dataMask?.extraFormData?.filters;
const label = extractLabel(filterState);
const filtersState = filterState?.filters;
const column =
filters?.[0]?.col || (filtersState && Object.keys(filtersState)[0]);
const dashboardLayoutItem = Object.values(dashboardLayout).find(
layoutItem => layoutItem?.meta?.chartId === chartId,
);
const filterObject: Indicator = {
column,
name:
dashboardLayoutItem?.meta?.sliceNameOverride ||
dashboardLayoutItem?.meta?.sliceName ||
'',
path: [
...(dashboardLayoutItem?.parents ?? []),
dashboardLayoutItem?.id || '',
],
value: label,
};
return filterObject;
};
const cachedIndicatorsForChart = {};
const cachedDashboardFilterDataForChart = {};
// inspects redux state to find what the filter indicators should be shown for a given chart
@ -233,17 +264,18 @@ const getStatus = ({
}): IndicatorStatus => {
// a filter is only considered unset if it's value is null
const hasValue = label !== null;
if (type === DataMaskType.CrossFilters && hasValue) {
return IndicatorStatus.CrossFilterApplied;
}
const APPLIED_STATUS =
type === DataMaskType.CrossFilters
? IndicatorStatus.CrossFilterApplied
: IndicatorStatus.Applied;
if (!column && hasValue) {
// Filter without datasource
return IndicatorStatus.Applied;
return APPLIED_STATUS;
}
if (column && rejectedColumns?.has(column))
return IndicatorStatus.Incompatible;
if (column && appliedColumns?.has(column) && hasValue) {
return IndicatorStatus.Applied;
return APPLIED_STATUS;
}
return IndicatorStatus.Unset;
};
@ -254,11 +286,12 @@ export const selectChartCrossFilters = (
chartId: number,
dashboardLayout: Layout,
chartConfiguration: ChartConfiguration = defaultChartConfig,
appliedColumns: Set<string>,
rejectedColumns: Set<string>,
filterEmitter = false,
): Indicator[] | CrossFilterIndicator[] => {
let crossFilterIndicators: any = [];
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
const dashboardLayoutValues = Object.values(dashboardLayout);
crossFilterIndicators = Object.values(chartConfiguration)
.filter(chartConfig => {
const inScope =
@ -272,34 +305,22 @@ export const selectChartCrossFilters = (
return false;
})
.map(chartConfig => {
const filterState = dataMask[chartConfig.id]?.filterState;
const extraFormData = dataMask[chartConfig.id]?.extraFormData;
const label = extractLabel(filterState);
const filtersState = filterState?.filters;
const column =
extraFormData?.filters?.[0]?.col ||
(filtersState && Object.keys(filtersState)[0]);
const dashboardLayoutItem = dashboardLayoutValues.find(
layoutItem => layoutItem?.meta?.chartId === chartConfig.id,
const filterIndicator = getCrossFilterIndicator(
chartConfig.id,
dataMask[chartConfig.id],
dashboardLayout,
);
const filterObject: Indicator = {
column,
name: dashboardLayoutItem?.meta?.sliceName as string,
path: [
...(dashboardLayoutItem?.parents ?? []),
dashboardLayoutItem?.id || '',
],
status: getStatus({
label,
type: DataMaskType.CrossFilters,
}),
value: label,
};
if (filterEmitter) {
(filterObject as CrossFilterIndicator).emitterId = chartId;
}
return filterObject;
const filterStatus = getStatus({
label: filterIndicator.value,
column: filterIndicator.column
? getColumnLabel(filterIndicator.column)
: undefined,
type: DataMaskType.CrossFilters,
appliedColumns,
rejectedColumns,
});
return { ...filterIndicator, status: filterStatus };
})
.filter(filter => filter.status === IndicatorStatus.CrossFilterApplied);
}
@ -369,6 +390,8 @@ export const selectNativeIndicatorsForChart = (
chartId,
dashboardLayout,
chartConfiguration,
appliedColumns,
rejectedColumns,
);
}
const indicators = crossFilterIndicators.concat(nativeFilterIndicators);