This is an automated email from the ASF dual-hosted git repository. arivero pushed a commit to branch table-time-comparison in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2f7feff1c10691c2636d8176756b734167017271 Author: Kamil Gabryjelski <[email protected]> AuthorDate: Mon Feb 26 16:44:33 2024 +0100 feat: Add comparison time label to Big Number with Period over Period plugin --- .../src/time-comparison/fetchTimeRange.ts | 18 +++++- .../src/plugin/controlPanel.ts | 6 ++ .../components/controls/ComparisonRangeLabel.tsx | 71 ++++++++++++++++++++++ .../DateFilterControl/utils/dateFilterUtils.ts | 1 + .../src/explore/components/controls/index.js | 2 + superset/views/api.py | 28 +++++++++ tests/integration_tests/charts/api_tests.py | 15 +++++ 7 files changed, 138 insertions(+), 3 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts index 50509af52b..8e50576741 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts @@ -17,7 +17,11 @@ * under the License. */ import rison from 'rison'; -import { SupersetClient, getClientErrorObject } from '@superset-ui/core'; +import { + SupersetClient, + getClientErrorObject, + ComparisonTimeRangeType, +} from '@superset-ui/core'; export const SEPARATOR = ' : '; @@ -42,9 +46,17 @@ export const formatTimeRange = ( export const fetchTimeRange = async ( timeRange: string, columnPlaceholder = 'col', + shift?: ComparisonTimeRangeType, ) => { - const query = rison.encode_uri(timeRange); - const endpoint = `/api/v1/time_range/?q=${query}`; + let query; + let endpoint; + if (shift) { + query = rison.encode_uri({ base_time_range: timeRange, shift }); + endpoint = `/api/v1/relative_time_range/?q=${query}`; + } else { + query = rison.encode_uri(timeRange); + endpoint = `/api/v1/time_range/?q=${query}`; + } try { const response = await SupersetClient.get({ endpoint }); const timeRangeString = buildTimeRangeString( diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts index 7feb3445df..bb70859d9a 100644 --- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts @@ -98,6 +98,12 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'comparison_range_label', + config: { type: 'ComparisonRangeLabel' }, + }, + ], [ { name: 'row_limit', diff --git a/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx b/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx new file mode 100644 index 0000000000..3d2c6a6e68 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx @@ -0,0 +1,71 @@ +/** + * 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, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + ComparisonTimeRangeType, + css, + SimpleAdhocFilter, + t, + fetchTimeRange, +} from '@superset-ui/core'; +import { RootState } from 'src/views/store'; +import { Tooltip } from 'src/components/Tooltip'; + +export const ComparisonRangeLabel = () => { + const [label, setLabel] = useState(''); + const currentTimeRange = useSelector<RootState, string>( + state => + state.explore.form_data.adhoc_filters.filter( + (adhoc_filter: SimpleAdhocFilter) => + adhoc_filter.operator === 'TEMPORAL_RANGE', + )[0]?.comparator, + ); + const shift = useSelector<RootState, ComparisonTimeRangeType>( + state => state.explore.form_data.time_comparison, + ); + + useEffect(() => { + if (shift === ComparisonTimeRangeType.Custom) { + setLabel(''); + } + }, [shift]); + + useEffect(() => { + if (shift !== ComparisonTimeRangeType.Custom) { + fetchTimeRange(currentTimeRange, 'col', shift).then(res => { + setLabel(res.value ?? ''); + }); + } + }, [currentTimeRange, shift]); + + return label ? ( + <Tooltip title={t('Actual time range for comparison')}> + <span + css={theme => css` + font-size: ${theme.typography.sizes.m}px; + color: ${theme.colors.grayscale.base}; + `} + > + {label} + </span> + </Tooltip> + ) : null; +}; 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 4be932e34a..39f3728eaa 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { NO_TIME_RANGE, JsonObject } from '@superset-ui/core'; import { useSelector } from 'react-redux'; import { diff --git a/superset-frontend/src/explore/components/controls/index.js b/superset-frontend/src/explore/components/controls/index.js index a5d65f7768..2bf2662d0c 100644 --- a/superset-frontend/src/explore/components/controls/index.js +++ b/superset-frontend/src/explore/components/controls/index.js @@ -48,6 +48,7 @@ import DndColumnSelectControl, { import XAxisSortControl from './XAxisSortControl'; import CurrencyControl from './CurrencyControl'; import ColumnConfigControl from './ColumnConfigControl'; +import { ComparisonRangeLabel } from './ComparisonRangeLabel'; const controlMap = { AnnotationLayerControl, @@ -80,6 +81,7 @@ const controlMap = { ConditionalFormattingControl, XAxisSortControl, ContourControl, + ComparisonRangeLabel, ...sharedControlComponents, }; export default controlMap; diff --git a/superset/views/api.py b/superset/views/api.py index d7b1b8434e..a44bf81234 100644 --- a/superset/views/api.py +++ b/superset/views/api.py @@ -42,6 +42,11 @@ if TYPE_CHECKING: get_time_range_schema = {"type": "string"} +get_relative_time_range_schema = { + "type": "object", + "properties": {"base_time_range": {"type": "string"}, "shift": {"type": "string"}}, +} + class Api(BaseSupersetView): query_context_factory = None @@ -108,6 +113,29 @@ class Api(BaseSupersetView): error_msg = {"message": _("Unexpected time range: %(error)s", error=error)} return self.json_response(error_msg, 400) + @api + @handle_api_exception + @has_access_api + @rison(ger_relative_time_range_schema) + @expose("/v1/relative_time_range/", methods=("GET",)) + def relative_time_range(self, **kwargs: Any) -> FlaskResponse: + """Get actually time range from human-readable string or datetime expression.""" + base_time_range, shift = kwargs["rison"].values() + try: + since, until = get_since_until( + time_range=base_time_range, instant_time_comparison_range=shift + ) + result = { + "since": since.isoformat() if since else "", + "until": until.isoformat() if until else "", + "baseTimeRange": base_time_range, + "shift": shift, + } + return self.json_response({"result": result}) + except (ValueError, TimeRangeParseFailError, TimeRangeAmbiguousError) as error: + error_msg = {"message": _("Unexpected time range: %(error)s", error=error)} + return self.json_response(error_msg, 400) + def get_query_context_factory(self) -> QueryContextFactory: if self.query_context_factory is None: # pylint: disable=import-outside-toplevel diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py index 829c7ba518..fe71df6ba5 100644 --- a/tests/integration_tests/charts/api_tests.py +++ b/tests/integration_tests/charts/api_tests.py @@ -31,6 +31,7 @@ from sqlalchemy.sql import func from superset.commands.chart.data.get_data_command import ChartDataCommand from superset.commands.chart.exceptions import ChartDataQueryFailedError from superset.connectors.sqla.models import SqlaTable +from superset.constants import InstantTimeComparison from superset.extensions import cache_manager, db, security_manager from superset.models.core import Database, FavStar, FavStarClassName from superset.models.dashboard import Dashboard @@ -1472,6 +1473,20 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase): self.assertEqual(rv.status_code, 200) self.assertEqual(len(data["result"]), 3) + def test_get_relative_time_range(self): + """ + Chart API: Test get shifted time range from human readable string + """ + self.login(username="admin") + humanize_time_range = "100 years ago : now" + shift = InstantTimeComparison.YEAR + + uri = f'api/v1/relative_time_range/?q={prison.dumps({ "base_time_range": humanize_time_range, "shift": shift })}' + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual(rv.status_code, 200) + self.assertEqual(len(data["result"]), 4) + def test_query_form_data(self): """ Chart API: Test query form data
