[ML] Convert Explorer Influencers List to EUI/React (#18773)

* [ML] Convert Explorer Influencers List to EUI/React

* [ML] Remove unused abbreviate_whole_number Angular filter

* [ML] Convert React Influencers List to stateless function
This commit is contained in:
Pete Harverson 2018-05-04 09:22:59 +01:00 committed by GitHub
parent bf71836c51
commit 5de7909350
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 208 additions and 233 deletions

View file

@ -5,7 +5,5 @@
*/
import './influencers_list_directive';
import './styles/main.less';

View file

@ -1,51 +0,0 @@
<div class="ml-influencers-list">
<div ng-if="showNoResultsMessage()" class="text-center visualize-error">
<div class="item top"></div>
<div class="item">
<h4 class="euiTitle euiTitle--small">No influencers found</h4>
</div>
<div class="item bottom"></div>
</div>
<div ng-repeat="(influencerfieldname, groupData) in influencers">
<div class="section-label">{{influencerfieldname}}</div>
<div ng-repeat="influencer in groupData" class="influencer-content">
<div class="field-label">
<div ng-if="influencerfieldname !== 'mlcategory'" class="influencerfieldvalue">{{influencer.influencerFieldValue}}</div>
<div ng-if="influencerfieldname === 'mlcategory'" class="influencerfieldvalue">mlcategory {{influencer.influencerFieldValue}}</div>
<div class="filter-buttons" style="display: none;">
<i ng-click="filter(influencerfieldname, influencer.influencerFieldValue, '+')"
tooltip="Filter for value" tooltip-append-to-body="1" class="fa fa-search-plus"></i>
<i ng-click="filter(influencerfieldname, influencer.influencerFieldValue, '-')"
tooltip="Filter out value" tooltip-append-to-body="1" class="fa fa-search-minus"></i>
</div>
</div>
<div class="progress" value="{{influencer.barScore}}" max="100">
<div class="progress-bar-holder">
<div class="progress-bar {{influencer.severity}}" ng-attr-style="width: {{influencer.barScore}}%;"
tooltip-placement="{{tooltipPlacement}}" tooltip-html-unsafe="{{influencer.tooltip}}" tooltip-append-to-body="true">
</div>
</div>
<span class="score-label">{{influencer.maxScoreLabel}}</span>
</div>
<div ng-if="influencer.totalScore > 0" class="score-label total-score-label" tooltip-placement="{{tooltipPlacement}}" tooltip-html-unsafe="{{influencer.tooltip}}" tooltip-append-to-body="true">
{{influencer.totalScore | abbreviateWholeNumber:4}}
</div>
<div ng-if="influencer.totalScore === 0" class="score-label total-score-label" tooltip-placement="{{tooltipPlacement}}" tooltip-html-unsafe="{{influencer.tooltip}}" tooltip-append-to-body="true">
&lt;&nbsp;1
</div>
</div>
</div>
</div>

View file

@ -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 (
<React.Fragment>
<p>Maximum anomaly score: {maxScoreLabel}</p>
<p>Total anomaly score: {totalScoreLabel}</p>
</React.Fragment>
);
}
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 (
<div>
<div className="field-label">
{(influencerFieldName !== 'mlcategory') ? (
<div className="field-value">{valueData.influencerFieldValue}</div>
) : (
<div className="field-value">mlcategory {valueData.influencerFieldValue}</div>
)}
</div>
<div className={`progress ${severity}`} value="{valueData.maxAnomalyScore}" max="100">
<div className="progress-bar-holder">
<div className="progress-bar" style={barStyle}/>
</div>
<div className="score-label">
<EuiToolTip
position="right"
className="ml-influencers-list-tooltip"
title={`${influencerFieldName}: ${valueData.influencerFieldValue}`}
content={tooltipContent}
>
<span>{maxScoreLabel}</span>
</EuiToolTip>
</div>
</div>
<div className="total-score-label">
<EuiToolTip
position="right"
className="ml-influencers-list-tooltip"
title={`${influencerFieldName}: ${valueData.influencerFieldValue}`}
content={tooltipContent}
>
<span>{(totalScore > 0) ? abbreviateWholeNumber(totalScore, 4) : totalScoreLabel}</span>
</EuiToolTip>
</div>
</div>
);
}
Influencer.propTypes = {
influencerFieldName: PropTypes.string.isRequired,
valueData: PropTypes.object.isRequired
};
function InfluencersByName({ influencerFieldName, fieldValues }) {
const influencerValues = fieldValues.map(valueData => (
<Influencer
key={valueData.influencerFieldValue}
influencerFieldName={influencerFieldName}
valueData={valueData}
/>
));
return (
<React.Fragment key={influencerFieldName}>
<EuiTitle size="xs">
<h4>{influencerFieldName}</h4>
</EuiTitle>
<EuiSpacer size="xs"/>
{influencerValues}
</React.Fragment>
);
}
InfluencersByName.propTypes = {
influencerFieldName: PropTypes.string.isRequired,
fieldValues: PropTypes.array.isRequired
};
export function InfluencersList({ influencers }) {
if (influencers === undefined || Object.keys(influencers).length === 0) {
return (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiSpacer size="xxl" />
<EuiText>
<h4>No influencers found</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
const influencersByName = Object.keys(influencers).map(influencerFieldName => (
<InfluencersByName
key={influencerFieldName}
influencerFieldName={influencerFieldName}
fieldValues={influencers[influencerFieldName]}
/>
));
return (
<div className="ml-influencers-list">
{influencersByName}
</div>
);
}
InfluencersList.propTypes = {
influencers: PropTypes.object
};

View file

@ -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');
const module = uiModules.get('apps/ml', ['react']);
module.directive('mlInfluencersList', function (Private) {
import { InfluencersList } from './influencers_list';
const filterManager = Private(FilterManagerProvider);
module.directive('mlInfluencersList', function ($injector) {
const reactDirective = $injector.get('reactDirective');
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(
'<div class="ml-influencers-list-tooltip"><%= influencerFieldName %>: <%= influencerFieldValue %>' +
'<hr/>Max anomaly score: <%= maxScoreLabel %>' +
'<hr/>Total anomaly score: <%= totalScoreLabel %></div>');
_.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);
};
scope.showNoResultsMessage = function () {
return (scope.influencersData === undefined) || (_.keys(scope.influencersData).length === 0);
};
}
return {
scope: {
influencersData: '=',
indexPatternId: '=',
tooltipPlacement: '@'
},
template,
link: link
};
return reactDirective(
InfluencersList,
undefined,
{ restrict: 'E' }
);
});

View file

@ -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;
}

View file

@ -37,10 +37,8 @@
Top Influencers
</span>
<ml-influencers-list
influencers-data="influencersData"
index-pattern-id="indexPatternId"
tooltip-placement="right">
</ml-influencers-list>
influencers="influencers"
/>
</div>
<div class="column col-xs-10">

View file

@ -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);
});

View file

@ -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);