[ML] Fix time range adjustment for the swim lane causing the infinite loop update (#86461)

* [ML] fix swim lane time selection adjustment

* [ML] fix adjustment

* [ML] fix tooManyBuckets condition

* [ML] fix typo
This commit is contained in:
Dima Arnautov 2020-12-21 19:54:45 +01:00 committed by GitHub
parent 256c8cdffb
commit 7de243e7fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 212 additions and 35 deletions

View file

@ -5,3 +5,4 @@
*/
export { useMlKibana } from './kibana_context';
export { useTimefilter } from './use_timefilter';

View file

@ -0,0 +1,40 @@
/*
* 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 { TimefilterContract } from '../../../../../../../../src/plugins/data/public';
import { Observable } from 'rxjs';
/**
* Copy from {@link '../../../../../../../../src/plugins/data/public/query/timefilter/timefilter_service.mock'}
*/
const timefilterMock: jest.Mocked<TimefilterContract> = {
isAutoRefreshSelectorEnabled: jest.fn(),
isTimeRangeSelectorEnabled: jest.fn(),
isTimeTouched: jest.fn(),
getEnabledUpdated$: jest.fn(),
getTimeUpdate$: jest.fn(),
getRefreshIntervalUpdate$: jest.fn(),
getAutoRefreshFetch$: jest.fn(() => new Observable<unknown>()),
getFetch$: jest.fn(),
getTime: jest.fn(),
setTime: jest.fn(),
setRefreshInterval: jest.fn(),
getRefreshInterval: jest.fn(),
getActiveBounds: jest.fn(),
disableAutoRefreshSelector: jest.fn(),
disableTimeRangeSelector: jest.fn(),
enableAutoRefreshSelector: jest.fn(),
enableTimeRangeSelector: jest.fn(),
getBounds: jest.fn(),
calculateBounds: jest.fn(),
createFilter: jest.fn(),
getRefreshIntervalDefaults: jest.fn(),
getTimeDefaults: jest.fn(),
};
export const useTimefilter = jest.fn(() => {
return timefilterMock;
});

View file

@ -694,7 +694,10 @@ function calculateChartRange(
chartRange.min = chartRange.min + maxBucketSpanMs;
}
if (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) {
if (
(chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) &&
chartRange.max - chartRange.min < selectedLatestMs - selectedEarliestMs
) {
tooManyBuckets = true;
}

View file

@ -0,0 +1,133 @@
/*
* 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 moment from 'moment';
import { renderHook } from '@testing-library/react-hooks';
import { useSelectedCells } from './use_selected_cells';
import { ExplorerAppState } from '../../../../common/types/ml_url_generator';
import { TimefilterContract } from '../../../../../../../src/plugins/data/public';
import { useTimefilter } from '../../contexts/kibana';
jest.mock('../../contexts/kibana');
describe('useSelectedCells', () => {
test('should not set state when the cell selection is correct', () => {
(useTimefilter() as jest.Mocked<TimefilterContract>).getBounds.mockReturnValue({
min: moment(1498824778 * 1000),
max: moment(1502366798 * 1000),
});
const urlState = {
mlExplorerSwimlane: {
selectedType: 'overall',
selectedLanes: ['Overall'],
selectedTimes: [1498780800, 1498867200],
showTopFieldValues: true,
viewByFieldName: 'apache2.access.remote_ip',
viewByFromPage: 1,
viewByPerPage: 10,
},
mlExplorerFilter: {},
} as ExplorerAppState;
const setUrlState = jest.fn();
const bucketInterval = 86400;
renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval));
expect(setUrlState).not.toHaveBeenCalled();
});
test('should reset cell selection when it is completely out of range', () => {
(useTimefilter() as jest.Mocked<TimefilterContract>).getBounds.mockReturnValue({
min: moment(1501071178 * 1000),
max: moment(1502366798 * 1000),
});
const urlState = {
mlExplorerSwimlane: {
selectedType: 'overall',
selectedLanes: ['Overall'],
selectedTimes: [1498780800, 1498867200],
showTopFieldValues: true,
viewByFieldName: 'apache2.access.remote_ip',
viewByFromPage: 1,
viewByPerPage: 10,
},
mlExplorerFilter: {},
} as ExplorerAppState;
const setUrlState = jest.fn();
const bucketInterval = 86400;
const { result } = renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval));
expect(result.current[0]).toEqual({
lanes: ['Overall'],
showTopFieldValues: true,
times: [1498780800, 1498867200],
type: 'overall',
viewByFieldName: 'apache2.access.remote_ip',
});
expect(setUrlState).toHaveBeenCalledWith({
mlExplorerSwimlane: {
viewByFieldName: 'apache2.access.remote_ip',
viewByFromPage: 1,
viewByPerPage: 10,
},
});
});
test('should adjust cell selection time boundaries based on the main time range', () => {
(useTimefilter() as jest.Mocked<TimefilterContract>).getBounds.mockReturnValue({
min: moment(1501071178 * 1000),
max: moment(1502366798 * 1000),
});
const urlState = {
mlExplorerSwimlane: {
selectedType: 'overall',
selectedLanes: ['Overall'],
selectedTimes: [1498780800, 1502366798],
showTopFieldValues: true,
viewByFieldName: 'apache2.access.remote_ip',
viewByFromPage: 1,
viewByPerPage: 10,
},
mlExplorerFilter: {},
} as ExplorerAppState;
const setUrlState = jest.fn();
const bucketInterval = 86400;
const { result } = renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval));
expect(result.current[0]).toEqual({
lanes: ['Overall'],
showTopFieldValues: true,
times: [1498780800, 1502366798],
type: 'overall',
viewByFieldName: 'apache2.access.remote_ip',
});
expect(setUrlState).toHaveBeenCalledWith({
mlExplorerSwimlane: {
selectedLanes: ['Overall'],
selectedTimes: [1500984778, 1502366798],
selectedType: 'overall',
showTopFieldValues: true,
viewByFieldName: 'apache2.access.remote_ip',
viewByFromPage: 1,
viewByPerPage: 10,
},
});
});
});

View file

@ -5,7 +5,6 @@
*/
import { useCallback, useEffect, useMemo } from 'react';
import { Duration } from 'moment';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
import { ExplorerAppState } from '../../../../common/types/ml_url_generator';
@ -14,10 +13,9 @@ import { useTimefilter } from '../../contexts/kibana';
export const useSelectedCells = (
appState: ExplorerAppState,
setAppState: (update: Partial<ExplorerAppState>) => void,
bucketInterval: Duration | undefined
bucketIntervalInSeconds: number | undefined
): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => {
const timeFilter = useTimefilter();
const timeBounds = timeFilter.getBounds();
// keep swimlane selection, restore selectedCells from AppState
@ -76,43 +74,45 @@ export const useSelectedCells = (
* Adjust cell selection with respect to the time boundaries.
* Reset it entirely when it out of range.
*/
useEffect(() => {
if (selectedCells?.times === undefined || bucketInterval === undefined) return;
useEffect(
function adjustSwimLaneTimeSelection() {
if (selectedCells?.times === undefined || bucketIntervalInSeconds === undefined) return;
let [selectedFrom, selectedTo] = selectedCells.times;
const [selectedFrom, selectedTo] = selectedCells.times;
const rangeFrom = timeBounds.min!.unix();
/**
* Because each cell on the swim lane represent the fixed bucket interval,
* the selection range could be outside of the time boundaries with
* correction within the bucket interval.
*/
const rangeTo = timeBounds.max!.unix() + bucketInterval.asSeconds();
/**
* Because each cell on the swim lane represent the fixed bucket interval,
* the selection range could be outside of the time boundaries with
* correction within the bucket interval.
*/
const rangeFrom = timeBounds.min!.unix() - bucketIntervalInSeconds;
const rangeTo = timeBounds.max!.unix() + bucketIntervalInSeconds;
selectedFrom = Math.max(selectedFrom, rangeFrom);
const resultFrom = Math.max(selectedFrom, rangeFrom);
const resultTo = Math.min(selectedTo, rangeTo);
selectedTo = Math.min(selectedTo, rangeTo);
const isSelectionOutOfRange = rangeFrom > resultTo || rangeTo < resultFrom;
const isSelectionOutOfRange = rangeFrom > selectedTo || rangeTo < selectedFrom;
if (isSelectionOutOfRange) {
// reset selection
setSelectedCells();
return;
}
if (isSelectionOutOfRange) {
// reset selection
setSelectedCells();
return;
}
if (selectedFrom === resultFrom && selectedTo === resultTo) {
// selection is correct, no need to adjust the range
return;
}
if (selectedFrom !== rangeFrom || selectedTo !== rangeTo) {
setSelectedCells({
...selectedCells,
times: [selectedFrom, selectedTo],
});
}
}, [
timeBounds.min?.valueOf(),
timeBounds.max?.valueOf(),
selectedCells,
bucketInterval?.asMilliseconds(),
]);
if (resultFrom !== rangeFrom || resultTo !== rangeTo) {
setSelectedCells({
...selectedCells,
times: [resultFrom, resultTo],
});
}
},
[timeBounds.min?.unix(), timeBounds.max?.unix(), selectedCells, bucketIntervalInSeconds]
);
return [selectedCells, setSelectedCells];
};

View file

@ -201,7 +201,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
const [selectedCells, setSelectedCells] = useSelectedCells(
explorerUrlState,
setExplorerUrlState,
explorerState?.swimlaneBucketInterval
explorerState?.swimlaneBucketInterval?.asSeconds()
);
useEffect(() => {