mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Deprecates our own implemention of event listeners in favour of RxJs' Observables.
This commit is contained in:
parent
40910cd406
commit
5a55e3240a
15 changed files with 297 additions and 390 deletions
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue