Commit 61bd33c2 by Torkel Ödegaard Committed by GitHub

System: Date formating options (#27216)

* Add support for local time formats in graph panel

* Enfore 24h format for backward compatibility

* Use existing Intl.DateTimeFormatOptions

* Pre-generate time scale, add tests

* Move localTimeFormat, add local format to units

* updated default fallback

* #25602, use navigator.languages to enforce locale in formatting

* Making options

* Worked new system settings

* things are working

* Local browser time formats working

* Support parsing dates in different formats

* settings updated

* Settings starting to work

* Fixed graph issue

* Logs fix

* refactored settings a bit

* Updated and name change

* Progress

* Changed config names

* Updated

* Updated

* Updated test

* Synced description

* fixed ts issue

* Added version notice

* Ts fix

* Updated heatmap and test

* Updated snapshot

* Updated

* fixed ts issue

* Fixes

Co-authored-by: Alex Shpak <alex-shpak@users.noreply.github.com>
parent 783391a8
......@@ -803,3 +803,22 @@ license_path =
[feature_toggles]
# enable features, separated by spaces
enable =
[date_formats]
# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
# Default system date format used in time range picker and other places where full time is displayed
full_date = YYYY-MM-DD HH:mm:ss
# Used by graph and other places where we only show small intervals
interval_second = HH:mm:ss
interval_minute = HH:mm
interval_hour = MM/DD HH:mm
interval_day = MM/DD
interval_month = YYYY-MM
interval_year = YYYY
# Experimental feature
use_browser_locale = false
......@@ -794,3 +794,20 @@
[feature_toggles]
# enable features, separated by spaces
;enable =
[date_formats]
# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
# Default system date format used in time range picker and other places where full time is displayed
;full_date = YYYY-MM-DD HH:mm:ss
# Used by graph and other places where we only show small intervals
;interval_second = HH:mm:ss
;interval_minute = HH:mm
;interval_hour = MM/DD HH:mm
;interval_day = MM/DD
;interval_month = YYYY-MM
;interval_year = YYYY
# Experimental feature
;use_browser_locale = false
......@@ -189,8 +189,6 @@ Folder that contains [provisioning]({{< relref "provisioning.md" >}}) config fil
`http`,`https`,`h2` or `socket`
> **Note:** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for SSL termination.
### http_addr
The IP address to bind to. If empty will bind to all interfaces
......@@ -231,8 +229,6 @@ callback URL to be correct).
### serve_from_sub_path
> Available in Grafana 6.3+.
Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons.
By enabling this setting and using a subpath in `root_url` above, e.g.
......@@ -1387,3 +1383,37 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise]({{<
### enable
Keys of alpha features to enable, separated by space. Available alpha features are: `transformations`, `standaloneAlerts`
## [date_formats]
> The date format options below are only available in Grafana v7.2+
This section controls system wide defaults for date formats used in time ranges, graphs and date input boxes.
The format patterns use [Moment.js](https://momentjs.com/docs/#/displaying/) formatting tokens.
### full_date
Full date format used by time range picker and in other places where a full date is rendered.
### intervals
These intervals formats are used in the graph to show only a partial date or time. For example if there are only
minutes between y-axis tick labels then the `interval_minute` format is used.
Defaults
```
interval_second = HH:mm:ss
interval_minute = HH:mm
interval_hour = MM/DD HH:mm
interval_day = MM/DD
interval_month = YYYY-MM
interval_year = YYYY
```
### use_browser_locale
Set this to `true` to have date formats be automatically be derived from browser locale. Defaults to `false`. This
is an experimental feature right now with a few problems that remain unsolved.
......@@ -13,6 +13,13 @@ export interface DateTimeOptions {
* user is used.
*/
timeZone?: TimeZone;
/**
* Specify a {@link https://momentjs.com/docs/#/displaying/format | momentjs} format to
* use a custom formatting pattern or parsing pattern. If no format is set,
* then system configured default format is used.
*/
format?: string;
}
/**
......
import { localTimeFormat } from './formats';
describe('Date Formats', () => {
it('localTimeFormat', () => {
const format = localTimeFormat(
{
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
},
''
);
expect(format).toBe('MM/DD/YYYY, HH:mm:ss A');
});
});
export const DEFAULT_DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export const MS_DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS';
export interface SystemDateFormatSettings {
fullDate: string;
interval: {
second: string;
minute: string;
hour: string;
day: string;
month: string;
year: string;
};
useBrowserLocale: boolean;
}
const DEFAULT_SYSTEM_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export class SystemDateFormatsState {
fullDate = DEFAULT_SYSTEM_DATE_FORMAT;
interval = {
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'MM/DD HH:mm',
day: 'MM/DD',
month: 'YYYY-MM',
year: 'YYYY',
};
update(settings: SystemDateFormatSettings) {
this.fullDate = settings.fullDate;
this.interval = settings.interval;
if (settings.useBrowserLocale) {
this.useBrowserLocale();
}
}
get fullDateMS() {
// Add millisecond to seconds part
return this.fullDate.replace('ss', 'ss.SSS');
}
useBrowserLocale() {
this.fullDate = localTimeFormat({
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
this.interval.second = localTimeFormat(
{ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false },
null,
this.interval.second
);
this.interval.minute = localTimeFormat(
{ hour: '2-digit', minute: '2-digit', hour12: false },
null,
this.interval.minute
);
this.interval.hour = localTimeFormat(
{ month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false },
null,
this.interval.hour
);
this.interval.day = localTimeFormat({ month: '2-digit', day: '2-digit', hour12: false }, null, this.interval.day);
this.interval.month = localTimeFormat(
{ year: 'numeric', month: '2-digit', hour12: false },
null,
this.interval.month
);
}
getTimeFieldUnit(useMsResolution?: boolean) {
return `time:${useMsResolution ? this.fullDateMS : this.fullDate}`;
}
}
/**
* localTimeFormat helps to generate date formats for momentjs based on browser's locale
*
* @param locale browser locale, or default
* @param options DateTimeFormatOptions to format date
* @param fallback default format if Intl API is not present
*/
export function localTimeFormat(
options: Intl.DateTimeFormatOptions,
locale?: string | string[] | null,
fallback?: string
): string {
if (!window.Intl) {
return fallback ?? DEFAULT_SYSTEM_DATE_FORMAT;
}
if (!locale) {
locale = [...navigator.languages];
}
// https://momentjs.com/docs/#/displaying/format/
const parts = new Intl.DateTimeFormat(locale, options).formatToParts(new Date());
const mapping: { [key: string]: string } = {
year: 'YYYY',
month: 'MM',
day: 'DD',
hour: 'HH',
minute: 'mm',
second: 'ss',
weekday: 'ddd',
era: 'N',
dayPeriod: 'A',
timeZoneName: 'Z',
};
return parts.map(part => mapping[part.type] || part.value).join('');
}
export const systemDateFormats = new SystemDateFormatsState();
......@@ -2,7 +2,7 @@
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 { systemDateFormats } from './formats';
import { DateTimeOptions, getTimeZone } from './common';
/**
......@@ -14,15 +14,7 @@ import { DateTimeOptions, getTimeZone } from './common';
*/
export interface DateTimeOptionsWithFormat extends DateTimeOptions {
/**
* Specify a {@link https://momentjs.com/docs/#/displaying/format | momentjs} format to
* use a custom formatting pattern of the date and time value. If no format is set,
* then {@link DEFAULT_DATE_TIME_FORMAT} is used.
*/
format?: string;
/**
* Set this value to `true` if you want to include milliseconds when formatting date and time
* values in the default {@link DEFAULT_DATE_TIME_FORMAT} format.
*/
defaultWithMS?: boolean;
}
......@@ -77,7 +69,7 @@ export const dateTimeFormatTimeAgo: DateTimeFormatter = (dateInUtc, options?) =>
* @public
*/
export const dateTimeFormatWithAbbrevation: DateTimeFormatter = (dateInUtc, options?) =>
toTz(dateInUtc, getTimeZone(options)).format(`${DEFAULT_DATE_TIME_FORMAT} z`);
toTz(dateInUtc, getTimeZone(options)).format(`${systemDateFormats.fullDate} z`);
/**
* Helper function to return only the time zone abbreviation for a given date and time value. If no options
......@@ -93,9 +85,9 @@ export const timeZoneAbbrevation: DateTimeFormatter = (dateInUtc, options?) =>
const getFormat = <T extends DateTimeOptionsWithFormat>(options?: T): string => {
if (options?.defaultWithMS) {
return options?.format ?? MS_DATE_TIME_FORMAT;
return options?.format ?? systemDateFormats.fullDateMS;
}
return options?.format ?? DEFAULT_DATE_TIME_FORMAT;
return options?.format ?? systemDateFormats.fullDate;
};
const toTz = (dateInUtc: DateTimeInput, timeZone: TimeZone): Moment => {
......
import { dateTimeParse } from './parser';
import { systemDateFormats } from './formats';
describe('dateTimeParse', () => {
it('should be able to parse using default format', () => {
const date = dateTimeParse('2020-03-02 15:00:22', { timeZone: 'utc' });
expect(date.format()).toEqual('2020-03-02T15:00:22Z');
});
it('should be able to parse using default format', () => {
systemDateFormats.update({
fullDate: 'MMMM D, YYYY, h:mm:ss a',
interval: {} as any,
useBrowserLocale: false,
});
const date = dateTimeParse('Aug 20, 2020 10:30:20 am', { timeZone: 'utc' });
expect(date.format()).toEqual('2020-08-20T10:30:20Z');
});
it('should be able to parse array formats used by calendar', () => {
const date = dateTimeParse([2020, 5, 10, 10, 30, 20], { timeZone: 'utc' });
expect(date.format()).toEqual('2020-06-10T10:30:20Z');
});
});
......@@ -4,6 +4,7 @@ import { DateTimeInput, DateTime, isDateTime } from './moment_wrapper';
import { DateTimeOptions, getTimeZone } from './common';
import { parse, isValid } from './datemath';
import { lowerCase } from 'lodash';
import { systemDateFormats } from './formats';
/**
* The type that describes options that can be passed when parsing a date and time value.
......@@ -25,7 +26,7 @@ type DateTimeParser<T extends DateTimeOptions = DateTimeOptions> = (value: DateT
/**
* Helper function to parse a number, text or Date to a DateTime value. If a timeZone is supplied the incoming value
* is parsed with that timeZone as a base. The only exception to this is if the passed value is in a UTC-based
* format. Then it will use UTC as the base. Examples on UTC-based values are Unix epoch and ISO formatted strings.
* format. Then it will use UTC as the base. If no format is specified the current system format will be assumed.
*
* It can also parse the Grafana quick date and time format, e.g. now-6h will be parsed as Date.now() - 6 hours and
* returned as a valid DateTime value.
......@@ -59,7 +60,20 @@ const parseString = (value: string, options?: DateTimeOptionsWhenParsing): DateT
return parsed || (moment() as DateTime);
}
return parseOthers(value, options);
const timeZone = getTimeZone(options);
const zone = moment.tz.zone(timeZone);
const format = options?.format ?? systemDateFormats.fullDate;
if (zone && zone.name) {
return moment.tz(value, format, zone.name) as DateTime;
}
switch (lowerCase(timeZone)) {
case 'utc':
return moment.utc(value, format) as DateTime;
default:
return moment(value, format) as DateTime;
}
};
const parseOthers = (value: DateTimeInput, options?: DateTimeOptionsWhenParsing): DateTime => {
......
import { DataSourceInstanceSettings } from './datasource';
import { PanelPluginMeta } from './panel';
import { GrafanaTheme } from './theme';
import { SystemDateFormatSettings } from '../datetime';
/**
* Describes the build information that will be available via the Grafana configuration.
......@@ -100,4 +101,5 @@ export interface GrafanaConfig {
featureToggles: FeatureToggles;
licenseInfo: LicenseInfo;
http2Enabled: boolean;
dateFormats?: SystemDateFormatSettings;
}
......@@ -4,6 +4,7 @@ import {
dateTimeAsIsoNoDateIfToday,
dateTimeAsUS,
dateTimeAsUSNoDateIfToday,
dateTimeAsLocal,
dateTimeFromNow,
toClockMilliseconds,
toClockSeconds,
......@@ -175,6 +176,7 @@ export const getCategories = (): ValueFormatCategory[] => [
{ name: 'Datetime ISO (No date if today)', id: 'dateTimeAsIsoNoDateIfToday', fn: dateTimeAsIsoNoDateIfToday },
{ name: 'Datetime US', id: 'dateTimeAsUS', fn: dateTimeAsUS },
{ name: 'Datetime US (No date if today)', id: 'dateTimeAsUSNoDateIfToday', fn: dateTimeAsUSNoDateIfToday },
{ name: 'Datetime local', id: 'dateTimeAsLocal', fn: dateTimeAsLocal },
{ name: 'From Now', id: 'dateTimeFromNow', fn: dateTimeFromNow },
],
},
......
......@@ -3,7 +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';
import { dateTimeFormat, dateTimeFormatTimeAgo, localTimeFormat } from '../datetime';
interface IntervalsInSeconds {
[interval: string]: number;
......@@ -369,6 +369,16 @@ export const dateTimeAsIso = toDateTimeValueFormatter('YYYY-MM-DD HH:mm:ss');
export const dateTimeAsIsoNoDateIfToday = toDateTimeValueFormatter('YYYY-MM-DD HH:mm:ss', 'HH:mm:ss');
export const dateTimeAsUS = toDateTimeValueFormatter('MM/DD/YYYY h:mm:ss a');
export const dateTimeAsUSNoDateIfToday = toDateTimeValueFormatter('MM/DD/YYYY h:mm:ss a', 'h:mm:ss a');
export const dateTimeAsLocal = toDateTimeValueFormatter(
localTimeFormat({
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
);
export function dateTimeFromNow(
value: number,
......
......@@ -9,6 +9,8 @@ import {
GrafanaThemeType,
LicenseInfo,
PanelPluginMeta,
systemDateFormats,
SystemDateFormatSettings,
} from '@grafana/data';
export class GrafanaBootConfig implements GrafanaConfig {
......@@ -59,6 +61,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;
http2Enabled = false;
dateFormats?: SystemDateFormatSettings;
constructor(options: GrafanaBootConfig) {
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
......@@ -84,6 +87,10 @@ export class GrafanaBootConfig implements GrafanaConfig {
};
merge(this, defaults, options);
if (this.dateFormats) {
systemDateFormats.update(this.dateFormats);
}
}
}
......
......@@ -8,7 +8,7 @@ import {
GrafanaThemeType,
Field,
} from '@grafana/data';
import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData } from './utils';
import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData, graphTimeFormat } from './utils';
const mockResult = (
value: string,
......@@ -196,4 +196,14 @@ describe('Graph utils', () => {
expect(findHoverIndexFromData(timeField!, 300)).toBe(2);
});
});
describe('graphTimeFormat', () => {
it('graphTimeFormat', () => {
expect(graphTimeFormat(5, 1, 45 * 5 * 1000)).toBe('HH:mm:ss');
expect(graphTimeFormat(5, 1, 7200 * 5 * 1000)).toBe('HH:mm');
expect(graphTimeFormat(5, 1, 80000 * 5 * 1000)).toBe('MM/DD HH:mm');
expect(graphTimeFormat(5, 1, 2419200 * 5 * 1000)).toBe('MM/DD');
expect(graphTimeFormat(5, 1, 12419200 * 5 * 1000)).toBe('YYYY-MM');
});
});
});
......@@ -6,6 +6,7 @@ import {
getFieldDisplayName,
TimeZone,
dateTimeFormat,
systemDateFormats,
} from '@grafana/data';
/**
......@@ -120,19 +121,22 @@ export const graphTimeFormat = (ticks: number | null, min: number | null, max: n
const oneYear = 31536000000;
if (secPerTick <= 45) {
return 'HH:mm:ss';
return systemDateFormats.interval.second;
}
if (secPerTick <= 7200 || range <= oneDay) {
return 'HH:mm';
return systemDateFormats.interval.minute;
}
if (secPerTick <= 80000) {
return 'MM/DD HH:mm';
return systemDateFormats.interval.hour;
}
if (secPerTick <= 2419200 || range <= oneYear) {
return 'MM/DD';
return systemDateFormats.interval.day;
}
return 'YYYY-MM';
if (secPerTick <= 31536000) {
return systemDateFormats.interval.month;
}
return systemDateFormats.interval.year;
}
return 'HH:mm';
return systemDateFormats.interval.minute;
};
......@@ -127,6 +127,12 @@ class UnThemedLogRow extends PureComponent<Props, State> {
});
};
renderTimeStamp(epochMs: number) {
return dateTimeFormat(epochMs, {
timeZone: this.props.timeZone,
});
}
renderLogRow(
context?: LogRowContextRows,
errors?: LogRowContextQueryErrors,
......@@ -143,7 +149,6 @@ class UnThemedLogRow extends PureComponent<Props, State> {
allowDetails,
row,
showDuplicates,
timeZone,
showContextToggle,
showLabels,
showTime,
......@@ -176,7 +181,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
</td>
)}
{showTime && <td className={style.logsRowLocalTime}>{dateTimeFormat(row.timeEpochMs, { timeZone })}</td>}
{showTime && <td className={style.logsRowLocalTime}>{this.renderTimeStamp(row.timeEpochMs)}</td>}
{showLabels && row.uniqueLabels && (
<td className={style.logsRowLabels}>
<LogLabels labels={row.uniqueLabels} />
......
......@@ -126,7 +126,6 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
logsRowLocalTime: css`
label: logs-row__localtime;
white-space: nowrap;
max-width: 12.5em;
`,
logsRowLabels: css`
label: logs-row__labels;
......
......@@ -233,15 +233,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-17T07:48:27Z",
"from": "2019-12-17 07:48:27",
"section": 3,
"to": "2019-12-18T07:48:27Z",
"to": "2019-12-18 07:48:27",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17T07:48:27Z",
"from": "2019-10-17 07:48:27",
"section": 3,
"to": "2019-10-18T07:48:27Z",
"to": "2019-10-18 07:48:27",
},
]
}
......@@ -297,15 +297,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-17T07:48:27Z",
"from": "2019-12-17 07:48:27",
"section": 3,
"to": "2019-12-18T07:48:27Z",
"to": "2019-12-18 07:48:27",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17T07:48:27Z",
"from": "2019-10-17 07:48:27",
"section": 3,
"to": "2019-10-18T07:48:27Z",
"to": "2019-10-18 07:48:27",
},
]
}
......
import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat, dateTimeFormatISO } from '@grafana/data';
import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat } from '@grafana/data';
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone);
......@@ -9,8 +9,8 @@ export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): Tim
const to = dateTimeFormat(range.to, { timeZone });
return {
from: dateTimeFormatISO(range.from, { timeZone }),
to: dateTimeFormatISO(range.to, { timeZone }),
from,
to,
section: 3,
display: `${from} to ${to}`,
};
......
......@@ -54,6 +54,8 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
return nil, err
}
settings["dateFormats"] = hs.Cfg.DateFormats
var data = dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserId,
......
......@@ -19,6 +19,7 @@ func init() {
func GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) error {
params := make([]interface{}, 0)
filter := ""
if len(query.User.Teams) > 0 {
filter = "(org_id=? AND team_id IN (?" + strings.Repeat(",?", len(query.User.Teams)-1) + ")) OR "
params = append(params, query.User.OrgId)
......@@ -26,6 +27,7 @@ func GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) e
params = append(params, v)
}
}
filter += "(org_id=? AND user_id=? AND team_id=0) OR (org_id=? AND team_id=0 AND user_id=0)"
params = append(params, query.User.OrgId)
params = append(params, query.User.UserId)
......
package setting
type DateFormats struct {
FullDate string `json:"fullDate"`
UseBrowserLocale bool `json:"useBrowserLocale"`
Interval DateFormatIntervals `json:"interval"`
}
type DateFormatIntervals struct {
Second string `json:"second"`
Minute string `json:"minute"`
Hour string `json:"hour"`
Day string `json:"day"`
Month string `json:"month"`
Year string `json:"year"`
}
func (cfg *Cfg) readDateFormats() {
dateFormats := cfg.Raw.Section("date_formats")
cfg.DateFormats.FullDate, _ = valueAsString(dateFormats, "full_date", "YYYY-MM-DD HH:mm:ss")
cfg.DateFormats.Interval.Second, _ = valueAsString(dateFormats, "interval_second", "HH:mm:ss")
cfg.DateFormats.Interval.Minute, _ = valueAsString(dateFormats, "interval_minute", "HH:mm")
cfg.DateFormats.Interval.Hour, _ = valueAsString(dateFormats, "interval_hour", "MM-DD HH:mm")
cfg.DateFormats.Interval.Day, _ = valueAsString(dateFormats, "interval_day", "YYYY-MM-DD")
cfg.DateFormats.Interval.Month, _ = valueAsString(dateFormats, "interval_month", "YYYY-MM")
cfg.DateFormats.Interval.Year = "YYYY"
cfg.DateFormats.UseBrowserLocale = dateFormats.Key("date_format_use_browser_locale").MustBool(false)
}
......@@ -300,10 +300,11 @@ type Cfg struct {
ApiKeyMaxSecondsToLive int64
// Use to enable new features which may still be in alpha/beta stage.
FeatureToggles map[string]bool
FeatureToggles map[string]bool
AnonymousHideVersion bool
DateFormats DateFormats
// Annotations
AlertingAnnotationCleanupSetting AnnotationCleanupSettings
DashboardAnnotationCleanupSettings AnnotationCleanupSettings
......@@ -835,6 +836,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
ConnStr: connStr,
}
cfg.readDateFormats()
return nil
}
......
import $ from 'jquery';
import { appEvents } from 'app/core/core';
import { CoreEvents } from 'app/types';
import { textUtil } from '@grafana/data';
import { textUtil, systemDateFormats } from '@grafana/data';
export default function GraphTooltip(this: any, elem: any, dashboard: any, scope: any, getSeriesFn: any) {
const self = this;
......@@ -233,9 +233,9 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
}
if (seriesList[0].hasMsResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
tooltipFormat = systemDateFormats.fullDateMS;
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
tooltipFormat = systemDateFormats.fullDate;
}
if (allSeriesMode) {
......
......@@ -12,8 +12,7 @@ import {
getSeriesTimeStep,
TimeZone,
hasMsResolution,
MS_DATE_TIME_FORMAT,
DEFAULT_DATE_TIME_FORMAT,
systemDateFormats,
FieldColor,
FieldColorMode,
FieldConfigSource,
......@@ -117,7 +116,7 @@ export const getGraphSeriesModel = (
...timeField,
type: timeField.type,
config: {
unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
unit: systemDateFormats.getTimeFieldUnit(useMsDateFormat),
},
},
});
......
......@@ -15,6 +15,7 @@ import {
formattedValueToString,
dateTimeFormat,
} from '@grafana/data';
import { graphTimeFormat } from '@grafana/ui';
import { CoreEvents } from 'app/types';
const MIN_CARD_SIZE = 1,
......@@ -155,9 +156,13 @@ export class HeatmapRenderer {
.range([0, this.chartWidth]);
const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
const format = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
const format = graphTimeFormat(ticks, this.timeRange.from.valueOf(), this.timeRange.to.valueOf());
const timeZone = this.ctrl.dashboard.getTimezone();
const formatter = (date: Date) => dateTimeFormat(date, { format, timeZone });
const formatter = (date: Date) =>
dateTimeFormat(date.valueOf(), {
format: format,
timeZone: timeZone,
});
const xAxis = d3
.axisBottom(this.xScale)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment