Skip to content
This repository was archived by the owner on Apr 6, 2022. It is now read-only.

Commit 807dc33

Browse files
authored
More recipe running (#19)
More recipe running
2 parents 53c13b9 + e54aa76 commit 807dc33

File tree

8 files changed

+225
-45
lines changed

8 files changed

+225
-45
lines changed

extension/content/components/filters/JexlColumn.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import PropTypes from "prop-types";
22
import React from "react";
33
import { Controlled as CodeMirror } from "react-codemirror2";
44

5-
// Mode and theme for Code Mirror
6-
import "codemirror/addon/selection/active-line";
7-
import "codemirror/mode/javascript/javascript";
8-
import "codemirror/theme/neo.css";
9-
105
export default class JexlColumn extends React.PureComponent {
116
static propTypes = {
127
filterExpression: PropTypes.string.isRequired,

extension/content/components/pages/RecipesPage.js

Lines changed: 196 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import autobind from "autobind-decorator";
22
import 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";
518
import RecipeListing from "devtools/components/recipes/RecipeListing";
619
import api from "devtools/utils/api";
720

@@ -11,11 +24,22 @@ const normandy = browser.experiments.normandy;
1124
class 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
}

extension/content/components/recipes/RecipeListing.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { Button, Icon, Panel, Tag } from "rsuite";
77

88
const normandy = browser.experiments.normandy;
99

10-
function convertToV1Recipe(v3Recipe) {
10+
function convertToV1Recipe(v3Recipe, environmentName) {
1111
// Normandy expects a v1-style recipe, but we have a v3-style recipe. Convert it.
12+
const idSuffix = environmentName !== "prod" ? `-${environmentName}` : "";
1213
return {
13-
id: v3Recipe.id,
14+
id: `${v3Recipe.id}${idSuffix}`,
1415
name: v3Recipe.latest_revision.name,
1516
enabled: v3Recipe.latest_revision.enabled,
1617
is_approved: v3Recipe.latest_revision.is_approved,
@@ -24,6 +25,7 @@ function convertToV1Recipe(v3Recipe) {
2425
@autobind
2526
class RecipeListing extends React.PureComponent {
2627
static propTypes = {
28+
environmentName: PropTypes.object.string,
2729
recipe: PropTypes.object.isRequired,
2830
};
2931

@@ -36,17 +38,17 @@ class RecipeListing extends React.PureComponent {
3638
}
3739

3840
async componentDidMount() {
39-
const { recipe } = this.props;
41+
const { environmentName, recipe } = this.props;
4042
let filterMatches = await normandy.checkRecipeFilter(
41-
convertToV1Recipe(recipe),
43+
convertToV1Recipe(recipe, environmentName),
4244
);
4345
this.setState({ filterMatches });
4446
}
4547

4648
async handleRunButtonClick(ev) {
47-
const { recipe } = this.props;
49+
const { environmentName, recipe } = this.props;
4850
this.setState({ running: true });
49-
await normandy.runRecipe(convertToV1Recipe(recipe));
51+
await normandy.runRecipe(convertToV1Recipe(recipe, environmentName));
5052
this.setState({ running: false });
5153
}
5254

extension/content/config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const ENVIRONMENTS = {
2+
prod: "https://normandy.cdn.mozilla.net/",
3+
stage: "https://stage.normandy.nonprod.cloudops.mozgcp.net/",
4+
dev: "https://dev.normandy.nonprod.cloudops.mozgcp.net/",
5+
};

extension/content/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import ErrorBoundary from "react-error-boundary";
55
import { Container, Icon, Nav, Sidebar, Sidenav } from "rsuite";
66

77
import "devtools/less/index.less";
8+
9+
// Mode and theme for Code Mirror
10+
import "codemirror/addon/selection/active-line";
11+
import "codemirror/mode/javascript/javascript";
12+
import "codemirror/theme/neo.css";
13+
814
import Logo from "devtools/components/svg/Logo";
915
import RecipesPage from "devtools/components/pages/RecipesPage";
1016
import FiltersPage from "devtools/components/pages/FiltersPage";

extension/content/less/filter-page.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262

6363
.rc-tree-title {
6464
font-family: monospace;
65+
font-size: @font-size-base - 1;
6566
padding-left: 2px;
6667
}
6768
}

extension/content/less/main.less

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,8 @@
5656
width: 100%;
5757
}
5858
}
59+
60+
.react-codemirror2,
61+
code.hljs {
62+
font-size: @font-size-base - 1;
63+
}

0 commit comments

Comments
 (0)