mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ML] Add options to exclude or include frozen data tier for Anomaly detection and Index data visualizer (#122306)
* Add exclude frozen data tier * Rename util name to addExcludeFrozenToQuery for clarity * Add menu option to let user pick * Add menu option to let user pick * Fix match_all which is not valid if we want to exclude * Update snapshot * Update texts * Fix translations * Add storage pref & radio * Update dv selector, snapshots * Fix message * Update texts * Update snapshots * Put in useMemo Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
703a451e41
commit
600abd7535
19 changed files with 705 additions and 79 deletions
|
@ -5,22 +5,45 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { TimefilterContract } from 'src/plugins/data/public';
|
||||
import { DataView } from 'src/plugins/data/common';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiRadioGroup,
|
||||
EuiRadioGroupOption,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { setFullTimeRange } from './full_time_range_selector_service';
|
||||
import { useDataVisualizerKibana } from '../../../kibana_context';
|
||||
import { DV_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage';
|
||||
|
||||
export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
|
||||
|
||||
interface Props {
|
||||
timefilter: TimefilterContract;
|
||||
indexPattern: IndexPattern;
|
||||
indexPattern: DataView;
|
||||
disabled: boolean;
|
||||
query?: Query;
|
||||
query?: QueryDslQueryContainer;
|
||||
callback?: (a: any) => void;
|
||||
}
|
||||
|
||||
const FROZEN_TIER_PREFERENCE = {
|
||||
EXCLUDE: 'exclude-frozen',
|
||||
INCLUDE: 'include-frozen',
|
||||
} as const;
|
||||
|
||||
type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
|
||||
|
||||
// Component for rendering a button which automatically sets the range of the time filter
|
||||
// to the time range of data in the index(es) mapped to the supplied Kibana data view or query.
|
||||
export const FullTimeRangeSelector: FC<Props> = ({
|
||||
|
@ -37,36 +60,144 @@ export const FullTimeRangeSelector: FC<Props> = ({
|
|||
} = useDataVisualizerKibana();
|
||||
|
||||
// wrapper around setFullTimeRange to allow for the calling of the optional callBack prop
|
||||
async function setRange(i: IndexPattern, q?: Query) {
|
||||
try {
|
||||
const fullTimeRange = await setFullTimeRange(timefilter, i, q);
|
||||
if (typeof callback === 'function') {
|
||||
callback(fullTimeRange);
|
||||
const setRange = useCallback(
|
||||
async (i: DataView, q?: QueryDslQueryContainer, excludeFrozenData?: boolean) => {
|
||||
try {
|
||||
const fullTimeRange = await setFullTimeRange(timefilter, i, q, excludeFrozenData);
|
||||
if (typeof callback === 'function') {
|
||||
callback(fullTimeRange);
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification',
|
||||
{
|
||||
defaultMessage: 'An error occurred setting the time range.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification',
|
||||
},
|
||||
[callback, timefilter, toasts]
|
||||
);
|
||||
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const [frozenDataPreference, setFrozenDataPreference] = useStorage<FrozenTierPreference>(
|
||||
DV_FROZEN_TIER_PREFERENCE,
|
||||
// By default we will exclude frozen data tier
|
||||
FROZEN_TIER_PREFERENCE.EXCLUDE
|
||||
);
|
||||
|
||||
const setPreference = useCallback(
|
||||
(id: string) => {
|
||||
setFrozenDataPreference(id as FrozenTierPreference);
|
||||
setRange(indexPattern, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE);
|
||||
closePopover();
|
||||
},
|
||||
[indexPattern, query, setFrozenDataPreference, setRange]
|
||||
);
|
||||
|
||||
const onButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false);
|
||||
};
|
||||
|
||||
const sortOptions: EuiRadioGroupOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
id: FROZEN_TIER_PREFERENCE.EXCLUDE,
|
||||
label: i18n.translate(
|
||||
'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
|
||||
{
|
||||
defaultMessage: 'An error occurred setting the time range.',
|
||||
defaultMessage: 'Exclude frozen data tier',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: FROZEN_TIER_PREFERENCE.INCLUDE,
|
||||
label: i18n.translate(
|
||||
'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
|
||||
{
|
||||
defaultMessage: 'Include frozen data tier',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() => (
|
||||
<EuiPanel>
|
||||
<EuiRadioGroup
|
||||
options={sortOptions}
|
||||
idSelected={frozenDataPreference}
|
||||
onChange={setPreference}
|
||||
compressed
|
||||
/>
|
||||
</EuiPanel>
|
||||
),
|
||||
[sortOptions, frozenDataPreference, setPreference]
|
||||
);
|
||||
|
||||
const buttonTooltip = useMemo(
|
||||
() =>
|
||||
frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? (
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
|
||||
defaultMessage="Use full range of data excluding frozen data tier."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
|
||||
defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
|
||||
/>
|
||||
),
|
||||
[frozenDataPreference]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
isDisabled={disabled}
|
||||
onClick={() => setRange(indexPattern, query)}
|
||||
data-test-subj="dataVisualizerButtonUseFullData"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
defaultMessage="Use full {indexPatternTitle} data"
|
||||
values={{
|
||||
indexPatternTitle: indexPattern.title,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
|
||||
<EuiToolTip content={buttonTooltip}>
|
||||
<EuiButton
|
||||
isDisabled={disabled}
|
||||
onClick={() => setRange(indexPattern, query, true)}
|
||||
data-test-subj="dataVisualizerButtonUseFullData"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
defaultMessage="Use full data"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id={'mlFullTimeRangeSelectorOption'}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
display="base"
|
||||
size="m"
|
||||
iconType="boxesVertical"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.dataVisualizer.index.fullTimeRangeSelector.moreOptionsButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'More options',
|
||||
}
|
||||
)}
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
{popoverContent}
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
|
||||
import moment from 'moment';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { Query, TimefilterContract } from 'src/plugins/data/public';
|
||||
import { TimefilterContract } from 'src/plugins/data/public';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
import { isPopulatedObject } from '../../../../../common/utils/object_utils';
|
||||
import { getTimeFieldRange } from '../../services/time_field_range';
|
||||
import { GetTimeFieldRangeResponse } from '../../../../../common/types/time_field_request';
|
||||
import { addExcludeFrozenToQuery } from '../../utils/query_utils';
|
||||
|
||||
export interface TimeRange {
|
||||
from: number;
|
||||
|
@ -22,14 +24,15 @@ export interface TimeRange {
|
|||
export async function setFullTimeRange(
|
||||
timefilter: TimefilterContract,
|
||||
indexPattern: IndexPattern,
|
||||
query?: Query
|
||||
query?: QueryDslQueryContainer,
|
||||
excludeFrozenData?: boolean
|
||||
): Promise<GetTimeFieldRangeResponse> {
|
||||
const runtimeMappings = indexPattern.getComputedFields()
|
||||
.runtimeFields as estypes.MappingRuntimeFields;
|
||||
const resp = await getTimeFieldRange({
|
||||
index: indexPattern.title,
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
query,
|
||||
query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query,
|
||||
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
|
||||
});
|
||||
timefilter.setTime({
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { useCallback, useState } from 'react';
|
||||
import { useDataVisualizerKibana } from '../../kibana_context';
|
||||
|
||||
export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference';
|
||||
|
||||
export type DV = Partial<{
|
||||
[DV_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen';
|
||||
}> | null;
|
||||
|
||||
export type DVKey = keyof Exclude<DV, null>;
|
||||
|
||||
/**
|
||||
* Hook for accessing and changing a value in the storage.
|
||||
* @param key - Storage key
|
||||
* @param initValue
|
||||
*/
|
||||
export function useStorage<T>(key: DVKey, initValue?: T): [T, (value: T) => void] {
|
||||
const {
|
||||
services: { storage },
|
||||
} = useDataVisualizerKibana();
|
||||
|
||||
const [val, setVal] = useState<T>(storage.get(key) ?? initValue);
|
||||
|
||||
const setStorage = useCallback(
|
||||
(value: T): void => {
|
||||
try {
|
||||
storage.set(key, value);
|
||||
setVal(value);
|
||||
} catch (e) {
|
||||
throw new Error('Unable to update storage with provided value');
|
||||
}
|
||||
},
|
||||
[key, storage]
|
||||
);
|
||||
|
||||
return [val, setStorage];
|
||||
}
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { lazyLoadModules } from '../../../lazy_load_bundle';
|
||||
import { GetTimeFieldRangeResponse } from '../../../../common/types/time_field_request';
|
||||
import { Query } from '../../../../../../../src/plugins/data/common/query';
|
||||
|
||||
export async function getTimeFieldRange({
|
||||
index,
|
||||
|
@ -18,7 +18,7 @@ export async function getTimeFieldRange({
|
|||
}: {
|
||||
index: string;
|
||||
timeFieldName?: string;
|
||||
query?: Query;
|
||||
query?: QueryDslQueryContainer;
|
||||
runtimeMappings?: estypes.MappingRuntimeFields;
|
||||
}) {
|
||||
const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings });
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { addExcludeFrozenToQuery } from './query_utils';
|
||||
|
||||
describe('Util: addExcludeFrozenToQuery()', () => {
|
||||
test('Validation checks.', () => {
|
||||
expect(
|
||||
addExcludeFrozenToQuery({
|
||||
match_all: {},
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toMatchObject({
|
||||
bool: {
|
||||
must: [{ match_all: {} }],
|
||||
must_not: [{ term: { _tier: { value: 'data_frozen' } } }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
addExcludeFrozenToQuery({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: {
|
||||
term: {
|
||||
category: {
|
||||
value: 'clothing',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toMatchObject({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [
|
||||
{ term: { category: { value: 'clothing' } } },
|
||||
{ term: { _tier: { value: 'data_frozen' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
addExcludeFrozenToQuery({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [{ term: { category: { value: 'clothing' } } }],
|
||||
},
|
||||
})
|
||||
).toMatchObject({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [
|
||||
{ term: { category: { value: 'clothing' } } },
|
||||
{ term: { _tier: { value: 'data_frozen' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(addExcludeFrozenToQuery(undefined)).toMatchObject({
|
||||
bool: {
|
||||
must_not: [{ term: { _tier: { value: 'data_frozen' } } }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { isPopulatedObject } from '../../../../common/utils/object_utils';
|
||||
|
||||
export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => {
|
||||
const FROZEN_TIER_TERM = {
|
||||
term: {
|
||||
_tier: {
|
||||
value: 'data_frozen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!originalQuery) {
|
||||
return {
|
||||
bool: {
|
||||
must_not: [FROZEN_TIER_TERM],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const query = cloneDeep(originalQuery);
|
||||
|
||||
delete query.match_all;
|
||||
|
||||
if (isPopulatedObject(query.bool)) {
|
||||
// Must_not can be both arrays or singular object
|
||||
if (Array.isArray(query.bool.must_not)) {
|
||||
query.bool.must_not.push(FROZEN_TIER_TERM);
|
||||
} else {
|
||||
// If there's already a must_not condition
|
||||
if (isPopulatedObject(query.bool.must_not)) {
|
||||
query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM];
|
||||
}
|
||||
if (query.bool.must_not === undefined) {
|
||||
query.bool.must_not = [FROZEN_TIER_TERM];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query.bool = {
|
||||
must_not: [FROZEN_TIER_TERM],
|
||||
};
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
|
@ -8,7 +8,11 @@
|
|||
import { CoreStart } from 'kibana/public';
|
||||
import { KibanaReactContextValue, useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import type { DataVisualizerStartDependencies } from '../plugin';
|
||||
import type { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
export type StartServices = CoreStart & DataVisualizerStartDependencies;
|
||||
export type StartServices = CoreStart &
|
||||
DataVisualizerStartDependencies & {
|
||||
storage: IStorageWrapper;
|
||||
};
|
||||
export type DataVisualizerKibanaReactContextValue = KibanaReactContextValue<StartServices>;
|
||||
export const useDataVisualizerKibana = () => useKibana<StartServices>();
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
import { IScopedClusterClient } from 'kibana/server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { isPopulatedObject } from '../common/utils';
|
||||
|
||||
export async function getTimeFieldRange(
|
||||
client: IScopedClusterClient,
|
||||
index: string[] | string,
|
||||
timeFieldName: string,
|
||||
query: any,
|
||||
query: QueryDslQueryContainer,
|
||||
runtimeMappings?: estypes.MappingRuntimeFields
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
|
|
|
@ -13,6 +13,8 @@ export const ML_APPLY_TIME_RANGE_CONFIG = 'ml.jobSelectorFlyout.applyTimeRange';
|
|||
|
||||
export const ML_GETTING_STARTED_CALLOUT_DISMISSED = 'ml.gettingStarted.isDismissed';
|
||||
|
||||
export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
|
||||
|
||||
export type PartitionFieldConfig =
|
||||
| {
|
||||
/**
|
||||
|
@ -44,6 +46,7 @@ export type MlStorage = Partial<{
|
|||
[ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig;
|
||||
[ML_APPLY_TIME_RANGE_CONFIG]: ApplyTimeRangeConfig;
|
||||
[ML_GETTING_STARTED_CALLOUT_DISMISSED]: boolean | undefined;
|
||||
[ML_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen';
|
||||
}> | null;
|
||||
|
||||
export type MlStorageKey = keyof Exclude<MlStorage, null>;
|
||||
|
|
76
x-pack/plugins/ml/common/util/query_utils.test.ts
Normal file
76
x-pack/plugins/ml/common/util/query_utils.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { addExcludeFrozenToQuery } from './query_utils';
|
||||
|
||||
describe('Util: addExcludeFrozenToQuery()', () => {
|
||||
test('Validation checks.', () => {
|
||||
expect(
|
||||
addExcludeFrozenToQuery({
|
||||
match_all: {},
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toMatchObject({
|
||||
bool: {
|
||||
must: [{ match_all: {} }],
|
||||
must_not: [{ term: { _tier: { value: 'data_frozen' } } }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
addExcludeFrozenToQuery({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: {
|
||||
term: {
|
||||
category: {
|
||||
value: 'clothing',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toMatchObject({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [
|
||||
{ term: { category: { value: 'clothing' } } },
|
||||
{ term: { _tier: { value: 'data_frozen' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
addExcludeFrozenToQuery({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [{ term: { category: { value: 'clothing' } } }],
|
||||
},
|
||||
})
|
||||
).toMatchObject({
|
||||
bool: {
|
||||
must: [],
|
||||
must_not: [
|
||||
{ term: { category: { value: 'clothing' } } },
|
||||
{ term: { _tier: { value: 'data_frozen' } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(addExcludeFrozenToQuery(undefined)).toMatchObject({
|
||||
bool: {
|
||||
must_not: [{ term: { _tier: { value: 'data_frozen' } } }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
53
x-pack/plugins/ml/common/util/query_utils.ts
Normal file
53
x-pack/plugins/ml/common/util/query_utils.ts
Normal file
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { isPopulatedObject } from './object_utils';
|
||||
|
||||
export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => {
|
||||
const FROZEN_TIER_TERM = {
|
||||
term: {
|
||||
_tier: {
|
||||
value: 'data_frozen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!originalQuery) {
|
||||
return {
|
||||
bool: {
|
||||
must_not: [FROZEN_TIER_TERM],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const query = cloneDeep(originalQuery);
|
||||
|
||||
delete query.match_all;
|
||||
|
||||
if (isPopulatedObject(query.bool)) {
|
||||
// Must_not can be both arrays or singular object
|
||||
if (Array.isArray(query.bool.must_not)) {
|
||||
query.bool.must_not.push(FROZEN_TIER_TERM);
|
||||
} else {
|
||||
// If there's already a must_not condition
|
||||
if (isPopulatedObject(query.bool.must_not)) {
|
||||
query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM];
|
||||
}
|
||||
if (query.bool.must_not === undefined) {
|
||||
query.bool.must_not = [FROZEN_TIER_TERM];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query.bool = {
|
||||
must_not: [FROZEN_TIER_TERM],
|
||||
};
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
|
@ -1,19 +1,77 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FullTimeRangeSelector renders the selector 1`] = `
|
||||
<EuiButton
|
||||
data-test-subj="mlButtonUseFullData"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use full {dataViewTitle} data"
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
values={
|
||||
Object {
|
||||
"dataViewTitle": "test-data-view",
|
||||
}
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiButton>
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="mlButtonUseFullData"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use full data"
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="More"
|
||||
display="base"
|
||||
iconType="boxesVertical"
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
display="inlineBlock"
|
||||
hasArrow={true}
|
||||
id="mlFullTimeRangeSelectorOption"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiPanel>
|
||||
<EuiRadioGroup
|
||||
compressed={true}
|
||||
idSelected="e"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"id": "exclude-frozen",
|
||||
"label": "Exclude frozen data tier",
|
||||
},
|
||||
Object {
|
||||
"id": "include-frozen",
|
||||
"label": "Include frozen data tier",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
|
|
@ -20,6 +20,12 @@ jest.mock('./full_time_range_selector_service', () => ({
|
|||
mockSetFullTimeRange(indexPattern, query),
|
||||
}));
|
||||
|
||||
jest.mock('../../contexts/ml/use_storage', () => {
|
||||
return {
|
||||
useStorage: jest.fn(() => 'exclude-frozen'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('FullTimeRangeSelector', () => {
|
||||
const dataView = {
|
||||
id: '0844fc70-5ab5-11e9-935e-836737467b0f',
|
||||
|
|
|
@ -5,44 +5,160 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Query } from 'src/plugins/data/public';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiButton,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiRadioGroup,
|
||||
EuiPanel,
|
||||
EuiToolTip,
|
||||
EuiPopover,
|
||||
EuiRadioGroupOption,
|
||||
} from '@elastic/eui';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '../../../../../../../src/plugins/data_views/public';
|
||||
import { setFullTimeRange } from './full_time_range_selector_service';
|
||||
import { useStorage } from '../../contexts/ml/use_storage';
|
||||
import { ML_FROZEN_TIER_PREFERENCE } from '../../../../common/types/storage';
|
||||
|
||||
interface Props {
|
||||
dataView: DataView;
|
||||
query: Query;
|
||||
query: QueryDslQueryContainer;
|
||||
disabled: boolean;
|
||||
callback?: (a: any) => void;
|
||||
}
|
||||
|
||||
const FROZEN_TIER_PREFERENCE = {
|
||||
EXCLUDE: 'exclude-frozen',
|
||||
INCLUDE: 'include-frozen',
|
||||
} as const;
|
||||
|
||||
type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
|
||||
|
||||
// Component for rendering a button which automatically sets the range of the time filter
|
||||
// to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query.
|
||||
export const FullTimeRangeSelector: FC<Props> = ({ dataView, query, disabled, callback }) => {
|
||||
// wrapper around setFullTimeRange to allow for the calling of the optional callBack prop
|
||||
async function setRange(i: DataView, q: Query) {
|
||||
const fullTimeRange = await setFullTimeRange(i, q);
|
||||
async function setRange(i: DataView, q: QueryDslQueryContainer, excludeFrozenData = true) {
|
||||
const fullTimeRange = await setFullTimeRange(i, q, excludeFrozenData);
|
||||
if (typeof callback === 'function') {
|
||||
callback(fullTimeRange);
|
||||
}
|
||||
}
|
||||
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const [frozenDataPreference, setFrozenDataPreference] = useStorage<FrozenTierPreference>(
|
||||
ML_FROZEN_TIER_PREFERENCE,
|
||||
FROZEN_TIER_PREFERENCE.EXCLUDE
|
||||
);
|
||||
|
||||
const onButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false);
|
||||
};
|
||||
|
||||
const sortOptions: EuiRadioGroupOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
id: FROZEN_TIER_PREFERENCE.EXCLUDE,
|
||||
label: i18n.translate(
|
||||
'xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel',
|
||||
{
|
||||
defaultMessage: 'Exclude frozen data tier',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: FROZEN_TIER_PREFERENCE.INCLUDE,
|
||||
label: i18n.translate(
|
||||
'xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel',
|
||||
{
|
||||
defaultMessage: 'Include frozen data tier',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const setPreference = useCallback((id: string) => {
|
||||
setFrozenDataPreference(id as FrozenTierPreference);
|
||||
setRange(dataView, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE);
|
||||
closePopover();
|
||||
}, []);
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() => (
|
||||
<EuiPanel>
|
||||
<EuiRadioGroup
|
||||
options={sortOptions}
|
||||
idSelected={frozenDataPreference}
|
||||
onChange={setPreference}
|
||||
compressed
|
||||
/>
|
||||
</EuiPanel>
|
||||
),
|
||||
[frozenDataPreference, sortOptions]
|
||||
);
|
||||
|
||||
const buttonTooltip = useMemo(
|
||||
() =>
|
||||
frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenButtonTooltip"
|
||||
defaultMessage="Use full range of data excluding frozen data tier."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenButtonTooltip"
|
||||
defaultMessage="Use full range of data including frozen data tier, which might have slower search results."
|
||||
/>
|
||||
),
|
||||
[frozenDataPreference]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
isDisabled={disabled}
|
||||
onClick={() => setRange(dataView, query)}
|
||||
data-test-subj="mlButtonUseFullData"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
defaultMessage="Use full {dataViewTitle} data"
|
||||
values={{
|
||||
dataViewTitle: dataView.title,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center">
|
||||
<EuiToolTip content={buttonTooltip}>
|
||||
<EuiButton
|
||||
isDisabled={disabled}
|
||||
onClick={() => setRange(dataView, query, true)}
|
||||
data-test-subj="mlButtonUseFullData"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
|
||||
defaultMessage="Use full data"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id={'mlFullTimeRangeSelectorOption'}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
display="base"
|
||||
size="m"
|
||||
iconType="boxesVertical"
|
||||
aria-label="More"
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
{popoverContent}
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,13 +8,14 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Query } from 'src/plugins/data/public';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getTimefilter, getToastNotifications } from '../../util/dependency_cache';
|
||||
import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service';
|
||||
import type { DataView } from '../../../../../../../src/plugins/data_views/public';
|
||||
import { isPopulatedObject } from '../../../../common/util/object_utils';
|
||||
import { RuntimeMappings } from '../../../../common/types/fields';
|
||||
import type { RuntimeMappings } from '../../../../common/types/fields';
|
||||
import { addExcludeFrozenToQuery } from '../../../../common/util/query_utils';
|
||||
|
||||
export interface TimeRange {
|
||||
from: number;
|
||||
|
@ -23,7 +24,8 @@ export interface TimeRange {
|
|||
|
||||
export async function setFullTimeRange(
|
||||
indexPattern: DataView,
|
||||
query: Query
|
||||
query: QueryDslQueryContainer,
|
||||
excludeFrozenData: boolean
|
||||
): Promise<GetTimeFieldRangeResponse> {
|
||||
try {
|
||||
const timefilter = getTimefilter();
|
||||
|
@ -31,7 +33,8 @@ export async function setFullTimeRange(
|
|||
const resp = await ml.getTimeFieldRange({
|
||||
index: indexPattern.title,
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
query,
|
||||
// By default we want to use full non-frozen time range
|
||||
query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query,
|
||||
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
|
||||
});
|
||||
timefilter.setTime({
|
||||
|
|
|
@ -42,6 +42,7 @@ import { TIME_FORMAT } from '../../../../../common/constants/time_format';
|
|||
import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning';
|
||||
import { isPopulatedObject } from '../../../../../common/util/object_utils';
|
||||
import { RuntimeMappings } from '../../../../../common/types/fields';
|
||||
import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils';
|
||||
import { MlPageHeader } from '../../../components/page_header';
|
||||
|
||||
export interface ModuleJobUI extends ModuleJob {
|
||||
|
@ -136,7 +137,8 @@ export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
|
|||
const { start, end } = await ml.getTimeFieldRange({
|
||||
index: dataView.title,
|
||||
timeFieldName: dataView.timeFieldName,
|
||||
query: combinedQuery,
|
||||
// By default we want to use full non-frozen time range
|
||||
query: addExcludeFrozenToQuery(combinedQuery),
|
||||
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
|
||||
});
|
||||
return {
|
||||
|
|
|
@ -16,7 +16,6 @@ import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
|
|||
import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs';
|
||||
import { RuntimeMappings } from '../../../common/types/fields';
|
||||
import { isPopulatedObject } from '../../../common/util/object_utils';
|
||||
|
||||
/**
|
||||
* Service for carrying out queries to obtain data
|
||||
* specific to fields in Elasticsearch indices.
|
||||
|
|
|
@ -8388,7 +8388,6 @@
|
|||
"xpack.dataVisualizer.index.fieldNameSelect": "フィールド名",
|
||||
"xpack.dataVisualizer.index.fieldTypeSelect": "フィールド型",
|
||||
"xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "時間範囲の設定中にエラーが発生しました。",
|
||||
"xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "完全な {indexPatternTitle} データを使用",
|
||||
"xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "異常検知は時間ベースのインデックスでのみ実行されます",
|
||||
"xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "インデックスパターン {indexPatternTitle} は時系列に基づくものではありません",
|
||||
"xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName}の平均",
|
||||
|
|
|
@ -8461,7 +8461,6 @@
|
|||
"xpack.dataVisualizer.index.fieldNameSelect": "字段名称",
|
||||
"xpack.dataVisualizer.index.fieldTypeSelect": "字段类型",
|
||||
"xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "设置时间范围时出错。",
|
||||
"xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "使用完整的 {indexPatternTitle} 数据",
|
||||
"xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "仅针对基于时间的索引运行异常检测",
|
||||
"xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "索引模式 {indexPatternTitle} 不基于时间序列",
|
||||
"xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName} 的平均值",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue