Skip to content

Commit 95e308a

Browse files
authored
[ML] Add Anomaly Explorer charts embeddable (#94396)
1 parent 796f679 commit 95e308a

File tree

101 files changed

+4546
-2035
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+4546
-2035
lines changed

x-pack/plugins/ml/common/types/es_client.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
* 2.0.
66
*/
77

8-
import { SearchResponse, ShardsResponse } from 'elasticsearch';
8+
import type { SearchResponse, ShardsResponse } from 'elasticsearch';
9+
import { buildEsQuery } from '../../../../../src/plugins/data/common/es_query/es_query';
10+
import type { DslQuery } from '../../../../../src/plugins/data/common/es_query/kuery';
11+
import type { JsonObject } from '../../../../../src/plugins/kibana_utils/common';
912

1013
export const HITS_TOTAL_RELATION = {
1114
EQ: 'eq',
@@ -30,3 +33,5 @@ export interface SearchResponse7<T = any> {
3033
hits: SearchResponse7Hits<T>;
3134
aggregations?: any;
3235
}
36+
37+
export type InfluencersFilterQuery = ReturnType<typeof buildEsQuery> | DslQuery | JsonObject;

x-pack/plugins/ml/common/types/ml_url_generator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ML_PAGES } from '../constants/ml_url_generator';
1515
import type { DataFrameAnalysisConfigType } from './data_frame_analytics';
1616
import type { SearchQueryLanguage } from '../constants/search';
1717
import type { ListingPageUrlState } from './common';
18+
import type { InfluencersFilterQuery } from './es_client';
1819

1920
type OptionalPageState = object | undefined;
2021

@@ -113,9 +114,9 @@ export interface ExplorerAppState {
113114
viewByFromPage?: number;
114115
};
115116
mlExplorerFilter: {
116-
influencersFilterQuery?: unknown;
117+
influencersFilterQuery?: InfluencersFilterQuery;
117118
filterActive?: boolean;
118-
filteredFields?: string[];
119+
filteredFields?: Array<string | number>;
119120
queryString?: string;
120121
};
121122
query?: any;

x-pack/plugins/ml/common/util/anomaly_utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@ export enum ENTITY_FIELD_TYPE {
2727
PARTITON = 'partition',
2828
}
2929

30+
export const ENTITY_FIELD_OPERATIONS = {
31+
ADD: '+',
32+
REMOVE: '-',
33+
} as const;
34+
35+
export type EntityFieldOperation = typeof ENTITY_FIELD_OPERATIONS[keyof typeof ENTITY_FIELD_OPERATIONS];
36+
3037
export interface EntityField {
3138
fieldName: string;
3239
fieldValue: string | number | undefined;
3340
fieldType?: ENTITY_FIELD_TYPE;
41+
operation?: EntityFieldOperation;
3442
}
3543

3644
// List of function descriptions for which actual values from record level results should be displayed.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import { coreMock } from '../../../../../src/core/public/mocks';
8+
import { createMlStartDepsMock } from './ml_start_deps';
9+
10+
export const createCoreStartMock = () =>
11+
coreMock.createSetup({ pluginStartDeps: createMlStartDepsMock() });
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks';
9+
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
10+
import { kibanaLegacyPluginMock } from '../../../../../src/plugins/kibana_legacy/public/mocks';
11+
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
12+
import { lensPluginMock } from '../../../lens/public/mocks';
13+
import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks';
14+
15+
export const createMlStartDepsMock = () => ({
16+
data: dataPluginMock.createStartContract(),
17+
share: {
18+
urlGenerators: { getUrlGenerator: jest.fn() },
19+
},
20+
kibanaLegacy: kibanaLegacyPluginMock.createStartContract(),
21+
uiActions: uiActionsPluginMock.createStartContract(),
22+
spaces: jest.fn(),
23+
embeddable: embeddablePluginMock.createStartContract(),
24+
maps: jest.fn(),
25+
lens: lensPluginMock.createStartContract(),
26+
triggersActionsUi: triggersActionsUiMock.createStart(),
27+
fileUpload: jest.fn(),
28+
});

x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { EuiSelect } from '@elastic/eui';
1010
import { i18n } from '@kbn/i18n';
1111
import { usePageUrlState } from '../../../util/url_state';
1212

13-
interface TableInterval {
13+
export interface TableInterval {
1414
display: string;
1515
val: string;
1616
}
@@ -64,16 +64,24 @@ export const useTableInterval = (): [TableInterval, (v: TableInterval) => void]
6464
export const SelectInterval: FC = () => {
6565
const [interval, setInterval] = useTableInterval();
6666

67-
const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
68-
setInterval(optionValueToInterval(e.target.value));
67+
return <SelectIntervalUI interval={interval} onChange={setInterval} />;
68+
};
69+
70+
interface SelectIntervalUIProps {
71+
interval: TableInterval;
72+
onChange: (interval: TableInterval) => void;
73+
}
74+
export const SelectIntervalUI: FC<SelectIntervalUIProps> = ({ interval, onChange }) => {
75+
const handleOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
76+
onChange(optionValueToInterval(e.target.value));
6977
};
7078

7179
return (
7280
<EuiSelect
7381
options={OPTIONS}
7482
className="ml-select-interval"
7583
value={interval.val}
76-
onChange={onChange}
84+
onChange={handleOnChange}
7785
/>
7886
);
7987
};

x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const optionsMap = {
3838
[criticalLabel]: ANOMALY_THRESHOLD.CRITICAL,
3939
};
4040

41-
interface TableSeverity {
41+
export interface TableSeverity {
4242
val: number;
4343
display: string;
4444
color: string;
@@ -67,7 +67,7 @@ export const SEVERITY_OPTIONS: TableSeverity[] = [
6767
},
6868
];
6969

70-
function optionValueToThreshold(value: number) {
70+
export function optionValueToThreshold(value: number) {
7171
// Get corresponding threshold object with required display and val properties from the specified value.
7272
let threshold = SEVERITY_OPTIONS.find((opt) => opt.val === value);
7373

@@ -121,17 +121,26 @@ interface Props {
121121
export const SelectSeverity: FC<Props> = ({ classNames } = { classNames: '' }) => {
122122
const [severity, setSeverity] = useTableSeverity();
123123

124-
const onChange = (valueDisplay: string) => {
125-
setSeverity(optionValueToThreshold(optionsMap[valueDisplay]));
124+
return <SelectSeverityUI severity={severity} onChange={setSeverity} />;
125+
};
126+
127+
export const SelectSeverityUI: FC<{
128+
classNames?: string;
129+
severity: TableSeverity;
130+
onChange: (s: TableSeverity) => void;
131+
}> = ({ classNames = '', severity, onChange }) => {
132+
const handleOnChange = (valueDisplay: string) => {
133+
onChange(optionValueToThreshold(optionsMap[valueDisplay]));
126134
};
127135

128136
return (
129137
<EuiSuperSelect
138+
data-test-subj={'mlAnomalySeverityThresholdControls'}
130139
className={classNames}
131140
hasDividers
132141
options={getSeverityOptions()}
133142
valueOfSelected={severity.display}
134-
onChange={onChange}
143+
onChange={handleOnChange}
135144
/>
136145
);
137146
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { useMemo } from 'react';
9+
import { useUiSettings } from '../../contexts/kibana';
10+
import { TimeBuckets } from '../../util/time_buckets';
11+
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common';
12+
13+
export const useTimeBuckets = () => {
14+
const uiSettings = useUiSettings();
15+
return useMemo(() => {
16+
return new TimeBuckets({
17+
'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
18+
'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
19+
dateFormat: uiSettings.get('dateFormat'),
20+
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
21+
});
22+
}, [uiSettings]);
23+
};

x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export function CustomSelectionTable({
324324
isSelected={isItemSelected(item[tableItemId])}
325325
isSelectable={true}
326326
hasActions={true}
327-
data-test-subj="mlCustomSelectionTableRow"
327+
data-test-subj={`mlCustomSelectionTableRow row-${item[tableItemId]}`}
328328
>
329329
{cells}
330330
</EuiTableRow>

x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
1212
import { i18n } from '@kbn/i18n';
1313
import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control';
1414
import { MLCATEGORY } from '../../../../common/constants/field_types';
15+
import { ENTITY_FIELD_OPERATIONS } from '../../../../common/util/anomaly_utils';
1516

1617
export type EntityCellFilter = (
1718
entityName: string,
@@ -40,7 +41,7 @@ function getAddFilter({ entityName, entityValue, filter }: EntityCellProps) {
4041
<EuiButtonIcon
4142
size="s"
4243
className="filter-button"
43-
onClick={() => filter(entityName, entityValue, '+')}
44+
onClick={() => filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.ADD)}
4445
iconType="plusInCircle"
4546
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel', {
4647
defaultMessage: 'Add filter',
@@ -65,7 +66,7 @@ function getRemoveFilter({ entityName, entityValue, filter }: EntityCellProps) {
6566
<EuiButtonIcon
6667
size="s"
6768
className="filter-button"
68-
onClick={() => filter(entityName, entityValue, '-')}
69+
onClick={() => filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.REMOVE)}
6970
iconType="minusInCircle"
7071
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel', {
7172
defaultMessage: 'Remove filter',

0 commit comments

Comments
 (0)