From 61bd33c24187184b058571c42f0e5f5129d22320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 Sep 2020 16:19:33 +0200 Subject: [PATCH] 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 --- conf/defaults.ini | 19 +++ conf/sample.ini | 17 +++ docs/sources/administration/configuration.md | 38 +++++- packages/grafana-data/src/datetime/common.ts | 7 ++ .../grafana-data/src/datetime/formats.test.ts | 19 +++ packages/grafana-data/src/datetime/formats.ts | 119 +++++++++++++++++- .../grafana-data/src/datetime/formatter.ts | 16 +-- .../grafana-data/src/datetime/parser.test.ts | 25 ++++ packages/grafana-data/src/datetime/parser.ts | 18 ++- packages/grafana-data/src/types/config.ts | 2 + .../src/valueFormats/categories.ts | 2 + .../src/valueFormats/dateTimeFormatters.ts | 12 +- packages/grafana-runtime/src/config.ts | 7 ++ .../src/components/Graph/utils.test.ts | 12 +- .../grafana-ui/src/components/Graph/utils.ts | 16 ++- .../grafana-ui/src/components/Logs/LogRow.tsx | 9 +- .../src/components/Logs/getLogRowStyles.ts | 1 - .../TimePickerContent.test.tsx.snap | 16 +-- .../TimePicker/TimeRangePicker/mapper.ts | 6 +- pkg/api/index.go | 2 + pkg/services/sqlstore/preferences.go | 2 + pkg/setting/date_formats.go | 28 +++++ pkg/setting/setting.go | 7 +- .../app/plugins/panel/graph/graph_tooltip.ts | 6 +- .../panel/graph2/getGraphSeriesModel.ts | 5 +- public/app/plugins/panel/heatmap/rendering.ts | 9 +- 26 files changed, 368 insertions(+), 52 deletions(-) create mode 100644 packages/grafana-data/src/datetime/formats.test.ts create mode 100644 packages/grafana-data/src/datetime/parser.test.ts create mode 100644 pkg/setting/date_formats.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 44fbbf67930..4bae80d98a0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -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 + + diff --git a/conf/sample.ini b/conf/sample.ini index 4e415cc05db..89b1a5edae3 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -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 diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index aa0a0ea2e39..c5f50ffbf4e 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -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. + diff --git a/packages/grafana-data/src/datetime/common.ts b/packages/grafana-data/src/datetime/common.ts index bdd291e87b3..a1a8445bec0 100644 --- a/packages/grafana-data/src/datetime/common.ts +++ b/packages/grafana-data/src/datetime/common.ts @@ -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; } /** diff --git a/packages/grafana-data/src/datetime/formats.test.ts b/packages/grafana-data/src/datetime/formats.test.ts new file mode 100644 index 00000000000..9b551fa1812 --- /dev/null +++ b/packages/grafana-data/src/datetime/formats.test.ts @@ -0,0 +1,19 @@ +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'); + }); +}); diff --git a/packages/grafana-data/src/datetime/formats.ts b/packages/grafana-data/src/datetime/formats.ts index 5a5e2b45f76..79344a77385 100644 --- a/packages/grafana-data/src/datetime/formats.ts +++ b/packages/grafana-data/src/datetime/formats.ts @@ -1,2 +1,117 @@ -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(); diff --git a/packages/grafana-data/src/datetime/formatter.ts b/packages/grafana-data/src/datetime/formatter.ts index d645a4e525b..108f0a7d9ff 100644 --- a/packages/grafana-data/src/datetime/formatter.ts +++ b/packages/grafana-data/src/datetime/formatter.ts @@ -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'; /** @@ -13,16 +13,8 @@ import { DateTimeOptions, getTimeZone } from './common'; * @public */ 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 = (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 => { diff --git a/packages/grafana-data/src/datetime/parser.test.ts b/packages/grafana-data/src/datetime/parser.test.ts new file mode 100644 index 00000000000..62209df8315 --- /dev/null +++ b/packages/grafana-data/src/datetime/parser.test.ts @@ -0,0 +1,25 @@ +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'); + }); +}); diff --git a/packages/grafana-data/src/datetime/parser.ts b/packages/grafana-data/src/datetime/parser.ts index ea505168260..8efabe579bb 100644 --- a/packages/grafana-data/src/datetime/parser.ts +++ b/packages/grafana-data/src/datetime/parser.ts @@ -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 = (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 => { diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index e4b68d72f49..e676f1c0312 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -1,6 +1,7 @@ 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; } diff --git a/packages/grafana-data/src/valueFormats/categories.ts b/packages/grafana-data/src/valueFormats/categories.ts index 958200ea811..89b0d439dea 100644 --- a/packages/grafana-data/src/valueFormats/categories.ts +++ b/packages/grafana-data/src/valueFormats/categories.ts @@ -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 }, ], }, diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts index da7a66475f6..f1b5062c4cf 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts @@ -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, diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 760dcd6c1de..42cf20ed0b6 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -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); + } } } diff --git a/packages/grafana-ui/src/components/Graph/utils.test.ts b/packages/grafana-ui/src/components/Graph/utils.test.ts index fc0b654c32d..32ec64d5a72 100644 --- a/packages/grafana-ui/src/components/Graph/utils.test.ts +++ b/packages/grafana-ui/src/components/Graph/utils.test.ts @@ -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'); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Graph/utils.ts b/packages/grafana-ui/src/components/Graph/utils.ts index 1b107755aec..1ce80c184cd 100644 --- a/packages/grafana-ui/src/components/Graph/utils.ts +++ b/packages/grafana-ui/src/components/Graph/utils.ts @@ -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; }; diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 49ed31ec276..0f5ccdf8918 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -127,6 +127,12 @@ class UnThemedLogRow extends PureComponent { }); }; + renderTimeStamp(epochMs: number) { + return dateTimeFormat(epochMs, { + timeZone: this.props.timeZone, + }); + } + renderLogRow( context?: LogRowContextRows, errors?: LogRowContextQueryErrors, @@ -143,7 +149,6 @@ class UnThemedLogRow extends PureComponent { allowDetails, row, showDuplicates, - timeZone, showContextToggle, showLabels, showTime, @@ -176,7 +181,7 @@ class UnThemedLogRow extends PureComponent { )} - {showTime && {dateTimeFormat(row.timeEpochMs, { timeZone })}} + {showTime && {this.renderTimeStamp(row.timeEpochMs)}} {showLabels && row.uniqueLabels && ( diff --git a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts index 037ad2c5e2f..18e05ef7c02 100644 --- a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts +++ b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts @@ -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; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap index f1083f9824d..db1ee65bf98 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap @@ -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", }, ] } diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/mapper.ts b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/mapper.ts index 9fcb8bd26a9..46567262600 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/mapper.ts +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/mapper.ts @@ -1,4 +1,4 @@ -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}`, }; diff --git a/pkg/api/index.go b/pkg/api/index.go index 9f4fadab0da..66c38fab3c5 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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, diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 24488029d52..4513a2b20f7 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -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) diff --git a/pkg/setting/date_formats.go b/pkg/setting/date_formats.go new file mode 100644 index 00000000000..94e1ce684b8 --- /dev/null +++ b/pkg/setting/date_formats.go @@ -0,0 +1,28 @@ +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) +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index dfc3f5d7744..a01a52711d3 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -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 } diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts index 9770e04014b..00e2b43ceeb 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.ts +++ b/public/app/plugins/panel/graph/graph_tooltip.ts @@ -1,7 +1,7 @@ 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) { diff --git a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts index 42f6a0cc5c2..9908ea16fd1 100644 --- a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts +++ b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts @@ -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), }, }, }); diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index dbf8b951b1d..00774df7c73 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -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)