feat: Horizontal filter bar states (#22064)

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
Geido 2022-11-18 14:15:28 +02:00 committed by GitHub
parent 896c832649
commit 25114a7b97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 914 additions and 458 deletions

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FilterBarOrientation } from 'src/dashboard/types';
export default {
id: 1234,
slug: 'dashboardSlug',
@ -36,4 +38,5 @@ export default {
flash_messages: [],
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
},
filterBarOrientation: FilterBarOrientation.VERTICAL,
};

View File

@ -71,60 +71,64 @@ export const mockStoreWithChartsInTabsAndRoot =
export const sliceIdWithAppliedFilter = sliceId + 1;
export const sliceIdWithRejectedFilter = sliceId + 2;
export const stateWithFilters = {
...mockState,
dashboardFilters,
dataMask: dataMaskWith2Filters,
charts: {
...mockState.charts,
[sliceIdWithAppliedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [{ column: 'region' }],
rejected_filters: [],
},
},
[sliceIdWithRejectedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }],
},
},
},
};
// has one chart with a filter that has been applied,
// one chart with a filter that has been rejected,
// and one chart with no filters set.
export const getMockStoreWithFilters = () =>
createStore(rootReducer, {
...mockState,
dashboardFilters,
dataMask: dataMaskWith2Filters,
charts: {
...mockState.charts,
[sliceIdWithAppliedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [{ column: 'region' }],
rejected_filters: [],
},
},
[sliceIdWithRejectedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }],
},
createStore(rootReducer, stateWithFilters);
export const stateWithNativeFilters = {
...mockState,
nativeFilters,
dataMask: dataMaskWith2Filters,
charts: {
...mockState.charts,
[sliceIdWithAppliedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [{ column: 'region' }],
rejected_filters: [],
},
},
});
[sliceIdWithRejectedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }],
},
},
},
};
export const getMockStoreWithNativeFilters = () =>
createStore(rootReducer, {
...mockState,
nativeFilters,
dataMask: dataMaskWith2Filters,
charts: {
...mockState.charts,
[sliceIdWithAppliedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [{ column: 'region' }],
rejected_filters: [],
},
},
[sliceIdWithRejectedFilter]: {
...mockState.charts[sliceId],
queryResponse: {
status: 'success',
applied_filters: [],
rejected_filters: [{ column: 'region', reason: 'not_in_datasource' }],
},
},
},
});
createStore(rootReducer, stateWithNativeFilters);
export const stateWithoutNativeFilters = {
...mockState,

View File

@ -43,6 +43,7 @@ const StyledDropdownButton = styled(
height: unset;
padding: 0;
border: none;
width: auto !important;
.anticon {
line-height: 0;

View File

@ -23,7 +23,7 @@ import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import {
DashboardInfo,
FilterBarLocation,
FilterBarOrientation,
RootState,
} from 'src/dashboard/types';
import { ChartConfiguration } from 'src/dashboard/reducers/types';
@ -120,16 +120,18 @@ export const setChartConfiguration =
}
};
export const SET_FILTER_BAR_LOCATION = 'SET_FILTER_BAR_LOCATION';
export interface SetFilterBarLocation {
type: typeof SET_FILTER_BAR_LOCATION;
filterBarLocation: FilterBarLocation;
export const SET_FILTER_BAR_ORIENTATION = 'SET_FILTER_BAR_ORIENTATION';
export interface SetFilterBarOrientation {
type: typeof SET_FILTER_BAR_ORIENTATION;
filterBarOrientation: FilterBarOrientation;
}
export function setFilterBarLocation(filterBarLocation: FilterBarLocation) {
return { type: SET_FILTER_BAR_LOCATION, filterBarLocation };
export function setFilterBarOrientation(
filterBarOrientation: FilterBarOrientation,
) {
return { type: SET_FILTER_BAR_ORIENTATION, filterBarOrientation };
}
export function saveFilterBarLocation(location: FilterBarLocation) {
export function saveFilterBarOrientation(orientation: FilterBarOrientation) {
return async (dispatch: Dispatch, getState: () => RootState) => {
const { id, metadata } = getState().dashboardInfo;
const updateDashboard = makeApi<
@ -143,15 +145,15 @@ export function saveFilterBarLocation(location: FilterBarLocation) {
const response = await updateDashboard({
json_metadata: JSON.stringify({
...metadata,
filter_bar_location: location,
filter_bar_orientation: orientation,
}),
});
const updatedDashboard = response.result;
const lastModifiedTime = response.last_modified_time;
if (updatedDashboard.json_metadata) {
const metadata = JSON.parse(updatedDashboard.json_metadata);
if (metadata.filter_bar_location) {
dispatch(setFilterBarLocation(metadata.filter_bar_location));
if (metadata.filter_bar_orientation) {
dispatch(setFilterBarOrientation(metadata.filter_bar_orientation));
}
}
if (lastModifiedTime) {

View File

@ -57,7 +57,7 @@ import getNativeFilterConfig from '../util/filterboxMigrationHelper';
import { updateColorSchema } from './dashboardInfo';
import { getChartIdsInFilterScope } from '../util/getChartIdsInFilterScope';
import updateComponentParentsList from '../util/updateComponentParentsList';
import { FilterBarLocation } from '../types';
import { FilterBarOrientation } from '../types';
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
@ -429,8 +429,8 @@ export const hydrateDashboard =
flash_messages: common?.flash_messages,
conf: common?.conf,
},
filterBarLocation:
metadata.filter_bar_location ?? FilterBarLocation.VERTICAL,
filterBarOrientation:
metadata.filter_bar_orientation ?? FilterBarOrientation.VERTICAL,
},
dataMask,
dashboardFilters,

View File

@ -40,7 +40,11 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { DashboardLayout, RootState } from 'src/dashboard/types';
import {
DashboardLayout,
FilterBarOrientation,
RootState,
} from 'src/dashboard/types';
import {
setDirectPathToChild,
setEditMode,
@ -241,6 +245,9 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
const fullSizeChartId = useSelector<RootState, number | null>(
state => state.dashboardState.fullSizeChartId,
);
const filterBarOrientation = useSelector<RootState, FilterBarOrientation>(
({ dashboardInfo }) => dashboardInfo.filterBarOrientation,
);
const handleChangeTab = useCallback(
({ pathToTabIndex }: { pathToTabIndex: string[] }) => {
@ -277,6 +284,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
uiConfig.hideTitle ||
standaloneMode === DashboardStandaloneMode.HIDE_NAV_AND_TITLE ||
isReport;
const [barTopOffset, setBarTopOffset] = useState(0);
useEffect(() => {
@ -312,6 +320,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
const filterSetEnabled = isFeatureEnabled(
FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET,
);
const showFilterBar = nativeFiltersEnabled && !editMode;
const offset =
FILTER_BAR_HEADER_HEIGHT +
@ -354,6 +363,13 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => (
<div>
{!hideDashboardHeader && <DashboardHeader />}
{showFilterBar &&
filterBarOrientation === FilterBarOrientation.HORIZONTAL && (
<FilterBar
directPathToChild={directPathToChild}
orientation={FilterBarOrientation.HORIZONTAL}
/>
)}
{dropIndicatorProps && <div {...dropIndicatorProps} />}
{!isReport && topLevelTabs && !uiConfig.hideNav && (
<WithPopoverMenu
@ -382,6 +398,9 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
</div>
),
[
directPathToChild,
nativeFiltersEnabled,
filterBarOrientation,
editMode,
handleChangeTab,
handleDeleteTopLevelTabs,
@ -394,7 +413,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
return (
<StyledDiv>
{nativeFiltersEnabled && !editMode && (
{showFilterBar && filterBarOrientation === FilterBarOrientation.VERTICAL && (
<>
<ResizableSidebar
id={`dashboard:${dashboardId}`}
@ -415,12 +434,15 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
<StickyPanel ref={containerRef} width={filterBarWidth}>
<ErrorBoundary>
<FilterBar
filtersOpen={dashboardFiltersOpen}
toggleFiltersBar={toggleDashboardFiltersOpen}
directPathToChild={directPathToChild}
width={filterBarWidth}
height={filterBarHeight}
offset={filterBarOffset}
orientation={FilterBarOrientation.VERTICAL}
verticalConfig={{
filtersOpen: dashboardFiltersOpen,
toggleFiltersBar: toggleDashboardFiltersOpen,
width: filterBarWidth,
height: filterBarHeight,
offset: filterBarOffset,
}}
/>
</ErrorBoundary>
</StickyPanel>

View File

@ -20,7 +20,7 @@ import React from 'react';
import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
import userEvent from '@testing-library/user-event';
import { render, screen } from 'spec/helpers/testing-library';
import { ActionButtons } from './index';
import ActionButtons from './index';
const createProps = () => ({
onApply: jest.fn(),

View File

@ -21,14 +21,15 @@ import {
css,
DataMaskState,
DataMaskStateWithId,
styled,
t,
isDefined,
SupersetTheme,
} from '@superset-ui/core';
import Button from 'src/components/Button';
import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
import { rgba } from 'emotion-rgba';
import { getFilterBarTestId } from '../index';
import { FilterBarOrientation } from 'src/dashboard/types';
import { getFilterBarTestId } from '../utils';
interface ActionButtonsProps {
width?: number;
@ -37,61 +38,77 @@ interface ActionButtonsProps {
dataMaskSelected: DataMaskState;
dataMaskApplied: DataMaskStateWithId;
isApplyDisabled: boolean;
filterBarOrientation?: FilterBarOrientation;
}
const ActionButtonsContainer = styled.div<{ width: number }>`
${({ theme, width }) => css`
display: flex;
flex-direction: column;
align-items: center;
const containerStyle = (theme: SupersetTheme) => css`
display: flex;
position: fixed;
z-index: 100;
// filter bar width minus 1px for border
width: ${width - 1}px;
bottom: 0;
padding: ${theme.gridUnit * 4}px;
padding-top: ${theme.gridUnit * 6}px;
background: linear-gradient(
${rgba(theme.colors.grayscale.light5, 0)},
${theme.colors.grayscale.light5} ${theme.opacity.mediumLight}
);
pointer-events: none;
& > button {
pointer-events: auto;
&& > .filter-clear-all-button {
color: ${theme.colors.grayscale.base};
margin-left: 0;
&:hover {
color: ${theme.colors.primary.dark1};
}
& > .filter-apply-button {
margin-bottom: ${theme.gridUnit * 3}px;
&[disabled],
&[disabled]:hover {
color: ${theme.colors.grayscale.light1};
}
&& > .filter-clear-all-button {
color: ${theme.colors.grayscale.base};
margin-left: 0;
&:hover {
color: ${theme.colors.primary.dark1};
}
&[disabled],
&[disabled]:hover {
color: ${theme.colors.grayscale.light1};
}
}
`};
}
`;
export const ActionButtons = ({
const verticalStyle = (theme: SupersetTheme, width: number) => css`
flex-direction: column;
align-items: center;
pointer-events: none;
position: fixed;
z-index: 100;
// filter bar width minus 1px for border
width: ${width - 1}px;
bottom: 0;
padding: ${theme.gridUnit * 4}px;
padding-top: ${theme.gridUnit * 6}px;
background: linear-gradient(
${rgba(theme.colors.grayscale.light5, 0)},
${theme.colors.grayscale.light5} ${theme.opacity.mediumLight}
);
& > button {
pointer-events: auto;
}
& > .filter-apply-button {
margin-bottom: ${theme.gridUnit * 3}px;
}
`;
const horizontalStyle = (theme: SupersetTheme) => css`
margin: 0 ${theme.gridUnit * 2}px;
&& > .filter-clear-all-button {
text-transform: capitalize;
font-weight: ${theme.typography.weights.normal};
}
& > .filter-apply-button {
&[disabled],
&[disabled]:hover {
color: ${theme.colors.grayscale.light1};
background: ${theme.colors.grayscale.light3};
}
}
`;
const ActionButtons = ({
width = OPEN_FILTER_BAR_WIDTH,
onApply,
onClearAll,
dataMaskApplied,
dataMaskSelected,
isApplyDisabled,
filterBarOrientation = FilterBarOrientation.VERTICAL,
}: ActionButtonsProps) => {
const isClearAllEnabled = useMemo(
() =>
@ -103,9 +120,16 @@ export const ActionButtons = ({
),
[dataMaskApplied, dataMaskSelected],
);
const isVertical = filterBarOrientation === FilterBarOrientation.VERTICAL;
return (
<ActionButtonsContainer data-test="filterbar-action-buttons" width={width}>
<div
css={(theme: SupersetTheme) => [
containerStyle(theme),
isVertical ? verticalStyle(theme, width) : horizontalStyle(theme),
]}
data-test="filterbar-action-buttons"
>
<Button
disabled={isApplyDisabled}
buttonStyle="primary"
@ -114,7 +138,7 @@ export const ActionButtons = ({
onClick={onApply}
{...getFilterBarTestId('apply-button')}
>
{t('Apply filters')}
{isVertical ? t('Apply filters') : t('Apply')}
</Button>
<Button
disabled={!isClearAllEnabled}
@ -126,6 +150,8 @@ export const ActionButtons = ({
>
{t('Clear all')}
</Button>
</ActionButtonsContainer>
</div>
);
};
export default ActionButtons;

View File

@ -29,7 +29,9 @@ import { TimeFilterPlugin, SelectFilterPlugin } from 'src/filters/components';
import { DATE_FILTER_TEST_KEY } from 'src/explore/components/controls/DateFilterControl';
import fetchMock from 'fetch-mock';
import { waitFor } from '@testing-library/react';
import FilterBar, { FILTER_BAR_TEST_ID } from '.';
import { FilterBarOrientation } from 'src/dashboard/types';
import { FILTER_BAR_TEST_ID } from './utils';
import FilterBar from '.';
import { FILTERS_CONFIG_MODAL_TEST_ID } from '../FiltersConfigModal/FiltersConfigModal';
jest.useFakeTimers();
@ -216,12 +218,23 @@ describe('FilterBar', () => {
});
const renderWrapper = (props = closedBarProps, state?: object) =>
render(<FilterBar {...props} width={280} height={400} offset={0} />, {
initialState: state,
useDnd: true,
useRedux: true,
useRouter: true,
});
render(
<FilterBar
orientation={FilterBarOrientation.VERTICAL}
verticalConfig={{
width: 280,
height: 400,
offset: 0,
...props,
}}
/>,
{
initialState: state,
useDnd: true,
useRedux: true,
useRouter: true,
},
);
it('should render', () => {
const { container } = renderWrapper();

View File

@ -22,9 +22,9 @@ import fetchMock from 'fetch-mock';
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen, within } from 'spec/helpers/testing-library';
import { DashboardInfo, FilterBarLocation } from 'src/dashboard/types';
import { DashboardInfo, FilterBarOrientation } from 'src/dashboard/types';
import * as mockedMessageActions from 'src/components/MessageToasts/actions';
import { FilterBarLocationSelect } from './index';
import FilterBarOrientationSelect from '.';
const initialState: { dashboardInfo: DashboardInfo } = {
dashboardInfo: {
@ -42,7 +42,7 @@ const initialState: { dashboardInfo: DashboardInfo } = {
},
json_metadata: '',
dash_edit_perm: true,
filterBarLocation: FilterBarLocation.VERTICAL,
filterBarOrientation: FilterBarOrientation.VERTICAL,
common: {
conf: {},
flash_messages: [],
@ -51,7 +51,7 @@ const initialState: { dashboardInfo: DashboardInfo } = {
};
const setup = (dashboardInfoOverride: Partial<DashboardInfo> = {}) =>
render(<FilterBarLocationSelect />, {
render(<FilterBarOrientationSelect />, {
useRedux: true,
initialState: {
...initialState,
@ -78,7 +78,7 @@ test('Popover opens with "Vertical" selected', async () => {
});
test('Popover opens with "Horizontal" selected', async () => {
setup({ filterBarLocation: FilterBarLocation.HORIZONTAL });
setup({ filterBarOrientation: FilterBarOrientation.HORIZONTAL });
userEvent.click(screen.getByLabelText('gear'));
expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument();
expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument();
@ -93,7 +93,7 @@ test('On selection change, send request and update checked value', async () => {
result: {
json_metadata: JSON.stringify({
...initialState.dashboardInfo.metadata,
filter_bar_location: 'HORIZONTAL',
filter_bar_orientation: 'HORIZONTAL',
}),
},
});
@ -124,7 +124,7 @@ test('On selection change, send request and update checked value', async () => {
JSON.stringify({
json_metadata: JSON.stringify({
...initialState.dashboardInfo.metadata,
filter_bar_location: 'HORIZONTAL',
filter_bar_orientation: 'HORIZONTAL',
}),
}),
),

View File

@ -21,60 +21,62 @@ import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { t, useTheme } from '@superset-ui/core';
import { MenuProps } from 'src/components/Menu';
import { FilterBarLocation, RootState } from 'src/dashboard/types';
import { saveFilterBarLocation } from 'src/dashboard/actions/dashboardInfo';
import { FilterBarOrientation, RootState } from 'src/dashboard/types';
import { saveFilterBarOrientation } from 'src/dashboard/actions/dashboardInfo';
import Icons from 'src/components/Icons';
import DropdownSelectableIcon from 'src/components/DropdownSelectableIcon';
export const FilterBarLocationSelect = () => {
const FilterBarOrientationSelect = () => {
const dispatch = useDispatch();
const theme = useTheme();
const filterBarLocation = useSelector<RootState, FilterBarLocation>(
({ dashboardInfo }) => dashboardInfo.filterBarLocation,
const filterBarOrientation = useSelector<RootState, FilterBarOrientation>(
({ dashboardInfo }) => dashboardInfo.filterBarOrientation,
);
const [selectedFilterBarLocation, setSelectedFilterBarLocation] =
useState(filterBarLocation);
const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] =
useState(filterBarOrientation);
const toggleFilterBarLocation = useCallback(
const toggleFilterBarOrientation = useCallback(
async (
selection: Parameters<
Required<Pick<MenuProps, 'onSelect'>>['onSelect']
>[0],
) => {
const selectedKey = selection.key as FilterBarLocation;
if (selectedKey !== filterBarLocation) {
const selectedKey = selection.key as FilterBarOrientation;
if (selectedKey !== filterBarOrientation) {
// set displayed selection in local state for immediate visual response after clicking
setSelectedFilterBarLocation(selectedKey);
setSelectedFilterBarOrientation(selectedKey);
try {
// save selection in Redux and backend
await dispatch(
saveFilterBarLocation(selection.key as FilterBarLocation),
saveFilterBarOrientation(selection.key as FilterBarOrientation),
);
} catch {
// revert local state in case of error when saving
setSelectedFilterBarLocation(filterBarLocation);
setSelectedFilterBarOrientation(filterBarOrientation);
}
}
},
[dispatch, filterBarLocation],
[dispatch, filterBarOrientation],
);
return (
<DropdownSelectableIcon
onSelect={toggleFilterBarLocation}
info={t('Placement of filter bar')}
onSelect={toggleFilterBarOrientation}
info={t('Orientation of filter bar')}
icon={<Icons.Gear name="gear" iconColor={theme.colors.grayscale.base} />}
menuItems={[
{
key: FilterBarLocation.VERTICAL,
key: FilterBarOrientation.VERTICAL,
label: t('Vertical (Left)'),
},
{
key: FilterBarLocation.HORIZONTAL,
key: FilterBarOrientation.HORIZONTAL,
label: t('Horizontal (Top)'),
},
]}
selectedKeys={[selectedFilterBarLocation]}
selectedKeys={[selectedFilterBarOrientation]}
/>
);
};
export default FilterBarOrientationSelect;

View File

@ -22,7 +22,7 @@ import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
import Button from 'src/components/Button';
import { FilterConfiguration, styled } from '@superset-ui/core';
import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
import { getFilterBarTestId } from '..';
import { getFilterBarTestId } from '../utils';
export interface FCBProps {
createNewOnOpen?: boolean;

View File

@ -24,7 +24,7 @@ import { checkIsMissingRequiredValue } from '../utils';
import FilterValue from './FilterValue';
import { FilterProps } from './types';
import { FilterCard } from '../../FilterCard';
import { FilterBarScrollContext } from '../index';
import { FilterBarScrollContext } from '../Vertical';
const StyledIcon = styled.div`
position: absolute;

View File

@ -27,7 +27,7 @@ import { ActionButtons } from './Footer';
import { useNativeFiltersDataMask, useFilters, useFilterSets } from '../state';
import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils';
import { useFilterSetNameDuplicated } from './state';
import { getFilterBarTestId } from '../index';
import { getFilterBarTestId } from '../utils';
const Wrapper = styled.div`
display: grid;

View File

@ -31,7 +31,7 @@ import { CheckOutlined, EllipsisOutlined } from '@ant-design/icons';
import Button from 'src/components/Button';
import { Tooltip } from 'src/components/Tooltip';
import FiltersHeader from './FiltersHeader';
import { getFilterBarTestId } from '..';
import { getFilterBarTestId } from '../utils';
const HeaderButton = styled(Button)`
padding: 0;

View File

@ -21,7 +21,7 @@ import { render, screen } from 'spec/helpers/testing-library';
import { mockStore } from 'spec/fixtures/mockStore';
import { Provider } from 'react-redux';
import FilterSets, { FilterSetsProps } from '.';
import { TabIds } from '../utils';
import { TabIds } from '../types';
const createProps = () => ({
disabled: false,

View File

@ -30,7 +30,7 @@ import Icons from 'src/components/Icons';
import { areObjectsEqual } from 'src/reduxUtils';
import { getFilterValueForDisplay } from './utils';
import { useFilters } from '../state';
import { getFilterBarTestId } from '../index';
import { getFilterBarTestId } from '../utils';
const FilterHeader = styled.div`
display: flex;

View File

@ -22,7 +22,7 @@ import Button from 'src/components/Button';
import { Tooltip } from 'src/components/Tooltip';
import { APPLY_FILTERS_HINT } from './utils';
import { useFilterSetNameDuplicated } from './state';
import { getFilterBarTestId } from '..';
import { getFilterBarTestId } from '../utils';
export type FooterProps = {
filterSetName: string;

View File

@ -40,8 +40,8 @@ import { findExistingFilterSet } from './utils';
import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state';
import Footer from './Footer';
import FilterSetUnit from './FilterSetUnit';
import { getFilterBarTestId } from '..';
import { TabIds } from '../utils';
import { getFilterBarTestId } from '../utils';
import { TabIds } from '../types';
const FilterSetsWrapper = styled.div`
display: grid;

View File

@ -32,8 +32,8 @@ import { useSelector } from 'react-redux';
import FilterConfigurationLink from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink';
import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state';
import { RootState } from 'src/dashboard/types';
import { getFilterBarTestId } from '..';
import { FilterBarLocationSelect } from '../FilterBarLocationSelect';
import { getFilterBarTestId } from '../utils';
import FilterBarOrientationSelect from '../FilterBarOrientationSelect';
const TitleArea = styled.h4`
display: flex;
@ -56,8 +56,13 @@ const HeaderButton = styled(Button)`
`;
const Wrapper = styled.div`
padding: ${({ theme }) => theme.gridUnit}px
${({ theme }) => theme.gridUnit * 2}px;
${({ theme }) => `
padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px;
.ant-dropdown-trigger span {
padding-right: ${theme.gridUnit * 2}px;
}
`}
`;
type HeaderProps = {
@ -100,7 +105,7 @@ const Header: FC<HeaderProps> = ({ toggleFiltersBar }) => {
<Wrapper>
<TitleArea>
<span>{t('Filters')}</span>
{canSetHorizontalFilterBar && <FilterBarLocationSelect />}
{canSetHorizontalFilterBar && <FilterBarOrientationSelect />}
<HeaderButton
{...getFilterBarTestId('collapse-button')}
buttonStyle="link"

View File

@ -0,0 +1,138 @@
/**
* 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 { styled, t } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import Loading from 'src/components/Loading';
import FilterControls from './FilterControls/FilterControls';
import { getFilterBarTestId } from './utils';
import { HorizontalBarProps } from './types';
import FilterBarOrientationSelect from './FilterBarOrientationSelect';
import FilterConfigurationLink from './FilterConfigurationLink';
const HorizontalBar = styled.div`
${({ theme }) => `
padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px;
background: ${theme.colors.grayscale.light5};
box-shadow: inset 0px -2px 2px -1px ${theme.colors.grayscale.light2};
`}
`;
const HorizontalBarContent = styled.div`
${({ theme }) => `
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
padding: 0 ${theme.gridUnit * 2}px;
line-height: 0;
.loading {
margin: ${theme.gridUnit * 2}px auto ${theme.gridUnit * 2}px;
padding: 0;
}
`}
`;
const FilterBarEmptyStateContainer = styled.div`
${({ theme }) => `
margin: 0 ${theme.gridUnit * 2}px 0 ${theme.gridUnit * 4}px;
font-weight: ${theme.typography.weights.bold};
color: ${theme.colors.grayscale.base};
font-size: ${theme.typography.sizes.s}px;
`}
`;
const FiltersLinkContainer = styled.div<{ hasFilters: boolean }>`
${({ theme, hasFilters }) => `
padding: 0 ${theme.gridUnit * 2}px;
border-right: ${
hasFilters ? `1px solid ${theme.colors.grayscale.light2}` : 0
};
button {
display: flex;
align-items: center;
text-transform: capitalize;
font-weight: ${theme.typography.weights.normal};
color: ${theme.colors.primary.base};
> .anticon {
height: 24px;
padding-right: ${theme.gridUnit * 2}px;
}
> .anticon + span, > .anticon {
margin-right: 0;
margin-left: 0;
}
}
`}
`;
const HorizontalFilterBar: React.FC<HorizontalBarProps> = ({
actions,
canEdit,
dashboardId,
dataMaskSelected,
filterValues,
isInitialized,
directPathToChild,
onSelectionChange,
}) => {
const hasFilters = filterValues.length > 0;
return (
<HorizontalBar {...getFilterBarTestId()}>
<HorizontalBarContent>
{!isInitialized ? (
<Loading position="inline-centered" />
) : (
<>
{canEdit && <FilterBarOrientationSelect />}
{!hasFilters && (
<FilterBarEmptyStateContainer>
{t('No filters are currently added to this dashboard.')}
</FilterBarEmptyStateContainer>
)}
{canEdit && (
<FiltersLinkContainer hasFilters={hasFilters}>
<FilterConfigurationLink
dashboardId={dashboardId}
createNewOnOpen={filterValues.length === 0}
>
<Icons.PlusSmall /> {t('Add/Edit Filters')}
</FilterConfigurationLink>
</FiltersLinkContainer>
)}
{hasFilters && (
<FilterControls
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
onFilterSelectionChange={onSelectionChange}
/>
)}
{actions}
</>
)}
</HorizontalBarContent>
</HorizontalBar>
);
};
export default React.memo(HorizontalFilterBar);

View File

@ -0,0 +1,105 @@
/**
* 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 { NativeFilterType } from '@superset-ui/core';
import React from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import HorizontalBar from './Horizontal';
const defaultProps = {
actions: null,
canEdit: true,
dashboardId: 1,
dataMaskSelected: {},
filterValues: [],
isInitialized: true,
onSelectionChange: jest.fn(),
};
const renderWrapper = (overrideProps?: Record<string, any>) =>
waitFor(() =>
render(<HorizontalBar {...defaultProps} {...overrideProps} />, {
useRedux: true,
}),
);
test('should render', async () => {
const { container } = await renderWrapper();
expect(container).toBeInTheDocument();
});
test('should not render the empty message', async () => {
await renderWrapper({
filterValues: [
{
id: 'test',
type: NativeFilterType.NATIVE_FILTER,
},
],
});
expect(
screen.queryByText('No filters are currently added to this dashboard.'),
).not.toBeInTheDocument();
});
test('should render the empty message', async () => {
await renderWrapper();
expect(
screen.getByText('No filters are currently added to this dashboard.'),
).toBeInTheDocument();
});
test('should render the gear icon', async () => {
await renderWrapper();
expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument();
});
test('should not render the gear icon', async () => {
await renderWrapper({
canEdit: false,
});
expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument();
});
test('should not render the loading icon', async () => {
await renderWrapper();
expect(
screen.queryByRole('status', { name: 'Loading' }),
).not.toBeInTheDocument();
});
test('should render the loading icon', async () => {
await renderWrapper({
isInitialized: false,
});
expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument();
});
test('should render Add/Edit Filters', async () => {
await renderWrapper();
expect(screen.getByText('Add/Edit Filters')).toBeInTheDocument();
});
test('should not render Add/Edit Filters', async () => {
await renderWrapper({
canEdit: false,
});
expect(screen.queryByText('Add/Edit Filters')).not.toBeInTheDocument();
});

View File

@ -0,0 +1,306 @@
/**
* 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.
*/
/* eslint-disable no-param-reassign */
import throttle from 'lodash/throttle';
import React, {
useEffect,
useState,
useCallback,
useMemo,
useRef,
createContext,
} from 'react';
import cx from 'classnames';
import { HandlerFunction, styled, t, isNativeFilter } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { AntdTabs } from 'src/components';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import Loading from 'src/components/Loading';
import { EmptyStateSmall } from 'src/components/EmptyState';
import { getFilterBarTestId } from './utils';
import { TabIds, VerticalBarProps } from './types';
import FilterSets from './FilterSets';
import { useFilterSets } from './state';
import EditSection from './FilterSets/EditSection';
import Header from './Header';
import FilterControls from './FilterControls/FilterControls';
const BarWrapper = styled.div<{ width: number }>`
width: ${({ theme }) => theme.gridUnit * 8}px;
& .ant-tabs-top > .ant-tabs-nav {
margin: 0;
}
&.open {
width: ${({ width }) => width}px; // arbitrary...
}
`;
const Bar = styled.div<{ width: number }>`
${({ theme, width }) => `
& .ant-typography-edit-content {
left: 0;
margin-top: 0;
width: 100%;
}
position: absolute;
top: 0;
left: 0;
flex-direction: column;
flex-grow: 1;
width: ${width}px;
background: ${theme.colors.grayscale.light5};
border-right: 1px solid ${theme.colors.grayscale.light2};
border-bottom: 1px solid ${theme.colors.grayscale.light2};
min-height: 100%;
display: none;
&.open {
display: flex;
}
`}
`;
const CollapsedBar = styled.div<{ offset: number }>`
${({ theme, offset }) => `
position: absolute;
top: ${offset}px;
left: 0;
height: 100%;
width: ${theme.gridUnit * 8}px;
padding-top: ${theme.gridUnit * 2}px;
display: none;
text-align: center;
&.open {
display: flex;
flex-direction: column;
align-items: center;
padding: ${theme.gridUnit * 2}px;
}
svg {
cursor: pointer;
}
`}
`;
const StyledCollapseIcon = styled(Icons.Collapse)`
${({ theme }) => `
color: ${theme.colors.primary.base};
margin-bottom: ${theme.gridUnit * 3}px;
`}
`;
const StyledFilterIcon = styled(Icons.Filter)`
color: ${({ theme }) => theme.colors.grayscale.base};
`;
const StyledTabs = styled(AntdTabs)`
& .ant-tabs-nav-list {
width: 100%;
}
& .ant-tabs-tab {
display: flex;
justify-content: center;
margin: 0;
flex: 1;
}
& > .ant-tabs-nav .ant-tabs-nav-operations {
display: none;
}
`;
const FilterBarEmptyStateContainer = styled.div`
margin-top: ${({ theme }) => theme.gridUnit * 8}px;
`;
export const FilterBarScrollContext = createContext(false);
const VerticalFilterBar: React.FC<VerticalBarProps> = ({
actions,
canEdit,
dataMaskSelected,
directPathToChild,
filtersOpen,
filterValues,
height,
isDisabled,
isInitialized,
offset,
onSelectionChange,
toggleFiltersBar,
width,
}) => {
const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null);
const filterSets = useFilterSets();
const filterSetFilterValues = Object.values(filterSets);
const [tab, setTab] = useState(TabIds.AllFilters);
const nativeFilterValues = filterValues.filter(isNativeFilter);
const [isScrolling, setIsScrolling] = useState(false);
const timeout = useRef<any>();
const openFiltersBar = useCallback(
() => toggleFiltersBar(true),
[toggleFiltersBar],
);
const onScroll = useMemo(
() =>
throttle(() => {
clearTimeout(timeout.current);
setIsScrolling(true);
timeout.current = setTimeout(() => {
setIsScrolling(false);
}, 300);
}, 200),
[],
);
useEffect(() => {
document.onscroll = onScroll;
return () => {
document.onscroll = null;
};
}, [onScroll]);
const tabPaneStyle = useMemo(
() => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }),
[height],
);
const numberOfFilters = nativeFilterValues.length;
return (
<FilterBarScrollContext.Provider value={isScrolling}>
<BarWrapper
{...getFilterBarTestId()}
className={cx({ open: filtersOpen })}
width={width}
>
<CollapsedBar
{...getFilterBarTestId('collapsable')}
className={cx({ open: !filtersOpen })}
onClick={openFiltersBar}
offset={offset}
>
<StyledCollapseIcon
{...getFilterBarTestId('expand-button')}
iconSize="l"
/>
<StyledFilterIcon
{...getFilterBarTestId('filter-icon')}
iconSize="l"
/>
</CollapsedBar>
<Bar className={cx({ open: filtersOpen })} width={width}>
<Header toggleFiltersBar={toggleFiltersBar} />
{!isInitialized ? (
<div css={{ height }}>
<Loading />
</div>
) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? (
<StyledTabs
centered
onChange={setTab as HandlerFunction}
defaultActiveKey={TabIds.AllFilters}
activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
>
<AntdTabs.TabPane
tab={t('All filters (%(filterCount)d)', {
filterCount: numberOfFilters,
})}
key={TabIds.AllFilters}
css={tabPaneStyle}
>
{editFilterSetId && (
<EditSection
dataMaskSelected={dataMaskSelected}
disabled={!isDisabled}
onCancel={() => setEditFilterSetId(null)}
filterSetId={editFilterSetId}
/>
)}
{filterValues.length === 0 ? (
<FilterBarEmptyStateContainer>
<EmptyStateSmall
title={t('No filters are currently added')}
image="filter.svg"
description={
canEdit &&
t(
'Click the button above to add a filter to the dashboard',
)
}
/>
</FilterBarEmptyStateContainer>
) : (
<FilterControls
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
onFilterSelectionChange={onSelectionChange}
/>
)}
</AntdTabs.TabPane>
<AntdTabs.TabPane
disabled={!!editFilterSetId}
tab={t('Filter sets (%(filterSetCount)d)', {
filterSetCount: filterSetFilterValues.length,
})}
key={TabIds.FilterSets}
css={tabPaneStyle}
>
<FilterSets
onEditFilterSet={setEditFilterSetId}
disabled={!isDisabled}
dataMaskSelected={dataMaskSelected}
tab={tab}
onFilterSelectionChange={onSelectionChange}
/>
</AntdTabs.TabPane>
</StyledTabs>
) : (
<div css={tabPaneStyle} onScroll={onScroll}>
{filterValues.length === 0 ? (
<FilterBarEmptyStateContainer>
<EmptyStateSmall
title={t('No filters are currently added')}
image="filter.svg"
description={
canEdit &&
t(
'Click the button above to add a filter to the dashboard',
)
}
/>
</FilterBarEmptyStateContainer>
) : (
<FilterControls
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
onFilterSelectionChange={onSelectionChange}
/>
)}
</div>
)}
{actions}
</Bar>
</BarWrapper>
</FilterBarScrollContext.Provider>
);
};
export default React.memo(VerticalFilterBar);

View File

@ -18,152 +18,38 @@
*/
/* eslint-disable no-param-reassign */
import throttle from 'lodash/throttle';
import React, {
useEffect,
useState,
useCallback,
useMemo,
useRef,
createContext,
} from 'react';
import React, { useEffect, useState, useCallback, createContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import cx from 'classnames';
import {
DataMaskStateWithId,
DataMaskWithId,
Filter,
DataMask,
HandlerFunction,
styled,
t,
SLOW_DEBOUNCE,
isNativeFilter,
} from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { AntdTabs } from 'src/components';
import { useHistory } from 'react-router-dom';
import { usePrevious } from 'src/hooks/usePrevious';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { updateDataMask, clearDataMask } from 'src/dataMask/actions';
import { useImmer } from 'use-immer';
import { isEmpty, isEqual, debounce } from 'lodash';
import { testWithId } from 'src/utils/testUtils';
import Loading from 'src/components/Loading';
import { getInitialDataMask } from 'src/dataMask/reducer';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { EmptyStateSmall } from 'src/components/EmptyState';
import { useTabId } from 'src/hooks/useTabId';
import { RootState } from 'src/dashboard/types';
import { checkIsApplyDisabled, TabIds } from './utils';
import FilterSets from './FilterSets';
import { FilterBarOrientation, RootState } from 'src/dashboard/types';
import { checkIsApplyDisabled } from './utils';
import { FiltersBarProps } from './types';
import {
useNativeFiltersDataMask,
useFilters,
useFilterSets,
useFilterUpdates,
useInitialization,
} from './state';
import { createFilterKey, updateFilterKey } from './keyValue';
import EditSection from './FilterSets/EditSection';
import Header from './Header';
import FilterControls from './FilterControls/FilterControls';
import { ActionButtons } from './ActionButtons';
export const FILTER_BAR_TEST_ID = 'filter-bar';
export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID);
const BarWrapper = styled.div<{ width: number }>`
width: ${({ theme }) => theme.gridUnit * 8}px;
& .ant-tabs-top > .ant-tabs-nav {
margin: 0;
}
&.open {
width: ${({ width }) => width}px; // arbitrary...
}
`;
const Bar = styled.div<{ width: number }>`
& .ant-typography-edit-content {
left: 0;
margin-top: 0;
width: 100%;
}
position: absolute;
top: 0;
left: 0;
flex-direction: column;
flex-grow: 1;
width: ${({ width }) => width}px;
background: ${({ theme }) => theme.colors.grayscale.light5};
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
min-height: 100%;
display: none;
&.open {
display: flex;
}
`;
const CollapsedBar = styled.div<{ offset: number }>`
position: absolute;
top: ${({ offset }) => offset}px;
left: 0;
height: 100%;
width: ${({ theme }) => theme.gridUnit * 8}px;
padding-top: ${({ theme }) => theme.gridUnit * 2}px;
display: none;
text-align: center;
&.open {
display: flex;
flex-direction: column;
align-items: center;
padding: ${({ theme }) => theme.gridUnit * 2}px;
}
svg {
cursor: pointer;
}
`;
const StyledCollapseIcon = styled(Icons.Collapse)`
color: ${({ theme }) => theme.colors.primary.base};
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
`;
const StyledFilterIcon = styled(Icons.Filter)`
color: ${({ theme }) => theme.colors.grayscale.base};
`;
const StyledTabs = styled(AntdTabs)`
& .ant-tabs-nav-list {
width: 100%;
}
& .ant-tabs-tab {
display: flex;
justify-content: center;
margin: 0;
flex: 1;
}
& > .ant-tabs-nav .ant-tabs-nav-operations {
display: none;
}
`;
const FilterBarEmptyStateContainer = styled.div`
margin-top: ${({ theme }) => theme.gridUnit * 8}px;
`;
export interface FiltersBarProps {
filtersOpen: boolean;
toggleFiltersBar: any;
directPathToChild?: string[];
width: number;
height: number | string;
offset: number;
}
import ActionButtons from './ActionButtons';
import Horizontal from './Horizontal';
import Vertical from './Vertical';
const EXCLUDED_URL_PARAMS: string[] = [
URL_PARAMS.nativeFilters.name,
@ -225,29 +111,22 @@ const publishDataMask = debounce(
export const FilterBarScrollContext = createContext(false);
const FilterBar: React.FC<FiltersBarProps> = ({
filtersOpen,
toggleFiltersBar,
directPathToChild,
width,
height,
offset,
orientation = FilterBarOrientation.VERTICAL,
verticalConfig,
}) => {
const history = useHistory();
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null);
const [dataMaskSelected, setDataMaskSelected] =
useImmer<DataMaskStateWithId>(dataMaskApplied);
const dispatch = useDispatch();
const [updateKey, setUpdateKey] = useState(0);
const tabId = useTabId();
const filterSets = useFilterSets();
const filterSetFilterValues = Object.values(filterSets);
const [tab, setTab] = useState(TabIds.AllFilters);
const filters = useFilters();
const previousFilters = usePrevious(filters);
const filterValues = Object.values(filters);
const nativeFilterValues = filterValues.filter(isNativeFilter);
const dashboardId = useSelector<any, string>(
const dashboardId = useSelector<any, number>(
({ dashboardInfo }) => dashboardInfo?.id,
);
const previousDashboardId = usePrevious(dashboardId);
@ -255,9 +134,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);
const [isScrolling, setIsScrolling] = useState(false);
const timeout = useRef<any>();
const handleFilterSelectionChange = useCallback(
(
filter: Pick<Filter, 'id'> & Partial<Filter>,
@ -352,29 +228,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({
});
}, [dataMaskSelected, dispatch, setDataMaskSelected]);
const openFiltersBar = useCallback(
() => toggleFiltersBar(true),
[toggleFiltersBar],
);
const onScroll = useCallback(
throttle(() => {
clearTimeout(timeout.current);
setIsScrolling(true);
timeout.current = setTimeout(() => {
setIsScrolling(false);
}, 300);
}, 200),
[],
);
useEffect(() => {
document.onscroll = onScroll;
return () => {
document.onscroll = null;
};
}, [onScroll]);
useFilterUpdates(dataMaskSelected, setDataMaskSelected);
const isApplyDisabled = checkIsApplyDisabled(
dataMaskSelected,
@ -382,136 +235,46 @@ const FilterBar: React.FC<FiltersBarProps> = ({
nativeFilterValues,
);
const isInitialized = useInitialization();
const tabPaneStyle = useMemo(
() => ({ overflow: 'auto', height, overscrollBehavior: 'contain' }),
[height],
const actions = (
<ActionButtons
filterBarOrientation={orientation}
width={verticalConfig?.width}
onApply={handleApply}
onClearAll={handleClearAll}
dataMaskSelected={dataMaskSelected}
dataMaskApplied={dataMaskApplied}
isApplyDisabled={isApplyDisabled}
/>
);
const numberOfFilters = nativeFilterValues.length;
return (
<FilterBarScrollContext.Provider value={isScrolling}>
<BarWrapper
{...getFilterBarTestId()}
className={cx({ open: filtersOpen })}
width={width}
>
<CollapsedBar
{...getFilterBarTestId('collapsable')}
className={cx({ open: !filtersOpen })}
onClick={openFiltersBar}
offset={offset}
>
<StyledCollapseIcon
{...getFilterBarTestId('expand-button')}
iconSize="l"
/>
<StyledFilterIcon
{...getFilterBarTestId('filter-icon')}
iconSize="l"
/>
</CollapsedBar>
<Bar className={cx({ open: filtersOpen })} width={width}>
<Header toggleFiltersBar={toggleFiltersBar} />
{!isInitialized ? (
<div css={{ height }}>
<Loading />
</div>
) : isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? (
<StyledTabs
centered
onChange={setTab as HandlerFunction}
defaultActiveKey={TabIds.AllFilters}
activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
>
<AntdTabs.TabPane
tab={t('All filters (%(filterCount)d)', {
filterCount: numberOfFilters,
})}
key={TabIds.AllFilters}
css={tabPaneStyle}
>
{editFilterSetId && (
<EditSection
dataMaskSelected={dataMaskSelected}
disabled={!isApplyDisabled}
onCancel={() => setEditFilterSetId(null)}
filterSetId={editFilterSetId}
/>
)}
{filterValues.length === 0 ? (
<FilterBarEmptyStateContainer>
<EmptyStateSmall
title={t('No filters are currently added')}
image="filter.svg"
description={
canEdit &&
t(
'Click the button above to add a filter to the dashboard',
)
}
/>
</FilterBarEmptyStateContainer>
) : (
<FilterControls
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
onFilterSelectionChange={handleFilterSelectionChange}
/>
)}
</AntdTabs.TabPane>
<AntdTabs.TabPane
disabled={!!editFilterSetId}
tab={t('Filter sets (%(filterSetCount)d)', {
filterSetCount: filterSetFilterValues.length,
})}
key={TabIds.FilterSets}
css={tabPaneStyle}
>
<FilterSets
onEditFilterSet={setEditFilterSetId}
disabled={!isApplyDisabled}
dataMaskSelected={dataMaskSelected}
tab={tab}
onFilterSelectionChange={handleFilterSelectionChange}
/>
</AntdTabs.TabPane>
</StyledTabs>
) : (
<div css={tabPaneStyle} onScroll={onScroll}>
{filterValues.length === 0 ? (
<FilterBarEmptyStateContainer>
<EmptyStateSmall
title={t('No filters are currently added')}
image="filter.svg"
description={
canEdit &&
t(
'Click the button above to add a filter to the dashboard',
)
}
/>
</FilterBarEmptyStateContainer>
) : (
<FilterControls
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
onFilterSelectionChange={handleFilterSelectionChange}
/>
)}
</div>
)}
<ActionButtons
width={width}
onApply={handleApply}
onClearAll={handleClearAll}
dataMaskSelected={dataMaskSelected}
dataMaskApplied={dataMaskApplied}
isApplyDisabled={isApplyDisabled}
/>
</Bar>
</BarWrapper>
</FilterBarScrollContext.Provider>
);
return orientation === FilterBarOrientation.HORIZONTAL ? (
<Horizontal
actions={actions}
canEdit={canEdit}
dashboardId={dashboardId}
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
filterValues={filterValues}
isInitialized={isInitialized}
onSelectionChange={handleFilterSelectionChange}
/>
) : verticalConfig ? (
<Vertical
actions={actions}
canEdit={canEdit}
dataMaskSelected={dataMaskSelected}
directPathToChild={directPathToChild}
filtersOpen={verticalConfig.filtersOpen}
filterValues={filterValues}
isInitialized={isInitialized}
isDisabled={isApplyDisabled}
height={verticalConfig.height}
offset={verticalConfig.offset}
onSelectionChange={handleFilterSelectionChange}
toggleFiltersBar={verticalConfig.toggleFiltersBar}
width={verticalConfig.width}
/>
) : null;
};
export default React.memo(FilterBar);

View File

@ -0,0 +1,67 @@
/**
* 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 {
DataMask,
DataMaskStateWithId,
Divider,
Filter,
} from '@superset-ui/core';
import { FilterBarOrientation } from 'src/dashboard/types';
interface CommonFiltersBarProps {
actions: React.ReactNode;
canEdit: boolean;
dataMaskSelected: DataMaskStateWithId;
directPathToChild?: string[];
filterValues: (Filter | Divider)[];
isInitialized: boolean;
onSelectionChange: (
filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>,
) => void;
}
interface VerticalBarConfig {
filtersOpen: boolean;
height: number | string;
offset: number;
toggleFiltersBar: any;
width: number;
}
export interface FiltersBarProps
extends Pick<CommonFiltersBarProps, 'directPathToChild'> {
orientation: FilterBarOrientation;
verticalConfig?: VerticalBarConfig;
}
export type HorizontalBarProps = CommonFiltersBarProps & {
dashboardId: number;
};
export type VerticalBarProps = Omit<FiltersBarProps, 'orientation'> &
CommonFiltersBarProps &
VerticalBarConfig & {
isDisabled: boolean;
};
export enum TabIds {
AllFilters = 'allFilters',
FilterSets = 'filterSets',
}

View File

@ -19,11 +19,7 @@
import { areObjectsEqual } from 'src/reduxUtils';
import { DataMaskStateWithId, Filter, FilterState } from '@superset-ui/core';
export enum TabIds {
AllFilters = 'allFilters',
FilterSets = 'filterSets',
}
import { testWithId } from 'src/utils/testUtils';
export const getOnlyExtraFormData = (data: DataMaskStateWithId) =>
Object.values(data).reduce(
@ -65,3 +61,6 @@ export const checkIsApplyDisabled = (
)
);
};
export const FILTER_BAR_TEST_ID = 'filter-bar';
export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID);

View File

@ -19,7 +19,7 @@
import {
DASHBOARD_INFO_UPDATED,
SET_FILTER_BAR_LOCATION,
SET_FILTER_BAR_ORIENTATION,
} from '../actions/dashboardInfo';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
@ -38,10 +38,10 @@ export default function dashboardStateReducer(state = {}, action) {
...action.data.dashboardInfo,
// set async api call data
};
case SET_FILTER_BAR_LOCATION:
case SET_FILTER_BAR_ORIENTATION:
return {
...state,
filterBarLocation: action.filterBarLocation,
filterBarOrientation: action.filterBarOrientation,
};
default:
return state;

View File

@ -52,7 +52,7 @@ export type Chart = ChartState & {
};
};
export enum FilterBarLocation {
export enum FilterBarOrientation {
VERTICAL = 'VERTICAL',
HORIZONTAL = 'HORIZONTAL',
}
@ -108,7 +108,7 @@ export type DashboardInfo = {
label_colors: JsonObject;
shared_label_colors: JsonObject;
};
filterBarLocation: FilterBarLocation;
filterBarOrientation: FilterBarOrientation;
};
export type ChartsState = { [key: string]: Chart };

View File

@ -133,7 +133,7 @@ class DashboardJSONMetadataSchema(Schema):
# used for v0 import/export
import_time = fields.Integer()
remote_id = fields.Integer()
filter_bar_location = fields.Str(allow_none=True)
filter_bar_orientation = fields.Str(allow_none=True)
class UserSchema(Schema):