[ML] Persisted URL state for Anomalies table (#84314)

* [ML] Persisted URL state for Anomalies table

* [ML] adjust cell selection according to the time range
This commit is contained in:
Dima Arnautov 2020-11-25 16:49:45 +01:00 committed by GitHub
parent ea8ea4e4e0
commit 280ce7e5fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 20 deletions

View file

@ -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 (
<React.Fragment>
<>
<RuleEditorFlyout
setShowFunction={this.setShowRuleEditorFlyoutFunction}
unsetShowFunction={this.unsetShowRuleEditorFlyoutFunction}
@ -209,26 +229,46 @@ class AnomaliesTable extends Component {
className="ml-anomalies-table eui-textOverflowWrap"
items={tableData.anomalies}
columns={columns}
pagination={{
pageSizeOptions: [10, 25, 100],
initialPageSize: 25,
}}
pagination={pagination}
sorting={sorting}
itemId="rowId"
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
compressed={true}
rowProps={getRowProps}
data-test-subj="mlAnomaliesTable"
onTableChange={this.onTableChange}
/>
</React.Fragment>
</>
);
}
}
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
{...props}
tableState={tableState}
updateTableState={updateTableState}
/>
);
};
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 };

View file

@ -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<ExplorerAppState>) => void
setAppState: (update: Partial<ExplorerAppState>) => 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];
};

View file

@ -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;

View file

@ -205,7 +205,12 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ 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);

View file

@ -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.