mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-23 15:19:04 -04:00
GraphQL Docs Explorer (#1616)
* Implement a GraphQLExplorer for visually browsing schemas * Fix some type stuff and theme * Adjust GraphQL explorer colors for fields to match Codemirror * Explorer state not modified when showing docs from dropdown * A few more small tweaks and fixes
This commit is contained in:
1 parent
465f7247f6
commit
bb5eaaa871
16 files changed
+690
-101
No files matched your search
Generated
+3
-3
@@ -8291,9 +8291,9 @@
|
||||
}
|
||||
},
|
||||
"flow-bin": {
|
||||
"version": "0.96.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.96.0.tgz",
|
||||
"integrity": "sha512-OSxERs0EdhVxEVCst/HmlT/RcnXsQQIRqcfK9J9wC8/93JQj+xQz4RtlsmYe1PSRYaozuDLyPS5pIA81Zwzaww==",
|
||||
"version": "0.104.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.104.0.tgz",
|
||||
"integrity": "sha512-EZXRRmf7m7ET5Lcnwm/I/T8G3d427Bq34vmO3qIlRcPIYloGuVoqRCwjaeezLRDntHkdciagAKbhJ+NTbDjnkw==",
|
||||
"dev": true
|
||||
},
|
||||
"flush-write-stream": {
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-jest": "^24.7.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"flow-bin": "^0.96.0",
|
||||
"flow-bin": "^0.104.0",
|
||||
"husky": "^1.0.0-rc.9",
|
||||
"jest": "^24.7.1",
|
||||
"lerna": "^3.14.1",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
[ignore]
|
||||
.*/node_modules/.*
|
||||
.*/__fixtures__/.*
|
||||
!<PROJECT_ROOT>/node_modules/graphql
|
||||
!<PROJECT_ROOT>/node_modules/iterall
|
||||
|
||||
[include]
|
||||
../../plugins
|
||||
../../packages
|
||||
|
||||
[libs]
|
||||
flow-typed/
|
||||
|
||||
[options]
|
||||
esproposal.decorators=ignore
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="dropdowns-container" style="z-index:1000000;position:relative"></div>
|
||||
<div id="graphql-explorer-container"></div>
|
||||
<script src="bundle.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -585,12 +585,20 @@ class CodeEditor extends React.Component {
|
||||
// Strip of charset if there is one
|
||||
const cm = this.codeMirror;
|
||||
Object.keys(options).map(key => {
|
||||
// Don't set the option if it hasn't changed
|
||||
if (deepEqual(options[key], cm.options[key])) {
|
||||
return;
|
||||
let shouldSetOption = false;
|
||||
|
||||
if (key === 'jump' || key === 'info' || key === 'lint' || key === 'hintOptions') {
|
||||
// Use stringify here because these could be infinitely recursive due to GraphQL
|
||||
// schemas
|
||||
shouldSetOption = JSON.stringify(options[key]) !== JSON.stringify(cm.options[key]);
|
||||
} else if (!deepEqual(options[key], cm.options[key])) {
|
||||
// Don't set the option if it hasn't changed
|
||||
shouldSetOption = true;
|
||||
}
|
||||
|
||||
cm.setOption(key, options[key]);
|
||||
if (shouldSetOption) {
|
||||
cm.setOption(key, options[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// @flow
|
||||
import classnames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import autobind from 'autobind-decorator';
|
||||
import { markdownToHTML } from '../../../../common/markdown-to-html';
|
||||
import { parse, print, typeFromAST, type Document as DocumentAST } from 'graphql';
|
||||
import type { GraphQLArgument, GraphQLField, GraphQLSchema, GraphQLType } from 'graphql';
|
||||
import { parse, print, typeFromAST } from 'graphql';
|
||||
import { introspectionQuery } from 'graphql/utilities/introspectionQuery';
|
||||
import { buildClientSchema } from 'graphql/utilities/buildClientSchema';
|
||||
import type { CodeMirror, TextMarker } from 'codemirror';
|
||||
@@ -25,6 +27,12 @@ import { newBodyRaw } from '../../../../models/request';
|
||||
import ResponseDebugModal from '../../modals/response-debug-modal';
|
||||
import Tooltip from '../../tooltip';
|
||||
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../../base/dropdown';
|
||||
import GraphqlExplorer from '../../graph-ql-explorer/graph-ql-explorer';
|
||||
|
||||
const explorerContainer = document.querySelector('#graphql-explorer-container');
|
||||
if (!explorerContainer) {
|
||||
throw new Error('Failed to find #graphql-explorer-container');
|
||||
}
|
||||
|
||||
type GraphQLBody = {
|
||||
query: string,
|
||||
@@ -49,7 +57,7 @@ type Props = {
|
||||
|
||||
type State = {
|
||||
body: GraphQLBody,
|
||||
schema: Object | null,
|
||||
schema: GraphQLSchema | null,
|
||||
schemaFetchError: {
|
||||
message: string,
|
||||
response: ResponsePatch | null,
|
||||
@@ -59,12 +67,18 @@ type State = {
|
||||
hideSchemaFetchErrors: boolean,
|
||||
variablesSyntaxError: string,
|
||||
automaticFetch: boolean,
|
||||
explorerVisible: boolean,
|
||||
activeReference: null | {
|
||||
type: GraphQLType | null,
|
||||
argument: GraphQLArgument | null,
|
||||
field: GraphQLField<any, any> | null,
|
||||
},
|
||||
};
|
||||
|
||||
@autobind
|
||||
class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
_disabledOperationMarkers: TextMarker[];
|
||||
_documentAST: null | DocumentAST;
|
||||
_documentAST: null | Object;
|
||||
_isMounted: boolean;
|
||||
_queryEditor: null | CodeMirror;
|
||||
_schemaFetchTimeout: TimeoutID;
|
||||
@@ -92,6 +106,8 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
schemaIsFetching: false,
|
||||
hideSchemaFetchErrors: false,
|
||||
variablesSyntaxError: '',
|
||||
activeReference: null,
|
||||
explorerVisible: false,
|
||||
automaticFetch,
|
||||
};
|
||||
}
|
||||
@@ -142,6 +158,15 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
return operationName;
|
||||
}
|
||||
|
||||
_handleCloseExplorer() {
|
||||
this.setState({ explorerVisible: false });
|
||||
}
|
||||
|
||||
_handleClickReference(reference: Object, e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
this.setState({ explorerVisible: true, activeReference: reference });
|
||||
}
|
||||
|
||||
_handleQueryFocus() {
|
||||
this._handleQueryUserActivity();
|
||||
}
|
||||
@@ -314,6 +339,12 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
return variableToType;
|
||||
}
|
||||
|
||||
_handleShowDocumentation() {
|
||||
this.setState({
|
||||
explorerVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleRefreshSchema(): Promise<void> {
|
||||
await this._fetchAndSetSchema(this.props.request);
|
||||
}
|
||||
@@ -474,6 +505,11 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
return message;
|
||||
}
|
||||
|
||||
static renderMarkdown(text: string) {
|
||||
const html = markdownToHTML(text);
|
||||
return `<div class="markdown-preview__content">${html}</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
content,
|
||||
@@ -492,6 +528,8 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
variablesSyntaxError,
|
||||
schemaIsFetching,
|
||||
automaticFetch,
|
||||
activeReference,
|
||||
explorerVisible,
|
||||
} = this.state;
|
||||
|
||||
const { query, variables: variablesObject } = GraphQLEditor._stringToGraphQL(content);
|
||||
@@ -500,6 +538,17 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
|
||||
const variableTypes = this._buildVariableTypes(schema);
|
||||
|
||||
// Create portal for GraphQL Explorer
|
||||
const graphQLExplorerPortal = ReactDOM.createPortal(
|
||||
<GraphqlExplorer
|
||||
schema={schema}
|
||||
visible={explorerVisible}
|
||||
reference={activeReference}
|
||||
handleClose={this._handleCloseExplorer}
|
||||
/>,
|
||||
explorerContainer,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="graphql-editor">
|
||||
<Dropdown right className="graphql-editor__schema-dropdown margin-bottom-xs">
|
||||
@@ -507,6 +556,9 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
schema <i className="fa fa-wrench" />
|
||||
</DropdownButton>
|
||||
<DropdownDivider>GraphQL Schema</DropdownDivider>
|
||||
<DropdownItem onClick={this._handleShowDocumentation} disabled={!schema}>
|
||||
<i className="fa fa-file-code-o" /> Show Documentation
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this._handleRefreshSchema} stayOpenAfterClick>
|
||||
<i className={'fa fa-refresh ' + (schemaIsFetching ? 'fa-spin' : '')} /> Refresh Schema
|
||||
</DropdownItem>
|
||||
@@ -532,16 +584,13 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
}}
|
||||
infoOptions={{
|
||||
schema: schema || null,
|
||||
renderDescription: text => {
|
||||
const html = markdownToHTML(text);
|
||||
return `<div class="markdown-preview__content">${html}</div>`;
|
||||
},
|
||||
// onClick: reference => console.log('CLICK', reference)
|
||||
renderDescription: GraphQLEditor.renderMarkdown,
|
||||
onClick: this._handleClickReference,
|
||||
}}
|
||||
jumpOptions={{
|
||||
schema: schema || null,
|
||||
onClick: this._handleClickReference,
|
||||
}}
|
||||
// jumpOptions={{
|
||||
// schema: schema || null,
|
||||
// onClick: reference => console.log('JUMP', reference)
|
||||
// }}
|
||||
lintOptions={schema ? { schema } : null}
|
||||
fontSize={settings.editorFontSize}
|
||||
indentSize={settings.editorIndentSize}
|
||||
@@ -619,6 +668,8 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
Prettify GraphQL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{graphQLExplorerPortal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import type { GraphQLField } from 'graphql';
|
||||
|
||||
type Props = {
|
||||
onNavigate: (type: Object) => void,
|
||||
field: GraphQLField<any, any>,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class GraphQLExplorerFieldLink extends React.PureComponent<Props> {
|
||||
_handleClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
const { onNavigate, field } = this.props;
|
||||
onNavigate(field);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field } = this.props;
|
||||
|
||||
return (
|
||||
<a href="#" onClick={this._handleClick} className="success">
|
||||
{field.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphQLExplorerFieldLink;
|
||||
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import type { GraphQLField, GraphQLType } from 'graphql';
|
||||
import GraphQLExplorerTypeLink from './graph-ql-explorer-type-link';
|
||||
import MarkdownPreview from '../markdown-preview';
|
||||
|
||||
type Props = {
|
||||
onNavigateType: (type: GraphQLType) => void,
|
||||
field: GraphQLField<any, any>,
|
||||
};
|
||||
|
||||
class GraphQLExplorerField extends React.PureComponent<Props> {
|
||||
renderDescription() {
|
||||
const { field } = this.props;
|
||||
return <MarkdownPreview markdown={field.description || '*no description*'} />;
|
||||
}
|
||||
|
||||
renderType() {
|
||||
const { field, onNavigateType } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2 className="graphql-explorer__subheading">Type</h2>
|
||||
<GraphQLExplorerTypeLink type={field.type} onNavigate={onNavigateType} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderArgumentsMaybe() {
|
||||
const { field, onNavigateType } = this.props;
|
||||
if (!field.args || field.args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2 className="graphql-explorer__subheading">Arguments</h2>
|
||||
<ul className="graphql-explorer__defs">
|
||||
{field.args.map(a => (
|
||||
<li key={a.name}>
|
||||
<span className="info">{a.name}</span>:{' '}
|
||||
<GraphQLExplorerTypeLink onNavigate={onNavigateType} type={a.type} />
|
||||
{a.description && <MarkdownPreview markdown={a.description} />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="graphql-explorer__field">
|
||||
{this.renderDescription()}
|
||||
{this.renderType()}
|
||||
{this.renderArgumentsMaybe()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphQLExplorerField;
|
||||
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import type { GraphQLSchema, GraphQLType } from 'graphql';
|
||||
import GraphQLExplorerTypeLink from './graph-ql-explorer-type-link';
|
||||
|
||||
type Props = {
|
||||
onNavigateType: (type: GraphQLType) => void,
|
||||
schema: GraphQLSchema,
|
||||
};
|
||||
|
||||
class GraphQLExplorerSchema extends React.PureComponent<Props> {
|
||||
renderQueryType() {
|
||||
const { schema, onNavigateType } = this.props;
|
||||
|
||||
const type = schema.getQueryType();
|
||||
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className="success">query</span>:{' '}
|
||||
<GraphQLExplorerTypeLink onNavigate={onNavigateType} type={type} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderMutationType() {
|
||||
const { schema, onNavigateType } = this.props;
|
||||
|
||||
const type = schema.getMutationType();
|
||||
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className="success">mutation</span>:{' '}
|
||||
<GraphQLExplorerTypeLink onNavigate={onNavigateType} type={type} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="graphql-explorer__schema">
|
||||
<p>A GraphQL schema provides a root type for each kind of operation.</p>
|
||||
<h2 className="graphql-explorer__subheading">Root Types</h2>
|
||||
<ul className="graphql-explorer__defs">
|
||||
<li>{this.renderQueryType()}</li>
|
||||
<li>{this.renderMutationType()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphQLExplorerSchema;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import { GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
import type { GraphQLType } from 'graphql';
|
||||
|
||||
type Props = {
|
||||
onNavigate: (type: Object) => void,
|
||||
type: GraphQLType,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class GraphQLExplorerTypeLink extends React.PureComponent<Props> {
|
||||
_handleClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
const { onNavigate, type } = this.props;
|
||||
onNavigate(type);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { type, onNavigate } = this.props;
|
||||
|
||||
if (type instanceof GraphQLList) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
[
|
||||
<GraphQLExplorerTypeLink onNavigate={onNavigate} type={type.ofType} />]
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (type instanceof GraphQLNonNull) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GraphQLExplorerTypeLink onNavigate={onNavigate} type={type.ofType} />!
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a href="#" onClick={this._handleClick} className="notice">
|
||||
{type.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphQLExplorerTypeLink;
|
||||
@@ -0,0 +1,101 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import GraphQLExplorerTypeLink from './graph-ql-explorer-type-link';
|
||||
import autobind from 'autobind-decorator';
|
||||
import MarkdownPreview from '../markdown-preview';
|
||||
import GraphQLExplorerFieldLink from './graph-ql-explorer-field-link';
|
||||
import type { GraphQLType, GraphQLField } from 'graphql';
|
||||
|
||||
type Props = {
|
||||
onNavigateType: (type: Object) => void,
|
||||
onNavigateField: (field: Object) => void,
|
||||
type: GraphQLType,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class GraphQLExplorerType extends React.PureComponent<Props> {
|
||||
_handleNavigateType(type: Object) {
|
||||
const { onNavigateType } = this.props;
|
||||
onNavigateType(type);
|
||||
}
|
||||
|
||||
_handleNavigateField(field: Object) {
|
||||
const { onNavigateField } = this.props;
|
||||
onNavigateField(field);
|
||||
}
|
||||
|
||||
renderDescription() {
|
||||
const { type } = this.props;
|
||||
return <MarkdownPreview markdown={type.description || '*no description*'} />;
|
||||
}
|
||||
|
||||
renderFieldsMaybe() {
|
||||
const { type, onNavigateType } = this.props;
|
||||
if (typeof type.getFields !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
const fields = type.getFields();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2 className="graphql-explorer__subheading">Fields</h2>
|
||||
<ul className="graphql-explorer__defs">
|
||||
{Object.keys(fields).map(key => {
|
||||
const field: GraphQLField<any, any> = (fields[key]: any);
|
||||
|
||||
let argLinks = null;
|
||||
const args = (field: any).args;
|
||||
if (args && args.length) {
|
||||
argLinks = (
|
||||
<React.Fragment>
|
||||
(
|
||||
{args.map(a => (
|
||||
<div key={a.name} className="graphql-explorer__defs__arg">
|
||||
<span className="info">{a.name}</span>:{' '}
|
||||
<GraphQLExplorerTypeLink onNavigate={onNavigateType} type={a.type} />
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const fieldLink = (
|
||||
<GraphQLExplorerFieldLink onNavigate={this._handleNavigateField} field={field} />
|
||||
);
|
||||
|
||||
const typeLink = (
|
||||
<GraphQLExplorerTypeLink onNavigate={this._handleNavigateType} type={field.type} />
|
||||
);
|
||||
|
||||
const description = field.description;
|
||||
return (
|
||||
<li key={key}>
|
||||
{fieldLink}
|
||||
{argLinks}: {typeLink}
|
||||
{description && (
|
||||
<div className="graphql-explorer__defs__description">
|
||||
<MarkdownPreview markdown={description} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="graphql-explorer__type">
|
||||
{this.renderDescription()}
|
||||
{this.renderFieldsMaybe()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphQLExplorerType;
|
||||
@@ -0,0 +1,200 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import GraphQLExplorerField from './graph-ql-explorer-field';
|
||||
import GraphQLExplorerType from './graph-ql-explorer-type';
|
||||
import type { GraphQLArgument, GraphQLField, GraphQLSchema, GraphQLType } from 'graphql';
|
||||
import GraphQLExplorerSchema from './graph-ql-explorer-schema';
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void,
|
||||
schema: GraphQLSchema | null,
|
||||
visible: boolean,
|
||||
reference: null | {
|
||||
type: GraphQLType | null,
|
||||
argument: GraphQLArgument | null,
|
||||
field: GraphQLField<any, any> | null,
|
||||
},
|
||||
};
|
||||
|
||||
type HistoryItem = {
|
||||
currentType: null | GraphQLType,
|
||||
currentField: null | GraphQLField<any, any>,
|
||||
};
|
||||
|
||||
type State = HistoryItem & {
|
||||
history: Array<HistoryItem>,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class GraphQLExplorer extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
history: [],
|
||||
currentType: null,
|
||||
currentField: null,
|
||||
};
|
||||
}
|
||||
|
||||
_handleNavigateType(type: GraphQLType) {
|
||||
this.setState({
|
||||
currentType: type,
|
||||
currentField: null,
|
||||
history: this._addToHistory(),
|
||||
});
|
||||
}
|
||||
|
||||
_handleNavigateField(field: GraphQLField<any, any>) {
|
||||
this.setState({
|
||||
currentType: field.type,
|
||||
currentField: field,
|
||||
history: this._addToHistory(),
|
||||
});
|
||||
}
|
||||
|
||||
_handlePopHistory() {
|
||||
this.setState(({ history }) => {
|
||||
const last = history[history.length - 1] || null;
|
||||
return {
|
||||
history: history.slice(0, history.length - 1),
|
||||
currentType: last ? last.currentType : null,
|
||||
currentField: last ? last.currentField : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_addToHistory() {
|
||||
const { currentType, currentField, history } = this.state;
|
||||
|
||||
// Nothing to add
|
||||
if (!currentType && !currentField) {
|
||||
return history;
|
||||
}
|
||||
|
||||
return [...history, { currentType, currentField }];
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (!nextProps.reference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, field } = nextProps.reference;
|
||||
const { currentType, currentField } = this.state;
|
||||
|
||||
const compare = (a: any, b: any) => (!a && !b) || (a && b && a.name === b.name);
|
||||
|
||||
const sameType = compare(currentType, type);
|
||||
const sameField = compare(currentField, field);
|
||||
const nothingChanged = sameType && sameField;
|
||||
|
||||
if (nothingChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
let history = this._addToHistory();
|
||||
|
||||
this.setState({
|
||||
history,
|
||||
currentType: type || null,
|
||||
currentField: field || null,
|
||||
});
|
||||
}
|
||||
|
||||
renderHistoryItem() {
|
||||
const { history, currentField, currentType } = this.state;
|
||||
|
||||
if (history.length === 0 && (currentType || currentField)) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="graphql-explorer__header__back-btn"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
this._handlePopHistory();
|
||||
}}>
|
||||
<i className="fa--skinny fa fa-angle-left" /> Schema
|
||||
</a>
|
||||
);
|
||||
} else if (history.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { currentType: lastType, currentField: lastField } = history[history.length - 1];
|
||||
|
||||
let name = null;
|
||||
|
||||
if (lastField) {
|
||||
name = (lastField: any).name || 'Unknown';
|
||||
} else if (lastType) {
|
||||
name = (lastType: any).name || 'Unknown';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="graphql-explorer__header__back-btn"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
this._handlePopHistory();
|
||||
}}>
|
||||
<i className="fa--skinny fa fa-angle-left" /> {name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { schema, handleClose, visible } = this.props;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { currentType, currentField } = this.state;
|
||||
|
||||
let child = null;
|
||||
if (currentField) {
|
||||
child = (
|
||||
<GraphQLExplorerField onNavigateType={this._handleNavigateType} field={currentField} />
|
||||
);
|
||||
} else if (currentType) {
|
||||
child = (
|
||||
<GraphQLExplorerType
|
||||
type={currentType}
|
||||
onNavigateType={this._handleNavigateType}
|
||||
onNavigateField={this._handleNavigateField}
|
||||
/>
|
||||
);
|
||||
} else if (schema) {
|
||||
child = <GraphQLExplorerSchema onNavigateType={this._handleNavigateType} schema={schema} />;
|
||||
}
|
||||
|
||||
if (!child) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldName = currentField ? (currentField: any).name : null;
|
||||
const typeName = currentType ? (currentType: any).name : null;
|
||||
const schemaName = schema ? 'Schema' : null;
|
||||
|
||||
return (
|
||||
<div className="graphql-explorer theme--dialog">
|
||||
<div className="graphql-explorer__header">
|
||||
{this.renderHistoryItem()}
|
||||
<h1>{fieldName || typeName || schemaName || 'Unknown'}</h1>
|
||||
<button
|
||||
className="btn btn--compact graphql-explorer__header__close-btn"
|
||||
onClick={handleClose}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="graphql-explorer__body">{child}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphQLExplorer;
|
||||
@@ -22,20 +22,25 @@ class MarkdownPreview extends PureComponent {
|
||||
*/
|
||||
_compileMarkdown(markdown) {
|
||||
clearTimeout(this._compileTimeout);
|
||||
this._compileTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const rendered = await this.props.handleRender(markdown);
|
||||
this.setState({
|
||||
compiled: markdownToHTML(rendered),
|
||||
renderError: '',
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
renderError: err.message,
|
||||
compiled: '',
|
||||
});
|
||||
}
|
||||
}, this.state.compiled ? this.props.debounceMillis : 0);
|
||||
this._compileTimeout = setTimeout(
|
||||
async () => {
|
||||
try {
|
||||
const { handleRender } = this.props;
|
||||
const rendered = handleRender ? await handleRender(markdown) : markdown;
|
||||
const compiled = markdownToHTML(rendered);
|
||||
this.setState({
|
||||
compiled,
|
||||
renderError: '',
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
renderError: err.message,
|
||||
compiled: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
this.state.compiled ? this.props.debounceMillis : 0,
|
||||
);
|
||||
}
|
||||
|
||||
_setPreviewRef(n) {
|
||||
@@ -106,9 +111,9 @@ class MarkdownPreview extends PureComponent {
|
||||
MarkdownPreview.propTypes = {
|
||||
// Required
|
||||
markdown: PropTypes.string.isRequired,
|
||||
handleRender: PropTypes.func.isRequired,
|
||||
|
||||
// Optional
|
||||
handleRender: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
debounceMillis: PropTypes.number,
|
||||
heading: PropTypes.string,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
@import '../constants/dimensions';
|
||||
@import '../constants/colors';
|
||||
|
||||
.graphql-explorer {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
z-index: 100;
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.4);
|
||||
|
||||
.markdown-preview__content *:first-child {
|
||||
margin-top: @padding-sm;
|
||||
}
|
||||
|
||||
&__subheading {
|
||||
border-bottom: 1px solid var(--hl-md);
|
||||
padding-bottom: @padding-sm;
|
||||
margin-top: @padding-lg;
|
||||
margin-bottom: @padding-md;
|
||||
}
|
||||
|
||||
&__header {
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid @hl-md;
|
||||
box-sizing: border-box;
|
||||
height: @height-nav;
|
||||
line-height: @line-height-md;
|
||||
color: var(--color-font);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
|
||||
h1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: @font-size-lg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& > * {
|
||||
white-space: nowrap;
|
||||
padding-left: @padding-md;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__close-btn {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
box-sizing: border-box;
|
||||
padding: @padding-md @padding-md 10rem;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
&__defs {
|
||||
&__description {
|
||||
margin-left: @padding-sm;
|
||||
color: var(--color-font);
|
||||
}
|
||||
|
||||
&__arg {
|
||||
margin-left: @padding-md;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: @padding-md;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
@import 'components/cookie-list';
|
||||
@import 'components/cookie-modify-modal';
|
||||
@import 'components/graph-ql-editor';
|
||||
@import 'components/graph-ql-explorer';
|
||||
@import 'components/dropdown';
|
||||
@import 'components/editable';
|
||||
@import 'components/environment-modal';
|
||||
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
// @flow
|
||||
|
||||
declare module 'graphql' {
|
||||
declare type Tok = {
|
||||
column: number,
|
||||
end: number,
|
||||
kind: string,
|
||||
line: number,
|
||||
next: Tok,
|
||||
prev: Tok,
|
||||
start: number,
|
||||
};
|
||||
|
||||
declare type Loc = {
|
||||
end: number,
|
||||
endToken: Tok,
|
||||
source: Source,
|
||||
start: number,
|
||||
startToken: Tok,
|
||||
};
|
||||
|
||||
declare type ParseOptions = mixed;
|
||||
declare type Source = mixed;
|
||||
declare type Document = {
|
||||
definitions: Array<{
|
||||
kind: string,
|
||||
loc: Loc,
|
||||
variableDefinitions: Array<{
|
||||
variable: {
|
||||
name: {
|
||||
kind: string,
|
||||
value: string,
|
||||
},
|
||||
},
|
||||
type: string,
|
||||
}>,
|
||||
name: {
|
||||
kind: string,
|
||||
value: string,
|
||||
},
|
||||
}>,
|
||||
};
|
||||
|
||||
declare function parse(source: Source | string, options?: ParseOptions): Document;
|
||||
|
||||
declare module.exports: {
|
||||
parse: typeof parse,
|
||||
print: *,
|
||||
typeFromAST: *,
|
||||
};
|
||||
}
|
||||
|
||||
declare module 'graphql/utilities/introspectionQuery' {
|
||||
declare module.exports: {
|
||||
introspectionQuery: string,
|
||||
};
|
||||
}
|
||||
|
||||
declare module 'graphql/utilities/buildClientSchema' {
|
||||
declare module.exports: {
|
||||
buildClientSchema: Function,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user