mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Refactor: move datemath to grafana/ui (#16890)
* move datemath to grafana/ui * don't reference @grafana/ui from its own component
This commit is contained in:
committed by
Torkel Ödegaard
parent
513c79d392
commit
d881976c9d
@@ -59,6 +59,7 @@
|
||||
"@types/react-custom-scrollbars": "4.0.5",
|
||||
"@types/react-test-renderer": "16.8.1",
|
||||
"@types/react-transition-group": "2.0.16",
|
||||
"@types/sinon": "^7.0.11",
|
||||
"@types/storybook__addon-actions": "3.4.2",
|
||||
"@types/storybook__addon-info": "4.1.1",
|
||||
"@types/storybook__addon-knobs": "4.0.4",
|
||||
@@ -77,6 +78,7 @@
|
||||
"rollup-plugin-terser": "4.0.4",
|
||||
"rollup-plugin-typescript2": "0.19.3",
|
||||
"rollup-plugin-visualizer": "0.9.2",
|
||||
"sinon": "1.17.6",
|
||||
"typescript": "3.4.1"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import moment from 'moment';
|
||||
import { TimeRange, TimeOptions, TimeOption, SelectOptionItem } from '@grafana/ui';
|
||||
import { ButtonSelect } from '@grafana/ui/src/components/Select/ButtonSelect';
|
||||
import { ButtonSelect } from '../Select/ButtonSelect';
|
||||
import { mapTimeOptionToTimeRange, mapTimeRangeToRangeString } from './time';
|
||||
import { Props as TimePickerPopoverProps } from './TimePickerPopover';
|
||||
import { TimePickerOptionGroup } from './TimePickerOptionGroup';
|
||||
import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
|
||||
import { Timezone } from '../../../../../public/app/core/utils/datemath';
|
||||
import { PopperContent } from '../Tooltip/PopperController';
|
||||
import { Timezone } from '../../utils/datemath';
|
||||
|
||||
export interface Props {
|
||||
value: TimeRange;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Calendar from 'react-calendar/dist/entry.nostyle';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { TimeFragment } from '@grafana/ui';
|
||||
import { Timezone } from '../../../../../public/app/core/utils/datemath';
|
||||
import { TimeFragment } from '../../types/time';
|
||||
import { Timezone } from '../../utils/datemath';
|
||||
|
||||
import { stringToMoment } from './time';
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import moment from 'moment';
|
||||
import { TimeFragment, TIME_FORMAT, Input } from '@grafana/ui';
|
||||
import { TimeFragment, TIME_FORMAT } from '../../types/time';
|
||||
|
||||
import { stringToMoment, isValidTimeString } from './time';
|
||||
import { Input } from '../Input/Input';
|
||||
|
||||
export interface Props {
|
||||
value: TimeFragment;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { Component, SyntheticEvent } from 'react';
|
||||
import { TimeRange, TimeOptions, TimeOption } from '@grafana/ui';
|
||||
import { TimeRange, TimeOptions, TimeOption } from '../../types/time';
|
||||
import { Moment } from 'moment';
|
||||
|
||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||
import { TimePickerInput } from './TimePickerInput';
|
||||
import { mapTimeOptionToTimeRange } from './time';
|
||||
import { Timezone } from '../../../../../public/app/core/utils/datemath';
|
||||
import { Timezone } from '../../utils/datemath';
|
||||
|
||||
export interface Props {
|
||||
value: TimeRange;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import moment, { Moment } from 'moment';
|
||||
import { TimeOption, TimeRange, TIME_FORMAT } from '@grafana/ui';
|
||||
|
||||
import * as dateMath from '../../../../../public/app/core/utils/datemath';
|
||||
import { describeTimeRange } from '../../../../../public/app/core/utils/rangeutil';
|
||||
import * as dateMath from '@grafana/ui/src/utils/datemath';
|
||||
import { describeTimeRange } from '@grafana/ui/src/utils/rangeutil';
|
||||
|
||||
export const mapTimeOptionToTimeRange = (
|
||||
timeOption: TimeOption,
|
||||
|
||||
133
packages/grafana-ui/src/utils/datemath.test.ts
Normal file
133
packages/grafana-ui/src/utils/datemath.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import sinon, { SinonFakeTimers } from 'sinon';
|
||||
|
||||
import * as dateMath from './datemath';
|
||||
import moment, { Moment, unitOfTime } from 'moment';
|
||||
import each from 'lodash/each';
|
||||
|
||||
describe('DateMath', () => {
|
||||
const spans: unitOfTime.Base[] = ['s', 'm', 'h', 'd', 'w', 'M', 'y'];
|
||||
const anchor = '2014-01-01T06:06:06.666Z';
|
||||
const unix = moment(anchor).valueOf();
|
||||
const format = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
|
||||
let clock: SinonFakeTimers;
|
||||
|
||||
describe('errors', () => {
|
||||
it('should return undefined if passed empty string', () => {
|
||||
expect(dateMath.parse('')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if I pass an operator besides [+-/]', () => {
|
||||
expect(dateMath.parse('now&1d')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if I pass a unit besides' + spans.toString(), () => {
|
||||
expect(dateMath.parse('now+5f')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if rounding unit is not 1', () => {
|
||||
expect(dateMath.parse('now/2y')).toBe(undefined);
|
||||
expect(dateMath.parse('now/0.5y')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not go into an infinite loop when missing a unit', () => {
|
||||
expect(dateMath.parse('now-0')).toBe(undefined);
|
||||
expect(dateMath.parse('now-00')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('now/d should set to start of current day', () => {
|
||||
const expected = new Date();
|
||||
expected.setHours(0);
|
||||
expected.setMinutes(0);
|
||||
expected.setSeconds(0);
|
||||
expected.setMilliseconds(0);
|
||||
|
||||
const startOfDay = dateMath.parse('now/d', false)!.valueOf();
|
||||
expect(startOfDay).toBe(expected.getTime());
|
||||
});
|
||||
|
||||
it('now/d on a utc dashboard should be start of the current day in UTC time', () => {
|
||||
const today = new Date();
|
||||
const expected = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0));
|
||||
|
||||
const startOfDay = dateMath.parse('now/d', false, 'utc')!.valueOf();
|
||||
expect(startOfDay).toBe(expected.getTime());
|
||||
});
|
||||
|
||||
describe('subtraction', () => {
|
||||
let now: Moment;
|
||||
let anchored: Moment;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers(unix);
|
||||
now = moment();
|
||||
anchored = moment(anchor);
|
||||
});
|
||||
|
||||
each(spans, span => {
|
||||
const nowEx = 'now-5' + span;
|
||||
const thenEx = anchor + '||-5' + span;
|
||||
|
||||
it('should return 5' + span + ' ago', () => {
|
||||
expect(dateMath.parse(nowEx)!.format(format)).toEqual(now.subtract(5, span).format(format));
|
||||
});
|
||||
|
||||
it('should return 5' + span + ' before ' + anchor, () => {
|
||||
expect(dateMath.parse(thenEx)!.format(format)).toEqual(anchored.subtract(5, span).format(format));
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('rounding', () => {
|
||||
let now: Moment;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers(unix);
|
||||
now = moment();
|
||||
});
|
||||
|
||||
each(spans, span => {
|
||||
it('should round now to the beginning of the ' + span, () => {
|
||||
expect(dateMath.parse('now/' + span)!.format(format)).toEqual(now.startOf(span).format(format));
|
||||
});
|
||||
|
||||
it('should round now to the end of the ' + span, () => {
|
||||
expect(dateMath.parse('now/' + span, true)!.format(format)).toEqual(now.endOf(span).format(format));
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValid', () => {
|
||||
it('should return false when invalid date text', () => {
|
||||
expect(dateMath.isValid('asd')).toBe(false);
|
||||
});
|
||||
it('should return true when valid date text', () => {
|
||||
expect(dateMath.isValid('now-1h')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('relative time to date parsing', () => {
|
||||
it('should handle negative time', () => {
|
||||
const date = dateMath.parseDateMath('-2d', moment([2014, 1, 5]));
|
||||
expect(date!.valueOf()).toEqual(moment([2014, 1, 3]).valueOf());
|
||||
});
|
||||
|
||||
it('should handle multiple math expressions', () => {
|
||||
const date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5]));
|
||||
expect(date!.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf());
|
||||
});
|
||||
|
||||
it('should return false when invalid expression', () => {
|
||||
const date = dateMath.parseDateMath('2', moment([2014, 1, 5]));
|
||||
expect(date).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
154
packages/grafana-ui/src/utils/datemath.ts
Normal file
154
packages/grafana-ui/src/utils/datemath.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import includes from 'lodash/includes';
|
||||
import isDate from 'lodash/isDate';
|
||||
import moment, { unitOfTime } from 'moment';
|
||||
|
||||
const units: unitOfTime.Base[] = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
||||
|
||||
export type Timezone = 'utc';
|
||||
|
||||
/**
|
||||
* Parses different types input to a moment instance. There is a specific formatting language that can be used
|
||||
* if text arg is string. See unit tests for examples.
|
||||
* @param text
|
||||
* @param roundUp See parseDateMath function.
|
||||
* @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used.
|
||||
*/
|
||||
export function parse(
|
||||
text: string | moment.Moment | Date,
|
||||
roundUp?: boolean,
|
||||
timezone?: Timezone
|
||||
): moment.Moment | undefined {
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof text !== 'string') {
|
||||
if (moment.isMoment(text)) {
|
||||
return text;
|
||||
}
|
||||
if (isDate(text)) {
|
||||
return moment(text);
|
||||
}
|
||||
// We got some non string which is not a moment nor Date. TS should be able to check for that but not always.
|
||||
return undefined;
|
||||
} else {
|
||||
let time;
|
||||
let mathString = '';
|
||||
let index;
|
||||
let parseString;
|
||||
|
||||
if (text.substring(0, 3) === 'now') {
|
||||
if (timezone === 'utc') {
|
||||
time = moment.utc();
|
||||
} else {
|
||||
time = moment();
|
||||
}
|
||||
mathString = text.substring('now'.length);
|
||||
} else {
|
||||
index = text.indexOf('||');
|
||||
if (index === -1) {
|
||||
parseString = text;
|
||||
mathString = ''; // nothing else
|
||||
} else {
|
||||
parseString = text.substring(0, index);
|
||||
mathString = text.substring(index + 2);
|
||||
}
|
||||
// We're going to just require ISO8601 timestamps, k?
|
||||
time = moment(parseString, moment.ISO_8601);
|
||||
}
|
||||
|
||||
if (!mathString.length) {
|
||||
return time;
|
||||
}
|
||||
|
||||
return parseDateMath(mathString, time, roundUp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text is a valid date which in this context means that it is either a Moment instance or it can be parsed
|
||||
* by parse function. See parse function to see what is considered acceptable.
|
||||
* @param text
|
||||
*/
|
||||
export function isValid(text: string | moment.Moment): boolean {
|
||||
const date = parse(text);
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (moment.isMoment(date)) {
|
||||
return date.isValid();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses math part of the time string and shifts supplied time according to that math. See unit tests for examples.
|
||||
* @param mathString
|
||||
* @param time
|
||||
* @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit.
|
||||
*/
|
||||
// TODO: Had to revert Andrejs `time: moment.Moment` to `time: any`
|
||||
export function parseDateMath(mathString: string, time: any, roundUp?: boolean): moment.Moment | undefined {
|
||||
const dateTime = time;
|
||||
let i = 0;
|
||||
const len = mathString.length;
|
||||
|
||||
while (i < len) {
|
||||
const c = mathString.charAt(i++);
|
||||
let type;
|
||||
let num;
|
||||
let unit;
|
||||
|
||||
if (c === '/') {
|
||||
type = 0;
|
||||
} else if (c === '+') {
|
||||
type = 1;
|
||||
} else if (c === '-') {
|
||||
type = 2;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isNaN(parseInt(mathString.charAt(i), 10))) {
|
||||
num = 1;
|
||||
} else if (mathString.length === 2) {
|
||||
num = mathString.charAt(i);
|
||||
} else {
|
||||
const numFrom = i;
|
||||
while (!isNaN(parseInt(mathString.charAt(i), 10))) {
|
||||
i++;
|
||||
if (i > 10) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
num = parseInt(mathString.substring(numFrom, i), 10);
|
||||
}
|
||||
|
||||
if (type === 0) {
|
||||
// rounding is only allowed on whole, single, units (eg M or 1M, not 0.5M or 2M)
|
||||
if (num !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
unit = mathString.charAt(i++);
|
||||
|
||||
if (!includes(units, unit)) {
|
||||
return undefined;
|
||||
} else {
|
||||
if (type === 0) {
|
||||
if (roundUp) {
|
||||
dateTime.endOf(unit);
|
||||
} else {
|
||||
dateTime.startOf(unit);
|
||||
}
|
||||
} else if (type === 1) {
|
||||
dateTime.add(num, unit);
|
||||
} else if (type === 2) {
|
||||
dateTime.subtract(num, unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
@@ -16,3 +16,6 @@ export * from './validate';
|
||||
export { getFlotPairs } from './flotPairs';
|
||||
export * from './object';
|
||||
export * from './fieldCache';
|
||||
|
||||
// Names are too general to export
|
||||
// rangeutils, datemath
|
||||
|
||||
171
packages/grafana-ui/src/utils/rangeutil.ts
Normal file
171
packages/grafana-ui/src/utils/rangeutil.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// @ts-ignore
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
|
||||
import * as dateMath from './datemath';
|
||||
|
||||
const spans: { [key: string]: { display: string; section?: number } } = {
|
||||
s: { display: 'second' },
|
||||
m: { display: 'minute' },
|
||||
h: { display: 'hour' },
|
||||
d: { display: 'day' },
|
||||
w: { display: 'week' },
|
||||
M: { display: 'month' },
|
||||
y: { display: 'year' },
|
||||
};
|
||||
|
||||
const rangeOptions = [
|
||||
{ from: 'now/d', to: 'now/d', display: 'Today', section: 2 },
|
||||
{ from: 'now/d', to: 'now', display: 'Today so far', section: 2 },
|
||||
{ from: 'now/w', to: 'now/w', display: 'This week', section: 2 },
|
||||
{ from: 'now/w', to: 'now', display: 'This week so far', section: 2 },
|
||||
{ from: 'now/M', to: 'now/M', display: 'This month', section: 2 },
|
||||
{ from: 'now/M', to: 'now', display: 'This month so far', section: 2 },
|
||||
{ from: 'now/y', to: 'now/y', display: 'This year', section: 2 },
|
||||
{ from: 'now/y', to: 'now', display: 'This year so far', section: 2 },
|
||||
|
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 1 },
|
||||
{
|
||||
from: 'now-2d/d',
|
||||
to: 'now-2d/d',
|
||||
display: 'Day before yesterday',
|
||||
section: 1,
|
||||
},
|
||||
{
|
||||
from: 'now-7d/d',
|
||||
to: 'now-7d/d',
|
||||
display: 'This day last week',
|
||||
section: 1,
|
||||
},
|
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 1 },
|
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 1 },
|
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 1 },
|
||||
|
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
|
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
|
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
|
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 },
|
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 },
|
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
|
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
|
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
|
||||
|
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days', section: 0 },
|
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 0 },
|
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 0 },
|
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days', section: 0 },
|
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months', section: 0 },
|
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 0 },
|
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 0 },
|
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
|
||||
];
|
||||
|
||||
const absoluteFormat = 'MMM D, YYYY HH:mm:ss';
|
||||
|
||||
const rangeIndex: any = {};
|
||||
_.each(rangeOptions, (frame: any) => {
|
||||
rangeIndex[frame.from + ' to ' + frame.to] = frame;
|
||||
});
|
||||
|
||||
export function getRelativeTimesList(timepickerSettings: any, currentDisplay: any) {
|
||||
const groups = _.groupBy(rangeOptions, (option: any) => {
|
||||
option.active = option.display === currentDisplay;
|
||||
return option.section;
|
||||
});
|
||||
|
||||
// _.each(timepickerSettings.time_options, (duration: string) => {
|
||||
// let info = describeTextRange(duration);
|
||||
// if (info.section) {
|
||||
// groups[info.section].push(info);
|
||||
// }
|
||||
// });
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function formatDate(date: any) {
|
||||
return date.format(absoluteFormat);
|
||||
}
|
||||
|
||||
// handles expressions like
|
||||
// 5m
|
||||
// 5m to now/d
|
||||
// now/d to now
|
||||
// now/d
|
||||
// if no to <expr> then to now is assumed
|
||||
export function describeTextRange(expr: any) {
|
||||
const isLast = expr.indexOf('+') !== 0;
|
||||
if (expr.indexOf('now') === -1) {
|
||||
expr = (isLast ? 'now-' : 'now') + expr;
|
||||
}
|
||||
|
||||
let opt = rangeIndex[expr + ' to now'];
|
||||
if (opt) {
|
||||
return opt;
|
||||
}
|
||||
|
||||
if (isLast) {
|
||||
opt = { from: expr, to: 'now' };
|
||||
} else {
|
||||
opt = { from: 'now', to: expr };
|
||||
}
|
||||
|
||||
const parts = /^now([-+])(\d+)(\w)/.exec(expr);
|
||||
if (parts) {
|
||||
const unit = parts[3];
|
||||
const amount = parseInt(parts[2], 10);
|
||||
const span = spans[unit];
|
||||
if (span) {
|
||||
opt.display = isLast ? 'Last ' : 'Next ';
|
||||
opt.display += amount + ' ' + span.display;
|
||||
opt.section = span.section;
|
||||
if (amount > 1) {
|
||||
opt.display += 's';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
opt.display = opt.from + ' to ' + opt.to;
|
||||
opt.invalid = true;
|
||||
}
|
||||
|
||||
return opt;
|
||||
}
|
||||
|
||||
export function describeTimeRange(range: RawTimeRange): string {
|
||||
const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()];
|
||||
if (option) {
|
||||
return option.display;
|
||||
}
|
||||
|
||||
if (moment.isMoment(range.from) && moment.isMoment(range.to)) {
|
||||
return formatDate(range.from) + ' to ' + formatDate(range.to);
|
||||
}
|
||||
|
||||
if (moment.isMoment(range.from)) {
|
||||
const toMoment = dateMath.parse(range.to, true);
|
||||
return toMoment ? formatDate(range.from) + ' to ' + toMoment.fromNow() : '';
|
||||
}
|
||||
|
||||
if (moment.isMoment(range.to)) {
|
||||
const from = dateMath.parse(range.from, false);
|
||||
return from ? from.fromNow() + ' to ' + formatDate(range.to) : '';
|
||||
}
|
||||
|
||||
if (range.to.toString() === 'now') {
|
||||
const res = describeTextRange(range.from);
|
||||
return res.display;
|
||||
}
|
||||
|
||||
return range.from.toString() + ' to ' + range.to.toString();
|
||||
}
|
||||
|
||||
export const isValidTimeSpan = (value: string) => {
|
||||
if (value.indexOf('$') === 0 || value.indexOf('+$') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const info = describeTextRange(value);
|
||||
return info.invalid !== true;
|
||||
};
|
||||
Reference in New Issue
Block a user