diff --git a/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx index 3026424d4..1c3b0904e 100644 --- a/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx @@ -19,7 +19,7 @@ /* eslint-disable no-unused-expressions */ import React from 'react'; import sinon from 'sinon'; -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import { Button, Label } from 'react-bootstrap'; import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; @@ -37,7 +37,7 @@ describe('DateFilterControl', () => { let wrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = mount(); }); it('renders a ControlHeader', () => { diff --git a/superset-frontend/spec/javascripts/explore/utils_spec.jsx b/superset-frontend/spec/javascripts/explore/utils_spec.jsx index b4821933d..ce3b064be 100644 --- a/superset-frontend/spec/javascripts/explore/utils_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/utils_spec.jsx @@ -25,6 +25,10 @@ import { getExploreLongUrl, shouldUseLegacyApi, } from 'src/explore/exploreUtils'; +import { + buildTimeRangeString, + formatTimeRange, +} from 'src/explore/dateFilterUtils'; import * as hostNamesConfig from 'src/utils/hostNamesConfig'; import { getChartMetadataRegistry } from '@superset-ui/chart'; @@ -245,4 +249,38 @@ describe('exploreUtils', () => { expect(useLegacyApi).toBe(false); }); }); + + describe('buildTimeRangeString', () => { + it('generates proper time range string', () => { + expect( + buildTimeRangeString('2010-07-30T00:00:00', '2020-07-30T00:00:00'), + ).toBe('2010-07-30T00:00:00 : 2020-07-30T00:00:00'); + expect(buildTimeRangeString('', '2020-07-30T00:00:00')).toBe( + ' : 2020-07-30T00:00:00', + ); + expect(buildTimeRangeString('', '')).toBe(' : '); + }); + }); + + describe('formatTimeRange', () => { + it('generates a readable time range', () => { + expect(formatTimeRange('Last 7 days')).toBe('Last 7 days'); + expect(formatTimeRange('No filter')).toBe('No filter'); + expect(formatTimeRange('Yesterday : Tomorrow')).toBe( + 'Yesterday < col < Tomorrow', + ); + expect( + formatTimeRange('2010-07-30T00:00:00 : 2020-07-30T00:00:00', [ + 'inclusive', + 'exclusive', + ]), + ).toBe('2010-07-30 ≤ col < 2020-07-30'); + expect( + formatTimeRange('2010-07-30T01:00:00 : ', ['exclusive', 'inclusive']), + ).toBe('2010-07-30T01:00:00 < col ≤ ∞'); + expect(formatTimeRange(' : 2020-07-30T00:00:00')).toBe( + '-∞ < col < 2020-07-30', + ); + }); + }); }); diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl.jsx b/superset-frontend/src/explore/components/controls/DateFilterControl.jsx index fca773070..f2a9e09e3 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl.jsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl.jsx @@ -37,7 +37,12 @@ import Datetime from 'react-datetime'; import 'react-datetime/css/react-datetime.css'; import moment from 'moment'; import { t } from '@superset-ui/translation'; +import { styled, withTheme } from '@superset-ui/style'; +import { + buildTimeRangeString, + formatTimeRange, +} from 'src/explore/dateFilterUtils'; import './DateFilterControl.less'; import ControlHeader from '../ControlHeader'; import PopoverSection from '../../../components/PopoverSection'; @@ -128,12 +133,18 @@ function getStateFromCommonTimeFrame(value) { tab: TABS.DEFAULTS, type: TYPES.DEFAULTS, common: value, - since: moment() - .utc() - .startOf('day') - .subtract(1, units) - .format(MOMENT_FORMAT), - until: moment().utc().startOf('day').format(MOMENT_FORMAT), + since: + value === 'No filter' + ? '' + : moment() + .utc() + .startOf('day') + .subtract(1, units) + .format(MOMENT_FORMAT), + until: + value === 'No filter' + ? '' + : moment().utc().startOf('day').format(MOMENT_FORMAT), }; } @@ -164,7 +175,13 @@ function getStateFromCustomRange(value) { }; } -export default class DateFilterControl extends React.Component { +const Styles = styled.div` + .radio { + margin: ${({ theme }) => theme.gridUnit}px 0; + } +`; + +class DateFilterControl extends React.Component { constructor(props) { super(props); this.state = { @@ -363,35 +380,38 @@ export default class DateFilterControl extends React.Component { key={grain} eventKey={grain} active={grain === this.state.grain} + fullWidth={false} > {grain} )); const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => { const nextState = getStateFromCommonTimeFrame(timeFrame); - const endpoints = this.props.endpoints; + + const timeRange = buildTimeRangeString(nextState.since, nextState.until); + return ( - - {nextState.since} {endpoints && `(${endpoints[0]})`} -
- {nextState.until} {endpoints && `(${endpoints[1]})`} - - } - > -
- this.setState(nextState)} - > - {timeFrame} - -
-
+ + + {formatTimeRange(timeRange, this.props.endpoints)} + + } + > +
+ this.setState(nextState)} + > + {timeFrame} + +
+
+
); }); return ( @@ -556,16 +576,7 @@ export default class DateFilterControl extends React.Component { ); } render() { - let value = this.props.value || defaultProps.value; - const endpoints = this.props.endpoints; - value = value - .split(SEPARATOR) - .map( - (v, idx, values) => - (v.replace('T00:00:00', '') || (idx === 0 ? '-∞' : '∞')) + - (endpoints && values.length > 1 ? ` (${endpoints[idx]})` : ''), - ) - .join(SEPARATOR); + const timeRange = this.props.value || defaultProps.value; return (
@@ -580,7 +591,7 @@ export default class DateFilterControl extends React.Component { onClick={this.handleClickTrigger} >
@@ -588,5 +599,7 @@ export default class DateFilterControl extends React.Component { } } +export default withTheme(DateFilterControl); + DateFilterControl.propTypes = propTypes; DateFilterControl.defaultProps = defaultProps; diff --git a/superset-frontend/src/explore/dateFilterUtils.ts b/superset-frontend/src/explore/dateFilterUtils.ts new file mode 100644 index 000000000..f113e6c3a --- /dev/null +++ b/superset-frontend/src/explore/dateFilterUtils.ts @@ -0,0 +1,42 @@ +/** + * 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 { TimeRangeEndpoints } from '@superset-ui/query'; + +const SEPARATOR = ' : '; + +export const buildTimeRangeString = (since: string, until: string): string => + `${since}${SEPARATOR}${until}`; + +const formatDateEndpoint = (dttm: string, isStart?: boolean): string => + dttm.replace('T00:00:00', '') || (isStart ? '-∞' : '∞'); + +export const formatTimeRange = ( + timeRange: string, + endpoints?: TimeRangeEndpoints, +) => { + const splitDateRange = timeRange.split(SEPARATOR); + if (splitDateRange.length === 1) return timeRange; + const formattedEndpoints = ( + endpoints || ['unknown', 'unknown'] + ).map((endpoint: string) => (endpoint === 'inclusive' ? '≤' : '<')); + + return `${formatDateEndpoint(splitDateRange[0], true)} ${ + formattedEndpoints[0] + } col ${formattedEndpoints[1]} ${formatDateEndpoint(splitDateRange[1])}`; +};