From e1f3e7e05a2e0ca2a270a911380efea4ca4ed27b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 11:27:45 +0200 Subject: [PATCH 01/15] [ML] First Anomaly Explorer Swimlane jest tests. --- .../__mocks__/mock_overall_swimlane.json | 160 ++++++++++++++++++ .../explorer_swimlane.test.js.snap | 3 + .../ml/public/explorer/explorer_swimlane.js | 4 + .../public/explorer/explorer_swimlane.test.js | 73 ++++++++ 4 files changed, 240 insertions(+) create mode 100644 x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json create mode 100644 x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap create mode 100644 x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js diff --git a/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json b/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json new file mode 100644 index 0000000000000..e50329e571c1c --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json @@ -0,0 +1,160 @@ +{ + "laneLabels": [ + "Overall" + ], + "points": [ + { + "laneLabel": "Overall", + "time": 1486425600, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486440000, + "value": 0.01107053 + }, + { + "laneLabel": "Overall", + "time": 1486454400, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486468800, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486483200, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486497600, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486512000, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486526400, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486540800, + "value": 0.02083592 + }, + { + "laneLabel": "Overall", + "time": 1486555200, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486569600, + "value": 0.1870243 + }, + { + "laneLabel": "Overall", + "time": 1486584000, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486598400, + "value": 0.5947769 + }, + { + "laneLabel": "Overall", + "time": 1486612800, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486627200, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486641600, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486656000, + "value": 90.69091 + }, + { + "laneLabel": "Overall", + "time": 1486670400, + "value": 0.015526682078025005 + }, + { + "laneLabel": "Overall", + "time": 1486684800, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486699200, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486713600, + "value": 0.13163353719117646 + }, + { + "laneLabel": "Overall", + "time": 1486728000, + "value": 0.009563844292490468 + }, + { + "laneLabel": "Overall", + "time": 1486742400, + "value": 0.06536897512646485 + }, + { + "laneLabel": "Overall", + "time": 1486756800, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486771200, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486785600, + "value": 0 + }, + { + "laneLabel": "Overall", + "time": 1486800000, + "value": 0.015148845839315169 + }, + { + "laneLabel": "Overall", + "time": 1486814400, + "value": 0.016276979403135612 + }, + { + "laneLabel": "Overall", + "time": 1486828800, + "value": 0.007951060758653105 + }, + { + "laneLabel": "Overall", + "time": 1486843200, + "value": 0.00750917782906428 + } + ], + "interval": 14400, + "earliest": 1486425600, + "latest": 1486857600 +} diff --git a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap new file mode 100644 index 0000000000000..19db487c38320 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
14400
1486425600
1486857600
"`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index a04e4d0b9276e..59b381da51604 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -10,6 +10,7 @@ * React component for rendering Explorer dashboard swimlanes. */ +import PropTypes from 'prop-types'; import React from 'react'; import _ from 'lodash'; @@ -26,6 +27,9 @@ import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip export class ExplorerSwimlane extends React.Component { static propTypes = { + appState: PropTypes.object.isRequired, + lanes: PropTypes.object.isRequired, + mlExplorerDashboardService: PropTypes.object.isRequired } constructor(props) { diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js new file mode 100644 index 0000000000000..d535c693eec59 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -0,0 +1,73 @@ +/* + * 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 mockOverallSwimlane from './__mocks__/mock_overall_swimlane.json'; + +import { mount } from 'enzyme'; +import React from 'react'; + +import { ExplorerSwimlane } from './explorer_swimlane'; + +describe('ExplorerSwimlane', () => { + const mockAppState = { + mlExplorerSwimlane: {}, + save: jest.fn() + }; + + const mockMlExplorerDashboardService = { + allowCellRangeSelection: false, + dragSelect: { + watch: jest.fn(), + unwatch: jest.fn() + }, + swimlaneCellClick: { + changed: jest.fn() + }, + swimlaneRenderDone: { + changed: jest.fn() + } + }; + + const mockMlTimeBuckets = jest.fn(() => ({ + setInterval: jest.fn(), + getScaledDateFormat: jest.fn() + })); + + const mockSwimlaneData = {}; + + const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; + const originalGetBBox = SVGElement.prototype.getBBox; + beforeEach(() => SVGElement.prototype.getBBox = () => mockedGetBBox); + afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); + + test('Minimal initialization', () => { + const wrapper = mount(); + + expect(wrapper.html()).toBe( + `
` + + `
` + ); + }); + + test('Overall swimlane', () => { + const wrapper = mount(); + + expect(wrapper.html()).toMatchSnapshot(); + }); + +}); From ce530a043e3b777866628b8d05c206178a3fdc18 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 12:00:04 +0200 Subject: [PATCH 02/15] [ML] Reduced size of data mock file. --- .../__mocks__/mock_overall_swimlane.json | 131 +----------------- .../explorer_swimlane.test.js.snap | 2 +- .../explorer_charts_container_service.js | 4 +- .../public/explorer/explorer_swimlane.test.js | 2 +- 4 files changed, 7 insertions(+), 132 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json b/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json index e50329e571c1c..16af0fdd36e07 100644 --- a/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json +++ b/x-pack/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json @@ -16,145 +16,20 @@ { "laneLabel": "Overall", "time": 1486454400, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486468800, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486483200, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486497600, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486512000, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486526400, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486540800, - "value": 0.02083592 - }, - { - "laneLabel": "Overall", - "time": 1486555200, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486569600, "value": 0.1870243 }, { "laneLabel": "Overall", - "time": 1486584000, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486598400, + "time": 1486468800, "value": 0.5947769 }, { "laneLabel": "Overall", - "time": 1486612800, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486627200, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486641600, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486656000, - "value": 90.69091 - }, - { - "laneLabel": "Overall", - "time": 1486670400, - "value": 0.015526682078025005 - }, - { - "laneLabel": "Overall", - "time": 1486684800, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486699200, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486713600, - "value": 0.13163353719117646 - }, - { - "laneLabel": "Overall", - "time": 1486728000, - "value": 0.009563844292490468 - }, - { - "laneLabel": "Overall", - "time": 1486742400, - "value": 0.06536897512646485 - }, - { - "laneLabel": "Overall", - "time": 1486756800, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486771200, - "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486785600, + "time": 1486483200, "value": 0 - }, - { - "laneLabel": "Overall", - "time": 1486800000, - "value": 0.015148845839315169 - }, - { - "laneLabel": "Overall", - "time": 1486814400, - "value": 0.016276979403135612 - }, - { - "laneLabel": "Overall", - "time": 1486828800, - "value": 0.007951060758653105 - }, - { - "laneLabel": "Overall", - "time": 1486843200, - "value": 0.00750917782906428 } ], "interval": 14400, "earliest": 1486425600, - "latest": 1486857600 + "latest": 1486483200 } diff --git a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap index 19db487c38320..83b1fd3e1c48a 100644 --- a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
14400
1486425600
1486857600
"`; +exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
[object Object],[object Object],[object Object],[object Object],[object Object]
14400
1486425600
1486483200
"`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index b114cd93fffb3..26aa4abc302bb 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -7,9 +7,9 @@ /* - * Angular controller for the container for the anomaly charts in the + * Service for the container for the anomaly charts in the * Machine Learning Explorer dashboard. - * The controller processes the data required to draw each of the charts + * The service processes the data required to draw each of the charts * and manages the layout of the charts in the containing div. */ 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 d535c693eec59..629eb6c18ea58 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -46,7 +46,7 @@ describe('ExplorerSwimlane', () => { test('Minimal initialization', () => { const wrapper = mount( Date: Fri, 7 Sep 2018 12:24:23 +0200 Subject: [PATCH 03/15] [ML] Refactor ExplorerSwimlane to avoid that=this. --- .../ml/public/explorer/explorer_swimlane.js | 341 +++++++++--------- 1 file changed, 177 insertions(+), 164 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 59b381da51604..a48ce6e3223aa 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -41,203 +41,216 @@ export class ExplorerSwimlane extends React.Component { componentWillUnmount() { const { mlExplorerDashboardService } = this.props; - mlExplorerDashboardService.dragSelect.unwatch(this.dragSelectListener); + mlExplorerDashboardService.dragSelect.unwatch(this.dragSelectListener.bind(this)); } componentDidMount() { const element = $(this.rootNode).parent(); - - const { - appState, - mlExplorerDashboardService, - swimlaneData, - swimlaneType - } = this.props; + const { mlExplorerDashboardService } = this.props; // Consider the setting to support to select a range of cells if (!mlExplorerDashboardService.allowCellRangeSelection) { element.addClass('ml-hide-range-selection'); } - // Listen for dragSelect events - const that = this; - this.dragSelectListener = function ({ action, elements = [] }) { - if (action === 'newSelection' && elements.length > 0) { - const firstCellData = $(elements[0]).data('click'); - if (typeof firstCellData !== 'undefined' && swimlaneType === firstCellData.swimlaneType) { - const selectedData = elements.reduce((d, e) => { - const cellData = $(e).data('click'); - d.bucketScore = Math.max(d.bucketScore, cellData.bucketScore); - d.laneLabels.push(cellData.laneLabel); - d.times.push(cellData.time); - return d; - }, { - bucketScore: 0, - laneLabels: [], - times: [] - }); + mlExplorerDashboardService.dragSelect.watch(this.dragSelectListener.bind(this)); - selectedData.laneLabels = _.uniq(selectedData.laneLabels); - selectedData.times = _.uniq(selectedData.times); - cellClick(elements, selectedData); - } + this.renderSwimlane(); + } - that.setState({ cellMouseoverActive: true }); - } else if (action === 'elementSelect') { - element.addClass('ml-dragselect-dragging'); - return; - } else if (action === 'dragStart') { - that.setState({ cellMouseoverActive: false }); - return; - } - element.removeClass('ml-dragselect-dragging'); - elements.map(e => $(e).removeClass('ds-selected')); - }; + componentDidUpdate() { + this.renderSwimlane(); + } - mlExplorerDashboardService.dragSelect.watch(this.dragSelectListener); + // Listen for dragSelect events + dragSelectListener({ action, elements = [] }) { + const element = $(this.rootNode).parent(); + const { swimlaneType } = this.props; + + if (action === 'newSelection' && elements.length > 0) { + const firstCellData = $(elements[0]).data('click'); + if (typeof firstCellData !== 'undefined' && swimlaneType === firstCellData.swimlaneType) { + const selectedData = elements.reduce((d, e) => { + const cellData = $(e).data('click'); + d.bucketScore = Math.max(d.bucketScore, cellData.bucketScore); + d.laneLabels.push(cellData.laneLabel); + d.times.push(cellData.time); + return d; + }, { + bucketScore: 0, + laneLabels: [], + times: [] + }); - function cellClick(cellsToSelect, { laneLabels, bucketScore, times }) { - if (cellsToSelect.length > 1 || bucketScore > 0) { - selectCell(cellsToSelect, laneLabels, times, bucketScore, true); - } else { - that.clearSelection(); + selectedData.laneLabels = _.uniq(selectedData.laneLabels); + selectedData.times = _.uniq(selectedData.times); + this.cellClick(elements, selectedData); } + + this.setState({ cellMouseoverActive: true }); + } else if (action === 'elementSelect') { + element.addClass('ml-dragselect-dragging'); + return; + } else if (action === 'dragStart') { + this.setState({ cellMouseoverActive: false }); + return; } - this.checkForSelection = function () { - // Check for selection in the AppState and reselect the corresponding swimlane cell - // if the time range and lane label are still in view. - const selectionState = appState.mlExplorerSwimlane; - const selectedType = _.get(selectionState, 'selectedType', undefined); - const viewBy = _.get(selectionState, 'viewBy', ''); - if (swimlaneType !== selectedType && selectedType !== undefined) { - $('.lane-label', element).addClass('lane-label-masked'); - $('.sl-cell-inner', element).addClass('sl-cell-inner-masked'); - } + element.removeClass('ml-dragselect-dragging'); + elements.map(e => $(e).removeClass('ds-selected')); + } - if ((swimlaneType !== selectedType) || - (swimlaneData.fieldName !== undefined && swimlaneData.fieldName !== viewBy)) { - // Not this swimlane which was selected. - return; - } + cellClick(cellsToSelect, { laneLabels, bucketScore, times }) { + if (cellsToSelect.length > 1 || bucketScore > 0) { + this.selectCell(cellsToSelect, laneLabels, times, bucketScore, true); + } else { + this.clearSelection(); + } + } - 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.find('.ml-swimlanes').first(); - const laneCells = $('div[data-lane-label="' + mlEscape(selectedLane) + '"]', $swimlanes); - if (laneCells.length === 0) { - return; - } + checkForSelection() { + const element = $(this.rootNode).parent(); - for (let i = 0; i < laneCells.length; i++) { - const cell = laneCells[i]; - const cellTime = $(cell).attr('data-time'); - if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { - cellsToSelect.push(cell); - } - } - } - }); - const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { - return Math.max(maxBucketScore, +$(cell).attr('data-score') || 0); - }, 0); - if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { - selectCell(cellsToSelect, selectedLanes, selectedTimes, selectedMaxBucketScore); - } else { - // Clear selection from state as previous selection is no longer applicable. - that.clearSelection(); - } - }; + const { + appState, + swimlaneData, + swimlaneType + } = this.props; - function selectCell(cellsToSelect, laneLabels, times, bucketScore, checkEqualSelection = false) { - $('.lane-label', '.ml-explorer-swimlane').addClass('lane-label-masked'); - $('.sl-cell-inner,.sl-cell-inner-dragselect', '.ml-explorer-swimlane').addClass('sl-cell-inner-masked'); - $('.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected', - '.ml-explorer-swimlane').removeClass('sl-cell-inner-selected'); - - $(cellsToSelect).find('.sl-cell-inner,.sl-cell-inner-dragselect') - .removeClass('sl-cell-inner-masked') - .addClass('sl-cell-inner-selected'); - - $('.lane-label').filter(function () { - return laneLabels.indexOf($(this).text()) > -1; - }).removeClass('lane-label-masked'); - - if (swimlaneType === 'viewBy') { - // If selecting a cell in the 'view by' swimlane, indicate the corresponding time in the Overall swimlane. - const overallSwimlane = $('ml-explorer-swimlane[swimlane-type="overall"]'); - times.forEach(time => { - const overallCell = $('div[data-time="' + time + '"]', overallSwimlane).find('.sl-cell-inner,.sl-cell-inner-dragselect'); - overallCell.addClass('sl-cell-inner-selected'); - }); - } + // Check for selection in the AppState and reselect the corresponding swimlane cell + // if the time range and lane label are still in view. + const selectionState = appState.mlExplorerSwimlane; + const selectedType = _.get(selectionState, 'selectedType', undefined); + const viewBy = _.get(selectionState, 'viewBy', ''); + if (swimlaneType !== selectedType && selectedType !== undefined) { + $('.lane-label', element).addClass('lane-label-masked'); + $('.sl-cell-inner', element).addClass('sl-cell-inner-masked'); + } - // 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 "scope.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 + 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.find('.ml-swimlanes').first(); + const laneCells = $('div[data-lane-label="' + mlEscape(selectedLane) + '"]', $swimlanes); + if (laneCells.length === 0) { + return; + } + + for (let i = 0; i < laneCells.length; i++) { + const cell = laneCells[i]; + const cellTime = $(cell).attr('data-time'); + if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { + cellsToSelect.push(cell); + } } - )) { - that.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 - }); } + }); + const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { + return Math.max(maxBucketScore, +$(cell).attr('data-score') || 0); + }, 0); + if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { + this.selectCell(cellsToSelect, selectedLanes, selectedTimes, selectedMaxBucketScore); + } else { + // Clear selection from state as previous selection is no longer applicable. + this.clearSelection(); } + } - this.clearSelection = function () { - $('.lane-label', '.ml-explorer-swimlane').removeClass('lane-label-masked'); - $('.sl-cell-inner', '.ml-explorer-swimlane').removeClass('sl-cell-inner-masked'); - $('.sl-cell-inner.sl-cell-inner-selected', '.ml-explorer-swimlane').removeClass('sl-cell-inner-selected'); - $('.sl-cell-inner-dragselect.sl-cell-inner-selected', '.ml-explorer-swimlane').removeClass('sl-cell-inner-selected'); - $('.ds-selected', '.ml-explorer-swimlane').removeClass('ds-selected'); + selectCell(cellsToSelect, laneLabels, times, bucketScore, checkEqualSelection = false) { + const { + appState, + mlExplorerDashboardService, + swimlaneData, + swimlaneType + } = this.props; - delete appState.mlExplorerSwimlane.selectedType; - delete appState.mlExplorerSwimlane.selectedLanes; - delete appState.mlExplorerSwimlane.selectedTimes; - appState.save(); + $('.lane-label', '.ml-explorer-swimlane').addClass('lane-label-masked'); + $('.sl-cell-inner,.sl-cell-inner-dragselect', '.ml-explorer-swimlane').addClass('sl-cell-inner-masked'); + $('.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected', + '.ml-explorer-swimlane').removeClass('sl-cell-inner-selected'); + + $(cellsToSelect).find('.sl-cell-inner,.sl-cell-inner-dragselect') + .removeClass('sl-cell-inner-masked') + .addClass('sl-cell-inner-selected'); + + $('.lane-label').filter(function () { + return laneLabels.indexOf($(this).text()) > -1; + }).removeClass('lane-label-masked'); + + if (swimlaneType === 'viewBy') { + // If selecting a cell in the 'view by' swimlane, indicate the corresponding time in the Overall swimlane. + const overallSwimlane = $('ml-explorer-swimlane[swimlane-type="overall"]'); + times.forEach(time => { + const overallCell = $('div[data-time="' + time + '"]', overallSwimlane).find('.sl-cell-inner,.sl-cell-inner-dragselect'); + overallCell.addClass('sl-cell-inner-selected'); + }); + } - mlExplorerDashboardService.swimlaneCellClick.changed({}); - }; + // 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(); - this.renderSwimlane(); + mlExplorerDashboardService.swimlaneCellClick.changed({ + fieldName: swimlaneData.fieldName, + laneLabels, + time: d3.extent(times), + interval: swimlaneData.interval, + score: bucketScore + }); + } } - componentDidUpdate() { - this.renderSwimlane(); + clearSelection() { + const { appState, mlExplorerDashboardService } = this.props; + $('.lane-label', '.ml-explorer-swimlane').removeClass('lane-label-masked'); + $('.sl-cell-inner', '.ml-explorer-swimlane').removeClass('sl-cell-inner-masked'); + $('.sl-cell-inner.sl-cell-inner-selected', '.ml-explorer-swimlane').removeClass('sl-cell-inner-selected'); + $('.sl-cell-inner-dragselect.sl-cell-inner-selected', '.ml-explorer-swimlane').removeClass('sl-cell-inner-selected'); + $('.ds-selected', '.ml-explorer-swimlane').removeClass('ds-selected'); + + delete appState.mlExplorerSwimlane.selectedType; + delete appState.mlExplorerSwimlane.selectedLanes; + delete appState.mlExplorerSwimlane.selectedTimes; + appState.save(); + + mlExplorerDashboardService.swimlaneCellClick.changed({}); } renderSwimlane() { From 3e4eb71f4237a0b8e9bf4aa2e7514af41d4d09e6 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 13:49:34 +0200 Subject: [PATCH 04/15] [ML] Use constants for drag select actions. --- .../ml/public/explorer/explorer_constants.js | 15 +++++++++++++++ .../ml/public/explorer/explorer_controller.js | 7 ++++--- .../ml/public/explorer/explorer_swimlane.js | 7 ++++--- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/ml/public/explorer/explorer_constants.js diff --git a/x-pack/plugins/ml/public/explorer/explorer_constants.js b/x-pack/plugins/ml/public/explorer/explorer_constants.js new file mode 100644 index 0000000000000..cd5dee537a0f6 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_constants.js @@ -0,0 +1,15 @@ +/* + * 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. + */ + +/* + * Contains values for ML anomaly explorer. + */ + +export const DRAG_SELECT_ACTION = { + NEW_SELECTION: 'newSelection', + ELEMENT_SELECT: 'elementSelect', + DRAG_START: 'dragStart' +}; diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 0f5c59d70620e..b62c98cd8414c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -40,6 +40,7 @@ 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'; uiRoutes .when('/explorer/?', { @@ -95,7 +96,7 @@ module.controller('MlExplorerController', function ( if (elements.length > 0) { mlExplorerDashboardService.dragSelect.changed({ - action: 'newSelection', + action: DRAG_SELECT_ACTION.NEW_SELECTION, elements }); } @@ -105,7 +106,7 @@ module.controller('MlExplorerController', function ( onDragStart() { if (ALLOW_CELL_RANGE_SELECTION) { mlExplorerDashboardService.dragSelect.changed({ - action: 'dragStart' + action: DRAG_SELECT_ACTION.DRAG_START }); disableDragSelectOnMouseLeave = false; } @@ -113,7 +114,7 @@ module.controller('MlExplorerController', function ( onElementSelect() { if (ALLOW_CELL_RANGE_SELECTION) { mlExplorerDashboardService.dragSelect.changed({ - action: 'elementSelect' + action: DRAG_SELECT_ACTION.ELEMENT_SELECT }); } } diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index a48ce6e3223aa..e70ef6b889b70 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -24,6 +24,7 @@ import { numTicksForDateFormat } from '../util/chart_utils'; import { getSeverityColor } from '../../common/util/anomaly_utils'; import { mlEscape } from '../util/string_utils'; import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service'; +import { DRAG_SELECT_ACTION } from './explorer_constants'; export class ExplorerSwimlane extends React.Component { static propTypes = { @@ -68,7 +69,7 @@ export class ExplorerSwimlane extends React.Component { const element = $(this.rootNode).parent(); const { swimlaneType } = this.props; - if (action === 'newSelection' && elements.length > 0) { + if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) { const firstCellData = $(elements[0]).data('click'); if (typeof firstCellData !== 'undefined' && swimlaneType === firstCellData.swimlaneType) { const selectedData = elements.reduce((d, e) => { @@ -89,10 +90,10 @@ export class ExplorerSwimlane extends React.Component { } this.setState({ cellMouseoverActive: true }); - } else if (action === 'elementSelect') { + } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { element.addClass('ml-dragselect-dragging'); return; - } else if (action === 'dragStart') { + } else if (action === DRAG_SELECT_ACTION.DRAG_START) { this.setState({ cellMouseoverActive: false }); return; } From b67bae8d9769521a7d9d72a6de3e5f83079b4fb9 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 14:22:55 +0200 Subject: [PATCH 05/15] [ML] Added expects to check mocked functions calls. --- .../public/explorer/explorer_swimlane.test.js | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 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 629eb6c18ea58..66da5c7e89ef7 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -11,13 +11,13 @@ import React from 'react'; import { ExplorerSwimlane } from './explorer_swimlane'; -describe('ExplorerSwimlane', () => { - const mockAppState = { +function getExplorerSwimlaneMocks() { + const appState = { mlExplorerSwimlane: {}, save: jest.fn() }; - const mockMlExplorerDashboardService = { + const mlExplorerDashboardService = { allowCellRangeSelection: false, dragSelect: { watch: jest.fn(), @@ -31,43 +31,76 @@ describe('ExplorerSwimlane', () => { } }; - const mockMlTimeBuckets = jest.fn(() => ({ + const MlTimeBucketsMethods = { setInterval: jest.fn(), getScaledDateFormat: jest.fn() - })); + }; + const MlTimeBuckets = jest.fn(() => MlTimeBucketsMethods); + MlTimeBuckets.mockMethods = MlTimeBucketsMethods; - const mockSwimlaneData = {}; + const swimlaneData = {}; + return { + appState, + mlExplorerDashboardService, + MlTimeBuckets, + swimlaneData + }; +} + +describe('ExplorerSwimlane', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; beforeEach(() => SVGElement.prototype.getBBox = () => mockedGetBBox); afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); test('Minimal initialization', () => { + 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.MlTimeBuckets.mockMethods.setInterval.mock.calls).toHaveLength(1); + expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); test('Overall swimlane', () => { + const mocks = getExplorerSwimlaneMocks(); + const wrapper = mount(); expect(wrapper.html()).toMatchSnapshot(); + + // 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.MlTimeBuckets.mockMethods.setInterval.mock.calls).toHaveLength(1); + expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); }); From 9dd47c02dd6d7a385f5bf5386c5e551c49857f2c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 14:45:39 +0200 Subject: [PATCH 06/15] [ML] Updated overall swimlane test. --- .../explorer_swimlane.test.js.snap | 2 +- .../ml/public/explorer/explorer_swimlane.js | 2 +- .../public/explorer/explorer_swimlane.test.js | 23 +++++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap index 83b1fd3e1c48a..bdddfe1f8c446 100644 --- a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
[object Object],[object Object],[object Object],[object Object],[object Object]
14400
1486425600
1486483200
"`; +exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
2017-02-07T01:00:00+01:002017-02-07T01:30:00+01:002017-02-07T02:00:00+01:002017-02-07T02:30:00+01:002017-02-07T03:00:00+01:002017-02-07T03:30:00+01:002017-02-07T04:00:00+01:002017-02-07T04:30:00+01:002017-02-07T05:00:00+01:002017-02-07T05:30:00+01:002017-02-07T06:00:00+01:002017-02-07T06:30:00+01:002017-02-07T07:00:00+01:002017-02-07T07:30:00+01:002017-02-07T08:00:00+01:002017-02-07T08:30:00+01:002017-02-07T09:00:00+01:002017-02-07T09:30:00+01:002017-02-07T10:00:00+01:002017-02-07T10:30:00+01:002017-02-07T11:00:00+01:002017-02-07T11:30:00+01:002017-02-07T12:00:00+01:002017-02-07T12:30:00+01:002017-02-07T13:00:00+01:002017-02-07T13:30:00+01:002017-02-07T14:00:00+01:002017-02-07T14:30:00+01:002017-02-07T15:00:00+01:002017-02-07T15:30:00+01:002017-02-07T16:00:00+01:002017-02-07T16:30:00+01:002017-02-07T17:00:00+01:00
"`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index e70ef6b889b70..46a9fc8abdb72 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -29,7 +29,7 @@ import { DRAG_SELECT_ACTION } from './explorer_constants'; export class ExplorerSwimlane extends React.Component { static propTypes = { appState: PropTypes.object.isRequired, - lanes: PropTypes.object.isRequired, + lanes: PropTypes.array.isRequired, mlExplorerDashboardService: PropTypes.object.isRequired } 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 66da5c7e89ef7..a30f03d3e232a 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import mockOverallSwimlane from './__mocks__/mock_overall_swimlane.json'; +import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; import { mount } from 'enzyme'; import React from 'react'; @@ -59,7 +59,7 @@ describe('ExplorerSwimlane', () => { const wrapper = mount( { const mocks = getExplorerSwimlaneMocks(); const wrapper = mount(); expect(wrapper.html()).toMatchSnapshot(); // test calls to mock functions - expect(mocks.appState.save.mock.calls).toHaveLength(1); + 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); - 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); }); - }); From 122684247b54ce62174c78ca3a465de3836ff507 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 15:38:07 +0200 Subject: [PATCH 07/15] [ML] Fixes a possible race condition with swimlaneCellClickListener. --- .../ml/public/explorer/explorer_controller.js | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index b62c98cd8414c..2a06973ee2501 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -56,6 +56,15 @@ uiRoutes import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); +function getDefaultViewBySwimlaneData() { + return { + 'fieldName': '', + 'laneLabels': [], + 'points': [], + 'interval': 3600 + }; +} + module.controller('MlExplorerController', function ( $scope, $timeout, @@ -128,8 +137,7 @@ module.controller('MlExplorerController', function ( }; $scope.viewBySwimlaneOptions = []; - $scope.viewBySwimlaneData = { 'fieldName': '', 'laneLabels': [], - 'points': [], 'interval': 3600 }; + $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); $scope.initializeVis = function () { // Initialize the AppState in which to store filters. @@ -343,6 +351,10 @@ module.controller('MlExplorerController', function ( return influencers; } + // 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. const swimlaneCellClickListener = function (cellData) { @@ -353,11 +365,22 @@ module.controller('MlExplorerController', function ( loadViewBySwimlane([]); } clearSelectedAnomalies(); + previousListenerData = null; } else { const timerange = getSelectionTimeRange(cellData); $scope.cellData = cellData; if (cellData.score > 0) { + const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? + cellData.laneLabels : $scope.getSelectedJobIds(); + const influencers = getSelectionInfluencers(cellData); + + const listenerData = { jobIds, influencers, start: timerange.earliestMs, end: timerange.latestMs, cellData }; + if (_.isEqual(listenerData, previousListenerData)) { + return; + } + previousListenerData = listenerData; + 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. @@ -365,12 +388,8 @@ module.controller('MlExplorerController', function ( $scope.viewByLoadedForTimeFormatted = moment(timerange.earliestMs).format('MMMM Do YYYY, HH:mm'); } - const jobIds = (cellData.fieldName === VIEW_BY_JOB_LABEL) ? - cellData.laneLabels : $scope.getSelectedJobIds(); - const influencers = getSelectionInfluencers(cellData); - - loadAnomaliesTableData(); loadDataForCharts(jobIds, influencers, timerange.earliestMs, timerange.latestMs); + loadAnomaliesTableData(); } else { // Multiple cells are selected, all with a score of 0 - clear all anomalies. $scope.$evalAsync(() => { @@ -724,7 +743,7 @@ module.controller('MlExplorerController', function ( if ($scope.selectedJobs === undefined || $scope.swimlaneViewByFieldName === undefined || $scope.swimlaneViewByFieldName === null) { - $scope.viewBySwimlaneData = { 'fieldName': '', 'laneLabels': [], 'points': [], 'interval': 3600 }; + $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); finish(); } else { // Ensure the search bounds align to the bucketing interval used in the swimlane so From 4bc2cbe4ca5b5ab68a56312be54fcc32689a4ef8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 16:27:23 +0200 Subject: [PATCH 08/15] [ML] Code style tweaks. --- .../ml/public/explorer/explorer_controller.js | 16 +++++++++++----- 1 file changed, 11 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 2a06973ee2501..55a2aa64a3f79 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -58,10 +58,10 @@ const module = uiModules.get('apps/ml'); function getDefaultViewBySwimlaneData() { return { - 'fieldName': '', - 'laneLabels': [], - 'points': [], - 'interval': 3600 + fieldName: '', + laneLabels: [], + points: [], + interval: 3600 }; } @@ -375,7 +375,13 @@ module.controller('MlExplorerController', function ( cellData.laneLabels : $scope.getSelectedJobIds(); const influencers = getSelectionInfluencers(cellData); - const listenerData = { jobIds, influencers, start: timerange.earliestMs, end: timerange.latestMs, cellData }; + const listenerData = { + jobIds, + influencers, + start: timerange.earliestMs, + end: timerange.latestMs, + cellData + }; if (_.isEqual(listenerData, previousListenerData)) { return; } From 7ae66433aad02c4c15d3f2d7c33a92e3f976dbf2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sat, 8 Sep 2018 11:20:19 +0200 Subject: [PATCH 09/15] [ML] Adds skipCellClicks to stop listening to mouse clicks on swimlanes while they are loading. --- .../plugins/ml/public/explorer/explorer_controller.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 55a2aa64a3f79..e52c7fb11c798 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -93,7 +93,10 @@ 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 let disableDragSelectOnMouseLeave = true; + // skip listening to clicks on swimlanes while they are loading to avoid race conditions + let skipCellClicks = true; $scope.queryFilters = []; const dragSelect = new DragSelect({ @@ -358,6 +361,10 @@ module.controller('MlExplorerController', function ( // Listener for click events in the swimlane and load corresponding anomaly data. // Empty cellData is passed on clicking outside a cell with score > 0. const swimlaneCellClickListener = function (cellData) { + if (skipCellClicks === true) { + return; + } + if (_.keys(cellData).length === 0) { // Swimlane deselection - clear anomalies section. if ($scope.viewByLoadedForTimeFormatted) { @@ -712,6 +719,7 @@ module.controller('MlExplorerController', function ( $timeout(() => { $scope.$broadcast('render'); mlExplorerDashboardService.swimlaneDataChange.changed('overall'); + skipCellClicks = false; }, 0); }); @@ -736,6 +744,7 @@ module.controller('MlExplorerController', function ( } function loadViewBySwimlane(fieldValues) { + 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() { @@ -743,6 +752,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'); }, 0); } From d1bff924b27b22d59a213ba10128e46dc1de4877 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 10 Sep 2018 12:14:11 +0200 Subject: [PATCH 10/15] [ML] Remove console.log --- .../explorer_charts/explorer_charts_container_service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 26aa4abc302bb..4d42aed56d902 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -384,7 +384,7 @@ export function explorerChartsContainerServiceFactory( }); - console.log('explorer charts aggregatedData is:', aggregatedData); + //console.log('explorer charts aggregatedData is:', aggregatedData); let recordsForSeries = []; // Convert to an array of the records with the highest record_score per unique series. _.each(aggregatedData, (detectorsForJob) => { From b6559ceae9bad1bfa75191c0d48bb40138af5b6f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 10 Sep 2018 17:39:55 +0200 Subject: [PATCH 11/15] [ML] Fixes race conditions. --- .../ml/public/explorer/explorer_controller.js | 10 ++++++++ .../ml/public/explorer/explorer_swimlane.js | 21 ++++++++++++++--- .../explorer/explorer_swimlane_directive.js | 23 +++++++++++++++---- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index e52c7fb11c798..23a698cc2bedf 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -484,7 +484,12 @@ module.controller('MlExplorerController', function ( navListener(); }); + // track the last request time to be able to ignore out of date requests + // and avoid race conditions ending up with the wrong charts. + let lastRequestTime = null; function loadDataForCharts(jobIds, influencers, earliestMs, latestMs) { + const requestTime = new Date().getTime(); + lastRequestTime = requestTime; // Loads the data used to populate the anomaly charts and the Top Influencers List. if (influencers.length === 0) { getTopInfluencers(jobIds, earliestMs, latestMs); @@ -495,6 +500,11 @@ module.controller('MlExplorerController', function ( jobIds, influencers, 0, earliestMs, latestMs, 500 ) .then((resp) => { + // Ignore this response if it's returned by an out of date request. + if (requestTime < lastRequestTime) { + return; + } + if ($scope.cellData !== undefined && _.keys($scope.cellData).length > 0) { $scope.anomalyChartRecords = resp.records; console.log('Explorer anomaly charts data set:', $scope.anomalyChartRecords); diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index 46a9fc8abdb72..d03e30b2b48db 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -42,7 +42,7 @@ export class ExplorerSwimlane extends React.Component { componentWillUnmount() { const { mlExplorerDashboardService } = this.props; - mlExplorerDashboardService.dragSelect.unwatch(this.dragSelectListener.bind(this)); + mlExplorerDashboardService.dragSelect.unwatch(this.boundDragSelectListener); } componentDidMount() { @@ -54,7 +54,11 @@ export class ExplorerSwimlane extends React.Component { element.addClass('ml-hide-range-selection'); } - mlExplorerDashboardService.dragSelect.watch(this.dragSelectListener.bind(this)); + // save the bound dragSelectListener to this property so it can be accessed again + // in componentWillUnmount(), otherwise mlExplorerDashboardService.dragSelect.unwatch + // is not able to check properly if it's still the same listener + this.boundDragSelectListener = this.dragSelectListener.bind(this); + mlExplorerDashboardService.dragSelect.watch(this.boundDragSelectListener); this.renderSwimlane(); } @@ -64,6 +68,13 @@ export class ExplorerSwimlane extends React.Component { this.renderSwimlane(); } + // property to remember the bound dragSelectListener + boundDragSelectListener = null; + + // property for cellClick data comparison to be able to filter + // consecutive click events with the same data. + previousSelectedData = null; + // Listen for dragSelect events dragSelectListener({ action, elements = [] }) { const element = $(this.rootNode).parent(); @@ -86,7 +97,10 @@ export class ExplorerSwimlane extends React.Component { selectedData.laneLabels = _.uniq(selectedData.laneLabels); selectedData.times = _.uniq(selectedData.times); - this.cellClick(elements, selectedData); + if (_.isEqual(selectedData, this.previousSelectedData) === false) { + this.cellClick(elements, selectedData); + this.previousSelectedData = selectedData; + } } this.setState({ cellMouseoverActive: true }); @@ -98,6 +112,7 @@ export class ExplorerSwimlane extends React.Component { return; } + this.previousSelectedData = null; element.removeClass('ml-dragselect-dragging'); elements.map(e => $(e).removeClass('ds-selected')); } 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 406f1bd82eb84..fc7adcce51b62 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane_directive.js @@ -10,6 +10,7 @@ * AngularJS directive for rendering Explorer dashboard swimlanes. */ +import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -33,11 +34,21 @@ module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDa 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) { return; @@ -57,10 +68,14 @@ module.directive('mlExplorerSwimlane', function ($compile, Private, mlExplorerDa appState: scope.appState }; - ReactDOM.render( - React.createElement(ExplorerSwimlane, props), - element[0] - ); + if (_.isEqual(props, previousProps) === false) { + ReactDOM.render( + React.createElement(ExplorerSwimlane, props), + element[0] + ); + previousProps = props; + } + } } From eb45c3e19ca302436d6eaa4fdfe4e2493072f2af Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 10 Sep 2018 20:02:31 +0200 Subject: [PATCH 12/15] [ML] Fixes even more race conditions and proper state propagation issues. --- .../ml/public/explorer/explorer_controller.js | 58 +++++++++++++++---- .../ml/public/explorer/explorer_swimlane.js | 3 +- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 23a698cc2bedf..bf9b3bea469f3 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -354,14 +354,29 @@ module.controller('MlExplorerController', function ( return influencers; } + // This queue tracks click events while the swimlanes are loading. + // To avoid race conditions we keep the click events cellData in this queue + // and trigger another event only after the current loading is done. + // The queue is necessary since a click in the overall swimlane triggers + // an update of the viewby swimlanes. If we'd just ignored click events + // during the loading, we could miss programmatically triggered events like + // 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. - const swimlaneCellClickListener = function (cellData) { + // 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) { if (skipCellClicks === true) { + swimlaneCellClickListenerQueue.push(cellData); return; } @@ -389,7 +404,7 @@ module.controller('MlExplorerController', function ( end: timerange.latestMs, cellData }; - if (_.isEqual(listenerData, previousListenerData)) { + if (_.isEqual(listenerData, previousListenerData) && skipComparison === false) { return; } previousListenerData = listenerData; @@ -430,7 +445,8 @@ module.controller('MlExplorerController', function ( const checkboxShowChartsListener = function () { const showCharts = mlCheckboxShowChartsService.state.get('showCharts'); if (showCharts && $scope.cellData !== undefined) { - swimlaneCellClickListener($scope.cellData); + // passing true as the second argument skips click event filtering + swimlaneCellClickListener($scope.cellData, true); } else { const timerange = getSelectionTimeRange($scope.cellData); mlExplorerDashboardService.anomalyDataChange.changed( @@ -484,12 +500,19 @@ module.controller('MlExplorerController', function ( navListener(); }); - // track the last request time to be able to ignore out of date requests + // track the request to be able to ignore out of date requests // and avoid race conditions ending up with the wrong charts. - let lastRequestTime = null; + let requestCount = 0; function loadDataForCharts(jobIds, influencers, earliestMs, latestMs) { - const requestTime = new Date().getTime(); - lastRequestTime = requestTime; + // Just skip doing the request when this function is called without + // the minimum required data. + if ($scope.cellData === undefined && influencers.length === 0) { + return; + } + + const newRequestCount = requestCount + 1; + requestCount = newRequestCount; + // Loads the data used to populate the anomaly charts and the Top Influencers List. if (influencers.length === 0) { getTopInfluencers(jobIds, earliestMs, latestMs); @@ -500,8 +523,8 @@ module.controller('MlExplorerController', function ( jobIds, influencers, 0, earliestMs, latestMs, 500 ) .then((resp) => { - // Ignore this response if it's returned by an out of date request. - if (requestTime < lastRequestTime) { + // Ignore this response if it's returned by an out of date promise + if (newRequestCount < requestCount) { return; } @@ -754,11 +777,20 @@ module.controller('MlExplorerController', function ( } function loadViewBySwimlane(fieldValues) { + // reset the swimlane data to avoid flickering where the old dataset would briefly show up. + $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); + 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() { console.log('Explorer view by swimlane data set:', $scope.viewBySwimlaneData); + if (swimlaneCellClickListenerQueue.length > 0) { + const cellData = swimlaneCellClickListenerQueue.pop(); + swimlaneCellClickListenerQueue.length = 0; + swimlaneCellClickListener(cellData); + return; + } // 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(() => { @@ -767,9 +799,11 @@ module.controller('MlExplorerController', function ( }, 0); } - if ($scope.selectedJobs === undefined || - $scope.swimlaneViewByFieldName === undefined || $scope.swimlaneViewByFieldName === null) { - $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); + if ( + $scope.selectedJobs === undefined || + $scope.swimlaneViewByFieldName === undefined || + $scope.swimlaneViewByFieldName === null + ) { finish(); } else { // Ensure the search bounds align to the bucketing interval used in the swimlane so diff --git a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js index d03e30b2b48db..bb20a73afdd9c 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.js @@ -43,7 +43,8 @@ export class ExplorerSwimlane extends React.Component { componentWillUnmount() { const { mlExplorerDashboardService } = this.props; mlExplorerDashboardService.dragSelect.unwatch(this.boundDragSelectListener); - + const element = $(this.rootNode); + element.empty(); } componentDidMount() { const element = $(this.rootNode).parent(); From a97b89a9736eb932cac276fbdc7c250c9556fbe2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Sep 2018 09:53:36 +0200 Subject: [PATCH 13/15] [ML] Tweak counter code. Re-enable console.log. --- .../explorer_charts/explorer_charts_container_service.js | 2 +- x-pack/plugins/ml/public/explorer/explorer_controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js index 4d42aed56d902..26aa4abc302bb 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js @@ -384,7 +384,7 @@ export function explorerChartsContainerServiceFactory( }); - //console.log('explorer charts aggregatedData is:', aggregatedData); + console.log('explorer charts aggregatedData is:', aggregatedData); let recordsForSeries = []; // Convert to an array of the records with the highest record_score per unique series. _.each(aggregatedData, (detectorsForJob) => { diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index bf9b3bea469f3..c4815658e730d 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -510,7 +510,7 @@ module.controller('MlExplorerController', function ( return; } - const newRequestCount = requestCount + 1; + const newRequestCount = ++requestCount; requestCount = newRequestCount; // Loads the data used to populate the anomaly charts and the Top Influencers List. From 2cf68c87c4b1fe4412b310555840962ce1537ba5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Sep 2018 09:55:35 +0200 Subject: [PATCH 14/15] [ML] Disabling test to inspect CI fails. --- x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js | 2 ++ 1 file changed, 2 insertions(+) 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 a30f03d3e232a..5ec5f80d65f0b 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -80,6 +80,7 @@ describe('ExplorerSwimlane', () => { expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); + /* test('Overall swimlane', () => { const mocks = getExplorerSwimlaneMocks(); @@ -108,4 +109,5 @@ describe('ExplorerSwimlane', () => { expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls).toHaveLength(1); expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); + */ }); From f5256d3aef6189ec5c96e2522ad1c5b7d0fd47b4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Sep 2018 10:45:04 +0200 Subject: [PATCH 15/15] [ML] Set a default timezone for tests so snapshot render always the same. --- .../__snapshots__/explorer_swimlane.test.js.snap | 2 +- .../ml/public/explorer/explorer_swimlane.test.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap index bdddfe1f8c446..d601b30ca757a 100644 --- a/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap +++ b/x-pack/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
2017-02-07T01:00:00+01:002017-02-07T01:30:00+01:002017-02-07T02:00:00+01:002017-02-07T02:30:00+01:002017-02-07T03:00:00+01:002017-02-07T03:30:00+01:002017-02-07T04:00:00+01:002017-02-07T04:30:00+01:002017-02-07T05:00:00+01:002017-02-07T05:30:00+01:002017-02-07T06:00:00+01:002017-02-07T06:30:00+01:002017-02-07T07:00:00+01:002017-02-07T07:30:00+01:002017-02-07T08:00:00+01:002017-02-07T08:30:00+01:002017-02-07T09:00:00+01:002017-02-07T09:30:00+01:002017-02-07T10:00:00+01:002017-02-07T10:30:00+01:002017-02-07T11:00:00+01:002017-02-07T11:30:00+01:002017-02-07T12:00:00+01:002017-02-07T12:30:00+01:002017-02-07T13:00:00+01:002017-02-07T13:30:00+01:002017-02-07T14:00:00+01:002017-02-07T14:30:00+01:002017-02-07T15:00:00+01:002017-02-07T15:30:00+01:002017-02-07T16:00:00+01:002017-02-07T16:30:00+01:002017-02-07T17:00:00+01:00
"`; +exports[`ExplorerSwimlane Overall swimlane 1`] = `"
Overall
2017-02-07T00:00:00Z2017-02-07T00:30:00Z2017-02-07T01:00:00Z2017-02-07T01:30:00Z2017-02-07T02:00:00Z2017-02-07T02:30:00Z2017-02-07T03:00:00Z2017-02-07T03:30:00Z2017-02-07T04:00:00Z2017-02-07T04:30:00Z2017-02-07T05:00:00Z2017-02-07T05:30:00Z2017-02-07T06:00:00Z2017-02-07T06:30:00Z2017-02-07T07:00:00Z2017-02-07T07:30:00Z2017-02-07T08:00:00Z2017-02-07T08:30:00Z2017-02-07T09:00:00Z2017-02-07T09:30:00Z2017-02-07T10:00:00Z2017-02-07T10:30:00Z2017-02-07T11:00:00Z2017-02-07T11:30:00Z2017-02-07T12:00:00Z2017-02-07T12:30:00Z2017-02-07T13:00:00Z2017-02-07T13:30:00Z2017-02-07T14:00:00Z2017-02-07T14:30:00Z2017-02-07T15:00:00Z2017-02-07T15:30:00Z2017-02-07T16:00:00Z
"`; 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 5ec5f80d65f0b..46ecaf020badb 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -6,6 +6,7 @@ import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; +import moment from 'moment-timezone'; import { mount } from 'enzyme'; import React from 'react'; @@ -51,8 +52,14 @@ function getExplorerSwimlaneMocks() { describe('ExplorerSwimlane', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; - beforeEach(() => SVGElement.prototype.getBBox = () => mockedGetBBox); - afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); + beforeEach(() => { + moment.tz.setDefault('UTC'); + SVGElement.prototype.getBBox = () => mockedGetBBox; + }); + afterEach(() => { + moment.tz.setDefault('Browser'); + SVGElement.prototype.getBBox = originalGetBBox; + }); test('Minimal initialization', () => { const mocks = getExplorerSwimlaneMocks(); @@ -80,7 +87,6 @@ describe('ExplorerSwimlane', () => { expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); - /* test('Overall swimlane', () => { const mocks = getExplorerSwimlaneMocks(); @@ -109,5 +115,4 @@ describe('ExplorerSwimlane', () => { expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls).toHaveLength(1); expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls).toHaveLength(1); }); - */ });