Skip to content

Commit

Permalink
wip: create anomalies layer in maps
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarezmelissa87 committed Jan 18, 2022
1 parent 4d771c3 commit af0cfdf
Show file tree
Hide file tree
Showing 17 changed files with 1,171 additions and 3 deletions.
13 changes: 13 additions & 0 deletions x-pack/plugins/ml/common/util/job_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ export function isMappableJob(job: CombinedJob, detectorIndex: number): boolean
return isMappable;
}

// Returns a boolean indicating whether the specified job is suitable for maps plugin.
export function isJobWithGeoData(job: Job): boolean {
let isMappable = false;
const { detectors } = job.analysis_config;

detectors.forEach((detector) => {
if (detector.function === ML_JOB_AGGREGATION.LAT_LONG) {
isMappable = true;
}
});
return isMappable;
}

/**
* Validates that composite definition only have sources that are only terms and date_histogram
* if composite is defined.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export const jobsApiProvider = (httpService: HttpService) => ({
});
},

jobsWithGeo() {
return httpService.http<string[]>({
path: `${ML_BASE_PATH}/jobs/jobs_with_geo`,
method: 'GET',
});
},

jobsWithTimerange(dateFormatTz: string) {
const body = JSON.stringify({ dateFormatTz });
return httpService.http<{
Expand Down
86 changes: 86 additions & 0 deletions x-pack/plugins/ml/public/maps/anomaly_job_selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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 React, { Component } from 'react';

import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isEqual } from 'lodash';

interface Props {
onJobChange: (jobId: string) => void;
mlJobsService: any; // todo: update types
}

interface State {
jobId?: string;
jobIdList?: Array<EuiComboBoxOptionOption<string>>;
}

export class AnomalyJobSelector extends Component<Props, State> {
private _isMounted: boolean = false;

state: State = {};

private async _loadJobs() {
const jobIdList = await this.props.mlJobsService.jobsWithGeo();
// TODO update types - remove any
const options = jobIdList.map((jobId: any) => {
return { label: jobId, value: jobId };
});
if (this._isMounted && !isEqual(options, this.state.jobIdList)) {
this.setState({
jobIdList: options,
});
}
}

componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any): void {
this._loadJobs();
}

componentDidMount(): void {
this._isMounted = true;
this._loadJobs();
}

componentWillUnmount() {
this._isMounted = false;
}

onJobIdSelect = (selectedOptions: Array<EuiComboBoxOptionOption<string>>) => {
const jobId: string = selectedOptions[0].value!;
if (this._isMounted) {
this.setState({ jobId });
this.props.onJobChange(jobId);
}
};

render() {
if (!this.state.jobIdList) {
return null;
}

return (
<EuiFormRow
label={i18n.translate('xpack.ml.maps.jobIdLabel', {
defaultMessage: 'JobId',
})}
display="columnCompressed"
>
<EuiComboBox
singleSelection={true}
onChange={this.onJobIdSelect}
options={this.state.jobIdList}
selectedOptions={
this.state.jobId ? [{ value: this.state.jobId, label: this.state.jobId }] : []
}
/>
</EuiFormRow>
);
}
}
29 changes: 29 additions & 0 deletions x-pack/plugins/ml/public/maps/anomaly_layer_wizard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { LAYER_WIZARD_CATEGORY } from '../../../maps/common';
import type { LayerWizard } from '../../../maps/public';

export const anomalyLayerWizard: Partial<LayerWizard> = {
categories: [LAYER_WIZARD_CATEGORY.SOLUTIONS],
description: i18n.translate('xpack.ml.maps.anomalyLayerDescription', {
defaultMessage: 'Create anomalies layers',
}),
disabledReason: i18n.translate('xpack.ml.maps.anomalyLayerUnavailableMessage', {
defaultMessage:
'Whatever reason the user cannot see ML card (likely because no enterprise license or no ML privileges)',
}),
icon: 'outlierDetectionJob',
getIsDisabled: () => {
// return false by default
return false;
},
title: i18n.translate('xpack.ml.maps.anomalyLayerTitle', {
defaultMessage: 'ML Anomalies',
}),
};
102 changes: 102 additions & 0 deletions x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 React from 'react';
import uuid from 'uuid';
import type { StartServicesAccessor } from 'kibana/public';
import type { LayerWizard, RenderWizardArguments } from '../../../maps/public';
import { COLOR_MAP_TYPE, FIELD_ORIGIN, LAYER_TYPE, STYLE_TYPE } from '../../../maps/common';
import { CreateAnomalySourceEditor } from './create_anomaly_source_editor';
import {
VectorLayerDescriptor,
VectorStylePropertiesDescriptor,
} from '../../../maps/common/descriptor_types';
import { AnomalySource, AnomalySourceDescriptor } from './anomaly_source';

import { HttpService } from '../application/services/http_service';
import type { MlPluginStart, MlStartDependencies } from '../plugin';
import type { MlDependencies } from '../application/app';

export const ML_ANOMALY = 'ML_ANOMALIES';

export class AnomalyLayerWizardFactory {
public readonly type = ML_ANOMALY;

constructor(
private getStartServices: StartServicesAccessor<MlStartDependencies, MlPluginStart>,
private canGetJobs: any
) {
this.canGetJobs = canGetJobs;
}

private async getServices(): Promise<any> {
const [coreStart, pluginsStart] = await this.getStartServices();
const { jobsApiProvider } = await import('../application/services/ml_api_service/jobs');

const httpService = new HttpService(coreStart.http);
const mlJobsService = jobsApiProvider(httpService);

return [coreStart, pluginsStart as MlDependencies, { mlJobsService }];
}

public async create(): Promise<LayerWizard> {
const services = await this.getServices();
const mlJobsService = services[2].mlJobsService;
const { anomalyLayerWizard } = await import('./anomaly_layer_wizard');

anomalyLayerWizard.getIsDisabled = () => !this.canGetJobs;

anomalyLayerWizard.renderWizard = ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: Partial<AnomalySourceDescriptor> | null) => {
if (!sourceConfig) {
previewLayers([]);
return;
}

const anomalyLayerDescriptor: VectorLayerDescriptor = {
id: uuid(),
type: LAYER_TYPE.GEOJSON_VECTOR,
sourceDescriptor: AnomalySource.createDescriptor({
jobId: sourceConfig.jobId,
typicalActual: sourceConfig.typicalActual,
}),
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: STYLE_TYPE.DYNAMIC,
options: {
color: 'Blue to Red',
colorCategory: 'palette_0',
fieldMetaOptions: { isEnabled: true, sigma: 3 },
type: COLOR_MAP_TYPE.ORDINAL,
field: {
name: 'record_score',
origin: FIELD_ORIGIN.SOURCE,
},
useCustomColorRamp: false,
},
},
} as unknown as VectorStylePropertiesDescriptor,
isTimeAware: false,
},
};

previewLayers([anomalyLayerDescriptor]);
};

return (
<CreateAnomalySourceEditor
onSourceConfigChange={onSourceConfigChange}
mlJobsService={mlJobsService}
/>
);
};

return anomalyLayerWizard as LayerWizard;
}
}
Loading

0 comments on commit af0cfdf

Please sign in to comment.