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:
parent
9eab29aeaa
commit
a43ee22f11
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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])}`;
|
||||
};
|
||||
Loading…
Reference in New Issue