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
Conversation
Opening as a draft with work in progress because:
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. |
d955415
to
d3000aa
Compare
Rebased against master to pick up the changes to PageSummary and approve / reject handlers |
d3000aa
to
07b3a25
Compare
Looks like I got the storybook to build, so here are some interesting places to look:
|
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.
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 If we do have another flow later that needs status=LIVE / status_next=LIVE, we should be able to alter |
07b3a25
to
67ad88d
Compare
changelogMessage: CHANGELOG_MESSAGES.REQUESTED_REVIEW_END, | ||
status: NimbusExperimentStatus.LIVE, | ||
statusNext: NimbusExperimentStatus.COMPLETE, |
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 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.
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. 😅 |
app/experimenter/nimbus-ui/src/components/ChangeApprovalOperations/RejectionReason.tsx
Show resolved
Hide resolved
Going to pick this back up tomorrow - PR #5935 will result in a totally different backend situation along with requiring we send 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. |
77081c0
to
4f32b39
Compare
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 |
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.
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.
@lmorchard FWIW I think that's taken care of in #5939 if you rebase |
4f32b39
to
ca60a06
Compare
I see what happened with the missing text & GQL query: I added an |
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 |
cb5f741
to
f408be1
Compare
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>
f408be1
to
d9052d8
Compare
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 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
@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): |
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.
Figured I might as well try exhausting all the cases here to make sure the pending vs published pause states worked as expected.
if (refetch) { | ||
setIsLoadingRefetch(true); | ||
await refetch(); | ||
setIsLoadingRefetch(false); | ||
} |
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 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 () => { |
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.
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
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.
Circle reveals all.
const mocks = customMocks.length | ||
? customMocks | ||
: mutationSets.reduce<MockedResponse[]>((acc, cur) => { |
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.
Seemed like customMocks
was never used in these tests?
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.
Huh, I must've been on some idea and then forgot to remove it. Thanks for catching!
type AppLayoutWithExperimentChildrenProps = { | ||
experiment: getExperiment_experimentBySlug; | ||
refetch: () => void; | ||
refetch: () => Promise<unknown>; | ||
analysis?: AnalysisData; | ||
}; |
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 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
const refetchState = { | ||
called: false, | ||
resolve: null as null | (() => void), | ||
}; | ||
const refetch = () => { | ||
refetchState.called = true; | ||
return new Promise( | ||
(resolve) => (refetchState.resolve = () => resolve(null)), | ||
); | ||
}; |
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 feels a little dirty, but seems like a decent way to predictably walk through the states around refetch.
def resolve_is_enrollment_pause_pending(self, info): | ||
return self.is_paused and not self.is_paused_published | ||
|
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 combined with status=LIVE / status_next = LIVE should give us all the hints needed on the frontend
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.
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?
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't infer specifically that this is a pending change to end enrollment, and I wanted to be specific #5852 (comment)
@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. |
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. |
Filed #5960 to capture a refactor idea for |
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.
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 |
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: "for Experiment" seems redundant?
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.
Such looong button.
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 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"> |
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 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?
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.
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 ? ( |
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.
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.
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.
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 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? |
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.
+1 for Context
const mocks = customMocks.length | ||
? customMocks | ||
: mutationSets.reduce<MockedResponse[]>((acc, cur) => { |
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.
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? |
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.
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
.
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, 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", |
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: I might phrase these as "X Requested", but def feel free to ignore :P
// TODO: EXP-1325 Need to check something else here for end enrollment in particular? | ||
pauseRequested: | ||
status === NimbusExperimentStatus.LIVE && | ||
statusNext === NimbusExperimentStatus.LIVE && | ||
isEnrollmentPausePending === 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.
The only LIVE>LIVE update we have right now is pausing so that should be sufficient to know we're in a pausing flow?
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.
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
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.
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.
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.
Tested locally, everything went off without a hitch. Looks great, works great, thanks @lmorchard ! 🎉 🎉 🎉 🎉
|
||
return ( | ||
<div className="mb-4" data-testid="enrollment-end"> | ||
{showEndConfirmation ? ( |
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 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 () => { |
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.
Circle reveals all.
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 |
Tried to capture some of @jodyheavener's feedback in follow ups:
|
Because:
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