Skip to content

Commit

Permalink
[ML] Convert Explorer Influencers List to EUI/React (#18773) (#18801)
Browse files Browse the repository at this point in the history
* [ML] Convert Explorer Influencers List to EUI/React

* [ML] Remove unused abbreviate_whole_number Angular filter

* [ML] Convert React Influencers List to stateless function
  • Loading branch information
peteharverson committed May 4, 2018
1 parent a77e2bf commit 375f5f1
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 233 deletions.
2 changes: 0 additions & 2 deletions x-pack/plugins/ml/public/components/influencers_list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,5 @@
*/




import './influencers_list_directive';
import './styles/main.less';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<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);
};
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' }
);
});
Loading

0 comments on commit 375f5f1

Please sign in to comment.