feat(explore): Time picker enhancement follow up (#12208)
* refactor layout * WIP * fix typing * styling * fix lint * frontend IT * rename variable * added quarter * typos * refine code structure
This commit is contained in:
parent
811e1b273b
commit
b349edef29
|
|
@ -128,7 +128,7 @@ describe('Time range filter', () => {
|
|||
cy.route('POST', '/superset/explore_json/**').as('postJson');
|
||||
});
|
||||
|
||||
it('Defaults to the correct tab for time_range params', () => {
|
||||
it('Advanced time_range params', () => {
|
||||
const formData = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
metrics: [NUM_METRIC],
|
||||
|
|
@ -142,18 +142,98 @@ describe('Time range filter', () => {
|
|||
cy.get('[data-test=time-range-trigger]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ant-modal-footer')
|
||||
.find('button')
|
||||
.its('length')
|
||||
.should('eq', 3);
|
||||
cy.get('.ant-modal-body').within(() => {
|
||||
cy.get('.footer').find('button').its('length').should('eq', 2);
|
||||
cy.get('.ant-popover-content').within(() => {
|
||||
cy.get('input[value="100 years ago"]');
|
||||
cy.get('input[value="now"]');
|
||||
});
|
||||
cy.get('[data-test=modal-cancel-button]').click();
|
||||
cy.get('[data-test=time-range-modal]').should('not.be.visible');
|
||||
cy.get('[data-test=cancel-button]').click();
|
||||
cy.get('.ant-popover').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('Common time_range params', () => {
|
||||
const formData = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
metrics: [NUM_METRIC],
|
||||
viz_type: 'line',
|
||||
time_range: 'Last year',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ant-radio-group').children().its('length').should('eq', 5);
|
||||
cy.get('.ant-radio-checked + span').contains('last year');
|
||||
cy.get('[data-test=cancel-button]').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('Previous time_range params', () => {
|
||||
const formData = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
metrics: [NUM_METRIC],
|
||||
viz_type: 'line',
|
||||
time_range:
|
||||
'DATETRUNC(DATEADD(DATETIME("TODAY"), -1, MONTH), MONTH) : LASTDAY(DATEADD(DATETIME("TODAY"), -1, MONTH), MONTH)',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ant-radio-group').children().its('length').should('eq', 3);
|
||||
cy.get('.ant-radio-checked + span').contains('previous calendar month');
|
||||
cy.get('[data-test=cancel-button]').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('Custom time_range params', () => {
|
||||
const formData = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
metrics: [NUM_METRIC],
|
||||
viz_type: 'line',
|
||||
time_range: 'DATEADD(DATETIME("today"), -7, day) : today',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('[data-test=custom-frame]').then(() => {
|
||||
cy.get('.ant-input-number-input-wrap > input')
|
||||
.invoke('attr', 'value')
|
||||
.should('eq', '7');
|
||||
});
|
||||
cy.get('[data-test=cancel-button]').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('No filter time_range params', () => {
|
||||
const formData = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
metrics: [NUM_METRIC],
|
||||
viz_type: 'line',
|
||||
time_range: 'No filter',
|
||||
};
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('[data-test=time-range-trigger]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('[data-test=no-filter]');
|
||||
});
|
||||
cy.get('[data-test=cancel-button]').click();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Groupby control', () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import rison from 'rison';
|
||||
import moment, { Moment } from 'moment';
|
||||
import {
|
||||
SupersetClient,
|
||||
styled,
|
||||
|
|
@ -29,252 +28,34 @@ import {
|
|||
import {
|
||||
buildTimeRangeString,
|
||||
formatTimeRange,
|
||||
SEPARATOR,
|
||||
} from 'src/explore/dateFilterUtils';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import Button from 'src/components/Button';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
import Label from 'src/components/Label';
|
||||
import Modal from 'src/common/components/Modal';
|
||||
import {
|
||||
Col,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
} from 'src/common/components';
|
||||
import Popover from 'src/common/components/Popover';
|
||||
import { Divider } from 'src/common/components';
|
||||
import Icon from 'src/components/Icon';
|
||||
import { Select } from 'src/components/Select';
|
||||
import { SelectOptionType, FrameType } from './types';
|
||||
import {
|
||||
TimeRangeFrameType,
|
||||
CommonRangeType,
|
||||
CalendarRangeType,
|
||||
CustomRangeType,
|
||||
CustomRangeDecodeType,
|
||||
CustomRangeKey,
|
||||
PreviousCalendarWeek,
|
||||
PreviousCalendarMonth,
|
||||
PreviousCalendarYear,
|
||||
} from './types';
|
||||
import {
|
||||
COMMON_RANGE_OPTIONS,
|
||||
CALENDAR_RANGE_OPTIONS,
|
||||
RANGE_FRAME_OPTIONS,
|
||||
SINCE_GRAIN_OPTIONS,
|
||||
UNTIL_GRAIN_OPTIONS,
|
||||
SINCE_MODE_OPTIONS,
|
||||
UNTIL_MODE_OPTIONS,
|
||||
COMMON_RANGE_VALUES_SET,
|
||||
CALENDAR_RANGE_VALUES_SET,
|
||||
FRAME_OPTIONS,
|
||||
} from './constants';
|
||||
import { customTimeRangeDecode } from './utils';
|
||||
import {
|
||||
CommonFrame,
|
||||
CalendarFrame,
|
||||
CustomFrame,
|
||||
AdvancedFrame,
|
||||
} from './frame';
|
||||
|
||||
const MOMENT_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss';
|
||||
const DEFAULT_SINCE = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.subtract(7, 'days')
|
||||
.format(MOMENT_FORMAT);
|
||||
const DEFAULT_UNTIL = moment().utc().startOf('day').format(MOMENT_FORMAT);
|
||||
|
||||
/**
|
||||
* RegExp to test a string for a full ISO 8601 Date
|
||||
* Does not do any sort of date validation, only checks if the string is according to the ISO 8601 spec.
|
||||
* YYYY-MM-DDThh:mm:ss
|
||||
* YYYY-MM-DDThh:mm:ssTZD
|
||||
* YYYY-MM-DDThh:mm:ss.sTZD
|
||||
* @see: https://www.w3.org/TR/NOTE-datetime
|
||||
*/
|
||||
const iso8601 = String.raw`\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|Z)?`;
|
||||
const datetimeConstant = String.raw`TODAY|NOW`;
|
||||
const grainValue = String.raw`[+-]?[1-9][0-9]*`;
|
||||
const grain = String.raw`YEAR|QUARTER|MONTH|WEEK|DAY|HOUR|MINUTE|SECOND`;
|
||||
const CUSTOM_RANGE_EXPRESSION = RegExp(
|
||||
String.raw`^DATEADD\(DATETIME\("(${iso8601}|${datetimeConstant})"\),\s(${grainValue}),\s(${grain})\)$`,
|
||||
'i',
|
||||
);
|
||||
export const ISO8601_AND_CONSTANT = RegExp(
|
||||
String.raw`^${iso8601}$|^${datetimeConstant}$`,
|
||||
'i',
|
||||
);
|
||||
|
||||
const DATETIME_CONSTANT = ['now', 'today'];
|
||||
const defaultCustomRange: CustomRangeType = {
|
||||
sinceDatetime: DEFAULT_SINCE,
|
||||
sinceMode: 'relative',
|
||||
sinceGrain: 'day',
|
||||
sinceGrainValue: -7,
|
||||
untilDatetime: DEFAULT_UNTIL,
|
||||
untilMode: 'specific',
|
||||
untilGrain: 'day',
|
||||
untilGrainValue: 7,
|
||||
anchorMode: 'now',
|
||||
anchorValue: 'now',
|
||||
};
|
||||
const SPECIFIC_MODE = ['specific', 'today', 'now'];
|
||||
|
||||
const COMMON_RANGE_OPTIONS_SET = new Set(
|
||||
COMMON_RANGE_OPTIONS.map(({ value }) => value),
|
||||
);
|
||||
const CALENDAR_RANGE_OPTIONS_SET = new Set(
|
||||
CALENDAR_RANGE_OPTIONS.map(({ value }) => value),
|
||||
);
|
||||
|
||||
const commonRangeSet: Set<CommonRangeType> = new Set([
|
||||
'Last day',
|
||||
'Last week',
|
||||
'Last month',
|
||||
'Last quarter',
|
||||
'Last year',
|
||||
]);
|
||||
const CalendarRangeSet: Set<CalendarRangeType> = new Set([
|
||||
PreviousCalendarWeek,
|
||||
PreviousCalendarMonth,
|
||||
PreviousCalendarYear,
|
||||
]);
|
||||
|
||||
const customTimeRangeDecode = (timeRange: string): CustomRangeDecodeType => {
|
||||
const splitDateRange = timeRange.split(SEPARATOR);
|
||||
|
||||
if (splitDateRange.length === 2) {
|
||||
const [since, until] = splitDateRange;
|
||||
|
||||
// specific : specific
|
||||
if (ISO8601_AND_CONSTANT.test(since) && ISO8601_AND_CONSTANT.test(until)) {
|
||||
const sinceMode = DATETIME_CONSTANT.includes(since) ? since : 'specific';
|
||||
const untilMode = DATETIME_CONSTANT.includes(until) ? until : 'specific';
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
sinceDatetime: since,
|
||||
untilDatetime: until,
|
||||
sinceMode,
|
||||
untilMode,
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
|
||||
// relative : specific
|
||||
const sinceCapturedGroup = since.match(CUSTOM_RANGE_EXPRESSION);
|
||||
if (
|
||||
sinceCapturedGroup &&
|
||||
ISO8601_AND_CONSTANT.test(until) &&
|
||||
since.includes(until)
|
||||
) {
|
||||
const [dttm, grainValue, grain] = sinceCapturedGroup.slice(1);
|
||||
const untilMode = DATETIME_CONSTANT.includes(until) ? until : 'specific';
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
sinceGrain: grain,
|
||||
sinceGrainValue: parseInt(grainValue, 10),
|
||||
untilDatetime: dttm,
|
||||
sinceMode: 'relative',
|
||||
untilMode,
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
|
||||
// specific : relative
|
||||
const untilCapturedGroup = until.match(CUSTOM_RANGE_EXPRESSION);
|
||||
if (
|
||||
ISO8601_AND_CONSTANT.test(since) &&
|
||||
untilCapturedGroup &&
|
||||
until.includes(since)
|
||||
) {
|
||||
const [dttm, grainValue, grain] = [...untilCapturedGroup.slice(1)];
|
||||
const sinceMode = DATETIME_CONSTANT.includes(since) ? since : 'specific';
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
untilGrain: grain,
|
||||
untilGrainValue: parseInt(grainValue, 10),
|
||||
sinceDatetime: dttm,
|
||||
untilMode: 'relative',
|
||||
sinceMode,
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
|
||||
// relative : relative
|
||||
if (sinceCapturedGroup && untilCapturedGroup) {
|
||||
const [sinceDttm, sinceGrainValue, sinceGrain] = [
|
||||
...sinceCapturedGroup.slice(1),
|
||||
];
|
||||
const [untileDttm, untilGrainValue, untilGrain] = [
|
||||
...untilCapturedGroup.slice(1),
|
||||
];
|
||||
if (sinceDttm === untileDttm) {
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
sinceGrain,
|
||||
sinceGrainValue: parseInt(sinceGrainValue, 10),
|
||||
untilGrain,
|
||||
untilGrainValue: parseInt(untilGrainValue, 10),
|
||||
anchorValue: sinceDttm,
|
||||
sinceMode: 'relative',
|
||||
untilMode: 'relative',
|
||||
anchorMode: sinceDttm === 'now' ? 'now' : 'specific',
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
customRange: defaultCustomRange,
|
||||
matchedFlag: false,
|
||||
};
|
||||
};
|
||||
|
||||
const customTimeRangeEncode = (customRange: CustomRangeType): string => {
|
||||
const {
|
||||
sinceDatetime,
|
||||
sinceMode,
|
||||
sinceGrain,
|
||||
sinceGrainValue,
|
||||
untilDatetime,
|
||||
untilMode,
|
||||
untilGrain,
|
||||
untilGrainValue,
|
||||
anchorValue,
|
||||
} = { ...customRange };
|
||||
// specific : specific
|
||||
if (SPECIFIC_MODE.includes(sinceMode) && SPECIFIC_MODE.includes(untilMode)) {
|
||||
const since = sinceMode === 'specific' ? sinceDatetime : sinceMode;
|
||||
const until = untilMode === 'specific' ? untilDatetime : untilMode;
|
||||
return `${since} : ${until}`;
|
||||
}
|
||||
|
||||
// specific : relative
|
||||
if (SPECIFIC_MODE.includes(sinceMode) && untilMode === 'relative') {
|
||||
const since = sinceMode === 'specific' ? sinceDatetime : sinceMode;
|
||||
const until = `DATEADD(DATETIME("${since}"), ${untilGrainValue}, ${untilGrain})`;
|
||||
return `${since} : ${until}`;
|
||||
}
|
||||
|
||||
// relative : specific
|
||||
if (sinceMode === 'relative' && SPECIFIC_MODE.includes(untilMode)) {
|
||||
const until = untilMode === 'specific' ? untilDatetime : untilMode;
|
||||
const since = `DATEADD(DATETIME("${until}"), ${-Math.abs(sinceGrainValue)}, ${sinceGrain})`; // eslint-disable-line
|
||||
return `${since} : ${until}`;
|
||||
}
|
||||
|
||||
// relative : relative
|
||||
const since = `DATEADD(DATETIME("${anchorValue}"), ${-Math.abs(sinceGrainValue)}, ${sinceGrain})`; // eslint-disable-line
|
||||
const until = `DATEADD(DATETIME("${anchorValue}"), ${untilGrainValue}, ${untilGrain})`;
|
||||
return `${since} : ${until}`;
|
||||
};
|
||||
|
||||
const guessTimeRangeFrame = (timeRange: string): TimeRangeFrameType => {
|
||||
if (COMMON_RANGE_OPTIONS_SET.has(timeRange)) {
|
||||
const guessFrame = (timeRange: string): FrameType => {
|
||||
if (COMMON_RANGE_VALUES_SET.has(timeRange)) {
|
||||
return 'Common';
|
||||
}
|
||||
if (CALENDAR_RANGE_OPTIONS_SET.has(timeRange)) {
|
||||
if (CALENDAR_RANGE_VALUES_SET.has(timeRange)) {
|
||||
return 'Calendar';
|
||||
}
|
||||
if (timeRange === 'No filter') {
|
||||
|
|
@ -286,16 +67,6 @@ const guessTimeRangeFrame = (timeRange: string): TimeRangeFrameType => {
|
|||
return 'Advanced';
|
||||
};
|
||||
|
||||
const dttmToMoment = (dttm: string): Moment => {
|
||||
if (dttm === 'now') {
|
||||
return moment().utc().startOf('second');
|
||||
}
|
||||
if (dttm === 'today') {
|
||||
return moment().utc().startOf('day');
|
||||
}
|
||||
return moment(dttm);
|
||||
};
|
||||
|
||||
const fetchTimeRange = async (
|
||||
timeRange: string,
|
||||
endpoints?: TimeRangeEndpoints,
|
||||
|
|
@ -320,7 +91,9 @@ const fetchTimeRange = async (
|
|||
}
|
||||
};
|
||||
|
||||
const StyledModalContainer = styled.div`
|
||||
const StyledPopover = styled(Popover)``;
|
||||
|
||||
const ContentStyleWrapper = styled.div`
|
||||
.ant-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
|
@ -329,6 +102,10 @@ const StyledModalContainer = styled.div`
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.frame-dropdown {
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.ant-picker {
|
||||
padding: 4px 17px 4px;
|
||||
border-radius: 4px;
|
||||
|
|
@ -361,11 +138,17 @@ const StyledModalContainer = styled.div`
|
|||
line-height: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledValidateBtn = styled.span`
|
||||
.validate-btn {
|
||||
float: left;
|
||||
.control-anchor-to {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.control-anchor-to-datetime {
|
||||
width: 217px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -373,11 +156,9 @@ const IconWrapper = styled.span`
|
|||
svg {
|
||||
margin-right: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
.text {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
.error {
|
||||
color: ${({ theme }) => theme.colors.error.base};
|
||||
|
|
@ -395,30 +176,16 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
|||
const { value = 'Last week', endpoints, onChange } = props;
|
||||
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
|
||||
|
||||
// State used for Modal
|
||||
const [show, setShow] = useState<boolean>(false);
|
||||
const [timeRangeFrame, setTimeRangeFrame] = useState<TimeRangeFrameType>(
|
||||
guessTimeRangeFrame(value),
|
||||
);
|
||||
const [commonRange, setCommonRange] = useState<CommonRangeType>(
|
||||
getDefaultOrCommonRange(value),
|
||||
);
|
||||
const [calendarRange, setCalendarRange] = useState<CalendarRangeType>(
|
||||
getDefaultOrCalendarRange(value),
|
||||
);
|
||||
const [customRange, setCustomRange] = useState<CustomRangeType>(
|
||||
customTimeRangeDecode(value).customRange,
|
||||
);
|
||||
const [advancedRange, setAdvancedRange] = useState<string>(
|
||||
getAdvancedRange(value),
|
||||
);
|
||||
const [frame, setFrame] = useState<FrameType>(guessFrame(value));
|
||||
const [timeRangeValue, setTimeRangeValue] = useState(value);
|
||||
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
|
||||
const [evalTimeRange, setEvalTimeRange] = useState<string>(value);
|
||||
const [evalResponse, setEvalResponse] = useState<string>(value);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTimeRange(value, endpoints).then(({ value, error }) => {
|
||||
if (error) {
|
||||
setEvalTimeRange(error || '');
|
||||
setEvalResponse(error || '');
|
||||
setValidTimeRange(false);
|
||||
} else {
|
||||
setActualTimeRange(value || '');
|
||||
|
|
@ -428,452 +195,136 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
|||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = getCurrentValue();
|
||||
fetchTimeRange(value, endpoints).then(({ value, error }) => {
|
||||
fetchTimeRange(timeRangeValue, endpoints).then(({ value, error }) => {
|
||||
if (error) {
|
||||
setEvalTimeRange(error || '');
|
||||
setEvalResponse(error || '');
|
||||
setValidTimeRange(false);
|
||||
} else {
|
||||
setEvalTimeRange(value || '');
|
||||
setEvalResponse(value || '');
|
||||
setValidTimeRange(true);
|
||||
}
|
||||
});
|
||||
}, [timeRangeFrame, commonRange, calendarRange, customRange]);
|
||||
}, [timeRangeValue]);
|
||||
|
||||
function getCurrentValue(): string {
|
||||
// get current time_range string
|
||||
let value = 'Last week';
|
||||
if (timeRangeFrame === 'Common') {
|
||||
value = commonRange;
|
||||
}
|
||||
if (timeRangeFrame === 'Calendar') {
|
||||
value = calendarRange;
|
||||
}
|
||||
if (timeRangeFrame === 'Custom') {
|
||||
value = customTimeRangeEncode(customRange);
|
||||
}
|
||||
if (timeRangeFrame === 'Advanced') {
|
||||
value = advancedRange;
|
||||
}
|
||||
if (timeRangeFrame === 'No Filter') {
|
||||
value = 'No filter';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getDefaultOrCommonRange(value: any): CommonRangeType {
|
||||
return commonRangeSet.has(value) ? value : 'Last week';
|
||||
}
|
||||
|
||||
function getDefaultOrCalendarRange(value: any): CalendarRangeType {
|
||||
return CalendarRangeSet.has(value) ? value : PreviousCalendarWeek;
|
||||
}
|
||||
|
||||
function getAdvancedRange(value: string): string {
|
||||
if (value.includes(SEPARATOR)) {
|
||||
return value;
|
||||
}
|
||||
if (value.startsWith('Last')) {
|
||||
return [value, ''].join(SEPARATOR);
|
||||
}
|
||||
if (value.startsWith('Next')) {
|
||||
return ['', value].join(SEPARATOR);
|
||||
}
|
||||
return SEPARATOR;
|
||||
}
|
||||
|
||||
function onAdvancedRangeChange(control: 'since' | 'until', value: string) {
|
||||
setValidTimeRange(false);
|
||||
setEvalTimeRange(t('Need to verify the time range.'));
|
||||
const [since, until] = advancedRange.split(SEPARATOR);
|
||||
if (control === 'since') {
|
||||
setAdvancedRange(`${value}${SEPARATOR}${until}`);
|
||||
} else {
|
||||
setAdvancedRange(`${since}${SEPARATOR}${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onCustomRangeChange(
|
||||
control: CustomRangeKey,
|
||||
value: string | number,
|
||||
) {
|
||||
setCustomRange({
|
||||
...customRange,
|
||||
[control]: value,
|
||||
});
|
||||
}
|
||||
|
||||
function onCustomRangeChangeAnchorMode(option: any) {
|
||||
const radioValue = option.target.value;
|
||||
if (radioValue === 'now') {
|
||||
setCustomRange({
|
||||
...customRange,
|
||||
anchorValue: 'now',
|
||||
anchorMode: radioValue,
|
||||
});
|
||||
} else {
|
||||
setCustomRange({
|
||||
...customRange,
|
||||
anchorValue: DEFAULT_UNTIL,
|
||||
anchorMode: radioValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showValidateBtn(): boolean {
|
||||
return timeRangeFrame === 'Advanced';
|
||||
}
|
||||
|
||||
function resetState(value: string) {
|
||||
setTimeRangeFrame(guessTimeRangeFrame(value));
|
||||
setCommonRange(getDefaultOrCommonRange(value));
|
||||
setCalendarRange(getDefaultOrCalendarRange(value));
|
||||
setCustomRange(customTimeRangeDecode(value).customRange);
|
||||
setAdvancedRange(getAdvancedRange(value));
|
||||
function onSave() {
|
||||
onChange(timeRangeValue);
|
||||
setShow(false);
|
||||
}
|
||||
|
||||
function onSave() {
|
||||
const currentValue = getCurrentValue();
|
||||
onChange(currentValue);
|
||||
resetState(currentValue);
|
||||
}
|
||||
|
||||
function onHide() {
|
||||
resetState(value);
|
||||
setFrame(guessFrame(value));
|
||||
setTimeRangeValue(value);
|
||||
setShow(false);
|
||||
}
|
||||
|
||||
function onValidate() {
|
||||
const value = getCurrentValue();
|
||||
fetchTimeRange(value, endpoints).then(({ value, error }) => {
|
||||
if (error) {
|
||||
setEvalTimeRange(error || '');
|
||||
setValidTimeRange(false);
|
||||
} else {
|
||||
setEvalTimeRange(value || '');
|
||||
setValidTimeRange(true);
|
||||
}
|
||||
});
|
||||
const togglePopover = () => {
|
||||
if (show) {
|
||||
onHide();
|
||||
} else {
|
||||
setShow(true);
|
||||
}
|
||||
};
|
||||
|
||||
function onFrame(option: SelectOptionType) {
|
||||
if (option.value === 'No Filter') {
|
||||
setTimeRangeValue('No filter');
|
||||
}
|
||||
setFrame(option.value as FrameType);
|
||||
}
|
||||
|
||||
function renderCommon() {
|
||||
const commonRangeValue =
|
||||
COMMON_RANGE_OPTIONS.find(({ value }) => value === commonRange)?.value ||
|
||||
'Last week';
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">
|
||||
{t('Configure Time Range: Last...')}
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={commonRangeValue}
|
||||
onChange={(e: any) => setCommonRange(e.target.value)}
|
||||
>
|
||||
{COMMON_RANGE_OPTIONS.map(({ value, label }) => (
|
||||
<Radio key={value} value={value} className="vertical-radio">
|
||||
{label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
const currentValue =
|
||||
CALENDAR_RANGE_OPTIONS.find(({ value }) => value === calendarRange)
|
||||
?.value || PreviousCalendarWeek;
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">
|
||||
{t('Configure Time Range: Previous...')}
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={currentValue}
|
||||
onChange={(e: any) => setCalendarRange(e.target.value)}
|
||||
>
|
||||
{CALENDAR_RANGE_OPTIONS.map(({ value, label }) => (
|
||||
<Radio key={value} value={value} className="vertical-radio">
|
||||
{label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAdvanced() {
|
||||
const [since, until] = advancedRange.split(SEPARATOR);
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">
|
||||
{t('Configure Advanced Time Range')}
|
||||
</div>
|
||||
<div className="control-label">{t('START')}</div>
|
||||
<Input
|
||||
key="since"
|
||||
value={since}
|
||||
onChange={e => onAdvancedRangeChange('since', e.target.value)}
|
||||
/>
|
||||
<div className="control-label">{t('END')}</div>
|
||||
<Input
|
||||
key="until"
|
||||
value={until}
|
||||
onChange={e => onAdvancedRangeChange('until', e.target.value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCustom() {
|
||||
const {
|
||||
sinceDatetime,
|
||||
sinceMode,
|
||||
sinceGrain,
|
||||
sinceGrainValue,
|
||||
untilDatetime,
|
||||
untilMode,
|
||||
untilGrain,
|
||||
untilGrainValue,
|
||||
anchorValue,
|
||||
anchorMode,
|
||||
} = { ...customRange };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">{t('Configure Custom Time Range')}</div>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<div className="control-label">{t('START')}</div>
|
||||
<Select
|
||||
options={SINCE_MODE_OPTIONS}
|
||||
value={SINCE_MODE_OPTIONS.filter(
|
||||
option => option.value === sinceMode,
|
||||
)}
|
||||
onChange={(option: any) =>
|
||||
onCustomRangeChange('sinceMode', option.value)
|
||||
}
|
||||
const overlayConetent = (
|
||||
<ContentStyleWrapper>
|
||||
<div className="control-label">{t('RANGE TYPE')}</div>
|
||||
<Select
|
||||
options={FRAME_OPTIONS}
|
||||
value={FRAME_OPTIONS.filter(({ value }) => value === frame)}
|
||||
onChange={onFrame}
|
||||
className="frame-dropdown"
|
||||
/>
|
||||
{frame !== 'No Filter' && <Divider />}
|
||||
{frame === 'Common' && (
|
||||
<CommonFrame value={timeRangeValue} onChange={setTimeRangeValue} />
|
||||
)}
|
||||
{frame === 'Calendar' && (
|
||||
<CalendarFrame value={timeRangeValue} onChange={setTimeRangeValue} />
|
||||
)}
|
||||
{frame === 'Advanced' && (
|
||||
<AdvancedFrame value={timeRangeValue} onChange={setTimeRangeValue} />
|
||||
)}
|
||||
{frame === 'Custom' && (
|
||||
<CustomFrame value={timeRangeValue} onChange={setTimeRangeValue} />
|
||||
)}
|
||||
{frame === 'No Filter' && <div data-test="no-filter" />}
|
||||
<Divider />
|
||||
<div>
|
||||
<div className="section-title">{t('Actual Time Range')}</div>
|
||||
{validTimeRange && <div>{evalResponse}</div>}
|
||||
{!validTimeRange && (
|
||||
<IconWrapper className="warning">
|
||||
<Icon
|
||||
name="error-solid-small"
|
||||
color={supersetTheme.colors.error.base}
|
||||
/>
|
||||
{sinceMode === 'specific' && (
|
||||
<Row>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={dttmToMoment(sinceDatetime)}
|
||||
onChange={(datetime: Moment) =>
|
||||
onCustomRangeChange(
|
||||
'sinceDatetime',
|
||||
datetime.format(MOMENT_FORMAT),
|
||||
)
|
||||
}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{sinceMode === 'relative' && (
|
||||
<Row gutter={8}>
|
||||
<Col span={10}>
|
||||
{/* Make sure sinceGrainValue looks like a positive integer */}
|
||||
<InputNumber
|
||||
placeholder={t('Relative quantity')}
|
||||
value={Math.abs(sinceGrainValue)}
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
onStep={value =>
|
||||
onCustomRangeChange('sinceGrainValue', value || 1)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<Select
|
||||
options={SINCE_GRAIN_OPTIONS}
|
||||
value={SINCE_GRAIN_OPTIONS.filter(
|
||||
option => option.value === sinceGrain,
|
||||
)}
|
||||
onChange={(option: any) =>
|
||||
onCustomRangeChange('sinceGrain', option.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="control-label">{t('END')}</div>
|
||||
<Select
|
||||
options={UNTIL_MODE_OPTIONS}
|
||||
value={UNTIL_MODE_OPTIONS.filter(
|
||||
option => option.value === untilMode,
|
||||
)}
|
||||
onChange={(option: any) =>
|
||||
onCustomRangeChange('untilMode', option.value)
|
||||
}
|
||||
/>
|
||||
{untilMode === 'specific' && (
|
||||
<Row>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={dttmToMoment(untilDatetime)}
|
||||
onChange={(datetime: Moment) =>
|
||||
onCustomRangeChange(
|
||||
'untilDatetime',
|
||||
datetime.format(MOMENT_FORMAT),
|
||||
)
|
||||
}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{untilMode === 'relative' && (
|
||||
<Row gutter={8}>
|
||||
<Col span={10}>
|
||||
<InputNumber
|
||||
placeholder={t('Relative quantity')}
|
||||
value={untilGrainValue}
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
onStep={value =>
|
||||
onCustomRangeChange('untilGrainValue', value || 1)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<Select
|
||||
options={UNTIL_GRAIN_OPTIONS}
|
||||
value={UNTIL_GRAIN_OPTIONS.filter(
|
||||
option => option.value === untilGrain,
|
||||
)}
|
||||
onChange={(option: any) =>
|
||||
onCustomRangeChange('untilGrain', option.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{sinceMode === 'relative' && untilMode === 'relative' && (
|
||||
<>
|
||||
<div className="control-label">{t('ANCHOR RELATIVE TO')}</div>
|
||||
<Row align="middle">
|
||||
<Col>
|
||||
<Radio.Group
|
||||
onChange={onCustomRangeChangeAnchorMode}
|
||||
defaultValue="now"
|
||||
value={anchorMode}
|
||||
>
|
||||
<Radio key="now" value="now">
|
||||
{t('NOW')}
|
||||
</Radio>
|
||||
<Radio key="specific" value="specific">
|
||||
{t('Date/Time')}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
{anchorMode !== 'now' && (
|
||||
<Col>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={dttmToMoment(anchorValue)}
|
||||
onChange={(datetime: Moment) =>
|
||||
onCustomRangeChange(
|
||||
'anchorValue',
|
||||
datetime.format(MOMENT_FORMAT),
|
||||
)
|
||||
}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
<span className="text error">{evalResponse}</span>
|
||||
</IconWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="footer">
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
cta
|
||||
key="cancel"
|
||||
onClick={onHide}
|
||||
data-test="cancel-button"
|
||||
>
|
||||
{t('CANCEL')}
|
||||
</Button>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
cta
|
||||
disabled={!validTimeRange}
|
||||
key="apply"
|
||||
onClick={onSave}
|
||||
>
|
||||
{t('APPLY')}
|
||||
</Button>
|
||||
</div>
|
||||
</ContentStyleWrapper>
|
||||
);
|
||||
|
||||
const title = (
|
||||
<IconWrapper>
|
||||
<Icon name="edit-alt" />
|
||||
<span className="text">{t('Edit Time Range')}</span>
|
||||
</IconWrapper>
|
||||
);
|
||||
|
||||
const overlayStyle = {
|
||||
width: '600px',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ControlHeader {...props} />
|
||||
<Label
|
||||
className="pointer"
|
||||
data-test="time-range-trigger"
|
||||
onClick={() => setShow(true)}
|
||||
<StyledPopover
|
||||
placement="right"
|
||||
trigger="click"
|
||||
content={overlayConetent}
|
||||
title={title}
|
||||
defaultVisible={show}
|
||||
visible={show}
|
||||
onVisibleChange={togglePopover}
|
||||
overlayStyle={overlayStyle}
|
||||
>
|
||||
{actualTimeRange}
|
||||
</Label>
|
||||
<Modal
|
||||
name="time-range" // data-test=time-range-modal
|
||||
title={
|
||||
<IconWrapper>
|
||||
<Icon name="edit-alt" />
|
||||
<span className="text">{t('Edit Time Range')}</span>
|
||||
</IconWrapper>
|
||||
}
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
footer={[
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
cta
|
||||
key="cancel"
|
||||
onClick={onHide}
|
||||
data-test="modal-cancel-button"
|
||||
>
|
||||
{t('CANCEL')}
|
||||
</Button>,
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
cta
|
||||
disabled={!validTimeRange}
|
||||
key="apply"
|
||||
onClick={onSave}
|
||||
>
|
||||
{t('APPLY')}
|
||||
</Button>,
|
||||
showValidateBtn() && (
|
||||
<StyledValidateBtn key="validate">
|
||||
<Button
|
||||
buttonStyle="tertiary"
|
||||
cta
|
||||
className="validate-btn"
|
||||
onClick={onValidate}
|
||||
>
|
||||
{t('Validate')}
|
||||
</Button>
|
||||
</StyledValidateBtn>
|
||||
),
|
||||
]}
|
||||
>
|
||||
<StyledModalContainer>
|
||||
<div className="control-label">{t('RANGE TYPE')}</div>
|
||||
<Select
|
||||
options={RANGE_FRAME_OPTIONS}
|
||||
value={RANGE_FRAME_OPTIONS.filter(
|
||||
({ value }) => value === timeRangeFrame,
|
||||
)}
|
||||
onChange={(_: any) => setTimeRangeFrame(_.value)}
|
||||
/>
|
||||
{timeRangeFrame !== 'No Filter' && <Divider />}
|
||||
{timeRangeFrame === 'Common' && renderCommon()}
|
||||
{timeRangeFrame === 'Calendar' && renderCalendar()}
|
||||
{timeRangeFrame === 'Advanced' && renderAdvanced()}
|
||||
{timeRangeFrame === 'Custom' && renderCustom()}
|
||||
<Divider />
|
||||
<div>
|
||||
<div className="section-title">{t('Actual Time Range')}</div>
|
||||
{validTimeRange && <div>{evalTimeRange}</div>}
|
||||
{!validTimeRange && (
|
||||
<IconWrapper className="warning">
|
||||
<Icon
|
||||
name="error-solid-small"
|
||||
color={supersetTheme.colors.error.base}
|
||||
/>
|
||||
<span className="text error">{evalTimeRange}</span>
|
||||
</IconWrapper>
|
||||
)}
|
||||
</div>
|
||||
</StyledModalContainer>
|
||||
</Modal>
|
||||
<Label
|
||||
className="pointer"
|
||||
data-test="time-range-trigger"
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
{actualTimeRange}
|
||||
</Label>
|
||||
</StyledPopover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,15 +16,18 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import { t } from '@superset-ui/core';
|
||||
import {
|
||||
SelectOptionType,
|
||||
PreviousCalendarWeek,
|
||||
PreviousCalendarMonth,
|
||||
PreviousCalendarYear,
|
||||
CommonRangeType,
|
||||
CalendarRangeType,
|
||||
} from './types';
|
||||
|
||||
export const RANGE_FRAME_OPTIONS: SelectOptionType[] = [
|
||||
export const FRAME_OPTIONS: SelectOptionType[] = [
|
||||
{ value: 'Common', label: t('Last') },
|
||||
{ value: 'Calendar', label: t('Previous') },
|
||||
{ value: 'Custom', label: t('Custom') },
|
||||
|
|
@ -33,18 +36,24 @@ export const RANGE_FRAME_OPTIONS: SelectOptionType[] = [
|
|||
];
|
||||
|
||||
export const COMMON_RANGE_OPTIONS: SelectOptionType[] = [
|
||||
{ value: 'Last day', label: t('Last day') },
|
||||
{ value: 'Last week', label: t('Last week') },
|
||||
{ value: 'Last month', label: t('Last month') },
|
||||
{ value: 'Last quarter', label: t('Last quarter') },
|
||||
{ value: 'Last year', label: t('Last year') },
|
||||
{ value: 'Last day', label: t('last day') },
|
||||
{ value: 'Last week', label: t('last week') },
|
||||
{ value: 'Last month', label: t('last month') },
|
||||
{ value: 'Last quarter', label: t('last quarter') },
|
||||
{ value: 'Last year', label: t('last year') },
|
||||
];
|
||||
export const COMMON_RANGE_VALUES_SET = new Set(
|
||||
COMMON_RANGE_OPTIONS.map(({ value }) => value),
|
||||
);
|
||||
|
||||
export const CALENDAR_RANGE_OPTIONS: SelectOptionType[] = [
|
||||
{ value: PreviousCalendarWeek, label: t('Previous Calendar week') },
|
||||
{ value: PreviousCalendarMonth, label: t('Previous Calendar month') },
|
||||
{ value: PreviousCalendarYear, label: t('Previous Calendar year') },
|
||||
{ value: PreviousCalendarWeek, label: t('previous calendar week') },
|
||||
{ value: PreviousCalendarMonth, label: t('previous calendar month') },
|
||||
{ value: PreviousCalendarYear, label: t('previous calendar year') },
|
||||
];
|
||||
export const CALENDAR_RANGE_VALUES_SET = new Set(
|
||||
CALENDAR_RANGE_OPTIONS.map(({ value }) => value),
|
||||
);
|
||||
|
||||
const GRAIN_OPTIONS = [
|
||||
{ value: 'second', label: (rel: string) => `${t('Seconds')} ${rel}` },
|
||||
|
|
@ -53,6 +62,7 @@ const GRAIN_OPTIONS = [
|
|||
{ value: 'day', label: (rel: string) => `${t('Days')} ${rel}` },
|
||||
{ value: 'week', label: (rel: string) => `${t('Weeks')} ${rel}` },
|
||||
{ value: 'month', label: (rel: string) => `${t('Months')} ${rel}` },
|
||||
{ value: 'quarter', label: (rel: string) => `${t('Quarters')} ${rel}` },
|
||||
{ value: 'year', label: (rel: string) => `${t('Years')} ${rel}` },
|
||||
];
|
||||
|
||||
|
|
@ -78,3 +88,25 @@ export const SINCE_MODE_OPTIONS: SelectOptionType[] = [
|
|||
];
|
||||
|
||||
export const UNTIL_MODE_OPTIONS: SelectOptionType[] = SINCE_MODE_OPTIONS.slice();
|
||||
|
||||
export const COMMON_RANGE_SET: Set<CommonRangeType> = new Set([
|
||||
'Last day',
|
||||
'Last week',
|
||||
'Last month',
|
||||
'Last quarter',
|
||||
'Last year',
|
||||
]);
|
||||
|
||||
export const CALENDAR_RANGE_SET: Set<CalendarRangeType> = new Set([
|
||||
PreviousCalendarWeek,
|
||||
PreviousCalendarMonth,
|
||||
PreviousCalendarYear,
|
||||
]);
|
||||
|
||||
export const MOMENT_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss';
|
||||
export const SEVEN_DAYS_AGO = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.subtract(7, 'days')
|
||||
.format(MOMENT_FORMAT);
|
||||
export const MIDNIGHT = moment().utc().startOf('day').format(MOMENT_FORMAT);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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 from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { SEPARATOR } from 'src/explore/dateFilterUtils';
|
||||
import { Input } from 'src/common/components';
|
||||
import { FrameComponentProps } from '../types';
|
||||
|
||||
export function AdvancedFrame(props: FrameComponentProps) {
|
||||
const [since, until] = getAdvancedRange(props.value || '').split(SEPARATOR);
|
||||
|
||||
function getAdvancedRange(value: string): string {
|
||||
if (value.includes(SEPARATOR)) {
|
||||
return value;
|
||||
}
|
||||
if (value.startsWith('Last')) {
|
||||
return [value, ''].join(SEPARATOR);
|
||||
}
|
||||
if (value.startsWith('Next')) {
|
||||
return ['', value].join(SEPARATOR);
|
||||
}
|
||||
return SEPARATOR;
|
||||
}
|
||||
|
||||
function onChange(control: 'since' | 'until', value: string) {
|
||||
if (control === 'since') {
|
||||
props.onChange(`${value}${SEPARATOR}${until}`);
|
||||
} else {
|
||||
props.onChange(`${since}${SEPARATOR}${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">{t('Configure Advanced Time Range')}</div>
|
||||
<div className="control-label">{t('START')}</div>
|
||||
<Input
|
||||
key="since"
|
||||
value={since}
|
||||
onChange={e => onChange('since', e.target.value)}
|
||||
/>
|
||||
<div className="control-label">{t('END')}</div>
|
||||
<Input
|
||||
key="until"
|
||||
value={until}
|
||||
onChange={e => onChange('until', e.target.value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* 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 from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Radio } from 'src/common/components';
|
||||
import { CALENDAR_RANGE_OPTIONS, CALENDAR_RANGE_SET } from '../constants';
|
||||
import {
|
||||
CalendarRangeType,
|
||||
PreviousCalendarWeek,
|
||||
FrameComponentProps,
|
||||
} from '../types';
|
||||
|
||||
export function CalendarFrame(props: FrameComponentProps) {
|
||||
let calendarRange = PreviousCalendarWeek;
|
||||
if (CALENDAR_RANGE_SET.has(props.value as CalendarRangeType)) {
|
||||
calendarRange = props.value;
|
||||
} else {
|
||||
props.onChange(calendarRange);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">
|
||||
{t('Configure Time Range: Previous...')}
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={calendarRange}
|
||||
onChange={(e: any) => props.onChange(e.target.value)}
|
||||
>
|
||||
{CALENDAR_RANGE_OPTIONS.map(({ value, label }) => (
|
||||
<Radio key={value} value={value} className="vertical-radio">
|
||||
{label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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 from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Radio } from 'src/common/components';
|
||||
import { COMMON_RANGE_OPTIONS, COMMON_RANGE_SET } from '../constants';
|
||||
import { CommonRangeType, FrameComponentProps } from '../types';
|
||||
|
||||
export function CommonFrame(props: FrameComponentProps) {
|
||||
let commonRange = 'Last week';
|
||||
if (COMMON_RANGE_SET.has(props.value as CommonRangeType)) {
|
||||
commonRange = props.value;
|
||||
} else {
|
||||
props.onChange(commonRange);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">{t('Configure Time Range: Last...')}</div>
|
||||
<Radio.Group
|
||||
value={commonRange}
|
||||
onChange={(e: any) => props.onChange(e.target.value)}
|
||||
>
|
||||
{COMMON_RANGE_OPTIONS.map(({ value, label }) => (
|
||||
<Radio key={value} value={value} className="vertical-radio">
|
||||
{label}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
/**
|
||||
* 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 from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { isInteger } from 'lodash';
|
||||
import {
|
||||
Col,
|
||||
DatePicker,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
} from 'src/common/components';
|
||||
import { Select } from 'src/components/Select';
|
||||
import {
|
||||
SINCE_GRAIN_OPTIONS,
|
||||
SINCE_MODE_OPTIONS,
|
||||
UNTIL_GRAIN_OPTIONS,
|
||||
UNTIL_MODE_OPTIONS,
|
||||
MOMENT_FORMAT,
|
||||
MIDNIGHT,
|
||||
} from '../constants';
|
||||
import { customTimeRangeDecode, customTimeRangeEncode } from '../utils';
|
||||
import {
|
||||
CustomRangeKey,
|
||||
SelectOptionType,
|
||||
FrameComponentProps,
|
||||
} from '../types';
|
||||
|
||||
const dttmToMoment = (dttm: string): Moment => {
|
||||
if (dttm === 'now') {
|
||||
return moment().utc().startOf('second');
|
||||
}
|
||||
if (dttm === 'today') {
|
||||
return moment().utc().startOf('day');
|
||||
}
|
||||
return moment(dttm);
|
||||
};
|
||||
|
||||
export function CustomFrame(props: FrameComponentProps) {
|
||||
const { customRange, matchedFlag } = customTimeRangeDecode(props.value);
|
||||
if (!matchedFlag) {
|
||||
props.onChange(customTimeRangeEncode(customRange));
|
||||
}
|
||||
const {
|
||||
sinceDatetime,
|
||||
sinceMode,
|
||||
sinceGrain,
|
||||
sinceGrainValue,
|
||||
untilDatetime,
|
||||
untilMode,
|
||||
untilGrain,
|
||||
untilGrainValue,
|
||||
anchorValue,
|
||||
anchorMode,
|
||||
} = { ...customRange };
|
||||
|
||||
function onChange(control: CustomRangeKey, value: string) {
|
||||
props.onChange(
|
||||
customTimeRangeEncode({
|
||||
...customRange,
|
||||
[control]: value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function onGrainValue(
|
||||
control: 'sinceGrainValue' | 'untilGrainValue',
|
||||
value: string | number,
|
||||
) {
|
||||
// only positive values in grainValue controls
|
||||
if (isInteger(value) && value > 0) {
|
||||
props.onChange(
|
||||
customTimeRangeEncode({
|
||||
...customRange,
|
||||
[control]: value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onAnchorMode(option: any) {
|
||||
const radioValue = option.target.value;
|
||||
if (radioValue === 'now') {
|
||||
props.onChange(
|
||||
customTimeRangeEncode({
|
||||
...customRange,
|
||||
anchorValue: 'now',
|
||||
anchorMode: radioValue,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
props.onChange(
|
||||
customTimeRangeEncode({
|
||||
...customRange,
|
||||
anchorValue: MIDNIGHT,
|
||||
anchorMode: radioValue,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-test="custom-frame">
|
||||
<div className="section-title">{t('Configure Custom Time Range')}</div>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<div className="control-label">{t('START')}</div>
|
||||
<Select
|
||||
options={SINCE_MODE_OPTIONS}
|
||||
value={SINCE_MODE_OPTIONS.filter(
|
||||
option => option.value === sinceMode,
|
||||
)}
|
||||
onChange={(option: SelectOptionType) =>
|
||||
onChange('sinceMode', option.value)
|
||||
}
|
||||
/>
|
||||
{sinceMode === 'specific' && (
|
||||
<Row>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={dttmToMoment(sinceDatetime)}
|
||||
onChange={(datetime: Moment) =>
|
||||
onChange('sinceDatetime', datetime.format(MOMENT_FORMAT))
|
||||
}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{sinceMode === 'relative' && (
|
||||
<Row gutter={8}>
|
||||
<Col span={11}>
|
||||
{/* Make sure sinceGrainValue looks like a positive integer */}
|
||||
<InputNumber
|
||||
placeholder={t('Relative quantity')}
|
||||
value={Math.abs(sinceGrainValue)}
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
onChange={value =>
|
||||
onGrainValue('sinceGrainValue', value || 1)
|
||||
}
|
||||
onStep={value => onGrainValue('sinceGrainValue', value || 1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={13}>
|
||||
<Select
|
||||
options={SINCE_GRAIN_OPTIONS}
|
||||
value={SINCE_GRAIN_OPTIONS.filter(
|
||||
option => option.value === sinceGrain,
|
||||
)}
|
||||
onChange={(option: SelectOptionType) =>
|
||||
onChange('sinceGrain', option.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="control-label">{t('END')}</div>
|
||||
<Select
|
||||
options={UNTIL_MODE_OPTIONS}
|
||||
value={UNTIL_MODE_OPTIONS.filter(
|
||||
option => option.value === untilMode,
|
||||
)}
|
||||
onChange={(option: SelectOptionType) =>
|
||||
onChange('untilMode', option.value)
|
||||
}
|
||||
/>
|
||||
{untilMode === 'specific' && (
|
||||
<Row>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={dttmToMoment(untilDatetime)}
|
||||
onChange={(datetime: Moment) =>
|
||||
onChange('untilDatetime', datetime.format(MOMENT_FORMAT))
|
||||
}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{untilMode === 'relative' && (
|
||||
<Row gutter={8}>
|
||||
<Col span={11}>
|
||||
<InputNumber
|
||||
placeholder={t('Relative quantity')}
|
||||
value={untilGrainValue}
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
onChange={value =>
|
||||
onGrainValue('untilGrainValue', value || 1)
|
||||
}
|
||||
onStep={value => onGrainValue('untilGrainValue', value || 1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={13}>
|
||||
<Select
|
||||
options={UNTIL_GRAIN_OPTIONS}
|
||||
value={UNTIL_GRAIN_OPTIONS.filter(
|
||||
option => option.value === untilGrain,
|
||||
)}
|
||||
onChange={(option: SelectOptionType) =>
|
||||
onChange('untilGrain', option.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{sinceMode === 'relative' && untilMode === 'relative' && (
|
||||
<div className="control-anchor-to">
|
||||
<div className="control-label">{t('ANCHOR TO')}</div>
|
||||
<Row align="middle">
|
||||
<Col>
|
||||
<Radio.Group
|
||||
onChange={onAnchorMode}
|
||||
defaultValue="now"
|
||||
value={anchorMode}
|
||||
>
|
||||
<Radio key="now" value="now">
|
||||
{t('NOW')}
|
||||
</Radio>
|
||||
<Radio key="specific" value="specific">
|
||||
{t('Date/Time')}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
{anchorMode !== 'now' && (
|
||||
<Col>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={dttmToMoment(anchorValue)}
|
||||
onChange={(datetime: Moment) =>
|
||||
onChange('anchorValue', datetime.format(MOMENT_FORMAT))
|
||||
}
|
||||
allowClear={false}
|
||||
className="control-anchor-to-datetime"
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export { CommonFrame } from './CommonFrame';
|
||||
export { CalendarFrame } from './CalendarFrame';
|
||||
export { CustomFrame } from './CustomFrame';
|
||||
export { AdvancedFrame } from './AdvancedFrame';
|
||||
|
|
@ -21,7 +21,7 @@ export type SelectOptionType = {
|
|||
label: string;
|
||||
};
|
||||
|
||||
export type TimeRangeFrameType =
|
||||
export type FrameType =
|
||||
| 'Common'
|
||||
| 'Calendar'
|
||||
| 'Custom'
|
||||
|
|
@ -35,6 +35,7 @@ export type DateTimeGrainType =
|
|||
| 'day'
|
||||
| 'week'
|
||||
| 'month'
|
||||
| 'quarter'
|
||||
| 'year';
|
||||
|
||||
export type CustomRangeKey =
|
||||
|
|
@ -49,14 +50,16 @@ export type CustomRangeKey =
|
|||
| 'anchorMode'
|
||||
| 'anchorValue';
|
||||
|
||||
export type DateTimeModeType = 'specific' | 'relative' | 'now' | 'today';
|
||||
|
||||
export type CustomRangeType = {
|
||||
sinceMode: string;
|
||||
sinceMode: DateTimeModeType;
|
||||
sinceDatetime: string;
|
||||
sinceGrain: string;
|
||||
sinceGrain: DateTimeGrainType;
|
||||
sinceGrainValue: number;
|
||||
untilMode: string;
|
||||
untilMode: 'specific' | 'relative' | 'now' | 'today';
|
||||
untilDatetime: string;
|
||||
untilGrain: string;
|
||||
untilGrain: DateTimeGrainType;
|
||||
untilGrainValue: number;
|
||||
anchorMode: 'now' | 'specific';
|
||||
anchorValue: string;
|
||||
|
|
@ -84,3 +87,8 @@ export type CalendarRangeType =
|
|||
| typeof PreviousCalendarWeek
|
||||
| typeof PreviousCalendarMonth
|
||||
| typeof PreviousCalendarYear;
|
||||
|
||||
export type FrameComponentProps = {
|
||||
onChange: (timeRange: string) => void;
|
||||
value: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* 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 { SEPARATOR } from 'src/explore/dateFilterUtils';
|
||||
import {
|
||||
CustomRangeDecodeType,
|
||||
CustomRangeType,
|
||||
DateTimeGrainType,
|
||||
DateTimeModeType,
|
||||
} from './types';
|
||||
import { SEVEN_DAYS_AGO, MIDNIGHT } from './constants';
|
||||
|
||||
/**
|
||||
* RegExp to test a string for a full ISO 8601 Date
|
||||
* Does not do any sort of date validation, only checks if the string is according to the ISO 8601 spec.
|
||||
* YYYY-MM-DDThh:mm:ss
|
||||
* YYYY-MM-DDThh:mm:ssTZD
|
||||
* YYYY-MM-DDThh:mm:ss.sTZD
|
||||
* @see: https://www.w3.org/TR/NOTE-datetime
|
||||
*/
|
||||
const iso8601 = String.raw`\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|Z)?`;
|
||||
const datetimeConstant = String.raw`TODAY|NOW`;
|
||||
const grainValue = String.raw`[+-]?[1-9][0-9]*`;
|
||||
const grain = String.raw`YEAR|QUARTER|MONTH|WEEK|DAY|HOUR|MINUTE|SECOND`;
|
||||
const CUSTOM_RANGE_EXPRESSION = RegExp(
|
||||
String.raw`^DATEADD\(DATETIME\("(${iso8601}|${datetimeConstant})"\),\s(${grainValue}),\s(${grain})\)$`,
|
||||
'i',
|
||||
);
|
||||
export const ISO8601_AND_CONSTANT = RegExp(
|
||||
String.raw`^${iso8601}$|^${datetimeConstant}$`,
|
||||
'i',
|
||||
);
|
||||
const DATETIME_CONSTANT = ['now', 'today'];
|
||||
const defaultCustomRange: CustomRangeType = {
|
||||
sinceDatetime: SEVEN_DAYS_AGO,
|
||||
sinceMode: 'relative',
|
||||
sinceGrain: 'day',
|
||||
sinceGrainValue: -7,
|
||||
untilDatetime: MIDNIGHT,
|
||||
untilMode: 'specific',
|
||||
untilGrain: 'day',
|
||||
untilGrainValue: 7,
|
||||
anchorMode: 'now',
|
||||
anchorValue: 'now',
|
||||
};
|
||||
const SPECIFIC_MODE = ['specific', 'today', 'now'];
|
||||
|
||||
export const customTimeRangeDecode = (
|
||||
timeRange: string,
|
||||
): CustomRangeDecodeType => {
|
||||
const splitDateRange = timeRange.split(SEPARATOR);
|
||||
|
||||
if (splitDateRange.length === 2) {
|
||||
const [since, until] = splitDateRange;
|
||||
|
||||
// specific : specific
|
||||
if (ISO8601_AND_CONSTANT.test(since) && ISO8601_AND_CONSTANT.test(until)) {
|
||||
const sinceMode = (DATETIME_CONSTANT.includes(since)
|
||||
? since
|
||||
: 'specific') as DateTimeModeType;
|
||||
const untilMode = (DATETIME_CONSTANT.includes(until)
|
||||
? until
|
||||
: 'specific') as DateTimeModeType;
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
sinceDatetime: since,
|
||||
untilDatetime: until,
|
||||
sinceMode,
|
||||
untilMode,
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
|
||||
// relative : specific
|
||||
const sinceCapturedGroup = since.match(CUSTOM_RANGE_EXPRESSION);
|
||||
if (
|
||||
sinceCapturedGroup &&
|
||||
ISO8601_AND_CONSTANT.test(until) &&
|
||||
since.includes(until)
|
||||
) {
|
||||
const [dttm, grainValue, grain] = sinceCapturedGroup.slice(1);
|
||||
const untilMode = (DATETIME_CONSTANT.includes(until)
|
||||
? until
|
||||
: 'specific') as DateTimeModeType;
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
sinceGrain: grain as DateTimeGrainType,
|
||||
sinceGrainValue: parseInt(grainValue, 10),
|
||||
untilDatetime: dttm,
|
||||
sinceMode: 'relative',
|
||||
untilMode,
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
|
||||
// specific : relative
|
||||
const untilCapturedGroup = until.match(CUSTOM_RANGE_EXPRESSION);
|
||||
if (
|
||||
ISO8601_AND_CONSTANT.test(since) &&
|
||||
untilCapturedGroup &&
|
||||
until.includes(since)
|
||||
) {
|
||||
const [dttm, grainValue, grain] = [...untilCapturedGroup.slice(1)];
|
||||
const sinceMode = (DATETIME_CONSTANT.includes(since)
|
||||
? since
|
||||
: 'specific') as DateTimeModeType;
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
untilGrain: grain as DateTimeGrainType,
|
||||
untilGrainValue: parseInt(grainValue, 10),
|
||||
sinceDatetime: dttm,
|
||||
untilMode: 'relative',
|
||||
sinceMode,
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
|
||||
// relative : relative
|
||||
if (sinceCapturedGroup && untilCapturedGroup) {
|
||||
const [sinceDttm, sinceGrainValue, sinceGrain] = [
|
||||
...sinceCapturedGroup.slice(1),
|
||||
];
|
||||
const [untileDttm, untilGrainValue, untilGrain] = [
|
||||
...untilCapturedGroup.slice(1),
|
||||
];
|
||||
if (sinceDttm === untileDttm) {
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
sinceGrain: sinceGrain as DateTimeGrainType,
|
||||
sinceGrainValue: parseInt(sinceGrainValue, 10),
|
||||
untilGrain: untilGrain as DateTimeGrainType,
|
||||
untilGrainValue: parseInt(untilGrainValue, 10),
|
||||
anchorValue: sinceDttm,
|
||||
sinceMode: 'relative',
|
||||
untilMode: 'relative',
|
||||
anchorMode: sinceDttm === 'now' ? 'now' : 'specific',
|
||||
},
|
||||
matchedFlag: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
customRange: defaultCustomRange,
|
||||
matchedFlag: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const customTimeRangeEncode = (customRange: CustomRangeType): string => {
|
||||
const {
|
||||
sinceDatetime,
|
||||
sinceMode,
|
||||
sinceGrain,
|
||||
sinceGrainValue,
|
||||
untilDatetime,
|
||||
untilMode,
|
||||
untilGrain,
|
||||
untilGrainValue,
|
||||
anchorValue,
|
||||
} = { ...customRange };
|
||||
// specific : specific
|
||||
if (SPECIFIC_MODE.includes(sinceMode) && SPECIFIC_MODE.includes(untilMode)) {
|
||||
const since = sinceMode === 'specific' ? sinceDatetime : sinceMode;
|
||||
const until = untilMode === 'specific' ? untilDatetime : untilMode;
|
||||
return `${since} : ${until}`;
|
||||
}
|
||||
|
||||
// specific : relative
|
||||
if (SPECIFIC_MODE.includes(sinceMode) && untilMode === 'relative') {
|
||||
const since = sinceMode === 'specific' ? sinceDatetime : sinceMode;
|
||||
const until = `DATEADD(DATETIME("${since}"), ${untilGrainValue}, ${untilGrain})`;
|
||||
return `${since} : ${until}`;
|
||||
}
|
||||
|
||||
// relative : specific
|
||||
if (sinceMode === 'relative' && SPECIFIC_MODE.includes(untilMode)) {
|
||||
const until = untilMode === 'specific' ? untilDatetime : untilMode;
|
||||
const since = `DATEADD(DATETIME("${until}"), ${-Math.abs(sinceGrainValue)}, ${sinceGrain})`; // eslint-disable-line
|
||||
return `${since} : ${until}`;
|
||||
}
|
||||
|
||||
// relative : relative
|
||||
const since = `DATEADD(DATETIME("${anchorValue}"), ${-Math.abs(sinceGrainValue)}, ${sinceGrain})`; // eslint-disable-line
|
||||
const until = `DATEADD(DATETIME("${anchorValue}"), ${untilGrainValue}, ${untilGrain})`;
|
||||
return `${since} : ${until}`;
|
||||
};
|
||||
Loading…
Reference in New Issue