blob: ed1df92ccc35a928864a8a2655f48946a2797c0c [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 { styled, t, ExtraFormData } from '@superset-ui/core';
import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import cx from 'classnames';
import Button from 'src/components/Button';
import Icon from 'src/components/Icon';
import { CurrentFilterState } from 'src/dashboard/reducers/types';
import FilterConfigurationLink from './FilterConfigurationLink';
import { useFilters, useSetExtraFormData } from './state';
import { useFilterConfiguration } from '../state';
import { Filter } from '../types';
import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
import CascadePopover from './CascadePopover';
const barWidth = `250px`;
const BarWrapper = styled.div`
width: ${({ theme }) => theme.gridUnit * 8}px;
&.open {
width: ${barWidth}; // arbitrary...
}
`;
const Bar = styled.div`
position: absolute;
top: 0;
left: 0;
flex-direction: column;
flex-grow: 1;
width: ${barWidth}; // arbitrary...
background: ${({ theme }) => theme.colors.grayscale.light5};
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
min-height: 100%;
display: none;
/* &.animated {
display: flex;
transform: translateX(-100%);
transition: transform ${({ theme }) => theme.transitionTiming}s;
transition-delay: 0s;
} */
&.open {
display: flex;
/* &.animated {
transform: translateX(0);
transition-delay: ${({ theme }) => theme.transitionTiming * 2}s;
} */
}
`;
const CollapsedBar = styled.div`
position: absolute;
top: 0;
left: 0;
height: 100%;
width: ${({ theme }) => theme.gridUnit * 8}px;
padding-top: ${({ theme }) => theme.gridUnit * 2}px;
display: none;
text-align: center;
/* &.animated {
display: block;
transform: translateX(-100%);
transition: transform ${({ theme }) => theme.transitionTiming}s;
transition-delay: 0s;
} */
&.open {
display: flex;
flex-direction: column;
align-items: center;
padding: ${({ theme }) => theme.gridUnit * 2}px;
/* &.animated {
transform: translateX(0);
transition-delay: ${({ theme }) => theme.transitionTiming * 3}s;
} */
}
svg {
width: ${({ theme }) => theme.gridUnit * 4}px;
height: ${({ theme }) => theme.gridUnit * 4}px;
cursor: pointer;
}
`;
const StyledCollapseIcon = styled(Icon)`
color: ${({ theme }) => theme.colors.primary.base};
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
`;
const TitleArea = styled.h4`
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0;
padding: ${({ theme }) => theme.gridUnit * 4}px;
& > span {
flex-grow: 1;
}
& :not(:first-child) {
margin-left: ${({ theme }) => theme.gridUnit}px;
&:hover {
cursor: pointer;
}
}
`;
const ActionButtons = styled.div`
display: flex;
flex-direction: row;
justify-content: space-around;
padding: ${({ theme }) => theme.gridUnit * 4}px;
padding-top: 0;
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
.btn {
flex: 1 1 50%;
}
`;
const FilterControls = styled.div`
padding: ${({ theme }) => theme.gridUnit * 4}px;
`;
interface FiltersBarProps {
filtersOpen: boolean;
toggleFiltersBar: any;
directPathToChild?: string[];
}
const FilterBar: React.FC<FiltersBarProps> = ({
filtersOpen,
toggleFiltersBar,
directPathToChild,
}) => {
const [filterData, setFilterData] = useState<{
[id: string]: {
extraFormData: ExtraFormData;
currentState: CurrentFilterState;
};
}>({});
const setExtraFormData = useSetExtraFormData();
const filterConfigs = useFilterConfiguration();
const filters = useFilters();
const canEdit = useSelector<any, boolean>(
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);
const [visiblePopoverId, setVisiblePopoverId] = useState<string | null>(null);
useEffect(() => {
if (filterConfigs.length === 0 && filtersOpen) {
toggleFiltersBar(false);
}
}, [filterConfigs]);
const cascadeChildren = useMemo(
() => mapParentFiltersToChildren(filterConfigs),
[filterConfigs],
);
const cascadeFilters = useMemo(() => {
const filtersWithValue = filterConfigs.map(filter => ({
...filter,
currentValue: filterData[filter.id]?.currentState?.value,
}));
return buildCascadeFiltersTree(filtersWithValue);
}, [filterConfigs]);
const handleFilterSelectionChange = (
filter: Filter,
extraFormData: ExtraFormData,
currentState: CurrentFilterState,
) => {
setFilterData(prevFilterData => ({
...prevFilterData,
[filter.id]: {
extraFormData,
currentState,
},
}));
const children = cascadeChildren[filter.id] || [];
// force instant updating for parent filters
if (filter.isInstant || children.length > 0) {
setExtraFormData(filter.id, extraFormData, currentState);
}
};
const handleApply = () => {
const filterIds = Object.keys(filterData);
filterIds.forEach(filterId => {
if (filterData[filterId]) {
setExtraFormData(
filterId,
filterData[filterId]?.extraFormData,
filterData[filterId]?.currentState,
);
}
});
};
const handleResetAll = () => {
filterConfigs.forEach(filter => {
setExtraFormData(filter.id, filterData[filter.id]?.extraFormData, {
...filterData[filter.id]?.currentState,
value: filters[filter.id]?.defaultValue,
});
});
};
return (
<BarWrapper data-test="filter-bar" className={cx({ open: filtersOpen })}>
<CollapsedBar
className={cx({ open: !filtersOpen })}
onClick={() => toggleFiltersBar(true)}
>
<StyledCollapseIcon name="collapse" />
<Icon name="filter" />
</CollapsedBar>
<Bar className={cx({ open: filtersOpen })}>
<TitleArea>
<span>
{t('Filters')} ({filterConfigs.length})
</span>
{canEdit && (
<FilterConfigurationLink
createNewOnOpen={filterConfigs.length === 0}
>
<Icon name="edit" data-test="create-filter" />
</FilterConfigurationLink>
)}
<Icon name="expand" onClick={() => toggleFiltersBar(false)} />
</TitleArea>
<ActionButtons>
<Button
buttonStyle="secondary"
buttonSize="small"
onClick={handleResetAll}
data-test="filter-reset-button"
>
{t('Reset all')}
</Button>
<Button
buttonStyle="primary"
htmlType="submit"
buttonSize="small"
onClick={handleApply}
data-test="filter-apply-button"
>
{t('Apply')}
</Button>
</ActionButtons>
<FilterControls>
{cascadeFilters.map(filter => (
<CascadePopover
data-test="cascade-filters-control"
key={filter.id}
visible={visiblePopoverId === filter.id}
onVisibleChange={visible =>
setVisiblePopoverId(visible ? filter.id : null)
}
filter={filter}
onFilterSelectionChange={handleFilterSelectionChange}
directPathToChild={directPathToChild}
/>
))}
</FilterControls>
</Bar>
</BarWrapper>
);
};
export default FilterBar;