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:
Marcus Andersson 2020-04-27 15:28:06 +02:00 committed by GitHub
parent 365de313f3
commit 1a0c1a39e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 821 additions and 576 deletions

View File

@ -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",

View 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;
};

View 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');
});
});
});

View 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();
}
};

View File

@ -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';

View 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;
}
};

View File

@ -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 };
};

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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 }) };
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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

View File

@ -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}
/>
);
}
};

View File

@ -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) => {

View File

@ -67,6 +67,7 @@ interface SeriesTableProps {
export const SeriesTable: React.FC<SeriesTableProps> = ({ timestamp, series }) => {
const theme = useTheme();
const styles = getSeriesTableRowStyles(theme);
return (
<>
{timestamp && (

View File

@ -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 (

View File

@ -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;
}

View File

@ -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';
};

View File

@ -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} />

View File

@ -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()];
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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",
},
]
}

View File

@ -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);
};

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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 });

View File

@ -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 }],
]);
}

View File

@ -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>

View File

@ -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,
};
});

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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';
}
/**

View File

@ -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,

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (
<>

View File

@ -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,

View File

@ -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() {

View File

@ -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() {

View File

@ -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);

View File

@ -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>
);
}
}

View File

@ -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">

View File

@ -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));
}

View File

@ -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));

View File

@ -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') {

View File

@ -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) {

View File

@ -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;

View File

@ -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);

View File

@ -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>
);

View File

@ -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 }));

View File

@ -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';

View File

@ -92,6 +92,7 @@ export class ResultProcessor {
field.display = getDisplayProcessor({
field,
theme: config.theme,
timeZone: this.timeZone,
});
}

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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 */

View File

@ -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];

View File

@ -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');
});
});
});

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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 (

View File

@ -187,6 +187,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
},
},
theme: config.theme,
timeZone: this.dashboard.getTimezone(),
});
// When we don't have any field
this.data = {

View File

@ -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,
});
};

View File

@ -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

View File

@ -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,
});
};
}

View File

@ -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);

View File

@ -15,7 +15,8 @@ API.txt for details.
timezone: null, // "browser" for local to the client or timezone for timezone-js
timeformat: null, // format string to use
twelveHourClock: false, // 12 or 24 time in time mode
monthNames: null // list of names of months
monthNames: null, // list of names of months
timeFormatter: null // external formatter with timezone support
}
};
@ -29,7 +30,6 @@ API.txt for details.
// A subset of the Open Group's strftime format is supported.
function formatDate(d, fmt, monthNames, dayNames) {
if (typeof d.strftime == "function") {
return d.strftime(fmt);
}
@ -356,12 +356,15 @@ API.txt for details.
};
axis.tickFormatter = function (v, axis) {
var d = dateGenerator(v, axis.options);
// first check global format
// first check global formatter
if (typeof opts.timeFormatter === "function") {
return opts.timeFormatter(d.getTime(), opts.timeformat);
}
if (opts.timeformat != null) {
// second check global format
if (opts.timeformat != null) {
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
}
@ -407,7 +410,6 @@ API.txt for details.
}
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
return rt;
};
}

View File

@ -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==