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:
parent
896c832649
commit
25114a7b97
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const StyledDropdownButton = styled(
|
|||
height: unset;
|
||||
padding: 0;
|
||||
border: none;
|
||||
width: auto !important;
|
||||
|
||||
.anticon {
|
||||
line-height: 0;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue