Improved graphene playground with router query params

This commit is contained in:
Syrus Akbary 2015-11-29 22:33:09 -08:00
parent 3c308b90fa
commit 18b66bd8a2
6 changed files with 386 additions and 250 deletions

View File

@ -17,7 +17,7 @@
"copy-webpack-plugin": "^0.2.0",
"es6-promise": "^3.0.2",
"extract-text-webpack-plugin": "^0.9.1",
"gatsby": "^0.7.2",
"gatsby": "^0.7.3",
"graphiql": "^0.4.2",
"graphql": "^0.4.13",
"jeet": "^6.1.2",

View File

@ -0,0 +1,234 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { RouteHandler, Link, State } from 'react-router';
import CodeMirror from 'codemirror';
import { graphql } from 'graphql';
import GraphiQL from 'graphiql';
import schema from './schema';
import pypyjs_vm from 'pypyjs';
import 'codemirror/mode/python/python';
import 'codemirror/addon/lint/lint';
import '../css/playground.styl';
if (typeof PUBLIC_PATH === "undefined") {
var PUBLIC_PATH = '';
}
pypyjs_vm.rootURL = `${PUBLIC_PATH}/playground/lib/`;
pypyjs_vm.cacheKey = 'graphene';
CodeMirror.registerHelper('lint', 'python', function (text, options, editor) {
return (options.errors || []).map((error) => {
var tokens = editor.getLineTokens(error.line - 1);
tokens = tokens.filter((token, pos) => {
return !!token.type || token.string.trim().length > 0;
});
if (!tokens) return [];
return {
message: `${error.name}: ${error.message}`,
severity: 'error',
type: 'syntax',
from: CodeMirror.Pos(error.line - 1, tokens[0].start),
to: CodeMirror.Pos(error.line - 1, tokens[tokens.length-1].end),
};
});
});
function graphQLFetcher(graphQLParams) {
return graphql(schema, graphQLParams.query);
}
var default_interpreter;
class Playground extends React.Component {
constructor() {
super();
this.state = {pypyjs: false, stdout: '', response:''};
}
stdout() {
console.log('stdout', arguments);
}
componentDidMount() {
if (default_interpreter) {
this.pypy_interpreter = default_interpreter;
this.pypy_interpreter.stdout = this.stdout.bind(this);
}
else {
this.pypy_interpreter = new pypyjs_vm({
stdin: function(){},
stdout: this.stdout.bind(this),
stderr: function(){},
rootURL: `${PUBLIC_PATH}/playground/lib/`
});
default_interpreter = this.pypy_interpreter;
}
this.pypyjs = this.pypy_interpreter.ready().then(() => {
return this.pypy_interpreter.exec(`
import graphene
import js
from collections import OrderedDict
from graphql.core.execution.executor import Executor
from graphql.core.execution.middlewares.sync import SynchronousExecutionMiddleware
from graphql.core.error import GraphQLError, format_error
def get_wrapped(f):
if hasattr(f, 'func_closure') and f.func_closure:
return get_wrapped(f.func_closure[0].cell_contents)
return f
class TrackResolver(SynchronousExecutionMiddleware):
@staticmethod
def run_resolve_fn(resolver, original_resolver):
if resolver.func.__module__ == '__main__':
line = get_wrapped(resolver.func).resolver.func_code.co_firstlineno
js.globals.markLine(line-3)
return SynchronousExecutionMiddleware.run_resolve_fn(resolver, original_resolver)
__graphene_executor = Executor([TrackResolver()], map_type=OrderedDict)
`);
}).then(() => {
this.createSchema(this.props.initialSchema);
}).then(() => {
this.setState({pypyjs: true, response:'"Execute the query for see the results"'});
});
window.markLine = (lineNo) => {
this.markLine(lineNo);
}
this.editor = CodeMirror(ReactDOM.findDOMNode(this.refs.schemaCode), {
value: this.props.initialSchema,
mode: "python",
theme: "graphene",
lineNumbers: true,
tabSize: 4,
indentUnit: 4,
gutters: ["CodeMirror-linenumbers", "breakpoints"],
lint: {
errors: [],
},
});
this.editor.on("change", this.onEditorChange.bind(this));
}
onEditorChange() {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
if (this.props.onEditSchema) {
var value = this.editor.getValue();
if (value != this.props.initialSchema) {
this.props.onEditSchema(value)
}
}
this.changeTimeout = setTimeout(() =>
this.updateSchema()
, 300);
}
updateSchema() {
this.createSchema(this.editor.getValue());
}
createSchema(code) {
if (this.previousCode == code) return;
console.log('createSchema');
this.validSchema = null;
this.pypyjs.then(() => {
return this.pypy_interpreter.exec(`
schema = None
${code}
assert schema, 'You have to define a schema'
`)
}).then(() => {
console.log('NO ERRORS');
this.removeErrors();
this.validSchema = true;
}, (err) => {
this.editor.options.lint.errors = [];
console.log('ERRORS', err);
this.logError(err);
this.validSchema = false;
}).then(this.updateGraphiQL.bind(this));
this.previousCode = code;
}
updateGraphiQL() {
if (this.validSchema) {
this.refs.graphiql.state.schema = null;
this.refs.graphiql.componentDidMount();
this.refs.graphiql.forceUpdate();
this.refs.graphiql.refs.docExplorer.forceUpdate();
}
}
logError(error) {
var lines = error.trace.split('\n');
var file_errors = lines.map((errorLine) => {
return errorLine.match(/File "<string>", line (\d+)/);
}).filter((x) => !! x);
if (!file_errors.length) return;
var line = parseInt(file_errors[file_errors.length-1][1]);
error.line = line-2;
if (error.name == "ImportError" && error.message == "No module named django") {
error.message = "Django is not supported yet in Playground editor";
}
this.editor.options.lint.errors.push(error);
CodeMirror.signal(this.editor, 'change', this.editor);
}
removeErrors() {
this.editor.options.lint.errors = [];
CodeMirror.signal(this.editor, 'change', this.editor);
}
fetcher (graphQLParams) {
if (!this.validSchema) {
return graphQLFetcher(arguments);
}
return this.execute(graphQLParams.query);
}
execute(query) {
// console.log('execute', query);
return this.pypyjs.then(() => {
var x = `
import json
result = __graphene_executor.execute(schema.schema, '''${query}''')
result_dict = {};
if result.errors:
result_dict['errors'] = [format_error(e) for e in result.errors]
if result.data:
result_dict['data'] = result.data
result_json = json.dumps(result_dict)
`;
return this.pypy_interpreter.exec(x)
}
).then(() =>
this.pypy_interpreter.get(`result_json`)
).then((data) => {
var json_data = JSON.parse(data);
return json_data;
});
}
markLine(lineNo) {
console.log(lineNo);
var hlLine = this.editor.addLineClass(lineNo, "text", "activeline");
// var mark = this.editor.markText({line: lineNo, ch: 0}, {line: lineNo, ch: 10}, {className: "called-function"});
setTimeout(() => {
this.editor.removeLineClass(lineNo, "text", "activeline");
}, 1200);
}
render() {
return (
<div className="playground">
{!this.state.pypyjs?<div className="loading" />:null}
<div className="playground-schema">
<header className="playground-schema-header">
Schema
</header>
<div className="playground-schema-editor" ref="schemaCode" />
</div>
<div className="playground-graphiql">
<GraphiQL ref="graphiql" fetcher={this.fetcher.bind(this)} response={this.state.response} onEditQuery={this.props.onEditQuery} query={this.props.initialQuery}/>
</div>
</div>
);
}
}
module.exports = Playground;

View File

@ -0,0 +1,4 @@
query {
hello
ping(to:"Peter")
}

View File

@ -0,0 +1,13 @@
import graphene
class Query(graphene.ObjectType):
hello = graphene.String()
ping = graphene.String(to=graphene.String())
def resolve_hello(self, args, info):
return 'World'
def resolve_ping(self, args, info):
return 'Pinging {}'.format(args.get('to'))
schema = graphene.Schema(query=Query)

View File

@ -60,13 +60,6 @@ if (typeof FunctionPromise === "undefined") {
throw "FunctionPromise object not found";
}
// Some extra goodies for nodejs.
if (typeof process !== 'undefined') {
if (Object.prototype.toString.call(process) === '[object process]') {
var fs = require("fs");
var path = require("path");
}
}
// Create functions for handling default stdio streams.
// These will be shared by all VM instances by default.

View File

@ -1,258 +1,150 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { RouteHandler, Link, State } from 'react-router';
import CodeMirror from 'codemirror';
import { graphql } from 'graphql';
import GraphiQL from 'graphiql';
import schema from './schema';
import pypyjs_vm from 'pypyjs';
import GraphenePlayground from './GraphenePlayground';
import 'codemirror/mode/python/python';
import 'codemirror/addon/lint/lint';
import '../css/playground.styl';
import _ from 'lodash';
if (typeof PUBLIC_PATH === "undefined") {
var PUBLIC_PATH = '';
const DEFAULT_CACHE_KEY = 'default';
function filterObject(object, callback, context) {
if (!object) {
return null;
}
var result = {};
for (var name in object) {
if (hasOwnProperty.call(object, name) &&
callback.call(context, object[name], name, object)) {
result[name] = object[name];
}
}
return result;
}
pypyjs_vm.rootURL = `${PUBLIC_PATH}/playground/lib/`;
pypyjs_vm.cacheKey = 'graphene';
var baseCode = `import graphene
class Query(graphene.ObjectType):
hello = graphene.String()
ping = graphene.String(to=graphene.String())
def resolve_hello(self, args, info):
return 'World'
def resolve_ping(self, args, info):
return 'Pinging {}'.format(args.get('to'))
schema = graphene.Schema(query=Query)
`;
CodeMirror.registerHelper('lint', 'python', function (text, options, editor) {
return (options.errors || []).map((error) => {
var tokens = editor.getLineTokens(error.line - 1);
tokens = tokens.filter((token, pos) => {
return !!token.type || token.string.trim().length > 0;
});
if (!tokens) return [];
return {
message: `${error.name}: ${error.message}`,
severity: 'error',
type: 'syntax',
from: CodeMirror.Pos(error.line - 1, tokens[0].start),
to: CodeMirror.Pos(error.line - 1, tokens[tokens.length-1].end),
};
});
});
// function graphQLFetcher(graphQLParams) {
// return fetch('http://swapi.graphene-python.org/graphql', {
// method: 'post',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(graphQLParams),
// }).then(response => response.json());
// }
function graphQLFetcher(graphQLParams) {
return graphql(schema, graphQLParams.query);
}
// var schema = null;
function syntaxError() {
var marker = document.createElement("div");
marker.style.color = "#822";
marker.innerHTML = "●";
return marker;
}
var default_interpreter;
class Playground extends React.Component {
constructor() {
super();
this.state = {pypyjs: false, stdout: '', response:''};
}
stdout() {
console.log('stdout', arguments);
}
componentDidMount() {
if (default_interpreter) {
this.pypy_interpreter = default_interpreter;
this.pypy_interpreter.stdout = this.stdout.bind(this);
}
else {
this.pypy_interpreter = new pypyjs_vm({
stdin: function(){},
stdout: this.stdout.bind(this),
stderr: function(){},
rootURL: `${PUBLIC_PATH}/playground/lib/`
});
default_interpreter = this.pypy_interpreter;
}
componentWillMount() {
var sourceWasInjected = false;
var queryParams = this.context.router.getCurrentQuery();
this.pypyjs = this.pypy_interpreter.ready().then(() => {
return this.pypy_interpreter.exec(`
import graphene
import js
from collections import OrderedDict
from graphql.core.execution.executor import Executor
from graphql.core.execution.middlewares.sync import SynchronousExecutionMiddleware
from graphql.core.error import GraphQLError, format_error
var {
cacheKey,
noCache,
} = queryParams;
noCache = (noCache !== undefined) && (noCache !== 'false');
if (noCache) {
cacheKey = undefined;
} else if (!cacheKey) {
cacheKey = DEFAULT_CACHE_KEY;
}
this.schemaCacheKey = `rp-${cacheKey}-schema`;
this.queryCacheKey = `rp-${cacheKey}-query`;
this.cacheKey = cacheKey;
def get_wrapped(f):
if hasattr(f, 'func_closure') and f.func_closure:
return get_wrapped(f.func_closure[0].cell_contents)
return f
class TrackResolver(SynchronousExecutionMiddleware):
@staticmethod
def run_resolve_fn(resolver, original_resolver):
if resolver.func.__module__ == '__main__':
line = get_wrapped(resolver.func).resolver.func_code.co_firstlineno
js.globals.markLine(line-3)
return SynchronousExecutionMiddleware.run_resolve_fn(resolver, original_resolver)
__graphene_executor = Executor([TrackResolver()], map_type=OrderedDict)
`);
}).then(() => {
this.createSchema(baseCode);
}).then(() => {
this.setState({pypyjs: true, response:'"Execute the query for see the results"'});
});
window.markLine = (lineNo) => {
this.markLine(lineNo);
var initialSchema;
var initialQuery;
var storedSchema = localStorage.getItem(this.schemaCacheKey);
var storedQuery = localStorage.getItem(this.queryCacheKey);
if (noCache) {
// Use case #1
// We use the noCache param to force a playground to have certain contents.
// eg. static example apps
initialSchema = queryParams.schema || '';
initialQuery = queryParams.query || '';
sourceWasInjected = true;
queryParams = {};
} else if (cacheKey === DEFAULT_CACHE_KEY) {
// Use case #2
// The user loaded the playground without a custom cache key.
// Allow code injection via the URL
// OR load code from localStorage
// OR prime the playground with some default 'hello world' code
if (queryParams.schema != null) {
initialSchema = queryParams.schema;
sourceWasInjected = queryParams.schema !== storedSchema;
} else if (storedSchema != null) {
initialSchema = storedSchema;
} else {
initialSchema = require('!raw!./examples/hello.schema.py');
}
this.editor = CodeMirror(ReactDOM.findDOMNode(this.refs.schemaCode), {
value: baseCode,
mode: "python",
theme: "graphene",
lineNumbers: true,
tabSize: 4,
indentUnit: 4,
gutters: ["CodeMirror-linenumbers", "breakpoints"],
lint: {
errors: [],
},
});
this.editor.on("change", this.onEditorChange.bind(this));
if (queryParams.query != null) {
initialQuery = queryParams.query;
sourceWasInjected = queryParams.query !== storedQuery;
} else if (storedQuery != null) {
initialQuery = storedQuery;
} else {
initialQuery = require('!raw!./examples/hello.graphql');
}
onEditorChange() {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
this.changeTimeout = setTimeout(() =>
this.updateSchema()
, 300);
}
updateSchema() {
this.createSchema(this.editor.getValue());
}
createSchema(code) {
if (this.previousCode == code) return;
console.log('createSchema');
this.validSchema = null;
this.pypyjs.then(() => {
return this.pypy_interpreter.exec(`
schema = None
${code}
assert schema, 'You have to define a schema'
`)
}).then(() => {
console.log('NO ERRORS');
this.removeErrors();
this.validSchema = true;
}, (err) => {
this.editor.options.lint.errors = [];
console.log('ERRORS', err);
this.logError(err);
this.validSchema = false;
// this.editor.setGutterMarker(5, "breakpoints", syntaxError());
}).then(this.updateGraphiQL.bind(this));
this.previousCode = code;
}
updateGraphiQL() {
if (this.validSchema) {
this.refs.graphiql.state.schema = null;
this.refs.graphiql.componentDidMount();
this.refs.graphiql.forceUpdate();
this.refs.graphiql.refs.docExplorer.forceUpdate();
queryParams = filterObject({
schema: queryParams.schema,
query: queryParams.query,
}, v => v !== undefined);
} else if (cacheKey) {
// Use case #3
// Custom cache keys are useful in cases where you want to embed a playground
// that features both custom boilerplate code AND saves the developer's
// progress, without overwriting the default code cache. eg. a tutorial.
if (storedSchema != null) {
initialSchema = storedSchema;
} else {
initialSchema = queryParams[`schema_${cacheKey}`];
if (initialSchema != null) {
sourceWasInjected = true;
}
}
logError(error) {
var lines = error.trace.split('\n');
var file_errors = lines.map((errorLine) => {
return errorLine.match(/File "<string>", line (\d+)/);
}).filter((x) => !! x);
if (!file_errors.length) return;
var line = parseInt(file_errors[file_errors.length-1][1]);
error.line = line-2;
if (error.name == "ImportError" && error.message == "No module named django") {
error.message = "Django is not supported yet in Playground editor";
if (storedQuery != null) {
initialQuery = storedQuery;
} else {
initialQuery = queryParams[`query_${cacheKey}`];
if (initialQuery != null) {
sourceWasInjected = true;
}
this.editor.options.lint.errors.push(error);
CodeMirror.signal(this.editor, 'change', this.editor);
}
removeErrors() {
this.editor.options.lint.errors = [];
CodeMirror.signal(this.editor, 'change', this.editor);
queryParams = {};
}
fetcher (graphQLParams) {
if (!this.validSchema) {
return graphQLFetcher(arguments);
this.changeParams(queryParams);
this.state = {initialSchema, initialQuery, sourceWasInjected};
this.queryParams = queryParams;
}
return this.execute(graphQLParams.query);
shouldComponentUpdate() {
return false;
}
execute(query) {
// console.log('execute', query);
return this.pypyjs.then(() => {
var x = `
import json
result = __graphene_executor.execute(schema.schema, '''${query}''')
result_dict = {};
if result.errors:
result_dict['errors'] = [format_error(e) for e in result.errors]
if result.data:
result_dict['data'] = result.data
result_json = json.dumps(result_dict)
`;
return this.pypy_interpreter.exec(x)
}
).then(() =>
this.pypy_interpreter.get(`result_json`)
).then((data) => {
var json_data = JSON.parse(data);
return json_data;
});
}
markLine(lineNo) {
console.log(lineNo);
var hlLine = this.editor.addLineClass(lineNo, "text", "activeline");
// var mark = this.editor.markText({line: lineNo, ch: 0}, {line: lineNo, ch: 10}, {className: "called-function"});
setTimeout(() => {
this.editor.removeLineClass(lineNo, "text", "activeline");
}, 1200);
changeParams(queryParams) {
var router = this.context.router;
var routeName = router.getCurrentPathname();
var params = router.getCurrentParams();
queryParams = _.mapValues(queryParams, encodeURIComponent);
router.replaceWith(routeName, params, queryParams);
}
render() {
return (
<div className="playground">
{!this.state.pypyjs?<div className="loading" />:null}
<div className="playground-schema">
<header className="playground-schema-header">
Schema
</header>
<div className="playground-schema-editor" ref="schemaCode" />
</div>
<div className="playground-graphiql">
<GraphiQL ref="graphiql" fetcher={this.fetcher.bind(this)} response={this.state.response} />
</div>
</div>
);
return (<GraphenePlayground
initialSchema={this.state.initialSchema}
initialQuery={this.state.initialQuery}
onEditSchema={(source) => {
localStorage.setItem(this.schemaCacheKey, source);
if (this.cacheKey === DEFAULT_CACHE_KEY) {
this.queryParams.schema = source;
if (!this.queryParams.query) {
this.queryParams.query = this.state.initialQuery;
}
this.changeParams(this.queryParams);
}
}}
onEditQuery={(source) => {
localStorage.setItem(this.queryCacheKey, source);
if (this.cacheKey === DEFAULT_CACHE_KEY) {
this.queryParams.query = source;
if (!this.queryParams.schema) {
this.queryParams.schema = this.state.initialSchema;
}
this.changeParams(this.queryParams);
}
}}
/>);
}
};
Playground.contextTypes = {
router: React.PropTypes.func
};
module.exports = Playground;