diff --git a/src/plugins/data/public/field_formats/converters/date_nanos.ts b/src/plugins/data/public/field_formats/converters/date_nanos.ts index 3345d49cac30e..cc61f8f5cd44e 100644 --- a/src/plugins/data/public/field_formats/converters/date_nanos.ts +++ b/src/plugins/data/public/field_formats/converters/date_nanos.ts @@ -79,9 +79,9 @@ export class DateNanosFormat extends FieldFormat { }); static fieldType = KBN_FIELD_TYPES.DATE; - private memoizedConverter: Function = noop; - private memoizedPattern: string = ''; - private timeZone: string = ''; + protected memoizedConverter: Function = noop; + protected memoizedPattern: string = ''; + protected timeZone: string = ''; getParamDefaults() { return { diff --git a/src/plugins/data/server/field_formats/converters/date_nanos_server.test.ts b/src/plugins/data/server/field_formats/converters/date_nanos_server.test.ts new file mode 100644 index 0000000000000..ba8e128f32728 --- /dev/null +++ b/src/plugins/data/server/field_formats/converters/date_nanos_server.test.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DateNanosFormat } from './date_nanos_server'; +import { FieldFormatsGetConfigFn } from 'src/plugins/data/common'; + +describe('Date Nanos Format: Server side edition', () => { + let convert: Function; + let mockConfig: Record; + let getConfig: FieldFormatsGetConfigFn; + + const dateTime = '2019-05-05T14:04:56.201900001Z'; + + beforeEach(() => { + mockConfig = {}; + mockConfig.dateNanosFormat = 'MMMM Do YYYY, HH:mm:ss.SSSSSSSSS'; + mockConfig['dateFormat:tz'] = 'Browser'; + + getConfig = (key: string) => mockConfig[key]; + }); + + test('should format according to the given timezone parameter', () => { + const dateNy = new DateNanosFormat({ timezone: 'America/New_York' }, getConfig); + convert = dateNy.convert.bind(dateNy); + expect(convert(dateTime)).toMatchInlineSnapshot(`"May 5th 2019, 10:04:56.201900001"`); + + const datePhx = new DateNanosFormat({ timezone: 'America/Phoenix' }, getConfig); + convert = datePhx.convert.bind(datePhx); + expect(convert(dateTime)).toMatchInlineSnapshot(`"May 5th 2019, 07:04:56.201900001"`); + }); + + test('should format according to UTC if no timezone parameter is given or exists in settings', () => { + const utcFormat = 'May 5th 2019, 14:04:56.201900001'; + const dateUtc = new DateNanosFormat({ timezone: 'UTC' }, getConfig); + convert = dateUtc.convert.bind(dateUtc); + expect(convert(dateTime)).toBe(utcFormat); + + const dateDefault = new DateNanosFormat({}, getConfig); + convert = dateDefault.convert.bind(dateDefault); + expect(convert(dateTime)).toBe(utcFormat); + }); + + test('should format according to dateFormat:tz if the setting is not "Browser"', () => { + mockConfig['dateFormat:tz'] = 'America/Phoenix'; + + const date = new DateNanosFormat({}, getConfig); + convert = date.convert.bind(date); + expect(convert(dateTime)).toMatchInlineSnapshot(`"May 5th 2019, 07:04:56.201900001"`); + }); + + test('should defer to meta params for timezone, not the UI config', () => { + mockConfig['dateFormat:tz'] = 'America/Phoenix'; + + const date = new DateNanosFormat({ timezone: 'America/New_York' }, getConfig); + convert = date.convert.bind(date); + expect(convert(dateTime)).toMatchInlineSnapshot(`"May 5th 2019, 10:04:56.201900001"`); + }); +}); diff --git a/src/plugins/data/server/field_formats/converters/date_nanos_server.ts b/src/plugins/data/server/field_formats/converters/date_nanos_server.ts index c5828fd9c51b9..2bf533ab85bef 100644 --- a/src/plugins/data/server/field_formats/converters/date_nanos_server.ts +++ b/src/plugins/data/server/field_formats/converters/date_nanos_server.ts @@ -17,80 +17,17 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import { memoize, noop } from 'lodash'; -import moment, { Moment } from 'moment-timezone'; +import { memoize } from 'lodash'; +import moment from 'moment-timezone'; import { - FieldFormat, - FIELD_FORMAT_IDS, - KBN_FIELD_TYPES, - TextContextTypeConvert, -} from '../../../common'; - -/** - * Analyse the given moment.js format pattern for the fractional sec part (S,SS,SSS...) - * returning length, match, pattern and an escaped pattern, that excludes the fractional - * part when formatting with moment.js -> e.g. [SSS] - */ -export function analysePatternForFract(pattern: string) { - const fracSecMatch = pattern.match('S+') as any; // extract fractional seconds sub-pattern - const fracSecMatchStr = fracSecMatch ? fracSecMatch[0] : ''; - - return { - length: fracSecMatchStr.length, - patternNanos: fracSecMatchStr, - pattern, - patternEscaped: fracSecMatchStr ? pattern.replace(fracSecMatch, `[${fracSecMatch}]`) : '', - }; -} - -/** - * Format a given moment.js date object - * Since momentjs would loose the exact value for fractional seconds with a higher resolution than - * milliseconds, the fractional pattern is replaced by the fractional value of the raw timestamp - */ -export function formatWithNanos( - dateMomentObj: Moment, - valRaw: string, - fracPatternObj: Record -) { - if (fracPatternObj.length <= 3) { - // S,SS,SSS is formatted correctly by moment.js - return dateMomentObj.format(fracPatternObj.pattern); - } else { - // Beyond SSS the precise value of the raw datetime string is used - const valFormatted = dateMomentObj.format(fracPatternObj.patternEscaped); - // Extract fractional value of ES formatted timestamp, zero pad if necessary: - // 2020-05-18T20:45:05.957Z -> 957000000 - // 2020-05-18T20:45:05.957000123Z -> 957000123 - // we do not need to take care of the year 10000 bug since max year of date_nanos is 2262 - const valNanos = valRaw - .substr(20, valRaw.length - 21) // remove timezone(Z) - .padEnd(9, '0') // pad shorter fractionals - .substr(0, fracPatternObj.patternNanos.length); - return valFormatted.replace(fracPatternObj.patternNanos, valNanos); - } -} - -export class DateNanosFormat extends FieldFormat { - static id = FIELD_FORMAT_IDS.DATE_NANOS; - static title = i18n.translate('data.fieldFormats.date_nanos.title', { - defaultMessage: 'Date nanos', - }); - static fieldType = KBN_FIELD_TYPES.DATE; - - private memoizedConverter: Function = noop; - private memoizedPattern: string = ''; - private timeZone: string = ''; - - getParamDefaults() { - return { - pattern: this.getConfig!('dateNanosFormat'), - fallbackPattern: this.getConfig!('dateFormat'), - timezone: this.getConfig!('dateFormat:tz'), - }; - } + analysePatternForFract, + DateNanosFormat, + formatWithNanos, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from 'src/plugins/data/public/field_formats/converters/date_nanos'; +import { TextContextTypeConvert } from '../../../common'; +class DateNanosFormatServer extends DateNanosFormat { textConvert: TextContextTypeConvert = (val) => { // don't give away our ref to converter so // we can hot-swap when config changes @@ -139,3 +76,5 @@ export class DateNanosFormat extends FieldFormat { return this.memoizedConverter(val); }; } + +export { DateNanosFormatServer as DateNanosFormat };