2017-07-25 04:15:24 +00:00
|
|
|
// @flow
|
|
|
|
import type {Request} from '../../../../models/request';
|
2017-08-10 18:59:23 +00:00
|
|
|
import {newBodyRaw} from '../../../../models/request';
|
2017-07-25 04:15:24 +00:00
|
|
|
import React from 'react';
|
|
|
|
import autobind from 'autobind-decorator';
|
|
|
|
import {parse, print} from 'graphql';
|
|
|
|
import {introspectionQuery} from 'graphql/utilities/introspectionQuery';
|
|
|
|
import {buildClientSchema} from 'graphql/utilities/buildClientSchema';
|
|
|
|
import clone from 'clone';
|
|
|
|
import CodeEditor from '../../codemirror/code-editor';
|
2017-08-10 18:59:23 +00:00
|
|
|
import {jsonParseOr} from '../../../../common/misc';
|
2017-07-25 04:15:24 +00:00
|
|
|
import HelpTooltip from '../../help-tooltip';
|
2017-08-10 18:59:23 +00:00
|
|
|
import {CONTENT_TYPE_JSON, DEBOUNCE_MILLIS} from '../../../../common/constants';
|
2017-07-25 04:15:24 +00:00
|
|
|
import {prettifyJson} from '../../../../common/prettify';
|
2017-08-10 18:59:23 +00:00
|
|
|
import * as network from '../../../../network/network';
|
|
|
|
import type {Workspace} from '../../../../models/workspace';
|
|
|
|
import type {Settings} from '../../../../models/settings';
|
|
|
|
import type {RenderedRequest} from '../../../../common/render';
|
|
|
|
import {getRenderedRequest} from '../../../../common/render';
|
2017-08-10 22:26:06 +00:00
|
|
|
import TimeFromNow from '../../time-from-now';
|
2017-07-25 04:15:24 +00:00
|
|
|
|
|
|
|
type GraphQLBody = {
|
|
|
|
query: string,
|
2017-08-03 21:44:55 +00:00
|
|
|
variables: Object,
|
2017-07-25 04:15:24 +00:00
|
|
|
operationName?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
onChange: Function,
|
|
|
|
content: string,
|
|
|
|
fontSize: number,
|
|
|
|
indentSize: number,
|
|
|
|
keyMap: string,
|
|
|
|
lineWrapping: boolean,
|
|
|
|
render: Function,
|
|
|
|
getRenderContext: Function,
|
|
|
|
request: Request,
|
2017-08-10 18:59:23 +00:00
|
|
|
workspace: Workspace,
|
|
|
|
settings: Settings,
|
|
|
|
environmentId: string,
|
2017-07-25 04:15:24 +00:00
|
|
|
|
|
|
|
// Optional
|
|
|
|
className?: string
|
|
|
|
};
|
|
|
|
|
|
|
|
@autobind
|
|
|
|
class GraphQLEditor extends React.PureComponent {
|
|
|
|
props: Props;
|
|
|
|
state: {
|
|
|
|
body: GraphQLBody,
|
|
|
|
schema: Object | null,
|
|
|
|
schemaFetchError: string,
|
2017-08-10 22:26:06 +00:00
|
|
|
schemaLastFetchTime: number,
|
|
|
|
schemaIsFetching: boolean,
|
2017-07-25 18:00:30 +00:00
|
|
|
hideSchemaFetchErrors: boolean,
|
2017-07-25 04:15:24 +00:00
|
|
|
variablesSyntaxError: string,
|
|
|
|
forceRefreshKey: number
|
|
|
|
};
|
2017-07-25 18:00:30 +00:00
|
|
|
_isMounted: boolean;
|
2017-07-25 04:15:24 +00:00
|
|
|
|
|
|
|
constructor (props: Props) {
|
|
|
|
super(props);
|
2017-07-25 18:00:30 +00:00
|
|
|
this._isMounted = false;
|
2017-07-25 04:15:24 +00:00
|
|
|
this.state = {
|
|
|
|
body: this._stringToGraphQL(props.content),
|
|
|
|
schema: null,
|
|
|
|
schemaFetchError: '',
|
2017-08-10 22:26:06 +00:00
|
|
|
schemaLastFetchTime: 0,
|
|
|
|
schemaIsFetching: false,
|
2017-07-25 18:00:30 +00:00
|
|
|
hideSchemaFetchErrors: false,
|
2017-07-25 04:15:24 +00:00
|
|
|
variablesSyntaxError: '',
|
|
|
|
forceRefreshKey: 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_hideSchemaFetchError () {
|
2017-07-25 18:00:30 +00:00
|
|
|
this.setState({hideSchemaFetchErrors: true});
|
2017-07-25 04:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async _fetchAndSetSchema (rawRequest: Request) {
|
2017-08-10 22:26:06 +00:00
|
|
|
this.setState({schemaIsFetching: true});
|
|
|
|
|
2017-08-10 18:59:23 +00:00
|
|
|
const {workspace, settings, environmentId} = this.props;
|
2017-08-14 18:56:34 +00:00
|
|
|
|
2017-08-10 22:26:06 +00:00
|
|
|
const newState = {
|
|
|
|
schema: this.state.schema,
|
|
|
|
schemaFetchError: '',
|
|
|
|
schemaLastFetchTime: this.state.schemaLastFetchTime,
|
|
|
|
schemaIsFetching: false
|
|
|
|
};
|
2017-07-25 18:00:30 +00:00
|
|
|
|
2017-08-14 19:04:46 +00:00
|
|
|
let request: RenderedRequest | null = null;
|
2017-08-14 18:56:34 +00:00
|
|
|
try {
|
|
|
|
request = await getRenderedRequest(rawRequest, environmentId);
|
|
|
|
} catch (err) {
|
|
|
|
newState.schemaFetchError = `Failed to fetch schema: ${err}`;
|
|
|
|
}
|
|
|
|
|
2017-08-14 19:04:46 +00:00
|
|
|
if (request) {
|
|
|
|
try {
|
|
|
|
// TODO: Use Insomnia's network stack to handle things like authentication
|
|
|
|
const bodyJson = JSON.stringify({query: introspectionQuery});
|
|
|
|
const introspectionRequest = Object.assign({}, request, {
|
|
|
|
body: newBodyRaw(bodyJson, CONTENT_TYPE_JSON),
|
|
|
|
|
|
|
|
// NOTE: We're not actually saving this request or response but let's pretend
|
|
|
|
// like we are by setting these properties to prevent bugs in the future.
|
|
|
|
_id: request._id + '.graphql',
|
|
|
|
parentId: request._id
|
|
|
|
});
|
|
|
|
|
|
|
|
const {bodyBuffer, response} = await network._actuallySend(
|
|
|
|
introspectionRequest,
|
|
|
|
workspace,
|
|
|
|
settings
|
|
|
|
);
|
|
|
|
|
|
|
|
const status = response.statusCode || 0;
|
|
|
|
|
|
|
|
if (response.error) {
|
|
|
|
newState.schemaFetchError = response.error;
|
|
|
|
} else if (status < 200 || status >= 300) {
|
|
|
|
const msg = `Got status ${status} fetching schema from "${request.url}"`;
|
|
|
|
newState.schemaFetchError = msg;
|
|
|
|
} else if (bodyBuffer) {
|
|
|
|
const {data} = JSON.parse(bodyBuffer.toString());
|
|
|
|
const schema = buildClientSchema(data);
|
|
|
|
newState.schema = schema;
|
|
|
|
newState.schemaLastFetchTime = Date.now();
|
|
|
|
} else {
|
|
|
|
newState.schemaFetchError = 'No response body received when fetching schema';
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.warn(`Failed to fetch GraphQL schema from ${request.url}`, err);
|
|
|
|
newState.schemaFetchError = `Failed to contact "${request.url}" to fetch schema`;
|
2017-07-25 04:15:24 +00:00
|
|
|
}
|
2017-07-25 18:00:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this._isMounted) {
|
|
|
|
this.setState(newState);
|
2017-07-25 04:15:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handlePrettify () {
|
|
|
|
const {body, forceRefreshKey} = this.state;
|
|
|
|
const {variables, query} = body;
|
2017-08-03 21:44:55 +00:00
|
|
|
const prettyQuery = query && print(parse(query));
|
2017-08-10 22:26:06 +00:00
|
|
|
const prettyVariables = variables && JSON.parse(prettifyJson(JSON.stringify(variables)));
|
2017-08-03 21:44:55 +00:00
|
|
|
this._handleBodyChange(prettyQuery, prettyVariables);
|
2017-07-25 04:15:24 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
this.setState({forceRefreshKey: forceRefreshKey + 1});
|
|
|
|
}, 200);
|
|
|
|
}
|
|
|
|
|
2017-08-03 21:44:55 +00:00
|
|
|
_handleBodyChange (query: string, variables: Object): void {
|
2017-07-25 04:15:24 +00:00
|
|
|
const body = clone(this.state.body);
|
|
|
|
const newState = {variablesSyntaxError: '', body};
|
|
|
|
|
|
|
|
newState.body.query = query;
|
|
|
|
newState.body.variables = variables;
|
|
|
|
this.setState(newState);
|
|
|
|
this.props.onChange(this._graphQLToString(body));
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleQueryChange (query: string): void {
|
|
|
|
this._handleBodyChange(query, this.state.body.variables);
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleVariablesChange (variables: string): void {
|
2017-08-03 21:44:55 +00:00
|
|
|
try {
|
|
|
|
const variablesObj = JSON.parse(variables || '{}');
|
|
|
|
this._handleBodyChange(this.state.body.query, variablesObj);
|
|
|
|
} catch (err) {
|
|
|
|
this.setState({variablesSyntaxError: err.message});
|
|
|
|
}
|
2017-07-25 04:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_stringToGraphQL (text: string): GraphQLBody {
|
|
|
|
let obj;
|
|
|
|
try {
|
|
|
|
obj = JSON.parse(text);
|
|
|
|
} catch (err) {
|
2017-08-03 21:44:55 +00:00
|
|
|
obj = {query: '', variables: {}};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof obj.variables === 'string') {
|
|
|
|
obj.variables = jsonParseOr(obj.variables, {});
|
2017-07-25 04:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
query: obj.query || '',
|
2017-08-03 21:44:55 +00:00
|
|
|
variables: obj.variables || {}
|
2017-07-25 04:15:24 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_graphQLToString (body: GraphQLBody): string {
|
|
|
|
return JSON.stringify(body);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillReceiveProps (nextProps: Props) {
|
|
|
|
if (nextProps.request.url !== this.props.request.url) {
|
|
|
|
(async () => await this._fetchAndSetSchema(nextProps.request))();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount () {
|
|
|
|
this._fetchAndSetSchema(this.props.request);
|
2017-07-25 18:00:30 +00:00
|
|
|
this._isMounted = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount () {
|
|
|
|
this._isMounted = false;
|
2017-07-25 04:15:24 +00:00
|
|
|
}
|
|
|
|
|
2017-08-10 22:26:06 +00:00
|
|
|
renderSchemaFetchMessage () {
|
|
|
|
let message;
|
|
|
|
const {schemaLastFetchTime, schemaIsFetching} = this.state;
|
|
|
|
if (schemaIsFetching) {
|
|
|
|
message = 'Fetching schema...';
|
|
|
|
} else if (schemaLastFetchTime > 0) {
|
|
|
|
message = <span>schema last fetched <TimeFromNow timestamp={schemaLastFetchTime}/></span>;
|
|
|
|
} else {
|
|
|
|
message = 'schema not yet fetched';
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="txt-sm super-faint italic pad-sm">
|
|
|
|
{message}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-07-25 04:15:24 +00:00
|
|
|
render () {
|
|
|
|
const {
|
|
|
|
content,
|
|
|
|
fontSize,
|
|
|
|
indentSize,
|
|
|
|
keyMap,
|
|
|
|
render,
|
|
|
|
getRenderContext,
|
|
|
|
lineWrapping,
|
|
|
|
className
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const {
|
|
|
|
schema,
|
|
|
|
schemaFetchError,
|
2017-07-25 18:00:30 +00:00
|
|
|
hideSchemaFetchErrors,
|
2017-07-25 04:15:24 +00:00
|
|
|
variablesSyntaxError,
|
|
|
|
forceRefreshKey
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
const {
|
|
|
|
query,
|
2017-08-03 21:44:55 +00:00
|
|
|
variables: variablesObject
|
2017-07-25 04:15:24 +00:00
|
|
|
} = this._stringToGraphQL(content);
|
|
|
|
|
2017-08-03 21:44:55 +00:00
|
|
|
const variables = prettifyJson(JSON.stringify(variablesObject));
|
|
|
|
|
2017-07-25 04:15:24 +00:00
|
|
|
return (
|
|
|
|
<div key={forceRefreshKey} className="graphql-editor">
|
|
|
|
<div className="graphql-editor__query">
|
|
|
|
<CodeEditor
|
|
|
|
dynamicHeight
|
|
|
|
manualPrettify
|
2017-07-25 18:00:30 +00:00
|
|
|
hintOptions={{
|
|
|
|
schema: schema || null,
|
|
|
|
completeSingle: false
|
|
|
|
}}
|
2017-07-25 04:15:24 +00:00
|
|
|
lintOptions={schema ? {schema} : null}
|
|
|
|
fontSize={fontSize}
|
|
|
|
indentSize={indentSize}
|
|
|
|
keyMap={keyMap}
|
|
|
|
defaultValue={query}
|
|
|
|
className={className}
|
|
|
|
onChange={this._handleQueryChange}
|
|
|
|
mode="graphql"
|
|
|
|
lineWrapping={lineWrapping}
|
2017-07-25 18:00:30 +00:00
|
|
|
placeholder=""
|
2017-07-25 04:15:24 +00:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="graphql-editor__schema-error">
|
2017-07-25 18:00:30 +00:00
|
|
|
{!hideSchemaFetchErrors && schemaFetchError && (
|
|
|
|
<div className="notice error margin no-margin-top margin-bottom-sm">
|
2017-07-25 04:15:24 +00:00
|
|
|
<button className="pull-right icon" onClick={this._hideSchemaFetchError}>
|
|
|
|
<i className="fa fa-times"/>
|
|
|
|
</button>
|
|
|
|
{schemaFetchError}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
2017-08-10 22:26:06 +00:00
|
|
|
<div className="graphql-editor__schema-notice">
|
|
|
|
{this.renderSchemaFetchMessage()}
|
|
|
|
</div>
|
2017-07-25 04:15:24 +00:00
|
|
|
<h2 className="no-margin pad-left-sm pad-top-sm pad-bottom-sm">
|
2017-07-25 18:00:30 +00:00
|
|
|
Query Variables <HelpTooltip>Variables to use in GraphQL query <br/>(JSON
|
|
|
|
format)</HelpTooltip>
|
2017-07-25 04:15:24 +00:00
|
|
|
{variablesSyntaxError && (
|
|
|
|
<span className="text-danger italic pull-right">
|
|
|
|
{variablesSyntaxError}
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</h2>
|
|
|
|
<div className="graphql-editor__variables">
|
|
|
|
<CodeEditor
|
|
|
|
dynamicHeight
|
|
|
|
debounceMillis={DEBOUNCE_MILLIS * 4}
|
|
|
|
manualPrettify={false}
|
|
|
|
fontSize={fontSize}
|
|
|
|
indentSize={indentSize}
|
|
|
|
keyMap={keyMap}
|
2017-08-03 21:44:55 +00:00
|
|
|
defaultValue={variables}
|
2017-07-25 04:15:24 +00:00
|
|
|
className={className}
|
|
|
|
render={render}
|
|
|
|
getRenderContext={getRenderContext}
|
|
|
|
onChange={this._handleVariablesChange}
|
|
|
|
mode="application/json"
|
|
|
|
lineWrapping={lineWrapping}
|
|
|
|
placeholder=""
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="pane__footer">
|
|
|
|
<button className="pull-right btn btn--compact" onClick={this._handlePrettify}>
|
|
|
|
Prettify GraphQL
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default GraphQLEditor;
|