mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -87,6 +87,7 @@
|
|||||||
"@types/lodash": "4.14.149",
|
"@types/lodash": "4.14.149",
|
||||||
"@types/lru-cache": "^5.1.0",
|
"@types/lru-cache": "^5.1.0",
|
||||||
"@types/marked": "0.6.5",
|
"@types/marked": "0.6.5",
|
||||||
|
"@types/moment-timezone": "0.5.13",
|
||||||
"@types/mousetrap": "1.6.3",
|
"@types/mousetrap": "1.6.3",
|
||||||
"@types/node": "13.7.0",
|
"@types/node": "13.7.0",
|
||||||
"@types/papaparse": "4.5.9",
|
"@types/papaparse": "4.5.9",
|
||||||
@@ -241,6 +242,7 @@
|
|||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"memoize-one": "5.1.1",
|
"memoize-one": "5.1.1",
|
||||||
"moment": "2.24.0",
|
"moment": "2.24.0",
|
||||||
|
"moment-timezone": "0.5.28",
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"mousetrap-global-bind": "1.1.0",
|
"mousetrap-global-bind": "1.1.0",
|
||||||
"nodemon": "2.0.2",
|
"nodemon": "2.0.2",
|
||||||
|
17
packages/grafana-data/src/datetime/common.ts
Normal file
17
packages/grafana-data/src/datetime/common.ts
Normal file
@@ -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 = <T extends DateTimeOptions>(options?: T): TimeZone => {
|
||||||
|
return options?.timeZone ?? defaultTimeZoneResolver() ?? DefaultTimeZone;
|
||||||
|
};
|
76
packages/grafana-data/src/datetime/formatter.test.ts
Normal file
76
packages/grafana-data/src/datetime/formatter.test.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
54
packages/grafana-data/src/datetime/formatter.ts
Normal file
54
packages/grafana-data/src/datetime/formatter.ts
Normal file
@@ -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<T extends DateTimeOptions = DateTimeOptions> = (
|
||||||
|
dateInUtc: DateTimeInput,
|
||||||
|
options?: T
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
export const dateTimeFormat: DateTimeFormatter<DateTimeOptionsWithFormat> = (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 = <T extends DateTimeOptionsWithFormat>(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();
|
||||||
|
}
|
||||||
|
};
|
@@ -4,4 +4,7 @@ import * as rangeUtil from './rangeutil';
|
|||||||
export * from './moment_wrapper';
|
export * from './moment_wrapper';
|
||||||
export * from './timezones';
|
export * from './timezones';
|
||||||
export * from './formats';
|
export * from './formats';
|
||||||
|
export * from './formatter';
|
||||||
|
export * from './parser';
|
||||||
export { dateMath, rangeUtil };
|
export { dateMath, rangeUtil };
|
||||||
|
export { DateTimeOptions, setTimeZoneResolver, TimeZoneResolver } from './common';
|
||||||
|
57
packages/grafana-data/src/datetime/parser.ts
Normal file
57
packages/grafana-data/src/datetime/parser.ts
Normal file
@@ -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<T extends DateTimeOptions = DateTimeOptions> = (
|
||||||
|
value: DateTimeInput,
|
||||||
|
options?: T
|
||||||
|
) => DateTime;
|
||||||
|
|
||||||
|
export const dateTimeParse: DateTimeParser<DateTimeOptionsWhenParsing> = (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;
|
||||||
|
}
|
||||||
|
};
|
@@ -1,10 +1,12 @@
|
|||||||
import each from 'lodash/each';
|
import each from 'lodash/each';
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
|
|
||||||
import { RawTimeRange } from '../types/time';
|
import { RawTimeRange, TimeRange, TimeZone } from '../types/time';
|
||||||
|
|
||||||
import * as dateMath from './datemath';
|
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 } } = {
|
const spans: { [key: string]: { display: string; section?: number } } = {
|
||||||
s: { display: 'second' },
|
s: { display: 'second' },
|
||||||
@@ -61,8 +63,6 @@ const rangeOptions = [
|
|||||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
|
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const absoluteFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
|
|
||||||
const rangeIndex: any = {};
|
const rangeIndex: any = {};
|
||||||
each(rangeOptions, (frame: any) => {
|
each(rangeOptions, (frame: any) => {
|
||||||
rangeIndex[frame.from + ' to ' + frame.to] = frame;
|
rangeIndex[frame.from + ' to ' + frame.to] = frame;
|
||||||
@@ -84,10 +84,6 @@ export function getRelativeTimesList(timepickerSettings: any, currentDisplay: an
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(date: DateTime) {
|
|
||||||
return date.format(absoluteFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles expressions like
|
// handles expressions like
|
||||||
// 5m
|
// 5m
|
||||||
// 5m to now/d
|
// 5m to now/d
|
||||||
@@ -144,24 +140,27 @@ export function describeTextRange(expr: any) {
|
|||||||
* @param range - a time range (usually specified by the TimePicker)
|
* @param range - a time range (usually specified by the TimePicker)
|
||||||
* @alpha
|
* @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()];
|
const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()];
|
||||||
|
|
||||||
if (option) {
|
if (option) {
|
||||||
return option.display;
|
return option.display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options = { timeZone };
|
||||||
|
|
||||||
if (isDateTime(range.from) && isDateTime(range.to)) {
|
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)) {
|
if (isDateTime(range.from)) {
|
||||||
const toMoment = dateMath.parse(range.to, true);
|
const parsed = dateMath.parse(range.to, true, 'utc');
|
||||||
return toMoment ? formatDate(range.from) + ' to ' + toMoment.fromNow() : '';
|
return parsed ? dateTimeFormat(range.from, options) + ' to ' + dateTimeFormatTimeAgo(parsed, options) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDateTime(range.to)) {
|
if (isDateTime(range.to)) {
|
||||||
const from = dateMath.parse(range.from, false);
|
const parsed = dateMath.parse(range.from, false, 'utc');
|
||||||
return from ? from.fromNow() + ' to ' + formatDate(range.to) : '';
|
return parsed ? dateTimeFormatTimeAgo(parsed, options) + ' to ' + dateTimeFormat(range.to, options) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.to.toString() === 'now') {
|
if (range.to.toString() === 'now') {
|
||||||
@@ -180,3 +179,17 @@ export const isValidTimeSpan = (value: string) => {
|
|||||||
const info = describeTextRange(value);
|
const info = describeTextRange(value);
|
||||||
return info.invalid !== true;
|
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 };
|
||||||
|
};
|
||||||
|
@@ -12,6 +12,7 @@ import {
|
|||||||
FieldType,
|
FieldType,
|
||||||
InterpolateFunction,
|
InterpolateFunction,
|
||||||
LinkModel,
|
LinkModel,
|
||||||
|
TimeZone,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { DataFrameView } from '../dataframe/DataFrameView';
|
import { DataFrameView } from '../dataframe/DataFrameView';
|
||||||
import { GraphSeriesValue } from '../types/graph';
|
import { GraphSeriesValue } from '../types/graph';
|
||||||
@@ -89,12 +90,13 @@ export interface GetFieldDisplayValuesOptions {
|
|||||||
sparkline?: boolean; // Calculate the sparkline
|
sparkline?: boolean; // Calculate the sparkline
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
autoMinMax?: boolean;
|
autoMinMax?: boolean;
|
||||||
|
timeZone?: TimeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
|
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
|
||||||
|
|
||||||
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
|
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 calcs = reduceOptions.calcs.length ? reduceOptions.calcs : [ReducerID.last];
|
||||||
|
|
||||||
const values: FieldDisplay[] = [];
|
const values: FieldDisplay[] = [];
|
||||||
@@ -127,6 +129,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
getDisplayProcessor({
|
getDisplayProcessor({
|
||||||
field,
|
field,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = config.title ? config.title : defaultTitle;
|
const title = config.title ? config.title : defaultTitle;
|
||||||
@@ -259,7 +262,7 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display
|
|||||||
|
|
||||||
function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay {
|
function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay {
|
||||||
const displayName = 'No data';
|
const displayName = 'No data';
|
||||||
const { fieldConfig } = options;
|
const { fieldConfig, timeZone } = options;
|
||||||
const { defaults } = fieldConfig;
|
const { defaults } = fieldConfig;
|
||||||
|
|
||||||
const displayProcessor = getDisplayProcessor({
|
const displayProcessor = getDisplayProcessor({
|
||||||
@@ -268,6 +271,7 @@ function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): Fiel
|
|||||||
config: defaults,
|
config: defaults,
|
||||||
},
|
},
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
const display = displayProcessor(null);
|
const display = displayProcessor(null);
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
InterpolateFunction,
|
InterpolateFunction,
|
||||||
ValueLinkConfig,
|
ValueLinkConfig,
|
||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
|
TimeZone,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||||
import { FieldMatcher } from '../types/transformations';
|
import { FieldMatcher } from '../types/transformations';
|
||||||
@@ -193,6 +194,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
|||||||
// Attach data links supplier
|
// Attach data links supplier
|
||||||
f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, {
|
f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, {
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
|
timeZone: options.timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
return f;
|
return f;
|
||||||
@@ -335,6 +337,7 @@ const getLinksSupplier = (
|
|||||||
replaceVariables: InterpolateFunction,
|
replaceVariables: InterpolateFunction,
|
||||||
options: {
|
options: {
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
|
timeZone?: TimeZone;
|
||||||
}
|
}
|
||||||
) => (config: ValueLinkConfig): Array<LinkModel<Field>> => {
|
) => (config: ValueLinkConfig): Array<LinkModel<Field>> => {
|
||||||
if (!field.config.links || field.config.links.length === 0) {
|
if (!field.config.links || field.config.links.length === 0) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import toNumber from 'lodash/toNumber';
|
import toNumber from 'lodash/toNumber';
|
||||||
import { DataFrame, DisplayValue, GrafanaTheme } from '../types';
|
import { DataFrame, DisplayValue, GrafanaTheme, TimeZone } from '../types';
|
||||||
import { getDisplayProcessor } from './displayProcessor';
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
import { formattedValueToString } from '../valueFormats';
|
import { formattedValueToString } from '../valueFormats';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ export function getFieldDisplayValuesProxy(
|
|||||||
rowIndex: number,
|
rowIndex: number,
|
||||||
options: {
|
options: {
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
|
timeZone?: TimeZone;
|
||||||
}
|
}
|
||||||
): Record<string, DisplayValue> {
|
): Record<string, DisplayValue> {
|
||||||
return new Proxy({} as Record<string, DisplayValue>, {
|
return new Proxy({} as Record<string, DisplayValue>, {
|
||||||
@@ -38,6 +39,7 @@ export function getFieldDisplayValuesProxy(
|
|||||||
field.display = getDisplayProcessor({
|
field.display = getDisplayProcessor({
|
||||||
field,
|
field,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
|
timeZone: options.timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const raw = field.values.get(rowIndex);
|
const raw = field.values.get(rowIndex);
|
||||||
|
@@ -3,6 +3,7 @@ import { toDuration as duration, toUtc, dateTime } from '../datetime/moment_wrap
|
|||||||
import { toFixed, toFixedScaled, FormattedValue, ValueFormatter } from './valueFormats';
|
import { toFixed, toFixedScaled, FormattedValue, ValueFormatter } from './valueFormats';
|
||||||
import { DecimalCount } from '../types/displayValue';
|
import { DecimalCount } from '../types/displayValue';
|
||||||
import { TimeZone } from '../types';
|
import { TimeZone } from '../types';
|
||||||
|
import { dateTimeFormat, dateTimeFormatTimeAgo } from '../datetime';
|
||||||
|
|
||||||
interface IntervalsInSeconds {
|
interface IntervalsInSeconds {
|
||||||
[interval: string]: number;
|
[interval: string]: number;
|
||||||
@@ -326,14 +327,14 @@ export function toClockSeconds(size: number, decimals: DecimalCount): FormattedV
|
|||||||
|
|
||||||
export function toDateTimeValueFormatter(pattern: string, todayPattern?: string): ValueFormatter {
|
export function toDateTimeValueFormatter(pattern: string, todayPattern?: string): ValueFormatter {
|
||||||
return (value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, timeZone?: TimeZone): FormattedValue => {
|
return (value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, timeZone?: TimeZone): FormattedValue => {
|
||||||
const isUtc = timeZone === 'utc';
|
|
||||||
const time = isUtc ? toUtc(value) : dateTime(value);
|
|
||||||
if (todayPattern) {
|
if (todayPattern) {
|
||||||
if (dateTime().isSame(value, 'day')) {
|
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,
|
scaledDecimals: DecimalCount,
|
||||||
timeZone?: TimeZone
|
timeZone?: TimeZone
|
||||||
): FormattedValue {
|
): FormattedValue {
|
||||||
const isUtc = timeZone === 'utc';
|
return { text: dateTimeFormatTimeAgo(value, { timeZone }) };
|
||||||
const time = isUtc ? toUtc(value) : dateTime(value);
|
|
||||||
return { text: time.fromNow() };
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Portal } from '../Portal/Portal';
|
import { Portal } from '../Portal/Portal';
|
||||||
import { Dimensions } from '@grafana/data';
|
import { Dimensions, TimeZone } from '@grafana/data';
|
||||||
import { FlotPosition } from '../Graph/types';
|
import { FlotPosition } from '../Graph/types';
|
||||||
import { TooltipContainer } from './TooltipContainer';
|
import { TooltipContainer } from './TooltipContainer';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export interface TooltipContentProps<T extends Dimensions = any> {
|
|||||||
// TODO: type this better, no good idea how yet
|
// TODO: type this better, no good idea how yet
|
||||||
dimensions: T; // Dimension[]
|
dimensions: T; // Dimension[]
|
||||||
activeDimensions?: ActiveDimensions<T>;
|
activeDimensions?: ActiveDimensions<T>;
|
||||||
// timeZone: TimeZone;
|
timeZone?: TimeZone;
|
||||||
pos: FlotPosition;
|
pos: FlotPosition;
|
||||||
mode: TooltipMode;
|
mode: TooltipMode;
|
||||||
}
|
}
|
||||||
|
@@ -3,21 +3,14 @@ import $ from 'jquery';
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
// Types
|
// Types
|
||||||
import {
|
import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data';
|
||||||
TimeRange,
|
|
||||||
GraphSeriesXY,
|
|
||||||
TimeZone,
|
|
||||||
DefaultTimeZone,
|
|
||||||
createDimension,
|
|
||||||
DateTimeInput,
|
|
||||||
dateTime,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { FlotPosition, FlotItem } from './types';
|
import { FlotPosition, FlotItem } from './types';
|
||||||
import { TooltipProps, TooltipContentProps, ActiveDimensions, Tooltip } from '../Chart/Tooltip';
|
import { TooltipProps, TooltipContentProps, ActiveDimensions, Tooltip } from '../Chart/Tooltip';
|
||||||
import { GraphTooltip } from './GraphTooltip/GraphTooltip';
|
import { GraphTooltip } from './GraphTooltip/GraphTooltip';
|
||||||
import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu';
|
import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu';
|
||||||
import { GraphDimensions } from './GraphTooltip/types';
|
import { GraphDimensions } from './GraphTooltip/types';
|
||||||
|
import { graphTimeFormat, graphTimeFormatter } from './utils';
|
||||||
|
|
||||||
export interface GraphProps {
|
export interface GraphProps {
|
||||||
children?: JSX.Element | JSX.Element[];
|
children?: JSX.Element | JSX.Element[];
|
||||||
@@ -126,7 +119,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTooltip = () => {
|
renderTooltip = () => {
|
||||||
const { children, series } = this.props;
|
const { children, series, timeZone } = this.props;
|
||||||
const { pos, activeItem, isTooltipVisible } = this.state;
|
const { pos, activeItem, isTooltipVisible } = this.state;
|
||||||
let tooltipElement: React.ReactElement<TooltipProps> | null = null;
|
let tooltipElement: React.ReactElement<TooltipProps> | null = null;
|
||||||
|
|
||||||
@@ -191,6 +184,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
activeDimensions,
|
activeDimensions,
|
||||||
pos,
|
pos,
|
||||||
mode: tooltipElementProps.mode || 'single',
|
mode: tooltipElementProps.mode || 'single',
|
||||||
|
timeZone,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps });
|
const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps });
|
||||||
@@ -234,10 +228,6 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (date: DateTimeInput, format?: string) => {
|
|
||||||
return dateTime(date)?.format(format);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeContext = () => this.setState({ isContextVisible: false });
|
const closeContext = () => this.setState({ isContextVisible: false });
|
||||||
|
|
||||||
const getContextMenuSource = () => {
|
const getContextMenuSource = () => {
|
||||||
@@ -256,7 +246,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
y: contextPos.pageY,
|
y: contextPos.pageY,
|
||||||
onClose: closeContext,
|
onClose: closeContext,
|
||||||
getContextMenuSource: getContextMenuSource,
|
getContextMenuSource: getContextMenuSource,
|
||||||
formatSourceDate: formatDate,
|
timeZone: this.props.timeZone,
|
||||||
dimensions,
|
dimensions,
|
||||||
contextDimensions,
|
contextDimensions,
|
||||||
};
|
};
|
||||||
@@ -330,8 +320,8 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
max: max,
|
max: max,
|
||||||
label: 'Datetime',
|
label: 'Datetime',
|
||||||
ticks: ticks,
|
ticks: ticks,
|
||||||
timeformat: timeFormat(ticks, min, max),
|
timeformat: graphTimeFormat(ticks, min, max),
|
||||||
timezone: timeZone ?? DefaultTimeZone,
|
timeFormatter: graphTimeFormatter(timeZone),
|
||||||
},
|
},
|
||||||
yaxes,
|
yaxes,
|
||||||
grid: {
|
grid: {
|
||||||
@@ -390,30 +380,4 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
export default Graph;
|
||||||
|
@@ -4,14 +4,13 @@ import { ThemeContext } from '../../themes';
|
|||||||
import { SeriesIcon } from '../Legend/SeriesIcon';
|
import { SeriesIcon } from '../Legend/SeriesIcon';
|
||||||
import { GraphDimensions } from './GraphTooltip/types';
|
import { GraphDimensions } from './GraphTooltip/types';
|
||||||
import {
|
import {
|
||||||
DateTimeInput,
|
|
||||||
FlotDataPoint,
|
FlotDataPoint,
|
||||||
getValueFromDimension,
|
getValueFromDimension,
|
||||||
getDisplayProcessor,
|
getDisplayProcessor,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
MS_DATE_TIME_FORMAT,
|
dateTimeFormat,
|
||||||
DEFAULT_DATE_TIME_FORMAT,
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
@@ -19,14 +18,14 @@ export type ContextDimensions<T extends Dimensions = any> = { [key in keyof T]:
|
|||||||
|
|
||||||
export type GraphContextMenuProps = ContextMenuProps & {
|
export type GraphContextMenuProps = ContextMenuProps & {
|
||||||
getContextMenuSource: () => FlotDataPoint | null;
|
getContextMenuSource: () => FlotDataPoint | null;
|
||||||
formatSourceDate: (date: DateTimeInput, format?: string) => string;
|
timeZone?: TimeZone;
|
||||||
dimensions?: GraphDimensions;
|
dimensions?: GraphDimensions;
|
||||||
contextDimensions?: ContextDimensions;
|
contextDimensions?: ContextDimensions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
||||||
getContextMenuSource,
|
getContextMenuSource,
|
||||||
formatSourceDate,
|
timeZone,
|
||||||
items,
|
items,
|
||||||
dimensions,
|
dimensions,
|
||||||
contextDimensions,
|
contextDimensions,
|
||||||
@@ -56,11 +55,20 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
|||||||
contextDimensions.yAxis[0],
|
contextDimensions.yAxis[0],
|
||||||
contextDimensions.yAxis[1]
|
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);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
@@ -69,7 +77,7 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
|||||||
z-index: ${theme.zIndex.tooltip};
|
z-index: ${theme.zIndex.tooltip};
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<strong>{formatSourceDate(source.datapoint[0], timeFormat)}</strong>
|
<strong>{formattedValue}</strong>
|
||||||
<div>
|
<div>
|
||||||
<SeriesIcon color={source.series.color} />
|
<SeriesIcon color={source.series.color} />
|
||||||
<span
|
<span
|
||||||
|
@@ -9,6 +9,7 @@ export const GraphTooltip: React.FC<TooltipContentProps<GraphDimensions>> = ({
|
|||||||
dimensions,
|
dimensions,
|
||||||
activeDimensions,
|
activeDimensions,
|
||||||
pos,
|
pos,
|
||||||
|
timeZone,
|
||||||
}) => {
|
}) => {
|
||||||
// When
|
// When
|
||||||
// [1] no active dimension or
|
// [1] no active dimension or
|
||||||
@@ -19,9 +20,16 @@ export const GraphTooltip: React.FC<TooltipContentProps<GraphDimensions>> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'single') {
|
if (mode === 'single') {
|
||||||
return <SingleModeGraphTooltip dimensions={dimensions} activeDimensions={activeDimensions} />;
|
return <SingleModeGraphTooltip dimensions={dimensions} activeDimensions={activeDimensions} timeZone={timeZone} />;
|
||||||
} else {
|
} else {
|
||||||
return <MultiModeGraphTooltip dimensions={dimensions} activeDimensions={activeDimensions} pos={pos} />;
|
return (
|
||||||
|
<MultiModeGraphTooltip
|
||||||
|
dimensions={dimensions}
|
||||||
|
activeDimensions={activeDimensions}
|
||||||
|
pos={pos}
|
||||||
|
timeZone={timeZone}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import { getValueFromDimension } from '@grafana/data';
|
|||||||
export const MultiModeGraphTooltip: React.FC<GraphTooltipContentProps & {
|
export const MultiModeGraphTooltip: React.FC<GraphTooltipContentProps & {
|
||||||
// We expect position to figure out correct values when not hovering over a datapoint
|
// We expect position to figure out correct values when not hovering over a datapoint
|
||||||
pos: FlotPosition;
|
pos: FlotPosition;
|
||||||
}> = ({ dimensions, activeDimensions, pos }) => {
|
}> = ({ dimensions, activeDimensions, pos, timeZone }) => {
|
||||||
let activeSeriesIndex: number | null = null;
|
let activeSeriesIndex: number | null = null;
|
||||||
// when no x-axis provided, skip rendering
|
// when no x-axis provided, skip rendering
|
||||||
if (activeDimensions.xAxis === null) {
|
if (activeDimensions.xAxis === null) {
|
||||||
@@ -24,7 +24,7 @@ export const MultiModeGraphTooltip: React.FC<GraphTooltipContentProps & {
|
|||||||
? getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1])
|
? getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1])
|
||||||
: pos.x;
|
: pos.x;
|
||||||
|
|
||||||
const hoverInfo = getMultiSeriesGraphHoverInfo(dimensions.yAxis.columns, dimensions.xAxis.columns, time);
|
const hoverInfo = getMultiSeriesGraphHoverInfo(dimensions.yAxis.columns, dimensions.xAxis.columns, time, timeZone);
|
||||||
const timestamp = hoverInfo.time;
|
const timestamp = hoverInfo.time;
|
||||||
|
|
||||||
const series = hoverInfo.results.map((s, i) => {
|
const series = hoverInfo.results.map((s, i) => {
|
||||||
|
@@ -67,6 +67,7 @@ interface SeriesTableProps {
|
|||||||
export const SeriesTable: React.FC<SeriesTableProps> = ({ timestamp, series }) => {
|
export const SeriesTable: React.FC<SeriesTableProps> = ({ timestamp, series }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getSeriesTableRowStyles(theme);
|
const styles = getSeriesTableRowStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{timestamp && (
|
{timestamp && (
|
||||||
|
@@ -8,7 +8,11 @@ import {
|
|||||||
import { SeriesTable } from './SeriesTable';
|
import { SeriesTable } from './SeriesTable';
|
||||||
import { GraphTooltipContentProps } from './types';
|
import { GraphTooltipContentProps } from './types';
|
||||||
|
|
||||||
export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({ dimensions, activeDimensions }) => {
|
export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({
|
||||||
|
dimensions,
|
||||||
|
activeDimensions,
|
||||||
|
timeZone,
|
||||||
|
}) => {
|
||||||
// not hovering over a point, skip rendering
|
// not hovering over a point, skip rendering
|
||||||
if (
|
if (
|
||||||
activeDimensions.yAxis === null ||
|
activeDimensions.yAxis === null ||
|
||||||
@@ -24,7 +28,7 @@ export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({ dim
|
|||||||
|
|
||||||
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
|
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
|
||||||
const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]);
|
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);
|
const disp = display(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ActiveDimensions, TooltipMode } from '../../Chart/Tooltip';
|
import { ActiveDimensions, TooltipMode } from '../../Chart/Tooltip';
|
||||||
import { Dimension, Dimensions } from '@grafana/data';
|
import { Dimension, Dimensions, TimeZone } from '@grafana/data';
|
||||||
|
|
||||||
export interface GraphTooltipOptions {
|
export interface GraphTooltipOptions {
|
||||||
mode: TooltipMode;
|
mode: TooltipMode;
|
||||||
@@ -13,4 +13,5 @@ export interface GraphDimensions extends Dimensions {
|
|||||||
export interface GraphTooltipContentProps {
|
export interface GraphTooltipContentProps {
|
||||||
dimensions: GraphDimensions; // Dimension[]
|
dimensions: GraphDimensions; // Dimension[]
|
||||||
activeDimensions: ActiveDimensions<GraphDimensions>;
|
activeDimensions: ActiveDimensions<GraphDimensions>;
|
||||||
|
timeZone?: TimeZone;
|
||||||
}
|
}
|
||||||
|
@@ -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
|
* Returns index of the closest datapoint BEFORE hover position
|
||||||
@@ -48,7 +55,8 @@ export const getMultiSeriesGraphHoverInfo = (
|
|||||||
yAxisDimensions: Field[],
|
yAxisDimensions: Field[],
|
||||||
xAxisDimensions: Field[],
|
xAxisDimensions: Field[],
|
||||||
/** Well, time basically */
|
/** Well, time basically */
|
||||||
xAxisPosition: number
|
xAxisPosition: number,
|
||||||
|
timeZone?: TimeZone
|
||||||
): {
|
): {
|
||||||
results: MultiSeriesHoverInfo[];
|
results: MultiSeriesHoverInfo[];
|
||||||
time?: GraphSeriesValue;
|
time?: GraphSeriesValue;
|
||||||
@@ -75,7 +83,7 @@ export const getMultiSeriesGraphHoverInfo = (
|
|||||||
minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime;
|
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));
|
const disp = display(field.values.get(hoverIndex));
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
@@ -93,3 +101,33 @@ export const getMultiSeriesGraphHoverInfo = (
|
|||||||
time: minTime,
|
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';
|
||||||
|
};
|
||||||
|
@@ -1,5 +1,13 @@
|
|||||||
import React, { PureComponent } from 'react';
|
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 { Icon } from '../Icon/Icon';
|
||||||
import { cx, css } from 'emotion';
|
import { cx, css } from 'emotion';
|
||||||
|
|
||||||
@@ -134,7 +142,6 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
const { showDetails, showContext, hasHoverBackground } = this.state;
|
const { showDetails, showContext, hasHoverBackground } = this.state;
|
||||||
const style = getLogRowStyles(theme, row.logLevel);
|
const style = getLogRowStyles(theme, row.logLevel);
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const showUtc = timeZone === 'utc';
|
|
||||||
const hoverBackground = cx(style.logsRow, { [styles.hoverBackground]: hasHoverBackground });
|
const hoverBackground = cx(style.logsRow, { [styles.hoverBackground]: hasHoverBackground });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -156,16 +163,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{showTime && showUtc && (
|
{showTime && <td className={style.logsRowLocalTime}>{dateTimeFormat(row.timeEpochMs, { timeZone })}</td>}
|
||||||
<td className={style.logsRowLocalTime} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
|
||||||
{row.timeUtc}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{showTime && !showUtc && (
|
|
||||||
<td className={style.logsRowLocalTime} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
|
||||||
{row.timeLocal}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{showLabels && row.uniqueLabels && (
|
{showLabels && row.uniqueLabels && (
|
||||||
<td className={style.logsRowLabels}>
|
<td className={style.logsRowLabels}>
|
||||||
<LogLabels labels={row.uniqueLabels} />
|
<LogLabels labels={row.uniqueLabels} />
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect, useCallback } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import Calendar from 'react-calendar/dist/entry.nostyle';
|
import Calendar from 'react-calendar/dist/entry.nostyle';
|
||||||
import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data';
|
import { GrafanaTheme, DateTime, TimeZone, dateTimeParse } from '@grafana/data';
|
||||||
import { stringToDateTimeType } from '../time';
|
|
||||||
import { useTheme, stylesFactory } from '../../../themes';
|
import { useTheme, stylesFactory } from '../../../themes';
|
||||||
import { TimePickerTitle } from './TimePickerTitle';
|
import { TimePickerTitle } from './TimePickerTitle';
|
||||||
import { Button } from '../../Button';
|
import { Button } from '../../Button';
|
||||||
@@ -192,12 +191,13 @@ const getHeaderStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
from: string;
|
from: DateTime;
|
||||||
to: string;
|
to: DateTime;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
onChange: (from: string, to: string) => void;
|
onChange: (from: DateTime, to: DateTime) => void;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
|
timeZone?: TimeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
|
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
|
||||||
@@ -247,9 +247,10 @@ const Header = memo<Props>(({ onClose }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const Body = memo<Props>(({ onChange, from, to }) => {
|
const Body = memo<Props>(({ onChange, from, to, timeZone }) => {
|
||||||
const [value, setValue] = useState<Date[]>();
|
const [value, setValue] = useState<Date[]>();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const onCalendarChange = useOnCalendarChange(onChange, timeZone);
|
||||||
const styles = getBodyStyles(theme);
|
const styles = getBodyStyles(theme);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -266,7 +267,7 @@ const Body = memo<Props>(({ onChange, from, to }) => {
|
|||||||
value={value}
|
value={value}
|
||||||
nextLabel={<Icon name="angle-right" />}
|
nextLabel={<Icon name="angle-right" />}
|
||||||
prevLabel={<Icon name="angle-left" />}
|
prevLabel={<Icon name="angle-left" />}
|
||||||
onChange={value => valueToInput(value, onChange)}
|
onChange={onCalendarChange}
|
||||||
locale="en"
|
locale="en"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -288,11 +289,9 @@ const Footer = memo<Props>(({ onClose, onApply }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function inputToValue(from: string, to: string): Date[] {
|
function inputToValue(from: DateTime, to: DateTime): Date[] {
|
||||||
const fromAsDateTime = stringToDateTimeType(from);
|
const fromAsDate = from.toDate();
|
||||||
const toAsDateTime = stringToDateTimeType(to);
|
const toAsDate = to.toDate();
|
||||||
const fromAsDate = fromAsDateTime.isValid() ? fromAsDateTime.toDate() : new Date();
|
|
||||||
const toAsDate = toAsDateTime.isValid() ? toAsDateTime.toDate() : new Date();
|
|
||||||
|
|
||||||
if (fromAsDate > toAsDate) {
|
if (fromAsDate > toAsDate) {
|
||||||
return [toAsDate, fromAsDate];
|
return [toAsDate, fromAsDate];
|
||||||
@@ -300,10 +299,22 @@ function inputToValue(from: string, to: string): Date[] {
|
|||||||
return [fromAsDate, toAsDate];
|
return [fromAsDate, toAsDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueToInput(value: Date | Date[], onChange: (from: string, to: string) => void): void {
|
function useOnCalendarChange(onChange: (from: DateTime, to: DateTime) => void, timeZone?: TimeZone) {
|
||||||
const [from, to] = value;
|
return useCallback(
|
||||||
const fromAsString = dateTime(from).format(TIME_FORMAT);
|
(value: Date | Date[]) => {
|
||||||
const toAsString = dateTime(to).format(TIME_FORMAT);
|
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()];
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,14 @@
|
|||||||
import React, { FormEvent, useState, useCallback } from 'react';
|
import React, { FormEvent, useState, useCallback } from 'react';
|
||||||
import { TIME_FORMAT, TimeZone, isDateTime, TimeRange, DateTime, dateMath } from '@grafana/data';
|
import {
|
||||||
import { stringToDateTimeType, isValidTimeString } from '../time';
|
TimeZone,
|
||||||
import { mapStringsToTimeRange } from './mapper';
|
isDateTime,
|
||||||
|
TimeRange,
|
||||||
|
DateTime,
|
||||||
|
dateMath,
|
||||||
|
dateTimeFormat,
|
||||||
|
dateTimeParse,
|
||||||
|
rangeUtil,
|
||||||
|
} from '@grafana/data';
|
||||||
import { TimePickerCalendar } from './TimePickerCalendar';
|
import { TimePickerCalendar } from './TimePickerCalendar';
|
||||||
import { Field } from '../../Forms/Field';
|
import { Field } from '../../Forms/Field';
|
||||||
import { Input } from '../../Input/Input';
|
import { Input } from '../../Input/Input';
|
||||||
@@ -51,11 +58,17 @@ export const TimeRangeForm: React.FC<Props> = props => {
|
|||||||
if (to.invalid || from.invalid) {
|
if (to.invalid || from.invalid) {
|
||||||
return;
|
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]);
|
}, [from, to, roundup, timeZone]);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(from: string, to: string) => {
|
(from: DateTime, to: DateTime) => {
|
||||||
setFrom(valueToState(from, false, timeZone));
|
setFrom(valueToState(from, false, timeZone));
|
||||||
setTo(valueToState(to, true, timeZone));
|
setTo(valueToState(to, true, timeZone));
|
||||||
},
|
},
|
||||||
@@ -89,11 +102,12 @@ export const TimeRangeForm: React.FC<Props> = props => {
|
|||||||
<TimePickerCalendar
|
<TimePickerCalendar
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
from={from.value}
|
from={dateTimeParse(from.value, { timeZone })}
|
||||||
to={to.value}
|
to={dateTimeParse(to.value, { timeZone })}
|
||||||
onApply={onApply}
|
onApply={onApply}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
timeZone={timeZone}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -104,23 +118,27 @@ function eventToState(event: FormEvent<HTMLInputElement>, roundup?: boolean, tim
|
|||||||
}
|
}
|
||||||
|
|
||||||
function valueToState(raw: DateTime | string, roundup?: boolean, timeZone?: TimeZone): InputState {
|
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);
|
const invalid = !isValid(value, roundup, timeZone);
|
||||||
return { value, invalid };
|
return { value, invalid };
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueAsString(value: DateTime | string): string {
|
function valueAsString(value: DateTime | string, timeZone?: TimeZone): string {
|
||||||
if (isDateTime(value)) {
|
if (isDateTime(value)) {
|
||||||
return value.format(TIME_FORMAT);
|
return dateTimeFormat(value, { timeZone });
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValid(value: string, roundup?: boolean, timeZone?: TimeZone): boolean {
|
function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean {
|
||||||
if (dateMath.isMathString(value)) {
|
if (isDateTime(value)) {
|
||||||
return isValidTimeString(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();
|
return parsed.isValid();
|
||||||
}
|
}
|
||||||
|
@@ -69,7 +69,7 @@ const Options: React.FC<Props> = ({ options, value, onSelect, timeZone }) => {
|
|||||||
key={keyForOption(option, index)}
|
key={keyForOption(option, index)}
|
||||||
value={option}
|
value={option}
|
||||||
selected={isEqual(option, value)}
|
selected={isEqual(option, value)}
|
||||||
onSelect={option => onSelect(mapOptionToTimeRange(option, timeZone))}
|
onSelect={option => onSelect(mapOptionToTimeRange(option))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -214,15 +214,15 @@ exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
|
|||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
|
"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,
|
"section": 3,
|
||||||
"to": "2019-12-18 07:48:27",
|
"to": "2019-12-18T07:48:27Z",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
|
"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,
|
"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 [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
|
"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,
|
"section": 3,
|
||||||
"to": "2019-12-18 07:48:27",
|
"to": "2019-12-18T07:48:27Z",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
|
"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,
|
"section": 3,
|
||||||
"to": "2019-10-18 07:48:27",
|
"to": "2019-10-18T07:48:27Z",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,86 +1,17 @@
|
|||||||
import {
|
import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat, dateTimeFormatISO } from '@grafana/data';
|
||||||
TimeOption,
|
|
||||||
TimeRange,
|
|
||||||
isDateTime,
|
|
||||||
DateTime,
|
|
||||||
TimeZone,
|
|
||||||
dateMath,
|
|
||||||
dateTime,
|
|
||||||
dateTimeForTimeZone,
|
|
||||||
TIME_FORMAT,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { stringToDateTimeType } from '../time';
|
|
||||||
|
|
||||||
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
|
export const mapOptionToTimeRange = (option: TimeOption): TimeRange => {
|
||||||
return {
|
return rangeUtil.convertRawToRange({ from: option.from, to: option.to });
|
||||||
from: stringToDateTime(option.from, false, timeZone),
|
|
||||||
to: stringToDateTime(option.to, true, timeZone),
|
|
||||||
raw: {
|
|
||||||
from: option.from,
|
|
||||||
to: option.to,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => {
|
export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => {
|
||||||
const formattedFrom = stringToDateTime(range.from, false, timeZone).format(TIME_FORMAT);
|
const from = dateTimeFormat(range.from, { timeZone });
|
||||||
const formattedTo = stringToDateTime(range.to, true, timeZone).format(TIME_FORMAT);
|
const to = dateTimeFormat(range.to, { timeZone });
|
||||||
const from = dateTimeToString(range.from, timeZone);
|
|
||||||
const to = dateTimeToString(range.to, timeZone);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from,
|
from: dateTimeFormatISO(range.from, { timeZone }),
|
||||||
to,
|
to: dateTimeFormatISO(range.to, { timeZone }),
|
||||||
section: 3,
|
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);
|
|
||||||
};
|
|
||||||
|
@@ -13,7 +13,7 @@ import { stylesFactory } from '../../themes/stylesFactory';
|
|||||||
import { withTheme, useTheme } from '../../themes/ThemeContext';
|
import { withTheme, useTheme } from '../../themes/ThemeContext';
|
||||||
|
|
||||||
// Types
|
// 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 { TimeRange, TimeOption, TimeZone, dateMath } from '@grafana/data';
|
||||||
import { Themeable } from '../../types';
|
import { Themeable } from '../../types';
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content={<TimePickerTooltip timeRange={value} />} placement="bottom">
|
<Tooltip content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />} placement="bottom">
|
||||||
<button aria-label="TimePicker Open Button" className={timePickerButtonClass} onClick={this.onOpen}>
|
<button aria-label="TimePicker Open Button" className={timePickerButtonClass} onClick={this.onOpen}>
|
||||||
<Icon name="clock-nine" className={cx(styles.clockIcon, timePickerIconClass)} size="lg" />
|
<Icon name="clock-nine" className={cx(styles.clockIcon, timePickerIconClass)} size="lg" />
|
||||||
<TimePickerButtonLabel {...this.props} />
|
<TimePickerButtonLabel {...this.props} />
|
||||||
@@ -206,44 +206,36 @@ const ZoomOutTooltip = () => (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TimePickerTooltip = ({ timeRange }: { timeRange: TimeRange }) => (
|
const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => (
|
||||||
<>
|
<>
|
||||||
{timeRange.from.format(TIME_FORMAT)}
|
{dateTimeFormatWithAbbrevation(timeRange.from, { timeZone })}
|
||||||
<div className="text-center">to</div>
|
<div className="text-center">to</div>
|
||||||
{timeRange.to.format(TIME_FORMAT)}
|
{dateTimeFormatWithAbbrevation(timeRange.to, { timeZone })}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TimePickerButtonLabel = memo<Props>(props => {
|
const TimePickerButtonLabel = memo<Props>(({ hideText, value, timeZone }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getLabelStyles(theme);
|
const styles = getLabelStyles(theme);
|
||||||
const isUTC = props.timeZone === 'utc';
|
|
||||||
|
|
||||||
if (props.hideText) {
|
if (hideText) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.container}>
|
<span className={styles.container}>
|
||||||
<span>{formattedRange(props.value, isUTC)}</span>
|
<span>{formattedRange(value, timeZone)}</span>
|
||||||
{isUTC && <span className={styles.utc}>UTC</span>}
|
<span className={styles.utc}>{rangeUtil.describeTimeRangeAbbrevation(value, timeZone)}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedRange = (value: TimeRange, isUTC: boolean) => {
|
const formattedRange = (value: TimeRange, timeZone?: TimeZone) => {
|
||||||
const adjustedTimeRange = {
|
const adjustedTimeRange = {
|
||||||
to: dateMath.isMathString(value.raw.to) ? value.raw.to : adjustedTime(value.to, isUTC),
|
to: dateMath.isMathString(value.raw.to) ? value.raw.to : value.to,
|
||||||
from: dateMath.isMathString(value.raw.from) ? value.raw.from : adjustedTime(value.from, isUTC),
|
from: dateMath.isMathString(value.raw.from) ? value.raw.from : value.from,
|
||||||
};
|
};
|
||||||
return rangeUtil.describeTimeRange(adjustedTimeRange);
|
return rangeUtil.describeTimeRange(adjustedTimeRange, timeZone);
|
||||||
};
|
|
||||||
|
|
||||||
const adjustedTime = (time: DateTime, isUTC: boolean) => {
|
|
||||||
if (isUTC) {
|
|
||||||
return time.utc() || null;
|
|
||||||
}
|
|
||||||
return time.local() || null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TimeRangePicker = withTheme(UnthemedTimeRangePicker);
|
export const TimeRangePicker = withTheme(UnthemedTimeRangePicker);
|
||||||
|
@@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
TimeRange,
|
|
||||||
TIME_FORMAT,
|
|
||||||
RawTimeRange,
|
|
||||||
TimeZone,
|
|
||||||
rangeUtil,
|
|
||||||
dateMath,
|
|
||||||
isDateTime,
|
|
||||||
dateTime,
|
|
||||||
DateTime,
|
|
||||||
dateTimeForTimeZone,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
export const rawToTimeRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => {
|
|
||||||
const from = stringToDateTimeType(raw.from, false, timeZone);
|
|
||||||
const to = stringToDateTimeType(raw.to, true, timeZone);
|
|
||||||
|
|
||||||
return { from, to, raw };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stringToDateTimeType = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => {
|
|
||||||
if (isDateTime(value)) {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapTimeRangeToRangeString = (timeRange: RawTimeRange): string => {
|
|
||||||
return rangeUtil.describeTimeRange(timeRange);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isValidTimeString = (text: string) => dateMath.isValid(text);
|
|
@@ -68,6 +68,7 @@ export { GraphContextMenu } from './Graph/GraphContextMenu';
|
|||||||
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
||||||
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
||||||
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
|
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
|
||||||
|
export { graphTimeFormat, graphTimeFormatter } from './Graph/utils';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
LegendOptions,
|
LegendOptions,
|
||||||
|
@@ -32,6 +32,7 @@ import {
|
|||||||
standardEditorsRegistry,
|
standardEditorsRegistry,
|
||||||
standardFieldConfigEditorRegistry,
|
standardFieldConfigEditorRegistry,
|
||||||
standardTransformersRegistry,
|
standardTransformersRegistry,
|
||||||
|
setTimeZoneResolver,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar';
|
import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar';
|
||||||
@@ -93,6 +94,7 @@ export class GrafanaApp {
|
|||||||
const app = angular.module('grafana', []);
|
const app = angular.module('grafana', []);
|
||||||
|
|
||||||
setLocale(config.bootData.user.locale);
|
setLocale(config.bootData.user.locale);
|
||||||
|
setTimeZoneResolver(() => config.bootData.user.timeZone);
|
||||||
|
|
||||||
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
||||||
|
|
||||||
|
@@ -31,6 +31,7 @@ import {
|
|||||||
} from '../features/dashboard/components/SaveDashboard/SaveDashboardButton';
|
} from '../features/dashboard/components/SaveDashboard/SaveDashboardButton';
|
||||||
import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer';
|
import { VariableEditorContainer } from '../features/variables/editor/VariableEditorContainer';
|
||||||
import { SearchField, SearchResults, SearchWrapper, SearchResultsFilter } from '../features/search';
|
import { SearchField, SearchResults, SearchWrapper, SearchResultsFilter } from '../features/search';
|
||||||
|
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('footer', Footer, []);
|
react2AngularDirective('footer', Footer, []);
|
||||||
@@ -151,7 +152,7 @@ export function registerAngularDirectives() {
|
|||||||
'items',
|
'items',
|
||||||
['onClose', { watchDepth: 'reference', wrapApply: true }],
|
['onClose', { watchDepth: 'reference', wrapApply: true }],
|
||||||
['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }],
|
['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }],
|
||||||
['formatSourceDate', { watchDepth: 'reference', wrapApply: true }],
|
['timeZone', { watchDepth: 'reference', wrapApply: true }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// We keep the drilldown terminology here because of as using data-* directive
|
// We keep the drilldown terminology here because of as using data-* directive
|
||||||
@@ -201,4 +202,11 @@ export function registerAngularDirectives() {
|
|||||||
['onSaveSuccess', { watchDepth: 'reference', wrapApply: true }],
|
['onSaveSuccess', { watchDepth: 'reference', wrapApply: true }],
|
||||||
]);
|
]);
|
||||||
react2AngularDirective('variableEditorContainer', VariableEditorContainer, []);
|
react2AngularDirective('variableEditorContainer', VariableEditorContainer, []);
|
||||||
|
react2AngularDirective('timePickerSettings', TimePickerSettings, [
|
||||||
|
['getDashboard', { watchDepth: 'reference', wrapApply: true }],
|
||||||
|
['onTimeZoneChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
|
['onRefreshIntervalChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
|
['onNowDelayChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
|
['onHideTimePickerChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ const { Select } = LegacyForms;
|
|||||||
|
|
||||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
resourceUri: string;
|
resourceUri: string;
|
||||||
@@ -23,12 +24,18 @@ const themes = [
|
|||||||
{ value: 'light', label: 'Light' },
|
{ value: 'light', label: 'Light' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const timezones = [
|
const grafanaTimeZones = [
|
||||||
{ value: '', label: 'Default' },
|
{ value: '', label: 'Default' },
|
||||||
{ value: 'browser', label: 'Local browser time' },
|
{ value: 'browser', label: 'Local browser time' },
|
||||||
{ value: 'utc', label: 'UTC' },
|
{ 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);
|
||||||
|
|
||||||
export class SharedPreferences extends PureComponent<Props, State> {
|
export class SharedPreferences extends PureComponent<Props, State> {
|
||||||
backendSrv = backendSrv;
|
backendSrv = backendSrv;
|
||||||
|
|
||||||
@@ -91,12 +98,18 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
onThemeChanged = (theme: string) => {
|
onThemeChanged = (theme: SelectableValue<string>) => {
|
||||||
this.setState({ theme });
|
if (!theme || !theme.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ theme: theme.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onTimeZoneChanged = (timezone: string) => {
|
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
|
||||||
this.setState({ timezone });
|
if (!timezone || typeof timezone.value !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ timezone: timezone.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onHomeDashboardChanged = (dashboardId: number) => {
|
onHomeDashboardChanged = (dashboardId: number) => {
|
||||||
@@ -122,7 +135,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
value={themes.find(item => item.value === theme)}
|
value={themes.find(item => item.value === theme)}
|
||||||
options={themes}
|
options={themes}
|
||||||
onChange={theme => this.onThemeChanged(theme.value)}
|
onChange={this.onThemeChanged}
|
||||||
width={20}
|
width={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,10 +159,10 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<label className="gf-form-label width-11">Timezone</label>
|
<label className="gf-form-label width-11">Timezone</label>
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={true}
|
||||||
value={timezones.find(item => item.value === timezone)}
|
value={timeZones.find(item => item.value === timezone)}
|
||||||
onChange={timezone => this.onTimeZoneChanged(timezone.value)}
|
onChange={this.onTimeZoneChanged}
|
||||||
options={timezones}
|
options={timeZones}
|
||||||
width={20}
|
width={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LocalStorageValueProvider } from '../LocalStorageValueProvider';
|
import { LocalStorageValueProvider } from '../LocalStorageValueProvider';
|
||||||
import { TimeRange, isDateTime, dateTime } from '@grafana/data';
|
import { TimeRange, isDateTime, toUtc } from '@grafana/data';
|
||||||
import { Props as TimePickerProps, TimeRangePicker } from '@grafana/ui/src/components/TimePicker/TimeRangePicker';
|
import { Props as TimePickerProps, TimeRangePicker } from '@grafana/ui/src/components/TimePicker/TimeRangePicker';
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history';
|
const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history';
|
||||||
@@ -33,8 +33,8 @@ function convertIfJson(history: TimeRange[]): TimeRange[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: dateTime(time.from),
|
from: toUtc(time.from),
|
||||||
to: dateTime(time.to),
|
to: toUtc(time.to),
|
||||||
raw: time.raw,
|
raw: time.raw,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -16,7 +16,8 @@ import {
|
|||||||
LogsMetaKind,
|
LogsMetaKind,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
GraphSeriesXY,
|
GraphSeriesXY,
|
||||||
toUtc,
|
dateTimeFormat,
|
||||||
|
dateTimeFormatTimeAgo,
|
||||||
NullValueMode,
|
NullValueMode,
|
||||||
toDataFrame,
|
toDataFrame,
|
||||||
FieldCache,
|
FieldCache,
|
||||||
@@ -250,8 +251,6 @@ function separateLogsAndMetrics(dataFrames: DataFrame[]) {
|
|||||||
return { logSeries, metricSeries };
|
return { logSeries, metricSeries };
|
||||||
}
|
}
|
||||||
|
|
||||||
const logTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
|
|
||||||
interface LogFields {
|
interface LogFields {
|
||||||
series: DataFrame;
|
series: DataFrame;
|
||||||
|
|
||||||
@@ -333,10 +332,10 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
|
|||||||
rowIndex: j,
|
rowIndex: j,
|
||||||
dataFrame: series,
|
dataFrame: series,
|
||||||
logLevel,
|
logLevel,
|
||||||
timeFromNow: time.fromNow(),
|
timeFromNow: dateTimeFormatTimeAgo(ts),
|
||||||
timeEpochMs: time.valueOf(),
|
timeEpochMs: time.valueOf(),
|
||||||
timeLocal: time.format(logTimeFormat),
|
timeLocal: dateTimeFormat(ts, { timeZone: 'browser' }),
|
||||||
timeUtc: toUtc(time.valueOf()).format(logTimeFormat),
|
timeUtc: dateTimeFormat(ts, { timeZone: 'utc' }),
|
||||||
uniqueLabels,
|
uniqueLabels,
|
||||||
hasAnsi,
|
hasAnsi,
|
||||||
searchWords,
|
searchWords,
|
||||||
|
@@ -9,7 +9,6 @@ import {
|
|||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
dateMath,
|
dateMath,
|
||||||
DefaultTimeZone,
|
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
IntervalValues,
|
IntervalValues,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
toUtc,
|
toUtc,
|
||||||
ExploreMode,
|
ExploreMode,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
|
DefaultTimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
@@ -115,7 +115,8 @@ export function buildQueryTransaction(
|
|||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
queryOptions: QueryOptions,
|
queryOptions: QueryOptions,
|
||||||
range: TimeRange,
|
range: TimeRange,
|
||||||
scanning: boolean
|
scanning: boolean,
|
||||||
|
timeZone?: TimeZone
|
||||||
): QueryTransaction {
|
): QueryTransaction {
|
||||||
const configuredQueries = queries.map(query => ({ ...query, ...queryOptions }));
|
const configuredQueries = queries.map(query => ({ ...query, ...queryOptions }));
|
||||||
const key = queries.reduce((combinedKey, query) => {
|
const key = queries.reduce((combinedKey, query) => {
|
||||||
@@ -135,7 +136,7 @@ export function buildQueryTransaction(
|
|||||||
app: CoreApp.Explore,
|
app: CoreApp.Explore,
|
||||||
dashboardId: 0,
|
dashboardId: 0,
|
||||||
// TODO probably should be taken from preferences but does not seem to be used anyway.
|
// TODO probably should be taken from preferences but does not seem to be used anyway.
|
||||||
timezone: DefaultTimeZone,
|
timezone: timeZone || DefaultTimeZone,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
interval,
|
interval,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { DataQuery, DataSourceApi, ExploreMode, dateTime, AppEvents, urlUtil } from '@grafana/data';
|
import { DataQuery, DataSourceApi, ExploreMode, dateTimeFormat, AppEvents, urlUtil } from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { serializeStateToUrlParam, SortOrder } from './explore';
|
import { serializeStateToUrlParam, SortOrder } from './explore';
|
||||||
@@ -237,7 +237,9 @@ export const createRetentionPeriodBoundary = (days: number, isLastTs: boolean) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function createDateStringFromTs(ts: number) {
|
export function createDateStringFromTs(ts: number) {
|
||||||
return dateTime(ts).format('MMMM D');
|
return dateTimeFormat(ts, {
|
||||||
|
format: 'MMMM D',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQueryDisplayText(query: DataQuery): string {
|
export function getQueryDisplayText(query: DataQuery): string {
|
||||||
|
@@ -165,21 +165,21 @@ export function grafanaTimeFormat(ticks: number, min: number, max: number) {
|
|||||||
const oneYear = 31536000000;
|
const oneYear = 31536000000;
|
||||||
|
|
||||||
if (secPerTick <= 45) {
|
if (secPerTick <= 45) {
|
||||||
return '%H:%M:%S';
|
return 'HH:mm:ss';
|
||||||
}
|
}
|
||||||
if (secPerTick <= 7200 || range <= oneDay) {
|
if (secPerTick <= 7200 || range <= oneDay) {
|
||||||
return '%H:%M';
|
return 'HH:mm';
|
||||||
}
|
}
|
||||||
if (secPerTick <= 80000) {
|
if (secPerTick <= 80000) {
|
||||||
return '%m/%d %H:%M';
|
return 'MM/DD HH:mm';
|
||||||
}
|
}
|
||||||
if (secPerTick <= 2419200 || range <= oneYear) {
|
if (secPerTick <= 2419200 || range <= oneYear) {
|
||||||
return '%m/%d';
|
return 'MM/DD';
|
||||||
}
|
}
|
||||||
return '%Y-%m';
|
return 'YYYY-MM';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '%H:%M';
|
return 'HH:mm';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,7 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
|||||||
import { NavModelSrv } from 'app/core/core';
|
import { NavModelSrv } from 'app/core/core';
|
||||||
import { User } from 'app/core/services/context_srv';
|
import { User } from 'app/core/services/context_srv';
|
||||||
import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types';
|
import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormatTimeAgo, dateTimeFormat } from '@grafana/data';
|
||||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||||
|
|
||||||
export default class AdminEditUserCtrl {
|
export default class AdminEditUserCtrl {
|
||||||
@@ -47,8 +47,8 @@ export default class AdminEditUserCtrl {
|
|||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
isActive: session.isActive,
|
isActive: session.isActive,
|
||||||
seenAt: dateTime(session.seenAt).fromNow(),
|
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||||
clientIp: session.clientIp,
|
clientIp: session.clientIp,
|
||||||
browser: session.browser,
|
browser: session.browser,
|
||||||
browserVersion: session.browserVersion,
|
browserVersion: session.browserVersion,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormat } from '@grafana/data';
|
||||||
import { SyncInfo, UserDTO } from 'app/types';
|
import { SyncInfo, UserDTO } from 'app/types';
|
||||||
import { Button, LinkButton } from '@grafana/ui';
|
import { Button, LinkButton } from '@grafana/ui';
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ interface Props {
|
|||||||
|
|
||||||
interface State {}
|
interface State {}
|
||||||
|
|
||||||
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||||
const debugLDAPMappingBaseURL = '/admin/ldap';
|
const debugLDAPMappingBaseURL = '/admin/ldap';
|
||||||
|
|
||||||
export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
||||||
@@ -21,9 +21,10 @@ export class UserLdapSyncInfo extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ldapSyncInfo, user } = this.props;
|
const { ldapSyncInfo, user } = this.props;
|
||||||
const nextSyncTime = dateTime(ldapSyncInfo.nextSync).format(syncTimeFormat);
|
|
||||||
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
||||||
const prevSyncTime = prevSyncSuccessful ? dateTime(ldapSyncInfo.prevSync.started).format(syncTimeFormat) : '';
|
const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync;
|
||||||
|
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : '';
|
||||||
|
const prevSyncTime = prevSyncSuccessful ? dateTimeFormat(ldapSyncInfo.prevSync.started, { format }) : '';
|
||||||
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?user=${user && user.login}`;
|
const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?user=${user && user.login}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormat } from '@grafana/data';
|
||||||
import { LdapUserSyncInfo } from 'app/types';
|
import { LdapUserSyncInfo } from 'app/types';
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ interface State {
|
|||||||
isSyncing: boolean;
|
isSyncing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||||
|
|
||||||
export class UserSyncInfo extends PureComponent<Props, State> {
|
export class UserSyncInfo extends PureComponent<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
@@ -35,9 +35,10 @@ export class UserSyncInfo extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { syncInfo, disableSync } = this.props;
|
const { syncInfo, disableSync } = this.props;
|
||||||
const { isSyncing } = this.state;
|
const { isSyncing } = this.state;
|
||||||
const nextSyncTime = syncInfo.nextSync ? dateTime(syncInfo.nextSync).format(syncTimeFormat) : '';
|
const nextSyncSuccessful = syncInfo && syncInfo.nextSync;
|
||||||
|
const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(syncInfo.nextSync, { format }) : '';
|
||||||
const prevSyncSuccessful = syncInfo && syncInfo.prevSync;
|
const prevSyncSuccessful = syncInfo && syncInfo.prevSync;
|
||||||
const prevSyncTime = prevSyncSuccessful ? dateTime(syncInfo.prevSync).format(syncTimeFormat) : '';
|
const prevSyncTime = prevSyncSuccessful ? dateTimeFormat(syncInfo.prevSync, { format }) : '';
|
||||||
const isDisabled = isSyncing || disableSync;
|
const isDisabled = isSyncing || disableSync;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormat } from '@grafana/data';
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
import { SyncInfo } from 'app/types';
|
import { SyncInfo } from 'app/types';
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ interface State {
|
|||||||
isSyncing: boolean;
|
isSyncing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncTimeFormat = 'dddd YYYY-MM-DD HH:mm zz';
|
const format = 'dddd YYYY-MM-DD HH:mm zz';
|
||||||
|
|
||||||
export class LdapSyncInfo extends PureComponent<Props, State> {
|
export class LdapSyncInfo extends PureComponent<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
@@ -26,9 +26,9 @@ export class LdapSyncInfo extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { ldapSyncInfo } = this.props;
|
const { ldapSyncInfo } = this.props;
|
||||||
const { isSyncing } = this.state;
|
const { isSyncing } = this.state;
|
||||||
const nextSyncTime = dateTime(ldapSyncInfo.nextSync).format(syncTimeFormat);
|
const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format });
|
||||||
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
const prevSyncSuccessful = ldapSyncInfo && ldapSyncInfo.prevSync;
|
||||||
const prevSyncTime = prevSyncSuccessful ? dateTime(ldapSyncInfo.prevSync.started).format(syncTimeFormat) : '';
|
const prevSyncTime = prevSyncSuccessful ? dateTimeFormat(ldapSyncInfo.prevSync!.started, { format }) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { updateLocation } from 'app/core/actions';
|
import { updateLocation } from 'app/core/actions';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
||||||
|
|
||||||
@@ -141,8 +141,8 @@ export function loadUserSessions(userId: number): ThunkResult<void> {
|
|||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
isActive: session.isActive,
|
isActive: session.isActive,
|
||||||
seenAt: dateTime(session.seenAt).fromNow(),
|
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||||
clientIp: session.clientIp,
|
clientIp: session.clientIp,
|
||||||
browser: session.browser,
|
browser: session.browser,
|
||||||
browserVersion: session.browserVersion,
|
browserVersion: session.browserVersion,
|
||||||
|
@@ -22,7 +22,7 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
const { Input, Switch } = LegacyForms;
|
const { Input, Switch } = LegacyForms;
|
||||||
import { dateTime, isDateTime, NavModel } from '@grafana/data';
|
import { NavModel, dateTimeFormat } from '@grafana/data';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||||
import { store } from 'app/store/store';
|
import { store } from 'app/store/store';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
@@ -174,11 +174,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
|||||||
if (!date) {
|
if (!date) {
|
||||||
return 'No expiration date';
|
return 'No expiration date';
|
||||||
}
|
}
|
||||||
date = isDateTime(date) ? date : dateTime(date);
|
const timeZone = getTimeZone(store.getState().user);
|
||||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
return dateTimeFormat(date, { format, timeZone });
|
||||||
const timezone = getTimeZone(store.getState().user);
|
|
||||||
|
|
||||||
return timezone === 'utc' ? date.utc().format(format) : date.format(format);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAddApiKeyForm() {
|
renderAddApiKeyForm() {
|
||||||
|
@@ -10,7 +10,7 @@ import { backendSrv } from 'app/core/services/backend_srv';
|
|||||||
import { DashboardSrv } from '../../services/DashboardSrv';
|
import { DashboardSrv } from '../../services/DashboardSrv';
|
||||||
import { CoreEvents } from 'app/types';
|
import { CoreEvents } from 'app/types';
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
import { AppEvents, locationUtil } from '@grafana/data';
|
import { AppEvents, locationUtil, TimeZone } from '@grafana/data';
|
||||||
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
||||||
|
|
||||||
export class SettingsCtrl {
|
export class SettingsCtrl {
|
||||||
@@ -255,6 +255,22 @@ export class SettingsCtrl {
|
|||||||
getDashboard = () => {
|
getDashboard = () => {
|
||||||
return this.dashboard;
|
return this.dashboard;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onRefreshIntervalChange = (intervals: string[]) => {
|
||||||
|
this.dashboard.timepicker.refresh_intervals = intervals;
|
||||||
|
};
|
||||||
|
|
||||||
|
onNowDelayChange = (nowDelay: string) => {
|
||||||
|
this.dashboard.timepicker.nowDelay = nowDelay;
|
||||||
|
};
|
||||||
|
|
||||||
|
onHideTimePickerChange = (hide: boolean) => {
|
||||||
|
this.dashboard.timepicker.hidden = hide;
|
||||||
|
};
|
||||||
|
|
||||||
|
onTimeZoneChange = (timeZone: TimeZone) => {
|
||||||
|
this.dashboard.timezone = timeZone;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dashboardSettings() {
|
export function dashboardSettings() {
|
||||||
|
@@ -1,82 +0,0 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
|
||||||
import { config } from 'app/core/config';
|
|
||||||
import kbn from 'app/core/utils/kbn';
|
|
||||||
|
|
||||||
export class TimePickerCtrl {
|
|
||||||
panel: any;
|
|
||||||
dashboard: DashboardModel;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.panel = this.dashboard.timepicker;
|
|
||||||
this.panel.refresh_intervals = this.panel.refresh_intervals || [
|
|
||||||
'5s',
|
|
||||||
'10s',
|
|
||||||
'30s',
|
|
||||||
'1m',
|
|
||||||
'5m',
|
|
||||||
'15m',
|
|
||||||
'30m',
|
|
||||||
'1h',
|
|
||||||
'2h',
|
|
||||||
'1d',
|
|
||||||
];
|
|
||||||
if (config.minRefreshInterval) {
|
|
||||||
this.panel.refresh_intervals = this.filterRefreshRates(this.panel.refresh_intervals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterRefreshRates(refreshRates: string[]) {
|
|
||||||
return refreshRates.filter(rate => {
|
|
||||||
return kbn.interval_to_ms(rate) > kbn.interval_to_ms(config.minRefreshInterval);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<div class="editor-row">
|
|
||||||
<h5 class="section-heading">Time Options</h5>
|
|
||||||
|
|
||||||
<div class="gf-form-group">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-10">Timezone</label>
|
|
||||||
<div class="gf-form-select-wrapper">
|
|
||||||
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in
|
|
||||||
[{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
|
||||||
<span class="gf-form-label width-10">Auto-refresh</span>
|
|
||||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<span class="gf-form-label width-10">Now delay now-</span>
|
|
||||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.nowDelay"
|
|
||||||
placeholder="0m"
|
|
||||||
valid-time-span
|
|
||||||
bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
|
|
||||||
data-placement="right">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<gf-form-switch class="gf-form" label="Hide time picker" checked="ctrl.panel.hidden" label-class="width-10">
|
|
||||||
</gf-form-switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function TimePickerSettings() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
template: template,
|
|
||||||
controller: TimePickerCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
scope: {
|
|
||||||
dashboard: '=',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('gfTimePickerSettings', TimePickerSettings);
|
|
@@ -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<Props, State> {
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
if (!event.currentTarget.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const intervals = event.currentTarget.value.split(',');
|
||||||
|
this.props.onRefreshIntervalChange(intervals);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNowDelayChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
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<string>) => {
|
||||||
|
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 (
|
||||||
|
<div className="editor-row">
|
||||||
|
<h5 className="section-heading">Time Options</h5>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label width-7">Timezone</label>
|
||||||
|
<Select isSearchable={true} value={value} onChange={this.onTimeZoneChange} options={timeZones} width={40} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label width-7">Auto-refresh</span>
|
||||||
|
<Input width={60} value={this.getRefreshIntervals()} onChange={this.onRefreshIntervalChange} />
|
||||||
|
</div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<span className="gf-form-label width-7">Now delay now-</span>
|
||||||
|
<Tooltip
|
||||||
|
placement="right"
|
||||||
|
content={'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
width={60}
|
||||||
|
invalid={!this.state.isNowDelayValid}
|
||||||
|
placeholder="0m"
|
||||||
|
onChange={this.onNowDelayChange}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<LegacyForms.Switch
|
||||||
|
labelClass="width-7"
|
||||||
|
label="Hide time picker"
|
||||||
|
checked={dashboard.timepicker.hidden ?? false}
|
||||||
|
onChange={this.onHideTimePickerChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -50,7 +50,7 @@
|
|||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-time-picker-settings dashboard="ctrl.dashboard"></gf-time-picker-settings>
|
<time-picker-settings getDashboard="ctrl.getDashboard" onTimeZoneChange="ctrl.onTimeZoneChange" onRefreshIntervalChange="ctrl.onRefreshIntervalChange" onNowDelayChange="ctrl.onNowDelayChange" onHideTimePickerChange="ctrl.onHideTimePickerChange"></time-picker-settings>
|
||||||
|
|
||||||
<h5 class="section-heading">Panel Options</h5>
|
<h5 class="section-heading">Panel Options</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
|
@@ -21,6 +21,7 @@ import {
|
|||||||
PanelPlugin,
|
PanelPlugin,
|
||||||
QueryResultMetaStat,
|
QueryResultMetaStat,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { getPanelInspectorStyles } from './styles';
|
import { getPanelInspectorStyles } from './styles';
|
||||||
@@ -235,6 +236,8 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dashboard } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ paddingBottom: '16px' }}>
|
<div style={{ paddingBottom: '16px' }}>
|
||||||
<div className="section-heading">{name}</div>
|
<div className="section-heading">{name}</div>
|
||||||
@@ -244,7 +247,7 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<tr key={`${stat.title}-${index}`}>
|
<tr key={`${stat.title}-${index}`}>
|
||||||
<td>{stat.title}</td>
|
<td>{stat.title}</td>
|
||||||
<td style={{ textAlign: 'right' }}>{formatStat(stat)}</td>
|
<td style={{ textAlign: 'right' }}>{formatStat(stat, dashboard.getTimezone())}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -331,13 +334,14 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatStat(stat: QueryResultMetaStat): string {
|
function formatStat(stat: QueryResultMetaStat, timeZone?: TimeZone): string {
|
||||||
const display = getDisplayProcessor({
|
const display = getDisplayProcessor({
|
||||||
field: {
|
field: {
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
config: stat,
|
config: stat,
|
||||||
},
|
},
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
return formattedValueToString(display(stat.value));
|
return formattedValueToString(display(stat.value));
|
||||||
}
|
}
|
||||||
|
@@ -47,6 +47,7 @@ describe('HistoryListCtrl', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
version: 3,
|
version: 3,
|
||||||
formatDate: jest.fn(() => 'date'),
|
formatDate: jest.fn(() => 'date'),
|
||||||
|
getRelativeTime: jest.fn(() => 'time ago'),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,6 +149,7 @@ describe('HistoryListCtrl', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
version: 3,
|
version: 3,
|
||||||
formatDate: jest.fn(() => 'date'),
|
formatDate: jest.fn(() => 'date'),
|
||||||
|
getRelativeTime: jest.fn(() => 'time ago'),
|
||||||
};
|
};
|
||||||
|
|
||||||
historySrv.calculateDiff = jest.fn(() => Promise.resolve(versionsResponse));
|
historySrv.calculateDiff = jest.fn(() => Promise.resolve(versionsResponse));
|
||||||
|
@@ -3,7 +3,7 @@ import angular, { ILocationService, IScope } from 'angular';
|
|||||||
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
import { CalculateDiffOptions, HistoryListOpts, HistorySrv, RevisionsModel } from './HistorySrv';
|
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 { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
import { CoreEvents } from 'app/types';
|
import { CoreEvents } from 'app/types';
|
||||||
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
import { promiseToDigest } from '../../../../core/utils/promiseToDigest';
|
||||||
@@ -76,9 +76,7 @@ export class HistoryListCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatBasicDate(date: DateTimeInput) {
|
formatBasicDate(date: DateTimeInput) {
|
||||||
const now = this.dashboard.timezone === 'browser' ? dateTime() : toUtc();
|
return this.dashboard.getRelativeTime(date);
|
||||||
const then = this.dashboard.timezone === 'browser' ? dateTime(date) : toUtc(date);
|
|
||||||
return then.from(now);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDiff(diff: 'basic' | 'json') {
|
getDiff(diff: 'basic' | 'json') {
|
||||||
|
@@ -12,14 +12,13 @@ import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel';
|
|||||||
import { DashboardMigrator } from './DashboardMigrator';
|
import { DashboardMigrator } from './DashboardMigrator';
|
||||||
import {
|
import {
|
||||||
AppEvent,
|
AppEvent,
|
||||||
dateTime,
|
|
||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
isDateTime,
|
|
||||||
PanelEvents,
|
PanelEvents,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
toUtc,
|
|
||||||
UrlQueryValue,
|
UrlQueryValue,
|
||||||
|
dateTimeFormat,
|
||||||
|
dateTimeFormatTimeAgo,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
|
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
|
||||||
import { getConfig } from '../../../core/config';
|
import { getConfig } from '../../../core/config';
|
||||||
@@ -814,11 +813,10 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatDate(date: DateTimeInput, format?: string) {
|
formatDate(date: DateTimeInput, format?: string) {
|
||||||
date = isDateTime(date) ? date : dateTime(date);
|
return dateTimeFormat(date, {
|
||||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
format,
|
||||||
const timezone = this.getTimezone();
|
timeZone: this.getTimezone(),
|
||||||
|
});
|
||||||
return timezone === 'browser' ? dateTime(date).format(format) : toUtc(date).format(format);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -933,13 +931,9 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRelativeTime(date: DateTimeInput) {
|
getRelativeTime(date: DateTimeInput) {
|
||||||
date = isDateTime(date) ? date : dateTime(date);
|
return dateTimeFormatTimeAgo(date, {
|
||||||
|
timeZone: this.getTimezone(),
|
||||||
return this.timezone === 'browser' ? dateTime(date).fromNow() : toUtc(date).fromNow();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
isTimezoneUtc() {
|
|
||||||
return this.getTimezone() === 'utc';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSnapshot() {
|
isSnapshot() {
|
||||||
@@ -947,7 +941,7 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTimezone(): TimeZone {
|
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) {
|
private updateSchema(old: any) {
|
||||||
|
@@ -24,6 +24,7 @@ import {
|
|||||||
ScopedVars,
|
ScopedVars,
|
||||||
applyFieldOverrides,
|
applyFieldOverrides,
|
||||||
DataConfigSource,
|
DataConfigSource,
|
||||||
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
export interface QueryRunnerOptions<
|
export interface QueryRunnerOptions<
|
||||||
@@ -56,6 +57,7 @@ export class PanelQueryRunner {
|
|||||||
private subscription?: Unsubscribable;
|
private subscription?: Unsubscribable;
|
||||||
private lastResult?: PanelData;
|
private lastResult?: PanelData;
|
||||||
private dataConfigSource: DataConfigSource;
|
private dataConfigSource: DataConfigSource;
|
||||||
|
private timeZone?: TimeZone;
|
||||||
|
|
||||||
constructor(dataConfigSource: DataConfigSource) {
|
constructor(dataConfigSource: DataConfigSource) {
|
||||||
this.subject = new ReplaySubject(1);
|
this.subject = new ReplaySubject(1);
|
||||||
@@ -90,6 +92,7 @@ export class PanelQueryRunner {
|
|||||||
processedData = {
|
processedData = {
|
||||||
...processedData,
|
...processedData,
|
||||||
series: applyFieldOverrides({
|
series: applyFieldOverrides({
|
||||||
|
timeZone: this.timeZone,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
data: processedData.series,
|
data: processedData.series,
|
||||||
...fieldConfig,
|
...fieldConfig,
|
||||||
@@ -118,6 +121,8 @@ export class PanelQueryRunner {
|
|||||||
minInterval,
|
minInterval,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
this.timeZone = timezone;
|
||||||
|
|
||||||
if (isSharedDashboardQuery(datasource)) {
|
if (isSharedDashboardQuery(datasource)) {
|
||||||
this.pipeToSubject(runSharedRequest(options));
|
this.pipeToSubject(runSharedRequest(options));
|
||||||
return;
|
return;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTimeForTimeZone } from '@grafana/data';
|
import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTime } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectThemeVariant,
|
selectThemeVariant,
|
||||||
@@ -106,13 +106,14 @@ class UnThemedExploreGraphPanel extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeRange = {
|
const timeRange = {
|
||||||
from: dateTimeForTimeZone(timeZone, absoluteRange.from),
|
from: dateTime(absoluteRange.from),
|
||||||
to: dateTimeForTimeZone(timeZone, absoluteRange.to),
|
to: dateTime(absoluteRange.to),
|
||||||
raw: {
|
raw: {
|
||||||
from: dateTimeForTimeZone(timeZone, absoluteRange.from),
|
from: dateTime(absoluteRange.from),
|
||||||
to: dateTimeForTimeZone(timeZone, absoluteRange.to),
|
to: dateTime(absoluteRange.to),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const height = showPanel === false ? 100 : showingGraph && showingTable ? 200 : 400;
|
const height = showPanel === false ? 100 : showingGraph && showingTable ? 200 : 400;
|
||||||
const lineWidth = showLines ? 1 : 5;
|
const lineWidth = showLines ? 1 : 5;
|
||||||
const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES);
|
const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES);
|
||||||
|
@@ -3,7 +3,7 @@ import { css, cx } from 'emotion';
|
|||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
import { Themeable, withTheme, getLogRowStyles, Icon } from '@grafana/ui';
|
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';
|
import ElapsedTime from './ElapsedTime';
|
||||||
|
|
||||||
@@ -137,7 +137,6 @@ class LiveLogs extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { theme, timeZone, onPause, onResume, isPaused } = this.props;
|
const { theme, timeZone, onPause, onResume, isPaused } = this.props;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const showUtc = timeZone === 'utc';
|
|
||||||
const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
|
const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -151,16 +150,7 @@ class LiveLogs extends PureComponent<Props, State> {
|
|||||||
{this.rowsToRender().map((row: LogRowModel) => {
|
{this.rowsToRender().map((row: LogRowModel) => {
|
||||||
return (
|
return (
|
||||||
<tr className={cx(logsRow, styles.logsRowFade)} key={row.uid}>
|
<tr className={cx(logsRow, styles.logsRowFade)} key={row.uid}>
|
||||||
{showUtc && (
|
<td className={cx(logsRowLocalTime)}>{dateTimeFormat(row.timeEpochMs, { timeZone })}</td>
|
||||||
<td className={cx(logsRowLocalTime)} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
|
||||||
{row.timeUtc}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{!showUtc && (
|
|
||||||
<td className={cx(logsRowLocalTime)} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
|
||||||
{row.timeLocal}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td className={cx(logsRowMessage)}>{row.entry}</td>
|
<td className={cx(logsRowMessage)}>{row.entry}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
@@ -468,8 +468,8 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const datasourceName = exploreItemState.requestedDatasourceName;
|
const datasourceName = exploreItemState.requestedDatasourceName;
|
||||||
|
const timeZone = getTimeZone(getState().user);
|
||||||
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning);
|
const transaction = buildQueryTransaction(queries, queryOptions, range, scanning, timeZone);
|
||||||
|
|
||||||
let firstResponse = true;
|
let firstResponse = true;
|
||||||
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
||||||
|
@@ -1,24 +1,7 @@
|
|||||||
const realMomentWrapper = jest.requireActual('@grafana/data/src/datetime/moment_wrapper');
|
jest.mock('@grafana/data/src/datetime/formatter', () => ({
|
||||||
|
dateTimeFormat: () => 'format() jest mocked',
|
||||||
jest.mock('@grafana/data/src/datetime/moment_wrapper', () => {
|
dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked',
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
import { ResultProcessor } from './ResultProcessor';
|
import { ResultProcessor } from './ResultProcessor';
|
||||||
import { ExploreItemState } from 'app/types/explore';
|
import { ExploreItemState } from 'app/types/explore';
|
||||||
|
@@ -92,6 +92,7 @@ export class ResultProcessor {
|
|||||||
field.display = getDisplayProcessor({
|
field.display = getDisplayProcessor({
|
||||||
field,
|
field,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
|
timeZone: this.timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormat } from '@grafana/data';
|
||||||
import { Forms, Form } from '@grafana/ui';
|
import { Forms, Form } from '@grafana/ui';
|
||||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||||
import { ImportDashboardForm } from './ImportDashboardForm';
|
import { ImportDashboardForm } from './ImportDashboardForm';
|
||||||
@@ -73,7 +73,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Updated on</td>
|
<td>Updated on</td>
|
||||||
<td>{dateTime(meta.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</td>
|
<td>{dateTimeFormat(meta.updatedAt)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -185,7 +185,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
queries: panel.targets,
|
queries: panel.targets,
|
||||||
panelId: panel.id,
|
panelId: panel.id,
|
||||||
dashboardId: this.dashboard.id,
|
dashboardId: this.dashboard.id,
|
||||||
timezone: this.dashboard.timezone,
|
timezone: this.dashboard.getTimezone(),
|
||||||
timeInfo: this.timeInfo,
|
timeInfo: this.timeInfo,
|
||||||
timeRange: this.range,
|
timeRange: this.range,
|
||||||
widthPixels: this.width,
|
widthPixels: this.width,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { coreModule, NavModelSrv } from 'app/core/core';
|
import { coreModule, NavModelSrv } from 'app/core/core';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||||
import { UserSession } from 'app/types';
|
import { UserSession } from 'app/types';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||||
@@ -36,8 +36,8 @@ export class ProfileCtrl {
|
|||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
isActive: session.isActive,
|
isActive: session.isActive,
|
||||||
seenAt: dateTime(session.seenAt).fromNow(),
|
seenAt: dateTimeFormatTimeAgo(session.seenAt),
|
||||||
createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'),
|
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }),
|
||||||
clientIp: session.clientIp,
|
clientIp: session.clientIp,
|
||||||
browser: session.browser,
|
browser: session.browser,
|
||||||
browserVersion: session.browserVersion,
|
browserVersion: session.browserVersion,
|
||||||
|
@@ -46,7 +46,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getValues = (): FieldDisplay[] => {
|
getValues = (): FieldDisplay[] => {
|
||||||
const { data, options, replaceVariables, fieldConfig } = this.props;
|
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||||
return getFieldDisplayValues({
|
return getFieldDisplayValues({
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
reduceOptions: options.reduceOptions,
|
reduceOptions: options.reduceOptions,
|
||||||
@@ -54,6 +54,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
|||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
data: data.series,
|
data: data.series,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -39,7 +39,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getValues = (): FieldDisplay[] => {
|
getValues = (): FieldDisplay[] => {
|
||||||
const { data, options, replaceVariables, fieldConfig } = this.props;
|
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||||
return getFieldDisplayValues({
|
return getFieldDisplayValues({
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
reduceOptions: options.reduceOptions,
|
reduceOptions: options.reduceOptions,
|
||||||
@@ -47,6 +47,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
|||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
data: data.series,
|
data: data.series,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import { GraphLegendProps, Legend } from './Legend/Legend';
|
import { GraphLegendProps, Legend } from './Legend/Legend';
|
||||||
|
|
||||||
import { GraphCtrl } from './module';
|
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 { provideTheme, getCurrentTheme } from 'app/core/utils/ConfigProvider';
|
||||||
import {
|
import {
|
||||||
toUtc,
|
toUtc,
|
||||||
@@ -270,6 +270,7 @@ class GraphElement {
|
|||||||
const fieldDisplay = getDisplayProcessor({
|
const fieldDisplay = getDisplayProcessor({
|
||||||
field: { config: fieldConfig, type: FieldType.number },
|
field: { config: fieldConfig, type: FieldType.number },
|
||||||
theme: getCurrentTheme(),
|
theme: getCurrentTheme(),
|
||||||
|
timeZone: this.dashboard.getTimezone(),
|
||||||
})(field.values.get(dataIndex));
|
})(field.values.get(dataIndex));
|
||||||
linksSupplier = links.length
|
linksSupplier = links.length
|
||||||
? getFieldLinksSupplier({
|
? getFieldLinksSupplier({
|
||||||
@@ -643,7 +644,8 @@ class GraphElement {
|
|||||||
max: max,
|
max: max,
|
||||||
label: 'Datetime',
|
label: 'Datetime',
|
||||||
ticks: ticks,
|
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));
|
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 */
|
/** @ngInject */
|
||||||
|
@@ -12,14 +12,7 @@ import { axesEditorComponent } from './axes_editor';
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
|
import { getProcessedDataFrames } from 'app/features/dashboard/state/runRequest';
|
||||||
import {
|
import { getColorFromHexRgbOrName, PanelEvents, DataFrame, DataLink, VariableSuggestion } from '@grafana/data';
|
||||||
getColorFromHexRgbOrName,
|
|
||||||
PanelEvents,
|
|
||||||
DataFrame,
|
|
||||||
DataLink,
|
|
||||||
DateTimeInput,
|
|
||||||
VariableSuggestion,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||||
@@ -344,9 +337,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
this.contextMenuCtrl.toggleMenu();
|
this.contextMenuCtrl.toggleMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
formatDate = (date: DateTimeInput, format?: string) => {
|
getTimeZone = () => this.dashboard.getTimezone();
|
||||||
return this.dashboard.formatDate.apply(this.dashboard, [date, format]);
|
|
||||||
};
|
|
||||||
|
|
||||||
getDataFrameByRefId = (refId: string) => {
|
getDataFrameByRefId = (refId: string) => {
|
||||||
return this.dataList.filter(dataFrame => dataFrame.refId === refId)[0];
|
return this.dataList.filter(dataFrame => dataFrame.refId === refId)[0];
|
||||||
|
@@ -517,7 +517,7 @@ describe('grafanaGraph', () => {
|
|||||||
|
|
||||||
it('should format dates as hours minutes', () => {
|
it('should format dates as hours minutes', () => {
|
||||||
const axis = ctx.plotOptions.xaxis;
|
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', () => {
|
it('should format dates as month days', () => {
|
||||||
const axis = ctx.plotOptions.xaxis;
|
const axis = ctx.plotOptions.xaxis;
|
||||||
expect(axis.timeformat).toBe('%m/%d');
|
expect(axis.timeformat).toBe('MM/DD');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -11,7 +11,7 @@ const template = `
|
|||||||
items="ctrl.contextMenuCtrl.menuItemsSupplier()"
|
items="ctrl.contextMenuCtrl.menuItemsSupplier()"
|
||||||
onClose="ctrl.onContextMenuClose"
|
onClose="ctrl.onContextMenuClose"
|
||||||
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
|
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
|
||||||
formatSourceDate="ctrl.formatDate"
|
timeZone="ctrl.getTimeZone()"
|
||||||
x="ctrl.contextMenuCtrl.position.x"
|
x="ctrl.contextMenuCtrl.position.x"
|
||||||
y="ctrl.contextMenuCtrl.position.y"
|
y="ctrl.contextMenuCtrl.position.y"
|
||||||
></graph-context-menu>
|
></graph-context-menu>
|
||||||
|
@@ -38,6 +38,7 @@ export const getGraphSeriesModel = (
|
|||||||
decimals: legendOptions.decimals,
|
decimals: legendOptions.decimals,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fieldColumnIndex = -1;
|
let fieldColumnIndex = -1;
|
||||||
@@ -103,7 +104,7 @@ export const getGraphSeriesModel = (
|
|||||||
}
|
}
|
||||||
: { ...field.config, color };
|
: { ...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
|
// Time step is used to determine bars width when graph is rendered as bar chart
|
||||||
const timeStep = getSeriesTimeStep(timeField);
|
const timeStep = getSeriesTimeStep(timeField);
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
getColorFromHexRgbOrName,
|
getColorFromHexRgbOrName,
|
||||||
getValueFormat,
|
getValueFormat,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
|
dateTimeFormat,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { CoreEvents } from 'app/types';
|
import { CoreEvents } from 'app/types';
|
||||||
|
|
||||||
@@ -154,19 +155,14 @@ export class HeatmapRenderer {
|
|||||||
.range([0, this.chartWidth]);
|
.range([0, this.chartWidth]);
|
||||||
|
|
||||||
const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
|
const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
|
||||||
const grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
|
const format = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
|
||||||
let timeFormat;
|
const timeZone = this.ctrl.dashboard.getTimezone();
|
||||||
const dashboardTimeZone = this.ctrl.dashboard.getTimezone();
|
const formatter = (date: Date) => dateTimeFormat(date, { format, timeZone });
|
||||||
if (dashboardTimeZone === 'utc') {
|
|
||||||
timeFormat = d3.utcFormat(grafanaTimeFormatter);
|
|
||||||
} else {
|
|
||||||
timeFormat = d3.timeFormat(grafanaTimeFormatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const xAxis = d3
|
const xAxis = d3
|
||||||
.axisBottom(this.xScale)
|
.axisBottom(this.xScale)
|
||||||
.ticks(ticks)
|
.ticks(ticks)
|
||||||
.tickFormat(timeFormat)
|
.tickFormat(formatter)
|
||||||
.tickPadding(X_AXIS_TICK_PADDING)
|
.tickPadding(X_AXIS_TICK_PADDING)
|
||||||
.tickSize(this.chartHeight);
|
.tickSize(this.chartHeight);
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ import { feedToDataFrame } from './utils';
|
|||||||
import { loadRSSFeed } from './rss';
|
import { loadRSSFeed } from './rss';
|
||||||
|
|
||||||
// Types
|
// 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 { NewsOptions, NewsItem } from './types';
|
||||||
import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants';
|
import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
@@ -79,7 +79,7 @@ export class NewsPanel extends PureComponent<Props, State> {
|
|||||||
<div key={index} className={styles.item}>
|
<div key={index} className={styles.item}>
|
||||||
<a href={item.link} target="_blank">
|
<a href={item.link} target="_blank">
|
||||||
<div className={styles.title}>{item.title}</div>
|
<div className={styles.title}>{item.title}</div>
|
||||||
<div className={styles.date}>{dateTime(item.date).format('MMM DD')} </div>
|
<div className={styles.date}>{dateTimeFormat(item.date, { format: 'MMM DD' })} </div>
|
||||||
</a>
|
</a>
|
||||||
<div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} />
|
<div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,7 +16,7 @@ interface Props extends PanelProps<PieChartOptions> {}
|
|||||||
|
|
||||||
export class PieChartPanel extends PureComponent<Props> {
|
export class PieChartPanel extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { width, height, options, data, replaceVariables, fieldConfig } = this.props;
|
const { width, height, options, data, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||||
|
|
||||||
const values = getFieldDisplayValues({
|
const values = getFieldDisplayValues({
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
@@ -24,6 +24,7 @@ export class PieChartPanel extends PureComponent<Props> {
|
|||||||
data: data.series,
|
data: data.series,
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
replaceVariables: replaceVariables,
|
replaceVariables: replaceVariables,
|
||||||
|
timeZone,
|
||||||
}).map(v => v.display);
|
}).map(v => v.display);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -187,6 +187,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
|
timeZone: this.dashboard.getTimezone(),
|
||||||
});
|
});
|
||||||
// When we don't have any field
|
// When we don't have any field
|
||||||
this.data = {
|
this.data = {
|
||||||
|
@@ -69,7 +69,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getValues = (): FieldDisplay[] => {
|
getValues = (): FieldDisplay[] => {
|
||||||
const { data, options, replaceVariables, fieldConfig } = this.props;
|
const { data, options, replaceVariables, fieldConfig, timeZone } = this.props;
|
||||||
|
|
||||||
return getFieldDisplayValues({
|
return getFieldDisplayValues({
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
@@ -79,6 +79,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
|
|||||||
data: data.series,
|
data: data.series,
|
||||||
sparkline: options.graphMode !== BigValueGraphMode.None,
|
sparkline: options.graphMode !== BigValueGraphMode.None,
|
||||||
autoMinMax: true,
|
autoMinMax: true,
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -133,7 +133,7 @@ export class TablePanelCtrl extends MetricsPanelCtrl {
|
|||||||
this.renderer = new TableRenderer(
|
this.renderer = new TableRenderer(
|
||||||
this.panel,
|
this.panel,
|
||||||
this.table,
|
this.table,
|
||||||
this.dashboard.isTimezoneUtc(),
|
this.dashboard.getTimezone(),
|
||||||
this.$sanitize,
|
this.$sanitize,
|
||||||
this.templateSrv,
|
this.templateSrv,
|
||||||
config.theme.type
|
config.theme.type
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
dateTime,
|
|
||||||
escapeStringForRegex,
|
escapeStringForRegex,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getColorFromHexRgbOrName,
|
getColorFromHexRgbOrName,
|
||||||
@@ -11,6 +10,9 @@ import {
|
|||||||
stringToJsRegex,
|
stringToJsRegex,
|
||||||
textUtil,
|
textUtil,
|
||||||
unEscapeStringFromRegex,
|
unEscapeStringFromRegex,
|
||||||
|
TimeZone,
|
||||||
|
dateTimeFormatISO,
|
||||||
|
dateTimeFormat,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
|
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
|
||||||
@@ -23,7 +25,7 @@ export class TableRenderer {
|
|||||||
constructor(
|
constructor(
|
||||||
private panel: { styles: ColumnStyle[]; pageSize: number },
|
private panel: { styles: ColumnStyle[]; pageSize: number },
|
||||||
private table: TableRenderModel,
|
private table: TableRenderModel,
|
||||||
private isUtc: boolean,
|
private timeZone: TimeZone,
|
||||||
private sanitize: (v: any) => any,
|
private sanitize: (v: any) => any,
|
||||||
private templateSrv: TemplateSrv,
|
private templateSrv: TemplateSrv,
|
||||||
private theme?: GrafanaThemeType
|
private theme?: GrafanaThemeType
|
||||||
@@ -119,13 +121,16 @@ export class TableRenderer {
|
|||||||
v = parseInt(v, 10);
|
v = parseInt(v, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
let date = dateTime(v);
|
if (!column.style.dateFormat) {
|
||||||
|
return dateTimeFormatISO(v, {
|
||||||
if (this.isUtc) {
|
timeZone: this.timeZone,
|
||||||
date = date.utc();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.format(column.style.dateFormat);
|
return dateTimeFormat(v, {
|
||||||
|
format: column.style.dateFormat,
|
||||||
|
timeZone: this.timeZone,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { TableRenderer } from '../renderer';
|
import { TableRenderer } from '../renderer';
|
||||||
import { getColorDefinitionByName, ScopedVars } from '@grafana/data';
|
import { getColorDefinitionByName, ScopedVars, TimeZone } from '@grafana/data';
|
||||||
import { ColumnRender } from '../types';
|
import { ColumnRender } from '../types';
|
||||||
|
|
||||||
|
const utc: TimeZone = 'utc';
|
||||||
|
|
||||||
const sanitize = (value: any): string => {
|
const sanitize = (value: any): string => {
|
||||||
return 'sanitized';
|
return 'sanitized';
|
||||||
};
|
};
|
||||||
@@ -210,7 +212,7 @@ describe('when rendering table', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//@ts-ignore
|
//@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', () => {
|
it('time column should be formatted', () => {
|
||||||
const html = renderer.renderCell(0, 0, 1388556366666);
|
const html = renderer.renderCell(0, 0, 1388556366666);
|
||||||
@@ -466,7 +468,7 @@ describe('when rendering table with different patterns', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//@ts-ignore
|
//@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);
|
const html = renderer.renderCell(1, 0, 1230);
|
||||||
|
|
||||||
expect(html).toBe(expected);
|
expect(html).toBe(expected);
|
||||||
@@ -536,7 +538,7 @@ describe('when rendering cells with different alignment options', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//@ts-ignore
|
//@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);
|
const html = renderer.renderCell(1, 0, 42);
|
||||||
|
|
||||||
expect(html).toBe(expected);
|
expect(html).toBe(expected);
|
||||||
|
14
public/vendor/flot/jquery.flot.time.js
vendored
14
public/vendor/flot/jquery.flot.time.js
vendored
@@ -15,7 +15,8 @@ API.txt for details.
|
|||||||
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
||||||
timeformat: null, // format string to use
|
timeformat: null, // format string to use
|
||||||
twelveHourClock: false, // 12 or 24 time in time mode
|
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.
|
// A subset of the Open Group's strftime format is supported.
|
||||||
|
|
||||||
function formatDate(d, fmt, monthNames, dayNames) {
|
function formatDate(d, fmt, monthNames, dayNames) {
|
||||||
|
|
||||||
if (typeof d.strftime == "function") {
|
if (typeof d.strftime == "function") {
|
||||||
return d.strftime(fmt);
|
return d.strftime(fmt);
|
||||||
}
|
}
|
||||||
@@ -356,12 +356,15 @@ API.txt for details.
|
|||||||
};
|
};
|
||||||
|
|
||||||
axis.tickFormatter = function (v, axis) {
|
axis.tickFormatter = function (v, axis) {
|
||||||
|
|
||||||
var d = dateGenerator(v, axis.options);
|
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);
|
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);
|
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
|
||||||
|
|
||||||
return rt;
|
return rt;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
42
yarn.lock
42
yarn.lock
@@ -5672,6 +5672,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||||
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
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":
|
"@types/moment@^2.13.0":
|
||||||
version "2.13.0"
|
version "2.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896"
|
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"
|
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
|
||||||
integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==
|
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"
|
version "2.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||||
@@ -20230,15 +20244,17 @@ rc-drawer@3.1.3:
|
|||||||
rc-util "^4.16.1"
|
rc-util "^4.16.1"
|
||||||
react-lifecycles-compat "^3.0.4"
|
react-lifecycles-compat "^3.0.4"
|
||||||
|
|
||||||
rc-slider@9.2.3:
|
rc-slider@8.7.1:
|
||||||
version "9.2.3"
|
version "8.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.2.3.tgz#b80f03ada71ef3ec35dfcb67d2e0f87d84ce2781"
|
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.7.1.tgz#9ed07362dc93489a38e654b21b8122ad70fd3c42"
|
||||||
integrity sha512-mlERrweLA4KNFvO0dkhQv3Du0Emq7DyAFV6N7jgrAwfUZsX4eB1T1iJWYMtsguHXbMyje+PACAqwsrfhZIN0bQ==
|
integrity sha512-WMT5mRFUEcrLWwTxsyS8jYmlaMsTVCZIGENLikHsNv+tE8ThU2lCoPfi/xFNUfJFNFSBFP3MwPez9ZsJmNp13g==
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime "6.x"
|
babel-runtime "6.x"
|
||||||
classnames "^2.2.5"
|
classnames "^2.2.5"
|
||||||
rc-tooltip "^4.0.0"
|
prop-types "^15.5.4"
|
||||||
|
rc-tooltip "^3.7.0"
|
||||||
rc-util "^4.0.4"
|
rc-util "^4.0.4"
|
||||||
|
react-lifecycles-compat "^3.0.4"
|
||||||
shallowequal "^1.1.0"
|
shallowequal "^1.1.0"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
@@ -20254,14 +20270,16 @@ rc-time-picker@^3.7.3:
|
|||||||
rc-trigger "^2.2.0"
|
rc-trigger "^2.2.0"
|
||||||
react-lifecycles-compat "^3.0.4"
|
react-lifecycles-compat "^3.0.4"
|
||||||
|
|
||||||
rc-tooltip@^4.0.0:
|
rc-tooltip@^3.7.0:
|
||||||
version "4.0.3"
|
version "3.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-4.0.3.tgz#728b760863643ec2e85827a2e7fb28d961b3b759"
|
resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.3.tgz#280aec6afcaa44e8dff0480fbaff9e87fc00aecc"
|
||||||
integrity sha512-HNyBh9/fPdds0DXja8JQX0XTIHmZapB3lLzbdn74aNSxXG1KUkt+GK4X1aOTRY5X9mqm4uUKdeFrn7j273H8gw==
|
integrity sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww==
|
||||||
dependencies:
|
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"
|
version "2.6.5"
|
||||||
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885"
|
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885"
|
||||||
integrity sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw==
|
integrity sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw==
|
||||||
|
Reference in New Issue
Block a user