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

[Cases] Improve bulk actions #142150

Merged
merged 38 commits into from
Oct 5, 2022
Merged

Conversation

cnasikas
Copy link
Member

@cnasikas cnasikas commented Sep 28, 2022

Summary

This PR refactors the current bulk actions and row actions of the cases table. Specifically:

  • Introduces actions that can be used across cases
  • Move modal & popover responsibilities outside of components
  • Restructure code files
  • Status bulk actions are available for all statuses
  • Disables changing the status of a case from the status column
  • Adds the ability to change statuses from the row actions
  • Changes bulk actions and row actions to be a context menu with panels.

Remarks:

  • Status is disabled if all selected case has the same status
  • Permissions are applied for each action

User flow

bulk_actions.mp4

Screenshots

Bulk actions

Screenshot 2022-09-30 at 1 16 20 PM

Screenshot 2022-09-30 at 1 15 48 PM

Row actions

Screenshot 2022-09-30 at 1 16 31 PM

Screenshot 2022-09-30 at 1 16 26 PM

Table

Screenshot 2022-10-04 at 3 09 08 PM

Disabled status

Screenshot 2022-10-03 at 3 27 39 PM

Permissions

Screenshot 2022-10-03 at 1 50 43 PM

Screenshot 2022-10-03 at 1 50 14 PM

Screenshot 2022-10-03 at 1 50 09 PM

Hide bulk actions

Screenshot 2022-10-04 at 12 58 19 PM
Screenshot 2022-10-04 at 12 58 12 PM

Checklist

Delete any items that are not applicable to this PR.

For maintainers

@cnasikas cnasikas added release_note:skip Skip the PR/issue when compiling release notes backport:skip This commit does not require backporting Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) Feature:Cases Cases feature v8.6.0 labels Sep 28, 2022
@cnasikas cnasikas self-assigned this Sep 28, 2022
Copy link
Contributor

@jonathan-buttner jonathan-buttner left a comment

Choose a reason for hiding this comment

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

Refactor is looking great! I left a few comments and questions.

One other thing I noticed while testing is that we used to disable the row actions (was only delete) when a row was selected but we're not doing that anymore. I think that's fine just wanted to make sure that was intended.

The new way:
image

Old way:

old way

selectedStatus: theCase.status,
});

const getPanels = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a useMemo?

Copy link
Member Author

Choose a reason for hiding this comment

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

I refactor it as you suggested in the comment below. For that reason, it needs to be a callback.

});

const getPanels = useCallback(
(): EuiContextMenuPanelDescriptor[] => [
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I don't know if this is more elegant, but it avoids the multiple checks on canUpdate and canDelete:
Pseudo code:

  const actionItems = [];
  const panels = [{id: 0, items: actionItems, ...}];

  if (canUpdate) {
    actionItems.push()
    panels.push({id: 1, title: 'status', ...})
  }

  const needsSeparator = actionItems.length > 0
  if (canDelete) {
    if (needsSeparator) {
      actionItems.push({ separator... })
    }

    actionItems.push({ delete... })
  }

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with you. It is much better as you suggested.

const getCasesWithChanges = (cases: Case[], status: CaseStatuses): Case[] =>
cases.filter((theCase) => theCase.status !== status);

const disableStatus = (cases: Case[], status: CaseStatuses) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we change the name to shouldDisableStatus?

I think we could simplify it since we don't need to create an array of cases:

const shouldDisableStatus = (...) => cases.some((theCase) => theCase.status !== status);

const getDeleteActionTitle = (totalCases: number): string =>
totalCases > 1 ? i18n.BULK_ACTION_DELETE_LABEL : i18n.DELETE_ACTION_LABEL;

export const useDeleteAction = ({ onAction, onActionSuccess, isDisabled }: UseActionProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it makes sense to move the permissions check within this hook (and the `use_status_action)? I know we'd still need it in the callers to do various other checks though. I think it'd encapsulate the logic a bit more though.

Copy link
Member Author

Choose a reason for hiding this comment

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

I moved the logic to the hooks as you suggested. To avoid the consumers doing permissions checks, the hooks will return canDelete and canUpdateStatus respectively.

return {
name: <EuiTextColor color={color}>{getDeleteActionTitle(selectedCases.length)}</EuiTextColor>,
onClick: () => openModal(selectedCases),
disabled: isDisabled,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this might be out of scope of this PR but I think we could probably get rid of this prop if we removed the bulk actions button when not rows are selected. Then we'd only hide this action if the user didn't have deletion permissions.

Gmail also displays additional "bulk" options after you've selected at least 1 row.

Copy link
Member Author

@cnasikas cnasikas Oct 4, 2022

Choose a reason for hiding this comment

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

I agree with you. We should hide the "bulk" options if the user has not selected any cases. @mdefazio What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

I am thinking of leaving the prop in case we need it in the future. I think it is nice for consumers to be able to disable the actions if needed. What do you think?

name: statuses[CaseStatuses.open].label,
icon: getStatusIcon(CaseStatuses.open),
onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses.open),
disabled: isDisabled || disableStatus(selectedCases, CaseStatuses.open),
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to add a hover text explaining why a status is disabled?

{
name: (
<FormattedMessage
defaultMessage="Status: {status}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think we need the current status listed? I feel like it's a bit redundant 🤷‍♂️

Maybe we change it to Change Status. The deletion action is phrased as Delete Case(s) so maybe it'd be more consistent to keep with the verbs.

Copy link
Member Author

Choose a reason for hiding this comment

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

This was a suggestion by @mdefazio. I will the decision to him 🙂

onActionSuccess,
});

const panels: EuiContextMenuPanelDescriptor[] = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we memoize this?

This looks similar to the one in use_actions: https://github.com/elastic/kibana/pull/142150/files#diff-42bdf17228f1685e18338f8651455291062e9007acaebd184986950f2ec82ebdR53

Should we place it in a hook?

Copy link
Member Author

Choose a reason for hiding this comment

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

I change it to a useCallback and refactored it as you suggested here #142150 (comment)

},
]
: []),
...(!isSelectorView && (permissions.update || permissions.delete) && actions ? [actions] : []),
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it'd be cleaner to have the use_actions hook manage whether a renderable component is returned? Since use_actions has to do permissions checks anyway maybe we could remove the checks here and have the hook return { actions: null } if it has no actions to display. So this line would look like:

...(!isSelectorView && actions ? [actions] : []),

Copy link
Member Author

Choose a reason for hiding this comment

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

Done! Btw, I refactored the columns as you suggested here #142150 (comment)

* Granular permission check for each action is performed
* in useBulkActions
*/
const showBulkActions = permissions.update || permissions.delete;
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment mentions that read permissions is required but we're not checking for here. Should this be const showBulkActions = permssions.read;

Would the user even be able to get to this component if they don't have read permissions though? Maybe we don't need to hide them?

Copy link
Member Author

Choose a reason for hiding this comment

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

This const showBulkActions = permssions.read; will show the bulk actions if they have read permissions. We only want to show the bulk actions if they can update or delete and not if they can read. If fixed the comment, it was wrong 🙂

@cnasikas
Copy link
Member Author

cnasikas commented Oct 4, 2022

Refactor is looking great! I left a few comments and questions.

One other thing I noticed while testing is that we used to disable the row actions (was only delete) when a row was selected but we're not doing that anymore. I think that's fine just wanted to make sure that was intended.

The new way: image

Old way:

old way

Thanks for the amazing feedback @jonathan-buttner! Regarding disabling the row actions, it was not intentional 🙂. Some tables in Kibana disable the actions some do not. The Rules table for example. @mdefazio What do you think? Should we disable the row actions when selecting multiple cases?

@mdefazio
Copy link
Contributor

mdefazio commented Oct 4, 2022

What do you think? Should we disable the row actions when selecting multiple cases?

After going through many of our list/table screens, it seems most of the time, the actions are disabled when selecting a row. This is also the pattern shown on the EUI examples.

So I would suggest we follow the common pattern here and disable the row actions. As we update our Rules page, I will be sure to note that there as well.

One other note that I had was to remove the 'bulk actions' when there is nothing selected. Since this menu is disabled anyway, there's no need to show this button/menu. (Also similar to the eui pattern linked above where the 'delete' button appears only after you've selected a row). I am reaching out to the other designers in Sec and O11y to make sure they are ok with this change, so we can all be consistent. (Is this a shared component that would update all case pages?)

@jonathan-buttner Thanks for catching this one.

@cnasikas cnasikas force-pushed the refactor_bulk_actions branch from 8702b89 to d96956b Compare October 4, 2022 11:26
@cnasikas
Copy link
Member Author

cnasikas commented Oct 4, 2022

What do you think? Should we disable the row actions when selecting multiple cases?

After going through many of our list/table screens, it seems most of the time, the actions are disabled when selecting a row. This is also the pattern shown on the EUI examples.

So I would suggest we follow the common pattern here and disable the row actions. As we update our Rules page, I will be sure to note that there as well.

One other note that I had was to remove the 'bulk actions' when there is nothing selected. Since this menu is disabled anyway, there's no need to show this button/menu. (Also similar to the eui pattern linked above where the 'delete' button appears only after you've selected a row). I am reaching out to the other designers in Sec and O11y to make sure they are ok with this change, so we can all be consistent. (Is this a shared component that would update all case pages?)

@jonathan-buttner Thanks for catching this one.

Thank you for the feedback @mdefazio! I removed the "bulk actions" when there is nothing selected in d96956b and disabled the row actions in 98a2032

(Is this a shared component that would update all case pages?)

Correct! The component is shared to all solutions.

const showActions = permissions.delete && !isSelectorView;

const columns = useCasesColumns({
const { columns } = useCasesColumns({
Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed, the vertical alignment of these controls is not quite right:

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Happy for this to be addressed separately as this hasn't been introduced in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you @peteharverson! Let's do it on another PR.

@peteharverson
Copy link
Contributor

For a user with read only permissions for cases, we still show the checkboxes but no bulk actions are available. Is it possible to hide the selection checkboxes as they don't serve any purpose do they?

image

@mdefazio
Copy link
Contributor

mdefazio commented Oct 4, 2022

For a user with read only permissions for cases, we still show the checkboxes but no bulk actions are available. Is it possible to hide the selection checkboxes as they don't serve any purpose do they?

image

Perhaps we instead disable the checkboxes rather than remove? We do this for other read-only content.

@cnasikas
Copy link
Member Author

cnasikas commented Oct 4, 2022

@peteharverson @mdefazio Done! I disabled the checkboxes for a user with read-only permissions.

Screenshot 2022-10-04 at 7 11 52 PM

Copy link
Contributor

@jonathan-buttner jonathan-buttner left a comment

Choose a reason for hiding this comment

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

Mostly nit, but another clarifying question about useCallback

@@ -927,6 +911,20 @@ describe('AllCasesListGeneric', () => {
expect(deleteCasesSpy).toHaveBeenCalledWith(['basic-case-id'], expect.anything());
});
});

it('should disable row actions when bulk selecting cases', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we add a test for disabling the row actions when a single checkbox is selected?

expect(res.queryByTestId('all-cases-modal')).toBeFalsy();
});

it('should hide bulk actions and row actions', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it hiding them by default because nothing was selected? If so, could we update the name to say why they are hidden, maybe something like:

should hide bulk actions and row actions when no rows are selected

Might be nice to have for the tests below too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Bulk actions and row actions should not be shown in the modal (on selector view). The user should not perform bulk actions when selecting a case from a modal. I changed the title to make it more clear.

const canDelete = deleteAction.canDelete;
const canUpdate = statusAction.canUpdateStatus;

const getPanels = useCallback((): EuiContextMenuPanelDescriptor[] => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I might be misunderstanding how useCallback works, but I think use callback returns a memoized function. So on each rerender the memory address of the function still stays the same but not necessarily the value it returns. That's helpful when we pass the function itself to another component. We're passing the value to another component because we're calling getPanels

panels={getPanels()}

I believe it will call getPanels on each rendering of the component and the array will get recreated each time.

I think if we were passing the function panels={getPanels} then we'd want a useCallback. I think we want useMemo since we ultimately want an array EuiContextMenuPanelDescriptor[] and not a function that returns an array right?

Copy link
Member Author

@cnasikas cnasikas Oct 5, 2022

Choose a reason for hiding this comment

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

oh, you are right!! My mistake 🙂! Fixed.

Copy link
Contributor

@jonathan-buttner jonathan-buttner left a comment

Choose a reason for hiding this comment

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

Great work!

@cnasikas cnasikas enabled auto-merge (squash) October 5, 2022 13:57
@cnasikas cnasikas merged commit 3469d64 into elastic:main Oct 5, 2022
@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
cases 498 502 +4

Async chunks

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

id before after diff
cases 371.0KB 372.7KB +1.7KB

History

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

cc @cnasikas

@cnasikas cnasikas deleted the refactor_bulk_actions branch October 5, 2022 13:58
WafaaNasr pushed a commit to WafaaNasr/kibana that referenced this pull request Oct 11, 2022
* Use react query for delete cases

* Convert delete to react query

* Convert update to react query

* Convert use_get_cases_status to react query

* Convert use_get_cases_metrics to react query

* Refresh metrics and statuses

* Show loading when updating cases

* Create query key builder

* Improve refreshing logic

* Improve delete messages

* Fix types and tests

* Improvements

* PR feedback

* Fix bug

* Refactor actions

* Add status actions

* Change status to panel

* Add status column

* Improvements

* Fix tests & types

* Remove comment

* Improve e2e tests

* Add unit tests

* Add permissions

* Fix delete e2e

* Disable statuses

* Fix i18n

* PR feedback

* Disable actions when cases are selected

* Improve modal tests

* Disables checkbox on read only

* PR feedback
WafaaNasr pushed a commit to WafaaNasr/kibana that referenced this pull request Oct 14, 2022
* Use react query for delete cases

* Convert delete to react query

* Convert update to react query

* Convert use_get_cases_status to react query

* Convert use_get_cases_metrics to react query

* Refresh metrics and statuses

* Show loading when updating cases

* Create query key builder

* Improve refreshing logic

* Improve delete messages

* Fix types and tests

* Improvements

* PR feedback

* Fix bug

* Refactor actions

* Add status actions

* Change status to panel

* Add status column

* Improvements

* Fix tests & types

* Remove comment

* Improve e2e tests

* Add unit tests

* Add permissions

* Fix delete e2e

* Disable statuses

* Fix i18n

* PR feedback

* Disable actions when cases are selected

* Improve modal tests

* Disables checkbox on read only

* PR feedback
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:Cases Cases feature release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v8.6.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants