This is an automated email from the ASF dual-hosted git repository. arivero pushed a commit to branch table-time-comparison-offset in repository https://gitbox.apache.org/repos/asf/superset.git
commit 3deaf2f228bb3df6d7ea4ca29cc8ace49be28652 Author: lilykuang <[email protected]> AuthorDate: Tue Apr 23 18:20:29 2024 -0700 implement start date offset controls --- .../src/sections/timeComparison.tsx | 9 ++ .../plugin-chart-table/src/transformProps.ts | 2 + .../plugins/plugin-chart-table/src/types.ts | 1 + .../components/controls/ComparisonRangeLabel.tsx | 29 ++++-- .../components/controls/TimeOffsetControl.tsx | 112 +++++++++++++++++++++ .../src/explore/components/controls/index.js | 2 + 6 files changed, 147 insertions(+), 8 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/timeComparison.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/timeComparison.tsx index f1a4aad6ac..2b77c9c311 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/timeComparison.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/timeComparison.tsx @@ -59,6 +59,15 @@ export const timeComparisonControls: ControlPanelSectionConfig = { }, }, ], + [ + { + name: 'start_date_offset', + config: { + type: 'TimeOffsetControl', + label: t('shift start date'), + }, + }, + ], [ { name: 'comparison_type', diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts index 47bd113147..0a9da3a590 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts @@ -569,6 +569,7 @@ const transformProps = ( conditionalFormatting, ); + const startDateOffset = chartProps.rawFormData?.start_date_offset; return { height, width, @@ -602,6 +603,7 @@ const transformProps = ( onContextMenu, isUsingTimeComparison, basicColorFormatters, + startDateOffset, basicColorColumnFormatters, }; }; diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts index 7aa14d06ac..1ec3cbe29d 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts @@ -145,6 +145,7 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> { isUsingTimeComparison?: boolean; basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[]; basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[]; + startDateOffset?: string; } export enum ColorSchemeEnum { diff --git a/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx b/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx index 1d34c6e129..d14ba60b90 100644 --- a/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx +++ b/superset-frontend/src/explore/components/controls/ComparisonRangeLabel.tsx @@ -20,6 +20,7 @@ import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { isEmpty, isEqual } from 'lodash'; +import moment from 'moment'; import { BinaryAdhocFilter, css, @@ -57,24 +58,36 @@ export const ComparisonRangeLabel = ({ const shifts = useSelector<RootState, string[]>( state => state.explore.form_data.time_compare, ); + const startDate = useSelector<RootState, string>( + state => state.explore.form_data.start_date_offset, + ); useEffect(() => { - if (isEmpty(currentTimeRangeFilters) || isEmpty(shifts)) { + if (isEmpty(currentTimeRangeFilters) || (isEmpty(shifts) && !startDate)) { setLabels([]); - } else if (!isEmpty(shifts)) { - const promises = currentTimeRangeFilters.map(filter => - fetchTimeRange( + } else if (!isEmpty(shifts) || startDate) { + const promises = currentTimeRangeFilters.map(filter => { + const startDateShift = moment( + (filter as any).comparator.split(' : ')[0], + ).diff(moment(startDate), 'days'); + const newshift = startDateShift + ? [`${startDateShift} days ago`] + : shifts + ? shifts.slice(0, 1) + : undefined; + + return fetchTimeRange( filter.comparator, filter.subject, - multi ? shifts : ensureIsArray(shifts).slice(0, 1), - ), - ); + multi ? shifts : newshift, + ); + }); Promise.all(promises).then(res => { // access the value property inside the res and set the labels with it in the state setLabels(res.map(r => r.value ?? '')); }); } - }, [currentTimeRangeFilters, shifts]); + }, [currentTimeRangeFilters, shifts, startDate]); return labels.length ? ( <> diff --git a/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx b/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx new file mode 100644 index 0000000000..b3f8892ad1 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/TimeOffsetControl.tsx @@ -0,0 +1,112 @@ +/** + * 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, { ReactNode } from 'react'; +import { isEqual } from 'lodash'; +import moment, { Moment } from 'moment'; +import { BinaryAdhocFilter, SimpleAdhocFilter, css } from '@superset-ui/core'; +import { DatePicker } from 'antd'; +import { RangePickerProps } from 'antd/lib/date-picker'; +import { useSelector } from 'react-redux'; + +import ControlHeader from 'src/explore/components/ControlHeader'; +import { RootState } from 'src/views/store'; + +export interface TimeOffsetControlsProps { + label?: ReactNode; + startDate?: string; + description?: string; + hovered?: boolean; + value?: Moment; + onChange: (datetime: string) => void; +} +const MOMENT_FORMAT = 'YYYY-MM-DD'; +const dttmToMoment = (dttm: string): Moment => { + if (dttm === 'now') { + return moment().utc().startOf('second'); + } + if (dttm === 'today' || dttm === 'No filter') { + return moment().utc().startOf('day'); + } + if (dttm === 'Last week') { + return moment().utc().startOf('day').subtract(7, 'day'); + } + if (dttm === 'Last month') { + return moment().utc().startOf('day').subtract(1, 'month'); + } + if (dttm === 'Last quarter') { + return moment().utc().startOf('day').subtract(1, 'quarter'); + } + if (dttm === 'Last year') { + return moment().utc().startOf('day').subtract(1, 'year'); + } + if (dttm === 'previous calendar week') { + return moment().utc().subtract(1, 'weeks').startOf('isoWeek'); + } + if (dttm === 'previous calendar month') { + return moment().utc().subtract(1, 'months').startOf('month'); + } + if (dttm === 'previous calendar year') { + return moment().utc().subtract(1, 'years').startOf('year'); + } + + return moment(dttm); +}; +const isTimeRangeEqual = ( + left: BinaryAdhocFilter[], + right: BinaryAdhocFilter[], +) => isEqual(left, right); + +export default function TimeOffsetControls({ + onChange, + ...props +}: TimeOffsetControlsProps) { + const currentTimeRangeFilters = useSelector<RootState, BinaryAdhocFilter[]>( + state => + state.explore.form_data.adhoc_filters.filter( + (adhoc_filter: SimpleAdhocFilter) => + adhoc_filter.operator === 'TEMPORAL_RANGE', + ), + isTimeRangeEqual, + ); + const startDate = currentTimeRangeFilters[0]?.comparator.split(' : ')[0]; + + const formatedDate = startDate + ? dttmToMoment(startDate) + : dttmToMoment('now'); + const disabledDate: RangePickerProps['disabledDate'] = current => + current && current >= formatedDate; + + return ( + <div> + <ControlHeader {...props} /> + <DatePicker + css={css` + width: 100%; + `} + onChange={(datetime: Moment) => + onChange(datetime ? datetime.format(MOMENT_FORMAT) : '') + } + defaultPickerValue={ + startDate ? moment(formatedDate).subtract(1, 'day') : undefined + } + disabledDate={disabledDate} + /> + </div> + ); +} diff --git a/superset-frontend/src/explore/components/controls/index.js b/superset-frontend/src/explore/components/controls/index.js index 2bf2662d0c..4c2cf00420 100644 --- a/superset-frontend/src/explore/components/controls/index.js +++ b/superset-frontend/src/explore/components/controls/index.js @@ -34,6 +34,7 @@ import SpatialControl from './SpatialControl'; import TextAreaControl from './TextAreaControl'; import TextControl from './TextControl'; import TimeSeriesColumnControl from './TimeSeriesColumnControl'; +import TimeOffsetControl from './TimeOffsetControl'; import ViewportControl from './ViewportControl'; import VizTypeControl from './VizTypeControl'; import MetricsControl from './MetricControl/MetricsControl'; @@ -82,6 +83,7 @@ const controlMap = { XAxisSortControl, ContourControl, ComparisonRangeLabel, + TimeOffsetControl, ...sharedControlComponents, }; export default controlMap;
