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