mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
feat(dashboard): Refactor FiltersBadge (#23286)
This commit is contained in:
parent
6311b40329
commit
c2b282ac71
@ -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');
|
||||||
});
|
});
|
||||||
|
@ -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 = [];
|
||||||
|
@ -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,30 +120,16 @@ const DetailsPanelPopover = ({
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<Reset>
|
<div>
|
||||||
<Collapse
|
|
||||||
ghost
|
|
||||||
light
|
|
||||||
activeKey={activePanels}
|
|
||||||
onChange={handleActivePanelChange}
|
|
||||||
>
|
|
||||||
{appliedCrossFilterIndicators.length ? (
|
{appliedCrossFilterIndicators.length ? (
|
||||||
<Collapse.Panel
|
<div>
|
||||||
key="appliedCrossFilters"
|
<SectionName>
|
||||||
header={
|
|
||||||
<Title bold color={theme.colors.primary.light1}>
|
|
||||||
<Icons.CursorTarget
|
|
||||||
css={{ fill: theme.colors.primary.light1 }}
|
|
||||||
iconSize="xl"
|
|
||||||
/>
|
|
||||||
{t(
|
{t(
|
||||||
'Applied Cross Filters (%d)',
|
'Applied cross-filters (%d)',
|
||||||
appliedCrossFilterIndicators.length,
|
appliedCrossFilterIndicators.length,
|
||||||
)}
|
)}
|
||||||
</Title>
|
</SectionName>
|
||||||
}
|
<FiltersContainer>
|
||||||
>
|
|
||||||
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
|
|
||||||
{appliedCrossFilterIndicators.map(indicator => (
|
{appliedCrossFilterIndicators.map(indicator => (
|
||||||
<FilterIndicator
|
<FilterIndicator
|
||||||
key={indicatorKey(indicator)}
|
key={indicatorKey(indicator)}
|
||||||
@ -199,20 +137,18 @@ const DetailsPanelPopover = ({
|
|||||||
onClick={onHighlightFilterSource}
|
onClick={onHighlightFilterSource}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Indent>
|
</FiltersContainer>
|
||||||
</Collapse.Panel>
|
</div>
|
||||||
|
) : null}
|
||||||
|
{appliedCrossFilterIndicators.length && appliedIndicators.length ? (
|
||||||
|
<Separator />
|
||||||
) : null}
|
) : null}
|
||||||
{appliedIndicators.length ? (
|
{appliedIndicators.length ? (
|
||||||
<Collapse.Panel
|
<div>
|
||||||
key="applied"
|
<SectionName>
|
||||||
header={
|
{t('Applied filters (%d)', appliedIndicators.length)}
|
||||||
<Title bold color={theme.colors.success.base}>
|
</SectionName>
|
||||||
<Icons.CheckCircleFilled css={iconReset} iconSize="m" />{' '}
|
<FiltersContainer>
|
||||||
{t('Applied Filters (%d)', appliedIndicators.length)}
|
|
||||||
</Title>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
|
|
||||||
{appliedIndicators.map(indicator => (
|
{appliedIndicators.map(indicator => (
|
||||||
<FilterIndicator
|
<FilterIndicator
|
||||||
key={indicatorKey(indicator)}
|
key={indicatorKey(indicator)}
|
||||||
@ -220,58 +156,11 @@ const DetailsPanelPopover = ({
|
|||||||
onClick={onHighlightFilterSource}
|
onClick={onHighlightFilterSource}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Indent>
|
</FiltersContainer>
|
||||||
</Collapse.Panel>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{incompatibleIndicators.length ? (
|
</div>
|
||||||
<Collapse.Panel
|
</FiltersDetailsContainer>
|
||||||
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>
|
||||||
|
@ -22,31 +22,30 @@ 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>
|
}
|
||||||
|
>
|
||||||
|
{onClick && (
|
||||||
|
<i>
|
||||||
<Icons.SearchOutlined
|
<Icons.SearchOutlined
|
||||||
iconSize="m"
|
iconSize="m"
|
||||||
css={css`
|
css={css`
|
||||||
@ -55,14 +54,16 @@ const FilterIndicator: FC<IndicatorProps> = ({
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</ItemIcon>
|
</i>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<FilterName>
|
||||||
{name}
|
{name}
|
||||||
{resultValue ? ': ' : ''}
|
{resultValue ? ': ' : ''}
|
||||||
</Title>
|
</FilterName>
|
||||||
<FilterValue>{resultValue}</FilterValue>
|
<FilterValue>{resultValue}</FilterValue>
|
||||||
</Item>
|
</div>
|
||||||
{text && <FilterIndicatorText>{text}</FilterIndicatorText>}
|
</FilterItem>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
padding-bottom: ${theme.gridUnit * 3}px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Indent = styled.div`
|
export const FiltersDetailsContainer = styled.div`
|
||||||
padding-left: ${({ theme }) => theme.gridUnit * 6}px;
|
${({ theme }) => css`
|
||||||
margin: -${({ theme }) => theme.gridUnit * 3}px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Panel = styled.div`
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
overflow-x: hidden;
|
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};
|
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;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
shouldFilterEmitters,
|
|
||||||
);
|
);
|
||||||
selectedCrossFilters = [
|
if (
|
||||||
...selectedCrossFilters,
|
isDefined(filterIndicator.column) &&
|
||||||
...(crossFilters as CrossFilterIndicator[]),
|
isDefined(filterIndicator.value)
|
||||||
];
|
) {
|
||||||
|
return { ...filterIndicator, emitterId: id };
|
||||||
}
|
}
|
||||||
return selectedCrossFilters;
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as CrossFilterIndicator[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default crossFiltersSelector;
|
export default crossFiltersSelector;
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
@ -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 || '',
|
|
||||||
],
|
|
||||||
status: getStatus({
|
|
||||||
label,
|
|
||||||
type: DataMaskType.CrossFilters,
|
type: DataMaskType.CrossFilters,
|
||||||
}),
|
appliedColumns,
|
||||||
value: label,
|
rejectedColumns,
|
||||||
};
|
});
|
||||||
if (filterEmitter) {
|
|
||||||
(filterObject as CrossFilterIndicator).emitterId = chartId;
|
return { ...filterIndicator, status: filterStatus };
|
||||||
}
|
|
||||||
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);
|
||||||
|
Loading…
Reference in New Issue
Block a user