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][Detections] Update read-only callouts from warning to info so they persist when dismissed #84904

Merged

Conversation

banderror
Copy link
Contributor

@banderror banderror commented Dec 3, 2020

Addresses: #76587

Summary

In this PR I'm doing basically 2 things:

  1. Making readonly callouts we have in Detections primary instead of warning and thus persistable in local storage (if a callout is dismissed, we remember it there)
  2. Creating a reusable implementation for that.

TODO:

  • Adjust all callouts used in Detections to be of type primary
  • Implement the local storage logic (dumb)
  • Implement the local storage logic (reusable)
  • Add a new user role: "Reader" (read-only user)
  • Add Cypress tests

Out of scope:

  • Add unit tests. I'll probably address this in a follow-up PR. Do you think it's worth it or better to wait until the rework (see the next point)?
  • Rework callouts to standardise them across Detections, Cases and other parts of the Security app? See my comment below in this PR.

Screen recordings

Quick demo of how this implementation works:

Additional notes

Cypress tests are based on the work done in #81866.

Checklist

Delete any items that are not applicable to this PR.

Found issues:

  • Keyboard navigation. There's a bug. If focus is on "+ Add filter" and I press Tab, it first goes to "+" and "Untitled timeline" at the bottom, and then goes back to "Dismiss" button of the callout. After that it works as expected. See https://puu.sh/GXMcF/93f194e7b1.png
  • Keyboard navigation in Safari is not working as expected on the page in general.

For maintainers

@banderror banderror added release_note:enhancement v8.0.0 Feature:Detection Rules Anything related to Security Solution's Detection Rules v7.11.0 Team:Detections and Resp Security Detection Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. labels Dec 3, 2020
@banderror banderror self-assigned this Dec 3, 2020
@banderror banderror force-pushed the make-readonly-callouts-of-type-info branch 4 times, most recently from 850e429 to a9bd663 Compare December 9, 2020 19:59
@banderror
Copy link
Contributor Author

banderror commented Dec 9, 2020

Further rework towards cases-like implementation?

In Cases, they have a slightly more sophisticated implementation of callouts. It's 5% bound to specifics of Cases and 95% generic from the code standpoint.

Maybe there's an opportunity to create a 100% generic implementation for use across the whole security_solution plugin that would give:

  • the users a standardised UI and us developers a common way to show and implement callouts: both single and multiple simultaneously;
  • grouping callouts of the same type so the user could click "Dismiss" only once (that grouping could be optional);
  • reusable components and logic;
  • easiness to drop a bunch of CallOutMessage instances to smth like reusable CallOutGroup to be able to show any number of callouts on a page.

Here's a screenshot. The first two callouts are our, from detections (and this is how it looks like in a real app when the user has read-only permissions). The next few callouts are rendered by the CaseCallOut component (added it for demo purposes).

Code-wise, here the page renders:

<NoWriteAlertsCallOut /> // 'You cannot change alert states'
<ReadOnlyCallOut /> // 'Rule permissions required'
<DemoCaseCallOut /> // 'Callout group title'

where DemoCaseCallOut is:

const message1 = {
  errorType: 'primary' as const,
  id: 'msg1',
  title: 'You cannot change alert states',
  description: <>{'You only have permissions to view alerts. If you need to update alert states (open or close alerts), contact your Kibana administrator.'}</>,
};

const message2 = {
  errorType: 'primary' as const,
  id: 'msg2',
  title: 'Rule permissions required',
  description: <>{'You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance.'}</>,
};

const message3 = {
  errorType: 'warning' as const,
  id: 'msg3',
  title: 'You cannot do nothing',
  description: <>{'You have no permissions...'}</>,
};

export const DemoCaseCallOut = () => {
  return <CaseCallOut title="Callout group title" messages={[message1, message2, message3]} />;
};

So do you think it might make sense to (eventually) rework/standardise callouts this way? Or we might never need it?

dismiss: (message: CallOutMessage) => void;
}

export const useCallOutStorage = (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this a right place for this kind of code?
We have some containers folders in the plugin, and they contain hooks (mostly, I think).
On the other hand, this guy useCallOutStorage is tightly coupled to the components (dumb and smart) represented in this folder.

More broad question: how do we structure components and business logic they use?

Comment on lines 24 to 35
const [visibilityState, setVisibilityState] = useMap(visibilityStateInitial);

const { getMessages, addMessage } = useMessagesStorage();

const dismissedMessagesKey = getDismissedMessagesStorageKey(namespace);

const getVisibleMessageIds = useCallback(() => {
return Object.entries(visibilityState)
.filter(([messageId, isVisible]) => isVisible)
.map(([messageId, isVisible]) => messageId);
}, [visibilityState]);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I implemented it with normal state and callback hooks, because at this point this is the approach I saw in the codebase.

Question: do we use useReducer, redux or maybe even rx or sagas to build app logic? I think I saw some redux usage somewhere within security_colution...
Do we have some conventions/agreements on how to build it?

return shouldRender ? <CallOut message={message} onDismiss={dismiss} /> : null;
};

export const CallOutSwitcher = memo(CallOutSwitcherComponent);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What's our approach to memoizing components?

I can see it's used pretty extensively throughout the code. The docs say "This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs."

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we typically memoize any exported component whether it be inline or on a new line like you've done here except in certain cases where they might be memoized elsewhere already (like some of the components in forms where the components they're passed to are memoized. Someone with a more in depth knowledge of the entire codebase might be able to give you more edge case examples)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, Davis 👍

@banderror banderror force-pushed the make-readonly-callouts-of-type-info branch from a9bd663 to 8e06ca4 Compare December 10, 2020 14:51
@spong
Copy link
Member

spong commented Dec 10, 2020

Further rework towards cases-like implementation?

In Cases, they have a slightly more sophisticated implementation of callouts. It's 5% bound to specifics of Cases and 95% generic from the code standpoint.

Maybe there's an opportunity to create a 100% generic implementation for use across the whole security_solution plugin that would give:

  • the users a standardised UI and us developers a common way to show and implement callouts: both single and multiple simultaneously;
  • grouping callouts of the same type so the user could click "Dismiss" only once (that grouping could be optional);
  • reusable components and logic;
  • easiness to drop a bunch of CallOutMessage instances to smth like reusable CallOutGroup to be able to show any number of callouts on a page.

Here's a screenshot. The first two callouts are our, from detections (and this is how it looks like in a real app when the user has read-only permissions). The next few callouts are rendered by the CaseCallOut component (added it for demo purposes).

So do you think it might make sense to (eventually) rework/standardise callouts this way? Or we might never need it?

I'm in agreement with all the above points @banderror. From a UX perspective seeing stacked callouts (of like type's) makes it a bit hard to decipher the message itself, and eats further into the vertical real estate. And from a developer perspective, having an easy to re-use component that supports local persistence and grouping would streamline implementations, and keep things consistent throughout the app. It's easy to miss if another component is adding a callout for the current page, so something where you could post your error to be added to the global list for the page sounds enticing (similar to how toasts are managed). I don't want to get too far into implementation land, but definitely support a future effort to better standardize callouts across the app and improve the dev experience as well (on top of what you've already added 🙂). This might even be something that @elastic/eui-design would be interested in driving, as I'm sure it'd be helpful to have across all solutions.

@banderror banderror force-pushed the make-readonly-callouts-of-type-info branch 2 times, most recently from fc5faf7 to 2092641 Compare December 14, 2020 20:21
@banderror banderror marked this pull request as ready for review December 14, 2020 20:22
@banderror banderror requested review from a team as code owners December 14, 2020 20:22
Copy link
Contributor

@rylnd rylnd left a comment

Choose a reason for hiding this comment

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

Code and behavior looks great so far! I still see a lot of questions, skipped tests, and TODOs so I don't think this is ready to approve/merge, but once that's cleaned up I think this'll be a quick LGTM 👍

});
});

const loadPageAsReadOnlyUser = (url: string) => {
Copy link
Member

Choose a reason for hiding this comment

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

Can we move all these methods to the tasks folder?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm 🤔 , to be honest I'm not sure!

I moved functions that seem to me truly reusable, like some actions on callouts.

This code, on the other hand, is quite coupled to the test suite:

const loadPageAsReadOnlyUser = (url: string) => {
  waitForPageWithoutDateRange(url, ROLES.reader);
  waitForPageTitleToBeShown();
};

const reloadPage = () => {
  cy.reload();
  waitForPageTitleToBeShown();
};

const waitForPageTitleToBeShown = () => {
  cy.get(PAGE_TITLE).should('be.visible');
};

Here, for example, it waits for the page title. This looks natural for this particular suite only because callouts are rendered on top of the page title. Other suites might have different needs in terms of what to wait for when we reloadPage() - they might need to wait less or more to pass and also be resilient.

Please let me know if you have any strong opinion on that!

Copy link
Member

Choose a reason for hiding this comment

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

I completely understand your point :)

We are used to work in the other way, so, if for whatever reason another colleague wants to extend the tests, will probably create a new spec in order to don't overload the current one. And most likely is going to check the tasks folder for the methods he can use rather than check the spec for them.

If you think that the scenario I mentioned is not going to happen and you want to keep it on the spec file I would suggest you to move it at the top of the file after the imports to give a bit of visibility ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure thing, easy! Did it. Thank you @MadameSheema !

@banderror banderror force-pushed the make-readonly-callouts-of-type-info branch from 2092641 to b336286 Compare December 15, 2020 13:36
Copy link
Contributor

@rylnd rylnd left a comment

Choose a reason for hiding this comment

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

This looks great! Truly appreciate the thought toward reusability here, and the comprehensive review materials are incredibly helpful!

It'll be nice to add some unit tests to these new pieces since the old ones weren't testing much, but in the meantime I think that the cypress tests cover us at the high level 👍


export const waitForPageWithoutDateRange = (url: string, role?: RolesType) => {
cy.visit(role ? getUrlWithRoute(role, url) : url);
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: We should capture this selector in a constant, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes +1, there's a room for cleanup in this file, I just didn't want to touch too many things in this PR.

@banderror banderror force-pushed the make-readonly-callouts-of-type-info branch from b336286 to c99615b Compare December 15, 2020 19:48

export const waitForPageWithoutDateRange = (url: string, role?: RolesType) => {
cy.visit(role ? getUrlWithRoute(role, url) : url);
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason this timeout is set so high?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure. What I did is took an existing function loginAndWaitForPageWithoutDateRange (it's above this one) and removed the login part from it to be able to implement the tests.

loginAndWaitForPageWithoutDateRange with this timeout was introduced by @XavierM along with the "defaultCommandTimeout": 120000 in cypress.json. See here:

1216b0f

I guess that's some legacy, because the current "defaultCommandTimeout": 60000

import { CallOutDescription } from './callout_description';
import { CallOutDismissButton } from './callout_dismiss_button';

export interface CallOutProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably don't need to export all these prop interfaces

import { CallOutMessage } from './callout_types';
import * as i18n from './translations';

export interface CallOutDismissButtonProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

same export comment as above

Copy link
Contributor

@dplumlee dplumlee left a comment

Choose a reason for hiding this comment

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

Mostly nits and questions. Great job!

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
securitySolution 2135 2145 +10

Async chunks

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

id before after diff
securitySolution 8.3MB 8.3MB +9.0KB

Distributable file count

id before after diff
default 47237 48007 +770

History

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

@banderror banderror merged commit 06907b8 into elastic:master Dec 15, 2020
banderror added a commit to banderror/kibana that referenced this pull request Dec 15, 2020
…g to info so they persist when dismissed (elastic#84904)

**Addresses:** elastic#76587

## Summary

In this PR I'm doing basically 2 things:

1. Making readonly callouts we have in Detections `primary` instead of `warning` and thus persistable in local storage (if a callout is dismissed, we remember it there)
2. Creating a reusable implementation for that.

TODO:

- [x] Adjust all callouts used in Detections to be of type `primary`
- [x] Implement the local storage logic (dumb)
- [x] Implement the local storage logic (reusable)
- [x] Add a new user role: "Reader" (read-only user)
- [x] Add Cypress tests

Out of scope:

- Add unit tests. I'll probably address this in a follow-up PR. Do you think it's worth it or better to wait until the rework (see the next point)?
- Rework callouts to standardise them across Detections, Cases and other parts of the Security app? See my comment below in this PR.

## Screen recordings

Quick demo of how this implementation works:

- [primary callouts](https://drive.google.com/file/d/1SYQd_ihKPvzlVUxELI8qNEqLBOkL18Gd/view?usp=sharing)
- [warning, danger](https://drive.google.com/file/d/1lrAFPyXNjOYSiEsUXxY_fjXsvmyDcdWY/view?usp=sharing) (callout types here were manually adjusted)

## Additional notes

Cypress tests are based on the work done in elastic#81866.

![](https://puu.sh/GXwOd/1c855cb03f.png)
@banderror banderror deleted the make-readonly-callouts-of-type-info branch December 15, 2020 22:46
spong pushed a commit that referenced this pull request Dec 16, 2020
…g to info so they persist when dismissed (#84904) (#86047)

**Addresses:** #76587

## Summary

In this PR I'm doing basically 2 things:

1. Making readonly callouts we have in Detections `primary` instead of `warning` and thus persistable in local storage (if a callout is dismissed, we remember it there)
2. Creating a reusable implementation for that.

TODO:

- [x] Adjust all callouts used in Detections to be of type `primary`
- [x] Implement the local storage logic (dumb)
- [x] Implement the local storage logic (reusable)
- [x] Add a new user role: "Reader" (read-only user)
- [x] Add Cypress tests

Out of scope:

- Add unit tests. I'll probably address this in a follow-up PR. Do you think it's worth it or better to wait until the rework (see the next point)?
- Rework callouts to standardise them across Detections, Cases and other parts of the Security app? See my comment below in this PR.

## Screen recordings

Quick demo of how this implementation works:

- [primary callouts](https://drive.google.com/file/d/1SYQd_ihKPvzlVUxELI8qNEqLBOkL18Gd/view?usp=sharing)
- [warning, danger](https://drive.google.com/file/d/1lrAFPyXNjOYSiEsUXxY_fjXsvmyDcdWY/view?usp=sharing) (callout types here were manually adjusted)

## Additional notes

Cypress tests are based on the work done in #81866.

![](https://puu.sh/GXwOd/1c855cb03f.png)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Detection Rules Anything related to Security Solution's Detection Rules release_note:enhancement Team:Detections and Resp Security Detection Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v7.11.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants