feat: support mulitple temporal filters in AdhocFilter and move the Time Section away (#21767)

This commit is contained in:
Yongjie Zhao
2022-11-02 08:21:17 +08:00
committed by GitHub
parent 25be9ab4bc
commit a9b229dd1d
59 changed files with 1276 additions and 237 deletions

View File

@@ -21,6 +21,7 @@ import { css, styled, t, useTheme, NO_TIME_RANGE } from '@superset-ui/core';
import Button from 'src/components/Button';
import ControlHeader from 'src/explore/components/ControlHeader';
import Label, { Type } from 'src/components/Label';
import Modal from 'src/components/Modal';
import { Divider } from 'src/components';
import Icons from 'src/components/Icons';
import Select from 'src/components/Select/Select';
@@ -32,10 +33,12 @@ import ControlPopover from '../ControlPopover/ControlPopover';
import { DateFilterControlProps, FrameType } from './types';
import {
DATE_FILTER_TEST_KEY,
fetchTimeRange,
FRAME_OPTIONS,
getDateFilterControlTestId,
guessFrame,
useDefaultTimeFilter,
} from './utils';
import {
CommonFrame,
@@ -44,7 +47,6 @@ import {
AdvancedFrame,
} from './components';
const StyledPopover = styled(ControlPopover)``;
const StyledRangeType = styled(Select)`
width: 272px;
`;
@@ -121,12 +123,15 @@ const IconWrapper = styled.span`
export default function DateFilterLabel(props: DateFilterControlProps) {
const {
value = NO_TIME_RANGE,
onChange,
type,
onOpenPopover = noOp,
onClosePopover = noOp,
overlayStyle = 'Popover',
} = props;
const defaultTimeFilter = useDefaultTimeFilter();
const value = props.value ?? defaultTimeFilter;
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
const [show, setShow] = useState<boolean>(false);
@@ -137,6 +142,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
const [evalResponse, setEvalResponse] = useState<string>(value);
const [tooltipTitle, setTooltipTitle] = useState<string>(value);
const theme = useTheme();
useEffect(() => {
if (value === NO_TIME_RANGE) {
@@ -180,6 +186,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
setValidTimeRange(true);
}
setLastFetchedTimeRange(value);
setEvalResponse(actualRange || value);
});
}, [value]);
@@ -225,7 +232,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
setShow(false);
}
const togglePopover = () => {
const toggleOverlay = () => {
if (show) {
onHide();
onClosePopover();
@@ -242,8 +249,6 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
setFrame(value);
}
const theme = useTheme();
const overlayContent = (
<ContentStyleWrapper>
<div className="control-label">{t('RANGE TYPE')}</div>
@@ -266,7 +271,9 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
{frame === 'Custom' && (
<CustomFrame value={timeRangeValue} onChange={setTimeRangeValue} />
)}
{frame === 'No filter' && <div data-test="no-filter" />}
{frame === 'No filter' && (
<div data-test={DATE_FILTER_TEST_KEY.noFilter} />
)}
<Divider />
<div>
<div className="section-title">{t('Actual time range')}</div>
@@ -285,7 +292,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
cta
key="cancel"
onClick={onHide}
data-test="cancel-button"
data-test={DATE_FILTER_TEST_KEY.cancelButton}
>
{t('CANCEL')}
</Button>
@@ -310,25 +317,56 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
</IconWrapper>
);
const popoverContent = (
<ControlPopover
placement="right"
trigger="click"
content={overlayContent}
title={title}
defaultVisible={show}
visible={show}
onVisibleChange={toggleOverlay}
overlayStyle={{ width: '600px' }}
>
<Tooltip placement="top" title={tooltipTitle}>
<Label
className="pointer"
data-test={DATE_FILTER_TEST_KEY.popoverOverlay}
>
{actualTimeRange}
</Label>
</Tooltip>
</ControlPopover>
);
const modalContent = (
<>
<Tooltip placement="top" title={tooltipTitle}>
<Label
className="pointer"
onClick={toggleOverlay}
data-test={DATE_FILTER_TEST_KEY.modalOverlay}
>
{actualTimeRange}
</Label>
</Tooltip>
<Modal
title={title}
show={show}
onHide={toggleOverlay}
width="600px"
hideFooter
zIndex={Number.MAX_SAFE_INTEGER}
>
{overlayContent}
</Modal>
</>
);
return (
<>
<ControlHeader {...props} />
<StyledPopover
placement="right"
trigger="click"
content={overlayContent}
title={title}
defaultVisible={show}
visible={show}
onVisibleChange={togglePopover}
overlayStyle={{ width: '600px' }}
>
<Tooltip placement="top" title={tooltipTitle}>
<Label className="pointer" data-test="time-range-trigger">
{actualTimeRange}
</Label>
</Tooltip>
</StyledPopover>
{overlayStyle === 'Modal' ? modalContent : popoverContent}
</>
);
}

View File

@@ -22,6 +22,7 @@ import { Radio } from 'src/components/Radio';
import {
COMMON_RANGE_OPTIONS,
COMMON_RANGE_SET,
DATE_FILTER_TEST_KEY,
} from 'src/explore/components/controls/DateFilterControl/utils';
import {
CommonRangeType,
@@ -38,7 +39,12 @@ export function CommonFrame(props: FrameComponentProps) {
return (
<>
<div className="section-title">{t('Configure Time Range: Last...')}</div>
<div
className="section-title"
data-test={DATE_FILTER_TEST_KEY.commonFrame}
>
{t('Configure Time Range: Last...')}
</div>
<Radio.Group
value={commonRange}
onChange={(e: any) => props.onChange(e.target.value)}

View File

@@ -21,4 +21,5 @@ export {
DATE_FILTER_CONTROL_TEST_ID,
fetchTimeRange,
guessFrame,
DATE_FILTER_TEST_KEY,
} from './utils';

View File

@@ -19,7 +19,7 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { AdvancedFrame } from '.';
import { AdvancedFrame } from '../components';
test('renders with default props', () => {
render(<AdvancedFrame onChange={jest.fn()} value="Last week" />);

View File

@@ -22,7 +22,7 @@ import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { CustomFrame } from '.';
import { CustomFrame } from '../components';
jest.useFakeTimers();

View File

@@ -0,0 +1,86 @@
/**
* 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 thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { NO_TIME_RANGE } from '@superset-ui/core';
import DateFilterLabel from '..';
import { DateFilterControlProps } from '../types';
import { DATE_FILTER_TEST_KEY } from '../utils';
const mockStore = configureStore([thunk]);
function setup(
props: Omit<DateFilterControlProps, 'name'>,
store: any = mockStore({}),
) {
return (
<Provider store={store}>
<DateFilterLabel
name="time_range"
onChange={props.onChange}
overlayStyle={props.overlayStyle}
/>
</Provider>
);
}
test('DateFilter with default props', () => {
render(setup({ onChange: () => {} }));
// label
expect(screen.getByText(NO_TIME_RANGE)).toBeInTheDocument();
// should be popover by default
userEvent.click(screen.getByText(NO_TIME_RANGE));
expect(
screen.getByTestId(DATE_FILTER_TEST_KEY.popoverOverlay),
).toBeInTheDocument();
});
test('DateFilter shoule be applied the overlayStyle props', () => {
render(setup({ onChange: () => {}, overlayStyle: 'Modal' }));
// should be Modal as overlay
userEvent.click(screen.getByText(NO_TIME_RANGE));
expect(
screen.getByTestId(DATE_FILTER_TEST_KEY.modalOverlay),
).toBeInTheDocument();
});
test('DateFilter shoule be applied the global config time_filter from the store', () => {
render(
setup(
{ onChange: () => {} },
mockStore({
common: { conf: { DEFAULT_TIME_FILTER: 'Last week' } },
}),
),
);
// the label should be 'Last week'
expect(screen.getByText('Last week')).toBeInTheDocument();
userEvent.click(screen.getByText('Last week'));
expect(
screen.getByTestId(DATE_FILTER_TEST_KEY.commonFrame),
).toBeInTheDocument();
});

View File

@@ -99,4 +99,5 @@ export interface DateFilterControlProps {
type?: Type;
onOpenPopover?: () => void;
onClosePopover?: () => void;
overlayStyle?: 'Modal' | 'Popover';
}

View File

@@ -137,3 +137,11 @@ export const DATE_FILTER_CONTROL_TEST_ID = 'date-filter-control';
export const getDateFilterControlTestId = testWithId(
DATE_FILTER_CONTROL_TEST_ID,
);
export enum DATE_FILTER_TEST_KEY {
commonFrame = 'common-frame',
modalOverlay = 'modal-overlay',
popoverOverlay = 'time-range-trigger',
noFilter = 'no-filter',
cancelButton = 'cancel-button',
}

View File

@@ -17,8 +17,9 @@
* under the License.
*/
import rison from 'rison';
import { SupersetClient, NO_TIME_RANGE } from '@superset-ui/core';
import { SupersetClient, NO_TIME_RANGE, JsonObject } from '@superset-ui/core';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { useSelector } from 'react-redux';
import {
COMMON_RANGE_VALUES_SET,
CALENDAR_RANGE_VALUES_SET,
@@ -84,3 +85,11 @@ export const fetchTimeRange = async (
};
}
};
export function useDefaultTimeFilter() {
return (
useSelector(
(state: JsonObject) => state?.common?.conf?.DEFAULT_TIME_FILTER,
) ?? NO_TIME_RANGE
);
}

View File

@@ -22,6 +22,7 @@ import { DndItemType } from 'src/explore/components/DndItemType';
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
import { OptionSortType } from 'src/explore/types';
import { useGetTimeRangeLabel } from 'src/explore/components/controls/FilterControl/utils';
import OptionWrapper from './OptionWrapper';
export interface DndAdhocFilterOptionProps {
@@ -45,6 +46,8 @@ export default function DndAdhocFilterOption({
partitionColumn,
index,
}: DndAdhocFilterOptionProps) {
const { actualTimeRange, title } = useGetTimeRangeLabel(adhocFilter);
return (
<AdhocFilterPopoverTrigger
key={index}
@@ -57,8 +60,8 @@ export default function DndAdhocFilterOption({
<OptionWrapper
key={index}
index={index}
label={adhocFilter.getDefaultLabel()}
tooltipTitle={adhocFilter.getTooltipTitle()}
label={actualTimeRange ?? adhocFilter.getDefaultLabel()}
tooltipTitle={title ?? adhocFilter.getTooltipTitle()}
clickClose={onClickClose}
onShiftOptions={onShiftOptions}
type={DndItemType.FilterOption}

View File

@@ -17,7 +17,19 @@
* under the License.
*/
import React from 'react';
import { FeatureFlag, GenericDataType } from '@superset-ui/core';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import {
ensureIsArray,
FeatureFlag,
GenericDataType,
QueryFormData,
} from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls';
import { TimeseriesDefaultFormData } from '@superset-ui/plugin-chart-echarts';
import { render, screen } from 'spec/helpers/testing-library';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import AdhocFilter, {
@@ -28,7 +40,6 @@ import {
DndFilterSelectProps,
} from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
import { TimeseriesDefaultFormData } from '@superset-ui/plugin-chart-echarts';
const defaultProps: DndFilterSelectProps = {
type: 'DndFilterSelect',
@@ -56,8 +67,32 @@ afterAll(() => {
window.featureFlags = {};
});
const mockStore = configureStore([thunk]);
const store = mockStore({});
function setup({
value = undefined,
formData = baseFormData,
columns = [],
}: {
value?: AdhocFilter;
formData?: QueryFormData;
columns?: ColumnMeta[];
} = {}) {
return (
<Provider store={store}>
<DndFilterSelect
{...defaultProps}
value={ensureIsArray(value)}
formData={formData}
columns={columns}
/>
</Provider>
);
}
test('renders with default props', async () => {
render(<DndFilterSelect {...defaultProps} />, { useDnd: true });
render(setup(), { useDnd: true });
expect(
await screen.findByText('Drop columns or metrics here'),
).toBeInTheDocument();
@@ -68,7 +103,7 @@ test('renders with value', async () => {
sqlExpression: 'COUNT(*)',
expressionType: EXPRESSION_TYPES.SQL,
});
render(<DndFilterSelect {...defaultProps} value={[value]} />, {
render(setup({ value }), {
useDnd: true,
});
expect(await screen.findByText('COUNT(*)')).toBeInTheDocument();
@@ -76,14 +111,13 @@ test('renders with value', async () => {
test('renders options with saved metric', async () => {
render(
<DndFilterSelect
{...defaultProps}
formData={{
setup({
formData: {
...baseFormData,
...TimeseriesDefaultFormData,
metrics: ['saved_metric'],
}}
/>,
},
}),
{
useDnd: true,
},
@@ -95,17 +129,16 @@ test('renders options with saved metric', async () => {
test('renders options with column', async () => {
render(
<DndFilterSelect
{...defaultProps}
columns={[
setup({
columns: [
{
id: 1,
type: 'VARCHAR',
type_generic: GenericDataType.STRING,
column_name: 'Column',
},
]}
/>,
],
}),
{
useDnd: true,
},
@@ -121,14 +154,13 @@ test('renders options with adhoc metric', async () => {
metric_name: 'avg__num',
});
render(
<DndFilterSelect
{...defaultProps}
formData={{
setup({
formData: {
...baseFormData,
...TimeseriesDefaultFormData,
metrics: [adhocMetric],
}}
/>,
},
}),
{
useDnd: true,
},

View File

@@ -19,6 +19,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
FeatureFlag,
hasGenericChartAxes,
isFeatureEnabled,
logging,
Metric,
@@ -27,7 +28,12 @@ import {
SupersetClient,
t,
} from '@superset-ui/core';
import { ColumnMeta, withDndFallback } from '@superset-ui/chart-controls';
import {
ColumnMeta,
isColumnMeta,
isTemporalColumn,
withDndFallback,
} from '@superset-ui/chart-controls';
import {
OPERATOR_ENUM_TO_OPERATOR_TYPE,
Operators,
@@ -50,6 +56,7 @@ import { DndItemType } from 'src/explore/components/DndItemType';
import { ControlComponentProps } from 'src/explore/components/Control';
import AdhocFilterControl from '../FilterControl/AdhocFilterControl';
import DndAdhocFilterOption from './DndAdhocFilterOption';
import { useDefaultTimeFilter } from '../DateFilterControl/utils';
const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [
@@ -324,6 +331,7 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
togglePopover(true);
}, [togglePopover]);
const defaultTimeFilter = useDefaultTimeFilter();
const adhocFilter = useMemo(() => {
if (isSavedMetric(droppedItem)) {
return new AdhocFilter({
@@ -346,6 +354,15 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
config.operator = OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.IN].operation;
config.operatorId = Operators.IN;
}
if (
hasGenericChartAxes &&
isColumnMeta(droppedItem) &&
isTemporalColumn(droppedItem?.column_name, props.datasource)
) {
config.operator = Operators.TEMPORAL_RANGE;
config.operatorId = Operators.TEMPORAL_RANGE;
config.comparator = defaultTimeFilter;
}
return new AdhocFilter(config);
}, [droppedItem]);

View File

@@ -62,7 +62,9 @@ function translateToSql(adhocMetric, { useSimple } = {}) {
const { subject, comparator } = adhocMetric;
const operator =
adhocMetric.operator &&
CUSTOM_OPERATIONS.indexOf(adhocMetric.operator) >= 0
// 'LATEST PARTITION' supported callback only
adhocMetric.operator ===
OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.LATEST_PARTITION].operation
? OPERATORS_TO_SQL[adhocMetric.operator](adhocMetric)
: OPERATORS_TO_SQL[adhocMetric.operator];
return getSimpleSQLExpression(subject, operator, comparator);

View File

@@ -18,8 +18,12 @@
*/
/* eslint-disable no-unused-expressions */
import React from 'react';
import * as redux from 'react-redux';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import AdhocFilter, {
EXPRESSION_TYPES,
@@ -37,6 +41,7 @@ import * as featureFlags from 'src/featureFlags';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { TestDataset } from '@superset-ui/chart-controls';
import AdhocFilterEditPopoverSimpleTabContent, {
useSimpleTabFilterProps,
Props,
@@ -99,10 +104,11 @@ const getAdvancedDataTypeTestProps = (overrides?: Record<string, any>) => {
onChange,
options: [{ type: 'DOUBLE', column_name: 'advancedDataType', id: 5 }],
datasource: {
id: 'test-id',
columns: [],
type: 'postgres',
filter_select: false,
...TestDataset,
...{
columns: [],
filter_select: false,
},
},
partitionColumn: 'test',
...overrides,
@@ -114,15 +120,18 @@ const getAdvancedDataTypeTestProps = (overrides?: Record<string, any>) => {
function setup(overrides?: Record<string, any>) {
const onChange = sinon.spy();
const validHandler = sinon.spy();
const spy = jest.spyOn(redux, 'useSelector');
spy.mockReturnValue({});
const props = {
adhocFilter: simpleAdhocFilter,
onChange,
options,
datasource: {
id: 'test-id',
columns: [],
type: 'postgres',
filter_select: false,
...TestDataset,
...{
columns: [],
filter_select: false,
},
},
partitionColumn: 'test',
...overrides,
@@ -372,14 +381,19 @@ fetchMock.get(ADVANCED_DATA_TYPE_ENDPOINT_INVALID, {
values: [],
},
});
const mockStore = configureStore([thunk]);
const store = mockStore({});
describe('AdhocFilterEditPopoverSimpleTabContent Advanced data Type Test', () => {
const setupFilter = async (props: Props) => {
await act(async () => {
render(
<ThemeProvider theme={supersetTheme}>
<AdhocFilterEditPopoverSimpleTabContent {...props} />
</ThemeProvider>,
<Provider store={store}>
<ThemeProvider theme={supersetTheme}>
<AdhocFilterEditPopoverSimpleTabContent {...props} />
</ThemeProvider>
,
</Provider>,
);
});
};

View File

@@ -19,7 +19,14 @@
import React, { useEffect, useState } from 'react';
import FormItem from 'src/components/Form/FormItem';
import { Select } from 'src/components';
import { t, SupersetClient, SupersetTheme, styled } from '@superset-ui/core';
import {
t,
SupersetClient,
SupersetTheme,
styled,
hasGenericChartAxes,
isDefined,
} from '@superset-ui/core';
import {
Operators,
OPERATORS_OPTIONS,
@@ -39,8 +46,14 @@ import { Tooltip } from 'src/components/Tooltip';
import { Input } from 'src/components/Input';
import { optionLabel } from 'src/utils/common';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { ColumnMeta } from '@superset-ui/chart-controls';
import {
ColumnMeta,
Dataset,
isTemporalColumn,
} from '@superset-ui/chart-controls';
import useAdvancedDataTypes from './useAdvancedDataTypes';
import { useDatePickerInAdhocFilter } from '../utils';
import { useDefaultTimeFilter } from '../../DateFilterControl/utils';
const StyledInput = styled(Input)`
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
@@ -88,12 +101,7 @@ export interface Props {
adhocFilter: AdhocFilter;
onChange: (filter: AdhocFilter) => void;
options: ColumnType[];
datasource: {
id: string;
columns: ColumnMeta[];
type: string;
filter_select: boolean;
};
datasource: Dataset;
partitionColumn: string;
operators?: Operators[];
validHandler: (isValid: boolean) => void;
@@ -106,6 +114,8 @@ export interface AdvancedDataTypesState {
}
export const useSimpleTabFilterProps = (props: Props) => {
const defaultTimeFilter = useDefaultTimeFilter();
const isOperatorRelevant = (operator: Operators, subject: string) => {
const column = props.datasource.columns?.find(
col => col.column_name === subject,
@@ -116,10 +126,14 @@ export const useSimpleTabFilterProps = (props: Props) => {
!!column && (column.type === 'INT' || column.type === 'INTEGER');
const isColumnFunction = !!column && !!column.expression;
if (operator && CUSTOM_OPERATORS.has(operator)) {
if (operator && operator === Operators.LATEST_PARTITION) {
const { partitionColumn } = props;
return partitionColumn && subject && subject === partitionColumn;
}
if (operator && operator === Operators.TEMPORAL_RANGE) {
// hide the TEMPORAL_RANGE operator
return false;
}
if (operator === Operators.IS_TRUE || operator === Operators.IS_FALSE) {
return isColumnBoolean || isColumnNumber || isColumnFunction;
}
@@ -152,17 +166,33 @@ export const useSimpleTabFilterProps = (props: Props) => {
subject = option.label;
clause = CLAUSES.HAVING;
}
const { operator, operatorId } = props.adhocFilter;
let { operator, operatorId, comparator } = props.adhocFilter;
operator =
operator && operatorId && isOperatorRelevant(operatorId, subject)
? OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId].operation
: null;
if (!isDefined(operator)) {
// if operator is `null`, use the `IN` and reset the comparator.
operator = Operators.IN;
operatorId = Operators.IN;
comparator = undefined;
}
if (hasGenericChartAxes && isTemporalColumn(id, props.datasource)) {
subject = id;
operator = Operators.TEMPORAL_RANGE;
operatorId = Operators.TEMPORAL_RANGE;
comparator = defaultTimeFilter;
}
props.onChange(
props.adhocFilter.duplicateWith({
subject,
clause,
operator:
operator && operatorId && isOperatorRelevant(operatorId, subject)
? OPERATOR_ENUM_TO_OPERATOR_TYPE[operatorId].operation
: null,
operator,
expressionType: EXPRESSION_TYPES.SIMPLE,
operatorId,
comparator,
}),
);
};
@@ -221,12 +251,23 @@ export const useSimpleTabFilterProps = (props: Props) => {
}),
);
};
const onDatePickerChange = (columnName: string, timeRange: string) => {
props.onChange(
props.adhocFilter.duplicateWith({
subject: columnName,
operator: Operators.TEMPORAL_RANGE,
comparator: timeRange,
expressionType: EXPRESSION_TYPES.SIMPLE,
}),
);
};
return {
onSubjectChange,
onOperatorChange,
onComparatorChange,
isOperatorRelevant,
clearOperator,
onDatePickerChange,
};
};
@@ -236,6 +277,7 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
onOperatorChange,
isOperatorRelevant,
onComparatorChange,
onDatePickerChange,
} = useSimpleTabFilterProps(props);
const [suggestions, setSuggestions] = useState<
Record<'label' | 'value', any>[]
@@ -343,6 +385,16 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
const labelText =
comparator && comparator.length > 0 && createSuggestionsPlaceholder();
const datePicker = useDatePickerInAdhocFilter({
columnName: props.adhocFilter.subject,
timeRange:
props.adhocFilter.operator === Operators.TEMPORAL_RANGE
? props.adhocFilter.comparator
: undefined,
datasource: props.datasource,
onChange: onDatePickerChange,
});
useEffect(() => {
const refreshComparatorSuggestions = () => {
const { datasource } = props;
@@ -375,7 +427,9 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
});
}
};
refreshComparatorSuggestions();
if (!datePicker) {
refreshComparatorSuggestions();
}
}, [props.adhocFilter.subject]);
useEffect(() => {
@@ -481,7 +535,7 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
return (
<>
{subjectComponent}
{operatorsAndOperandComponent}
{datePicker ?? operatorsAndOperandComponent}
</>
);
};

View File

@@ -23,6 +23,7 @@ import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterCon
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
import { OptionSortType } from 'src/explore/types';
import { Operators } from 'src/explore/constants';
import { useGetTimeRangeLabel } from '../utils';
export interface AdhocFilterOptionProps {
adhocFilter: AdhocFilter;
@@ -51,6 +52,8 @@ export default function AdhocFilterOption({
sections,
operators,
}: AdhocFilterOptionProps) {
const { actualTimeRange, title } = useGetTimeRangeLabel(adhocFilter);
return (
<AdhocFilterPopoverTrigger
sections={sections}
@@ -62,8 +65,8 @@ export default function AdhocFilterOption({
partitionColumn={partitionColumn}
>
<OptionControlLabel
label={adhocFilter.getDefaultLabel()}
tooltipTitle={adhocFilter.getTooltipTitle()}
label={actualTimeRange ?? adhocFilter.getDefaultLabel()}
tooltipTitle={title ?? adhocFilter.getTooltipTitle()}
onRemove={onRemoveFilter}
onMoveLabel={onMoveLabel}
onDropLabel={onDropLabel}

View File

@@ -0,0 +1,20 @@
/**
* 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.
*/
export { useGetTimeRangeLabel } from './useGetTimeRangeLabel';
export { useDatePickerInAdhocFilter } from './useDatePickerInAdhocFilter';

View File

@@ -0,0 +1,52 @@
/**
* 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 { hasGenericChartAxes, t } from '@superset-ui/core';
import { Dataset, isTemporalColumn } from '@superset-ui/chart-controls';
import DateFilterControl from 'src/explore/components/controls/DateFilterControl/DateFilterLabel';
import ControlHeader from 'src/explore/components/ControlHeader';
interface DatePickerInFilterProps {
columnName: string;
timeRange?: string;
datasource: Dataset;
onChange: (columnName: string, timeRange: string) => void;
}
export const useDatePickerInAdhocFilter = ({
columnName,
timeRange,
datasource,
onChange,
}: DatePickerInFilterProps): React.ReactElement | undefined => {
const onTimeRangeChange = (val: string) => onChange(columnName, val);
return hasGenericChartAxes && isTemporalColumn(columnName, datasource) ? (
<>
<ControlHeader label={t('Time Range')} />
<DateFilterControl
value={timeRange}
name="time_range"
onChange={onTimeRangeChange}
overlayStyle="Modal"
/>
</>
) : undefined;
};

View File

@@ -0,0 +1,64 @@
/**
* 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 { renderHook } from '@testing-library/react-hooks';
import { TestDataset } from '@superset-ui/chart-controls';
import * as supersetCoreModule from '@superset-ui/core';
import { useDatePickerInAdhocFilter } from './useDatePickerInAdhocFilter';
test('should return undefined if Generic Axis is disabled', () => {
Object.defineProperty(supersetCoreModule, 'hasGenericChartAxes', {
value: false,
});
const { result } = renderHook(() =>
useDatePickerInAdhocFilter({
columnName: 'ds',
datasource: TestDataset,
onChange: jest.fn(),
}),
);
expect(result.current).toBeUndefined();
});
test('should return undefined if column is not temporal', () => {
Object.defineProperty(supersetCoreModule, 'hasGenericChartAxes', {
value: true,
});
const { result } = renderHook(() =>
useDatePickerInAdhocFilter({
columnName: 'gender',
datasource: TestDataset,
onChange: jest.fn(),
}),
);
expect(result.current).toBeUndefined();
});
test('should return JSX', () => {
Object.defineProperty(supersetCoreModule, 'hasGenericChartAxes', {
value: true,
});
const { result } = renderHook(() =>
useDatePickerInAdhocFilter({
columnName: 'ds',
datasource: TestDataset,
onChange: jest.fn(),
}),
);
expect(result.current).not.toBeUndefined();
});

View File

@@ -0,0 +1,103 @@
/**
* 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 { renderHook } from '@testing-library/react-hooks';
import { NO_TIME_RANGE } from '@superset-ui/core';
import { Operators } from 'src/explore/constants';
import * as FetchTimeRangeModule from 'src/explore/components/controls/DateFilterControl';
import { useGetTimeRangeLabel } from './useGetTimeRangeLabel';
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from '../AdhocFilter';
test('should return empty object if operator is not TEMPORAL_RANGE', () => {
const adhocFilter = new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: 'value',
operator: '>',
comparator: '10',
clause: CLAUSES.WHERE,
});
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({});
});
test('should return empty object if expressionType is SQL', () => {
const adhocFilter = new AdhocFilter({
expressionType: EXPRESSION_TYPES.SQL,
subject: 'temporal column',
operator: Operators.TEMPORAL_RANGE,
comparator: 'Last week',
clause: CLAUSES.WHERE,
});
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({});
});
test('should get "No filter" label', () => {
const adhocFilter = new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: 'temporal column',
operator: Operators.TEMPORAL_RANGE,
comparator: NO_TIME_RANGE,
clause: CLAUSES.WHERE,
});
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({
actualTimeRange: 'temporal column (No filter)',
title: 'No filter',
});
});
test('should get actualTimeRange and title', async () => {
jest
.spyOn(FetchTimeRangeModule, 'fetchTimeRange')
.mockResolvedValue({ value: 'MOCK TIME' });
const adhocFilter = new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: 'temporal column',
operator: Operators.TEMPORAL_RANGE,
comparator: 'Last week',
clause: CLAUSES.WHERE,
});
const { result } = await renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({
actualTimeRange: 'MOCK TIME',
title: 'Last week',
});
});
test('should get actualTimeRange and title when gets an error', async () => {
jest
.spyOn(FetchTimeRangeModule, 'fetchTimeRange')
.mockResolvedValue({ error: 'MOCK ERROR' });
const adhocFilter = new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: 'temporal column',
operator: Operators.TEMPORAL_RANGE,
comparator: 'Last week',
clause: CLAUSES.WHERE,
});
const { result } = await renderHook(() => useGetTimeRangeLabel(adhocFilter));
expect(result.current).toEqual({
actualTimeRange: 'temporal column (Last week)',
title: 'MOCK ERROR',
});
});

View File

@@ -0,0 +1,75 @@
/**
* 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 { useEffect, useState } from 'react';
import { NO_TIME_RANGE } from '@superset-ui/core';
import { fetchTimeRange } from 'src/explore/components/controls/DateFilterControl';
import { Operators } from 'src/explore/constants';
import AdhocFilter, { EXPRESSION_TYPES } from '../AdhocFilter';
interface Results {
actualTimeRange?: string;
title?: string;
}
export const useGetTimeRangeLabel = (adhocFilter: AdhocFilter): Results => {
const [actualTimeRange, setActualTimeRange] = useState<Results>({});
useEffect(() => {
if (
adhocFilter.operator !== Operators.TEMPORAL_RANGE ||
adhocFilter.expressionType !== EXPRESSION_TYPES.SIMPLE
) {
setActualTimeRange({});
}
if (
adhocFilter.operator === Operators.TEMPORAL_RANGE &&
adhocFilter.comparator === NO_TIME_RANGE
) {
setActualTimeRange({
actualTimeRange: `${adhocFilter.subject} (${NO_TIME_RANGE})`,
title: NO_TIME_RANGE,
});
}
if (
adhocFilter.operator === Operators.TEMPORAL_RANGE &&
adhocFilter.expressionType === EXPRESSION_TYPES.SIMPLE &&
adhocFilter.comparator !== NO_TIME_RANGE &&
actualTimeRange.title !== adhocFilter.comparator
) {
fetchTimeRange(adhocFilter.comparator, adhocFilter.subject).then(
({ value, error }) => {
if (error) {
setActualTimeRange({
actualTimeRange: `${adhocFilter.subject} (${adhocFilter.comparator})`,
title: error,
});
} else {
setActualTimeRange({
actualTimeRange: value ?? '',
title: adhocFilter.comparator,
});
}
},
);
}
}, [adhocFilter]);
return actualTimeRange;
};