style: replace inclusive/exclusive on DateFilterControl with </≤ (#10420)

* feat: improve filter control tooltips

* add styles

* break out utils into own file

* lint

* add tests

* styled component now working

* lint

* add license headers

* replace shallow with mount due to withTheme

Co-authored-by: Evan Rusackas <evan@preset.io>
This commit is contained in:
Ville Brofeldt 2020-07-31 20:06:04 +03:00 committed by GitHub
parent 9eab29aeaa
commit a43ee22f11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 42 deletions

View File

@ -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(<DateFilterControl {...defaultProps} />);
wrapper = mount(<DateFilterControl {...defaultProps} />);
});
it('renders a ControlHeader', () => {

View File

@ -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',
);
});
});
});

View File

@ -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}
</MenuItem>
));
const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => {
const nextState = getStateFromCommonTimeFrame(timeFrame);
const endpoints = this.props.endpoints;
const timeRange = buildTimeRangeString(nextState.since, nextState.until);
return (
<OverlayTrigger
key={timeFrame}
placement="left"
overlay={
<Tooltip id={`tooltip-${timeFrame}`}>
{nextState.since} {endpoints && `(${endpoints[0]})`}
<br />
{nextState.until} {endpoints && `(${endpoints[1]})`}
</Tooltip>
}
>
<div>
<Radio
key={timeFrame.replace(' ', '').toLowerCase()}
checked={this.state.common === timeFrame}
onChange={() => this.setState(nextState)}
>
{timeFrame}
</Radio>
</div>
</OverlayTrigger>
<Styles theme={this.props.theme}>
<OverlayTrigger
key={timeFrame}
placement="right"
overlay={
<Tooltip id={`tooltip-${timeFrame}`}>
{formatTimeRange(timeRange, this.props.endpoints)}
</Tooltip>
}
>
<div style={{ display: 'inline-block' }}>
<Radio
key={timeFrame.replace(' ', '').toLowerCase()}
checked={this.state.common === timeFrame}
onChange={() => this.setState(nextState)}
>
{timeFrame}
</Radio>
</div>
</OverlayTrigger>
</Styles>
);
});
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 (
<div>
<ControlHeader {...this.props} />
@ -580,7 +591,7 @@ export default class DateFilterControl extends React.Component {
onClick={this.handleClickTrigger}
>
<Label name="popover-trigger" style={{ cursor: 'pointer' }}>
{value}
{formatTimeRange(timeRange, this.props.endpoints)}
</Label>
</OverlayTrigger>
</div>
@ -588,5 +599,7 @@ export default class DateFilterControl extends React.Component {
}
}
export default withTheme(DateFilterControl);
DateFilterControl.propTypes = propTypes;
DateFilterControl.defaultProps = defaultProps;

View File

@ -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])}`;
};