diff --git a/README.md b/README.md index 78ea4505d..a338f9311 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ $ brew cask install graphql-playground ## Features -* ✨ Context-aware autocompletion & error highlighting -* 📚 Interactive, multi-column docs (keyboard support) -* ⚡️ Supports real-time GraphQL Subscriptions -* ⚙ GraphQL Config support with multiple Projects & Endpoints -* 🚥 Apollo Tracing support +- ✨ Context-aware autocompletion & error highlighting +- 📚 Interactive, multi-column docs (keyboard support) +- ⚡️ Supports real-time GraphQL Subscriptions +- ⚙ GraphQL Config support with multiple Projects & Endpoints +- 🚥 Apollo Tracing support ## FAQ @@ -27,12 +27,12 @@ $ brew cask install graphql-playground GraphQL Playground uses components of GraphiQL under the hood but is meant as a more powerful GraphQL IDE enabling better (local) development workflows. Compared to GraphiQL, the GraphQL Playground ships with the following additional features: -* Interactive, multi-column schema documentation -* Automatic schema reloading -* Support for GraphQL Subscriptions -* Query history -* Configuration of HTTP headers -* Tabs +- Interactive, multi-column schema documentation +- Automatic schema reloading +- Support for GraphQL Subscriptions +- Query history +- Configuration of HTTP headers +- Tabs See the following question for more additonal features. @@ -40,8 +40,8 @@ See the following question for more additonal features. The desktop app is the same as the web version but includes these additional features: -* Partial support for [graphql-config](https://github.com/prismagraphql/graphql-config) enabling features like multi-environment setups (no support for sending HTTP headers). -* Double click on `*.graphql` files. +- Partial support for [graphql-config](https://github.com/prismagraphql/graphql-config) enabling features like multi-environment setups (no support for sending HTTP headers). +- Double click on `*.graphql` files. ### How does GraphQL Bin work? @@ -77,43 +77,44 @@ These are the settings currently available: The React component `` and all middlewares expose the following options: -* `props` (Middlewares & React Component) - * `endpoint` [`string`](optional) - the GraphQL endpoint url. - * `subscriptionEndpoint` [`string`](optional) - the GraphQL subscriptions endpoint url. - * `workspaceName` [`string`](optional) - in case you provide a GraphQL Config, you can name your workspace here - * `config` [`string`](optional) - the JSON of a GraphQL Config. See an example [here](https://github.com/prismagraphql/graphql-playground/blob/master/packages/graphql-playground-react/src/localDevIndex.tsx#L47) - * `settings` [`ISettings`](optional) - Editor settings in json format as [described here](https://github.com/prismagraphql/graphql-playground#settings) +- `props` (Middlewares & React Component) + - `endpoint` [`string`](optional) - the GraphQL endpoint url. + - `subscriptionEndpoint` [`string`](optional) - the GraphQL subscriptions endpoint url. + - `workspaceName` [`string`](optional) - in case you provide a GraphQL Config, you can name your workspace here + - `config` [`string`](optional) - the JSON of a GraphQL Config. See an example [here](https://github.com/prismagraphql/graphql-playground/blob/master/packages/graphql-playground-react/src/localDevIndex.tsx#L47) + - `settings` [`ISettings`](optional) - Editor settings in json format as [described here](https://github.com/prismagraphql/graphql-playground#settings) ```ts interface ISettings { - 'general.betaUpdates': boolean - 'editor.theme': Theme - 'editor.reuseHeaders': boolean - 'tracing.hideTracingResponse': boolean - 'editor.fontSize': number - 'editor.fontFamily': string - 'request.credentials': string + 'general.betaUpdates': boolean + 'editor.theme': Theme + 'editor.reuseHeaders': boolean + 'tracing.hideTracingResponse': boolean + 'editor.fontSize': number + 'editor.fontFamily': string + 'request.credentials': string + 'schema.disableComments': boolean } ``` -* `schema` [`IntrospectionResult`](optional) - The result of an introspection query (an object of this form: `{__schema: {...}}`) The playground automatically fetches the schema from the endpoint. This is only needed when you want to override the schema. -* `tabs` [`Tab[]`](optional) - An array of tabs to inject. **Note: When using this feature, tabs will be resetted each time the page is reloaded** +- `schema` [`IntrospectionResult`](optional) - The result of an introspection query (an object of this form: `{__schema: {...}}`) The playground automatically fetches the schema from the endpoint. This is only needed when you want to override the schema. +- `tabs` [`Tab[]`](optional) - An array of tabs to inject. **Note: When using this feature, tabs will be resetted each time the page is reloaded** ```ts interface Tab { - endpoint: string - query: string - name?: string - variables?: string - responses?: string[] - headers?: { [key: string]: string } + endpoint: string + query: string + name?: string + variables?: string + responses?: string[] + headers?: { [key: string]: string } } ``` In addition to this, the React app provides some more properties: -* `props` (React Component) -* `createApolloLink` [`(session: Session) => ApolloLink`] - this is the equivalent to the `fetcher` of GraphiQL. For each query that is being executed, this function will be called +- `props` (React Component) +- `createApolloLink` [`(session: Session) => ApolloLink`] - this is the equivalent to the `fetcher` of GraphiQL. For each query that is being executed, this function will be called `createApolloLink` is only available in the React Component and not the middlewares, because the content must be serializable as it is being printed into a HTML template. @@ -145,7 +146,10 @@ The GraphQL Playground requires **React 16**. Including Fonts (`1.`) ```html - + ``` Including stylesheet and the component (`2., 3.`) @@ -157,10 +161,10 @@ import { Provider } from 'react-redux' import { Playground, store } from 'graphql-playground-react' ReactDOM.render( - - - , - document.body, + + + , + document.body, ) ``` @@ -180,13 +184,13 @@ yarn add graphql-playground-middleware-lambda We have a full example for each of the frameworks below: -* **Express:** See [packages/graphql-playground-middleware-express/examples/basic](https://github.com/prismagraphql/graphql-playground/tree/master/packages/graphql-playground-middleware-express/examples/basic) +- **Express:** See [packages/graphql-playground-middleware-express/examples/basic](https://github.com/prismagraphql/graphql-playground/tree/master/packages/graphql-playground-middleware-express/examples/basic) -* **Hapi:** See [packages/graphql-playground-middleware-hapi](https://github.com/prismagraphql/graphql-playground/tree/master/packages/graphql-playground-middleware-hapi) +- **Hapi:** See [packages/graphql-playground-middleware-hapi](https://github.com/prismagraphql/graphql-playground/tree/master/packages/graphql-playground-middleware-hapi) -* **Koa:** See [packages/graphql-playground-middleware-koa](https://github.com/prismagraphql/graphql-playground/tree/master/packages/graphql-playground-middleware-koa) +- **Koa:** See [packages/graphql-playground-middleware-koa](https://github.com/prismagraphql/graphql-playground/tree/master/packages/graphql-playground-middleware-koa) -* **Lambda (as serverless handler):** See [serverless-graphql-apollo](https://github.com/serverless/serverless-graphql-apollo) or a quick example below. +- **Lambda (as serverless handler):** See [serverless-graphql-apollo](https://github.com/serverless/serverless-graphql-apollo) or a quick example below. ### As serverless handler @@ -206,18 +210,18 @@ import lambdaPlayground from 'graphql-playground-middleware-lambda' // const lambdaPlayground = require('graphql-playground-middleware-lambda').default exports.graphqlHandler = function graphqlHandler(event, context, callback) { - function callbackFilter(error, output) { - // eslint-disable-next-line no-param-reassign - output.headers['Access-Control-Allow-Origin'] = '*' - callback(error, output) - } - - const handler = graphqlLambda({ schema: myGraphQLSchema }) - return handler(event, context, callbackFilter) + function callbackFilter(error, output) { + // eslint-disable-next-line no-param-reassign + output.headers['Access-Control-Allow-Origin'] = '*' + callback(error, output) + } + + const handler = graphqlLambda({ schema: myGraphQLSchema }) + return handler(event, context, callbackFilter) } exports.playgroundHandler = lambdaPlayground({ - endpoint: '/dev/graphql', + endpoint: '/dev/graphql', }) ``` @@ -228,17 +232,17 @@ functions: graphql: handler: handler.graphqlHandler events: - - http: - path: graphql - method: post - cors: true + - http: + path: graphql + method: post + cors: true playground: handler: handler.playgroundHandler events: - - http: - path: playground - method: get - cors: true + - http: + path: playground + method: get + cors: true ``` ## Development @@ -253,32 +257,33 @@ Open [localhost:3000/localDev.html?endpoint=https://api.graph.cool/simple/v1/swapi](http://localhost:3000/localDev.html?endpoint=https://api.graph.cool/simple/v1/swapi) for local development! ## Custom Theme + From `graphql-playground-react@1.7.0` on you can provide a `codeTheme` property to the React Component to customize your color theme. These are the available options: -```ts +```ts export interface EditorColours { - property: string - comment: string - punctuation: string - keyword: string - def: string - qualifier: string - attribute: string - number: string - string: string - builtin: string - string2: string - variable: string - meta: string - atom: string - ws: string - selection: string - cursorColor: string - editorBackground: string - resultBackground: string - leftDrawerBackground: string - rightDrawerBackground: string + property: string + comment: string + punctuation: string + keyword: string + def: string + qualifier: string + attribute: string + number: string + string: string + builtin: string + string2: string + variable: string + meta: string + atom: string + ws: string + selection: string + cursorColor: string + editorBackground: string + resultBackground: string + leftDrawerBackground: string + rightDrawerBackground: string } ``` @@ -290,13 +295,13 @@ This is repository is a "mono repo" and contains multiple packages using [Yarn w In the folder `packages` you'll find the following packages: -* `graphql-playground-electron`: Cross-platform electron app which uses `graphql-playground-react` -* `graphql-playground-html`: Simple HTML page rendering a version of `graphql-playground-react` hosted on JSDeliver -* `graphql-playground-middleware-express`: Express middleware using `graphql-playground-html` -* `graphql-playground-middleware-hapi`: Hapi middleware using `graphql-playground-html` -* `graphql-playground-middleware-koa`: Koa middleware using `graphql-playground-html` -* `graphql-playground-middleware-lambda`: AWS Lambda middleware using `graphql-playground-html` -* `graphql-playground-react`: Core of GraphQL Playground built with ReactJS +- `graphql-playground-electron`: Cross-platform electron app which uses `graphql-playground-react` +- `graphql-playground-html`: Simple HTML page rendering a version of `graphql-playground-react` hosted on JSDeliver +- `graphql-playground-middleware-express`: Express middleware using `graphql-playground-html` +- `graphql-playground-middleware-hapi`: Hapi middleware using `graphql-playground-html` +- `graphql-playground-middleware-koa`: Koa middleware using `graphql-playground-html` +- `graphql-playground-middleware-lambda`: AWS Lambda middleware using `graphql-playground-html` +- `graphql-playground-react`: Core of GraphQL Playground built with ReactJS diff --git a/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsStyles.tsx b/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsStyles.tsx index 7a5323143..f4d05071c 100644 --- a/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsStyles.tsx +++ b/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsStyles.tsx @@ -6,7 +6,7 @@ const Title = styled.div` cursor: default; font-size: 14px; font-weight: 600; - text-transform: uppercase; + text-transform: uppercase !important; letter-spacing: 1px; padding: 16px; user-select: none; diff --git a/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/EnumTypeSchema.tsx b/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/EnumTypeSchema.tsx index 25ca1343a..795219b89 100644 --- a/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/EnumTypeSchema.tsx +++ b/packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/EnumTypeSchema.tsx @@ -4,10 +4,11 @@ import { DocType } from './DocType' export interface EnumTypeSchemaProps { type: any + sdlType?: boolean } -const EnumTypeSchema = ({ type }: EnumTypeSchemaProps) => { - const values = type.getValues() +const EnumTypeSchema = ({ type, sdlType }: EnumTypeSchemaProps) => { + const values = sdlType ? type._values : type.getValues() const deprecatedValues = values.filter((value: any) => value.isDeprecated) return ( diff --git a/packages/graphql-playground-react/src/components/Playground/DocExplorer/ErrorContainer.tsx b/packages/graphql-playground-react/src/components/Playground/DocExplorer/ErrorContainer.tsx new file mode 100644 index 000000000..0c536bdac --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/DocExplorer/ErrorContainer.tsx @@ -0,0 +1,13 @@ +import { styled } from '../../../styled' +export const ErrorContainer = styled.div` + font-weight: bold; + left: 0; + letter-spacing: 1px; + opacity: 0.5; + position: absolute; + right: 0; + text-align: center; + text-transform: uppercase; + top: 50%; + transform: translate(0, -50%); +` diff --git a/packages/graphql-playground-react/src/components/Playground/DocExplorer/GraphDocs.tsx b/packages/graphql-playground-react/src/components/Playground/DocExplorer/GraphDocs.tsx index 382a6ae20..99e0c0eff 100644 --- a/packages/graphql-playground-react/src/components/Playground/DocExplorer/GraphDocs.tsx +++ b/packages/graphql-playground-react/src/components/Playground/DocExplorer/GraphDocs.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import * as keycode from 'keycode' -import { getLeft } from 'graphiql/dist/utility/elementPosition' import FieldDoc from './FieldDoc' import ColumnDoc from './ColumnDoc' import { @@ -21,10 +20,11 @@ import { serializeRoot, getElement, } from '../util/stack' -import { GraphQLSchema } from 'graphql' import { getSessionDocs } from '../../../state/docs/selectors' import { getSelectedSessionIdFromRoot } from '../../../state/sessions/selectors' import { createStructuredSelector } from 'reselect' +import { SideTabContentProps } from '../ExplorerTabs/SideTabs' +import { ErrorContainer } from './ErrorContainer' import { styled } from '../../../styled' interface StateFromProps { @@ -44,24 +44,17 @@ interface DispatchFromProps { changeKeyMove: (sessionId: string, move: boolean) => any } -export interface Props { - schema: GraphQLSchema - sessionId: string -} - export interface State { searchValue: string widthMap: any } class GraphDocs extends React.Component< - Props & StateFromProps & DispatchFromProps, + SideTabContentProps & StateFromProps & DispatchFromProps, State > { ref - private refDocExplorer: any - private clientX: number = 0 - private clientY: number = 0 + // private refDocExplorer: any; constructor(props) { super(props) @@ -72,7 +65,7 @@ class GraphDocs extends React.Component< ;(window as any).d = this } - componentWillReceiveProps(nextProps: Props & StateFromProps) { + componentWillReceiveProps(nextProps: SideTabContentProps & StateFromProps) { // If user use default column size % columnWidth // Make the column follow the clicks if ( @@ -86,13 +79,7 @@ class GraphDocs extends React.Component< } setWidth(props: any = this.props) { - requestAnimationFrame(() => { - const width = this.getWidth(props) - this.props.changeWidthDocs( - props.sessionId, - Math.min(width, window.innerWidth - 86), - ) - }) + this.props.setWidth(props) } getWidth(props: any = this.props) { @@ -109,10 +96,8 @@ class GraphDocs extends React.Component< } render() { - const { docsOpen, docsWidth, navStack } = this.props.docs + const { navStack } = this.props.docs const { schema } = this.props - const docsStyle = { width: docsOpen ? docsWidth : 0 } - let emptySchema if (schema === undefined) { // Schema is undefined when it is being loaded via introspection. @@ -124,44 +109,36 @@ class GraphDocs extends React.Component< } return ( - - Schema - - - - - {emptySchema && {emptySchema}} - {!emptySchema && - schema && ( - - )} - {navStack.map((stack, index) => ( - - - - ))} - - - + + {emptySchema && {emptySchema}} + {!emptySchema && + schema && ( + + )} + {navStack.map((stack, index) => ( + + + + ))} + ) } @@ -170,26 +147,13 @@ class GraphDocs extends React.Component< } public showDocFromType = type => { - this.props.setDocsVisible(this.props.sessionId, true) this.props.addStack(this.props.sessionId, type, 0, 0) } - private setDocExplorerRef = ref => { - this.refDocExplorer = ref - } - private handleSearch = (value: string) => { this.setState({ searchValue: value }) } - private handleToggleDocs = () => { - if (!this.props.docs.docsOpen && this.refDocExplorer) { - this.refDocExplorer.focus() - } - this.props.toggleDocs(this.props.sessionId) - this.setWidth() - } - private handleKeyDown = e => { // we don't want to interfere with inputs if ( @@ -279,58 +243,6 @@ class GraphDocs extends React.Component< break } } - - private handleDocsResizeStart = downEvent => { - downEvent.preventDefault() - - const hadWidth = this.props.docs.docsWidth - const offset = downEvent.clientX - getLeft(downEvent.target) - - let onMouseMove: any = moveEvent => { - if (moveEvent.buttons === 0) { - return onMouseUp() - } - - const app = this.ref - const cursorPos = moveEvent.clientX - getLeft(app) - offset - const newSize = app.clientWidth - cursorPos - const maxSize = window.innerWidth - 50 - const docsSize = maxSize < newSize ? maxSize : newSize - - if (docsSize < 100) { - this.props.setDocsVisible(this.props.sessionId, false) - } else { - this.props.setDocsVisible(this.props.sessionId, true) - this.props.changeWidthDocs(this.props.sessionId, docsSize) - } - } - - let onMouseUp: any = () => { - if (!this.props.docs.docsOpen) { - this.props.changeWidthDocs(this.props.sessionId, hadWidth) - } - - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - onMouseMove = null - onMouseUp = null - } - - document.addEventListener('mousemove', onMouseMove) - document.addEventListener('mouseup', onMouseUp) - } - - private handleMouseMove = e => { - this.clientX = e.clientX - this.clientY = e.clientY - if ( - this.props.docs.keyMove && - this.clientX !== e.clientX && - this.clientY !== e.clientY - ) { - this.props.changeKeyMove(this.props.sessionId, false) - } - } } const mapDispatchToProps = dispatch => @@ -350,71 +262,13 @@ const mapStateToProps = createStructuredSelector({ sessionId: getSelectedSessionIdFromRoot, }) -export default connect( +export default connect( mapStateToProps, mapDispatchToProps, null, { withRef: true }, )(GraphDocs) -interface DocsProps { - open: boolean -} - -const Docs = styled('div')` - background: white; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); - position: absolute; - right: -2px; - z-index: ${p => (p.open ? 2000 : 3)}; - height: 100%; - font-family: 'Open Sans', sans-serif; - -webkit-font-smoothing: antialiased; - - .doc-type-description p { - padding: 16px; - font-size: 14px; - } - - .field-name { - color: #1f61a0; - } - .type-name { - color: rgb(245, 160, 0); - } - .arg-name { - color: #1f61a9; - } - - code { - font-family: 'Source Code Pro', monospace; - border-radius: 2px; - padding: 1px 2px; - background: rgba(0, 0, 0, 0.06); - } -` - -const DocsExplorer = styled.div` - background: white; - display: flex; - position: relative; - height: 100%; - letter-spacing: 0.3px; - outline: none; - box-shadow: -1px 1px 6px 0 rgba(0, 0, 0, 0.3); - - &::before { - top: 0; - bottom: 0; - background: ${props => props.theme.colours.green}; - position: absolute; - z-index: 3; - left: 0px; - content: ''; - width: 6px; - } -` - const DocsExplorerContainer = styled.div` display: flex; position: relative; @@ -422,64 +276,5 @@ const DocsExplorerContainer = styled.div` width: 100%; overflow-x: auto; overflow-y: hidden; -` - -const DocsResizer = styled.div` - cursor: col-resize; - height: 100%; - left: -5px; - position: absolute; - top: 0; - bottom: 0; - width: 10px; - z-index: 10; -` - -const ErrorContainer = styled.div` - font-weight: bold; - left: 0; - letter-spacing: 1px; - opacity: 0.5; - position: absolute; - right: 0; - text-align: center; - text-transform: uppercase; - top: 50%; - transform: translate(0, -50%); -` - -const DocsButton = styled.div` - position: absolute; - z-index: 2; - left: -50px; - top: 129px; - padding: 6px 10px; - transform: rotate(-90deg); - border-top-left-radius: 2px; - border-top-right-radius: 2px; - color: ${p => p.theme.colours.white}; - background: ${p => p.theme.colours.green}; - box-shadow: -1px 1px 6px 0 rgba(0, 0, 0, 0.3); - text-transform: uppercase; - font-weight: 600; - font-size: 12px; - line-height: 17px; - letter-spacing: 0.45px; - cursor: pointer; -` - -const DocsGradient = styled.div` - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 20px; - z-index: 1; - pointer-events: none; - content: ''; - background: linear-gradient( - to right, - rgba(255, 255, 255, 1) 30%, - rgba(255, 255, 255, 0) - ); + outline: none !important; ` diff --git a/packages/graphql-playground-react/src/components/Playground/DocExplorer/SearchBox.tsx b/packages/graphql-playground-react/src/components/Playground/DocExplorer/SearchBox.tsx index f66763e38..e895a2dfb 100644 --- a/packages/graphql-playground-react/src/components/Playground/DocExplorer/SearchBox.tsx +++ b/packages/graphql-playground-react/src/components/Playground/DocExplorer/SearchBox.tsx @@ -43,7 +43,7 @@ export default class SearchBox extends React.Component { onChange={this.handleChange} type="text" value={this.state.value} - placeholder={this.props.placeholder || 'Search the schema ...'} + placeholder={this.props.placeholder || 'Search the docs ...'} /> ) diff --git a/packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTab.tsx b/packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTab.tsx new file mode 100644 index 000000000..83214db0b --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTab.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { styled } from '../../../styled' + +export interface Props { + label: string + activeColor: string + children: any + active?: boolean + tabWidth?: string + onClick?: () => any +} + +export default class SideTab extends React.PureComponent { + render() { + const { label, activeColor, active, onClick, tabWidth } = this.props + return ( + + {label} + + ) + } +} + +export interface TabProps { + active: boolean + activeColor: string +} + +const Tab = styled('div')` + z-index: ${p => (p.active ? 10 : 2)}; + padding: 8px 8px 8px 8px; + border-radius: 2px 2px 0px 0px; + color: ${p => + p.theme.mode === 'dark' + ? p.theme.colours.white + : p.theme.colours[p.active ? 'white' : 'darkBlue']}; + background: ${p => + p.active && p.activeColor + ? p.theme.colours[p.activeColor] + : p.theme.mode === 'dark' + ? '#3D5866' + : '#DBDEE0'}; + box-shadow: -1px 1px 6px 0 rgba(0, 0, 0, 0.3); + text-transform: uppercase; + text-align: center; + font-weight: 600; + font-size: 12px; + line-height: 12px; + letter-spacing: 0.45px; + cursor: pointer; + transform: rotate(-90deg); + transform-origin: bottom left; + margin-top: 65px; + width: ${p => p.tabWidth || '100%'}; +` diff --git a/packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTabs.tsx b/packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTabs.tsx new file mode 100644 index 000000000..80bd94e9a --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTabs.tsx @@ -0,0 +1,389 @@ +import * as React from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import * as keycode from 'keycode' +import { getLeft } from 'graphiql/dist/utility/elementPosition' +import { + addStack, + toggleDocs, + changeWidthDocs, + changeKeyMove, + setDocsVisible, +} from '../../../state/docs/actions' +import { GraphQLSchema } from 'graphql' +import { getSessionDocs } from '../../../state/docs/selectors' +import { getSelectedSessionIdFromRoot } from '../../../state/sessions/selectors' +import { createStructuredSelector } from 'reselect' +import { styled } from '../../../styled' +import SideTab from './SideTab' + +interface StateFromProps { + docs: { + navStack: any[] + docsOpen: boolean + docsWidth: number + keyMove: boolean + activeTabIdx: number + } +} + +interface DispatchFromProps { + addStack: (sessionId: string, field: any, x: number, y: number) => any + toggleDocs: (sessionId: string, activeTabIdx?: number | null) => any + setDocsVisible: (sessionId: string, open: boolean, idx?: number | null) => any + changeWidthDocs: (sessionId: string, width: number) => any + changeKeyMove: (sessionId: string, move: boolean) => any +} + +export interface Props { + schema: GraphQLSchema + sessionId: string + children: Array> +} + +export interface SideTabContentProps { + schema: GraphQLSchema + sessionId: string + setWidth: (props: any) => any +} + +export interface State { + searchValue: string + widthMap: any +} + +class SideTabs extends React.Component< + Props & StateFromProps & DispatchFromProps, + State +> { + ref + public activeContentComponent: any // later React.Component<...> + private refContentContainer: any + private clientX: number = 0 + private clientY: number = 0 + constructor(props) { + super(props) + ;(window as any).d = this + } + + setWidth = (props: any = this.props) => { + if (!this.activeContentComponent) { + return + } + if (!this.props.docs.docsOpen) { + return + } + requestAnimationFrame(() => { + const width = this.activeContentComponent.getWidth(props) + this.props.changeWidthDocs( + props.sessionId, + Math.min(width, window.innerWidth - 86), + ) + }) + } + setActiveContentRef = ref => { + if (ref) { + this.activeContentComponent = ref.getWrappedInstance() + } + } + + componentDidUpdate(prevProps) { + if (!prevProps.docs.activeTabIdx && this.props.docs.activeTabIdx) { + this.props.setDocsVisible( + this.props.sessionId, + true, + this.props.docs.activeTabIdx, + ) + } + if (prevProps.activeTabIdx && !this.props.docs.activeTabIdx) { + this.props.setDocsVisible(this.props.sessionId, false) + } + return this.setWidth() + } + + componentDidMount() { + if (!this.props.docs.activeTabIdx) { + this.props.setDocsVisible(this.props.sessionId, false) + } + return this.setWidth() + } + + render() { + const { docsOpen, docsWidth, activeTabIdx } = this.props.docs + const docsStyle = { width: docsOpen ? docsWidth : 0 } + const activeTab = + docsOpen && + (React.Children.toArray(this.props.children)[ + activeTabIdx + ] as React.ReactElement) + return ( + + + {React.Children.toArray(this.props.children).map( + (child: React.ReactElement, index) => { + return React.cloneElement(child, { + ...child.props, + key: index, + onClick: this.handleTabClick(index), + active: index === activeTabIdx, + }) + }, + )} + + + + + {activeTab && + React.cloneElement(activeTab.props.children, { + ...activeTab.props, + ref: this.setActiveContentRef, + setWidth: this.setWidth, + })} + + + ) + } + + setRef = ref => { + this.ref = ref + } + + public showDocFromType = type => { + this.props.setDocsVisible(this.props.sessionId, true, 0) + this.activeContentComponent.showDocFromType(type) + } + + private setContentContainerRef = ref => { + this.refContentContainer = ref + } + + private handleTabClick = idx => () => { + if (!this.props.docs.docsOpen && this.refContentContainer) { + this.refContentContainer.focus() + } + if (this.props.docs.activeTabIdx === idx) { + this.props.setDocsVisible(this.props.sessionId, false) + return this.setWidth() + } + if (this.props.docs.activeTabIdx !== idx) { + this.props.setDocsVisible( + this.props.sessionId, + false, + this.props.docs.activeTabIdx, + ) + this.props.setDocsVisible(this.props.sessionId, true, idx) + return this.setWidth() + } else { + this.props.setDocsVisible(this.props.sessionId, true, idx) + return this.setWidth() + } + } + + private handleKeyDown = e => { + // we don't want to interfere with inputs + if ( + e.target instanceof HTMLInputElement || + e.metaKey || + e.shiftKey || + e.altKey || + e.ctrlKey + ) { + return + } + const keyPressed = keycode(e) + switch (keyPressed) { + case 'esc': + this.props.changeKeyMove(this.props.sessionId, true) + e.preventDefault() + this.props.setDocsVisible(this.props.sessionId, false) + break + } + } + + private handleDocsResizeStart = downEvent => { + downEvent.preventDefault() + + const hadWidth = this.props.docs.docsWidth + const offset = downEvent.clientX - getLeft(downEvent.target) + + let onMouseMove: any = moveEvent => { + if (moveEvent.buttons === 0) { + return onMouseUp() + } + + const app = this.ref + const cursorPos = moveEvent.clientX - getLeft(app) - offset + const newSize = app.clientWidth - cursorPos + const maxSize = window.innerWidth - 50 + const docsSize = maxSize < newSize ? maxSize : newSize + + if (docsSize < 100) { + this.props.setDocsVisible( + this.props.sessionId, + false, + this.props.docs.activeTabIdx, + ) + } else { + this.props.setDocsVisible( + this.props.sessionId, + true, + this.props.docs.activeTabIdx, + ) + this.props.changeWidthDocs(this.props.sessionId, docsSize) + } + } + + let onMouseUp: any = () => { + if (!this.props.docs.docsOpen) { + this.props.changeWidthDocs(this.props.sessionId, hadWidth) + } + + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + onMouseMove = null + onMouseUp = null + } + + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + } + private handleMouseMove = e => { + this.clientX = e.clientX + this.clientY = e.clientY + if ( + this.props.docs.keyMove && + this.clientX !== e.clientX && + this.clientY !== e.clientY + ) { + this.props.changeKeyMove(this.props.sessionId, false) + } + } +} + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + addStack, + toggleDocs, + changeWidthDocs, + changeKeyMove, + setDocsVisible, + }, + dispatch, + ) + +const mapStateToProps = createStructuredSelector({ + docs: getSessionDocs, + sessionId: getSelectedSessionIdFromRoot, +}) + +const ConnectedGraphDocs = connect( + mapStateToProps, + mapDispatchToProps, + null, + { withRef: true }, +)(SideTabs) + +ConnectedGraphDocs.Tab = SideTab + +export default ConnectedGraphDocs + +interface TabsProps { + open: boolean +} + +const Tabs = styled('div')` + background: white; + outline: none; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); + position: absolute; + right: 0px; + z-index: ${p => (p.open ? 2000 : 3)}; + height: 100%; + font-family: 'Open Sans', sans-serif; + -webkit-font-smoothing: antialiased; + .doc-type-description p { + padding: 16px; + font-size: 14px; + } + .field-name { + color: #1f61a0; + } + .type-name { + color: rgb(245, 160, 0); + } + .arg-name { + color: #1f61a9; + } + code { + font-family: 'Source Code Pro', monospace; + border-radius: 2px; + padding: 1px 2px; + background: rgba(0, 0, 0, 0.06); + } +` + +const TabContentContainer = styled.div` + background: white; + display: flex; + position: relative; + height: 100%; + letter-spacing: 0.3px; + box-shadow: -1px 1px 6px 0 rgba(0, 0, 0, 0.3); + outline: none; + user-select: none; + &::before { + top: 0; + bottom: 0; + background: ${props => props.theme.colours[props.color] || '#3D5866'}; + position: absolute; + z-index: 3; + left: 0px; + content: ''; + width: 6px; + } +` + +const TabContentResizer = styled.div` + cursor: col-resize; + outline: none !important; + height: 100%; + left: -5px; + position: absolute; + top: 0; + bottom: 0; + width: 10px; + z-index: 10; +` + +const TabsContainer = styled.div` + position: absolute; + outline: none !important; + z-index: 2; + height: 0; + top: 129px; +` + +const TabsGradient = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 20px; + z-index: 1; + pointer-events: none; + content: ''; + background: ${p => + p.index === 0 + ? `linear-gradient( + to right, + rgba(255, 255, 255, 1) 30%, + rgba(255, 255, 255, 0))` + : `transparent`}; +` diff --git a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx index 1e5e541a6..e69514b7b 100644 --- a/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx +++ b/packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx @@ -1,24 +1,34 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' import { isNamedType, GraphQLSchema } from 'graphql' +import { List } from 'immutable' + +// Query & Response Components import ExecuteButton from './ExecuteButton' import QueryEditor from './QueryEditor' import EditorWrapper, { Container } from './EditorWrapper' import CodeMirrorSizer from 'graphiql/dist/utility/CodeMirrorSizer' -import { fillLeafs } from 'graphiql/dist/utility/fillLeafs' -import { getLeft, getTop } from 'graphiql/dist/utility/elementPosition' -import { connect } from 'react-redux' - -import Spinner from '../Spinner' -import Results from './Results' -import ResponseTracing from './ResponseTracing' -import GraphDocs from './DocExplorer/GraphDocs' -import { styled } from '../../styled/index' import TopBar from './TopBar/TopBar' import { VariableEditorComponent, HeadersEditorComponent, } from './VariableEditor' +import Spinner from '../Spinner' +import Results from './Results' +import ResponseTracing from './ResponseTracing' +import { fillLeafs } from 'graphiql/dist/utility/fillLeafs' +import { getLeft, getTop } from 'graphiql/dist/utility/elementPosition' + +// Explorer Components +import SideTab from './ExplorerTabs/SideTab' +import SideTabs from './ExplorerTabs/SideTabs' +import SDLView from './SchemaExplorer/SDLView' +import GraphDocs from './DocExplorer/GraphDocs' + +import { styled } from '../../styled/index' + +// Redux Dependencies +import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getQueryRunning, @@ -55,7 +65,6 @@ import { toggleVariables, fetchSchema, } from '../../state/sessions/actions' -import { List } from 'immutable' import { ResponseRecord } from '../../state/sessions/reducers' /** @@ -123,7 +132,8 @@ class GraphQLEditor extends React.PureComponent { public resultComponent public editorBarComponent public docExplorerComponent: any // later React.Component<...> - + public graphExplorerComponent: any + public schemaExplorerComponent: any private queryResizer: any private responseResizer: any private queryVariablesRef @@ -250,7 +260,21 @@ class GraphQLEditor extends React.PureComponent { - + + + + + + + + ) } @@ -292,6 +316,16 @@ class GraphQLEditor extends React.PureComponent { this.docExplorerComponent = ref.getWrappedInstance() } } + setGraphExplorerRef = ref => { + if (ref) { + this.graphExplorerComponent = ref.getWrappedInstance() + } + } + setSchemaExplorerRef = ref => { + if (ref) { + this.schemaExplorerComponent = ref.getWrappedInstance() + } + } handleClickReference = reference => { this.docExplorerComponent.showDocFromType(reference.field || reference) diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx new file mode 100644 index 000000000..39f9ca763 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx @@ -0,0 +1,162 @@ +import * as React from 'react' +import { GraphQLSchema } from 'graphql' +import EditorWrapper from '../EditorWrapper' +import { styled } from '../../../styled' +import { getSDL } from '../util/createSDL' +import { ISettings } from '../../../types' + +export interface Props { + schema?: GraphQLSchema | null + getRef?: (ref: SDLEditor) => void + width?: number + sessionId?: string + settings: ISettings +} + +class SDLEditor extends React.PureComponent { + cachedValue: string + private editor: any + private node: any + + constructor(props) { + super(props) + this.state = { + overflowY: false, + } + // Keep a cached version of the value, this cache will be updated when the + // editor is updated, which can later be used to protect the editor from + // unnecessary updates during the update lifecycle. + this.cachedValue = props.value || '' + if (this.props.getRef) { + this.props.getRef(this) + } + } + + componentDidMount() { + // Lazily require to ensure requiring GraphiQL outside of a Browser context + // does not produce an error. + const CodeMirror = require('codemirror') + require('codemirror/addon/fold/brace-fold') + require('codemirror/addon/comment/comment') + require('codemirror-graphql/mode') + + const gutters: any[] = [] + gutters.push('CodeMirror-linenumbers') + + this.editor = CodeMirror(this.node, { + autofocus: false, + value: + getSDL( + this.props.schema, + this.props.settings['schema.disableComments'], + ) || '', + lineNumbers: false, + showCursorWhenSelecting: false, + tabSize: 1, + mode: 'graphql', + theme: 'graphiql', + // lineWrapping: true, + keyMap: 'sublime', + readOnly: true, + gutters, + }) + ;(global as any).editor = this.editor + this.editor.on('scroll', this.handleScroll) + this.editor.refresh() + } + + componentDidUpdate(prevProps: Props) { + const CodeMirror = require('codemirror') + if (this.props.schema !== prevProps.schema) { + this.cachedValue = + getSDL( + this.props.schema, + this.props.settings['schema.disableComments'], + ) || '' + this.editor.setValue( + getSDL( + this.props.schema, + this.props.settings['schema.disableComments'], + ), + ) + CodeMirror.signal(this.editor, 'change', this.editor) + } + if (this.props.width !== prevProps.width) { + this.editor.refresh() + } + if ( + this.props.settings['schema.disableComments'] !== + prevProps.settings['schema.disableComments'] + ) { + this.editor.refresh() + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.sessionId !== nextProps.sessionId) { + this.editor.scrollTo(0, 0) + } + } + + componentWillUnmount() { + this.editor.off('scroll') + this.editor = null + } + + handleScroll = e => { + if (e.doc.scrollTop > 0) { + return this.setState({ + overflowY: true, + }) + } + return this.setState({ + overflowY: false, + }) + } + + render() { + const { overflowY } = this.state + return ( + + {overflowY && } + + + ) + } + + setRef = ref => { + this.node = ref + } + + getCodeMirror() { + return this.editor + } + getClientHeight() { + return this.node && this.node.clientHeight + } +} + +export default SDLEditor + +const Editor = styled.div` + flex: 1; + height: auto; + overflow-x: hidden; + overflow-y: scroll; + .CodeMirror { + background: ${p => + p.theme.mode === 'dark' + ? p.theme.editorColours.editorBackground + : 'white'}; + padding-left: 20px; + } +` +const OverflowShadow = styled.div` + position: fixed: + top: 0; + left: 0; + right: 0; + height: 1px; + box-shadow: 0px 1px 3px rgba(17, 17, 17, 0.1); + z-index: 1000; +` diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx new file mode 100644 index 000000000..d9c93d5f1 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx @@ -0,0 +1,160 @@ +import * as React from 'react' +import { styled } from '../../../styled' +import { Button } from '../TopBar/TopBar' +import { GraphQLSchema } from 'graphql' +import { downloadSchema } from '../util/createSDL' + +interface SDLHeaderProps { + schema: GraphQLSchema +} + +interface State { + open: boolean +} + +export default class SDLHeader extends React.Component { + ref + private node: any + constructor(props) { + super(props) + this.state = { + open: false, + } + } + + componentWillMount() { + document.addEventListener('mousedown', this.handleClick, false) + } + + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleClick, false) + } + + handleClick = e => { + if (this.node.contains(e.target)) { + return + } + return this.setState({ + open: false, + }) + } + + showOptions = () => { + this.setState({ + open: !this.state.open, + }) + } + + printSDL = () => { + return downloadSchema(this.props.schema, 'sdl') + } + + printIntrospection = () => { + return downloadSchema(this.props.schema, 'json') + } + + setRef = ref => { + this.node = ref + } + + render() { + const { open } = this.state + return ( + + Schema + + + Download + + {open && ( + + + + + )} + + + ) + } +} + +const SchemaHeader = styled.div` + display: flex; + height: 64px; + width: 100%; + align-items: center; + justify-content: space-between; + outline: none; + user-select: none; +` + +const Box = styled.div` + position: absolute; + top: 16px; + right: 2em; + width: 108px; + display: flex; + flex-wrap: wrap; + flex-direction: column; +` + +const Title = styled.div` + flex: 1; + color: ${p => styleHelper(p).title}; + cursor: default; + font-size: 14px; + font-weight: 600; + text-transform: uppercase !important; + font-family: 'Open Sans', sans-serif !important; + letter-spacing: 1px; + user-select: none !important; + padding: 16px; +` + +const Download = styled(Button)` + flex: 1; + color: ${p => styleHelper(p).download['text']}; + background: ${p => styleHelper(p).download['button']}; + height: 32px; + border-radius: 2px; + &:hover { + color: ${p => styleHelper(p).buttonTextHover}; + background-color: ${p => styleHelper(p).buttonHover}; + } +` + +const Option = styled(Download)` + text-align: left; + width: 100%; + margin-left: 0px; + border-radius: 0px; + z-index: 2000; + background: ${p => styleHelper(p).button}; +` + +const styleHelper = p => { + if (p.theme.mode === 'dark') { + return { + title: 'white', + download: { + text: p.open ? '#8B959C' : 'white', + button: p.theme.colours.darkBlue, + }, + buttonText: 'white', + button: p.theme.colours.darkBlue, + buttonHover: '#2B3C48', + buttonTextHover: 'white', + } + } + return { + title: p.theme.colours.darkBlue, + download: { + text: p.open ? 'rgba(61, 88, 102, 0.5)' : p.theme.colours.darkBlue, + button: '#f6f6f6', + }, + buttonText: p.theme.colours.darkBlue, + button: '#f6f6f6', + buttonHover: '#EDEDED', + buttonTextHover: p.theme.colours.darkBlue, + } +} diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLDocType.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLDocType.tsx new file mode 100644 index 000000000..45c04deb8 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLDocType.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import SDLType from './SDLType' +import { styled } from '../../../../styled' + +export interface DocTypeSchemaProps { + type: any + fields: any[] + interfaces: any[] +} + +export default ({ type, fields, interfaces }: DocTypeSchemaProps) => { + const nonDeprecatedFields = fields.filter(data => !data.isDeprecated) + const deprecatedFields = fields.filter(data => data.isDeprecated) + return ( + + + {type.instanceOf}{' '} + {type.name}{' '} + {interfaces.length === 0 && {`{`}} + + {interfaces.map((data, index) => ( + implements} + afterNode={ + index === interfaces.length - 1 ? {'{'} : null + } + /> + ))} + {nonDeprecatedFields.map(data => ( + + ))} + {deprecatedFields.length > 0 &&
} + {deprecatedFields.map((data, index) => ( +
+ + # Deprecated: {data.deprecationReason} + + +
+ ))} + + {'}'} + +
+ ) +} + +const DocTypeSchema = styled.div` + font-size: 14px; + flex: 1; + .doc-category-item { + padding-left: 32px; + } +` + +const DocTypeLine = styled.div` + padding: 6px 16px; + white-space: nowrap; +` + +const DocsTypeName = styled.span` + color: #f25c54; +` + +const DocsTypeInferface = styled(SDLType)` + padding-left: 16px; + .field-name { + color: rgb(245, 160, 0); + } + .type-name { + color: #f25c54; + } +` + +const DocsValueComment = styled.span` + color: ${p => p.theme.colours.black50}; + padding-right: 16px; + padding-left: 32px; +` + +const Brace = styled.span` + font-weight: 600; + color: ${p => p.theme.colours.darkBlue50}; +` diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLFieldDoc.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLFieldDoc.tsx new file mode 100644 index 000000000..2f0c21e56 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLFieldDoc.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import MarkdownContent from 'graphiql/dist/components/DocExplorer/MarkdownContent' +import SDLDocType from './SDLDocType' +import ScalarTypeSchema from '../../DocExplorer/DocsTypes/ScalarType' +import EnumTypeSchema from '../../DocExplorer/DocsTypes/EnumTypeSchema' +import SDLUnionType from './SDLUnionType' +import { CategoryTitle } from '../../DocExplorer/DocsStyles' +import { styled } from '../../../../styled' + +export interface Props { + schema: any + type: any +} + +export interface State { + showDeprecated: boolean +} + +export default class FieldDoc extends React.Component { + state = { showDeprecated: false } + + render() { + const { type, schema } = this.props + return ( +
+ {`${type.name} details`} + {type.description && + type.description.length > 0 && ( + + )} + {type.instanceOf === 'scalar' && } + {type.instanceOf === 'enum' && ( + + )} + {type.instanceOf === 'union' && ( + + )} + {type.fields.length > 0 && ( + + )} +
+ ) + } +} + +const DocsDescription = styled(MarkdownContent)` + font-size: 14px; + padding: 0 16px 20px 16px; + color: rgba(0, 0, 0, 0.5); +` diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLStyles.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLStyles.tsx new file mode 100644 index 000000000..ea87d1389 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLStyles.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { styled } from '../../../../styled' +import { columnWidth } from '../../../../constants' + +export const SchemaExplorerContainer = styled.div` + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + padding: 0px 8px 8px 8px; + background: ${p => + p.theme.mode === 'dark' ? p.theme.editorColours.editorBackground : 'white'}; + font-family: ${p => p.theme.settings['editor.fontFamily']}; + font-size: ${p => `${p.theme.settings['editor.fontSize']}px`}; + outline: none !important; +` + +export interface SDLColumnProps { + children: any + width?: number +} + +const SDLColumn = ({ children, width = columnWidth }: SDLColumnProps) => { + return {children} +} + +export { SDLColumn } + +const Column = styled('div')` + display: flex; + flex: 1 0 auto; + flex-flow: column; + padding-bottom: 20px; + border-right: 1px solid ${p => p.theme.colours.black10}; + overflow: hidden; +` diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLType.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLType.tsx new file mode 100644 index 000000000..b240375fc --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLType.tsx @@ -0,0 +1,130 @@ +import * as React from 'react' +import { GraphQLList, GraphQLNonNull, isType } from 'graphql' +import ArgumentInline from '../../DocExplorer/ArgumentInline' +import { styled } from '../../../../styled' + +export interface Props { + type: any + className?: string + beforeNode?: JSX.Element | null | false + afterNode?: JSX.Element | null | false + showParentName?: boolean + collapsable?: boolean +} + +export default class SDLType extends React.Component { + render() { + const { + type, + className, + beforeNode, + afterNode, + showParentName, + } = this.props + const isGraphqlType = isType(type) + + const fieldName = + showParentName && type.parent ? ( + + {type.parent.name}.{type.name} + + ) : ( + type.name + ) + + return ( + + {beforeNode} + {beforeNode && ' '} + {!isGraphqlType && ( + + {fieldName} + {type.args && + type.args.length > 0 && [ + '(', + + {type.args.map(arg => ( + + ))} + , + ')', + ]} + {': '} + + )} + {renderType(type.type || type)} + {type.defaultValue !== undefined ? ( + + {' '} + = {`${type.defaultValue}`} + + ) : ( + undefined + )} + {afterNode && ' '} + {afterNode} + + ) + } +} + +function renderType(type) { + if (type instanceof GraphQLNonNull) { + return ( + + {renderType(type.ofType)} + {'!'} + + ) + } + if (type instanceof GraphQLList) { + return ( + + {'['} + {renderType(type.ofType)} + {']'} + + ) + } + return {type.name} +} + +interface DocsCategoryItemProps { + clickable?: boolean + active?: boolean +} + +const DocsCategoryItem = styled('div')` + position: relative; + padding: 6px 16px; + overflow: auto; + font-size: 14px; + transition: 0.1s background-color; + background: ${p => + p.active ? p.theme.colours.black07 : p.theme.colours.white}; + + cursor: ${p => (p.clickable ? 'pointer' : 'select')}; + + // &:hover { + // color: ${p => p.theme.colours.white}; + // background: #2a7ed3; + // .field-name, + // .type-name, + // .arg-name, + // span { + // color: ${p => p.theme.colours.white} !important; + // } + // } + b { + font-weight: 600; + } +` + +const DefaultValue = styled.span` + color: ${p => p.theme.colours.black30}; + span { + color: #1f61a9; + } +` diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLUnionType.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLUnionType.tsx new file mode 100644 index 000000000..582220870 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLUnionType.tsx @@ -0,0 +1,22 @@ +import SDLType from './SDLType' +import * as React from 'react' +import { DocType } from '../../DocExplorer/DocsTypes/DocType' + +export interface EnumTypeSchemaProps { + schema: any + type: any +} + +const UnionTypeSchema = ({ schema, type }: EnumTypeSchemaProps) => { + const types = schema.getPossibleTypes(type) + return ( + + union{' '} + {type.name} + {' = '} + {types.map(value => )} + + ) +} + +export default UnionTypeSchema diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx new file mode 100644 index 000000000..7050a23f6 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx @@ -0,0 +1,123 @@ +import * as React from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { + toggleDocs, + changeWidthDocs, + setDocsVisible, +} from '../../../state/docs/actions' +import Spinner from '../../Spinner' +import { columnWidth } from '../../../constants' +import { SideTabContentProps } from '../ExplorerTabs/SideTabs' +import { getSelectedSessionIdFromRoot } from '../../../state/sessions/selectors' +import { getSessionDocs } from '../../../state/docs/selectors' +import { createStructuredSelector } from 'reselect' +import { ErrorContainer } from '../DocExplorer/ErrorContainer' +import { SchemaExplorerContainer, SDLColumn } from './SDLTypes/SDLStyles' +import SDLHeader from './SDLHeader' +import SDLEditor from './SDLEditor' +import { getSettings } from '../../../state/workspace/reducers' + +interface StateFromProps { + docs: { + navStack: any[] + docsOpen: boolean + docsWidth: number + keyMove: boolean + } + settings +} + +interface DispatchFromProps { + toggleDocs: (sessionId: string) => any + setDocsVisible: (sessionId: string, open: boolean) => any + changeWidthDocs: (sessionId: string, width: number) => any +} + +class SDLView extends React.Component< + SideTabContentProps & StateFromProps & DispatchFromProps +> { + ref + constructor(props) { + super(props) + ;(window as any).d = this + } + componentWillReceiveProps(nextProps: SideTabContentProps & StateFromProps) { + // If user use default column size % columnWidth + // Make the column follow the clicks + if (!this.props.schema && nextProps.schema) { + this.setWidth(nextProps) + } + } + + setWidth(props: any = this.props) { + this.props.setWidth(props) + } + + getWidth(props: any = this.props) { + const rootWidth = props.docs.docsWidth || columnWidth + return rootWidth + } + componentDidMount() { + this.setWidth() + } + + render() { + const { schema, settings } = this.props + let emptySchema + if (schema === undefined) { + // Schema is undefined when it is being loaded via introspection. + emptySchema = + } else if (schema === null) { + // Schema is null when it explicitly does not exist, typically due to + // an error during introspection. + emptySchema = {'No Schema Available'} + } + // let types + // if (schema instanceof GraphQLSchema) { + // types = sdlArray(schema) + // } + return ( + + {emptySchema ? ( + {emptySchema} + ) : ( + + + + + )} + + ) + } + setRef = ref => { + this.ref = ref + } +} + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + toggleDocs, + changeWidthDocs, + setDocsVisible, + }, + dispatch, + ) + +const mapStateToProps = createStructuredSelector({ + settings: getSettings, + docs: getSessionDocs, + sessionId: getSelectedSessionIdFromRoot, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, + null, + { withRef: true }, +)(SDLView) diff --git a/packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts b/packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts index 54d998c23..938fcd6d0 100644 --- a/packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts +++ b/packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts @@ -71,7 +71,11 @@ export class SchemaFetcher { return new Promise((resolve, reject) => { execute(link, operation).subscribe({ next: schemaData => { - if (schemaData && ((schemaData.errors && schemaData.errors.length > 0) || !schemaData.data)) { + if ( + schemaData && + ((schemaData.errors && schemaData.errors.length > 0) || + !schemaData.data) + ) { throw new Error(JSON.stringify(schemaData, null, 2)) } diff --git a/packages/graphql-playground-react/src/components/Playground/util/createSDL.ts b/packages/graphql-playground-react/src/components/Playground/util/createSDL.ts new file mode 100644 index 000000000..d00071fd3 --- /dev/null +++ b/packages/graphql-playground-react/src/components/Playground/util/createSDL.ts @@ -0,0 +1,135 @@ +import { + GraphQLEnumType, + GraphQLUnionType, + GraphQLInterfaceType, + GraphQLInputObjectType, + GraphQLSchema, + printSchema, +} from 'graphql' +import { serialize } from './stack' +import { prettify } from '../../../utils' +// import { getRootMap } from './stack' + +interface Options { + commentDescriptions?: boolean +} + +const defaultTypes = [ + '__Schema', + '__Directive', + '__DirectiveLocation', + '__Type', + '__Field', + '__InputValue', + '__EnumValue', + '__TypeKind', + 'String', + 'ID', + 'Boolean', + 'Int', + 'Float', +] + +/* Creates an array of SchemaTypes for the SDLFieldDocs +(A component that is similar to the DocsExplorer) to consume */ +export function sdlArray(schema: GraphQLSchema, options?: Options) { + const objectValues = + Object.values || (obj => Object.keys(obj).map(key => obj[key])) + const typeMap = schema.getTypeMap() + const types = objectValues(typeMap) + .sort((type1, type2) => type1.name.localeCompare(type2.name)) + .filter(type => !defaultTypes.includes(type.name)) + .map(type => ({ + ...type, + ...serialize(schema, type), + instanceOf: getTypeInstance(type), + })) + return types +} + +function getTypeInstance(type) { + if (type instanceof GraphQLInterfaceType) { + return 'interface' + } else if (type instanceof GraphQLUnionType) { + return 'union' + } else if (type instanceof GraphQLEnumType) { + return 'enum' + } else if (type instanceof GraphQLInputObjectType) { + return 'input' + } else { + return 'type' + } +} + +// Adds Line Breaks to Schema View +function addLineBreaks(sdl: string, commentsDisabled?: boolean) { + const noNewLines = sdl.replace(/^\s*$(?:\r\n?|\n)/gm, '') + // Line Break all Brackets + const breakBrackets = noNewLines.replace(/[}]/gm, '$&\r\n') + // Line Break all Scalars + const withLineBreaks = breakBrackets.replace(/(?:scalar )\w+/g, '$&\r\n') + + if (commentsDisabled) { + return withLineBreaks + } + // Special Line Breaking for Comments + const withCommentLineBreaks = withLineBreaks.replace( + /(?:\#[\w\'\s\r\n\*](.*)$)/gm, + '$&\r', + ) + return withCommentLineBreaks +} + +// Returns a prettified schema +export function getSDL( + schema: GraphQLSchema | null | undefined, + commentsDisabled: boolean, +) { + if (schema instanceof GraphQLSchema) { + const rawSdl = printSchema(schema, { commentDescriptions: false }) + if (commentsDisabled) { + // Removes Comments but still has new lines + const sdlWithNewLines = rawSdl.replace(/(\#[\w\'\s\r\n\*](.*)$)/gm, '') + // Removes newlines left behind by Comments + const sdlWithoutComments = prettify(sdlWithNewLines, 80) + return addLineBreaks(sdlWithoutComments, commentsDisabled) + } + const sdl = prettify(rawSdl, 80) + return addLineBreaks(sdl) + } + return '' +} + +// Downloads the schema in either .json or .graphql format +export function downloadSchema(schema: GraphQLSchema, type: string) { + if (type === 'sdl') { + const data = getSDL(schema, false) + const filename = 'schema.graphql' + return download(data, filename) + } else { + const data = JSON.stringify(schema) + const filename = 'instrospectionSchema.json' + return download(data, filename) + } +} + +// Performant option for downloading files +function download(data: any, filename: string, mime?: string) { + const blob = new Blob([data], { type: mime || 'application/octet-stream' }) + if (typeof window.navigator.msSaveBlob !== 'undefined') { + window.navigator.msSaveBlob(blob, filename) + } else { + const blobURL = window.URL.createObjectURL(blob) + const tempLink = document.createElement('a') + tempLink.style.display = 'none' + tempLink.href = blobURL + tempLink.setAttribute('download', filename) + if (typeof tempLink.download === 'undefined') { + tempLink.setAttribute('target', '_blank') + } + document.body.appendChild(tempLink) + tempLink.click() + document.body.removeChild(tempLink) + window.URL.revokeObjectURL(blobURL) + } +} diff --git a/packages/graphql-playground-react/src/components/asyncComponent.tsx b/packages/graphql-playground-react/src/components/asyncComponent.tsx new file mode 100644 index 000000000..f07cdd43e --- /dev/null +++ b/packages/graphql-playground-react/src/components/asyncComponent.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import Spinner from './Spinner' + +export interface State { + component?: any +} + +const asyncComponent = importComponent => { + return class extends React.Component { + state: State = { + component: null, + } + componentDidMount() { + importComponent().then(cmp => { + this.setState({ component: cmp.default }) + }) + } + + render() { + const C = this.state.component as any + return C ? : + } + } +} + +export default asyncComponent diff --git a/packages/graphql-playground-react/src/localDevIndex.tsx b/packages/graphql-playground-react/src/localDevIndex.tsx index e8af959b9..6b4838197 100644 --- a/packages/graphql-playground-react/src/localDevIndex.tsx +++ b/packages/graphql-playground-react/src/localDevIndex.tsx @@ -3,9 +3,9 @@ import * as ReactDOM from 'react-dom' import MiddlewareApp from './components/MiddlewareApp' import './index.css' // import { Tab } from './state/sessions/reducers' -// import { LinkCreatorProps } from './state/sessions/fetchingSagas' -// import { ApolloLink } from 'apollo-link' -// import { HttpLink } from 'apollo-link-http' +import { LinkCreatorProps } from './state/sessions/fetchingSagas' +import { ApolloLink } from 'apollo-link' +import { HttpLink } from 'apollo-link-http' // import { exampleSchema } from './fixtures/exampleSchema' if (process.env.NODE_ENV !== 'production') { @@ -22,11 +22,11 @@ if (process.env.NODE_ENV !== 'production') { setTitle={true} showNewWorkspace={false} {...options} - config={config} - configString={configString} + // config={config} + // configString={configString} // codeTheme={lightEditorColours} // tabs={tabs} - // createApolloLink={customLinkCreator} + createApolloLink={customLinkCreator} // schema={exampleSchema} />, element, @@ -34,50 +34,50 @@ if (process.env.NODE_ENV !== 'production') { }, } -const configString = `projects: -app: - schemaPath: "src/schema.graphql" - extensions: - endpoints: - default: "http://localhost:4000" -database: - schemaPath: "src/generated/prisma.graphql" - extensions: - prisma: database/prisma.yml` +// const configString = `projects: +// app: +// schemaPath: "src/schema.graphql" +// extensions: +// endpoints: +// default: "http://localhost:4000" +// database: +// schemaPath: "src/generated/prisma.graphql" +// extensions: +// prisma: database/prisma.yml` -const config = { - projects: { - prisma: { - schemaPath: 'src/generated/prisma.graphql', - includes: ['database/seed.graphql'], - extensions: { - prisma: 'database/prisma.yml', - 'prepare-binding': { - output: 'src/generated/prisma.ts', - generator: 'prisma-ts', - }, - endpoints: { - dev2: { - url: 'https://eu1.prisma.sh/public-asdf/session65/dev', - // headers: { - // Authorization: - // 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InNlcnZpY2UiOiJhc2RmQGRldiIsInJvbGVzIjpbImFkbWluIl19LCJpYXQiOjE1MjM1MTg3NTYsImV4cCI6MTUyNDEyMzU1Nn0.fzKhXa1JpN9M1UGTbS6p2KMUWDrKLxYD3i3a9eVfOQQ', - // }, - }, - }, - }, - }, - app: { - schemaPath: 'src/schema.graphql', - includes: ['queries/{booking,queries}.graphql'], - extensions: { - endpoints: { - default: 'http://localhost:4000', - }, - }, - }, - }, -} +// const config = { +// projects: { +// prisma: { +// schemaPath: 'src/generated/prisma.graphql', +// includes: ['database/seed.graphql'], +// extensions: { +// prisma: 'database/prisma.yml', +// 'prepare-binding': { +// output: 'src/generated/prisma.ts', +// generator: 'prisma-ts', +// }, +// endpoints: { +// dev2: { +// url: 'https://eu1.prisma.sh/public-asdf/session65/dev', +// // headers: { +// // Authorization: +// // 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InNlcnZpY2UiOiJhc2RmQGRldiIsInJvbGVzIjpbImFkbWluIl19LCJpYXQiOjE1MjM1MTg3NTYsImV4cCI6MTUyNDEyMzU1Nn0.fzKhXa1JpN9M1UGTbS6p2KMUWDrKLxYD3i3a9eVfOQQ', +// // }, +// }, +// }, +// }, +// }, +// app: { +// schemaPath: 'src/schema.graphql', +// includes: ['queries/{booking,queries}.graphql'], +// extensions: { +// endpoints: { +// default: 'http://localhost:4000', +// }, +// }, +// }, +// }, +// } // const tabs: Tab[] = [ // { @@ -91,21 +91,21 @@ const config = { // }, // ] -// const customLinkCreator = ( -// session: LinkCreatorProps, -// wsEndpoint?: string, -// ): { link: ApolloLink } => { -// const { headers, credentials } = session +const customLinkCreator = ( + session: LinkCreatorProps, + wsEndpoint?: string, +): { link: ApolloLink } => { + const { headers, credentials } = session -// const link = new HttpLink({ -// uri: session.endpoint, -// fetch, -// headers, -// credentials, -// }) + const link = new HttpLink({ + uri: session.endpoint, + fetch, + headers, + credentials, + }) -// return { link } -// } + return { link } +} // const lightEditorColours = { // property: '#328c8c', diff --git a/packages/graphql-playground-react/src/state/docs/actions.ts b/packages/graphql-playground-react/src/state/docs/actions.ts index 650ec8ba1..074324dfc 100644 --- a/packages/graphql-playground-react/src/state/docs/actions.ts +++ b/packages/graphql-playground-react/src/state/docs/actions.ts @@ -4,15 +4,19 @@ export const { setStacks, addStack, toggleDocs, + setDocsVisible, changeWidthDocs, changeKeyMove, showDocForReference, - setDocsVisible, } = createActions({ SET_STACKS: (sessionId, stacks) => ({ sessionId, stacks }), ADD_STACK: (sessionId, field, x, y) => ({ sessionId, field, x, y }), - TOGGLE_DOCS: sessionId => ({ sessionId }), - SET_DOCS_VISIBLE: (sessionId, open) => ({ sessionId, open }), + TOGGLE_DOCS: (sessionId, activeTabIdx) => ({ sessionId, activeTabIdx }), + SET_DOCS_VISIBLE: (sessionId, open, activeTabIdx?) => ({ + sessionId, + open, + activeTabIdx, + }), CHANGE_WIDTH_DOCS: (sessionId, width) => ({ sessionId, width }), CHANGE_KEY_MOVE: (sessionId, move) => ({ sessionId, move }), SHOW_DOC_FOR_REFERENCE: reference => ({ reference }), diff --git a/packages/graphql-playground-react/src/state/docs/reducers.ts b/packages/graphql-playground-react/src/state/docs/reducers.ts index 3c5c57325..87de43f36 100644 --- a/packages/graphql-playground-react/src/state/docs/reducers.ts +++ b/packages/graphql-playground-react/src/state/docs/reducers.ts @@ -16,12 +16,14 @@ export interface DocsSessionState { readonly docsOpen: boolean readonly docsWidth: number readonly keyMove: boolean + readonly activeTabIdx: number | null } export class DocsSession extends Record({ navStack: List([]), docsOpen: false, docsWidth: columnWidth, + activeTabIdx: null, keyMove: false, }) { toJSON() { @@ -60,14 +62,28 @@ export default handleActions( }) return state.set(sessionId, session) }, - TOGGLE_DOCS: (state, { payload: { sessionId } }) => { + TOGGLE_DOCS: (state, { payload: { sessionId, activeTabIdx } }) => { let session = getSession(state, sessionId) session = session.set('docsOpen', !session.docsOpen) + if (typeof activeTabIdx === 'number') { + session = session.set( + 'activeTabIdx', + session.docsOpen ? activeTabIdx : null, + ) + } return state.set(sessionId, session) }, - SET_DOCS_VISIBLE: (state, { payload: { sessionId, open } }) => { + SET_DOCS_VISIBLE: ( + state, + { payload: { sessionId, open, activeTabIdx } }, + ) => { let session = getSession(state, sessionId) session = session.set('docsOpen', !!open) + if (!session.docsOpen) { + session = session.set('activeTabIdx', null) + } else if (typeof activeTabIdx === 'number') { + session = session.set('activeTabIdx', activeTabIdx) + } return state.set(sessionId, session) }, CHANGE_WIDTH_DOCS: (state, { payload: { sessionId, width } }) => { diff --git a/packages/graphql-playground-react/src/state/workspace/reducers.ts b/packages/graphql-playground-react/src/state/workspace/reducers.ts index 1e9700194..58b9e0079 100644 --- a/packages/graphql-playground-react/src/state/workspace/reducers.ts +++ b/packages/graphql-playground-react/src/state/workspace/reducers.ts @@ -50,6 +50,7 @@ export const defaultSettings: ISettings = { 'prettier.printWidth': 80, 'request.credentials': 'omit', 'tracing.hideTracingResponse': true, + 'schema.disableComments': true, } // tslint:disable-next-line:max-classes-per-file diff --git a/packages/graphql-playground-react/src/styled/theme.ts b/packages/graphql-playground-react/src/styled/theme.ts index 8bd101dff..aba1b68b2 100644 --- a/packages/graphql-playground-react/src/styled/theme.ts +++ b/packages/graphql-playground-react/src/styled/theme.ts @@ -162,7 +162,6 @@ export const lightColours: Colours = { orange: 'rgba(241, 143, 1, 1)', blue: 'rgba(42, 126, 210, 1)', purple: 'rgb(164, 3, 111)', - paleText: 'rgba(0, 0, 0, 0.5)', paleGrey: '#f3f4f4', // use for bgs, borders, etc lightGrey: '#eeeff0', @@ -190,7 +189,6 @@ export const darkEditorColours: EditorColours = { ws: 'rgba(255, 255, 255, 0.4)', selection: 'rgba(255, 255, 255, 0.1)', cursorColor: 'rgba(255, 255, 255, 0.4)', - text: '#fff', textInactive: 'rgba(255, 255, 255, 0.6)', background: '#09141c', @@ -245,7 +243,6 @@ export const lightEditorColours: EditorColours = { ws: 'rgba(23, 42, 58, 0.8)', // selection: '#d1e9fd', cursorColor: 'rgba(0, 0, 0, 0.4)', - text: 'rgba(0, 0, 0, 0.7)', textInactive: 'rgba(0, 0, 0, 0.3)', background: '#dbdee0', diff --git a/packages/graphql-playground-react/src/types.ts b/packages/graphql-playground-react/src/types.ts index 20b83d923..2ae56f7ba 100644 --- a/packages/graphql-playground-react/src/types.ts +++ b/packages/graphql-playground-react/src/types.ts @@ -26,4 +26,5 @@ export interface ISettings { ['prettier.printWidth']: number ['tracing.hideTracingResponse']: boolean ['request.credentials']: 'omit' | 'include' | 'same-origin' + ['schema.disableComments']: boolean }