[SIEM] [Detection Engine] Fixes Signals Table bulk selection issues (#56825) (#57943)

This PR fixes regressions around the bulk selection action, including:

* Incorrect total when opening/closing signals
* Selection total persisting after opening/closing signals
* `Select All` checkbox remaining selected after opening/closing signals
* Bulk action not being enabled after opening/closing via single action while others are selected

<details><summary>Before</summary>
<p>

![selection_persisted_wrong_count](https://user-images.githubusercontent.com/2946766/73814700-0d7faf80-47a1-11ea-9ec1-c6cb6a3d30c3.gif)

</p>
</details>

<details><summary>After</summary>
<p>

![selection_persisted_wrong_count_fix](https://user-images.githubusercontent.com/2946766/73814704-107aa000-47a1-11ea-9eb9-4f56d3d3f8c2.gif)
</p>
</details>

<details><summary>Before</summary>
<p>

![selection_disabled_bulk_action](https://user-images.githubusercontent.com/2946766/73814695-09ec2880-47a1-11ea-8a37-ae35a19979ab.gif)
</p>
</details>

<details><summary>After</summary>
<p>

![selection_disabled_bulk_action_fixed](https://user-images.githubusercontent.com/2946766/73814701-0f497300-47a1-11ea-8c73-3a131bda59f6.gif)
</p>
</details>

Delete any items that are not applicable to this PR.

- [ ] ~Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~
- [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~
- [ ] ~[Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~
- [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~
- [ ] ~This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)~
- [ ] ~This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~

- [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
This commit is contained in:
Garrett Spong 2020-02-18 19:04:17 -07:00 committed by GitHub
parent 741c30cdff
commit b66e88fd99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 8 deletions

View file

@ -161,7 +161,6 @@ const EventsViewerComponent: React.FC<Props> = ({
totalCountMinusDeleted
) ?? i18n.UNIT(totalCountMinusDeleted)}`;
// TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt)
return (
<>
<HeaderSection

View file

@ -19,7 +19,7 @@ export interface QueryTemplateProps {
startDate?: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FetchMoreOptionsArgs<TData, TVariables> = FetchMoreQueryOptions<any, any> &
export type FetchMoreOptionsArgs<TData, TVariables> = FetchMoreQueryOptions<any, any> &
FetchMoreOptions<TData, TVariables>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -40,6 +40,19 @@ export class QueryTemplate<
tiebreaker?: string
) => FetchMoreOptionsArgs<TData, TVariables>;
private refetch!: (variables?: TVariables) => Promise<ApolloQueryResult<TData>>;
private executeBeforeFetchMore!: ({ id }: { id?: string }) => void;
private executeBeforeRefetch!: ({ id }: { id?: string }) => void;
public setExecuteBeforeFetchMore = (val: ({ id }: { id?: string }) => void) => {
this.executeBeforeFetchMore = val;
};
public setExecuteBeforeRefetch = (val: ({ id }: { id?: string }) => void) => {
this.executeBeforeRefetch = val;
};
public setFetchMore = (
val: (fetchMoreOptions: FetchMoreOptionsArgs<TData, TVariables>) => PromiseApolloQueryResult
) => {
@ -52,6 +65,17 @@ export class QueryTemplate<
this.fetchMoreOptions = val;
};
public wrappedLoadMore = (newCursor: string, tiebreaker?: string) =>
this.fetchMore(this.fetchMoreOptions(newCursor, tiebreaker));
public setRefetch = (val: (variables?: TVariables) => Promise<ApolloQueryResult<TData>>) => {
this.refetch = val;
};
public wrappedLoadMore = (newCursor: string, tiebreaker?: string) => {
this.executeBeforeFetchMore({ id: this.props.id });
return this.fetchMore(this.fetchMoreOptions(newCursor, tiebreaker));
};
public wrappedRefetch = (variables?: TVariables) => {
this.executeBeforeRefetch({ id: this.props.id });
return this.refetch(variables);
};
}

View file

@ -8,7 +8,7 @@ import { getOr } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import React from 'react';
import { Query } from 'react-apollo';
import { compose } from 'redux';
import { compose, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns';
@ -26,6 +26,8 @@ import { createFilter } from '../helpers';
import { QueryTemplate, QueryTemplateProps } from '../query_template';
import { EventType } from '../../store/timeline/model';
import { timelineQuery } from './index.gql_query';
import { timelineActions } from '../../store/timeline';
import { SIGNALS_PAGE_TIMELINE_ID } from '../../pages/detection_engine/components/signals';
export interface TimelineArgs {
events: TimelineItem[];
@ -40,6 +42,7 @@ export interface TimelineArgs {
}
export interface TimelineQueryReduxProps {
clearSignalsState: ({ id }: { id?: string }) => void;
isInspected: boolean;
}
@ -71,6 +74,7 @@ class TimelineQueryComponent extends QueryTemplate<
public render() {
const {
children,
clearSignalsState,
eventType = 'raw',
id,
indexPattern,
@ -100,6 +104,7 @@ class TimelineQueryComponent extends QueryTemplate<
defaultIndex,
inspect: isInspected,
};
return (
<Query<GetTimelineQuery.Query, GetTimelineQuery.Variables>
query={timelineQuery}
@ -108,6 +113,10 @@ class TimelineQueryComponent extends QueryTemplate<
variables={variables}
>
{({ data, loading, fetchMore, refetch }) => {
this.setRefetch(refetch);
this.setExecuteBeforeRefetch(clearSignalsState);
this.setExecuteBeforeFetchMore(clearSignalsState);
const timelineEdges = getOr([], 'source.Timeline.edges', data);
this.setFetchMore(fetchMore);
this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({
@ -141,7 +150,7 @@ class TimelineQueryComponent extends QueryTemplate<
return children!({
id,
inspect: getOr(null, 'source.Timeline.inspect', data),
refetch,
refetch: this.wrappedRefetch,
loading,
totalCount: getOr(0, 'source.Timeline.totalCount', data),
pageInfo: getOr({}, 'source.Timeline.pageInfo', data),
@ -171,7 +180,16 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
clearSignalsState: ({ id }: { id?: string }) => {
if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) {
dispatch(timelineActions.clearEventsLoading({ id }));
dispatch(timelineActions.clearEventsDeleted({ id }));
}
},
});
export const TimelineQuery = compose<React.ComponentClass<OwnProps>>(
connect(makeMapStateToProps),
connect(makeMapStateToProps, mapDispatchToProps),
withKibana
)(TimelineQueryComponent);

View file

@ -51,7 +51,7 @@ import {
} from './types';
import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers';
const SIGNALS_PAGE_TIMELINE_ID = 'signals-page';
export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page';
interface ReduxProps {
globalQuery: Query;

View file

@ -114,6 +114,7 @@ const SignalsUtilityBarComponent: React.FC<SignalsUtilityBarProps> = ({
export const SignalsUtilityBar = React.memo(
SignalsUtilityBarComponent,
(prevProps, nextProps) =>
prevProps.areEventsLoading === nextProps.areEventsLoading &&
prevProps.selectedEventIds === nextProps.selectedEventIds &&
prevProps.totalCount === nextProps.totalCount &&
prevProps.showClearSelection === nextProps.showClearSelection

View file

@ -1161,11 +1161,22 @@ export const setDeletedTimelineEvents = ({
? union(timeline.deletedEventIds, eventIds)
: timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId));
const selectedEventIds = Object.fromEntries(
Object.entries(timeline.selectedEventIds).filter(
([selectedEventId]) => !deletedEventIds.includes(selectedEventId)
)
);
const isSelectAllChecked =
Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false;
return {
...timelineById,
[id]: {
...timeline,
deletedEventIds,
selectedEventIds,
isSelectAllChecked,
},
};
};