[ML] Transforms: Add support for timezone for date histogram pivot config (#155535)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2023-04-25 16:52:24 -05:00 committed by GitHub
parent 90eee76572
commit 90d3006678
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 7 deletions

View file

@ -30,6 +30,7 @@ export interface DateHistogramAgg {
field: EsFieldName;
calendar_interval: string;
missing_bucket?: boolean;
time_zone?: string;
};
}

View file

@ -59,6 +59,7 @@ interface GroupByDateHistogram extends GroupByConfigBase {
field: EsFieldName;
calendar_interval: string;
missing_bucket?: boolean;
time_zone?: string;
}
interface GroupByHistogram extends GroupByConfigBase {

View file

@ -172,6 +172,7 @@ export const getRequestPayload = (
date_histogram: {
field: g.field,
calendar_interval: g.calendar_interval,
time_zone: g.time_zone,
...getMissingBucketConfig(g),
},
};

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isValidTimeZone } from './time_zone_utils';
describe('Overrides utilities', () => {
it('should return true for case-sensitive, acceptable timezones', async () => {
expect(isValidTimeZone('America/New_York')).toEqual(true);
expect(isValidTimeZone('America/Chicago')).toEqual(true);
expect(isValidTimeZone('UTC')).toEqual(true);
});
it('should return false for invalid input', async () => {
expect(isValidTimeZone('')).toEqual(false);
expect(isValidTimeZone()).toEqual(false);
expect(isValidTimeZone('Browser')).toEqual(false);
expect(isValidTimeZone('EST')).toEqual(false);
expect(isValidTimeZone('HST')).toEqual(false);
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment-timezone';
import { isDefined } from '@kbn/ml-is-defined';
// Partial list of packages/core/ui-settings/core-ui-settings-common/src/timezones.ts
export const ACCEPTED_TIMEZONES = new Set([
...moment.tz
.names()
// We need to filter out some time zones, that moment.js knows about, but Elasticsearch
// does not understand and would fail thus with a 400 bad request when using them.
.filter((tz) => !['America/Nuuk', 'EST', 'HST', 'ROC', 'MST'].includes(tz)),
]);
export const isValidTimeZone = (arg?: unknown): arg is string =>
isDefined(arg) && typeof arg === 'string' && ACCEPTED_TIMEZONES.has(arg);

View file

@ -14,6 +14,7 @@ import {
EuiButton,
EuiCheckbox,
EuiCodeBlock,
EuiComboBox,
EuiFieldText,
EuiForm,
EuiFormRow,
@ -22,6 +23,9 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
import { isDefined } from '@kbn/ml-is-defined';
import { ACCEPTED_TIMEZONES, isValidTimeZone } from '../../../../common/time_zone_utils';
import { AggName } from '../../../../../../common/types/aggregations';
import { dictionaryToArray } from '../../../../../../common/types/common';
@ -109,6 +113,21 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
isPivotGroupByConfigWithUiSupport(defaultData) ? defaultData.field : ''
);
const [interval, setInterval] = useState(getDefaultInterval(defaultData));
const TIMEZONE_OPTIONS = useMemo(
() => [...ACCEPTED_TIMEZONES].map((value) => ({ value, label: value })),
[]
);
// Should default to time zone that user sets in json editor first
const [selectedTimeZone, setSelectedTimeZone] = useState<Array<EuiComboBoxOptionOption<string>>>(
isGroupByDateHistogram(defaultData) && isValidTimeZone(defaultData.time_zone)
? [TIMEZONE_OPTIONS.find((v) => v.value === defaultData.time_zone)!]
: []
);
const timeZone = useMemo(() => selectedTimeZone[0]?.value, [selectedTimeZone]);
const [missingBucket, setMissingBucket] = useState(
isPivotGroupByConfigWithUiSupport(defaultData) && defaultData.missing_bucket
);
@ -122,6 +141,10 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
updatedItem.interval = interval;
} else if (isGroupByDateHistogram(updatedItem) && interval !== undefined) {
updatedItem.calendar_interval = interval;
if (isValidTimeZone(timeZone)) {
updatedItem.time_zone = timeZone;
}
}
// Casting to PivotGroupByConfig because TS would otherwise complain about the
@ -169,9 +192,14 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
(isGroupByDateHistogram(defaultData) || isGroupByHistogram(defaultData)) &&
isIntervalValid(interval, defaultData.agg);
const timeZoneValid = isGroupByDateHistogram(defaultData) && isValidTimeZone(timeZone);
let formValid = validAggName;
if (formValid && (isGroupByDateHistogram(defaultData) || isGroupByHistogram(defaultData))) {
formValid = isIntervalValid(interval, defaultData.agg);
if (isGroupByDateHistogram(defaultData) && isDefined(defaultData.time_zone)) {
formValid = timeZoneValid;
}
}
return (
@ -225,13 +253,11 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
)}
{(isGroupByDateHistogram(defaultData) || isGroupByHistogram(defaultData)) && (
<EuiFormRow
error={
!validInterval && [
i18n.translate('xpack.transform.groupBy.popoverForm.intervalError', {
defaultMessage: 'Invalid interval.',
}),
]
}
error={[
i18n.translate('xpack.transform.groupBy.popoverForm.intervalError', {
defaultMessage: 'Invalid interval.',
}),
]}
isInvalid={!validInterval}
label={i18n.translate('xpack.transform.groupBy.popoverForm.intervalLabel', {
defaultMessage: 'Interval',
@ -263,6 +289,28 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
</>
</EuiFormRow>
)}
{isGroupByDateHistogram(defaultData) && isDefined(defaultData.time_zone) ? (
<EuiFormRow
error={i18n.translate('xpack.transform.groupBy.popoverForm.timeZoneError', {
defaultMessage: 'Invalid time zone.',
})}
isInvalid={!timeZoneValid}
label={i18n.translate('xpack.transform.groupBy.popoverForm.timeZoneLabel', {
defaultMessage: 'Time zone',
})}
>
<EuiComboBox
options={TIMEZONE_OPTIONS}
onChange={(opt) => setSelectedTimeZone(opt)}
selectedOptions={selectedTimeZone}
aria-label={i18n.translate('xpack.transform.groupBy.popoverForm.timeZoneAriaLabel', {
defaultMessage: 'Time zone',
})}
singleSelection={{ asPlainText: true }}
/>
</EuiFormRow>
) : null}
{!isUnsupportedAgg && (
<EuiFormRow
helpText={