Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Simplify bulk action execution #144091

Merged
merged 27 commits into from
Nov 2, 2022

Conversation

maximpn
Copy link
Contributor

@maximpn maximpn commented Oct 27, 2022

Relates to: #142748

Summary

Improves typing and simplifies usage of bulk editing hooks.

Details

This patch includes the following changes

  • improved typing allows to strictly control the input
  • processing rules loading state for the rules table handling logic is moved inside the hooks
  • hooks are covered by unit tests

Checklist

@maximpn maximpn self-assigned this Oct 27, 2022
@maximpn maximpn added release_note:skip Skip the PR/issue when compiling release notes backport:skip This commit does not require backporting Team:Detections and Resp Security Detection Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. Team:Detection Rule Management Security Detection Rule Management Team v8.6.0 Feature:Detection Rules Security Solution rules and Detection Engine labels Oct 27, 2022
@maximpn maximpn linked an issue Oct 27, 2022 that may be closed by this pull request
edit?: BulkActionEditPayload[];
isDryRun?: boolean;
}
export type QueryOrIds = { query: string } | { ids: string[] };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using a discriminated union instead?

export type QueryOrIds = { query: string; ids?: undefined } | { query?: undefined; ids: string[] };

Copy link
Contributor Author

@maximpn maximpn Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A discriminated union is discriminated by some field. In this case there is no way to discriminate types so { query: string; ids?: undefined } | { query?: undefined; ids: string[] } is the same as { query: string } | { ids: string[] } which is the same as { query?: string; ids?: string[]; } if compare which object type can be assigned. So that it's possible to assign { query: 'some query', ids: ['id1', 'id2'] } to all of the above types but the under hood behaviour is undefined if both query and ids are specified.

string | string[] looks as a better option here

Copy link
Contributor

@xcrzx xcrzx Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A discriminated union is discriminated by some field. In this case there is no way to discriminate types so { query: string; ids?: undefined } | { query?: undefined; ids: string[] } is the same as { query: string } | { ids: string[] } which is the same as { query?: string; ids?: string[]; }

No, this is not correct. Both query and ids properties become a common discriminant in this case as they have mutually exclusive types in the union:

Screenshot 2022-10-28 at 10 54 51

Copy link
Contributor Author

@maximpn maximpn Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I see another picture. I tried it inside Kibana and TS playground. TS does not complain on passing { query: 'myQuery', ids: ['id'] }. It complains on an empty object and passing readonly array when a mutable one expected (which also can create some issues).

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, your QueryOrIds type is not correct 🙂 You should use this one:

type QueryOrIds = { query: string; ids?: undefined } | { query?: undefined; ids: string[] };

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sorry, it's my bad. I didn't pay enough attention to undefined. Basically the key is still assignable but this way it doesn't not create issues.

@maximpn maximpn force-pushed the simplify-bulk-action-execution branch 3 times, most recently from 40d5ffb to 8a74864 Compare October 30, 2022 17:50
@maximpn maximpn marked this pull request as ready for review October 30, 2022 21:49
@maximpn maximpn requested a review from a team as a code owner October 30, 2022 21:49
@maximpn maximpn requested a review from xcrzx October 30, 2022 21:49
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-detections-response (Team:Detections and Resp)

@elasticmachine
Copy link
Contributor

Pinging @elastic/security-solution (Team: SecuritySolution)

@maximpn
Copy link
Contributor Author

maximpn commented Oct 31, 2022

@elasticmachine merge upstream

2 similar comments
@maximpn
Copy link
Contributor Author

maximpn commented Oct 31, 2022

@elasticmachine merge upstream

@maximpn
Copy link
Contributor Author

maximpn commented Oct 31, 2022

@elasticmachine merge upstream

Copy link
Contributor

@xcrzx xcrzx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the refactoring, @maximpn. I tested the PR locally and looked through the changes. The code looks much cleaner now 👍
I've left a couple of suggestions, primarily minor improvements, nothing critical.

...(isDryRun ? { dry_run: isDryRun } : {}),
},
body: JSON.stringify(params),
query: dryRun ? { dry_run: true } : {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be further simplified?

Suggested change
query: dryRun ? { dry_run: true } : {},
query: { dry_run: dryRun },

type: BulkAction.edit;
editPayload: BulkActionEditPayload[];
} & QueryOrIds;
export type BulkActionDescriptor = PlainBulkActionDescriptor | EditBulkActionDescriptor;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe it's just my previous experience, but the descriptor suffix looks a bit confusing. In JavaScript, it usually refers to property descriptors, and, for example, in Python to property getters and setters. But I've never seen method arguments being named as descriptors.

Can we use a more consistent naming for these API method arguments, like BulkActionProps? The Props suffix is used for almost every other method in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that descriptor isn't the best match here though Prop looks like a universal stub. I finally decided to rename BulkAction to BulkActionType which is consistent with BulkActionEditType and rename BulkActionDescriptor to BulkAction which makes it concise and simple.

Comment on lines 30 to 36
setLoadingRules?.({
ids: queryOrIds.ids ?? [],
action: BulkAction.export,
});
return await mutateAsync(queryOrIds);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what changed, but previously we showed a loading indicator when exporting rules.

Before

Screen.Recording.2022-10-31.at.12.53.47.mov

After

Screen.Recording.2022-10-31.at.12.54.37.mov

Although, in the current main, I don't see the same behavior (loading overlay) for other bulk actions. Rules remain interactable during a preflight request.

Should we return the previous behavior for the export action and update other actions accordingly for consistency, or is it OK not to show the overlay?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, there was a problem which was fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the user clicked any of the bulk actions, we should "lock" the table: the user shouldn't be able to change the filters and the rule selection until the bulk action is sent or canceled. If we don't do this like in the 2nd video, there's a chance that the user could make some changes in the table which would lead to different arguments of the dry run request and the final request. Which would be a bug.

I think we should fix this. @maximpn could we fix this for all the bulk actions in a follow-up PR after merging this one? @xcrzx could you please write up a ticket for this issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a separate ticket: #144264


const executeBulkAction = useCallback(
async ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method looks much cleaner now 👍

await executeBulkActionFn(bulkActionDescriptor);
}

describe('useExecuteBulkAction', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the tests 🙌

import { explainBulkSuccess, summarizeBulkSuccess } from './translations';

export function showBulkSuccessToast(
toasts: UseAppToasts,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably make sense to transform this function into a hook, so toasts will become a detail of internal implementation rather than passed from outside. It could be used then as follows:

// No knowledge about the `toasts` service on the call side
const showBulkSuccessToast = useShowBulkSuccessToast()
showBulkSuccessToast(action, summary)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible to use hooks inside usual functions. downloadExportedRules uses showBulkSuccessToast as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think downloadExportedRules should not know anything about toasts and provide generic onError and onSuccess callbacks instead. But let's leave this improvement for another time.

Comment on lines 267 to 271
const params = {
action: BulkAction.export,
...(queryOrIds.query ? { query: queryOrIds.query } : {}),
...(queryOrIds.ids ? { ids: queryOrIds.ids } : {}),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that work?

Suggested change
const params = {
action: BulkAction.export,
...(queryOrIds.query ? { query: queryOrIds.query } : {}),
...(queryOrIds.ids ? { ids: queryOrIds.ids } : {}),
};
const params = {
action: BulkAction.export,
query: queryOrIds.query,
ids: queryOrIds.ids,
};

Comment on lines 241 to 248
const params = {
action: bulkActionDescriptor.type,
...(bulkActionDescriptor.query ? { query: bulkActionDescriptor.query } : {}),
...(bulkActionDescriptor.ids ? { ids: bulkActionDescriptor.ids } : {}),
...(bulkActionDescriptor.type === BulkAction.edit
? { edit: bulkActionDescriptor.editPayload }
: {}),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that work?

Suggested change
const params = {
action: bulkActionDescriptor.type,
...(bulkActionDescriptor.query ? { query: bulkActionDescriptor.query } : {}),
...(bulkActionDescriptor.ids ? { ids: bulkActionDescriptor.ids } : {}),
...(bulkActionDescriptor.type === BulkAction.edit
? { edit: bulkActionDescriptor.editPayload }
: {}),
};
const params = {
action: bulkActionDescriptor.type,
query: bulkActionDescriptor.query,
ids: bulkActionDescriptor.ids,
edit:
bulkActionDescriptor.type === BulkAction.edit ? bulkActionDescriptor.editPayload : undefined,
};

Comment on lines 238 to 236
bulkActionDescriptor: BulkActionDescriptor,
dryRun?: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use an object for method arguments for consistency with other methods in this file. It also makes it much more understandable for other developers to see what is being passed to the function.

Compare:

// What does this "true" mean? It is not possible to say without looking into the function's implementation
performBulkAction(bulkActionDescriptor, true)

// It's now clear that "true" enables the dry run mode
performBulkAction({ bulkActionDescriptor, dryRun: true })

@maximpn maximpn force-pushed the simplify-bulk-action-execution branch from 6788b84 to 353b482 Compare October 31, 2022 14:50
Comment on lines +37 to +43
case BulkActionType.enable:
case BulkActionType.disable: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I'm fine with this specific renaming, BulkActionType is a more precise name.

Comment on lines 30 to 36
setLoadingRules?.({
ids: queryOrIds.ids ?? [],
action: BulkAction.export,
});
return await mutateAsync(queryOrIds);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the user clicked any of the bulk actions, we should "lock" the table: the user shouldn't be able to change the filters and the rule selection until the bulk action is sent or canceled. If we don't do this like in the 2nd video, there's a chance that the user could make some changes in the table which would lead to different arguments of the dry run request and the final request. Which would be a bug.

I think we should fix this. @maximpn could we fix this for all the bulk actions in a follow-up PR after merging this one? @xcrzx could you please write up a ticket for this issue?

Copy link
Contributor

@banderror banderror left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maximpn Thank you for the clean-up and the added tests 👍 I just left a comment on some of the renamings that could have been avoided. Let's revert them if it won't take too much time.

I think an important thing to address would be to preserve the "locked" state of the table during the bulk action workflow: #144091 (comment)

import { explainBulkSuccess, summarizeBulkSuccess } from './translations';

export function showBulkSuccessToast(
toasts: UseAppToasts,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think downloadExportedRules should not know anything about toasts and provide generic onError and onSuccess callbacks instead. But let's leave this improvement for another time.


return useCallback(
(bulkActionType: BulkActionType) => {
const allRules = rulesTableContext?.state.isAllSelected ? rulesTableContext.state.rules : [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this logic. If isAllSelected is true, we get all rules from the table's state, but why an empty array otherwise? How it works if the user selects only a couple of rules, for example?

Copy link
Contributor Author

@maximpn maximpn Nov 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performBulkAction accepts a query or ids. It's clear what rules are processing if ids is passed but it's impossible to say anything about processing rules in the easy way when a query is passed. To handle a specific case when all rules are selected and a query is passed isAllSelected is used to detect that.

I've changed the hood name to reflect its purpose better.

return useMutation<BulkActionResponse, Error, BulkActionProps>(
(action: BulkActionProps) => performBulkAction(action),
return useMutation<BulkActionResponse, Error, BulkAction>(
(bulkAction: BulkAction) => performBulkAction({ bulkAction }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add back all the arguments accepted by the performBulkAction method.

Hooks located in api/hooks have to be generic. They are designed as an abstraction layer above the API client methods matching them one to one. And these hooks' only responsibility is to define query keys and invalidate other related queries. So hook's interface should match its underlying API method exactly.

We will prohibit direct API usage in the future so that these hooks will become the only API "client" facing users. See #143923

@maximpn maximpn force-pushed the simplify-bulk-action-execution branch from 01da93e to bc3202e Compare November 1, 2022 18:24
@maximpn maximpn force-pushed the simplify-bulk-action-execution branch from c7cfdb4 to 15d440c Compare November 1, 2022 19:34
@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
securitySolution 3197 3200 +3

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
securitySolution 9.5MB 9.5MB +1.8KB
Unknown metric groups

ESLint disabled in files

id before after diff
osquery 1 2 +1

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
fleet 57 63 +6
osquery 103 108 +5
securitySolution 439 443 +4
total +17

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
fleet 65 71 +6
osquery 104 110 +6
securitySolution 516 520 +4
total +18

History

  • 💔 Build #84447 failed c7cfdb432e27a0e8ee5289f76954ddb9bae8cf32
  • 💚 Build #84199 succeeded aaf692a74662b4a1211812c6a6158204281cfc6b
  • 💔 Build #84146 failed e54e9ff8f3810c5ca8be36abc3eececf0a53916f
  • 💔 Build #84116 failed fdf8c0855fc60ea3a82a9a7b199319fdc7af5873
  • 💔 Build #84002 failed 353b482a677a137190664fcbda7edd6d12eb6088

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @maximpn

Copy link
Contributor

@xcrzx xcrzx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested locally for the second time, but the code changes LGTM 👍 Thank you, @maximpn

@maximpn maximpn merged commit e391276 into elastic:main Nov 2, 2022
@maximpn maximpn deleted the simplify-bulk-action-execution branch November 2, 2022 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Detection Rules Security Solution rules and Detection Engine release_note:skip Skip the PR/issue when compiling release notes Team:Detection Rule Management Security Detection Rule Management Team Team:Detections and Resp Security Detection Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v8.6.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Security Solution] Improve bulk action executor
5 participants