mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
683463ea43
commit
490bee238b
7 changed files with 168 additions and 91 deletions
|
@ -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];
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue