diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js index 0a2c67a3b0dcb..ebc782fe4625b 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js @@ -25,8 +25,9 @@ import { mlTableService } from '../../services/table_service'; import { RuleEditorFlyout } from '../rule_editor'; import { ml } from '../../services/ml_api_service'; import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants'; +import { usePageUrlState } from '../../util/url_state'; -class AnomaliesTable extends Component { +export class AnomaliesTableInternal extends Component { constructor(props) { super(props); @@ -145,8 +146,20 @@ class AnomaliesTable extends Component { }); }; + onTableChange = ({ page, sort }) => { + const { tableState, updateTableState } = this.props; + const result = { + pageIndex: page && page.index !== undefined ? page.index : tableState.pageIndex, + pageSize: page && page.size !== undefined ? page.size : tableState.pageSize, + sortField: sort && sort.field !== undefined ? sort.field : tableState.sortField, + sortDirection: + sort && sort.direction !== undefined ? sort.direction : tableState.sortDirection, + }; + updateTableState(result); + }; + render() { - const { bounds, tableData, filter, influencerFilter } = this.props; + const { bounds, tableData, filter, influencerFilter, tableState } = this.props; if ( tableData === undefined || @@ -186,8 +199,8 @@ class AnomaliesTable extends Component { const sorting = { sort: { - field: 'severity', - direction: 'desc', + field: tableState.sortField, + direction: tableState.sortDirection, }, }; @@ -199,8 +212,15 @@ class AnomaliesTable extends Component { }; }; + const pagination = { + pageIndex: tableState.pageIndex, + pageSize: tableState.pageSize, + totalItemCount: tableData.anomalies.length, + pageSizeOptions: [10, 25, 100], + }; + return ( - + <> - + ); } } -AnomaliesTable.propTypes = { + +export const getDefaultAnomaliesTableState = () => ({ + pageIndex: 0, + pageSize: 25, + sortField: 'severity', + sortDirection: 'desc', +}); + +export const AnomaliesTable = (props) => { + const [tableState, updateTableState] = usePageUrlState( + 'mlAnomaliesTable', + getDefaultAnomaliesTableState() + ); + return ( + + ); +}; + +AnomaliesTableInternal.propTypes = { bounds: PropTypes.object.isRequired, tableData: PropTypes.object, filter: PropTypes.func, influencerFilter: PropTypes.func, + tableState: PropTypes.object.isRequired, + updateTableState: PropTypes.func.isRequired, }; - -export { AnomaliesTable }; 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 7602954b4c8c3..f940fdc2387e2 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 @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; +import { Duration } from 'moment'; import { SWIMLANE_TYPE } from '../explorer_constants'; -import { AppStateSelectedCells } from '../explorer_utils'; +import { AppStateSelectedCells, TimeRangeBounds } from '../explorer_utils'; import { ExplorerAppState } from '../../../../common/types/ml_url_generator'; export const useSelectedCells = ( appState: ExplorerAppState, - setAppState: (update: Partial) => void + setAppState: (update: Partial) => void, + timeBounds: TimeRangeBounds | undefined, + bucketInterval: Duration | undefined ): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => { // keep swimlane selection, restore selectedCells from AppState const selectedCells = useMemo(() => { @@ -28,7 +31,7 @@ export const useSelectedCells = ( }, [JSON.stringify(appState?.mlExplorerSwimlane)]); const setSelectedCells = useCallback( - (swimlaneSelectedCells: AppStateSelectedCells) => { + (swimlaneSelectedCells?: AppStateSelectedCells) => { const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane, } as ExplorerAppState['mlExplorerSwimlane']; @@ -65,5 +68,47 @@ export const useSelectedCells = ( [appState?.mlExplorerSwimlane, selectedCells, setAppState] ); + /** + * Adjust cell selection with respect to the time boundaries. + * Reset it entirely when it out of range. + */ + useEffect(() => { + if ( + timeBounds === undefined || + selectedCells?.times === undefined || + bucketInterval === undefined + ) + return; + + let [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(); + + selectedFrom = Math.max(selectedFrom, rangeFrom); + + selectedTo = Math.min(selectedTo, rangeTo); + + const isSelectionOutOfRange = rangeFrom > selectedTo || rangeTo < selectedFrom; + + if (isSelectionOutOfRange) { + // reset selection + setSelectedCells(); + return; + } + + if (selectedFrom !== rangeFrom || selectedTo !== rangeTo) { + setSelectedCells({ + ...selectedCells, + times: [selectedFrom, selectedTo], + }); + } + }, [timeBounds, selectedCells, bucketInterval]); + return [selectedCells, setSelectedCells]; }; 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 ea9a8b5c18054..14b0a6033999c 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Duration } from 'moment'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; import { Dictionary } from '../../../../../common/types/common'; @@ -43,7 +44,7 @@ export interface ExplorerState { queryString: string; selectedCells: AppStateSelectedCells | undefined; selectedJobs: ExplorerJob[] | null; - swimlaneBucketInterval: any; + swimlaneBucketInterval: Duration | undefined; swimlaneContainerWidth: number; tableData: AnomaliesTableData; tableQueryString: string; 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 f8b4de6903ad2..2126cbceae6b1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -205,7 +205,12 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); - const [selectedCells, setSelectedCells] = useSelectedCells(explorerUrlState, setExplorerUrlState); + const [selectedCells, setSelectedCells] = useSelectedCells( + explorerUrlState, + setExplorerUrlState, + explorerState?.bounds, + explorerState?.swimlaneBucketInterval + ); useEffect(() => { explorerService.setSelectedCells(selectedCells); diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index fdc6dd135cd69..6cdc069096dcc 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -162,7 +162,7 @@ export const useUrlState = (accessor: Accessor) => { return [urlState, setUrlState]; }; -type AppStateKey = 'mlSelectSeverity' | 'mlSelectInterval' | MlPages; +type AppStateKey = 'mlSelectSeverity' | 'mlSelectInterval' | 'mlAnomaliesTable' | MlPages; /** * Hook for managing the URL state of the page.