Skip to content

Commit 2ae086b

Browse files
committed
Allow re-enrollment in recipe
This change checks for existing enrollments, and if present shows the user a modal asking whether they want to force enroll. If so, it unenrolls them from the previous one, and deletes it before proceeding with the new enrollment. Fixes #50
1 parent 6ee2747 commit 2ae086b

File tree

6 files changed

+201
-106
lines changed

6 files changed

+201
-106
lines changed

src/apis/nimbus.js

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,39 @@ var nimbus = class extends ExtensionAPI {
3535
return {
3636
experiments: {
3737
nimbus: {
38-
async enrollInExperiment(jsonData) {
38+
async enrollInExperiment(jsonData, forceEnroll) {
3939
try {
40+
const slugExistsInStore = ExperimentManager.store
41+
.getAll()
42+
.some((experiment) => experiment.slug === jsonData.slug);
43+
const activeEnrollment =
44+
ExperimentManager.store
45+
.getAll()
46+
.find(
47+
(experiment) =>
48+
experiment.slug === jsonData.slug && experiment.active,
49+
)?.slug ?? null;
50+
if (slugExistsInStore || activeEnrollment) {
51+
if (!forceEnroll) {
52+
return {
53+
enrolled: false,
54+
error: { slugExistsInStore, activeEnrollment },
55+
};
56+
}
57+
58+
if (activeEnrollment) {
59+
this.unenroll(activeEnrollment);
60+
}
61+
if (slugExistsInStore) {
62+
this.deleteInactiveEnrollment(jsonData.slug);
63+
}
64+
}
65+
4066
const result = await ExperimentManager.enroll(
4167
jsonData,
4268
"nimbus-devtools",
4369
);
44-
return result !== null;
70+
return { enrolled: result !== null, error: null };
4571
} catch (error) {
4672
console.error(error);
4773
throw error;
@@ -85,16 +111,17 @@ var nimbus = class extends ExtensionAPI {
85111
"userFacingDescription": "Testing the feature with feature ID: ${featureId}."
86112
}`);
87113

88-
const experimentStore = ExperimentManager.store.getAll();
89-
const slugExistsInStore = experimentStore.some(
90-
(experiment) => experiment.slug === recipe.slug,
91-
);
114+
const slugExistsInStore = ExperimentManager.store
115+
.getAll()
116+
.some((experiment) => experiment.slug === recipe.slug);
92117
const activeEnrollment =
93-
experimentStore.find(
94-
(experiment) =>
95-
experiment.featureIds.includes(featureId) &&
96-
experiment.active,
97-
)?.slug || null;
118+
ExperimentManager.store
119+
.getAll()
120+
.find(
121+
(experiment) =>
122+
experiment.featureIds.includes(featureId) &&
123+
experiment.active,
124+
)?.slug ?? null;
98125

99126
if (slugExistsInStore || activeEnrollment) {
100127
if (!forceEnroll) {

src/apis/nimbus.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"type": "object",
1515
"additionalProperties": true,
1616
"description": "An object representing the experiment to enroll in."
17+
},
18+
{
19+
"name": "forceEnroll",
20+
"type": "boolean",
21+
"description": "A boolean representing whether or not the enrollment should be forced."
1722
}
1823
]
1924
},

src/types/global.d.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,28 +57,31 @@ declare module "mozjexl/lib/parser/Parser" {
5757
}
5858
}
5959

60-
declare namespace browser.experiments.nimbus {
61-
type EnrollWithFeatureConfigResult =
62-
| {
63-
enrolled: true;
64-
error: null;
65-
}
66-
| {
67-
enrolled: false;
68-
error: {
69-
slugExistsInStore: boolean;
70-
activeEnrollment: string | null;
71-
};
60+
type EnrollInExperimentResult =
61+
| {
62+
enrolled: true;
63+
error: null;
64+
}
65+
| {
66+
enrolled: false;
67+
error: {
68+
slugExistsInStore: boolean;
69+
activeEnrollment: string | null;
7270
};
71+
};
7372

74-
function enrollInExperiment(jsonData: object): Promise<boolean>;
73+
declare namespace browser.experiments.nimbus {
74+
function enrollInExperiment(
75+
jsonData: object,
76+
forceEnroll: boolean,
77+
): Promise<EnrollInExperimentResult>;
7578

7679
function enrollWithFeatureConfig(
7780
featureId: string,
7881
featureValue: object,
7982
isRollout: boolean,
8083
forceEnroll: boolean,
81-
): Promise<EnrollWithFeatureConfigResult>;
84+
): Promise<EnrollInExperimentResult>;
8285

8386
function getFeatureConfigs(): Promise<string[]>;
8487

src/ui/components/EnrollmentError.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { FC } from "react";
2+
3+
const EnrollmentError: FC<{
4+
slug: string;
5+
enrollError: EnrollInExperimentResult["error"] | null;
6+
}> = ({ slug, enrollError }) => {
7+
if (!enrollError) {
8+
return null;
9+
}
10+
const { activeEnrollment, slugExistsInStore } = enrollError;
11+
12+
if (activeEnrollment && slugExistsInStore && slug == activeEnrollment) {
13+
return (
14+
<p>
15+
There is already an enrollment for the slug: <strong>{slug}</strong>.
16+
Would you like to proceed with force enrollment by unenrolling,
17+
deleting, and enrolling into the new configuration?
18+
</p>
19+
);
20+
} else if (
21+
activeEnrollment &&
22+
slugExistsInStore &&
23+
slug !== activeEnrollment
24+
) {
25+
return (
26+
<p>
27+
There is already an active enrollment for the feature with a different
28+
slug. Would you like to proceed with force enrollment by unenrolling,
29+
deleting, and enrolling into the new configuration?
30+
</p>
31+
);
32+
}
33+
34+
if (activeEnrollment) {
35+
return (
36+
<p>
37+
There is an active enrollment for the slug:{" "}
38+
<strong>{activeEnrollment}</strong>. Would you like to unenroll from the
39+
active enrollment and enroll into the new configuration?
40+
</p>
41+
);
42+
}
43+
44+
if (slugExistsInStore) {
45+
return (
46+
<p>
47+
There is an inactive enrollment stored for the slug:{" "}
48+
<strong>{slug}</strong>. Would you like to delete the inactive
49+
enrollment and enroll into the new configuration?
50+
</p>
51+
);
52+
}
53+
54+
return null;
55+
};
56+
57+
export default EnrollmentError;

src/ui/components/FeatureConfigPage.tsx

Lines changed: 6 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,14 @@ import { Form, Container, Button, Row, Col, Modal } from "react-bootstrap";
33

44
import { useToastsContext } from "../hooks/useToasts";
55
import DropdownMenu from "./DropdownMenu";
6-
7-
type EnrollWithFeatureConfigResult =
8-
| {
9-
enrolled: true;
10-
error: null;
11-
}
12-
| {
13-
enrolled: false;
14-
error: {
15-
slugExistsInStore: boolean;
16-
activeEnrollment: string | null;
17-
};
18-
};
6+
import EnrollmentError from "./EnrollmentError";
197

208
const FeatureConfigPage: FC = () => {
219
const [jsonInput, setJsonInput] = useState("");
2210
const [selectedFeatureId, setSelectedFeatureId] = useState("");
2311
const [isRollout, setIsRollout] = useState(false);
2412
const [enrollError, setEnrollError] =
25-
useState<EnrollWithFeatureConfigResult["error"]>(null);
13+
useState<EnrollInExperimentResult["error"]>(null);
2614
const { addToast } = useToastsContext();
2715

2816
const slug = useMemo(() => {
@@ -54,8 +42,6 @@ const FeatureConfigPage: FC = () => {
5442
event?: React.MouseEvent<HTMLButtonElement>,
5543
forceEnroll = false,
5644
) => {
57-
event?.preventDefault();
58-
5945
if (selectedFeatureId === "") {
6046
addToast({
6147
message: "Invalid Input: Select feature",
@@ -65,7 +51,7 @@ const FeatureConfigPage: FC = () => {
6551
addToast({ message: "Invalid Input: Enter JSON", variant: "danger" });
6652
} else {
6753
try {
68-
const result: EnrollWithFeatureConfigResult =
54+
const result =
6955
await browser.experiments.nimbus.enrollWithFeatureConfig(
7056
selectedFeatureId,
7157
JSON.parse(jsonInput) as object,
@@ -100,49 +86,6 @@ const FeatureConfigPage: FC = () => {
10086
setEnrollError(null);
10187
}, [setEnrollError]);
10288

103-
function EnrollmentError(
104-
slug: string,
105-
enrollError: EnrollWithFeatureConfigResult["error"],
106-
) {
107-
if (!enrollError) {
108-
return null;
109-
}
110-
const { activeEnrollment, slugExistsInStore } = enrollError;
111-
112-
if (activeEnrollment && slugExistsInStore) {
113-
return (
114-
<p>
115-
There already is an enrollment for the feature:{" "}
116-
<strong>{slug}</strong>. Would you like to proceed with force
117-
enrollment by unenrolling, deleting, and re-enrolling into the new
118-
configuration?
119-
</p>
120-
);
121-
}
122-
123-
if (activeEnrollment) {
124-
return (
125-
<p>
126-
There is an active enrollment for the feature: <strong>{slug}</strong>
127-
. Would you like to unenroll from the active enrollment and re-enroll
128-
into the new configuration?
129-
</p>
130-
);
131-
}
132-
133-
if (slugExistsInStore) {
134-
return (
135-
<p>
136-
There is an inactive enrollment stored for the feature:{" "}
137-
<strong>{slug}</strong>. Would you like to delete the inactive
138-
enrollment and re-enroll into the new configuration?
139-
</p>
140-
);
141-
}
142-
143-
return null;
144-
}
145-
14689
return (
14790
<Container className="main-content p-2 overflow-hidden">
14891
<Form>
@@ -183,7 +126,9 @@ const FeatureConfigPage: FC = () => {
183126
<Modal.Header closeButton>
184127
<Modal.Title>Force Enrollment</Modal.Title>
185128
</Modal.Header>
186-
<Modal.Body>{EnrollmentError(slug, enrollError)}</Modal.Body>
129+
<Modal.Body>
130+
<EnrollmentError slug={slug} enrollError={enrollError} />
131+
</Modal.Body>
187132
<Modal.Footer>
188133
<Button variant="secondary" onClick={handleModalClose}>
189134
Cancel

0 commit comments

Comments
 (0)