From 1a0c1a39e4974220890f6a8d003e545847c15863 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 27 Apr 2020 15:28:06 +0200 Subject: [PATCH] DateTime: adding support to select preferred timezone for presentation of date and time values. (#23586) * added moment timezone package. * added a qnd way of selecting timezone. * added a first draft to display how it can be used. * fixed failing tests. * made moment.local to be in utc when running tests. * added tests to verify that the timeZone support works as expected. * Fixed so we use the formatter in the graph context menu. * changed so we will format d3 according to timeZone. * changed from class base to function based for easier consumption. * fixed so tests got green. * renamed to make it shorter. * fixed formatting in logRow. * removed unused value. * added time formatter to flot. * fixed failing tests. * changed so history will use the formatting with support for timezone. * added todo. * added so we append the correct abbrivation behind time. * added time zone abbrevation in timepicker. * adding timezone in rangeutil tool. * will use timezone when formatting range. * changed so we use new functions to format date so timezone is respected. * wip - dashboard settings. * changed so the time picker settings is in react. * added force update. * wip to get the react graph to work. * fixed formatting and parsing on the timepicker. * updated snap to be correct. * fixed so we format values properly in time picker. * make sure we pass timezone on all the proper places. * fixed so we use correct timeZone in explore. * fixed failing tests. * fixed so we always parse from local to selected timezone. * removed unused variable. * reverted back. * trying to fix issue with directive. * fixed issue. * fixed strict null errors. * fixed so we still can select default. * make sure we reads the time zone from getTimezone --- package.json | 2 + packages/grafana-data/src/datetime/common.ts | 17 ++ .../src/datetime/formatter.test.ts | 76 +++++++++ .../grafana-data/src/datetime/formatter.ts | 54 +++++++ packages/grafana-data/src/datetime/index.ts | 3 + packages/grafana-data/src/datetime/parser.ts | 57 +++++++ .../grafana-data/src/datetime/rangeutil.ts | 41 +++-- .../grafana-data/src/field/fieldDisplay.ts | 8 +- .../grafana-data/src/field/fieldOverrides.ts | 3 + .../src/field/getFieldDisplayValuesProxy.ts | 4 +- .../src/valueFormats/dateTimeFormatters.ts | 13 +- .../src/components/Chart/Tooltip.tsx | 4 +- .../grafana-ui/src/components/Graph/Graph.tsx | 50 +----- .../src/components/Graph/GraphContextMenu.tsx | 24 ++- .../Graph/GraphTooltip/GraphTooltip.tsx | 12 +- .../GraphTooltip/MultiModeGraphTooltip.tsx | 4 +- .../Graph/GraphTooltip/SeriesTable.tsx | 1 + .../GraphTooltip/SingleModeGraphTooltip.tsx | 8 +- .../components/Graph/GraphTooltip/types.ts | 3 +- .../grafana-ui/src/components/Graph/utils.ts | 44 ++++- .../grafana-ui/src/components/Logs/LogRow.tsx | 22 ++- .../TimePickerContent/TimePickerCalendar.tsx | 47 +++--- .../TimePickerContent/TimeRangeForm.tsx | 46 ++++-- .../TimePickerContent/TimeRangeList.tsx | 2 +- .../TimePickerContent.test.tsx.snap | 16 +- .../TimePicker/TimePickerContent/mapper.ts | 85 +--------- .../components/TimePicker/TimeRangePicker.tsx | 34 ++-- .../src/components/TimePicker/time.ts | 42 ----- packages/grafana-ui/src/components/index.ts | 1 + public/app/app.ts | 2 + public/app/core/angular_wrappers.ts | 10 +- .../SharedPreferences/SharedPreferences.tsx | 33 ++-- .../TimePicker/TimePickerWithHistory.tsx | 6 +- public/app/core/logs_model.ts | 11 +- public/app/core/utils/explore.ts | 7 +- public/app/core/utils/richHistory.ts | 6 +- public/app/core/utils/ticks.ts | 12 +- .../app/features/admin/AdminEditUserCtrl.ts | 6 +- .../app/features/admin/UserLdapSyncInfo.tsx | 9 +- public/app/features/admin/UserSyncInfo.tsx | 9 +- .../app/features/admin/ldap/LdapSyncInfo.tsx | 8 +- public/app/features/admin/state/actions.ts | 6 +- public/app/features/api-keys/ApiKeysPage.tsx | 9 +- .../DashboardSettings/SettingsCtrl.ts | 18 ++- .../DashboardSettings/TimePickerSettings.ts | 82 ---------- .../DashboardSettings/TimePickerSettings.tsx | 151 ++++++++++++++++++ .../DashboardSettings/template.html | 2 +- .../components/Inspector/PanelInspector.tsx | 8 +- .../VersionHistory/HistoryListCtrl.test.ts | 2 + .../VersionHistory/HistoryListCtrl.ts | 6 +- .../dashboard/state/DashboardModel.ts | 26 ++- .../dashboard/state/PanelQueryRunner.ts | 5 + .../features/explore/ExploreGraphPanel.tsx | 11 +- public/app/features/explore/LiveLogs.tsx | 14 +- public/app/features/explore/state/actions.ts | 4 +- .../explore/utils/ResultProcessor.test.ts | 25 +-- .../features/explore/utils/ResultProcessor.ts | 1 + .../components/ImportDashboardOverview.tsx | 4 +- .../app/features/panel/metrics_panel_ctrl.ts | 2 +- public/app/features/profile/ProfileCtrl.ts | 6 +- .../plugins/panel/bargauge/BarGaugePanel.tsx | 3 +- public/app/plugins/panel/gauge/GaugePanel.tsx | 3 +- public/app/plugins/panel/graph/graph.ts | 33 +--- public/app/plugins/panel/graph/module.ts | 13 +- .../plugins/panel/graph/specs/graph.test.ts | 4 +- public/app/plugins/panel/graph/template.ts | 2 +- .../panel/graph2/getGraphSeriesModel.ts | 3 +- public/app/plugins/panel/heatmap/rendering.ts | 14 +- public/app/plugins/panel/news/NewsPanel.tsx | 4 +- .../plugins/panel/piechart/PieChartPanel.tsx | 3 +- public/app/plugins/panel/singlestat/module.ts | 1 + public/app/plugins/panel/stat/StatPanel.tsx | 3 +- public/app/plugins/panel/table-old/module.ts | 2 +- .../app/plugins/panel/table-old/renderer.ts | 19 ++- .../panel/table-old/specs/renderer.test.ts | 10 +- public/vendor/flot/jquery.flot.time.js | 14 +- yarn.lock | 42 +++-- 77 files changed, 821 insertions(+), 576 deletions(-) create mode 100644 packages/grafana-data/src/datetime/common.ts create mode 100644 packages/grafana-data/src/datetime/formatter.test.ts create mode 100644 packages/grafana-data/src/datetime/formatter.ts create mode 100644 packages/grafana-data/src/datetime/parser.ts delete mode 100644 packages/grafana-ui/src/components/TimePicker/time.ts delete mode 100644 public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.ts create mode 100644 public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx diff --git a/package.json b/package.json index a94533be48f..b412701a08b 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@types/lodash": "4.14.149", "@types/lru-cache": "^5.1.0", "@types/marked": "0.6.5", + "@types/moment-timezone": "0.5.13", "@types/mousetrap": "1.6.3", "@types/node": "13.7.0", "@types/papaparse": "4.5.9", @@ -241,6 +242,7 @@ "md5": "^2.2.1", "memoize-one": "5.1.1", "moment": "2.24.0", + "moment-timezone": "0.5.28", "mousetrap": "1.6.5", "mousetrap-global-bind": "1.1.0", "nodemon": "2.0.2", diff --git a/packages/grafana-data/src/datetime/common.ts b/packages/grafana-data/src/datetime/common.ts new file mode 100644 index 00000000000..2c7625dddab --- /dev/null +++ b/packages/grafana-data/src/datetime/common.ts @@ -0,0 +1,17 @@ +import { TimeZone, DefaultTimeZone } from '../types/time'; + +export interface DateTimeOptions { + timeZone?: TimeZone; +} + +export type TimeZoneResolver = () => TimeZone | undefined; + +let defaultTimeZoneResolver: TimeZoneResolver = () => DefaultTimeZone; + +export const setTimeZoneResolver = (resolver: TimeZoneResolver) => { + defaultTimeZoneResolver = resolver ?? defaultTimeZoneResolver; +}; + +export const getTimeZone = (options?: T): TimeZone => { + return options?.timeZone ?? defaultTimeZoneResolver() ?? DefaultTimeZone; +}; diff --git a/packages/grafana-data/src/datetime/formatter.test.ts b/packages/grafana-data/src/datetime/formatter.test.ts new file mode 100644 index 00000000000..a97a6d534d0 --- /dev/null +++ b/packages/grafana-data/src/datetime/formatter.test.ts @@ -0,0 +1,76 @@ +import { dateTimeFormat } from './formatter'; + +describe('dateTimeFormat', () => { + describe('when no time zone have been set', () => { + const browserTime = dateTimeFormat(1587126975779, { timeZone: 'browser' }); + + it('should format with default formatting in browser/local time zone', () => { + expect(dateTimeFormat(1587126975779)).toBe(browserTime); + }); + }); + + describe('when invalid time zone have been set', () => { + const browserTime = dateTimeFormat(1587126975779, { timeZone: 'browser' }); + const options = { timeZone: 'asdf123' }; + + it('should format with default formatting in browser/local time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe(browserTime); + }); + }); + + describe('when UTC time zone have been set', () => { + const options = { timeZone: 'utc' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 12:36:15'); + }); + }); + + describe('when Europe/Stockholm time zone have been set', () => { + const options = { timeZone: 'Europe/Stockholm' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 14:36:15'); + }); + }); + + describe('when Australia/Perth time zone have been set', () => { + const options = { timeZone: 'Australia/Perth' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 20:36:15'); + }); + }); + + describe('when Asia/Yakutsk time zone have been set', () => { + const options = { timeZone: 'Asia/Yakutsk' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 21:36:15'); + }); + }); + + describe('when America/Panama time zone have been set', () => { + const options = { timeZone: 'America/Panama' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 07:36:15'); + }); + }); + + describe('when America/Los_Angeles time zone have been set', () => { + const options = { timeZone: 'America/Los_Angeles' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 05:36:15'); + }); + }); + + describe('when Africa/Djibouti time zone have been set', () => { + const options = { timeZone: 'Africa/Djibouti' }; + + it('should format with default formatting in correct time zone', () => { + expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17 15:36:15'); + }); + }); +}); diff --git a/packages/grafana-data/src/datetime/formatter.ts b/packages/grafana-data/src/datetime/formatter.ts new file mode 100644 index 00000000000..5d0034e0af7 --- /dev/null +++ b/packages/grafana-data/src/datetime/formatter.ts @@ -0,0 +1,54 @@ +/* eslint-disable id-blacklist, no-restricted-imports, @typescript-eslint/ban-types */ +import moment, { MomentInput, Moment } from 'moment-timezone'; +import { TimeZone } from '../types'; +import { DateTimeInput } from './moment_wrapper'; +import { DEFAULT_DATE_TIME_FORMAT, MS_DATE_TIME_FORMAT } from './formats'; +import { DateTimeOptions, getTimeZone } from './common'; + +export interface DateTimeOptionsWithFormat extends DateTimeOptions { + format?: string; + defaultWithMS?: boolean; +} + +export type DateTimeFormatter = ( + dateInUtc: DateTimeInput, + options?: T +) => string; + +export const dateTimeFormat: DateTimeFormatter = (dateInUtc, options?) => + toTz(dateInUtc, getTimeZone(options)).format(getFormat(options)); + +export const dateTimeFormatISO: DateTimeFormatter = (dateInUtc, options?) => + toTz(dateInUtc, getTimeZone(options)).format(); + +export const dateTimeFormatTimeAgo: DateTimeFormatter = (dateInUtc, options?) => + toTz(dateInUtc, getTimeZone(options)).fromNow(); + +export const dateTimeFormatWithAbbrevation: DateTimeFormatter = (dateInUtc, options?) => + toTz(dateInUtc, getTimeZone(options)).format(`${DEFAULT_DATE_TIME_FORMAT} z`); + +export const timeZoneAbbrevation: DateTimeFormatter = (dateInUtc, options?) => + toTz(dateInUtc, getTimeZone(options)).format('z'); + +const getFormat = (options?: T): string => { + if (options?.defaultWithMS) { + return options?.format ?? MS_DATE_TIME_FORMAT; + } + return options?.format ?? DEFAULT_DATE_TIME_FORMAT; +}; + +const toTz = (dateInUtc: DateTimeInput, timeZone: TimeZone): Moment => { + const date = dateInUtc as MomentInput; + const zone = moment.tz.zone(timeZone); + + if (zone && zone.name) { + return moment.utc(date).tz(zone.name); + } + + switch (timeZone) { + case 'utc': + return moment.utc(date); + default: + return moment.utc(date).local(); + } +}; diff --git a/packages/grafana-data/src/datetime/index.ts b/packages/grafana-data/src/datetime/index.ts index 51b32326f58..1e38fc76260 100644 --- a/packages/grafana-data/src/datetime/index.ts +++ b/packages/grafana-data/src/datetime/index.ts @@ -4,4 +4,7 @@ import * as rangeUtil from './rangeutil'; export * from './moment_wrapper'; export * from './timezones'; export * from './formats'; +export * from './formatter'; +export * from './parser'; export { dateMath, rangeUtil }; +export { DateTimeOptions, setTimeZoneResolver, TimeZoneResolver } from './common'; diff --git a/packages/grafana-data/src/datetime/parser.ts b/packages/grafana-data/src/datetime/parser.ts new file mode 100644 index 00000000000..3d50bf971ac --- /dev/null +++ b/packages/grafana-data/src/datetime/parser.ts @@ -0,0 +1,57 @@ +/* eslint-disable id-blacklist, no-restricted-imports, @typescript-eslint/ban-types */ +import moment, { MomentInput } from 'moment-timezone'; +import { DateTimeInput, DateTime, isDateTime } from './moment_wrapper'; +import { DateTimeOptions, getTimeZone } from './common'; +import { parse, isValid } from './datemath'; +import { lowerCase } from 'lodash'; + +export interface DateTimeOptionsWhenParsing extends DateTimeOptions { + roundUp?: boolean; +} + +export type DateTimeParser = ( + value: DateTimeInput, + options?: T +) => DateTime; + +export const dateTimeParse: DateTimeParser = (value, options?): DateTime => { + if (isDateTime(value)) { + return value; + } + + if (typeof value === 'string') { + return parseString(value, options); + } + + return parseOthers(value, options); +}; + +const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateTime => { + if (value.indexOf('now') !== -1) { + if (!isValid(value)) { + return moment() as DateTime; + } + + const parsed = parse(value, options?.roundUp, options?.timeZone); + return parsed || (moment() as DateTime); + } + + return parseOthers(value, options); +}; + +const parseOthers = (value: DateTimeInput, options?: DateTimeOptionsWhenParsing): DateTime => { + const date = value as MomentInput; + const timeZone = getTimeZone(options); + const zone = moment.tz.zone(timeZone); + + if (zone && zone.name) { + return moment.tz(date, zone.name) as DateTime; + } + + switch (lowerCase(timeZone)) { + case 'utc': + return moment.utc(date) as DateTime; + default: + return moment(date) as DateTime; + } +}; diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts index 30a63350e22..e49af43b4c1 100644 --- a/packages/grafana-data/src/datetime/rangeutil.ts +++ b/packages/grafana-data/src/datetime/rangeutil.ts @@ -1,10 +1,12 @@ import each from 'lodash/each'; import groupBy from 'lodash/groupBy'; -import { RawTimeRange } from '../types/time'; +import { RawTimeRange, TimeRange, TimeZone } from '../types/time'; import * as dateMath from './datemath'; -import { isDateTime, DateTime } from './moment_wrapper'; +import { isDateTime } from './moment_wrapper'; +import { timeZoneAbbrevation, dateTimeFormat, dateTimeFormatTimeAgo } from './formatter'; +import { dateTimeParse } from './parser'; const spans: { [key: string]: { display: string; section?: number } } = { s: { display: 'second' }, @@ -61,8 +63,6 @@ const rangeOptions = [ { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 }, ]; -const absoluteFormat = 'YYYY-MM-DD HH:mm:ss'; - const rangeIndex: any = {}; each(rangeOptions, (frame: any) => { rangeIndex[frame.from + ' to ' + frame.to] = frame; @@ -84,10 +84,6 @@ export function getRelativeTimesList(timepickerSettings: any, currentDisplay: an return groups; } -function formatDate(date: DateTime) { - return date.format(absoluteFormat); -} - // handles expressions like // 5m // 5m to now/d @@ -144,24 +140,27 @@ export function describeTextRange(expr: any) { * @param range - a time range (usually specified by the TimePicker) * @alpha */ -export function describeTimeRange(range: RawTimeRange): string { +export function describeTimeRange(range: RawTimeRange, timeZone?: TimeZone): string { const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()]; + if (option) { return option.display; } + const options = { timeZone }; + if (isDateTime(range.from) && isDateTime(range.to)) { - return formatDate(range.from) + ' to ' + formatDate(range.to); + return dateTimeFormat(range.from, options) + ' to ' + dateTimeFormat(range.to, options); } if (isDateTime(range.from)) { - const toMoment = dateMath.parse(range.to, true); - return toMoment ? formatDate(range.from) + ' to ' + toMoment.fromNow() : ''; + const parsed = dateMath.parse(range.to, true, 'utc'); + return parsed ? dateTimeFormat(range.from, options) + ' to ' + dateTimeFormatTimeAgo(parsed, options) : ''; } if (isDateTime(range.to)) { - const from = dateMath.parse(range.from, false); - return from ? from.fromNow() + ' to ' + formatDate(range.to) : ''; + const parsed = dateMath.parse(range.from, false, 'utc'); + return parsed ? dateTimeFormatTimeAgo(parsed, options) + ' to ' + dateTimeFormat(range.to, options) : ''; } if (range.to.toString() === 'now') { @@ -180,3 +179,17 @@ export const isValidTimeSpan = (value: string) => { const info = describeTextRange(value); return info.invalid !== true; }; + +export const describeTimeRangeAbbrevation = (range: TimeRange, timeZone?: TimeZone) => { + if (isDateTime(range.from)) { + return timeZoneAbbrevation(range.from, { timeZone }); + } + const parsed = dateMath.parse(range.from, true); + return parsed ? timeZoneAbbrevation(parsed, { timeZone }) : ''; +}; + +export const convertRawToRange = (raw: RawTimeRange): TimeRange => { + const from = dateTimeParse(raw.from, { roundUp: false }); + const to = dateTimeParse(raw.to, { roundUp: true }); + return { from, to, raw }; +}; diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index daaed05e7d1..8c56db60dbf 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -12,6 +12,7 @@ import { FieldType, InterpolateFunction, LinkModel, + TimeZone, } from '../types'; import { DataFrameView } from '../dataframe/DataFrameView'; import { GraphSeriesValue } from '../types/graph'; @@ -89,12 +90,13 @@ export interface GetFieldDisplayValuesOptions { sparkline?: boolean; // Calculate the sparkline theme: GrafanaTheme; autoMinMax?: boolean; + timeZone?: TimeZone; } export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25; export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => { - const { replaceVariables, reduceOptions, fieldConfig } = options; + const { replaceVariables, reduceOptions, fieldConfig, timeZone } = options; const calcs = reduceOptions.calcs.length ? reduceOptions.calcs : [ReducerID.last]; const values: FieldDisplay[] = []; @@ -127,6 +129,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi getDisplayProcessor({ field, theme: options.theme, + timeZone, }); const title = config.title ? config.title : defaultTitle; @@ -259,7 +262,7 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay { const displayName = 'No data'; - const { fieldConfig } = options; + const { fieldConfig, timeZone } = options; const { defaults } = fieldConfig; const displayProcessor = getDisplayProcessor({ @@ -268,6 +271,7 @@ function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): Fiel config: defaults, }, theme: options.theme, + timeZone, }); const display = displayProcessor(null); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index d6c0b543017..c103e803bc5 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -15,6 +15,7 @@ import { InterpolateFunction, ValueLinkConfig, GrafanaTheme, + TimeZone, } from '../types'; import { fieldMatchers, ReducerID, reduceField } from '../transformations'; import { FieldMatcher } from '../types/transformations'; @@ -193,6 +194,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra // Attach data links supplier f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, { theme: options.theme, + timeZone: options.timeZone, }); return f; @@ -335,6 +337,7 @@ const getLinksSupplier = ( replaceVariables: InterpolateFunction, options: { theme: GrafanaTheme; + timeZone?: TimeZone; } ) => (config: ValueLinkConfig): Array> => { if (!field.config.links || field.config.links.length === 0) { diff --git a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts index bfc70aec755..c5fdf1b05dd 100644 --- a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts +++ b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts @@ -1,5 +1,5 @@ import toNumber from 'lodash/toNumber'; -import { DataFrame, DisplayValue, GrafanaTheme } from '../types'; +import { DataFrame, DisplayValue, GrafanaTheme, TimeZone } from '../types'; import { getDisplayProcessor } from './displayProcessor'; import { formattedValueToString } from '../valueFormats'; @@ -15,6 +15,7 @@ export function getFieldDisplayValuesProxy( rowIndex: number, options: { theme: GrafanaTheme; + timeZone?: TimeZone; } ): Record { return new Proxy({} as Record, { @@ -38,6 +39,7 @@ export function getFieldDisplayValuesProxy( field.display = getDisplayProcessor({ field, theme: options.theme, + timeZone: options.timeZone, }); } const raw = field.values.get(rowIndex); diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts index 361a506449f..12b5cab6bb4 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts @@ -3,6 +3,7 @@ import { toDuration as duration, toUtc, dateTime } from '../datetime/moment_wrap import { toFixed, toFixedScaled, FormattedValue, ValueFormatter } from './valueFormats'; import { DecimalCount } from '../types/displayValue'; import { TimeZone } from '../types'; +import { dateTimeFormat, dateTimeFormatTimeAgo } from '../datetime'; interface IntervalsInSeconds { [interval: string]: number; @@ -326,14 +327,14 @@ export function toClockSeconds(size: number, decimals: DecimalCount): FormattedV export function toDateTimeValueFormatter(pattern: string, todayPattern?: string): ValueFormatter { return (value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, timeZone?: TimeZone): FormattedValue => { - const isUtc = timeZone === 'utc'; - const time = isUtc ? toUtc(value) : dateTime(value); if (todayPattern) { if (dateTime().isSame(value, 'day')) { - return { text: time.format(todayPattern) }; + return { + text: dateTimeFormat(value, { format: todayPattern, timeZone }), + }; } } - return { text: time.format(pattern) }; + return { text: dateTimeFormat(value, { format: pattern, timeZone }) }; }; } @@ -346,7 +347,5 @@ export function dateTimeFromNow( scaledDecimals: DecimalCount, timeZone?: TimeZone ): FormattedValue { - const isUtc = timeZone === 'utc'; - const time = isUtc ? toUtc(value) : dateTime(value); - return { text: time.fromNow() }; + return { text: dateTimeFormatTimeAgo(value, { timeZone }) }; } diff --git a/packages/grafana-ui/src/components/Chart/Tooltip.tsx b/packages/grafana-ui/src/components/Chart/Tooltip.tsx index b75d53e578a..cf4a877390e 100644 --- a/packages/grafana-ui/src/components/Chart/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Chart/Tooltip.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { css } from 'emotion'; import { Portal } from '../Portal/Portal'; -import { Dimensions } from '@grafana/data'; +import { Dimensions, TimeZone } from '@grafana/data'; import { FlotPosition } from '../Graph/types'; import { TooltipContainer } from './TooltipContainer'; @@ -21,7 +21,7 @@ export interface TooltipContentProps { // TODO: type this better, no good idea how yet dimensions: T; // Dimension[] activeDimensions?: ActiveDimensions; - // timeZone: TimeZone; + timeZone?: TimeZone; pos: FlotPosition; mode: TooltipMode; } diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index d34a5ea9df2..8d2b223c575 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -3,21 +3,14 @@ import $ from 'jquery'; import React, { PureComponent } from 'react'; import uniqBy from 'lodash/uniqBy'; // Types -import { - TimeRange, - GraphSeriesXY, - TimeZone, - DefaultTimeZone, - createDimension, - DateTimeInput, - dateTime, -} from '@grafana/data'; +import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data'; import _ from 'lodash'; import { FlotPosition, FlotItem } from './types'; import { TooltipProps, TooltipContentProps, ActiveDimensions, Tooltip } from '../Chart/Tooltip'; import { GraphTooltip } from './GraphTooltip/GraphTooltip'; import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu'; import { GraphDimensions } from './GraphTooltip/types'; +import { graphTimeFormat, graphTimeFormatter } from './utils'; export interface GraphProps { children?: JSX.Element | JSX.Element[]; @@ -126,7 +119,7 @@ export class Graph extends PureComponent { } renderTooltip = () => { - const { children, series } = this.props; + const { children, series, timeZone } = this.props; const { pos, activeItem, isTooltipVisible } = this.state; let tooltipElement: React.ReactElement | null = null; @@ -191,6 +184,7 @@ export class Graph extends PureComponent { activeDimensions, pos, mode: tooltipElementProps.mode || 'single', + timeZone, }; const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps }); @@ -234,10 +228,6 @@ export class Graph extends PureComponent { ), }; - const formatDate = (date: DateTimeInput, format?: string) => { - return dateTime(date)?.format(format); - }; - const closeContext = () => this.setState({ isContextVisible: false }); const getContextMenuSource = () => { @@ -256,7 +246,7 @@ export class Graph extends PureComponent { y: contextPos.pageY, onClose: closeContext, getContextMenuSource: getContextMenuSource, - formatSourceDate: formatDate, + timeZone: this.props.timeZone, dimensions, contextDimensions, }; @@ -330,8 +320,8 @@ export class Graph extends PureComponent { max: max, label: 'Datetime', ticks: ticks, - timeformat: timeFormat(ticks, min, max), - timezone: timeZone ?? DefaultTimeZone, + timeformat: graphTimeFormat(ticks, min, max), + timeFormatter: graphTimeFormatter(timeZone), }, yaxes, grid: { @@ -390,30 +380,4 @@ export class Graph extends PureComponent { } } -// Copied from graph.ts -function timeFormat(ticks: number, min: number, max: number): string { - if (min && max && ticks) { - const range = max - min; - const secPerTick = range / ticks / 1000; - const oneDay = 86400000; - const oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; -} - export default Graph; diff --git a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx index 76df5d60785..0fd029d0fc0 100644 --- a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx @@ -4,14 +4,13 @@ import { ThemeContext } from '../../themes'; import { SeriesIcon } from '../Legend/SeriesIcon'; import { GraphDimensions } from './GraphTooltip/types'; import { - DateTimeInput, FlotDataPoint, getValueFromDimension, getDisplayProcessor, formattedValueToString, Dimensions, - MS_DATE_TIME_FORMAT, - DEFAULT_DATE_TIME_FORMAT, + dateTimeFormat, + TimeZone, } from '@grafana/data'; import { css } from 'emotion'; @@ -19,14 +18,14 @@ export type ContextDimensions = { [key in keyof T]: export type GraphContextMenuProps = ContextMenuProps & { getContextMenuSource: () => FlotDataPoint | null; - formatSourceDate: (date: DateTimeInput, format?: string) => string; + timeZone?: TimeZone; dimensions?: GraphDimensions; contextDimensions?: ContextDimensions; }; export const GraphContextMenu: React.FC = ({ getContextMenuSource, - formatSourceDate, + timeZone, items, dimensions, contextDimensions, @@ -56,11 +55,20 @@ export const GraphContextMenu: React.FC = ({ contextDimensions.yAxis[0], contextDimensions.yAxis[1] ); - const display = source.series.valueField.display ?? getDisplayProcessor({ field: source.series.valueField }); + const display = + source.series.valueField.display ?? + getDisplayProcessor({ + field: source.series.valueField, + timeZone, + }); value = display(valueFromDimensions); } - const timeFormat = source.series.hasMsResolution ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT; + const formattedValue = dateTimeFormat(source.datapoint[0], { + defaultWithMS: source.series.hasMsResolution, + timeZone, + }); + return (
= ({ z-index: ${theme.zIndex.tooltip}; `} > - {formatSourceDate(source.datapoint[0], timeFormat)} + {formattedValue}
> = ({ dimensions, activeDimensions, pos, + timeZone, }) => { // When // [1] no active dimension or @@ -19,9 +20,16 @@ export const GraphTooltip: React.FC> = ({ } if (mode === 'single') { - return ; + return ; } else { - return ; + return ( + + ); } }; diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx index deed5b94f70..40af15f6f6c 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.tsx @@ -8,7 +8,7 @@ import { getValueFromDimension } from '@grafana/data'; export const MultiModeGraphTooltip: React.FC = ({ dimensions, activeDimensions, pos }) => { +}> = ({ dimensions, activeDimensions, pos, timeZone }) => { let activeSeriesIndex: number | null = null; // when no x-axis provided, skip rendering if (activeDimensions.xAxis === null) { @@ -24,7 +24,7 @@ export const MultiModeGraphTooltip: React.FC { diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx index b328be4ad6a..8d691063bf9 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx @@ -67,6 +67,7 @@ interface SeriesTableProps { export const SeriesTable: React.FC = ({ timestamp, series }) => { const theme = useTheme(); const styles = getSeriesTableRowStyles(theme); + return ( <> {timestamp && ( diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx index 7f3450d49c9..4e0c945e0a3 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx @@ -8,7 +8,11 @@ import { import { SeriesTable } from './SeriesTable'; import { GraphTooltipContentProps } from './types'; -export const SingleModeGraphTooltip: React.FC = ({ dimensions, activeDimensions }) => { +export const SingleModeGraphTooltip: React.FC = ({ + dimensions, + activeDimensions, + timeZone, +}) => { // not hovering over a point, skip rendering if ( activeDimensions.yAxis === null || @@ -24,7 +28,7 @@ export const SingleModeGraphTooltip: React.FC = ({ dim const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]); const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]); - const display = valueField.display ?? getDisplayProcessor({ field: valueField }); + const display = valueField.display ?? getDisplayProcessor({ field: valueField, timeZone }); const disp = display(value); return ( diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts b/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts index c0803a7f815..7fea3fa7293 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/types.ts @@ -1,5 +1,5 @@ import { ActiveDimensions, TooltipMode } from '../../Chart/Tooltip'; -import { Dimension, Dimensions } from '@grafana/data'; +import { Dimension, Dimensions, TimeZone } from '@grafana/data'; export interface GraphTooltipOptions { mode: TooltipMode; @@ -13,4 +13,5 @@ export interface GraphDimensions extends Dimensions { export interface GraphTooltipContentProps { dimensions: GraphDimensions; // Dimension[] activeDimensions: ActiveDimensions; + timeZone?: TimeZone; } diff --git a/packages/grafana-ui/src/components/Graph/utils.ts b/packages/grafana-ui/src/components/Graph/utils.ts index 6b4c157a1b9..d1a41174f76 100644 --- a/packages/grafana-ui/src/components/Graph/utils.ts +++ b/packages/grafana-ui/src/components/Graph/utils.ts @@ -1,4 +1,11 @@ -import { GraphSeriesValue, Field, formattedValueToString, getDisplayProcessor } from '@grafana/data'; +import { + GraphSeriesValue, + Field, + formattedValueToString, + getDisplayProcessor, + TimeZone, + dateTimeFormat, +} from '@grafana/data'; /** * Returns index of the closest datapoint BEFORE hover position @@ -48,7 +55,8 @@ export const getMultiSeriesGraphHoverInfo = ( yAxisDimensions: Field[], xAxisDimensions: Field[], /** Well, time basically */ - xAxisPosition: number + xAxisPosition: number, + timeZone?: TimeZone ): { results: MultiSeriesHoverInfo[]; time?: GraphSeriesValue; @@ -75,7 +83,7 @@ export const getMultiSeriesGraphHoverInfo = ( minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime; } - const display = field.display ?? getDisplayProcessor({ field }); + const display = field.display ?? getDisplayProcessor({ field, timeZone }); const disp = display(field.values.get(hoverIndex)); results.push({ @@ -93,3 +101,33 @@ export const getMultiSeriesGraphHoverInfo = ( time: minTime, }; }; + +export const graphTimeFormatter = (timeZone?: TimeZone) => (epoch: number, format: string) => + dateTimeFormat(epoch, { format, timeZone }); + +export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + // Need have 10 millisecond margin on the day range + // As sometimes last 24 hour dashboard evaluates to more than 86400000 + const oneDay = 86400010; + const oneYear = 31536000000; + + if (secPerTick <= 45) { + return 'HH:mm:ss'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return 'HH:mm'; + } + if (secPerTick <= 80000) { + return 'MM/DD HH:mm'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return 'MM/DD'; + } + return 'YYYY-MM'; + } + + return 'HH:mm'; +}; diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 52f10509450..24f1abd05d6 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -1,5 +1,13 @@ import React, { PureComponent } from 'react'; -import { Field, LinkModel, LogRowModel, TimeZone, DataQueryResponse, GrafanaTheme } from '@grafana/data'; +import { + Field, + LinkModel, + LogRowModel, + TimeZone, + DataQueryResponse, + GrafanaTheme, + dateTimeFormat, +} from '@grafana/data'; import { Icon } from '../Icon/Icon'; import { cx, css } from 'emotion'; @@ -134,7 +142,6 @@ class UnThemedLogRow extends PureComponent { const { showDetails, showContext, hasHoverBackground } = this.state; const style = getLogRowStyles(theme, row.logLevel); const styles = getStyles(theme); - const showUtc = timeZone === 'utc'; const hoverBackground = cx(style.logsRow, { [styles.hoverBackground]: hasHoverBackground }); return ( @@ -156,16 +163,7 @@ class UnThemedLogRow extends PureComponent { )} - {showTime && showUtc && ( - - {row.timeUtc} - - )} - {showTime && !showUtc && ( - - {row.timeLocal} - - )} + {showTime && {dateTimeFormat(row.timeEpochMs, { timeZone })}} {showLabels && row.uniqueLabels && ( diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx index 90485723947..0ef60953e15 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx @@ -1,8 +1,7 @@ -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useState, useEffect, useCallback } from 'react'; import { css } from 'emotion'; import Calendar from 'react-calendar/dist/entry.nostyle'; -import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data'; -import { stringToDateTimeType } from '../time'; +import { GrafanaTheme, DateTime, TimeZone, dateTimeParse } from '@grafana/data'; import { useTheme, stylesFactory } from '../../../themes'; import { TimePickerTitle } from './TimePickerTitle'; import { Button } from '../../Button'; @@ -192,12 +191,13 @@ const getHeaderStyles = stylesFactory((theme: GrafanaTheme) => { interface Props { isOpen: boolean; - from: string; - to: string; + from: DateTime; + to: DateTime; onClose: () => void; onApply: () => void; - onChange: (from: string, to: string) => void; + onChange: (from: DateTime, to: DateTime) => void; isFullscreen: boolean; + timeZone?: TimeZone; } const stopPropagation = (event: React.MouseEvent) => event.stopPropagation(); @@ -247,9 +247,10 @@ const Header = memo(({ onClose }) => { ); }); -const Body = memo(({ onChange, from, to }) => { +const Body = memo(({ onChange, from, to, timeZone }) => { const [value, setValue] = useState(); const theme = useTheme(); + const onCalendarChange = useOnCalendarChange(onChange, timeZone); const styles = getBodyStyles(theme); useEffect(() => { @@ -266,7 +267,7 @@ const Body = memo(({ onChange, from, to }) => { value={value} nextLabel={} prevLabel={} - onChange={value => valueToInput(value, onChange)} + onChange={onCalendarChange} locale="en" /> ); @@ -288,11 +289,9 @@ const Footer = memo(({ onClose, onApply }) => { ); }); -function inputToValue(from: string, to: string): Date[] { - const fromAsDateTime = stringToDateTimeType(from); - const toAsDateTime = stringToDateTimeType(to); - const fromAsDate = fromAsDateTime.isValid() ? fromAsDateTime.toDate() : new Date(); - const toAsDate = toAsDateTime.isValid() ? toAsDateTime.toDate() : new Date(); +function inputToValue(from: DateTime, to: DateTime): Date[] { + const fromAsDate = from.toDate(); + const toAsDate = to.toDate(); if (fromAsDate > toAsDate) { return [toAsDate, fromAsDate]; @@ -300,10 +299,22 @@ function inputToValue(from: string, to: string): Date[] { return [fromAsDate, toAsDate]; } -function valueToInput(value: Date | Date[], onChange: (from: string, to: string) => void): void { - const [from, to] = value; - const fromAsString = dateTime(from).format(TIME_FORMAT); - const toAsString = dateTime(to).format(TIME_FORMAT); +function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) { + return useCallback( + (value: Date | Date[]) => { + if (!Array.isArray(value)) { + return console.error('onCalendarChange: should be run in selectRange={true}'); + } - return onChange(fromAsString, toAsString); + const from = dateTimeParse(dateInfo(value[0]), { timeZone }); + const to = dateTimeParse(dateInfo(value[1]), { timeZone }); + + onChange(from, to); + }, + [onChange] + ); +} + +function dateInfo(date: Date): number[] { + return [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()]; } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeForm.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeForm.tsx index d8a09f27083..68bf02430b6 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeForm.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeForm.tsx @@ -1,7 +1,14 @@ import React, { FormEvent, useState, useCallback } from 'react'; -import { TIME_FORMAT, TimeZone, isDateTime, TimeRange, DateTime, dateMath } from '@grafana/data'; -import { stringToDateTimeType, isValidTimeString } from '../time'; -import { mapStringsToTimeRange } from './mapper'; +import { + TimeZone, + isDateTime, + TimeRange, + DateTime, + dateMath, + dateTimeFormat, + dateTimeParse, + rangeUtil, +} from '@grafana/data'; import { TimePickerCalendar } from './TimePickerCalendar'; import { Field } from '../../Forms/Field'; import { Input } from '../../Input/Input'; @@ -51,11 +58,17 @@ export const TimeRangeForm: React.FC = props => { if (to.invalid || from.invalid) { return; } - props.onApply(mapStringsToTimeRange(from.value, to.value, roundup, timeZone)); + + const timeRange = rangeUtil.convertRawToRange({ + from: dateTimeParse(from.value, { timeZone }), + to: dateTimeParse(to.value, { timeZone }), + }); + + props.onApply(timeRange); }, [from, to, roundup, timeZone]); const onChange = useCallback( - (from: string, to: string) => { + (from: DateTime, to: DateTime) => { setFrom(valueToState(from, false, timeZone)); setTo(valueToState(to, true, timeZone)); }, @@ -89,11 +102,12 @@ export const TimeRangeForm: React.FC = props => { setOpen(false)} onChange={onChange} + timeZone={timeZone} /> ); @@ -104,23 +118,27 @@ function eventToState(event: FormEvent, roundup?: boolean, tim } function valueToState(raw: DateTime | string, roundup?: boolean, timeZone?: TimeZone): InputState { - const value = valueAsString(raw); + const value = valueAsString(raw, timeZone); const invalid = !isValid(value, roundup, timeZone); return { value, invalid }; } -function valueAsString(value: DateTime | string): string { +function valueAsString(value: DateTime | string, timeZone?: TimeZone): string { if (isDateTime(value)) { - return value.format(TIME_FORMAT); + return dateTimeFormat(value, { timeZone }); } return value; } -function isValid(value: string, roundup?: boolean, timeZone?: TimeZone): boolean { - if (dateMath.isMathString(value)) { - return isValidTimeString(value); +function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean { + if (isDateTime(value)) { + return value.isValid(); } - const parsed = stringToDateTimeType(value, roundup, timeZone); + if (dateMath.isMathString(value)) { + return dateMath.isValid(value); + } + + const parsed = dateTimeParse(value, { roundUp, timeZone }); return parsed.isValid(); } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeList.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeList.tsx index b5713e8527f..ea5cc51b62f 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeList.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeList.tsx @@ -69,7 +69,7 @@ const Options: React.FC = ({ options, value, onSelect, timeZone }) => { key={keyForOption(option, index)} value={option} selected={isEqual(option, value)} - onSelect={option => onSelect(mapOptionToTimeRange(option, timeZone))} + onSelect={option => onSelect(mapOptionToTimeRange(option))} /> ))}
diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap index 8ce88d10416..f0b7e2f672c 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap @@ -214,15 +214,15 @@ exports[`TimePickerContent renders recent absolute ranges correctly 1`] = ` Array [ Object { "display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27", - "from": "2019-12-17 07:48:27", + "from": "2019-12-17T07:48:27Z", "section": 3, - "to": "2019-12-18 07:48:27", + "to": "2019-12-18T07:48:27Z", }, Object { "display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27", - "from": "2019-10-17 07:48:27", + "from": "2019-10-17T07:48:27Z", "section": 3, - "to": "2019-10-18 07:48:27", + "to": "2019-10-18T07:48:27Z", }, ] } @@ -277,15 +277,15 @@ exports[`TimePickerContent renders recent absolute ranges correctly 1`] = ` Array [ Object { "display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27", - "from": "2019-12-17 07:48:27", + "from": "2019-12-17T07:48:27Z", "section": 3, - "to": "2019-12-18 07:48:27", + "to": "2019-12-18T07:48:27Z", }, Object { "display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27", - "from": "2019-10-17 07:48:27", + "from": "2019-10-17T07:48:27Z", "section": 3, - "to": "2019-10-18 07:48:27", + "to": "2019-10-18T07:48:27Z", }, ] } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/mapper.ts b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/mapper.ts index 01f4487c755..3775ecccef2 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/mapper.ts +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/mapper.ts @@ -1,86 +1,17 @@ -import { - TimeOption, - TimeRange, - isDateTime, - DateTime, - TimeZone, - dateMath, - dateTime, - dateTimeForTimeZone, - TIME_FORMAT, -} from '@grafana/data'; -import { stringToDateTimeType } from '../time'; +import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat, dateTimeFormatISO } from '@grafana/data'; -export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => { - return { - from: stringToDateTime(option.from, false, timeZone), - to: stringToDateTime(option.to, true, timeZone), - raw: { - from: option.from, - to: option.to, - }, - }; +export const mapOptionToTimeRange = (option: TimeOption): TimeRange => { + return rangeUtil.convertRawToRange({ from: option.from, to: option.to }); }; export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => { - const formattedFrom = stringToDateTime(range.from, false, timeZone).format(TIME_FORMAT); - const formattedTo = stringToDateTime(range.to, true, timeZone).format(TIME_FORMAT); - const from = dateTimeToString(range.from, timeZone); - const to = dateTimeToString(range.to, timeZone); + const from = dateTimeFormat(range.from, { timeZone }); + const to = dateTimeFormat(range.to, { timeZone }); return { - from, - to, + from: dateTimeFormatISO(range.from, { timeZone }), + to: dateTimeFormatISO(range.to, { timeZone }), section: 3, - display: `${formattedFrom} to ${formattedTo}`, + display: `${from} to ${to}`, }; }; - -export const mapStringsToTimeRange = (from: string, to: string, roundup?: boolean, timeZone?: TimeZone): TimeRange => { - const fromDate = stringToDateTimeType(from, roundup, timeZone); - const toDate = stringToDateTimeType(to, roundup, timeZone); - - const timeRangeObject: any = { - from: fromDate, - to: toDate, - raw: { - from: dateMath.isMathString(from) ? from : fromDate, - to: dateMath.isMathString(to) ? to : toDate, - }, - }; - - return timeRangeObject; -}; - -const stringToDateTime = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => { - if (isDateTime(value)) { - if (timeZone === 'utc') { - return value.utc(); - } - return value; - } - - if (value.indexOf('now') !== -1) { - if (!dateMath.isValid(value)) { - return dateTime(); - } - - const parsed = dateMath.parse(value, roundUp, timeZone); - return parsed || dateTime(); - } - - return dateTimeForTimeZone(timeZone, value, TIME_FORMAT); -}; - -const dateTimeToString = (value: DateTime, timeZone?: TimeZone): string => { - if (!isDateTime(value)) { - return value; - } - - const isUtc = timeZone === 'utc'; - if (isUtc) { - return value.utc().format(TIME_FORMAT); - } - - return value.format(TIME_FORMAT); -}; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx index 7c6e819bbfa..88b15f0e915 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx @@ -13,7 +13,7 @@ import { stylesFactory } from '../../themes/stylesFactory'; import { withTheme, useTheme } from '../../themes/ThemeContext'; // Types -import { isDateTime, DateTime, rangeUtil, GrafanaTheme, TIME_FORMAT } from '@grafana/data'; +import { isDateTime, rangeUtil, GrafanaTheme, dateTimeFormatWithAbbrevation } from '@grafana/data'; import { TimeRange, TimeOption, TimeZone, dateMath } from '@grafana/data'; import { Themeable } from '../../types'; @@ -160,7 +160,7 @@ export class UnthemedTimeRangePicker extends PureComponent { )}
- } placement="bottom"> + } placement="bottom">
@@ -146,10 +159,10 @@ export class SharedPreferences extends PureComponent {
- -
-
- -
- Auto-refresh - -
-
- Now delay now- - -
- - - - - -`; - -export function TimePickerSettings() { - return { - restrict: 'E', - template: template, - controller: TimePickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - dashboard: '=', - }, - }; -} - -coreModule.directive('gfTimePickerSettings', TimePickerSettings); diff --git a/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx new file mode 100644 index 00000000000..9a2759c44ab --- /dev/null +++ b/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx @@ -0,0 +1,151 @@ +import React, { PureComponent } from 'react'; +import { Select, Input, Tooltip, LegacyForms } from '@grafana/ui'; +import { DashboardModel } from '../../state/DashboardModel'; +import { getTimeZoneGroups, TimeZone, rangeUtil, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import kbn from 'app/core/utils/kbn'; +import isEmpty from 'lodash/isEmpty'; + +const grafanaTimeZones = [ + { value: '', label: 'Default' }, + { value: 'browser', label: 'Local browser time' }, + { value: 'utc', label: 'UTC' }, +]; + +const timeZones = getTimeZoneGroups().reduce((tzs, group) => { + const options = group.options.map(tz => ({ value: tz, label: tz })); + tzs.push.apply(tzs, options); + return tzs; +}, grafanaTimeZones); + +interface Props { + getDashboard: () => DashboardModel; + onTimeZoneChange: (timeZone: TimeZone) => void; + onRefreshIntervalChange: (interval: string[]) => void; + onNowDelayChange: (nowDelay: string) => void; + onHideTimePickerChange: (hide: boolean) => void; +} + +interface State { + isNowDelayValid: boolean; +} + +export class TimePickerSettings extends PureComponent { + state: State = { isNowDelayValid: true }; + + componentDidMount() { + const { timepicker } = this.props.getDashboard(); + let intervals: string[] = timepicker.refresh_intervals ?? [ + '5s', + '10s', + '30s', + '1m', + '5m', + '15m', + '30m', + '1h', + '2h', + '1d', + ]; + + if (config.minRefreshInterval) { + intervals = intervals.filter(rate => { + return kbn.interval_to_ms(rate) > kbn.interval_to_ms(config.minRefreshInterval); + }); + } + + this.props.onRefreshIntervalChange(intervals); + } + + getRefreshIntervals = () => { + const dashboard = this.props.getDashboard(); + if (!Array.isArray(dashboard.timepicker.refresh_intervals)) { + return ''; + } + return dashboard.timepicker.refresh_intervals.join(','); + }; + + onRefreshIntervalChange = (event: React.FormEvent) => { + if (!event.currentTarget.value) { + return; + } + const intervals = event.currentTarget.value.split(','); + this.props.onRefreshIntervalChange(intervals); + this.forceUpdate(); + }; + + onNowDelayChange = (event: React.FormEvent) => { + const value = event.currentTarget.value; + + if (isEmpty(value)) { + this.setState({ isNowDelayValid: true }); + return this.props.onNowDelayChange(value); + } + + if (rangeUtil.isValidTimeSpan(value)) { + this.setState({ isNowDelayValid: true }); + return this.props.onNowDelayChange(value); + } + + this.setState({ isNowDelayValid: false }); + }; + + onHideTimePickerChange = () => { + const dashboard = this.props.getDashboard(); + this.props.onHideTimePickerChange(!dashboard.timepicker.hidden); + this.forceUpdate(); + }; + + onTimeZoneChange = (timeZone: SelectableValue) => { + if (!timeZone || typeof timeZone.value !== 'string') { + return; + } + this.props.onTimeZoneChange(timeZone.value); + this.forceUpdate(); + }; + + render() { + const dashboard = this.props.getDashboard(); + const value = timeZones.find(item => item.value === dashboard.timezone); + + return ( +
+
Time Options
+
+
+ + +
+
+ Now delay now- + + + +
+ +
+ +
+
+
+ ); + } +} diff --git a/public/app/features/dashboard/components/DashboardSettings/template.html b/public/app/features/dashboard/components/DashboardSettings/template.html index b8b662896c2..dd03b74a372 100644 --- a/public/app/features/dashboard/components/DashboardSettings/template.html +++ b/public/app/features/dashboard/components/DashboardSettings/template.html @@ -50,7 +50,7 @@ - +
Panel Options
diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index a9bdbe7d5c1..cde20e10d07 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -21,6 +21,7 @@ import { PanelPlugin, QueryResultMetaStat, SelectableValue, + TimeZone, } from '@grafana/data'; import { config } from 'app/core/config'; import { getPanelInspectorStyles } from './styles'; @@ -235,6 +236,8 @@ export class PanelInspectorUnconnected extends PureComponent { return null; } + const { dashboard } = this.props; + return (
{name}
@@ -244,7 +247,7 @@ export class PanelInspectorUnconnected extends PureComponent { return ( {stat.title} - {formatStat(stat)} + {formatStat(stat, dashboard.getTimezone())} ); })} @@ -331,13 +334,14 @@ export class PanelInspectorUnconnected extends PureComponent { } } -function formatStat(stat: QueryResultMetaStat): string { +function formatStat(stat: QueryResultMetaStat, timeZone?: TimeZone): string { const display = getDisplayProcessor({ field: { type: FieldType.number, config: stat, }, theme: config.theme, + timeZone, }); return formattedValueToString(display(stat.value)); } diff --git a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts index d9cd7140eb4..0e74b9dbdc2 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts @@ -47,6 +47,7 @@ describe('HistoryListCtrl', () => { id: 2, version: 3, formatDate: jest.fn(() => 'date'), + getRelativeTime: jest.fn(() => 'time ago'), }; }); @@ -148,6 +149,7 @@ describe('HistoryListCtrl', () => { id: 2, version: 3, formatDate: jest.fn(() => 'date'), + getRelativeTime: jest.fn(() => 'time ago'), }; historySrv.calculateDiff = jest.fn(() => Promise.resolve(versionsResponse)); diff --git a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts index 859006d5682..8d86412a496 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts @@ -3,7 +3,7 @@ import angular, { ILocationService, IScope } from 'angular'; import { DashboardModel } from '../../state/DashboardModel'; import { CalculateDiffOptions, HistoryListOpts, HistorySrv, RevisionsModel } from './HistorySrv'; -import { AppEvents, dateTime, DateTimeInput, locationUtil, toUtc } from '@grafana/data'; +import { AppEvents, DateTimeInput, locationUtil } from '@grafana/data'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { CoreEvents } from 'app/types'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; @@ -76,9 +76,7 @@ export class HistoryListCtrl { } formatBasicDate(date: DateTimeInput) { - const now = this.dashboard.timezone === 'browser' ? dateTime() : toUtc(); - const then = this.dashboard.timezone === 'browser' ? dateTime(date) : toUtc(date); - return then.from(now); + return this.dashboard.getRelativeTime(date); } getDiff(diff: 'basic' | 'json') { diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index f38b3117b92..5051cdfbbee 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -12,14 +12,13 @@ import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; import { AppEvent, - dateTime, DateTimeInput, - isDateTime, PanelEvents, TimeRange, TimeZone, - toUtc, UrlQueryValue, + dateTimeFormat, + dateTimeFormatTimeAgo, } from '@grafana/data'; import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types'; import { getConfig } from '../../../core/config'; @@ -814,11 +813,10 @@ export class DashboardModel { } formatDate(date: DateTimeInput, format?: string) { - date = isDateTime(date) ? date : dateTime(date); - format = format || 'YYYY-MM-DD HH:mm:ss'; - const timezone = this.getTimezone(); - - return timezone === 'browser' ? dateTime(date).format(format) : toUtc(date).format(format); + return dateTimeFormat(date, { + format, + timeZone: this.getTimezone(), + }); } destroy() { @@ -933,13 +931,9 @@ export class DashboardModel { } getRelativeTime(date: DateTimeInput) { - date = isDateTime(date) ? date : dateTime(date); - - return this.timezone === 'browser' ? dateTime(date).fromNow() : toUtc(date).fromNow(); - } - - isTimezoneUtc() { - return this.getTimezone() === 'utc'; + return dateTimeFormatTimeAgo(date, { + timeZone: this.getTimezone(), + }); } isSnapshot() { @@ -947,7 +941,7 @@ export class DashboardModel { } getTimezone(): TimeZone { - return (this.timezone ? this.timezone : contextSrv.user.timezone) as TimeZone; + return (this.timezone ? this.timezone : contextSrv?.user?.timezone) as TimeZone; } private updateSchema(old: any) { diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index 72f9ac82daa..441289ece5e 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -24,6 +24,7 @@ import { ScopedVars, applyFieldOverrides, DataConfigSource, + TimeZone, } from '@grafana/data'; export interface QueryRunnerOptions< @@ -56,6 +57,7 @@ export class PanelQueryRunner { private subscription?: Unsubscribable; private lastResult?: PanelData; private dataConfigSource: DataConfigSource; + private timeZone?: TimeZone; constructor(dataConfigSource: DataConfigSource) { this.subject = new ReplaySubject(1); @@ -90,6 +92,7 @@ export class PanelQueryRunner { processedData = { ...processedData, series: applyFieldOverrides({ + timeZone: this.timeZone, autoMinMax: true, data: processedData.series, ...fieldConfig, @@ -118,6 +121,8 @@ export class PanelQueryRunner { minInterval, } = options; + this.timeZone = timezone; + if (isSharedDashboardQuery(datasource)) { this.pipeToSubject(runSharedRequest(options)); return; diff --git a/public/app/features/explore/ExploreGraphPanel.tsx b/public/app/features/explore/ExploreGraphPanel.tsx index 209564612a6..c17066bc095 100644 --- a/public/app/features/explore/ExploreGraphPanel.tsx +++ b/public/app/features/explore/ExploreGraphPanel.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { css, cx } from 'emotion'; -import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTimeForTimeZone } from '@grafana/data'; +import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTime } from '@grafana/data'; import { selectThemeVariant, @@ -106,13 +106,14 @@ class UnThemedExploreGraphPanel extends PureComponent { } const timeRange = { - from: dateTimeForTimeZone(timeZone, absoluteRange.from), - to: dateTimeForTimeZone(timeZone, absoluteRange.to), + from: dateTime(absoluteRange.from), + to: dateTime(absoluteRange.to), raw: { - from: dateTimeForTimeZone(timeZone, absoluteRange.from), - to: dateTimeForTimeZone(timeZone, absoluteRange.to), + from: dateTime(absoluteRange.from), + to: dateTime(absoluteRange.to), }, }; + const height = showPanel === false ? 100 : showingGraph && showingTable ? 200 : 400; const lineWidth = showLines ? 1 : 5; const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES); diff --git a/public/app/features/explore/LiveLogs.tsx b/public/app/features/explore/LiveLogs.tsx index 15cd720994c..9afd24c83a8 100644 --- a/public/app/features/explore/LiveLogs.tsx +++ b/public/app/features/explore/LiveLogs.tsx @@ -3,7 +3,7 @@ import { css, cx } from 'emotion'; import tinycolor from 'tinycolor2'; import { Themeable, withTheme, getLogRowStyles, Icon } from '@grafana/ui'; -import { GrafanaTheme, LogRowModel, TimeZone } from '@grafana/data'; +import { GrafanaTheme, LogRowModel, TimeZone, dateTimeFormat } from '@grafana/data'; import ElapsedTime from './ElapsedTime'; @@ -137,7 +137,6 @@ class LiveLogs extends PureComponent { render() { const { theme, timeZone, onPause, onResume, isPaused } = this.props; const styles = getStyles(theme); - const showUtc = timeZone === 'utc'; const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme); return ( @@ -151,16 +150,7 @@ class LiveLogs extends PureComponent { {this.rowsToRender().map((row: LogRowModel) => { return ( - {showUtc && ( - - {row.timeUtc} - - )} - {!showUtc && ( - - {row.timeLocal} - - )} + {dateTimeFormat(row.timeEpochMs, { timeZone })} {row.entry} ); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index c59983991b5..898e353ef01 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -468,8 +468,8 @@ export const runQueries = (exploreId: ExploreId): ThunkResult => { }; const datasourceName = exploreItemState.requestedDatasourceName; - - const transaction = buildQueryTransaction(queries, queryOptions, range, scanning); + const timeZone = getTimeZone(getState().user); + const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone); let firstResponse = true; dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading })); diff --git a/public/app/features/explore/utils/ResultProcessor.test.ts b/public/app/features/explore/utils/ResultProcessor.test.ts index 7b08ebeb199..ad2dd837e97 100644 --- a/public/app/features/explore/utils/ResultProcessor.test.ts +++ b/public/app/features/explore/utils/ResultProcessor.test.ts @@ -1,24 +1,7 @@ -const realMomentWrapper = jest.requireActual('@grafana/data/src/datetime/moment_wrapper'); - -jest.mock('@grafana/data/src/datetime/moment_wrapper', () => { - const momentMock = { - dateTime: (ts: any) => { - return { - valueOf: () => ts, - fromNow: () => 'fromNow() jest mocked', - format: (fmt: string) => 'format() jest mocked', - }; - }, - toUtc: null as any, - isDateTime: realMomentWrapper.isDateTime, - }; - momentMock.toUtc = (ts: any) => ({ - format: (fmt: string) => 'format() jest mocked', - local: () => momentMock.dateTime(ts), - }); - - return momentMock; -}); +jest.mock('@grafana/data/src/datetime/formatter', () => ({ + dateTimeFormat: () => 'format() jest mocked', + dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked', +})); import { ResultProcessor } from './ResultProcessor'; import { ExploreItemState } from 'app/types/explore'; diff --git a/public/app/features/explore/utils/ResultProcessor.ts b/public/app/features/explore/utils/ResultProcessor.ts index df2ac0c1346..c5541d7b442 100644 --- a/public/app/features/explore/utils/ResultProcessor.ts +++ b/public/app/features/explore/utils/ResultProcessor.ts @@ -92,6 +92,7 @@ export class ResultProcessor { field.display = getDisplayProcessor({ field, theme: config.theme, + timeZone: this.timeZone, }); } diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx index 83afa2a2e6f..04ca75ae0d5 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { dateTime } from '@grafana/data'; +import { dateTimeFormat } from '@grafana/data'; import { Forms, Form } from '@grafana/ui'; import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { ImportDashboardForm } from './ImportDashboardForm'; @@ -73,7 +73,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent { Updated on - {dateTime(meta.updatedAt).format('YYYY-MM-DD HH:mm:ss')} + {dateTimeFormat(meta.updatedAt)} diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index f2beb68d10a..a2f2cb51346 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -185,7 +185,7 @@ class MetricsPanelCtrl extends PanelCtrl { queries: panel.targets, panelId: panel.id, dashboardId: this.dashboard.id, - timezone: this.dashboard.timezone, + timezone: this.dashboard.getTimezone(), timeInfo: this.timeInfo, timeRange: this.range, widthPixels: this.width, diff --git a/public/app/features/profile/ProfileCtrl.ts b/public/app/features/profile/ProfileCtrl.ts index 6193735e7bd..44d6b57a6cf 100644 --- a/public/app/features/profile/ProfileCtrl.ts +++ b/public/app/features/profile/ProfileCtrl.ts @@ -1,5 +1,5 @@ import { coreModule, NavModelSrv } from 'app/core/core'; -import { dateTime } from '@grafana/data'; +import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; import { UserSession } from 'app/types'; import { getBackendSrv } from '@grafana/runtime'; import { promiseToDigest } from 'app/core/utils/promiseToDigest'; @@ -36,8 +36,8 @@ export class ProfileCtrl { return { id: session.id, isActive: session.isActive, - seenAt: dateTime(session.seenAt).fromNow(), - createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), + seenAt: dateTimeFormatTimeAgo(session.seenAt), + createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), clientIp: session.clientIp, browser: session.browser, browserVersion: session.browserVersion, diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index b0d4b7dca25..7f603142888 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -46,7 +46,7 @@ export class BarGaugePanel extends PureComponent> { }; getValues = (): FieldDisplay[] => { - const { data, options, replaceVariables, fieldConfig } = this.props; + const { data, options, replaceVariables, fieldConfig, timeZone } = this.props; return getFieldDisplayValues({ fieldConfig, reduceOptions: options.reduceOptions, @@ -54,6 +54,7 @@ export class BarGaugePanel extends PureComponent> { theme: config.theme, data: data.series, autoMinMax: true, + timeZone, }); }; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index f187825705d..de049e42d5d 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -39,7 +39,7 @@ export class GaugePanel extends PureComponent> { }; getValues = (): FieldDisplay[] => { - const { data, options, replaceVariables, fieldConfig } = this.props; + const { data, options, replaceVariables, fieldConfig, timeZone } = this.props; return getFieldDisplayValues({ fieldConfig, reduceOptions: options.reduceOptions, @@ -47,6 +47,7 @@ export class GaugePanel extends PureComponent> { theme: config.theme, data: data.series, autoMinMax: true, + timeZone, }); }; diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ed76653941a..fba507e2a4b 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -24,7 +24,7 @@ import ReactDOM from 'react-dom'; import { GraphLegendProps, Legend } from './Legend/Legend'; import { GraphCtrl } from './module'; -import { ContextMenuGroup, ContextMenuItem } from '@grafana/ui'; +import { ContextMenuGroup, ContextMenuItem, graphTimeFormatter, graphTimeFormat } from '@grafana/ui'; import { provideTheme, getCurrentTheme } from 'app/core/utils/ConfigProvider'; import { toUtc, @@ -270,6 +270,7 @@ class GraphElement { const fieldDisplay = getDisplayProcessor({ field: { config: fieldConfig, type: FieldType.number }, theme: getCurrentTheme(), + timeZone: this.dashboard.getTimezone(), })(field.values.get(dataIndex)); linksSupplier = links.length ? getFieldLinksSupplier({ @@ -643,7 +644,8 @@ class GraphElement { max: max, label: 'Datetime', ticks: ticks, - timeformat: this.time_format(ticks, min, max), + timeformat: graphTimeFormat(ticks, min, max), + timeFormatter: graphTimeFormatter(this.dashboard.getTimezone()), }; } @@ -900,33 +902,6 @@ class GraphElement { return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals)); }; } - - time_format(ticks: number, min: number | null, max: number | null) { - if (min && max && ticks) { - const range = max - min; - const secPerTick = range / ticks / 1000; - // Need have 10 millisecond margin on the day range - // As sometimes last 24 hour dashboard evaluates to more than 86400000 - const oneDay = 86400010; - const oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; - } } /** @ngInject */ diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 10feb9cddcc..892fa7b36f0 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -12,14 +12,7 @@ import { axesEditorComponent } from './axes_editor'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest'; -import { - getColorFromHexRgbOrName, - PanelEvents, - DataFrame, - DataLink, - DateTimeInput, - VariableSuggestion, -} from '@grafana/data'; +import { getColorFromHexRgbOrName, PanelEvents, DataFrame, DataLink, VariableSuggestion } from '@grafana/data'; import { GraphContextMenuCtrl } from './GraphContextMenuCtrl'; import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; @@ -344,9 +337,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.contextMenuCtrl.toggleMenu(); }; - formatDate = (date: DateTimeInput, format?: string) => { - return this.dashboard.formatDate.apply(this.dashboard, [date, format]); - }; + getTimeZone = () => this.dashboard.getTimezone(); getDataFrameByRefId = (refId: string) => { return this.dataList.filter(dataFrame => dataFrame.refId === refId)[0]; diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index c21fd2e87b2..88986fa422e 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -517,7 +517,7 @@ describe('grafanaGraph', () => { it('should format dates as hours minutes', () => { const axis = ctx.plotOptions.xaxis; - expect(axis.timeformat).toBe('%H:%M'); + expect(axis.timeformat).toBe('HH:mm'); }); }); @@ -531,7 +531,7 @@ describe('grafanaGraph', () => { it('should format dates as month days', () => { const axis = ctx.plotOptions.xaxis; - expect(axis.timeformat).toBe('%m/%d'); + expect(axis.timeformat).toBe('MM/DD'); }); }); }); diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts index e1ddde2a34d..27c41178eab 100644 --- a/public/app/plugins/panel/graph/template.ts +++ b/public/app/plugins/panel/graph/template.ts @@ -11,7 +11,7 @@ const template = ` items="ctrl.contextMenuCtrl.menuItemsSupplier()" onClose="ctrl.onContextMenuClose" getContextMenuSource="ctrl.contextMenuCtrl.getSource" - formatSourceDate="ctrl.formatDate" + timeZone="ctrl.getTimeZone()" x="ctrl.contextMenuCtrl.position.x" y="ctrl.contextMenuCtrl.position.y" > diff --git a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts index 53cea97756e..a836fc73867 100644 --- a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts +++ b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts @@ -38,6 +38,7 @@ export const getGraphSeriesModel = ( decimals: legendOptions.decimals, }, }, + timeZone, }); let fieldColumnIndex = -1; @@ -103,7 +104,7 @@ export const getGraphSeriesModel = ( } : { ...field.config, color }; - field.display = getDisplayProcessor({ field }); + field.display = getDisplayProcessor({ field, timeZone }); // Time step is used to determine bars width when graph is rendered as bar chart const timeStep = getSeriesTimeStep(timeField); diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 257d16d93cc..eefb2b5fc95 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -13,6 +13,7 @@ import { getColorFromHexRgbOrName, getValueFormat, formattedValueToString, + dateTimeFormat, } from '@grafana/data'; import { CoreEvents } from 'app/types'; @@ -154,19 +155,14 @@ export class HeatmapRenderer { .range([0, this.chartWidth]); const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; - const grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); - let timeFormat; - const dashboardTimeZone = this.ctrl.dashboard.getTimezone(); - if (dashboardTimeZone === 'utc') { - timeFormat = d3.utcFormat(grafanaTimeFormatter); - } else { - timeFormat = d3.timeFormat(grafanaTimeFormatter); - } + const format = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); + const timeZone = this.ctrl.dashboard.getTimezone(); + const formatter = (date: Date) => dateTimeFormat(date, { format, timeZone }); const xAxis = d3 .axisBottom(this.xScale) .ticks(ticks) - .tickFormat(timeFormat) + .tickFormat(formatter) .tickPadding(X_AXIS_TICK_PADDING) .tickSize(this.chartHeight); diff --git a/public/app/plugins/panel/news/NewsPanel.tsx b/public/app/plugins/panel/news/NewsPanel.tsx index 54ee950b2b6..e52ca149e3c 100755 --- a/public/app/plugins/panel/news/NewsPanel.tsx +++ b/public/app/plugins/panel/news/NewsPanel.tsx @@ -9,7 +9,7 @@ import { feedToDataFrame } from './utils'; import { loadRSSFeed } from './rss'; // Types -import { PanelProps, DataFrameView, dateTime, GrafanaTheme, textUtil } from '@grafana/data'; +import { PanelProps, DataFrameView, dateTimeFormat, GrafanaTheme, textUtil } from '@grafana/data'; import { NewsOptions, NewsItem } from './types'; import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants'; import { css } from 'emotion'; @@ -79,7 +79,7 @@ export class NewsPanel extends PureComponent {
{item.title}
-
{dateTime(item.date).format('MMM DD')}
+
{dateTimeFormat(item.date, { format: 'MMM DD' })}
diff --git a/public/app/plugins/panel/piechart/PieChartPanel.tsx b/public/app/plugins/panel/piechart/PieChartPanel.tsx index f7fafab3e7b..ebb7db8c342 100644 --- a/public/app/plugins/panel/piechart/PieChartPanel.tsx +++ b/public/app/plugins/panel/piechart/PieChartPanel.tsx @@ -16,7 +16,7 @@ interface Props extends PanelProps {} export class PieChartPanel extends PureComponent { render() { - const { width, height, options, data, replaceVariables, fieldConfig } = this.props; + const { width, height, options, data, replaceVariables, fieldConfig, timeZone } = this.props; const values = getFieldDisplayValues({ fieldConfig, @@ -24,6 +24,7 @@ export class PieChartPanel extends PureComponent { data: data.series, theme: config.theme, replaceVariables: replaceVariables, + timeZone, }).map(v => v.display); return ( diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index bb9dd189300..7c547ddd825 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -187,6 +187,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }, }, theme: config.theme, + timeZone: this.dashboard.getTimezone(), }); // When we don't have any field this.data = { diff --git a/public/app/plugins/panel/stat/StatPanel.tsx b/public/app/plugins/panel/stat/StatPanel.tsx index 5e0fa1fcfde..db4985a6dbb 100644 --- a/public/app/plugins/panel/stat/StatPanel.tsx +++ b/public/app/plugins/panel/stat/StatPanel.tsx @@ -69,7 +69,7 @@ export class StatPanel extends PureComponent> { }; getValues = (): FieldDisplay[] => { - const { data, options, replaceVariables, fieldConfig } = this.props; + const { data, options, replaceVariables, fieldConfig, timeZone } = this.props; return getFieldDisplayValues({ fieldConfig, @@ -79,6 +79,7 @@ export class StatPanel extends PureComponent> { data: data.series, sparkline: options.graphMode !== BigValueGraphMode.None, autoMinMax: true, + timeZone, }); }; diff --git a/public/app/plugins/panel/table-old/module.ts b/public/app/plugins/panel/table-old/module.ts index 040e8a7b0e5..f9f45a4dba9 100644 --- a/public/app/plugins/panel/table-old/module.ts +++ b/public/app/plugins/panel/table-old/module.ts @@ -133,7 +133,7 @@ export class TablePanelCtrl extends MetricsPanelCtrl { this.renderer = new TableRenderer( this.panel, this.table, - this.dashboard.isTimezoneUtc(), + this.dashboard.getTimezone(), this.$sanitize, this.templateSrv, config.theme.type diff --git a/public/app/plugins/panel/table-old/renderer.ts b/public/app/plugins/panel/table-old/renderer.ts index 94cb88bae15..dbd04b865ca 100644 --- a/public/app/plugins/panel/table-old/renderer.ts +++ b/public/app/plugins/panel/table-old/renderer.ts @@ -1,6 +1,5 @@ import _ from 'lodash'; import { - dateTime, escapeStringForRegex, formattedValueToString, getColorFromHexRgbOrName, @@ -11,6 +10,9 @@ import { stringToJsRegex, textUtil, unEscapeStringFromRegex, + TimeZone, + dateTimeFormatISO, + dateTimeFormat, } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { ColumnRender, TableRenderModel, ColumnStyle } from './types'; @@ -23,7 +25,7 @@ export class TableRenderer { constructor( private panel: { styles: ColumnStyle[]; pageSize: number }, private table: TableRenderModel, - private isUtc: boolean, + private timeZone: TimeZone, private sanitize: (v: any) => any, private templateSrv: TemplateSrv, private theme?: GrafanaThemeType @@ -119,13 +121,16 @@ export class TableRenderer { v = parseInt(v, 10); } - let date = dateTime(v); - - if (this.isUtc) { - date = date.utc(); + if (!column.style.dateFormat) { + return dateTimeFormatISO(v, { + timeZone: this.timeZone, + }); } - return date.format(column.style.dateFormat); + return dateTimeFormat(v, { + format: column.style.dateFormat, + timeZone: this.timeZone, + }); }; } diff --git a/public/app/plugins/panel/table-old/specs/renderer.test.ts b/public/app/plugins/panel/table-old/specs/renderer.test.ts index 1873b210726..d99e9af7059 100644 --- a/public/app/plugins/panel/table-old/specs/renderer.test.ts +++ b/public/app/plugins/panel/table-old/specs/renderer.test.ts @@ -1,9 +1,11 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; import { TableRenderer } from '../renderer'; -import { getColorDefinitionByName, ScopedVars } from '@grafana/data'; +import { getColorDefinitionByName, ScopedVars, TimeZone } from '@grafana/data'; import { ColumnRender } from '../types'; +const utc: TimeZone = 'utc'; + const sanitize = (value: any): string => { return 'sanitized'; }; @@ -210,7 +212,7 @@ describe('when rendering table', () => { }; //@ts-ignore - const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); + const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv); it('time column should be formatted', () => { const html = renderer.renderCell(0, 0, 1388556366666); @@ -466,7 +468,7 @@ describe('when rendering table with different patterns', () => { }; //@ts-ignore - const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); + const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv); const html = renderer.renderCell(1, 0, 1230); expect(html).toBe(expected); @@ -536,7 +538,7 @@ describe('when rendering cells with different alignment options', () => { }; //@ts-ignore - const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); + const renderer = new TableRenderer(panel, table, utc, sanitize, templateSrv); const html = renderer.renderCell(1, 0, 42); expect(html).toBe(expected); diff --git a/public/vendor/flot/jquery.flot.time.js b/public/vendor/flot/jquery.flot.time.js index 8e510f1948f..a9094cabebd 100644 --- a/public/vendor/flot/jquery.flot.time.js +++ b/public/vendor/flot/jquery.flot.time.js @@ -15,7 +15,8 @@ API.txt for details. timezone: null, // "browser" for local to the client or timezone for timezone-js timeformat: null, // format string to use twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months + monthNames: null, // list of names of months + timeFormatter: null // external formatter with timezone support } }; @@ -29,7 +30,6 @@ API.txt for details. // A subset of the Open Group's strftime format is supported. function formatDate(d, fmt, monthNames, dayNames) { - if (typeof d.strftime == "function") { return d.strftime(fmt); } @@ -356,12 +356,15 @@ API.txt for details. }; axis.tickFormatter = function (v, axis) { - var d = dateGenerator(v, axis.options); - // first check global format + // first check global formatter + if (typeof opts.timeFormatter === "function") { + return opts.timeFormatter(d.getTime(), opts.timeformat); + } - if (opts.timeformat != null) { + // second check global format + if (opts.timeformat != null) { return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); } @@ -407,7 +410,6 @@ API.txt for details. } var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - return rt; }; } diff --git a/yarn.lock b/yarn.lock index 061be335ba0..235235859ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5672,6 +5672,13 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/moment-timezone@0.5.13": + version "0.5.13" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.13.tgz#0317ccc91eb4c7f4901704166166395c39276528" + integrity sha512-SWk1qM8DRssS5YR9L4eEX7WUhK/wc96aIr4nMa6p0kTk9YhGGOJjECVhIdPEj13fvJw72Xun69gScXSZ/UmcPg== + dependencies: + moment ">=2.14.0" + "@types/moment@^2.13.0": version "2.13.0" resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896" @@ -17135,7 +17142,14 @@ module-alias@2.2.2: resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== -moment@*, moment@2.24.0, moment@2.x, moment@^2.18.1: +moment-timezone@0.5.28: + version "0.5.28" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" + integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw== + dependencies: + moment ">= 2.9.0" + +moment@*, moment@2.24.0, moment@2.x, "moment@>= 2.9.0", moment@>=2.14.0, moment@^2.18.1: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== @@ -20230,15 +20244,17 @@ rc-drawer@3.1.3: rc-util "^4.16.1" react-lifecycles-compat "^3.0.4" -rc-slider@9.2.3: - version "9.2.3" - resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.2.3.tgz#b80f03ada71ef3ec35dfcb67d2e0f87d84ce2781" - integrity sha512-mlERrweLA4KNFvO0dkhQv3Du0Emq7DyAFV6N7jgrAwfUZsX4eB1T1iJWYMtsguHXbMyje+PACAqwsrfhZIN0bQ== +rc-slider@8.7.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.7.1.tgz#9ed07362dc93489a38e654b21b8122ad70fd3c42" + integrity sha512-WMT5mRFUEcrLWwTxsyS8jYmlaMsTVCZIGENLikHsNv+tE8ThU2lCoPfi/xFNUfJFNFSBFP3MwPez9ZsJmNp13g== dependencies: babel-runtime "6.x" classnames "^2.2.5" - rc-tooltip "^4.0.0" + prop-types "^15.5.4" + rc-tooltip "^3.7.0" rc-util "^4.0.4" + react-lifecycles-compat "^3.0.4" shallowequal "^1.1.0" warning "^4.0.3" @@ -20254,14 +20270,16 @@ rc-time-picker@^3.7.3: rc-trigger "^2.2.0" react-lifecycles-compat "^3.0.4" -rc-tooltip@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-4.0.3.tgz#728b760863643ec2e85827a2e7fb28d961b3b759" - integrity sha512-HNyBh9/fPdds0DXja8JQX0XTIHmZapB3lLzbdn74aNSxXG1KUkt+GK4X1aOTRY5X9mqm4uUKdeFrn7j273H8gw== +rc-tooltip@^3.7.0: + version "3.7.3" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.3.tgz#280aec6afcaa44e8dff0480fbaff9e87fc00aecc" + integrity sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww== dependencies: - rc-trigger "^4.0.0" + babel-runtime "6.x" + prop-types "^15.5.8" + rc-trigger "^2.2.2" -rc-trigger@^2.2.0: +rc-trigger@^2.2.0, rc-trigger@^2.2.2: version "2.6.5" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885" integrity sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw==