11import autobind from "autobind-decorator" ;
22import React from "react" ;
3- import { Header , Icon , Loader , Nav , Navbar , Pagination } from "rsuite" ;
3+ import { Controlled as CodeMirror } from "react-codemirror2" ;
4+ import {
5+ Button ,
6+ Drawer ,
7+ Header ,
8+ Icon ,
9+ Loader ,
10+ Modal ,
11+ Nav ,
12+ Navbar ,
13+ Pagination ,
14+ SelectPicker ,
15+ } from "rsuite" ;
416
17+ import { ENVIRONMENTS } from "devtools/config" ;
518import RecipeListing from "devtools/components/recipes/RecipeListing" ;
619import api from "devtools/utils/api" ;
720
@@ -11,11 +24,22 @@ const normandy = browser.experiments.normandy;
1124class RecipesPage extends React . PureComponent {
1225 constructor ( props ) {
1326 super ( props ) ;
27+
28+ const recipePages = { } ;
29+ Object . values ( ENVIRONMENTS ) . forEach ( v => {
30+ recipePages [ v ] = { } ;
31+ } ) ;
32+
1433 this . state = {
15- recipePages : { } ,
34+ arbitraryRecipe : "" ,
35+ count : 0 ,
36+ environment : ENVIRONMENTS . prod ,
1637 loading : false ,
1738 page : 1 ,
18- count : 0 ,
39+ runningArbitrary : false ,
40+ showSettings : false ,
41+ showWriteRecipes : false ,
42+ recipePages,
1943 } ;
2044 }
2145
@@ -24,45 +48,64 @@ class RecipesPage extends React.PureComponent {
2448 }
2549
2650 async componentDidMount ( ) {
27- const { page } = this . state ;
28-
29- if ( page in this . state . recipePages ) {
30- // cache hit
31- this . setState ( { page } ) ;
32- return ;
33- }
34-
35- this . setState ( { loading : true } ) ;
36-
37- let data = await api . fetchRecipePage ( page , { ordering : "-id" } ) ;
38- this . setState ( ( { recipePages } ) => ( {
39- recipePages : { ...recipePages , [ page ] : data . results } ,
40- loading : false ,
41- count : data . count ,
42- } ) ) ;
51+ const { environment, page } = this . state ;
52+ this . refreshRecipeList ( environment , page ) ;
4353 }
4454
45- async handlePageChange ( page ) {
46- if ( page in this . state . recipePages ) {
55+ async refreshRecipeList ( environment , page ) {
56+ if (
57+ environment in this . state . recipePages &&
58+ page in this . state . recipePages [ environment ]
59+ ) {
4760 // cache hit
4861 this . setState ( { page } ) ;
4962 return ;
5063 }
5164
5265 // cache miss
5366 this . setState ( { loading : true } ) ;
54- let data = await api . fetchRecipePage ( page ) ;
67+ let data = await api . fetchRecipePage ( environment , page , {
68+ ordering : "-id" ,
69+ } ) ;
5570 this . setState ( ( { recipePages } ) => ( {
56- recipePages : { ...recipePages , [ page ] : data . results } ,
71+ recipePages : {
72+ ...recipePages ,
73+ [ environment ] : {
74+ ...recipePages . environment ,
75+ [ page ] : data . results ,
76+ } ,
77+ } ,
5778 page,
5879 loading : false ,
5980 count : data . count ,
6081 } ) ) ;
6182 }
6283
84+ handlePageChange ( page ) {
85+ const { environment } = this . state ;
86+ this . refreshRecipeList ( environment , page ) ;
87+ }
88+
89+ handleEnvironmentChange ( environment ) {
90+ this . setState ( { environment } ) ;
91+ this . refreshRecipeList ( environment , 1 ) ;
92+ }
93+
94+ showSettings ( ) {
95+ this . setState ( { showSettings : true } ) ;
96+ }
97+
98+ hideSettings ( ) {
99+ this . setState ( { showSettings : false } ) ;
100+ }
101+
63102 renderRecipeList ( ) {
64- const { loading, page, recipePages } = this . state ;
65- const recipes = recipePages [ page ] ;
103+ const { environment, loading, page, recipePages } = this . state ;
104+ const recipes = recipePages [ environment ] [ page ] ;
105+
106+ let envName = Object . keys ( ENVIRONMENTS ) . find (
107+ v => ENVIRONMENTS [ v ] === environment ,
108+ ) ;
66109
67110 if ( loading ) {
68111 return (
@@ -72,13 +115,128 @@ class RecipesPage extends React.PureComponent {
72115 ) ;
73116 } else if ( recipes ) {
74117 return recipes . map ( recipe => (
75- < RecipeListing key = { recipe . id } recipe = { recipe } />
118+ < RecipeListing
119+ key = { recipe . id }
120+ recipe = { recipe }
121+ environmentName = { envName }
122+ />
76123 ) ) ;
77124 }
78125
79126 return null ;
80127 }
81128
129+ renderSettingsDrawer ( ) {
130+ const { showSettings, environment } = this . state ;
131+
132+ const envOptions = Object . keys ( ENVIRONMENTS ) . reduce ( ( reduced , value ) => {
133+ reduced . push ( {
134+ label : value . charAt ( 0 ) . toUpperCase ( ) + value . slice ( 1 ) ,
135+ value : ENVIRONMENTS [ value ] ,
136+ } ) ;
137+ return reduced ;
138+ } , [ ] ) ;
139+
140+ return (
141+ < Drawer
142+ placement = "right"
143+ show = { showSettings }
144+ onHide = { this . hideSettings }
145+ size = "xs"
146+ >
147+ < Drawer . Header > Settings</ Drawer . Header >
148+ < Drawer . Body >
149+ < h5 > Environment</ h5 >
150+ < SelectPicker
151+ data = { envOptions }
152+ defaultValue = { environment }
153+ cleanable = { false }
154+ searchable = { false }
155+ onChange = { this . handleEnvironmentChange }
156+ />
157+ </ Drawer . Body >
158+ </ Drawer >
159+ ) ;
160+ }
161+
162+ showWriteRecipePopup ( ) {
163+ this . setState ( { showWriteRecipes : true } ) ;
164+ }
165+
166+ hideWriteRecipePopup ( ) {
167+ this . setState ( { showWriteRecipes : false } ) ;
168+ }
169+
170+ handleArbitraryRecipeChange ( editor , data , value ) {
171+ this . setState ( { arbitraryRecipe : value } ) ;
172+ }
173+
174+ async runArbitraryRecipe ( ) {
175+ const { arbitraryRecipe } = this . state ;
176+ this . setState ( { runningArbitrary : true } ) ;
177+ try {
178+ await normandy . runRecipe ( JSON . parse ( arbitraryRecipe ) ) ;
179+ } catch ( ex ) {
180+ throw ex ;
181+ } finally {
182+ this . setState ( { runningArbitrary : false } ) ;
183+ }
184+ }
185+
186+ renderWriteRecipeModal ( ) {
187+ const { arbitraryRecipe, runningArbitrary } = this . state ;
188+
189+ return (
190+ < Modal
191+ show = { this . state . showWriteRecipes }
192+ onHide = { this . hideWriteRecipePopup }
193+ >
194+ < Modal . Header >
195+ < Modal . Title > Write a recipe</ Modal . Title >
196+ </ Modal . Header >
197+ < Modal . Body >
198+ < CodeMirror
199+ options = { {
200+ mode : "javascript" ,
201+ theme : "neo" ,
202+ lineNumbers : true ,
203+ styleActiveLine : true ,
204+ } }
205+ value = { arbitraryRecipe }
206+ style = { {
207+ height : "auto" ,
208+ } }
209+ onBeforeChange = { this . handleArbitraryRecipeChange }
210+ />
211+ </ Modal . Body >
212+ < Modal . Footer >
213+ < Button
214+ onClick = { this . runArbitraryRecipe }
215+ appearance = "primary"
216+ disabled = { runningArbitrary }
217+ >
218+ Run
219+ </ Button >
220+ < Button onClick = { this . hideWriteRecipePopup } appearance = "subtle" >
221+ Cancel
222+ </ Button >
223+ </ Modal . Footer >
224+ </ Modal >
225+ ) ;
226+ }
227+
228+ renderRunButton ( ) {
229+ const { environment } = this . state ;
230+ if ( environment !== ENVIRONMENTS . prod ) {
231+ return null ;
232+ }
233+ return (
234+ < Nav . Item icon = { < Icon icon = "play" /> } onClick = { this . runNormandy } >
235+ Run Normandy
236+ </ Nav . Item >
237+ ) ;
238+ }
239+
82240 render ( ) {
83241 const { count, page } = this . state ;
84242
@@ -87,8 +245,15 @@ class RecipesPage extends React.PureComponent {
87245 < Header >
88246 < Navbar >
89247 < Nav pullRight >
90- < Nav . Item icon = { < Icon icon = "play" /> } onClick = { this . runNormandy } >
91- Run Normandy
248+ < Nav . Item
249+ icon = { < Icon icon = "edit" /> }
250+ onClick = { this . showWriteRecipePopup }
251+ >
252+ Write & Run Arbitrary
253+ </ Nav . Item >
254+ { this . renderRunButton ( ) }
255+ < Nav . Item icon = { < Icon icon = "gear" /> } onClick = { this . showSettings } >
256+ Settings
92257 </ Nav . Item >
93258 </ Nav >
94259 </ Navbar >
@@ -112,6 +277,9 @@ class RecipesPage extends React.PureComponent {
112277 />
113278 </ div >
114279 </ div >
280+
281+ { this . renderSettingsDrawer ( ) }
282+ { this . renderWriteRecipeModal ( ) }
115283 </ React . Fragment >
116284 ) ;
117285 }
0 commit comments