[ML] Convert anomalies controls to EUI / React (#19856)

* [ML] Convert anomalies controls to EUI / React

* [ML] Edits to anomaly controls following review
This commit is contained in:
Pete Harverson 2018-06-14 11:25:20 +01:00 committed by GitHub
parent 2ada1b348a
commit 9d1ec94fac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 347 additions and 255 deletions

View file

@ -1,4 +0,0 @@
<div ng-show="visible" class="ml-table-controls-element">
<input id="ml-anomalies-controls-show-charts" type="checkbox" ng-model="showCharts" ng-change="toggleChartsVisibility()" />
<label for="ml-anomalies-controls-show-charts" class="kuiLabel">Show Charts</label>
</div>

View file

@ -7,37 +7,49 @@
/*
* AngularJS directive+service for a checkbox element to toggle charts display.
* React component for a checkbox element to toggle charts display.
*/
import React, { Component } from 'react';
import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
import {
EuiCheckbox
} from '@elastic/eui';
import template from './checkbox_showcharts.html';
import 'plugins/ml/components/controls/controls_select';
import makeId from '@elastic/eui/lib/components/form/form_row/make_id';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
class CheckboxShowCharts extends Component {
constructor(props) {
super(props);
module
.service('mlCheckboxShowChartsService', function (Private) {
const stateFactory = Private(stateFactoryProvider);
this.state = stateFactory('mlCheckboxShowCharts', {
showCharts: true
});
})
.directive('mlCheckboxShowCharts', function (mlCheckboxShowChartsService) {
return {
restrict: 'E',
template,
scope: {
visible: '='
},
link: function (scope) {
scope.showCharts = mlCheckboxShowChartsService.state.get('showCharts');
scope.toggleChartsVisibility = function () {
mlCheckboxShowChartsService.state.set('showCharts', scope.showCharts);
mlCheckboxShowChartsService.state.changed();
};
}
// Restore the checked setting from the state.
this.mlCheckboxShowChartsService = this.props.mlCheckboxShowChartsService;
const showCharts = this.mlCheckboxShowChartsService.state.get('showCharts');
this.state = {
checked: showCharts
};
});
}
onChange = (e) => {
const showCharts = e.target.checked;
this.mlCheckboxShowChartsService.state.set('showCharts', showCharts);
this.mlCheckboxShowChartsService.state.changed();
this.setState({
checked: showCharts,
});
};
render() {
return (
<EuiCheckbox
id={makeId()}
label="Show charts"
checked={this.state.checked}
onChange={this.onChange}
/>
);
}
}
export { CheckboxShowCharts };

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { CheckboxShowCharts } from './checkbox_showcharts';
module.service('mlCheckboxShowChartsService', function (Private) {
const stateFactory = Private(stateFactoryProvider);
this.state = stateFactory('mlCheckboxShowCharts', {
showCharts: true
});
})
.directive('mlCheckboxShowCharts', function ($injector) {
const reactDirective = $injector.get('reactDirective');
const mlCheckboxShowChartsService = $injector.get('mlCheckboxShowChartsService');
return reactDirective(
CheckboxShowCharts,
undefined,
{ restrict: 'E' },
{ mlCheckboxShowChartsService }
);
});

View file

@ -5,5 +5,4 @@
*/
import './checkbox_showcharts';
import './checkbox_showcharts_directive';

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import ngMock from 'ng_mock';
import expect from 'expect.js';
describe('ML - <ml-select-interval>', () => {
let $scope;
let $compile;
beforeEach(ngMock.module('kibana'));
beforeEach(() => {
ngMock.inject(function ($injector) {
$compile = $injector.get('$compile');
const $rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
});
});
afterEach(() => {
$scope.$destroy();
});
it('Initialization doesn\'t throw an error', () => {
expect(function () {
$compile('<ml-select-interval />')($scope);
}).to.not.throwError('Not initialized.');
expect($scope.setInterval).to.be.a('function');
expect($scope.interval).to.eql({ display: 'Auto', val: 'auto' });
expect($scope.intervalOptions).to.eql([
{ display: 'Auto', val: 'auto' },
{ display: '1 hour', val: 'hour' },
{ display: '1 day', val: 'day' },
{ display: 'Show all', val: 'second' }
]);
});
});

View file

@ -5,5 +5,4 @@
*/
import './select_interval.js';
import './select_interval_directive';

View file

@ -1,8 +0,0 @@
<ml-controls-select
identifier="Interval"
label="Interval"
options="intervalOptions"
selected="interval"
show-icons="false"
update-fn="setInterval"
/>

View file

@ -7,65 +7,72 @@
/*
* AngularJS directive for rendering a select element with various interval levels.
* React component for rendering a select element with various aggregation interval levels.
*/
import _ from 'lodash';
import React, { Component } from 'react';
import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
import {
EuiSelect
} from '@elastic/eui';
import template from './select_interval.html';
import 'plugins/ml/components/controls/controls_select';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
const OPTIONS = [
{ value: 'auto', text: 'Auto' },
{ value: 'hour', text: '1 hour' },
{ value: 'day', text: '1 day' },
{ value: 'second', text: 'Show all' }
];
module
.service('mlSelectIntervalService', function (Private) {
const stateFactory = Private(stateFactoryProvider);
this.state = stateFactory('mlSelectInterval', {
interval: { display: 'Auto', val: 'auto' }
});
})
.directive('mlSelectInterval', function (mlSelectIntervalService) {
return {
restrict: 'E',
template,
link: function (scope, element) {
scope.intervalOptions = [
{ display: 'Auto', val: 'auto' },
{ display: '1 hour', val: 'hour' },
{ display: '1 day', val: 'day' },
{ display: 'Show all', val: 'second' }
];
function optionValueToInterval(value) {
// Builds the corresponding interval object with the required display and val properties
// from the specified value.
const option = OPTIONS.find(opt => (opt.value === value));
const intervalState = mlSelectIntervalService.state.get('interval');
const intervalValue = _.get(intervalState, 'val', 'auto');
let intervalOption = scope.intervalOptions.find(d => d.val === intervalValue);
if (intervalOption === undefined) {
// Attempt to set value in URL which doesn't map to one of the options.
intervalOption = scope.intervalOptions.find(d => d.val === 'auto');
}
scope.interval = intervalOption;
mlSelectIntervalService.state.set('interval', scope.interval);
// Default to auto if supplied value doesn't map to one of the options.
let interval = OPTIONS[0];
if (option !== undefined) {
interval = { display: option.text, val: option.value };
}
scope.setInterval = function (interval) {
if (!_.isEqual(scope.interval, interval)) {
scope.interval = interval;
mlSelectIntervalService.state.set('interval', scope.interval).changed();
}
};
return interval;
}
function setScopeInterval() {
scope.setInterval(mlSelectIntervalService.state.get('interval'));
}
class SelectInterval extends Component {
constructor(props) {
super(props);
mlSelectIntervalService.state.watch(setScopeInterval);
// Restore the interval from the state, or default to auto.
this.mlSelectIntervalService = this.props.mlSelectIntervalService;
const intervalState = this.mlSelectIntervalService.state.get('interval');
const intervalValue = _.get(intervalState, 'val', 'auto');
const interval = optionValueToInterval(intervalValue);
this.mlSelectIntervalService.state.set('interval', interval);
element.on('$destroy', () => {
mlSelectIntervalService.state.unwatch(setScopeInterval);
scope.$destroy();
});
}
this.state = {
value: interval.val
};
});
}
onChange = (e) => {
this.setState({
value: e.target.value,
});
const interval = optionValueToInterval(e.target.value);
this.mlSelectIntervalService.state.set('interval', interval).changed();
};
render() {
return (
<EuiSelect
options={OPTIONS}
className="ml-select-interval"
value={this.state.value}
onChange={this.onChange}
/>
);
}
}
export { SelectInterval };

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { SelectInterval } from './select_interval';
module.service('mlSelectIntervalService', function (Private) {
const stateFactory = Private(stateFactoryProvider);
this.state = stateFactory('mlSelectInterval', {
interval: { display: 'Auto', val: 'auto' }
});
})
.directive('mlSelectInterval', function ($injector) {
const reactDirective = $injector.get('reactDirective');
const mlSelectIntervalService = $injector.get('mlSelectIntervalService');
return reactDirective(
SelectInterval,
undefined,
{ restrict: 'E' },
{ mlSelectIntervalService }
);
});

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import ngMock from 'ng_mock';
import expect from 'expect.js';
describe('ML - <ml-select-severity>', () => {
let $scope;
let $compile;
beforeEach(ngMock.module('kibana'));
beforeEach(() => {
ngMock.inject(function ($injector) {
$compile = $injector.get('$compile');
const $rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
});
});
afterEach(() => {
$scope.$destroy();
});
it('Initialization doesn\'t throw an error', () => {
expect(function () {
$compile('<ml-select-severity />')($scope);
}).to.not.throwError('Not initialized.');
expect($scope.setThreshold).to.be.a('function');
expect($scope.threshold).to.eql({ display: 'warning', val: 0 });
expect($scope.thresholdOptions).to.eql([
{ display: 'critical', val: 75 },
{ display: 'major', val: 50 },
{ display: 'minor', val: 25 },
{ display: 'warning', val: 0 }
]);
});
});

View file

@ -5,5 +5,5 @@
*/
import './select_severity.js';
import './select_severity_directive';
import './styles/main.less';

View file

@ -1,8 +0,0 @@
<ml-controls-select
identifier="Severity"
label="Severity threshold"
options="thresholdOptions"
selected="threshold"
show-icons="true"
update-fn="setThreshold"
/>

View file

@ -7,65 +7,101 @@
/*
* AngularJS directive for rendering a select element with threshold levels.
*/
* React component for rendering a select element with threshold levels.
*/
import _ from 'lodash';
import React, { Component } from 'react';
import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
import {
EuiComboBox,
EuiHighlight,
EuiHealth,
} from '@elastic/eui';
import template from './select_severity.html';
import 'plugins/ml/components/controls/controls_select';
import { getSeverityColor } from 'plugins/ml/../common/util/anomaly_utils';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
const OPTIONS = [
{ value: 0, label: 'warning', color: getSeverityColor(0) },
{ value: 25, label: 'minor', color: getSeverityColor(25) },
{ value: 50, label: 'major', color: getSeverityColor(50) },
{ value: 75, label: 'critical', color: getSeverityColor(75) }
];
module
.service('mlSelectSeverityService', function (Private) {
const stateFactory = Private(stateFactoryProvider);
this.state = stateFactory('mlSelectSeverity', {
threshold: { display: 'warning', val: 0 }
});
})
.directive('mlSelectSeverity', function (mlSelectSeverityService) {
return {
restrict: 'E',
template,
link: function (scope, element) {
scope.thresholdOptions = [
{ display: 'critical', val: 75 },
{ display: 'major', val: 50 },
{ display: 'minor', val: 25 },
{ display: 'warning', val: 0 }
];
function optionValueToThreshold(value) {
// Builds the corresponding threshold object with the required display and val properties
// from the specified value.
const option = OPTIONS.find(opt => (opt.value === value));
const thresholdState = mlSelectSeverityService.state.get('threshold');
const thresholdValue = _.get(thresholdState, 'val', 0);
let thresholdOption = scope.thresholdOptions.find(d => d.val === thresholdValue);
if (thresholdOption === undefined) {
// Attempt to set value in URL which doesn't map to one of the options.
thresholdOption = scope.thresholdOptions.find(d => d.val === 0);
}
scope.threshold = thresholdOption;
mlSelectSeverityService.state.set('threshold', scope.threshold);
// Default to warning if supplied value doesn't map to one of the options.
let threshold = OPTIONS[0];
if (option !== undefined) {
threshold = { display: option.label, val: option.value };
}
scope.setThreshold = function (threshold) {
if(!_.isEqual(scope.threshold, threshold)) {
scope.threshold = threshold;
mlSelectSeverityService.state.set('threshold', scope.threshold).changed();
}
};
return threshold;
}
function setThreshold() {
scope.setThreshold(mlSelectSeverityService.state.get('threshold'));
}
class SelectSeverity extends Component {
constructor(props) {
super(props);
mlSelectSeverityService.state.watch(setThreshold);
// Restore the threshold from the state, or default to warning.
this.mlSelectSeverityService = this.props.mlSelectSeverityService;
const thresholdState = this.mlSelectSeverityService.state.get('threshold');
const thresholdValue = _.get(thresholdState, 'val', 0);
const threshold = optionValueToThreshold(thresholdValue);
const selectedOption = OPTIONS.find(opt => (opt.value === threshold.val));
element.on('$destroy', () => {
mlSelectSeverityService.state.unwatch(setThreshold);
scope.$destroy();
});
}
this.mlSelectSeverityService.state.set('threshold', threshold);
this.state = {
selectedOptions: [selectedOption]
};
});
}
onChange = (selectedOptions) => {
if (selectedOptions.length === 0) {
// Don't allow no options to be selected.
return;
}
this.setState({
selectedOptions,
});
const threshold = optionValueToThreshold(selectedOptions[0].value);
this.mlSelectSeverityService.state.set('threshold', threshold).changed();
};
renderOption = (option, searchValue, contentClassName) => {
const { color, label, value } = option;
return (
<EuiHealth color={color}>
<span className={contentClassName}>
<EuiHighlight search={searchValue}>
{label}
</EuiHighlight>
&nbsp;
<span>({value})</span>
</span>
</EuiHealth>
);
};
render() {
const { selectedOptions } = this.state;
return (
<EuiComboBox
placeholder="Select severity"
className="ml-select-severity"
singleSelection={true}
options={OPTIONS}
selectedOptions={selectedOptions}
onChange={this.onChange}
renderOption={this.renderOption}
/>
);
}
}
export { SelectSeverity };

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { SelectSeverity } from './select_severity';
module.service('mlSelectSeverityService', function (Private) {
const stateFactory = Private(stateFactoryProvider);
this.state = stateFactory('mlSelectSeverity', {
threshold: { display: 'warning', val: 0 }
});
})
.directive('mlSelectSeverity', function ($injector) {
const reactDirective = $injector.get('reactDirective');
const mlSelectSeverityService = $injector.get('mlSelectSeverityService');
return reactDirective(
SelectSeverity,
undefined,
{ restrict: 'E' },
{ mlSelectSeverityService }
);
});

View file

@ -0,0 +1,5 @@
.ml-select-severity {
.euiFormControlLayoutClearButton {
display: none;
}
}

View file

@ -19,7 +19,7 @@ describe('ML - Explorer Controller', () => {
const scope = $rootScope.$new();
$controller('MlExplorerController', { $scope: scope });
expect(scope.showCharts).to.be.true;
expect(scope.loading).to.be(true);
});
});
});

View file

@ -119,12 +119,30 @@
Anomalies
</span>
<div class="ml-table-controls">
<ml-select-severity />
<ml-select-interval />
<ml-checkbox-show-charts
visible="anomalyChartRecords.length > 0"
/>
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive ml-anomalies-controls">
<div class="euiFlexItem euiFlexItem--flexGrowZero" style="width:170px">
<div class="euiFormRow" id="select_severity_control">
<label class="euiFormLabel" for="select_severity">Severity threshold</label>
<div class="euiFormControlLayout">
<ml-select-severity id="select_severity" />
</div>
</div>
</div>
<div class="euiFlexItem euiFlexItem--flexGrowZero" style="width:170px">
<div class="euiFormRow" id="select_interval_control">
<label class="euiFormLabel" for="select_interval">Interval</label>
<div class="euiFormControlLayout">
<ml-select-interval id="select_interval" />
</div>
</div>
</div>
<div class="euiFlexItem euiFlexItem--flexGrowZero" ng-show="anomalyChartRecords.length > 0">
<div class="euiFormRow" id="show_charts_checkbox_control">
<div class="euiFormControlLayout">
<ml-checkbox-show-charts id="show_charts_checkbox" />
</div>
</div>
</div>
</div>
<div ng-controller="MlExplorerChartsContainerController" class="euiText">
@ -141,7 +159,6 @@
</ml-explorer-charts-container>
</div>
<ml-anomalies-table
table-data="tableData"
/>

View file

@ -7,7 +7,7 @@
<span ng-if="series.entityFields.length > 0">{{series.detectorLabel}} - </span>
<span ng-if="series.entityFields.length === 0">{{series.detectorLabel}}</span>
<span ng-repeat='entity in series.entityFields'>
{{entity.fieldName}} {{entity.fieldValue}} {{entity.fieldValueTest}}
{{entity.fieldName}} {{entity.fieldValue}}
</span>
</div>
<i aria-hidden="true" class="fa fa-info-circle" tooltip-placement="left" tooltip-html-unsafe="{{series.infoTooltip}}" tooltip-append-to-body="false"></i>

View file

@ -113,6 +113,14 @@
}
}
.ml-anomalies-controls {
padding-top: 5px;
#show_charts_checkbox_control {
padding-top: 28px;
}
}
.ml-explorer-swimlane {
-webkit-user-select: none;
-moz-user-select: none;

View file

@ -81,6 +81,10 @@
float: right;
}
.ml-anomalies-controls {
padding-top: 5px;
}
.ml-timeseries-chart {
svg {
font-size: 12px;

View file

@ -121,9 +121,23 @@
Anomalies
</span>
<div class="ml-table-controls">
<ml-select-severity />
<ml-select-interval />
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive ml-anomalies-controls">
<div class="euiFlexItem euiFlexItem--flexGrowZero" style="width:170px">
<div class="euiFormRow" id="select_severity_control">
<label class="euiFormLabel" for="select_severity">Severity threshold</label>
<div class="euiFormControlLayout">
<ml-select-severity id="select_severity" />
</div>
</div>
</div>
<div class="euiFlexItem euiFlexItem--flexGrowZero" style="width:170px">
<div class="euiFormRow" id="select_interval_control">
<label class="euiFormLabel" for="select_interval">Interval</label>
<div class="euiFormControlLayout">
<ml-select-interval id="select_interval" />
</div>
</div>
</div>
</div>
<ml-anomalies-table