diff --git a/x-pack/plugins/ml/public/components/influencers_list/index.js b/x-pack/plugins/ml/public/components/influencers_list/index.js index 43aca379549976..ab50fbb57f37a7 100644 --- a/x-pack/plugins/ml/public/components/influencers_list/index.js +++ b/x-pack/plugins/ml/public/components/influencers_list/index.js @@ -5,7 +5,5 @@ */ - - import './influencers_list_directive'; import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/components/influencers_list/influencers_list.html b/x-pack/plugins/ml/public/components/influencers_list/influencers_list.html deleted file mode 100644 index 7f64cc3b3dad54..00000000000000 --- a/x-pack/plugins/ml/public/components/influencers_list/influencers_list.html +++ /dev/null @@ -1,51 +0,0 @@ -
- -
-
-
-

No influencers found

-
-
-
- -
- - - -
- -
-
{{influencer.influencerFieldValue}}
-
mlcategory {{influencer.influencerFieldValue}}
- - -
- -
-
-
-
-
- {{influencer.maxScoreLabel}} - -
- -
- {{influencer.totalScore | abbreviateWholeNumber:4}} -
-
- < 1 -
- -
- -
- -
- diff --git a/x-pack/plugins/ml/public/components/influencers_list/influencers_list.js b/x-pack/plugins/ml/public/components/influencers_list/influencers_list.js new file mode 100644 index 00000000000000..f0f7061d3d6069 --- /dev/null +++ b/x-pack/plugins/ml/public/components/influencers_list/influencers_list.js @@ -0,0 +1,150 @@ +/* + * 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. + */ + + +/* + * React component for rendering a list of Machine Learning influencers. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip +} from '@elastic/eui'; + +import { abbreviateWholeNumber } from 'plugins/ml/formatters/abbreviate_whole_number'; +import { getSeverity } from 'plugins/ml/util/anomaly_utils'; + + +function getTooltipContent(maxScoreLabel, totalScoreLabel) { + return ( + +

Maximum anomaly score: {maxScoreLabel}

+

Total anomaly score: {totalScoreLabel}

+
+ ); +} + +function Influencer({ influencerFieldName, valueData }) { + const maxScorePrecise = valueData.maxAnomalyScore; + const maxScore = parseInt(maxScorePrecise); + const maxScoreLabel = (maxScore !== 0) ? maxScore : '< 1'; + const severity = getSeverity(maxScore); + const totalScore = parseInt(valueData.sumAnomalyScore); + const totalScoreLabel = (totalScore !== 0) ? totalScore : '< 1'; + + // Ensure the bar has some width for 0 scores. + const barScore = (maxScore !== 0) ? maxScore : 1; + const barStyle = { + width: `${barScore}%` + }; + + const tooltipContent = getTooltipContent(maxScoreLabel, totalScoreLabel); + + return ( +
+
+ {(influencerFieldName !== 'mlcategory') ? ( +
{valueData.influencerFieldValue}
+ ) : ( +
mlcategory {valueData.influencerFieldValue}
+ )} +
+
+
+
+
+
+ + {maxScoreLabel} + +
+
+
+ + {(totalScore > 0) ? abbreviateWholeNumber(totalScore, 4) : totalScoreLabel} + +
+
+ ); +} +Influencer.propTypes = { + influencerFieldName: PropTypes.string.isRequired, + valueData: PropTypes.object.isRequired +}; + +function InfluencersByName({ influencerFieldName, fieldValues }) { + const influencerValues = fieldValues.map(valueData => ( + + )); + + return ( + + +

{influencerFieldName}

+
+ + {influencerValues} +
+ ); +} +InfluencersByName.propTypes = { + influencerFieldName: PropTypes.string.isRequired, + fieldValues: PropTypes.array.isRequired +}; + +export function InfluencersList({ influencers }) { + + if (influencers === undefined || Object.keys(influencers).length === 0) { + return ( + + + + +

No influencers found

+
+
+
+ ); + } + + const influencersByName = Object.keys(influencers).map(influencerFieldName => ( + + )); + + return ( +
+ {influencersByName} +
+ ); +} +InfluencersList.propTypes = { + influencers: PropTypes.object +}; diff --git a/x-pack/plugins/ml/public/components/influencers_list/influencers_list_directive.js b/x-pack/plugins/ml/public/components/influencers_list/influencers_list_directive.js index fb2daccb2732af..114620b49ab336 100644 --- a/x-pack/plugins/ml/public/components/influencers_list/influencers_list_directive.js +++ b/x-pack/plugins/ml/public/components/influencers_list/influencers_list_directive.js @@ -5,113 +5,19 @@ */ - -/* - * AngularJS directive for rendering a list of Machine Learning influencers. - */ - -import _ from 'lodash'; - -import 'plugins/ml/lib/angular_bootstrap_patch'; -import 'plugins/ml/formatters/abbreviate_whole_number'; - -import template from './influencers_list.html'; -import { getSeverity } from 'plugins/ml/util/anomaly_utils'; -import { mlEscape } from 'plugins/ml/util/string_utils'; - -import { FilterManagerProvider } from 'ui/filter_manager'; +import 'ngreact'; import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.directive('mlInfluencersList', function (Private) { - - const filterManager = Private(FilterManagerProvider); - - function link(scope, element) { - - scope.$on('render', function () { - render(); - }); - - element.on('$destroy', function () { - scope.$destroy(); - }); - - scope.tooltipPlacement = scope.tooltipPlacement === undefined ? 'top' : scope.tooltipPlacement; - - function render() { - if (scope.influencersData === undefined) { - return; - } - - const dataByViewBy = {}; - - // TODO - position tooltip so it doesn't go off edge of window. - const compiledTooltip = _.template( - '
<%= influencerFieldName %>: <%= influencerFieldValue %>' + - '
Max anomaly score: <%= maxScoreLabel %>' + - '
Total anomaly score: <%= totalScoreLabel %>
'); - - _.each(scope.influencersData, (fieldValues, influencerFieldName) => { - const valuesForViewBy = []; - - _.each(fieldValues, function (valueData) { - const influencerFieldValue = valueData.influencerFieldValue; - const maxScorePrecise = valueData.maxAnomalyScore; - const maxScore = parseInt(maxScorePrecise); - const totalScore = parseInt(valueData.sumAnomalyScore); - const barScore = maxScore !== 0 ? maxScore : 1; - const maxScoreLabel = maxScore !== 0 ? maxScore : '< 1'; - const totalScoreLabel = totalScore !== 0 ? totalScore : '< 1'; - const severity = getSeverity(maxScore); - - // Store the data for each influencerfieldname in an array to ensure - // reliable sorting by max score. - // If it was sorted as an object, the order when rendered using the AngularJS - // ngRepeat directive could not be relied upon to be the same as they were - // returned in the ES aggregation e.g. for numeric keys from a mlcategory influencer. - valuesForViewBy.push({ - influencerFieldValue, - maxScorePrecise, - barScore, - maxScoreLabel, - totalScore, - severity, - tooltip: compiledTooltip({ - influencerFieldName: mlEscape(influencerFieldName), - influencerFieldValue: mlEscape(influencerFieldValue), - maxScoreLabel, - totalScoreLabel - }) - }); - }); - - - dataByViewBy[influencerFieldName] = _.sortBy(valuesForViewBy, 'maxScorePrecise').reverse(); - }); - - scope.influencers = dataByViewBy; - } - - // Provide a filter function so filters can be added. - scope.filter = function (field, value, operator) { - filterManager.add(field, value, operator, scope.indexPatternId); - }; +const module = uiModules.get('apps/ml', ['react']); - scope.showNoResultsMessage = function () { - return (scope.influencersData === undefined) || (_.keys(scope.influencersData).length === 0); - }; +import { InfluencersList } from './influencers_list'; - } +module.directive('mlInfluencersList', function ($injector) { + const reactDirective = $injector.get('reactDirective'); - return { - scope: { - influencersData: '=', - indexPatternId: '=', - tooltipPlacement: '@' - }, - template, - link: link - }; + return reactDirective( + InfluencersList, + undefined, + { restrict: 'E' } + ); }); diff --git a/x-pack/plugins/ml/public/components/influencers_list/styles/main.less b/x-pack/plugins/ml/public/components/influencers_list/styles/main.less index ba36498b065b04..3fe182b89c7cea 100644 --- a/x-pack/plugins/ml/public/components/influencers_list/styles/main.less +++ b/x-pack/plugins/ml/public/components/influencers_list/styles/main.less @@ -1,59 +1,26 @@ .ml-influencers-list { - width: 100%; - padding: 0px 0px; line-height: 1.45; - .visualize-error { - display: inline; - - h4 { - font-size: 16px; - margin-top: 65px; - } - } - - .section-label { - background-color: #9c9fa6; - color: #ffffff; - font-size:13px; - /* eui h3 equivalent font-weight */ - font-weight: 500; - margin-bottom: 5px; - padding: 2px 5px; - } - - .influencer-content { - padding-right: 4px; - padding-left: 4px; - } - .field-label { font-size: 12px; - padding-left: 2px; text-align: left; - .influencerfieldvalue { - max-width: calc(~"100% - 40px"); + .field-value { + max-width: calc(~"100% - 34px"); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; display: inline-block; vertical-align: bottom; } - - .filter-buttons { - padding-left: 5px; - display: inline-block; - vertical-align: bottom; - } } .progress { display:inline-block; - width: calc(~"100% - 40px"); - height: 20px; + width: calc(~"100% - 34px"); + height: 22px; min-width: 70px; - margin-bottom: 2px; + margin-bottom: 0px; color: #555; background-color : transparent; @@ -62,63 +29,76 @@ } .progress-bar { - height: 6px; - margin: 6px 0px; + height: 2px; + margin-top: 8px; text-align: right; line-height: 18px; - border-radius: 5px; display: inline-block; } + } - .progress-bar.critical { + .progress.critical { + .progress-bar { background-color: #fe5050; } + .score-label { + border-color: #fe5050; + } + } - .progress-bar.major { + .progress.major { + .progress-bar { background-color: #fba740; } + .score-label { + border-color: #fba740; + } + } - .progress-bar.minor { + .progress.minor { + .progress-bar { background-color: #ffdd00; } + .score-label { + border-color: #ffdd00; + } + } - .progress-bar.warning { + .progress.warning { + .progress-bar { background-color: #8bc8fb; } - + .score-label { + border-color: #8bc8fb; + } } .score-label { - margin-left: 3px; text-align: center; - color: #444444; - padding: 2px 2px 2px 2px; - border-radius: 4px; line-height: 14px; white-space: nowrap; font-size: 12px; - display: inline-block; + display: inline; + margin-left: 4px; } - .score-label.total-score-label { + .total-score-label { width: 32px; vertical-align: top; - background-color: #444444; - color: white; + text-align: center; + color: #555; font-size: 11px; + line-height: 14px; + border-radius: 4px; + padding: 2px; + margin-top: 1px; + display: inline-block; + border: 1px solid #bbbbbb; } } .ml-influencers-list-tooltip { - color: #ffffff; - font-family: Roboto, Droid, Helvetica Neue, Helvetica, Arial, sans-serif; - font-size: 12px; - text-align: left; - - hr { - margin-top: 3px; - margin-bottom: 3px; - border-color: #95a5a6; - } + word-break: break-all; } + diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index 9fcf3d90b9aac8..3ec8ca7056be8d 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -37,10 +37,8 @@ Top Influencers - + influencers="influencers" + />
diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 74c0e1cbec2806..2ac6c515a760e8 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -588,8 +588,8 @@ module.controller('MlExplorerController', function ( MAX_INFLUENCER_FIELD_VALUES ).then((resp) => { // TODO - sort the influencers keys so that the partition field(s) are first. - $scope.influencersData = resp.influencers; - console.log('Explorer top influencers data set:', $scope.influencersData); + $scope.influencers = resp.influencers; + console.log('Explorer top influencers data set:', $scope.influencers); finish(counter); }); diff --git a/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js b/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js index 51748c71fe1095..802aeb3a5d19c3 100644 --- a/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js +++ b/x-pack/plugins/ml/public/formatters/abbreviate_whole_number.js @@ -12,9 +12,6 @@ */ import numeral from '@elastic/numeral'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - export function abbreviateWholeNumber(value, maxDigits) { const maxNumDigits = (maxDigits !== undefined ? maxDigits : 3); if (Math.abs(value) < Math.pow(10, maxNumDigits)) { @@ -23,6 +20,3 @@ export function abbreviateWholeNumber(value, maxDigits) { return numeral(value).format('0a'); } } - -// TODO - remove the filter once all uses of the abbreviateWholeNumber Angular filter have been removed. -module.filter('abbreviateWholeNumber', () => abbreviateWholeNumber);