From ac284f07f3bbfd2cfb1f219cf33b613fd0a45477 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 19 Sep 2018 14:11:13 +0200 Subject: [PATCH 01/22] [ML] Swimlanes don't rely on directive scope, gets now passed via listener in explorer_controller. --- .../plugins/ml/public/explorer/explorer.html | 18 +----- .../ml/public/explorer/explorer_controller.js | 60 ++++++++++++++---- .../ml/public/explorer/explorer_swimlane.js | 27 ++++---- .../explorer/explorer_swimlane_directive.js | 61 +++++-------------- .../ml/public/explorer/styles/main.less | 3 + 5 files changed, 78 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index 81f2b2e76985f1..857a3755a77717 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -55,14 +55,7 @@ ng-mouseenter="setSwimlaneSelectActive(true)" ng-mouseleave="setSwimlaneSelectActive(false)" > - - +
@@ -95,14 +88,7 @@ ng-mouseenter="setSwimlaneSelectActive(true)" ng-mouseleave="setSwimlaneSelectActive(false)" > - - +
diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index f7b93edf7b40a6..79aadddd2de49b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -65,6 +65,11 @@ function getDefaultViewBySwimlaneData() { }; } +const SWIMLANE_TYPE = { + OVERALL: 'overall', + VIEW_BY: 'viewBy' +}; + module.controller('MlExplorerController', function ( $scope, $timeout, @@ -93,7 +98,7 @@ module.controller('MlExplorerController', function ( const VIEW_BY_JOB_LABEL = 'job ID'; const ALLOW_CELL_RANGE_SELECTION = mlExplorerDashboardService.allowCellRangeSelection; - // make sure dragSelect is only available if the mouse point is actually over a swimlane + // make sure dragSelect is only available if the mouse pointer is actually over a swimlane let disableDragSelectOnMouseLeave = true; // skip listening to clicks on swimlanes while they are loading to avoid race conditions let skipCellClicks = true; @@ -274,12 +279,7 @@ module.controller('MlExplorerController', function ( // Listen for changes to job selection. mlJobSelectService.listenJobSelectionChange($scope, (event, selections) => { - // Clear swimlane selection from state. - delete $scope.appState.mlExplorerSwimlane.selectedType; - delete $scope.appState.mlExplorerSwimlane.selectedLane; - delete $scope.appState.mlExplorerSwimlane.selectedTime; - delete $scope.appState.mlExplorerSwimlane.selectedInterval; - + clearSwimlaneSelectionFromAppState(); $scope.setSelectedJobs(selections); }); @@ -299,12 +299,45 @@ module.controller('MlExplorerController', function ( }, 300); }); + function clearSwimlaneSelectionFromAppState() { + delete $scope.appState.mlExplorerSwimlane.selectedType; + delete $scope.appState.mlExplorerSwimlane.selectedLane; + delete $scope.appState.mlExplorerSwimlane.selectedTime; + delete $scope.appState.mlExplorerSwimlane.selectedInterval; + } + + function getSwimlaneData(swimlaneType) { + switch (swimlaneType) { + case SWIMLANE_TYPE.OVERALL: + return $scope.overallSwimlaneData; + case SWIMLANE_TYPE.VIEW_BY: + return $scope.viewBySwimlaneData; + } + } + + function mapScopeToSwimlaneProps(swimlaneType) { + const swimlaneData = getSwimlaneData(swimlaneType); + return { + lanes: swimlaneData.laneLabels, + startTime: swimlaneData.earliest, + endTime: swimlaneData.latest, + stepSecs: swimlaneData.interval, + points: swimlaneData.points, + chartWidth: $scope.swimlaneWidth, + MlTimeBuckets: TimeBuckets, + swimlaneData, + swimlaneType, + mlExplorerDashboardService, + appState: $scope.appState + }; + } + function redrawOnResize() { $scope.swimlaneWidth = getSwimlaneContainerWidth(); $scope.$apply(); - mlExplorerDashboardService.swimlaneDataChange.changed('overall'); - mlExplorerDashboardService.swimlaneDataChange.changed('viewBy'); + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); if ( mlCheckboxShowChartsService.state.get('showCharts') && @@ -740,6 +773,7 @@ module.controller('MlExplorerController', function ( overallBucketsBounds.max.valueOf(), $scope.swimlaneBucketInterval.asSeconds() + 's' ).then((resp) => { + skipCellClicks = false; processOverallResults(resp.results, searchBounds); console.log('Explorer overall swimlane data set:', $scope.overallSwimlaneData); @@ -758,8 +792,7 @@ module.controller('MlExplorerController', function ( // Need to use $timeout to ensure the broadcast happens after the child scope is updated with the new data. $timeout(() => { $scope.$broadcast('render'); - mlExplorerDashboardService.swimlaneDataChange.changed('overall'); - skipCellClicks = false; + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); }, 0); }); @@ -791,6 +824,7 @@ module.controller('MlExplorerController', function ( // finish() function, called after each data set has been loaded and processed. // The last one to call it will trigger the page render. function finish() { + skipCellClicks = false; console.log('Explorer view by swimlane data set:', $scope.viewBySwimlaneData); if (swimlaneCellClickListenerQueue.length > 0) { const cellData = swimlaneCellClickListenerQueue.pop(); @@ -801,8 +835,7 @@ module.controller('MlExplorerController', function ( // Fire event to indicate swimlane data has changed. // Need to use $timeout to ensure this happens after the child scope is updated with the new data. $timeout(() => { - skipCellClicks = false; - mlExplorerDashboardService.swimlaneDataChange.changed('viewBy'); + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); }, 0); } @@ -965,6 +998,7 @@ module.controller('MlExplorerController', function ( loadTopInfluencers(jobIds, earliestMs, latestMs); loadDataForCharts(jobIds, earliestMs, latestMs); loadAnomaliesTableData(); + clearSwimlaneSelectionFromAppState(); } function calculateSwimlaneBucketInterval() { diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 2c0000a264fbe2..cb11d8ebb62f9d 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -257,7 +257,7 @@ export class ExplorerSwimlane extends React.Component { clearSelection() { - const { appState, mlExplorerDashboardService } = this.props; + const { mlExplorerDashboardService } = this.props; // This selects both overall and viewby swimlane const wrapper = d3.selectAll('.ml-explorer-swimlane'); @@ -268,11 +268,6 @@ export class ExplorerSwimlane extends React.Component { wrapper.selectAll('.sl-cell-inner-dragselect.sl-cell-inner-selected').classed('sl-cell-inner-selected', false); wrapper.selectAll('.ds-selected').classed('sl-cell-inner-selected', false); - delete appState.mlExplorerSwimlane.selectedType; - delete appState.mlExplorerSwimlane.selectedLanes; - delete appState.mlExplorerSwimlane.selectedTimes; - appState.save(); - mlExplorerDashboardService.swimlaneCellClick.changed({}); } @@ -322,6 +317,16 @@ export class ExplorerSwimlane extends React.Component { timeBuckets.setInterval(`${stepSecs}s`); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + function cellMouseOverFactory(time, i) { + // Don't use an arrow function here because we need access to `this`, + // which is where d3 supplies a reference to the corresponding DOM element. + return function (lane) { + const bucketScore = getBucketScore(lane, time); + if (bucketScore === 0) { return; } + cellMouseover(this, lane, bucketScore, i, time); + }; + } + function cellMouseover(target, laneLabel, bucketScore, index, time) { if (bucketScore === undefined || cellMouseoverActive === false) { return; @@ -371,16 +376,6 @@ export class ExplorerSwimlane extends React.Component { } }); - function cellMouseOverFactory(time, i) { - // Don't use an arrow function here because we need access to `this`, - // which is where d3 supplies a reference to the corresponding DOM element. - return function (lane) { - const bucketScore = getBucketScore(lane, time); - if (bucketScore === 0) { return; } - cellMouseover(this, lane, bucketScore, i, time); - }; - } - const cellsContainer = d3LanesEnter.append('div').classed('cells-container', true); function getBucketScore(lane, time) { diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js index fc7adcce51b620..2e324687a80ec3 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js @@ -14,7 +14,6 @@ import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets'; import { ExplorerSwimlane } from './explorer_swimlane'; import { uiModules } from 'ui/modules'; @@ -23,51 +22,17 @@ const module = uiModules.get('apps/ml'); module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDashboardService) { function link(scope, element) { + let previousProps = null; // Re-render the swimlane whenever the underlying data changes. - function swimlaneDataChangeListener(swimlaneType) { - if (swimlaneType === scope.swimlaneType) { - render(); + function swimlaneDataChangeListener(props) { + if (props.swimlaneType !== scope.swimlaneType) { + return; } - } - - mlExplorerDashboardService.swimlaneDataChange.watch(swimlaneDataChangeListener); - - element.on('$destroy', () => { - mlExplorerDashboardService.swimlaneDataChange.unwatch(swimlaneDataChangeListener); - // unmountComponentAtNode() needs to be called so dragSelectListener within - // the ExplorerSwimlane component gets unwatched properly. - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - - const MlTimeBuckets = Private(IntervalHelperProvider); - // This triggers the render function quite aggressively, but we want to make sure we don't miss - // any updates to related scopes of directives and/or controllers. However, we do a deep comparison - // of current and future props to filter redundant render triggers. - scope.$watch(function () { - render(); - }); - let previousProps = null; - function render() { - if (scope.swimlaneData === undefined) { + if (props.swimlaneData === undefined) { return; } - const props = { - lanes: scope.swimlaneData.laneLabels, - startTime: scope.swimlaneData.earliest, - endTime: scope.swimlaneData.latest, - stepSecs: scope.swimlaneData.interval, - points: scope.swimlaneData.points, - chartWidth: scope.chartWidth, - MlTimeBuckets, - swimlaneData: scope.swimlaneData, - swimlaneType: scope.swimlaneType, - mlExplorerDashboardService, - appState: scope.appState - }; - if (_.isEqual(props, previousProps) === false) { ReactDOM.render( React.createElement(ExplorerSwimlane, props), @@ -75,17 +40,21 @@ module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDa ); previousProps = props; } - } + mlExplorerDashboardService.swimlaneDataChange.watch(swimlaneDataChangeListener); + + element.on('$destroy', () => { + mlExplorerDashboardService.swimlaneDataChange.unwatch(swimlaneDataChangeListener); + // unmountComponentAtNode() needs to be called so dragSelectListener within + // the ExplorerSwimlane component gets unwatched properly. + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); } return { scope: { - swimlaneType: '@', - swimlaneData: '=', - selectedJobIds: '=', - chartWidth: '=', - appState: '=' + swimlaneType: '@' }, link }; diff --git a/x-pack/plugins/ml/public/explorer/styles/main.less b/x-pack/plugins/ml/public/explorer/styles/main.less index b3c297821bcbf2..462343bfcde345 100644 --- a/x-pack/plugins/ml/public/explorer/styles/main.less +++ b/x-pack/plugins/ml/public/explorer/styles/main.less @@ -143,6 +143,9 @@ border-radius: 2px; margin-bottom: 5px; } + + width: 100%; + height: 250px; } ml-explorer-swimlane.ml-dragselect-dragging { From 51a9852dce65aaa8736be8291335a0df6a3aed2f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Sep 2018 12:55:06 +0200 Subject: [PATCH 02/22] [ML] Reduce swimlane state and event handling complexity. --- .../ml/public/explorer/explorer_controller.js | 23 ++- .../ml/public/explorer/explorer_swimlane.js | 140 +++++++++--------- .../explorer/explorer_swimlane_directive.js | 25 ++-- 3 files changed, 99 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 79aadddd2de49b..cfa40cbab08aa7 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -301,9 +301,10 @@ module.controller('MlExplorerController', function ( function clearSwimlaneSelectionFromAppState() { delete $scope.appState.mlExplorerSwimlane.selectedType; - delete $scope.appState.mlExplorerSwimlane.selectedLane; - delete $scope.appState.mlExplorerSwimlane.selectedTime; + delete $scope.appState.mlExplorerSwimlane.selectedLanes; + delete $scope.appState.mlExplorerSwimlane.selectedTimes; delete $scope.appState.mlExplorerSwimlane.selectedInterval; + $scope.appState.save(); } function getSwimlaneData(swimlaneType) { @@ -328,7 +329,7 @@ module.controller('MlExplorerController', function ( swimlaneData, swimlaneType, mlExplorerDashboardService, - appState: $scope.appState + selection: $scope.appState.mlExplorerSwimlane }; } @@ -432,6 +433,11 @@ module.controller('MlExplorerController', function ( clearSelectedAnomalies(); previousListenerData = null; } else { + $scope.appState.mlExplorerSwimlane.selectedType = cellData.selection.selectedType; + $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.selection.selectedLanes; + $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.selection.selectedTimes; + $scope.appState.save(); + const timerange = getSelectionTimeRange(cellData); $scope.cellData = cellData; @@ -461,6 +467,8 @@ module.controller('MlExplorerController', function ( // pass influencers on to loadDataForCharts(), // it will take care of calling loadTopInfluencers() in this case. + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs, influencers); loadAnomaliesTableData(); } else { @@ -992,13 +1000,20 @@ module.controller('MlExplorerController', function ( const bounds = timefilter.getActiveBounds(); const earliestMs = bounds.min.valueOf(); const latestMs = bounds.max.valueOf(); + + clearSwimlaneSelectionFromAppState(); + if ($scope.overallSwimlaneData !== undefined) { + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); + } + if ($scope.viewBySwimlaneData !== undefined) { + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); + } mlExplorerDashboardService.anomalyDataChange.changed($scope.anomalyChartRecords, earliestMs, latestMs); // Load all top influencers right away because the filtering // done in loadDataForCharts() isn't neccessary here. loadTopInfluencers(jobIds, earliestMs, latestMs); loadDataForCharts(jobIds, earliestMs, latestMs); loadAnomaliesTableData(); - clearSwimlaneSelectionFromAppState(); } function calculateSwimlaneBucketInterval() { diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index cb11d8ebb62f9d..17fe7ec4a227a5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -27,17 +27,14 @@ import { DRAG_SELECT_ACTION } from './explorer_constants'; export class ExplorerSwimlane extends React.Component { static propTypes = { - appState: PropTypes.object.isRequired, lanes: PropTypes.array.isRequired, mlExplorerDashboardService: PropTypes.object.isRequired } - constructor(props) { - super(props); - this.state = { - cellMouseoverActive: true - }; - } + // Since this component is mostly rendered using d3 and cellMouseoverActive is only + // relevant for d3 based interaction, we don't manage this using React's state + // and intentionally circumvent the component lifecycle when updating it. + cellMouseoverActive = true; componentWillUnmount() { const { mlExplorerDashboardService } = this.props; @@ -45,6 +42,7 @@ export class ExplorerSwimlane extends React.Component { const element = d3.select(this.rootNode); element.html(''); } + componentDidMount() { const element = d3.select(this.rootNode.parentNode); const { mlExplorerDashboardService } = this.props; @@ -63,7 +61,6 @@ export class ExplorerSwimlane extends React.Component { this.renderSwimlane(); } - componentDidUpdate() { this.renderSwimlane(); } @@ -71,7 +68,7 @@ export class ExplorerSwimlane extends React.Component { // property to remember the bound dragSelectListener boundDragSelectListener = null; - // property for cellClick data comparison to be able to filter + // property for data comparison to be able to filter // consecutive click events with the same data. previousSelectedData = null; @@ -99,17 +96,19 @@ export class ExplorerSwimlane extends React.Component { selectedData.laneLabels = _.uniq(selectedData.laneLabels); selectedData.times = _.uniq(selectedData.times); if (_.isEqual(selectedData, this.previousSelectedData) === false) { - this.cellClick(elements, selectedData); + const cellsToSelect = elements; + const { laneLabels, bucketScore, times } = selectedData; + this.selectCell(cellsToSelect, laneLabels, times, bucketScore, true); this.previousSelectedData = selectedData; } } - this.setState({ cellMouseoverActive: true }); + this.cellMouseoverActive = true; } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { element.classed('ml-dragselect-dragging', true); return; } else if (action === DRAG_SELECT_ACTION.DRAG_START) { - this.setState({ cellMouseoverActive: false }); + this.cellMouseoverActive = false; return; } @@ -118,28 +117,23 @@ export class ExplorerSwimlane extends React.Component { elements.map(e => d3.select(e).classed('ds-selected', false)); } - cellClick(cellsToSelect, { laneLabels, bucketScore, times }) { - if (cellsToSelect.length > 1 || bucketScore > 0) { - this.selectCell(cellsToSelect, laneLabels, times, bucketScore, true); - } else { - this.clearSelection(); - } - } - checkForSelection() { const element = d3.select(this.rootNode.parentNode); const { - appState, + selection, swimlaneData, swimlaneType } = this.props; - // Check for selection in the AppState and reselect the corresponding swimlane cell + // Check for selection and reselect the corresponding swimlane cell // if the time range and lane label are still in view. - const selectionState = appState.mlExplorerSwimlane; + const selectionState = selection; const selectedType = _.get(selectionState, 'selectedType', undefined); const viewBy = _.get(selectionState, 'viewBy', ''); + + // If a selection was done in the other swimlane, add the "masked" classes + // to de-emphasize the swimlane cells. if (swimlaneType !== selectedType && selectedType !== undefined) { element.selectAll('.lane-label').classed('lane-label-masked', true); element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); @@ -175,25 +169,71 @@ export class ExplorerSwimlane extends React.Component { }); } }); + const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); }, 0); + if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { - this.selectCell(cellsToSelect, selectedLanes, selectedTimes, selectedMaxBucketScore); + this.highlightSelection(cellsToSelect, selectedLanes, selectedTimes); } else { - // Clear selection from state as previous selection is no longer applicable. this.clearSelection(); } } selectCell(cellsToSelect, laneLabels, times, bucketScore, checkEqualSelection = false) { const { - appState, + selection, mlExplorerDashboardService, swimlaneData, swimlaneType } = this.props; + let triggerNewSelection = false; + + if (cellsToSelect.length > 1 || bucketScore > 0) { + triggerNewSelection = true; + } + + // Check if the same cells were selected again, if so clear the selection, + // otherwise activate the new selection. The two objects are built for + // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" + // since it also includes the "viewBy" attribute which might differ depending + // on whether the overall or viewby swimlane was selected. + const oldSelection = { + selectedType: selection.selectedType, + selectedLanes: selection.selectedLanes, + selectedTimes: selection.selectedTimes + }; + + const newSelection = { + selectedType: swimlaneType, + selectedLanes: laneLabels, + selectedTimes: times + }; + + if ((checkEqualSelection && _.isEqual(oldSelection, newSelection))) { + triggerNewSelection = false; + } + + if (triggerNewSelection === false) { + mlExplorerDashboardService.swimlaneCellClick.changed({}); + return; + } + + mlExplorerDashboardService.swimlaneCellClick.changed({ + fieldName: swimlaneData.fieldName, + laneLabels, + time: d3.extent(times), + interval: swimlaneData.interval, + score: bucketScore, + selection: newSelection + }); + } + + highlightSelection(cellsToSelect, laneLabels, times) { + const { swimlaneType } = this.props; + // This selects both overall and viewby swimlane const wrapper = d3.selectAll('.ml-explorer-swimlane'); @@ -209,7 +249,7 @@ export class ExplorerSwimlane extends React.Component { const rootParent = d3.select(this.rootNode.parentNode); rootParent.selectAll('.lane-label') .classed('lane-label-masked', function () { - return (laneLabels.indexOf(d3.select(this).text()) > -1); + return (laneLabels.indexOf(d3.select(this).text()) === -1); }); if (swimlaneType === 'viewBy') { @@ -220,42 +260,8 @@ export class ExplorerSwimlane extends React.Component { overallCell.classed('sl-cell-inner-selected', true); }); } - - // Check if the same cells were selected again, if so clear the selection, - // otherwise activate the new selection. The two objects are built for - // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" - // since it also includes the "viewBy" attribute which might differ depending - // on whether the overall or viewby swimlane was selected. - if (checkEqualSelection && _.isEqual( - { - selectedType: appState.mlExplorerSwimlane.selectedType, - selectedLanes: appState.mlExplorerSwimlane.selectedLanes, - selectedTimes: appState.mlExplorerSwimlane.selectedTimes - }, - { - selectedType: swimlaneType, - selectedLanes: laneLabels, - selectedTimes: times - } - )) { - this.clearSelection(); - } else { - appState.mlExplorerSwimlane.selectedType = swimlaneType; - appState.mlExplorerSwimlane.selectedLanes = laneLabels; - appState.mlExplorerSwimlane.selectedTimes = times; - appState.save(); - - mlExplorerDashboardService.swimlaneCellClick.changed({ - fieldName: swimlaneData.fieldName, - laneLabels, - time: d3.extent(times), - interval: swimlaneData.interval, - score: bucketScore - }); - } } - clearSelection() { const { mlExplorerDashboardService } = this.props; @@ -274,9 +280,7 @@ export class ExplorerSwimlane extends React.Component { renderSwimlane() { const element = d3.select(this.rootNode.parentNode); - const { - cellMouseoverActive - } = this.state; + const cellMouseoverActive = this.cellMouseoverActive; const { lanes, @@ -289,7 +293,7 @@ export class ExplorerSwimlane extends React.Component { swimlaneData, swimlaneType, mlExplorerDashboardService, - appState + selection } = this.props; function colorScore(value) { @@ -353,8 +357,6 @@ export class ExplorerSwimlane extends React.Component { mlChartTooltipService.hide(); } - const that = this; - const d3Lanes = swimlanes.selectAll('.lane').data(lanes); const d3LanesEnter = d3Lanes.enter().append('div').classed('lane', true); @@ -363,8 +365,8 @@ export class ExplorerSwimlane extends React.Component { .style('width', `${laneLabelWidth}px`) .html(label => mlEscape(label)) .on('click', () => { - if (typeof appState.mlExplorerSwimlane.selectedLanes !== 'undefined') { - that.clearSelection(); + if (typeof selection.selectedLanes !== 'undefined') { + mlExplorerDashboardService.swimlaneCellClick.changed({}); } }) .each(function () { diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js index 2e324687a80ec3..a777785b49fd85 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js @@ -7,10 +7,9 @@ /* - * AngularJS directive for rendering Explorer dashboard swimlanes. + * AngularJS directive wrapper for rendering Anomaly Explorer's ExplorerSwimlane React component. */ -import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -22,24 +21,18 @@ const module = uiModules.get('apps/ml'); module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDashboardService) { function link(scope, element) { - let previousProps = null; - // Re-render the swimlane whenever the underlying data changes. function swimlaneDataChangeListener(props) { - if (props.swimlaneType !== scope.swimlaneType) { + if ( + props.swimlaneType !== scope.swimlaneType || + props.swimlaneData === undefined + ) { return; } - if (props.swimlaneData === undefined) { - return; - } - - if (_.isEqual(props, previousProps) === false) { - ReactDOM.render( - React.createElement(ExplorerSwimlane, props), - element[0] - ); - previousProps = props; - } + ReactDOM.render( + React.createElement(ExplorerSwimlane, props), + element[0] + ); } mlExplorerDashboardService.swimlaneDataChange.watch(swimlaneDataChangeListener); From 9534c0a1928a669fc852a4db8ee566ef08dd0456 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Sep 2018 20:00:15 +0200 Subject: [PATCH 03/22] [ML] Trying to fix anomaly explorer issues. --- .../explorer_charts_container_directive.js | 2 + .../ml/public/explorer/explorer_controller.js | 135 +++++++++++++----- .../explorer/explorer_dashboard_service.js | 2 + .../ml/public/explorer/explorer_swimlane.js | 15 +- 4 files changed, 109 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js index 58b9994b6a650b..45f2ae31cac2e8 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_directive.js @@ -65,6 +65,8 @@ module.directive('mlExplorerChartsContainer', function ( element[0] ); } + + mlExplorerDashboardService.chartsInitDone.changed(); } return { diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index cfa40cbab08aa7..159e3752bff55e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -147,6 +147,16 @@ module.controller('MlExplorerController', function ( $scope.viewBySwimlaneOptions = []; $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); + + let chartsInitialized = false; + let chartsCallback = () => {}; + function initializeAfterChartsContainerDone() { + if (chartsInitialized === false) { + chartsCallback(); + } + chartsInitialized = true; + } + $scope.initializeVis = function () { // Initialize the AppState in which to store filters. const stateDefaults = { @@ -164,7 +174,7 @@ module.controller('MlExplorerController', function ( // Select any jobs set in the global state (i.e. passed in the URL). const selectedJobIds = mlJobSelectService.getSelectedJobIds(true); - $scope.setSelectedJobs(selectedJobIds); + $scope.setSelectedJobs(selectedJobIds, true); } else { $scope.loading = false; } @@ -174,6 +184,7 @@ module.controller('MlExplorerController', function ( }); mlExplorerDashboardService.init(); + mlExplorerDashboardService.chartsInitDone.watch(initializeAfterChartsContainerDone); }; // create new job objects based on standard job config objects @@ -185,7 +196,25 @@ module.controller('MlExplorerController', function ( }); } - $scope.setSelectedJobs = function (selectedIds) { + function restoreCellDataFromAppState() { + // restore cellData from AppState + if ( + $scope.cellData === undefined && + $scope.appState.mlExplorerSwimlane.selectedType !== undefined + ) { + $scope.cellData = { + type: $scope.appState.mlExplorerSwimlane.selectedType, + lanes: $scope.appState.mlExplorerSwimlane.selectedLanes, + times: $scope.appState.mlExplorerSwimlane.selectedTimes + }; + if ($scope.cellData.type === SWIMLANE_TYPE.VIEW_BY) { + $scope.cellData.fieldName = $scope.appState.mlExplorerSwimlane.viewBy; + } + $scope.swimlaneViewByFieldName = $scope.appState.mlExplorerSwimlane.viewBy; + } + } + + $scope.setSelectedJobs = function (selectedIds, keepSwimlaneSelection = false) { let previousSelected = 0; if ($scope.selectedJobs !== null) { previousSelected = $scope.selectedJobs.length; @@ -236,8 +265,13 @@ module.controller('MlExplorerController', function ( .finally(() => { // Load the data - if the FieldFormats failed to populate // the default formatting will be used for metric values. - clearSelectedAnomalies(); loadOverallData(); + if (keepSwimlaneSelection === false) { + clearSelectedAnomalies(); + } else { + restoreCellDataFromAppState(); + updateExplorer(); + } }); }; @@ -303,7 +337,6 @@ module.controller('MlExplorerController', function ( delete $scope.appState.mlExplorerSwimlane.selectedType; delete $scope.appState.mlExplorerSwimlane.selectedLanes; delete $scope.appState.mlExplorerSwimlane.selectedTimes; - delete $scope.appState.mlExplorerSwimlane.selectedInterval; $scope.appState.save(); } @@ -371,14 +404,15 @@ module.controller('MlExplorerController', function ( let earliestMs = bounds.min.valueOf(); let latestMs = bounds.max.valueOf(); - if (cellData !== undefined && cellData.time !== undefined) { + if (cellData !== undefined && cellData.times !== undefined) { // time property of the cell data is an array, with the elements being // the start times of the first and last cell selected. - earliestMs = (cellData.time[0] !== undefined) ? cellData.time[0] * 1000 : bounds.min.valueOf(); + earliestMs = (cellData.times[0] !== undefined) ? cellData.times[0] * 1000 : bounds.min.valueOf(); latestMs = bounds.max.valueOf(); - if (cellData.time[1] !== undefined) { + if (cellData.times[1] !== undefined) { // Subtract 1 ms so search does not include start of next bucket. - latestMs = ((cellData.time[1] + cellData.interval) * 1000) - 1; + const interval = $scope.swimlaneBucketInterval.asSeconds(); + latestMs = ((cellData.times[1] + interval) * 1000) - 1; } } @@ -388,9 +422,12 @@ module.controller('MlExplorerController', function ( function getSelectionInfluencers(cellData) { const influencers = []; - if (cellData !== undefined && cellData.fieldName !== undefined && - cellData.fieldName !== VIEW_BY_JOB_LABEL) { - cellData.laneLabels.forEach((laneLabel) =>{ + if ( + cellData !== undefined && + cellData.fieldName !== undefined && + cellData.fieldName !== VIEW_BY_JOB_LABEL + ) { + cellData.lanes.forEach((laneLabel) =>{ influencers.push({ fieldName: $scope.swimlaneViewByFieldName, fieldValue: laneLabel }); }); } @@ -433,17 +470,16 @@ module.controller('MlExplorerController', function ( clearSelectedAnomalies(); previousListenerData = null; } else { - $scope.appState.mlExplorerSwimlane.selectedType = cellData.selection.selectedType; - $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.selection.selectedLanes; - $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.selection.selectedTimes; + $scope.appState.mlExplorerSwimlane.selectedType = cellData.type; + $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.lanes; + $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.times; $scope.appState.save(); const timerange = getSelectionTimeRange(cellData); $scope.cellData = cellData; if (cellData.score > 0) { - const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? - cellData.laneLabels : $scope.getSelectedJobIds(); + const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.laneLabels : $scope.getSelectedJobIds(); const influencers = getSelectionInfluencers(cellData); const listenerData = { @@ -469,7 +505,13 @@ module.controller('MlExplorerController', function ( // it will take care of calling loadTopInfluencers() in this case. mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); - loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs, influencers); + + if (influencers.length === 0) { + loadTopInfluencers(jobIds, timerange.earliestMs, timerange.latestMs); + loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs); + } else { + loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs, influencers); + } loadAnomaliesTableData(); } else { // Multiple cells are selected, all with a score of 0 - clear all anomalies. @@ -547,6 +589,7 @@ module.controller('MlExplorerController', function ( mlSelectIntervalService.state.unwatch(tableControlsListener); mlSelectSeverityService.state.unwatch(tableControlsListener); mlSelectLimitService.state.unwatch(swimlaneLimitListener); + mlExplorerDashboardService.chartsInitDone.unwatch(initializeAfterChartsContainerDone); delete $scope.cellData; refreshWatcher.cancel(); // Cancel listening for updates to the global nav state. @@ -560,6 +603,7 @@ module.controller('MlExplorerController', function ( // Just skip doing the request when this function is called without // the minimum required data. if ($scope.cellData === undefined && influencers.length === 0) { + mlExplorerDashboardService.anomalyDataChange.changed([], earliestMs, latestMs); return; } @@ -782,7 +826,7 @@ module.controller('MlExplorerController', function ( $scope.swimlaneBucketInterval.asSeconds() + 's' ).then((resp) => { skipCellClicks = false; - processOverallResults(resp.results, searchBounds); + $scope.overallSwimlaneData = processOverallResults(resp.results, searchBounds); console.log('Explorer overall swimlane data set:', $scope.overallSwimlaneData); if ($scope.overallSwimlaneData.points && $scope.overallSwimlaneData.points.length > 0) { @@ -935,7 +979,7 @@ module.controller('MlExplorerController', function ( function loadAnomaliesTableData() { const cellData = $scope.cellData; const jobIds = ($scope.cellData !== undefined && cellData.fieldName === VIEW_BY_JOB_LABEL) ? - cellData.laneLabels : $scope.getSelectedJobIds(); + cellData.lanes : $scope.getSelectedJobIds(); const influencers = getSelectionInfluencers(cellData); const timeRange = getSelectionTimeRange(cellData); @@ -990,30 +1034,47 @@ module.controller('MlExplorerController', function ( }); } - function clearSelectedAnomalies() { - $scope.anomalyChartRecords = []; - $scope.viewByLoadedForTimeFormatted = null; - delete $scope.cellData; - - // With no swimlane selection, display anomalies over all time in the table. - const jobIds = $scope.getSelectedJobIds(); - const bounds = timefilter.getActiveBounds(); - const earliestMs = bounds.min.valueOf(); - const latestMs = bounds.max.valueOf(); + function updateExplorer() { + const cellData = $scope.cellData; + const timerange = getSelectionTimeRange(cellData); - clearSwimlaneSelectionFromAppState(); if ($scope.overallSwimlaneData !== undefined) { mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); } if ($scope.viewBySwimlaneData !== undefined) { mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); } - mlExplorerDashboardService.anomalyDataChange.changed($scope.anomalyChartRecords, earliestMs, latestMs); - // Load all top influencers right away because the filtering - // done in loadDataForCharts() isn't neccessary here. - loadTopInfluencers(jobIds, earliestMs, latestMs); - loadDataForCharts(jobIds, earliestMs, latestMs); - loadAnomaliesTableData(); + mlExplorerDashboardService.anomalyDataChange.changed($scope.anomalyChartRecords, timerange.earliestMs, timerange.latestMs); + + const jobIds = (cellData !== undefined && cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.laneLabels : $scope.getSelectedJobIds(); + const influencers = getSelectionInfluencers(cellData); + + // The following is to avoid running into a race condition where loading a swimlane selection from URL/AppState + // would fail because the Explorer Charts Container's directive wasn't linked yet and not being subscribed + // to the anomalyDataChange listener used in loadDataForCharts(). + function finish() { + if (influencers.length === 0) { + loadTopInfluencers(jobIds, timerange.earliestMs, timerange.latestMs); + loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs); + } else { + loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs, influencers); + } + loadAnomaliesTableData(); + } + + if (chartsInitialized) { + finish(); + } else { + chartsCallback = finish; + } + } + + function clearSelectedAnomalies() { + $scope.anomalyChartRecords = []; + $scope.viewByLoadedForTimeFormatted = null; + delete $scope.cellData; + clearSwimlaneSelectionFromAppState(); + updateExplorer(); } function calculateSwimlaneBucketInterval() { @@ -1087,7 +1148,7 @@ module.controller('MlExplorerController', function ( }); } - $scope.overallSwimlaneData = dataset; + return dataset; } function processViewByResults(scoresByInfluencerAndTime, sortedLaneValues) { diff --git a/x-pack/plugins/ml/public/explorer/explorer_dashboard_service.js b/x-pack/plugins/ml/public/explorer/explorer_dashboard_service.js index d18fb706f0d324..ea6dcb0faab2b5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_dashboard_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_dashboard_service.js @@ -24,6 +24,7 @@ module.service('mlExplorerDashboardService', function () { const swimlaneCellClick = this.swimlaneCellClick = listenerFactory(); const swimlaneDataChange = this.swimlaneDataChange = listenerFactory(); const swimlaneRenderDone = this.swimlaneRenderDone = listenerFactory(); + const chartsInitDone = this.chartsInitDone = listenerFactory(); this.anomalyDataChange = listenerFactory(); this.init = function () { @@ -32,6 +33,7 @@ module.service('mlExplorerDashboardService', function () { swimlaneCellClick.unwatchAll(); swimlaneDataChange.unwatchAll(); swimlaneRenderDone.unwatchAll(); + chartsInitDone.unwatchAll(); }; }); diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 17fe7ec4a227a5..4e348872423a3a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -181,7 +181,7 @@ export class ExplorerSwimlane extends React.Component { } } - selectCell(cellsToSelect, laneLabels, times, bucketScore, checkEqualSelection = false) { + selectCell(cellsToSelect, lanes, times, bucketScore, checkEqualSelection = false) { const { selection, mlExplorerDashboardService, @@ -208,8 +208,8 @@ export class ExplorerSwimlane extends React.Component { const newSelection = { selectedType: swimlaneType, - selectedLanes: laneLabels, - selectedTimes: times + selectedLanes: lanes, + selectedTimes: d3.extent(times) }; if ((checkEqualSelection && _.isEqual(oldSelection, newSelection))) { @@ -223,11 +223,10 @@ export class ExplorerSwimlane extends React.Component { mlExplorerDashboardService.swimlaneCellClick.changed({ fieldName: swimlaneData.fieldName, - laneLabels, - time: d3.extent(times), - interval: swimlaneData.interval, - score: bucketScore, - selection: newSelection + lanes, + times: d3.extent(times), + type: swimlaneType, + score: bucketScore }); } From d1e245ff8e379578be72c6d0083fc4652d3dfc53 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Sep 2018 21:03:09 +0200 Subject: [PATCH 04/22] [ML] Fixes show charts button. --- .../plugins/ml/public/explorer/explorer_controller.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 159e3752bff55e..765ff19587783c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -148,13 +148,13 @@ module.controller('MlExplorerController', function ( $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); - let chartsInitialized = false; + let isChartsContainerInitialized = false; let chartsCallback = () => {}; function initializeAfterChartsContainerDone() { - if (chartsInitialized === false) { + if (isChartsContainerInitialized === false) { chartsCallback(); } - chartsInitialized = true; + isChartsContainerInitialized = true; } $scope.initializeVis = function () { @@ -540,8 +540,7 @@ module.controller('MlExplorerController', function ( const checkboxShowChartsListener = function () { const showCharts = mlCheckboxShowChartsService.state.get('showCharts'); if (showCharts && $scope.cellData !== undefined) { - // passing true as the second argument skips click event filtering - swimlaneCellClickListener($scope.cellData, true); + updateExplorer(); } else { const timerange = getSelectionTimeRange($scope.cellData); mlExplorerDashboardService.anomalyDataChange.changed( @@ -1062,7 +1061,7 @@ module.controller('MlExplorerController', function ( loadAnomaliesTableData(); } - if (chartsInitialized) { + if (isChartsContainerInitialized) { finish(); } else { chartsCallback = finish; From f17a764add86a7c1ef48cf1d795e05b8fe8e1420 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Sep 2018 21:21:07 +0200 Subject: [PATCH 05/22] [ML] Fixes view by jobs swimlane selection. --- .../ml/public/explorer/explorer_controller.js | 24 +++++++++---------- .../ml/public/explorer/explorer_swimlane.js | 5 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 765ff19587783c..8ac886ca2523bf 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -479,7 +479,8 @@ module.controller('MlExplorerController', function ( $scope.cellData = cellData; if (cellData.score > 0) { - const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.laneLabels : $scope.getSelectedJobIds(); + const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.lanes : $scope.getSelectedJobIds(); + const influencers = getSelectionInfluencers(cellData); const listenerData = { @@ -874,7 +875,11 @@ module.controller('MlExplorerController', function ( skipCellClicks = true; // finish() function, called after each data set has been loaded and processed. // The last one to call it will trigger the page render. - function finish() { + function finish(resp) { + if (resp !== undefined) { + $scope.viewBySwimlaneData = processViewByResults(resp.results, fieldValues); + } + skipCellClicks = false; console.log('Explorer view by swimlane data set:', $scope.viewBySwimlaneData); if (swimlaneCellClickListenerQueue.length > 0) { @@ -918,10 +923,7 @@ module.controller('MlExplorerController', function ( searchBounds.max.valueOf(), interval, swimlaneLimit - ).then((resp) => { - processViewByResults(resp.results, fieldValues); - finish(); - }); + ).then(finish); } else { const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; mlResultsService.getScoresByBucket( @@ -930,10 +932,7 @@ module.controller('MlExplorerController', function ( searchBounds.max.valueOf(), interval, swimlaneLimit - ).then((resp) => { - processViewByResults(resp.results, fieldValues); - finish(); - }); + ).then(finish); } } @@ -1158,7 +1157,8 @@ module.controller('MlExplorerController', function ( const dataset = { fieldName: $scope.swimlaneViewByFieldName, points: [], - interval: $scope.swimlaneBucketInterval.asSeconds() }; + interval: $scope.swimlaneBucketInterval.asSeconds() + }; // Set the earliest and latest to be the same as the overall swimlane. dataset.earliest = $scope.overallSwimlaneData.earliest; @@ -1206,7 +1206,7 @@ module.controller('MlExplorerController', function ( }); } - $scope.viewBySwimlaneData = dataset; + return dataset; } }); diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 4e348872423a3a..21431d0c87fe2b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -221,13 +221,14 @@ export class ExplorerSwimlane extends React.Component { return; } - mlExplorerDashboardService.swimlaneCellClick.changed({ + const cellData = { fieldName: swimlaneData.fieldName, lanes, times: d3.extent(times), type: swimlaneType, score: bucketScore - }); + }; + mlExplorerDashboardService.swimlaneCellClick.changed(cellData); } highlightSelection(cellsToSelect, laneLabels, times) { From 09932740ac28399a0dddc43e4bdfd9f9a07375d3 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Sep 2018 22:15:38 +0200 Subject: [PATCH 06/22] [ML] Fixes restoring selection from URL/AppState. --- .../ml/public/explorer/explorer_controller.js | 96 ++++++++++++------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 8ac886ca2523bf..6014fb7698bad5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -600,10 +600,9 @@ module.controller('MlExplorerController', function ( // and avoid race conditions ending up with the wrong charts. let requestCount = 0; function loadDataForCharts(jobIds, earliestMs, latestMs, influencers = []) { - // Just skip doing the request when this function is called without - // the minimum required data. + // Just skip doing the request when this function + // is called without the minimum required data. if ($scope.cellData === undefined && influencers.length === 0) { - mlExplorerDashboardService.anomalyDataChange.changed([], earliestMs, latestMs); return; } @@ -631,6 +630,14 @@ module.controller('MlExplorerController', function ( } } + // While the charts were loaded, other events could reset cellData, + // so check if it's still present. This can happen if a cell selection + // gets restored from URL/AppState and we find out it's not applicable + // to the view by swimlanes currently on display. + if ($scope.cellData === undefined) { + return; + } + if (influencers.length > 0) { // Filter the Top Influencers list to show just the influencers from // the records in the selected time range. @@ -878,6 +885,23 @@ module.controller('MlExplorerController', function ( function finish(resp) { if (resp !== undefined) { $scope.viewBySwimlaneData = processViewByResults(resp.results, fieldValues); + + // do a sanity check against cellData. I can happen that a previously + // selected lane loaded via URL/AppState is not be available anymore. + if ( + $scope.cellData !== undefined && + $scope.cellData.type === SWIMLANE_TYPE.VIEW_BY + ) { + let selectionExists = false; + $scope.cellData.lanes.forEach((lane) => { + if ($scope.viewBySwimlaneData.laneLabels.indexOf(lane) > -1) { + selectionExists = true; + } + }); + if (selectionExists === false) { + clearSelectedAnomalies(); + } + } } skipCellClicks = false; @@ -901,40 +925,40 @@ module.controller('MlExplorerController', function ( $scope.swimlaneViewByFieldName === null ) { finish(); - } else { - // Ensure the search bounds align to the bucketing interval used in the swimlane so - // that the first and last buckets are complete. - const bounds = timefilter.getActiveBounds(); - const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); - const selectedJobIds = $scope.getSelectedJobIds(); - const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit.val; - - // load scores by influencer/jobId value and time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets - // which wouldn't be the case if e.g. '1M' was used. - const interval = $scope.swimlaneBucketInterval.asSeconds() + 's'; - if ($scope.swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { - mlResultsService.getInfluencerValueMaxScoreByTime( - selectedJobIds, - $scope.swimlaneViewByFieldName, - fieldValues, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit - ).then(finish); - } else { - const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; - mlResultsService.getScoresByBucket( - jobIds, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit - ).then(finish); + return; + } - } + // Ensure the search bounds align to the bucketing interval used in the swimlane so + // that the first and last buckets are complete. + const bounds = timefilter.getActiveBounds(); + const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); + const selectedJobIds = $scope.getSelectedJobIds(); + const limit = mlSelectLimitService.state.get('limit'); + const swimlaneLimit = (limit === undefined) ? 10 : limit.val; + + // load scores by influencer/jobId value and time. + // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets + // which wouldn't be the case if e.g. '1M' was used. + const interval = $scope.swimlaneBucketInterval.asSeconds() + 's'; + if ($scope.swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { + mlResultsService.getInfluencerValueMaxScoreByTime( + selectedJobIds, + $scope.swimlaneViewByFieldName, + fieldValues, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + interval, + swimlaneLimit + ).then(finish); + } else { + const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; + mlResultsService.getScoresByBucket( + jobIds, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + interval, + swimlaneLimit + ).then(finish); } } From db4b60bf54d63d8de6577ae29df1b9cb50d4343d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 20 Sep 2018 22:35:57 +0200 Subject: [PATCH 07/22] [ML] Only trigger a swimlane update if the props are valid. --- x-pack/plugins/ml/public/explorer/explorer_controller.js | 4 ++-- .../ml/public/explorer/explorer_swimlane_directive.js | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 6014fb7698bad5..991e6707b61529 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -502,8 +502,6 @@ module.controller('MlExplorerController', function ( $scope.viewByLoadedForTimeFormatted = moment(timerange.earliestMs).format('MMMM Do YYYY, HH:mm'); } - // pass influencers on to loadDataForCharts(), - // it will take care of calling loadTopInfluencers() in this case. mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); @@ -511,6 +509,8 @@ module.controller('MlExplorerController', function ( loadTopInfluencers(jobIds, timerange.earliestMs, timerange.latestMs); loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs); } else { + // pass influencers on to loadDataForCharts(), + // it will take care of calling loadTopInfluencers() in this case. loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs, influencers); } loadAnomaliesTableData(); diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js index a777785b49fd85..8ec8b19a52ce2f 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js @@ -18,13 +18,15 @@ import { ExplorerSwimlane } from './explorer_swimlane'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); -module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDashboardService) { +module.directive('mlExplorerSwimlane', function (mlExplorerDashboardService) { function link(scope, element) { function swimlaneDataChangeListener(props) { if ( props.swimlaneType !== scope.swimlaneType || - props.swimlaneData === undefined + props.swimlaneData === undefined || + props.startTime === undefined || + props.endTime === undefined ) { return; } From e00fe01ddd462a7c6bf6816fccd7b5de1a84e989 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 00:36:28 +0200 Subject: [PATCH 08/22] [ML] Migrates the limit dropdown to use React. --- .../ml/public/explorer/explorer_controller.js | 4 +- .../ml/public/explorer/select_limit/index.js | 2 +- .../explorer/select_limit/select_limit.html | 9 -- .../explorer/select_limit/select_limit.js | 125 ++++++++++-------- .../select_limit/select_limit_directive.js | 33 +++++ 5 files changed, 105 insertions(+), 68 deletions(-) delete mode 100644 x-pack/plugins/ml/public/explorer/select_limit/select_limit.html create mode 100644 x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 991e6707b61529..f124d226e7ac88 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -934,7 +934,7 @@ module.controller('MlExplorerController', function ( const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit.val; + const swimlaneLimit = (limit === undefined) ? 10 : limit; // load scores by influencer/jobId value and time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets @@ -965,7 +965,7 @@ module.controller('MlExplorerController', function ( function loadViewBySwimlaneForSelectedTime(earliestMs, latestMs) { const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit.val; + const swimlaneLimit = (limit === undefined) ? 10 : limit; // Find the top field values for the selected time, and then load the 'view by' // swimlane over the full time range for those specific field values. diff --git a/x-pack/plugins/ml/public/explorer/select_limit/index.js b/x-pack/plugins/ml/public/explorer/select_limit/index.js index 36b86095b19ab8..5733e470812df0 100644 --- a/x-pack/plugins/ml/public/explorer/select_limit/index.js +++ b/x-pack/plugins/ml/public/explorer/select_limit/index.js @@ -6,4 +6,4 @@ -import './select_limit.js'; +import './select_limit_directive.js'; diff --git a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.html b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.html deleted file mode 100644 index b1ac06eb94485f..00000000000000 --- a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js index bdb276c727b1d5..5da7daff16ef3e 100644 --- a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js +++ b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js @@ -7,65 +7,78 @@ /* -* AngularJS directive for rendering a select element with limit levels. -*/ + * React component for rendering a select element with various aggregation limits. + */ -import _ from 'lodash'; +import React, { Component } from 'react'; -import { stateFactoryProvider } from 'plugins/ml/factories/state_factory'; +import { + EuiSelect +} from '@elastic/eui'; -import template from './select_limit.html'; -import 'plugins/ml/components/controls/controls_select'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); +const OPTIONS = [ + { text: '5', value: '5' }, + { text: '10', value: '10' }, + { text: '25', value: '25' }, + { text: '50', value: '50' } +]; -module - .service('mlSelectLimitService', function (Private) { - const stateFactory = Private(stateFactoryProvider); - this.state = stateFactory('mlSelectLimit', { - limit: { display: '10', val: 10 } - }); - }) - .directive('mlSelectLimit', function (mlSelectLimitService) { - return { - restrict: 'E', - template, - link: function (scope, element) { - scope.limitOptions = [ - { display: '5', val: 5 }, - { display: '10', val: 10 }, - { display: '25', val: 25 }, - { display: '50', val: 50 } - ]; - - const limitState = mlSelectLimitService.state.get('limit'); - const limitValue = _.get(limitState, 'val', 0); - let limitOption = scope.limitOptions.find(d => d.val === limitValue); - if (limitOption === undefined) { - // Attempt to set value in URL which doesn't map to one of the options. - limitOption = scope.limitOptions.find(d => d.val === 10); - } - scope.limit = limitOption; - mlSelectLimitService.state.set('limit', scope.limit); - - scope.setLimit = function (limit) { - if (!_.isEqual(scope.limit, limit)) { - scope.limit = limit; - mlSelectLimitService.state.set('limit', scope.limit).changed(); - } - }; - - function setLimit() { - scope.setLimit(mlSelectLimitService.state.get('limit')); - } - - mlSelectLimitService.state.watch(setLimit); - - element.on('$destroy', () => { - mlSelectLimitService.state.unwatch(setLimit); - scope.$destroy(); - }); - } +function optionValueToLimit(value) { + // Builds the corresponding limit object with + // the required display and val properties + // from the specified value. + const option = OPTIONS.find(opt => (opt.value === value)); + + // Default to 10 if supplied value doesn't map to one of the options. + let limit = +OPTIONS[1].value; + if (option !== undefined) { + limit = +option.value; + } + + return limit; +} + +class SelectLimit extends Component { + constructor(props) { + super(props); + + // Restore the limit from the state, or default to 10. + this.mlSelectLimitService = this.props.mlSelectLimitService; + const limitValue = this.mlSelectLimitService.state.get('limit'); + const limit = optionValueToLimit(limitValue); + this.mlSelectLimitService.state.set('limit', limit); + + this.state = { + value: limit }; - }); + } + + onChange = (e) => { + this.setState({ + value: e.target.value, + }); + + const limit = optionValueToLimit(e.target.value); + this.mlSelectLimitService.state.set('limit', +limit).changed(); + }; + + render() { + return ( + + +
+ +
+
+ ); + } +} + +export { SelectLimit }; diff --git a/x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js b/x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js new file mode 100644 index 00000000000000..fb1f65141b75bf --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js @@ -0,0 +1,33 @@ +/* + * 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 'ngreact'; + +import { stateFactoryProvider } from 'plugins/ml/factories/state_factory'; + +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { SelectLimit } from './select_limit'; + +module.service('mlSelectLimitService', function (Private) { + const stateFactory = Private(stateFactoryProvider); + this.state = stateFactory('mlSelectLimit', { + limit: 25 + }); +}) + .directive('mlSelectLimit', function ($injector) { + const reactDirective = $injector.get('reactDirective'); + const mlSelectLimitService = $injector.get('mlSelectLimitService'); + + return reactDirective( + SelectLimit, + undefined, + { restrict: 'E' }, + { mlSelectLimitService } + ); + }); From 001acf9f6bb7ec46908962b304a8a307e3161ed1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 00:40:48 +0200 Subject: [PATCH 09/22] [ML] Fixes comment. --- x-pack/plugins/ml/public/explorer/explorer_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index f124d226e7ac88..3590995e8a56dd 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -886,8 +886,8 @@ module.controller('MlExplorerController', function ( if (resp !== undefined) { $scope.viewBySwimlaneData = processViewByResults(resp.results, fieldValues); - // do a sanity check against cellData. I can happen that a previously - // selected lane loaded via URL/AppState is not be available anymore. + // do a sanity check against cellData. It can happen that a previously + // selected lane loaded via URL/AppState is not available anymore. if ( $scope.cellData !== undefined && $scope.cellData.type === SWIMLANE_TYPE.VIEW_BY From 27ece03c789063cb5cdc3bbd827778441396d1f4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 08:03:40 +0200 Subject: [PATCH 10/22] [ML] Reuse updateExplorer() for the cell click listener. --- .../ml/public/explorer/explorer_controller.js | 88 ++++++------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 3590995e8a56dd..277227d94e8715 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -444,18 +444,8 @@ module.controller('MlExplorerController', function ( // those coming via AppState when a selection is part of the URL. const swimlaneCellClickListenerQueue = []; - // swimlaneCellClickListener could trigger multiple times with the same data. - // we track the previous click data here to be able to compare it and filter - // consecutive calls with the same data. - let previousListenerData = null; - - // Listener for click events in the swimlane and load corresponding anomaly data. - // Empty cellData is passed on clicking outside a cell with score > 0. - // The reset argument is useful when we intentionally want to reset state comparison - // of click events and want to pass through. - // For example, toggling showCharts isn't considered in the comparison - // and would therefor fail to update properly. - const swimlaneCellClickListener = function (cellData, skipComparison = false) { + // Listener for click events in the swimlane to load corresponding anomaly data. + const swimlaneCellClickListener = function (cellData) { if (skipCellClicks === true) { swimlaneCellClickListenerQueue.push(cellData); return; @@ -468,52 +458,16 @@ module.controller('MlExplorerController', function ( loadViewBySwimlane([]); } clearSelectedAnomalies(); - previousListenerData = null; } else { - $scope.appState.mlExplorerSwimlane.selectedType = cellData.type; - $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.lanes; - $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.times; - $scope.appState.save(); - - const timerange = getSelectionTimeRange(cellData); - $scope.cellData = cellData; - if (cellData.score > 0) { - const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.lanes : $scope.getSelectedJobIds(); - - const influencers = getSelectionInfluencers(cellData); + $scope.appState.mlExplorerSwimlane.selectedType = cellData.type; + $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.lanes; + $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.times; + $scope.appState.save(); - const listenerData = { - jobIds, - influencers, - start: timerange.earliestMs, - end: timerange.latestMs, - cellData - }; - if (_.isEqual(listenerData, previousListenerData) && skipComparison === false) { - return; - } - previousListenerData = listenerData; + $scope.cellData = cellData; - if (cellData.fieldName === undefined) { - // Click is in one of the cells in the Overall swimlane - reload the 'view by' swimlane - // to show the top 'view by' values for the selected time. - loadViewBySwimlaneForSelectedTime(timerange.earliestMs, timerange.latestMs); - $scope.viewByLoadedForTimeFormatted = moment(timerange.earliestMs).format('MMMM Do YYYY, HH:mm'); - } - - mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); - mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); - - if (influencers.length === 0) { - loadTopInfluencers(jobIds, timerange.earliestMs, timerange.latestMs); - loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs); - } else { - // pass influencers on to loadDataForCharts(), - // it will take care of calling loadTopInfluencers() in this case. - loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs, influencers); - } - loadAnomaliesTableData(); + updateExplorer(); } else { // Multiple cells are selected, all with a score of 0 - clear all anomalies. $scope.$evalAsync(() => { @@ -528,6 +482,7 @@ module.controller('MlExplorerController', function ( }; }); + const timerange = getSelectionTimeRange(cellData); mlExplorerDashboardService.anomalyDataChange.changed( [], timerange.earliestMs, @@ -1058,23 +1013,30 @@ module.controller('MlExplorerController', function ( function updateExplorer() { const cellData = $scope.cellData; - const timerange = getSelectionTimeRange(cellData); - - if ($scope.overallSwimlaneData !== undefined) { - mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); - } - if ($scope.viewBySwimlaneData !== undefined) { - mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); - } - mlExplorerDashboardService.anomalyDataChange.changed($scope.anomalyChartRecords, timerange.earliestMs, timerange.latestMs); const jobIds = (cellData !== undefined && cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.laneLabels : $scope.getSelectedJobIds(); + const timerange = getSelectionTimeRange(cellData); const influencers = getSelectionInfluencers(cellData); // The following is to avoid running into a race condition where loading a swimlane selection from URL/AppState // would fail because the Explorer Charts Container's directive wasn't linked yet and not being subscribed // to the anomalyDataChange listener used in loadDataForCharts(). function finish() { + if ($scope.overallSwimlaneData !== undefined) { + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.OVERALL)); + } + if ($scope.viewBySwimlaneData !== undefined) { + mlExplorerDashboardService.swimlaneDataChange.changed(mapScopeToSwimlaneProps(SWIMLANE_TYPE.VIEW_BY)); + } + mlExplorerDashboardService.anomalyDataChange.changed($scope.anomalyChartRecords || [], timerange.earliestMs, timerange.latestMs); + + if (cellData !== undefined && cellData.fieldName === undefined) { + // Click is in one of the cells in the Overall swimlane - reload the 'view by' swimlane + // to show the top 'view by' values for the selected time. + loadViewBySwimlaneForSelectedTime(timerange.earliestMs, timerange.latestMs); + $scope.viewByLoadedForTimeFormatted = moment(timerange.earliestMs).format('MMMM Do YYYY, HH:mm'); + } + if (influencers.length === 0) { loadTopInfluencers(jobIds, timerange.earliestMs, timerange.latestMs); loadDataForCharts(jobIds, timerange.earliestMs, timerange.latestMs); From 944285cfb270b456281f5f6c4bf873c1a292ec2e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 08:08:46 +0200 Subject: [PATCH 11/22] [ML] Revert if/else. --- .../ml/public/explorer/explorer_controller.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 277227d94e8715..c34bb9c6a9a986 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -881,39 +881,39 @@ module.controller('MlExplorerController', function ( ) { finish(); return; - } - - // Ensure the search bounds align to the bucketing interval used in the swimlane so - // that the first and last buckets are complete. - const bounds = timefilter.getActiveBounds(); - const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); - const selectedJobIds = $scope.getSelectedJobIds(); - const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit; - - // load scores by influencer/jobId value and time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets - // which wouldn't be the case if e.g. '1M' was used. - const interval = $scope.swimlaneBucketInterval.asSeconds() + 's'; - if ($scope.swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { - mlResultsService.getInfluencerValueMaxScoreByTime( - selectedJobIds, - $scope.swimlaneViewByFieldName, - fieldValues, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit - ).then(finish); } else { - const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; - mlResultsService.getScoresByBucket( - jobIds, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - interval, - swimlaneLimit - ).then(finish); + // Ensure the search bounds align to the bucketing interval used in the swimlane so + // that the first and last buckets are complete. + const bounds = timefilter.getActiveBounds(); + const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); + const selectedJobIds = $scope.getSelectedJobIds(); + const limit = mlSelectLimitService.state.get('limit'); + const swimlaneLimit = (limit === undefined) ? 10 : limit; + + // load scores by influencer/jobId value and time. + // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets + // which wouldn't be the case if e.g. '1M' was used. + const interval = $scope.swimlaneBucketInterval.asSeconds() + 's'; + if ($scope.swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { + mlResultsService.getInfluencerValueMaxScoreByTime( + selectedJobIds, + $scope.swimlaneViewByFieldName, + fieldValues, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + interval, + swimlaneLimit + ).then(finish); + } else { + const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; + mlResultsService.getScoresByBucket( + jobIds, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + interval, + swimlaneLimit + ).then(finish); + } } } From 69761a955badd3927239174e19c24042391d1f18 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 08:19:40 +0200 Subject: [PATCH 12/22] [ML] Simplifies the props required for the swimlanes. --- .../ml/public/explorer/explorer_controller.js | 5 ----- .../ml/public/explorer/explorer_swimlane.js | 21 ++++++++++++------- .../explorer/explorer_swimlane_directive.js | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index c34bb9c6a9a986..851d8f464be4aa 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -352,11 +352,6 @@ module.controller('MlExplorerController', function ( function mapScopeToSwimlaneProps(swimlaneType) { const swimlaneData = getSwimlaneData(swimlaneType); return { - lanes: swimlaneData.laneLabels, - startTime: swimlaneData.earliest, - endTime: swimlaneData.latest, - stepSecs: swimlaneData.interval, - points: swimlaneData.points, chartWidth: $scope.swimlaneWidth, MlTimeBuckets: TimeBuckets, swimlaneData, diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 21431d0c87fe2b..32ff75aa50c30c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -27,8 +27,12 @@ import { DRAG_SELECT_ACTION } from './explorer_constants'; export class ExplorerSwimlane extends React.Component { static propTypes = { - lanes: PropTypes.array.isRequired, - mlExplorerDashboardService: PropTypes.object.isRequired + chartWidth: PropTypes.number.isRequired, + MlTimeBuckets: PropTypes.func.isRequired, + swimlaneData: PropTypes.object.isRequired, + swimlaneType: PropTypes.string.isRequired, + mlExplorerDashboardService: PropTypes.object.isRequired, + selection: PropTypes.object } // Since this component is mostly rendered using d3 and cellMouseoverActive is only @@ -283,11 +287,6 @@ export class ExplorerSwimlane extends React.Component { const cellMouseoverActive = this.cellMouseoverActive; const { - lanes, - startTime, - endTime, - stepSecs, - points, chartWidth, MlTimeBuckets, swimlaneData, @@ -296,6 +295,14 @@ export class ExplorerSwimlane extends React.Component { selection } = this.props; + const { + laneLabels: lanes, + earliest: startTime, + latest: endTime, + interval: stepSecs, + points + } = swimlaneData; + function colorScore(value) { return getSeverityColor(value); } diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js index 8ec8b19a52ce2f..d57e829c3bdd48 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js @@ -25,8 +25,8 @@ module.directive('mlExplorerSwimlane', function (mlExplorerDashboardService) { if ( props.swimlaneType !== scope.swimlaneType || props.swimlaneData === undefined || - props.startTime === undefined || - props.endTime === undefined + props.swimlaneData.earliest === undefined || + props.swimlaneData.latest === undefined ) { return; } From 96e3b650da443b7b72a59735cca84faa07ec31bd Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 08:33:20 +0200 Subject: [PATCH 13/22] [ML] Don't pass bucket score around any longer. --- .../ml/public/explorer/explorer_controller.js | 39 ++++--------------- .../ml/public/explorer/explorer_swimlane.js | 13 +++---- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 851d8f464be4aa..db0077c662083c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -446,44 +446,21 @@ module.controller('MlExplorerController', function ( return; } + // If cellData is an empty object we clear any existing selection, + // otherwise we save the new selection in AppState and update the Explorer. if (_.keys(cellData).length === 0) { - // Swimlane deselection - clear anomalies section. if ($scope.viewByLoadedForTimeFormatted) { // Reload 'view by' swimlane over full time range. loadViewBySwimlane([]); } clearSelectedAnomalies(); } else { - if (cellData.score > 0) { - $scope.appState.mlExplorerSwimlane.selectedType = cellData.type; - $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.lanes; - $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.times; - $scope.appState.save(); - - $scope.cellData = cellData; - - updateExplorer(); - } else { - // Multiple cells are selected, all with a score of 0 - clear all anomalies. - $scope.$evalAsync(() => { - $scope.influencers = {}; - $scope.anomalyChartRecords = []; - - $scope.tableData = { - anomalies: [], - interval: mlSelectIntervalService.state.get('interval').val, - examplesByJobId: {}, - showViewSeriesLink: true - }; - }); - - const timerange = getSelectionTimeRange(cellData); - mlExplorerDashboardService.anomalyDataChange.changed( - [], - timerange.earliestMs, - timerange.latestMs - ); - } + $scope.appState.mlExplorerSwimlane.selectedType = cellData.type; + $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.lanes; + $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.times; + $scope.appState.save(); + $scope.cellData = cellData; + updateExplorer(); } }; mlExplorerDashboardService.swimlaneCellClick.watch(swimlaneCellClickListener); diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 32ff75aa50c30c..1754d2f1bbc5f6 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -100,9 +100,7 @@ export class ExplorerSwimlane extends React.Component { selectedData.laneLabels = _.uniq(selectedData.laneLabels); selectedData.times = _.uniq(selectedData.times); if (_.isEqual(selectedData, this.previousSelectedData) === false) { - const cellsToSelect = elements; - const { laneLabels, bucketScore, times } = selectedData; - this.selectCell(cellsToSelect, laneLabels, times, bucketScore, true); + this.selectCell(elements, selectedData, true); this.previousSelectedData = selectedData; } } @@ -185,7 +183,7 @@ export class ExplorerSwimlane extends React.Component { } } - selectCell(cellsToSelect, lanes, times, bucketScore, checkEqualSelection = false) { + selectCell(cellsToSelect, { laneLabels, bucketScore, times }, checkEqualSelection = false) { const { selection, mlExplorerDashboardService, @@ -212,7 +210,7 @@ export class ExplorerSwimlane extends React.Component { const newSelection = { selectedType: swimlaneType, - selectedLanes: lanes, + selectedLanes: laneLabels, selectedTimes: d3.extent(times) }; @@ -227,10 +225,9 @@ export class ExplorerSwimlane extends React.Component { const cellData = { fieldName: swimlaneData.fieldName, - lanes, + lanes: laneLabels, times: d3.extent(times), - type: swimlaneType, - score: bucketScore + type: swimlaneType }; mlExplorerDashboardService.swimlaneCellClick.changed(cellData); } From 0a6b5583dda240df1cc02c7e5ae4f777bea665fa Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 08:44:38 +0200 Subject: [PATCH 14/22] [ML] Fix selection when view by swimlanes are showing jobs. --- .../ml/public/explorer/explorer_controller.js | 5 +- .../ml/public/explorer/explorer_swimlane.js | 111 ++++++++---------- 2 files changed, 51 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index db0077c662083c..b33a9683afa17c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -350,11 +350,10 @@ module.controller('MlExplorerController', function ( } function mapScopeToSwimlaneProps(swimlaneType) { - const swimlaneData = getSwimlaneData(swimlaneType); return { chartWidth: $scope.swimlaneWidth, MlTimeBuckets: TimeBuckets, - swimlaneData, + swimlaneData: getSwimlaneData(swimlaneType), swimlaneType, mlExplorerDashboardService, selection: $scope.appState.mlExplorerSwimlane @@ -986,7 +985,7 @@ module.controller('MlExplorerController', function ( function updateExplorer() { const cellData = $scope.cellData; - const jobIds = (cellData !== undefined && cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.laneLabels : $scope.getSelectedJobIds(); + const jobIds = (cellData !== undefined && cellData.fieldName === VIEW_BY_JOB_LABEL) ? cellData.lanes : $scope.getSelectedJobIds(); const timerange = getSelectionTimeRange(cellData); const influencers = getSelectionInfluencers(cellData); diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 1754d2f1bbc5f6..beb89b3e4578fc 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -120,67 +120,6 @@ export class ExplorerSwimlane extends React.Component { } checkForSelection() { - const element = d3.select(this.rootNode.parentNode); - - const { - selection, - swimlaneData, - swimlaneType - } = this.props; - - // Check for selection and reselect the corresponding swimlane cell - // if the time range and lane label are still in view. - const selectionState = selection; - const selectedType = _.get(selectionState, 'selectedType', undefined); - const viewBy = _.get(selectionState, 'viewBy', ''); - - // If a selection was done in the other swimlane, add the "masked" classes - // to de-emphasize the swimlane cells. - if (swimlaneType !== selectedType && selectedType !== undefined) { - element.selectAll('.lane-label').classed('lane-label-masked', true); - element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); - } - - if ((swimlaneType !== selectedType) || - (swimlaneData.fieldName !== undefined && swimlaneData.fieldName !== viewBy)) { - // Not this swimlane which was selected. - return; - } - - const cellsToSelect = []; - const selectedLanes = _.get(selectionState, 'selectedLanes', []); - const selectedTimes = _.get(selectionState, 'selectedTimes', []); - const selectedTimeExtent = d3.extent(selectedTimes); - - const lanes = swimlaneData.laneLabels; - const startTime = swimlaneData.earliest; - const endTime = swimlaneData.latest; - - selectedLanes.forEach((selectedLane) => { - if (lanes.indexOf(selectedLane) > -1 && selectedTimeExtent[0] >= startTime && selectedTimeExtent[1] <= endTime) { - // Locate matching cell - look for exact time, otherwise closest before. - const swimlanes = element.select('.ml-swimlanes'); - const laneCells = swimlanes.selectAll(`div[data-lane-label="${mlEscape(selectedLane)}"]`); - - laneCells.each(function () { - const cell = d3.select(this); - const cellTime = cell.attr('data-time'); - if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { - cellsToSelect.push(cell.node()); - } - }); - } - }); - - const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { - return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); - }, 0); - - if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { - this.highlightSelection(cellsToSelect, selectedLanes, selectedTimes); - } else { - this.clearSelection(); - } } selectCell(cellsToSelect, { laneLabels, bucketScore, times }, checkEqualSelection = false) { @@ -486,7 +425,55 @@ export class ExplorerSwimlane extends React.Component { mlExplorerDashboardService.swimlaneRenderDone.changed(); - this.checkForSelection(); + // Check for selection and reselect the corresponding swimlane cell + // if the time range and lane label are still in view. + const selectionState = selection; + const selectedType = _.get(selectionState, 'selectedType', undefined); + const viewBy = _.get(selectionState, 'viewBy', ''); + + // If a selection was done in the other swimlane, add the "masked" classes + // to de-emphasize the swimlane cells. + if (swimlaneType !== selectedType && selectedType !== undefined) { + element.selectAll('.lane-label').classed('lane-label-masked', true); + element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); + } + + if ((swimlaneType !== selectedType) || + (swimlaneData.fieldName !== undefined && swimlaneData.fieldName !== viewBy)) { + // Not this swimlane which was selected. + return; + } + + const cellsToSelect = []; + const selectedLanes = _.get(selectionState, 'selectedLanes', []); + const selectedTimes = _.get(selectionState, 'selectedTimes', []); + const selectedTimeExtent = d3.extent(selectedTimes); + + selectedLanes.forEach((selectedLane) => { + if (lanes.indexOf(selectedLane) > -1 && selectedTimeExtent[0] >= startTime && selectedTimeExtent[1] <= endTime) { + // Locate matching cell - look for exact time, otherwise closest before. + const swimlaneElements = element.select('.ml-swimlanes'); + const laneCells = swimlaneElements.selectAll(`div[data-lane-label="${mlEscape(selectedLane)}"]`); + + laneCells.each(function () { + const cell = d3.select(this); + const cellTime = cell.attr('data-time'); + if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { + cellsToSelect.push(cell.node()); + } + }); + } + }); + + const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { + return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); + }, 0); + + if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { + this.highlightSelection(cellsToSelect, selectedLanes, selectedTimes); + } else { + this.clearSelection(); + } } shouldComponentUpdate() { From 284705e52d3437d26138d4497440b071e79a0364 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 21 Sep 2018 08:48:23 +0200 Subject: [PATCH 15/22] [ML] Remove unneccessary argument. --- x-pack/plugins/ml/public/explorer/explorer_swimlane.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index beb89b3e4578fc..fecb8418ebe9c4 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -100,7 +100,7 @@ export class ExplorerSwimlane extends React.Component { selectedData.laneLabels = _.uniq(selectedData.laneLabels); selectedData.times = _.uniq(selectedData.times); if (_.isEqual(selectedData, this.previousSelectedData) === false) { - this.selectCell(elements, selectedData, true); + this.selectCell(elements, selectedData); this.previousSelectedData = selectedData; } } @@ -119,10 +119,7 @@ export class ExplorerSwimlane extends React.Component { elements.map(e => d3.select(e).classed('ds-selected', false)); } - checkForSelection() { - } - - selectCell(cellsToSelect, { laneLabels, bucketScore, times }, checkEqualSelection = false) { + selectCell(cellsToSelect, { laneLabels, bucketScore, times }) { const { selection, mlExplorerDashboardService, @@ -153,7 +150,7 @@ export class ExplorerSwimlane extends React.Component { selectedTimes: d3.extent(times) }; - if ((checkEqualSelection && _.isEqual(oldSelection, newSelection))) { + if (_.isEqual(oldSelection, newSelection)) { triggerNewSelection = false; } From b7342f02c81d884a5962d0502a53d9914f120e47 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sat, 22 Sep 2018 11:29:19 +0200 Subject: [PATCH 16/22] [ML] Make sure to fetch AppState before updating and saving it. --- x-pack/plugins/ml/public/explorer/explorer_controller.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index b33a9683afa17c..6fb248ea3e3322 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -254,6 +254,7 @@ module.controller('MlExplorerController', function ( // Clear viewBy from the state if we are moving from single // to multi selection, or vice-versa. + $scope.appState.fetch(); if ((previousSelected <= 1 && $scope.selectedJobs.length > 1) || ($scope.selectedJobs.length === 1 && previousSelected > 1)) { delete $scope.appState.mlExplorerSwimlane.viewBy; @@ -289,6 +290,7 @@ module.controller('MlExplorerController', function ( $scope.swimlaneViewByFieldName = viewByFieldName; // Save the 'view by' field name to the AppState so that it can restored from the URL. + $scope.appState.fetch(); $scope.appState.mlExplorerSwimlane.viewBy = viewByFieldName; $scope.appState.save(); @@ -334,6 +336,7 @@ module.controller('MlExplorerController', function ( }); function clearSwimlaneSelectionFromAppState() { + $scope.appState.fetch(); delete $scope.appState.mlExplorerSwimlane.selectedType; delete $scope.appState.mlExplorerSwimlane.selectedLanes; delete $scope.appState.mlExplorerSwimlane.selectedTimes; @@ -454,6 +457,7 @@ module.controller('MlExplorerController', function ( } clearSelectedAnomalies(); } else { + $scope.appState.fetch(); $scope.appState.mlExplorerSwimlane.selectedType = cellData.type; $scope.appState.mlExplorerSwimlane.selectedLanes = cellData.lanes; $scope.appState.mlExplorerSwimlane.selectedTimes = cellData.times; @@ -715,6 +719,7 @@ module.controller('MlExplorerController', function ( } + $scope.appState.fetch(); $scope.appState.mlExplorerSwimlane.viewBy = $scope.swimlaneViewByFieldName; $scope.appState.save(); } From c71fcfcf584b7301cf79e9776354802a31ed189d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sat, 22 Sep 2018 11:45:09 +0200 Subject: [PATCH 17/22] [ML] Updated PropTypes and jest tests to reflect new structure and event handling. --- .../ml/public/explorer/explorer_swimlane.js | 4 ++- .../public/explorer/explorer_swimlane.test.js | 26 +++++-------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index fecb8418ebe9c4..f7d27fd5ff756e 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -29,7 +29,9 @@ export class ExplorerSwimlane extends React.Component { static propTypes = { chartWidth: PropTypes.number.isRequired, MlTimeBuckets: PropTypes.func.isRequired, - swimlaneData: PropTypes.object.isRequired, + swimlaneData: PropTypes.shape({ + laneLabels: PropTypes.array.isRequired + }).isRequired, swimlaneType: PropTypes.string.isRequired, mlExplorerDashboardService: PropTypes.object.isRequired, selection: PropTypes.object diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js index 46ecaf020badb8..537a51e1131d48 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -13,11 +13,6 @@ import React from 'react'; import { ExplorerSwimlane } from './explorer_swimlane'; function getExplorerSwimlaneMocks() { - const appState = { - mlExplorerSwimlane: {}, - save: jest.fn() - }; - const mlExplorerDashboardService = { allowCellRangeSelection: false, dragSelect: { @@ -39,10 +34,9 @@ function getExplorerSwimlaneMocks() { const MlTimeBuckets = jest.fn(() => MlTimeBucketsMethods); MlTimeBuckets.mockMethods = MlTimeBucketsMethods; - const swimlaneData = {}; + const swimlaneData = { laneLabels: [] }; return { - appState, mlExplorerDashboardService, MlTimeBuckets, swimlaneData @@ -65,24 +59,23 @@ describe('ExplorerSwimlane', () => { const mocks = getExplorerSwimlaneMocks(); const wrapper = mount(); expect(wrapper.html()).toBe( - `
` + + `
` + `
` ); // test calls to mock functions - expect(mocks.appState.save.mock.calls).toHaveLength(1); expect(mocks.mlExplorerDashboardService.swimlaneRenderDone.changed.mock.calls).toHaveLength(1); expect(mocks.mlExplorerDashboardService.dragSelect.watch.mock.calls).toHaveLength(1); expect(mocks.mlExplorerDashboardService.dragSelect.unwatch.mock.calls).toHaveLength(0); - expect(mocks.mlExplorerDashboardService.swimlaneCellClick.changed.mock.calls).toHaveLength(1); + expect(mocks.mlExplorerDashboardService.swimlaneCellClick.changed.mock.calls).toHaveLength(0); expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls).toHaveLength(1); expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); @@ -91,23 +84,16 @@ describe('ExplorerSwimlane', () => { const mocks = getExplorerSwimlaneMocks(); const wrapper = mount(); expect(wrapper.html()).toMatchSnapshot(); // test calls to mock functions - expect(mocks.appState.save.mock.calls).toHaveLength(0); expect(mocks.mlExplorerDashboardService.swimlaneRenderDone.changed.mock.calls).toHaveLength(1); expect(mocks.mlExplorerDashboardService.dragSelect.watch.mock.calls).toHaveLength(1); expect(mocks.mlExplorerDashboardService.dragSelect.unwatch.mock.calls).toHaveLength(0); From 193e71045cd2221db67ccf758a380dd971f937a1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Sep 2018 11:24:34 +0200 Subject: [PATCH 18/22] Revert "[ML] Migrates the limit dropdown to use React." This reverts commit e00fe01ddd462a7c6bf6816fccd7b5de1a84e989. --- .../ml/public/explorer/explorer_controller.js | 4 +- .../ml/public/explorer/select_limit/index.js | 2 +- .../explorer/select_limit/select_limit.html | 9 ++ .../explorer/select_limit/select_limit.js | 125 ++++++++---------- .../select_limit/select_limit_directive.js | 33 ----- 5 files changed, 68 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/ml/public/explorer/select_limit/select_limit.html delete mode 100644 x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 6fb248ea3e3322..fdac74d6d6f0a2 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -864,7 +864,7 @@ module.controller('MlExplorerController', function ( const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit; + const swimlaneLimit = (limit === undefined) ? 10 : limit.val; // load scores by influencer/jobId value and time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets @@ -896,7 +896,7 @@ module.controller('MlExplorerController', function ( function loadViewBySwimlaneForSelectedTime(earliestMs, latestMs) { const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit; + const swimlaneLimit = (limit === undefined) ? 10 : limit.val; // Find the top field values for the selected time, and then load the 'view by' // swimlane over the full time range for those specific field values. diff --git a/x-pack/plugins/ml/public/explorer/select_limit/index.js b/x-pack/plugins/ml/public/explorer/select_limit/index.js index 5733e470812df0..36b86095b19ab8 100644 --- a/x-pack/plugins/ml/public/explorer/select_limit/index.js +++ b/x-pack/plugins/ml/public/explorer/select_limit/index.js @@ -6,4 +6,4 @@ -import './select_limit_directive.js'; +import './select_limit.js'; diff --git a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.html b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.html new file mode 100644 index 00000000000000..b1ac06eb94485f --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.html @@ -0,0 +1,9 @@ + diff --git a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js index 5da7daff16ef3e..bdb276c727b1d5 100644 --- a/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js +++ b/x-pack/plugins/ml/public/explorer/select_limit/select_limit.js @@ -7,78 +7,65 @@ /* - * React component for rendering a select element with various aggregation limits. - */ - -import React, { Component } from 'react'; - -import { - EuiSelect -} from '@elastic/eui'; - - -const OPTIONS = [ - { text: '5', value: '5' }, - { text: '10', value: '10' }, - { text: '25', value: '25' }, - { text: '50', value: '50' } -]; - -function optionValueToLimit(value) { - // Builds the corresponding limit object with - // the required display and val properties - // from the specified value. - const option = OPTIONS.find(opt => (opt.value === value)); +* AngularJS directive for rendering a select element with limit levels. +*/ - // Default to 10 if supplied value doesn't map to one of the options. - let limit = +OPTIONS[1].value; - if (option !== undefined) { - limit = +option.value; - } +import _ from 'lodash'; - return limit; -} +import { stateFactoryProvider } from 'plugins/ml/factories/state_factory'; -class SelectLimit extends Component { - constructor(props) { - super(props); +import template from './select_limit.html'; +import 'plugins/ml/components/controls/controls_select'; - // Restore the limit from the state, or default to 10. - this.mlSelectLimitService = this.props.mlSelectLimitService; - const limitValue = this.mlSelectLimitService.state.get('limit'); - const limit = optionValueToLimit(limitValue); - this.mlSelectLimitService.state.set('limit', limit); +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml'); - this.state = { - value: limit - }; - } - - onChange = (e) => { - this.setState({ - value: e.target.value, +module + .service('mlSelectLimitService', function (Private) { + const stateFactory = Private(stateFactoryProvider); + this.state = stateFactory('mlSelectLimit', { + limit: { display: '10', val: 10 } }); - - const limit = optionValueToLimit(e.target.value); - this.mlSelectLimitService.state.set('limit', +limit).changed(); - }; - - render() { - return ( - - -
- -
-
- ); - } -} - -export { SelectLimit }; + }) + .directive('mlSelectLimit', function (mlSelectLimitService) { + return { + restrict: 'E', + template, + link: function (scope, element) { + scope.limitOptions = [ + { display: '5', val: 5 }, + { display: '10', val: 10 }, + { display: '25', val: 25 }, + { display: '50', val: 50 } + ]; + + const limitState = mlSelectLimitService.state.get('limit'); + const limitValue = _.get(limitState, 'val', 0); + let limitOption = scope.limitOptions.find(d => d.val === limitValue); + if (limitOption === undefined) { + // Attempt to set value in URL which doesn't map to one of the options. + limitOption = scope.limitOptions.find(d => d.val === 10); + } + scope.limit = limitOption; + mlSelectLimitService.state.set('limit', scope.limit); + + scope.setLimit = function (limit) { + if (!_.isEqual(scope.limit, limit)) { + scope.limit = limit; + mlSelectLimitService.state.set('limit', scope.limit).changed(); + } + }; + + function setLimit() { + scope.setLimit(mlSelectLimitService.state.get('limit')); + } + + mlSelectLimitService.state.watch(setLimit); + + element.on('$destroy', () => { + mlSelectLimitService.state.unwatch(setLimit); + scope.$destroy(); + }); + } + }; + }); diff --git a/x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js b/x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js deleted file mode 100644 index fb1f65141b75bf..00000000000000 --- a/x-pack/plugins/ml/public/explorer/select_limit/select_limit_directive.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 'ngreact'; - -import { stateFactoryProvider } from 'plugins/ml/factories/state_factory'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { SelectLimit } from './select_limit'; - -module.service('mlSelectLimitService', function (Private) { - const stateFactory = Private(stateFactoryProvider); - this.state = stateFactory('mlSelectLimit', { - limit: 25 - }); -}) - .directive('mlSelectLimit', function ($injector) { - const reactDirective = $injector.get('reactDirective'); - const mlSelectLimitService = $injector.get('mlSelectLimitService'); - - return reactDirective( - SelectLimit, - undefined, - { restrict: 'E' }, - { mlSelectLimitService } - ); - }); From 3e0b88ac6702882aa10dcb47758fcaeeae3f2628 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Sep 2018 11:33:52 +0200 Subject: [PATCH 19/22] [ML] Use a constant for the limit's default value. --- .../ml/public/explorer/explorer_constants.js | 7 +++++++ .../ml/public/explorer/explorer_controller.js | 14 +++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_constants.js b/x-pack/plugins/ml/public/explorer/explorer_constants.js index cd5dee537a0f61..7d69bc563b3f4f 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_constants.js +++ b/x-pack/plugins/ml/public/explorer/explorer_constants.js @@ -13,3 +13,10 @@ export const DRAG_SELECT_ACTION = { ELEMENT_SELECT: 'elementSelect', DRAG_START: 'dragStart' }; + +export const SWIMLANE_DEFAULT_LIMIT = 10; + +export const SWIMLANE_TYPE = { + OVERALL: 'overall', + VIEW_BY: 'viewBy' +}; diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index fdac74d6d6f0a2..33c0fbcd66bd18 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -40,7 +40,11 @@ import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; import { JobSelectServiceProvider } from 'plugins/ml/components/job_select_list/job_select_service'; import { isTimeSeriesViewDetector } from 'plugins/ml/../common/util/job_utils'; import { timefilter } from 'ui/timefilter'; -import { DRAG_SELECT_ACTION } from './explorer_constants'; +import { + DRAG_SELECT_ACTION, + SWIMLANE_DEFAULT_LIMIT, + SWIMLANE_TYPE +} from './explorer_constants'; uiRoutes .when('/explorer/?', { @@ -65,10 +69,6 @@ function getDefaultViewBySwimlaneData() { }; } -const SWIMLANE_TYPE = { - OVERALL: 'overall', - VIEW_BY: 'viewBy' -}; module.controller('MlExplorerController', function ( $scope, @@ -864,7 +864,7 @@ module.controller('MlExplorerController', function ( const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit.val; + const swimlaneLimit = (limit === undefined) ? SWIMLANE_DEFAULT_LIMIT : limit.val; // load scores by influencer/jobId value and time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets @@ -896,7 +896,7 @@ module.controller('MlExplorerController', function ( function loadViewBySwimlaneForSelectedTime(earliestMs, latestMs) { const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); - const swimlaneLimit = (limit === undefined) ? 10 : limit.val; + const swimlaneLimit = (limit === undefined) ? SWIMLANE_DEFAULT_LIMIT : limit.val; // Find the top field values for the selected time, and then load the 'view by' // swimlane over the full time range for those specific field values. From 5d883e31a200cc61044c2be0c4b181ed63dad94b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Sep 2018 14:24:38 +0200 Subject: [PATCH 20/22] [ML] Use .some() instead of .forEach() to avoid unnecesary loops for the check. --- x-pack/plugins/ml/public/explorer/explorer_controller.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 33c0fbcd66bd18..bbce82047bebf0 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -823,11 +823,8 @@ module.controller('MlExplorerController', function ( $scope.cellData !== undefined && $scope.cellData.type === SWIMLANE_TYPE.VIEW_BY ) { - let selectionExists = false; - $scope.cellData.lanes.forEach((lane) => { - if ($scope.viewBySwimlaneData.laneLabels.indexOf(lane) > -1) { - selectionExists = true; - } + const selectionExists = $scope.cellData.lanes.some((lane) => { + return ($scope.viewBySwimlaneData.laneLabels.includes(lane)); }); if (selectionExists === false) { clearSelectedAnomalies(); From dab438e516791701d2dfcf06ae6551ff2bf9536e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Sep 2018 14:28:43 +0200 Subject: [PATCH 21/22] [ML] Move the chartWidth of 800 to constant 'mockChartWidth'. --- x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js index 537a51e1131d48..d0e93f256beb30 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -43,6 +43,8 @@ function getExplorerSwimlaneMocks() { }; } +const mockChartWidth = 800; + describe('ExplorerSwimlane', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; @@ -59,7 +61,7 @@ describe('ExplorerSwimlane', () => { const mocks = getExplorerSwimlaneMocks(); const wrapper = mount( { const mocks = getExplorerSwimlaneMocks(); const wrapper = mount( Date: Mon, 24 Sep 2018 14:53:13 +0200 Subject: [PATCH 22/22] [ML] Review feedback: Changed bucketScore check and tweaked swimlane jest test. --- x-pack/plugins/ml/public/explorer/explorer_swimlane.js | 5 +++-- x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index f7d27fd5ff756e..baaa1cf94f7870 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -268,8 +268,9 @@ export class ExplorerSwimlane extends React.Component { // which is where d3 supplies a reference to the corresponding DOM element. return function (lane) { const bucketScore = getBucketScore(lane, time); - if (bucketScore === 0) { return; } - cellMouseover(this, lane, bucketScore, i, time); + if (bucketScore !== 0) { + cellMouseover(this, lane, bucketScore, i, time); + } }; } diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js index d0e93f256beb30..278ac487bfc2e5 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -69,7 +69,7 @@ describe('ExplorerSwimlane', () => { />); expect(wrapper.html()).toBe( - `
` + + `
` + `
` );