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 }, { name: 'test_12', column: 'year', datasetId: 2 },
]); ]);
setFilterBarOrientation('horizontal'); setFilterBarOrientation('horizontal');
openMoreFilters();
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.getBySel('slice-header').within(() => { 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.getBySel('dropdown-content').should('be.visible');
cy.get('.ant-select-focused').should('be.visible'); cy.get('.ant-select-focused').should('be.visible');
}); });

View File

@ -86,7 +86,7 @@ const createProps = () => ({
onHighlightFilterSource: jest.fn(), onHighlightFilterSource: jest.fn(),
}); });
test('Should render "appliedCrossFilterIndicators"', () => { test('Should render "appliedCrossFilterIndicators"', async () => {
const props = createProps(); const props = createProps();
props.appliedIndicators = []; props.appliedIndicators = [];
props.incompatibleIndicators = []; props.incompatibleIndicators = [];
@ -99,8 +99,10 @@ test('Should render "appliedCrossFilterIndicators"', () => {
{ useRedux: true }, { useRedux: true },
); );
userEvent.click(screen.getByTestId('details-panel-content')); userEvent.hover(screen.getByTestId('details-panel-content'));
expect(screen.getByText('Applied Cross Filters (1)')).toBeInTheDocument(); expect(
await screen.findByText('Applied cross-filters (1)'),
).toBeInTheDocument();
expect( expect(
screen.getByRole('button', { name: 'Clinical Stage' }), screen.getByRole('button', { name: 'Clinical Stage' }),
).toBeInTheDocument(); ).toBeInTheDocument();
@ -118,7 +120,7 @@ test('Should render "appliedCrossFilterIndicators"', () => {
]); ]);
}); });
test('Should render "appliedIndicators"', () => { test('Should render "appliedIndicators"', async () => {
const props = createProps(); const props = createProps();
props.appliedCrossFilterIndicators = []; props.appliedCrossFilterIndicators = [];
props.incompatibleIndicators = []; props.incompatibleIndicators = [];
@ -131,8 +133,8 @@ test('Should render "appliedIndicators"', () => {
{ useRedux: true }, { useRedux: true },
); );
userEvent.click(screen.getByTestId('details-panel-content')); userEvent.hover(screen.getByTestId('details-panel-content'));
expect(screen.getByText('Applied Filters (1)')).toBeInTheDocument(); expect(await screen.findByText('Applied filters (1)')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Country' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Country' })).toBeInTheDocument();
expect(props.onHighlightFilterSource).toBeCalledTimes(0); 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', () => { test('Should render empty', () => {
const props = createProps(); const props = createProps();
props.appliedCrossFilterIndicators = []; props.appliedCrossFilterIndicators = [];

View File

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

View File

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

View File

@ -36,7 +36,6 @@ import {
import { sliceId } from 'spec/fixtures/mockChartQueries'; import { sliceId } from 'spec/fixtures/mockChartQueries';
import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters'; import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters';
import { dashboardWithFilter } from 'spec/fixtures/mockDashboardLayout'; import { dashboardWithFilter } from 'spec/fixtures/mockDashboardLayout';
import Icons from 'src/components/Icons';
import { FeatureFlag } from 'src/featureFlags'; import { FeatureFlag } from 'src/featureFlags';
const defaultStore = getMockStoreWithFilters(); const defaultStore = getMockStoreWithFilters();
@ -111,36 +110,6 @@ describe('FiltersBadge', () => {
); );
expect(wrapper.find('WarningFilled')).not.toExist(); 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', () => { describe('for native filters', () => {
@ -189,37 +158,5 @@ describe('FiltersBadge', () => {
); );
expect(wrapper.find('WarningFilled')).not.toExist(); 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` export const Pill = styled.div`
${({ theme }) => css` ${({ theme }) => css`
display: inline-block; display: flex;
color: ${theme.colors.grayscale.light5}; color: ${theme.colors.grayscale.light5};
background: ${theme.colors.grayscale.base}; background: ${theme.colors.grayscale.base};
border-radius: 1em; border-radius: 1em;
@ -36,7 +36,6 @@ export const Pill = styled.div`
svg { svg {
position: relative; position: relative;
top: -2px;
color: ${theme.colors.grayscale.light5}; color: ${theme.colors.grayscale.light5};
width: 1em; width: 1em;
height: 1em; height: 1em;
@ -55,64 +54,27 @@ export const Pill = styled.div`
background: ${theme.colors.primary.dark1}; background: ${theme.colors.primary.dark1};
} }
} }
`}
`;
&.has-incompatible-filters { export const SectionName = styled.span`
color: ${theme.colors.grayscale.dark2}; ${({ theme }) => css`
background: ${theme.colors.alert.base}; font-weight: ${theme.typography.weights.bold};
&:hover { `}
background: ${theme.colors.alert.dark1}; `;
} export const FilterName = styled.span`
svg { ${({ theme }) => css`
color: ${theme.colors.grayscale.dark2}; padding-right: ${theme.gridUnit}px;
} font-style: italic;
} & > * {
margin-right: ${theme.gridUnit}px;
&.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 interface TitleProps { export const FilterItem = styled.button`
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`
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-wrap: wrap;
text-align: left; text-align: left;
padding: 0; padding: 0;
border: none; border: none;
@ -134,34 +96,36 @@ export const Item = styled.button`
} }
`; `;
export const Reset = styled.div` export const FiltersContainer = styled.div`
margin: 0 -${({ theme }) => theme.gridUnit * 4}px; ${({ theme }) => css`
`; margin-top: ${theme.gridUnit}px;
&:not(:last-child) {
export const Indent = styled.div` padding-bottom: ${theme.gridUnit * 3}px;
padding-left: ${({ theme }) => theme.gridUnit * 6}px; }
margin: -${({ theme }) => theme.gridUnit * 3}px 0; `}
`; `;
export const Panel = styled.div` export const FiltersDetailsContainer = styled.div`
min-width: 200px; ${({ theme }) => css`
max-width: 300px; min-width: 200px;
overflow-x: hidden; max-width: 300px;
`; overflow-x: hidden;
export const FilterValue = styled.div` color: ${theme.colors.grayscale.light5};
max-width: 100%; `}
flex-grow: 1; `;
overflow: auto;
color: ${({ theme }) => theme.colors.grayscale.light5}; export const FilterValue = styled.span`
`; max-width: 100%;
flex-grow: 1;
export const FilterIndicatorText = styled.div` overflow: auto;
${({ theme }) => ` `;
padding-top: ${theme.gridUnit * 3}px;
max-width: 100%; export const Separator = styled.div`
flex-grow: 1; ${({ theme }) => css`
overflow: auto; width: 100%;
color: ${theme.colors.grayscale.light5}; 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], [indicators],
); );
const unsetIndicators = useMemo(
() =>
indicators.filter(
indicator => indicator.status === IndicatorStatus.Unset,
),
[indicators],
);
const incompatibleIndicators = useMemo(
() =>
indicators.filter(
indicator => indicator.status === IndicatorStatus.Incompatible,
),
[indicators],
);
if ( if (!appliedCrossFilterIndicators.length && !appliedIndicators.length) {
!appliedCrossFilterIndicators.length &&
!appliedIndicators.length &&
!incompatibleIndicators.length &&
!unsetIndicators.length
) {
return null; return null;
} }
const isInactive =
!appliedCrossFilterIndicators.length &&
!appliedIndicators.length &&
!incompatibleIndicators.length;
return ( return (
<DetailsPanelPopover <DetailsPanelPopover
appliedCrossFilterIndicators={appliedCrossFilterIndicators} appliedCrossFilterIndicators={appliedCrossFilterIndicators}
appliedIndicators={appliedIndicators} appliedIndicators={appliedIndicators}
unsetIndicators={unsetIndicators}
incompatibleIndicators={incompatibleIndicators}
onHighlightFilterSource={onHighlightFilterSource} onHighlightFilterSource={onHighlightFilterSource}
> >
<Pill <Pill
className={cx( className={cx(
'filter-counts', 'filter-counts',
!!incompatibleIndicators.length && 'has-incompatible-filters',
!!appliedCrossFilterIndicators.length && 'has-cross-filters', !!appliedCrossFilterIndicators.length && 'has-cross-filters',
isInactive && 'filters-inactive',
)} )}
> >
<Icons.Filter iconSize="m" /> <Icons.Filter iconSize="m" />
{!isInactive && ( <span data-test="applied-filter-count">
<span data-test="applied-filter-count"> {appliedIndicators.length + appliedCrossFilterIndicators.length}
{appliedIndicators.length + appliedCrossFilterIndicators.length} </span>
</span>
)}
{incompatibleIndicators.length ? (
<>
{' '}
<Icons.AlertSolid />
<span data-test="incompatible-filter-count">
{incompatibleIndicators.length}
</span>
</>
) : null}
</Pill> </Pill>
</DetailsPanelPopover> </DetailsPanelPopover>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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