Skip to content

Commit d04b7f0

Browse files
douglasmuraokadavimacedo
authored andcommitted
GraphQL API playground (#1123)
* Refactor API console into REST console * feat: GraphQL console component Adds the GraphQLConsole component, which renders the graphql-playground-react component at the route `/api_console/graphql`. The GraphQL server endpoint must be retrieved from the Parse app instance. * feat: Set default GraphQL playground headers * Fix GraphQL playground style import * fix package-lock.json * GraphQL API Console not configured state * Fix GraphQL console empty state horizontal alignment * Add default toolbar into GraphQL console
1 parent 2a7b7b0 commit d04b7f0

9 files changed

+2265
-1076
lines changed

package-lock.json

Lines changed: 1951 additions & 891 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
"create-react-class": "15.6.3",
4343
"csurf": "1.10.0",
4444
"express": "4.17.1",
45+
"graphql": "^14.3.1",
46+
"graphql-playground-react": "^1.7.20",
4547
"history": "4.9.0",
46-
"immutable": "3.8.1",
48+
"immutable": "^4.0.0-rc.9",
4749
"immutable-devtools": "0.1.3",
4850
"js-beautify": "1.10.0",
4951
"json-file-plus": "3.2.0",
@@ -59,6 +61,7 @@
5961
"react-dnd-html5-backend": "8.0.3",
6062
"react-dom": "16.8.6",
6163
"react-helmet": "5.2.1",
64+
"react-redux": "^5.1.1",
6265
"react-router": "5.0.1",
6366
"react-router-dom": "5.0.1"
6467
},

src/dashboard/Dashboard.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Config from './Data/Config/Config.react';
1818
import Explorer from './Analytics/Explorer/Explorer.react';
1919
import FourOhFour from 'components/FourOhFour/FourOhFour.react';
2020
import GeneralSettings from './Settings/GeneralSettings.react';
21+
import GraphQLConsole from './Data/ApiConsole/GraphQLConsole.react';
2122
import history from 'dashboard/history';
2223
import HostingSettings from './Settings/HostingSettings.react';
2324
import Icon from 'components/Icon/Icon.react';
@@ -35,6 +36,7 @@ import PushIndex from './Push/PushIndex.react';
3536
import PushNew from './Push/PushNew.react';
3637
import PushSettings from './Settings/PushSettings.react';
3738
import React from 'react';
39+
import RestConsole from './Data/ApiConsole/RestConsole.react';
3840
import Retention from './Analytics/Retention/Retention.react';
3941
import SchemaOverview from './Data/Browser/SchemaOverview.react';
4042
import SecuritySettings from './Settings/SecuritySettings.react';
@@ -246,6 +248,22 @@ export default class Dashboard extends React.Component {
246248
return <Browser {...props} params={ props.match.params } />
247249
}
248250

251+
const ApiConsoleRoute = (props) => (
252+
<Switch>
253+
<Route path={ props.match.path + '/rest' } render={props => (
254+
<ApiConsole {...props}>
255+
<RestConsole />
256+
</ApiConsole>
257+
)} />
258+
<Route path={ props.match.path + '/graphql' } render={props => (
259+
<ApiConsole {...props}>
260+
<GraphQLConsole />
261+
</ApiConsole>
262+
)} />
263+
<Redirect from={ props.match.path } to='/apps/:appId/api_console/rest' />
264+
</Switch>
265+
)
266+
249267
const AppRoute = ({ match }) => (
250268
<AppData params={ match.params }>
251269
<Switch>
@@ -265,8 +283,8 @@ export default class Dashboard extends React.Component {
265283
<Redirect from={ match.path + '/logs' } to='/apps/:appId/logs/info' />
266284

267285
<Route path={ match.path + '/config' } component={Config} />
268-
<Route path={ match.path + '/api_console' } component={ApiConsole} />
269-
<Route path={ match.path + '/migration' } component={Migration} />/>
286+
<Route path={ match.path + '/api_console' } component={ApiConsoleRoute} />
287+
<Route path={ match.path + '/migration' } component={Migration} />
270288

271289

272290
<Redirect exact from={ match.path + '/push' } to='/apps/:appId/push/new' />

src/dashboard/Data/ApiConsole/ApiConsole.react.js

Lines changed: 17 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -5,198 +5,34 @@
55
* This source code is licensed under the license found in the LICENSE file in
66
* the root directory of this source tree.
77
*/
8-
import PropTypes from 'lib/PropTypes';
9-
import Button from 'components/Button/Button.react';
10-
import DashboardView from 'dashboard/DashboardView.react';
11-
import Dropdown from 'components/Dropdown/Dropdown.react';
12-
import Field from 'components/Field/Field.react';
13-
import Fieldset from 'components/Fieldset/Fieldset.react';
14-
import fieldStyle from 'components/Field/Field.scss';
15-
import FlowFooter from 'components/FlowFooter/FlowFooter.react';
16-
import FormNote from 'components/FormNote/FormNote.react';
17-
import generateCurl from 'dashboard/Data/ApiConsole/generateCurl';
18-
import JsonPrinter from 'components/JsonPrinter/JsonPrinter.react';
19-
import Label from 'components/Label/Label.react';
20-
import Modal from 'components/Modal/Modal.react';
21-
import Option from 'components/Dropdown/Option.react';
22-
import Parse from 'parse';
23-
import ParseApp from 'lib/ParseApp';
24-
import React from 'react';
25-
import request from 'dashboard/Data/ApiConsole/request';
26-
import styles from 'dashboard/Data/ApiConsole/ApiConsole.scss';
27-
import TextInput from 'components/TextInput/TextInput.react';
28-
import Toggle from 'components/Toggle/Toggle.react';
29-
import Toolbar from 'components/Toolbar/Toolbar.react';
8+
import React from 'react'
9+
import CategoryList from 'components/CategoryList/CategoryList.react'
10+
import DashboardView from 'dashboard/DashboardView.react'
3011

3112
export default class ApiConsole extends DashboardView {
3213

3314
constructor() {
3415
super();
3516
this.section = 'Core';
3617
this.subsection = 'API Console';
37-
38-
this.state = {
39-
method: 'GET',
40-
endpoint: '',
41-
useMasterKey: false,
42-
runAsIdentifier: '',
43-
sessionToken: null,
44-
parameters: '',
45-
response: {results:[]},
46-
fetchingUser: false,
47-
inProgress: false,
48-
error: false,
49-
curlModal: false,
50-
};
51-
}
52-
53-
fetchUser() {
54-
if (this.state.runAsIdentifier.length === 0) {
55-
this.setState({ error: false, sessionToken: null });
56-
return;
57-
}
58-
Parse.Query.or(
59-
new Parse.Query(Parse.User).equalTo('username', this.state.runAsIdentifier ),
60-
new Parse.Query(Parse.User).equalTo('objectId', this.state.runAsIdentifier )
61-
).first({ useMasterKey: true }).then((found) => {
62-
if (found) {
63-
if (found.getSessionToken()) {
64-
this.setState({ sessionToken: found.getSessionToken(), error: false, fetchingUser: false });
65-
} else {
66-
// Check the Sessions table
67-
new Parse.Query(Parse.Session).equalTo('user', found).first({ useMasterKey: true }).then((session) => {
68-
if (session) {
69-
this.setState({ sessionToken: session.getSessionToken(), error: false, fetchingUser: false });
70-
} else {
71-
this.setState({ error: 'Unable to find any active sessions for that user.', fetchingUser: false });
72-
}
73-
}, () => {
74-
this.setState({ error: 'Unable to find any active sessions for that user.', fetchingUser: false });
75-
});
76-
}
77-
} else {
78-
this.setState({ error: 'Unable to find that user.', fetchingUser: false });
79-
}
80-
}, () => {
81-
this.setState({ error: 'Unable to find that user.', fetchingUser: false });
82-
});
83-
this.setState({ fetchingUser: true });
8418
}
8519

86-
makeRequest() {
87-
let endpoint = this.state.endpoint + (this.state.method === 'GET' ? `?${this.state.parameters}` : '');
88-
let payload = (this.state.method === 'DELETE' || this.state.method === 'GET') ? null : this.state.parameters;
89-
let options = {};
90-
if (this.state.useMasterKey) {
91-
options.useMasterKey = true;
92-
}
93-
if (this.state.sessionToken) {
94-
options.sessionToken = this.state.sessionToken;
95-
}
96-
request(
97-
this.context.currentApp,
98-
this.state.method,
99-
endpoint,
100-
payload,
101-
options
102-
).then((response) => {
103-
this.setState({ response });
104-
document.body.scrollTop = 540;
105-
});
106-
}
107-
108-
showCurl() {
109-
this.setState({ curlModal: true });
20+
renderSidebar() {
21+
const { path } = this.props.match
22+
const current = path.substr(path.lastIndexOf('/') + 1, path.length - 1)
23+
return (
24+
<CategoryList current={current} linkPrefix={'api_console/'} categories={[
25+
{ name: 'REST Console', id: 'rest' },
26+
{ name: 'GraphQL Console', id: 'graphql' }
27+
]} />
28+
)
11029
}
11130

11231
renderContent() {
113-
const methodDropdown =
114-
<Dropdown onChange={(method) => this.setState({method})} value={this.state.method}>
115-
<Option value='GET'>GET</Option>
116-
<Option value='POST'>POST</Option>
117-
<Option value='PUT'>PUT</Option>
118-
<Option value='DELETE'>DELETE</Option>
119-
</Dropdown>
120-
121-
let hasError = this.state.fetchingUser ||
122-
this.state.endpoint.length === 0 ||
123-
(this.state.runAsIdentifier.length > 0 && !this.state.sessionToken);
124-
let parameterPlaceholder = 'where={"username":"johndoe"}';
125-
if (this.state.method === 'POST' || this.state.method === 'PUT') {
126-
parameterPlaceholder = '{"name":"John"}';
127-
}
128-
129-
let modal = null;
130-
if (this.state.curlModal) {
131-
let payload = this.state.method === 'DELETE' ? null : this.state.parameters;
132-
let options = {};
133-
if (this.state.useMasterKey) {
134-
options.useMasterKey = true;
135-
}
136-
if (this.state.sessionToken) {
137-
options.sessionToken = this.state.sessionToken;
138-
}
139-
let content = generateCurl(
140-
this.context.currentApp,
141-
this.state.method,
142-
this.state.endpoint,
143-
payload,
144-
options
145-
);
146-
modal = (
147-
<Modal
148-
title='cURL Request'
149-
subtitle='Use this to replicate the request'
150-
icon='laptop-outline'
151-
customFooter={
152-
<div className={styles.footer}>
153-
<Button primary={true} value='Close' onClick={() => this.setState({ curlModal: false })} />
154-
</div>
155-
}>
156-
<div className={styles.curl}>{content}</div>
157-
</Modal>
158-
);
159-
}
160-
161-
return (
162-
<div style={{ padding: '120px 0 60px 0' }}>
163-
<Fieldset
164-
legend='Send a test query'
165-
description='Try out some queries, and take a look at what they return.'>
166-
<Field
167-
label={<Label text='What type of request?' />}
168-
input={methodDropdown} />
169-
<Field
170-
label={<Label text='Which endpoint?' description={<span>Not sure what endpoint you need?<br />Take a look at our <a href="http://docs.parseplatform.org/rest/guide/">REST API guide</a>.</span>} />}
171-
input={<TextInput value={this.state.endpoint} monospace={true} placeholder={'classes/_User'} onChange={(endpoint) => this.setState({endpoint})} />} />
172-
<Field
173-
label={<Label text='Use Master Key?' description={'This will bypass any ACL/CLPs.'} />}
174-
input={<Toggle value={this.state.useMasterKey} onChange={(useMasterKey) => this.setState({ useMasterKey })} />} />
175-
<Field
176-
label={<Label text='Run as...' description={'Send your query as a specific user. You can use their username or Object ID.'} />}
177-
input={<TextInput value={this.state.runAsIdentifier} monospace={true} placeholder={'Username or ID'} onChange={(runAsIdentifier) => this.setState({runAsIdentifier})} onBlur={this.fetchUser.bind(this)} />} />
178-
<FormNote color='red' show={!!this.state.error}>{this.state.error}</FormNote>
179-
<Field
180-
label={<Label text='Query parameters' description={<span>Learn more about query parameters in our <a href="http://docs.parseplatform.org/rest/guide/#queries">REST API guide</a>.</span>} />}
181-
input={<TextInput value={this.state.parameters} monospace={true} multiline={true} placeholder={parameterPlaceholder} onChange={(parameters) => this.setState({parameters})} />} />
182-
</Fieldset>
183-
<Fieldset
184-
legend='Results'
185-
description=''>
186-
<div className={fieldStyle.field}>
187-
<JsonPrinter object={this.state.response} />
188-
</div>
189-
</Fieldset>
190-
<Toolbar section='Core' subsection='API Console' />
191-
<FlowFooter
192-
primary={<Button primary={true} disabled={hasError} value='Send Query' progress={this.state.inProgress} onClick={this.makeRequest.bind(this)} />}
193-
secondary={<Button disabled={hasError} value='Export to cURL' onClick={this.showCurl.bind(this)} />} />
194-
{modal}
195-
</div>
196-
);
32+
const child = React.Children.only(this.props.children);
33+
return React.cloneElement(
34+
child,
35+
{ ...child.props }
36+
)
19737
}
19838
}
199-
200-
ApiConsole.contextTypes = {
201-
currentApp: PropTypes.instanceOf(ParseApp)
202-
};

src/dashboard/Data/ApiConsole/ApiConsole.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,17 @@
2020
padding: 10px 0;
2121
text-align: center;
2222
}
23+
24+
.content {
25+
position: relative;
26+
min-height: 100vh;
27+
padding-top: 96px;
28+
}
29+
30+
.empty {
31+
position: absolute;
32+
top: 96px;
33+
left: 0;
34+
right: 0;
35+
bottom: 0;
36+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
import ParseApp from 'lib/ParseApp';
9+
import PropTypes from 'lib/PropTypes';
10+
import React, { Component } from 'react';
11+
import { Provider } from 'react-redux'
12+
import { Playground, store } from 'graphql-playground-react';
13+
import EmptyState from 'components/EmptyState/EmptyState.react';
14+
import Toolbar from 'components/Toolbar/Toolbar.react';
15+
import styles from 'dashboard/Data/ApiConsole/ApiConsole.scss';
16+
17+
export default class GraphQLConsole extends Component {
18+
render() {
19+
const { applicationId, graphQLServerURL, masterKey } = this.context.currentApp;
20+
let content;
21+
if (!graphQLServerURL) {
22+
content = (
23+
<div className={styles.empty}>
24+
<EmptyState
25+
title='GraphQL API Console'
26+
description='Please update Parse-Server to version equal or above
27+
3.5.0 and define the "graphQLServerURL" on your app configuration
28+
in order to use the GraphQL API Console.'
29+
icon='info-solid' />
30+
</div>
31+
);
32+
} else {
33+
const headers = {
34+
'X-Parse-Application-Id': applicationId,
35+
'X-Parse-Master-Key': masterKey
36+
}
37+
content = (
38+
<Provider store={store}>
39+
<Playground endpoint={graphQLServerURL} headers={headers} />
40+
</Provider>
41+
);
42+
}
43+
44+
return (
45+
<>
46+
<Toolbar section='Core' subsection='GraphQL API Console' />
47+
<div className={styles.content}>
48+
{content}
49+
</div>
50+
</>
51+
);
52+
}
53+
}
54+
55+
GraphQLConsole.contextTypes = {
56+
currentApp: PropTypes.instanceOf(ParseApp)
57+
};

0 commit comments

Comments
 (0)