-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Conversation
edit?: BulkActionEditPayload[]; | ||
isDryRun?: boolean; | ||
} | ||
export type QueryOrIds = { query: string } | { ids: string[] }; |
There was a problem hiding this comment.
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[] };
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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[] };
There was a problem hiding this comment.
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.
40d5ffb
to
8a74864
Compare
Pinging @elastic/security-detections-response (Team:Detections and Resp) |
Pinging @elastic/security-solution (Team: SecuritySolution) |
@elasticmachine merge upstream |
There was a problem hiding this 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 } : {}, |
There was a problem hiding this comment.
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?
query: dryRun ? { dry_run: true } : {}, | |
query: { dry_run: dryRun }, |
type: BulkAction.edit; | ||
editPayload: BulkActionEditPayload[]; | ||
} & QueryOrIds; | ||
export type BulkActionDescriptor = PlainBulkActionDescriptor | EditBulkActionDescriptor; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
setLoadingRules?.({ | ||
ids: queryOrIds.ids ?? [], | ||
action: BulkAction.export, | ||
}); | ||
return await mutateAsync(queryOrIds); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ({ |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
const params = { | ||
action: BulkAction.export, | ||
...(queryOrIds.query ? { query: queryOrIds.query } : {}), | ||
...(queryOrIds.ids ? { ids: queryOrIds.ids } : {}), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would that work?
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, | |
}; |
const params = { | ||
action: bulkActionDescriptor.type, | ||
...(bulkActionDescriptor.query ? { query: bulkActionDescriptor.query } : {}), | ||
...(bulkActionDescriptor.ids ? { ids: bulkActionDescriptor.ids } : {}), | ||
...(bulkActionDescriptor.type === BulkAction.edit | ||
? { edit: bulkActionDescriptor.editPayload } | ||
: {}), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would that work?
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, | |
}; |
bulkActionDescriptor: BulkActionDescriptor, | ||
dryRun?: boolean |
There was a problem hiding this comment.
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 })
6788b84
to
353b482
Compare
...urity_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts
Outdated
Show resolved
Hide resolved
case BulkActionType.enable: | ||
case BulkActionType.disable: { |
There was a problem hiding this comment.
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.
setLoadingRules?.({ | ||
ids: queryOrIds.ids ?? [], | ||
action: BulkAction.export, | ||
}); | ||
return await mutateAsync(queryOrIds); |
There was a problem hiding this comment.
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?
There was a problem hiding this 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, |
There was a problem hiding this comment.
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 : []; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 }), |
There was a problem hiding this comment.
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
BulkAction -> BulkActionType (which is consistent with BulkActionEditType) BulkActionDescriptor -> BulkAction
01da93e
to
bc3202e
Compare
c7cfdb4
to
15d440c
Compare
💚 Build Succeeded
Metrics [docs]Module Count
Async chunks
Unknown metric groupsESLint disabled in files
ESLint disabled line counts
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: cc @maximpn |
There was a problem hiding this 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
Relates to: #142748
Summary
Improves typing and simplifies usage of bulk editing hooks.
Details
This patch includes the following changes
Checklist