Skip to content

Commit 280ce7e

Browse files
authored
[ML] Persisted URL state for Anomalies table (#84314)
* [ML] Persisted URL state for Anomalies table * [ML] adjust cell selection according to the time range
1 parent ea8ea4e commit 280ce7e

File tree

5 files changed

+111
-20
lines changed

5 files changed

+111
-20
lines changed

x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import { mlTableService } from '../../services/table_service';
2525
import { RuleEditorFlyout } from '../rule_editor';
2626
import { ml } from '../../services/ml_api_service';
2727
import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants';
28+
import { usePageUrlState } from '../../util/url_state';
2829

29-
class AnomaliesTable extends Component {
30+
export class AnomaliesTableInternal extends Component {
3031
constructor(props) {
3132
super(props);
3233

@@ -145,8 +146,20 @@ class AnomaliesTable extends Component {
145146
});
146147
};
147148

149+
onTableChange = ({ page, sort }) => {
150+
const { tableState, updateTableState } = this.props;
151+
const result = {
152+
pageIndex: page && page.index !== undefined ? page.index : tableState.pageIndex,
153+
pageSize: page && page.size !== undefined ? page.size : tableState.pageSize,
154+
sortField: sort && sort.field !== undefined ? sort.field : tableState.sortField,
155+
sortDirection:
156+
sort && sort.direction !== undefined ? sort.direction : tableState.sortDirection,
157+
};
158+
updateTableState(result);
159+
};
160+
148161
render() {
149-
const { bounds, tableData, filter, influencerFilter } = this.props;
162+
const { bounds, tableData, filter, influencerFilter, tableState } = this.props;
150163

151164
if (
152165
tableData === undefined ||
@@ -186,8 +199,8 @@ class AnomaliesTable extends Component {
186199

187200
const sorting = {
188201
sort: {
189-
field: 'severity',
190-
direction: 'desc',
202+
field: tableState.sortField,
203+
direction: tableState.sortDirection,
191204
},
192205
};
193206

@@ -199,8 +212,15 @@ class AnomaliesTable extends Component {
199212
};
200213
};
201214

215+
const pagination = {
216+
pageIndex: tableState.pageIndex,
217+
pageSize: tableState.pageSize,
218+
totalItemCount: tableData.anomalies.length,
219+
pageSizeOptions: [10, 25, 100],
220+
};
221+
202222
return (
203-
<React.Fragment>
223+
<>
204224
<RuleEditorFlyout
205225
setShowFunction={this.setShowRuleEditorFlyoutFunction}
206226
unsetShowFunction={this.unsetShowRuleEditorFlyoutFunction}
@@ -209,26 +229,46 @@ class AnomaliesTable extends Component {
209229
className="ml-anomalies-table eui-textOverflowWrap"
210230
items={tableData.anomalies}
211231
columns={columns}
212-
pagination={{
213-
pageSizeOptions: [10, 25, 100],
214-
initialPageSize: 25,
215-
}}
232+
pagination={pagination}
216233
sorting={sorting}
217234
itemId="rowId"
218235
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
219236
compressed={true}
220237
rowProps={getRowProps}
221238
data-test-subj="mlAnomaliesTable"
239+
onTableChange={this.onTableChange}
222240
/>
223-
</React.Fragment>
241+
</>
224242
);
225243
}
226244
}
227-
AnomaliesTable.propTypes = {
245+
246+
export const getDefaultAnomaliesTableState = () => ({
247+
pageIndex: 0,
248+
pageSize: 25,
249+
sortField: 'severity',
250+
sortDirection: 'desc',
251+
});
252+
253+
export const AnomaliesTable = (props) => {
254+
const [tableState, updateTableState] = usePageUrlState(
255+
'mlAnomaliesTable',
256+
getDefaultAnomaliesTableState()
257+
);
258+
return (
259+
<AnomaliesTableInternal
260+
{...props}
261+
tableState={tableState}
262+
updateTableState={updateTableState}
263+
/>
264+
);
265+
};
266+
267+
AnomaliesTableInternal.propTypes = {
228268
bounds: PropTypes.object.isRequired,
229269
tableData: PropTypes.object,
230270
filter: PropTypes.func,
231271
influencerFilter: PropTypes.func,
272+
tableState: PropTypes.object.isRequired,
273+
updateTableState: PropTypes.func.isRequired,
232274
};
233-
234-
export { AnomaliesTable };

x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { useCallback, useMemo } from 'react';
7+
import { useCallback, useEffect, useMemo } from 'react';
8+
import { Duration } from 'moment';
89
import { SWIMLANE_TYPE } from '../explorer_constants';
9-
import { AppStateSelectedCells } from '../explorer_utils';
10+
import { AppStateSelectedCells, TimeRangeBounds } from '../explorer_utils';
1011
import { ExplorerAppState } from '../../../../common/types/ml_url_generator';
1112

1213
export const useSelectedCells = (
1314
appState: ExplorerAppState,
14-
setAppState: (update: Partial<ExplorerAppState>) => void
15+
setAppState: (update: Partial<ExplorerAppState>) => void,
16+
timeBounds: TimeRangeBounds | undefined,
17+
bucketInterval: Duration | undefined
1518
): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => {
1619
// keep swimlane selection, restore selectedCells from AppState
1720
const selectedCells = useMemo(() => {
@@ -28,7 +31,7 @@ export const useSelectedCells = (
2831
}, [JSON.stringify(appState?.mlExplorerSwimlane)]);
2932

3033
const setSelectedCells = useCallback(
31-
(swimlaneSelectedCells: AppStateSelectedCells) => {
34+
(swimlaneSelectedCells?: AppStateSelectedCells) => {
3235
const mlExplorerSwimlane = {
3336
...appState.mlExplorerSwimlane,
3437
} as ExplorerAppState['mlExplorerSwimlane'];
@@ -65,5 +68,47 @@ export const useSelectedCells = (
6568
[appState?.mlExplorerSwimlane, selectedCells, setAppState]
6669
);
6770

71+
/**
72+
* Adjust cell selection with respect to the time boundaries.
73+
* Reset it entirely when it out of range.
74+
*/
75+
useEffect(() => {
76+
if (
77+
timeBounds === undefined ||
78+
selectedCells?.times === undefined ||
79+
bucketInterval === undefined
80+
)
81+
return;
82+
83+
let [selectedFrom, selectedTo] = selectedCells.times;
84+
85+
const rangeFrom = timeBounds.min!.unix();
86+
/**
87+
* Because each cell on the swim lane represent the fixed bucket interval,
88+
* the selection range could be outside of the time boundaries with
89+
* correction within the bucket interval.
90+
*/
91+
const rangeTo = timeBounds.max!.unix() + bucketInterval.asSeconds();
92+
93+
selectedFrom = Math.max(selectedFrom, rangeFrom);
94+
95+
selectedTo = Math.min(selectedTo, rangeTo);
96+
97+
const isSelectionOutOfRange = rangeFrom > selectedTo || rangeTo < selectedFrom;
98+
99+
if (isSelectionOutOfRange) {
100+
// reset selection
101+
setSelectedCells();
102+
return;
103+
}
104+
105+
if (selectedFrom !== rangeFrom || selectedTo !== rangeTo) {
106+
setSelectedCells({
107+
...selectedCells,
108+
times: [selectedFrom, selectedTo],
109+
});
110+
}
111+
}, [timeBounds, selectedCells, bucketInterval]);
112+
68113
return [selectedCells, setSelectedCells];
69114
};

x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { Duration } from 'moment';
78
import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns';
89
import { Dictionary } from '../../../../../common/types/common';
910

@@ -43,7 +44,7 @@ export interface ExplorerState {
4344
queryString: string;
4445
selectedCells: AppStateSelectedCells | undefined;
4546
selectedJobs: ExplorerJob[] | null;
46-
swimlaneBucketInterval: any;
47+
swimlaneBucketInterval: Duration | undefined;
4748
swimlaneContainerWidth: number;
4849
tableData: AnomaliesTableData;
4950
tableQueryString: string;

x-pack/plugins/ml/public/application/routing/routes/explorer.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,12 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
205205
const [tableInterval] = useTableInterval();
206206
const [tableSeverity] = useTableSeverity();
207207

208-
const [selectedCells, setSelectedCells] = useSelectedCells(explorerUrlState, setExplorerUrlState);
208+
const [selectedCells, setSelectedCells] = useSelectedCells(
209+
explorerUrlState,
210+
setExplorerUrlState,
211+
explorerState?.bounds,
212+
explorerState?.swimlaneBucketInterval
213+
);
209214

210215
useEffect(() => {
211216
explorerService.setSelectedCells(selectedCells);

x-pack/plugins/ml/public/application/util/url_state.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const useUrlState = (accessor: Accessor) => {
162162
return [urlState, setUrlState];
163163
};
164164

165-
type AppStateKey = 'mlSelectSeverity' | 'mlSelectInterval' | MlPages;
165+
type AppStateKey = 'mlSelectSeverity' | 'mlSelectInterval' | 'mlAnomaliesTable' | MlPages;
166166

167167
/**
168168
* Hook for managing the URL state of the page.

0 commit comments

Comments
 (0)