[ML] Deprecates custom event listeners. (#31426) (#31566)

Deprecates our own implemention of event listeners in favour of RxJs' Observables.
This commit is contained in:
Walter Rafelsberger 2019-02-20 14:43:44 +01:00 committed by GitHub
parent 40910cd406
commit 5a55e3240a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 297 additions and 390 deletions

View file

@ -15,7 +15,6 @@ import 'ui/persisted_log';
import 'ui/autoload/all';
import 'plugins/ml/access_denied';
import 'plugins/ml/factories/listener_factory';
import 'plugins/ml/factories/state_factory';
import 'plugins/ml/lib/angular_bootstrap_patch';
import 'plugins/ml/jobs';

View file

@ -209,14 +209,14 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component {
if (this.mouseOverRecord !== undefined) {
if (this.mouseOverRecord.rowId !== record.rowId) {
// Mouse is over a different row, fire mouseleave on the previous record.
mlTableService.rowMouseleave.changed(this.mouseOverRecord, 'annotation');
mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' });
// fire mouseenter on the new record.
mlTableService.rowMouseenter.changed(record, 'annotation');
mlTableService.rowMouseenter$.next({ record, type: 'annotation' });
}
} else {
// Mouse is now over a row, fire mouseenter on the record.
mlTableService.rowMouseenter.changed(record, 'annotation');
mlTableService.rowMouseenter$.next({ record, type: 'annotation' });
}
this.mouseOverRecord = record;
@ -224,7 +224,7 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component {
onMouseLeaveRow = () => {
if (this.mouseOverRecord !== undefined) {
mlTableService.rowMouseleave.changed(this.mouseOverRecord, 'annotation');
mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' });
this.mouseOverRecord = undefined;
}
};

View file

@ -118,14 +118,14 @@ class AnomaliesTable extends Component {
if (this.mouseOverRecord !== undefined) {
if (this.mouseOverRecord.rowId !== record.rowId) {
// Mouse is over a different row, fire mouseleave on the previous record.
mlTableService.rowMouseleave.changed(this.mouseOverRecord);
mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord });
// fire mouseenter on the new record.
mlTableService.rowMouseenter.changed(record);
mlTableService.rowMouseenter$.next({ record });
}
} else {
// Mouse is now over a row, fire mouseenter on the record.
mlTableService.rowMouseenter.changed(record);
mlTableService.rowMouseenter$.next({ record });
}
this.mouseOverRecord = record;
@ -133,7 +133,7 @@ class AnomaliesTable extends Component {
onMouseLeaveRow = () => {
if (this.mouseOverRecord !== undefined) {
mlTableService.rowMouseleave.changed(this.mouseOverRecord);
mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord });
this.mouseOverRecord = undefined;
}
};

View file

@ -35,7 +35,7 @@ import { ExplorerSwimlane } from './explorer_swimlane';
import { formatHumanReadableDateTime } from '../util/date_utils';
import { getBoundsRoundedToInterval } from 'plugins/ml/util/ml_time_buckets';
import { InfluencersList } from '../components/influencers_list';
import { mlExplorerDashboardService } from './explorer_dashboard_service';
import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, explorer$ } from './explorer_dashboard_service';
import { mlResultsService } from 'plugins/ml/services/results_service';
import { LoadingIndicator } from '../components/loading_indicator/loading_indicator';
import { CheckboxShowCharts, mlCheckboxShowChartsService } from '../components/controls/checkbox_showcharts/checkbox_showcharts';
@ -122,16 +122,65 @@ export const Explorer = injectI18n(
state = getExplorerDefaultState();
// helper to avoid calling `setState()` in the listener for chart updates.
_isMounted = false;
// make sure dragSelect is only available if the mouse pointer is actually over a swimlane
disableDragSelectOnMouseLeave = true;
// skip listening to clicks on swimlanes while they are loading to avoid race conditions
skipCellClicks = true;
updateCharts = explorerChartsContainerServiceFactory((data) => {
if (this._isMounted) {
// initialize an empty callback, this will be set in componentDidMount()
updateCharts = () => {};
dragSelect = new DragSelect({
selectables: document.getElementsByClassName('sl-cell'),
callback(elements) {
if (elements.length > 1 && !ALLOW_CELL_RANGE_SELECTION) {
elements = [elements[0]];
}
if (elements.length > 0) {
dragSelect$.next({
action: DRAG_SELECT_ACTION.NEW_SELECTION,
elements
});
}
this.disableDragSelectOnMouseLeave = true;
},
onDragStart() {
if (ALLOW_CELL_RANGE_SELECTION) {
dragSelect$.next({
action: DRAG_SELECT_ACTION.DRAG_START
});
this.disableDragSelectOnMouseLeave = false;
}
},
onElementSelect() {
if (ALLOW_CELL_RANGE_SELECTION) {
dragSelect$.next({
action: DRAG_SELECT_ACTION.ELEMENT_SELECT
});
}
}
});
// Listens to render updates of the swimlanes to update dragSelect
swimlaneRenderDoneListener = () => {
this.dragSelect.clearSelection();
this.dragSelect.setSelectables(document.getElementsByClassName('sl-cell'));
};
// These are observable subscriptions, they get assigned in componentDidMount().
// In componentWillUnmount() they will be unsubscribed again.
annotationsRefreshSub = null;
explorerSub = null;
showChartsSub = null;
limitSub = null;
chartsSeveritySub = null;
intervalSub = null;
tableSeveritySub = null;
componentDidMount() {
this.updateCharts = explorerChartsContainerServiceFactory((data) => {
this.setState({
chartsData: {
...getDefaultChartsData(),
@ -141,180 +190,131 @@ export const Explorer = injectI18n(
tooManyBuckets: !!data.tooManyBuckets,
}
});
}
});
});
ALLOW_CELL_RANGE_SELECTION = mlExplorerDashboardService.allowCellRangeSelection;
this.explorerSub = explorer$.subscribe(({ action, payload = {} }) => {
// Listen to the initial loading of jobs
if (action === EXPLORER_ACTION.INITIALIZE) {
const { noJobsFound, selectedCells, selectedJobs, swimlaneViewByFieldName } = payload;
let currentSelectedCells = this.state.selectedCells;
let currentSwimlaneViewByFieldName = this.state.swimlaneViewByFieldName;
dragSelect = new DragSelect({
selectables: document.getElementsByClassName('sl-cell'),
callback(elements) {
if (elements.length > 1 && !this.ALLOW_CELL_RANGE_SELECTION) {
elements = [elements[0]];
if (selectedCells !== undefined && currentSelectedCells === null) {
currentSelectedCells = selectedCells;
currentSwimlaneViewByFieldName = swimlaneViewByFieldName;
}
const stateUpdate = {
noInfluencersConfigured: !selectedJobsHaveInfluencers(selectedJobs),
noJobsFound,
selectedCells: currentSelectedCells,
selectedJobs,
swimlaneViewByFieldName: currentSwimlaneViewByFieldName
};
this.updateExplorer(stateUpdate, true);
}
if (elements.length > 0) {
mlExplorerDashboardService.dragSelect.changed({
action: DRAG_SELECT_ACTION.NEW_SELECTION,
elements
});
// Listen for changes to job selection.
if (action === EXPLORER_ACTION.JOB_SELECTION_CHANGE) {
const { selectedJobs } = payload;
const stateUpdate = {
noInfluencersConfigured: !selectedJobsHaveInfluencers(selectedJobs),
selectedJobs,
};
this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION);
Object.assign(stateUpdate, getClearedSelectedAnomaliesState());
if (selectedJobs.length > 1) {
this.props.appStateHandler(
APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME,
{ swimlaneViewByFieldName: VIEW_BY_JOB_LABEL },
);
stateUpdate.swimlaneViewByFieldName = VIEW_BY_JOB_LABEL;
// enforce a state update for swimlaneViewByFieldName
this.setState({ swimlaneViewByFieldName: VIEW_BY_JOB_LABEL }, () => {
this.updateExplorer(stateUpdate, true);
});
return;
}
this.updateExplorer(stateUpdate, true);
}
this.disableDragSelectOnMouseLeave = true;
},
onDragStart() {
if (this.ALLOW_CELL_RANGE_SELECTION) {
mlExplorerDashboardService.dragSelect.changed({
action: DRAG_SELECT_ACTION.DRAG_START
});
this.disableDragSelectOnMouseLeave = false;
}
},
onElementSelect() {
if (this.ALLOW_CELL_RANGE_SELECTION) {
mlExplorerDashboardService.dragSelect.changed({
action: DRAG_SELECT_ACTION.ELEMENT_SELECT
});
}
}
});
dashboardListener = ((action, payload = {}) => {
// Listen to the initial loading of jobs
if (action === EXPLORER_ACTION.INITIALIZE) {
const { noJobsFound, selectedCells, selectedJobs, swimlaneViewByFieldName } = payload;
let currentSelectedCells = this.state.selectedCells;
let currentSwimlaneViewByFieldName = this.state.swimlaneViewByFieldName;
if (selectedCells !== undefined && currentSelectedCells === null) {
currentSelectedCells = selectedCells;
currentSwimlaneViewByFieldName = swimlaneViewByFieldName;
// RELOAD reloads full Anomaly Explorer and clears the selection.
if (action === EXPLORER_ACTION.RELOAD) {
this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION);
this.updateExplorer({ ...payload, ...getClearedSelectedAnomaliesState() }, true);
}
const stateUpdate = {
noInfluencersConfigured: !selectedJobsHaveInfluencers(selectedJobs),
noJobsFound,
selectedCells: currentSelectedCells,
selectedJobs,
swimlaneViewByFieldName: currentSwimlaneViewByFieldName
};
this.updateExplorer(stateUpdate, true);
}
// Listen for changes to job selection.
if (action === EXPLORER_ACTION.JOB_SELECTION_CHANGE) {
const { selectedJobs } = payload;
const stateUpdate = {
noInfluencersConfigured: !selectedJobsHaveInfluencers(selectedJobs),
selectedJobs,
};
this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION);
Object.assign(stateUpdate, getClearedSelectedAnomaliesState());
if (selectedJobs.length > 1) {
this.props.appStateHandler(
APP_STATE_ACTION.SAVE_SWIMLANE_VIEW_BY_FIELD_NAME,
{ swimlaneViewByFieldName: VIEW_BY_JOB_LABEL },
);
stateUpdate.swimlaneViewByFieldName = VIEW_BY_JOB_LABEL;
// enforce a state update for swimlaneViewByFieldName
this.setState({ swimlaneViewByFieldName: VIEW_BY_JOB_LABEL }, () => {
this.updateExplorer(stateUpdate, true);
});
return;
// REDRAW reloads Anomaly Explorer and tries to retain the selection.
if (action === EXPLORER_ACTION.REDRAW) {
this.updateExplorer({}, false);
}
});
this.updateExplorer(stateUpdate, true);
}
this.showChartsSub = mlCheckboxShowChartsService.state.watch(() => {
const showCharts = mlCheckboxShowChartsService.state.get('showCharts');
const { selectedCells, selectedJobs } = this.state;
// RELOAD reloads full Anomaly Explorer and clears the selection.
if (action === EXPLORER_ACTION.RELOAD) {
this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION);
this.updateExplorer({ ...payload, ...getClearedSelectedAnomaliesState() }, true);
}
// REDRAW reloads Anomaly Explorer and tries to retain the selection.
if (action === EXPLORER_ACTION.REDRAW) {
this.updateExplorer({}, false);
}
});
checkboxShowChartsListener = () => {
const showCharts = mlCheckboxShowChartsService.state.get('showCharts');
const { selectedCells, selectedJobs } = this.state;
const bounds = timefilter.getActiveBounds();
const timerange = getSelectionTimeRange(
selectedCells,
this.getSwimlaneBucketInterval(selectedJobs).asSeconds(),
bounds,
);
if (showCharts && selectedCells !== null) {
this.updateCharts(
this.state.anomalyChartRecords, timerange.earliestMs, timerange.latestMs
);
} else {
this.updateCharts(
[], timerange.earliestMs, timerange.latestMs
);
}
};
anomalyChartsSeverityListener = () => {
const showCharts = mlCheckboxShowChartsService.state.get('showCharts');
const { anomalyChartRecords, selectedCells, selectedJobs } = this.state;
if (showCharts && selectedCells !== null) {
const bounds = timefilter.getActiveBounds();
const timerange = getSelectionTimeRange(
selectedCells,
this.getSwimlaneBucketInterval(selectedJobs).asSeconds(),
bounds,
);
this.updateCharts(
anomalyChartRecords, timerange.earliestMs, timerange.latestMs
if (showCharts && selectedCells !== null) {
this.updateCharts(
this.state.anomalyChartRecords, timerange.earliestMs, timerange.latestMs
);
} else {
this.updateCharts(
[], timerange.earliestMs, timerange.latestMs
);
}
});
this.limitSub = mlSelectLimitService.state.watch(() => {
this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION);
this.updateExplorer(getClearedSelectedAnomaliesState(), false);
});
this.chartsSeveritySub = mlSelectSeverityService.state.watch(() => {
const showCharts = mlCheckboxShowChartsService.state.get('showCharts');
const { anomalyChartRecords, selectedCells, selectedJobs } = this.state;
if (showCharts && selectedCells !== null) {
const bounds = timefilter.getActiveBounds();
const timerange = getSelectionTimeRange(
selectedCells,
this.getSwimlaneBucketInterval(selectedJobs).asSeconds(),
bounds,
);
this.updateCharts(
anomalyChartRecords, timerange.earliestMs, timerange.latestMs
);
}
});
const tableControlsListener = async () => {
const { dateFormatTz } = this.props;
const { selectedCells, swimlaneViewByFieldName, selectedJobs } = this.state;
const bounds = timefilter.getActiveBounds();
const tableData = await loadAnomaliesTableData(
selectedCells,
selectedJobs,
dateFormatTz,
this.getSwimlaneBucketInterval(selectedJobs).asSeconds(),
bounds,
swimlaneViewByFieldName
);
}
};
this.setState({ tableData });
};
tableControlsListener = async () => {
const { dateFormatTz } = this.props;
const { selectedCells, swimlaneViewByFieldName, selectedJobs } = this.state;
const bounds = timefilter.getActiveBounds();
const tableData = await loadAnomaliesTableData(
selectedCells,
selectedJobs,
dateFormatTz,
this.getSwimlaneBucketInterval(selectedJobs).asSeconds(),
bounds,
swimlaneViewByFieldName
);
this.setState({ tableData });
};
this.intervalSub = mlSelectIntervalService.state.watch(tableControlsListener);
this.tableSeveritySub = mlSelectSeverityService.state.watch(tableControlsListener);
swimlaneLimitListener = () => {
this.props.appStateHandler(APP_STATE_ACTION.CLEAR_SELECTION);
this.updateExplorer(getClearedSelectedAnomaliesState(), false);
};
// Listens to render updates of the swimlanes to update dragSelect
swimlaneRenderDoneListener = () => {
this.dragSelect.clearSelection();
this.dragSelect.setSelectables(document.getElementsByClassName('sl-cell'));
};
annotationsRefreshSub = null;
componentDidMount() {
this._isMounted = true;
mlExplorerDashboardService.explorer.watch(this.dashboardListener);
mlCheckboxShowChartsService.state.watch(this.checkboxShowChartsListener);
mlSelectLimitService.state.watch(this.swimlaneLimitListener);
mlSelectSeverityService.state.watch(this.anomalyChartsSeverityListener);
mlSelectIntervalService.state.watch(this.tableControlsListener);
mlSelectSeverityService.state.watch(this.tableControlsListener);
this.annotationsRefreshSub = annotationsRefresh$.subscribe(() => {
// clear the annotations cache and trigger an update
this.annotationsTablePreviousArgs = null;
@ -324,13 +324,12 @@ export const Explorer = injectI18n(
}
componentWillUnmount() {
this._isMounted = false;
mlExplorerDashboardService.explorer.unwatch(this.dashboardListener);
mlCheckboxShowChartsService.state.unwatch(this.checkboxShowChartsListener);
mlSelectLimitService.state.unwatch(this.swimlaneLimitListener);
mlSelectSeverityService.state.unwatch(this.anomalyChartsSeverityListener);
mlSelectIntervalService.state.unwatch(this.tableControlsListener);
mlSelectSeverityService.state.unwatch(this.tableControlsListener);
this.explorerSub.unsubscribe();
this.showChartsSub.unsubscribe();
this.limitSub.unsubscribe();
this.chartsSeveritySub.unsubscribe();
this.intervalSub.unsubscribe();
this.tableSeveritySub.unsubscribe();
this.annotationsRefreshSub.unsubscribe();
}

View file

@ -31,7 +31,7 @@ import { checkGetJobsPrivilege } from '../privilege/check_privilege';
import { getIndexPatterns, loadIndexPatterns } from '../util/index_utils';
import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets';
import { JobSelectServiceProvider } from '../components/job_select_list/job_select_service';
import { mlExplorerDashboardService } from './explorer_dashboard_service';
import { explorer$ } from './explorer_dashboard_service';
import { mlFieldFormatService } from 'plugins/ml/services/field_format_service';
import { mlJobService } from '../services/job_service';
import { refreshIntervalWatcher } from '../util/refresh_interval_watcher';
@ -55,7 +55,6 @@ import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.controller('MlExplorerController', function (
$route,
$injector,
$scope,
$timeout,
@ -86,8 +85,6 @@ module.controller('MlExplorerController', function (
let resizeTimeout = null;
mlExplorerDashboardService.init();
function jobSelectionUpdate(action, { fullJobs, selectedCells, selectedJobIds }) {
const jobs = createJobs(fullJobs).map((job) => {
job.selected = selectedJobIds.some((id) => job.id === id);
@ -102,11 +99,14 @@ module.controller('MlExplorerController', function (
const noJobsFound = ($scope.jobs.length === 0);
mlExplorerDashboardService.explorer.changed(action, {
loading: false,
noJobsFound,
selectedCells,
selectedJobs,
explorer$.next({
action,
payload: {
loading: false,
noJobsFound,
selectedCells,
selectedJobs,
}
});
}
@ -130,7 +130,7 @@ module.controller('MlExplorerController', function (
// Calling loadJobs() ensures the full datafeed config is available for building the charts.
// Using this listener ensures the jobs will only be loaded and passed on after
// <ml-explorer-react-wrapper /> and <Explorer /> have been initialized.
function loadJobsListener(action) {
function loadJobsListener({ action }) {
if (action === EXPLORER_ACTION.LOAD_JOBS) {
mlJobService.loadJobs()
.then((resp) => {
@ -157,9 +157,12 @@ module.controller('MlExplorerController', function (
swimlaneViewByFieldName: $scope.appState.mlExplorerSwimlane.viewByFieldName,
});
} else {
mlExplorerDashboardService.explorer.changed(EXPLORER_ACTION.RELOAD, {
loading: false,
noJobsFound: true,
explorer$.next({
action: EXPLORER_ACTION.RELOAD,
payload: {
loading: false,
noJobsFound: true,
}
});
}
})
@ -169,7 +172,7 @@ module.controller('MlExplorerController', function (
}
}
mlExplorerDashboardService.explorer.watch(loadJobsListener);
const explorerSubscriber = explorer$.subscribe(loadJobsListener);
// Listen for changes to job selection.
$scope.mlJobSelectService.listenJobSelectionChange($scope, (event, selectedJobIds) => {
@ -178,13 +181,13 @@ module.controller('MlExplorerController', function (
// Refresh all the data when the time range is altered.
$scope.$listenAndDigestAsync(timefilter, 'fetch', () => {
mlExplorerDashboardService.explorer.changed(EXPLORER_ACTION.RELOAD);
explorer$.next({ action: EXPLORER_ACTION.RELOAD });
});
// Add a watcher for auto-refresh of the time filter to refresh all the data.
const refreshWatcher = Private(refreshIntervalWatcher);
refreshWatcher.init(async () => {
mlExplorerDashboardService.explorer.changed(EXPLORER_ACTION.RELOAD);
explorer$.next({ action: EXPLORER_ACTION.RELOAD });
});
// Redraw the swimlane when the window resizes or the global nav is toggled.
@ -206,7 +209,7 @@ module.controller('MlExplorerController', function (
});
function redrawOnResize() {
mlExplorerDashboardService.explorer.changed(EXPLORER_ACTION.REDRAW);
explorer$.next({ action: EXPLORER_ACTION.REDRAW });
}
$scope.appStateHandler = ((action, payload) => {
@ -238,7 +241,7 @@ module.controller('MlExplorerController', function (
});
$scope.$on('$destroy', () => {
mlExplorerDashboardService.explorer.unwatch(loadJobsListener);
explorerSubscriber.unsubscribe();
refreshWatcher.cancel();
$(window).off('resize', jqueryRedrawOnResize);
// Cancel listening for updates to the global nav state.

View file

@ -11,24 +11,9 @@
* components in the Explorer dashboard.
*/
import { listenerFactoryProvider } from 'plugins/ml/factories/listener_factory';
import { Subject } from 'rxjs';
function mlExplorerDashboardServiceFactory() {
const service = {
allowCellRangeSelection: false
};
export const ALLOW_CELL_RANGE_SELECTION = false;
const listenerFactory = listenerFactoryProvider();
const dragSelect = service.dragSelect = listenerFactory();
const explorer = service.explorer = listenerFactory();
service.init = function () {
// Clear out any old listeners.
dragSelect.unwatchAll();
explorer.unwatchAll();
};
return service;
}
export const mlExplorerDashboardService = mlExplorerDashboardServiceFactory();
export const dragSelect$ = new Subject();
export const explorer$ = new Subject();

View file

@ -20,7 +20,7 @@ import { I18nContext } from 'ui/i18n';
import { mapScopeToProps } from './explorer_utils';
import { EXPLORER_ACTION } from './explorer_constants';
import { mlExplorerDashboardService } from './explorer_dashboard_service';
import { explorer$ } from './explorer_dashboard_service';
module.directive('mlExplorerReactWrapper', function () {
function link(scope, element) {
@ -29,7 +29,8 @@ module.directive('mlExplorerReactWrapper', function () {
element[0]
);
mlExplorerDashboardService.explorer.changed(EXPLORER_ACTION.LOAD_JOBS);
explorer$.next({ action: EXPLORER_ACTION.LOAD_JOBS });
element.on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);

View file

@ -24,7 +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 { mlExplorerDashboardService } from './explorer_dashboard_service';
import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service';
import { DRAG_SELECT_ACTION } from './explorer_constants';
import { injectI18n } from '@kbn/i18n/react';
@ -51,18 +51,55 @@ export const ExplorerSwimlane = injectI18n(class ExplorerSwimlane extends React.
// and intentionally circumvent the component lifecycle when updating it.
cellMouseoverActive = true;
componentWillUnmount() {
mlExplorerDashboardService.dragSelect.unwatch(this.boundDragSelectListener);
const element = d3.select(this.rootNode);
element.html('');
}
dragSelectSubscriber = null;
componentDidMount() {
// 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);
// property for data comparison to be able to filter
// consecutive click events with the same data.
let previousSelectedData = null;
// Listen for dragSelect events
this.dragSelectSubscriber = dragSelect$.subscribe(({ action, elements = [] }) => {
const element = d3.select(this.rootNode.parentNode);
const { swimlaneType } = this.props;
if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) {
const firstSelectedCell = d3.select(elements[0]).node().__clickData__;
if (typeof firstSelectedCell !== 'undefined' && swimlaneType === firstSelectedCell.swimlaneType) {
const selectedData = elements.reduce((d, e) => {
const cell = d3.select(e).node().__clickData__;
d.bucketScore = Math.max(d.bucketScore, cell.bucketScore);
d.laneLabels.push(cell.laneLabel);
d.times.push(cell.time);
return d;
}, {
bucketScore: 0,
laneLabels: [],
times: []
});
selectedData.laneLabels = _.uniq(selectedData.laneLabels);
selectedData.times = _.uniq(selectedData.times);
if (_.isEqual(selectedData, previousSelectedData) === false) {
this.selectCell(elements, selectedData);
previousSelectedData = selectedData;
}
}
this.cellMouseoverActive = true;
} else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) {
element.classed(SCSS.mlDragselectDragging, true);
return;
} else if (action === DRAG_SELECT_ACTION.DRAG_START) {
this.cellMouseoverActive = false;
return;
}
previousSelectedData = null;
element.classed(SCSS.mlDragselectDragging, false);
elements.map(e => d3.select(e).classed('ds-selected', false));
});
this.renderSwimlane();
}
@ -71,54 +108,12 @@ export const ExplorerSwimlane = injectI18n(class ExplorerSwimlane extends React.
this.renderSwimlane();
}
// property to remember the bound dragSelectListener
boundDragSelectListener = null;
// property for 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 = d3.select(this.rootNode.parentNode);
const { swimlaneType } = this.props;
if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) {
const firstSelectedCell = d3.select(elements[0]).node().__clickData__;
if (typeof firstSelectedCell !== 'undefined' && swimlaneType === firstSelectedCell.swimlaneType) {
const selectedData = elements.reduce((d, e) => {
const cell = d3.select(e).node().__clickData__;
d.bucketScore = Math.max(d.bucketScore, cell.bucketScore);
d.laneLabels.push(cell.laneLabel);
d.times.push(cell.time);
return d;
}, {
bucketScore: 0,
laneLabels: [],
times: []
});
selectedData.laneLabels = _.uniq(selectedData.laneLabels);
selectedData.times = _.uniq(selectedData.times);
if (_.isEqual(selectedData, this.previousSelectedData) === false) {
this.selectCell(elements, selectedData);
this.previousSelectedData = selectedData;
}
}
this.cellMouseoverActive = true;
} else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) {
element.classed(SCSS.mlDragselectDragging, true);
return;
} else if (action === DRAG_SELECT_ACTION.DRAG_START) {
this.cellMouseoverActive = false;
return;
componentWillUnmount() {
if (this.dragSelectSubscriber !== null) {
this.dragSelectSubscriber.unsubscribe();
}
this.previousSelectedData = null;
element.classed(SCSS.mlDragselectDragging, false);
elements.map(e => d3.select(e).classed('ds-selected', false));
const element = d3.select(this.rootNode);
element.html('');
}
selectCell(cellsToSelect, { laneLabels, bucketScore, times }) {
@ -216,7 +211,7 @@ export const ExplorerSwimlane = injectI18n(class ExplorerSwimlane extends React.
const element = d3.select(this.rootNode.parentNode);
// Consider the setting to support to select a range of cells
if (!mlExplorerDashboardService.allowCellRangeSelection) {
if (!ALLOW_CELL_RANGE_SELECTION) {
element.classed(SCSS.mlHideRangeSelection, true);
}

View file

@ -10,7 +10,7 @@ import moment from 'moment-timezone';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { mlExplorerDashboardService } from './explorer_dashboard_service';
import { dragSelect$ } from './explorer_dashboard_service';
import { ExplorerSwimlane } from './explorer_swimlane';
jest.mock('ui/chrome', () => ({
@ -21,13 +21,11 @@ jest.mock('ui/chrome', () => ({
}));
jest.mock('./explorer_dashboard_service', () => ({
mlExplorerDashboardService: {
allowCellRangeSelection: false,
dragSelect: {
watch: jest.fn(),
unwatch: jest.fn()
}
}
dragSelect$: {
subscribe: jest.fn(() => ({
unsubscribe: jest.fn(),
})),
},
}));
function getExplorerSwimlaneMocks() {
@ -79,8 +77,8 @@ describe('ExplorerSwimlane', () => {
);
// test calls to mock functions
expect(mlExplorerDashboardService.dragSelect.watch.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(mlExplorerDashboardService.dragSelect.unwatch.mock.calls).toHaveLength(0);
expect(dragSelect$.subscribe.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(wrapper.instance().dragSelectSubscriber.unsubscribe.mock.calls).toHaveLength(0);
expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(swimlaneRenderDoneListener.mock.calls.length).toBeGreaterThanOrEqual(1);
@ -102,8 +100,8 @@ describe('ExplorerSwimlane', () => {
expect(wrapper.html()).toMatchSnapshot();
// test calls to mock functions
expect(mlExplorerDashboardService.dragSelect.watch.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(mlExplorerDashboardService.dragSelect.unwatch.mock.calls).toHaveLength(0);
expect(dragSelect$.subscribe.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(wrapper.instance().dragSelectSubscriber.unsubscribe.mock.calls).toHaveLength(0);
expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(swimlaneRenderDoneListener.mock.calls.length).toBeGreaterThanOrEqual(1);

View file

@ -1,37 +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 expect from 'expect.js';
import ngMock from 'ng_mock';
import { listenerFactoryProvider } from '../listener_factory';
describe('ML - mlListenerFactory', () => {
let listenerFactory;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(($injector) => {
const Private = $injector.get('Private');
listenerFactory = Private(listenerFactoryProvider);
}));
it('Calling factory doesn\'t throw.', () => {
expect(() => listenerFactory()).to.not.throwError('Not initialized.');
});
it('Fires an event and listener receives value.', (done) => {
const listener = listenerFactory();
listener.watch((value) => {
expect(value).to.be('test');
done();
});
listener.changed('test');
});
});

View file

@ -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.
*/
// A refactor of the original ML listener (three separate functions) into
// an object providing them as methods.
export function listenerFactoryProvider() {
return function () {
const listeners = [];
return {
changed(...args) {
listeners.forEach((listener) => listener(...args));
},
watch(listener) {
listeners.push(listener);
},
unwatch(listener) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
},
unwatchAll() {
listeners.splice(0);
}
};
};
}

View file

@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import { listenerFactoryProvider } from './listener_factory';
import { Subject } from 'rxjs';
// A data store to be able to share persistent state across directives
// in services more conveniently when the structure of angular directives
@ -55,9 +53,7 @@ export function stateFactoryProvider(AppState) {
let appState = initializeAppState(stateName, defaultState);
// () two times here, because the Provider first returns
// the Factory, which then returns the actual listener
const listener = listenerFactoryProvider()();
const listener = new Subject();
let changed = false;
@ -89,13 +85,14 @@ export function stateFactoryProvider(AppState) {
}
return state;
},
watch: listener.watch,
unwatch: listener.unwatch,
watch(l) {
return listener.subscribe(l);
},
// wrap the listener's changed() method to only fire it
// if the state changed.
changed(...args) {
changed(d) {
if (changed) {
listener.changed(...args);
listener.next(d);
changed = false;
}
}

View file

@ -4,21 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
/*
* Service for firing and registering for events in the
* anomalies or annotations table component.
*/
import { listenerFactoryProvider } from '../factories/listener_factory';
import { Subject } from 'rxjs';
class TableService {
constructor() {
const listenerFactory = listenerFactoryProvider();
this.rowMouseenter = listenerFactory();
this.rowMouseleave = listenerFactory();
}
}
export const mlTableService = new TableService();
export const mlTableService = {
rowMouseenter$: new Subject(),
rowMouseleave$: new Subject(),
};

View file

@ -116,12 +116,19 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
zoomTo: PropTypes.object
};
rowMouseenterSubscriber = null;
rowMouseleaveSubscriber = null;
componentWillUnmount() {
const element = d3.select(this.rootNode);
element.html('');
mlTableService.rowMouseenter.unwatch(this.tableRecordMousenterListener);
mlTableService.rowMouseleave.unwatch(this.tableRecordMouseleaveListener);
if (this.rowMouseenterSubscriber !== null) {
this.rowMouseenterSubscriber.unsubscribe();
}
if (this.rowMouseleaveSubscriber !== null) {
this.rowMouseleaveSubscriber.unsubscribe();
}
}
componentDidMount() {
@ -171,26 +178,26 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
// to highlight the corresponding anomaly mark in the focus chart.
const highlightFocusChartAnomaly = this.highlightFocusChartAnomaly.bind(this);
const boundHighlightFocusChartAnnotation = highlightFocusChartAnnotation.bind(this);
this.tableRecordMousenterListener = function (record, type = 'anomaly') {
function tableRecordMousenterListener({ record, type = 'anomaly' }) {
if (type === 'anomaly') {
highlightFocusChartAnomaly(record);
} else if (type === 'annotation') {
boundHighlightFocusChartAnnotation(record);
}
};
}
const unhighlightFocusChartAnomaly = this.unhighlightFocusChartAnomaly.bind(this);
const boundUnhighlightFocusChartAnnotation = unhighlightFocusChartAnnotation.bind(this);
this.tableRecordMouseleaveListener = function (record, type = 'anomaly') {
function tableRecordMouseleaveListener({ record, type = 'anomaly' }) {
if (type === 'anomaly') {
unhighlightFocusChartAnomaly(record);
} else {
boundUnhighlightFocusChartAnnotation(record);
}
};
}
mlTableService.rowMouseenter.watch(this.tableRecordMousenterListener);
mlTableService.rowMouseleave.watch(this.tableRecordMouseleaveListener);
this.rowMouseenterSubscriber = mlTableService.rowMouseenter$.subscribe(tableRecordMousenterListener);
this.rowMouseleaveSubscriber = mlTableService.rowMouseleave$.subscribe(tableRecordMouseleaveListener);
this.renderChart();
this.drawContextChartSelection();

View file

@ -651,15 +651,15 @@ module.controller('MlTimeSeriesExplorerController', function (
loadAnomaliesTableData($scope.zoomFrom.getTime(), $scope.zoomTo.getTime());
}
};
mlSelectIntervalService.state.watch(tableControlsListener);
mlSelectSeverityService.state.watch(tableControlsListener);
const intervalSub = mlSelectIntervalService.state.watch(tableControlsListener);
const severitySub = mlSelectSeverityService.state.watch(tableControlsListener);
const annotationsRefreshSub = annotationsRefresh$.subscribe($scope.refresh);
$scope.$on('$destroy', () => {
refreshWatcher.cancel();
mlSelectIntervalService.state.unwatch(tableControlsListener);
mlSelectSeverityService.state.unwatch(tableControlsListener);
intervalSub.unsubscribe();
severitySub.unsubscribe();
annotationsRefreshSub.unsubscribe();
});