From bc94853708444a23bef3b6b5802356be88f6fd00 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 24 May 2018 13:19:22 +0100 Subject: [PATCH] [ML] Edits to EUI / React anomalies table after review --- .../anomalies_table/anomalies_table.js | 94 +++++++++---------- .../anomalies_table/anomaly_details.js | 18 ++-- .../components/anomalies_table/entity_cell.js | 2 +- .../anomalies_table/influencers_cell.js | 2 +- .../components/anomalies_table/links_menu.js | 37 +++++--- .../ml/public/explorer/explorer_controller.js | 4 +- .../timeseriesexplorer_controller.js | 4 +- .../timeseriesexplorer_utils.js | 8 +- 8 files changed, 86 insertions(+), 83 deletions(-) diff --git a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js index 92be8401395a9..ef28da7ae2316 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js @@ -91,13 +91,11 @@ function getColumns( { field: 'severity', name: `${(isAggregatedData === true) ? 'max ' : ''}severity`, - render: (score) => { - return ( - - {score >= 1 ? Math.floor(score) : '< 1'} - - ); - }, + render: (score) => ( + + {score >= 1 ? Math.floor(score) : '< 1'} + + ), sortable: true }, { @@ -111,15 +109,13 @@ function getColumns( columns.push({ field: 'entityValue', name: 'found for', - render: (entityValue, item) => { - return ( - - ); - }, + render: (entityValue, item) => ( + + ), sortable: true }); } @@ -128,14 +124,12 @@ function getColumns( columns.push({ field: 'influencers', name: 'influenced by', - render: (influencers) => { - return ( - - ); - }, + render: (influencers) => ( + + ), sortable: true }); } @@ -176,14 +170,12 @@ function getColumns( columns.push({ field: 'metricDescriptionSort', name: 'description', - render: (metricDescriptionSort, item) => { - return ( - - ); - }, + render: (metricDescriptionSort, item) => ( + + ), sortable: true }); } @@ -196,7 +188,7 @@ function getColumns( }); const showExamples = items.some(item => item.entityName === 'mlcategory'); - const showLinks = showViewSeriesLink === true && items.some((item) => showLinksMenuForItem(item)); + const showLinks = (showViewSeriesLink === true) || items.some(item => showLinksMenuForItem(item)); if (showLinks === true) { columns.push({ @@ -264,7 +256,7 @@ class AnomaliesTable extends Component { return anomaly.rowId === rowId; }); - return matching === undefined; + return (matching === undefined); }); if (prevExpandedNotInData !== undefined) { @@ -283,7 +275,7 @@ class AnomaliesTable extends Component { if (itemIdToExpandedRowMap[item.rowId]) { delete itemIdToExpandedRowMap[item.rowId]; } else { - const examples = item.entityName === 'mlcategory' ? + const examples = (item.entityName === 'mlcategory') ? _.get(this.props.tableData, ['examplesByJobId', item.jobId, item.entityValue]) : undefined; itemIdToExpandedRowMap[item.rowId] = ( ); } @@ -310,7 +303,7 @@ class AnomaliesTable extends Component { if (expandButton.length > 0) { const rowId = expandButton.attr('data-row-id'); mouseOverRecord = this.props.tableData.anomalies.find((anomaly) => { - return anomaly.rowId === rowId; + return (anomaly.rowId === rowId); }); } } @@ -325,11 +318,9 @@ class AnomaliesTable extends Component { mlAnomaliesTableService.anomalyRecordMouseenter.changed(mouseOverRecord); } } - } else { - if (mouseOverRecord !== undefined) { - // Mouse is now over a row, fire mouseenter on the record. - mlAnomaliesTableService.anomalyRecordMouseenter.changed(mouseOverRecord); - } + } else if (mouseOverRecord !== undefined) { + // Mouse is now over a row, fire mouseenter on the record. + mlAnomaliesTableService.anomalyRecordMouseenter.changed(mouseOverRecord); } this.mouseOverRecord = mouseOverRecord; @@ -342,9 +333,10 @@ class AnomaliesTable extends Component { }; render() { - if (this.props.tableData === undefined || - this.props.tableData.anomalies === undefined || - this.props.tableData.anomalies.length === 0) { + const { timefilter, tableData, filter } = this.props; + + if (tableData === undefined || + tableData.anomalies === undefined || tableData.anomalies.length === 0) { return ( @@ -357,15 +349,15 @@ class AnomaliesTable extends Component { } const columns = getColumns( - this.props.tableData.anomalies, - this.props.tableData.examplesByJobId, + tableData.anomalies, + tableData.examplesByJobId, this.isShowingAggregatedData(), - this.props.tableData.interval, - this.props.timefilter, - this.props.tableData.showViewSeriesLink, + tableData.interval, + timefilter, + tableData.showViewSeriesLink, this.state.itemIdToExpandedRowMap, this.toggleRow, - this.props.filter); + filter); const sorting = { sort: { @@ -377,7 +369,7 @@ class AnomaliesTable extends Component { return ( 0) { examples.forEach((example, index) => { - const title = index === 0 ? 'category examples' : ''; + const title = (index === 0) ? 'category examples' : ''; items.push({ title, description: example }); }); } items.push({ title: 'function', - description: source.function !== 'metric' ? source.function : source.function_description + description: (source.function !== 'metric') ? source.function : source.function_description }); if (source.field_name !== undefined) { @@ -162,7 +161,7 @@ function getDetailsItems(anomaly, examples, filter) { // will already have been added for display. if (causes.length > 1) { causes.forEach((cause, index) => { - const title = index === 0 ? `${cause.entityName} values` : ''; + const title = (index === 0) ? `${cause.entityName} values` : ''; let description = `${cause.entityValue} (actual ${formatValue(cause.actual, source.function)}, `; description += `typical ${formatValue(cause.typical, source.function)}, probability ${cause.probability})`; items.push({ title, description }); @@ -213,7 +212,7 @@ export class AnomalyDetails extends Component {
Description
{anomalyDescription} - {mvDescription !== undefined && + {(mvDescription !== undefined) && {mvDescription} @@ -254,8 +253,8 @@ export class AnomalyDetails extends Component { let othersCount = 0; let numToDisplay = 0; if (anomalyInfluencers !== undefined) { - numToDisplay = this.state.showAllInfluencers === true ? - anomalyInfluencers.length : Math.min(INFLUENCERS_LIMIT, anomalyInfluencers.length); + numToDisplay = (this.state.showAllInfluencers === true) ? + anomalyInfluencers.length : Math.min(this.props.influencersLimit, anomalyInfluencers.length); othersCount = Math.max(anomalyInfluencers.length - numToDisplay, 0); if (othersCount === 1) { @@ -293,7 +292,7 @@ export class AnomalyDetails extends Component { and {othersCount} more } - {numToDisplay > (INFLUENCERS_LIMIT + 1) && + {numToDisplay > (this.props.influencersLimit + 1) && this.toggleAllInfluencers()} > @@ -322,5 +321,6 @@ AnomalyDetails.propTypes = { anomaly: PropTypes.object.isRequired, examples: PropTypes.array, isAggregatedData: PropTypes.bool, - filter: PropTypes.func + filter: PropTypes.func, + influencersLimit: PropTypes.number }; diff --git a/x-pack/plugins/ml/public/components/anomalies_table/entity_cell.js b/x-pack/plugins/ml/public/components/anomalies_table/entity_cell.js index 36c67bfe06a2a..ca6d3cf7e68cd 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/entity_cell.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/entity_cell.js @@ -19,7 +19,7 @@ import { * a filter on this entity. */ export function EntityCell({ entityName, entityValue, filter }) { - const valueText = entityName !== 'mlcategory' ? entityValue : `mlcategory ${entityValue}`; + const valueText = (entityName !== 'mlcategory') ? entityValue : `mlcategory ${entityValue}`; return ( {valueText} diff --git a/x-pack/plugins/ml/public/components/anomalies_table/influencers_cell.js b/x-pack/plugins/ml/public/components/anomalies_table/influencers_cell.js index a04152fb8c8d8..78400b3fe7ebd 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/influencers_cell.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/influencers_cell.js @@ -81,7 +81,7 @@ export class InfluencersCell extends Component { const recordInfluencers = this.props.influencers || []; const influencers = []; - _.each(recordInfluencers, (influencer) => { + recordInfluencers.forEach((influencer) => { _.each(influencer, (influencerFieldValue, influencerFieldName) => { influencers.push({ influencerFieldName, diff --git a/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js index 05a79b0cc9bf9..d77716049237e 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js @@ -46,13 +46,14 @@ export class LinksMenu extends Component { } openCustomUrl = (customUrl) => { - console.log('Anomalies Table - open customUrl for record:', this.props.anomaly); + const { anomaly, interval, isAggregatedData } = this.props; + + console.log('Anomalies Table - open customUrl for record:', anomaly); // If url_value contains $earliest$ and $latest$ tokens, add in times to the source record. // Create a copy of the record as we are adding properties into it. - const record = _.cloneDeep(this.props.anomaly.source); + const record = _.cloneDeep(anomaly.source); const timestamp = record.timestamp; - const interval = this.props.interval; const configuredUrlValue = customUrl.url_value; const timeRangeInterval = parseInterval(customUrl.time_range); if (configuredUrlValue.includes('$earliest$')) { @@ -74,7 +75,7 @@ export class LinksMenu extends Component { if (timeRangeInterval !== null) { latestMoment.add(timeRangeInterval); } else { - if (this.props.isAggregatedData === true) { + if (isAggregatedData === true) { latestMoment = moment(timestamp).endOf(interval); if (interval === 'hour') { // Show to the end of the next hour. @@ -97,9 +98,9 @@ export class LinksMenu extends Component { ml.results.getCategoryDefinition(jobId, categoryId) .then((resp) => { - // Prefix each of the terms with '+' so that the Elasticsearch Query String query - // run in a drilldown Kibana dashboard has to match on all terms. - const termsArray = _.map(resp.terms.split(' '), (term) => { return '+' + term; }); + // Prefix each of the terms with '+' so that the Elasticsearch Query String query + // run in a drilldown Kibana dashboard has to match on all terms. + const termsArray = resp.terms.split(' ').map(term => `+${term}`); record.mlcategoryterms = termsArray.join(' '); record.mlcategoryregex = resp.regex; @@ -188,10 +189,8 @@ export class LinksMenu extends Component { }); // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. - let path = chrome.getBasePath(); - path += '/app/ml#/timeseriesexplorer'; - path += '?_g=' + _g; - path += '&_a=' + encodeURIComponent(_a); + let path = `${chrome.getBasePath()}/app/ml#/timeseriesexplorer`; + path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; window.open(path, '_blank'); } @@ -201,6 +200,12 @@ export class LinksMenu extends Component { const indexPatterns = getIndexPatterns(); const job = mlJobService.getJob(this.props.anomaly.jobId); + if (job === undefined) { + console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`); + toastNotifications.addDanger( + `Unable to view examples as no details could be found for job ID ${this.props.anomaly.jobId}`); + return; + } const categorizationFieldName = job.analysis_config.categorization_field_name; const datafeedIndices = job.datafeed_config.indices; // Find the type of the categorization field i.e. text (preferred) or keyword. @@ -330,6 +335,8 @@ export class LinksMenu extends Component { }; render() { + const { anomaly, showViewSeriesLink } = this.props; + const button = ( { + if (anomaly.customUrls !== undefined) { + anomaly.customUrls.forEach((customUrl, index) => { items.push( { + $scope.$evalAsync(() => { $scope.tableData = { anomalies, interval: resp.interval, examplesByJobId: resp.examplesByJobId, showViewSeriesLink: true }; - }, 0); + }); }).catch((resp) => { console.log('Explorer - error loading data for anomalies table:', resp); diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js index 627d72ec78f70..19b51da3d65ee 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js @@ -708,14 +708,14 @@ module.controller('MlTimeSeriesExplorerController', function ( } }); - $timeout(() => { + $scope.$evalAsync(() => { $scope.tableData = { anomalies, interval: resp.interval, examplesByJobId: resp.examplesByJobId, showViewSeriesLink: false }; - }, 0); + }); }).catch((resp) => { console.log('Time series explorer - error loading data for anomalies table:', resp); diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js index a4761d172ac0e..c59d5e1c86041 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js @@ -107,9 +107,9 @@ export function processDataForFocusAnomalies( const recordTime = record[timeFieldName]; let chartPoint = findNearestChartPointToTime(chartData, recordTime); - // TODO - handle case where there is an anomaly due to the absense of data + // TODO - handle case where there is an anomaly due to the absence of data // and there is no model plot. - if (chartPoint === undefined && chartData.length) { + if (chartPoint === undefined && chartData !== undefined && chartData.length) { // In case there is a record with a time after that of the last chart point, set the score // for the last chart point to that of the last record, if that record has a higher score. const lastChartPoint = chartData[chartData.length - 1]; @@ -167,6 +167,10 @@ export function processScheduledEventsForChart(chartData, scheduledEvents) { export function findNearestChartPointToTime(chartData, time) { let chartPoint; + if(chartData === undefined) { + return chartPoint; + } + for (let i = 0; i < chartData.length; i++) { if (chartData[i].date.getTime() === time) { chartPoint = chartData[i];