From 80dcaffdfdcefbbfb527bc4c416e21b41de8659b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 30 Aug 2018 16:24:46 +0200 Subject: [PATCH 1/8] [ML] Initial explorer charts react migration. --- .../loading_indicator/loading_indicator.js | 24 ++++ .../loading_indicator/styles/main.less | 15 +++ .../plugins/ml/public/explorer/explorer.html | 2 +- .../explorer_charts/explore_series.js | 76 +++++++++++ ...r_chart_directive.js => explorer_chart.js} | 108 +++++++++------- .../explorer_chart_config_builder.js | 121 ++++++++---------- .../explorer_charts_container.html | 25 ---- .../explorer_charts_container.js | 114 +++++++++++++++++ ...r.js => explorer_charts_container_data.js} | 43 ++++--- .../explorer_charts_container_directive.js | 118 +++++++---------- .../public/explorer/explorer_charts/index.js | 2 - ...art_directive.less => explorer_chart.less} | 3 +- ...ve.less => explorer_charts_container.less} | 9 +- 13 files changed, 423 insertions(+), 237 deletions(-) create mode 100644 x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explore_series.js rename x-pack/plugins/ml/public/explorer/explorer_charts/{explorer_chart_directive.js => explorer_chart.js} (86%) delete mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js rename x-pack/plugins/ml/public/explorer/explorer_charts/{explorer_charts_container_controller.js => explorer_charts_container_data.js} (94%) rename x-pack/plugins/ml/public/explorer/explorer_charts/styles/{explorer_chart_directive.less => explorer_chart.less} (96%) rename x-pack/plugins/ml/public/explorer/explorer_charts/styles/{explorer_charts_container_directive.less => explorer_charts_container.less} (93%) diff --git a/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js b/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js new file mode 100644 index 00000000000000..a363771c77b694 --- /dev/null +++ b/x-pack/plugins/ml/public/components/loading_indicator/loading_indicator.js @@ -0,0 +1,24 @@ +/* + * 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 './styles/main.less'; + +import PropTypes from 'prop-types'; +import React from 'react'; + +export function LoadingIndicator({ height }) { + height = height ? +height : 100; + return ( +
+
+
+ ); +} +LoadingIndicator.propTypes = { + height: PropTypes.number +}; diff --git a/x-pack/plugins/ml/public/components/loading_indicator/styles/main.less b/x-pack/plugins/ml/public/components/loading_indicator/styles/main.less index 6b41ed7de1c5a8..addb8a931248b3 100644 --- a/x-pack/plugins/ml/public/components/loading_indicator/styles/main.less +++ b/x-pack/plugins/ml/public/components/loading_indicator/styles/main.less @@ -1,3 +1,4 @@ +/* angular */ ml-loading-indicator { .loading-indicator { text-align: center; @@ -12,3 +13,17 @@ ml-loading-indicator { } } } + +/* react */ +.ml-loading-indicator { + text-align: center; + font-size: 17px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .loading-spinner { + font-size: 24px; + } +} diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index 1abfcbdff8b6c6..19c1c8a86fa336 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -145,7 +145,7 @@

No {{swimlaneViewByFieldName}} influencers -
+
{ + entityCondition[entity.fieldName] = entity.fieldValue; + }); + + // Use rison to build the URL . + const _g = rison.encode({ + ml: { + jobIds: [series.jobId] + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0 + }, + time: { + from: from, + to: to, + mode: 'absolute' + } + }); + + const _a = rison.encode({ + mlTimeSeriesExplorer: { + zoom: { + from: zoomFrom, + to: zoomTo + }, + detectorIndex: series.detectorIndex, + entities: entityCondition, + }, + filters: [], + query: { + query_string: { + analyze_wildcard: true, + query: '*' + } + } + }); + + let path = chrome.getBasePath(); + path += '/app/ml#/timeseriesexplorer'; + path += '?_g=' + _g; + path += '&_a=' + encodeURIComponent(_a); + $window.open(path, '_blank'); + + }; +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_directive.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js similarity index 86% rename from x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_directive.js rename to x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js index befa1afc4dd3bd..ec8663f2bf1f42 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart.js @@ -4,43 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ - - /* - * AngularJS directive for rendering a chart of anomalies in the raw data in + * React component for rendering a chart of anomalies in the raw data in * the Machine Learning Explorer dashboard. */ -import './styles/explorer_chart_directive.less'; +import './styles/explorer_chart.less'; + +import PropTypes from 'prop-types'; +import React from 'react'; import _ from 'lodash'; import d3 from 'd3'; -import angular from 'angular'; +import $ from 'jquery'; import moment from 'moment'; import { formatValue } from 'plugins/ml/formatters/format_value'; import { getSeverityWithLow } from 'plugins/ml/../common/util/anomaly_utils'; import { drawLineChartDots, numTicksForDateFormat } from 'plugins/ml/util/chart_utils'; import { TimeBuckets } from 'ui/time_buckets'; -import loadingIndicatorWrapperTemplate from 'plugins/ml/components/loading_indicator/loading_indicator_wrapper.html'; +import { LoadingIndicator } from 'plugins/ml/components/loading_indicator/loading_indicator'; import { mlEscape } from 'plugins/ml/util/string_utils'; import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); +export class ExplorerChart extends React.Component { + static propTypes = { + seriesConfig: PropTypes.object.isRequired + } + + componentDidUpdate() { + const { + mlSelectSeverityService, + mlChartTooltipService + } = this.props; -module.directive('mlExplorerChart', function ( - mlChartTooltipService, - Private, - mlSelectSeverityService) { + const element = this._rootNode; + const config = this.props.seriesConfig; - function link(scope, element) { - console.log('ml-explorer-chart directive link series config:', scope.seriesConfig); - if (typeof scope.seriesConfig === 'undefined') { + if ( + typeof config === 'undefined' || + Array.isArray(config.chartData) === false + ) { // just return so the empty directive renders without an error later on return; } - const config = scope.seriesConfig; + const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); let vizWidth = 0; @@ -56,35 +64,22 @@ module.directive('mlExplorerChart', function ( let lineChartGroup; let lineChartValuesLine = null; - // create a chart loading placeholder - scope.isLoading = config.loading; - if (Array.isArray(config.chartData)) { - // make sure we wait for the previous digest cycle to finish - // or the chart's wrapping elements might not have their - // right widths yet and we need them to define the SVG's width - scope.$evalAsync(() => { - init(config.chartLimits); - drawLineChart(config.chartData); - }); - } - - element.on('$destroy', function () { - scope.$destroy(); - }); + init(config.chartLimits); + drawLineChart(config.chartData); function init(chartLimits) { - const $el = angular.element('ml-explorer-chart'); + const $el = $('.ml-explorer-chart'); // Clear any existing elements from the visualization, // then build the svg elements for the chart. - const chartElement = d3.select(element.get(0)).select('.content-wrapper'); + const chartElement = d3.select(element).select('.content-wrapper'); chartElement.select('svg').remove(); const svgWidth = $el.width(); const svgHeight = chartHeight + margin.top + margin.bottom; const svg = chartElement.append('svg') - .attr('width', svgWidth) + .attr('width', svgWidth) .attr('height', svgHeight); // Set the size of the left margin according to the width of the largest y axis tick label. @@ -122,7 +117,7 @@ module.directive('mlExplorerChart', function ( d3.select('.temp-axis-label').remove(); margin.left = (Math.max(maxYAxisLabelWidth, 40)); - vizWidth = svgWidth - margin.left - margin.right; + vizWidth = svgWidth - margin.left - margin.right; // Set the x axis domain to match the request plot range. // This ensures ranges on different charts will match, even when there aren't @@ -319,12 +314,37 @@ module.directive('mlExplorerChart', function ( } } - return { - restrict: 'E', - scope: { - seriesConfig: '=' - }, - link: link, - template: loadingIndicatorWrapperTemplate - }; -}); + shouldComponentUpdate() { + // Prevents component re-rendering + return true; + } + + _setRef(componentNode) { + this._rootNode = componentNode; + } + + render() { + const { + seriesConfig + } = this.props; + + if (typeof seriesConfig === 'undefined') { + // just return so the empty directive renders without an error later on + return null; + } + + // create a chart loading placeholder + const isLoading = seriesConfig.loading; + + return ( +
+ {isLoading && ( + + )} + {!isLoading && ( +
+ )} +
+ ); + } +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js index 07a9bc0885e605..d5b3bfe09ef579 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js @@ -18,83 +18,66 @@ import { buildConfigFromDetector } from 'plugins/ml/util/chart_config_builder'; import { mlEscape } from 'plugins/ml/util/string_utils'; import { mlJobService } from 'plugins/ml/services/job_service'; -export function explorerChartConfigBuilder() { - - const compiledTooltip = _.template( - '
job ID: <%= jobId %>
' + - 'aggregation interval: <%= aggregationInterval %>
' + - 'chart function: <%= chartFunction %>' + - '<% for(let i = 0; i < entityFields.length; ++i) { %>' + - '
<%= entityFields[i].fieldName %>: <%= entityFields[i].fieldValue %>' + - '<% } %>' + - '
'); - - // Builds the chart configuration for the provided anomaly record, returning - // an object with properties used for the display (series function and field, aggregation interval etc), - // and properties for the data feed used for the job (index pattern, time field etc). - function buildConfig(record) { - const job = mlJobService.getJob(record.job_id); - const detectorIndex = record.detector_index; - const config = buildConfigFromDetector(job, detectorIndex); - - // Add extra properties used by the explorer dashboard charts. - config.functionDescription = record.function_description; - config.bucketSpanSeconds = parseInterval(job.analysis_config.bucket_span).asSeconds(); - - config.detectorLabel = record.function; - if ((_.has(mlJobService.detectorsByJob, record.job_id)) && - (detectorIndex < mlJobService.detectorsByJob[record.job_id].length)) { - config.detectorLabel = mlJobService.detectorsByJob[record.job_id][detectorIndex].detector_description; - } else { - if (record.field_name !== undefined) { - config.detectorLabel += ` ${config.fieldName}`; - } - } - +// Builds the chart configuration for the provided anomaly record, returning +// an object with properties used for the display (series function and field, aggregation interval etc), +// and properties for the data feed used for the job (index pattern, time field etc). +export function buildConfig(record) { + const job = mlJobService.getJob(record.job_id); + const detectorIndex = record.detector_index; + const config = buildConfigFromDetector(job, detectorIndex); + + // Add extra properties used by the explorer dashboard charts. + config.functionDescription = record.function_description; + config.bucketSpanSeconds = parseInterval(job.analysis_config.bucket_span).asSeconds(); + + config.detectorLabel = record.function; + if ((_.has(mlJobService.detectorsByJob, record.job_id)) && + (detectorIndex < mlJobService.detectorsByJob[record.job_id].length)) { + config.detectorLabel = mlJobService.detectorsByJob[record.job_id][detectorIndex].detector_description; + } else { if (record.field_name !== undefined) { - config.fieldName = record.field_name; - config.metricFieldName = record.field_name; - } - - // Add the 'entity_fields' i.e. the partition, by, over fields which - // define the metric series to be plotted. - config.entityFields = []; - if (_.has(record, 'partition_field_name')) { - config.entityFields.push({ fieldName: record.partition_field_name, fieldValue: record.partition_field_value }); + config.detectorLabel += ` ${config.fieldName}`; } + } - if (_.has(record, 'over_field_name')) { - config.entityFields.push({ fieldName: record.over_field_name, fieldValue: record.over_field_value }); - } + if (record.field_name !== undefined) { + config.fieldName = record.field_name; + config.metricFieldName = record.field_name; + } - // For jobs with by and over fields, don't add the 'by' field as this - // field will only be added to the top-level fields for record type results - // if it also an influencer over the bucket. - if (_.has(record, 'by_field_name') && !(_.has(record, 'over_field_name'))) { - config.entityFields.push({ fieldName: record.by_field_name, fieldValue: record.by_field_value }); - } + // Add the 'entity_fields' i.e. the partition, by, over fields which + // define the metric series to be plotted. + config.entityFields = []; + if (_.has(record, 'partition_field_name')) { + config.entityFields.push({ fieldName: record.partition_field_name, fieldValue: record.partition_field_value }); + } - // Build the tooltip for the chart info icon, showing further details on what is being plotted. - let functionLabel = config.metricFunction; - if (config.metricFieldName !== undefined) { - functionLabel += ` ${mlEscape(config.metricFieldName)}`; - } + if (_.has(record, 'over_field_name')) { + config.entityFields.push({ fieldName: record.over_field_name, fieldValue: record.over_field_value }); + } - config.infoTooltip = compiledTooltip({ - jobId: record.job_id, - aggregationInterval: config.interval, - chartFunction: functionLabel, - entityFields: config.entityFields.map((f) => ({ - fieldName: mlEscape(f.fieldName), - fieldValue: mlEscape(f.fieldValue), - })) - }); + // For jobs with by and over fields, don't add the 'by' field as this + // field will only be added to the top-level fields for record type results + // if it also an influencer over the bucket. + if (_.has(record, 'by_field_name') && !(_.has(record, 'over_field_name'))) { + config.entityFields.push({ fieldName: record.by_field_name, fieldValue: record.by_field_value }); + } - return config; + // Build the tooltip for the chart info icon, showing further details on what is being plotted. + let functionLabel = config.metricFunction; + if (config.metricFieldName !== undefined) { + functionLabel += ` ${mlEscape(config.metricFieldName)}`; } - return { - buildConfig + config.infoTooltip = { + jobId: record.job_id, + aggregationInterval: config.interval, + chartFunction: functionLabel, + entityFields: config.entityFields.map((f) => ({ + fieldName: mlEscape(f.fieldName), + fieldValue: mlEscape(f.fieldValue), + })) }; -} + return config; +} 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 deleted file mode 100644 index 34e9ac9f9b014e..00000000000000 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.html +++ /dev/null @@ -1,25 +0,0 @@ -
-
- -
-
-
- {{series.detectorLabel}} - - {{series.detectorLabel}} - - {{entity.fieldName}} {{entity.fieldValue}} - -
- - - - View - -
- -
- -
- -
diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js new file mode 100644 index 00000000000000..42f032e6bd6b5b --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js @@ -0,0 +1,114 @@ +/* + * 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 PropTypes from 'prop-types'; +import React from 'react'; + +import { EuiIconTip } from '@elastic/eui'; + +import { ExplorerChart } from './explorer_chart'; + +function CompiledTooltip({ + jobId, + aggregationInterval, + chartFunction, + entityFields = [], +}) { + return ( +
+ job ID: {jobId}
+ aggregation interval: {aggregationInterval}
+ chart function: {chartFunction} + {entityFields.map((entityField) => { + return ( +
{entityField.fieldName}: {entityField.fieldValue}
+ ); + })} +
+ ); +} +CompiledTooltip.propTypes = { + jobId: PropTypes.string.isRequired, + aggregationInterval: PropTypes.string, + chartFunction: PropTypes.string, + entityFields: PropTypes.array +}; + + +export function ExplorerChartsContainer({ + exploreSeries, + seriesToPlot, + layoutCellsPerChart, + tooManyBuckets, + mlSelectSeverityService, + mlChartTooltipService +}) { + return ( +
+ {(seriesToPlot.length > 0) && + seriesToPlot.map((series) => { + + // create a somewhat unique ID from charts metadata + const { + jobId, + detectorLabel, + entityFields, + } = series; + const entities = entityFields.map((ef) => { + return `${ef.fieldName}/${ef.fieldValue}`; + }).join(','); + const id = `${jobId}_${detectorLabel}_${entities}`; + + return ( +
+
+
+ {(entityFields.length > 0) && ( + {detectorLabel} - + )} + {(entityFields.length === 0) && ( + {detectorLabel} + )} + {entityFields.map((entity, j) => { + return ( + {entity.fieldName} {entity.fieldValue} + ); + })} +
+ } position="left" size="s" /> + {tooManyBuckets && ( + + )} + exploreSeries(series)}> + View