Skip to content

Commit a422f8a

Browse files
committed
Generate test IDs for preview experiments
Added the generate test IDs option in the action dropdown for experiments in the preview collection Fixes #61
1 parent 1fad365 commit a422f8a

File tree

2 files changed

+141
-154
lines changed

2 files changed

+141
-154
lines changed
Lines changed: 140 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useCallback, useEffect, useState } from "react";
1+
import { FC, useCallback, useEffect, useMemo, useState } from "react";
22
import { NimbusExperiment } from "@mozilla/nimbus-schemas";
33
import {
44
Table,
@@ -10,7 +10,7 @@ import {
1010
Dropdown,
1111
} from "react-bootstrap";
1212

13-
import { useToastsContext } from "../hooks/useToasts";
13+
import { AddToastParams, useToastsContext } from "../hooks/useToasts";
1414

1515
const PROD_URL =
1616
"https://experimenter.services.mozilla.com/api/v6/experiments/";
@@ -24,13 +24,91 @@ enum Environment {
2424
STAGE = "stage",
2525
}
2626

27+
const ExperimentRow: FC<{ experiment: NimbusExperiment }> = ({
28+
experiment,
29+
}) => {
30+
const { addToast } = useToastsContext();
31+
const branchSlugs = useMemo(
32+
() => experiment.branches?.map((b) => b.slug),
33+
[experiment],
34+
);
35+
const [selectedBranch, setSelectedBranch] = useState<string>("");
36+
37+
const branchSlugOptions = useMemo(
38+
() =>
39+
branchSlugs.map((slug) => (
40+
<option key={slug} value={slug}>
41+
{slug}
42+
</option>
43+
)),
44+
[branchSlugs],
45+
);
46+
47+
const onSelectedBranchChanged = useCallback(
48+
(e: React.ChangeEvent<HTMLSelectElement>) => {
49+
setSelectedBranch(e.target.value);
50+
},
51+
[setSelectedBranch],
52+
);
53+
54+
const handleGenerateTestIds = useCallback(async () => {
55+
const toast = await tryGenerateTestId(experiment, selectedBranch);
56+
addToast(toast);
57+
}, [experiment, selectedBranch, addToast]);
58+
59+
const handleEnroll = useCallback(async () => {
60+
const toast = await tryEnroll(experiment, selectedBranch);
61+
addToast(toast);
62+
}, [experiment, selectedBranch, addToast]);
63+
64+
return (
65+
<tr>
66+
<td className="align-middle ps-0 py-3 w-50">
67+
<strong>{experiment.userFacingName}</strong>:{" "}
68+
{experiment.userFacingDescription}
69+
</td>
70+
<td className="text-center align-middle px-2">{experiment.channel}</td>
71+
<td className="text-center align-middle px-2">
72+
{experiment.schemaVersion}
73+
</td>
74+
<td className="text-center align-middle px-2">
75+
{experiment.isEnrollmentPaused ? "Enrolling" : "Enrollment Paused"}
76+
</td>
77+
<td className="text-end align-middle wide-column">
78+
<Container className="d-flex align-items-center">
79+
<Form.Select
80+
value={selectedBranch}
81+
onChange={onSelectedBranchChanged}
82+
className="grey-border small-font rounded p-2 m-0 font-monospace"
83+
>
84+
<option value="">Select branch</option>
85+
{branchSlugOptions}
86+
</Form.Select>
87+
<Dropdown>
88+
<Dropdown.Toggle
89+
variant={!selectedBranch ? "secondary" : "primary"}
90+
className="option-button primary-fg mx-2 py-2 px-3 rounded small-font fw-bold grey-border light-bg"
91+
disabled={!selectedBranch}
92+
>
93+
Actions
94+
</Dropdown.Toggle>
95+
<Dropdown.Menu>
96+
<Dropdown.Item onClick={handleEnroll}>Force Enroll</Dropdown.Item>
97+
<Dropdown.Item onClick={handleGenerateTestIds}>
98+
Generate Test IDs
99+
</Dropdown.Item>
100+
</Dropdown.Menu>
101+
</Dropdown>
102+
</Container>
103+
</td>
104+
</tr>
105+
);
106+
};
107+
27108
const ExperimentBrowserPage: FC = () => {
28109
const [environment, setEnvironment] = useState<Environment>(Environment.PROD);
29110
const [status, setStatus] = useState<Status>("Live");
30111
const [experiments, setExperiments] = useState<NimbusExperiment[]>([]);
31-
const [selectedBranches, setSelectedBranches] = useState<{
32-
[key: string]: string;
33-
}>({});
34112
const { addToast } = useToastsContext();
35113

36114
const fetchExperiments = useCallback(
@@ -60,74 +138,13 @@ const ExperimentBrowserPage: FC = () => {
60138
void fetchExperiments();
61139
}, [fetchExperiments]);
62140

63-
const handleEnroll = async (experimentId: string, branchSlug: string) => {
64-
if (branchSlug) {
65-
const recipe = experiments.find((exp) => exp.id === experimentId);
66-
try {
67-
const result = await browser.experiments.nimbus.forceEnroll(
68-
recipe,
69-
branchSlug,
70-
);
71-
if (result) {
72-
addToast({ message: "Enrollment successful", variant: "success" });
73-
} else {
74-
addToast({ message: "Enrollment failed", variant: "danger" });
75-
}
76-
} catch (error) {
77-
addToast({
78-
message: `Error enrolling into experiment: ${(error as Error).message ?? String(error)}`,
79-
variant: "danger",
80-
});
81-
}
82-
} else {
83-
addToast({
84-
message: "Select a branch before enrolling",
85-
variant: "danger",
86-
});
87-
}
88-
};
89-
90-
const handleGenerateTestIds = async (
91-
experimentId: string,
92-
branchSlug: string,
93-
) => {
94-
if (branchSlug) {
95-
const recipe = experiments.find((exp) => exp.id === experimentId);
96-
try {
97-
const result = await browser.experiments.nimbus.generateTestIds(
98-
recipe,
99-
branchSlug,
100-
);
101-
if (result) {
102-
await navigator.clipboard.writeText(result);
103-
addToast({
104-
message: `Id successfully generated and copied to clipboard. Test Id: ${result}`,
105-
variant: "success",
106-
autohide: false,
107-
});
108-
} else {
109-
addToast({ message: "Test Id generation failed", variant: "danger" });
110-
}
111-
} catch (error) {
112-
addToast({
113-
message: `Error generating test Id: ${(error as Error).message ?? String(error)}`,
114-
variant: "danger",
115-
});
116-
}
117-
} else {
118-
addToast({
119-
message: "Select a branch before generating test Id",
120-
variant: "danger",
121-
});
122-
}
123-
};
124-
125-
const handleBranchChange = (experimentId: string, branchSlug: string) => {
126-
setSelectedBranches((prevSelectedBranches) => ({
127-
...prevSelectedBranches,
128-
[experimentId]: branchSlug,
129-
}));
130-
};
141+
const experimentRows = useMemo(
142+
() =>
143+
experiments.map((experiment) => (
144+
<ExperimentRow key={experiment.slug} experiment={experiment} />
145+
)),
146+
[experiments],
147+
);
131148

132149
return (
133150
<Container>
@@ -178,89 +195,59 @@ const ExperimentBrowserPage: FC = () => {
178195
<th className="text-center primary-fg light-bg">Actions</th>
179196
</tr>
180197
</thead>
181-
<tbody>
182-
{experiments.map((experiment) => (
183-
<tr key={experiment.id}>
184-
<td className="align-middle ps-0 py-3 w-50">
185-
<strong>{experiment.userFacingName}</strong>:{" "}
186-
{experiment.userFacingDescription}
187-
</td>
188-
<td className="text-center align-middle px-2">
189-
{experiment.channel}
190-
</td>
191-
<td className="text-center align-middle px-2">
192-
{experiment.schemaVersion}
193-
</td>
194-
<td className="text-center align-middle px-2">
195-
{experiment.isEnrollmentPaused
196-
? "Enrolling"
197-
: "Enrollment Paused"}
198-
</td>
199-
<td className="text-end align-middle wide-column">
200-
<Container className="d-flex align-items-center">
201-
<Form.Select
202-
value={selectedBranches[experiment.id]}
203-
onChange={(e) =>
204-
handleBranchChange(experiment.id, e.target.value)
205-
}
206-
className="grey-border small-font rounded p-2 m-0 font-monospace"
207-
>
208-
<option value="">Select branch</option>
209-
{experiment.branches?.map((branch) => (
210-
<option key={branch.slug} value={branch.slug}>
211-
{branch.slug}
212-
</option>
213-
))}
214-
</Form.Select>
215-
{status === "Live" ? (
216-
<Dropdown>
217-
<Dropdown.Toggle className="option-button primary-fg py-2 my-1 mx-2 rounded small-font fw-bold grey-border light-bg">
218-
Actions
219-
</Dropdown.Toggle>
220-
<Dropdown.Menu>
221-
<Dropdown.Item
222-
onClick={() =>
223-
handleEnroll(
224-
experiment.id,
225-
selectedBranches[experiment.id],
226-
)
227-
}
228-
>
229-
Force Enroll
230-
</Dropdown.Item>
231-
<Dropdown.Item
232-
onClick={() =>
233-
handleGenerateTestIds(
234-
experiment.id,
235-
selectedBranches[experiment.id],
236-
)
237-
}
238-
>
239-
Generate Test IDs
240-
</Dropdown.Item>
241-
</Dropdown.Menu>
242-
</Dropdown>
243-
) : (
244-
<Button
245-
className="option-button primary-fg py-0 my-1 mx-1 rounded small-font fw-bold grey-border light-bg"
246-
onClick={() =>
247-
handleEnroll(
248-
experiment.id,
249-
selectedBranches[experiment.id],
250-
)
251-
}
252-
>
253-
Force Enroll
254-
</Button>
255-
)}
256-
</Container>
257-
</td>
258-
</tr>
259-
))}
260-
</tbody>
198+
<tbody>{experimentRows}</tbody>
261199
</Table>
262200
</Container>
263201
);
264202
};
265203

204+
async function tryEnroll(
205+
experiment: NimbusExperiment,
206+
branchSlug: string,
207+
): Promise<AddToastParams> {
208+
try {
209+
const enrolled = await browser.experiments.nimbus.forceEnroll(
210+
experiment,
211+
branchSlug,
212+
);
213+
if (enrolled) {
214+
return { message: "Enrollment successful", variant: "success" };
215+
} else {
216+
return { message: "Enrollment failed", variant: "danger" };
217+
}
218+
} catch (error) {
219+
return {
220+
message: `Error enrolling into experiment: ${(error as Error).message ?? String(error)}`,
221+
variant: "danger",
222+
};
223+
}
224+
}
225+
226+
async function tryGenerateTestId(
227+
experiment: NimbusExperiment,
228+
branchSlug: string,
229+
): Promise<AddToastParams> {
230+
try {
231+
const result = await browser.experiments.nimbus.generateTestIds(
232+
experiment,
233+
branchSlug,
234+
);
235+
if (result) {
236+
await navigator.clipboard.writeText(result);
237+
return {
238+
message: `Id copied to clipboard. Test Id: ${result}`,
239+
variant: "success",
240+
autohide: false,
241+
};
242+
} else {
243+
return { message: "Test Id generation failed", variant: "danger" };
244+
}
245+
} catch (error) {
246+
return {
247+
message: `Error generating test Id: ${(error as Error).message ?? String(error)}`,
248+
variant: "danger",
249+
};
250+
}
251+
}
252+
266253
export default ExperimentBrowserPage;

src/ui/hooks/useToasts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface UseToasts {
1313
removeToast: (id: string) => void;
1414
}
1515

16-
type AddToastParams = {
16+
export type AddToastParams = {
1717
message: string;
1818
variant: "success" | "danger";
1919
autohide?: boolean;

0 commit comments

Comments
 (0)