blob: b5a9432b01b3f45f2f8cc19904f5168c6beffb80 [file] [log] [blame]
/**
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
import { ExtraFormData, styled, t } from '@superset-ui/core';
import Popover from 'src/common/components/Popover';
import Icon from 'src/components/Icon';
import { Pill } from 'src/dashboard/components/FiltersBadge/Styles';
import { CurrentFilterState } from 'src/dashboard/reducers/types';
import { useFilterState } from './state';
import FilterControl from './FilterControl';
import CascadeFilterControl from './CascadeFilterControl';
import { CascadeFilter } from './types';
import { Filter } from '../types';
interface CascadePopoverProps {
filter: CascadeFilter;
visible: boolean;
directPathToChild?: string[];
onVisibleChange: (visible: boolean) => void;
onFilterSelectionChange: (
filter: Filter,
extraFormData: ExtraFormData,
currentState: CurrentFilterState,
) => void;
}
const StyledTitleBox = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: ${({ theme }) => theme.colors.grayscale.light4};
margin: ${({ theme }) => theme.gridUnit * -1}px
${({ theme }) => theme.gridUnit * -4}px; // to override default antd padding
padding: ${({ theme }) => theme.gridUnit * 2}px
${({ theme }) => theme.gridUnit * 4}px;
& > *:last-child {
cursor: pointer;
}
`;
const StyledTitle = styled.h4`
display: flex;
align-items: center;
color: ${({ theme }) => theme.colors.grayscale.dark1};
margin: 0;
padding: 0;
`;
const StyledIcon = styled(Icon)`
margin-right: ${({ theme }) => theme.gridUnit}px;
color: ${({ theme }) => theme.colors.grayscale.dark1};
width: ${({ theme }) => theme.gridUnit * 4}px;
`;
const StyledPill = styled(Pill)`
padding: ${({ theme }) => theme.gridUnit}px
${({ theme }) => theme.gridUnit * 2}px;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
background: ${({ theme }) => theme.colors.grayscale.light1};
`;
const CascadePopover: React.FC<CascadePopoverProps> = ({
filter,
visible,
onVisibleChange,
onFilterSelectionChange,
directPathToChild,
}) => {
const [currentPathToChild, setCurrentPathToChild] = useState<string[]>();
const filterState = useFilterState(filter.id);
useEffect(() => {
setCurrentPathToChild(directPathToChild);
// clear local copy of directPathToChild after 500ms
// to prevent triggering multiple focus
const timeout = setTimeout(() => setCurrentPathToChild(undefined), 500);
return () => clearTimeout(timeout);
}, [directPathToChild, setCurrentPathToChild]);
const getActiveChildren = useCallback(
(filter: CascadeFilter): CascadeFilter[] | null => {
const children = filter.cascadeChildren || [];
const currentValue = filterState.currentState?.value;
const activeChildren = children.flatMap(
childFilter => getActiveChildren(childFilter) || [],
);
if (activeChildren.length > 0) {
return activeChildren;
}
if (currentValue) {
return [filter];
}
return null;
},
[filterState],
);
const getAllFilters = (filter: CascadeFilter): CascadeFilter[] => {
const children = filter.cascadeChildren || [];
const allChildren = children.flatMap(getAllFilters);
return [filter, ...allChildren];
};
const allFilters = getAllFilters(filter);
const activeFilters = useMemo(() => getActiveChildren(filter) || [filter], [
filter,
getActiveChildren,
]);
useEffect(() => {
const focusedFilterId = currentPathToChild?.[0];
// filters not directly displayed in the Filter Bar
const inactiveFilters = allFilters.filter(
filterEl => !activeFilters.includes(filterEl),
);
const focusedInactiveFilter = inactiveFilters.some(
cascadeChild => cascadeChild.id === focusedFilterId,
);
if (focusedInactiveFilter) {
onVisibleChange(true);
}
}, [currentPathToChild]);
if (!filter.cascadeChildren?.length) {
return (
<FilterControl
filter={filter}
directPathToChild={directPathToChild}
onFilterSelectionChange={onFilterSelectionChange}
/>
);
}
const title = (
<StyledTitleBox>
<StyledTitle>
<StyledIcon name="edit" />
{t('Select parent filters')} ({allFilters.length})
</StyledTitle>
<StyledIcon name="close" onClick={() => onVisibleChange(false)} />
</StyledTitleBox>
);
const content = (
<CascadeFilterControl
data-test="cascade-filters-control"
key={filter.id}
filter={filter}
directPathToChild={visible ? currentPathToChild : undefined}
onFilterSelectionChange={onFilterSelectionChange}
/>
);
return (
<Popover
content={content}
title={title}
trigger="click"
visible={visible}
onVisibleChange={onVisibleChange}
placement="rightTop"
id={filter.id}
overlayStyle={{ minWidth: '400px', maxWidth: '600px' }}
>
<div>
{activeFilters.map(activeFilter => (
<FilterControl
key={activeFilter.id}
filter={activeFilter}
onFilterSelectionChange={onFilterSelectionChange}
directPathToChild={currentPathToChild}
icon={
<>
{filter.cascadeChildren.length !== 0 && (
<StyledPill onClick={() => onVisibleChange(true)}>
<Icon name="filter" /> {allFilters.length}
</StyledPill>
)}
</>
}
/>
))}
</div>
</Popover>
);
};
export default CascadePopover;