Skip to content

GraphQL API playground #1123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,842 changes: 1,951 additions & 891 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
"create-react-class": "15.6.3",
"csurf": "1.10.0",
"express": "4.17.1",
"graphql": "^14.3.1",
"graphql-playground-react": "^1.7.20",
"history": "4.9.0",
"immutable": "3.8.1",
"immutable": "^4.0.0-rc.9",
"immutable-devtools": "0.1.3",
"js-beautify": "1.10.0",
"json-file-plus": "3.2.0",
Expand All @@ -59,6 +61,7 @@
"react-dnd-html5-backend": "8.0.3",
"react-dom": "16.8.6",
"react-helmet": "5.2.1",
"react-redux": "^5.1.1",
"react-router": "5.0.1",
"react-router-dom": "5.0.1"
},
Expand Down
22 changes: 20 additions & 2 deletions src/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Config from './Data/Config/Config.react';
import Explorer from './Analytics/Explorer/Explorer.react';
import FourOhFour from 'components/FourOhFour/FourOhFour.react';
import GeneralSettings from './Settings/GeneralSettings.react';
import GraphQLConsole from './Data/ApiConsole/GraphQLConsole.react';
import history from 'dashboard/history';
import HostingSettings from './Settings/HostingSettings.react';
import Icon from 'components/Icon/Icon.react';
Expand All @@ -35,6 +36,7 @@ import PushIndex from './Push/PushIndex.react';
import PushNew from './Push/PushNew.react';
import PushSettings from './Settings/PushSettings.react';
import React from 'react';
import RestConsole from './Data/ApiConsole/RestConsole.react';
import Retention from './Analytics/Retention/Retention.react';
import SchemaOverview from './Data/Browser/SchemaOverview.react';
import SecuritySettings from './Settings/SecuritySettings.react';
Expand Down Expand Up @@ -246,6 +248,22 @@ export default class Dashboard extends React.Component {
return <Browser {...props} params={ props.match.params } />
}

const ApiConsoleRoute = (props) => (
<Switch>
<Route path={ props.match.path + '/rest' } render={props => (
<ApiConsole {...props}>
<RestConsole />
</ApiConsole>
)} />
<Route path={ props.match.path + '/graphql' } render={props => (
<ApiConsole {...props}>
<GraphQLConsole />
</ApiConsole>
)} />
<Redirect from={ props.match.path } to='/apps/:appId/api_console/rest' />
</Switch>
)

const AppRoute = ({ match }) => (
<AppData params={ match.params }>
<Switch>
Expand All @@ -265,8 +283,8 @@ export default class Dashboard extends React.Component {
<Redirect from={ match.path + '/logs' } to='/apps/:appId/logs/info' />

<Route path={ match.path + '/config' } component={Config} />
<Route path={ match.path + '/api_console' } component={ApiConsole} />
<Route path={ match.path + '/migration' } component={Migration} />/>
<Route path={ match.path + '/api_console' } component={ApiConsoleRoute} />
<Route path={ match.path + '/migration' } component={Migration} />


<Redirect exact from={ match.path + '/push' } to='/apps/:appId/push/new' />
Expand Down
198 changes: 17 additions & 181 deletions src/dashboard/Data/ApiConsole/ApiConsole.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,198 +5,34 @@
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import PropTypes from 'lib/PropTypes';
import Button from 'components/Button/Button.react';
import DashboardView from 'dashboard/DashboardView.react';
import Dropdown from 'components/Dropdown/Dropdown.react';
import Field from 'components/Field/Field.react';
import Fieldset from 'components/Fieldset/Fieldset.react';
import fieldStyle from 'components/Field/Field.scss';
import FlowFooter from 'components/FlowFooter/FlowFooter.react';
import FormNote from 'components/FormNote/FormNote.react';
import generateCurl from 'dashboard/Data/ApiConsole/generateCurl';
import JsonPrinter from 'components/JsonPrinter/JsonPrinter.react';
import Label from 'components/Label/Label.react';
import Modal from 'components/Modal/Modal.react';
import Option from 'components/Dropdown/Option.react';
import Parse from 'parse';
import ParseApp from 'lib/ParseApp';
import React from 'react';
import request from 'dashboard/Data/ApiConsole/request';
import styles from 'dashboard/Data/ApiConsole/ApiConsole.scss';
import TextInput from 'components/TextInput/TextInput.react';
import Toggle from 'components/Toggle/Toggle.react';
import Toolbar from 'components/Toolbar/Toolbar.react';
import React from 'react'
import CategoryList from 'components/CategoryList/CategoryList.react'
import DashboardView from 'dashboard/DashboardView.react'

export default class ApiConsole extends DashboardView {

constructor() {
super();
this.section = 'Core';
this.subsection = 'API Console';

this.state = {
method: 'GET',
endpoint: '',
useMasterKey: false,
runAsIdentifier: '',
sessionToken: null,
parameters: '',
response: {results:[]},
fetchingUser: false,
inProgress: false,
error: false,
curlModal: false,
};
}

fetchUser() {
if (this.state.runAsIdentifier.length === 0) {
this.setState({ error: false, sessionToken: null });
return;
}
Parse.Query.or(
new Parse.Query(Parse.User).equalTo('username', this.state.runAsIdentifier ),
new Parse.Query(Parse.User).equalTo('objectId', this.state.runAsIdentifier )
).first({ useMasterKey: true }).then((found) => {
if (found) {
if (found.getSessionToken()) {
this.setState({ sessionToken: found.getSessionToken(), error: false, fetchingUser: false });
} else {
// Check the Sessions table
new Parse.Query(Parse.Session).equalTo('user', found).first({ useMasterKey: true }).then((session) => {
if (session) {
this.setState({ sessionToken: session.getSessionToken(), error: false, fetchingUser: false });
} else {
this.setState({ error: 'Unable to find any active sessions for that user.', fetchingUser: false });
}
}, () => {
this.setState({ error: 'Unable to find any active sessions for that user.', fetchingUser: false });
});
}
} else {
this.setState({ error: 'Unable to find that user.', fetchingUser: false });
}
}, () => {
this.setState({ error: 'Unable to find that user.', fetchingUser: false });
});
this.setState({ fetchingUser: true });
}

makeRequest() {
let endpoint = this.state.endpoint + (this.state.method === 'GET' ? `?${this.state.parameters}` : '');
let payload = (this.state.method === 'DELETE' || this.state.method === 'GET') ? null : this.state.parameters;
let options = {};
if (this.state.useMasterKey) {
options.useMasterKey = true;
}
if (this.state.sessionToken) {
options.sessionToken = this.state.sessionToken;
}
request(
this.context.currentApp,
this.state.method,
endpoint,
payload,
options
).then((response) => {
this.setState({ response });
document.body.scrollTop = 540;
});
}

showCurl() {
this.setState({ curlModal: true });
renderSidebar() {
const { path } = this.props.match
const current = path.substr(path.lastIndexOf('/') + 1, path.length - 1)
return (
<CategoryList current={current} linkPrefix={'api_console/'} categories={[
{ name: 'REST Console', id: 'rest' },
{ name: 'GraphQL Console', id: 'graphql' }
]} />
)
}

renderContent() {
const methodDropdown =
<Dropdown onChange={(method) => this.setState({method})} value={this.state.method}>
<Option value='GET'>GET</Option>
<Option value='POST'>POST</Option>
<Option value='PUT'>PUT</Option>
<Option value='DELETE'>DELETE</Option>
</Dropdown>

let hasError = this.state.fetchingUser ||
this.state.endpoint.length === 0 ||
(this.state.runAsIdentifier.length > 0 && !this.state.sessionToken);
let parameterPlaceholder = 'where={"username":"johndoe"}';
if (this.state.method === 'POST' || this.state.method === 'PUT') {
parameterPlaceholder = '{"name":"John"}';
}

let modal = null;
if (this.state.curlModal) {
let payload = this.state.method === 'DELETE' ? null : this.state.parameters;
let options = {};
if (this.state.useMasterKey) {
options.useMasterKey = true;
}
if (this.state.sessionToken) {
options.sessionToken = this.state.sessionToken;
}
let content = generateCurl(
this.context.currentApp,
this.state.method,
this.state.endpoint,
payload,
options
);
modal = (
<Modal
title='cURL Request'
subtitle='Use this to replicate the request'
icon='laptop-outline'
customFooter={
<div className={styles.footer}>
<Button primary={true} value='Close' onClick={() => this.setState({ curlModal: false })} />
</div>
}>
<div className={styles.curl}>{content}</div>
</Modal>
);
}

return (
<div style={{ padding: '120px 0 60px 0' }}>
<Fieldset
legend='Send a test query'
description='Try out some queries, and take a look at what they return.'>
<Field
label={<Label text='What type of request?' />}
input={methodDropdown} />
<Field
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>} />}
input={<TextInput value={this.state.endpoint} monospace={true} placeholder={'classes/_User'} onChange={(endpoint) => this.setState({endpoint})} />} />
<Field
label={<Label text='Use Master Key?' description={'This will bypass any ACL/CLPs.'} />}
input={<Toggle value={this.state.useMasterKey} onChange={(useMasterKey) => this.setState({ useMasterKey })} />} />
<Field
label={<Label text='Run as...' description={'Send your query as a specific user. You can use their username or Object ID.'} />}
input={<TextInput value={this.state.runAsIdentifier} monospace={true} placeholder={'Username or ID'} onChange={(runAsIdentifier) => this.setState({runAsIdentifier})} onBlur={this.fetchUser.bind(this)} />} />
<FormNote color='red' show={!!this.state.error}>{this.state.error}</FormNote>
<Field
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>} />}
input={<TextInput value={this.state.parameters} monospace={true} multiline={true} placeholder={parameterPlaceholder} onChange={(parameters) => this.setState({parameters})} />} />
</Fieldset>
<Fieldset
legend='Results'
description=''>
<div className={fieldStyle.field}>
<JsonPrinter object={this.state.response} />
</div>
</Fieldset>
<Toolbar section='Core' subsection='API Console' />
<FlowFooter
primary={<Button primary={true} disabled={hasError} value='Send Query' progress={this.state.inProgress} onClick={this.makeRequest.bind(this)} />}
secondary={<Button disabled={hasError} value='Export to cURL' onClick={this.showCurl.bind(this)} />} />
{modal}
</div>
);
const child = React.Children.only(this.props.children);
return React.cloneElement(
child,
{ ...child.props }
)
}
}

ApiConsole.contextTypes = {
currentApp: PropTypes.instanceOf(ParseApp)
};
14 changes: 14 additions & 0 deletions src/dashboard/Data/ApiConsole/ApiConsole.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,17 @@
padding: 10px 0;
text-align: center;
}

.content {
position: relative;
min-height: 100vh;
padding-top: 96px;
}

.empty {
position: absolute;
top: 96px;
left: 0;
right: 0;
bottom: 0;
}
57 changes: 57 additions & 0 deletions src/dashboard/Data/ApiConsole/GraphQLConsole.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2016-present, Parse, LLC
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import ParseApp from 'lib/ParseApp';
import PropTypes from 'lib/PropTypes';
import React, { Component } from 'react';
import { Provider } from 'react-redux'
import { Playground, store } from 'graphql-playground-react';
import EmptyState from 'components/EmptyState/EmptyState.react';
import Toolbar from 'components/Toolbar/Toolbar.react';
import styles from 'dashboard/Data/ApiConsole/ApiConsole.scss';

export default class GraphQLConsole extends Component {
render() {
const { applicationId, graphQLServerURL, masterKey } = this.context.currentApp;
let content;
if (!graphQLServerURL) {
content = (
<div className={styles.empty}>
<EmptyState
title='GraphQL API Console'
description='Please update Parse-Server to version equal or above
3.5.0 and define the "graphQLServerURL" on your app configuration
in order to use the GraphQL API Console.'
icon='info-solid' />
</div>
);
} else {
const headers = {
'X-Parse-Application-Id': applicationId,
'X-Parse-Master-Key': masterKey
}
content = (
<Provider store={store}>
<Playground endpoint={graphQLServerURL} headers={headers} />
</Provider>
);
}

return (
<>
<Toolbar section='Core' subsection='GraphQL API Console' />
<div className={styles.content}>
{content}
</div>
</>
);
}
}

GraphQLConsole.contextTypes = {
currentApp: PropTypes.instanceOf(ParseApp)
};
Loading