[ML] Anomaly Detection: Adds View in Maps item to Actions menu in the anomalies table (#131284)

* add link to actions menu in anomalies table

* set map start time to bucket start

* simplify isGeoRecord value setting

* lint fix

* adds query and timerange to link

* substract ms so as not to go into next bucket start time

* lint fix
This commit is contained in:
Melissa Alvarez 2022-05-04 13:43:54 -06:00 committed by GitHub
parent 683463ea43
commit 490bee238b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 91 deletions

View file

@ -278,6 +278,11 @@ export interface AnomaliesTableRecord {
* which can be plotted by the ML UI in an anomaly chart.
*/
isTimeSeriesViewRecord?: boolean;
/**
* Returns true if the anomaly record represented by the table row can be shown in the maps plugin
*/
isGeoRecord?: boolean;
}
export type PartitionFieldsType = typeof PARTITION_FIELDS[number];

View file

@ -18,6 +18,7 @@ import {
formatHumanReadableDateTime,
formatHumanReadableDateTimeSeconds,
} from '../../../../common/util/date_utils';
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
import { DescriptionCell } from './description_cell';
import { DetectorCell } from './detector_cell';
@ -47,7 +48,8 @@ function showLinksMenuForItem(item, showViewSeriesLink) {
canConfigureRules ||
(showViewSeriesLink && item.isTimeSeriesViewRecord) ||
item.entityName === 'mlcategory' ||
item.customUrls !== undefined
item.customUrls !== undefined ||
item.detector.includes(ML_JOB_AGGREGATION.LAT_LONG)
);
}

View file

@ -8,7 +8,9 @@
import { cloneDeep } from 'lodash';
import moment from 'moment';
import rison, { RisonValue } from 'rison-node';
import { escapeKuery } from '@kbn/es-query';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common';
import {
EuiButtonIcon,
EuiContextMenuItem,
@ -20,8 +22,10 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { MAPS_APP_LOCATOR } from '@kbn/maps-plugin/public';
import { mlJobService } from '../../services/job_service';
import { getDataViewIdFromName } from '../../util/index_utils';
import { getInitialAnomaliesLayers } from '../../../maps/util';
import {
formatHumanReadableDateTimeSeconds,
timeFormatter,
@ -33,7 +37,7 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator';
import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search';
// @ts-ignore
import { escapeDoubleQuotes } from '../../explorer/explorer_utils';
import { escapeDoubleQuotes, getDateFormatTz } from '../../explorer/explorer_utils';
import { isCategorizationAnomaly, isRuleSupported } from '../../../../common/util/anomaly_utils';
import { checkPermission } from '../../capabilities/check_capabilities';
import type {
@ -49,6 +53,7 @@ import type { AnomaliesTableRecord } from '../../../../common/types/anomalies';
interface LinksMenuProps {
anomaly: AnomaliesTableRecord;
bounds: TimeRangeBounds;
showMapsLink: boolean;
showViewSeriesLink: boolean;
isAggregatedData: boolean;
interval: 'day' | 'hour' | 'second';
@ -66,9 +71,39 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const kibana = useMlKibana();
const {
services: { share, application },
services: { data, share, application },
} = kibana;
const getMapsLink = async (anomaly: AnomaliesTableRecord) => {
const initialLayers = getInitialAnomaliesLayers(anomaly.jobId);
const anomalyBucketStartMoment = moment(anomaly.time).tz(getDateFormatTz());
const anomalyBucketStart = anomalyBucketStartMoment.toISOString();
const anomalyBucketEnd = anomalyBucketStartMoment
.add(anomaly.source.bucket_span, 'seconds')
.subtract(1, 'ms')
.toISOString();
const timeRange = data.query.timefilter.timefilter.getTime();
// Set 'from' in timeRange to start bucket time for the specific anomaly
timeRange.from = anomalyBucketStart;
timeRange.to = anomalyBucketEnd;
const locator = share.url.locators.get(MAPS_APP_LOCATOR);
const location = await locator?.getLocation({
initialLayers,
timeRange,
...(anomaly.entityName && anomaly.entityValue
? {
query: {
language: SEARCH_QUERY_LANGUAGE.KUERY,
query: `${escapeKuery(anomaly.entityName)}:${escapeKuery(anomaly.entityValue)}`,
},
}
: {}),
});
return location;
};
useEffect(() => {
let unmounted = false;
const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR');
@ -561,23 +596,44 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
);
}
if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) {
items.push(
<EuiContextMenuItem
key="view_series"
icon="visLine"
onClick={() => {
closePopover();
viewSeries();
}}
data-test-subj="mlAnomaliesListRowActionViewSeriesButton"
>
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewSeriesLabel"
defaultMessage="View series"
/>
</EuiContextMenuItem>
);
if (showViewSeriesLink === true) {
if (anomaly.isTimeSeriesViewRecord) {
items.push(
<EuiContextMenuItem
key="view_series"
icon="visLine"
onClick={() => {
closePopover();
viewSeries();
}}
data-test-subj="mlAnomaliesListRowActionViewSeriesButton"
>
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewSeriesLabel"
defaultMessage="View series"
/>
</EuiContextMenuItem>
);
}
if (anomaly.isGeoRecord === true) {
items.push(
<EuiContextMenuItem
key="view_in_maps"
icon="gisApp"
onClick={async () => {
const mapsLink = await getMapsLink(anomaly);
await application.navigateToApp(MAPS_APP_ID, { path: mapsLink?.path });
}}
data-test-subj="mlAnomaliesListRowActionViewInMapsButton"
>
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewInMapsLabel"
defaultMessage="View in Maps"
/>
</EuiContextMenuItem>
);
}
}
if (application.capabilities.discover?.show && isCategorizationAnomalyRecord) {

View file

@ -16,7 +16,6 @@ import {
EuiFlexItem,
EuiIconTip,
EuiToolTip,
htmlIdGenerator,
} from '@elastic/eui';
import {
@ -36,15 +35,13 @@ import { MlTooltipComponent } from '../../components/chart_tooltip';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { useMlKibana } from '../../contexts/kibana';
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
import { AnomalySource } from '../../../maps/anomaly_source';
import { CUSTOM_COLOR_RAMP } from '../../../maps/anomaly_layer_wizard_factory';
import { LAYER_TYPE, APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common';
import { getInitialAnomaliesLayers } from '../../../maps/util';
import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common';
import { MAPS_APP_LOCATOR } from '@kbn/maps-plugin/public';
import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts';
import { addItemToRecentlyAccessed } from '../../util/recently_accessed';
import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map';
import { useActiveCursor } from '@kbn/charts-plugin/public';
import { ML_ANOMALY_LAYERS } from '../../../maps/util';
import { Chart, Settings } from '@elastic/charts';
import useObservable from 'react-use/lib/useObservable';
@ -114,59 +111,7 @@ function ExplorerChartContainer({
const getMapsLink = useCallback(async () => {
const { queryString, query } = getEntitiesQuery(series);
const initialLayers = [];
const typicalStyle = {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#98A2B2',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
};
const style = {
type: 'VECTOR',
properties: {
fillColor: CUSTOM_COLOR_RAMP,
lineColor: CUSTOM_COLOR_RAMP,
},
isTimeAware: false,
};
for (const layer in ML_ANOMALY_LAYERS) {
if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) {
initialLayers.push({
id: htmlIdGenerator()(),
type: LAYER_TYPE.GEOJSON_VECTOR,
sourceDescriptor: AnomalySource.createDescriptor({
jobId: series.jobId,
typicalActual: ML_ANOMALY_LAYERS[layer],
}),
style: ML_ANOMALY_LAYERS[layer] === ML_ANOMALY_LAYERS.TYPICAL ? typicalStyle : style,
});
}
}
const initialLayers = getInitialAnomaliesLayers(series.jobId);
const locator = share.url.locators.get(MAPS_APP_LOCATOR);
const location = await locator.getLocation({

View file

@ -19,6 +19,7 @@ import {
} from '../../../common/constants/search';
import { EntityField, getEntityFieldList } from '../../../common/util/anomaly_utils';
import { extractErrorMessage } from '../../../common/util/errors';
import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types';
import {
isSourceDataChartableForDetector,
isModelPlotChartableForDetector,
@ -495,6 +496,8 @@ export async function loadAnomaliesTableData(
}
anomaly.isTimeSeriesViewRecord = isChartable;
anomaly.isGeoRecord =
detector !== undefined && detector.function === ML_JOB_AGGREGATION.LAT_LONG;
if (mlJobService.customUrlsByJob[jobId] !== undefined) {
anomaly.customUrls = mlJobService.customUrlsByJob[jobId];

View file

@ -11,13 +11,13 @@ import type { StartServicesAccessor } from '@kbn/core/public';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { SerializableRecord } from '@kbn/utility-types';
import type { LayerWizard, RenderWizardArguments } from '@kbn/maps-plugin/public';
import { FIELD_ORIGIN, LAYER_TYPE, STYLE_TYPE } from '@kbn/maps-plugin/common';
import { LAYER_TYPE } from '@kbn/maps-plugin/common';
import {
VectorLayerDescriptor,
VectorStylePropertiesDescriptor,
} from '@kbn/maps-plugin/common/descriptor_types';
import { SEVERITY_COLOR_RAMP } from '../../common';
import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator';
import { CUSTOM_COLOR_RAMP } from './util';
import { CreateAnomalySourceEditor } from './create_anomaly_source_editor';
import { AnomalySource, AnomalySourceDescriptor } from './anomaly_source';
@ -26,17 +26,6 @@ import type { MlPluginStart, MlStartDependencies } from '../plugin';
import type { MlApiServices } from '../application/services/ml_api_service';
export const ML_ANOMALY = 'ML_ANOMALIES';
export const CUSTOM_COLOR_RAMP = {
type: STYLE_TYPE.DYNAMIC,
options: {
customColorRamp: SEVERITY_COLOR_RAMP,
field: {
name: 'record_score',
origin: FIELD_ORIGIN.SOURCE,
},
useCustomColorRamp: true,
},
};
export class AnomalyLayerWizardFactory {
public readonly type = ML_ANOMALY;

View file

@ -7,14 +7,19 @@
import { FeatureCollection, Feature, Geometry } from 'geojson';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { htmlIdGenerator } from '@elastic/eui';
import { FIELD_ORIGIN, STYLE_TYPE } from '@kbn/maps-plugin/common';
import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query';
import { ESSearchResponse } from '@kbn/core/types/elasticsearch';
import { VectorSourceRequestMeta } from '@kbn/maps-plugin/common';
import { LAYER_TYPE } from '@kbn/maps-plugin/common';
import { SEVERITY_COLOR_RAMP } from '../../common';
import { formatHumanReadableDateTimeSeconds } from '../../common/util/date_utils';
import type { MlApiServices } from '../application/services/ml_api_service';
import { MLAnomalyDoc } from '../../common/types/anomalies';
import { SEARCH_QUERY_LANGUAGE } from '../../common/constants/search';
import { getIndexPattern } from '../application/explorer/reducers/explorer_reducer/get_index_pattern';
import { AnomalySource } from './anomaly_source';
export const ML_ANOMALY_LAYERS = {
TYPICAL: 'typical',
@ -22,6 +27,57 @@ export const ML_ANOMALY_LAYERS = {
TYPICAL_TO_ACTUAL: 'typical to actual',
} as const;
export const CUSTOM_COLOR_RAMP = {
type: STYLE_TYPE.DYNAMIC,
options: {
customColorRamp: SEVERITY_COLOR_RAMP,
field: {
name: 'record_score',
origin: FIELD_ORIGIN.SOURCE,
},
useCustomColorRamp: true,
},
};
export const ACTUAL_STYLE = {
type: 'VECTOR',
properties: {
fillColor: CUSTOM_COLOR_RAMP,
lineColor: CUSTOM_COLOR_RAMP,
},
isTimeAware: false,
};
export const TYPICAL_STYLE = {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: '#98A2B2',
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
};
export type MlAnomalyLayersType = typeof ML_ANOMALY_LAYERS[keyof typeof ML_ANOMALY_LAYERS];
// Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs
@ -32,6 +88,27 @@ function getCoordinates(latLonString: string): number[] {
.reverse();
}
export function getInitialAnomaliesLayers(jobId: string) {
const initialLayers = [];
for (const layer in ML_ANOMALY_LAYERS) {
if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) {
initialLayers.push({
id: htmlIdGenerator()(),
type: LAYER_TYPE.GEOJSON_VECTOR,
sourceDescriptor: AnomalySource.createDescriptor({
jobId,
typicalActual: ML_ANOMALY_LAYERS[layer as keyof typeof ML_ANOMALY_LAYERS],
}),
style:
ML_ANOMALY_LAYERS[layer as keyof typeof ML_ANOMALY_LAYERS] === ML_ANOMALY_LAYERS.TYPICAL
? TYPICAL_STYLE
: ACTUAL_STYLE,
});
}
}
return initialLayers;
}
export async function getResultsForJobId(
mlResultsService: MlApiServices['results'],
jobId: string,