Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[vega] Handle removal of deprecated date histogram interval #109090

Merged
merged 17 commits into from Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -22,6 +22,13 @@ describe('parseEsInterval', () => {
expect(parseEsInterval('1y')).toEqual({ value: 1, unit: 'y', type: 'calendar' });
});

it('should correctly parse an user-friendly intervals', () => {
expect(parseEsInterval('minute')).toEqual({ value: 1, unit: 'm', type: 'calendar' });
expect(parseEsInterval('hour')).toEqual({ value: 1, unit: 'h', type: 'calendar' });
expect(parseEsInterval('month')).toEqual({ value: 1, unit: 'M', type: 'calendar' });
expect(parseEsInterval('year')).toEqual({ value: 1, unit: 'y', type: 'calendar' });
});

it('should correctly parse an interval containing unit and multiple value', () => {
expect(parseEsInterval('250ms')).toEqual({ value: 250, unit: 'ms', type: 'fixed' });
expect(parseEsInterval('90s')).toEqual({ value: 90, unit: 's', type: 'fixed' });
Expand Down
Expand Up @@ -7,16 +7,37 @@
*/

import dateMath, { Unit } from '@elastic/datemath';

import { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error';
import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error';

const ES_INTERVAL_STRING_REGEX = new RegExp(
'^([1-9][0-9]*)\\s*(' + dateMath.units.join('|') + ')$'
);

export type ParsedInterval = ReturnType<typeof parseEsInterval>;

/** ES allows to work at user-friendly intervals.
* This method matches between these intervals and the intervals accepted by parseEsInterval.
* @internal **/
const mapToEquivalentInterval = (interval: string) => {
switch (interval) {
case 'minute':
return '1m';
case 'hour':
return '1h';
case 'day':
return '1d';
case 'week':
return '1w';
case 'month':
return '1M';
case 'quarter':
return '1q';
case 'year':
return '1y';
}
return interval;
};

/**
* Extracts interval properties from an ES interval string. Disallows unrecognized interval formats
* and fractional values. Converts some intervals from "calendar" to "fixed" when the number of
Expand All @@ -37,7 +58,7 @@ export type ParsedInterval = ReturnType<typeof parseEsInterval>;
*
*/
export function parseEsInterval(interval: string) {
const matches = String(interval).trim().match(ES_INTERVAL_STRING_REGEX);
const matches = String(mapToEquivalentInterval(interval)).trim().match(ES_INTERVAL_STRING_REGEX);

if (!matches) {
throw new InvalidEsIntervalFormatError(interval);
Expand Down
@@ -0,0 +1,135 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { shouldShowDeprecatedHistogramIntervalInfo } from './deprecated_interval_info';

describe('shouldShowDeprecatedHistogramIntervalInfo', () => {
test('should show deprecated histogram interval', () => {
expect(
shouldShowDeprecatedHistogramIntervalInfo({
data: {
url: {
body: {
aggs: {
test: {
date_histogram: {
interval: 'day',
},
},
},
},
},
},
})
).toBeTruthy();

expect(
shouldShowDeprecatedHistogramIntervalInfo({
data: [
{
url: {
body: {
aggs: {
test: {
date_histogram: {
interval: 'day',
},
},
},
},
},
},
{
url: {
body: {
aggs: {
test: {
date_histogram: {
calendar_interval: 'day',
},
},
},
},
},
},
],
})
).toBeTruthy();
});

test('should not show deprecated histogram interval', () => {
expect(
shouldShowDeprecatedHistogramIntervalInfo({
data: {
url: {
body: {
aggs: {
test: {
date_histogram: {
interval: { '%autointerval%': true },
},
},
},
},
},
},
})
).toBeFalsy();

expect(
shouldShowDeprecatedHistogramIntervalInfo({
data: {
url: {
body: {
aggs: {
test: {
auto_date_histogram: {
field: 'bytes',
},
},
},
},
},
},
})
).toBeFalsy();

expect(
shouldShowDeprecatedHistogramIntervalInfo({
data: [
{
url: {
body: {
aggs: {
test: {
date_histogram: {
calendar_interval: 'week',
},
},
},
},
},
},
{
url: {
body: {
aggs: {
test: {
date_histogram: {
fixed_interval: '23d',
},
},
},
},
},
},
],
})
).toBeFalsy();
});
});
@@ -0,0 +1,53 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { EuiCallOut, EuiButtonIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { VegaSpec } from '../data_model/types';
import { getDocLinks } from '../services';

import { BUCKET_TYPES } from '../../../../data/public';

export const DeprecatedHistogramIntervalInfo = () => (
<EuiCallOut
className="hide-for-sharing"
data-test-subj="deprecatedHistogramIntervalInfo"
size="s"
title={
<FormattedMessage
id="visTypeVega.deprecatedHistogramIntervalInfo.message"
defaultMessage="Combined 'interval' field has been deprecated in favor of two new,
explicit fields: 'calendar_interval' and 'fixed_interval'. {dateHistogramDoc}"
values={{
dateHistogramDoc: (
<EuiButtonIcon
iconType="popout"
href={getDocLinks().links.aggs.date_histogram}
target="_blank"
/>
),
}}
/>
}
iconType="help"
/>
);

export const shouldShowDeprecatedHistogramIntervalInfo = (spec: VegaSpec) => {
const data = Array.isArray(spec.data) ? spec.data : [spec.data];

return data.some((dataItem) => {
const aggs = dataItem.url?.body?.aggs ?? {};

return Object.keys(aggs).some((key) => {
const dateHistogram = aggs[key]?.[BUCKET_TYPES.DATE_HISTOGRAM] || {};
return 'interval' in dateHistogram && typeof dateHistogram.interval !== 'object';
});
});
};
Expand Up @@ -6,55 +6,37 @@
* Side Public License, v 1.
*/

import { parse } from 'hjson';
import React from 'react';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Vis } from '../../../../visualizations/public';

function ExperimentalMapLayerInfo() {
const title = (
<FormattedMessage
id="visTypeVega.mapView.experimentalMapLayerInfo"
defaultMessage="Map layer is experimental and is not subject to the support SLA of official GA features.
For feedback, please create an issue in {githubLink}."
values={{
githubLink: (
<EuiLink
external
href="https://github.com/elastic/kibana/issues/new/choose"
target="_blank"
>
GitHub
</EuiLink>
),
}}
/>
);

return (
<EuiCallOut
className="hide-for-sharing"
data-test-subj="experimentalMapLayerInfo"
size="s"
title={title}
iconType="beaker"
/>
);
}
import type { VegaSpec } from '../data_model/types';

export const getInfoMessage = (vis: Vis) => {
if (vis.params.spec) {
try {
const spec = parse(vis.params.spec, { legacyRoot: false, keepWsc: true });

if (spec.config?.kibana?.type === 'map') {
return <ExperimentalMapLayerInfo />;
}
} catch (e) {
// spec is invalid
export const ExperimentalMapLayerInfo = () => (
<EuiCallOut
className="hide-for-sharing"
data-test-subj="experimentalMapLayerInfo"
size="s"
title={
<FormattedMessage
id="visTypeVega.mapView.experimentalMapLayerInfo"
defaultMessage="Map layer is experimental and is not subject to the support SLA of official GA features.
For feedback, please create an issue in {githubLink}."
values={{
githubLink: (
<EuiLink
external
href="https://github.com/elastic/kibana/issues/new/choose"
target="_blank"
>
GitHub
</EuiLink>
),
}}
/>
}
}
iconType="beaker"
/>
);

return null;
};
export const shouldShowMapLayerInfo = (spec: VegaSpec) => spec.config?.kibana?.type === 'map';
45 changes: 45 additions & 0 deletions src/plugins/vis_types/vega/public/components/vega_info_message.tsx
@@ -0,0 +1,45 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { useMemo } from 'react';
import { parse } from 'hjson';
import { ExperimentalMapLayerInfo, shouldShowMapLayerInfo } from './experimental_map_vis_info';
import {
DeprecatedHistogramIntervalInfo,
shouldShowDeprecatedHistogramIntervalInfo,
} from './deprecated_interval_info';

import type { Vis } from '../../../../visualizations/public';
import type { VegaSpec } from '../data_model/types';

const parseSpec = (spec: string) => {
if (spec) {
try {
return parse(spec, { legacyRoot: false, keepWsc: true });
} catch (e) {
// spec is invalid
}
}
};

const InfoMessage = ({ spec }: { spec: string }) => {
const vegaSpec: VegaSpec = useMemo(() => parseSpec(spec), [spec]);

if (!vegaSpec) {
return null;
}

return (
<>
{shouldShowMapLayerInfo(vegaSpec) && <ExperimentalMapLayerInfo />}
{shouldShowDeprecatedHistogramIntervalInfo(vegaSpec) && <DeprecatedHistogramIntervalInfo />}
</>
);
};

export const getInfoMessage = (vis: Vis) => <InfoMessage spec={vis.params.spec} />;
Expand Up @@ -178,11 +178,11 @@ describe(`EsQueryParser.injectQueryContextVars`, () => {
);
test(
`%autointerval% = true`,
check({ interval: { '%autointerval%': true } }, { interval: `1h` }, ctxObj)
check({ interval: { '%autointerval%': true } }, { calendar_interval: `1h` }, ctxObj)
);
test(
`%autointerval% = 10`,
check({ interval: { '%autointerval%': 10 } }, { interval: `3h` }, ctxObj)
check({ interval: { '%autointerval%': 10 } }, { fixed_interval: `3h` }, ctxObj)
);
test(`%timefilter% = min`, check({ a: { '%timefilter%': 'min' } }, { a: rangeStart }));
test(`%timefilter% = max`, check({ a: { '%timefilter%': 'max' } }, { a: rangeEnd }));
Expand Down