feat(cross-filter): Cross filter badge (#13687)

This commit is contained in:
simcha90 2021-03-18 17:59:13 +02:00 committed by GitHub
parent 9efe1a44ec
commit 577ecc284f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 468 additions and 290 deletions

View File

@ -0,0 +1,22 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="12" fill="transparent" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6177 16.3979C18.957 14.0586 18.957 10.2519 16.6177 7.91263C14.2784 5.57335 10.4717 5.57335 8.13245 7.91263L7.07178 6.85197C9.99603 3.92773 14.7541 3.92773 17.6784 6.85197C20.6026 9.77621 20.6026 14.5343 17.6784 17.4586C14.7541 20.3828 9.99603 20.3828 7.07178 17.4586L8.13245 16.3979C10.4717 18.7372 14.2779 18.7377 16.6177 16.3979ZM4.50001 11.7499C4.50001 11.4738 4.72387 11.2499 5.00001 11.2499L6.75001 11.2499L6.75001 10.7499C6.75001 10.3379 7.22039 10.1027 7.55001 10.3499L9.75001 11.9999C9.86952 12.072 8.45348 13.0769 7.52541 13.7191C7.19597 13.947 6.75001 13.71 6.75001 13.3094L6.75001 12.7499H5.00001C4.72387 12.7499 4.50001 12.5261 4.50001 12.2499V11.7499ZM10.2538 10.034C11.4237 8.86404 13.327 8.86457 14.4964 10.034C15.6658 11.2033 15.6663 13.1067 14.4964 14.2766C13.3265 15.4465 11.4231 15.446 10.2538 14.2766L9.19311 15.3373C10.948 17.0921 13.8022 17.0921 15.5571 15.3373C17.3119 13.5824 17.3119 10.7282 15.5571 8.97329C13.8022 7.21843 10.948 7.21843 9.19311 8.97329L10.2538 10.034Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -24,6 +24,7 @@ import { ReactComponent as AlertSolidSmallIcon } from 'images/icons/alert_solid_
import { ReactComponent as BinocularsIcon } from 'images/icons/binoculars.svg';
import { ReactComponent as BoltIcon } from 'images/icons/bolt.svg';
import { ReactComponent as BoltSmallIcon } from 'images/icons/bolt_small.svg';
import { ReactComponent as CrossFilterBadge } from 'images/icons/cross-filter-badge.svg';
import { ReactComponent as BoltSmallRunIcon } from 'images/icons/bolt_small_run.svg';
import { ReactComponent as CalendarIcon } from 'images/icons/calendar.svg';
import { ReactComponent as CancelIcon } from 'images/icons/cancel.svg';
@ -165,6 +166,7 @@ export type IconName =
| 'caret-right'
| 'caret-up'
| 'certified'
| 'cross-filter-badge'
| 'check'
| 'checkbox-half'
| 'checkbox-off'
@ -281,6 +283,7 @@ export const iconsRegistry: Record<
'alert-solid-small': AlertSolidSmallIcon,
'bolt-small': BoltSmallIcon,
'bolt-small-run': BoltSmallRunIcon,
'cross-filter-badge': CrossFilterBadge,
'cancel-solid': CancelSolidIcon,
'cancel-x': CancelXIcon,
'card-view': CardViewIcon,

View File

@ -25,6 +25,7 @@ import { useForceUpdate } from '../nativeFilters/FiltersConfigModal/FiltersConfi
import { CrossFilterScopingFormType } from './types';
type CrossFilterScopingFormProps = {
chartId: number;
scope: Scope;
form: FormInstance<CrossFilterScopingFormType>;
};
@ -32,6 +33,7 @@ type CrossFilterScopingFormProps = {
const CrossFilterScopingForm: FC<CrossFilterScopingFormProps> = ({
form,
scope,
chartId,
}) => {
const forceUpdate = useForceUpdate();
const formScope = form.getFieldValue('scope');
@ -44,6 +46,7 @@ const CrossFilterScopingForm: FC<CrossFilterScopingFormProps> = ({
});
}}
scope={scope}
chartId={chartId}
formScope={formScope}
forceUpdate={forceUpdate}
formScoping={formScoping}

View File

@ -49,7 +49,10 @@ const CrossFilterScopingModal: FC<CrossFilterScopingModalProps> = ({
dispatch(
setChartConfiguration({
...chartConfig,
[chartId]: { crossFilters: { scope: form.getFieldValue('scope') } },
[chartId]: {
id: chartId,
crossFilters: { scope: form.getFieldValue('scope') },
},
}),
);
onClose();
@ -88,7 +91,7 @@ const CrossFilterScopingModal: FC<CrossFilterScopingModalProps> = ({
}
>
<StyledForm preserve={false} form={form} layout="vertical">
<CrossFilterScopingForm form={form} scope={scope} />
<CrossFilterScopingForm form={form} scope={scope} chartId={chartId} />
</StyledForm>
</StyledModal>
);

View File

@ -19,7 +19,6 @@
import React, { useState } from 'react';
import { t, useTheme, css } from '@superset-ui/core';
import {
SearchOutlined,
MinusCircleFilled,
CheckCircleFilled,
ExclamationCircleFilled,
@ -27,43 +26,13 @@ import {
import { Popover } from 'src/common/components/index';
import Collapse from 'src/common/components/Collapse';
import { Global } from '@emotion/core';
import {
Indent,
Item,
ItemIcon,
Panel,
Reset,
Title,
FilterValue,
} from './Styles';
import Icon from 'src/components/Icon';
import { Indent, Panel, Reset, Title } from './Styles';
import { Indicator } from './selectors';
import { getFilterValueForDisplay } from '../nativeFilters/FilterBar/FilterSets/utils';
export interface IndicatorProps {
indicator: Indicator;
onClick: (path: string[]) => void;
}
const Indicator = ({
indicator: { column, name, value = [], path },
onClick,
}: IndicatorProps) => {
const resultValue = getFilterValueForDisplay(value);
return (
<Item onClick={() => onClick([...path, `LABEL-${column}`])}>
<Title bold>
<ItemIcon>
<SearchOutlined />
</ItemIcon>
{name}
{resultValue ? ': ' : ''}
</Title>
<FilterValue>{resultValue}</FilterValue>
</Item>
);
};
import FilterIndicator from './FilterIndicator';
export interface DetailsPanelProps {
appliedCrossFilterIndicators: Indicator[];
appliedIndicators: Indicator[];
incompatibleIndicators: Indicator[];
unsetIndicators: Indicator[];
@ -72,6 +41,7 @@ export interface DetailsPanelProps {
}
const DetailsPanelPopover = ({
appliedCrossFilterIndicators = [],
appliedIndicators = [],
incompatibleIndicators = [],
unsetIndicators = [],
@ -80,21 +50,34 @@ const DetailsPanelPopover = ({
}: DetailsPanelProps) => {
const theme = useTheme();
function defaultActivePanel() {
if (incompatibleIndicators.length) return 'incompatible';
if (appliedIndicators.length) return 'applied';
return 'unset';
}
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[]>(() => [
defaultActivePanel(),
...getDefaultActivePanel(),
]);
function handlePopoverStatus(isOpen: boolean) {
// every time the popover opens, make sure the most relevant panel is active
if (isOpen) {
if (!activePanels.includes(defaultActivePanel())) {
setActivePanels([...activePanels, defaultActivePanel()]);
if (
!activePanels.find(panel => getDefaultActivePanel().includes(panel))
) {
setActivePanels([...activePanels, ...getDefaultActivePanel()]);
}
}
}
@ -168,6 +151,33 @@ const DetailsPanelPopover = ({
activeKey={activePanels}
onChange={handleActivePanelChange}
>
{appliedCrossFilterIndicators.length ? (
<Collapse.Panel
key="appliedCrossFilters"
header={
<Title bold color={theme.colors.primary.light1}>
<Icon
name="cross-filter-badge"
css={{ fill: theme.colors.primary.light1 }}
/>
{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"
@ -180,7 +190,7 @@ const DetailsPanelPopover = ({
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{appliedIndicators.map(indicator => (
<Indicator
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
@ -204,7 +214,7 @@ const DetailsPanelPopover = ({
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{incompatibleIndicators.map(indicator => (
<Indicator
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}
@ -226,7 +236,7 @@ const DetailsPanelPopover = ({
>
<Indent css={{ paddingBottom: theme.gridUnit * 3 }}>
{unsetIndicators.map(indicator => (
<Indicator
<FilterIndicator
key={indicatorKey(indicator)}
indicator={indicator}
onClick={onHighlightFilterSource}

View File

@ -0,0 +1,51 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SearchOutlined } from '@ant-design/icons';
import React, { FC } from 'react';
import { getFilterValueForDisplay } from '../nativeFilters/FilterBar/FilterSets/utils';
import { FilterValue, Item, ItemIcon, Title } from './Styles';
import { Indicator } from './selectors';
export interface IndicatorProps {
indicator: Indicator;
onClick?: (path: string[]) => void;
}
const FilterIndicator: FC<IndicatorProps> = ({
indicator: { column, name, value, path = [] },
onClick = () => {},
}) => {
const resultValue = getFilterValueForDisplay(value);
return (
<Item onClick={() => onClick([...path, `LABEL-${column}`])}>
<Title bold>
<ItemIcon>
<SearchOutlined />
</ItemIcon>
{name}
{resultValue ? ': ' : ''}
</Title>
<FilterValue>{resultValue}</FilterValue>
</Item>
);
};
export default FilterIndicator;

View File

@ -48,6 +48,13 @@ export const Pill = styled.div`
background: ${({ theme }) => theme.colors.grayscale.dark1};
}
&.has-cross-filters {
background: ${({ theme }) => theme.colors.primary.base};
&:hover {
background: ${({ theme }) => theme.colors.primary.dark1};
}
}
&.has-incompatible-filters {
color: ${({ theme }) => theme.colors.grayscale.dark2};
background: ${({ theme }) => theme.colors.alert.base};
@ -73,15 +80,6 @@ export const Pill = styled.div`
}
`;
export const WarningPill = styled(Pill)`
background: ${({ theme }) => theme.colors.alert.base};
color: ${({ theme }) => theme.colors.grayscale.dark1};
`;
export const UnsetPill = styled(Pill)`
background: ${({ theme }) => theme.colors.grayscale.light1};
`;
export interface TitleProps {
bold?: boolean;
color?: string;
@ -95,6 +93,11 @@ export const Title = styled.span<TitleProps>`
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`

View File

@ -24,6 +24,7 @@ import { Pill } from './Styles';
import { Indicator } from './selectors';
export interface FiltersBadgeProps {
appliedCrossFilterIndicators: Indicator[];
appliedIndicators: Indicator[];
unsetIndicators: Indicator[];
incompatibleIndicators: Indicator[];
@ -31,12 +32,14 @@ export interface FiltersBadgeProps {
}
const FiltersBadge = ({
appliedCrossFilterIndicators,
appliedIndicators,
unsetIndicators,
incompatibleIndicators,
onHighlightFilterSource,
}: FiltersBadgeProps) => {
if (
!appliedCrossFilterIndicators.length &&
!appliedIndicators.length &&
!incompatibleIndicators.length &&
!unsetIndicators.length
@ -45,10 +48,13 @@ const FiltersBadge = ({
}
const isInactive =
!appliedIndicators.length && !incompatibleIndicators.length;
!appliedCrossFilterIndicators.length &&
!appliedIndicators.length &&
!incompatibleIndicators.length;
return (
<DetailsPanelPopover
appliedCrossFilterIndicators={appliedCrossFilterIndicators}
appliedIndicators={appliedIndicators}
unsetIndicators={unsetIndicators}
incompatibleIndicators={incompatibleIndicators}
@ -58,13 +64,14 @@ const FiltersBadge = ({
className={cx(
'filter-counts',
!!incompatibleIndicators.length && 'has-incompatible-filters',
!!appliedCrossFilterIndicators.length && 'has-cross-filters',
isInactive && 'filters-inactive',
)}
>
<Icon name="filter" />
{!isInactive && (
<span data-test="applied-filter-count">
{appliedIndicators.length}
{appliedIndicators.length + appliedCrossFilterIndicators.length}
</span>
)}
{incompatibleIndicators.length ? (

View File

@ -18,16 +18,19 @@
*/
import { TIME_FILTER_MAP } from 'src/explore/constants';
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
import { NativeFiltersState } from 'src/dashboard/reducers/types';
import { DataMaskStateWithId } from 'src/dataMask/types';
import {
ChartConfiguration,
NativeFiltersState,
} from 'src/dashboard/reducers/types';
import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
import { Layout } from '../../types';
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
import { FilterValue } from '../nativeFilters/types';
export enum IndicatorStatus {
Unset = 'UNSET',
Applied = 'APPLIED',
Incompatible = 'INCOMPATIBLE',
CrossFilterApplied = 'CROSS_FILTER_APPLIED',
}
const TIME_GRANULARITY_FIELDS = new Set(Object.values(TIME_FILTER_MAP));
@ -53,7 +56,7 @@ const selectIndicatorValue = (
columnKey: string,
filter: Filter,
datasource: Datasource,
): FilterValue => {
): any => {
const values = filter.columns[columnKey];
const arrValues = Array.isArray(values) ? values : [values];
@ -133,9 +136,9 @@ const getRejectedColumns = (chart: any): Set<string> =>
export type Indicator = {
column?: string;
name: string;
value: FilterValue;
status: IndicatorStatus;
path: string[];
value?: any;
status?: IndicatorStatus;
path?: string[];
};
// inspects redux state to find what the filter indicators should be shown for a given chart
@ -179,22 +182,32 @@ export const selectNativeIndicatorsForChart = (
chartId: number,
charts: any,
dashboardLayout: Layout,
chartConfiguration: ChartConfiguration = {},
): Indicator[] => {
const chart = charts[chartId];
const appliedColumns = getAppliedColumns(chart);
const rejectedColumns = getRejectedColumns(chart);
const getStatus = (
value: FilterValue,
isAffectedByScope: boolean,
column?: string,
): IndicatorStatus => {
const getStatus = ({
value,
isAffectedByScope,
column,
type = DataMaskType.NativeFilters,
}: {
value: any;
isAffectedByScope: boolean;
column?: string;
type?: DataMaskType;
}): IndicatorStatus => {
// a filter is only considered unset if it's value is null
const hasValue = value !== null;
if (!isAffectedByScope) {
return IndicatorStatus.Unset;
}
if (type === DataMaskType.CrossFilters && hasValue) {
return IndicatorStatus.CrossFilterApplied;
}
if (!column && hasValue) {
// Filter without datasource
return IndicatorStatus.Applied;
@ -207,26 +220,59 @@ export const selectNativeIndicatorsForChart = (
return IndicatorStatus.Unset;
};
const indicators = Object.values(nativeFilters.filters).map(nativeFilter => {
const isAffectedByScope = getTreeCheckedItems(
nativeFilter.scope,
dashboardLayout,
).some(
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
);
const column = nativeFilter.targets[0]?.column?.name;
const dataMaskNativeFilters = dataMask.nativeFilters?.[nativeFilter.id];
let value = dataMaskNativeFilters?.currentState?.value ?? [];
if (!Array.isArray(value)) {
value = [value];
}
return {
column,
name: nativeFilter.name,
path: [nativeFilter.id],
status: getStatus(value, isAffectedByScope, column),
value,
};
});
return indicators;
const nativeFilterIndicators = Object.values(nativeFilters.filters).map(
nativeFilter => {
const isAffectedByScope = getTreeCheckedItems(
nativeFilter.scope,
dashboardLayout,
).some(
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
);
const column = nativeFilter.targets[0]?.column?.name;
const dataMaskNativeFilters = dataMask.nativeFilters?.[nativeFilter.id];
let value = dataMaskNativeFilters?.currentState?.value ?? null;
if (!Array.isArray(value) && value !== null) {
value = [value];
}
return {
column,
name: nativeFilter.name,
path: [nativeFilter.id],
status: getStatus({ value, isAffectedByScope, column }),
value,
};
},
);
const crossFilterIndicators = Object.values(chartConfiguration).map(
chartConfig => {
const scope = chartConfig?.crossFilters?.scope;
const isAffectedByScope = getTreeCheckedItems(
scope,
dashboardLayout,
).some(
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
);
const dataMaskCrossFilters = dataMask.crossFilters?.[chartConfig.id];
let value = dataMaskCrossFilters?.currentState?.value ?? null;
if (!Array.isArray(value) && value !== null) {
value = [value];
}
return {
name: Object.values(dashboardLayout).find(
layoutItem => layoutItem?.meta?.chartId === chartConfig.id,
)?.meta?.sliceName as string,
path: [`${chartConfig.id}`],
status: getStatus({
value,
isAffectedByScope,
type: DataMaskType.CrossFilters,
}),
value,
};
},
);
return crossFilterIndicators.concat(nativeFilterIndicators);
};

View File

@ -1,179 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@superset-ui/core';
import { Tooltip } from 'src/common/components/Tooltip';
import EditableTitle from '../../components/EditableTitle';
import SliceHeaderControls from './SliceHeaderControls';
import FiltersBadge from '../containers/FiltersBadge';
const propTypes = {
innerRef: PropTypes.func,
slice: PropTypes.object.isRequired,
isExpanded: PropTypes.bool,
isCached: PropTypes.arrayOf(PropTypes.bool),
cachedDttm: PropTypes.arrayOf(PropTypes.string),
updatedDttm: PropTypes.number,
updateSliceName: PropTypes.func,
toggleExpandSlice: PropTypes.func,
forceRefresh: PropTypes.func,
exploreChart: PropTypes.func,
exportCSV: PropTypes.func,
editMode: PropTypes.bool,
annotationQuery: PropTypes.object,
annotationError: PropTypes.object,
sliceName: PropTypes.string,
supersetCanExplore: PropTypes.bool,
supersetCanCSV: PropTypes.bool,
sliceCanEdit: PropTypes.bool,
componentId: PropTypes.string.isRequired,
dashboardId: PropTypes.number.isRequired,
filters: PropTypes.object.isRequired,
addSuccessToast: PropTypes.func.isRequired,
addDangerToast: PropTypes.func.isRequired,
handleToggleFullSize: PropTypes.func.isRequired,
chartStatus: PropTypes.string.isRequired,
};
const defaultProps = {
innerRef: null,
forceRefresh: () => ({}),
updateSliceName: () => ({}),
toggleExpandSlice: () => ({}),
exploreChart: () => ({}),
exportCSV: () => ({}),
editMode: false,
annotationQuery: {},
annotationError: {},
cachedDttm: null,
updatedDttm: null,
isCached: [],
isExpanded: [],
sliceName: '',
supersetCanExplore: false,
supersetCanCSV: false,
sliceCanEdit: false,
};
const annoationsLoading = t('Annotation layers are still loading.');
const annoationsError = t('One ore more annotation layers failed loading.');
class SliceHeader extends React.PureComponent {
render() {
const {
slice,
isExpanded,
isCached,
cachedDttm,
updatedDttm,
toggleExpandSlice,
forceRefresh,
exploreChart,
exportCSV,
innerRef,
sliceName,
supersetCanExplore,
supersetCanCSV,
sliceCanEdit,
editMode,
updateSliceName,
annotationQuery,
annotationError,
componentId,
dashboardId,
addSuccessToast,
addDangerToast,
handleToggleFullSize,
isFullSize,
chartStatus,
} = this.props;
return (
<div className="chart-header" ref={innerRef}>
<div className="header-title">
<EditableTitle
title={
sliceName ||
(editMode
? '---' // this makes an empty title clickable
: '')
}
canEdit={editMode}
emptyText=""
onSaveTitle={updateSliceName}
showTooltip={false}
/>
{!!Object.values(annotationQuery).length && (
<Tooltip
id="annotations-loading-tooltip"
placement="top"
title={annoationsLoading}
>
<i className="fa fa-refresh warning" />
</Tooltip>
)}
{!!Object.values(annotationError).length && (
<Tooltip
id="annoation-errors-tooltip"
placement="top"
title={annoationsError}
>
<i className="fa fa-exclamation-circle danger" />
</Tooltip>
)}
</div>
<div className="header-controls">
{!editMode && (
<>
<FiltersBadge chartId={slice.slice_id} />
<SliceHeaderControls
slice={slice}
isCached={isCached}
isExpanded={isExpanded}
cachedDttm={cachedDttm}
updatedDttm={updatedDttm}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={forceRefresh}
exploreChart={exploreChart}
exportCSV={exportCSV}
supersetCanExplore={supersetCanExplore}
supersetCanCSV={supersetCanCSV}
sliceCanEdit={sliceCanEdit}
componentId={componentId}
dashboardId={dashboardId}
addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast}
handleToggleFullSize={handleToggleFullSize}
isFullSize={isFullSize}
chartStatus={chartStatus}
/>
</>
)}
</div>
</div>
);
}
}
SliceHeader.propTypes = propTypes;
SliceHeader.defaultProps = defaultProps;
export default SliceHeader;

View File

@ -0,0 +1,184 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { FC } from 'react';
import { styled, t } from '@superset-ui/core';
import { Tooltip } from 'src/common/components/Tooltip';
import { useSelector } from 'react-redux';
import EditableTitle from '../../components/EditableTitle';
import SliceHeaderControls from './SliceHeaderControls';
import FiltersBadge from '../containers/FiltersBadge';
import Icon from '../../components/Icon';
import { RootState } from '../types';
import { Slice } from '../../types/Chart';
import FilterIndicator from './FiltersBadge/FilterIndicator';
type SliceHeaderProps = {
innerRef?: string;
slice: Slice;
isExpanded?: boolean;
isCached?: boolean[];
cachedDttm?: string[];
updatedDttm?: number;
updateSliceName?: (arg0: string) => void;
toggleExpandSlice?: Function;
forceRefresh?: Function;
exploreChart?: Function;
exportCSV?: Function;
editMode?: boolean;
isFullSize?: boolean;
annotationQuery?: object;
annotationError?: object;
sliceName?: string;
supersetCanExplore?: boolean;
supersetCanCSV?: boolean;
sliceCanEdit?: boolean;
componentId: string;
dashboardId: number;
filters: object;
addSuccessToast: Function;
addDangerToast: Function;
handleToggleFullSize: Function;
chartStatus: string;
};
const annoationsLoading = t('Annotation layers are still loading.');
const annoationsError = t('One ore more annotation layers failed loading.');
const CrossFilterIcon = styled(Icon)`
fill: ${({ theme }) => theme.colors.grayscale.light5};
& circle {
fill: ${({ theme }) => theme.colors.primary.base};
}
`;
const SliceHeader: FC<SliceHeaderProps> = ({
innerRef = null,
forceRefresh = () => ({}),
updateSliceName = () => ({}),
toggleExpandSlice = () => ({}),
exploreChart = () => ({}),
exportCSV = () => ({}),
editMode = false,
annotationQuery = {},
annotationError = {},
cachedDttm = null,
updatedDttm = null,
isCached = [],
isExpanded = [],
sliceName = '',
supersetCanExplore = false,
supersetCanCSV = false,
sliceCanEdit = false,
slice,
componentId,
dashboardId,
addSuccessToast,
addDangerToast,
handleToggleFullSize,
isFullSize,
chartStatus,
}) => {
// TODO: change to indicator field after it will be implemented
const crossFilterValue = useSelector<RootState, any>(
state =>
state.dataMask?.crossFilters?.[slice?.slice_id]?.currentState?.value,
);
return (
<div className="chart-header" ref={innerRef}>
<div className="header-title">
<EditableTitle
title={
sliceName ||
(editMode
? '---' // this makes an empty title clickable
: '')
}
canEdit={editMode}
emptyText=""
onSaveTitle={updateSliceName}
showTooltip={false}
/>
{!!Object.values(annotationQuery).length && (
<Tooltip
id="annotations-loading-tooltip"
placement="top"
title={annoationsLoading}
>
<i className="fa fa-refresh warning" />
</Tooltip>
)}
{!!Object.values(annotationError).length && (
<Tooltip
id="annoation-errors-tooltip"
placement="top"
title={annoationsError}
>
<i className="fa fa-exclamation-circle danger" />
</Tooltip>
)}
</div>
<div className="header-controls">
{!editMode && (
<>
{crossFilterValue && (
<Tooltip
placement="top"
title={
<FilterIndicator
indicator={{
value: crossFilterValue,
name: t('Emitted values'),
}}
/>
}
>
<CrossFilterIcon name="cross-filter-badge" />
</Tooltip>
)}
<FiltersBadge chartId={slice.slice_id} />
<SliceHeaderControls
slice={slice}
isCached={isCached}
isExpanded={isExpanded}
cachedDttm={cachedDttm}
updatedDttm={updatedDttm}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={forceRefresh}
exploreChart={exploreChart}
exportCSV={exportCSV}
supersetCanExplore={supersetCanExplore}
supersetCanCSV={supersetCanCSV}
sliceCanEdit={sliceCanEdit}
componentId={componentId}
dashboardId={dashboardId}
addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast}
handleToggleFullSize={handleToggleFullSize}
isFullSize={isFullSize}
chartStatus={chartStatus}
/>
</>
)}
</div>
</div>
);
};
export default SliceHeader;

View File

@ -33,6 +33,7 @@ type FilterScopeProps = {
forceUpdate: Function;
scope?: Scope;
formScoping?: Scoping;
chartId?: number;
};
const Wrapper = styled.div`
@ -54,9 +55,10 @@ const FilterScope: FC<FilterScopeProps> = ({
forceUpdate,
scope,
updateFormValues,
chartId,
}) => {
const initialScope = scope || getDefaultScopeValue();
const initialScoping = isScopingAll(initialScope)
const initialScope = scope || getDefaultScopeValue(chartId);
const initialScoping = isScopingAll(initialScope, chartId)
? Scoping.all
: Scoping.specific;
@ -70,8 +72,9 @@ const FilterScope: FC<FilterScopeProps> = ({
<Radio.Group
onChange={({ target: { value } }) => {
if (value === Scoping.all) {
const scope = getDefaultScopeValue(chartId);
updateFormValues({
scope: getDefaultScopeValue(),
scope,
});
}
forceUpdate();
@ -94,6 +97,7 @@ const FilterScope: FC<FilterScopeProps> = ({
initialScope={initialScope}
formScope={formScope}
forceUpdate={forceUpdate}
chartId={chartId}
/>
)}
<CleanFormItem

View File

@ -29,6 +29,7 @@ type ScopingTreeProps = {
updateFormValues: (values: any) => void;
formScope?: Scope;
initialScope: Scope;
chartId?: number;
};
const ScopingTree: FC<ScopingTreeProps> = ({
@ -36,22 +37,28 @@ const ScopingTree: FC<ScopingTreeProps> = ({
initialScope,
forceUpdate,
updateFormValues,
chartId,
}) => {
const [expandedKeys, setExpandedKeys] = useState<string[]>([
DASHBOARD_ROOT_ID,
]);
const { treeData, layout } = useFilterScopeTree();
const { treeData, layout } = useFilterScopeTree(chartId);
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
const handleExpand = (expandedKeys: string[]) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const handleCheck = (checkedKeys: string[]) => {
forceUpdate();
const scope = findFilterScope(checkedKeys, layout);
if (chartId !== undefined) {
scope.excluded = [...new Set([...scope.excluded, chartId])];
}
updateFormValues({
scope: findFilterScope(checkedKeys, layout),
scope,
});
};

View File

@ -29,7 +29,9 @@ import { TreeItem } from './types';
import { buildTree } from './utils';
// eslint-disable-next-line import/prefer-default-export
export function useFilterScopeTree(): {
export function useFilterScopeTree(
currentChartId?: number,
): {
treeData: [TreeItem];
layout: Layout;
} {
@ -49,12 +51,12 @@ export function useFilterScopeTree(): {
const validNodes = useMemo(
() =>
Object.values(layout).reduce<string[]>((acc, cur) => {
if (cur?.type === CHART_TYPE) {
if (cur?.type === CHART_TYPE && currentChartId !== cur?.meta?.chartId) {
return [...new Set([...acc, ...cur?.parents, cur.id])];
}
return acc;
}, []),
[layout],
[layout, currentChartId],
);
useMemo(() => {

View File

@ -145,10 +145,12 @@ export const findFilterScope = (
};
};
export const getDefaultScopeValue = () => ({
export const getDefaultScopeValue = (chartId?: number): Scope => ({
rootPath: [DASHBOARD_ROOT_ID],
excluded: [],
excluded: chartId ? [chartId] : [],
});
export const isScopingAll = (scope: Scope) =>
!scope || (scope.rootPath[0] === DASHBOARD_ROOT_ID && !scope.excluded.length);
export const isScopingAll = (scope: Scope, chartId?: number) =>
!scope ||
(scope.rootPath[0] === DASHBOARD_ROOT_ID &&
!scope.excluded.filter(item => item !== chartId).length);

View File

@ -37,12 +37,10 @@ export interface Target {
// clarityColumns?: Column[];
}
export type FilterValue = string | number | (string | number)[] | null;
export interface Filter {
cascadeParentIds: string[];
defaultValue: FilterValue;
currentValue?: FilterValue;
defaultValue: any;
currentValue?: any;
isInstant: boolean;
id: string; // randomly generated at filter creation
name: string;

View File

@ -87,6 +87,7 @@ function mapStateToProps(
supersetCanCSV: !!dashboardInfo.superset_can_csv,
sliceCanEdit: !!dashboardInfo.slice_can_edit,
ownCurrentState: dataMask.ownFilters?.[id]?.currentState,
crossFilterCurrentState: dataMask.crossFilters?.[id]?.currentState,
};
}

View File

@ -47,7 +47,9 @@ const sortByStatus = (indicators: Indicator[]): Indicator[] => {
IndicatorStatus.Incompatible,
];
return indicators.sort(
(a, b) => statuses.indexOf(a.status) - statuses.indexOf(b.status),
(a, b) =>
statuses.indexOf(a.status as IndicatorStatus) -
statuses.indexOf(b.status as IndicatorStatus),
);
};
@ -56,6 +58,7 @@ const mapStateToProps = (
datasources,
dashboardFilters,
nativeFilters,
dashboardInfo,
charts,
dataMask,
dashboardLayout: { present },
@ -75,6 +78,7 @@ const mapStateToProps = (
chartId,
charts,
present,
dashboardInfo.metadata?.chart_configuration,
);
const indicators = uniqWith(
@ -86,6 +90,9 @@ const mapStateToProps = (
ind2.status !== IndicatorStatus.Applied),
);
const appliedCrossFilterIndicators = indicators.filter(
indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
);
const appliedIndicators = indicators.filter(
indicator => indicator.status === IndicatorStatus.Applied,
);
@ -99,6 +106,7 @@ const mapStateToProps = (
return {
chartId,
appliedIndicators,
appliedCrossFilterIndicators,
unsetIndicators,
incompatibleIndicators,
};

View File

@ -27,7 +27,8 @@ export enum Scoping {
}
export type ChartConfiguration = {
[chartId: string]: {
[chartId: number]: {
id: number;
crossFilters: {
scope: Scope;
};

View File

@ -19,6 +19,7 @@
import { ChartProps } from '@superset-ui/core';
import { chart } from 'src/chart/chartReducer';
import componentTypes from 'src/dashboard/util/componentTypes';
import { DataMaskStateWithId } from '../dataMask/types';
export type ChartReducerInitialState = typeof chart;
@ -44,6 +45,7 @@ export type RootState = {
charts: { [key: string]: Chart };
dashboardLayout: { present: { [key: string]: LayoutItem } };
dashboardFilters: {};
dataMask: DataMaskStateWithId;
};
/** State of dashboardLayout in redux */