diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx index edfcf9b2383bd..e5ef553267969 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx @@ -53,6 +53,7 @@ import { AdvancedFrame, DateLabel, } from './components'; +import { CurrentCalendarFrame } from './components/CurrentCalendarFrame'; const StyledRangeType = styled(Select)` width: 272px; @@ -201,6 +202,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) { if ( guessedFrame === 'Common' || guessedFrame === 'Calendar' || + guessedFrame === 'Current' || guessedFrame === 'No filter' ) { setActualTimeRange(value); @@ -296,6 +298,12 @@ export default function DateFilterLabel(props: DateFilterControlProps) { {frame === 'Calendar' && ( )} + {frame === 'Current' && ( + + )} {frame === 'Advanced' && ( )} diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame.tsx new file mode 100644 index 0000000000000..78fcacf5c05f9 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame.tsx @@ -0,0 +1,65 @@ +/** + * 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, { useEffect } from 'react'; +import { t } from '@superset-ui/core'; +import { Radio } from 'src/components/Radio'; +import { + CURRENT_RANGE_OPTIONS, + CURRENT_CALENDAR_RANGE_SET, +} from 'src/explore/components/controls/DateFilterControl/utils'; +import { CurrentRangeType, CurrentWeek, FrameComponentProps } from '../types'; + +export function CurrentCalendarFrame({ onChange, value }: FrameComponentProps) { + useEffect(() => { + if (!CURRENT_CALENDAR_RANGE_SET.has(value as CurrentRangeType)) { + onChange(CurrentWeek); + } + }, [value]); + + if (!CURRENT_CALENDAR_RANGE_SET.has(value as CurrentRangeType)) { + return null; + } + + return ( + <> +
+ {t('Configure Time Range: Current...')} +
+ { + let newValue = e.target.value; + // Sanitization: Trim whitespace + newValue = newValue.trim(); + // Validation: Check if the value is non-empty + if (newValue === '') { + return; + } + onChange(newValue); + }} + > + {CURRENT_RANGE_OPTIONS.map(({ value, label }) => ( + + {label} + + ))} + + + ); +} diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts index 0d46ee7a97d19..29449c74d3462 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts @@ -18,6 +18,7 @@ */ export { CommonFrame } from './CommonFrame'; export { CalendarFrame } from './CalendarFrame'; +export { CurrentCalendarFrame } from './CurrentCalendarFrame'; export { CustomFrame } from './CustomFrame'; export { AdvancedFrame } from './AdvancedFrame'; export { DateLabel } from './DateLabel'; diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CurrentCalendarFrame.test.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CurrentCalendarFrame.test.tsx new file mode 100644 index 0000000000000..ae33aa119ee3f --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/CurrentCalendarFrame.test.tsx @@ -0,0 +1,37 @@ +/** + * 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 { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // For advanced DOM assertions +import { CurrentCalendarFrame } from '../components/CurrentCalendarFrame'; +import { CurrentWeek } from '../types'; + +const mockOnChange = jest.fn(); + +test('calls onChange(CurrentWeek) when value is invalid', () => { + render(); + expect(mockOnChange).toHaveBeenCalledWith(CurrentWeek); +}); + +test('returns null if value is not a valid CurrentRangeType', () => { + const { container } = render( + , + ); + expect(container.childNodes.length).toBe(0); +}); diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/types.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/types.ts index ce5e8127698cb..7062b4858065b 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/types.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/types.ts @@ -24,6 +24,7 @@ export type SelectOptionType = { export type FrameType = | 'Common' | 'Calendar' + | 'Current' | 'Custom' | 'Advanced' | 'No filter'; @@ -85,6 +86,18 @@ export type CalendarRangeType = | typeof PreviousCalendarMonth | typeof PreviousCalendarYear; +export const CurrentDay = 'Current day'; +export const CurrentWeek = 'Current week'; +export const CurrentMonth = 'Current month'; +export const CurrentYear = 'Current year'; +export const CurrentQuarter = 'Current quarter'; +export type CurrentRangeType = + | typeof CurrentDay + | typeof CurrentWeek + | typeof CurrentMonth + | typeof CurrentQuarter + | typeof CurrentYear; + export type FrameComponentProps = { onChange: (timeRange: string) => void; value: string; diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts index ddd9bf2e5836a..cad7a0e816230 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/constants.ts @@ -25,11 +25,18 @@ import { PreviousCalendarYear, CommonRangeType, CalendarRangeType, + CurrentRangeType, + CurrentWeek, + CurrentMonth, + CurrentYear, + CurrentQuarter, + CurrentDay, } from 'src/explore/components/controls/DateFilterControl/types'; export const FRAME_OPTIONS: SelectOptionType[] = [ { value: 'Common', label: t('Last') }, { value: 'Calendar', label: t('Previous') }, + { value: 'Current', label: t('Current') }, { value: 'Custom', label: t('Custom') }, { value: 'Advanced', label: t('Advanced') }, { value: 'No filter', label: t('No filter') }, @@ -48,16 +55,24 @@ export const COMMON_RANGE_VALUES_SET = new Set( export const CALENDAR_RANGE_OPTIONS: SelectOptionType[] = [ { value: PreviousCalendarWeek, label: t('previous calendar week') }, - { - value: PreviousCalendarMonth, - label: t('previous calendar month'), - }, + { value: PreviousCalendarMonth, label: t('previous calendar month') }, { value: PreviousCalendarYear, label: t('previous calendar year') }, ]; export const CALENDAR_RANGE_VALUES_SET = new Set( CALENDAR_RANGE_OPTIONS.map(({ value }) => value), ); +export const CURRENT_RANGE_OPTIONS: SelectOptionType[] = [ + { value: CurrentDay, label: t('Current day') }, + { value: CurrentWeek, label: t('Current week') }, + { value: CurrentMonth, label: t('Current month') }, + { value: CurrentQuarter, label: t('Current quarter') }, + { value: CurrentYear, label: t('Current year') }, +]; +export const CURRENT_RANGE_VALUES_SET = new Set( + CURRENT_RANGE_OPTIONS.map(({ value }) => value), +); + const GRAIN_OPTIONS = [ { value: 'second', label: (rel: string) => t('Seconds %s', rel) }, { value: 'minute', label: (rel: string) => t('Minutes %s', rel) }, @@ -107,6 +122,14 @@ export const CALENDAR_RANGE_SET: Set = new Set([ PreviousCalendarYear, ]); +export const CURRENT_CALENDAR_RANGE_SET: Set = new Set([ + CurrentDay, + CurrentWeek, + CurrentMonth, + CurrentQuarter, + CurrentYear, +]); + export const MOMENT_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss'; export const SEVEN_DAYS_AGO = moment() .utc() diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts index 4be932e34ab22..835492e40bc6c 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts @@ -21,6 +21,7 @@ import { useSelector } from 'react-redux'; import { COMMON_RANGE_VALUES_SET, CALENDAR_RANGE_VALUES_SET, + CURRENT_RANGE_VALUES_SET, customTimeRangeDecode, } from '.'; import { FrameType } from '../types'; @@ -32,6 +33,9 @@ export const guessFrame = (timeRange: string): FrameType => { if (CALENDAR_RANGE_VALUES_SET.has(timeRange)) { return 'Calendar'; } + if (CURRENT_RANGE_VALUES_SET.has(timeRange)) { + return 'Current'; + } if (timeRange === NO_TIME_RANGE) { return 'No filter'; } diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py index bffe50c6242c3..a3736b0ab0200 100644 --- a/superset/utils/date_parser.py +++ b/superset/utils/date_parser.py @@ -207,6 +207,36 @@ def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-m and separator not in time_range ): time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, YEAR), YEAR) : DATETRUNC(DATETIME('today'), YEAR)" # pylint: disable=line-too-long,useless-suppression + if ( + time_range + and time_range.startswith("Current day") + and separator not in time_range + ): + time_range = "DATETRUNC(DATEADD(DATETIME('today'), 0, DAY), DAY) : DATETRUNC(DATEADD(DATETIME('today'), 1, DAY), DAY)" # pylint: disable=line-too-long,useless-suppression + if ( + time_range + and time_range.startswith("Current week") + and separator not in time_range + ): + time_range = "DATETRUNC(DATEADD(DATETIME('today'), 0, WEEK), WEEK) : DATETRUNC(DATEADD(DATETIME('today'), 1, WEEK), WEEK)" # pylint: disable=line-too-long,useless-suppression + if ( + time_range + and time_range.startswith("Current month") + and separator not in time_range + ): + time_range = "DATETRUNC(DATEADD(DATETIME('today'), 0, MONTH), MONTH) : DATETRUNC(DATEADD(DATETIME('today'), 1, MONTH), MONTH)" # pylint: disable=line-too-long,useless-suppression + if ( + time_range + and time_range.startswith("Current quarter") + and separator not in time_range + ): + time_range = "DATETRUNC(DATEADD(DATETIME('today'), 0, QUARTER), QUARTER) : DATETRUNC(DATEADD(DATETIME('today'), 1, QUARTER), QUARTER)" # pylint: disable=line-too-long,useless-suppression + if ( + time_range + and time_range.startswith("Current year") + and separator not in time_range + ): + time_range = "DATETRUNC(DATEADD(DATETIME('today'), 0, YEAR), YEAR) : DATETRUNC(DATEADD(DATETIME('today'), 1, YEAR), YEAR)" # pylint: disable=line-too-long,useless-suppression if time_range and separator in time_range: time_range_lookup = [ diff --git a/tests/unit_tests/utils/date_parser_tests.py b/tests/unit_tests/utils/date_parser_tests.py index e007c17e829c5..a5a3f8b0ac926 100644 --- a/tests/unit_tests/utils/date_parser_tests.py +++ b/tests/unit_tests/utils/date_parser_tests.py @@ -160,6 +160,26 @@ def test_get_since_until() -> None: expected = datetime(2015, 1, 1, 0, 0, 0), datetime(2016, 1, 1, 0, 0, 0) assert result == expected + result = get_since_until("Current day") + expected = datetime(2016, 11, 7, 0, 0, 0), datetime(2016, 11, 8, 0, 0, 0) + assert result == expected + + result = get_since_until("Current week") + expected = datetime(2016, 11, 7, 0, 0, 0), datetime(2016, 11, 14, 0, 0, 0) + assert result == expected + + result = get_since_until("Current month") + expected = datetime(2016, 11, 1, 0, 0, 0), datetime(2016, 12, 1, 0, 0, 0) + assert result == expected + + result = get_since_until("Current quarter") + expected = datetime(2016, 10, 1, 0, 0, 0), datetime(2017, 1, 1, 0, 0, 0) + assert result == expected + + result = get_since_until("Current year") + expected = expected = datetime(2016, 1, 1, 0, 0, 0), datetime(2017, 1, 1, 0, 0, 0) + assert result == expected + # Tests for our new instant_time_comparison logic and Feature Flag off result = get_since_until( time_range="2000-01-01T00:00:00 : 2018-01-01T00:00:00",