[ML] Fix Anomaly Explorer data refresh with relative time bounds (#86142)

* [ML] use time bounds from the timeFilter

* [ML] add manual type casting for times
This commit is contained in:
Dima Arnautov 2020-12-17 15:14:17 +01:00 committed by GitHub
parent b70cabbdcb
commit bf60796cd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 56 additions and 53 deletions

View file

@ -90,7 +90,7 @@ export interface ExplorerAppState {
mlExplorerSwimlane: {
selectedType?: 'overall' | 'viewBy';
selectedLanes?: string[];
selectedTimes?: number[];
selectedTimes?: [number, number];
showTopFieldValues?: boolean;
viewByFieldName?: string;
viewByPerPage?: number;

View file

@ -26,7 +26,6 @@ import {
loadTopInfluencers,
AppStateSelectedCells,
ExplorerJob,
TimeRangeBounds,
} from '../explorer_utils';
import { ExplorerState } from '../reducers';
import { useMlKibana, useTimefilter } from '../../contexts/kibana';
@ -34,6 +33,7 @@ import { AnomalyTimelineService } from '../../services/anomaly_timeline_service'
import { mlResultsServiceProvider } from '../../services/results_service';
import { isViewBySwimLaneData } from '../swimlane_container';
import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants';
import { TimefilterContract } from '../../../../../../../src/plugins/data/public';
// Memoize the data fetching methods.
// wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument
@ -63,7 +63,6 @@ const memoizedLoadTopInfluencers = memoize(loadTopInfluencers);
const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData);
export interface LoadExplorerDataConfig {
bounds: TimeRangeBounds;
influencersFilterQuery: any;
lastRefresh: number;
noInfluencersConfigured: boolean;
@ -82,7 +81,6 @@ export interface LoadExplorerDataConfig {
export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => {
return (
arg !== undefined &&
arg.bounds !== undefined &&
arg.selectedJobs !== undefined &&
arg.selectedJobs !== null &&
arg.viewBySwimlaneFieldName !== undefined
@ -92,7 +90,10 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi
/**
* Fetches the data necessary for the Anomaly Explorer using observables.
*/
const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService) => {
const loadExplorerDataProvider = (
anomalyTimelineService: AnomalyTimelineService,
timefilter: TimefilterContract
) => {
const memoizedLoadOverallData = memoize(
anomalyTimelineService.loadOverallData,
anomalyTimelineService
@ -107,7 +108,6 @@ const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService
}
const {
bounds,
lastRefresh,
influencersFilterQuery,
noInfluencersConfigured,
@ -125,6 +125,9 @@ const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService
const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
const bounds = timefilter.getBounds();
const timerange = getSelectionTimeRange(
selectedCells,
swimlaneBucketInterval.asSeconds(),
@ -287,12 +290,12 @@ export const useExplorerData = (): [Partial<ExplorerState> | undefined, (d: any)
} = useMlKibana();
const loadExplorerData = useMemo(() => {
const service = new AnomalyTimelineService(
const anomalyTimelineService = new AnomalyTimelineService(
timefilter,
uiSettings,
mlResultsServiceProvider(mlApiServices)
);
return loadExplorerDataProvider(service);
return loadExplorerDataProvider(anomalyTimelineService, timefilter);
}, []);
const loadExplorerData$ = useMemo(() => new Subject<LoadExplorerDataConfig>(), []);

View file

@ -20,7 +20,6 @@ export const EXPLORER_ACTION = {
CLEAR_INFLUENCER_FILTER_SETTINGS: 'clearInfluencerFilterSettings',
CLEAR_JOBS: 'clearJobs',
JOB_SELECTION_CHANGE: 'jobSelectionChange',
SET_BOUNDS: 'setBounds',
SET_CHARTS: 'setCharts',
SET_EXPLORER_DATA: 'setExplorerData',
SET_FILTER_DATA: 'setFilterData',

View file

@ -19,7 +19,7 @@ import { DeepPartial } from '../../../common/types/common';
import { jobSelectionActionCreator } from './actions';
import { ExplorerChartsData } from './explorer_charts/explorer_charts_container_service';
import { EXPLORER_ACTION } from './explorer_constants';
import { AppStateSelectedCells, TimeRangeBounds } from './explorer_utils';
import { AppStateSelectedCells } from './explorer_utils';
import { explorerReducer, getExplorerDefaultState, ExplorerState } from './reducers';
import { ExplorerAppState } from '../../../common/types/ml_url_generator';
@ -115,9 +115,6 @@ export const explorerService = {
updateJobSelection: (selectedJobIds: string[]) => {
explorerAction$.next(jobSelectionActionCreator(selectedJobIds));
},
setBounds: (payload: TimeRangeBounds) => {
explorerAction$.next({ type: EXPLORER_ACTION.SET_BOUNDS, payload });
},
setCharts: (payload: ExplorerChartsData) => {
explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS, payload });
},

View file

@ -186,7 +186,7 @@ export declare interface FilterData {
export declare interface AppStateSelectedCells {
type: SwimlaneType;
lanes: string[];
times: number[];
times: [number, number];
showTopFieldValues?: boolean;
viewByFieldName?: string;
}

View file

@ -7,15 +7,19 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Duration } from 'moment';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells, TimeRangeBounds } from '../explorer_utils';
import { AppStateSelectedCells } from '../explorer_utils';
import { ExplorerAppState } from '../../../../common/types/ml_url_generator';
import { useTimefilter } from '../../contexts/kibana';
export const useSelectedCells = (
appState: ExplorerAppState,
setAppState: (update: Partial<ExplorerAppState>) => void,
timeBounds: TimeRangeBounds | undefined,
bucketInterval: Duration | undefined
): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => {
const timeFilter = useTimefilter();
const timeBounds = timeFilter.getBounds();
// keep swimlane selection, restore selectedCells from AppState
const selectedCells = useMemo(() => {
return appState?.mlExplorerSwimlane?.selectedType !== undefined
@ -73,12 +77,7 @@ export const useSelectedCells = (
* Reset it entirely when it out of range.
*/
useEffect(() => {
if (
timeBounds === undefined ||
selectedCells?.times === undefined ||
bucketInterval === undefined
)
return;
if (selectedCells?.times === undefined || bucketInterval === undefined) return;
let [selectedFrom, selectedTo] = selectedCells.times;
@ -108,7 +107,33 @@ export const useSelectedCells = (
times: [selectedFrom, selectedTo],
});
}
}, [timeBounds, selectedCells, bucketInterval]);
}, [
timeBounds.min?.valueOf(),
timeBounds.max?.valueOf(),
selectedCells,
bucketInterval?.asMilliseconds(),
]);
return [selectedCells, setSelectedCells];
};
export interface SelectionTimeRange {
earliestMs: number;
latestMs: number;
}
export function getTimeBoundsFromSelection(
selectedCells: AppStateSelectedCells | undefined
): SelectionTimeRange | undefined {
if (selectedCells?.times === undefined) {
return;
}
// time property of the cell data is an array, with the elements being
// the start times of the first and last cell selected.
return {
earliestMs: selectedCells.times[0] * 1000,
// Subtract 1 ms so search does not include start of next bucket.
latestMs: selectedCells.times[1] * 1000 - 1,
};
}

View file

@ -12,7 +12,6 @@ import { Action } from '../../explorer_dashboard_service';
import {
getClearedSelectedAnomaliesState,
getDefaultSwimlaneData,
getSelectionTimeRange,
getSwimlaneBucketInterval,
getViewBySwimlaneOptions,
} from '../../explorer_utils';
@ -23,6 +22,7 @@ import { jobSelectionChange } from './job_selection_change';
import { ExplorerState } from './state';
import { setInfluencerFilterSettings } from './set_influencer_filter_settings';
import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder';
import { getTimeBoundsFromSelection } from '../../hooks/use_selected_cells';
export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => {
const { type, payload } = nextAction;
@ -48,10 +48,6 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
nextState = jobSelectionChange(state, payload);
break;
case EXPLORER_ACTION.SET_BOUNDS:
nextState = { ...state, bounds: payload };
break;
case EXPLORER_ACTION.SET_CHARTS:
nextState = {
...state,
@ -144,7 +140,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
nextState = state;
}
if (nextState.selectedJobs === null || nextState.bounds === undefined) {
if (nextState.selectedJobs === null) {
return nextState;
}
@ -164,21 +160,16 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
selectedCells: nextState.selectedCells!,
});
const { bounds, selectedCells } = nextState;
const { selectedCells } = nextState;
const timerange = getSelectionTimeRange(
selectedCells,
swimlaneBucketInterval.asSeconds(),
bounds
);
const timeRange = getTimeBoundsFromSelection(selectedCells);
return {
...nextState,
swimlaneBucketInterval,
viewByLoadedForTimeFormatted:
selectedCells !== undefined && selectedCells.showTopFieldValues === true
? formatHumanReadableDateTime(timerange.earliestMs)
: null,
viewByLoadedForTimeFormatted: timeRange
? formatHumanReadableDateTime(timeRange.earliestMs)
: null,
viewBySwimlaneFieldName,
viewBySwimlaneOptions,
...checkSelectedCells(nextState),

View file

@ -17,7 +17,6 @@ import {
AnomaliesTableData,
ExplorerJob,
AppStateSelectedCells,
TimeRangeBounds,
OverallSwimlaneData,
SwimlaneData,
ViewBySwimLaneData,
@ -27,7 +26,6 @@ import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../explorer_constants';
export interface ExplorerState {
annotations: AnnotationsTable;
bounds: TimeRangeBounds | undefined;
chartsData: ExplorerChartsData;
fieldFormatsLoading: boolean;
filterActive: boolean;
@ -69,7 +67,6 @@ export function getExplorerDefaultState(): ExplorerState {
annotationsData: [],
aggregations: {},
},
bounds: undefined,
chartsData: getDefaultChartsData(),
fieldFormatsLoading: false,
filterActive: false,

View file

@ -255,7 +255,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
onBrushEnd: (e: HeatmapBrushEvent) => {
onCellsSelection({
lanes: e.y as string[],
times: e.x.map((v) => (v as number) / 1000),
times: e.x.map((v) => (v as number) / 1000) as [number, number],
type: swimlaneType,
viewByFieldName: swimlaneData.fieldName,
});
@ -326,7 +326,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
const startTime = (cell.datum.x as number) / 1000;
const payload = {
lanes: [String(cell.datum.y)],
times: [startTime, startTime + swimlaneData.interval],
times: [startTime, startTime + swimlaneData.interval] as [number, number],
type: swimlaneType,
viewByFieldName: swimlaneData.fieldName,
};

View file

@ -119,13 +119,6 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
from: globalState.time.from,
to: globalState.time.to,
});
const timefilterBounds = timefilter.getBounds();
// Only if both min/max bounds are valid moment times set the bounds.
// An invalid string restored from globalState might return `undefined`.
if (timefilterBounds?.min !== undefined && timefilterBounds?.max !== undefined) {
explorerService.setBounds(timefilterBounds);
}
}
}, [globalState?.time?.from, globalState?.time?.to]);
@ -208,7 +201,6 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
const [selectedCells, setSelectedCells] = useSelectedCells(
explorerUrlState,
setExplorerUrlState,
explorerState?.bounds,
explorerState?.swimlaneBucketInterval
);
@ -219,7 +211,6 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
const loadExplorerDataConfig =
explorerState !== undefined
? {
bounds: explorerState.bounds,
lastRefresh,
influencersFilterQuery: explorerState.influencersFilterQuery,
noInfluencersConfigured: explorerState.noInfluencersConfigured,