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.