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

[RAM] Add Snooze UI and Unsnooze API #128214

Merged
merged 15 commits into from
Mar 24, 2022
Merged

Conversation

Zacqary
Copy link
Contributor

@Zacqary Zacqary commented Mar 21, 2022

Summary

Closes #128011

Adds the Snooze UI to the rules list. Also adds the _unsnooze API, necessary for the Cancel Snooze button.

SnoozeUIRecording

Also handles "muting" as "snooze indefinitely":
Screen Shot 2022-03-22 at 4 48 10 PM

Checklist

Delete any items that are not applicable to this PR.

@Zacqary Zacqary added release_note:enhancement Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) Feature:Alerting/RulesManagement Issues related to the Rules Management UX v8.2.0 labels Mar 21, 2022
@Zacqary
Copy link
Contributor Author

Zacqary commented Mar 21, 2022

Not sure how to get the snooze end time to show up on the same line as "Snooze". EUI doesn't seem to like changing the text color in the middle of a context menu item the way it's spec'd out:

Screen Shot 2022-03-21 at 5 10 03 PM

Also having trouble getting the EUIButton to be any smaller than 112px, I think there might be some hard-coded EUI minimum size going on, so I compensated by shrinking the number field.

Screen Shot 2022-03-21 at 5 11 31 PM

@mdefazio does this look all right or should I keep hacking at EUI to get this to look exactly like the Figma?

@mdefazio
Copy link
Contributor

@Zacqary This looks great. Thanks for putting this together. I'm good with having 2 lines in the context menu for snooze time. The sizing of the number input and button also is no issue.

@gmmorris
Copy link
Contributor

@elasticmachine merge upstream


describe('snoozeAlertRoute', () => {
beforeAll(() => {
jest.useFakeTimers('modern');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it turns out Jest fake timers doesn't actually work for mocking the output of Date or moment several imports deep. We just didn't realize this because the snooze_rule test isn't actually validating that the snooze date is in the future.

I was able to use jest.spyOn to mock Date.now() for the dropdown UI tests, but I haven't been able to find a reliable solution to mock new Date() or moment() with no arguments. We can work around this by using new Date(Date.now() or moment(Date.now()) in any code where we want to test against the current date.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd love to help figure out a way, but I don't know if I understand the problem. Can you elaborate more on what you're trying to do and what's not working?

Copy link
Contributor Author

@Zacqary Zacqary Mar 24, 2022

Choose a reason for hiding this comment

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

There's code in the snooze API that uses Moment to determine if the snoozeEndTime is in the future:

moment().isBefore(snoozeEndTime);

And also in the new RuleStatusDropdown to determine how far in the future snoozeEndTime is:

moment(item.snoozeEndTime).fromNow(true)

We thought that using jest.useFakeTimers would enable us to change the return result of new Date() and Date.now(). For example, in this test we called jest.setSystemTime(new Date(2020, 3, 1)) to set the value of now to March 2020 (which, now that I think about it, oh god, why would anyone want to make anything permanently March 2020, oh no oh no oh no).

Turns out, this did not actually work as expected. It seems like Jest's fake timers system only applies to Date() called within the Jest tests themselves, and not to any modules that the tests import.

We didn't realize that this was broken because snooze_rule.test.ts isn't actually testing the part of the snooze API that tries to check if the snooze end time is in the future. But we thought that maybe it was.

It appears that moment.fromNow() uses Date.now() to determine the value of now, as opposed to moment([no arguments]) which uses new Date(). jest.spyOn allows us to mock the value of global.Date.now throughout all imports, but mocking global.Date requires you to mock the entire Date object and there's no easy way to just override the constructor.

So therefore, we can work around that limitation by doing something like:

moment(Date.now()).isBefore(snoozeEndTime);

in any code that we need to test.

@Zacqary Zacqary marked this pull request as ready for review March 22, 2022 17:48
@Zacqary Zacqary requested review from a team as code owners March 22, 2022 17:48
@elasticmachine
Copy link
Contributor

Pinging @elastic/response-ops (Team:ResponseOps)

@Zacqary Zacqary enabled auto-merge (squash) March 22, 2022 18:36
@Zacqary
Copy link
Contributor Author

Zacqary commented Mar 22, 2022

@mdefazio Wanted to make sure we're expecting that selecting "Enabled" from the context menu will also cancel the snooze? Or do we only want the Cancel Snooze button to do this?
SnoozeCancel

@@ -695,17 +680,31 @@ export const RulesList: React.FunctionComponent = () => {
{
field: 'executionStatus.status',
name: i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle',
{ defaultMessage: 'Status' }
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastResponseTitle',
Copy link
Contributor

@mgiota mgiota Mar 22, 2022

Choose a reason for hiding this comment

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

@Zacqary Make sure to update also the current status filter to be called Last response. I refer to this component https://github.com/elastic/kibana/blob/main/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx

Copy link
Contributor

Choose a reason for hiding this comment

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

A new Status filter needs to be created as well, but I would start with renaming current one to last response.

Copy link
Contributor

@jportner jportner left a comment

Choose a reason for hiding this comment

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

Platform security privilege changes look fine, though I have a couple of concerns below.

Comment on lines +1670 to +1682
private async unsnoozeWithOCC({ id }: { id: string }) {
const { attributes, version } = await this.unsecuredSavedObjectsClient.get<RawRule>(
'alert',
id
);

try {
await this.authorization.ensureAuthorized({
ruleTypeId: attributes.alertTypeId,
consumer: attributes.consumer,
operation: WriteOperations.Unsnooze,
entity: AlertingAuthorizationEntity.Rule,
});
Copy link
Contributor

@jportner jportner Mar 22, 2022

Choose a reason for hiding this comment

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

I haven't tested this, but it appears that the authorization check will never occur if: 1. the user tries to unsnooze an alert, and 2. the alert is not found. In that case it will throw a 404 error before the authZ check occurs (and thus, it will never get audited). At least that's what it appears to be doing.

It looks like the rest of the rules client functions the same way, too. Can you confirm? CC @XavierM

@mdefazio
Copy link
Contributor

Wanted to make sure we're expecting that selecting "Enabled" from the context menu will also cancel the snooze? Or do we only want the Cancel Snooze button to do this?

Yes, switching to 'Enabled' should also cancel the snooze.

Mentioning again here, the design issue has been updated with mockups for the rule detail view. This replaces the previous Stack rule Enable/Mute switches for this UI. The functionality of this badge should not change. I've moved it to the left in the new layout so we can keep the snooze timing in the same place. Also, since this uses an EUIBadge as does the rule type descriptor, I think it makes sense to group these closer instead of different sides of the header.

Copy link
Contributor

@chrisronline chrisronline left a comment

Choose a reason for hiding this comment

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

Made a first pass and it looks great! Some questions and thoughts

const rulesClient = context.alerting.getRulesClient();
const params = req.params;
try {
await rulesClient.unsnooze({ ...params });
Copy link
Contributor

Choose a reason for hiding this comment

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

Just for my own information, we feel comfortable spreading the parameters here because the schema should ensure there isn't anything extra in the object?

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 think so. This is how all the other routes do it.

updatedBy: await this.getUserName(),
updatedAt: new Date().toISOString(),
});
const updateOptions = { version };
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't seem to pass this in when update the rule from the task runner - why is this different here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No idea I'm just copypasting from what the snooze method does, and that was in turn copypasted from the muteAll method, so I assume it does something for some reason

const INDEFINITELY = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.remainingSnoozeIndefinite',
{ defaultMessage: 'Indefinitely' }
);
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 make these files smaller in the future? Like move some of these into a lib folder (which also makes it easier to unit test)?

}: ComponentOpts) => {
const [isEnabled, setIsEnabled] = useState<boolean>(item.enabled);
const [isSnoozed, setIsSnoozed] = useState<boolean>(isItemSnoozed(item));
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to listen for this? I think the entire component will re-render when the items change, or what's the use case here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is necessary because we store isEnabled and isSnooze in state to prevent the loading spinner from flickering before switching a status. Consequently we have to manually listen for any outside updates of the item.

Here's a recording of what it looks like if we don't use these useState/useEffect combos and just relied on an item prop update to re-render the dropdown:
Flicker

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm so it sounds like we're doing this to make the UI for responsive - in that we update the UI right after the user interaction while simultaneously sending the request to the server? That's the goal here? And without the useState/useEffect, we have to wait for the server to respond with the updated SO and if that takes a small amount of time (500ms to 1s) we see this weird flickering of the loading icon, then the updated value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No the server has already responded with the updated SO, the wait time is just from React. Keeping things in useState/useEffect gives us control over when things are rendered.

Note that this same code already existed in the previous switch control.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay we can defer to discuss this in the future as it's not a new patttern!

[setIntervalUnit]
);

const onApply1h = useCallback(() => applySnooze(1, 'h'), [applySnooze]);
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels weird to hard code it like this - is it possible to define the commonly used time frames in some constant at the top and then build out the UI from that list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From an i18n perspective, that could work if we were okay with Moment.js converting 1h to an hour instead of 1 hour but I don't think we are? Unfortunately I'm not sure how to customize Moment's humanize function to use 1 instead of an (or equivalent, depending on locale; fr uses une minute for example).

If we're okay with changing the language then this would work but otherwise we're constrained by i18n.

@Zacqary Zacqary requested a review from jportner March 23, 2022 20:49
Copy link
Contributor

@jportner jportner left a comment

Choose a reason for hiding this comment

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

Thank you!

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
triggersActionsUi 335 338 +3

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
triggersActionsUi 273 281 +8

Async chunks

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

id before after diff
triggersActionsUi 663.2KB 670.5KB +7.3KB

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
triggersActionsUi 96.6KB 97.0KB +435.0B
Unknown metric groups

API count

id before after diff
triggersActionsUi 285 295 +10

History

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

Copy link
Contributor

@chrisronline chrisronline left a comment

Choose a reason for hiding this comment

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

LGTM!

@Zacqary Zacqary merged commit d102213 into elastic:main Mar 24, 2022
@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 128214 or prevent reminders by adding the backport:skip label.

@kibanamachine kibanamachine added the backport missing Added to PRs automatically when the are determined to be missing a backport. label Mar 28, 2022
@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 128214 or prevent reminders by adding the backport:skip label.

1 similar comment
@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 128214 or prevent reminders by adding the backport:skip label.

@spalger spalger added the backport:skip This commit does not require backporting label Mar 30, 2022
@kibanamachine kibanamachine removed the backport missing Added to PRs automatically when the are determined to be missing a backport. label Mar 30, 2022
@tylersmalley tylersmalley added ci:cloud-deploy Create or update a Cloud deployment and removed ci:deploy-cloud labels Aug 17, 2022
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 ci:cloud-deploy Create or update a Cloud deployment Feature:Alerting/RulesManagement Issues related to the Rules Management UX release_note:enhancement Team:ResponseOps Label for the ResponseOps team (formerly the Cases and Alerting teams) v8.2.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RAM] Implement UI to snooze a single rule