1
- import { FC , useCallback , useEffect , useState } from "react" ;
1
+ import { FC , useCallback , useEffect , useMemo , useState } from "react" ;
2
2
import { NimbusExperiment } from "@mozilla/nimbus-schemas" ;
3
3
import {
4
4
Table ,
@@ -10,7 +10,7 @@ import {
10
10
Dropdown ,
11
11
} from "react-bootstrap" ;
12
12
13
- import { useToastsContext } from "../hooks/useToasts" ;
13
+ import { AddToastParams , useToastsContext } from "../hooks/useToasts" ;
14
14
15
15
const PROD_URL =
16
16
"https://experimenter.services.mozilla.com/api/v6/experiments/" ;
@@ -24,13 +24,91 @@ enum Environment {
24
24
STAGE = "stage" ,
25
25
}
26
26
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
+
27
108
const ExperimentBrowserPage : FC = ( ) => {
28
109
const [ environment , setEnvironment ] = useState < Environment > ( Environment . PROD ) ;
29
110
const [ status , setStatus ] = useState < Status > ( "Live" ) ;
30
111
const [ experiments , setExperiments ] = useState < NimbusExperiment [ ] > ( [ ] ) ;
31
- const [ selectedBranches , setSelectedBranches ] = useState < {
32
- [ key : string ] : string ;
33
- } > ( { } ) ;
34
112
const { addToast } = useToastsContext ( ) ;
35
113
36
114
const fetchExperiments = useCallback (
@@ -60,74 +138,13 @@ const ExperimentBrowserPage: FC = () => {
60
138
void fetchExperiments ( ) ;
61
139
} , [ fetchExperiments ] ) ;
62
140
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
+ ) ;
131
148
132
149
return (
133
150
< Container >
@@ -178,89 +195,59 @@ const ExperimentBrowserPage: FC = () => {
178
195
< th className = "text-center primary-fg light-bg" > Actions</ th >
179
196
</ tr >
180
197
</ 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 >
261
199
</ Table >
262
200
</ Container >
263
201
) ;
264
202
} ;
265
203
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
+
266
253
export default ExperimentBrowserPage ;
0 commit comments