diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index c91e3615514f..fb432189c6dd 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -90,7 +90,7 @@ export interface ExplorerAppState { mlExplorerSwimlane: { selectedType?: 'overall' | 'viewBy'; selectedLanes?: string[]; - selectedTimes?: number[]; + selectedTimes?: [number, number]; showTopFieldValues?: boolean; viewByFieldName?: string; viewByPerPage?: number; diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 5712f3c4843b..297da075d6d6 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -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 | 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(), []); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index 7440bf321341..3f5f016fc365 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -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', diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 8a95e5c6adbd..2e718990d749 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -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 }); }, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts index 94690b74a6f8..143a281be695 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -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; } diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index f940fdc2387e..7a279afc22ae 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -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) => 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, + }; +} diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index 546f782f4d86..04aebe2a39d8 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -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), diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index 14b0a6033999..0ae09a794bc5 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -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, diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index b166d90f040a..101d4857a89b 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -255,7 +255,7 @@ export const SwimlaneContainer: FC = ({ 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 = ({ 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, }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 2126cbceae6b..15ce3847f4d9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -119,13 +119,6 @@ const ExplorerUrlStateManager: FC = ({ 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 = ({ jobsWithTim const [selectedCells, setSelectedCells] = useSelectedCells( explorerUrlState, setExplorerUrlState, - explorerState?.bounds, explorerState?.swimlaneBucketInterval ); @@ -219,7 +211,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const loadExplorerDataConfig = explorerState !== undefined ? { - bounds: explorerState.bounds, lastRefresh, influencersFilterQuery: explorerState.influencersFilterQuery, noInfluencersConfigured: explorerState.noInfluencersConfigured,