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

fix #5742 feat(nimbus): implement UI for manual enrollment end #5852

Merged
merged 1 commit into from Jul 15, 2021

Conversation

lmorchard
Copy link
Contributor

@lmorchard lmorchard commented Jun 26, 2021

Because:

  • we want to add UI to enable manual enrollment end

This commit:

  • add isEnrollmentPaused to GQL experiment input for consistency with
    serialized experiment type, mapped to is_paused in Django model

  • add support for isEnrollmentPausePending to indicate when the review
    is in progress

  • rework useChangeOperationMutation to indicate loading while awaiting
    experiment refetch

  • update "rejecting this review" copy to "rejecting the request to
    {actionDescription}"

  • refactors handling of strings describing lifecycle review flows, moves
    all the strings into shared constants

  • refactors the Settings page and ChangeApprovalOperations components

  • adds more exhaustive stories for launch, end enrollment, and end cases
    on PageSummary

  • reworks rejection reason display to more properly use old_status and
    old_status_next to determine what kind of review was rejected

  • adds more status-based logic to make the correct GQL queries on
    approval / rejection of review

Co-authored-by: Lauren Zugai lauren@zugai.com

@lmorchard
Copy link
Contributor Author

Opening as a draft with work in progress because:

  • There's still work to be done around tests & coverage

  • I'll be out for another week after the wellness week and someone else might want to grab the baton from me

There's also possibly some some thinking to do about how manual enrollment end is handled on the backend, which could require some further tweaks.

For automated pause, we do not set is_paused on the Experimenter side first.

Instead, we set "isEnrollmentPaused" in the Remote Settings record. Then, we wait until it's approved in Remote Settings before setting is_paused=True in Experimenter. That means, on the Experimenter side, we never show that enrollment is paused until it's fully resolved on the Remote Settings side.

For manual pause, we might want to set is_paused=True on the Experimenter side first.

Otherwise, if we follow that same pattern as automated pause, then the only thing that represents a request to pause is status=LIVE / status_next=LIVE. (Because is_paused=False until the very end.)

Could also be a thing to punt to follow-ups, if/when we want other status=LIVE / status_next=LIVE updates other than enrollment ending.

@lmorchard
Copy link
Contributor Author

Rebased against master to pick up the changes to PageSummary and approve / reject handlers

@LZoog
Copy link
Contributor

LZoog commented Jul 7, 2021

Responding to Les' comment + leaving some notes for myself and the reviewer:

Per Jared's comment here, automatically triggered pausing will be removed in favor of requiring manual end enrollment.

Otherwise, if we follow that same pattern as automated pause, then the only thing that represents a request to pause is status=LIVE / status_next=LIVE. (Because is_paused=False until the very end.)

I think for now, this is OK since there's not any other statuses/flows that use this set and we won't need to differentiate between automatic or manual enrollment requests. That status set (which we're calling pauseRequested) does seem a little more fragile than say, endRequested, which checks for status=LIVE / status_next=COMPLETE and I could see that perhaps is_paused (isEnrollmentPaused) could potentially do a little more in the FE for us but we are using isEnrollmentPaused in conjunction with pauseRequested to conditionally show the EndEnrollment component. 🤔 FWIW we also aren't using it for end enrollment RS timeouts either since we just check for timeout on the experiment.

If we do have another flow later that needs status=LIVE / status_next=LIVE, we should be able to alter pauseRequested on the FE fairly easily. With all of this said, I think this PR mostly just needed some hard squinting at, test fixes and additions, warning fixes, and a couple follow up issues filed, all of which I've got underway. Just wanted to dump my thoughts here before finishing those up.

Comment on lines 128 to 130
changelogMessage: CHANGELOG_MESSAGES.REQUESTED_REVIEW_END,
status: NimbusExperimentStatus.LIVE,
statusNext: NimbusExperimentStatus.COMPLETE,
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW we should end up using shared common props for flows when we do #5851. I didn't pull in the mocks from PageSummary since we probably need to pull those shared props into a separate file at a higher level which would require some more refactoring and make this PR even longer, plus that's what #5851 is for.

@LZoog
Copy link
Contributor

LZoog commented Jul 9, 2021

To follow up here, yesterday I pushed FE test fixes and additions plus fixed some compilation warnings and made a couple of other tweaks, but left BE test fixes (5 fail) since we need to wait until there's a PR closing #5739.

I also already rebased/squashed the commits because of habit and I didn't realize until after pushing that this PR is probably going to stick around by the time @lmorchard gets back. 😅

@lmorchard
Copy link
Contributor Author

Going to pick this back up tomorrow - PR #5935 will result in a totally different backend situation along with requiring we send is_paused={True,False} from the frontend.

So, some reworking will be required. But, hopefully not more than ripping out the backend changes in this PR and just updating the GQL queries on the frontend though.

@lmorchard lmorchard force-pushed the 5742-end-enrollment-ui branch 2 times, most recently from 77081c0 to 4f32b39 Compare July 13, 2021 22:51
@lmorchard lmorchard marked this pull request as ready for review July 13, 2021 23:22
@lmorchard
Copy link
Contributor Author

Alright, I think this is ready for review now. Stripped out the defunct backend changes, added some to allow mutating isEnrollmentPaused / is_paused. Also tossed in a fix for #5927 as a quick bonus. The Co-authored-by might disqualify @LZoog from reviewing, since she helped carry this PR for the week I was out and wrapped up test coverage.

@jaredlockhart
Copy link
Collaborator

jaredlockhart commented Jul 14, 2021

Clicking the End Experiment button sends an API request but doesn't update the UI. Refreshing the page kicked it over, but looks like we're missing something there.

2021-07-14 10-20-39 2021-07-14 10_21_26

Edit: Same behaviour for End Enrollment

@jaredlockhart
Copy link
Collaborator

Approve dialog missing some text for End Enrollment:

Screen Shot 2021-07-14 at 10 22 58 AM

@jaredlockhart
Copy link
Collaborator

jaredlockhart commented Jul 14, 2021

Clicking reject also seems to not update UI:

2021-07-14 10-24-08 2021-07-14 10_24_36

Edit: also looks like it's not firing an API request from looking at my server logs

Copy link
Collaborator

@jaredlockhart jaredlockhart left a comment

Choose a reason for hiding this comment

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

Hit a bunch of snags up front, added some gifs. I'll stop testing there for now and look again when I can get further through the flow.

@LZoog
Copy link
Contributor

LZoog commented Jul 14, 2021

Clicking the End Experiment button sends an API request but doesn't update the UI. Refreshing the page kicked it over, but looks like we're missing something there.

@lmorchard FWIW I think that's taken care of in #5939 if you rebase

@lmorchard
Copy link
Contributor Author

lmorchard commented Jul 14, 2021

I see what happened with the missing text & GQL query: I added an isEnrollmentPaused condition to status.pauseRequested and that broke things. Turns out isEnrollmentPaused is now derived from is_paused_published rather than is_paused, and that only turns true after review completion. Might need to add another property to represent is_paused vs is_paused_published

@lmorchard
Copy link
Contributor Author

@lmorchard FWIW I think that's taken care of in #5939 if you rebase

Hmm, that only takes care of the end experiment UI, not this new UI. Need to copy that over / see if it can be more generalized

@lmorchard lmorchard force-pushed the 5742-end-enrollment-ui branch 2 times, most recently from cb5f741 to f408be1 Compare July 14, 2021 20:56
Because:

* we want to add UI to enable manual enrollment end

This commit:

* add isEnrollmentPaused to GQL experiment input for consistency with
  serialized experiment type, mapped to is_paused in Django model

* add support for isEnrollmentPausePending to indicate when the review
  is in progress

* rework useChangeOperationMutation to indicate loading while awaiting
  experiment refetch

* update "rejecting this review" copy to "rejecting the request to
  {actionDescription}"

* refactors handling of strings describing lifecycle review flows, moves
  all the strings into shared constants

* refactors the Settings page and ChangeApprovalOperations components

* adds more exhaustive stories for launch, end enrollment, and end cases
  on PageSummary

* reworks rejection reason display to more properly use old_status and
  old_status_next to determine what kind of review was rejected

* adds more status-based logic to make the correct GQL queries on
  approval / rejection of review

Co-authored-by: Lauren Zugai <lauren@zugai.com>
Copy link
Contributor Author

@lmorchard lmorchard left a comment

Choose a reason for hiding this comment

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

This PR has become monstrous, but I think it's got some needed refactorings. Commenting on some additions that should fix the UI with respect text, loading indication, and using the correct GQL mutation for end enrollment

Comment on lines +749 to +760
@parameterized.expand(
[
[NimbusExperimentFactory.Lifecycles.PAUSING_REVIEW_REQUESTED, False, True],
[NimbusExperimentFactory.Lifecycles.PAUSING_APPROVE, False, True],
[NimbusExperimentFactory.Lifecycles.PAUSING_APPROVE_WAITING, False, True],
[NimbusExperimentFactory.Lifecycles.PAUSING_APPROVE_TIMEOUT, False, True],
[NimbusExperimentFactory.Lifecycles.PAUSING_APPROVE_APPROVE, True, False],
[NimbusExperimentFactory.Lifecycles.PAUSING_REJECT, False, False],
[NimbusExperimentFactory.Lifecycles.PAUSING_APPROVE_REJECT, False, False],
]
)
def test_experiment_pause_pending(self, lifecycle, expected_paused, expected_pending):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Figured I might as well try exhausting all the cases here to make sure the pending vs published pause states worked as expected.

Comment on lines +64 to +68
if (refetch) {
setIsLoadingRefetch(true);
await refetch();
setIsLoadingRefetch(false);
}
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 seems to better ensure that the UI reflects that something's still in-flight for the refetch after the mutation

expect(isLoading).toBeFalsy();
await act(async () => void callback());
waitFor(() => expect(isLoading).toBeTruthy());
it("indicates when loading mutation", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reworking these tests to use await waitForNextUpdate() seems to exercise the functionality better. But I suspect there might be an intermittent race condition in here still that I haven't been able to flush out

Copy link
Collaborator

Choose a reason for hiding this comment

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

Circle reveals all.

Comment on lines -59 to -61
const mocks = customMocks.length
? customMocks
: mutationSets.reduce<MockedResponse[]>((acc, cur) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seemed like customMocks was never used in these tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Huh, I must've been on some idea and then forgot to remove it. Thanks for catching!

Comment on lines 19 to 23
type AppLayoutWithExperimentChildrenProps = {
experiment: getExperiment_experimentBySlug;
refetch: () => void;
refetch: () => Promise<unknown>;
analysis?: AnalysisData;
};
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 where else we're using refetch - this change doesn't seem to have broken any other tests, but we might need to update this type elsewhere eventually. Technically the refetch from Apollo is an async function and not () => void, but we didn't have that plumbed through for use in useChangeOperationMutation

Comment on lines +50 to +59
const refetchState = {
called: false,
resolve: null as null | (() => void),
};
const refetch = () => {
refetchState.called = true;
return new Promise(
(resolve) => (refetchState.resolve = () => resolve(null)),
);
};
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 feels a little dirty, but seems like a decent way to predictably walk through the states around refetch.

Comment on lines +226 to +228
def resolve_is_enrollment_pause_pending(self, info):
return self.is_paused and not self.is_paused_published

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 combined with status=LIVE / status_next = LIVE should give us all the hints needed on the frontend

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wait explain this. Shouldn't you just be able to infer all this based on publish_status? Why do you need to check the difference between the pause and published pause state?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can't infer specifically that this is a pending change to end enrollment, and I wanted to be specific #5852 (comment)

@jaredlockhart
Copy link
Collaborator

@lmorchard If you feel like there's refactors in here that are necessary but don't actually implement the new UI directly you could pull them out into separate more isolated PRs, review and land those first, and then rebase this PR to just contain the actual logic to implement the new UI. Up to you.

@lmorchard
Copy link
Contributor Author

lmorchard commented Jul 14, 2021

If you feel like there's refactors in here that are necessary but don't actually implement the new UI directly you could pull them out into separate more isolated PRs, review and land those first, and then rebase this PR to just contain the actual logic to implement the new UI. Up to you.

No, it's all pretty much inseparable at this point. The refactors were necessary to get the shared logic working between the now three review cases we have.

@lmorchard
Copy link
Contributor Author

lmorchard commented Jul 14, 2021

Filed #5960 to capture a refactor idea for useChangeOperationMutation and keep this PR from getting bigger.

Copy link
Contributor

@jodyheavener jodyheavener left a comment

Choose a reason for hiding this comment

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

Stared at this for a bit and had a few thoughts, but nothing major. I was able to go through the flow from start to end (as well as rejections) and didn't get hung up on anything. 🥳

disabled={isLoading}
data-testid="end-enrollment-start"
>
End Enrollment for Experiment
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "for Experiment" seems redundant?

Copy link
Contributor

Choose a reason for hiding this comment

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

Such looong button.

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 can't find an issue (which I thought existed) but I think we've been pushing to be more verbose and explicit about these messages. Might be worth a follow-up issue to revisit though

);

return (
<div className="mb-4" data-testid="enrollment-end">
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume this is not easily doable due to the layout complexity of the other states once you click the button, but is it possible to place the "End Experiment" and "End Enrollment for Experiment" buttons inline?

Copy link
Contributor Author

@lmorchard lmorchard Jul 15, 2021

Choose a reason for hiding this comment

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

Could be worth a follow-up issue, e.g. to combine these two buttons into a component side-by-side with state to display the confirmation form mutually exclusively for one or the other. Seems a decent enough chunk of work to defer and avoid making this PR bigger


return (
<div className="mb-4" data-testid="enrollment-end">
{showEndConfirmation ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

I can enter into both of these states at the same time (though, when one is confirmed the other does disappear). Should we hide the other button when one is clicked?

Screen Shot 2021-07-15 at 10 31 11 AM

Copy link
Contributor

Choose a reason for hiding this comment

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

Bonus rant: this is maybe more of a design/product thought, but these two flows have considerably different outcomes while having very similar-looking UI. I know we have the RS safeguard in place, but I wonder if we can do better to distinguish these two flows. "End experiment" almost feels like a red or yellow button to me now, instead of blue.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah good point, interesting thought. You're right that they're pretty heavily guarded by the multiple review steps. I'd say leave it and if it starts tripping people up we can revisit it, but good thing to point out. 👍

actionDescription,
isLoading,
status,
// TODO: refactor to just take `experiment` rather than all these separate props?
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for Context

Comment on lines -59 to -61
const mocks = customMocks.length
? customMocks
: mutationSets.reduce<MockedResponse[]>((acc, cur) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Huh, I must've been on some idea and then forgot to remove it. Thanks for catching!

actionDescription: string;
isLoading: boolean;
canReview: boolean;
// TODO: refactor to just take `experiment` rather than all these separate props?
Copy link
Contributor

Choose a reason for hiding this comment

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

Somewhat ironically, if this took experiment (or we were using Context) we also wouldn't need to pass in invalidPages or InvalidPageList since the component could itself just use useReviewCheck.

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, I keep thinking some kind of Context with experiment could be a good refactor, maybe also take the place of that functional child pattern we use for all the experiment-using pages

PAUSE: {
buttonTitle: "End Enrollment for Experiment",
description: "end enrollment for this experiment",
requestSummary: "Requested End Enrollment",
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 might phrase these as "X Requested", but def feel free to ignore :P

Comment on lines +35 to +39
// TODO: EXP-1325 Need to check something else here for end enrollment in particular?
pauseRequested:
status === NimbusExperimentStatus.LIVE &&
statusNext === NimbusExperimentStatus.LIVE &&
isEnrollmentPausePending === true,
Copy link
Collaborator

Choose a reason for hiding this comment

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

The only LIVE>LIVE update we have right now is pausing so that should be sufficient to know we're in a pausing flow?

Copy link
Contributor Author

@lmorchard lmorchard Jul 15, 2021

Choose a reason for hiding this comment

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

Wanted to be specific here and only catch the pending end enrollment case. It's the only update right now, but didn't seem like a lot to make it specific to pausing

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh I see what you mean. I think we'll end up having to do a lot more infrastructure before we do any non pause live updates, so we shouldn't really need to worry about it for now.

Copy link
Collaborator

@jaredlockhart jaredlockhart left a comment

Choose a reason for hiding this comment

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

Tested locally, everything went off without a hitch. Looks great, works great, thanks @lmorchard ! 🎉 🎉 🎉 🎉


return (
<div className="mb-4" data-testid="enrollment-end">
{showEndConfirmation ? (
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah good point, interesting thought. You're right that they're pretty heavily guarded by the multiple review steps. I'd say leave it and if it starts tripping people up we can revisit it, but good thing to point out. 👍

expect(isLoading).toBeFalsy();
await act(async () => void callback());
waitFor(() => expect(isLoading).toBeTruthy());
it("indicates when loading mutation", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Circle reveals all.

@lmorchard
Copy link
Contributor Author

Alright, I'm gonna hit the merge button to get this monster landed, then file a few follow up issues that are hopefully quick maintenance sized things

@lmorchard lmorchard merged commit 26ead8a into main Jul 15, 2021
@lmorchard lmorchard deleted the 5742-end-enrollment-ui branch July 15, 2021 20:51
@lmorchard
Copy link
Contributor Author

Tried to capture some of @jodyheavener's feedback in follow ups:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants