diff --git a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.html b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.html deleted file mode 100644 index 5b759059fa888..0000000000000 --- a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
diff --git a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js index 621c48464c362..4ac974d371a58 100644 --- a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js +++ b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js @@ -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 ( + + ); + } +} + +export { CheckboxShowCharts }; diff --git a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts_directive.js b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts_directive.js new file mode 100644 index 0000000000000..fe30a0980925a --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts_directive.js @@ -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 } + ); + }); diff --git a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/index.js b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/index.js index 331a69a70eea2..0b0705d617adb 100644 --- a/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/index.js +++ b/x-pack/plugins/ml/public/components/controls/checkbox_showcharts/index.js @@ -5,5 +5,4 @@ */ - -import './checkbox_showcharts'; +import './checkbox_showcharts_directive'; diff --git a/x-pack/plugins/ml/public/components/controls/select_interval/__tests__/select_interval.js b/x-pack/plugins/ml/public/components/controls/select_interval/__tests__/select_interval.js deleted file mode 100644 index aa1e21bf9219a..0000000000000 --- a/x-pack/plugins/ml/public/components/controls/select_interval/__tests__/select_interval.js +++ /dev/null @@ -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 - ', () => { - 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('')($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' } - ]); - }); - -}); diff --git a/x-pack/plugins/ml/public/components/controls/select_interval/index.js b/x-pack/plugins/ml/public/components/controls/select_interval/index.js index df14bdeed0703..8fe80d63bb99c 100644 --- a/x-pack/plugins/ml/public/components/controls/select_interval/index.js +++ b/x-pack/plugins/ml/public/components/controls/select_interval/index.js @@ -5,5 +5,4 @@ */ - -import './select_interval.js'; +import './select_interval_directive'; diff --git a/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.html b/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.html deleted file mode 100644 index 6d59a3403f55c..0000000000000 --- a/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.js b/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.js index cf2752974ebc6..b4cfdd7db7cb2 100644 --- a/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.js +++ b/x-pack/plugins/ml/public/components/controls/select_interval/select_interval.js @@ -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' } - ]; - - 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); - - scope.setInterval = function (interval) { - if (!_.isEqual(scope.interval, interval)) { - scope.interval = interval; - mlSelectIntervalService.state.set('interval', scope.interval).changed(); - } - }; - - function setScopeInterval() { - scope.setInterval(mlSelectIntervalService.state.get('interval')); - } - - mlSelectIntervalService.state.watch(setScopeInterval); - - element.on('$destroy', () => { - mlSelectIntervalService.state.unwatch(setScopeInterval); - scope.$destroy(); - }); - } +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)); + + // 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 }; + } + + return interval; +} + +class SelectInterval extends Component { + constructor(props) { + super(props); + + // 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); + + 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 ( + + ); + } +} + +export { SelectInterval }; diff --git a/x-pack/plugins/ml/public/components/controls/select_interval/select_interval_directive.js b/x-pack/plugins/ml/public/components/controls/select_interval/select_interval_directive.js new file mode 100644 index 0000000000000..6cc3bd560d7e2 --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/select_interval/select_interval_directive.js @@ -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 } + ); + }); diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/__tests__/select_severity.js b/x-pack/plugins/ml/public/components/controls/select_severity/__tests__/select_severity.js deleted file mode 100644 index 282443ce6406c..0000000000000 --- a/x-pack/plugins/ml/public/components/controls/select_severity/__tests__/select_severity.js +++ /dev/null @@ -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 - ', () => { - 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('')($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 } - ]); - }); - -}); diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/index.js b/x-pack/plugins/ml/public/components/controls/select_severity/index.js index 7fe0165f39939..070cc16d19313 100644 --- a/x-pack/plugins/ml/public/components/controls/select_severity/index.js +++ b/x-pack/plugins/ml/public/components/controls/select_severity/index.js @@ -5,5 +5,5 @@ */ - -import './select_severity.js'; +import './select_severity_directive'; +import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.html b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.html deleted file mode 100644 index a9227734afda2..0000000000000 --- a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js index af249cfc0ae5e..59ce7af3272bf 100644 --- a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js +++ b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity.js @@ -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 } - ]; - - 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); - - scope.setThreshold = function (threshold) { - if(!_.isEqual(scope.threshold, threshold)) { - scope.threshold = threshold; - mlSelectSeverityService.state.set('threshold', scope.threshold).changed(); - } - }; - - function setThreshold() { - scope.setThreshold(mlSelectSeverityService.state.get('threshold')); - } - - mlSelectSeverityService.state.watch(setThreshold); - - element.on('$destroy', () => { - mlSelectSeverityService.state.unwatch(setThreshold); - scope.$destroy(); - }); - } +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)); + + // 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 }; + } + + return threshold; +} + +class SelectSeverity extends Component { + constructor(props) { + super(props); + + // 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)); + + 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 ( + + + + {label} + +   + ({value}) + + + ); + }; + + render() { + const { selectedOptions } = this.state; + return ( + + ); + } +} + +export { SelectSeverity }; diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/select_severity_directive.js b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity_directive.js new file mode 100644 index 0000000000000..d2d363bb8098e --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/select_severity/select_severity_directive.js @@ -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 } + ); + }); diff --git a/x-pack/plugins/ml/public/components/controls/select_severity/styles/main.less b/x-pack/plugins/ml/public/components/controls/select_severity/styles/main.less new file mode 100644 index 0000000000000..564fed3084e4d --- /dev/null +++ b/x-pack/plugins/ml/public/components/controls/select_severity/styles/main.less @@ -0,0 +1,5 @@ +.ml-select-severity { + .euiFormControlLayoutClearButton { + display: none; + } +} diff --git a/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js b/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js index 195a1beb36247..40109b4c329ff 100644 --- a/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/__tests__/explorer_controller.js @@ -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); }); }); }); diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index 9c6d3cb41ac6d..1abfcbdff8b6c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -119,12 +119,30 @@

No {{swimlaneViewByFieldName}} influencers Anomalies -
- - - +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+
@@ -141,7 +159,6 @@

No {{swimlaneViewByFieldName}} influencers

- diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html index d9497e61ae21b..34e9ac9f9b014 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html @@ -7,7 +7,7 @@ {{series.detectorLabel}} - {{series.detectorLabel}} - {{entity.fieldName}} {{entity.fieldValue}} {{entity.fieldValueTest}} + {{entity.fieldName}} {{entity.fieldValue}}
diff --git a/x-pack/plugins/ml/public/explorer/styles/main.less b/x-pack/plugins/ml/public/explorer/styles/main.less index 867a3df4de2cf..79b848135ef8d 100644 --- a/x-pack/plugins/ml/public/explorer/styles/main.less +++ b/x-pack/plugins/ml/public/explorer/styles/main.less @@ -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; diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less b/x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less index ffebc258e4522..4c05ff134222a 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less +++ b/x-pack/plugins/ml/public/timeseriesexplorer/styles/main.less @@ -81,6 +81,10 @@ float: right; } + .ml-anomalies-controls { + padding-top: 5px; + } + .ml-timeseries-chart { svg { font-size: 12px; diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html index a3549c52a0dfb..6db9a87d735ca 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html @@ -121,9 +121,23 @@ Anomalies -
- - +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+