diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index ed1df92ccc35a..6e4dadade9479 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -184,17 +184,23 @@ const FilterBar: React.FC = ({ extraFormData: ExtraFormData, currentState: CurrentFilterState, ) => { - setFilterData(prevFilterData => ({ - ...prevFilterData, - [filter.id]: { - extraFormData, - currentState, - }, - })); + let isInitialized = false; + setFilterData(prevFilterData => { + if (filter.id in prevFilterData) { + isInitialized = true; + } + return { + ...prevFilterData, + [filter.id]: { + extraFormData, + currentState, + }, + }; + }); const children = cascadeChildren[filter.id] || []; - // force instant updating for parent filters - if (filter.isInstant || children.length > 0) { + // force instant updating on initialization or for parent filters + if (!isInitialized || filter.isInstant || children.length > 0) { setExtraFormData(filter.id, extraFormData, currentState); } }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx index 6d25f874e5e18..5104ec15cc258 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterValue.tsx @@ -76,6 +76,7 @@ const FilterValue: React.FC = ({ defaultValue, currentValue, inverseSelection, + inputRef, }); if (!areObjectsEqual(formData || {}, newFormData)) { setFormData(newFormData); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx index 181f440bbb14f..09b6d9053e009 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigForm.tsx @@ -249,8 +249,8 @@ export const FilterConfigForm: React.FC = ({ formFilter?.column && formFilter?.defaultValueQueriesData && ( !scope || (scope.rootPath[0] === DASHBOARD_ROOT_ID && !scope.excluded.length); - -type AppendFormData = { - filters: { - val?: number | string | null; - }[]; -}; - -export const extractDefaultValue = { - [FilterType.filter_select]: (appendFormData: AppendFormData) => - appendFormData.filters?.[0]?.val, - [FilterType.filter_range]: (appendFormData: AppendFormData) => ({ - min: appendFormData.filters?.[0].val, - max: appendFormData.filters?.[1].val, - }), -}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/types.ts index ecce3a6ba8c11..12e88209adfe4 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts @@ -30,6 +30,7 @@ export interface Scope { export enum FilterType { filter_select = 'filter_select', filter_range = 'filter_range', + filter_time = 'filter_time', } /** The target of a filter is the datasource/column being filtered */ diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx index 3e4833d1c1302..0e84bc3ee7b3f 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterControl.tsx @@ -245,6 +245,12 @@ export default function DateFilterControl(props: DateFilterLabelProps) { setShow(false); } + function onOpen() { + setTimeRangeValue(value); + setFrame(guessFrame(value)); + setShow(true); + } + function onHide() { setTimeRangeValue(value); setFrame(guessFrame(value)); @@ -355,7 +361,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) { diff --git a/superset-frontend/src/filters/components/Time/AntdTimeFilter.tsx b/superset-frontend/src/filters/components/Time/AntdTimeFilter.tsx new file mode 100644 index 0000000000000..c00b988e6c856 --- /dev/null +++ b/superset-frontend/src/filters/components/Time/AntdTimeFilter.tsx @@ -0,0 +1,70 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { styled } from '@superset-ui/core'; +import React, { useState, useEffect } from 'react'; +import DateFilterControl from 'src/explore/components/controls/DateFilterControl/DateFilterControl'; +import { AntdPluginFilterStylesProps } from '../types'; +import { AntdPluginFilterTimeProps } from './types'; + +const DEFAULT_VALUE = 'Last week'; + +const Styles = styled.div` + height: ${({ height }) => height}px; + width: ${({ width }) => width}px; + overflow-x: scroll; +`; + +export default function AntdTimeFilter(props: AntdPluginFilterTimeProps) { + const { formData, setExtraFormData, width } = props; + const { defaultValue, currentValue } = formData; + + const [value, setValue] = useState(defaultValue ?? DEFAULT_VALUE); + + const handleTimeRangeChange = (timeRange: string): void => { + setExtraFormData({ + // @ts-ignore + extraFormData: { + override_form_data: { + time_range: timeRange, + }, + }, + currentState: { value: timeRange }, + }); + setValue(timeRange); + }; + + useEffect(() => { + handleTimeRangeChange(currentValue ?? DEFAULT_VALUE); + }, [currentValue]); + + useEffect(() => { + handleTimeRangeChange(defaultValue ?? DEFAULT_VALUE); + }, [defaultValue]); + + return ( + // @ts-ignore + + + + ); +} diff --git a/superset-frontend/src/filters/components/Time/controlPanel.ts b/superset-frontend/src/filters/components/Time/controlPanel.ts new file mode 100644 index 0000000000000..1b5f12b9a78c2 --- /dev/null +++ b/superset-frontend/src/filters/components/Time/controlPanel.ts @@ -0,0 +1,26 @@ +/** + * 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 { ControlPanelConfig } from '@superset-ui/chart-controls'; + +const config: ControlPanelConfig = { + // For control input types, see: superset-frontend/src/explore/components/controls/index.js + controlPanelSections: [], +}; + +export default config; diff --git a/superset-frontend/src/filters/components/Time/images/thumbnail.png b/superset-frontend/src/filters/components/Time/images/thumbnail.png new file mode 100644 index 0000000000000..7afef30bd4e6e Binary files /dev/null and b/superset-frontend/src/filters/components/Time/images/thumbnail.png differ diff --git a/superset-frontend/src/filters/components/Time/index.ts b/superset-frontend/src/filters/components/Time/index.ts new file mode 100644 index 0000000000000..f6e2bd6f6245e --- /dev/null +++ b/superset-frontend/src/filters/components/Time/index.ts @@ -0,0 +1,40 @@ +/** + * 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 { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core'; +import controlPanel from './controlPanel'; +import transformProps from './transformProps'; +import thumbnail from './images/thumbnail.png'; + +export default class TimeFilterPlugin extends ChartPlugin { + constructor() { + const metadata = new ChartMetadata({ + name: t('Time range filter plugin'), + description: 'Custom time filter plugin', + behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER], + thumbnail, + }); + + super({ + controlPanel, + loadChart: () => import('./AntdTimeFilter'), + metadata, + transformProps, + }); + } +} diff --git a/superset-frontend/src/filters/components/Time/transformProps.ts b/superset-frontend/src/filters/components/Time/transformProps.ts new file mode 100644 index 0000000000000..fc3acb0b21c51 --- /dev/null +++ b/superset-frontend/src/filters/components/Time/transformProps.ts @@ -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 { ChartProps } from '@superset-ui/core'; +import { DEFAULT_FORM_DATA } from './types'; + +export default function transformProps(chartProps: ChartProps) { + const { formData, height, hooks, queriesData, width } = chartProps; + const { setExtraFormData } = hooks; + const { data } = queriesData[0]; + + return { + data, + formData: { + ...DEFAULT_FORM_DATA, + ...formData, + }, + height, + setExtraFormData, + width, + }; +} diff --git a/superset-frontend/src/filters/components/Time/types.ts b/superset-frontend/src/filters/components/Time/types.ts new file mode 100644 index 0000000000000..7676e0f79fdfd --- /dev/null +++ b/superset-frontend/src/filters/components/Time/types.ts @@ -0,0 +1,44 @@ +/** + * 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 { + QueryFormData, + DataRecord, + SetExtraFormDataHook, +} from '@superset-ui/core'; +import { AntdPluginFilterStylesProps } from '../types'; + +interface PluginFilterTimeCustomizeProps { + defaultValue?: string | null; + currentValue?: string | null; +} + +export type AntdPluginFilterSelectQueryFormData = QueryFormData & + AntdPluginFilterStylesProps & + PluginFilterTimeCustomizeProps; + +export type AntdPluginFilterTimeProps = AntdPluginFilterStylesProps & { + data: DataRecord[]; + setExtraFormData: SetExtraFormDataHook; + formData: AntdPluginFilterSelectQueryFormData; +}; + +export const DEFAULT_FORM_DATA: PluginFilterTimeCustomizeProps = { + defaultValue: null, + currentValue: null, +}; diff --git a/superset-frontend/src/filters/components/index.ts b/superset-frontend/src/filters/components/index.ts index fadaf1cdec122..0f9582af3f2d4 100644 --- a/superset-frontend/src/filters/components/index.ts +++ b/superset-frontend/src/filters/components/index.ts @@ -18,3 +18,4 @@ */ export { default as AntdSelectFilterPlugin } from './Select'; export { default as AntdRangeFilterPlugin } from './Range'; +export { default as TimeFilterPlugin } from './Time'; diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js index 0aec9ecbbe15c..0bee4148f6ea3 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -62,6 +62,7 @@ import { import { AntdSelectFilterPlugin, AntdRangeFilterPlugin, + TimeFilterPlugin, } from 'src/filters/components/'; import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin'; import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin'; @@ -113,6 +114,7 @@ export default class MainPreset extends Preset { }), new AntdSelectFilterPlugin().configure({ key: 'filter_select' }), new AntdRangeFilterPlugin().configure({ key: 'filter_range' }), + new TimeFilterPlugin().configure({ key: 'filter_time' }), ], }); } diff --git a/superset/utils/core.py b/superset/utils/core.py index edbbe63b29f60..cf90963cd17cc 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1027,8 +1027,41 @@ def to_adhoc( return result +def merge_extra_form_data(form_data: Dict[str, Any]) -> None: + """ + Merge extra form data (appends and overrides) into the main payload + and add applied time extras to the payload. + """ + time_extras = { + "time_range": "__time_range", + "granularity_sqla": "__time_col", + "time_grain_sqla": "__time_grain", + "druid_time_origin": "__time_origin", + "granularity": "__granularity", + } + applied_time_extras = form_data.get("applied_time_extras", {}) + form_data["applied_time_extras"] = applied_time_extras + extra_form_data = form_data.pop("extra_form_data", {}) + append_form_data = extra_form_data.pop("append_form_data", {}) + append_filters = append_form_data.get("filters", None) + override_form_data = extra_form_data.pop("override_form_data", {}) + for key, value in override_form_data.items(): + form_data[key] = value + # mark as temporal overrides as applied time extras + time_extra = time_extras.get(key) + if time_extra: + applied_time_extras[time_extra] = value + + adhoc_filters = form_data.get("adhoc_filters", []) + form_data["adhoc_filters"] = adhoc_filters + if append_filters: + adhoc_filters.extend( + [to_adhoc({"isExtra": True, **fltr}) for fltr in append_filters if fltr] + ) + + def merge_extra_filters( # pylint: disable=too-many-branches - form_data: Dict[str, Any] + form_data: Dict[str, Any], ) -> None: # extra_filters are temporary/contextual filters (using the legacy constructs) # that are external to the slice definition. We use those for dynamic @@ -1038,16 +1071,7 @@ def merge_extra_filters( # pylint: disable=too-many-branches form_data["applied_time_extras"] = applied_time_extras adhoc_filters = form_data.get("adhoc_filters", []) form_data["adhoc_filters"] = adhoc_filters - # extra_overrides contains additional props to be added/overridden in the form_data - # and will deprecate `extra_filters`. For now only `filters` is supported, - # but additional props will be added later (time grains, groupbys etc) - extra_form_data = form_data.pop("extra_form_data", {}) - append_form_data = extra_form_data.pop("append_form_data", {}) - append_filters = append_form_data.get("filters", None) - if append_filters: - adhoc_filters.extend( - [to_adhoc({"isExtra": True, **fltr}) for fltr in append_filters if fltr] - ) + merge_extra_form_data(form_data) if "extra_filters" in form_data: # __form and __to are special extra_filters that target time # boundaries. The rest of extra_filters are simple diff --git a/tests/utils_tests.py b/tests/utils_tests.py index ab2f6ea33d9f6..571590d579798 100644 --- a/tests/utils_tests.py +++ b/tests/utils_tests.py @@ -57,6 +57,7 @@ JSONEncodedDict, memoized, merge_extra_filters, + merge_extra_form_data, merge_request_params, parse_ssl_cert, parse_js_uri_path_item, @@ -902,6 +903,35 @@ def test_build_extra_filters(self): layout, filter_scopes, default_filters, box_plot.id ) == [{"col": "region", "op": "==", "val": "North America"}] + def test_merge_extra_filters_with_no_extras(self): + form_data = { + "time_range": "Last 10 days", + } + merge_extra_form_data(form_data) + self.assertEqual( + form_data, + { + "time_range": "Last 10 days", + "applied_time_extras": {}, + "adhoc_filters": [], + }, + ) + + def test_merge_extra_filters_with_extras(self): + form_data = { + "time_range": "Last 10 days", + "extra_form_data": { + "append_form_data": { + "filters": [{"col": "foo", "op": "IN", "val": "bar"}] + }, + "override_form_data": {"time_range": "Last 100 years",}, + }, + } + merge_extra_form_data(form_data) + assert form_data["applied_time_extras"] == {"__time_range": "Last 100 years"} + assert form_data["time_range"] == "Last 100 years" + assert len(form_data["adhoc_filters"]) == 1 + def test_ssl_certificate_parse(self): parsed_certificate = parse_ssl_cert(ssl_certificate) self.assertEqual(parsed_certificate.serial_number, 12355228710836649848)