Skip to content

Commit

Permalink
[7.x] [ML] Add Anomaly Explorer charts embeddable (#94396) (#95793)
Browse files Browse the repository at this point in the history
  • Loading branch information
qn895 authored Mar 30, 2021
1 parent af9f768 commit f525801
Show file tree
Hide file tree
Showing 101 changed files with 4,546 additions and 2,035 deletions.
7 changes: 6 additions & 1 deletion x-pack/plugins/ml/common/types/es_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* 2.0.
*/

import { SearchResponse, ShardsResponse } from 'elasticsearch';
import type { SearchResponse, ShardsResponse } from 'elasticsearch';
import { buildEsQuery } from '../../../../../src/plugins/data/common/es_query/es_query';
import type { DslQuery } from '../../../../../src/plugins/data/common/es_query/kuery';
import type { JsonObject } from '../../../../../src/plugins/kibana_utils/common';

export const HITS_TOTAL_RELATION = {
EQ: 'eq',
Expand All @@ -30,3 +33,5 @@ export interface SearchResponse7<T = any> {
hits: SearchResponse7Hits<T>;
aggregations?: any;
}

export type InfluencersFilterQuery = ReturnType<typeof buildEsQuery> | DslQuery | JsonObject;
5 changes: 3 additions & 2 deletions x-pack/plugins/ml/common/types/ml_url_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ML_PAGES } from '../constants/ml_url_generator';
import type { DataFrameAnalysisConfigType } from './data_frame_analytics';
import type { SearchQueryLanguage } from '../constants/search';
import type { ListingPageUrlState } from './common';
import type { InfluencersFilterQuery } from './es_client';

type OptionalPageState = object | undefined;

Expand Down Expand Up @@ -113,9 +114,9 @@ export interface ExplorerAppState {
viewByFromPage?: number;
};
mlExplorerFilter: {
influencersFilterQuery?: unknown;
influencersFilterQuery?: InfluencersFilterQuery;
filterActive?: boolean;
filteredFields?: string[];
filteredFields?: Array<string | number>;
queryString?: string;
};
query?: any;
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/ml/common/util/anomaly_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ export enum ENTITY_FIELD_TYPE {
PARTITON = 'partition',
}

export const ENTITY_FIELD_OPERATIONS = {
ADD: '+',
REMOVE: '-',
} as const;

export type EntityFieldOperation = typeof ENTITY_FIELD_OPERATIONS[keyof typeof ENTITY_FIELD_OPERATIONS];

export interface EntityField {
fieldName: string;
fieldValue: string | number | undefined;
fieldType?: ENTITY_FIELD_TYPE;
operation?: EntityFieldOperation;
}

// List of function descriptions for which actual values from record level results should be displayed.
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/ml/public/__mocks__/core_start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { coreMock } from '../../../../../src/core/public/mocks';
import { createMlStartDepsMock } from './ml_start_deps';

export const createCoreStartMock = () =>
coreMock.createSetup({ pluginStartDeps: createMlStartDepsMock() });
28 changes: 28 additions & 0 deletions x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { kibanaLegacyPluginMock } from '../../../../../src/plugins/kibana_legacy/public/mocks';
import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks';
import { lensPluginMock } from '../../../lens/public/mocks';
import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks';

export const createMlStartDepsMock = () => ({
data: dataPluginMock.createStartContract(),
share: {
urlGenerators: { getUrlGenerator: jest.fn() },
},
kibanaLegacy: kibanaLegacyPluginMock.createStartContract(),
uiActions: uiActionsPluginMock.createStartContract(),
spaces: jest.fn(),
embeddable: embeddablePluginMock.createStartContract(),
maps: jest.fn(),
lens: lensPluginMock.createStartContract(),
triggersActionsUi: triggersActionsUiMock.createStart(),
fileUpload: jest.fn(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { usePageUrlState } from '../../../util/url_state';

interface TableInterval {
export interface TableInterval {
display: string;
val: string;
}
Expand Down Expand Up @@ -64,16 +64,24 @@ export const useTableInterval = (): [TableInterval, (v: TableInterval) => void]
export const SelectInterval: FC = () => {
const [interval, setInterval] = useTableInterval();

const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setInterval(optionValueToInterval(e.target.value));
return <SelectIntervalUI interval={interval} onChange={setInterval} />;
};

interface SelectIntervalUIProps {
interval: TableInterval;
onChange: (interval: TableInterval) => void;
}
export const SelectIntervalUI: FC<SelectIntervalUIProps> = ({ interval, onChange }) => {
const handleOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onChange(optionValueToInterval(e.target.value));
};

return (
<EuiSelect
options={OPTIONS}
className="ml-select-interval"
value={interval.val}
onChange={onChange}
onChange={handleOnChange}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const optionsMap = {
[criticalLabel]: ANOMALY_THRESHOLD.CRITICAL,
};

interface TableSeverity {
export interface TableSeverity {
val: number;
display: string;
color: string;
Expand Down Expand Up @@ -67,7 +67,7 @@ export const SEVERITY_OPTIONS: TableSeverity[] = [
},
];

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

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

const onChange = (valueDisplay: string) => {
setSeverity(optionValueToThreshold(optionsMap[valueDisplay]));
return <SelectSeverityUI severity={severity} onChange={setSeverity} />;
};

export const SelectSeverityUI: FC<{
classNames?: string;
severity: TableSeverity;
onChange: (s: TableSeverity) => void;
}> = ({ classNames = '', severity, onChange }) => {
const handleOnChange = (valueDisplay: string) => {
onChange(optionValueToThreshold(optionsMap[valueDisplay]));
};

return (
<EuiSuperSelect
data-test-subj={'mlAnomalySeverityThresholdControls'}
className={classNames}
hasDividers
options={getSeverityOptions()}
valueOfSelected={severity.display}
onChange={onChange}
onChange={handleOnChange}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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 { useMemo } from 'react';
import { useUiSettings } from '../../contexts/kibana';
import { TimeBuckets } from '../../util/time_buckets';
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common';

export const useTimeBuckets = () => {
const uiSettings = useUiSettings();
return useMemo(() => {
return new TimeBuckets({
'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
dateFormat: uiSettings.get('dateFormat'),
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
});
}, [uiSettings]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export function CustomSelectionTable({
isSelected={isItemSelected(item[tableItemId])}
isSelectable={true}
hasActions={true}
data-test-subj="mlCustomSelectionTableRow"
data-test-subj={`mlCustomSelectionTableRow row-${item[tableItemId]}`}
>
{cells}
</EuiTableRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control';
import { MLCATEGORY } from '../../../../common/constants/field_types';
import { ENTITY_FIELD_OPERATIONS } from '../../../../common/util/anomaly_utils';

export type EntityCellFilter = (
entityName: string,
Expand Down Expand Up @@ -40,7 +41,7 @@ function getAddFilter({ entityName, entityValue, filter }: EntityCellProps) {
<EuiButtonIcon
size="s"
className="filter-button"
onClick={() => filter(entityName, entityValue, '+')}
onClick={() => filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.ADD)}
iconType="plusInCircle"
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel', {
defaultMessage: 'Add filter',
Expand All @@ -65,7 +66,7 @@ function getRemoveFilter({ entityName, entityValue, filter }: EntityCellProps) {
<EuiButtonIcon
size="s"
className="filter-button"
onClick={() => filter(entityName, entityValue, '-')}
onClick={() => filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.REMOVE)}
iconType="minusInCircle"
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel', {
defaultMessage: 'Remove filter',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
* 2.0.
*/

export const useMlKibana = jest.fn(() => {
return {
services: {
application: {
navigateToApp: jest.fn(),
export const kibanaContextMock = {
services: {
chrome: { recentlyAccessed: { add: jest.fn() } },
application: { navigateToApp: jest.fn() },
http: {
basePath: {
get: jest.fn(),
},
},
};
share: {
urlGenerators: { getUrlGenerator: jest.fn() },
},
},
};

export const useMlKibana = jest.fn(() => {
return kibanaContextMock;
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';

const timefilterMock = dataPluginMock.createStartContract().query.timefilter.timefilter;
export const timefilterMock = dataPluginMock.createStartContract().query.timefilter.timefilter;

export const useTimefilter = jest.fn(() => {
return timefilterMock;
Expand Down
Loading

0 comments on commit f525801

Please sign in to comment.