Repository: graphql/graphql-playground Branch: main Commit: 91ed7d8b1c1b Files: 316 Total size: 869.0 KB Directory structure: gitextract_3l4bavka/ ├── .dependabot/ │ └── config.yml ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .vscode/ │ └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ │ └── security/ │ ├── 2020-xss-template-injection.md │ └── 2021-schema-xss-phishing-attack.md ├── lerna.json ├── package.json ├── packages/ │ ├── graphql-playground-electron/ │ │ ├── .babelrc │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── main/ │ │ │ │ ├── createWindow.ts │ │ │ │ ├── index.ts │ │ │ │ ├── menu.ts │ │ │ │ ├── notify.ts │ │ │ │ ├── types.ts │ │ │ │ └── updates.ts │ │ │ ├── renderer/ │ │ │ │ ├── components/ │ │ │ │ │ ├── App.tsx │ │ │ │ │ ├── InitialView/ │ │ │ │ │ │ ├── InitialView.tsx │ │ │ │ │ │ ├── Toggle.tsx │ │ │ │ │ │ └── data.ts │ │ │ │ │ ├── Loading.tsx │ │ │ │ │ └── Root.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.html │ │ │ │ ├── index.tsx │ │ │ │ ├── redux/ │ │ │ │ │ ├── actions/ │ │ │ │ │ │ └── history.ts │ │ │ │ │ ├── createStore.ts │ │ │ │ │ └── reducers/ │ │ │ │ │ ├── history.ts │ │ │ │ │ └── index.ts │ │ │ │ └── utils/ │ │ │ │ └── errify.ts │ │ │ └── shared/ │ │ │ └── utils.ts │ │ ├── static/ │ │ │ └── icons/ │ │ │ └── icon.icns │ │ ├── tsconfig.json │ │ ├── tslint.json │ │ ├── typings/ │ │ │ ├── custom.d.ts │ │ │ └── styled-jsx.d.ts │ │ ├── webpack.config.build.js │ │ └── webpack.config.js │ ├── graphql-playground-html/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── examples/ │ │ │ └── xss-attack/ │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── minimal.html │ │ ├── minimalWithoutCDN.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── get-loading-markup.ts │ │ │ ├── index.ts │ │ │ └── render-playground-page.ts │ │ ├── tsconfig.json │ │ └── withAnimation.html │ ├── graphql-playground-middleware-express/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── basic/ │ │ │ │ ├── .envrc │ │ │ │ ├── .graphqlconfig.yml │ │ │ │ ├── README.md │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ └── graphcool/ │ │ │ ├── .envrc │ │ │ ├── .graphqlconfig.yml │ │ │ ├── README.md │ │ │ ├── datamodel.graphql │ │ │ ├── graphcool.yml │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ ├── prisma.yml │ │ │ └── schema.graphql │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── graphql-playground-middleware-hapi/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── examples/ │ │ │ └── basic/ │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── graphql-playground-middleware-koa/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── examples/ │ │ │ └── basic/ │ │ │ ├── .graphqlconfig.yml │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── graphql-playground-middleware-lambda/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── examples/ │ │ │ └── basic/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── handler.js │ │ │ ├── package.json │ │ │ └── serverless.yml │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ └── graphql-playground-react/ │ ├── .babelrc │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── config/ │ │ ├── env.js │ │ ├── jest/ │ │ │ ├── cssTransform.js │ │ │ ├── fileTransform.js │ │ │ └── typescriptTransform.js │ │ ├── paths.js │ │ ├── polyfills.js │ │ ├── webpack.config.dev.js │ │ ├── webpack.config.prod.js │ │ └── webpackDevServer.config.js │ ├── jest.config.js │ ├── package.json │ ├── public/ │ │ ├── _headers │ │ ├── _redirects │ │ ├── affiliates.graphql │ │ ├── auth.graphql │ │ ├── blogging.graphql │ │ ├── chat.graphql │ │ ├── conferences.graphql │ │ ├── empty.graphql │ │ ├── freecom.graphql │ │ ├── hn-relay-full.graphql │ │ ├── hn-relay.graphql │ │ ├── hn-starter.graphql │ │ ├── hn.graphql │ │ ├── index.html │ │ ├── insta-auth0.graphql │ │ ├── insta-email.graphql │ │ ├── insta-expo-auth.graphql │ │ ├── insta-files.graphql │ │ ├── instagram-full.graphql │ │ ├── instagram.graphql │ │ ├── movies-import.graphql │ │ ├── pokedex.graphql │ │ ├── simple-pokedex.graphql │ │ ├── todo.graphql │ │ ├── twitter.graphql │ │ ├── welcome-rp.graphql │ │ ├── welcome.graphql │ │ └── worldchat.graphql │ ├── release.sh │ ├── scripts/ │ │ ├── build.js │ │ ├── start.js │ │ └── test.js │ ├── src/ │ │ ├── __snapshots__/ │ │ │ └── index.test.tsx.snap │ │ ├── components/ │ │ │ ├── Button.tsx │ │ │ ├── Copy.tsx │ │ │ ├── EndpointPopup.tsx │ │ │ ├── FileEditor.tsx │ │ │ ├── GraphQLBinApp.tsx │ │ │ ├── HistoryPopup/ │ │ │ │ ├── HistoryChooser.tsx │ │ │ │ ├── HistoryHeader.tsx │ │ │ │ └── HistoryItems.tsx │ │ │ ├── HistoryPopup.tsx │ │ │ ├── Icons/ │ │ │ │ └── index.tsx │ │ │ ├── MiddlewareApp.tsx │ │ │ ├── Modal.tsx │ │ │ ├── Playground/ │ │ │ │ ├── ConfigEditor.tsx │ │ │ │ ├── DocExplorer/ │ │ │ │ │ ├── Argument.tsx │ │ │ │ │ ├── ArgumentInline.tsx │ │ │ │ │ ├── ColumnDoc.tsx │ │ │ │ │ ├── DocTypeSchema.tsx │ │ │ │ │ ├── DocsStyles.tsx │ │ │ │ │ ├── DocsTypes/ │ │ │ │ │ │ ├── DocType.tsx │ │ │ │ │ │ ├── EnumTypeSchema.tsx │ │ │ │ │ │ ├── ScalarType.tsx │ │ │ │ │ │ └── UnionTypeSchema.tsx │ │ │ │ │ ├── ErrorContainer.tsx │ │ │ │ │ ├── FieldDoc.tsx │ │ │ │ │ ├── GraphDocs.tsx │ │ │ │ │ ├── GraphDocsRoot.tsx │ │ │ │ │ ├── RootColumn.tsx │ │ │ │ │ ├── SchemaDoc.tsx │ │ │ │ │ ├── SearchBox.tsx │ │ │ │ │ ├── SearchResults.tsx │ │ │ │ │ └── TypeLink.tsx │ │ │ │ ├── EditorWrapper.tsx │ │ │ │ ├── ExecuteButton.tsx │ │ │ │ ├── ExecuteButtonOperation.tsx │ │ │ │ ├── ExplorerTabs/ │ │ │ │ │ ├── SideTab.tsx │ │ │ │ │ └── SideTabs.tsx │ │ │ │ ├── GraphQLEditor.tsx │ │ │ │ ├── QueryEditor.tsx │ │ │ │ ├── ResponseTracing.tsx │ │ │ │ ├── ResultViewer.tsx │ │ │ │ ├── Results.tsx │ │ │ │ ├── SchemaExplorer/ │ │ │ │ │ ├── SDLEditor.tsx │ │ │ │ │ ├── SDLHeader.tsx │ │ │ │ │ ├── SDLTypes/ │ │ │ │ │ │ ├── SDLDocType.tsx │ │ │ │ │ │ ├── SDLFieldDoc.tsx │ │ │ │ │ │ ├── SDLStyles.tsx │ │ │ │ │ │ ├── SDLType.tsx │ │ │ │ │ │ └── SDLUnionType.tsx │ │ │ │ │ └── SDLView.tsx │ │ │ │ ├── SchemaFetcher.ts │ │ │ │ ├── Tab.tsx │ │ │ │ ├── TabBar.tsx │ │ │ │ ├── TopBar/ │ │ │ │ │ ├── Polling.tsx │ │ │ │ │ ├── PollingIcon.tsx │ │ │ │ │ ├── Positioner.tsx │ │ │ │ │ ├── Reload.tsx │ │ │ │ │ ├── ReloadIcon.tsx │ │ │ │ │ ├── SchemaReload.tsx │ │ │ │ │ └── TopBar.tsx │ │ │ │ ├── TracingRow.tsx │ │ │ │ ├── VariableEditor.tsx │ │ │ │ ├── onHasCompletion.tsx │ │ │ │ └── util/ │ │ │ │ ├── InvalidSchemaError.ts │ │ │ │ ├── NoSchemaError.ts │ │ │ │ ├── ageOfDate.ts │ │ │ │ ├── createSDL.ts │ │ │ │ ├── fibonacci-backoff.ts │ │ │ │ ├── getQueryFacts.ts │ │ │ │ ├── getQueryTypes.ts │ │ │ │ ├── getSelectedOperationName.ts │ │ │ │ ├── getWorkspaceId.ts │ │ │ │ ├── hasSubscription.ts │ │ │ │ ├── immutableMemoize.ts │ │ │ │ ├── makeOperation.ts │ │ │ │ ├── parseHeaders.ts │ │ │ │ ├── session.ts │ │ │ │ ├── shallowEqual.ts │ │ │ │ ├── shouldUpdate.ts │ │ │ │ ├── stack.ts │ │ │ │ ├── toJS.tsx │ │ │ │ └── whatChanged.ts │ │ │ ├── Playground.tsx │ │ │ ├── PlaygroundWrapper.tsx │ │ │ ├── Popup.tsx │ │ │ ├── ProjectsSideNav.tsx │ │ │ ├── ProjectsSideNavItem.tsx │ │ │ ├── Root.tsx │ │ │ ├── Settings.tsx │ │ │ ├── SettingsEditor.tsx │ │ │ ├── Share.tsx │ │ │ ├── Spinner.tsx │ │ │ ├── Toggle.tsx │ │ │ ├── ToggleButton.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── asyncComponent.tsx │ │ │ ├── resolveRefStrings.ts │ │ │ └── util.ts │ │ ├── constants.ts │ │ ├── fixtures/ │ │ │ ├── exampleSchema.ts │ │ │ └── sdl.ts │ │ ├── graphqlConfig.ts │ │ ├── index.css │ │ ├── index.test.tsx │ │ ├── index.tsx │ │ ├── lib.tsx │ │ ├── localDevIndex.tsx │ │ ├── middlewareIndex.tsx │ │ ├── setupEnzyme.ts │ │ ├── state/ │ │ │ ├── appHistory/ │ │ │ │ ├── actions.ts │ │ │ │ └── reducers.ts │ │ │ ├── createStore.ts │ │ │ ├── docs/ │ │ │ │ ├── actions.ts │ │ │ │ ├── reducers.ts │ │ │ │ └── selectors.ts │ │ │ ├── general/ │ │ │ │ ├── actions.ts │ │ │ │ ├── reducers.ts │ │ │ │ └── selectors.ts │ │ │ ├── history/ │ │ │ │ ├── actions.ts │ │ │ │ ├── reducers.ts │ │ │ │ └── selectors.ts │ │ │ ├── localStorage.ts │ │ │ ├── rootSaga.ts │ │ │ ├── sessions/ │ │ │ │ ├── WebSocketLink.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── fetchingSagas.ts │ │ │ │ ├── reducers.ts │ │ │ │ ├── sagas.ts │ │ │ │ └── selectors.ts │ │ │ ├── sharing/ │ │ │ │ ├── actions.ts │ │ │ │ ├── reducers.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── sharingSaga.ts │ │ │ └── workspace/ │ │ │ ├── actions.ts │ │ │ ├── deserialize.ts │ │ │ └── reducers.ts │ │ ├── styled/ │ │ │ ├── index.ts │ │ │ ├── styled.ts │ │ │ └── theme.ts │ │ ├── types.ts │ │ ├── utils/ │ │ │ └── performance.ts │ │ └── utils.ts │ ├── tests/ │ │ └── schema.faker.graphql │ ├── tsconfig.build.json │ ├── tsconfig.jest.json │ ├── tsconfig.json │ ├── tslint.json │ └── typings/ │ ├── custom.d.ts │ └── styled-jsx.d.ts ├── renovate.json └── scripts/ ├── build.sh ├── release-html.sh ├── release-react.sh └── versions.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dependabot/config.yml ================================================ version: 1 update_configs: - package_manager: 'javascript' directory: '/' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'chore' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-react' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'chore' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-html' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'chore' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-electron' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-hapi' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-express' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-koa' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-lambda' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-hapi/examples/basic' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-express/examples/basic' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-express/examples/graphcool' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-lambda/examples/basic' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true - package_manager: 'javascript' directory: '/packages/graphql-playground-middleware-koa/examples/basic' update_schedule: 'live' allowed_updates: - match: update_type: 'security' commit_message: prefix: 'fix' prefix_development: 'fix' include_scope: true ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to this project [fork]: https://github.com/graphcool/graphql-playground/fork [pr]: https://github.com/graphcool/graphql-playground/compare [code-of-conduct]: ../CODE_OF_CONDUCT.md Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. ## Contribution Agreement As a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code, you (and, if applicable, your employer) are licensing the submitted code to the open source community subject to the MIT license. ## Submitting a pull request 0. [Fork][fork] and clone the repository 0. Create a new branch: `git checkout -b feature/my-new-feature-name` 0. Run `npm install` or `yarn install` to make sure you've got the latest dependencies. 0. Make your change 0. Run the unit tests and make sure they pass and have 100% coverage. (`npm test`) 0. Push to your fork and [submit a pull request][pr] 0. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). - In your pull request description, provide as much detail as possible. This context helps the reviewer to understand the motivation for and impact of the change. - Make sure that all the unit tests still pass. PRs with failing tests won't be merged. ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - [GitHub Help](https://help.github.com) ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ #### This issue pertains to the following package(s): - [ ] GraphQL Playground - Electron App - [ ] GraphQL Playground HTML - [ ] GraphQL Playground - [ ] GraphQL Playground Express Middleware - [ ] GraphQL Playground Hapi Middleware - [ ] GraphQL Playground Koa Middleware - [ ] GraphQL Playground Lambda Middleware #### What OS and OS version are you experiencing the issue(s) on? #### What version of graphql-playground(-electron/-middleware) are you experiencing the issue(s) on? #### What is the expected behavior? #### What is the actual behavior? #### What steps may we take to reproduce the behavior? _Please provide a gif or image of the issue for a quicker response/fix._ ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Fixes #. Changes proposed in this pull request: - - - ================================================ FILE: .gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules example/node_modules example/yarn.lock # testing /coverage # production /build # misc .DS_Store .env npm-debug.log .idea lib /middleware playground.css *.log **/examples/*/yarn.lock ================================================ FILE: .nvmrc ================================================ lts/dubnium ================================================ FILE: .vscode/settings.json ================================================ { "prettier.singleQuote": true, "prettier.trailingComma": "all", "prettier.semi": false, } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@graph.cool. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Graphcool Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

[![npm version](https://badge.fury.io/js/graphql-playground-react.svg)](https://badge.fury.io/js/graphql-playground-react) [![graphql](https://circleci.com/gh/graphql/graphql-playground.svg?style=shield)](https://circleci.com/gh/graphql/graphql-playground) > **SECURITY WARNING:** both `graphql-playground-html` and [all four (4) of it's middleware dependents](#impacted-packages) until `graphql-playground-html@1.6.22` were subject to an **XSS Reflection attack vulnerability only to unsanitized user input strings** to the functions therein. This was resolved in `graphql-playground-html@^1.6.22`. [More Information](#security-details) [CVE-2020-4038](https://github.com/graphql/graphql-playground/security/advisories/GHSA-4852-vrh7-28rf) **Future of this repository**: See [this issue](https://github.com/graphql/graphql-playground/issues/1366#issuecomment-1062088978) for details. --- GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).
[![](https://i.imgur.com/AE5W6OW.png)](https://graphqlbin.com/v2/6RQ6TM) ## Installation ```sh $ brew install --cask 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 ## Security Details > **NOTE: only _unsanitized user input_ to the functions in these packages is vulnerable** to the recently reported XSS Reflection attack. ### Impact > Impacted are any and all unsanitized **user-defined** input to: -`renderPlaygroundPage()` -`koaPlayground()` -`expressPlayground()` -`koaPlayground()` -`lambdaPlayground() > If you used static values, such as `graphql-playground-electron` does in [it's webpack config](https://github.com/prisma-labs/graphql-playground/blob/main/packages/graphql-playground-electron/webpack.config.build.js#L16), as well as the most common middleware implementations out there, they were not vulnerable to the attack. The only reason this vulnerability exists is because we are using template strings in `renderPlaygroundPage()` with potentially unsanitized user defined variables. This allows an attacker to inject html and javascript into the page. - [Read more about preventing XSS in react](https://pragmaticwebsecurity.com/files/cheatsheets/reactxss.pdf) Common examples may be user-defined path parameters, query string, unsanitized UI provided values in database, etc., that are used to build template strings or passed directly to a `renderPlaygroundPage()` or the matching middleware function equivalent listed above. ### Impacted Packages **All versions of these packages are impacted until the ones specified below**, which are now safe for user defined input: - `graphql-playground-html`: **☔ safe** @ `1.6.22` - `graphql-playground-express` **☔ safe** @ `1.7.16` - `graphql-playground-koa` **☔ safe** @ `1.6.15` - `graphql-playground-hapi` **☔ safe** @ `1.6.13` - `graphql-playground-lambda` **☔ safe** @ `1.7.17` - `graphql-playground-electron` has always been **☔ safe** from XSS attacks! This is because configuration is statically defined [it's webpack config](https://github.com/prisma-labs/graphql-playground/blob/main/packages/graphql-playground-electron/webpack.config.build.js#L16) - `graphql-playground-react` is safe because it does not use `renderPlaygroundPage()` anywhere, and thus is not susceptible to template string XSS reflection attacks. ### More Information See the [security docs](./SECURITY.md) for more details on how your implementation might be impacted by this vulnerability. It contains safe examples, unsafe examples, workarounds, and more details. We've also provided ['an example of the xss using the express middleware]('https://github.com/prisma-labs/graphql-playground/tree/main/packages/graphql-playground-html/examples/xss-attack') ## FAQ ### How is this different from [GraphiQL](https://github.com/graphql/graphiql)? 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 See the following question for more additonal features. ### What's the difference between the desktop app and the web version? 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. ### How does GraphQL Bin work? You can easily share your Playgrounds with others by clicking on the "Share" button and sharing the generated link. You can think about GraphQL Bin like Pastebin for your GraphQL queries including the context (endpoint, HTTP headers, open tabs etc). > You can also find the announcement blog post [here](https://blog.graph.cool/introducing-graphql-playground-f1e0a018f05d). ## Settings In the top right corner of the Playground window you can click on the settings icon. These are the settings currently available: ```js { 'editor.cursorShape': 'line', // possible values: 'line', 'block', 'underline' 'editor.fontFamily': `'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace`, 'editor.fontSize': 14, 'editor.reuseHeaders': true, // new tab reuses headers from last tab 'editor.theme': 'dark', // possible values: 'dark', 'light' 'general.betaUpdates': false, 'prettier.printWidth': 80, 'prettier.tabWidth': 2, 'prettier.useTabs': false, 'request.credentials': 'omit', // possible values: 'omit', 'include', 'same-origin' 'schema.polling.enable': true, // enables automatic schema polling 'schema.polling.endpointFilter': '*localhost*', // endpoint filter for schema polling 'schema.polling.interval': 2000, // schema polling interval in ms 'schema.disableComments': boolean, 'tracing.hideTracingResponse': true, 'tracing.tracingSupported': true, // set false to remove x-apollo-tracing header from Schema fetch requests } ``` ## Usage ### Properties 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/main/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 { 'editor.cursorShape': 'line' | 'block' | 'underline' 'editor.fontFamily': string 'editor.fontSize': number 'editor.reuseHeaders': boolean 'editor.theme': 'dark' | 'light' 'general.betaUpdates': boolean 'prettier.printWidth': number 'prettier.tabWidth': number 'prettier.useTabs': boolean 'request.credentials': 'omit' | 'include' | 'same-origin' 'request.globalHeaders': { [key: string]: string } 'schema.polling.enable': boolean 'schema.polling.endpointFilter': string 'schema.polling.interval': number 'schema.disableComments': boolean 'tracing.hideTracingResponse': boolean 'tracing.tracingSupported': 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** ```ts interface Tab { 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, subscriptionEndpoint?: string) => 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. ### As HTML Page If you simply want to render the Playground HTML on your own, for example when implementing a GraphQL Server, there are 2 options for you: 1. [The bare minimum HTML needed to render the Playground](https://github.com/prismagraphql/graphql-playground/blob/main/packages/graphql-playground-html/minimal.html) 2. [The Playground HTML with full loading animation](https://github.com/prismagraphql/graphql-playground/blob/main/packages/graphql-playground-html/withAnimation.html) Note: In case you do not want to serve assets from a CDN (like jsDelivr) and instead use a local copy, you will need to install `graphql-playground-react` from npm, and then replace all instances of `//cdn.jsdelivr.net/npm` with `./node_modules`. An example can be found [here](https://github.com/prismagraphql/graphql-playground/blob/main/packages/graphql-playground-html/minimalWithoutCDN.html) ### As React Component #### Install ```sh yarn add graphql-playground-react ``` #### Use GraphQL Playground provides a React component responsible for rendering the UI and Session management. There are **3 dependencies** needed in order to run the `graphql-playground-react` React component. 1. _Open Sans_ and _Source Code Pro_ fonts 2. Rendering the `` component The GraphQL Playground requires **React 16**. Including Fonts (`1.`) ```html ``` Including stylesheet and the component (`2., 3.`) ```js import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { Playground, store } from 'graphql-playground-react' ReactDOM.render( , document.body, ) ``` ### As Server Middleware #### Install ```sh # Pick the one that matches your server framework yarn add graphql-playground-middleware-express # for Express or Connect yarn add graphql-playground-middleware-hapi yarn add graphql-playground-middleware-koa yarn add graphql-playground-middleware-lambda ``` #### Usage with example 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/main/packages/graphql-playground-middleware-express/examples/basic) - **Hapi:** See [packages/graphql-playground-middleware-hapi](https://github.com/prismagraphql/graphql-playground/tree/main/packages/graphql-playground-middleware-hapi) - **Koa:** See [packages/graphql-playground-middleware-koa](https://github.com/prismagraphql/graphql-playground/tree/main/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. ### As serverless handler #### Install ```sh yarn add graphql-playground-middleware-lambda ``` #### Usage `handler.js` ```js import lambdaPlayground from 'graphql-playground-middleware-lambda' // or using require() // 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) } exports.playgroundHandler = lambdaPlayground({ endpoint: '/dev/graphql', }) ``` `serverless.yml` ```yaml functions: graphql: handler: handler.graphqlHandler events: - http: path: graphql method: post cors: true playground: handler: handler.playgroundHandler events: - http: path: playground method: get cors: true ``` #### Security Issue There is an [XSS Reflection Vulnerability](./SECURITY.md) when using these middlewares with unsanitized user input before ## Development ```sh $ cd packages/graphql-playground-react $ yarn $ yarn start ``` 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! ### Contributing to this project This repository is managed by EasyCLA. Project participants must sign the free ([GraphQL Specification Membership agreement](https://preview-spec-membership.graphql.org) before making a contribution. You only need to do this one time, and it can be signed by [individual contributors](http://individual-spec-membership.graphql.org/) or their [employers](http://corporate-spec-membership.graphql.org/). To initiate the signature process please open a PR against this repo. The EasyCLA bot will block the merge if we still need a membership agreement from you. You can find [detailed information here](https://github.com/graphql/graphql-wg/tree/main/membership). If you have issues, please email [operations@graphql.org](mailto:operations@graphql.org). If your company benefits from GraphQL and you would like to provide essential financial support for the systems and people that power our community, please also consider membership in the [GraphQL Foundation](https://foundation.graphql.org/join). ## 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 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 } ``` ### Versions This is repository is a "mono repo" and contains multiple packages using [Yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). Please be aware that versions are **not** synchronised between packages. The versions of the [release page](https://github.com/graphcool/graphql-playground/releases) refer to the electron app. ### Packages 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 ## Help & Community [![Discord](https://img.shields.io/discord/586999333447270440.svg)](https://discord.gg/EXUYPaY) Join our [Discord Server](https://discord.gg/EXUYPaY) if you run into issues or have questions. We love talking to you!

Prisma

================================================ FILE: SECURITY.md ================================================ # Known Vulnerabilities - [2021: Introspection Schema Phishing Attack XSS Vulnerability](docs/security/2021-schema-xss-phishing-attack.md) - [2020: XSS Reflection Vulnerability](docs/security/2020-xss-template-injection.md) ================================================ FILE: docs/security/2020-xss-template-injection.md ================================================ ## XSS Reflection Vulnerability the origin of the vulnerability is in `renderPlaygroundPage`, found in `graphql-playground-html` ### Impact When using - `renderPlaygroundPage()`, - `koaPlayground()` - `expressPlayground()` - `koaPlayground()` - `lambdaPlayground()` - any downstream dependent packages that use these functions without sanitization of user input, your application is vulnerable to an XSS Reflection Attack. This is a serious vulnerability that could allow for exfiltration of data or user credentials, or to disrupt systems. We've provided ['an example of the xss using the express middleware]('https://github.com/prisma-labs/graphql-playground/tree/main/packages/graphql-playground-middleware-express/examples/xss-attack') ### Impacted Packages **All versions of these packages are impacted until those specified below**, which are now safe for user defined input: - `graphql-playground-html`: **☔ safe** @ `1.6.22` - `graphql-playground-express` **☔ safe** @ `1.7.16` - `graphql-playground-koa` **☔ safe** @ `1.6.15` - `graphql-playground-hapi` **☔ safe** @ `1.6.13` - `graphql-playground-lambda` **☔ safe** @ `1.7.17` ### Static input was always safe These examples are safe for _all versions_ **because input is static** with `express` and `renderPlaygroundPage`: ```js app.get('/playground', (req) => { res.html( renderPlaygroundPage({ endpoint: `/our/graphql`, }), ) next() }) ``` with `expressPlayground`: ```js // params app.get('/playground', (req) => expressPlayground({ endpoint: `/our/graphql`, settings: { 'editor.theme': req.query.darkMode ? 'dark' : 'light' }, }), ) ``` with `koaPlayground`: ```js const koa = require('koa') const koaRouter = require('koa-router') const koaPlayground = require('graphql-playground-middleware-koa') const app = new koa() const router = new koaRouter() router.all('/playground', koaPlayground({ endpoint: '/graphql' })) ``` ### Vulnerable Examples Here are some examples where the vulnerability would be present before the patch, because of unfiltered user input ```js const express = require('express') const expressPlayground = require('graphql-playground-middleware-express') .default const app = express() app.use(express.json()) // params app.get('/playground/:id', (req) => expressPlayground({ endpoint: `/our/graphql/${req.params.id}`, }), ) // params app.get('/playground', (req) => expressPlayground({ endpoint: `/our/graphql`, // any settings that are unsanitized user input, not just `endpoint` settings: { 'editor.fontFamily': req.query.font }, }), ) ``` [See a proof of concept](packages/graphql-playground-html/examples/xss-attack) to understand the vulnerability better ### Workaround To fix this issue without the update, you can sanitize however you want. We suggest using [`xss`](https://www.npmjs.com/package/xss) (what we use for our own fix) For example, with `graphql-playground-middleware-express`: ```js const express = require('express') const { filterXSS } = require('xss') const expressPlayground = require('graphql-playground-middleware-express') .default const app = express() const filter = (val) => filterXSS(val, { whitelist: [], stripIgnoreTag: true, stripIgnoreTagBody: ['script'] }) // simple example app.get('/playground/:id', (req) => expressPlayground({ endpoint: `/graphql/${filter(req.params.id)}` }) // advanced params app.get('/playground', (req) => expressPlayground(JSON.parse(filter(JSON.stringify(req.query)))) ``` [See a proof of concept workaround](packages/graphql-playground-html/examples/xss-attack), example #3 ================================================ FILE: docs/security/2021-schema-xss-phishing-attack.md ================================================ ## GraphQL Playground introspection schema template injection attack: Advisory Statement This is a security advisory for an XSS vulnerability in `graphql-playground`. A similar vulnerability affects `graphiql`, the package from which `graphql-playground` was forked. There is a corresponding `graphiql` [advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8). - [1. Impact](#1-impact) - [2. Scope](#2-scope) - [3. Patches](#3-patches) - [4. Reproducing the exploit](#4-reproducing-the-exploit) - [5. Credit](#5-credit) - [6. For more information](#6-for-more-information) ### 1. Impact All versions of `graphql-playground-react` older than `graphql-playground-react@1.7.28` are vulnerable to compromised HTTP schema introspection responses or `schema` prop values with malicious GraphQL type names, exposing a dynamic XSS attack surface that can allow code injection on operation autocomplete. In order for the attack to take place, the user must load a malicious schema in `graphql-playground`. There are several ways this can occur, including by specifying the URL to a malicious schema in the `endpoint` query parameter. If a user clicks on a link to a GraphQL Playground installation that specifies a malicious server, arbitrary JavaScript can run in the user's browser, which can be used to exfiltrate user credentials or other harmful goals. ### 2. Scope This advisory describes the impact on the `graphql-playground-react` package. The vulnerability also affects `graphiql`, the package from which `graphql-playground` was forked, with a less severe impact; see the [`graphiql` advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) for details. It affects all versions of `graphql-playground-react` older than `v1.7.28`. This vulnerability was introduced with the first public release of `graphql-playground`, so it impacts both the original legacy `graphql-playground` and the contemporary `graphql-playground-react` npm package. It is most easily exploited on `graphql-playground-react@1.7.0` and newer, as that release added functionality which made it possible to override the endpoint URL via query parameter even if it is explicitly specified in the code. `graphql-playground-react` is commonly loaded via the `graphql-playground-html` package or a middleware package that wraps it (`graphql-playground-express`, `graphql-playground-middleware-koa`, `graphql-playground-middleware-hapi`, or `graphql-playground-middleware-lambda`). By default, these packages render an HTML page which loads the *latest* version of `graphql-playground-react` through a CDN. If you are using one of these packages to install GraphQL Playground on your domain *and you do not explicitly pass the `version` option to `renderPlaygroundPage` or the middleware function*, then you do not need to take any action to resolve this vulnerability, as the latest version of the React app will automatically be loaded. `graphql-playground-react` is also commonly loaded via HTML served by Apollo Server. Apollo Server always pins a specific version of `graphql-playground-react`, so if you are using Apollo Server you do need to take action to resolve this vulnerability. See the [Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) for details. ### 3. Patches `graphql-playground-react@1.7.28` addresses this issue via defense in depth: - **HTML-escaping text** that should be treated as text rather than HTML. In most of the app, this happens automatically because React escapes all interpolated text by default. However, one vulnerable component uses the unsafe `innerHTML` API and interpolated type names directly into HTML. We now properly escape that type name, which fixes the known vulnerability. - **Validates the schema** upon receiving the introspection response or schema changes. Schemas with names that violate the GraphQL spec will no longer be loaded. (This includes preventing the Doc Explorer from loading.) This change is also sufficient to fix the known vulnerability. - **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in `description` and `deprecationReason` fields, and the web app renders them to HTML using the `markdown-it` library. Prior to `graphql-playground-react@1.7.28`, GraphQL Playground used two separate libraries to render Markdown: `markdown-it` and `marked`. As part of the development of `graphql-playground-react@1.7.28`, we verified that our use of `markdown-it` prevents the inclusion of arbitrary HTML. We use `markdown-it` without setting `html: true`, so we are comfortable relying on [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) here. We considered running a second level of sanitization over all rendered Markdown using a library such as `dompurify` but believe that is unnecessary as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.3` does update to the latest version of `markdown-it` (v12, from v10) so that any security fixes in v11 and v12 will take effect. On the other hand, [`marked`](https://github.com/markedjs/marked) recommends the use of a separate HTML sanitizer if its input is untrusted. In this release, we switch the one component which uses `marked` to use `markdown-it` like the rest of the app. **If you are using `graphql-playground-react` directly in your client app**, upgrade to version 1.7.28 or later. **If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are passing the `version` option to a function imported from that package, change that `version` option to be at least `"1.7.28"`. **If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are **NOT** passing the `version` option to a function imported from that package, no action is necessary; your app automatically loads the latest version of `graphql-playground-react` from CDN. ### 4. Reproducing the exploit We are hosting a "malicious" server at https://graphql-xss-schema.netlify.app/graphql . This server has a hard-coded introspection result that includes unsafe HTML in type names. If you manually change a GraphQL Playground installation to use that endpoint, clear the operation pane, and type `{x` into the operation pane, an alert will pop up; this demonstrates execution of code provided by the malicious server. An URL like https://YOUR-PLAYGROUND-SERVER/?endpoint=https%3A%2F%2Fgraphql-xss-schema.netlify.app%2Fgraphql&query=%7B will load already configured with the endpoint in question. (This URL-based exploit works on `graphql-playground-react@1.7.0` and newer; older versions may be protected from this particular URL-based exploit depending on their configuration.) ### 5. Credit This vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank you! :1st_place_medal: Others who contributed: - extensive help from [@glasser](https://github.com/glasser) at [Apollo](https://github.com/apollographql) - [@acao](https://github.com/acao) - [@imolorhe](https://github.com/imolorhe) - [@divyenduz](https://github.com/divyenduz) - [@dotansimha](https://github.com/dotansimha) - [@timsuchanek](http://github.com/timsuchanek) - [@benjie](https://github.com/Ry0taK) and many others who provided morale support ### 6. For more information If you have any questions or comments about this advisory: - The `graphiql` advisory document contains [more information](https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md#2-more-details-on-the-vulnerability) about how both the client-side and server-side vulnerabilities work - Open an issue in the [graphql-playground repo](https://github.com/graphql/graphql-playground/new/issues) ================================================ FILE: lerna.json ================================================ { "version": "independent", "packages": ["packages/*"], "npmClient": "yarn", "useWorkspaces": true, "command": { "publish": { "allowBranch": ["main"], "preDistTag": "next", "distTag": "latest", "conventionalCommits": true }, "version": { "ignoreChanges": [ "'__tests__' '**/*.spec.js' '**/*.spec.js' '**/*.spec.ts'", "**/examples/**", "**/public/**", "public/**", "packages/*/yarn.lock", "**/*.md" ], "message": "chore(release): publish" } } } ================================================ FILE: package.json ================================================ { "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "bash scripts/build.sh", "show-versions": "bash scripts/versions.sh", "release-html": "bash scripts/release-html.sh", "release-react": "bash scripts/release-react.sh", "prepublishOnly": "yarn build" }, "devDependencies": { "@hapi/hapi": "19.1.1", "@types/node": "12.12.34", "aws-lambda": "1.0.5", "express": "4.17.1", "koa": "2.11.0", "lerna": "^3.22.0", "rimraf": "3.0.2" }, "dependencies": { "dotenv": "8.2.0" } } ================================================ FILE: packages/graphql-playground-electron/.babelrc ================================================ { "presets": [ "@babel/preset-env", "@babel/preset-react" ], "plugins": [ "styled-jsx-postcss/babel", "@babel/plugin-proposal-object-rest-spread", "babel-plugin-styled-components" ] } ================================================ FILE: packages/graphql-playground-electron/.gitignore ================================================ .DS_STORE *.log .vscode node_modules dist .tmp lib .idea *.iml *.swp *.swo .envrc .netlify .tags .yarnclean .awcache dll .happypack build-electron ================================================ FILE: packages/graphql-playground-electron/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.8.15](https://github.com/graphcool/graphql-playground/compare/graphql-playground-electron@1.8.14...graphql-playground-electron@1.8.15) (2020-10-20) **Note:** Version bump only for package graphql-playground-electron ## [1.8.14](https://github.com/graphcool/graphql-playground/compare/graphql-playground-electron@1.8.13...graphql-playground-electron@1.8.14) (2020-09-15) **Note:** Version bump only for package graphql-playground-electron ## [1.8.13](https://github.com/graphcool/graphql-playground/compare/graphql-playground-electron@1.8.12...graphql-playground-electron@1.8.13) (2020-08-30) **Note:** Version bump only for package graphql-playground-electron ## [1.8.12](https://github.com/graphcool/graphql-playground/compare/graphql-playground-electron@1.8.11...graphql-playground-electron@1.8.12) (2020-08-30) ### Bug Fixes * upgrades, schema viewer display & width adjustment ([2bb34bb](https://github.com/graphcool/graphql-playground/commit/2bb34bb8fb8c356e10435727a3f82cd23464b6b6)) ## 1.8.11 (2020-06-07) ### Bug Fixes * hapi and koa mws for next release ([#1217](https://github.com/graphcool/graphql-playground/issues/1217)) ([40c35fc](https://github.com/graphcool/graphql-playground/commit/40c35fc4c73b939d002c9d2dff51eed5dd0b6aa9)) * **deps:** [security] bump lodash.merge ([2e6b2e5](https://github.com/graphcool/graphql-playground/commit/2e6b2e5fc59c9f7fbbad5398d0defc1a0b3bd849)) * **deps:** [security] bump minimist ([a77ca5d](https://github.com/graphcool/graphql-playground/commit/a77ca5d64d9fe7b209e8a732d189570ba750eb3e)) * **deps:** update deps and toolchain, move back to using yarn… ([#1191](https://github.com/graphcool/graphql-playground/issues/1191)) ([824c7a5](https://github.com/graphcool/graphql-playground/commit/824c7a57f0284f022726a8b8840aafc3e8720ccd)) * **deps-dev:** [security] bump electron ([98ffd72](https://github.com/graphcool/graphql-playground/commit/98ffd722362820a867a9eef9ef51e5098f45afde)) * **deps-dev:** [security] bump webpack-dev-server ([a0a784d](https://github.com/graphcool/graphql-playground/commit/a0a784dd57af53799e781b876c9c80354e37c6a0)) ## 1.8.10 (2019-02-23) ## 1.8.9 (2019-02-01) ## 1.8.7 (2019-01-28) ## 1.8.2 (2018-11-14) # 1.8.0 (2018-10-16) ### Features * update graphql-config dependency ([bb6baa1](https://github.com/graphcool/graphql-playground/commit/bb6baa1c26a0f72af73018acbe722c979f892322)) ## 1.6.2 (2018-07-06) ## 1.6.1 (2018-06-26) # 1.6.0 (2018-05-31) ### Bug Fixes * **deps:** update dependency styled-jsx to v2.2.6 ([#543](https://github.com/graphcool/graphql-playground/issues/543)) ([b110cc9](https://github.com/graphcool/graphql-playground/commit/b110cc95c3d68cb7fae136b5d1dbbeb49b8a603d)) ## 1.5.9 (2018-05-25) ### Bug Fixes * update react dep ([9588758](https://github.com/graphcool/graphql-playground/commit/95887589a7d56570ce6007cd7d164bfc89c8a690)) ## 1.5.7 (2018-05-08) ### Bug Fixes * bump version ([2fdab26](https://github.com/graphcool/graphql-playground/commit/2fdab263f904fc38e3373f5cc3c8dcaa9342d42e)) ## 1.5.6 (2018-04-24) ### Bug Fixes * **electron:** update graphql-config-extension-prisma ([8fd45a1](https://github.com/graphcool/graphql-playground/commit/8fd45a1c27172722c335b4c84b793813c2d52b7e)) ## 1.5.4 (2018-04-12) ## 1.5.2 (2018-04-02) ## 1.5.1 (2018-04-02) # 1.5.0 (2018-04-01) # 1.5.0-rc.5 (2018-03-28) ### Bug Fixes * **electron:** endpoint injection ([39a1110](https://github.com/graphcool/graphql-playground/commit/39a1110a0284ef05c73ada3892c585c85e6e14a2)) # 1.5.0-rc.4 (2018-03-26) # 1.5.0-rc.2 (2018-03-26) # 1.5.0-rc.1 (2018-03-23) ## 1.4.5 (2018-03-15) ## 1.4.4 (2018-03-02) ### Bug Fixes * **deps:** rm not needed graphql-playground dep ([79b3ef1](https://github.com/graphcool/graphql-playground/commit/79b3ef1f987484923c272dbbc18890433e5f3340)) * **deps:** update dependency electron-updater to v2.20.1 ([#528](https://github.com/graphcool/graphql-playground/issues/528)) ([ab2e9c3](https://github.com/graphcool/graphql-playground/commit/ab2e9c3d43f2225b546d93473bbde47535365f8e)) * **deps:** update dependency graphql-config-extension-graphcool to v1.0.6 ([#545](https://github.com/graphcool/graphql-playground/issues/545)) ([9ef55be](https://github.com/graphcool/graphql-playground/commit/9ef55bec0fd3359121ab65fa2a5ff55231feeaec)) * **deps:** update deps ([c17a8d2](https://github.com/graphcool/graphql-playground/commit/c17a8d25779ba7d3e6e0b18e9b45df86c410c4f5)) * **deps:** update deps in electron app ([772a04c](https://github.com/graphcool/graphql-playground/commit/772a04cfc684c2e38c376a9ef3abe311090c96d4)) * **settings:** Expose settings programmatically, update deps ([0356557](https://github.com/graphcool/graphql-playground/commit/03565573869f240675aaa5399bb5f0ac097455c5)) ## 1.4.2 (2018-01-22) ## 1.4.1 (2018-01-22) ### Bug Fixes * **electron:** Add zoom on CMD + and CMD - ([cc2c4ed](https://github.com/graphcool/graphql-playground/commit/cc2c4edc48c986aa55a76c56362e5109eb393b1f)) * **electron:** postcss config ([55187dc](https://github.com/graphcool/graphql-playground/commit/55187dcbed2be9c48f24424e6d3ee5f2d60e11e5)) ## 1.3.24 (2018-01-13) ### Bug Fixes * **build:** Fix build, temporarily disable yarn workspace, update graphcool-styles & graphcool-ui de ([af501d7](https://github.com/graphcool/graphql-playground/commit/af501d7a754a14dbacc76439a77434f892828482)) * **deps:** Updated graphql-config-extension-graphcool ([ef83c09](https://github.com/graphcool/graphql-playground/commit/ef83c097c018a42f7ee65529d6af4ea3928a4281)) ## 1.3.21 (2018-01-04) ## 1.3.20 (2018-01-04) ## 1.3.19 (2018-01-03) ### Bug Fixes * electron without graphql config ([bf5a722](https://github.com/graphcool/graphql-playground/commit/bf5a722f484a644a1a64e536ba6bd375dbfce928)) ## 1.3.17 (2018-01-03) ## 1.3.16 (2017-12-27) ## 1.3.14 (2017-12-26) ## 1.3.12 (2017-12-24) ## 1.3.10 (2017-12-20) ### Bug Fixes * should suggest `graphql-cli` instead of `graphql` ([bbfa0ca](https://github.com/graphcool/graphql-playground/commit/bbfa0ca935dea618208f76c58b86363f27ddf595)) ## 1.3.9 (2017-12-14) ## 1.3.8-beta.1 (2017-12-12) ### Bug Fixes * Capture keyboard event so it's always called (regardless of any stopPropagation by children) ([56634d0](https://github.com/graphcool/graphql-playground/commit/56634d0c7079854888a7d2d5ec0936d8fd0b06a5)) * change package.json > author > url to match URI schema ([0a84b89](https://github.com/graphcool/graphql-playground/commit/0a84b89f6a94aeecdf48217b1de661181e0b5741)) ### Features * Add Settings to the menu with the shortcut to open on CMD + , (Fixes [#290](https://github.com/graphcool/graphql-playground/issues/290)) ([ead0cae](https://github.com/graphcool/graphql-playground/commit/ead0cae5c4d069f87b61a6cd17d5855f5e215b81)) # 1.3.0 (2017-12-01) # 1.2.0 (2017-11-24) ## 1.1.1 (2017-11-03) ### Bug Fixes * **desktop:** allow urls without tld - fixes [#138](https://github.com/graphcool/graphql-playground/issues/138) ([be3d2cf](https://github.com/graphcool/graphql-playground/commit/be3d2cfe219211393b6438332b553e6e3d9b7493)) ================================================ FILE: packages/graphql-playground-electron/README.md ================================================ # Playground Repository for the electron playground app. ## Development ```sh yarn install yarn start ``` ## Production build ```sh yarn release ``` This will create a `build-electron` folder where you will have the compiled electron app. (The assets will automatically be uploaded to Github if the `GH_TOKEN` env variable is set. See [here](https://www.electron.build/publishing-artifacts) for more.) ================================================ FILE: packages/graphql-playground-electron/package.json ================================================ { "name": "graphql-playground-electron", "productName": "GraphQL Playground", "homepage": "https://github.com/graphcool/graphql-playground", "repository": "graphcool/graphql-playground", "description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration)", "version": "1.8.15", "private": true, "author": { "name": "Graphcool", "email": "hello@graph.cool", "url": "https://www.graph.cool" }, "main": "lib/main", "build": { "appId": "cool.graph.playground", "mac": { "category": "public.app-category.developer-tools", "icon": "static/icons/icon.icns" }, "win": { "icon": "static/icons/icon.ico" }, "linux": { "category": "Development", "target": [ "AppImage", "snap", "deb" ] }, "nsis": { "oneClick": false, "perMachine": true, "allowElevation": true, "allowToChangeInstallationDirectory": true, "runAfterFinish": false }, "files": [ "lib/**/!(*.map)" ], "directories": { "output": "build-electron" }, "fileAssociations": { "ext": "graphql", "name": "GraphQL", "role": "Editor" }, "protocols": { "name": "graphql-playground-protocol", "schemes": [ "graphql-playground" ] } }, "scripts": { "build:ts": "rimraf lib && tsc", "build:webpack": "rimraf ./dist && NODE_ENV=production GRAPHQL_ENDPOINT=$BACKEND_ADDR/system webpack --config webpack.config.build.js && cp -r static/* dist", "build": "npm run build:ts && npm run build:webpack && cp -r dist lib && rimraf ./build:electron", "release": "npm run build && electron-builder -lmw", "release:mac": "npm run build && electron-builder -m", "release:win": "npm run build && electron-builder -w", "release:linux": "npm run build && electron-builder -l", "library": "rimraf .happypack && NODE_ENV=production webpack --config webpack.library.js -p", "lint": "tslint \"src/**/*.ts{,x}\" && lint-staged", "start": "yarn build:ts && concurrently \"yarn start:react\" \"wait-on http://localhost:4040/ && yarn start:electron\"", "start:react": "webpack-dev-server --hot --profile --history-api-fallback --host 0.0.0.0 --port 4040", "start:electron": "electron lib/main", "stats": "NODE_ENV=production webpack --config webpack.library.js --profile --json > stats.json", "test": "npm run lint", "precommit": "lint-staged", "prettier": "prettier --single-quote --no-semi --trailing-comma all --write '*.{js,ts,tsx}' 'src/**/*.{js,ts,tsx}'" }, "lint-staged": { "*.{ts,tsx}": [ "prettier --single-quote --no-semi --trailing-comma all --write", "tslint", "git add" ] }, "pre-push": [ "test-quick" ], "dependencies": { "@types/ms": "0.7.30", "classnames": "2.2.5", "core-js": "^3.6.5", "date-fns": "1.29.0", "electron-is-dev": "0.3.0", "electron-localshortcut": "3.1.0", "electron-log": "2.2.14", "electron-updater": "^4.0.0", "find-up": "^2.1.0", "graphcool-styles": "0.2.7", "graphcool-ui": "^0.0.14", "graphql": "^15.3.0", "graphql-config": "^2.2.2", "graphql-config-extension-graphcool": "^1.0.11", "graphql-config-extension-prisma": "^0.3.0", "graphql-playground-html": "^1.6.29", "graphql-playground-react": "^1.7.27", "immutable": "4.0.0-rc.9", "js-yaml": "^3.11.0", "lodash.merge": "^4.6.2", "minimist": "^1.2.3", "ms": "^2.1.1", "query-string": "^5.0.1", "raven": "^2.4.2", "react": "16.13.1", "react-dom": "^16.4.0", "react-redux": "^7.2.1", "redux": "^4.0.5", "redux-localstorage": "^1.0.0-rc5", "redux-localstorage-filter": "^0.1.1", "regenerator-runtime": "^0.13.7", "reselect": "^3.0.1", "semver": "^5.5.0", "styled-jsx": "2.2.6", "styled-jsx-postcss": "git+https://github.com/timsuchanek/styled-jsx-postcss#build3", "sweetalert2": "^7.21.0", "validator": "^9.2.0" }, "devDependencies": { "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-json-strings": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/plugin-transform-runtime": "^7.12.0", "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@types/classnames": "2.2.3", "@types/deasync": "0.1.0", "@types/jest": "22.2.3", "@types/node": "12.12.34", "@types/react": "16.9.32", "@types/react-addons-test-utils": "0.14.20", "@types/react-dom": "~16.9.6", "@types/react-redux": "7.1.9", "@types/zen-observable": "^0.5.3", "awesome-typescript-loader": "5.0.0", "babel-core": "^7.0.0-bridge.0", "babel-jest": "^23.4.2", "babel-loader": "^8.0.0", "babel-plugin-styled-components": "^1.8.0", "babili-webpack-plugin": "0.1.2", "concurrently": "3.5.1", "css-loader": "0.28.11", "electron": "2.0.18", "electron-builder": "^22.8.1", "electron-devtools-installer": "3.1.1", "extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.11", "fork-ts-checker-webpack-plugin": "0.4.1", "happypack": "5.0.0", "html-loader": "^1.3.1", "html-webpack-plugin": "3.2.0", "identity-obj-proxy": "3.0.0", "json-loader": "0.5.7", "lint-staged": "7.1.2", "node-noop": "1.0.0", "polished": "1.9.2", "postcss-inherit": "git+https://github.com/timsuchanek/postcss-inherit#build3", "postcss-loader": "0.9.1", "postcss-simple-vars": "3.1.0", "prettier": "2.0.2", "raw-loader": "0.5.1", "react-addons-test-utils": "15.6.2", "react-test-renderer": "16.4.0", "redux-mock-store": "1.5.1", "rimraf": "3.0.2", "style-loader": "0.20.1", "svgo-loader": "1.2.1", "ts-loader": "3.4.0", "tslint": "5.10.0", "tslint-graphcool-frontend": "0.0.3", "tslint-loader": "3.6.0", "typescript": "3.8.3", "url-loader": "0.6.2", "wait-on": "2.1.0", "webpack": "4.44.1", "webpack-bundle-analyzer": "3.3.2", "webpack-cli": "^3.3.12", "webpack-dev-server": "3.11.0", "webpack-uglify-parallel": "0.1.4" }, "resolutions": { "**/graphql": "0.13.2", "**/**/graphql": "0.13.2", "**/**/**/graphql": "0.13.2", "**/**/**/**/graphql": "0.13.2", "**/**/**/**/**/graphql": "0.13.2", "**/**/**/**/**/**/graphql": "0.13.2" } } ================================================ FILE: packages/graphql-playground-electron/postcss.config.js ================================================ module.exports = ctx => ({ plugins: [ require('postcss-simple-vars')({ variables: () => require('graphcool-styles/dist/variables/variables.js'), }), require('postcss-inherit')({ globalStyles: 'node_modules/graphcool-styles/dist/styles.css', propertyRegExp: /^(inherit|extend|p)s?:?$/i, }), ], }) ================================================ FILE: packages/graphql-playground-electron/src/main/createWindow.ts ================================================ import { BrowserWindow, app, ipcMain } from 'electron' import * as path from 'path' import dev = require('electron-is-dev') import { newWindowConfig } from '../shared/utils' import * as log from 'electron-log' import { WindowContext } from './types' export function createWindow(windowContext: WindowContext) { // Create the browser window. const newWindow = new BrowserWindow(newWindowConfig) newWindow.loadURL( dev ? 'http://localhost:4040' // Dev server ran by react-scripts : `file://${path.join(__dirname, '..', '/dist/index.html')}`, // Bundled application ) if (dev) { // If dev mode install react and redux extension // Also open the devtools const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS, } = require('electron-devtools-installer') installExtension(REACT_DEVELOPER_TOOLS) .then(name => log.info(`Added Extension: ${name}`)) .catch(err => log.info('An error occurred: ', err)) installExtension(REDUX_DEVTOOLS) .then(name => log.info(`Added Extension: ${name}`)) .catch(err => log.info('An error occurred: ', err)) // newWindow.webContents.openDevTools() } windowContext.windows.add(newWindow) windowContext.windowById.set(newWindow.id, newWindow) // Emitted when the window is closed. const id = newWindow.id newWindow.on('closed', () => { if (process.platform !== 'darwin' && windowContext.windows.size === 0) { app.quit() } windowContext.windows.delete(newWindow) windowContext.windowById.delete(id) windowContext.windowByPath.forEach((window, cwd) => { if (window === newWindow) { windowContext.windowByPath.delete(cwd) } }) }) // electronLocalShortcut.register(newWindow, 'Cmd+Shift+]', () => { // send('Tab', 'Next') // }) // electronLocalShortcut.register(newWindow, 'Cmd+Shift+[', () => { // send('Tab', 'Prev') // }) windowContext.readyWindowsPromises[newWindow.id] = new Promise(resolve => ipcMain.once('ready', resolve), ) return newWindow } ================================================ FILE: packages/graphql-playground-electron/src/main/index.ts ================================================ // TODO enable tslint // /* tslint:disable */ import { app, BrowserWindow, globalShortcut, ipcMain, protocol, Menu, } from 'electron' import * as queryString from 'query-string' import * as fs from 'fs' import * as log from 'electron-log' import { buildTemplate } from './menu' import { createWindow } from './createWindow' import { WindowContext } from './types' import { startUpdates } from './updates' import * as Raven from 'raven' Raven.config( 'https://cce868d3730e473e9350f1436da7d908:ff5d65389e404b28b5af1d97d8024414@sentry.io/297194', ).install() log.transports.file.level = 'info' log.transports.console.level = 'debug' const windowContext: WindowContext = { readyWindowsPromises: {}, windows: new Set(), windowById: new Map(), windowByPath: new Map(), } let appResolve const appPromise = new Promise(resolve => (appResolve = resolve)) app.setAsDefaultProtocolClient('graphql-playground') app.on('open-url', (event, url) => { event.preventDefault() const cutIndex = url.indexOf('//') const query = url.slice(cutIndex + 2) const input = queryString.parse(query) let env if (input.env) { try { env = JSON.parse(input.env) } catch (e) { // could console.log('could not get env') } } if (input.envPath) { try { env = JSON.parse(fs.readFileSync(input.envPath, 'utf-8')) fs.unlinkSync(input.envPath) } catch (e) { // } } const msg = JSON.stringify({ ...input, env, }) forceSend('OpenUrl', msg, input.cwd) }) app.on('open-file', (event, filePath) => { event.preventDefault() forceSend('OpenSelectedFile', filePath, filePath) }) ipcMain.on('online-status-changed', (event, status) => { process.env.CONNECTION = status }) ipcMain.on('async', (event, arg) => { const focusedWindow = BrowserWindow.getFocusedWindow() if (focusedWindow) { focusedWindow.close() } }) ipcMain.on('cwd', (event, msg) => { const { cwd, id } = JSON.parse(msg) const window = windowContext.windowById.get(id) windowContext.windowByPath.set(cwd, window) }) ipcMain.on('CloseWindow', (event, msg) => { const { id } = JSON.parse(msg) const window = windowContext.windowById.get(id) window.close() }) // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', () => { createWindow(windowContext) startUpdates() const menu = Menu.buildFromTemplate(buildTemplate(windowContext)) Menu.setApplicationMenu(menu) ipcMain.on('get-file-data', event => { log.info('get-file-data', event) // this.fileAdded(event) }) ipcMain.on('load-file-content', (event, filePath) => { log.info('load-file-content', event, filePath) }) protocol.registerFileProtocol('file:', (request, filePath) => { log.info('file:', request, filePath) }) if (appResolve) { appResolve() } }) // Quit when all windows are closed. app.on('window-all-closed', () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow(windowContext) } }) app.on('will-quit', () => { // Unregister all shortcuts. globalShortcut.unregisterAll() }) async function forceSend(channel: string, arg: string, byPath?: string) { await appPromise const currentWindows = BrowserWindow.getAllWindows() let window if (byPath) { window = windowContext.windowByPath.get(byPath) if ( !window && currentWindows.length === 1 && windowContext.windowByPath.size === 0 ) { window = currentWindows[0] } } else { window = currentWindows[0] } let destroyed = null try { destroyed = window ? window.isDestroyed() : null } catch (e) { // } if (!window || destroyed) { window = createWindow(windowContext) } await windowContext.readyWindowsPromises[window.id] window.webContents.send(channel, arg) } ================================================ FILE: packages/graphql-playground-electron/src/main/menu.ts ================================================ import { MenuItemConstructorOptions, BrowserWindow, app, autoUpdater, } from 'electron' import * as log from 'electron-log' import { WindowContext } from './types' import { createWindow } from './createWindow' import { notify } from './notify' export const buildTemplate = ( windowContext: WindowContext, ): MenuItemConstructorOptions[] => [ { label: 'Application', submenu: [ { label: 'About GraphQL Playground', selector: 'orderFrontStandardAboutPanel:', } as MenuItemConstructorOptions, { label: 'Check For Updates', click: () => { autoUpdater.once('update-not-available', () => { notify({ title: 'GraphQL Playground Updates', body: 'Already up to date.', }) }) autoUpdater.checkForUpdates() }, }, { type: 'separator' }, { label: 'Settings', accelerator: 'CmdOrCtrl+,', click: () => send('Tab', 'Settings'), }, { type: 'separator' }, { label: 'Hide GraphQL Playground', accelerator: 'Cmd+H', role: 'hide', }, { label: 'Hide Others', accelerator: 'Option+Cmd+H', role: 'hideOthers', }, { label: 'Show All', role: 'unhide', }, { type: 'separator', visible: process.platform === 'darwin' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit(), }, ], }, { label: 'Workspace', submenu: [ { label: 'New Workspace', accelerator: 'CmdOrCtrl+N', click: () => { createWindow(windowContext) }, }, { label: 'New Tab', accelerator: 'CmdOrCtrl+T', click: () => send('Tab', 'New'), }, { type: 'separator' }, { label: 'Close Workspace', accelerator: 'CmdOrCtrl+Shift+W', role: 'close', }, { label: 'Close Tab', accelerator: 'CmdOrCtrl+W', click: () => send('Tab', 'Close'), }, { label: 'Open File', accelerator: 'CmdOrCtrl+O', click: () => send('File', 'Open'), }, { label: 'Save File', accelerator: 'CmdOrCtrl+S', click: () => send('File', 'Save'), }, { label: 'Reload Schema', accelerator: 'CmdOrCtrl+R', click: () => send('Tab', 'ReloadSchema'), }, ], }, { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:', } as MenuItemConstructorOptions, { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:', } as MenuItemConstructorOptions, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:', } as MenuItemConstructorOptions, { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:', } as MenuItemConstructorOptions, { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:', } as MenuItemConstructorOptions, { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:', } as MenuItemConstructorOptions, ], }, { label: 'Window', submenu: [ { label: 'Next Tab', accelerator: 'CmdOrCtrl+Alt+Right', click: () => send('Tab', 'Next'), }, { label: 'Previous Tab', accelerator: 'CmdOrCtrl+Alt+Left', click: () => send('Tab', 'Next'), }, { label: 'Minimize', accelerator: 'CmdOrCtrl+M', click: () => { const focusedWindow = BrowserWindow.getFocusedWindow() if (focusedWindow) { focusedWindow.minimize() } }, }, { type: 'separator' }, { label: 'Toggle Developer Tools', role: 'toggledevtools' }, ], }, ] function send(channel: string, arg: string) { const focusedWindow = BrowserWindow.getFocusedWindow() if (focusedWindow) { log.info('sending to focused window', channel, arg) focusedWindow.webContents.send(channel, arg) } else { log.info('no focused window') } } ================================================ FILE: packages/graphql-playground-electron/src/main/notify.ts ================================================ import { shell, Notification } from 'electron' interface NotificationOptions { title: string body: string url?: string onClick?: () => void } export const notify = ({ title, body, url, onClick }: NotificationOptions) => { const notification = new Notification({ title, body, silent: true, }) if (url || onClick) { notification.on('click', () => { if (onClick) { return onClick() } shell.openExternal(url) }) } notification.show() console.log(`[Notification] ${title}: ${body}`) } ================================================ FILE: packages/graphql-playground-electron/src/main/types.ts ================================================ import { BrowserWindow } from 'electron' export interface WindowContext { readyWindowsPromises: { [windowId: number]: Promise } windows: Set windowById: Map windowByPath: Map } ================================================ FILE: packages/graphql-playground-electron/src/main/updates.ts ================================================ import { app, autoUpdater, ipcMain, BrowserWindow, dialog } from 'electron' import ms = require('ms') import dev = require('electron-is-dev') import * as log from 'electron-log' import { notify } from './notify' export const startUpdates = () => { if (!dev) { startAppUpdates() } } const setUpdateURL = async () => { let betaUpdates = false await new Promise(resolve => { ipcMain.once('SettingsResponse', (event, settingsString) => { log.info('settings', settingsString) betaUpdates = getBetaUpdates(settingsString) resolve() }) send('SettingsRequest', '') }) const channel = betaUpdates ? 'kygnjrcroc' : 'ppbimurjwk' const server = `https://hazel-server-${channel}.now.sh/update` autoUpdater.setFeedURL({url: `${server}/${process.platform}/${app.getVersion()}`}) } const checkForUpdates = async () => { if (process.env.CONNECTION === 'offline') { // Try again after half an hour setTimeout(checkForUpdates, ms('30m')) return } // Ensure we're pulling from the correct channel try { await setUpdateURL() } catch (err) { log.error(err) // Retry later if setting the update URL failed return } // Then ask the server for updates autoUpdater.checkForUpdates() } const startAppUpdates = () => { autoUpdater.on('error', error => { log.error(error) setTimeout(checkForUpdates, ms('15m')) }) autoUpdater.on('checking-for-update', () => { log.info('Checking for app updates...') }) autoUpdater.on('update-downloaded', () => { log.info('Update downloaded') const buttonIndex = dialog.showMessageBox({ message: `Update downloaded. Install now?`, buttons: ['Install Update & Restart', 'Later'], }) if (buttonIndex === 0) { autoUpdater.quitAndInstall() app.quit() } }) autoUpdater.on('update-available', () => { log.info('Found update for the app! Downloading...') notify({ title: 'GraphQL Playground Updates', body: 'Update available. Downloading...', }) }) autoUpdater.on('update-not-available', () => { log.info('No updates found. Checking again in 5 minutes...') setTimeout(checkForUpdates, ms('5m')) }) setTimeout(checkForUpdates, ms('10s')) } function send(channel: string, arg: string) { const window = BrowserWindow.getAllWindows()[0] if (window) { log.info('sending to open window', channel, arg) window.webContents.send(channel, arg) } else { log.info('no opened window') } } function getBetaUpdates(settingsString: string | undefined): boolean { try { const settings = JSON.parse(settingsString) return !!settings['general.betaUpdates'] } catch (e) { // } return false } ================================================ FILE: packages/graphql-playground-electron/src/renderer/components/App.tsx ================================================ import * as React from 'react' import 'core-js/stable' import 'regenerator-runtime/runtime' import { remote, ipcRenderer, webFrame } from 'electron' import * as cx from 'classnames' import { Playground as IPlayground } from 'graphql-playground-react/lib/components/Playground' import { merge, set } from 'immutable' import Playground, { openSettingsTab, selectNextTab, selectPrevTab, closeSelectedTab, refetchSchema, newSession, store, getSessionsState, saveFile, newFileTab, getEndpoint, selectAppHistoryItem, AppHistoryItem, } from 'graphql-playground-react' import { getGraphQLConfig, findGraphQLConfigFile, GraphQLConfigData, resolveEnvsInValues, } from 'graphql-config' import { createRemoteWindow } from '../../shared/utils' import InitialView from './InitialView/InitialView' import * as minimist from 'minimist' import * as fs from 'fs' import * as path from 'path' import * as os from 'os' import * as yaml from 'js-yaml' import * as findUp from 'find-up' import { patchEndpointsToConfigData as patchPrismaEndpointsToConfigData, makeConfigFromPath, } from 'graphql-config-extension-prisma' import { patchEndpointsToConfigData } from 'graphql-config-extension-graphcool' import { connect } from 'react-redux' import { errify } from '../utils/errify' import { createStructuredSelector } from 'reselect' import * as dotenv from 'dotenv' // import { PermissionSession } from 'graphql-playground/lib/types' const { dialog } = remote // declare var p: IPlayground interface State { endpoint?: string openTooltipTheme: boolean theme: string shareUrl?: string loading: boolean session?: any platformToken?: string configString?: string configPath?: string folderName?: string env?: any config?: GraphQLConfigData } ipcRenderer.on('SettingsRequest', () => { ipcRenderer.send('SettingsResponse', localStorage.getItem('settings')) }) const events: any[] = [] ipcRenderer.on('OpenSelectedFile', pushSelectedFile) ipcRenderer.on('OpenUrl', pushOpenUrl) function pushSelectedFile() { events.push({ type: 'OpenSelectedFile', args: arguments, }) } function pushOpenUrl() { events.push({ type: 'OpenUrl', args: arguments, }) } interface ReduxProps { openSettingsTab: () => void selectNextTab: () => void selectPrevTab: () => void closeSelectedTab: () => void refetchSchema: () => void newSession: (endpoint: string) => void saveFile: () => void newFileTab: (fileName: string, filePath: string, file: string) => void selectAppHistoryItem: (item: AppHistoryItem) => void endpoint: string } class App extends React.Component { private playground: IPlayground constructor(props) { super(props) const { endpoint, platformToken } = this.getArgs() this.state = { openTooltipTheme: false, theme: 'dark', endpoint, platformToken, loading: false, } ; (global as any).a = this ; (global as any).r = remote } fileAdded = event => { // console.log(event) } getArgs(): any { const argv = remote.process.argv const args = minimist(argv.slice(1)) return { endpoint: args.endpoint, subscriptionsEndpoint: args['subscriptions-endpoint'], platformToken: args['platform-token'] || localStorage.getItem('platformToken'), env: args.env, } } handleSelectEndpoint = (endpoint: string) => { this.setState({ endpoint } as State) } handleSelectFolder = async (folderPath: string) => { try { // Get config from folderPath const envPath = path.join(folderPath, '.env') let env = process.env if (fs.existsSync(envPath)) { const envString = fs.readFileSync(envPath) const localEnv = dotenv.parse(envString) if (localEnv) { env = merge(env, localEnv) } } const configPath = findGraphQLConfigFile(folderPath) const configString = fs.readFileSync(configPath, 'utf-8') /* tslint:disable-next-line */ // if (configString.includes('${env:')) { // errify(`You opened a .graphqlconfig file that includes environment variables. // In order to use environment variables in the Playground, please start it from the graphql cli. Install with // npm install -g graphql-cli // Then open the graphql config with: // cd ${folderPath}; graphql playground`) // } const configDir = path.dirname(configPath) let config try { config = await patchEndpointsToConfigData( resolveEnvsInValues(getGraphQLConfig(configDir).config, env), configDir, env, ) config = await patchPrismaEndpointsToConfigData( resolveEnvsInValues(getGraphQLConfig(configDir).config, env), configDir, env, ) } catch (e) { const ymlPath = path.join(configDir, 'prisma.yml') if (!fs.existsSync(ymlPath)) { throw e } config = await makeConfigFromPath(configDir) } ipcRenderer.send( 'cwd', JSON.stringify({ cwd: configDir, id: remote.getCurrentWindow().id }), ) const state = { configString, configPath, config, folderName: path.basename(folderPath), env, } this.setState(state as State) this.props.selectAppHistoryItem(merge(state, { type: 'local', path: configPath, }) as any) } catch (error) { errify(error) } } handleOpenNewWindow = () => { createRemoteWindow() } openSettingsTab = () => { this.props.openSettingsTab() } nextTab = () => { this.props.selectNextTab() } prevTab = () => { this.props.selectPrevTab() } newTab = () => { this.props.newSession(this.props.endpoint) } closeTab = () => { this.props.closeSelectedTab() } reloadSchema = () => { this.props.refetchSchema() } componentDidMount() { ipcRenderer.removeListener('OpenUrl', pushOpenUrl) ipcRenderer.removeListener('OpenSelectedFile', pushSelectedFile) ipcRenderer.on('Tab', this.readTabMessage) ipcRenderer.on('File', this.readFileMessage) ipcRenderer.on('OpenSelectedFile', this.readOpenSelectedFileMessage) ipcRenderer.on('OpenUrl', this.handleUrl) window.addEventListener('keydown', this.handleKeyDown, true) this.consumeEvents() ipcRenderer.send('ready', '') // if ( // !this.state.endpoint && // !this.state.config && // !this.state.configPath && // !this.state.configString // ) { // const workspace = this.deserializeWorkspace() // if (workspace) { // this.setState(workspace) // } // } } consumeEvents() { while (events.length > 0) { const event = events.shift() switch (event.type) { case 'OpenSelectedFile': return this.readOpenSelectedFileMessage.call(this, ...event.args) case 'OpenUrl': return this.handleUrl.call(this, ...event.args) } } } componentWillUnmount() { ipcRenderer.removeListener('Tab', this.readTabMessage) ipcRenderer.removeListener('File', this.readFileMessage) ipcRenderer.removeListener( 'OpenSelectedFile', this.readOpenSelectedFileMessage, ) ipcRenderer.removeListener('OpenUrl', this.handleUrl) window.removeEventListener('keydown', this.handleKeyDown, true) } handleKeyDown = e => { if (e.key === '{' && e.metaKey) { this.prevTab() } else if (e.key === '}' && e.metaKey) { this.nextTab() } else if (e.key >= 1 && e.key <= 9 && e.metaKey) { this.playground.switchTab(e.key) } else if (e.key === '=' && e.metaKey) { const zoom = webFrame.getZoomFactor() webFrame.setZoomFactor(zoom + 0.1) } else if (e.key === '-' && e.metaKey) { const zoom = webFrame.getZoomFactor() webFrame.setZoomFactor(zoom - 0.1) } } handleUrl = async (event, msg) => { const input = JSON.parse(msg) const endpoint = input.endpoint let configString let folderName let configPath const platformToken = input.platformToken let config if (input.cwd) { // use the endpoint as an alternative, only log the error try { configPath = findUp.sync(['.graphqlconfig', '.graphqlconfig.yml'], { cwd: input.cwd, }) configString = configPath ? fs.readFileSync(configPath, 'utf-8') : undefined folderName = configPath ? path.basename(path.dirname(configPath)) : undefined try { const rawConfig = getGraphQLConfig(input.cwd).config const resolvedConfig = resolveEnvsInValues(rawConfig, input.env) config = await patchEndpointsToConfigData( resolvedConfig, input.cwd, input.env, ) config = await patchPrismaEndpointsToConfigData( resolvedConfig, input.cwd, input.env, ) } catch (e) { const ymlPath = path.join(input.cwd, 'prisma.yml') if (!fs.existsSync(ymlPath)) { throw e } config = (await makeConfigFromPath(input.cwd, input.env)).config configPath = ymlPath folderName = path.basename(input.cwd) configString = JSON.stringify(config) } if (!this.configContainsEndpoints(config)) { const graphcoolNote = configString.includes('graphcool') ? 'Please make sure to add stages to your graphcool.yml' : '' errify( `${configPath} does not include any endpoints. ${graphcoolNote}`, ) return } } catch (e) { console.error(e) errify(e) } } ipcRenderer.send( 'cwd', JSON.stringify({ cwd: input.cwd, id: remote.getCurrentWindow().id }), ) const state = { configString, folderName, configPath, env: input.env, endpoint, config, platformToken, } this.props.selectAppHistoryItem(merge(state, { type: 'endpoint', path: configPath, }) as any) this.setState(state) } configContainsEndpoints(config: GraphQLConfigData): boolean { if ( Object.keys((config.extensions && config.extensions.endpoints) || {}) .length > 0 ) { return true } return Object.keys(config.projects).reduce((acc, curr) => { const project = config.projects[curr] if ( project.extensions && project.extensions.endpoints && Object.keys(project.extensions.endpoints).length > 0 ) { return true } return acc }, false) } readFileMessage = (event, message) => { switch (message) { case 'Open': this.showOpenDialog() break case 'Save': this.getSaveFileName() break } } readOpenSelectedFileMessage = (event, selectedFile) => { if (selectedFile) { this.openFile(selectedFile) } } async openFile(fileName: string) { const file = fs.readFileSync(fileName, 'utf-8') if (!this.playground) { this.handleSelectFolder(path.dirname(fileName)) } while (!this.playground) { await new Promise(r => setTimeout(r, 200)) } await new Promise(r => setTimeout(r, 200)) this.props.newFileTab(path.basename(fileName), fileName, file) } showOpenDialog() { dialog.showOpenDialog( { title: 'Choose a .graphql file to edit', properties: ['openFile'], // filters: [{ // name: '*', // extensions: ['graphql'] // }] }, fileNames => { if (fileNames && fileNames.length > 0) { const file = fileNames[0] this.openFile(file) } }, ) } getSaveFileName(): Promise { return new Promise((resolve, reject) => { // save current tab if (this.playground) { const session = getSessionsState(store.getState()) if (session.isConfigTab) { this.playground.handleSaveConfig() } if (session.isSettingsTab) { this.playground.handleSaveSettings() } if (session.isFile && session.filePath) { // TODO // dialog.showSaveDialog( // { // title: 'Save Permission Query', // filters: [ // { // name: 'Permission File', // extensions: ['graphql'], // }, // ], // }, // (fileName: any) => { // resolve(fileName) // }, // ) this.props.saveFile() fs.writeFileSync(session.filePath, session.file) } this.playground.handleSaveConfig() } }) } saveConfig = (configString: string) => { fs.writeFileSync(this.state.configPath, configString) this.setState({ configString }) } // async saveFile() { // const session = this.playground.state.sessions[ // this.playground.state.selectedSessionIndex // ] // ;(this.playground as any).setValueInSession(session.id, 'hasChanged', false) // const fileName = // (session as any).absolutePath || (await this.getSaveFileName()) // // if (!(session as any).absolutePath) { // ;(this.playground as any).setValueInSession( // session.id, // 'name', // path.basename(fileName), // ) // ;(this.playground as any).setValueInSession( // session.id, // 'absolutePath', // fileName, // ) // // } // const query = session.query // fs.writeFileSync(fileName, query) // } getGraphcoolYml(from: string): { ymlPath: string; yml: any } | null { const ymlPath = findUp.sync('graphcool.yml', { cwd: from }) if (ymlPath) { const file = fs.readFileSync(ymlPath) try { return { yml: yaml.safeLoad(file), ymlPath, } } catch (e) { // console.error(e) } } return null } getGraphcoolRc(): any | null { const graphcoolRc = path.join(os.homedir(), '.graphcoolrc') if (fs.existsSync(graphcoolRc)) { const file = fs.readFileSync(graphcoolRc) try { return yaml.safeLoad(file) } catch (e) { // console.error(e) } } return null } readTabMessage = (error, message) => { switch (message) { case 'Next': this.nextTab() break case 'Prev': this.prevTab() break case 'New': this.newTab() break case 'Close': if (!this.state.endpoint && !this.state.config) { ipcRenderer.send( 'CloseWindow', JSON.stringify({ id: remote.getCurrentWindow().id }), ) } else { this.closeTab() } break case 'Settings': this.openSettingsTab() break case 'ReloadSchema': this.reloadSchema() break } } render() { const { theme, endpoint, platformToken, configString, config } = this.state return (
{(endpoint || configString) && (
)}
) } private handleSelectItem = ({ type, ...item }) => { this.setState(item as any) this.props.selectAppHistoryItem(set(item, 'lastOpened', new Date()) as any) } private setRef = ref => { this.playground = ref } } const mapStateToProps = createStructuredSelector({ endpoint: getEndpoint, }) export default connect( mapStateToProps, { openSettingsTab, selectNextTab, selectPrevTab, closeSelectedTab, refetchSchema, newSession, saveFile, newFileTab, selectAppHistoryItem, }, )(App) ================================================ FILE: packages/graphql-playground-electron/src/renderer/components/InitialView/InitialView.tsx ================================================ import * as React from 'react' import * as isURL from 'validator/lib/isURL' import { remote } from 'electron' import { existsSync } from 'fs' import { resolve } from 'path' import { Icon, $v } from 'graphcool-styles' import Modal from 'graphcool-ui/lib/Modal' import { connect } from 'react-redux' import * as format from 'date-fns/format' import Toggle from './Toggle' import { examples } from './data' import { getAppHistory, AppHistoryItem } from 'graphql-playground-react' import { createStructuredSelector, createSelector } from 'reselect' import { OrderedMap } from 'immutable' interface StateFromProps { history: OrderedMap } interface State { endpoint: string selectedMode: string } export interface Props { isOpen: boolean onSelectEndpoint: (endpoint: string) => void onSelectFolder: (config: string) => void selectHistory: (history: AppHistoryItem) => any } const modalStyle = { overlay: { zIndex: 20, backgroundColor: 'rgba(15,32,46,.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', '-webkit-app-region': 'drag', }, content: { position: 'relative', width: 976, height: 'auto', top: 'initial', left: 'initial', right: 'initial', bottom: 'initial', borderRadius: 2, padding: 0, border: 'none', background: 'none', boxShadow: '0 1px 7px rgba(0,0,0,.2)', }, } class InitialView extends React.Component { state = { endpoint: '', selectedMode: 'local', } handleRequestClose = () => null handleSubmit = e => { e.preventDefault() if (isURL(this.state.endpoint, { require_tld: false })) { this.props.selectHistory( new AppHistoryItem({ type: 'endpoint', path: this.state.endpoint, }), ) this.props.onSelectEndpoint(this.state.endpoint) } else { alert('Endpoint is not a valid url') } } handleClickLocal = () => { const paths = remote.dialog.showOpenDialog({ properties: ['openDirectory'], }) if (paths && paths[0]) { const path = paths[0] // Check if there is a .graphqlconfig file in the folder if ( !existsSync(resolve(path, '.graphqlconfig')) && !existsSync(resolve(path, '.graphqlconfig.yml')) && !existsSync(resolve(path, '.graphqlconfig.yaml')) ) { alert('No .graphqlconfig found in this folder') return } this.setState({ endpoint: path } as State) this.props.onSelectFolder(path) } } handleChangeEndpoint = e => { this.setState({ endpoint: e.target.value } as State) } handleChangeMode = selectedMode => { this.setState({ selectedMode } as State) } handleClickHistory = (history: AppHistoryItem) => { this.props.selectHistory(history) } render() { const { isOpen, history } = this.props const { endpoint, selectedMode } = this.state const choicesMode = ['local', 'url endpoint'] const items = history.toJS() return (
{history.size > 0 ? (
RECENT
{Object.keys(items) .reverse() .map((key) => { const data = items[key] const name = data.folderName || data.endpoint || data.path return (
this.handleClickHistory(data)} >
{name}
Last opened{' '} {format(data.lastOpened, 'DD.MM.YYYY')}
) })}
) : (
EXAMPLES
{examples.map((example) => (
this.props.onSelectEndpoint(example.endpoint) } >
{example.name}
{example.endpoint}
))}
)}

New Workspace

Either load a local repository with a .graphqlconfig file, or just open a HTTP endpoint

{selectedMode === 'url endpoint' && (
)} {selectedMode === 'local' && (
)}
) } } const itemsSelector = createSelector([getAppHistory], state => state.items) const mapStateToProps = createStructuredSelector({ history: itemsSelector, }) export default connect(mapStateToProps)(InitialView as any) as any ================================================ FILE: packages/graphql-playground-electron/src/renderer/components/InitialView/Toggle.tsx ================================================ import * as React from 'react' import * as cn from 'classnames' export interface ToggleProps { choices: string[] onChange: (choice: string, i: number) => void activeChoice: string } /* tslint:disable */ const Toggle = ({ choices, onChange, activeChoice }: ToggleProps) => { return (
{choices.map((choice, i) => (
onChange(choice, i)} > {choice}
))}
) } export default Toggle ================================================ FILE: packages/graphql-playground-electron/src/renderer/components/InitialView/data.ts ================================================ export const examples = [ { name: 'Movie Database', endpoint: 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr', }, { name: 'Instagram', endpoint: 'https://api.graph.cool/simple/v1/cixne4sn40c7m0122h8fabni1', }, { name: 'Pokemon', endpoint: 'https://graphql-pokemon.now.sh', }, ] ================================================ FILE: packages/graphql-playground-electron/src/renderer/components/Loading.tsx ================================================ import * as React from 'react' interface Props { color?: string width?: number height?: number className?: string } export default class Loading extends React.Component { render() { const width = this.props.width || 30 const height = this.props.height || 30 const backgroundColor = this.props.color || '#000' return (
) } } ================================================ FILE: packages/graphql-playground-electron/src/renderer/components/Root.tsx ================================================ import * as React from 'react' import App from './App' import { store } from 'graphql-playground-react' import { Provider } from 'react-redux' export default class Root extends React.Component { render() { return ( ) } } ================================================ FILE: packages/graphql-playground-electron/src/renderer/index.css ================================================ body { overflow: hidden; } ================================================ FILE: packages/graphql-playground-electron/src/renderer/index.html ================================================ GraphQL Playground
Loading GraphQL Playground
================================================ FILE: packages/graphql-playground-electron/src/renderer/index.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom' import Root from './components/Root' import 'graphcool-styles/dist/styles.css' import './index.css' ReactDOM.render(, document.getElementById('root')) ================================================ FILE: packages/graphql-playground-electron/src/renderer/redux/actions/history.ts ================================================ export type SELECT_HISTORY = 'select history' export const SELECT_HISTORY: SELECT_HISTORY = 'select history' export interface SelectHistoryAction { type: SELECT_HISTORY history: History } export interface History { type: 'local' | 'endpoint' path: string lastOpened?: Date } export const selectHistory = (history: History): SelectHistoryAction => ({ type: SELECT_HISTORY, history, }) ================================================ FILE: packages/graphql-playground-electron/src/renderer/redux/createStore.ts ================================================ import { compose, createStore } from 'redux' import persistState, { mergePersistedState } from 'redux-localstorage' import filter from 'redux-localstorage-filter' import * as adapter from 'redux-localstorage/lib/adapters/localStorage' import { merge } from 'lodash' import combinedReducers from './reducers' let localStorage: any = null if (typeof window !== 'undefined') { localStorage = window.localStorage } else { localStorage = { clearItem: () => null, getItem: () => null, setItem: () => null, } } const composeEnhancers = compose const storage = composeEnhancers(filter(['history.history']))( adapter(localStorage), ) const reducer = composeEnhancers( mergePersistedState((initialState, persistedState) => { return merge({}, initialState, persistedState) }), )(combinedReducers) const enhancer = composeEnhancers( persistState(storage, 'graphql-playground-electron'), ) const functions = [enhancer] // // if (window.__REDUX_DEVTOOLS_EXTENSION__) { // functions.push(window.__REDUX_DEVTOOLS_EXTENSION__()) // } export default () => createStore(reducer, composeEnhancers.apply(null, functions)) ================================================ FILE: packages/graphql-playground-electron/src/renderer/redux/reducers/history.ts ================================================ import { SELECT_HISTORY, SelectHistoryAction, History, } from '../actions/history' export type HistoryAction = SelectHistoryAction export interface State { readonly history: History[] } const defaultState: State = { history: [], } export default function historyReducer( state: State = defaultState, action: HistoryAction, ): State { switch (action.type) { case SELECT_HISTORY: const { history } = action history.lastOpened = new Date() // See if already in list const index = state.history.findIndex(data => data.path === history.path) let newHistory = state.history if (index !== -1) { newHistory[index] = history } else { newHistory = [...state.history, history] } // Sort by date newHistory.sort( (a, b) => (new Date(a.lastOpened) < new Date(b.lastOpened) ? -1 : 1), ) return { ...state, history: newHistory, } } return state } ================================================ FILE: packages/graphql-playground-electron/src/renderer/redux/reducers/index.ts ================================================ import { combineReducers } from 'redux' import graphiqlDocs from 'graphql-playground-react/lib/reducers/graphiql-docs' import history from './history' const combinedReducers = combineReducers({ graphiqlDocs, history, }) export default combinedReducers ================================================ FILE: packages/graphql-playground-electron/src/renderer/utils/errify.ts ================================================ import swal from 'sweetalert2' export function errify(error: Error | string) { const message = typeof error === 'string' ? error : error.message swal({ title: 'Error', text: message, type: 'error', confirmButtonText: 'Ok', }) } ================================================ FILE: packages/graphql-playground-electron/src/shared/utils.ts ================================================ import { remote } from 'electron' import * as dev from 'electron-is-dev' import * as path from 'path' export const newWindowConfig: Electron.BrowserWindowConstructorOptions = { title: 'GraphQL Playground', width: 1200, height: 800, titleBarStyle: 'hiddenInset', icon: path.join(__dirname, '../static/icons/icon.icns'), backgroundColor: '#0F202D', } export function createRemoteWindow() { const win = new remote.BrowserWindow(newWindowConfig) const url = dev ? 'http://localhost:4040' : `file://${path.join(__dirname, '..', '/dist/index.html')}` win.loadURL(url) } ================================================ FILE: packages/graphql-playground-electron/tsconfig.json ================================================ { "compilerOptions": { "removeComments": true, "module": "commonjs", "jsx": "react", "sourceMap": true, "target": "esnext", "moduleResolution": "node", "allowJs": true, "noImplicitReturns": true, "noImplicitThis": true, "noUnusedLocals": true, "lib": ["es2015", "dom"], "outDir": "lib", "baseUrl": "./", "skipDefaultLibCheck": true, "skipLibCheck": true }, "exclude": ["node_modules", "dist", "lib"], "include": ["typings/custom.d.ts", "typings/styled-jsx.d.ts"], "files": ["src/renderer/index.tsx", "src/main/index.ts"] } ================================================ FILE: packages/graphql-playground-electron/tslint.json ================================================ { "extends": [ "tslint-graphcool-frontend" ], "rules": { "no-console": false, "no-implicit-dependencies": false, "no-submodule-imports": false, "jsx-no-lambda": false } } ================================================ FILE: packages/graphql-playground-electron/typings/custom.d.ts ================================================ interface Window { GraphQLPlayground: any __REDUX_DEVTOOLS_EXTENSION__: any __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any } ================================================ FILE: packages/graphql-playground-electron/typings/styled-jsx.d.ts ================================================ import 'react' declare module 'react' { interface HTMLProps { jsx?: boolean global?: boolean } } declare module 'react' { interface StyleHTMLAttributes extends React.HTMLAttributes { jsx?: boolean global?: boolean } } ================================================ FILE: packages/graphql-playground-electron/webpack.config.build.js ================================================ const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const cssnano = require('cssnano') const path = require('path') const config = require('./webpack.config') const HappyPack = require('happypack') const os = require('os') const fs = require('fs') const UglifyJSParallelPlugin = require('webpack-uglify-parallel') const { renderPlaygroundPage } = require('graphql-playground-html') const appEntrypoint = 'src/renderer/index.html' // Create the playground entry point if it doesn't exist if (!fs.existsSync(appEntrypoint)) { fs.writeFileSync(appEntrypoint, renderPlaygroundPage({ env: 'react' })) } module.exports = { devtool: 'source-map', mode: 'production', target: 'electron-renderer', entry: { app: ['./src/renderer'], }, output: { path: __dirname + '/dist', filename: '[name].[hash].js', sourceMapFilename: '[file].map', publicPath: './', }, node: { __dirname: false, __filename: false, }, module: { rules: [ { enforce: 'pre', test: /\.ts(x?)$/, loader: 'tslint-loader', exclude: /node_modules/, }, { test: /\.css$/, loader: 'style-loader!css-loader', }, { test: /\.scss$/, loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader!sass-loader', }, { test: /\.ts(x?)$/, include: __dirname + '/src', use: [ { loader: 'happypack/loader?id=babel', }, { loader: 'happypack/loader?id=ts', }, ], }, { test: /\.js$/, loader: 'happypack/loader?id=babel', include: __dirname + '/src', }, { test: /\.mp3$/, loader: 'file-loader', }, { test: /icons\/.*\.svg$/, loader: 'raw-loader!svgo-loader?{"plugins":[{"removeStyleElement":true}]}', }, { test: /graphics\/.*\.svg$/, loader: 'file-loader', }, { test: /.*\.(png|gif)$/, loader: 'file-loader', }, ], }, plugins: [ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production'), }, __EXAMPLE_ADDR__: '"https://dynamic-resources.graph.cool"', }), new HtmlWebpackPlugin({ favicon: 'static/favicon.png', template: appEntrypoint, }), new webpack.optimize.OccurrenceOrderPlugin(), /* new UglifyJSParallelPlugin({ workers: os.cpus().length, compress: { unused: true, dead_code: true, warnings: false, }, sourceMap: false, mangle: false, }), */ // https://github.com/graphql/graphql-language-service/issues/111 new webpack.ContextReplacementPlugin( /graphql-language-service-interface[\/\\]dist/, /\.js$/, ), new webpack.NormalModuleReplacementPlugin(/\/iconv-loader$/, 'node-noop'), // new webpack.optimize.CommonsChunkPlugin('vendor'), new webpack.optimize.ModuleConcatenationPlugin(), new webpack.LoaderOptionsPlugin({ options: { postcss: [ cssnano({ autoprefixer: { add: true, remove: true, browsers: ['last 2 versions'], }, discardComments: { removeAll: true, }, safe: true, }), ], svgo: { plugins: [{ removeStyleElement: true }], }, }, }), new HappyPack({ id: 'ts', threads: 2, loaders: ['ts-loader?' + JSON.stringify({ happyPackMode: true })], }), new HappyPack({ id: 'babel', threads: 2, loaders: ['babel-loader'], }), ], resolve: { modules: [path.resolve('./src'), 'node_modules'], extensions: ['.mjs', '.js', '.ts', '.tsx'], }, } ================================================ FILE: packages/graphql-playground-electron/webpack.config.js ================================================ const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') .BundleAnalyzerPlugin const path = require('path') const fs = require('fs') const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') const HappyPack = require('happypack') const { renderPlaygroundPage } = require('graphql-playground-html') const appEntrypoint = 'src/renderer/index.html' // Create the playground entry point if it doesn't exist if (!fs.existsSync(appEntrypoint)) { fs.writeFileSync(appEntrypoint, renderPlaygroundPage({ env: 'react' })) } module.exports = { devtool: 'cheap-module-eval-source-map', mode: 'development', entry: './src/renderer', target: 'electron-renderer', output: { filename: '[name].[hash].js', publicPath: '/' }, module: { rules: [ { enforce: 'pre', test: /\.ts(x?)$/, loader: 'tslint-loader', exclude: /node_modules/, }, { test: /\.css$/, loader: 'style-loader!css-loader', }, { test: /\.ts(x?)$/, include: [__dirname + '/src'], use: [ { loader: 'happypack/loader?id=babel', }, { loader: 'happypack/loader?id=ts', }, ], }, { test: /\.js$/, loader: 'babel-loader', }, { test: /\.mp3$/, loader: 'file-loader', }, { test: /(icons|node_modules)\/.*\.svg$/, loader: 'raw-loader!svgo-loader', }, { test: /graphics\/.*\.svg$/, loader: 'file-loader', }, { test: /.*\.(png|gif)$/, loader: 'file-loader', }, ], }, plugins: [ new webpack.HotModuleReplacementPlugin(), new ForkTsCheckerWebpackPlugin({}), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('development'), }, __EXAMPLE_ADDR__: '"https://dynamic-resources.graph.cool"', }), new HtmlWebpackPlugin({ favicon: 'static/favicon.png', template: 'src/renderer/index.html', }), new webpack.NormalModuleReplacementPlugin(/\/iconv-loader$/, 'node-noop'), // See https://github.com/graphql/graphql-language-service/issues/111 new webpack.ContextReplacementPlugin( /graphql-language-service-interface[\/\\]dist/, /\.js$/, ), new webpack.LoaderOptionsPlugin({ options: { svgo: { plugins: [{ removeStyleElement: true }], }, }, }), new HappyPack({ id: 'ts', threads: 2, loaders: ['ts-loader?' + JSON.stringify({ happyPackMode: true })], }), new HappyPack({ id: 'babel', threads: 2, loaders: ['babel-loader'], }), // new BundleAnalyzerPlugin(), ], resolve: { modules: [path.resolve('./src'), 'node_modules'], extensions: ['.mjs', '.js', '.ts', '.tsx'], }, } ================================================ FILE: packages/graphql-playground-html/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules example/node_modules example/yarn.lock # testing /coverage # production /dist # misc .DS_Store .env npm-debug.log .idea ================================================ FILE: packages/graphql-playground-html/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.6.29](https://github.com/graphcool/graphql-playground/compare/graphql-playground-html@1.6.28...graphql-playground-html@1.6.29) (2020-10-20) **Note:** Version bump only for package graphql-playground-html ## [1.6.28](https://github.com/graphcool/graphql-playground/compare/graphql-playground-html@1.6.27...graphql-playground-html@1.6.28) (2020-09-15) ### Bug Fixes * add schema.polling* to ISettings interface. ([#1212](https://github.com/graphcool/graphql-playground/issues/1212)) ([b7e6d4d](https://github.com/graphcool/graphql-playground/commit/b7e6d4d7590766183a77910a517ea946b95f2a84)) ## [1.6.27](https://github.com/graphcool/graphql-playground/compare/graphql-playground-html@1.6.26...graphql-playground-html@1.6.27) (2020-08-30) **Note:** Version bump only for package graphql-playground-html ## [1.6.26](https://github.com/graphcool/graphql-playground/compare/graphql-playground-html@1.6.23...graphql-playground-html@1.6.26) (2020-08-30) ### Bug Fixes * cdn url ([#1238](https://github.com/graphcool/graphql-playground/issues/1238)) ([e574bb6](https://github.com/graphcool/graphql-playground/commit/e574bb69e8adcda816fa62acc7e3adf19f31947a)) * **examples:** fix examples of reflected XSS attack ([#1256](https://github.com/graphcool/graphql-playground/issues/1256)) ([12b61b9](https://github.com/graphcool/graphql-playground/commit/12b61b9d69286b12a6ac74b12aae705e6b060f3b)) ## 1.6.23 (2020-06-07) ### Bug Fixes * hide config element 😆 ([#1224](https://github.com/graphcool/graphql-playground/issues/1224)) ([a7bdcaa](https://github.com/graphcool/graphql-playground/commit/a7bdcaa669f21603ded80bb9c59c4ab41597161a)) * rectify all versions and references ([#1223](https://github.com/graphcool/graphql-playground/issues/1223)) ([239289b](https://github.com/graphcool/graphql-playground/commit/239289b3e9da1744b23b7ef2694b1ed6370e3c16)) * X-Apollo-Tracing No Schema Issue ([#1112](https://github.com/graphcool/graphql-playground/issues/1112)) ([1ca035d](https://github.com/graphcool/graphql-playground/commit/1ca035d06f71cbe02aa8f36e7fce2095c2854ba6)) * **deps:** update deps and toolchain, move back to using yarn… ([#1191](https://github.com/graphcool/graphql-playground/issues/1191)) ([824c7a5](https://github.com/graphcool/graphql-playground/commit/824c7a57f0284f022726a8b8840aafc3e8720ccd)) ## 1.8.10 (2019-02-23) ## 1.8.9 (2019-02-01) ## 1.8.7 (2019-01-28) ## 1.8.6 (2019-01-27) ### Bug Fixes * **graphql 14:** version bump via graphql-config ([#861](https://github.com/graphcool/graphql-playground/issues/861)) ([5ea711c](https://github.com/graphcool/graphql-playground/commit/5ea711c590c1265c873324b28cd3483d3e05dc98)) * close body tag ([#833](https://github.com/graphcool/graphql-playground/issues/833)) ([3d2732d](https://github.com/graphcool/graphql-playground/commit/3d2732dbd90f71f8b48465b95c7b7b5bc8bc7a1c)) # 1.6.0 (2018-05-31) # 1.4.0 (2018-01-15) ### Bug Fixes * **deps:** make graphql-config a normal dep ([9e4d93e](https://github.com/graphcool/graphql-playground/commit/9e4d93e0cf7ebd3ba1806407383e071fda37cb55)) * **deps:** Remove extension dependencies ([72ce36c](https://github.com/graphcool/graphql-playground/commit/72ce36cdd96f35efefd916993a949e646c5f94b2)), closes [#493](https://github.com/graphcool/graphql-playground/issues/493) * **deps:** Updated graphql-config-extension-graphcool ([ef83c09](https://github.com/graphcool/graphql-playground/commit/ef83c097c018a42f7ee65529d6af4ea3928a4281)) ## 1.3.13 (2017-12-24) ## 1.3.12 (2017-12-24) ## 1.3.9 (2017-12-14) ## 1.3.6 (2017-12-04) ## 1.3.5 (2017-12-04) ### Features * **middleware:** draft animated loading screen ([c082c07](https://github.com/graphcool/graphql-playground/commit/c082c07cdcfeae50dd0c43a5ae225729a91556ef)) ================================================ FILE: packages/graphql-playground-html/README.md ================================================ # graphql-playground-html > **SECURITY WARNING:** This package and all of it's dependendents had a severe XSS Reflection attack vulnerability until version `1.6.22` of this package. You must sanitize any and all user input values to `renderPlaygroundPage()` values. If you used static values in your middlewares, including ours, you were not vulnerable to the attack. This package is being used by the GraphQL Playground middlewares. For local development, you can `yarn link` this package, then use `yarn link graphql-playground-html` in the middleware you want to develop. ================================================ FILE: packages/graphql-playground-html/examples/xss-attack/README.md ================================================ # GraphQL Playground HTML XSS Reflection Attack Example This shows the simplest possible example for how one might re-create the XSS Reflection Vulnerability reported by Cure53. Notice we force the resolution to `graphql-playground-html@1.6.20`, which is the last version susceptible. All prior versions are susceptible to the attack. Dynamic, unsanitized input that resembles some of the configuration you see is a simple example - if url parameters, query parameters, unsanitized database text strings, etc are passed to `expressPlayground()`, `renderPlaygroundPage()` or equivalent middleware functions such as `koaPlayground()`, they are all vulnerable to this attack. ## Reccomendations Here we use `xss` because it was easy to provide for node.js, however [DOMPurify](https://github.com/cure53/DOMPurify) is also an excellent choice for sanitizing strings for unwanted html. By default it requires the browser DOM, but you can load it with JSDOM for server side purposes as well. here are a few more tips to prevent other XSS vulnerabilities that might exist in your own applications: - `DOMPurify.sanitize` url values with user input to be used for rendering ` 'world', }, } const PORT = 4000 const server = new ApolloServer({ typeDefs, resolvers }) const app = express() server.applyMiddleware({ app }) // Example 1: Query Parameters app.get('/example-1', (req, res, next) => { res.write( renderPlaygroundPage({ endpoint: `/graphql/${req.query.id}`, }), ) res.status(200) next() }) // Example 2: mock database example const db = { async get() { return { 'editor.fontFamily': `
Loading GraphQL Playground
================================================ FILE: packages/graphql-playground-html/minimalWithoutCDN.html ================================================ GraphQL Playground
Loading GraphQL Playground
================================================ FILE: packages/graphql-playground-html/package.json ================================================ { "name": "graphql-playground-html", "version": "1.6.29", "homepage": "https://github.com/graphcool/graphql-playground/tree/main/packages/graphql-playground-html", "description": "GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).", "contributors": [ "Tim Suchanek ", "Johannes Schickling ", "Mohammad Rajabifard " ], "repository": "http://github.com/graphcool/graphql-playground.git", "license": "MIT", "main": "dist/index.js", "files": [ "dist" ], "scripts": { "build": "rimraf dist && tsc", "prepare": "npm run build" }, "keywords": [ "graphql", "graphiql", "playground", "graphcool" ], "devDependencies": { "@types/node": "12.12.34", "rimraf": "3.0.2", "typescript": "3.8.3" }, "typings": "dist/index.d.ts", "typescript": { "definition": "dist/index.d.ts" }, "dependencies": { "xss": "^1.0.6" }, "gitHead": "53d233f9deb9dcc86f99e41268ff92b6367e0713" } ================================================ FILE: packages/graphql-playground-html/src/get-loading-markup.ts ================================================ const getLoadingMarkup = () => ({ script: ` const loadingWrapper = document.getElementById('loading-wrapper'); if (loadingWrapper) { loadingWrapper.classList.add('fadeOut'); } `, container: `
Loading GraphQL Playground
`, }) export default getLoadingMarkup ================================================ FILE: packages/graphql-playground-html/src/index.ts ================================================ export { renderPlaygroundPage, MiddlewareOptions, RenderPageOptions, } from './render-playground-page' ================================================ FILE: packages/graphql-playground-html/src/render-playground-page.ts ================================================ import { filterXSS } from 'xss'; import getLoadingMarkup from './get-loading-markup' export interface MiddlewareOptions { endpoint?: string subscriptionEndpoint?: string workspaceName?: string env?: any config?: any settings?: Partial schema?: IntrospectionResult tabs?: Tab[] codeTheme?: EditorColours } export type CursorShape = 'line' | 'block' | 'underline' export type Theme = 'dark' | 'light' export interface ISettings { 'general.betaUpdates': boolean 'editor.cursorShape': CursorShape 'editor.theme': Theme 'editor.reuseHeaders': boolean 'tracing.hideTracingResponse': boolean 'tracing.tracingSupported': boolean 'editor.fontSize': number 'editor.fontFamily': string 'request.credentials': string 'request.globalHeaders': { [key: string]: string; } 'schema.polling.enable': boolean 'schema.polling.endpointFilter': string 'schema.polling.interval': number } 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 } export interface IntrospectionResult { __schema: any } export interface RenderPageOptions extends MiddlewareOptions { version?: string cdnUrl?: string env?: any title?: string faviconUrl?: string | null } export interface Tab { endpoint: string query: string name?: string variables?: string responses?: string[] headers?: { [key: string]: string } } const filter = (val) => { return filterXSS(val, { // @ts-ignore whiteList: [], stripIgnoreTag: true, stripIgnoreTagBody: ["script"] }) } const loading = getLoadingMarkup() const CONFIG_ID = 'playground-config'; const getCdnMarkup = ({ version, cdnUrl = '//cdn.jsdelivr.net/npm', faviconUrl }) => { const buildCDNUrl = (packageName: string, suffix: string) => filter(`${cdnUrl}/${packageName}${version ? `@${version}` : ''}/${suffix}` || '') return ` ${typeof faviconUrl === 'string' ? `` : ''} ${faviconUrl === undefined ? `` : ''} `} const renderConfig = (config) => { return filterXSS(`
${JSON.stringify(config)}
`, { whiteList: { div: ['id'] }, }) } export function renderPlaygroundPage(options: RenderPageOptions) { const extendedOptions: any = { ...options, canSaveConfig: false, } // for compatibility if ((options as any).subscriptionsEndpoint) { extendedOptions.subscriptionEndpoint = filter((options as any).subscriptionsEndpoint || '') } if (options.config) { extendedOptions.configString = JSON.stringify(options.config, null, 2) } if (!extendedOptions.endpoint && !extendedOptions.configString) { /* tslint:disable-next-line */ console.warn( `WARNING: You didn't provide an endpoint and don't have a .graphqlconfig. Make sure you have at least one of them.`, ) } else if (extendedOptions.endpoint) { extendedOptions.endpoint = filter(extendedOptions.endpoint || '') } return ` ${extendedOptions.title || 'GraphQL Playground'} ${extendedOptions.env === 'react' || extendedOptions.env === 'electron' ? '' : getCdnMarkup(extendedOptions) } ${loading.container} ${renderConfig(extendedOptions)}
` } ================================================ FILE: packages/graphql-playground-html/tsconfig.json ================================================ { "compilerOptions": { "outDir": "dist", "target": "es5", "lib": ["esnext", "dom"], "sourceMap": true, "rootDir": "src", "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "noImplicitThis": true, "noImplicitAny": false, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "declaration": true } } ================================================ FILE: packages/graphql-playground-html/withAnimation.html ================================================ GraphQL Playground
Loading GraphQL Playground
================================================ FILE: packages/graphql-playground-middleware-express/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules example/node_modules example/yarn.lock # testing /coverage # production /dist # misc .DS_Store .env npm-debug.log .idea ================================================ FILE: packages/graphql-playground-middleware-express/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.7.22](https://github.com/graphcool/graphql-playground/compare/graphql-playground-middleware-express@1.7.21...graphql-playground-middleware-express@1.7.22) (2020-10-20) **Note:** Version bump only for package graphql-playground-middleware-express ## [1.7.21](https://github.com/graphcool/graphql-playground/compare/graphql-playground-middleware-express@1.7.20...graphql-playground-middleware-express@1.7.21) (2020-09-15) **Note:** Version bump only for package graphql-playground-middleware-express ## [1.7.20](https://github.com/graphcool/graphql-playground/compare/graphql-playground-middleware-express@1.7.19...graphql-playground-middleware-express@1.7.20) (2020-08-30) **Note:** Version bump only for package graphql-playground-middleware-express ## [1.7.19](https://github.com/graphcool/graphql-playground/compare/graphql-playground-middleware-express@1.7.15...graphql-playground-middleware-express@1.7.19) (2020-08-30) **Note:** Version bump only for package graphql-playground-middleware-express ## 1.7.15 (2020-06-07) ### Bug Fixes * hide config element 😆 ([#1224](https://github.com/graphcool/graphql-playground/issues/1224)) ([a7bdcaa](https://github.com/graphcool/graphql-playground/commit/a7bdcaa669f21603ded80bb9c59c4ab41597161a)) * rectify all versions and references ([#1223](https://github.com/graphcool/graphql-playground/issues/1223)) ([239289b](https://github.com/graphcool/graphql-playground/commit/239289b3e9da1744b23b7ef2694b1ed6370e3c16)) * **deps:** update deps and toolchain, move back to using yarn… ([#1191](https://github.com/graphcool/graphql-playground/issues/1191)) ([824c7a5](https://github.com/graphcool/graphql-playground/commit/824c7a57f0284f022726a8b8840aafc3e8720ccd)) * **deps:** Update express mw to latest graphql-playground-html ([#984](https://github.com/graphcool/graphql-playground/issues/984)) ([0bc65e8](https://github.com/graphcool/graphql-playground/commit/0bc65e8643b10b0c3b2158865e817e7910411afe)) ## 1.8.10 (2019-02-23) ## 1.8.9 (2019-02-01) ## 1.8.7 (2019-01-28) ### Bug Fixes * **graphql 14:** version bump via graphql-config ([#861](https://github.com/graphcool/graphql-playground/issues/861)) ([5ea711c](https://github.com/graphcool/graphql-playground/commit/5ea711c590c1265c873324b28cd3483d3e05dc98)) ## 1.6.2 (2018-07-06) ## 1.6.1 (2018-06-26) ## 1.5.9 (2018-05-25) ## 1.5.8 (2018-05-10) # 1.4.0 (2018-01-15) ### Bug Fixes * **deps:** make graphql-config a normal dep ([9e4d93e](https://github.com/graphcool/graphql-playground/commit/9e4d93e0cf7ebd3ba1806407383e071fda37cb55)) * **deps:** Remove extension dependencies ([72ce36c](https://github.com/graphcool/graphql-playground/commit/72ce36cdd96f35efefd916993a949e646c5f94b2)), closes [#493](https://github.com/graphcool/graphql-playground/issues/493) * **subscriptions:** fixed subscriptions url normalization ([f675517](https://github.com/graphcool/graphql-playground/commit/f67551718fb93d9170ca393e996e588a8fa834c8)) ## 1.3.23 (2018-01-08) ### Bug Fixes * **deps:** Update graphql-playground-middleware-express ([0ac03f3](https://github.com/graphcool/graphql-playground/commit/0ac03f3557ef2b348de606eb8a24b88d5f0b0a0b)) ## 1.3.22 (2018-01-05) ## 1.3.12 (2017-12-24) ## 1.3.11 (2017-12-24) ## 1.3.9 (2017-12-14) ## 1.3.6 (2017-12-04) ## 1.3.5 (2017-12-04) ### Features * **middleware:** draft animated loading screen ([c082c07](https://github.com/graphcool/graphql-playground/commit/c082c07cdcfeae50dd0c43a5ae225729a91556ef)) # 1.3.0 (2017-12-01) # 1.2.0 (2017-11-24) ### Bug Fixes * fix example ([714d571](https://github.com/graphcool/graphql-playground/commit/714d571b530143a1b55a349714e5c6679a757fa8)) ### Features * sparate express into its own package ([8e250d4](https://github.com/graphcool/graphql-playground/commit/8e250d4b4929e3bceba61484d8f85444036c3dc5)) ================================================ FILE: packages/graphql-playground-middleware-express/README.md ================================================ # graphql-playground-middleware-express > Express middleware to expose an endpoint for the GraphQL Playground IDE > **SECURITY NOTE**: All versions of `graphql-playground-express` until `1.7.16` or later have a security vulnerability when unsanitized user input is used while invoking `expressPlayground()`. [Read more below](#security-notes) ## Installation Using yarn: ```console yarn add graphql-playground-middleware-express ``` Or npm: ```console npm install graphql-playground-middleware-express --save ``` ## Usage See full example in [examples/basic](https://github.com/prisma/graphql-playground/tree/main/packages/graphql-playground-middleware-express/examples/basic). ```js const express = require('express') const expressPlayground = require('graphql-playground-middleware-express') .default const app = express() app.get('/playground', expressPlayground({ endpoint: '/graphql' })) ``` ## Security Notes All versions before `1.7.16` were vulnerable to user-defined input to `expressPlayground()`. Read more in [the security notes](https://github.com/prisma/graphql-playground/tree/main/SECURITY.md) ### Security Upgrade Steps To fix the issue, you can upgrade to `1.6.12` or later. If you aren't able to upgrade, see the security notes for a workaround. **yarn:** `yarn add graphql-playground-express@^1.7.16` **npm:** `npm install --save graphql-playground-express@^1.7.16` ================================================ FILE: packages/graphql-playground-middleware-express/examples/basic/.envrc ================================================ export PORT=4000 ================================================ FILE: packages/graphql-playground-middleware-express/examples/basic/.graphqlconfig.yml ================================================ schemaPath: schema.graphql extensions: endpoints: default: url: 'https://airbnb.now.sh' subscription: 'wss://airbnb.now.sh' local: url: 'http://localhost:${env:PORT}/graphql' subscription: 'ws://localhost:${env:PORT}' ================================================ FILE: packages/graphql-playground-middleware-express/examples/basic/README.md ================================================ # GraphQL Playground Express.js Example ```sh $ yarn $ node index.js ``` ================================================ FILE: packages/graphql-playground-middleware-express/examples/basic/index.js ================================================ const express = require('express') const { ApolloServer, gql } = require('apollo-server-express') const expressPlayground = require('../../dist/index').default const typeDefs = gql` type Query { hello: String! } schema { query: Query } ` const resolvers = { Query: { hello: () => 'world', }, } const PORT = 4000 const server = new ApolloServer({ typeDefs, resolvers }) const app = express() server.applyMiddleware({ app }) app.get( '/playground', expressPlayground({ endpoint: '/graphql/ ================================================ FILE: packages/graphql-playground-react/public/insta-auth0.graphql ================================================ type Post @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! description: String! imageUrl: String! } type User @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String! emailAddress: String! emailSubscription: Boolean! } ================================================ FILE: packages/graphql-playground-react/public/insta-email.graphql ================================================ type Post @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! description: String! imageUrl: String! } type User @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String! email: String! emailSubscription: Boolean! } ================================================ FILE: packages/graphql-playground-react/public/insta-expo-auth.graphql ================================================ type User @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! posts: [Post!]! @relation(name: "PostsByUser") name: String! comments: [Comment!]! @relation(name: "CommentsByUser") } type Post @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! description: String! imageUrl: String! createdBy: User! @relation(name: "PostsByUser") comments: [Comment!]! @relation(name: "CommentsOnPost") } type Comment @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! content: String! post: Post! @relation(name: "CommentsOnPost") author: User! @relation(name: "CommentsByUser") } ================================================ FILE: packages/graphql-playground-react/public/insta-files.graphql ================================================ type Post @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! description: String! image: File @relation(name: "PostImage") } type File @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! post: Post @relation(name: "PostImage") } ================================================ FILE: packages/graphql-playground-react/public/instagram-full.graphql ================================================ # The `BigDecimal` scalar type represents signed fractional values with arbitrary precision. scalar BigDecimal # The `BigInt` scalar type represents non-fractional signed whole numeric values. BigInt can represent arbitrary big values. scalar BigInt input CreateFile { name: String! } input CreateFileInput { name: String! clientMutationId: String! } type CreateFilePayload { viewer: Viewer! clientMutationId: String! file: File edge: FileEdge } input CreatePost { description: String! imageUrl: String! } input CreatePostInput { description: String! imageUrl: String! clientMutationId: String! } type CreatePostPayload { viewer: Viewer! clientMutationId: String! post: Post edge: PostEdge } # If authentication was successful the payload contains the user and a token. If unsuccessful this payload is null. type CreateUserPayload { user: User clientMutationId: String viewer: Viewer! } scalar DateTime input DeleteFileInput { id: ID! clientMutationId: String! } type DeleteFilePayload { viewer: Viewer! clientMutationId: String! file: File edge: FileEdge deletedId: ID } input DeletePostInput { id: ID! clientMutationId: String! } type DeletePostPayload { viewer: Viewer! clientMutationId: String! post: Post edge: PostEdge deletedId: ID } input DeleteUserInput { id: ID! clientMutationId: String! } type DeleteUserPayload { viewer: Viewer! clientMutationId: String! user: User edge: UserEdge deletedId: ID } type File implements Node { contentType: String! createdAt: DateTime! id: ID! name: String! secret: String! size: Int! updatedAt: DateTime! url: String! } # A connection to a list of items. type FileConnection { # Information to aid in pagination. pageInfo: PageInfo! # A list of edges. edges: [FileEdge] # Count of filtered result set without considering pagination arguments count: Int! } # An edge in a connection. type FileEdge { # The item at the end of the edge. node: File! # A cursor for use in pagination. cursor: String! } input FileFilter { AND: [FileFilter!] OR: [FileFilter!] contentType: String contentType_not: String contentType_in: [String!] contentType_not_in: [String!] contentType_lt: String contentType_lte: String contentType_gt: String contentType_gte: String contentType_contains: String contentType_not_contains: String contentType_starts_with: String contentType_not_starts_with: String contentType_ends_with: String contentType_not_ends_with: String createdAt: DateTime createdAt_not: DateTime createdAt_in: [DateTime!] createdAt_not_in: [DateTime!] createdAt_lt: DateTime createdAt_lte: DateTime createdAt_gt: DateTime createdAt_gte: DateTime id: ID id_not: ID id_in: [ID!] id_not_in: [ID!] id_lt: ID id_lte: ID id_gt: ID id_gte: ID id_contains: ID id_not_contains: ID id_starts_with: ID id_not_starts_with: ID id_ends_with: ID id_not_ends_with: ID name: String name_not: String name_in: [String!] name_not_in: [String!] name_lt: String name_lte: String name_gt: String name_gte: String name_contains: String name_not_contains: String name_starts_with: String name_not_starts_with: String name_ends_with: String name_not_ends_with: String secret: String secret_not: String secret_in: [String!] secret_not_in: [String!] secret_lt: String secret_lte: String secret_gt: String secret_gte: String secret_contains: String secret_not_contains: String secret_starts_with: String secret_not_starts_with: String secret_ends_with: String secret_not_ends_with: String size: Int size_not: Int size_in: [Int!] size_not_in: [Int!] size_lt: Int size_lte: Int size_gt: Int size_gte: Int updatedAt: DateTime updatedAt_not: DateTime updatedAt_in: [DateTime!] updatedAt_not_in: [DateTime!] updatedAt_lt: DateTime updatedAt_lte: DateTime updatedAt_gt: DateTime updatedAt_gte: DateTime url: String url_not: String url_in: [String!] url_not_in: [String!] url_lt: String url_lte: String url_gt: String url_gte: String url_contains: String url_not_contains: String url_starts_with: String url_not_starts_with: String url_ends_with: String url_not_ends_with: String } enum FileOrderBy { contentType_ASC contentType_DESC createdAt_ASC createdAt_DESC id_ASC id_DESC name_ASC name_DESC secret_ASC secret_DESC size_ASC size_DESC updatedAt_ASC updatedAt_DESC url_ASC url_DESC } # The `Long` scalar type represents non-fractional signed whole numeric values. # Long can represent values between -(2^63) and 2^63 - 1. scalar Long type Mutation { createFile(input: CreateFileInput!): CreateFilePayload createPost(input: CreatePostInput!): CreatePostPayload updateFile(input: UpdateFileInput!): UpdateFilePayload updatePost(input: UpdatePostInput!): UpdatePostPayload updateUser(input: UpdateUserInput!): UpdateUserPayload updateOrCreateFile(input: UpdateOrCreateFileInput!): UpdateOrCreateFilePayload updateOrCreatePost(input: UpdateOrCreatePostInput!): UpdateOrCreatePostPayload updateOrCreateUser(input: UpdateOrCreateUserInput!): UpdateOrCreateUserPayload deleteFile(input: DeleteFileInput!): DeleteFilePayload deletePost(input: DeletePostInput!): DeletePostPayload deleteUser(input: DeleteUserInput!): DeleteUserPayload createUser(input: SignupUserInput!): CreateUserPayload! } # An object with an ID interface Node { # The id of the object. id: ID! } # Information about pagination in a connection. type PageInfo { # When paginating forwards, are there more items? hasNextPage: Boolean # When paginating backwards, are there more items? hasPreviousPage: Boolean # When paginating backwards, the cursor to continue. startCursor: String # When paginating forwards, the cursor to continue. endCursor: String } type Post implements Node { createdAt: DateTime! description: String! id: ID! imageUrl: String! updatedAt: DateTime! } # A connection to a list of items. type PostConnection { # Information to aid in pagination. pageInfo: PageInfo # A list of edges. edges: [PostEdge] # Count of filtered result set without considering pagination arguments count: Int! } # An edge in a connection. type PostEdge { # The item at the end of the edge. node: Post # A cursor for use in pagination. cursor: String } input PostFilter { AND: [PostFilter!] OR: [PostFilter!] createdAt: DateTime createdAt_not: DateTime createdAt_in: [DateTime!] createdAt_not_in: [DateTime!] createdAt_lt: DateTime createdAt_lte: DateTime createdAt_gt: DateTime createdAt_gte: DateTime description: String description_not: String description_in: [String!] description_not_in: [String!] description_lt: String description_lte: String description_gt: String description_gte: String description_contains: String description_not_contains: String description_starts_with: String description_not_starts_with: String description_ends_with: String description_not_ends_with: String id: ID id_not: ID id_in: [ID!] id_not_in: [ID!] id_lt: ID id_lte: ID id_gt: ID id_gte: ID id_contains: ID id_not_contains: ID id_starts_with: ID id_not_starts_with: ID id_ends_with: ID id_not_ends_with: ID imageUrl: String imageUrl_not: String imageUrl_in: [String!] imageUrl_not_in: [String!] imageUrl_lt: String imageUrl_lte: String imageUrl_gt: String imageUrl_gte: String imageUrl_contains: String imageUrl_not_contains: String imageUrl_starts_with: String imageUrl_not_starts_with: String imageUrl_ends_with: String imageUrl_not_ends_with: String updatedAt: DateTime updatedAt_not: DateTime updatedAt_in: [DateTime!] updatedAt_not_in: [DateTime!] updatedAt_lt: DateTime updatedAt_lte: DateTime updatedAt_gt: DateTime updatedAt_gte: DateTime } enum PostOrderBy { createdAt_ASC createdAt_DESC description_ASC description_DESC id_ASC id_DESC imageUrl_ASC imageUrl_DESC updatedAt_ASC updatedAt_DESC } type Query { viewer: Viewer! # Fetches an object given its ID node( # The ID of an object id: ID! ): Node } input SignupUserInput { clientMutationId: String! } input UpdateFile { id: ID! name: String } input UpdateFileInput { id: ID! name: String clientMutationId: String! } type UpdateFilePayload { viewer: Viewer! clientMutationId: String! file: File edge: FileEdge } input UpdateOrCreateFileInput { update: UpdateFile! create: CreateFile! clientMutationId: String! } type UpdateOrCreateFilePayload { viewer: Viewer! clientMutationId: String! file: File edge: FileEdge } input UpdateOrCreatePostInput { update: UpdatePost! create: CreatePost! clientMutationId: String! } type UpdateOrCreatePostPayload { viewer: Viewer! clientMutationId: String! post: Post edge: PostEdge } input UpdateOrCreateUserInput { update: UpdateUser! clientMutationId: String! } type UpdateOrCreateUserPayload { viewer: Viewer! clientMutationId: String! user: User edge: UserEdge } input UpdatePost { description: String id: ID! imageUrl: String } input UpdatePostInput { description: String id: ID! imageUrl: String clientMutationId: String! } type UpdatePostPayload { viewer: Viewer! clientMutationId: String! post: Post edge: PostEdge } input UpdateUser { id: ID! } input UpdateUserInput { id: ID! clientMutationId: String! } type UpdateUserPayload { viewer: Viewer! clientMutationId: String! user: User edge: UserEdge } type User implements Node { createdAt: DateTime! id: ID! updatedAt: DateTime! } # A connection to a list of items. type UserConnection { # Information to aid in pagination. pageInfo: PageInfo! # A list of edges. edges: [UserEdge] # Count of filtered result set without considering pagination arguments count: Int! } # An edge in a connection. type UserEdge { # The item at the end of the edge. node: User! # A cursor for use in pagination. cursor: String! } input UserFilter { AND: [UserFilter!] OR: [UserFilter!] createdAt: DateTime createdAt_not: DateTime createdAt_in: [DateTime!] createdAt_not_in: [DateTime!] createdAt_lt: DateTime createdAt_lte: DateTime createdAt_gt: DateTime createdAt_gte: DateTime id: ID id_not: ID id_in: [ID!] id_not_in: [ID!] id_lt: ID id_lte: ID id_gt: ID id_gte: ID id_contains: ID id_not_contains: ID id_starts_with: ID id_not_starts_with: ID id_ends_with: ID id_not_ends_with: ID updatedAt: DateTime updatedAt_not: DateTime updatedAt_in: [DateTime!] updatedAt_not_in: [DateTime!] updatedAt_lt: DateTime updatedAt_lte: DateTime updatedAt_gt: DateTime updatedAt_gte: DateTime } enum UserOrderBy { createdAt_ASC createdAt_DESC id_ASC id_DESC updatedAt_ASC updatedAt_DESC } # This is the famous Relay viewer object type Viewer { allFiles(filter: FileFilter, orderBy: FileOrderBy, skip: Int, after: String, before: String, first: Int, last: Int): FileConnection! allPosts(filter: PostFilter, orderBy: PostOrderBy, skip: Int, after: String, before: String, first: Int, last: Int): PostConnection allUsers(filter: UserFilter, orderBy: UserOrderBy, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection! user: User File(id: ID, secret: String, url: String): File Post(id: ID): Post User(id: ID): User id: ID! } ================================================ FILE: packages/graphql-playground-react/public/instagram.graphql ================================================ type Post @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! description: String! imageUrl: String! } ================================================ FILE: packages/graphql-playground-react/public/movies-import.graphql ================================================ type Movie @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! oldid: String description: String released: String title: String } ================================================ FILE: packages/graphql-playground-react/public/pokedex.graphql ================================================ type Pokemon @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String } ================================================ FILE: packages/graphql-playground-react/public/simple-pokedex.graphql ================================================ type Pokemon @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String! url: String! } ================================================ FILE: packages/graphql-playground-react/public/todo.graphql ================================================ type Todo @model { id: ID! @isUnique text: String! complete: Boolean! } ================================================ FILE: packages/graphql-playground-react/public/twitter.graphql ================================================ type User @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! username: String! followers: [User!]! @relation(name: "Followers") tweets: [Tweet!]! @relation(name: "Tweets") } type Tweet @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! text: String! author: User @relation(name: "Tweets") } ================================================ FILE: packages/graphql-playground-react/public/welcome-rp.graphql ================================================ type Customer @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String! email: String! } ================================================ FILE: packages/graphql-playground-react/public/welcome.graphql ================================================ type Customer @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String! email: String } ================================================ FILE: packages/graphql-playground-react/public/worldchat.graphql ================================================ type Traveller @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String! location: Location! @relation(name: "TravellerLocation") messages: [Message!]! @relation(name: "MessagesFromTraveller") } type Message @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! text: String! sentBy: Traveller! @relation(name: "MessagesFromTraveller") } type Location @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! traveller: Traveller! @relation(name: "TravellerLocation") latitude: Float! longitude: Float! } ================================================ FILE: packages/graphql-playground-react/release.sh ================================================ #!/bin/bash set -e #npm publish curl -X POST \ http://purge.jsdelivr.net/ \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "path": [ "/npm/graphql-playground-react/build/static/css/middleware.css", "/npm/graphql-playground-react/build/static/js/middleware.js" ] }' ================================================ FILE: packages/graphql-playground-react/scripts/build.js ================================================ // Do this as the first thing so that any code reading it knows the right env. process.env.NODE_ENV = 'production'; // Load environment variables from .env file. Suppress warnings using silent // if this file is missing. dotenv will never modify any environment variables // that have already been set. // https://github.com/motdotla/dotenv require('dotenv').config({silent: true}); var chalk = require('chalk'); var fs = require('fs-extra'); var path = require('path'); var filesize = require('filesize'); var gzipSize = require('gzip-size').sync; var webpack = require('webpack'); var config = require('../config/webpack.config.prod'); var paths = require('../config/paths'); var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); var recursive = require('recursive-readdir'); var stripAnsi = require('strip-ansi'); var useYarn = fs.existsSync(paths.yarnLockFile); const { renderPlaygroundPage } = require('graphql-playground-html'); // Create the playground entry point if it doesn't exist if(!fs.existsSync(paths.appHtml)) { fs.writeFileSync(paths.appHtml, renderPlaygroundPage({env: 'react'})); } // Warn and crash if required files are missing if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { process.exit(1); } // Input: /User/dan/app/build/static/js/main.82be8.js // Output: /static/js/main.ts function removeFileNameHash(fileName) { return fileName .replace(paths.appBuild, '') .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); } // Input: 1024, 2048 // Output: "(+1 KB)" function getDifferenceLabel(currentSize, previousSize) { var FIFTY_KILOBYTES = 1024 * 50; var difference = currentSize - previousSize; var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; if (difference >= FIFTY_KILOBYTES) { return chalk.red('+' + fileSize); } else if (difference < FIFTY_KILOBYTES && difference > 0) { return chalk.yellow('+' + fileSize); } else if (difference < 0) { return chalk.green(fileSize); } else { return ''; } } // First, read the current file sizes in build directory. // This lets us display how much they changed later. recursive(paths.appBuild, (err, fileNames) => { var previousSizeMap = (fileNames || []) .filter(fileName => /\.(js|css)$/.test(fileName)) .reduce((memo, fileName) => { var contents = fs.readFileSync(fileName); var key = removeFileNameHash(fileName); memo[key] = gzipSize(contents); return memo; }, {}); // Remove all content but keep the directory so that // if you're in it, you don't end up in Trash fs.emptyDirSync(paths.appBuild); // Start the webpack build build(previousSizeMap); // Merge with the public folder copyPublicFolder(); }); // Print a detailed summary of build files. function printFileSizes(stats, previousSizeMap) { var assets = stats.toJson().assets .filter(asset => /\.(js|css)$/.test(asset.name)) .map(asset => { var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); var size = gzipSize(fileContents); var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; var difference = getDifferenceLabel(size, previousSize); return { folder: path.join('build', path.dirname(asset.name)), name: path.basename(asset.name), size: size, sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') }; }); assets.sort((a, b) => b.size - a.size); var longestSizeLabelLength = Math.max.apply(null, assets.map(a => stripAnsi(a.sizeLabel).length) ); assets.forEach(asset => { var sizeLabel = asset.sizeLabel; var sizeLength = stripAnsi(sizeLabel).length; if (sizeLength < longestSizeLabelLength) { var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); sizeLabel += rightPadding; } console.log( ' ' + sizeLabel + ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) ); }); } // Print out errors function printErrors(summary, errors) { console.log(chalk.red(summary)); console.log(); errors.forEach(err => { console.log(err.message || err); console.log(); }); } // Create the production build and print the deployment instructions. function build(previousSizeMap) { console.log('Creating an optimized production build...'); webpack(config).run((err, stats) => { if (err) { printErrors('Failed to compile.', [err]); process.exit(1); } if (stats.compilation.errors.length) { printErrors('Failed to compile.', stats.compilation.errors); process.exit(1); } if (process.env.CI && stats.compilation.warnings.length) { printErrors('Failed to compile.', stats.compilation.warnings); process.exit(1); } console.log(chalk.green('Compiled successfully.')); console.log(); console.log('File sizes after gzip:'); console.log(); printFileSizes(stats, previousSizeMap); console.log(); var openCommand = process.platform === 'win32' ? 'start' : 'open'; var appPackage = require(paths.appPackageJson); var homepagePath = appPackage.homepage; var publicPath = config.output.publicPath; if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { // "homepage": "http://user.github.io/project" console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); console.log(); console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); // If script deploy has been added to package.json, skip the instructions if (typeof appPackage.scripts.deploy === 'undefined') { console.log(); if (useYarn) { console.log(' ' + chalk.cyan('yarn') + ' add --dev gh-pages'); } else { console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages'); } console.log(); console.log('Add the following script in your ' + chalk.cyan('package.json') + '.'); console.log(); console.log(' ' + chalk.dim('// ...')); console.log(' ' + chalk.yellow('"scripts"') + ': {'); console.log(' ' + chalk.dim('// ...')); console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"npm run build&&gh-pages -d build"')); console.log(' }'); console.log(); console.log('Then run:'); } console.log(); console.log(' ' + chalk.cyan(useYarn ? 'yarn' : 'npm') + ' run deploy'); console.log(); } else if (publicPath !== '/') { // "homepage": "http://mywebsite.com/project" console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); console.log(); console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); console.log(); } else { // no homepage or "homepage": "http://mywebsite.com" console.log('The project was built assuming it is hosted at the server root.'); if (homepagePath) { // "homepage": "http://mywebsite.com" console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); console.log(); } else { // no homepage console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); console.log('For example, add this to build it for GitHub Pages:') console.log(); console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); console.log(); } console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); console.log('You may also serve it locally with a static server:') console.log(); if (useYarn) { console.log(' ' + chalk.cyan('yarn') + ' global add pushstate-server'); } else { console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); } console.log(' ' + chalk.cyan('pushstate-server') + ' build'); console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); console.log(); } }); } function copyPublicFolder() { fs.copySync(paths.appPublic, paths.appBuild, { dereference: true, filter: file => file !== paths.appHtml }); } ================================================ FILE: packages/graphql-playground-react/scripts/start.js ================================================ 'use strict'; // Do this as the first thing so that any code reading it knows the right env. process.env.BABEL_ENV = 'development'; process.env.NODE_ENV = 'development'; // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code. process.on('unhandledRejection', err => { throw err; }); // Ensure environment variables are read. require('../config/env'); const fs = require('fs'); const chalk = require('chalk'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const clearConsole = require('react-dev-utils/clearConsole'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); const { choosePort, createCompiler, prepareProxy, prepareUrls, } = require('react-dev-utils/WebpackDevServerUtils'); const openBrowser = require('react-dev-utils/openBrowser'); const paths = require('../config/paths'); const config = require('../config/webpack.config.dev'); const createDevServerConfig = require('../config/webpackDevServer.config'); const useYarn = fs.existsSync(paths.yarnLockFile); const isInteractive = process.stdout.isTTY; const { renderPlaygroundPage } = require('graphql-playground-html'); // Create the playground entry point if it doesn't exist if(!fs.existsSync(paths.appHtml)) { fs.writeFileSync(paths.appHtml, renderPlaygroundPage({env: 'react'})); } // Warn and crash if required files are missing if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { process.exit(1); } // Tools like Cloud9 rely on this. const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; const HOST = process.env.HOST || '0.0.0.0'; // We attempt to use the default port but if it is busy, we offer the user to // run on a different port. `detect()` Promise resolves to the next free port. choosePort(HOST, DEFAULT_PORT) .then(port => { if (port == null) { // We have not found a port. return; } const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; const appName = require(paths.appPackageJson).name; const urls = prepareUrls(protocol, HOST, port); // Create a webpack compiler that is configured with custom messages. const compiler = createCompiler({webpack, config, appName, urls, useYarn}); // Load proxy config const proxySetting = require(paths.appPackageJson).proxy; const proxyConfig = prepareProxy(proxySetting, paths.appPublic); // Serve webpack assets generated by the compiler over a web sever. const serverConfig = createDevServerConfig( proxyConfig, urls.lanUrlForConfig ); const devServer = new WebpackDevServer(compiler, serverConfig); // Launch WebpackDevServer. devServer.listen(port, HOST, err => { if (err) { return console.log(err); } if (isInteractive) { clearConsole(); } console.log(chalk.cyan('Starting the development server...\n')); openBrowser(urls.localUrlForBrowser); }); ['SIGINT', 'SIGTERM'].forEach(function(sig) { process.on(sig, function() { devServer.close(); process.exit(); }); }); }) .catch(err => { if (err && err.message) { console.log(err.message); } process.exit(1); }); ================================================ FILE: packages/graphql-playground-react/scripts/test.js ================================================ process.env.NODE_ENV = 'test'; process.env.PUBLIC_URL = ''; // Load environment variables from .env file. Suppress warnings using silent // if this file is missing. dotenv will never modify any environment variables // that have already been set. // https://github.com/motdotla/dotenv require('dotenv').config({silent: true}); const jest = require('jest'); const argv = process.argv.slice(2); // Watch unless on CI or in coverage mode if (!process.env.CI && argv.indexOf('--coverage') < 0) { argv.push('--watch'); } // A temporary hack to clear terminal correctly. // You can remove this after updating to Jest 18 when it's out. // https://github.com/facebook/jest/pull/2230 var realWrite = process.stdout.write; var CLEAR = process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H'; process.stdout.write = function(chunk, encoding, callback) { if (chunk === '\x1B[2J\x1B[H') { chunk = CLEAR; } return realWrite.call(this, chunk, encoding, callback); }; jest.run(argv); ================================================ FILE: packages/graphql-playground-react/src/__snapshots__/index.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`test MiddleWareApp passed default headers 1`] = `
New Tab
Close Tab
Opens a New Tab
Query Variables HTTP Headers (1)
Press the Play Button to get a response here
Tracing
This GraphQL server doesn’t support tracing. See the following page for instructions:
https://github.com/apollographql/apollo-tracing
Docs
Schema
Settings
`; exports[`test MiddleWareApp with tabs 1`] = `
New Tab
Close Tab
Opens a New Tab
Query Variables HTTP Headers
Press the Play Button to get a response here
Tracing
This GraphQL server doesn’t support tracing. See the following page for instructions:
https://github.com/apollographql/apollo-tracing
Docs
Schema
Settings
`; exports[`test MiddleWareApp without tabs 1`] = `
New Tab
Close Tab
Opens a New Tab
Query Variables HTTP Headers
Press the Play Button to get a response here
Tracing
This GraphQL server doesn’t support tracing. See the following page for instructions:
https://github.com/apollographql/apollo-tracing
Docs
Schema
Settings
`; ================================================ FILE: packages/graphql-playground-react/src/components/Button.tsx ================================================ import * as React from 'react' import { styled } from '../styled' import { FullArrowRightIcon } from './Icons' export interface Props { purple?: boolean hideArrow?: boolean children?: any onClick?: (e?: any) => void } export const Button = ({ purple, hideArrow, children, onClick }: Props) => ( {children ? children : 'Learn more'} {!hideArrow && } ) interface ButtonProps { purple?: boolean } export const ButtonBox = styled('div')` display: flex; align-items: center; padding: 6px 16px; border-radius: 2px; background: ${p => (p.purple ? 'rgb(218, 27, 127)' : '#2a7ed2')}; color: ${p => p.theme.colours.white}; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); text-transform: uppercase; font-weight: 600; font-size: 14px; letter-spacing: 1px; white-space: nowrap; transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease; cursor: pointer; &:hover { background: ${p => (p.purple ? 'rgb(164, 3, 111)' : '#3f8ad7')}; transform: ${p => p.purple ? 'translate3D(0, 0, 0)' : 'translate3D(0, -1px, 0)'}; svg { animation: move 1s ease infinite; } } svg { margin-left: 10px; fill: ${p => p.theme.colours.white}; } @keyframes move { 0% { transform: translate3D(0, 0, 0); } 50% { transform: translate3D(3px, 0, 0); } 100% { transform: translate3D(0, 0, 0); } } ` ================================================ FILE: packages/graphql-playground-react/src/components/Copy.tsx ================================================ import * as React from 'react' import { styled } from '../styled' import * as CopyToClipboard from 'react-copy-to-clipboard' export interface Props { text: string color?: string className?: string } export interface State { copied: boolean } export default class Copy extends React.Component { private copyTimer: any constructor(props) { super(props) this.state = { copied: false, } } componentWillUnmount() { clearTimeout(this.copyTimer) } render() { const { text, color } = this.props return ( {this.state.copied && Copied} {this.props.children} ) } private onCopy = () => { this.setState({ copied: true } as State) this.copyTimer = window.setTimeout( () => this.setState({ copied: false } as State), 500, ) } } const CopyBox = styled.div` position: relative; ` const Indicator = styled.div` position: absolute; top: -20px; left: 50%; transform: translate(-50%, 0); animation: copying 700ms linear; color: ${p => (p.color ? p.color : p.theme.colours.darkBlue30)}; @keyframes copying { 0% { opacity: 0; transform: translate(-50%, 0); } 50% { opacity: 1; } 100% { opacity: 0; transform: translate(-50%, -50px); } } ` ================================================ FILE: packages/graphql-playground-react/src/components/EndpointPopup.tsx ================================================ import * as React from 'react' import * as fetch from 'isomorphic-fetch' import Popup from './Popup' import { throttle } from 'lodash' import { Button } from './Button' import { styled, css } from '../styled' import { createClient } from 'graphql-ws'; // @ts-ignore import imageSource from '../assets/logo.png' export interface Props { onRequestClose: (endpoint: string) => void endpoint: string } export interface State { endpoint: string valid?: boolean } export default class EndpointPopup extends React.Component { checkEndpoint = throttle(() => { if (this.state.endpoint.match(/^(https?|wss?):\/\/\w+(\.\w+)*(:[0-9]+)?\/?.*$/)) { if (this.state.endpoint.match(/^(wss?)/)) { const client = createClient({ url: this.state.endpoint, retryAttempts:0 }); const unsubscribe = client.subscribe( { query: `{ __schema { queryType { kind } } }`, }, { next: () => { this.setState({ valid: true }) unsubscribe() }, error: () => { this.setState({ valid: false }); unsubscribe() }, complete: () => {}, }, ); return; } fetch(this.state.endpoint, { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `{ __schema { queryType { kind } } }`, }), }) .then(res => { this.setState({ valid: res.status < 400 }) }) .catch(err => { this.setState({ valid: false }) }) } }, 500) as any constructor(props) { super(props) this.state = { endpoint: props.endpoint, } } componentDidMount() { this.checkEndpoint() } render() { const { valid } = this.state return ( GraphQL Playground
{valid && ( )}
) } private onChangeEndpoint = e => { this.setState({ endpoint: e.target.value }, this.checkEndpoint) } private submit = e => { e.preventDefault() this.close() } private close = () => { if (this.state.valid) { this.props.onRequestClose(this.state.endpoint) } } } const Wrapper = styled.div` box-sizing: border-box; ` const Form = styled.form` width: 100%; display: flex; flex: 1 1 auto; .button.button { padding-right: ${p => p.theme.sizes.small16}; padding-left: ${p => p.theme.sizes.small16}; background: #da1b7f; &:hover { background: ${p => p.theme.colours.purple}; } } ` interface InputProps { valid: boolean invalid: boolean } // prettier-ignore const Input = styled('input')` background: ${p => p.theme.colours.white10}; border-radius: ${p => p.theme.sizes.smallRadius}; padding: ${p => `${p.theme.sizes.small16} ${p.theme.sizes.medium25}`}; font-weight: ${p => p.theme.sizes.fontSemiBold}; color: white; font-size: 16px; display: block; width: 100%; text-align: center; flex: 1 1 auto; display: flex; transition: 250ms color; ${(p: any) => p.valid ? css` color: ${k => k.theme.colours.green}; ` : p.invalid ? css` color: ${k => k.theme.colours.red}; ` : `` } ` const Heading = styled.h1` margin-left: 38px; font-weight: 400; color: ${p => p.theme.colours.white80}; ` const LogoWrapper = styled.div` display: flex; justify-content: center; align-items: center; ` const Logo = styled.div` display: flex; justify-content: space-between; align-items: center; margin-bottom: 60px; img { width: 78px; height: 78px; } ` ================================================ FILE: packages/graphql-playground-react/src/components/FileEditor.tsx ================================================ import * as React from 'react' import { styled } from '../styled' import { QueryEditor } from './Playground/QueryEditor' import { createStructuredSelector } from 'reselect' import { getFile } from '../state/sessions/selectors' import { editFile } from '../state/sessions/actions' import EditorWrapper, { Container } from './Playground/EditorWrapper' import { connect } from 'react-redux' export interface Props { value: string onChange: (value: string) => void } class FileEditor extends React.Component { render() { return ( ) } } const mapStateToProps = createStructuredSelector({ value: getFile, }) export default connect( mapStateToProps, { onChange: editFile }, )(FileEditor) const Wrapper = styled.div` background: ${p => p.theme.editorColours.resultBackground}; position: relative; .variable-editor { height: 100% !important; } .CodeMirror { background: none !important; .CodeMirror-code { color: rgba(255, 255, 255, 0.7); } .cm-atom { color: rgba(42, 126, 210, 1); } } ` const QueryWrap = styled.div` display: flex; flex-direction: column; flex: 1; ` ================================================ FILE: packages/graphql-playground-react/src/components/GraphQLBinApp.tsx ================================================ import * as React from 'react' import { Provider, connect } from 'react-redux' import createStore from '../state/createStore' import 'isomorphic-fetch' import EndpointPopup from './EndpointPopup' import { styled, ThemeProvider, theme as styledTheme } from '../styled' import { Store } from 'redux' import PlaygroundWrapper from './PlaygroundWrapper' import { injectState } from '../state/workspace/actions' export const store: Store = createStore() function getParameterByName(name: string): string { const url = window.location.href name = name.replace(/[\[\]]/g, '\\$&') const regexa = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') const results = regexa.exec(url) if (!results || !results[2]) { return '' } return decodeURIComponent(results[2].replace(/\+/g, ' ')) } export interface Props { endpoint?: string subscriptionEndpoint?: string history?: any match?: any headers?: any } export interface State { endpoint?: string subscriptionEndpoint?: string shareUrl?: string loading: boolean headers: any } export interface ReduxProps { injectState: (state: any) => void } class GraphQLBinApp extends React.Component { constructor(props: Props & ReduxProps) { super(props) this.state = { endpoint: props.endpoint, subscriptionEndpoint: props.subscriptionEndpoint, loading: false, headers: props.headers || {}, } } UNSAFE_componentWillMount() { if (this.props.match.params.id) { if (this.props.match.params.id === 'new') { return } this.setState({ loading: true }) // DOM side-effect: // #loading-wrapper is a hardcoded DOM element in the HTML entrypoint const loadingWrapper = document.getElementById('loading-wrapper') if (loadingWrapper) { loadingWrapper.classList.remove('fadeOut') } setTimeout(() => { if (loadingWrapper) { loadingWrapper.remove() } }, 1000) fetch('https://api.graphqlbin.com', { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: ` query ($id: String!) { session(id: $id) { data endpoint } } `, variables: { id: this.props.match.params.id }, }), }) .then(res => res.json()) .then(res => { if (loadingWrapper) { loadingWrapper.classList.add('fadeOut') } if (!res.data || res.data.session === null) { location.href = `${location.origin}/v2/new` } const state = JSON.parse(res.data.session.data) this.props.injectState(state) this.setState({ endpoint: res.data.session.endpoint, loading: false, }) }) } } render() { let { endpoint, subscriptionEndpoint } = this.state // If no  endpoint passed tries to get one from url if (!endpoint) { endpoint = getParameterByName('endpoint') } if (!subscriptionEndpoint) { subscriptionEndpoint = getParameterByName('subscription') } return ( {this.state.loading ? null : !this.state.endpoint || this.state.endpoint.length === 0 ? ( ) : ( )} ) } private handleChangeEndpoint = endpoint => { this.setState({ endpoint }) localStorage.setItem('last-endpoint', endpoint) } } const ConnectedGraphQLBinApp = connect( null, { injectState }, )(GraphQLBinApp) // tslint:disable export default class GraphQLBinAppHOC extends React.Component { render() { return ( ) } } const Wrapper = styled.div` width: 100%; height: 100%; ` ================================================ FILE: packages/graphql-playground-react/src/components/HistoryPopup/HistoryChooser.tsx ================================================ import * as React from 'react' import { HistoryFilter } from '../../types' import { styled, withTheme, ThemeInterface } from '../../styled' import { Star, History } from '../Icons' interface Props { theme: ThemeInterface selectedFilter: HistoryFilter onSelectFilter: (filter: any) => void } /* tslint:disable */ const HistoryChooser = ({ selectedFilter, onSelectFilter, theme }: Props) => ( onSelectFilter('HISTORY')} > History onSelectFilter('STARRED')} > Starred ) export default withTheme(HistoryChooser) const Chooser = styled.div` display: flex; align-items: center; ` interface FilterProps { active: boolean } const Filter = styled('div')` box-sizing: content-box; height: 24px; z-index: ${p => (p.active ? 2 : 0)}; display: flex; align-items: center; margin: 0 -2px; padding: ${p => (p.active ? '7px 9px 8px 9px' : '5px 13px 6px 13px')}; background: ${p => p.active ? p.theme.colours.green : p.theme.colours.black07}; color: ${p => (p.active ? p.theme.colours.white : p.theme.colours.black30)}; font-size: 14px; font-weight: 600; text-transform: uppercase; border-radius: 2px; cursor: pointer; ` const FilterText = styled.p` margin-left: 6px; ` ================================================ FILE: packages/graphql-playground-react/src/components/HistoryPopup/HistoryHeader.tsx ================================================ import * as React from 'react' import HistoryChooser from './HistoryChooser' import { HistoryFilter } from '../../types' import { styled } from '../../styled' import SearchBox from '../Playground/DocExplorer/SearchBox' export interface Props { selectedFilter: HistoryFilter onSelectFilter: (filter: any) => void onSearch: (value: string) => void } export default (props: Props) => ( ) const HistoryHeader = styled.div` display: flex; justify-content: space-between; align-items: center; padding: 16px; background: ${p => p.theme.colours.black02}; ` ================================================ FILE: packages/graphql-playground-react/src/components/HistoryPopup/HistoryItems.tsx ================================================ import * as React from 'react' import { Session } from '../../state/sessions/reducers' import { OrderedMap } from 'immutable' import { Star } from '../Icons' import { styled, withTheme, ThemeInterface } from '../../styled' export interface Props { items: OrderedMap selectedItemIndex: string onItemSelect: (index: string) => void onItemStarToggled: (sessionId: string) => void searchTerm: string theme: ThemeInterface } /* tslint:disable */ export default withTheme( ({ items, onItemSelect, selectedItemIndex, onItemStarToggled, theme, }: Props) => ( {items .map((item, index) => ( onItemSelect(index)} > onItemStarToggled(item.id)} stroke={!item.starred ? 'rgb(221,171,0)' : undefined} fill={item.starred ? 'rgb(221,171,0)' : undefined} strokeWidth={0.5} width={25} height={25} /> {item.operationName || item.queryTypes.firstOperationName || 'New Session'} {item.queryTypes.query && Q} {item.queryTypes.mutation && M} {item.queryTypes.subscription && ( S )} {item.date && ( )} )) .toArray() .map(x => x[1])} ), ) const HistoryItems = styled.div` overflow-y: scroll; max-height: calc(100vh - 121px); ` interface ItemProps { active: boolean } const HistoryItem = styled('div')` display: flex; align-items: center; justify-content: space-between; padding: 25px 20px; cursor: pointer; border-bottom: 1px solid; border-color: ${p => p.theme.colours.black10}; background: ${p => p.active ? p.theme.colours.black04 : p.theme.colours.white}; ` const Operation = styled.div` display: flex; align-items: center; margin-left: 20px; ` const OperationSide = styled.div` display: flex; align-items: center; ` const OperationText = styled.p` font-weight: 300; font-size: 20px; margin-right: 16px; ` const OperationIcon = styled.div` height: 21px; width: 21px; display: flex; justify-content: center; align-items: center; margin-right: 4px; border-radius: 2px; font-weight: 700; font-size: 12px; color: ${p => p.theme.colours.white}; ` const QueryIcon = styled(OperationIcon)` background: ${p => p.theme.colours.blue}; ` const MutationIcon = styled(OperationIcon)` background: ${p => p.theme.colours.orange}; ` const SubscriptionIcon = styled(OperationIcon)` background: ${p => p.theme.colours.purple}; ` const Time = styled.time` color: ${p => p.theme.colours.black40}; font-size: 14px; margin-left: 16px; ` ================================================ FILE: packages/graphql-playground-react/src/components/HistoryPopup.tsx ================================================ import * as React from 'react' import * as Modal from 'react-modal' import HistoryHeader from './HistoryPopup/HistoryHeader' import { HistoryFilter } from '../types' import HistoryItems from './HistoryPopup/HistoryItems' import { modalStyle } from '../constants' import { QueryEditor } from './Playground/QueryEditor' import { styled } from '../styled' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getHistory } from '../state/history/selectors' import { getHistoryOpen } from '../state/general/selectors' import { closeHistory, openHistory } from '../state/general/actions' import { duplicateSession } from '../state/sessions/actions' import { toggleHistoryItemStarring } from '../state/history/actions' import { Session } from '../state/sessions/reducers' import { OrderedMap } from 'immutable' import { Container } from './Playground/EditorWrapper' import { ArrowRight } from './Icons' export interface ReduxProps { isOpen: boolean closeHistory: () => void items: OrderedMap toggleHistoryItemStarring: (sessionId: string) => void duplicateSession: (session: Session) => void } export interface State { selectedFilter: HistoryFilter selectedItemIndex: string searchTerm: string } class HistoryPopup extends React.Component { constructor(props: ReduxProps) { super(props) const selectedItemIndex: any = props.items.keySeq().first() || '' this.state = { selectedFilter: 'HISTORY', selectedItemIndex, searchTerm: '', } } render() { const { searchTerm, selectedFilter } = this.state const items = this.props.items.filter(item => { return selectedFilter === 'STARRED' ? item.starred : true && (searchTerm && searchTerm.length > 0 ? item.query.toLowerCase().includes(searchTerm.toLowerCase()) : true) }) let selectedItem = this.props.items.get( this.state.selectedItemIndex!, ) as any selectedItem = selectedItem && selectedItem.toJS ? selectedItem.toJS() : undefined return ( {Boolean(selectedItem) ? ( Use ) : ( No History yet )} ) } private handleClickUse = () => { const { items } = this.props const selectedItem = items.get(this.state.selectedItemIndex)! this.props.duplicateSession(selectedItem) this.props.closeHistory() } private handleItemSelect = (index: string) => { this.setState({ selectedItemIndex: index } as State) } private handleSelectFilter = (filter: HistoryFilter) => { this.setState({ selectedFilter: filter } as State) } private handleSearch = (term: string) => { this.setState({ searchTerm: term } as State) } } const mapStateToProps = createStructuredSelector({ items: getHistory, isOpen: getHistoryOpen, }) export default connect( mapStateToProps, { closeHistory, openHistory, duplicateSession, toggleHistoryItemStarring, }, )(HistoryPopup) const Wrapper = styled.div` display: flex; min-height: 500px; ` const Left = styled.div` flex: 1; background: white; ` const Right = styled.div` flex: 0 0 464px; z-index: 2; ` const RightHeader = styled.div` display: flex; justify-content: space-between; align-items: center; padding-left: ${p => p.theme.sizes.medium25}; padding-right: ${p => p.theme.sizes.medium25}; padding-top: 20px; padding-bottom: 20px; background: ${p => p.theme.editorColours.resultBackground}; ` const RightEmpty = styled.div` height: 100%; display: flex; justify-content: center; align-items: center; background: ${p => p.theme.editorColours.resultBackground}; ` const RightEmptyText = styled.div` font-size: 16px; color: ${p => p.theme.editorColours.text}; ` const View = styled.div` font-size: ${p => p.theme.sizes.fontSmall}; font-weight: ${p => p.theme.sizes.fontSemiBold}; text-transform: uppercase; color: rgba(255, 255, 255, 0.4); ` const Use = styled.div` display: flex; align-items: center; padding-top: ${p => p.theme.sizes.small10}; padding-bottom: ${p => p.theme.sizes.small10}; padding-left: ${p => p.theme.sizes.small16}; padding-right: ${p => p.theme.sizes.small16}; font-size: ${p => p.theme.sizes.fontSmall}; font-weight: ${p => p.theme.sizes.fontSemiBold}; border-radius: ${p => p.theme.sizes.smallRadius}; background: ${p => p.theme.colours.green}; cursor: pointer; ` const UseText = styled.div` margin-right: ${p => p.theme.sizes.small6}; color: white; ` const Big = styled.div` height: calc(100% - 81px); display: flex; flex: 1 1 auto; ` const GraphiqlWrapper = styled(Big)` width: 100%; height: 100%; position: relative; display: flex; flex: 1 1 auto; ` const QueryWrap = styled.div` display: flex; flex-direction: column; flex: 1; ` ================================================ FILE: packages/graphql-playground-react/src/components/Icons/index.tsx ================================================ import * as React from 'react' interface IProps { title?: string color?: string width?: number height?: number stroke?: string fill?: string strokeWidth?: number className?: string children?: any viewBox?: string y?: string x?: string onClick?: () => void } const Svg = ({ title, children, ...props }: IProps) => ( {title ? {title} : undefined} {children} ) export const AddIcon = (props: IProps) => ( ) export const AddFullIcon = (props: IProps) => ( ) export const FullArrowRightIcon = (props: IProps) => ( ) // export const ArrowRight = ({ width, height, color }: IProps) => ( // // // // ) export const SettingsIcon = (props: IProps) => ( ) export const CrossIcon = (props: IProps) => ( ) export const ArrowRight = (props: IProps) => ( ) export const History = (props: IProps) => ( ) export const Star = ({ height, width, stroke, fill, strokeWidth, onClick, ...props }: IProps) => ( ) export const Search = ({ height, width, strokeWidth, color, ...props }: IProps) => ( ) export const ShareIcon = ({ width, height, color, ...props }: IProps) => ( ) export const Triangle = (props: IProps) => ( ) ================================================ FILE: packages/graphql-playground-react/src/components/MiddlewareApp.tsx ================================================ import * as React from 'react' import { Provider } from 'react-redux' import createStore from '../state/createStore' import { getSettings } from '../state/workspace/reducers' import { setSettingsString } from '../state/general/actions' import PlaygroundWrapper, { PlaygroundWrapperProps } from './PlaygroundWrapper' const store = createStore() export default class MiddlewareApp extends React.Component< PlaygroundWrapperProps > { componentDidMount() { const initialSettings = getSettings(store.getState()) const mergedSettings = { ...initialSettings, ...this.props.settings } const settingsString = JSON.stringify(mergedSettings, null, 2) store.dispatch(setSettingsString(settingsString)) } render() { return ( ) } } ================================================ FILE: packages/graphql-playground-react/src/components/Modal.tsx ================================================ import * as React from 'react' import * as Modal from 'react-modal' import { modalStyle } from '../constants' export interface Props { isOpen: boolean contentLabel: string onRequestClose: () => void children: JSX.Element } const ModalComponent = ({ isOpen, onRequestClose, contentLabel, children, }: Props) => { return ( {children} ) } export default ModalComponent ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ConfigEditor.tsx ================================================ import * as React from 'react' import EditorWrapper from './EditorWrapper' import { styled } from '../../styled' export interface Props { value?: string onEdit?: (value: string) => void onPrettifyQuery?: () => void onRunQuery?: () => void editorTheme?: string readOnly?: boolean isYaml?: boolean } /** * The editor to edit json and yaml */ export class ConfigEditor extends React.Component { editor: any cachedValue: any node: HTMLDivElement ignoreChangeEvent: boolean constructor(props) { super(props) // 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 || '' } componentDidMount() { // Lazily require to ensure requiring GraphiQL outside of a Browser context // does not produce an error. const CodeMirror = require('codemirror') require('codemirror/addon/hint/show-hint') require('codemirror/addon/edit/matchbrackets') require('codemirror/addon/edit/closebrackets') require('codemirror/addon/fold/brace-fold') require('codemirror/addon/fold/foldgutter') require('codemirror/addon/lint/lint') require('codemirror/addon/search/searchcursor') require('codemirror/addon/search/jump-to-line') require('codemirror/addon/dialog/dialog') require('codemirror/keymap/sublime') require('codemirror/mode/yaml/yaml') require('codemirror-graphql/variables/hint') require('codemirror-graphql/variables/lint') require('codemirror-graphql/variables/mode') this.editor = CodeMirror(this.node, { value: this.props.value || '', lineNumbers: true, tabSize: 2, mode: this.props.isYaml ? 'yaml' : 'graphql-variables', theme: this.props.editorTheme || 'graphiql', keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, showCursorWhenSelecting: true, readOnly: this.props.readOnly ? 'nocursor' : false, foldGutter: { minFoldSize: 4, }, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], extraKeys: { 'Cmd-Space': () => this.editor.showHint({ completeSingle: false }), 'Ctrl-Space': () => this.editor.showHint({ completeSingle: false }), 'Alt-Space': () => this.editor.showHint({ completeSingle: false }), 'Shift-Space': () => this.editor.showHint({ completeSingle: false }), 'Cmd-Enter': () => { if (this.props.onRunQuery) { this.props.onRunQuery() } }, 'Ctrl-Enter': () => { if (this.props.onRunQuery) { this.props.onRunQuery() } }, 'Shift-Ctrl-P': () => { if (this.props.onPrettifyQuery) { this.props.onPrettifyQuery() } }, // Persistent search box in Query Editor 'Cmd-F': 'findPersistent', 'Ctrl-F': 'findPersistent', // Editor improvements 'Ctrl-Left': 'goSubwordLeft', 'Ctrl-Right': 'goSubwordRight', 'Alt-Left': 'goGroupLeft', 'Alt-Right': 'goGroupRight', }, }) this.editor.on('change', this.onEdit) this.editor.on('keyup', this.onKeyUp) } componentDidUpdate(prevProps) { // Ensure the changes caused by this update are not interpretted as // user-input changes which could otherwise result in an infinite // event loop. this.ignoreChangeEvent = true if ( this.props.value !== prevProps.value && this.props.value !== this.cachedValue ) { this.cachedValue = this.props.value this.editor.setValue(this.props.value) } this.ignoreChangeEvent = false } componentWillUnmount() { this.editor.off('change', this.onEdit) this.editor.off('keyup', this.onKeyUp) this.editor = null } render() { return ( ) } setNode = node => { this.node = node } /** * Public API for retrieving the CodeMirror instance from this * React component. */ getCodeMirror() { return this.editor } /** * Public API for retrieving the DOM client height for this component. */ getClientHeight() { return this.node && this.node.clientHeight } private onKeyUp = (cm, event) => { const code = event.keyCode if ( (code >= 65 && code <= 90) || // letters (!event.shiftKey && code >= 48 && code <= 57) || // numbers (event.shiftKey && code === 189) || // underscore (event.shiftKey && code === 222) // " ) { this.editor.execCommand('autocomplete') } } private onEdit = () => { if (!this.ignoreChangeEvent) { this.cachedValue = this.editor.getValue() if (this.props.onEdit) { this.props.onEdit(this.cachedValue) } } } } const Editor = styled.div` flex: 1; height: 100%; position: relative; .CodeMirror-linenumbers { background: ${p => p.theme.editorColours.resultBackground}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/Argument.tsx ================================================ import * as React from 'react' import { astFromValue, print } from 'graphql' import TypeLink from './TypeLink' export interface Props { arg: any x: number y: number showDefaultValue?: boolean sessionId: string } export default function Argument({ arg, showDefaultValue, x, y, }: // sessionId, Props) { return ( {' = '} {print(astFromValue(arg.defaultValue, arg.type))} ) } /> ) } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/ArgumentInline.tsx ================================================ import * as React from 'react' import { astFromValue, print, GraphQLList, GraphQLNonNull } from 'graphql' import { styled } from '../../../styled' export interface Props { arg: any showDefaultValue?: boolean } export default function Argument({ arg, showDefaultValue }: Props) { return ( {arg.name} {': '} {renderType(arg.type)} {arg.defaultValue !== undefined && showDefaultValue !== false && ( {' = '} {print(astFromValue(arg.defaultValue, arg.type))} )} ) } function renderType(type) { if (type instanceof GraphQLNonNull) { return ( {renderType(type.ofType)} {'!'} ) } if (type instanceof GraphQLList) { return ( {'['} {renderType(type.ofType)} {']'} ) } return {type.name} } const ArgumentLine = styled.div` margin-left: 16px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/ColumnDoc.tsx ================================================ import * as React from 'react' import { columnWidth } from '../../../constants' import { styled } from '../../../styled' export interface Props { children: any overflow?: boolean width?: number } const ColumnDoc = ({ children, overflow = true, width = columnWidth, }: Props) => { return ( {children} ) } export default ColumnDoc interface ColumnProps { verticalScroll: boolean } const Column = styled('div')` display: flex; flex: 0 0 auto; flex-flow: column; padding-bottom: 20px; border-right: 1px solid ${p => p.theme.colours.black10}; overflow-x: ${p => (p.verticalScroll ? 'hidden' : 'auto')} overflow-y: ${p => (p.verticalScroll ? 'scroll' : 'auto')} ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/DocTypeSchema.tsx ================================================ import { GraphQLInterfaceType } from 'graphql' import * as React from 'react' import TypeLink from './TypeLink' import { styled } from '../../../styled' export interface DocTypeSchemaProps { type: any fields: any[] interfaces: any[] level: number sessionId: string } export default ({ type, fields, interfaces, level }: DocTypeSchemaProps) => { const nonDeprecatedFields = fields.filter(data => !data.isDeprecated) const deprecatedFields = fields.filter(data => data.isDeprecated) const typeInstance = type instanceof GraphQLInterfaceType ? 'interface ' : 'type' return ( {typeInstance}{' '} {type.name}{' '} {interfaces.length === 0 && {`{`}} {interfaces.map((data, index) => ( implements} afterNode={ index === interfaces.length - 1 ? {'{'} : null } lastActive={false} /> ))} {nonDeprecatedFields.map((data, index) => ( ))} {deprecatedFields.length > 0 &&
} {deprecatedFields.map((data, index) => (
# Deprecated: {data.deprecationReason}
))} {'}'}
) } const DocTypeSchema = styled.div` font-size: 14px; overflow: auto; .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(TypeLink)` padding-left: 16px; .field-name { color: rgb(245, 160, 0); } .type-name { color: #f25c54; } ` const DocsValueComment = styled.p` 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}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsStyles.tsx ================================================ import * as React from 'react' import { styled } from '../../../styled' const Title = styled.div` color: rgba(0, 0, 0, 0.3); cursor: default; font-size: 14px; font-weight: 600; text-transform: uppercase !important; letter-spacing: 1px; padding: 16px; user-select: none; ` export const CategoryTitle = ({ children }) => {children} ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/DocType.tsx ================================================ import { styled } from '../../../../styled' export const DocType = styled.div` padding: 20px 16px 0 16px; overflow: auto; font-size: 14px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/EnumTypeSchema.tsx ================================================ import * as React from 'react' import { styled } from '../../../../styled' import { DocType } from './DocType' export interface EnumTypeSchemaProps { type: any sdlType?: boolean } const EnumTypeSchema = ({ type, sdlType }: EnumTypeSchemaProps) => { const values = sdlType ? type._values : type.getValues() const deprecatedValues = values.filter((value: any) => value.isDeprecated) return ( enum{' '} {type.name}{' '} {'{'} {values .filter((value: any) => !value.isDeprecated) .map((value, index) => ( ))} {deprecatedValues.length > 0 &&
} {deprecatedValues.map((value, index) => ( ))} {'}'}
) } export default EnumTypeSchema interface ValueProps { value: any isDeprecated?: boolean first: boolean } const Value = ({ value, isDeprecated, first }: ValueProps) => (
{value.name}
{value.description && ( {value.description} )} {isDeprecated && ( Deprecated: {value.deprecationReason} )}
) interface DocsValueProps { first: boolean } const DocsValue = styled('div')` margin-top: ${p => (p.first ? 0 : 6)}px; .field-name { padding: 0 16px; color: red; } ` const DocsValueComment = styled.div` padding: 0 16px; color: ${p => p.theme.colours.black50}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/ScalarType.tsx ================================================ import * as React from 'react' import { DocType } from './DocType' export interface ScalarTypeSchemaProps { type: any } const ScalarTypeSchema = ({ type }: ScalarTypeSchemaProps) => { return ( scalar{' '} {type.name} ) } export default ScalarTypeSchema ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/DocsTypes/UnionTypeSchema.tsx ================================================ import TypeLink from '../TypeLink' import * as React from 'react' import { DocType } from './DocType' export interface EnumTypeSchemaProps { schema: any type: any level: number sessionId: string } const UnionTypeSchema = ({ schema, type, level, sessionId, }: EnumTypeSchemaProps) => { const types = schema.getPossibleTypes(type) return ( union{' '} {type.name} {' = '} {types.map((value, index) => ( ))} ) } export default UnionTypeSchema ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/ErrorContainer.tsx ================================================ 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%); ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/FieldDoc.tsx ================================================ import * as React from 'react' import Argument from './Argument' import { GraphQLInterfaceType, GraphQLEnumType, GraphQLUnionType, GraphQLScalarType, } from 'graphql' import MarkdownContent from 'graphiql/dist/components/DocExplorer/MarkdownContent' import TypeLink from './TypeLink' import DocTypeSchema from './DocTypeSchema' import ScalarTypeSchema from './DocsTypes/ScalarType' import EnumTypeSchema from './DocsTypes/EnumTypeSchema' import UnionTypeSchema from './DocsTypes/UnionTypeSchema' import { getDeeperType, serialize } from '../util/stack' import { CategoryTitle } from './DocsStyles' import { styled } from '../../../styled' export interface Props { schema: any field: any level: number sessionId: string } export interface State { showDeprecated: boolean } export default class FieldDoc extends React.Component { state = { showDeprecated: false } ref componentDidMount() { this.scrollToRight() } shouldComponentUpdate(nextProps) { if (this.props.field !== nextProps.field) { this.scrollToRight() return true } return false } scrollToRight() { const explorer = this.ref const explorerDoc: any = explorer.parentNode && explorer.parentNode.parentNode // TODO see browser compatibility scrollWidth && scrollLeft scrollToRight(explorerDoc, explorerDoc.scrollWidth, 50) } setRef = ref => { this.ref = ref } render() { const { schema, field, level } = this.props let type = field.type || field const obj = serialize(schema, field) type = getDeeperType(type) const argsOffset = obj.fields.length + obj.interfaces.length const implementationsOffset = obj.fields.length + obj.interfaces.length + obj.args.length let typeInstance if (type instanceof GraphQLInterfaceType) { typeInstance = 'interface' } else if (type instanceof GraphQLUnionType) { typeInstance = 'union' } else if (type instanceof GraphQLEnumType) { typeInstance = 'enum' } else { typeInstance = 'type' } return (
{`${typeInstance} details`} {type.description && type.description.length > 0 && ( )} {type instanceof GraphQLScalarType && } {type instanceof GraphQLEnumType && } {type instanceof GraphQLUnionType && ( )} {obj.fields && obj.fields.length > 0 && ( )} {obj.args && obj.args.length > 0 && (
arguments {obj.args.map((arg, index) => (
))}
)} {obj.implementations && obj.implementations.length > 0 && (
implementations {obj.implementations.map((data, index) => ( ))}
)}
) } } const scrollToRight = (element: Element, to: number, duration: number) => { if (duration <= 0) { return } const difference = to - element.scrollLeft const perTick = (difference / duration) * 10 setTimeout(() => { element.scrollLeft = element.scrollLeft + perTick if (element.scrollLeft === to) { return } scrollToRight(element, to, duration - 10) }, 10) } const DocsHeader = styled.div` background: ${p => p.theme.colours.black02}; padding-top: 20px; padding-bottom: 10px; .doc-category-item { font-size: 14px; font-weight: 600; word-wrap: break-word; } .doc-category-item .field-name { color: #f25c54; } div { background: transparent; pointer-events: none; } ` const DocsDescription = styled(MarkdownContent)` font-size: 14px; padding: 0 16px 20px 16px; color: rgba(0, 0, 0, 0.5); ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/GraphDocs.tsx ================================================ import * as React from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import * as keycode from 'keycode' import FieldDoc from './FieldDoc' import ColumnDoc from './ColumnDoc' import { addStack, toggleDocs, changeWidthDocs, changeKeyMove, setDocsVisible, } from '../../../state/docs/actions' import Spinner from '../../Spinner' import { columnWidth } from '../../../constants' import RootColumn from './RootColumn' import { serialize, getElementRoot, serializeRoot, getElement, } from '../util/stack' 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 { docs: { navStack: any[] docsOpen: boolean docsWidth: number keyMove: boolean } } interface DispatchFromProps { addStack: (sessionId: string, field: any, x: number, y: number) => any toggleDocs: (sessionId: string) => any setDocsVisible: (sessionId: string, open: boolean) => any changeWidthDocs: (sessionId: string, width: number) => any changeKeyMove: (sessionId: string, move: boolean) => any } export interface State { searchValue: string widthMap: any } class GraphDocs extends React.Component< SideTabContentProps & StateFromProps & DispatchFromProps, State > { ref // private refDocExplorer: any; constructor(props) { super(props) this.state = { searchValue: '', widthMap: {}, } ;(window as any).d = this } UNSAFE_componentWillReceiveProps(nextProps: SideTabContentProps & StateFromProps) { // If user use default column size % columnWidth // Make the column follow the clicks if ( this.props.docs.navStack.length !== nextProps.docs.navStack.length || this.props.docs.navStack.slice(-1)[0] !== nextProps.docs.navStack.slice(-1)[0] || (!this.props.schema && nextProps.schema) ) { this.setWidth(nextProps) } } setWidth(props: any = this.props) { this.props.setWidth(props) } getWidth(props: any = this.props) { const rootWidth = this.state.widthMap.root || columnWidth const stackWidths = props.docs.navStack.map( stack => this.state.widthMap[stack.field.path] || columnWidth, ) return [rootWidth].concat(stackWidths).reduce((acc, curr) => acc + curr, 0) } componentDidMount() { this.setWidth() } render() { const { navStack } = this.props.docs const { schema } = 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'} } return ( {emptySchema && {emptySchema}} {!emptySchema && schema && ( )} {navStack.map((stack, index) => ( ))} ) } setRef = ref => { this.ref = ref } public showDocFromType = type => { this.props.addStack(this.props.sessionId, type, 0, 0) } private handleSearch = (value: string) => { this.setState({ searchValue: value }) } 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 } e.preventDefault() this.props.changeKeyMove(this.props.sessionId, true) const lastNavStack = this.props.docs.navStack.length > 0 && this.props.docs.navStack[this.props.docs.navStack.length - 1] const beforeLastNavStack = this.props.docs.navStack.length > 0 && this.props.docs.navStack[this.props.docs.navStack.length - 2] const keyPressed = keycode(e) switch (keyPressed) { case 'esc': this.props.setDocsVisible(this.props.sessionId, false) break case 'left': if (beforeLastNavStack) { this.props.addStack( this.props.sessionId, beforeLastNavStack.field, beforeLastNavStack.x, beforeLastNavStack.y, ) } break case 'right': if (lastNavStack) { const obj = serialize(this.props.schema, lastNavStack.field) const firstElement = getElement(obj, 0) if (firstElement) { this.props.addStack( this.props.sessionId, firstElement, lastNavStack.x + 1, 0, ) } } else { const obj = serializeRoot(this.props.schema) const element = getElementRoot(obj, 0) if (element) { this.props.addStack(this.props.sessionId, element, 0, 0) } } break case 'up': case 'down': if (beforeLastNavStack) { const obj = serialize(this.props.schema, beforeLastNavStack.field) const element = getElement( obj, keyPressed === 'up' ? lastNavStack.y - 1 : lastNavStack.y + 1, ) if (element) { this.props.addStack( this.props.sessionId, element, lastNavStack.x, keyPressed === 'up' ? lastNavStack.y - 1 : lastNavStack.y + 1, ) } } else { const obj = serializeRoot(this.props.schema) const y = lastNavStack ? lastNavStack.y : 0 const element = getElementRoot( obj, keyPressed === 'up' ? y - 1 : y + 1, ) if (element) { this.props.addStack( this.props.sessionId, element, 0, keyPressed === 'up' ? y - 1 : y + 1, ) } } break } } } const mapDispatchToProps = dispatch => bindActionCreators( { addStack, toggleDocs, changeWidthDocs, changeKeyMove, setDocsVisible, }, dispatch, ) const mapStateToProps = createStructuredSelector({ docs: getSessionDocs, sessionId: getSelectedSessionIdFromRoot, }) export default connect( mapStateToProps, mapDispatchToProps, null, { forwardRef: true }, )(GraphDocs) const DocsExplorerContainer = styled.div` display: flex; position: relative; height: 100%; width: 100%; overflow-x: auto; overflow-y: hidden; outline: none !important; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/GraphDocsRoot.tsx ================================================ import * as React from 'react' import TypeLink from './TypeLink' import { serializeRoot } from '../util/stack' import { CategoryTitle } from './DocsStyles' import { styled } from '../../../styled' export interface Props { schema: any sessionId: string } export default class GraphDocsRoot extends React.PureComponent { render() { const { schema, sessionId } = this.props const obj = serializeRoot(schema) return ( {obj.mutations.length > 0 && ( )} {obj.subscriptions.length > 0 && ( )} ) } } interface ShowRootTypeProps { name: string fields: any[] offset: number sessionId: string } function ShowRootType({ name, fields, offset }: ShowRootTypeProps) { const nonDeprecatedFields = fields.filter(data => !data.isDeprecated) const deprecatedFields = fields.filter(data => data.isDeprecated) return (
{name} {nonDeprecatedFields.map((field, index) => ( ))} {deprecatedFields.length > 0 &&
} {deprecatedFields.map((field, index) => (
# Deprecated: {field.deprecationReason}
))}
) } const DocsRoot = styled.div` padding-left: 6px; .doc-category-item .field-name { color: #f25c54; } ` const DocsValueComment = styled.p` color: ${p => p.theme.colours.black50}; padding-right: 16px; padding-left: 16px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/RootColumn.tsx ================================================ import * as React from 'react' import ColumnDoc from './ColumnDoc' import SearchResults from './SearchResults' import GraphDocsRoot from './GraphDocsRoot' import SearchBox from './SearchBox' import { styled } from '../../../styled' export interface Props { searchValue: string schema: any width: number handleSearch: (value: string) => void sessionId: string } export default class RootColumn extends React.PureComponent { render() { const { searchValue, schema, width, sessionId, handleSearch } = this.props return ( {searchValue && ( )} {!searchValue && ( )} ) } } const Column = styled.div` overflow: auto; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/SchemaDoc.tsx ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import * as React from 'react' import TypeLink from 'graphiql/dist/components/DocExplorer/TypeLink' import MarkdownContent from 'graphiql/dist/components/DocExplorer/MarkdownContent' export interface Props { schema: any onClickType: any } // Render the top level Schema export default class SchemaDoc extends React.Component { shouldComponentUpdate(nextProps) { return this.props.schema !== nextProps.schema } render() { const schema = this.props.schema const queryType = schema.getQueryType() const mutationType = schema.getMutationType && schema.getMutationType() const subscriptionType = schema.getSubscriptionType && schema.getSubscriptionType() return (
{'root types'}
{'query'} {': '}
{mutationType && (
{'mutation'} {': '}
)} {subscriptionType && (
{'subscription'} {': '}
)}
) } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/SearchBox.tsx ================================================ import * as React from 'react' import debounce from 'graphiql/dist/utility/debounce' import { Search } from '../../Icons' import { styled } from '../../../styled' export interface Props { onSearch: (value: string) => void placeholder?: string clean?: boolean } export interface State { value: string } export default class SearchBox extends React.Component { private debouncedOnSearch: any constructor(props) { super(props) this.state = { value: '' } this.debouncedOnSearch = debounce(200, () => { this.props.onSearch(this.state.value) }) } shouldComponentUpdate(nextProps, nextState) { return nextState.value !== this.state.value } render() { const LabelComponent = ( ) if (this.props.clean) { return LabelComponent } return {LabelComponent} } handleChange = event => { this.setState({ value: event.target.value }) this.debouncedOnSearch() } } const SearchContainer = styled.div` position: relative; flex: 0 0 auto; z-index: 1; display: flex; margin-left: 6px; padding: 25px; background: ${p => p.theme.colours.black02}; border-bottom: 1px solid ${p => p.theme.colours.black10}; div { width: 100%; } ` const Label = styled.div` box-sizing: border-box; display: flex; align-items: center; padding: 12px 14px 13px 15px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); background: ${p => p.theme.colours.white}; ` const Input = styled.input` font-size: 16px; margin-left: 10px; &::placeholder { color: ${p => p.theme.colours.black30}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/SearchResults.tsx ================================================ import * as React from 'react' import { styled } from '../../../styled' import TypeLink from './TypeLink' export interface Props { schema: any withinType?: any searchValue: string level: number sessionId: string } export default class SearchResults extends React.Component { shouldComponentUpdate(nextProps) { return ( this.props.schema !== nextProps.schema || this.props.searchValue !== nextProps.searchValue ) } render() { const { level } = this.props const searchValue = this.props.searchValue const withinType = this.props.withinType const schema = this.props.schema const matchedWithin: any[] = [] const matchedTypes: any[] = [] const matchedFields: any[] = [] const typeMap = schema.getTypeMap() let typeNames = Object.keys(typeMap) // Move the within type name to be the first searched. if (withinType) { typeNames = typeNames.filter(n => n !== withinType.name) typeNames.unshift(withinType.name) } let count = 0 for (const typeName of typeNames) { if ( matchedWithin.length + matchedTypes.length + matchedFields.length >= 100 ) { break } const type = typeMap[typeName] if (withinType !== type && isMatch(typeName, searchValue)) { matchedTypes.push(
, ) } if (type.getFields) { const fields = type.getFields() Object.keys(fields).forEach(fieldName => { const field = fields[fieldName] field.parent = type let matchingArgs if (!isMatch(fieldName, searchValue)) { if (field.args && field.args.length) { matchingArgs = field.args.filter(arg => isMatch(arg.name, searchValue), ) if (matchingArgs.length === 0) { return } } else { return } } const match = (
) if (withinType === type) { matchedWithin.push(match) } else { matchedFields.push(match) } }) } } if ( matchedWithin.length + matchedTypes.length + matchedFields.length === 0 ) { return No results found. } if (withinType && matchedTypes.length + matchedFields.length > 0) { return (
{matchedWithin}
{'other results'}
{matchedTypes} {matchedFields}
) } return (
{matchedWithin} {matchedTypes} {matchedFields}
) } } function isMatch(sourceText, searchValue) { try { const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, ch => '\\' + ch) return sourceText.search(new RegExp(escaped, 'i')) !== -1 } catch (e) { return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1 } } const NoResult = styled.span` display: block; margin-top: 16px; margin-left: 16px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/DocExplorer/TypeLink.tsx ================================================ import * as React from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { GraphQLList, GraphQLNonNull, isType } from 'graphql' import ArgumentInline from './ArgumentInline' import { Triangle } from '../../Icons' import { toJS } from '../util/toJS' import { addStack } from '../../../state/docs/actions' import { getSessionDocsState } from '../../../state/docs/selectors' import { // getSelectedSessionId, getSelectedSessionIdFromRoot, } from '../../../state/sessions/selectors' import { createSelector } from 'reselect' import { styled } from '../../../styled' interface ReduxProps { keyMove: boolean isActive: boolean } interface DispatchFromProps { addStack: (sessionId: string, field: any, x: number, y: number) => any } export interface Props { type: any // X position in the list x: number // Y position in the list y: number clickable?: boolean className?: string beforeNode?: JSX.Element | null | false afterNode?: JSX.Element | null | false onSetWidth?: (width: number) => void showParentName?: boolean collapsable?: boolean lastActive: boolean sessionId?: string } interface State { collapsed: boolean } class TypeLink extends React.Component< Props & ReduxProps & DispatchFromProps, State > { static defaultProps: Partial = { clickable: true, collapsable: false, } private ref: any constructor(props) { super(props) this.state = { collapsed: false, } } shouldComponentUpdate(nextProps: Props & ReduxProps, nextState: State) { return ( this.props.type !== nextProps.type || this.props.keyMove !== nextProps.keyMove || this.props.isActive !== nextProps.isActive || this.state.collapsed !== nextState.collapsed ) } onClick = () => { if (this.props.clickable) { this.props.addStack( this.props.sessionId, this.props.type, this.props.x, this.props.y, ) } } componentDidMount() { this.updateSize() } componentDidUpdate() { this.updateSize() } updateSize() { if (this.ref) { if (typeof this.props.onSetWidth === 'function') { this.props.onSetWidth(this.ref.scrollWidth) } const LINE_HEIGHT = 31 if ( this.ref.scrollHeight > LINE_HEIGHT && !this.state.collapsed && this.props.collapsable ) { this.setState({ collapsed: true }) } } } setRef = ref => { this.ref = ref } render() { const { type, clickable, className, beforeNode, afterNode, showParentName, isActive, } = 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 && [ '(', {this.state.collapsed ? ( ... ) : ( type.args.map(arg => ( )) )} , ')', ]} {': '} )} {renderType(type.type || type)} {type.defaultValue !== undefined ? ( {' '} = {`${JSON.stringify(type.defaultValue, null, 2)}`} ) : ( undefined )} {clickable && ( )} {afterNode && ' '} {afterNode} ) } } function renderType(type) { if (type instanceof GraphQLNonNull) { return ( {renderType(type.ofType)} {'!'} ) } if (type instanceof GraphQLList) { return ( {'['} {renderType(type.ofType)} {']'} ) } return {type.name} } const mapStateToProps = (state, { x, y }) => { const docs = getSessionDocsState(state) const sessionId = getSelectedSessionIdFromRoot(state) if (docs) { const nav = docs.navStack.get(x) if (nav) { const isActive = nav.get('x') === x && nav.get('y') === y return { isActive, keyMove: docs.keyMove, lastActive: isActive && x === docs.navStack.length - 1, sessionId, } } } return { isActive: false, keyMove: false, lastActive: false, sessionId, } } const selector = createSelector([mapStateToProps], s => s) const mapDispatchToProps = dispatch => bindActionCreators( { addStack, }, dispatch, ) export default connect( selector, mapDispatchToProps, )(toJS(TypeLink)) 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 Dots = styled.span` font-weight: 600; ` const IconBox = styled.div` position: absolute; right: 10px; top: 50%; transform: translateY(-50%); ` const DefaultValue = styled.span` color: ${p => p.theme.colours.black30}; span { color: #1f61a9; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/EditorWrapper.tsx ================================================ import * as React from 'react' import { styled, createGlobalStyle } from '../../styled' const EditorWrapper = styled.div` /* Comment */ .cm-comment { color: ${p => p.theme.editorColours.comment}; } /* Punctuation */ .cm-punctuation { color: ${p => p.theme.editorColours.punctuation}; } /* Proppery */ .cm-property { color: ${p => p.theme.editorColours.property}; } /* Keyword */ .cm-keyword { color: ${p => p.theme.editorColours.keyword}; } /* OperationName, FragmentName */ .cm-def { color: ${p => p.theme.editorColours.def}; } /* FieldAlias */ .cm-qualifier { color: ${p => p.theme.editorColours.def}; } /* ArgumentName and ObjectFieldName */ .cm-attribute { color: ${p => p.theme.editorColours.attribute}; } /* Number */ .cm-number { color: ${p => p.theme.editorColours.number}; } /* String */ .cm-string { color: ${p => p.theme.editorColours.string}; } /* Boolean */ .cm-builtin { color: ${p => p.theme.editorColours.builtin}; } /* EnumValue */ .cm-string-2 { color: ${p => p.theme.editorColours.string2}; } /* Variable */ .cm-variable { color: ${p => p.theme.editorColours.variable}; } /* Directive */ .cm-meta { color: ${p => p.theme.editorColours.meta}; } /* Type */ .cm-atom { color: ${p => p.theme.editorColours.atom}; } /* Comma */ .cm-ws { color: ${p => p.theme.editorColours.ws}; } position: relative; display: flex; flex: 1 1 0%; flex-flow: column; .CodeMirror { color: rgba(255, 255, 255, 0.3); font-family: ${p => p.theme.settings['editor.fontFamily']}; font-size: ${p => `${p.theme.settings['editor.fontSize']}px`}; height: 100%; left: 0; position: absolute; top: 0; width: 100%; } .CodeMirror-lines { padding: 20px 0; } .CodeMirror-gutters { border-right: none; } .CodeMirror span[role='presentation'] { color: ${p => p.theme.colours.text}; } /* CURSOR */ .CodeMirror div.CodeMirror-cursor { background: ${p => p.theme.settings['editor.cursorShape'] === 'block' ? p.theme.editorColours.cursorColor : 'transparent'}; border-left: ${p => p.theme.settings['editor.cursorShape'] === 'line' ? `1px solid ${p.theme.editorColours.cursorColor}` : 0}; border-bottom: ${p => p.theme.settings['editor.cursorShape'] === 'underline' ? `1px solid ${p.theme.editorColours.cursorColor}` : 0}; } /* Shown when moving in bi-directional text */ .CodeMirror div.CodeMirror-secondarycursor { border-left: 1px solid silver; } .CodeMirror.cm-fat-cursor div.CodeMirror-cursor { background: rgba(255, 255, 255, 0.6); color: white; border: 0; width: auto; } .CodeMirror.cm-fat-cursor div.CodeMirror-cursors { z-index: 1; } .cm-animate-fat-cursor { -webkit-animation: blink 1.06s steps(1) infinite; animation: blink 1.06s steps(1) infinite; border: 0; width: auto; } @-webkit-keyframes blink { 0% { background: #7e7; } 50% { background: none; } 100% { background: #7e7; } } @keyframes blink { 0% { background: #7e7; } 50% { background: none; } 100% { background: #7e7; } } .CodeMirror-foldmarker { border-radius: 4px; background: #08f; background: linear-gradient(#43a8ff, #0f83e8); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); color: white; font-family: arial; font-size: 12px; line-height: 0; margin: 0 3px; padding: 0px 4px 1px; text-shadow: 0 -1px rgba(0, 0, 0, 0.1); } div.CodeMirror span.CodeMirror-matchingbracket { /* color: rgba(255, 255, 255, 0.4); */ text-decoration: underline; } div.CodeMirror span.CodeMirror-nonmatchingbracket { color: rgb(242, 92, 84); } .toolbar-button { background: #fdfdfd; background: linear-gradient(#fbfbfb, #f8f8f8); border-color: #d3d3d3 #d0d0d0 #bababa; border-radius: 4px; border-style: solid; border-width: 0.5px; box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.13), inset 0 1px #fff; color: #444; cursor: pointer; display: inline-block; margin: 0 5px 0; padding: 2px 8px 4px; text-decoration: none; } .toolbar-button:active { background: linear-gradient(#ececec, #d8d8d8); border-color: #cacaca #c9c9c9 #b0b0b0; box-shadow: 0 1px 0 #fff, inset 0 1px rgba(255, 255, 255, 0.2), inset 0 1px 1px rgba(0, 0, 0, 0.08); } .toolbar-button.error { background: linear-gradient(#fdf3f3, #e6d6d7); color: #b00; } .autoInsertedLeaf.cm-property { -webkit-animation-duration: 6s; animation-duration: 6s; -webkit-animation-name: insertionFade; animation-name: insertionFade; border-bottom: 2px solid rgba(255, 255, 255, 0); border-radius: 2px; margin: -2px -4px -1px; padding: 2px 4px 1px; } @-webkit-keyframes insertionFade { from, to { background: rgba(255, 255, 255, 0); border-color: rgba(255, 255, 255, 0); } 15%, 85% { background: #fbffc9; border-color: #f0f3c0; } } @keyframes insertionFade { from, to { background: rgba(255, 255, 255, 0); border-color: rgba(255, 255, 255, 0); } 15%, 85% { background: #fbffc9; border-color: #f0f3c0; } } .CodeMirror pre { padding: 0 4px; /* Horizontal padding of content */ } .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { background-color: white; /* The little square between H and V scrollbars */ } /* GUTTER */ .CodeMirror-gutters { background-color: transparent; border: none; white-space: nowrap; } .CodeMirror-linenumbers { background: ${p => p.theme.editorColours.editorBackground}; } .CodeMirror-linenumber { font-family: Open Sans, sans-serif; font-weight: 600; font-size: ${p => `${p.theme.settings['editor.fontSize'] - 2}px`}; color: ${p => p.theme.colours.textInactive}; min-width: 20px; padding: 0 3px 0 5px; text-align: right; white-space: nowrap; } .CodeMirror-guttermarker { color: black; } .CodeMirror-guttermarker-subtle { color: #999; } .cm-tab { display: inline-block; text-decoration: inherit; } .CodeMirror-ruler { border-left: 1px solid #ccc; position: absolute; } .cm-negative { color: #d44; } .cm-positive { color: #292; } .cm-header, .cm-strong { font-weight: bold; } .cm-em { font-style: italic; } .cm-link { text-decoration: underline; } .cm-strikethrough { text-decoration: line-through; } .cm-s-default .cm-error { color: #f00; } .cm-invalidchar { color: #f00; } .CodeMirror-composing { border-bottom: 2px solid; } .CodeMirror-matchingtag { background: rgba(255, 150, 0, 0.3); } .CodeMirror-activeline-background { background: #e8f2ff; } /* The rest of this file contains styles related to the mechanics of the editor. You probably shouldn't touch them. */ .CodeMirror { background: white; overflow: hidden; line-height: 1.6; } .CodeMirror-scroll { height: 100%; /* 30px is the magic margin used to hide the element's real scrollbars */ /* See overflow: hidden in .CodeMirror */ /* margin-bottom: -30px; margin-right: -30px; */ outline: none; /* Prevent dragging from highlighting the element */ overflow: hidden; /* padding-bottom: 30px; */ position: relative; &:hover { overflow: scroll !important; } } .CodeMirror-sizer { border-right: 30px solid transparent; position: relative; } /* The fake, visible scrollbars. Used to force redraw during scrolling before actual scrolling happens, thus preventing shaking and flickering artifacts. */ .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { display: none !important; position: absolute; z-index: 6; } .CodeMirror-vscrollbar { overflow-x: hidden; overflow-y: scroll; right: 0; top: 0; } .CodeMirror-hscrollbar { bottom: 0; left: 0; overflow-x: scroll; overflow-y: hidden; } .CodeMirror-scrollbar-filler { right: 0; bottom: 0; } .CodeMirror-gutter-filler { left: 0; bottom: 0; } .CodeMirror-gutters { min-height: 100%; position: absolute; left: 0; top: 0; z-index: 3; margin-left: 3px; } .CodeMirror-gutter { display: inline-block; height: 100%; margin-bottom: -30px; vertical-align: top; white-space: normal; /* Hack to make IE7 behave */ *zoom: 1; *display: inline; } .CodeMirror-gutter-wrapper { background: none !important; border: none !important; position: absolute; z-index: 4; } .CodeMirror-gutter-background { position: absolute; top: 0; bottom: 0; z-index: 4; } .CodeMirror-gutter-elt { cursor: default; position: absolute; z-index: 4; } .CodeMirror-gutter-wrapper { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .CodeMirror-lines { cursor: text; min-height: 1px; /* prevents collapsing before first draw */ } .CodeMirror pre { -webkit-tap-highlight-color: transparent; /* Reset some styles that the rest of the page might have set */ background: transparent; border-radius: 0; border-width: 0; color: inherit; font-family: inherit; font-size: inherit; -webkit-font-variant-ligatures: none; font-variant-ligatures: none; line-height: inherit; margin: 0; overflow: visible; position: relative; white-space: pre; word-wrap: normal; z-index: 2; } .CodeMirror-wrap pre { word-wrap: break-word; white-space: pre-wrap; word-break: normal; } .CodeMirror-linebackground { position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 0; } .CodeMirror-linewidget { overflow: auto; position: relative; z-index: 2; } .CodeMirror-widget { } .CodeMirror-code { outline: none; } /* Force content-box sizing for the elements where we expect it */ .CodeMirror-scroll, .CodeMirror-sizer, .CodeMirror-gutter, .CodeMirror-gutters, .CodeMirror-linenumber { box-sizing: content-box; } .CodeMirror-measure { height: 0; overflow: hidden; position: absolute; visibility: hidden; width: 100%; } .CodeMirror-cursor { position: absolute; } .CodeMirror-measure pre { position: static; } div.CodeMirror-cursors { position: relative; visibility: hidden; z-index: 3; } div.CodeMirror-dragcursors { visibility: visible; } .CodeMirror-focused div.CodeMirror-cursors { visibility: visible; } .CodeMirror-selected { background: ${p => p.theme.editorColours.selection}; } .CodeMirror-focused .CodeMirror-selected { background: ${p => p.theme.editorColours.selection}; } .CodeMirror-crosshair { cursor: crosshair; } .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: ${p => p.theme.editorColours.selection}; } .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: ${p => p.theme.editorColours.selection}; } .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: ${p => p.theme.editorColours.selection}; } .cm-searching { background: #ffa; background: rgba(255, 255, 0, 0.4); } /* IE7 hack to prevent it from returning funny offsetTops on the spans */ .CodeMirror span { *vertical-align: text-bottom; } /* Used to force a border model for a node */ .cm-force-border { padding-right: 0.1px; } @media print { /* Hide the cursor when printing */ .CodeMirror div.CodeMirror-cursors { visibility: hidden; } } /* See issue #2901 */ .cm-tab-wrap-hack:after { content: ''; } /* Help users use markselection to safely style text background */ span.CodeMirror-selectedtext { background: none; } .CodeMirror-dialog { background: inherit; color: inherit; left: 0; right: 0; overflow: hidden; padding: 0.1em 0.8em; position: absolute; z-index: 15; } .CodeMirror-dialog-top { border-bottom: 1px solid #eee; top: 0; } .CodeMirror-dialog-bottom { border-top: 1px solid #eee; bottom: 0; } .CodeMirror-dialog input { background: transparent; border: 1px solid #d3d6db; color: inherit; font-family: monospace; outline: none; width: 20em; } .CodeMirror-dialog span.CodeMirror-search-label { color: ${p => p.theme.colours.text}; } .CodeMirror-dialog input.CodeMirror-search-field { color: ${p => p.theme.colours.text}; background: ${p => p.theme.colours.background}; } .CodeMirror-dialog button { font-size: 70%; } .CodeMirror-foldgutter { width: 0.7em; } .CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded { cursor: pointer; } .CodeMirror-foldgutter-open:after { content: '▾'; } .CodeMirror-foldgutter-folded:after { content: '▸'; } /* The lint marker gutter */ .CodeMirror-lint-markers { width: 16px; } .CodeMirror-jump-token { cursor: pointer; text-decoration: underline; } ` // Styling of portal for hints // .CodeMirror-info info for types breaks stack trace // tslint:disable-next-line const GlobalStyle = createGlobalStyle` *::-webkit-scrollbar { -webkit-appearance: none; width: 7px; height: 7px; } *::-webkit-scrollbar-track-piece { background-color: rgba(255, 255, 255, 0); } *::-webkit-scrollbar-track { background-color: inherit; } *::-webkit-scrollbar-thumb { max-height: 100px; border-radius: 3px; background-color: rgba(1, 1, 1, 0.23); } *::-webkit-scrollbar-thumb:hover { background-color: rgba(1, 1, 1, 0.35); } *::-webkit-scrollbar-thumb:active { background-color: rgba(1, 1, 1, 0.48); } *::-webkit-scrollbar-corner { background: rgba(0,0,0,0); } .CodeMirror-lint-tooltip, .CodeMirror-info { background-color: white; border-radius: 4px 4px 4px 4px; border: 1px solid black; color: #09141C; font-family: Open Sans, monospace; font-size: 14px; max-width: 600px; opacity: 0; overflow: hidden; padding: 12px 14px; position: fixed; -webkit-transition: opacity 0.4s; transition: opacity 0.4s; z-index: 100; } .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { padding-left: 18px; } .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { background-position: left bottom; background-repeat: repeat-x; } .CodeMirror-lint-mark-error { background-image: url('data:image/svg+xml;utf8, '); } .CodeMirror-lint-mark-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII='); } .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { background-position: center center; background-repeat: no-repeat; cursor: pointer; display: inline-block; height: 16px; position: relative; vertical-align: middle; width: 16px; } .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { background-position: top left; background-repeat: no-repeat; padding-left: 22px; } .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { background-image: url('data:image/svg+xml;utf8, '); background-position: 0 50%; } .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII='); } .CodeMirror-lint-marker-multiple { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC'); background-position: right bottom; background-repeat: no-repeat; width: 100%; height: 100%; } .CodeMirror-lint-mark-error { &:before { content: ''; width: 50px; height: 14px; position: absolute; background: #FF4F56; left: -80px; top: 50%; transform: translateY(-50%); z-index: 10; } } .CodeMirror-lint-message-error span { color: white; background: #FF4F56; font-family: 'Source Code Pro', monospace; font-weight: 600; border-radius: 2px; padding: 0 4px; } .CodeMirror-hints { background: white; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); font-size: 14px; list-style: none; margin-left: -6px; margin: 0; max-height: 20em; overflow: hidden; padding: 0; position: absolute; z-index: 10; border-radius: 2px; top: 0; left: 0; &:hover { overflow-y: overlay; } } .CodeMirror-hints-wrapper { font-family: 'Open Sans', sans-serif; background: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); margin-left: -6px; position: absolute; z-index: 10; } .CodeMirror-hints-wrapper .CodeMirror-hints { box-shadow: none; margin-left: 0; position: relative; z-index: 0; } .CodeMirror-hint { color: rgba(15, 32, 45, 0.6); cursor: pointer; margin: 0; max-width: 300px; overflow: hidden; padding: 6px 12px; white-space: pre; } li.CodeMirror-hint-active { background-color: #2a7ed3; border-top-color: white; color: white; } .CodeMirror-hint-information { border-top: solid 1px rgba(0, 0, 0, 0.1); max-width: 300px; padding: 10px 12px; position: relative; z-index: 1; background-color: rgba(15, 32, 45, 0.03); font-size: 14px; } .CodeMirror-hint-information:first-child { border-bottom: solid 1px #c0c0c0; border-top: none; margin-bottom: -1px; } .CodeMirror-hint-information .content { color: rgba(15, 32, 45, 0.6); box-orient: vertical; display: -webkit-box; display: -ms-flexbox; display: flex; line-clamp: 3; line-height: 1.36; max-height: 59px; overflow: hidden; text-overflow: -o-ellipsis-lastline; } .CodeMirror-hint-information .content p:first-child { margin-top: 0; } .CodeMirror-hint-information .content p:last-child { margin-bottom: 0; } .CodeMirror-hint-information .infoType { color: rgb(241, 143, 1); cursor: pointer; display: inline; margin-right: 0.5em; } ` const Wrapper = ({ children }) => { function lintMessage(e) { if (e.target.classList.contains('CodeMirror-lint-mark-error')) { const items = document.getElementsByClassName( 'CodeMirror-lint-message-error', ) for (const item of Array.from(items)) { item.innerHTML = item.innerHTML.replace(/"(.*?)"/g, '$1') } } } return ( {children} ) } const GraphqlContainer = styled.div` color: #141823; display: flex; flex-direction: row; font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; font-size: 14px; height: 100%; margin: 0; overflow: hidden; width: 100%; ` export class Container extends React.PureComponent { private graphqlContainer render() { return ( {this.props.children} ) } getWidth = () => { return this.graphqlContainer.offsetWidth } private setGraphqlContainer = ref => { this.graphqlContainer = ref } } export default Wrapper ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ExecuteButton.tsx ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import * as React from 'react' import ExecuteButtonOperation from './ExecuteButtonOperation' import { styled } from '../../styled' import { connect } from 'react-redux' import { runQuery, stopQuery } from '../../state/sessions/actions' import { createStructuredSelector } from 'reselect' import { getQueryRunning, getOperations, getSelectedSessionIdFromRoot, } from '../../state/sessions/selectors' import { toJS } from './util/toJS' export interface ReduxProps { runQuery: (operationName?: string) => void stopQuery: (sessionId: string) => void queryRunning: boolean operations: any[] sessionId: string } export interface State { optionsOpen: boolean highlight: any } let firstTime = true /** * ExecuteButton * * What a nice round shiny button. Shows a drop-down when there are multiple * queries to run. */ class ExecuteButton extends React.Component { constructor(props) { super(props) this.state = { optionsOpen: false, highlight: null, } } render() { const { operations } = this.props const optionsOpen = this.state.optionsOpen const hasOptions = operations && operations.length > 1 let options: any = null if (hasOptions && optionsOpen) { const highlight = this.state.highlight options = ( {operations.map(operation => ( ))} ) } // Allow click event if there is a running query or if there are not options // for which operation to run. let onClick if (this.props.queryRunning || !hasOptions) { onClick = this.onClick } // Allow mouse down if there is no running query, there are options for // which operation to run, and the dropdown is currently closed. let onMouseDown if (!this.props.queryRunning && hasOptions && !optionsOpen) { onMouseDown = this.onOptionsOpen } const pathJSX = this.props.queryRunning ? ( ) : ( ) return ( {options} ) } private handleMouseOver = (operation: any) => { this.setState({ highlight: operation }) } private handleMouseOut = () => { this.setState({ highlight: null }) } private handleMouseUp = (operation: any) => { this.onOptionSelected(operation) } private onClick = () => { if (this.props.queryRunning) { this.props.stopQuery(this.props.sessionId) } else { this.props.runQuery() } } private onOptionSelected = operation => { this.setState({ optionsOpen: false } as State) if (!operation) { return } this.props.runQuery(operation.name && operation.name.value) } private onOptionsOpen = downEvent => { let initialPress = true const downTarget = downEvent.target this.setState({ highlight: null, optionsOpen: true }) let onMouseUp: any = upEvent => { if (initialPress && upEvent.target === downTarget) { initialPress = false } else { document.removeEventListener('mouseup', onMouseUp) onMouseUp = null if (downTarget.parentNode) { const isOptionsMenuClicked = // tslint:disable-next-line downTarget.parentNode.compareDocumentPosition(upEvent.target) & Node.DOCUMENT_POSITION_CONTAINED_BY if (!isOptionsMenuClicked) { // menu calls setState if it was clicked this.setState({ optionsOpen: false } as State) } if (firstTime) { this.onOptionSelected( this.props.operations.find( op => op.name.value === upEvent.target.textContent, ) || this.props.operations[0], ) firstTime = false } } } } document.addEventListener('mouseup', onMouseUp) } } const mapStateToProps = createStructuredSelector({ queryRunning: getQueryRunning, operations: getOperations, sessionId: getSelectedSessionIdFromRoot, }) export default connect( mapStateToProps, { runQuery, stopQuery }, )(toJS(ExecuteButton)) const Wrapper = styled.div` position: absolute; left: -62px; z-index: 5; top: 15px; margin: 0 14px 0 28px; ` interface ButtonProps { isRunning: boolean } const Button = styled('div')` width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 100%; transition: background-color 100ms; background-color: ${p => p.isRunning ? p.theme.editorColours.executeButtonSubscription : p.theme.editorColours.executeButton}; border: 6px solid ${p => p.theme.editorColours.executeButtonBorder}; cursor: pointer; user-select: none; svg { fill: ${p => (p.theme.mode === 'light' ? 'white' : 'inherit')}; } &:hover { background-color: ${p => p.isRunning ? p.theme.editorColours.executeButtonSubscriptionHover : p.theme.editorColours.executeButtonHover}; } ` const ExecuteBox = styled.div` background: #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.25); padding: 8px 0; left: -1px; margin: 0; position: absolute; top: 78px; z-index: 100; user-select: none; &:before { position: absolute; background: white; content: ''; top: -4px; left: 34px; transform: rotate(45deg); width: 8px; height: 8px; } ` const ExecuteOptions = styled.ul` max-height: 270px; overflow: scroll; li { cursor: pointer; list-style: none; min-width: 100px; padding: 2px 30px 4px 10px; } li.selected { background: rgb(39, 174, 96); color: white; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ExecuteButtonOperation.tsx ================================================ import * as React from 'react' export interface Props { operation: any onMouseOver: (operation: any) => void onMouseOut: () => void onMouseUp: (operation: any) => void highlight: any } class ExecuteButtonOperation extends React.PureComponent { render() { return (
  • {this.props.operation.name ? this.props.operation.name.value : ''}
  • ) } private onMouseOver = () => { this.props.onMouseOver(this.props.operation) } private onMouseUp = () => { this.props.onMouseUp(this.props.operation) } } export default ExecuteButtonOperation ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTab.tsx ================================================ 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%'}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ExplorerTabs/SideTabs.tsx ================================================ 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, changeKeyMove, setDocsVisible, changeWidthDocs } 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> maxWidth: number setWidth: (props?: any) => any setActiveContentRef: (ref: any) => void } export interface SideTabContentProps { schema: GraphQLSchema sessionId?: string ref?: any setWidth?: (props?: any) => any } export interface State { searchValue: string widthMap: any } class SideTabs extends React.Component< Props & StateFromProps & DispatchFromProps, State > { ref private refContentContainer: any private clientX: number = 0 private clientY: number = 0 constructor(props) { super(props) ;(window as any).d = this } 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) } this.props.setWidth() if ( this.props.docs.activeTabIdx !== prevProps.docs.activeTabIdx && this.refContentContainer ) { this.refContentContainer.focus() } } componentDidMount() { if (!this.props.docs.activeTabIdx) { this.props.setDocsVisible(this.props.sessionId, false) } return this.props.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.props.setActiveContentRef, setWidth: this.props.setWidth, })} ) } setRef = ref => { this.ref = ref } private setContentContainerRef = ref => { this.refContentContainer = ref } private handleTabClick = idx => () => { if (this.props.docs.activeTabIdx === idx) { this.props.setDocsVisible(this.props.sessionId, false) return this.props.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.props.setWidth() } else { this.props.setDocsVisible(this.props.sessionId, true, idx) return this.props.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, changeKeyMove, setDocsVisible, changeWidthDocs }, dispatch, ) const mapStateToProps = createStructuredSelector({ docs: getSessionDocs, sessionId: getSelectedSessionIdFromRoot, }) const ConnectedGraphDocs = connect( mapStateToProps, // @ts-ignore mapDispatchToProps, null, { forwardRef: true }, )(SideTabs) // @ts-ignore 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; &::before { top: 0; bottom: 0; background: ${props => props.color ? 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`}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/GraphQLEditor.tsx ================================================ 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 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, getResponses, getSubscriptionActive, getVariableEditorOpen, getVariableEditorHeight, getResponseTracingOpen, getResponseTracingHeight, getResponseExtensions, getCurrentQueryStartTime, getCurrentQueryEndTime, getTracingSupported, getEditorFlex, getQueryVariablesActive, getHeaders, getOperations, getOperationName, getHeadersCount, getSelectedSessionIdFromRoot, } from '../../state/sessions/selectors' import { updateQueryFacts, stopQuery, runQueryAtPosition, closeQueryVariables, openQueryVariables, openVariables, closeVariables, openTracing, closeTracing, toggleTracing, setEditorFlex, toggleVariables, fetchSchema, } from '../../state/sessions/actions' import { ResponseRecord } from '../../state/sessions/reducers' import { getDocsOpen } from '../../state/docs/selectors' import { changeWidthDocs } from '../../state/docs/actions' /** * The top-level React component for GraphQLEditor, intended to encompass the entire * browser viewport. */ export interface Props { onRef?: any shareEnabled?: boolean fixedEndpoint?: boolean schema?: GraphQLSchema } export interface ReduxProps { setStacks: (sessionId: string, stack: any[]) => void updateQueryFacts: () => void runQueryAtPosition: (position: number) => void fetchSchema: () => void openQueryVariables: () => void closeQueryVariables: () => void openVariables: (height: number) => void closeVariables: (height: number) => void openTracing: (height: number) => void closeTracing: (height: number) => void toggleTracing: () => void toggleVariables: () => void setEditorFlex: (flex: number) => void stopQuery: (sessionId: string) => void changeWidthDocs: (sessionId: string, width: number) => void navStack: any[] docsOpen: boolean // sesion props queryRunning: boolean responses: List subscriptionActive: boolean variableEditorOpen: boolean variableEditorHeight: number currentQueryStartTime?: Date currentQueryEndTime?: Date responseTracingOpen: boolean responseTracingHeight: number responseExtensions: any tracingSupported?: boolean editorFlex: number headers: string headersCount: number queryVariablesActive: boolean operationName: string query: string sessionId: string } export interface SimpleProps { children?: any } export interface ToolbarButtonProps extends SimpleProps { onClick: (e: any) => void title: string label: string } class GraphQLEditor extends React.PureComponent { public codeMirrorSizer public queryEditorComponent public variableEditorComponent public resultComponent public editorBarComponent public docExplorerComponent: any // later React.Component<...> public graphExplorerComponent: any public schemaExplorerComponent: any private queryResizer: any private responseResizer: any private queryVariablesRef private httpHeadersRef private containerComponent private activeSideTabContent componentDidMount() { // Ensure a form of a schema exists (including `null`) and // if not, fetch one using an introspection query. // this.props.fetchSchema() // Utility for keeping CodeMirror correctly sized. this.codeMirrorSizer = new CodeMirrorSizer() ;(global as any).g = this } componentDidUpdate() { // If this update caused DOM nodes to have changed sizes, update the // corresponding CodeMirror instance sizes to match. // const components = [ // this.queryEditorComponent, // this.variableEditorComponent, // this.resultComponent, // ] // this.codeMirrorSizer.updateSizes(components) if (this.resultComponent && Boolean(this.props.subscriptionActive)) { this.resultComponent.scrollTop = this.resultComponent.scrollHeight } } render() { return ( Query Variables {'HTTP Headers ' + (this.props.headersCount && this.props.headersCount > 0 ? `(${this.props.headersCount})` : '')} {this.props.queryVariablesActive ? ( ) : ( )} {this.props.queryRunning && this.props.responses.size === 0 && } {!this.props.queryRunning && (!this.props.responses || this.props.responses.size === 0) && ( Press the Play Button to get a response here )} {this.props.subscriptionActive && ( Listening … )} Tracing ) } setQueryVariablesRef = ref => { this.queryVariablesRef = ref } setHttpHeadersRef = ref => { this.httpHeadersRef = ref } setQueryResizer = ref => { this.queryResizer = ReactDOM.findDOMNode(ref) } setResponseResizer = ref => { this.responseResizer = ReactDOM.findDOMNode(ref) } setEditorBarComponent = ref => { this.editorBarComponent = ref } setQueryEditorComponent = ref => { this.queryEditorComponent = ref } setVariableEditorComponent = ref => { this.variableEditorComponent = ref } setResultComponent = ref => { this.resultComponent = ref } setDocExplorerRef = ref => { if (ref) { this.docExplorerComponent = ref.getWrappedInstance() } } setGraphExplorerRef = ref => { if (ref) { this.graphExplorerComponent = ref.getWrappedInstance() } } setSchemaExplorerRef = ref => { if (ref) { this.schemaExplorerComponent = ref } } setContainerComponent = ref => { this.containerComponent = ref } handleClickReference = reference => { if (this.docExplorerComponent) { this.docExplorerComponent.showDocFromType(reference.field || reference) } } setSideTabActiveContentRef = ref => { if (ref) { this.activeSideTabContent = ref } } /** * Inspect the query, automatically filling in selection sets for non-leaf * fields which do not yet have them. * * @public */ autoCompleteLeafs() { const { insertions, result } = fillLeafs( this.props.schema, this.props.query, ) as { insertions: Array<{ index: number; string: string }> result: string } if (insertions && insertions.length > 0) { const editor = this.queryEditorComponent.getCodeMirror() editor.operation(() => { const cursor = editor.getCursor() const cursorIndex = editor.indexFromPos(cursor) editor.setValue(result) let added = 0 try { /* tslint:disable-next-line */ const markers = insertions.map(({ index, string }) => editor.markText( editor.posFromIndex(index + added), editor.posFromIndex(index + (added += string.length)), { className: 'autoInsertedLeaf', clearOnEnter: true, title: 'Automatically added leaf fields', }, ), ) setTimeout(() => markers.forEach(marker => marker.clear()), 7000) } catch (e) { // } let newCursorIndex = cursorIndex /* tslint:disable-next-line */ insertions.forEach(({ index, string }) => { if (index < cursorIndex && string) { newCursorIndex += string.length } }) editor.setCursor(editor.posFromIndex(newCursorIndex)) }) } return result } private runQueryAtCursor = () => { if (this.props.queryRunning) { this.props.stopQuery(this.props.sessionId) return } const editor = this.queryEditorComponent.getCodeMirror() const cursor = editor.getCursor() const cursorIndex = editor.indexFromPos(cursor) this.props.runQueryAtPosition(cursorIndex) } private handleHintInformationRender = elem => { elem.addEventListener('click', this.onClickHintInformation) let onRemoveFn elem.addEventListener( 'DOMNodeRemoved', (onRemoveFn = () => { elem.removeEventListener('DOMNodeRemoved', onRemoveFn) elem.removeEventListener('click', this.onClickHintInformation) }), ) } private handleResizeStart = downEvent => { if (!this.didClickDragBar(downEvent)) { return } downEvent.preventDefault() const offset = downEvent.clientX - getLeft(downEvent.target) let onMouseMove: any = moveEvent => { if (moveEvent.buttons === 0) { return onMouseUp() } const editorBar = ReactDOM.findDOMNode(this.editorBarComponent) as Element const leftSize = moveEvent.clientX - getLeft(editorBar) - offset const rightSize = editorBar.clientWidth - leftSize this.props.setEditorFlex(leftSize / rightSize) } let onMouseUp: any = () => { document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) onMouseMove = null onMouseUp = null } document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) } private didClickDragBar(event) { // Only for primary unmodified clicks return ( event.target === this.queryResizer || event.target === this.responseResizer ) } private handleTracingResizeStart = downEvent => { downEvent.preventDefault() let didMove = false const hadHeight = this.props.responseTracingHeight const offset = downEvent.clientY - getTop(downEvent.target) let onMouseMove: any = moveEvent => { if (moveEvent.buttons === 0) { return onMouseUp() } didMove = true const editorBar = ReactDOM.findDOMNode(this.editorBarComponent) as Element const topSize = moveEvent.clientY - getTop(editorBar) - offset const bottomSize = editorBar.clientHeight - topSize if (bottomSize < 60) { this.props.closeTracing(hadHeight) } else { this.props.openTracing(hadHeight) } } let onMouseUp: any = () => { if (!didMove) { this.props.toggleTracing() } document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) onMouseMove = null onMouseUp = null } document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) } private handleVariableResizeStart = downEvent => { downEvent.preventDefault() let didMove = false const wasOpen = this.props.variableEditorOpen const hadHeight = this.props.variableEditorHeight const offset = downEvent.clientY - getTop(downEvent.target) if ( wasOpen && (downEvent.target === this.queryVariablesRef || downEvent.target === this.httpHeadersRef) ) { return } let onMouseMove: any = moveEvent => { if (moveEvent.buttons === 0) { return onMouseUp() } didMove = true const editorBar = ReactDOM.findDOMNode(this.editorBarComponent) as Element const topSize = moveEvent.clientY - getTop(editorBar) - offset const bottomSize = editorBar.clientHeight - topSize if (bottomSize < 60) { this.props.closeVariables(hadHeight) } else { this.props.openVariables(bottomSize) } } let onMouseUp: any = () => { if (!didMove) { this.props.toggleVariables() } document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) onMouseMove = null onMouseUp = null } document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) } private onClickHintInformation = event => { if (event.target.className === 'typeName') { const typeName = event.target.innerHTML const schema = this.props.schema if (schema) { // TODO: There is no way as of now to retrieve the NAMED_TYPE of a GraphQLList(Type). // We're therefore removing any '[' or '!' characters, to properly find its NAMED_TYPE. (eg. [Type!]! => Type) // This should be removed as soon as there's a safer way to do that. const namedTypeName = typeName.replace(/[\]\[!]/g, '') const type = schema.getType(namedTypeName) if (isNamedType(type)) { this.docExplorerComponent.showDocFromType(type) } } } } private setDocsWidth = (props: any = this.props) => { if (!this.activeSideTabContent) { return } if (!this.props.docsOpen) { return } requestAnimationFrame(() => { const width = this.activeSideTabContent.getWidth() const maxWidth = this.containerComponent.getWidth() - 86 this.props.changeWidthDocs(props.sessionId, Math.min(width, maxWidth)) }) } } const mapStateToProps = createStructuredSelector({ queryRunning: getQueryRunning, responses: getResponses, subscriptionActive: getSubscriptionActive, variableEditorOpen: getVariableEditorOpen, variableEditorHeight: getVariableEditorHeight, responseTracingOpen: getResponseTracingOpen, responseTracingHeight: getResponseTracingHeight, responseExtensions: getResponseExtensions, currentQueryStartTime: getCurrentQueryStartTime, currentQueryEndTime: getCurrentQueryEndTime, tracingSupported: getTracingSupported, editorFlex: getEditorFlex, queryVariablesActive: getQueryVariablesActive, headers: getHeaders, operations: getOperations, operationName: getOperationName, headersCount: getHeadersCount, sessionId: getSelectedSessionIdFromRoot, docsOpen: getDocsOpen, }) export default // TODO fix redux types connect( mapStateToProps, { updateQueryFacts, stopQuery, runQueryAtPosition, openQueryVariables, closeQueryVariables, openVariables, closeVariables, openTracing, closeTracing, toggleTracing, setEditorFlex, toggleVariables, fetchSchema, changeWidthDocs, }, null, { forwardRef: true, }, )(GraphQLEditor) const EditorBar = styled.div` display: flex; flex-direction: row; flex: 1; height: 100%; ` const ResultWrap = styled.div` display: flex; flex-direction: column; flex: 1; height: 100%; position: relative; border-left: none; background: ${p => p.theme.editorColours.resultBackground}; overflow-anchor: auto; ` const DragBar = styled.div` width: 15px; position: absolute; top: 0; bottom: 0; cursor: col-resize; ` const QueryDragBar = styled(DragBar)` right: 0px; ` const ResultDragBar = styled(DragBar)` left: 0px; z-index: 1; ` interface DrawerProps { isOpen: boolean height: number } const BottomDrawer = styled('div')` display: flex; background: #0b1924; flex-direction: column; position: relative; height: ${props => (props.isOpen ? `${props.height}px` : '43px')}; ` interface TitleProps { isOpen: boolean onMouseDown?: any onClick?: any ref?: any } const BottomDrawerTitle = styled.div` background: #0b1924; text-transform: uppercase; font-weight: 600; letter-spacing: 0.53px; line-height: 14px; font-size: 14px; padding: 14px 14px 15px 21px; user-select: none; ` const VariableEditor = styled(BottomDrawer)` .CodeMirror { padding-left: 4px; width: calc(100% - 4px); background: ${p => p.theme.editorColours.leftDrawerBackground}; } .CodeMirror-lines { padding: 10px 0 20px 0; } .CodeMirror-linenumbers { background: ${p => p.theme.editorColours.leftDrawerBackground}; } ` const VariableEditorTitle = styled(({ isOpen, ...rest }) => ( ))` cursor: ${p => (p.isOpen ? 'row-resize' : 'n-resize')}; background: ${p => p.theme.editorColours.leftDrawerBackground}; ` const VariableEditorSubtitle = styled('span')` margin-right: 10px; cursor: pointer; color: ${p => p.isOpen ? p.theme.editorColours.drawerText : p.theme.editorColours.drawerTextInactive}; &:last-child { margin-right: 0; } ` const ResponseTracking = styled(BottomDrawer)` background: ${p => p.theme.editorColours.rightDrawerBackground}; ` const ResponseTrackingTitle = styled(({ isOpen, ...rest }) => ( ))` text-align: right; background: ${p => p.theme.editorColours.rightDrawerBackground}; cursor: ${props => (props.isOpen ? 's-resize' : 'n-resize')}; color: ${p => p.theme.editorColours.drawerTextInactive}; ` interface QueryProps { flex: number } const QueryWrap = styled('div')` position: relative; display: flex; flex-direction: column; flex: ${props => props.flex} 1 0%; border-top: 8px solid ${props => props.theme.editorColours.resultBackground}; ` const Intro = styled.div` width: 235px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: ${p => p.theme.colours.textInactive}; font-size: ${p => p.theme.sizes.small16}; font-family: 'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; text-align: center; letter-spacing: 0.6px; ` const Listening = styled.div` position: absolute; bottom: 0; color: ${p => p.theme.editorColours.text}; background: ${p => p.theme.editorColours.resultBackground}; font-size: ${p => p.theme.sizes.small16}; font-family: ${p => p.theme.settings['editor.fontFamily']}; letter-spacing: 0.6px; padding-left: 24px; padding-bottom: 60px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/QueryEditor.tsx ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import * as React from 'react' import { GraphQLSchema } from 'graphql' import * as MD from 'markdown-it' import { connect } from 'react-redux' import onHasCompletion from './onHasCompletion' import { editQuery, setScrollTop } from '../../state/sessions/actions' import { createStructuredSelector } from 'reselect' import { getQuery, getSelectedSessionIdFromRoot, getScrollTop, getTabWidth, getUseTabs, } from '../../state/sessions/selectors' import EditorWrapper from './EditorWrapper' import { styled } from '../../styled' import { isIframe } from '../../utils' /** * QueryEditor * * Maintains an instance of CodeMirror responsible for editing a GraphQL query. * * Props: * * - schema: A GraphQLSchema instance enabling editor linting and hinting. * - value: The text of the editor. * - onEdit: A function called when the editor changes, given the edited text. * */ export interface Props { schema?: GraphQLSchema | null onHintInformationRender?: (elem: any) => void onRunQuery?: () => void onClickReference?: (reference: any) => void getRef?: (ref: QueryEditor) => void } export interface ReduxProps { showDocForReference?: (reference: any) => void onChange?: (query: string) => void setScrollTop?: (sessionId: string, value: number) => void value: string sessionId?: string scrollTop?: number tabWidth?: number useTabs?: boolean } const md = new MD() // const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/ export class QueryEditor extends React.PureComponent { private cachedValue: string private editor: any private ignoreChangeEvent: boolean private node: any constructor(props) { super(props) // 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/hint/show-hint') require('codemirror/addon/comment/comment') require('codemirror/addon/edit/matchbrackets') require('codemirror/addon/edit/closebrackets') require('codemirror/addon/fold/foldgutter') require('codemirror/addon/fold/brace-fold') require('codemirror/addon/search/search') require('codemirror/addon/search/searchcursor') require('codemirror/addon/search/jump-to-line') require('codemirror/addon/dialog/dialog') require('codemirror/addon/lint/lint') require('codemirror/keymap/sublime') require('codemirror/keymap/vim') require('codemirror-graphql/hint') require('codemirror-graphql/lint') require('codemirror-graphql/info') require('codemirror-graphql/jump') require('codemirror-graphql/mode') const gutters: any[] = [] gutters.push('CodeMirror-linenumbers') gutters.push('CodeMirror-foldgutter') this.editor = CodeMirror(this.node, { autofocus: !isIframe() ? true : false, value: this.props.value || '', lineNumbers: true, tabSize: this.props.tabWidth || 2, indentWithTabs: this.props.useTabs || false, mode: 'graphql', theme: 'graphiql', keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, showCursorWhenSelecting: true, readOnly: false, foldGutter: { minFoldSize: 4, }, lint: { schema: this.props.schema, }, hintOptions: { schema: this.props.schema, closeOnUnfocus: true, completeSingle: false, }, info: { schema: this.props.schema, renderDescription: text => md.render(text), onClick: this.props.onClickReference, }, jump: { schema: this.props.schema, onClick: this.props.onClickReference, }, gutters, extraKeys: { 'Cmd-Space': () => this.editor.showHint({ completeSingle: true }), 'Ctrl-Space': () => this.editor.showHint({ completeSingle: true }), 'Alt-Space': () => this.editor.showHint({ completeSingle: true }), 'Shift-Space': () => this.editor.showHint({ completeSingle: true }), 'Cmd-Enter': () => { if (this.props.onRunQuery) { this.props.onRunQuery() } }, 'Ctrl-Enter': () => { if (this.props.onRunQuery) { this.props.onRunQuery() } }, // Editor improvements 'Ctrl-Left': 'goSubwordLeft', 'Ctrl-Right': 'goSubwordRight', 'Alt-Left': 'goGroupLeft', 'Alt-Right': 'goGroupRight', 'Cmd-F': 'findPersistent', 'Ctrl-F': 'findPersistent', }, }) this.editor.on('change', this.onEdit) this.editor.on('keyup', this.onKeyUp) this.editor.on('hasCompletion', this.onHasCompletion) ;(global as any).editor = this.editor if (this.props.scrollTop) { this.scrollTo(this.props.scrollTop) } } componentDidUpdate(prevProps) { const CodeMirror = require('codemirror') // Ensure the changes caused by this update are not interpretted as // user-input changes which could otherwise result in an infinite // event loop. this.ignoreChangeEvent = true if (this.props.schema !== prevProps.schema) { this.editor.options.lint.schema = this.props.schema this.editor.options.hintOptions.schema = this.props.schema this.editor.options.info.schema = this.props.schema this.editor.options.jump.schema = this.props.schema CodeMirror.signal(this.editor, 'change', this.editor) } if ( this.props.value !== prevProps.value && this.props.value !== this.cachedValue ) { this.cachedValue = this.props.value this.editor.setValue(this.props.value) } this.ignoreChangeEvent = false setTimeout(() => { if (this.props.sessionId !== prevProps.sessionId) { if (this.props.scrollTop) { this.scrollTo(this.props.scrollTop) } } }) } UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.sessionId !== nextProps.sessionId) { this.closeCompletion() this.updateSessionScrollTop() if (!isIframe()) { this.editor.focus() } } } scrollTo(y) { this.node.querySelector('.CodeMirror-scroll').scrollTop = y } updateSessionScrollTop() { if (this.props.setScrollTop && this.props.sessionId) { this.props.setScrollTop( this.props.sessionId!, this.node.querySelector('.CodeMirror-scroll').scrollTop, ) } } componentWillUnmount() { this.updateSessionScrollTop() this.editor.off('change', this.onEdit) this.editor.off('keyup', this.onKeyUp) this.editor.off('hasCompletion', this.onHasCompletion) this.editor = null } render() { return ( ) } setRef = ref => { this.node = ref } /** * Public API for retrieving the CodeMirror instance from this * React component. */ getCodeMirror() { return this.editor } /** * Public API for retrieving the DOM client height for this component. */ getClientHeight() { return this.node && this.node.clientHeight } private onKeyUp = (_, event) => { const code = event.keyCode if (code === 86) { return } if ( (code >= 65 && code <= 90) || // letters (!event.shiftKey && code >= 48 && code <= 57) || // numbers (event.shiftKey && code === 189) || // underscore (event.shiftKey && code === 50) || // @ (event.shiftKey && code === 57) // ( ) { this.editor.execCommand('autocomplete') } } private onEdit = () => { if (!this.ignoreChangeEvent && this.props.onChange) { this.cachedValue = this.editor.getValue() this.props.onChange(this.cachedValue) } } /** * Render a custom UI for CodeMirror's hint which includes additional info * about the type and description for the selected context. */ private onHasCompletion = (cm, data) => { onHasCompletion(cm, data, this.props.onHintInformationRender) } private closeCompletion = () => { if ( this.editor.state.completionActive && typeof this.editor.state.completionActive.close === 'function' ) { this.editor.state.completionActive.close() } } } const mapStateToProps = createStructuredSelector({ value: getQuery, sessionId: getSelectedSessionIdFromRoot, scrollTop: getScrollTop, tabWidth: getTabWidth, useTabs: getUseTabs, }) export default connect( mapStateToProps, { onChange: editQuery, setScrollTop }, )(QueryEditor) const Editor = styled.div` flex: 1 1 0%; position: relative; .CodeMirror { width: 100%; background: ${p => p.theme.editorColours.editorBackground}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ResponseTracing.tsx ================================================ import * as React from 'react' import TracingRow from './TracingRow' import styled from '../../styled/styled' import { createStructuredSelector } from 'reselect' import { getTracing, getCurrentQueryStartTime, getCurrentQueryEndTime, getTracingSupported, getQueryRunning, } from '../../state/sessions/selectors' import { connect } from 'react-redux' export interface TracingFormat { version: 1 startTime: string endTime: string duration: number execution: { resolvers: Array<{ path: Array parentType: string fieldName: string returnType: string startOffset: number duration: number }> } } export interface ReduxProps { tracing?: TracingFormat tracingSupported?: boolean startTime?: Date endTime?: Date queryRunning: boolean } const TracingWrapper = styled.div` padding-top: 6px; padding-left: 25px; padding-right: 25px; color: ${p => p.theme.editorColours.text}; overflow: auto; position: relative; height: 100%; ` const ReRun = styled.div` font-size: 14px; ` const NotSupported = styled.div` font-size: 14px; color: rgba(241, 143, 1, 1); ` const TracingRows = styled.div` padding-left: 100px; padding-bottom: 100px; padding-top: 16px; position: absolute; overflow: auto; top: 0; left: 0; width: calc(100% + 100px); height: calc(100% + 116px); ` interface Props { open: boolean } class ResponseTracing extends React.PureComponent { render() { const { tracing, tracingSupported, startTime, endTime, open } = this.props const requestMs = tracing && startTime ? Math.abs(new Date(tracing.startTime).getTime() - startTime.getTime()) : 0 const responseMs = tracing && endTime ? Math.abs(endTime.getTime() - new Date(tracing.endTime).getTime()) : 0 const requestDuration = 1000 * 1000 * requestMs return ( {tracing && open ? ( {tracing.execution.resolvers.map(res => ( ))} ) : tracingSupported ? ( {this.props.queryRunning ? 'Running query ...' : 'Please re-run the query to show tracing results.'} ) : ( This GraphQL server doesn’t support tracing. See the following page for instructions:
    https://github.com/apollographql/apollo-tracing
    )}
    ) } } const mapStateToProps = createStructuredSelector({ tracing: getTracing, startTime: getCurrentQueryStartTime, endTime: getCurrentQueryEndTime, tracingSupported: getTracingSupported, queryRunning: getQueryRunning, }) export default connect(mapStateToProps)(ResponseTracing) ================================================ FILE: packages/graphql-playground-react/src/components/Playground/ResultViewer.tsx ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import * as React from 'react' import { styled } from '../../styled' export interface Props { value: string isSubscription: boolean hideGutters?: boolean } /** * ResultViewer * * Maintains an instance of CodeMirror for viewing a GraphQL response. * * Props: * * - value: The text of the editor. * */ export class ResultViewer extends React.Component { private node: any private viewer: any componentDidMount() { const CodeMirror = require('codemirror') require('codemirror/addon/fold/foldgutter') require('codemirror/addon/fold/brace-fold') require('codemirror/addon/dialog/dialog') require('codemirror/addon/search/search') require('codemirror/addon/search/searchcursor') require('codemirror/addon/search/jump-to-line') require('codemirror/keymap/sublime') require('codemirror-graphql/results/mode') const gutters: any[] = [] if (!this.props.hideGutters) { gutters.push('CodeMirror-foldgutter') } let foldGutter: any = {} if (!this.props.hideGutters) { foldGutter = { minFoldSize: 4, } } const value = this.props.value || '' this.viewer = CodeMirror(this.node, { lineWrapping: true, value, readOnly: true, theme: 'graphiql', mode: 'graphql-results', keyMap: 'sublime', foldGutter, gutters, extraKeys: { // Persistent search box in Query Editor 'Cmd-F': 'findPersistent', 'Ctrl-F': 'findPersistent', // Editor improvements 'Ctrl-Left': 'goSubwordLeft', 'Ctrl-Right': 'goSubwordRight', 'Alt-Left': 'goGroupLeft', 'Alt-Right': 'goGroupRight', }, }) } shouldComponentUpdate(nextProps) { return this.props.value !== nextProps.value } componentDidUpdate() { const value = this.props.value || '' this.viewer.setValue(value) } componentWillUnmount() { this.viewer = null } render() { return ( ) } setRef = ref => { this.node = ref } /** * Public API for retrieving the CodeMirror instance from this * React component. */ getCodeMirror() { return this.viewer } /** * Public API for retrieving the DOM client height for this component. */ getClientHeight() { return this.node && this.node.clientHeight } } interface ResultProps { isSubscription: boolean } const Result = styled('div')` position: relative; display: flex; flex: 1; height: ${props => (props.isSubscription ? 'auto' : '100%')}; .CodeMirror { height: ${props => (props.isSubscription ? 'auto' : '100%')}; position: ${props => (props.isSubscription ? 'relative' : 'absolute%')}; box-sizing: border-box; background: none; padding-left: 38px; } .CodeMirror-cursor { display: none !important; } .CodeMirror-scroll { overflow: scroll !important; margin-right: 10px; } .CodeMirror-sizer { margin-bottom: 0 !important; } .CodeMirror-lines { margin: 20px 0; padding: 0; } .cm-string { color: ${p => p.theme.editorColours.property} !important; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/Results.tsx ================================================ import * as React from 'react' import ageOfDate from './util/ageOfDate' import { ResultViewer } from './ResultViewer' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getResponses } from '../../state/sessions/selectors' import { List } from 'immutable' import { styled } from '../../styled' import { ResponseRecord } from '../../state/sessions/reducers' export interface Props { setRef: (ref: any) => void } export interface ReduxProps { responses: List } const defaultResponseRecord = new ResponseRecord({ date: '', time: new Date(), resultID: 'default-id', }) const Results: React.SFC = ({ setRef, responses }) => { const response1 = responses.get(0) || defaultResponseRecord const isSubscription = responses.size > 1 return ( {responses.size <= 1 ? ( {responses.size > 1 && response1.time && ( {ageOfDate(response1.time)} )} ) : ( responses.map(response => ( {responses.size > 1 && response.time && ( {ageOfDate(response.time)} )} 1}> )) )} ) } const mapStateToProps = createStructuredSelector({ responses: getResponses, }) export default connect(mapStateToProps)(Results) const ResultWindow = styled('div')` flex: 1; height: ${props => (props.isSubscription ? 'auto' : '100%')}; position: relative; overflow: ${props => (props.isSubscription ? 'auto' : 'visible')}; max-height: none !important; .cm-string { color: rgb(41, 185, 115); } .cm-def { color: rgb(241, 143, 1); } .cm-property { color: rgb(51, 147, 220); } &::-webkit-scrollbar { display: none; } .CodeMirror { background: ${p => p.theme.editorColours.resultBackground}; } .CodeMirror-gutters { cursor: col-resize; } .CodeMirror-foldgutter, .CodeMirror-foldgutter-open:after, .CodeMirror-foldgutter-folded:after { padding-left: 3px; } ` const Response = styled('div')` position: relative; display: flex; flex: 1; height: ${props => (props.isSubscription ? `auto` : '100%')}; flex-direction: column; &:not(:first-child):last-of-type { margin-bottom: 48px; } ` const SubscriptionTime = styled.div` position: relative; height: 17px; margin-top: 12px; margin-bottom: 4px; &:before { position: absolute; width: 100%; content: ''; top: 9px; left: 95px; border-top: 1px solid ${p => p.theme.editorColours.subscriptionTimeBoaderTop}; } ` const SubscriptionTimeText = styled.div` font-size: 12px; color: ${p => p.theme.editorColours.subscriptionTimeText}; padding-left: 15px; ` interface ResultWrapperProps { isSubscription: boolean } const ResultWrapper = styled('div')` display: flex; flex: 1; height: ${props => (props.isSubscription ? `auto` : '100%')}; position: ${props => (props.isSubscription ? `relative` : 'static')}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLEditor.tsx ================================================ import * as React from 'react' import { GraphQLSchema, printSchema } 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 isPollingSchema: boolean 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') const currentSchemaStr = this.props.schema && printSchema(this.props.schema) const prevSchemaStr = prevProps.schema && printSchema(prevProps.schema) if (currentSchemaStr !== prevSchemaStr) { const initialScroll = this.editor.getScrollInfo() this.cachedValue = getSDL( this.props.schema, this.props.settings['schema.disableComments'], ) || '' this.editor.setValue( getSDL( this.props.schema, this.props.settings['schema.disableComments'], ), ) if (this.props.isPollingSchema) { this.editor.scrollTo(initialScroll.left, initialScroll.top) } 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() } } UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.sessionId !== nextProps.sessionId) { this.editor.scrollTo(0, 0) } } componentWillUnmount() { if(this.editor) { 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; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLHeader.tsx ================================================ 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, } } UNSAFE_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; flex-direction: row; height: 64px; width: 100%; margin-right: 108px; align-items: center; justify-content: flex-start; 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` 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; padding-right: 5px; ` 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', subtitle: '#8B959C', 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, subtitle: 'rgba(61, 88, 102, 0.5)', 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, } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLDocType.tsx ================================================ 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}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLFieldDoc.tsx ================================================ 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); ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLStyles.tsx ================================================ 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; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLType.tsx ================================================ 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; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLTypes/SDLUnionType.tsx ================================================ 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 ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaExplorer/SDLView.tsx ================================================ 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, getIsPollingSchema, } 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' import { ISettings } from '../../../types' interface StateFromProps { docs: { navStack: any[] docsOpen: boolean docsWidth: number keyMove: boolean } isPollingSchema: boolean settings: ISettings } interface DispatchFromProps { toggleDocs: (sessionId: string) => any setDocsVisible: (sessionId: string, open: boolean) => any changeWidthDocs: (sessionId: string, width: number) => any setSchemaUpdated: () => void } class SDLView extends React.Component< SideTabContentProps & StateFromProps > { ref constructor(props) { super(props) ;(window as any).d = this } UNSAFE_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, isPollingSchema } = 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, isPollingSchema: getIsPollingSchema, }) export default connect( mapStateToProps, // @ts-ignore mapDispatchToProps, null, { forwardRef: true }, )(SDLView) ================================================ FILE: packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts ================================================ import { GraphQLSchema, getIntrospectionQuery, buildClientSchema, validateSchema, IntrospectionQuery, } from 'graphql' import { NoSchemaError } from './util/NoSchemaError' import { InvalidSchemaError } from './util/InvalidSchemaError' import { ApolloLink, execute, toPromise } from 'apollo-link' import { Map, set } from 'immutable' import { makeOperation } from './util/makeOperation' import { parseHeaders } from './util/parseHeaders' import { LinkCreatorProps } from '../../state/sessions/fetchingSagas' import * as LRU from 'lru-cache' export interface TracingSchemaTuple { schema: GraphQLSchema tracingSupported: boolean } export interface SchemaFetchProps { endpoint: string headers?: string useTracingHeader?: boolean } export type LinkGetter = (session: LinkCreatorProps) => { link: ApolloLink } /** * The SchemaFetcher class servers the purpose of providing the GraphQLSchema. * All sagas and every part of the UI is using this as a singleton to prevent * unnecessary calls to the server. We're not storing this information in Redux, * as it's a good practice to only store serializable data in Redux. * GraphQLSchema objects are serializable, but can easily exceed the localStorage * max. Another reason to keep this in a separate class is, that we have more * advanced requirements like caching. */ export class SchemaFetcher { /** * The `sessionCache` property is used for UI components, that need fast access to the current schema. * If the relevant information of the session didn't change (endpoint and headers), * the cached schema will be returned. */ sessionCache: LRU.Cache /** * The `schemaInstanceCache` property is used to prevent unnecessary buildClientSchema calls. * It's tested by stringifying the introspection result, which is orders of magnitude * faster than rebuilding the schema. */ schemaInstanceCache: LRU.Cache /** * The `linkGetter` property is a callback that provides an ApolloLink instance. * This can be overriden by the user. */ linkGetter: LinkGetter /** * In order to prevent duplicate fetching of the same schema, we keep track * of all subsequent calls to `.fetch` with the `fetching` property. */ fetching: Map> /** * Other parts of the application can subscribe to change of a schema for a * particular session. These subscribers are being kept track of in the * `subscriptions` property */ subscriptions: Map void> = Map() constructor(linkGetter: LinkGetter) { this.sessionCache = new LRU({ max: 10 }) this.schemaInstanceCache = new LRU({ max: 10 }) this.fetching = Map() this.linkGetter = linkGetter } async fetch(session: SchemaFetchProps) { const hash = this.hash(session) const cachedSchema = this.sessionCache.get(hash) if (cachedSchema) { return cachedSchema } const fetching = this.fetching.get(hash) if (fetching) { return fetching } const promise = this.fetchSchema(session) this.fetching = this.fetching.set(hash, promise) return promise } subscribe(session: SchemaFetchProps, cb: (schema: GraphQLSchema) => void) { const hash = this.hash(session) this.subscriptions = this.subscriptions.set(hash, cb) } refetch(session: SchemaFetchProps) { return this.fetchSchema(session) } hash(session: SchemaFetchProps) { return `${session.endpoint}~${session.headers || ''}` } private getSchema(data: IntrospectionQuery) { const schemaString = JSON.stringify(data) const cachedSchema = this.schemaInstanceCache.get(schemaString) if (cachedSchema) { return cachedSchema } const schema = buildClientSchema(data) const validationErrors = validateSchema(schema) if (validationErrors && validationErrors.length > 0) { throw new InvalidSchemaError(validationErrors) } this.schemaInstanceCache.set(schemaString, schema) return schema } private async fetchSchema( session: SchemaFetchProps, ): Promise<{ schema: GraphQLSchema; tracingSupported: boolean } | null> { const hash = this.hash(session) try { const { endpoint } = session const headersTracing = { ...parseHeaders(session.headers), 'X-Apollo-Tracing': '1', } const headersNoTracing = { ...parseHeaders(session.headers), } const headers = session.useTracingHeader ? headersTracing : headersNoTracing const options = set(session, 'headers', headers) as any const { link } = this.linkGetter(options) const operation = makeOperation({ query: getIntrospectionQuery() }) const schemaData = await toPromise(execute(link, operation)) if ( schemaData && ((schemaData.errors && schemaData.errors.length > 0) || !schemaData.data) ) { throw new Error(JSON.stringify(schemaData, null, 2)) } if (!schemaData) { throw new NoSchemaError(endpoint) } const schema = this.getSchema(schemaData.data as IntrospectionQuery) const tracingSupported = (schemaData.extensions && Boolean(schemaData.extensions.tracing)) || false const result: TracingSchemaTuple = { schema, tracingSupported, } this.sessionCache.set(this.hash(session), result) const subscription = this.subscriptions.get(hash) if (subscription) { subscription(result.schema) } return result } finally { this.fetching.remove(hash) } } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/Tab.tsx ================================================ import * as React from 'react' import { SettingsIcon, CrossIcon } from '../Icons' import { connect } from 'react-redux' import { closeTab, selectTab, editName } from '../../state/sessions/actions' import { styled } from '../../styled' import { Session } from '../../state/sessions/reducers' import AutosizeInput from 'react-input-autosize' export interface Props { session: Session selectedSessionId: string } export interface ReduxProps { selectTab: (sessionId: string) => void closeTab: (sessionId: string) => void editName: (name: string) => void } export interface State { overCross: boolean editingName: boolean } class Tab extends React.PureComponent { constructor(props) { super(props) this.state = { overCross: false, editingName: false, } } render() { const { session, selectedSessionId } = this.props const { queryTypes } = session const active = session.id === selectedSessionId const name = session.name || session.operationName || queryTypes.firstOperationName || 'New Tab' return ( {session.subscriptionActive && } {queryTypes.query && Q} {(session.isSettingsTab || session.isConfigTab) && ( )} {queryTypes.mutation && M} {queryTypes.subscription && S} {this.state.editingName ? ( ) : ( {name} )} {session.isFile && session.changed && !this.state.overCross ? ( ) : ( )} ) } private startEditName = () => { this.setState({ editingName: true }) } private stopEditName = () => { this.setState({ editingName: false }) } private handleKeyDown = e => { if (e.keyCode === 13) { this.setState({ editingName: false }) } } private handleMouseOverCross = () => { this.setState({ overCross: true }) } private handleMouseOutCross = () => { this.setState({ overCross: false }) } private handleSelectSession = () => { this.props.selectTab(this.props.session.id) } private handleCloseSession = (e: any) => { e.stopPropagation() this.props.closeTab(this.props.session.id) } private handleEditName = e => { this.props.editName(e.target.value) } } export default connect( null, { closeTab, selectTab, editName }, )(Tab) interface TabItemProps { active: boolean hasCircle?: boolean } const TabItem = styled('div')` -webkit-app-region: no-drag; flex: 0 0 auto; display: flex; align-items: center; height: 43px; padding: 10px; padding-top: 9px; margin-right: 10px; font-size: 14px; border-radius: 2px; border-bottom: 2px solid ${p => p.theme.editorColours.navigationBar}; box-sizing: border-box; cursor: pointer; user-select: none; background: ${p => p.active ? p.theme.editorColours.tab : p.theme.editorColours.tabInactive}; &:hover { background: ${p => p.theme.editorColours.tab}; .close { opacity: 1; } } ` const OperationName = styled('div')` opacity: ${p => (p.active ? 1 : 0.5)}; background: transparent; color: ${p => p.theme.editorColours.tabText}; font-size: 14px; margin-left: 2px; display: inline; letter-spacing: 0.53px; ` const OperationNameInput = styled(AutosizeInput)` input { background: transparent; color: ${p => p.theme.editorColours.tabText}; font-size: 14px; margin-left: 2px; display: inline; letter-spacing: 0.53px; } ` const Icons = styled('div')` display: flex; align-items: center; opacity: ${p => (p.active ? 1 : 0.5)}; ` const QueryTypes = styled.div` display: flex; color: white; ` const QueryType = styled.div` height: 22px; width: 22px; display: flex; align-items: center; justify-content: center; margin-right: 4px; font-size: 12px; font-weight: 700; border-radius: 2px; ` const Query = styled(QueryType)` background: ${p => p.theme.colours.blue}; ` const Mutation = styled(QueryType)` background: ${p => p.theme.colours.orange}; ` const Subscription = styled(QueryType)` background: ${p => p.theme.colours.purple}; ` const RedDot = styled.div` width: 7px; height: 7px; background: rgba(242, 92, 84, 1); border-radius: 100%; margin-right: 10px; ` const Circle = styled.div` position: relative; top: -2px; font-size: 9px; background: ${p => p.theme.editorColours.circle}; ` const Close = styled('div')` position: relative; display: flex; margin-left: 10px; top: 1px; height: 13px; width: 13px; opacity: ${p => (p.active || p.hasCircle ? 1 : 0)}; svg { stroke: ${p => p.theme.editorColours.icon}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TabBar.tsx ================================================ import * as React from 'react' import { styled } from '../../styled' import { AddIcon } from '../Icons' import Tab, { Props as TabProps } from './Tab' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getSessionsArray, getSelectedSessionIdFromRoot, } from '../../state/sessions/selectors' import { Session } from '../../state/sessions/reducers' import { reorderTabs } from '../../state/sessions/actions' import { SortableContainer, SortableElement, SortStart, SortEnd, } from 'react-sortable-hoc' export interface Props { onNewSession: any isApp?: boolean } export interface ReduxProps { sessions: Session[] selectedSessionId: string reorderTabs: (src: number, dest: number) => void } interface State { sorting: boolean } const SortableTab = SortableElement(Tab) class TabBar extends React.PureComponent { state = { sorting: false } render() { const { sessions, isApp, selectedSessionId, onNewSession } = this.props const { sorting } = this.state return ( {sessions.map((session, ndx) => ( ))} ) } private onSortStart = ({ index }: SortStart) => { this.setState({ sorting: true }) } private onSortEnd = ({ oldIndex, newIndex }: SortEnd) => { this.props.reorderTabs(oldIndex, newIndex) this.setState({ sorting: false }) } private getHelperDimensions = ({ node }: SortStart) => { const { width, height } = node.getBoundingClientRect() return { width, height } } } const mapStateToProps = createStructuredSelector({ sessions: getSessionsArray, selectedSessionId: getSelectedSessionIdFromRoot, }) export default connect( mapStateToProps, { reorderTabs }, )(TabBar) const StyledTabBar = styled.div` color: white; height: 57px; background: ${p => p.theme.editorColours.background}; overflow: hidden; -webkit-app-region: drag; &:hover { overflow-x: overlay; } ` const SortableTabBar = SortableContainer(StyledTabBar) interface TabsProps { isApp?: boolean } const Tabs = styled('div')` display: flex; align-items: center; margin-top: 16px; padding-left: ${p => (p.isApp ? '43px' : '0')}; ` interface PlusProps { sorting: boolean } const Plus = styled('div')` -webkit-app-region: no-drag; box-sizing: border-box; display: flex; visibility: ${p => (p.sorting ? 'hidden' : 'visible')} height: 43px; width: 43px; border-radius: 2px; border-bottom: 2px solid ${p => p.theme.editorColours.navigationBar}; background: ${p => p.theme.editorColours.tabInactive}; justify-content: center; align-items: center; svg { stroke: ${p => p.theme.editorColours.icon}; } &:hover { background: ${p => p.theme.editorColours.tab}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/Polling.tsx ================================================ import * as React from 'react' import PollingIcon from './PollingIcon' export interface Props { interval: number onReloadSchema: () => void } interface State { windowVisible: boolean } class SchemaPolling extends React.Component { timer: any constructor(props) { super(props) this.state = { windowVisible: true, } } componentDidMount() { this.updatePolling() document.addEventListener('visibilitychange', this.setWindowVisibility) } componentWillUnmount() { this.clearTimer() document.removeEventListener('visibilitychange', this.setWindowVisibility) } setWindowVisibility = () => { if (document.visibilityState === 'visible') { this.setState( { windowVisible: true, }, this.updatePolling, ) } if (document.visibilityState === 'hidden') { this.setState( { windowVisible: false, }, this.updatePolling, ) } } UNSAFE_componentWillReceiveProps(nextProps: Props) { this.updatePolling(nextProps) } render() { return } private updatePolling = (props: Props = this.props) => { this.clearTimer() if (this.state.windowVisible) { // timer starts only when introspection not in flight this.timer = setInterval(() => props.onReloadSchema(), props.interval) } } private clearTimer() { if (this.timer) { clearInterval(this.timer) this.timer = null } } } export default SchemaPolling ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/PollingIcon.tsx ================================================ import * as React from 'react' import { styled, keyframes, css } from '../../../styled/index' import BasePositioner from './Positioner' export interface Props { animate: boolean disabled?: boolean onClick?: () => void } const PollingIcon: React.SFC = props => ( ) export default PollingIcon const pulse = keyframes` 0% { box-shadow: 0 0 0 0 rgba(139, 149, 156, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(139, 149, 156, 0); } 100% { box-shadow: 0 0 0 0 rgba(139, 149, 156, 0); } ` const Positioner = styled(BasePositioner)` display: flex; justify-content: center; align-items: center; ` const Icon = styled.div` display: block; width: 8px; height: 8px; border-radius: 50%; background: ${p => p.theme.editorColours.pollingIcon}; box-shadow: 0 0 0 ${p => p.theme.editorColours.pollingIconShadow}; ${p => p.animate ? css` animation: ${pulse} 2s infinite; ` : undefined}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/Positioner.tsx ================================================ import { styled } from '../../../styled/index' export default styled.div` width: 20px; height: 20px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/Reload.tsx ================================================ import * as React from 'react' import ReloadIcon from './ReloadIcon' export interface Props { isReloadingSchema: boolean onReloadSchema?: () => void } const Reload: React.SFC = props => ( ) export default Reload ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/ReloadIcon.tsx ================================================ import * as React from 'react' import { styled, keyframes, css } from '../../../styled/index' import BasePositioner from './Positioner' export interface Props { animate: boolean disabled?: boolean onClick?: () => void } const ReloadIcon: React.SFC = props => ( ) export default ReloadIcon const refreshFrames = keyframes` 0% { transform: rotate(0deg); stroke-dashoffset: 7.92; } 50% { transform: rotate(720deg); stroke-dashoffset: 37.68; } 100% { transform: rotate(1080deg); stroke-dashoffset: 7.92; } ` // same result for these 2 keyframes, however when the props change // it makes the element animated with these keyframes to trigger // again the animation const reloadAction = props => keyframes` 0% { transform: rotate(${props.animate ? 0 : 360}deg); } 100% { transform: rotate(${props.animate ? 360 : 720}deg); }` const Svg = styled.svg` fill: ${p => p.theme.editorColours.icon}; transition: 0.1s linear all; ${p => p.disabled ? undefined : css` &:hover { fill: ${p => p.theme.editorColours.iconHover}; } `}; ` const Positioner = styled(BasePositioner)` cursor: ${({ disabled = false }) => (disabled ? 'auto' : 'pointer')}; transform: rotateY(180deg); ` const Circle = styled('circle')` fill: none; stroke: ${p => p.theme.editorColours.icon}; stroke-dasharray: 37.68; transition: opacity 0.3s ease-in-out; opacity: ${p => (p.animate ? 1 : 0)}; transform-origin: 9.5px 10px; animation: ${refreshFrames} 2s linear ${p => (p.animate ? 'infinite' : '')}; ` const Icon = styled('path')` transition: opacity 0.3s ease-in-out; opacity: ${p => (p.animate ? 0 : 1)}; transform-origin: 9.5px 10px; animation: ${reloadAction} 0.5s linear; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/SchemaReload.tsx ================================================ import * as React from 'react' import ReloadIcon from './Reload' import Polling from './Polling' import { ISettings } from '../../../types' import { createStructuredSelector } from 'reselect' import { getIsReloadingSchema } from '../../../state/sessions/selectors' import { connect } from 'react-redux' export interface Props { isPollingSchema: boolean isReloadingSchema: boolean onReloadSchema: () => any settings: ISettings } const SchemaReload = (props: Props) => { if (props.isPollingSchema) { return ( ) } return ( ) } const mapStateToProps = createStructuredSelector({ isReloadingSchema: getIsReloadingSchema, }) export default connect(mapStateToProps)(SchemaReload) ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx ================================================ import * as React from 'react' import { styled } from '../../../styled/index' import * as copy from 'copy-to-clipboard' import Share from '../../Share' import SchemaReload from './SchemaReload' import { createStructuredSelector } from 'reselect' import { getEndpoint, getSelectedSession, getEndpointUnreachable, getIsPollingSchema, } from '../../../state/sessions/selectors' import { connect } from 'react-redux' import { getFixedEndpoint } from '../../../state/general/selectors' import * as PropTypes from 'prop-types' import { editEndpoint, prettifyQuery, refetchSchema, } from '../../../state/sessions/actions' import { share } from '../../../state/sharing/actions' import { openHistory } from '../../../state/general/actions' import { getSettings } from '../../../state/workspace/reducers' import { Session } from '../../../state/sessions/reducers' import { ISettings } from '../../../types' export interface Props { endpoint: string shareEnabled?: boolean fixedEndpoint?: boolean isPollingSchema: boolean endpointUnreachable: boolean session: Session editEndpoint: (value: string) => void prettifyQuery: () => void openHistory: () => void share: () => void refetchSchema: () => void settings: ISettings } class TopBar extends React.Component { static contextTypes = { store: PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, getState: PropTypes.func.isRequired, }), } render() { const { endpointUnreachable, settings } = this.props return ( {endpointUnreachable ? ( Server cannot be reached ) : (
    )}
    {this.props.shareEnabled && ( )}
    ) } copyCurlToClipboard = () => { const curl = this.getCurl() copy(curl) } onChange = e => { this.props.editEndpoint(e.target.value) } onKeyDown = e => { if (e.keyCode === 13) { this.props.refetchSchema() } } openHistory = () => { this.props.openHistory() } getCurl = () => { const session = this.props.session let variables try { variables = JSON.parse(session.variables) } catch (e) { // } const data = JSON.stringify({ query: session.query, variables, operationName: session.operationName, }) let sessionHeaders try { sessionHeaders = JSON.parse(session.headers!) } catch (e) { // } const globalHeaders = this.props.settings['request.globalHeaders'] const headers = { 'Accept-Encoding': 'gzip, deflate, br', 'Content-Type': 'application/json', Accept: 'application/json', Connection: 'keep-alive', DNT: '1', Origin: location.origin || session.endpoint, ...globalHeaders, ...sessionHeaders, } const headersString = Object.keys(headers) .map(key => { const value = headers[key] return `-H '${key}: ${value}'` }) .join(' ') return `curl '${ session.endpoint }' ${headersString} --data-binary '${data}' --compressed` } } const mapStateToProps = createStructuredSelector({ endpoint: getEndpoint, fixedEndpoint: getFixedEndpoint, isPollingSchema: getIsPollingSchema, endpointUnreachable: getEndpointUnreachable, settings: getSettings, session: getSelectedSession, }) export default connect( mapStateToProps, { editEndpoint, prettifyQuery, openHistory, share, refetchSchema, }, )(TopBar) export const Button = styled.button` text-transform: uppercase; font-weight: 600; color: ${p => p.theme.editorColours.buttonText}; background: ${p => p.theme.editorColours.button}; border-radius: 2px; flex: 0 0 auto; letter-spacing: 0.53px; font-size: 14px; padding: 6px 9px 7px 10px; margin-left: 6px; cursor: pointer; transition: 0.1s linear background-color; &:first-child { margin-left: 0; } &:hover { background-color: ${p => p.theme.editorColours.buttonHover}; } ` const TopBarWrapper = styled.div` display: flex; background: ${p => p.theme.editorColours.navigationBar}; padding: 10px 10px 4px; align-items: center; ` interface UrlBarProps { active: boolean } const UrlBar = styled('input')` background: ${p => p.theme.editorColours.button}; border-radius: 4px; color: ${p => p.active ? p.theme.editorColours.navigationBarText : p.theme.editorColours.textInactive}; border: 1px solid ${p => p.theme.editorColours.background}; padding: 6px 12px; padding-left: 30px; font-size: 13px; flex: 1; ` const UrlBarWrapper = styled.div` flex: 1; margin-left: 6px; position: relative; display: flex; align-items: center; ` const ReachError = styled.div` position: absolute; right: 5px; display: flex; align-items: center; color: #f25c54; ` const Pulse = styled.div` width: 16px; height: 16px; background-color: ${p => p.theme.editorColours.icon}; border-radius: 100%; ` const SpinnerWrapper = styled.div` position: relative; margin: 6px; ` const Spinner = () => ( ) ================================================ FILE: packages/graphql-playground-react/src/components/Playground/TracingRow.tsx ================================================ import * as React from 'react' import styled from '../../styled/styled' const Row = styled.div` position: relative; font-size: 12px; display: table; padding-right: 25px; color: ${p => p.theme.editorColours.text}; ` const Bar = styled.span` display: inline-block; position: relative; margin: 0 10px; height: 1.5px; bottom: 4px; background: ${p => p.theme.editorColours.text}; ` const Duration = styled.span` font-size: 10px; color: ${p => p.theme.editorColours.textInactive}; ` const NameWrapper = styled.span` position: absolute; left: 0; transform: translateX(-100%); display: inline-flex; align-items: center; text-align: right; ` const Name = styled.span` margin-left: 10px; ` export interface TracingRowProps { path: Array startOffset: number duration: number } export interface TracingRowState { collapsed: boolean } export default class TracingRow extends React.Component< TracingRowProps, TracingRowState > { state = { collapsed: false, } render() { const { path, startOffset, duration } = this.props const factor = 1000 * 1000 const offsetLeft = startOffset / factor const barWidth = duration / factor return ( {path.slice(-2).map((p, index) => ( {`${index > 0 ? '.' : ''}${p}`} ))} {this.printDuration(duration)} ) } private printDuration(nanoSeconds) { const microSeconds = Math.round(nanoSeconds / 1000) if (microSeconds > 1000) { const ms = Math.round(microSeconds / 1000) return `${ms} ms` } return `${microSeconds} µs` } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/VariableEditor.tsx ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import * as React from 'react' import onHasCompletion from './onHasCompletion' import { connect } from 'react-redux' import { editVariables, editHeaders } from '../../state/sessions/actions' import { getVariables, getVariableToType, getHeaders, } from '../../state/sessions/selectors' import { createStructuredSelector } from 'reselect' import { VariableToType } from '../../state/sessions/reducers' import { styled } from '../../styled' /* tslint:disable */ interface Props { onHintInformationRender: (elem) => void onRunQuery: () => void prettifyQuery?: () => void getRef?: (editor: VariableEditor) => void } interface ReduxProps { value: string variableToType?: VariableToType onChange: (variable: string) => void } /** * VariableEditor * * An instance of CodeMirror for editing variables defined in QueryEditor. * * Props: * * - variableToType: A mapping of variable name to GraphQLType. * - value: The text of the editor. * - onEdit: A function called when the editor changes, given the edited text. * - readOnly: Turns the editor to read-only mode. * */ class VariableEditor extends React.PureComponent { cachedValue: any editor: any ignoreChangeEvent: boolean _node: any constructor(props) { super(props) // 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/hint/show-hint') require('codemirror/addon/edit/matchbrackets') require('codemirror/addon/edit/closebrackets') require('codemirror/addon/fold/brace-fold') require('codemirror/addon/fold/foldgutter') require('codemirror/addon/lint/lint') require('codemirror/addon/search/searchcursor') require('codemirror/addon/search/jump-to-line') require('codemirror/addon/dialog/dialog') require('codemirror/keymap/sublime') require('codemirror-graphql/variables/hint') require('codemirror-graphql/variables/lint') require('codemirror-graphql/variables/mode') this.editor = CodeMirror(this._node, { value: this.props.value || '', lineNumbers: true, tabSize: 2, mode: 'graphql-variables', theme: 'graphiql', keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, showCursorWhenSelecting: true, readOnly: false, foldGutter: { minFoldSize: 4, }, lint: { variableToType: this.props.variableToType ? this.props.variableToType.toJS() : undefined, }, hintOptions: { variableToType: this.props.variableToType ? this.props.variableToType.toJS() : undefined, closeOnUnfocus: false, completeSingle: false, }, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], extraKeys: { 'Cmd-Space': () => this.editor.showHint({ completeSingle: false }), 'Ctrl-Space': () => this.editor.showHint({ completeSingle: false }), 'Alt-Space': () => this.editor.showHint({ completeSingle: false }), 'Shift-Space': () => this.editor.showHint({ completeSingle: false }), 'Cmd-Enter': () => { if (this.props.onRunQuery) { this.props.onRunQuery() } }, 'Ctrl-Enter': () => { if (this.props.onRunQuery) { this.props.onRunQuery() } }, 'Shift-Ctrl-P': () => { if (this.props.prettifyQuery) { this.props.prettifyQuery() } }, // Persistent search box in Query Editor 'Cmd-F': 'findPersistent', 'Ctrl-F': 'findPersistent', // Editor improvements 'Ctrl-Left': 'goSubwordLeft', 'Ctrl-Right': 'goSubwordRight', 'Alt-Left': 'goGroupLeft', 'Alt-Right': 'goGroupRight', }, }) this.editor.on('change', this._onEdit) this.editor.on('keyup', this._onKeyUp) this.editor.on('hasCompletion', this._onHasCompletion) } componentDidUpdate(prevProps) { const CodeMirror = require('codemirror') // Ensure the changes caused by this update are not interpretted as // user-input changes which could otherwise result in an infinite // event loop. this.ignoreChangeEvent = true if (this.props.variableToType !== prevProps.variableToType) { this.editor.options.lint.variableToType = this.props.variableToType ? this.props.variableToType.toJS() : undefined this.editor.options.hintOptions.variableToType = this.props.variableToType ? this.props.variableToType.toJS() : undefined CodeMirror.signal(this.editor, 'change', this.editor) } if ( this.props.value !== prevProps.value && this.props.value !== this.cachedValue ) { this.cachedValue = this.props.value this.editor.setValue(this.props.value) } this.ignoreChangeEvent = false } componentWillUnmount() { this.editor.off('change', this._onEdit) this.editor.off('keyup', this._onKeyUp) this.editor.off('hasCompletion', this._onHasCompletion) this.editor = null } render() { return ( { this._node = node }} /> ) } /** * Public API for retrieving the CodeMirror instance from this * React component. */ getCodeMirror() { return this.editor } /** * Public API for retrieving the DOM client height for this component. */ getClientHeight() { return this._node && this._node.clientHeight } _onKeyUp = (cm, event) => { const code = event.keyCode if ( (code >= 65 && code <= 90) || // letters (!event.shiftKey && code >= 48 && code <= 57) || // numbers (event.shiftKey && code === 189) || // underscore (event.shiftKey && code === 222) // " ) { this.editor.execCommand('autocomplete') } } _onEdit = () => { if (!this.ignoreChangeEvent) { this.cachedValue = this.editor.getValue() this.props.onChange(this.cachedValue) } } _onHasCompletion = (cm, data) => { onHasCompletion(cm, data, this.props.onHintInformationRender) } } const mapStateToVariablesProps = createStructuredSelector({ value: getVariables, variableToType: getVariableToType, }) export const VariableEditorComponent = connect( mapStateToVariablesProps, { onChange: editVariables, }, )(VariableEditor) const mapStateToHeadersProps = createStructuredSelector({ value: getHeaders, }) export const HeadersEditorComponent = connect( mapStateToHeadersProps, { onChange: editHeaders, }, )(VariableEditor) const Editor = styled.div` flex: 1; height: 100%; position: relative; ` ================================================ FILE: packages/graphql-playground-react/src/components/Playground/onHasCompletion.tsx ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ const escapeHTML = require('escape-html'); import * as MD from 'markdown-it'; const md = new MD(); /** * Render a custom UI for CodeMirror's hint which includes additional info * about the type and description for the selected context. */ export default function onHasCompletion(cm, data, onHintInformationRender) { const CodeMirror = require('codemirror') let wrapper let information let deprecation // When a hint result is selected, we touch the UI. CodeMirror.on(data, 'select', (ctx, el) => { // Only the first time (usually when the hint UI is first displayed) // do we create the wrapping node. if (!wrapper) { // Wrap the existing hint UI, so we have a place to put information. const hintsUl = el.parentNode const container = hintsUl.parentNode wrapper = document.createElement('div') container.appendChild(wrapper) // CodeMirror vertically inverts the hint UI if there is not enough // space below the cursor. Since this modified UI appends to the bottom // of CodeMirror's existing UI, it could cover the cursor. This adjusts // the positioning of the hint UI to accomodate. let top = hintsUl.style.top let bottom = '' const cursorTop = cm.cursorCoords().top if (parseInt(top, 10) < cursorTop) { top = '' bottom = window.innerHeight - cursorTop + 3 + 'px' } // Style the wrapper, remove positioning from hints. Note that usage // of this option will need to specify CSS to remove some styles from // the existing hint UI. wrapper.className = 'CodeMirror-hints-wrapper' wrapper.style.left = hintsUl.style.left wrapper.style.top = top wrapper.style.bottom = bottom hintsUl.style.left = '' hintsUl.style.top = '' // This "information" node will contain the additional info about the // highlighted typeahead option. information = document.createElement('div') information.className = 'CodeMirror-hint-information' // This "deprecation" node will contain info about deprecated usage. deprecation = document.createElement('div') deprecation.className = 'CodeMirror-hint-deprecation' if (bottom) { wrapper.appendChild(deprecation) wrapper.appendChild(information) wrapper.appendChild(hintsUl) } else { wrapper.appendChild(hintsUl) wrapper.appendChild(information) wrapper.appendChild(deprecation) } const wrapperHeight = wrapper.clientHeight const currentTop = parseFloat(String(top).replace('px', '')) let newTop = currentTop if (wrapperHeight + currentTop > window.innerHeight) { newTop = window.innerHeight - 40 - wrapperHeight } wrapper.style.top = `${newTop}px` ;(global as any).wrapper = wrapper // When CodeMirror attempts to remove the hint UI, we detect that it was // removed from our wrapper and in turn remove the wrapper from the // original container. let onRemoveFn wrapper.addEventListener( 'DOMNodeRemoved', (onRemoveFn = event => { if (event.target === hintsUl) { wrapper.removeEventListener('DOMNodeRemoved', onRemoveFn) wrapper.parentNode.removeChild(wrapper) wrapper = null information = null onRemoveFn = null } }), ) } // Now that the UI has been set up, add info to information. const description = ctx.description ? md.render(ctx.description) : '' const type = ctx.type && ctx.type !== 'undefined' ? '' + renderType(ctx.type) + '' : '' information.innerHTML = '
    ' + (description.slice(0, 3) === '

    ' ? '

    ' + type + description.slice(3) : type + description) + '

    ' if (ctx.isDeprecated) { const reason = ctx.deprecationReason ? md.render(ctx.deprecationReason) : '' deprecation.innerHTML = 'Deprecated' + reason deprecation.style.display = 'block' } else { deprecation.style.display = 'none' } // Additional rendering? if (onHintInformationRender) { onHintInformationRender(information) } }) } function renderType(type) { return `${escapeHTML(type.toString())}` } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/InvalidSchemaError.ts ================================================ import type { GraphQLError } from "graphql"; export class InvalidSchemaError extends Error { constructor(validationErrors: readonly GraphQLError[]) { super( `Invalid schema Error:\n${validationErrors.join('\n')}`, ) } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/NoSchemaError.ts ================================================ export class NoSchemaError extends Error { constructor(endpoint: string) { super( `Schema could not be fetched.\nPlease check if the endpoint '${endpoint}' is a valid GraphQL Endpoint.`, ) } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/ageOfDate.ts ================================================ export default function ageOfDate(date: Date) { const now = new Date() const diffMs = Math.abs(date.getTime() - now.getTime()) const diffDays = Math.floor(diffMs / 86400000) const diffHrs = Math.floor((diffMs % 86400000) / 3600000) const diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000) if (diffDays > 0) { return `${diffDays} days ago` } if (diffHrs > 0) { return `${diffHrs} h ago` } if (diffMins > 0) { return `${diffMins} min ago` } const sec = Math.round(diffMs / 1000) return `${sec} sec${sec > 1 ? 's' : ''} ago` } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/createSDL.ts ================================================ 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' } } // Returns a prettified schema export function getSDL( schema: GraphQLSchema | null | undefined, commentsDisabled: boolean = true, ) { if (schema instanceof GraphQLSchema) { const rawSdl = printSchema(schema, { commentDescriptions: true }) if (commentsDisabled) { // Removes Comments but still has new lines // Removes newlines left behind by Comments return prettify(rawSdl, { printWidth: 80, tabWidth: 2, useTabs: false, }) } return prettify(rawSdl, { printWidth: 80, tabWidth: 2, useTabs: false, }) } 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 = 'introspectionSchema.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) } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/fibonacci-backoff.ts ================================================ import { memoize } from 'lodash' const fibonacci = memoize(num => { if (num <= 1) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2) }) export class Backoff { cb count = 1 running = true timeout maxRetries = 20 constructor(cb) { this.cb = cb } async start() { const fn = async () => { await this.cb() this.count++ // The first 5 attempts are fast, then fibonacci starts with n = 3 if (this.running && this.count < this.maxRetries) { this.timeout = setTimeout( fn, (this.count < 3 ? 5 : fibonacci(this.count - 5)) * 1000, ) } } fn() } stop = () => { this.running = false clearTimeout(this.timeout) } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/getQueryFacts.ts ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ import { typeFromAST, DocumentNode } from 'graphql' /** * Provided previous "queryFacts", a GraphQL schema, and a query document * string, return a set of facts about that query useful for GraphiQL features. * * If the query cannot be parsed, returns undefined. */ export function getQueryFacts(schema, documentAST: DocumentNode): any { const variableToType = schema ? collectVariables(schema, documentAST) : null // Collect operations by their names. const operations: any[] = [] documentAST.definitions.forEach(def => { if (def.kind === 'OperationDefinition') { operations.push(def) } }) return { variableToType, operations } } /** * Provided a schema and a document, produces a `variableToType` Object. */ export function collectVariables(schema, documentAST) { const variableToType = Object.create(null) documentAST.definitions.forEach(definition => { if (definition.kind === 'OperationDefinition') { const variableDefinitions = definition.variableDefinitions if (variableDefinitions) { variableDefinitions.forEach(({ variable, type }) => { const inputType = typeFromAST(schema, type) if (inputType) { variableToType[variable.name.value] = inputType } }) } } }) return variableToType } // function getDeepType(type) { // if (type.type) { // return getDeepType(type.type) // } // return type // } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/getQueryTypes.ts ================================================ import { QueryTypes } from '../../../state/sessions/reducers' export const getQueryTypes = (ast): QueryTypes => { let hasSubscription = false let hasQuery = false let hasMutation = false let firstOperationName = null // let operations: OperationDefinition[] = [] if (ast && ast.definitions) { ast.definitions.forEach(definition => { if (!firstOperationName) { firstOperationName = definition.name && definition.name.value } if (!firstOperationName) { firstOperationName = definition.selectionSet && definition.selectionSet.selections && definition.selectionSet.selections.length > 0 && definition.selectionSet.selections[0].name.value } if (definition.operation === 'subscription') { hasSubscription = true } if (definition.operation === 'query') { hasQuery = true } if (definition.operation === 'mutation') { hasMutation = true } // if (definition.name) { // operations.push({ // name: definition.name.value, // startLine: definition.loc.startToken.line, // endLine: definition.loc.endToken.line, // }) // } }) } return { firstOperationName, subscription: hasSubscription, query: hasQuery, mutation: hasMutation, // operations, } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/getSelectedOperationName.ts ================================================ /** * Copyright (c) Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ /** * Provided optional previous operations and selected name, and a next list of * operations, determine what the next selected operation should be. */ export default function getSelectedOperationName( prevOperations, prevSelectedOperationName, operations, ) { // If there are not enough operations to bother with, return nothing. if (!operations || operations.length < 1) { return } // If a previous selection still exists, continue to use it. const names = operations.map(op => op.name && op.name.value) if ( prevSelectedOperationName && names.indexOf(prevSelectedOperationName) !== -1 ) { return prevSelectedOperationName } // If a previous selection was the Nth operation, use the same Nth. if (prevSelectedOperationName && prevOperations) { const prevNames = prevOperations.map(op => op.name && op.name.value) const prevIndex = prevNames.indexOf(prevSelectedOperationName) if (prevIndex !== -1 && prevIndex < names.length) { return names[prevIndex] } } // Use the first operation. return names[0] } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/getWorkspaceId.ts ================================================ export function getWorkspaceId(props: { configPath?: string workspaceName?: string endpoint?: string }) { const configPathString = props.configPath ? `${props.configPath}~` : '' const workspaceNameString = props.workspaceName ? `${props.workspaceName}~` : '' return `${configPathString}${workspaceNameString}${props.endpoint}` } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/hasSubscription.ts ================================================ import { Operation } from 'apollo-link' import { OperationDefinitionNode } from 'graphql' export function isSubscription(operation: Operation): boolean { const selectedOperation = getSelectedOperation(operation) if (selectedOperation) { return selectedOperation.operation === 'subscription' } return false } function getSelectedOperation( operation: Operation, ): OperationDefinitionNode | undefined { if (operation.query.definitions.length === 1) { return operation.query.definitions[0] as OperationDefinitionNode } return operation.query.definitions.find( d => d.kind === 'OperationDefinition' && !!d.name && d.name.value === operation.operationName, ) as OperationDefinitionNode } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/immutableMemoize.ts ================================================ import { is } from 'immutable' export function immutableMemoize(fn) { let lastValue return arg => { const newValue = fn(arg) if (!is(lastValue, newValue)) { lastValue = newValue } return lastValue } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/makeOperation.ts ================================================ import { setIn } from 'immutable' import { parse } from 'graphql' import { Operation } from 'apollo-link' export interface GraphQLRequestData { query: string variables?: any operationName?: string extensions?: any } export function makeOperation(request: GraphQLRequestData): Operation { return setIn(request, ['query'], parse(request.query)) as any } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/parseHeaders.ts ================================================ export function parseHeaders(headers?: string) { if (!headers) { return {} } try { return JSON.parse(headers) } catch (e) { return {} } } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/session.ts ================================================ import { Session } from '../../../state/sessions/reducers' export function isSharingAuthorization(sharableSessions: Session[]): boolean { // If user's gonna share an Authorization header, // let's warn her // Check all sessions for (const session of sharableSessions) { // Check every header of each session for (const header of Object.keys(session.headers || {})) { // If there's a Authorization header present, // set the flag to `true` and stop the loop if (header.toLowerCase() === 'authorization') { // break return true } } } return false } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/shallowEqual.ts ================================================ const hasOwn = Object.prototype.hasOwnProperty function is(x, y) { if (x === y) { return x !== 0 || y !== 0 || 1 / x === 1 / y } else { return x !== x && y !== y } } export default function shallowEqual(objA, objB) { if (is(objA, objB)) { return true } if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false } const keysA = Object.keys(objA) const keysB = Object.keys(objB) if (keysA.length !== keysB.length) { return false } /* tslint:disable-next-line */ for (let i = 0; i < keysA.length; i++) { if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { return false } } return true } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/shouldUpdate.ts ================================================ // import { pick } from 'lodash' // // const cache: any = {} // // // TODO remove // export default function shouldUpdate( // name: string | null, // instance, // nextProps, // nextState, // ) { // const { props } = instance // let fields: any[] = [] // // if (name && cache[name]) { // fields = cache[name] // } // // if ((name && !cache[name]) || !name) { // fields = Object.keys(props).filter(k => typeof props[k] !== 'function') // // if (name) { // cache[name] = fields // } // } // // const oldProps = pick(props, fields) // const newProps = pick(nextProps, fields) // // const propsEqual = shallowEqual(oldProps, newProps) // const stateEqual = shallowEqual(instance.state, nextState) // // return !propsEqual && !stateEqual // } // // const hasOwnProperty = Object.prototype.hasOwnProperty // // function is(x, y) { // // SameValue algorithm // if (x === y) { // // Steps 1-5, 7-10 // // Steps 6.b-6.e: +0 != -0 // // Added the nonzero y check to make Flow happy, but it is redundant // return x !== 0 || y !== 0 || 1 / x === 1 / y // } else { // // Step 6.a: NaN == NaN // return x !== x && y !== y // } // } // // /** // * Performs equality by iterating through keys on an object and returning false // * when any key has values which are not strictly equal between the arguments. // * Returns true when the values of all keys are strictly equal. // */ // function shallowEqual(objA, objB) { // if (is(objA, objB)) { // return true // } // // if ( // typeof objA !== 'object' || // objA === null || // typeof objB !== 'object' || // objB === null // ) { // return false // } // // const keysA = Object.keys(objA) // const keysB = Object.keys(objB) // // if (keysA.length !== keysB.length) { // return false // } // // // Test for A's keys different from B. // for (const key of keysA) { // if (!hasOwnProperty.call(objB, key) || !is(objA[key], objB[key])) { // return false // } // } // // return true // } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/stack.ts ================================================ import { isType, GraphQLInterfaceType, GraphQLObjectType } from 'graphql' import { Map } from 'immutable' export function getNewStack(root, schema, stack: Map) { const path = stack.getIn(['field', 'path']) const splittedPath = path.split('/') let pointer: any = null let count = 0 let lastPointer: any = null let y = -1 while (splittedPath.length > 0) { const currentPath: string = splittedPath.shift()! if (count === 0) { pointer = root[currentPath] y = Object.keys(root).indexOf(currentPath) } else { const argFound = pointer.args.find(arg => arg.name === currentPath) lastPointer = pointer if (argFound) { pointer = argFound } else { if (pointer.type.ofType) { pointer = getDeeperType(pointer.type.ofType) } if (pointer.type) { pointer = pointer.type } pointer = pointer.getFields()[currentPath] || pointer.getInterfaces().find(i => i.name === currentPath) } } if (lastPointer) { y = getElementIndex(schema, lastPointer, pointer) } count++ } if (!pointer) { return null } pointer.path = path pointer.parent = lastPointer return stack.merge({ y, field: pointer, }) } // Return the deeper type found on object // For example [[[Company]!]!]! will return only Company export function getDeeperType(type: any, depth: number = 0): any { if (type.ofType && depth < 5) { return getDeeperType(type.ofType, depth + 1) } return type } export interface SerializedRoot { queries: any[] mutations: any[] subscriptions: any[] } export function getRootMap(schema): any { return { ...schema.getQueryType().getFields(), ...(schema.getMutationType && schema.getMutationType() && schema.getMutationType().getFields()), ...(schema.getSubscriptionType && schema.getSubscriptionType() && schema.getSubscriptionType().getFields()), } } // Serialize schema to get root object export function serializeRoot(schema): SerializedRoot { const obj: SerializedRoot = { queries: [], mutations: [], subscriptions: [], } const queryType = schema.getQueryType() const queryFieldMap = queryType.getFields() obj.queries = Object.keys(queryFieldMap).map(fieldName => { const field = queryFieldMap[fieldName] field.path = fieldName field.parent = null return field }) const mutationType = schema.getMutationType && schema.getMutationType() if (mutationType) { const mutationFieldMap = mutationType.getFields() obj.mutations = Object.keys(mutationFieldMap).map(fieldName => { const field = mutationFieldMap[fieldName] field.path = fieldName field.parent = null return field }) } ;(window as any).ss = schema const subscriptionType = schema.getSubscriptionType && schema.getSubscriptionType() if (subscriptionType) { const subscriptionFieldMap = subscriptionType.getFields() obj.subscriptions = Object.keys(subscriptionFieldMap).map(fieldName => { const field = subscriptionFieldMap[fieldName] field.path = fieldName field.parent = null return field }) } return obj } // Return element that match index on root object export function getElementRoot(obj: any, index: number) { let i = 0 if (obj.queries[index + i]) { return obj.queries[index + i] } i += obj.queries.length if (obj.mutations[index - i]) { return obj.mutations[index - i] } i += obj.mutations.length if (obj.subscriptions[index - i]) { return obj.subscriptions[index - i] } } export interface SerializedObj { fields: any[] interfaces: any[] args: any[] implementations: any[] } // Serialize field export function serialize(schema, field) { const obj: SerializedObj = { fields: [], interfaces: [], args: [], implementations: [], } let type = field.type || field const isVarType = isType(type) if (type.ofType) { type = getDeeperType(type.ofType) } // Get fields if (type.getFields) { const fieldMap = type.getFields() obj.fields = Object.keys(fieldMap).map(name => { const f = fieldMap[name] f.parent = field f.path = field.path + `/${name}` return f }) } // Get interfaces if (type instanceof GraphQLObjectType) { obj.interfaces = type.getInterfaces() } // Get args obj.args = field.args ? field.args : [] // Get implementations if (isVarType && type instanceof GraphQLInterfaceType) { obj.implementations = schema.getPossibleTypes(type) } return obj } // Return element that match index on object export function getElement(obj: any, index: number) { let i = 0 if (obj.interfaces[index + i]) { return obj.interfaces[index + i] } i += obj.interfaces.length if (obj.fields[index - i]) { return obj.fields[index - i] } i += obj.fields.length if (obj.args[index - i]) { return obj.args[index - i] } i += obj.args.length if (obj.implementations[index - i]) { return obj.implementations[index - i] } } export function getElementIndex(schema: any, main: any, element: any) { const obj = serialize(schema, main) const interfaceIndex = obj.interfaces.indexOf(element) if (interfaceIndex > -1) { return interfaceIndex } const fieldsIndex = obj.fields.indexOf(element) if (fieldsIndex > -1) { return obj.interfaces.length + fieldsIndex } const argsIndex = obj.args.indexOf(element) if (argsIndex > -1) { return obj.interfaces.length + obj.fields.length + argsIndex } const implementationIndex = obj.implementations.indexOf(element) if (implementationIndex > -1) { return ( obj.interfaces.length + obj.fields.length + obj.args.length + implementationIndex ) } return 0 } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/toJS.tsx ================================================ import * as React from 'react' import { isImmutable } from 'immutable' export const toJS = WrappedComponent => wrappedComponentProps => { const KEY = 0 const VALUE = 1 const propsJS = Object.entries(wrappedComponentProps).reduce( (newProps, wrappedComponentProp: any) => { newProps[wrappedComponentProp[KEY]] = isImmutable( wrappedComponentProp[VALUE], ) ? wrappedComponentProp[VALUE].toJS() : wrappedComponentProp[VALUE] return newProps }, {}, ) return } ================================================ FILE: packages/graphql-playground-react/src/components/Playground/util/whatChanged.ts ================================================ export function whatChanged( oldProps, newProps, oldState?: any, newState?: any, ) { return { props: getUnequalProps(oldProps, newProps), state: oldState && newState ? getUnequalProps(oldState, newState) : null, } } function getUnequalProps(obj1, obj2) { return Object.keys(obj1).filter(key => { return obj1[key] !== obj2[key] }) } ================================================ FILE: packages/graphql-playground-react/src/components/Playground.tsx ================================================ import * as React from 'react' import GraphQLEditor from './Playground/GraphQLEditor' import TabBar from './Playground/TabBar' import { ISettings } from '../types' import HistoryPopup from './HistoryPopup' import { styled } from '../styled' import Settings from './Settings' import { PlaygroundSettingsEditor, GraphQLConfigEditor } from './SettingsEditor' import { GraphQLConfig } from '../graphqlConfig' import FileEditor from './FileEditor' import { ApolloLink } from 'apollo-link' import * as app from '../../package.json' import { connect } from 'react-redux' import { selectTabIndex, selectNextTab, selectPrevTab, newSession, closeSelectedTab, saveSettings, saveConfig, setTracingSupported, injectHeaders, schemaFetchingError, schemaFetchingSuccess, } from '../state/sessions/actions' import { setConfigString } from '../state/general/actions' import { initState } from '../state/workspace/actions' import { GraphQLSchema, validateSchema } from 'graphql' import { createStructuredSelector } from 'reselect' import { getIsConfigTab, getIsSettingsTab, getIsFile, getFile, getHeaders, getIsReloadingSchema, getEndpoint, getIsPollingSchema, } from '../state/sessions/selectors' import { getHistoryOpen } from '../state/general/selectors' import { setLinkCreator, schemaFetcher, setSubscriptionEndpoint, } from '../state/sessions/fetchingSagas' import { Session } from '../state/sessions/reducers' import { getWorkspaceId } from './Playground/util/getWorkspaceId' import { getSettings, getSettingsString } from '../state/workspace/reducers' import { Backoff } from './Playground/util/fibonacci-backoff' import { debounce } from 'lodash' import { cachedPrintSchema } from './util' import { InvalidSchemaError } from './Playground/util/InvalidSchemaError' export interface Response { resultID: string date: string time: Date } export interface Props { endpoint: string sessionEndpoint: string subscriptionEndpoint?: string projectId?: string shareEnabled?: boolean fixedEndpoint?: boolean onSuccess?: (graphQLParams: any, response: any) => void isEndpoint?: boolean isApp?: boolean onChangeEndpoint?: (endpoint: string) => void share?: (state: any) => void shareUrl?: string onChangeSubscriptionsEndpoint?: (endpoint: string) => void getRef?: (ref: Playground) => void graphqlConfig?: any onSaveSettings?: () => void onChangeSettings?: (settingsString: string) => void onSaveConfig: () => void onChangeConfig: (configString: string) => void onUpdateSessionCount?: () => void config: GraphQLConfig configString: string configIsYaml: boolean canSaveConfig: boolean fixedEndpoints: boolean headers?: { [key: string]: string } configPath?: string createApolloLink?: ( session: Session, subscriptionEndpoint?: string, ) => ApolloLink workspaceName?: string schema?: GraphQLSchema } export interface ReduxProps { selectTabIndex: (index: number) => void selectNextTab: () => void selectPrevTab: () => void closeSelectedTab: () => void newSession: (endpoint: string, reuseHeaders: boolean) => void initState: (workspaceId: string, endpoint: string) => void saveConfig: () => void saveSettings: () => void setTracingSupported: (value: boolean) => void injectHeaders: ( headers: string | { [key: string]: string } | void, endpoint: string, ) => void setConfigString: (str: string) => void schemaFetchingError: (endpoint: string, error: string) => void schemaFetchingSuccess: ( endpoint: string, tracingSupported: boolean, isPollingSchema: boolean, ) => void isReloadingSchema: boolean isPollingSchema: boolean isConfigTab: boolean isSettingsTab: boolean isFile: boolean historyOpen: boolean file: string sessionHeaders?: any settings: ISettings settingsString: string } export interface State { schema?: GraphQLSchema } export interface CursorPosition { line: number ch: number } export { GraphQLEditor } export class Playground extends React.PureComponent { static defaultProps = { shareEnabled: false, } apolloLinks: { [sessionId: string]: any } = {} observers: { [sessionId: string]: any } = {} graphiqlComponents: any[] = [] // debounce as we call this on each http header or endpoint edit getSchema = debounce( async (props: Props & ReduxProps = this.props) => { if (props.schema) { return } if (this.mounted && this.state.schema && !props.isPollingSchema) { this.setState({ schema: undefined }) } let first = true if (this.backoff) { this.backoff.stop() } this.backoff = new Backoff(async () => { if (first) { await this.schemaGetter(props) first = false } else { await this.schemaGetter() } }) this.backoff.start() }, 600, { trailing: true }, // important to not miss the last call ) as any private backoff: Backoff private initialIndex: number = -1 private mounted = false private initialSchemaFetch = true constructor(props: Props & ReduxProps) { super(props) if (props.schema) { const validationErrors = validateSchema(props.schema) if (validationErrors && validationErrors.length > 0) { throw new InvalidSchemaError(validationErrors); } } this.state = { schema: props.schema, } ;(global as any).p = this if (typeof this.props.getRef === 'function') { this.props.getRef(this) } setLinkCreator(props.createApolloLink) this.getSchema() setSubscriptionEndpoint(props.subscriptionEndpoint) } UNSAFE_componentWillMount() { // init redux this.props.initState(getWorkspaceId(this.props), this.props.endpoint) this.props.setConfigString(this.props.configString) this.props.injectHeaders(this.props.headers, this.props.endpoint) } componentDidMount() { if (this.initialIndex > -1) { this.setState({ selectedSessionIndex: this.initialIndex, } as State) } this.mounted = true } UNSAFE_componentWillReceiveProps(nextProps: Props & ReduxProps) { if (this.props.createApolloLink !== nextProps.createApolloLink) { setLinkCreator(nextProps.createApolloLink) } if ( nextProps.headers !== this.props.headers || nextProps.endpoint !== this.props.endpoint || nextProps.workspaceName !== this.props.workspaceName || nextProps.sessionHeaders !== this.props.sessionHeaders || nextProps.sessionEndpoint !== this.props.sessionEndpoint ) { this.getSchema(nextProps) } if (this.props.isReloadingSchema && !nextProps.isReloadingSchema) { setTimeout(() => { this.getSchema(nextProps) }) } if ( this.props.endpoint !== nextProps.endpoint || this.props.configPath !== nextProps.configPath || nextProps.workspaceName !== this.props.workspaceName ) { this.props.initState(getWorkspaceId(nextProps), nextProps.endpoint) } if (this.props.subscriptionEndpoint !== nextProps.subscriptionEndpoint) { setSubscriptionEndpoint(nextProps.subscriptionEndpoint) } if (nextProps.headers !== this.props.headers) { this.props.injectHeaders(nextProps.headers, nextProps.endpoint) } if (nextProps.configString !== this.props.configString) { this.props.setConfigString(nextProps.configString) } if (nextProps.schema !== this.props.schema) { const validationErrors = validateSchema(nextProps.schema) if (validationErrors && validationErrors.length > 0) { throw new InvalidSchemaError(validationErrors); } this.setState({ schema: nextProps.schema }) } } async schemaGetter(propsInput?: Props & ReduxProps) { const props = propsInput || this.props const endpoint = props.sessionEndpoint || props.endpoint const currentSchema = this.state.schema const globalHeaders = props.settings['request.globalHeaders'] try { const data = { endpoint, headers: props.sessionHeaders && props.sessionHeaders.length > 0 ? JSON.stringify({ ...globalHeaders, ...JSON.parse(props.sessionHeaders), }) : JSON.stringify({ ...globalHeaders, ...props.headers, }), credentials: props.settings['request.credentials'], useTracingHeader: !this.initialSchemaFetch && props.settings['tracing.tracingSupported'], } const schema = await schemaFetcher.fetch(data) schemaFetcher.subscribe(data, newSchema => { if ( data.endpoint === this.props.endpoint || data.endpoint === this.props.sessionEndpoint ) { this.updateSchema(currentSchema, newSchema, props) } }) if (schema) { this.updateSchema(currentSchema, schema.schema, props) if (this.initialSchemaFetch) { this.props.schemaFetchingSuccess( data.endpoint, schema.tracingSupported, props.isPollingSchema, ) this.initialSchemaFetch = false } this.backoff.stop() } } catch (e) { // tslint:disable-next-line console.error(e) this.props.schemaFetchingError(endpoint, e.message) } } render() { const { version }: any = app window.version = version return ( {this.props.isConfigTab ? ( ) : this.props.isSettingsTab ? ( ) : this.props.isFile && this.props.file ? ( ) : ( )} {this.props.historyOpen && this.renderHistoryPopup()} ) } renderHistoryPopup() { return } setRef = (index: number, ref: any) => { this.graphiqlComponents[index] = ref ? ref.getWrappedInstance() : ref } public closeTab = () => { this.props.closeSelectedTab() } public nextTab = () => { this.props.selectNextTab() } public prevTab = () => { this.props.selectPrevTab() } public switchTab = (index: number) => { this.props.selectTabIndex(index) } handleSaveConfig = () => { this.props.saveConfig() this.props.onSaveConfig() } handleSaveSettings = () => { this.props.saveSettings() this.props.onSaveSettings() } private createSession = () => { this.props.newSession( this.props.endpoint, this.props.settings['editor.reuseHeaders'], ) } private updateSchema( currentSchema: GraphQLSchema | undefined, newSchema: GraphQLSchema, props: Readonly<{ children?: React.ReactNode }> & Readonly, ) { // first check for reference equality if (currentSchema !== newSchema) { // if references are not equal, do an equality check on the printed schema const currentSchemaStr = currentSchema ? cachedPrintSchema(currentSchema) : null const newSchemaStr = cachedPrintSchema(newSchema) if (newSchemaStr !== currentSchemaStr || !props.isPollingSchema) { this.setState({ schema: newSchema }) } } } get httpApiPrefix() { return this.props.endpoint.match(/(https?:\/\/.*?)\/?/)![1] } } const mapStateToProps = createStructuredSelector({ isConfigTab: getIsConfigTab, isSettingsTab: getIsSettingsTab, isFile: getIsFile, historyOpen: getHistoryOpen, file: getFile, sessionHeaders: getHeaders, settings: getSettings, settingsString: getSettingsString, isReloadingSchema: getIsReloadingSchema, isPollingSchema: getIsPollingSchema, sessionEndpoint: getEndpoint, }) export default connect( mapStateToProps, { selectTabIndex, selectNextTab, selectPrevTab, newSession, closeSelectedTab, initState, saveSettings, saveConfig, setTracingSupported, injectHeaders, setConfigString, schemaFetchingError, schemaFetchingSuccess, }, )(Playground) const PlaygroundContainer = styled.div` flex: 1; display: flex; flex-direction: column; height: 100%; margin: 0; padding: 0; overflow: hidden; margin-right: -1px !important; line-height: 1.5; font-family: 'Open Sans', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; letter-spacing: 0.53px; color: rgba(0, 0, 0, 0.8); a:active, a:focus, button:focus, input:focus { outline: none; } ` const GraphiqlsContainer = styled.div` height: calc(100vh - 57px); position: relative; overflow: hidden; ` const GraphiqlWrapper = styled.div` width: 100%; height: 100%; position: relative; overflow: hidden; visibility: hidden; &.active { visibility: visible; } ` ================================================ FILE: packages/graphql-playground-react/src/components/PlaygroundWrapper.tsx ================================================ import * as React from 'react' import Playground, { Playground as IPlayground } from './Playground' import { Helmet } from 'react-helmet' import { GraphQLConfig } from '../graphqlConfig' import * as yaml from 'js-yaml' import ProjectsSideNav from './ProjectsSideNav' import { styled, ThemeProvider, theme as styledTheme, keyframes, } from '../styled' import { darkColours, lightColours, darkEditorColours, lightEditorColours, EditorColours, } from '../styled/theme' // import OldThemeProvider from './Theme/ThemeProvider' import { getActiveEndpoints } from './util' import { ISettings } from '../types' import { connect } from 'react-redux' import { getTheme, getSettings } from '../state/workspace/reducers' import { Session, Tab } from '../state/sessions/reducers' import { ApolloLink } from 'apollo-link' import { injectTabs } from '../state/workspace/actions' import { buildSchema, buildClientSchema, GraphQLSchema } from 'graphql' function getParameterByName(name: string, uri?: string): string | null { const url = uri || window.location.href name = name.replace(/[\[\]]/g, '\\$&') const regexa = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') const results = regexa.exec(url) if (!results || !results[2]) { return null } return decodeURIComponent(results[2].replace(/\+/g, ' ')) } export interface PlaygroundWrapperProps { endpoint?: string endpointUrl?: string subscriptionEndpoint?: string setTitle?: boolean settings?: ISettings shareEnabled?: boolean fixedEndpoint?: boolean folderName?: string configString?: string showNewWorkspace?: boolean isElectron?: boolean canSaveConfig?: boolean onSaveConfig?: (configString: string) => void onNewWorkspace?: () => void getRef?: (ref: any) => void platformToken?: string env?: any config?: GraphQLConfig configPath?: string injectedState?: any createApolloLink?: ( session: Session, subscriptionEndpoint?: string, ) => ApolloLink tabs?: Tab[] schema?: { __schema: any } // introspection result codeTheme?: EditorColours workspaceName?: string headers?: any } export interface ReduxProps { theme: string injectTabs: (tabs: Tab[]) => void } export interface State { endpoint: string subscriptionPrefix?: string subscriptionEndpoint?: string shareUrl?: string platformToken?: string configIsYaml?: boolean configString?: string activeProjectName?: string activeEnv?: string headers?: any schema?: GraphQLSchema } class PlaygroundWrapper extends React.Component< PlaygroundWrapperProps & ReduxProps, State > { playground: IPlayground constructor(props: PlaygroundWrapperProps & ReduxProps) { super(props) ;(global as any).m = this this.state = this.mapPropsToState(props) this.removeLoader() } mapPropsToState(props: PlaygroundWrapperProps): State { const configIsYaml = props.configString ? this.isConfigYaml(props.configString) : false const { activeEnv, projectName } = this.getInitialActiveEnv(props.config) let endpoint = props.endpoint || props.endpointUrl || getParameterByName('endpoint') || location.href const result = this.extractEndpointAndHeaders(endpoint) endpoint = result.endpoint let headers = result.headers let subscriptionEndpoint: any = props.subscriptionEndpoint || getParameterByName('subscriptionEndpoint') if (props.configString && props.config && activeEnv) { const endpoints = getActiveEndpoints(props.config, activeEnv, projectName) endpoint = endpoints.endpoint subscriptionEndpoint = endpoints.subscriptionEndpoint headers = endpoints.headers } subscriptionEndpoint = this.normalizeSubscriptionUrl(endpoint, subscriptionEndpoint) || undefined return { endpoint: this.absolutizeUrl(endpoint), platformToken: props.platformToken || localStorage.getItem('platform-token') || undefined, subscriptionEndpoint, configIsYaml, configString: props.configString, activeEnv, activeProjectName: projectName, headers, } } extractEndpointAndHeaders(endpoint) { const splitted = endpoint.split('?') if (splitted.length === 1) { return { endpoint } } try { const headers = getParameterByName('headers', endpoint) if (headers) { return { headers: JSON.parse(headers), endpoint: splitted[0] } } } catch (e) { // } return { endpoint: splitted[0] } } removeLoader() { const loadingWrapper = document.getElementById('loading-wrapper') if (loadingWrapper) { loadingWrapper.remove() } } normalizeSubscriptionUrl(endpoint, subscriptionEndpoint) { if (subscriptionEndpoint) { if (subscriptionEndpoint.startsWith('/')) { const secure = endpoint.includes('https') || location.href.includes('https') ? 's' : '' return `ws${secure}://${location.host}${subscriptionEndpoint}` } else { return subscriptionEndpoint.replace(/^http/, 'ws') } } return this.getGraphcoolSubscriptionEndpoint(endpoint).replace( /^http/, 'ws', ) } getGraphcoolSubscriptionEndpoint(endpoint) { if (endpoint.includes('api.graph.cool')) { return `wss://subscriptions.graph.cool/v1/${ endpoint.split('/').slice(-1)[0] }` } return endpoint.replace(/^http/, 'ws') } UNSAFE_componentWillReceiveProps(nextProps: PlaygroundWrapperProps & ReduxProps) { // Reactive props (props that cause a state change upon being changed) if ( nextProps.endpoint !== this.props.endpoint || nextProps.endpointUrl !== this.props.endpointUrl || nextProps.subscriptionEndpoint !== this.props.subscriptionEndpoint || nextProps.configString !== this.props.configString || nextProps.platformToken !== this.props.platformToken || nextProps.config !== this.props.config ) { this.setState(this.mapPropsToState(nextProps)) this.setInitialWorkspace(nextProps) } } getInitialActiveEnv( config?: GraphQLConfig, ): { projectName?: string; activeEnv?: string } { if (config) { if (config.extensions && config.extensions.endpoints) { return { activeEnv: Object.keys(config.extensions.endpoints)[0], } } if (config.projects) { const projectName = Object.keys(config.projects)[0] const project = config.projects[projectName] if (project.extensions && project.extensions.endpoints) { return { activeEnv: Object.keys(project.extensions.endpoints)[0], projectName, } } } } return {} } isConfigYaml(configString: string) { try { yaml.safeLoad(configString) return true } catch (e) { // } return false } absolutizeUrl(url) { if (url.startsWith('/')) { return location.origin + url } return url } UNSAFE_componentWillMount() { const platformToken = getParameterByName('platform-token') if (platformToken && platformToken.length > 0) { localStorage.setItem('platform-token', platformToken) window.location.replace(window.location.origin + window.location.pathname) } } componentDidMount() { if (this.state.subscriptionEndpoint === '') { this.updateSubscriptionsUrl() } setTimeout(() => { this.removePlaygroundInClass() }, 5000) this.setInitialWorkspace() if (this.props.tabs) { this.props.injectTabs(this.props.tabs) } else { const query = getParameterByName('query') if (query) { const endpoint = getParameterByName('endpoint') || this.state.endpoint this.props.injectTabs([{ query, endpoint }]) } else { const tabsString = getParameterByName('tabs') if (tabsString) { try { const tabs = JSON.parse(tabsString) this.props.injectTabs(tabs) } catch (e) { // } } } } if (this.props.schema) { // in this case it's sdl if (typeof this.props.schema === 'string') { this.setState({ schema: buildSchema(this.props.schema) }) // if it's an object, it must be an introspection query } else { this.setState({ schema: buildClientSchema(this.props.schema) }) } } } setInitialWorkspace(props = this.props) { if (props.config) { const activeEnv = this.getInitialActiveEnv(props.config) const endpoints = getActiveEndpoints( props.config, activeEnv.activeEnv!, activeEnv.projectName, ) const endpoint = endpoints.endpoint const subscriptionEndpoint = endpoints.subscriptionEndpoint || this.normalizeSubscriptionUrl(endpoint, endpoints.subscriptionEndpoint) const headers = endpoints.headers this.setState({ endpoint, subscriptionEndpoint, headers, activeEnv: activeEnv.activeEnv, activeProjectName: activeEnv.projectName, }) } } removePlaygroundInClass() { const root = document.getElementById('root') if (root) { root.classList.remove('playgroundIn') } } render() { const title = this.props.setTitle ? ( {this.getTitle()} ) : null const defaultHeaders = this.props.headers || {} const stateHeaders = this.state.headers || {} const combinedHeaders = { ...defaultHeaders, ...stateHeaders } const { theme } = this.props return (
    {title} {this.props.config && this.state.activeEnv && ( )}
    ) } handleUpdateSessionCount = () => { this.forceUpdate() } getPlaygroundRef = ref => { this.playground = ref if (typeof this.props.getRef === 'function') { this.props.getRef(ref) } } handleChangeConfig = (configString: string) => { this.setState({ configString }) } handleSaveConfig = () => { /* tslint:disable-next-line */ if (typeof this.props.onSaveConfig === 'function') { /* tslint:disable-next-line */ this.props.onSaveConfig(this.state.configString!) } } handleSelectEnv = (env: string, projectName?: string) => { const { endpoint, subscriptionEndpoint, headers } = getActiveEndpoints( this.props.config!, env, projectName, )! this.setState({ activeEnv: env, endpoint, headers, subscriptionEndpoint: this.normalizeSubscriptionUrl( endpoint, subscriptionEndpoint, ), activeProjectName: projectName, }) } private handleChangeEndpoint = endpoint => { this.setState({ endpoint }) } private handleChangeSubscriptionsEndpoint = subscriptionEndpoint => { this.setState({ subscriptionEndpoint }) } private getTitle() { if ( this.state.platformToken || this.state.endpoint.includes('api.graph.cool') ) { const projectId = this.getProjectId(this.state.endpoint) const cluster = this.state.endpoint.includes('api.graph.cool') ? 'shared' : 'local' return `${cluster}/${projectId} - Playground` } return `Playground - ${this.state.endpoint}` } private async updateSubscriptionsUrl() { const candidates = this.getSubscriptionsUrlCandidated(this.state.endpoint) const validCandidate = await find(candidates, candidate => this.wsEndpointValid(candidate), ) if (validCandidate) { this.setState({ subscriptionEndpoint: validCandidate }) } } private getSubscriptionsUrlCandidated(endpoint): string[] { const candidates: string[] = [] candidates.push(endpoint.replace('https', 'wss').replace('http', 'ws')) if (endpoint.includes('graph.cool')) { candidates.push( `wss://subscriptions.graph.cool/v1/${this.getProjectId(endpoint)}`, ) } if (endpoint.includes('/simple/v1/')) { // it's a graphcool local endpoint const host = endpoint.match(/https?:\/\/(.*?)\//) candidates.push( `ws://${host![1]}/subscriptions/v1/${this.getProjectId(endpoint)}`, ) } return candidates } private wsEndpointValid(url): Promise { return new Promise(resolve => { const socket = new WebSocket(url, 'graphql-ws') socket.addEventListener('open', event => { socket.send(JSON.stringify({ type: 'connection_init' })) }) socket.addEventListener('message', event => { const data = JSON.parse(event.data) if (data.type === 'connection_ack') { resolve(true) } }) socket.addEventListener('error', event => { resolve(false) }) setTimeout(() => { resolve(false) }, 1000) }) } private getProjectId(endpoint) { return endpoint.split('/').slice(-1)[0] } } const mapStateToProps = (state, ownProps) => { const theme = ownProps.theme || getTheme(state, ownProps.settings) const settings = getSettings(state) return { theme, settings } } export default connect( mapStateToProps, { injectTabs }, )(PlaygroundWrapper) async function find( iterable: any[], predicate: (item?: any, index?: number) => Promise, ): Promise { for (let i = 0; i < iterable.length; i++) { const element = iterable[i] const result = await predicate(element, i) if (result) { return element } } return null } const appearIn = keyframes` from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } ` const App = styled.div` display: flex; width: 100%; opacity: 0; transform: translateY(10px); animation: ${appearIn} 0.5s ease-out forwards 0.2s; ` ================================================ FILE: packages/graphql-playground-react/src/components/Popup.tsx ================================================ import * as React from 'react' import * as Modal from 'react-modal' import { styled } from '../styled' export interface Props { onRequestClose: () => void width?: number closeInside?: boolean darkBg?: boolean } export const fieldModalStyle = { overlay: { zIndex: 100, backgroundColor: 'rgba(255,255,255,.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', }, content: { position: 'relative', width: 554, height: 'auto', top: 'initial', left: 'initial', right: 'initial', bottom: 'initial', borderRadius: 2, padding: 0, border: 'none', background: 'none', // boxShadow: '0 1px 7px rgba(0,0,0,.2)', overflow: 'visible', }, } export default class Popup extends React.Component { render() { const { darkBg } = this.props const modalStyle = { overlay: { ...fieldModalStyle.overlay, background: darkBg ? 'rgba(23,42,58,1.0)' : 'rgba(255,255,255,.9)', }, content: { ...fieldModalStyle.content, width: this.props.width || 560, }, } return ( {this.props.children} ) } } const ContentWrapper = styled.div`` ================================================ FILE: packages/graphql-playground-react/src/components/ProjectsSideNav.tsx ================================================ import * as React from 'react' import { GraphQLConfig, GraphQLConfigEnpointsMapData } from '../graphqlConfig' import ProjectsSideNavItem from './ProjectsSideNavItem' import { SettingsIcon, AddFullIcon } from './Icons' import { styled } from '../styled/index' import { getEndpointFromEndpointConfig } from './util' import { createStructuredSelector } from 'reselect' import { connect } from 'react-redux' import { getSessionCounts } from '../state/workspace/reducers' import { Map } from 'immutable' import { getWorkspaceId } from './Playground/util/getWorkspaceId' import { openConfigTab } from '../state/sessions/actions' export interface Props { config: GraphQLConfig folderName: string theme: string activeEnv: string onSelectEnv: (endpoint: string, projectName?: string) => void onNewWorkspace?: () => void showNewWorkspace: boolean isElectron: boolean activeProjectName?: string configPath?: string } export interface ReduxProps { counts: Map openConfigTab: () => void } class ProjectsSideNav extends React.Component { render() { const { config, folderName, onNewWorkspace, isElectron } = this.props const endpoints = config.extensions && config.extensions.endpoints const projects = config.projects return ( {folderName} {endpoints && this.renderEndpoints(endpoints)} {projects && Object.keys(projects).map(projectName => { const project = projects[projectName] const projectEndpoints = project.extensions && project.extensions.endpoints if (!projectEndpoints) { return null } return ( {projectName} {this.renderEndpoints(projectEndpoints, projectName)} ) })} {isElectron && (
    NEW WORKSPACE
    )}
    ) } private renderEndpoints( endpoints: GraphQLConfigEnpointsMapData, projectName?: string, ) { return Object.keys(endpoints).map(env => { const { endpoint } = getEndpointFromEndpointConfig(endpoints[env]) const count = this.props.counts.get( getWorkspaceId({ endpoint, configPath: this.props.configPath, workspaceName: projectName, }), ) || 1 return ( ) }) } } const mapStateToProps = createStructuredSelector({ counts: getSessionCounts, }) export default connect( mapStateToProps, { openConfigTab }, )(ProjectsSideNav) const SideNav = styled.div` display: flex; flex-direction: column; justify-content: space-between; background: ${p => p.theme.editorColours.sidebar}; flex-basis: 222px; color: ${p => p.theme.editorColours.text}; border-right: 6px solid ${p => p.theme.editorColours.background}; ` const List = styled.div` -webkit-app-region: drag; padding-top: ${(p: any) => (p.isElectron ? 48 : 20)}px; display: flex; flex-direction: column; background: ${p => p.theme.editorColours.sidebarTop}; ` const Title = styled.div` font-size: 16px; font-weight: 600; color: ${p => p.theme.editorColours.text}; word-break: break-word; ` const TitleRow = styled.div` display: flex; align-items: center; justify-content: space-evenly; margin: 0 15px 20px 15px; svg { -webkit-app-region: no-drag; min-width: 18px; min-height: 18px; cursor: pointer; fill: ${p => p.theme.editorColours.icon}; transition: 0.1s linear fill; } &:hover { svg { fill: ${p => p.theme.editorColours.iconHover}; } } ` const Project = styled.div` display: flex; flex-direction: column; & + & { margin-top: 12px; } &:last-child { margin-bottom: 32px; } ` const ProjectName = styled.div` font-size: 14px; color: ${p => p.theme.editorColours.text}; font-weight: 600; letter-spacing: 0.53px; margin: 0 10px 6px 30px; word-break: break-word; ` const Footer = styled.div` display: flex; justify-content: center; margin: 32px 0; background: ${p => p.theme.editorColours.sidebarBottom}; ` const WorkspaceButton = styled.button` padding: 10px; display: flex; align-items: center; border-radius: 2px; cursor: pointer; font-size: 14px; font-weight: 600; letter-spacing: 0.53px; color: ${p => p.theme.editorColours.buttonWorkspaceText}; background-color: ${p => p.theme.editorColours.buttonWorkspace}; transition: 0.1s linear all; &:hover { background-color: ${p => p.theme.editorColours.buttonWorkspaceHover}; } i { margin-right: 6px; } svg { min-width: 18px; min-height: 18px; stroke: ${p => p.theme.editorColours.buttonWorkspaceText}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/ProjectsSideNavItem.tsx ================================================ import * as React from 'react' import { styled } from '../styled/index' export interface Props { env: string onSelectEnv: (env: string, projectName?: string) => void activeEnv: string count: number deep: boolean projectName?: string activeProjectName?: string } export default class ProjectsSideNavItem extends React.Component { render() { const { env, activeEnv, count, deep, activeProjectName, projectName, } = this.props const active = activeEnv === env && activeProjectName === projectName return ( {env} {count} ) } private selectEndpoint = () => { this.props.onSelectEnv(this.props.env, this.props.projectName) } } interface SidebarItemProps { active: boolean deep?: boolean } const ListItem = styled('div')` padding: 10px 10px 10px ${p => (p.deep ? '43px' : '38px')}; word-break: break-word; font-weight: 600; cursor: pointer; font-size: 12px; display: flex; justify-content: space-between; align-items: center; background: ${p => p.active ? p.theme.editorColours.sidebarItemActive : 'transparent'}; border-left: 4px solid ${p => (p.active ? p.theme.editorColours.sidebarItemSide : 'transparent')}; border-radius: 2px; &:hover { background: ${p => p.theme.editorColours.sidebarItemActive}; } ` const Count = styled('div')` border-radius: 6px; min-width: 18px; min-height: 18px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold; background: ${p => p.theme.editorColours.sidebarItemSessions}; color: ${p => p.theme.editorColours.text}; opacity: ${p => (p.active ? 1 : 0.6)}; transition: 0.1s linear all; ` as any ================================================ FILE: packages/graphql-playground-react/src/components/Root.tsx ================================================ import * as React from 'react' import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom' import GraphQLBinApp from './GraphQLBinApp' export default class Root extends React.Component<{}, {}> { render() { return ( ) } } const RedirectToOldPlayground = props => { location.href = `https://legacy.graphqlbin.com${location.pathname}${ location.search }` return null } ================================================ FILE: packages/graphql-playground-react/src/components/Settings.tsx ================================================ import * as React from 'react' import { SettingsIcon } from './Icons' import { styled } from '../styled' import { openSettingsTab } from '../state/sessions/actions' import { connect } from 'react-redux' export interface Props { onClick: () => void } class Settings extends React.Component { render() { return ( ) } } export default connect( null, { onClick: openSettingsTab }, )(Settings) const Wrapper = styled.div` position: absolute; z-index: 1005; right: 20px; top: 17px; ` const IconWrapper = styled.div` position: relative; cursor: pointer; svg { fill: ${p => p.theme.editorColours.icon}; transition: 0.1s linear fill; } &:hover { svg { fill: ${p => p.theme.editorColours.iconHover}; } } ` ================================================ FILE: packages/graphql-playground-react/src/components/SettingsEditor.tsx ================================================ import * as React from 'react' import { styled } from '../styled' import { Button } from './Playground/TopBar/TopBar' import { ConfigEditor } from './Playground/ConfigEditor' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getConfigString } from '../state/general/selectors' import { setSettingsString, setConfigString } from '../state/general/actions' import { editSettings, saveSettings } from '../state/sessions/actions' import { getSettingsString } from '../state/workspace/reducers' import EditorWrapper, { Container } from './Playground/EditorWrapper' export interface Props { value: string onChange: (value: string) => void onSave: () => void isYaml?: boolean isConfig?: boolean readOnly?: boolean } // TODO: Trigger onSave on CMD+S or CTRL+S export class SettingsEditor extends React.Component { componentDidMount() { window.addEventListener('keydown', this.handleKeydown, true) } render() { const { isConfig } = this.props return ( {window.version} {!this.props.readOnly && ( )} ) } private handleKeydown = (e: KeyboardEvent) => { if (e.key === 's' && e.metaKey) { e.preventDefault() this.props.onSave() } } } const playgroundSettingsSelector = createStructuredSelector({ value: getSettingsString, }) interface HOCProps { editSettings: () => void saveSettings: () => void onSave: (value: string) => void } // tslint:disable class SettingsEditorHOC extends React.Component< Props & HOCProps, { value: string } > { constructor(props) { super(props) this.state = { value: props.value } } UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.value !== this.props.value) { this.setState({ value: nextProps.value }) } } render() { return ( ) } handleChange = value => { this.setState({ value }) this.props.editSettings() } handleSave = () => { this.props.onChange(this.state.value) this.props.saveSettings() } } export const PlaygroundSettingsEditor = connect( playgroundSettingsSelector, { onChange: setSettingsString, editSettings, saveSettings, }, )(SettingsEditorHOC) const configSelector = createStructuredSelector({ value: getConfigString, }) export const GraphQLConfigEditor = connect( configSelector, { onChange: setConfigString, }, )(SettingsEditor) const Wrapper = styled.div` background: ${p => p.theme.editorColours.resultBackground}; position: relative; display: flex; flex-flow: column; flex: 1 1 0; .CodeMirror { background: ${p => p.theme.editorColours.resultBackground}; .CodeMirror-code { color: rgba(255, 255, 255, 0.7); } .cm-atom { color: rgba(42, 126, 210, 1); } } ` const ButtonWrapper = styled.div` position: absolute; top: 16px; right: 16px; z-index: 2; ` const PlaygroundVersion = styled.span` position: absolute; right: 20px; bottom: 17px; color: ${p => p.theme.editorColours.textInactive}; font-weight: 700; margin-right: 14px; ` ================================================ FILE: packages/graphql-playground-react/src/components/Share.tsx ================================================ import * as React from 'react' import { ShareIcon } from './Icons' import ToggleButton from './ToggleButton' import Tooltip from './Tooltip' import { Button } from './Button' import Copy from './Copy' import { keyframes, styled, ThemeInterface, withTheme } from '../styled' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getSharingHistory, getSharingHeaders, getSharingAllTabs, getShareUrl, } from '../state/sharing/selectors' import { toggleShareHistory, toggleShareHeaders, toggleShareAllTabs, share, } from '../state/sharing/actions' export interface SharingProps { allTabs: boolean headers: boolean history: boolean theme: ThemeInterface toggleShareHistory: () => void toggleShareHeaders: () => void toggleShareAllTabs: () => void share: () => void shareUrl?: string reshare: boolean isSharingAuthorization: boolean children?: any } export interface State { open: boolean } class Share extends React.Component { constructor(props) { super(props) this.state = { open: false, } } render() { const { open } = this.state const { allTabs, headers, history, shareUrl, reshare, theme } = this.props return (
    {this.props.children}
    {open && (
    Share all tabs{' '} HTTP headers{' '} History{' '} {shareUrl && ( )}
    )} ) } private share = () => { this.props.share() } private renderAuthSharingWarning = () => { if (!this.props.isSharingAuthorization) { return null } return } private toggleTooltip = () => { this.setState(state => ({ open: !state.open })) } } const mapStateToProps = createStructuredSelector({ history: getSharingHistory, headers: getSharingHeaders, allTabs: getSharingAllTabs, shareUrl: getShareUrl, }) export default withTheme( connect( mapStateToProps, { toggleShareAllTabs, toggleShareHeaders, toggleShareHistory, share, }, )(Share), ) const AuthSharingWarning = () => ( Watch out! You’re sharing your Authorization header with the world! ) // TODO: use theme const pulse = keyframes` 0% { transform: scale(1.04); } 100% { transform: scale(1); } ` const Message = styled.div` padding: 12px 16px; margin-top: 10px; font-size: 14px; letter-spacing: normal; cursor: default; border-radius: 2px; background: #f3f4f4; box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.15); animation: ${pulse} 0.7s ease-in-out infinite alternate; ` const MessageTitle = styled.div` margin-right: 3px; margin-bottom: 2px; font-weight: bold; color: #2a7ed2; ` // Main styled components const Wrapper = styled.div` z-index: 1005; height: 100%; margin-left: 6px; ` const TooltipText = styled.div` margin-right: 10px; font-size: ${p => p.theme.sizes.fontSmall}; font-weight: ${p => p.theme.sizes.fontSemiBold}; text-transform: uppercase; letter-spacing: 0.53px; color: ${p => p.theme.colours.darkBlue50}; ` const IconWrapper = styled.div` position: relative; cursor: pointer; ` const TooltipWrapper = styled.div` position: absolute; right: 0px; ` const Row = styled.div` position: relative; min-width: 245px; margin-top: ${p => p.theme.sizes.small16}; display: flex; align-items: center; justify-content: space-between; &:first-child { margin-top: 0; } ` const CopyWrapper = styled.div` position: absolute; right: 0; &:hover { svg { fill: ${p => p.theme.colours.darkBlue60}; } } ` const Input = styled.input` display: block; width: 100%; padding: ${p => p.theme.sizes.small6} ${p => p.theme.sizes.small10}; padding-right: 25px; font-weight: ${p => p.theme.sizes.fontSemiBold}; font-size: ${p => p.theme.sizes.fontTiny}; border-radius: ${p => p.theme.sizes.smallRadius}; background: ${p => p.theme.colours.darkBlue10}; color: ${p => p.theme.colours.darkBlue}; ` ================================================ FILE: packages/graphql-playground-react/src/components/Spinner.tsx ================================================ import * as React from 'react' import { keyframes, styled } from '../styled' const Spinner = () => ( ) const rotation = keyframes` from { transform: rotate(0deg); } to { transform: rotate(359deg); } ` const Wrapper = styled.div` height: 36px; left: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); width: 36px; z-index: 10; ` const SpinnerNode = styled.div` position: absolute; display: inline-block; height: 24px; width: 24px; vertical-align: middle; animation: ${rotation} 0.6s infinite linear; border-radius: 100%; border-bottom: 6px solid rgba(150, 150, 150, 0.15); border-left: 6px solid rgba(150, 150, 150, 0.15); border-right: 6px solid rgba(150, 150, 150, 0.15); border-top: 6px solid rgba(150, 150, 150, 0.8); ` export default Spinner ================================================ FILE: packages/graphql-playground-react/src/components/Toggle.tsx ================================================ import * as React from 'react' import { css, styled } from '../styled' export interface ToggleProps { choices: string[] onChange: (choice: string, i: number) => void activeChoice: string } const Toggle: React.SFC = ({ choices, onChange, activeChoice, }) => ( {choices.map((choice, i) => ( onChange(choice, i)} > {choice} ))} ) const Wrapper = styled.div` display: flex; ` interface ChoiceProps { active: boolean } // prettier-ignore const Choice = styled.div` padding: 4px 8px; margin-right: ${p => p.theme.sizes.small6}; font-weight: 600; text-transform: uppercase; letter-spacing: 0.2px; cursor: pointer; border-radius: ${p => p.theme.sizes.smallRadius}; color: ${p => p.theme.colours.black40}; ${(p: ChoiceProps) => p.active ? css` color: rgba(0, 0, 0, 0.7); background: #b8bfc4; ` : css` /* hover state when it's not active */ &:hover { color: rgba(0, 0, 0, 0.7); } `} ` export default Toggle ================================================ FILE: packages/graphql-playground-react/src/components/ToggleButton.tsx ================================================ import * as React from 'react' import { styled } from '../styled' export interface Props { checked: boolean onChange: (e: any) => void className?: string } const ToggleButton = ({ checked, onChange, className }: Props) => { return ( ) } export default ToggleButton const Wrapper = styled.div` position: relative; display: inline-block; width: 39px; height: 21px; ` const Input = styled.input` display: none; ` interface SliderProps { checked: boolean } const Slider = styled('div')` position: absolute; top: 0; left: 0; right: 0; bottom: 0; transition: transform 70ms linear; border-radius: 23px; cursor: pointer; background: ${p => p.checked ? p.theme.colours.green : p.theme.colours.black40}; &:before { position: absolute; content: ''; height: 23px; width: 23px; left: -1px; bottom: -1px; background-color: white; border-radius: 50%; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); transition: transform 70ms linear; transform: ${p => (p.checked ? 'translateX(19px)' : '')}; } ` ================================================ FILE: packages/graphql-playground-react/src/components/Tooltip.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom' import { css, styled } from '../styled' export interface Props { open: boolean children: any anchorOrigin?: { vertical?: 'bottom' | 'top' horizontal?: 'left' | 'right' | 'center' } renderAfterContent?: () => any onClick?: () => void onClose?: (e?: any) => void } class Tooltip extends React.PureComponent { static defaultProps: Partial = { anchorOrigin: { vertical: 'top', horizontal: 'center', }, } componentDidMount() { document.addEventListener('click', this.handleClickOutside, true) } componentWillUnmount() { document.removeEventListener( 'click', this.handleClickOutside.bind(this), true, ) } handleClickOutside = event => { if (!this.props.open) { return } try { const domNode = ReactDOM.findDOMNode(this as any) if ( (!domNode || !domNode.contains(event.target)) && typeof this.props.onClose !== 'undefined' ) { this.props.onClose(event) } } catch (e) { // } } render() { const { open, children, renderAfterContent, onClick } = this.props const anchorOrigin = this.props.anchorOrigin! return ( {children} {renderAfterContent && renderAfterContent()} ) } } export default Tooltip interface WrapperProps { visible?: boolean anchorTop?: boolean anchorBottom?: boolean anchorLeft?: boolean anchorRight?: boolean anchorCenter?: boolean } const Wrapper = styled.div` position: absolute; z-index: 9999; text-align: left; transform: translateX(-50%); transition: opacity ease-out 0.2s; ${(p: WrapperProps) => p.visible ? css` visibility: visible; opacity: 1; ` : css` visibility: hidden; opacity: 0; `} ${(p: WrapperProps) => p.anchorTop ? css` bottom: 100%; margin-bottom: 16px; ${BigTriangle} { bottom: -10px; } ` : ''} ${(p: WrapperProps) => p.anchorBottom ? css` top: 100%; margin-top: 16px; ${BigTriangle} { top: -10px; border-width: 0 10px 10px 10px; border-color: ${k => k.theme.colours.paleGrey} transparent ${k => k.theme.colours.paleGrey} transparent; } ` : ''} ${(p: WrapperProps) => p.anchorLeft ? css` left: 0; transform: none; ${BigTriangle} { left: 25px; } ` : ''} ${(p: WrapperProps) => p.anchorRight ? css` right: 0; transform: none; ${BigTriangle} { right: 25px; } ` : ''} ${(p: WrapperProps) => p.anchorCenter ? css` left: 50%; ${BigTriangle} { left: calc(50% - 10px); } ` : ''}; ` const Content = styled.div` display: flex; align-items: center; padding: ${p => p.theme.sizes.small12} ${p => p.theme.sizes.small16}; white-space: nowrap; box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.15); background-color: ${p => p.theme.colours.paleGrey}; border-radius: ${p => p.theme.sizes.smallRadius}; color: ${p => p.theme.colours.paleText}; ` const BigTriangle = styled.div` position: absolute; width: 0; height: 0; border-style: solid; border-width: 10px 10px 0 10px; border-color: ${p => p.theme.colours.paleGrey} transparent transparent transparent; ` ================================================ FILE: packages/graphql-playground-react/src/components/asyncComponent.tsx ================================================ 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 ================================================ FILE: packages/graphql-playground-react/src/components/resolveRefStrings.ts ================================================ export function resolveRefString(str: string, values?: object): string { const { strings, rawRefs } = parse(str) const refValues = rawRefs.map(ref => resolveRef(ref, values)) let res = '' for (let i = 0; i < refValues.length; i++) { res += strings[i] res += refValues[i] } res += strings.pop() return res } export function resolveEnvsInValues( inputConfig: T, env: { [name: string]: string | undefined }, ): T { const config = { ...(inputConfig as any) } Object.keys(config).forEach(key => { const value = config[key] if (typeof value === 'string') { config[key] = resolveRefString(value, { env }) } else if (typeof value === 'object') { config[key] = resolveEnvsInValues(value, env) } }) return config } export function getUsedEnvs(config: any): { [name: string]: string } { const result = {} const traverse = val => { if (typeof val === 'string') { const rawRefs = parse(val).rawRefs for (const ref of rawRefs) { result[parseRef(ref).ref] = resolveRef(ref, {}, false) } } else if (typeof val === 'object') { Object.keys(config).forEach(key => { traverse(config[key]) }) } } traverse(config) return result } function parseRef(rawRef: string): { type: string; ref: string } { const [type, ref] = rawRef.split(/\s*:\s*/) return { type, ref } } function resolveRef( rawRef: string, values: any = {}, throwIfUndef: boolean = true, ): string | null { const { type, ref } = parseRef(rawRef) if (type === 'env') { if (!ref) { throw new Error(`Reference value is not present for ${type}: ${rawRef}`) } const refValue = (values.env && values.env[ref]) || process.env[ref] if (!refValue) { if (throwIfUndef) { throw new Error(`Environment variable ${ref} is not set`) } else { return null } } return refValue } else { // support only 'env' for now throw new Error( 'Undefined reference type \\$\\{refType}. Only "env" is supported', ) } } function parse(str: string): { strings: string[]; rawRefs: string[] } { const regex = /\${([^}]*)}/g const strings: string[] = [] const rawRefs: string[] = [] let prevIdx = 0 let match // tslint:disable-next-line:no-conditional-assignment while ((match = regex.exec(str)) !== null) { if (match.index > 0 && str[match.index - 1] === '\\') { continue } strings.push(str.substring(prevIdx, match.index)) rawRefs.push(match[1]) prevIdx = match.index + match[0].length } strings.push(str.substring(prevIdx)) return { strings, rawRefs } } ================================================ FILE: packages/graphql-playground-react/src/components/util.ts ================================================ import { GraphQLConfig, GraphQLConfigEnpointConfig } from '../graphqlConfig' import { GraphQLSchema, printSchema } from 'graphql' import * as LRU from 'lru-cache' export function getActiveEndpoints( config: GraphQLConfig, envName: string, projectName?: string, ): { endpoint: string; subscriptionEndpoint?: string; headers?: any } { if (projectName) { const env = config.projects![projectName].extensions!.endpoints![envName]! return getEndpointFromEndpointConfig(env) } else { const env = config.extensions!.endpoints![envName]! return getEndpointFromEndpointConfig(env) } } export function getEndpointFromEndpointConfig( env: GraphQLConfigEnpointConfig | string, ) { if (typeof env === 'string') { return { endpoint: env, subscriptionEndpoint: undefined, } } else { return { endpoint: env.url, subscriptionEndpoint: env.subscription ? env.subscription!.url : undefined, headers: env.headers, } } } const printSchemaCache: LRU.Cache = new LRU({ max: 10 }) /** * A cached version of `printSchema` * @param schema GraphQLSchema instance */ export function cachedPrintSchema(schema: GraphQLSchema) { const cachedString = printSchemaCache.get(schema) if (cachedString) { return cachedString } const schemaString = printSchema(schema) printSchemaCache.set(schema, schemaString) return schemaString } ================================================ FILE: packages/graphql-playground-react/src/constants.ts ================================================ import * as cuid from 'cuid' import { getIntrospectionQuery } from 'graphql' import { getQueryTypes } from './components/Playground/util/getQueryTypes' import { List, Map } from 'immutable' export const columnWidth = 300 export const introspectionQuery = getIntrospectionQuery() export const defaultQuery = '# Write your query or mutation here\n' export const modalStyle = { overlay: { zIndex: 99999, backgroundColor: 'rgba(15,32,46,.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', }, content: { position: 'relative', width: 976, height: 'auto', top: 'initial', left: 'initial', right: 'initial', bottom: 'initial', borderRadius: 2, padding: 0, border: 'none', background: 'none', boxShadow: '0 1px 7px rgba(0,0,0,.2)', }, } export function getDefaultSession(endpoint: string) { return { id: cuid(), query: defaultQuery, variables: '', responses: List([]), endpoint, operationName: undefined, hasMutation: false, hasSubscription: false, hasQuery: false, queryTypes: getQueryTypes(defaultQuery), subscriptionActive: false, date: new Date(), starred: false, queryRunning: false, operations: List([]), isReloadingSchema: false, isSchemaPendingUpdate: false, responseExtensions: {}, queryVariablesActive: false, endpointUnreachable: false, editorFlex: 1, variableEditorOpen: false, variableEditorHeight: 200, responseTracingOpen: false, responseTracingHeight: 300, docExplorerWidth: 350, variableToType: Map({}), headers: '', file: undefined, isFile: false, name: undefined, filePath: undefined, selectedUserToken: undefined, hasChanged: undefined, absolutePath: undefined, isSettingsTab: undefined, isConfigTab: undefined, currentQueryStartTime: undefined, currentQueryEndTime: undefined, nextQueryStartTime: undefined, tracingSupported: undefined, changed: undefined, scrollTop: undefined, } as any } ================================================ FILE: packages/graphql-playground-react/src/fixtures/exampleSchema.ts ================================================ export const exampleSchema = { __schema: { directives: [ { name: 'include', description: 'Directs the executor to include this field or fragment only when the `if` argument is true.', locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], args: [ { name: 'if', description: 'Included when true.', type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, defaultValue: null, }, ], }, { name: 'skip', description: 'Directs the executor to skip this field or fragment when the `if` argument is true.', locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], args: [ { name: 'if', description: 'Included when true.', type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, defaultValue: null, }, ], }, { name: 'deprecated', description: 'Marks an element of a GraphQL schema as no longer supported.', locations: ['ENUM_VALUE', 'FIELD_DEFINITION'], args: [ { name: 'reason', description: 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: '"No longer supported"', }, ], }, ], mutationType: { name: 'Mutation' }, subscriptionType: { name: 'Subscription' }, queryType: { name: 'Query' }, types: [ { inputFields: null, name: 'BatchPayload', description: '', interfaces: [], enumValues: null, fields: [ { name: 'count', description: 'The number of nodes that have been affected by the Batch operation.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Long', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'Mutation', description: null, interfaces: [], enumValues: null, fields: [ { name: 'createUser', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'data', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserCreateInput', ofType: null, }, }, defaultValue: null, }, ], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'User', ofType: null }, }, }, { name: 'updateUser', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'data', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserUpdateInput', ofType: null, }, }, defaultValue: null, }, { name: 'where', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereUniqueInput', ofType: null, }, }, defaultValue: null, }, ], type: { kind: 'OBJECT', name: 'User', ofType: null }, }, { name: 'deleteUser', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereUniqueInput', ofType: null, }, }, defaultValue: null, }, ], type: { kind: 'OBJECT', name: 'User', ofType: null }, }, { name: 'upsertUser', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereUniqueInput', ofType: null, }, }, defaultValue: null, }, { name: 'create', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserCreateInput', ofType: null, }, }, defaultValue: null, }, { name: 'update', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserUpdateInput', ofType: null, }, }, defaultValue: null, }, ], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'User', ofType: null }, }, }, { name: 'updateManyUsers', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'data', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserUpdateInput', ofType: null, }, }, defaultValue: null, }, { name: 'where', description: '', type: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, defaultValue: null, }, ], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'BatchPayload', ofType: null }, }, }, { name: 'deleteManyUsers', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: '', type: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, defaultValue: null, }, ], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'BatchPayload', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'MutationType', description: null, interfaces: null, enumValues: [ { name: 'CREATED', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'UPDATED', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'DELETED', description: null, isDeprecated: false, deprecationReason: null, }, ], fields: null, kind: 'ENUM', possibleTypes: null, }, { inputFields: null, name: 'Subscription', description: null, interfaces: [], enumValues: null, fields: [ { name: 'user', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: '', type: { kind: 'INPUT_OBJECT', name: 'UserSubscriptionWhereInput', ofType: null, }, defaultValue: null, }, ], type: { kind: 'OBJECT', name: 'UserSubscriptionPayload', ofType: null, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: [ { name: 'name', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, defaultValue: null, }, { name: 'email', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, defaultValue: null, }, ], name: 'UserCreateInput', description: null, interfaces: null, enumValues: null, fields: null, kind: 'INPUT_OBJECT', possibleTypes: null, }, { inputFields: null, name: 'UserPreviousValues', description: null, interfaces: [], enumValues: null, fields: [ { name: 'id', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, }, }, { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, { name: 'email', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'UserSubscriptionPayload', description: null, interfaces: [], enumValues: null, fields: [ { name: 'mutation', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'ENUM', name: 'MutationType', ofType: null }, }, }, { name: 'node', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'OBJECT', name: 'User', ofType: null }, }, { name: 'updatedFields', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, }, { name: 'previousValues', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'OBJECT', name: 'UserPreviousValues', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: [ { name: 'AND', description: 'Logical AND on all given filters.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserSubscriptionWhereInput', ofType: null, }, }, }, defaultValue: null, }, { name: 'OR', description: 'Logical OR on all given filters.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserSubscriptionWhereInput', ofType: null, }, }, }, defaultValue: null, }, { name: 'NOT', description: 'Logical NOT on all given filters combined by AND.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserSubscriptionWhereInput', ofType: null, }, }, }, defaultValue: null, }, { name: 'mutation_in', description: "The subscription event gets dispatched when it's listed in mutation_in", type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'ENUM', name: 'MutationType', ofType: null }, }, }, defaultValue: null, }, { name: 'updatedFields_contains', description: 'The subscription event gets only dispatched when one of the updated fields names is included in this list', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'updatedFields_contains_every', description: 'The subscription event gets only dispatched when all of the field names included in this list have been updated', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, defaultValue: null, }, { name: 'updatedFields_contains_some', description: 'The subscription event gets only dispatched when some of the field names included in this list have been updated', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, defaultValue: null, }, { name: 'node', description: null, type: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, defaultValue: null, }, ], name: 'UserSubscriptionWhereInput', description: null, interfaces: null, enumValues: null, fields: null, kind: 'INPUT_OBJECT', possibleTypes: null, }, { inputFields: [ { name: 'name', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, ], name: 'UserUpdateInput', description: null, interfaces: null, enumValues: null, fields: null, kind: 'INPUT_OBJECT', possibleTypes: null, }, { inputFields: null, name: 'AggregateUser', description: null, interfaces: [], enumValues: null, fields: [ { name: 'count', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Int', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'Node', description: 'An object with an ID', interfaces: null, enumValues: null, fields: [ { name: 'id', description: 'The id of the object.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, }, }, ], kind: 'INTERFACE', possibleTypes: [{ kind: 'OBJECT', name: 'User', ofType: null }], }, { inputFields: null, name: 'PageInfo', description: 'Information about pagination in a connection.', interfaces: [], enumValues: null, fields: [ { name: 'hasNextPage', description: 'When paginating forwards, are there more items?', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, { name: 'hasPreviousPage', description: 'When paginating backwards, are there more items?', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, { name: 'startCursor', description: 'When paginating backwards, the cursor to continue.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'endCursor', description: 'When paginating forwards, the cursor to continue.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'Query', description: null, interfaces: [], enumValues: null, fields: [ { name: 'users', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: '', type: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, defaultValue: null, }, { name: 'orderBy', description: null, type: { kind: 'ENUM', name: 'UserOrderByInput', ofType: null }, defaultValue: null, }, { name: 'skip', description: null, type: { kind: 'SCALAR', name: 'Int', ofType: null }, defaultValue: null, }, { name: 'after', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'before', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'first', description: null, type: { kind: 'SCALAR', name: 'Int', ofType: null }, defaultValue: null, }, { name: 'last', description: null, type: { kind: 'SCALAR', name: 'Int', ofType: null }, defaultValue: null, }, ], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'OBJECT', name: 'User', ofType: null }, }, }, }, { name: 'user', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: null, type: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereUniqueInput', ofType: null, }, }, defaultValue: null, }, ], type: { kind: 'OBJECT', name: 'User', ofType: null }, }, { name: 'usersConnection', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'where', description: '', type: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, defaultValue: null, }, { name: 'orderBy', description: null, type: { kind: 'ENUM', name: 'UserOrderByInput', ofType: null }, defaultValue: null, }, { name: 'skip', description: null, type: { kind: 'SCALAR', name: 'Int', ofType: null }, defaultValue: null, }, { name: 'after', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'before', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'first', description: null, type: { kind: 'SCALAR', name: 'Int', ofType: null }, defaultValue: null, }, { name: 'last', description: null, type: { kind: 'SCALAR', name: 'Int', ofType: null }, defaultValue: null, }, ], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'UserConnection', ofType: null }, }, }, { name: 'node', description: 'Fetches an object given its ID', isDeprecated: false, deprecationReason: null, args: [ { name: 'id', description: 'The ID of an object', type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, }, defaultValue: null, }, ], type: { kind: 'INTERFACE', name: 'Node', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'User', description: null, interfaces: [{ kind: 'INTERFACE', name: 'Node', ofType: null }], enumValues: null, fields: [ { name: 'id', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, }, }, { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, { name: 'email', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'UserConnection', description: 'A connection to a list of items.', interfaces: [], enumValues: null, fields: [ { name: 'pageInfo', description: 'Information to aid in pagination.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'PageInfo', ofType: null }, }, }, { name: 'edges', description: 'A list of edges.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'OBJECT', name: 'UserEdge', ofType: null }, }, }, }, { name: 'aggregate', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'AggregateUser', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'UserEdge', description: 'An edge in a connection.', interfaces: [], enumValues: null, fields: [ { name: 'node', description: 'The item at the end of the edge.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: 'User', ofType: null }, }, }, { name: 'cursor', description: 'A cursor for use in pagination.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: 'UserOrderByInput', description: null, interfaces: null, enumValues: [ { name: 'id_ASC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'id_DESC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'name_ASC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'name_DESC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'email_ASC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'email_DESC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'updatedAt_ASC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'updatedAt_DESC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'createdAt_ASC', description: null, isDeprecated: false, deprecationReason: null, }, { name: 'createdAt_DESC', description: null, isDeprecated: false, deprecationReason: null, }, ], fields: null, kind: 'ENUM', possibleTypes: null, }, { inputFields: [ { name: 'AND', description: 'Logical AND on all given filters.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, }, }, defaultValue: null, }, { name: 'OR', description: 'Logical OR on all given filters.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, }, }, defaultValue: null, }, { name: 'NOT', description: 'Logical NOT on all given filters combined by AND.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'INPUT_OBJECT', name: 'UserWhereInput', ofType: null, }, }, }, defaultValue: null, }, { name: 'id', description: '', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_not', description: 'All values that are not equal to given value.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_in', description: 'All values that are contained in given list.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, }, }, defaultValue: null, }, { name: 'id_not_in', description: 'All values that are not contained in given list.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'ID', ofType: null }, }, }, defaultValue: null, }, { name: 'id_lt', description: 'All values less than the given value.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_lte', description: 'All values less than or equal the given value.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_gt', description: 'All values greater than the given value.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_gte', description: 'All values greater than or equal the given value.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_contains', description: 'All values containing the given string.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_not_contains', description: 'All values not containing the given string.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_starts_with', description: 'All values starting with the given string.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_not_starts_with', description: 'All values not starting with the given string.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_ends_with', description: 'All values ending with the given string.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'id_not_ends_with', description: 'All values not ending with the given string.', type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'name', description: '', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_not', description: 'All values that are not equal to given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_in', description: 'All values that are contained in given list.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, defaultValue: null, }, { name: 'name_not_in', description: 'All values that are not contained in given list.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, defaultValue: null, }, { name: 'name_lt', description: 'All values less than the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_lte', description: 'All values less than or equal the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_gt', description: 'All values greater than the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_gte', description: 'All values greater than or equal the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_contains', description: 'All values containing the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_not_contains', description: 'All values not containing the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_starts_with', description: 'All values starting with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_not_starts_with', description: 'All values not starting with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_ends_with', description: 'All values ending with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'name_not_ends_with', description: 'All values not ending with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email', description: '', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_not', description: 'All values that are not equal to given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_in', description: 'All values that are contained in given list.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, defaultValue: null, }, { name: 'email_not_in', description: 'All values that are not contained in given list.', type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, defaultValue: null, }, { name: 'email_lt', description: 'All values less than the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_lte', description: 'All values less than or equal the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_gt', description: 'All values greater than the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_gte', description: 'All values greater than or equal the given value.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_contains', description: 'All values containing the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_not_contains', description: 'All values not containing the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_starts_with', description: 'All values starting with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_not_starts_with', description: 'All values not starting with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_ends_with', description: 'All values ending with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, { name: 'email_not_ends_with', description: 'All values not ending with the given string.', type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, ], name: 'UserWhereInput', description: null, interfaces: null, enumValues: null, fields: null, kind: 'INPUT_OBJECT', possibleTypes: null, }, { inputFields: [ { name: 'id', description: null, type: { kind: 'SCALAR', name: 'ID', ofType: null }, defaultValue: null, }, { name: 'email', description: null, type: { kind: 'SCALAR', name: 'String', ofType: null }, defaultValue: null, }, ], name: 'UserWhereUniqueInput', description: null, interfaces: null, enumValues: null, fields: null, kind: 'INPUT_OBJECT', possibleTypes: null, }, { inputFields: null, name: '__Directive', description: 'A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL’s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.', interfaces: [], enumValues: null, fields: [ { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, { name: 'description', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'locations', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'ENUM', name: '__DirectiveLocation', ofType: null, }, }, }, }, }, { name: 'args', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__InputValue', ofType: null, }, }, }, }, }, { name: 'onOperation', description: null, isDeprecated: true, deprecationReason: 'Use `locations`.', args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, { name: 'onFragment', description: null, isDeprecated: true, deprecationReason: 'Use `locations`.', args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, { name: 'onField', description: null, isDeprecated: true, deprecationReason: 'Use `locations`.', args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: '__DirectiveLocation', description: 'A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.', interfaces: null, enumValues: [ { name: 'QUERY', description: 'Location adjacent to a query operation.', isDeprecated: false, deprecationReason: null, }, { name: 'MUTATION', description: 'Location adjacent to a mutation operation.', isDeprecated: false, deprecationReason: null, }, { name: 'SUBSCRIPTION', description: 'Location adjacent to a subscription operation.', isDeprecated: false, deprecationReason: null, }, { name: 'FIELD', description: 'Location adjacent to a field.', isDeprecated: false, deprecationReason: null, }, { name: 'FRAGMENT_DEFINITION', description: 'Location adjacent to a fragment definition.', isDeprecated: false, deprecationReason: null, }, { name: 'FRAGMENT_SPREAD', description: 'Location adjacent to a fragment spread.', isDeprecated: false, deprecationReason: null, }, { name: 'INLINE_FRAGMENT', description: 'Location adjacent to an inline fragment.', isDeprecated: false, deprecationReason: null, }, { name: 'SCHEMA', description: 'Location adjacent to a schema definition.', isDeprecated: false, deprecationReason: null, }, { name: 'SCALAR', description: 'Location adjacent to a scalar definition.', isDeprecated: false, deprecationReason: null, }, { name: 'OBJECT', description: 'Location adjacent to an object type definition.', isDeprecated: false, deprecationReason: null, }, { name: 'FIELD_DEFINITION', description: 'Location adjacent to a field definition.', isDeprecated: false, deprecationReason: null, }, { name: 'ARGUMENT_DEFINITION', description: 'Location adjacent to an argument definition.', isDeprecated: false, deprecationReason: null, }, { name: 'INTERFACE', description: 'Location adjacent to an interface definition.', isDeprecated: false, deprecationReason: null, }, { name: 'UNION', description: 'Location adjacent to a union definition.', isDeprecated: false, deprecationReason: null, }, { name: 'ENUM', description: 'Location adjacent to an enum definition.', isDeprecated: false, deprecationReason: null, }, { name: 'ENUM_VALUE', description: 'Location adjacent to an enum value definition.', isDeprecated: false, deprecationReason: null, }, { name: 'INPUT_OBJECT', description: 'INPUT_OBJECT', isDeprecated: false, deprecationReason: null, }, { name: 'INPUT_FIELD_DEFINITION', description: 'Location adjacent to an input object field definition.', isDeprecated: false, deprecationReason: null, }, ], fields: null, kind: 'ENUM', possibleTypes: null, }, { inputFields: null, name: '__EnumValue', description: 'One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.', interfaces: [], enumValues: null, fields: [ { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, { name: 'description', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'isDeprecated', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, { name: 'deprecationReason', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: '__Field', description: 'Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.', interfaces: [], enumValues: null, fields: [ { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, { name: 'description', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'args', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__InputValue', ofType: null, }, }, }, }, }, { name: 'type', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Type', ofType: null }, }, }, { name: 'isDeprecated', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'Boolean', ofType: null }, }, }, { name: 'deprecationReason', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: '__InputValue', description: 'Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.', interfaces: [], enumValues: null, fields: [ { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'SCALAR', name: 'String', ofType: null }, }, }, { name: 'description', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'type', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Type', ofType: null }, }, }, { name: 'defaultValue', description: 'A GraphQL-formatted string representing the default value for this input value.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: '__Schema', description: 'A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.', interfaces: [], enumValues: null, fields: [ { name: 'types', description: 'A list of all types supported by this server.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Type', ofType: null }, }, }, }, }, { name: 'queryType', description: 'The type that query operations will be rooted at.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Type', ofType: null }, }, }, { name: 'mutationType', description: 'If this server supports mutation, the type that mutation operations will be rooted at.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'OBJECT', name: '__Type', ofType: null }, }, { name: 'subscriptionType', description: 'If this server support subscription, the type that subscription operations will be rooted at.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'OBJECT', name: '__Type', ofType: null }, }, { name: 'directives', description: 'A list of all directives supported by this server.', isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Directive', ofType: null }, }, }, }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: '__Type', description: 'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.', interfaces: [], enumValues: null, fields: [ { name: 'kind', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'NON_NULL', name: null, ofType: { kind: 'ENUM', name: '__TypeKind', ofType: null }, }, }, { name: 'name', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'description', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'SCALAR', name: 'String', ofType: null }, }, { name: 'fields', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'includeDeprecated', description: null, type: { kind: 'SCALAR', name: 'Boolean', ofType: null }, defaultValue: 'false', }, ], type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Field', ofType: null }, }, }, }, { name: 'interfaces', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Type', ofType: null }, }, }, }, { name: 'possibleTypes', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__Type', ofType: null }, }, }, }, { name: 'enumValues', description: null, isDeprecated: false, deprecationReason: null, args: [ { name: 'includeDeprecated', description: null, type: { kind: 'SCALAR', name: 'Boolean', ofType: null }, defaultValue: 'false', }, ], type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__EnumValue', ofType: null }, }, }, }, { name: 'inputFields', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'LIST', name: null, ofType: { kind: 'NON_NULL', name: null, ofType: { kind: 'OBJECT', name: '__InputValue', ofType: null }, }, }, }, { name: 'ofType', description: null, isDeprecated: false, deprecationReason: null, args: [], type: { kind: 'OBJECT', name: '__Type', ofType: null }, }, ], kind: 'OBJECT', possibleTypes: null, }, { inputFields: null, name: '__TypeKind', description: 'An enum describing what kind of type a given `__Type` is.', interfaces: null, enumValues: [ { name: 'SCALAR', description: 'Indicates this type is a scalar.', isDeprecated: false, deprecationReason: null, }, { name: 'OBJECT', description: 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', isDeprecated: false, deprecationReason: null, }, { name: 'INTERFACE', description: 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', isDeprecated: false, deprecationReason: null, }, { name: 'UNION', description: 'Indicates this type is a union. `possibleTypes` is a valid field.', isDeprecated: false, deprecationReason: null, }, { name: 'ENUM', description: 'Indicates this type is an enum. `enumValues` is a valid field.', isDeprecated: false, deprecationReason: null, }, { name: 'INPUT_OBJECT', description: 'Indicates this type is an input object. `inputFields` is a valid field.', isDeprecated: false, deprecationReason: null, }, { name: 'LIST', description: 'Indicates this type is a list. `ofType` is a valid field.', isDeprecated: false, deprecationReason: null, }, { name: 'NON_NULL', description: 'Indicates this type is a non-null. `ofType` is a valid field.', isDeprecated: false, deprecationReason: null, }, ], fields: null, kind: 'ENUM', possibleTypes: null, }, { inputFields: null, name: 'Boolean', description: 'The `Boolean` scalar type represents `true` or `false`.', interfaces: null, enumValues: null, fields: null, kind: 'SCALAR', possibleTypes: null, }, { inputFields: null, name: 'ID', description: 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.', interfaces: null, enumValues: null, fields: null, kind: 'SCALAR', possibleTypes: null, }, { inputFields: null, name: 'Int', description: 'The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.', interfaces: null, enumValues: null, fields: null, kind: 'SCALAR', possibleTypes: null, }, { inputFields: null, name: 'Long', description: 'The `Long` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1.', interfaces: null, enumValues: null, fields: null, kind: 'SCALAR', possibleTypes: null, }, { inputFields: null, name: 'String', description: 'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', interfaces: null, enumValues: null, fields: null, kind: 'SCALAR', possibleTypes: null, }, ], }, } ================================================ FILE: packages/graphql-playground-react/src/fixtures/sdl.ts ================================================ export const exampleSdl = `type A implements Node { id: ID! name: String! } """A connection to a list of items.""" type AConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [AEdge]! aggregate: AggregateA! } input ACreateInput { name: String! } """An edge in a connection.""" type AEdge { """The item at the end of the edge.""" node: A! """A cursor for use in pagination.""" cursor: String! } type AggregateA { count: Int! } type AggregateB { count: Int! } type AggregateUser { count: Int! } enum AOrderByInput { id_ASC id_DESC name_ASC name_DESC updatedAt_ASC updatedAt_DESC createdAt_ASC createdAt_DESC } type APreviousValues { id: ID! name: String! } type ASubscriptionPayload { mutation: MutationType! node: A updatedFields: [String!] previousValues: APreviousValues } input ASubscriptionWhereInput { """Logical AND on all given filters.""" AND: [ASubscriptionWhereInput!] """Logical OR on all given filters.""" OR: [ASubscriptionWhereInput!] """Logical NOT on all given filters combined by AND.""" NOT: [ASubscriptionWhereInput!] """ The subscription event gets dispatched when it's listed in mutation_in """ mutation_in: [MutationType!] """ The subscription event gets only dispatched when one of the updated fields names is included in this list """ updatedFields_contains: String """ The subscription event gets only dispatched when all of the field names included in this list have been updated """ updatedFields_contains_every: [String!] """ The subscription event gets only dispatched when some of the field names included in this list have been updated """ updatedFields_contains_some: [String!] node: AWhereInput } input AUpdateInput { name: String } input AWhereInput { """Logical AND on all given filters.""" AND: [AWhereInput!] """Logical OR on all given filters.""" OR: [AWhereInput!] """Logical NOT on all given filters combined by AND.""" NOT: [AWhereInput!] id: ID """All values that are not equal to given value.""" id_not: ID """All values that are contained in given list.""" id_in: [ID!] """All values that are not contained in given list.""" id_not_in: [ID!] """All values less than the given value.""" id_lt: ID """All values less than or equal the given value.""" id_lte: ID """All values greater than the given value.""" id_gt: ID """All values greater than or equal the given value.""" id_gte: ID """All values containing the given string.""" id_contains: ID """All values not containing the given string.""" id_not_contains: ID """All values starting with the given string.""" id_starts_with: ID """All values not starting with the given string.""" id_not_starts_with: ID """All values ending with the given string.""" id_ends_with: ID """All values not ending with the given string.""" id_not_ends_with: ID name: String """All values that are not equal to given value.""" name_not: String """All values that are contained in given list.""" name_in: [String!] """All values that are not contained in given list.""" name_not_in: [String!] """All values less than the given value.""" name_lt: String """All values less than or equal the given value.""" name_lte: String """All values greater than the given value.""" name_gt: String """All values greater than or equal the given value.""" name_gte: String """All values containing the given string.""" name_contains: String """All values not containing the given string.""" name_not_contains: String """All values starting with the given string.""" name_starts_with: String """All values not starting with the given string.""" name_not_starts_with: String """All values ending with the given string.""" name_ends_with: String """All values not ending with the given string.""" name_not_ends_with: String } input AWhereUniqueInput { id: ID } type B implements Node { id: ID! name: String! } type BatchPayload { """The number of nodes that have been affected by the Batch operation.""" count: Long! } """A connection to a list of items.""" type BConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [BEdge]! aggregate: AggregateB! } input BCreateInput { name: String! } """An edge in a connection.""" type BEdge { """The item at the end of the edge.""" node: B! """A cursor for use in pagination.""" cursor: String! } enum BOrderByInput { id_ASC id_DESC name_ASC name_DESC updatedAt_ASC updatedAt_DESC createdAt_ASC createdAt_DESC } type BPreviousValues { id: ID! name: String! } type BSubscriptionPayload { mutation: MutationType! node: B updatedFields: [String!] previousValues: BPreviousValues } input BSubscriptionWhereInput { """Logical AND on all given filters.""" AND: [BSubscriptionWhereInput!] """Logical OR on all given filters.""" OR: [BSubscriptionWhereInput!] """Logical NOT on all given filters combined by AND.""" NOT: [BSubscriptionWhereInput!] """ The subscription event gets dispatched when it's listed in mutation_in """ mutation_in: [MutationType!] """ The subscription event gets only dispatched when one of the updated fields names is included in this list """ updatedFields_contains: String """ The subscription event gets only dispatched when all of the field names included in this list have been updated """ updatedFields_contains_every: [String!] """ The subscription event gets only dispatched when some of the field names included in this list have been updated """ updatedFields_contains_some: [String!] node: BWhereInput } input BUpdateInput { name: String } input BWhereInput { """Logical AND on all given filters.""" AND: [BWhereInput!] """Logical OR on all given filters.""" OR: [BWhereInput!] """Logical NOT on all given filters combined by AND.""" NOT: [BWhereInput!] id: ID """All values that are not equal to given value.""" id_not: ID """All values that are contained in given list.""" id_in: [ID!] """All values that are not contained in given list.""" id_not_in: [ID!] """All values less than the given value.""" id_lt: ID """All values less than or equal the given value.""" id_lte: ID """All values greater than the given value.""" id_gt: ID """All values greater than or equal the given value.""" id_gte: ID """All values containing the given string.""" id_contains: ID """All values not containing the given string.""" id_not_contains: ID """All values starting with the given string.""" id_starts_with: ID """All values not starting with the given string.""" id_not_starts_with: ID """All values ending with the given string.""" id_ends_with: ID """All values not ending with the given string.""" id_not_ends_with: ID name: String """All values that are not equal to given value.""" name_not: String """All values that are contained in given list.""" name_in: [String!] """All values that are not contained in given list.""" name_not_in: [String!] """All values less than the given value.""" name_lt: String """All values less than or equal the given value.""" name_lte: String """All values greater than the given value.""" name_gt: String """All values greater than or equal the given value.""" name_gte: String """All values containing the given string.""" name_contains: String """All values not containing the given string.""" name_not_contains: String """All values starting with the given string.""" name_starts_with: String """All values not starting with the given string.""" name_not_starts_with: String """All values ending with the given string.""" name_ends_with: String """All values not ending with the given string.""" name_not_ends_with: String } input BWhereUniqueInput { id: ID } """ The \`Long\` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1. """ scalar Long type Mutation { createUser(data: UserCreateInput!): User! createA(data: ACreateInput!): A! createB(data: BCreateInput!): B! updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User updateA(data: AUpdateInput!, where: AWhereUniqueInput!): A updateB(data: BUpdateInput!, where: BWhereUniqueInput!): B deleteUser(where: UserWhereUniqueInput!): User deleteA(where: AWhereUniqueInput!): A deleteB(where: BWhereUniqueInput!): B upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! upsertA(where: AWhereUniqueInput!, create: ACreateInput!, update: AUpdateInput!): A! upsertB(where: BWhereUniqueInput!, create: BCreateInput!, update: BUpdateInput!): B! updateManyUsers(data: UserUpdateInput!, where: UserWhereInput): BatchPayload! updateManyAs(data: AUpdateInput!, where: AWhereInput): BatchPayload! updateManyBs(data: BUpdateInput!, where: BWhereInput): BatchPayload! deleteManyUsers(where: UserWhereInput): BatchPayload! deleteManyAs(where: AWhereInput): BatchPayload! deleteManyBs(where: BWhereInput): BatchPayload! } enum MutationType { CREATED UPDATED DELETED } """An object with an ID""" interface Node { """The id of the object.""" id: ID! } """Information about pagination in a connection.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]! as(where: AWhereInput, orderBy: AOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [A]! bs(where: BWhereInput, orderBy: BOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [B]! user(where: UserWhereUniqueInput!): User a(where: AWhereUniqueInput!): A b(where: BWhereUniqueInput!): B usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection! asConnection(where: AWhereInput, orderBy: AOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): AConnection! bsConnection(where: BWhereInput, orderBy: BOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): BConnection! """Fetches an object given its ID""" node( """The ID of an object""" id: ID! ): Node } type Subscription { user(where: UserSubscriptionWhereInput): UserSubscriptionPayload a(where: ASubscriptionWhereInput): ASubscriptionPayload b(where: BSubscriptionWhereInput): BSubscriptionPayload } type User implements Node { id: ID! name: String! } """A connection to a list of items.""" type UserConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [UserEdge]! aggregate: AggregateUser! } input UserCreateInput { name: String! } """An edge in a connection.""" type UserEdge { """The item at the end of the edge.""" node: User! """A cursor for use in pagination.""" cursor: String! } enum UserOrderByInput { id_ASC id_DESC name_ASC name_DESC updatedAt_ASC updatedAt_DESC createdAt_ASC createdAt_DESC } type UserPreviousValues { id: ID! name: String! } type UserSubscriptionPayload { mutation: MutationType! node: User updatedFields: [String!] previousValues: UserPreviousValues } input UserSubscriptionWhereInput { """Logical AND on all given filters.""" AND: [UserSubscriptionWhereInput!] """Logical OR on all given filters.""" OR: [UserSubscriptionWhereInput!] """Logical NOT on all given filters combined by AND.""" NOT: [UserSubscriptionWhereInput!] """ The subscription event gets dispatched when it's listed in mutation_in """ mutation_in: [MutationType!] """ The subscription event gets only dispatched when one of the updated fields names is included in this list """ updatedFields_contains: String """ The subscription event gets only dispatched when all of the field names included in this list have been updated """ updatedFields_contains_every: [String!] """ The subscription event gets only dispatched when some of the field names included in this list have been updated """ updatedFields_contains_some: [String!] node: UserWhereInput } input UserUpdateInput { name: String } input UserWhereInput { """Logical AND on all given filters.""" AND: [UserWhereInput!] """Logical OR on all given filters.""" OR: [UserWhereInput!] """Logical NOT on all given filters combined by AND.""" NOT: [UserWhereInput!] id: ID """All values that are not equal to given value.""" id_not: ID """All values that are contained in given list.""" id_in: [ID!] """All values that are not contained in given list.""" id_not_in: [ID!] """All values less than the given value.""" id_lt: ID """All values less than or equal the given value.""" id_lte: ID """All values greater than the given value.""" id_gt: ID """All values greater than or equal the given value.""" id_gte: ID """All values containing the given string.""" id_contains: ID """All values not containing the given string.""" id_not_contains: ID """All values starting with the given string.""" id_starts_with: ID """All values not starting with the given string.""" id_not_starts_with: ID """All values ending with the given string.""" id_ends_with: ID """All values not ending with the given string.""" id_not_ends_with: ID name: String """All values that are not equal to given value.""" name_not: String """All values that are contained in given list.""" name_in: [String!] """All values that are not contained in given list.""" name_not_in: [String!] """All values less than the given value.""" name_lt: String """All values less than or equal the given value.""" name_lte: String """All values greater than the given value.""" name_gt: String """All values greater than or equal the given value.""" name_gte: String """All values containing the given string.""" name_contains: String """All values not containing the given string.""" name_not_contains: String """All values starting with the given string.""" name_starts_with: String """All values not starting with the given string.""" name_not_starts_with: String """All values ending with the given string.""" name_ends_with: String """All values not ending with the given string.""" name_not_ends_with: String } input UserWhereUniqueInput { id: ID } ` ================================================ FILE: packages/graphql-playground-react/src/graphqlConfig.ts ================================================ export interface GraphQLConfigExtensions { endpoints?: GraphQLConfigEnpointsData [name: string]: any } export interface GraphQLResolvedConfigData { schemaPath: string includes?: string[] excludes?: string[] extensions?: GraphQLConfigExtensions } export type GraphQLConfig = GraphQLResolvedConfigData & { projects?: { [projectName: string]: GraphQLResolvedConfigData } } export interface GraphQLConfigEnpointsSubscription { url: string connectionParams?: { [name: string]: string | undefined } } export interface GraphQLConfigEnpointConfig { url: string headers?: { [name: string]: string } subscription?: GraphQLConfigEnpointsSubscription } export interface GraphQLConfigEnpointsMapData { [env: string]: GraphQLConfigEnpointConfig | string } export interface GraphQLConfigEnpointsMap { [env: string]: GraphQLConfigEnpointConfig } export type GraphQLConfigEnpointsData = GraphQLConfigEnpointsMapData ================================================ FILE: packages/graphql-playground-react/src/index.css ================================================ body { margin: 0; padding: 0; font-family: sans-serif; overflow: hidden; } #root { height: 100%; } body { font-family: 'Open Sans', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: rgba(0,0,0,.8); line-height: 1.5; height: 100vh; letter-spacing: 0.53px; margin-right: -1px !important; } html, body, p, a, h1, h2, h3, h4, ul, pre, code { margin: 0; padding: 0; color: inherit; } a:active, a:focus, button:focus, input:focus { outline: none; } input, button, submit { border: none; } input, button, pre { font-family: 'Open Sans', sans-serif; } code { font-family: Consolas, monospace; } ================================================ FILE: packages/graphql-playground-react/src/index.test.tsx ================================================ import * as React from 'react' import { render, mount } from 'enzyme' import MiddlewareApp from './components/MiddlewareApp' test('test MiddleWareApp without tabs', () => { const wrapper = render( , ) expect(wrapper).toMatchSnapshot() }) test('test MiddleWareApp with tabs', () => { const wrapper = render( , ) expect(wrapper).toMatchSnapshot() }) test('test MiddleWareApp with one tab and click execute', () => { const wrapper = mount( , ) const executeButtons = wrapper.find('[title="Execute Query (Ctrl-Enter)"]') const executeButtonElement = { ...executeButtons.get(0), props: { onClick: jest.fn(), }, } const executeButton = wrapper.wrap(executeButtonElement) expect(executeButton.length).toBe(1) }) test('test MiddleWareApp passed default headers', () => { const wrapper = render( , ) expect(wrapper).toMatchSnapshot() }) ================================================ FILE: packages/graphql-playground-react/src/index.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom' import Root from './components/Root' import './index.css' if (process.env.NODE_ENV !== 'production') { /* tslint:disable-next-line */ // const { whyDidYouUpdate } = require('why-did-you-update') // whyDidYouUpdate(React) } /* tslint:disable-next-line */ ;(window as any)['GraphQLPlayground'] = { init(element: HTMLElement, options) { ReactDOM.render(, element) }, } ================================================ FILE: packages/graphql-playground-react/src/lib.tsx ================================================ import PlaygroundWrapper from './components/PlaygroundWrapper' import GraphQLEditor from './components/Playground/GraphQLEditor' import { store } from './components/GraphQLBinApp' export { PlaygroundWrapper as Playground, GraphQLEditor } export default PlaygroundWrapper export { store } export * from './state/sessions/actions' export * from './state/sessions/selectors' export * from './state/sharing/actions' export * from './state/sharing/selectors' export * from './state/workspace/actions' export * from './state/workspace/reducers' export * from './state/history/actions' export * from './state/history/selectors' export * from './state/docs/actions' export * from './state/docs/selectors' export * from './state/general/actions' export * from './state/general/selectors' export * from './state/appHistory/actions' export * from './state/appHistory/reducers' ================================================ FILE: packages/graphql-playground-react/src/localDevIndex.tsx ================================================ import * as React from 'react' 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 { exampleSchema } from './fixtures/exampleSchema' if (process.env.NODE_ENV !== 'production') { /* tslint:disable-next-line */ // const { whyDidYouUpdate } = require('why-did-you-update') // whyDidYouUpdate(React) } /* tslint:disable-next-line */ ;(window as any)['GraphQLPlayground'] = { init(element: HTMLElement, options) { ReactDOM.render( , element, ) }, } // 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 tabs: Tab[] = [ // { // query: '{ users { id } }', // endpoint: 'https://eu1.prisma.sh/public-asdf/session65/dev', // responses: ['{}'], // }, // { // query: '{ users { id } }', // endpoint: 'https://eu1.prisma.sh/public-asdf/session65/dev', // }, // ] const customLinkCreator = ( session: LinkCreatorProps, wsEndpoint?: string, ): { link: ApolloLink } => { const { headers, credentials } = session const link = new HttpLink({ uri: session.endpoint, fetch, headers, credentials, }) return { link } } // const lightEditorColours = { // property: '#328c8c', // comment: 'rgba(0, 0, 0, 0.3)', // punctuation: 'rgba(23,42,58,.8)', // keyword: '#366b6b', // def: 'rgb(56, 189, 193)', // qualifier: '#1c92a9', // attribute: '#b56531', // number: '#1f6ed6;', // string: '#d64292', // builtin: '#d47509', // string2: '#0b7fc7', // variable: 'rgb(236, 95, 103)', // meta: '#b33086', // atom: 'rgb(245, 160, 0)', // ws: 'rgba(23, 42, 58, 0.8)', // selection: '#d1e9fd', // cursorColor: 'rgba(0, 0, 0, 0.4)', // editorBackground: '#f6f7f7', // resultBackground: '#eeeff0', // leftDrawerBackground: '#e9eaea', // rightDrawerBackground: '#e5e7e7', // } ================================================ FILE: packages/graphql-playground-react/src/middlewareIndex.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom' import MiddlewareApp from './components/MiddlewareApp' import './index.css' if (process.env.NODE_ENV !== 'production') { /* tslint:disable-next-line */ // const { whyDidYouUpdate } = require('why-did-you-update') // whyDidYouUpdate(React) } /* tslint:disable-next-line */ ;(window as any)['GraphQLPlayground'] = { init(element: HTMLElement, options) { ReactDOM.render( , element, ) }, } ================================================ FILE: packages/graphql-playground-react/src/setupEnzyme.ts ================================================ import { configure } from 'enzyme' import * as EnzymeAdapter from 'enzyme-adapter-react-16' import { JSDOM } from 'jsdom' configure({ adapter: new EnzymeAdapter() }) // TODO: Fix/document this hack // https://github.com/jsdom/jsdom#intervening-before-parsing const jsdom = new JSDOM('', { beforeParse(window) { window.focus = jest.fn() }, }) const { window } = jsdom function copyProps(src, target) { const props = Object.getOwnPropertyNames(src) .filter((prop) => typeof target[prop] === 'undefined') .reduce( (result, prop) => ({ ...result, [prop]: Object.getOwnPropertyDescriptor(src, prop), }), {}, ) Object.defineProperties(target, props) } ;(global as any).window = window as any ;(global as any).document = window.document as any ;(global as any).navigator = { userAgent: 'node.js', } copyProps(window, global) // TODO: Fix/document this hack ;(global as any).document.createRange = () => { return { setEnd: () => {}, setStart: () => {}, getBoundingClientRect: () => { return { right: 0, } }, getClientRects: () => { return { length: 0, left: 0, right: 0, } }, } } ================================================ FILE: packages/graphql-playground-react/src/state/appHistory/actions.ts ================================================ import { createActions } from 'redux-actions' export const { selectAppHistoryItem } = createActions({ SELECT_APP_HISTORY_ITEM: item => ({ item }), }) ================================================ FILE: packages/graphql-playground-react/src/state/appHistory/reducers.ts ================================================ import { Record, OrderedMap } from 'immutable' import { handleActions } from 'redux-actions' // tslint:disable export class AppHistory extends Record({ items: OrderedMap(), }) { items: OrderedMap } export class AppHistoryItem extends Record({ type: 'local', configString: undefined, configPath: undefined, endpoint: undefined, folderName: undefined, env: undefined, platformToken: undefined, lastOpened: new Date(), config: undefined, } as any) { type: 'local' | 'endpoint' configString?: string configPath?: string endpoint?: string folderName?: string env?: any platformToken?: string lastOpened: Date config?: any } export default handleActions( { SELECT_APP_HISTORY_ITEM: (state, { payload }) => state.setIn(['items', payload.item.path], payload.item), }, new AppHistory(), ) export const getAppHistory = state => state.appHistory ================================================ FILE: packages/graphql-playground-react/src/state/createStore.ts ================================================ import { compose, createStore, Store, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootSaga from './rootSaga' import rootReducer from './workspace/reducers' import { getSelectedSession } from './sessions/selectors' import { serializeState, deserializeState } from './localStorage' const sagaMiddleware = createSagaMiddleware() const functions = [applyMiddleware(sagaMiddleware)] const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose const initialState = deserializeState() export default (): Store => { const store = createStore( rootReducer, initialState, composeEnhancers.apply(null, functions), ) store.subscribe(serializeState(store)) ;(window as any).s = store ;(window as any).session = () => { return getSelectedSession(store.getState()) } sagaMiddleware.run(rootSaga) return store } ================================================ FILE: packages/graphql-playground-react/src/state/docs/actions.ts ================================================ import { createActions } from 'redux-actions' export const { setStacks, addStack, toggleDocs, setDocsVisible, changeWidthDocs, changeKeyMove, showDocForReference, } = createActions({ SET_STACKS: (sessionId, stacks) => ({ sessionId, stacks }), ADD_STACK: (sessionId, field, x, y) => ({ sessionId, field, x, y }), 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 }), }) ================================================ FILE: packages/graphql-playground-react/src/state/docs/reducers.ts ================================================ import { GraphQLField } from 'graphql' import { Map, List, Record, set } from 'immutable' import { handleActions } from 'redux-actions' import { columnWidth } from '../../constants' export type DocsState = Map export interface NavItem { x: number y: number field: GraphQLField } export interface DocsSessionState { readonly navStack: List> 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() { const obj = this.toObject() // don't serialize navStack, as it could contain circular references in the type definitions return set(obj, 'navStack', List([])) } } const defaultState: DocsState = Map({ '': new DocsSession() }) export default handleActions( { SET_STACKS: (state, { payload: { sessionId, stacks } }) => { let session = getSession(state, sessionId) session = session.set('navStack', stacks) return state.set(sessionId, session) }, ADD_STACK: (state, { payload: { sessionId, field, x, y } }) => { if (!field.path) { field.path = field.name } let session = getSession(state, sessionId) session = session.update('navStack', navStack => { let newNavStack = navStack if (x < newNavStack.count()) { newNavStack = newNavStack.slice(0, x) } return newNavStack.push( Map({ x, y, field, }), ) }) return state.set(sessionId, session) }, 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, 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 } }) => { let session = getSession(state, sessionId) session = session.set('docsWidth', width) return state.set(sessionId, session) }, CHANGE_KEY_MOVE: (state, { payload: { sessionId, keyMove } }) => { let session = getSession(state, sessionId) session = session.set('keyMove', keyMove) return state.set(sessionId, session) }, }, defaultState, ) function getSession(state, sessionId) { if (!sessionId) { throw new Error('sessionId cant be null') } return state.get(sessionId) || new DocsSession() } ================================================ FILE: packages/graphql-playground-react/src/state/docs/selectors.ts ================================================ import { createSelector } from 'reselect' import { DocsSession } from './reducers' import { getSelectedWorkspace } from '../workspace/reducers' export const getSessionDocsState = createSelector( [getSelectedWorkspace], state => { const sessionId = state.sessions.selectedSessionId return state.docs.get(sessionId) || new DocsSession() }, ) export const getSessionDocs = createSelector([getSessionDocsState], state => { return state.toJS() }) export const getDocsOpen = createSelector([getSessionDocsState], state => { return state.get('docsOpen') }) ================================================ FILE: packages/graphql-playground-react/src/state/general/actions.ts ================================================ import { createActions } from 'redux-actions' export const { setSettingsString, setConfigString, openHistory, closeHistory, } = createActions({ SET_SETTINGS_STRING: settingsString => ({ settingsString }), SET_CONFIG_STRING: configString => ({ configString }), OPEN_HISTORY: () => ({}), CLOSE_HISTORY: () => ({}), }) ================================================ FILE: packages/graphql-playground-react/src/state/general/reducers.ts ================================================ import { Record } from 'immutable' import { handleActions } from 'redux-actions' export class GeneralState extends Record({ historyOpen: false, fixedEndpoint: false, endpoint: '', configString: '', envVars: {}, }) { historyOpen: boolean fixedEndpoint: boolean endpoint: string configString: string envVars: any } export default handleActions( { OPEN_HISTORY: state => state.set('historyOpen', true), CLOSE_HISTORY: state => state.set('historyOpen', false), SET_ENDPOINT_DISABLED: (state, { payload: { value } }) => state.set('endpointDisabled', value), SET_CONFIG_STRING: (state, { payload: { configString } }) => state.set('configString', configString), }, new GeneralState(), ) ================================================ FILE: packages/graphql-playground-react/src/state/general/selectors.ts ================================================ const makeGeneralSelector = key => state => { return state.general.get(key) } export const getFixedEndpoint = makeGeneralSelector('fixedEndpoint') export const getHistoryOpen = makeGeneralSelector('historyOpen') export const getConfigString = makeGeneralSelector('configString') ================================================ FILE: packages/graphql-playground-react/src/state/history/actions.ts ================================================ import { createActions } from 'redux-actions' export const { toggleHistoryItemStarring, addHistoryItem } = createActions({ TOGGLE_HISTORY_ITEM_STARRING: sessionId => ({ sessionId }), ADD_HISTORY_ITEM: session => ({ session }), }) ================================================ FILE: packages/graphql-playground-react/src/state/history/reducers.ts ================================================ import { OrderedMap, List } from 'immutable' import { handleActions } from 'redux-actions' import * as cuid from 'cuid' import { SessionStateProps } from '../sessions/reducers' export type HistoryState = OrderedMap export const defaultHistoryState: HistoryState = OrderedMap({}) export default handleActions( { TOGGLE_HISTORY_ITEM_STARRING: (state, { payload: { sessionId } }) => state.setIn([sessionId, 'starred'], !state.getIn([sessionId, 'starred'])), ADD_HISTORY_ITEM: (state, { payload: { session } }) => { const id = cuid() return state.slice(-40).set( id, session.merge({ id, date: new Date(), responses: List(), }), ) }, }, defaultHistoryState, ) ================================================ FILE: packages/graphql-playground-react/src/state/history/selectors.ts ================================================ import { createSelector } from 'reselect' import { getSelectedWorkspace } from '../workspace/reducers' export const getHistory = createSelector( [getSelectedWorkspace], state => state.history, ) ================================================ FILE: packages/graphql-playground-react/src/state/localStorage.ts ================================================ import { debounce } from 'lodash' import { deserializePersistedState } from './workspace/deserialize' export function serializeState(store) { return debounce( () => { const state = store.getState() if (!state.stateInjected) { localStorage.setItem('graphql-playground', JSON.stringify(state)) } }, 300, { trailing: true }, ) as any } export function deserializeState() { try { // let before = performance.now() const state = localStorage.getItem('graphql-playground') if (state) { // console.log( // `Needed ${performance.now() - before}ms to get ${ // state.length // } bytes from localstorage`, // ) // before = performance.now() const json = JSON.parse(state) // console.log(`Needed ${performance.now() - before}ms to parse state`) // before = performance.now() const result = deserializePersistedState(json) as any // console.log( // `Needed ${performance.now() - before}ms to deserialize the parsed json`, // ) return result } } catch (e) { // } return undefined } ================================================ FILE: packages/graphql-playground-react/src/state/rootSaga.ts ================================================ import { fecthingSagas } from './sessions/fetchingSagas' import { sessionsSagas } from './sessions/sagas' import { all, AllEffect } from 'redux-saga/effects' import { sharingSagas } from './sharing/sharingSaga' export default function* rootSaga() { yield all([...sessionsSagas, ...fecthingSagas, ...sharingSagas]) } export { AllEffect } ================================================ FILE: packages/graphql-playground-react/src/state/sessions/WebSocketLink.ts ================================================ import { ApolloLink, Operation, FetchResult, Observable } from 'apollo-link'; import { print, GraphQLError } from 'graphql'; import { Client } from 'graphql-ws'; export class WebSocketLink extends ApolloLink { constructor(private client: Client) { super(); } public request(operation: Operation): Observable { return new Observable((sink) => { return this.client.subscribe( { ...operation, query: print(operation.query) }, { next: sink.next.bind(sink), complete: sink.complete.bind(sink), error: (err) => { if (err instanceof Error) { sink.error(err); } else if (err instanceof CloseEvent) { sink.error( new Error( `Socket closed with event ${err.code}` + err.reason ? `: ${err.reason}` // reason will be available on clean closes : '', ), ); } else { sink.error( new Error( (err as GraphQLError[]) .map(({ message }) => message) .join(', '), ), ); } }, }, ); }); } } ================================================ FILE: packages/graphql-playground-react/src/state/sessions/actions.ts ================================================ import { createActions } from 'redux-actions' export const { editQuery, editVariables, setOperationName, editHeaders, editEndpoint, setVariableToType, setOperations, startQuery, stopQuery, setEditorFlex, openQueryVariables, closeQueryVariables, setVariableEditorHeight, setResponseTracingHeight, setTracingSupported, closeTracing, openTracing, closeVariables, openVariables, addResponse, setResponse, clearResponses, openSettingsTab, schemaFetchingSuccess, schemaFetchingError, setEndpointUnreachable, renewStacks, runQuery, prettifyQuery, fetchSchema, updateQueryFacts, runQueryAtPosition, toggleTracing, toggleVariables, newSession, newSessionFromQuery, newFileTab, closeTab, closeSelectedTab, editSettings, saveSettings, editConfig, saveConfig, editFile, saveFile, selectTab, selectTabIndex, selectNextTab, selectPrevTab, duplicateSession, querySuccess, queryError, setSubscriptionActive, setQueryTypes, injectHeaders, openConfigTab, editName, setResponseExtensions, setCurrentQueryStartTime, setCurrentQueryEndTime, refetchSchema, setScrollTop, reorderTabs, } = createActions({ // simple property setting EDIT_QUERY: query => ({ query }), EDIT_HEADERS: simpleAction('headers'), EDIT_ENDPOINT: simpleAction('endpoint'), EDIT_VARIABLES: simpleAction('variables'), SET_OPERATION_NAME: simpleAction('operationName'), SET_VARIABLE_TO_TYPE: simpleAction('variableToType'), SET_OPERATIONS: simpleAction('operations'), SET_EDITOR_FLEX: simpleAction('editorFlex'), EDIT_NAME: simpleAction('name'), OPEN_QUERY_VARIABLES: () => ({ queryVariablesActive: true }), CLOSE_QUERY_VARIABLES: () => ({ queryVariablesActive: false }), SET_VARIABLE_EDITOR_HEIGHT: simpleAction('variableEditorHeight'), SET_RESPONSE_TRACING_HEIGHT: simpleAction('responceTracingHeight'), SET_TRACING_SUPPORTED: simpleAction('tracingSupported'), SET_SUBSCRIPTION_ACTIVE: simpleAction('subscriptionActive'), SET_QUERY_TYPES: simpleAction('queryTypes'), SET_RESPONSE_EXTENSIONS: simpleAction('responseExtensions'), SET_CURRENT_QUERY_START_TIME: simpleAction('currentQueryStartTime'), SET_CURRENT_QUERY_END_TIME: simpleAction('currentQueryEndTime'), UPDATE_QUERY_FACTS: simpleAction(), PRETTIFY_QUERY: simpleAction(), INJECT_HEADERS: (headers, endpoint) => ({ headers, endpoint }), // setting multiple props /* this.setState({ responseTracingOpen: false, responseTracingHeight: hadHeight, } as State) */ CLOSE_TRACING: simpleAction('responseTracingHeight'), OPEN_TRACING: simpleAction('responseTracingHeight'), TOGGLE_TRACING: simpleAction(), // setting multiple props /* this.setState({ responseTracingOpen: false, responseTracingHeight: hadHeight, } as State) */ CLOSE_VARIABLES: simpleAction('variableEditorHeight'), OPEN_VARIABLES: simpleAction('variableEditorHeight'), TOGGLE_VARIABLES: simpleAction(), /* a littlebit more complex state mutations */ ADD_RESPONSE: (workspaceId, sessionId, response) => ({ workspaceId, sessionId, response, }), SET_RESPONSE: (workspaceId, sessionId, response) => ({ workspaceId, sessionId, response, }), CLEAR_RESPONSES: simpleAction(), FETCH_SCHEMA: simpleAction(), REFETCH_SCHEMA: simpleAction(), SET_ENDPOINT_UNREACHABLE: simpleAction('endpoint'), SET_SCROLL_TOP: (sessionId, scrollTop) => ({ sessionId, scrollTop }), SCHEMA_FETCHING_SUCCESS: (endpoint, tracingSupported, isPollingSchema) => ({ endpoint, tracingSupported, isPollingSchema, }), /* this.setState({ schema, isReloadingSchema: false, endpointUnreachable: false, + tracingSupported }) */ SCHEMA_FETCHING_ERROR: (endpoint, error) => ({ endpoint, error }), /* this.setState({ isReloadingSchema: false, endpointUnreachable: true, }) */ RENEW_STACKS: simpleAction(), /* GraphQLEditor.renewStacks() */ RUN_QUERY: operationName => ({ operationName }), QUERY_SUCCESS: simpleAction(), QUERY_ERROR: simpleAction(), RUN_QUERY_AT_POSITION: position => ({ position }), START_QUERY: simpleAction('queryRunning', true), STOP_QUERY: (sessionId, workspaceId) => ({ workspaceId, sessionId }), /* GraphQLEditor.handleRunQuery */ OPEN_SETTINGS_TAB: () => ({}), OPEN_CONFIG_TAB: () => ({}), NEW_SESSION: (endpoint, reuseHeaders) => ({ endpoint, reuseHeaders }), NEW_SESSION_FROM_QUERY: (query: string) => ({ query }), NEW_FILE_TAB: (fileName: string, filePath: string, file: string) => ({ fileName, filePath, file, }), DUPLICATE_SESSION: simpleAction('session'), CLOSE_SELECTED_TAB: () => ({}), SELECT_NEXT_TAB: () => ({}), SELECT_PREV_TAB: () => ({}), SELECT_TAB: simpleAction('sessionId'), SELECT_TAB_INDEX: simpleAction('index'), CLOSE_TAB: simpleAction('sessionId'), REORDER_TABS: (src, dest) => ({ src, dest }), // files, settings, config EDIT_SETTINGS: simpleAction(), SAVE_SETTINGS: simpleAction(), EDIT_CONFIG: simpleAction(), SAVE_CONFIG: simpleAction(), EDIT_FILE: simpleAction(), SAVE_FILE: simpleAction(), }) function simpleAction(key?: any, defaultValue?: any) { return value => ({ [key]: value || defaultValue }) } ================================================ FILE: packages/graphql-playground-react/src/state/sessions/fetchingSagas.ts ================================================ import { ApolloLink, execute } from 'apollo-link' import { parseHeaders } from '../../components/Playground/util/parseHeaders' import { HttpLink } from 'apollo-link-http' import { isSubscription } from '../../components/Playground/util/hasSubscription' import { takeLatest, ForkEffect, put, select, takeEvery, take, } from 'redux-saga/effects' import { eventChannel, END } from 'redux-saga' import { makeOperation } from '../../components/Playground/util/makeOperation' import { setSubscriptionActive, stopQuery, startQuery, addResponse, setResponseExtensions, setCurrentQueryStartTime, setCurrentQueryEndTime, setEndpointUnreachable, clearResponses, setResponse, } from './actions' import { getSelectedSession, getSessionsState, getParsedVariablesFromSession, } from './selectors' import { SchemaFetcher } from '../../components/Playground/SchemaFetcher' import { getSelectedWorkspaceId, getSettings } from '../workspace/reducers' import * as cuid from 'cuid' import { Session, ResponseRecord } from './reducers' import { addHistoryItem } from '../history/actions' import { safely } from '../../utils' import { set } from 'immutable' import { SubscriptionClient as SubscriptionClientSTWS } from 'subscriptions-transport-ws' import { WebSocketLink as WebSocketLinkALW } from 'apollo-link-ws' import { createClient as createSubscriptionClient, Client as SubscriptionClientGWS } from 'graphql-ws' import { WebSocketLink as WebSocketLinkGW } from './WebSocketLink' // tslint:disable let subscriptionEndpoint export function setSubscriptionEndpoint(endpoint) { subscriptionEndpoint = endpoint } export interface LinkCreatorProps { endpoint: string headers?: Headers credentials?: string subscriptionTransport?: string } export interface Headers { [key: string]: string | number | null } const isWSEndpoint = (endpoint: string): boolean => !!endpoint.match(/wss?/); export const defaultLinkCreator = ( session: LinkCreatorProps, subscriptionEndpoint?: string, ): { link: ApolloLink; subscriptionClient?: SubscriptionClientGWS | SubscriptionClientSTWS } => { let connectionParams = {} const { headers, credentials, subscriptionTransport } = session if (headers) { connectionParams = { ...headers } } const httpLink = new HttpLink({ uri: session.endpoint, headers, credentials, }) // ws endpoint => graphql-ws default link if (isWSEndpoint(session.endpoint)) { const subscriptionClient = createSubscriptionClient({ retryAttempts: 1000, retryWait: () => new Promise(resolve => setTimeout(resolve, 20000)), lazy: true, connectionParams, url: session.endpoint, }) return { link: new WebSocketLinkGW(subscriptionClient), subscriptionClient, } } // http endpoint & graphql-ws => default link = http + graphql-ws subscriptions if (subscriptionTransport === 'graphql-ws') { const subscriptionClient = createSubscriptionClient({ retryWait: () => new Promise(resolve => setTimeout(resolve, 20000)), lazy: true, connectionParams, url: subscriptionEndpoint || session.endpoint.replace('http', 'ws'), }) return { subscriptionClient, link: new WebSocketLinkGW(subscriptionClient) } } // http endpoint => default link = http + subscriptions-transport-ws subscriptions const subscriptionClient = new SubscriptionClientSTWS( subscriptionEndpoint || session.endpoint.replace('http', 'ws'), { timeout: 20000, lazy: true, connectionParams, } ) const webSocketLink = new WebSocketLinkALW(subscriptionClient); return { link: ApolloLink.split( operation => isSubscription(operation), webSocketLink, httpLink, ), subscriptionClient, } } let linkCreator = defaultLinkCreator export let schemaFetcher: SchemaFetcher = new SchemaFetcher(linkCreator) ;(window as any).schemaFetcher = schemaFetcher export function setLinkCreator(newLinkCreator) { if (newLinkCreator) { linkCreator = newLinkCreator schemaFetcher = new SchemaFetcher(newLinkCreator) } } const subscriptions = {} const isSubscriptionClientSTWS = ( client: SubscriptionClientGWS | SubscriptionClientSTWS ): client is SubscriptionClientSTWS => { return !!(client as SubscriptionClientSTWS).onDisconnected } function* runQuerySaga(action) { // run the query const { operationName } = action.payload const selectedWorkspaceId = yield select(getSelectedWorkspaceId) const session: Session = yield select(getSelectedSession) const request = { query: session.query, operationName, variables: getParsedVariablesFromSession(session), } const operation = makeOperation(request) const operationIsSubscription = isSubscription(operation) const workspace = yield select(getSelectedWorkspaceId) const settings = yield select(getSettings) yield put(setSubscriptionActive(isSubscription(operation))) yield put(startQuery()) let headers = parseHeaders(session.headers) if (session.tracingSupported && session.responseTracingOpen) { headers = set(headers, 'X-Apollo-Tracing', '1') } const lol = { endpoint: session.endpoint, headers: { ...settings['request.globalHeaders'], ...headers, }, subscriptionTransport: settings['subscriptions.protocol'], credentials: settings['request.credentials'], } const { link, subscriptionClient } = linkCreator(lol, subscriptionEndpoint) yield put(setCurrentQueryStartTime(new Date())) let firstResponse = false const channel = eventChannel(emitter => { let closed = false if (subscriptionClient && operationIsSubscription) { const onDisconnect = () => { closed = true emitter({ error: new Error( `Could not connect to websocket endpoint ${subscriptionEndpoint}. Please check if the endpoint url is correct.`, ), }) emitter(END) } if (isSubscriptionClientSTWS(subscriptionClient)) { subscriptionClient.onDisconnected(onDisconnect) } else { subscriptionClient.on('closed', onDisconnect) } } const subscription = execute(link, operation).subscribe({ next: function(value) { emitter({ value }) }, error: error => { emitter({ error }) emitter(END) }, complete: () => { emitter(END) }, }) const unsubscribe = () => { if (!closed) { try { subscription.unsubscribe() } catch (e) { console.error(e) } } } const key = `${workspace}~${session.id}` subscriptions[key] = { unsubscribe } return unsubscribe }) try { while (true) { const { value, error } = yield take(channel) if (value && value.extensions) { const extensions = value.extensions yield put(setResponseExtensions(extensions)) if ( value.extensions.tracing && settings['tracing.hideTracingResponse'] ) { delete value.extensions.tracing } } const response = new ResponseRecord({ date: JSON.stringify(value ? value : formatError(error), null, 2), time: new Date(), resultID: cuid(), }) const errorMessage = extractMessage(error) if (errorMessage === 'Failed to fetch') { yield put(setEndpointUnreachable(session.endpoint)) } if (operationIsSubscription) { if (firstResponse) { yield put(clearResponses()) firstResponse = false } yield put(addResponse(selectedWorkspaceId, session.id, response)) } else { yield put(setResponse(selectedWorkspaceId, session.id, response)) } yield put(addHistoryItem(session)) } } finally { yield put(setCurrentQueryEndTime(new Date())) yield put(stopQuery(session.id, selectedWorkspaceId)) } } export function formatError(error, fetchingSchema: boolean = false) { const message = extractMessage(error) if (message === 'Failed to fetch') { const schemaMessage = fetchingSchema ? ' schema' : '' return { error: `${message}${schemaMessage}. Please check your connection` } } try { const ee = JSON.parse(message) return ee } catch (e) { // } return { error: message } } function extractMessage(error) { if (error instanceof Error) { // Errors from apollo-link-http may include a "result" object, which is a JSON response from // the server. We should surface that to the client if (!!error['result'] && typeof error['result'] === 'object') { return (error as any).result } return error.message } if (typeof error === 'string') { return error } return error } function* stopQuerySaga(action) { const { sessionId, workspaceId } = action.payload const { sessions } = yield select(getSessionsState) const session = sessions.get(sessionId) const workspace = yield workspaceId || select(getSelectedWorkspaceId) const key = `${workspace}~${session.id}` const subscription = subscriptions[key] if (subscription && subscription.unsubscribe) { subscription.unsubscribe() } delete subscriptions[key] } export const fecthingSagas = [ takeEvery('RUN_QUERY', safely(runQuerySaga)), takeLatest('STOP_QUERY', safely(stopQuerySaga)), ] // needed to fix typescript export { ForkEffect } ================================================ FILE: packages/graphql-playground-react/src/state/sessions/reducers.ts ================================================ import { OrderedMap, Map, List, Record, merge } from 'immutable' import { handleActions, combineActions } from 'redux-actions' import { editQuery, editVariables, editHeaders, editEndpoint, setEditorFlex, openQueryVariables, closeQueryVariables, setVariableEditorHeight, setResponseTracingHeight, setTracingSupported, setVariableToType, setOperations, setOperationName, setSubscriptionActive, startQuery, setQueryTypes, editName, setResponseExtensions, setCurrentQueryStartTime, setCurrentQueryEndTime, } from './actions' import { getSelectedSessionId } from './selectors' import { getDefaultSession, defaultQuery } from '../../constants' import * as cuid from 'cuid' import { formatError } from './fetchingSagas' import { arrayMove } from 'react-sortable-hoc' export interface SessionStateProps { sessions: OrderedMap selectedSessionId: string sessionCount: number headers?: string } export interface Tab { endpoint: string query: string name?: string variables?: string responses?: string[] headers?: { [key: string]: string } } // tslint:disable export class Session extends Record(getDefaultSession('')) { id: string endpoint: string query: string file?: string variables: string responses?: List operationName?: string queryRunning: boolean subscriptionActive: boolean // query facts operations: List variableToType: VariableToType // additional props that are interactive in graphiql, these are not represented in graphiqls state queryTypes: QueryTypes date: Date hasMutation: boolean hasSubscription: boolean hasQuery: boolean isFile?: boolean starred?: boolean name?: string filePath?: string selectedUserToken?: string headers?: string absolutePath?: string isSettingsTab?: boolean isConfigTab?: boolean currentQueryStartTime?: Date currentQueryEndTime?: Date isReloadingSchema: boolean isSchemaPendingUpdate: boolean responseExtensions: any queryVariablesActive: boolean endpointUnreachable: boolean // editor settings editorFlex: number variableEditorOpen: boolean variableEditorHeight: number responseTracingOpen: boolean responseTracingHeight: number nextQueryStartTime?: Date tracingSupported?: boolean docExplorerWidth: number changed?: boolean scrollTop?: number toJSON() { const obj = this.toObject() const override: any = { queryRunning: false, subscriptionActive: false, responseExtensions: {}, } // dont serialize very big responses as the localStorage size is limited if ( obj.responses && obj.responses.size > 0 && (obj.responses.size > 20 || obj.responses.get(0).date.length > 2000) ) { override.responses = List() } return merge(obj, override) } } export type VariableToType = Map export interface QueryTypes { firstOperationName: string | null subscription: boolean query: boolean mutation: boolean } export interface OperationDefinition { startLine: number endLine: number name: string } export interface ResponseType { resultID: string date: string time: Date } export class ResponseRecord extends Record({ resultID: '', date: '', time: new Date(), isSchemaError: false, }) { resultID: string date: string time: Date isSchemaError: boolean } function makeSession(endpoint = '') { return new Session({ endpoint }).set('id', cuid()) } export function sessionFromTab(tab: Tab): Session { return new Session({ ...tab, headers: tab.headers ? JSON.stringify(tab.headers, null, 2) : '', responses: tab.responses && tab.responses.length > 0 ? List(tab.responses.map(r => new ResponseRecord({ date: r }))) : List(), }).set('id', cuid()) } export class SessionState extends Record({ sessions: OrderedMap({}), selectedSessionId: '', sessionCount: 0, headers: '', }) { sessions: OrderedMap selectedSessionId: string sessionCount: number headers: string } export function makeSessionState(endpoint) { const session = new Session({ endpoint: endpoint || '' }) return new SessionState({ sessions: OrderedMap({ [session.id]: session }), selectedSessionId: session.id, sessionCount: 1, }) } const reducer = handleActions( { [combineActions( editQuery, editVariables, editHeaders, editEndpoint, setEditorFlex, openQueryVariables, closeQueryVariables, setVariableEditorHeight, setResponseTracingHeight, setTracingSupported, setVariableToType, setOperations, setOperationName, setSubscriptionActive, startQuery, setQueryTypes, editName, setResponseExtensions, setCurrentQueryStartTime, setCurrentQueryEndTime, )]: (state, { payload }) => { const keys = Object.keys(payload) const keyName = keys.length === 1 ? keys[0] : keys[1] const path = ['sessions', getSelectedSessionId(state), keyName] return state.setIn(path, payload[keyName]) }, START_QUERY: state => { return state .setIn(['sessions', getSelectedSessionId(state), 'queryRunning'], true) .setIn( ['sessions', getSelectedSessionId(state), 'responseExtensions'], undefined, ) }, CLOSE_TRACING: (state, { payload: { responseTracingHeight } }) => { return state.mergeDeepIn( ['sessions', getSelectedSessionId(state)], Map({ responseTracingHeight, responseTracingOpen: false }), ) }, TOGGLE_TRACING: state => { const path = [ 'sessions', getSelectedSessionId(state), 'responseTracingOpen', ] return state.setIn(path, !state.getIn(path)) }, OPEN_TRACING: (state, { payload: { responseTracingHeight } }) => { return state.mergeDeepIn( ['sessions', getSelectedSessionId(state)], Map({ responseTracingHeight, responseTracingOpen: true }), ) }, CLOSE_VARIABLES: (state, { payload: { variableEditorHeight } }) => { return state.mergeDeepIn( ['sessions', getSelectedSessionId(state)], Map({ variableEditorHeight, variableEditorOpen: false }), ) }, OPEN_VARIABLES: (state, { payload: { variableEditorHeight } }) => { return state.mergeDeepIn( ['sessions', getSelectedSessionId(state)], Map({ variableEditorHeight, variableEditorOpen: true }), ) }, TOGGLE_VARIABLES: state => { const path = [ 'sessions', getSelectedSessionId(state), 'variableEditorOpen', ] return state.setIn(path, !state.getIn(path)) }, ADD_RESPONSE: (state, { payload: { response, sessionId } }) => { return state.updateIn(['sessions', sessionId, 'responses'], responses => responses.push(response), ) }, SET_RESPONSE: (state, { payload: { response, sessionId } }) => { return state.setIn(['sessions', sessionId, 'responses'], List([response])) }, CLEAR_RESPONSES: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'responses'], List(), ) }, FETCH_SCHEMA: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'isReloadingSchema'], true, ) }, REFETCH_SCHEMA: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'isReloadingSchema'], true, ) }, STOP_QUERY: (state, { payload: { sessionId } }) => { return state.mergeIn(['sessions', sessionId], { queryRunning: false, subscriptionActive: false, }) }, SET_SCROLL_TOP: (state, { payload: { sessionId, scrollTop } }) => { if (state.sessions.get(sessionId)) { return state.setIn(['sessions', sessionId, 'scrollTop'], scrollTop) } return state }, SCHEMA_FETCHING_SUCCESS: (state, { payload }) => { const newSessions = state.get('sessions').map((session: Session) => { if (session.endpoint === payload.endpoint) { // if there was an error, clear it away const data: any = { tracingSupported: payload.tracingSupported, isReloadingSchema: false, endpointUnreachable: false, } const response = session.responses ? session.responses!.first() : null if ( response && session.responses!.size === 1 && // @ts-ignore response.isSchemaError ) { data.responses = List([]) } return session.merge(Map(data)) } return session }) return state.set('sessions', newSessions) }, SET_ENDPOINT_UNREACHABLE: (state, { payload }) => { const newSessions = state.get('sessions').map((session, sessionId) => { if (session.get('endpoint') === payload.endpoint) { return session.merge( Map({ endpointUnreachable: true, }), ) } return session }) return state.set('sessions', newSessions) }, SCHEMA_FETCHING_ERROR: (state, { payload }) => { const newSessions = state.get('sessions').map((session, sessionId) => { if (session.get('endpoint') === payload.endpoint) { let { responses } = session // Only override the responses if there is one or zero and that one is a schemaError // Don't override user's responses! if (responses.size <= 1) { let response = session.responses ? session.responses!.first() : null if (!response || response.isSchemaError) { response = new ResponseRecord({ resultID: cuid(), isSchemaError: true, date: JSON.stringify(formatError(payload.error, true), null, 2), time: new Date(), }) } responses = List([response]) } return session.merge( Map({ isReloadingSchema: false, endpointUnreachable: true, responses, }), ) } return session }) return state.set('sessions', newSessions) }, SET_SELECTED_SESSION_ID: (state, { payload: { sessionId } }) => state.set('selectedSessionId', sessionId), OPEN_SETTINGS_TAB: (state: any) => { let newState = state let settingsTab = state.sessions.find(value => value.get('isSettingsTab', false), ) if (!settingsTab) { settingsTab = makeSession().merge({ isSettingsTab: true, isFile: true, name: 'Settings', changed: false, }) newState = newState.setIn(['sessions', settingsTab.id], settingsTab) } return newState.set('selectedSessionId', settingsTab.id) }, OPEN_CONFIG_TAB: state => { let newState = state let configTab = state.sessions.find(value => value.get('isConfigTab', false), ) if (!configTab) { configTab = makeSession().merge({ isConfigTab: true, isFile: true, name: 'GraphQL Config', changed: false, }) newState = newState.setIn(['sessions', configTab.id], configTab) } return newState.set('selectedSessionId', configTab.id) }, NEW_FILE_TAB: (state, { payload: { fileName, filePath, file } }) => { let newState = state let fileTab = state.sessions.find( value => value.get('name', '') === fileName, ) if (!fileTab) { fileTab = makeSession().merge({ isFile: true, name: fileName, changed: false, file, filePath, }) newState = newState.setIn(['sessions', fileTab.id], fileTab) } return newState .set('selectedSessionId', fileTab.id) .set('sessionCount', newState.sessions.size) }, NEW_SESSION: (state, { payload: { reuseHeaders, endpoint } }) => { const currentSession = state.sessions.first() const newSession: any = { query: '', isReloadingSchema: currentSession.isReloadingSchema, endpointUnreachable: currentSession.endpointUnreachable, } if (currentSession.endpointUnreachable) { newSession.responses = currentSession.responses } let session = makeSession(endpoint || currentSession.endpoint).merge( newSession, ) if (reuseHeaders) { const selectedSessionId = getSelectedSessionId(state) const currentSession = state.sessions.get(selectedSessionId) session = session.set('headers', currentSession.headers) } else { session = session.set('headers', state.headers) } return state .setIn(['sessions', session.id], session) .set('selectedSessionId', session.id) .set('sessionCount', state.sessions.size + 1) }, // inject headers is used for graphql config // it makes sure, that there definitely is a tab open with the correct header INJECT_HEADERS: (state, { payload: { headers, endpoint } }) => { // if there are no headers to inject, there's nothing to do if (!headers || headers === '' || Object.keys(headers).length === 0) { return state } const headersString = typeof headers === 'string' ? headers : JSON.stringify(headers, null, 2) const selectedSessionId = getSelectedSessionId(state) let newState = state.set('headers', headersString) const currentSession = state.sessions.get(selectedSessionId) if (currentSession.headers === headersString) { return newState } if (currentSession.query === defaultQuery) { return newState.setIn( ['sessions', selectedSessionId, 'headers'], headersString, ) } const session = makeSession(endpoint).set('headers', headersString) return newState .setIn(['sessions', session.id], session) .set('selectedSessionId', session.id) .set('sessionCount', state.sessions.size + 1) }, DUPLICATE_SESSION: (state, { payload: { session } }) => { const newSession = session.set('id', cuid()) return state .setIn(['sessions', newSession.id], newSession) .set('selectedSessionId', newSession.id) .set('sessionCount', state.sessions.size + 1) }, NEW_SESSION_FROM_QUERY: (state, { payload: { query } }) => { const session = makeSession().set('query', query) return state .setIn(['sessions', session.id], session) .set('sessionCount', state.sessions.size + 1) }, CLOSE_SELECTED_TAB: state => { const selectedSessionId = getSelectedSessionId(state) return closeTab(state, selectedSessionId).set( 'sessionCount', state.sessions.size - 1, ) }, SELECT_NEXT_TAB: state => { const selectedSessionId = getSelectedSessionId(state) const count = state.sessions.size const keys = state.sessions.keySeq() const index = keys.indexOf(selectedSessionId) if (index + 1 < count) { return state.set('selectedSessionId', keys.get(index + 1)) } return state.set('selectedSessionId', keys.get(0)) }, SELECT_PREV_TAB: state => { const selectedSessionId = getSelectedSessionId(state) const count = state.sessions.size const keys = state.sessions.keySeq() const index = keys.indexOf(selectedSessionId) if (index - 1 >= 0) { return state.set('selectedSessionId', keys.get(index - 1)) } return state.set('selectedSessionId', keys.get(count - 1)) }, SELECT_TAB_INDEX: (state, { payload: { index } }) => { const keys = state.sessions.keySeq() return state.set('selectedSessionId', keys.get(index)) }, SELECT_TAB: (state, { payload: { sessionId } }) => { return state.set('selectedSessionId', sessionId) }, CLOSE_TAB: (state, { payload: { sessionId } }) => { return closeTab(state, sessionId).set( 'sessionCount', state.sessions.size - 1, ) }, REORDER_TABS: (state, { payload: { src, dest } }) => { const seq = state.sessions.toIndexedSeq() const indexes: number[] = [] for (let i = 0; i < seq.size; i++) indexes.push(i) const newIndexes = arrayMove(indexes, src, dest) let newSessions = OrderedMap() for (let i = 0; i < seq.size; i++) { const ndx = newIndexes[i] const val = seq.get(ndx) newSessions = newSessions.set(val.id, val) } return state.set('sessions', newSessions) }, EDIT_SETTINGS: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'changed'], true, ) }, SAVE_SETTINGS: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'changed'], false, ) }, EDIT_CONFIG: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'changed'], true, ) }, SAVE_CONFIG: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'changed'], false, ) }, EDIT_FILE: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'changed'], true, ) }, SAVE_FILE: state => { return state.setIn( ['sessions', getSelectedSessionId(state), 'changed'], false, ) }, }, makeSessionState(''), ) // add a self-healing wrapper to clean up broken states export default (state, action) => { const newState: SessionState = reducer(state, action) if (newState.selectedSessionId === '' && state.sessions.size > 0) { return newState.set('selectedSessionId', state.sessions.first().id) } return newState } function closeTab(state, sessionId) { const length = state.sessions.size const keys = state.sessions.keySeq() let newState = state.removeIn(['sessions', sessionId]) const session = state.sessions.get(sessionId) // if there is only one session, delete it and replace it by a new one // and keep the endpoint & headers of the last one if (length === 1) { const newSessionData: any = { query: '', headers: session.headers, isReloadingSchema: session.isReloadingSchema, endpointUnreachable: session.endpointUnreachable, } if (session.endpointUnreachable) { newSessionData.responses = session.responses } const newSession = makeSession(session.endpoint).merge(newSessionData) newState = newState.set('selectedSessionId', newSession.id) return newState.setIn(['sessions', newSession.id], newSession) } const selectedSessionId = getSelectedSessionId(state) const sessionIndex = keys.indexOf(sessionId) // if the session to be closed is selected, unselect it if (selectedSessionId === sessionId) { const leftNeighbour = sessionIndex - 1 // if its the first session on the left, take the right neighbour if (leftNeighbour < 0) { return newState.set('selectedSessionId', keys.get(1)) } return newState.set('selectedSessionId', keys.get(leftNeighbour)) } else { // otherwise the old selected session still can be selected, only the session has to be removed return newState } } ================================================ FILE: packages/graphql-playground-react/src/state/sessions/sagas.ts ================================================ import { takeLatest, ForkEffect, delay, select, takeEvery, put, } from 'redux-saga/effects' import { getSelectedSession, getIsPollingSchema } from './selectors' import getSelectedOperationName from '../../components/Playground/util/getSelectedOperationName' import { getQueryFacts } from '../../components/Playground/util/getQueryFacts' import { fromJS, is } from 'immutable' import { editQuery, setVariableToType, setOperations, setOperationName, schemaFetchingSuccess, schemaFetchingError, // fetchSchema, runQuery, setTracingSupported, setQueryTypes, refetchSchema, fetchSchema, } from './actions' import { getRootMap, getNewStack } from '../../components/Playground/util/stack' import { DocsSessionState } from '../docs/reducers' import { setStacks } from '../docs/actions' import { HistoryState } from '../history/reducers' import { addHistoryItem } from '../history/actions' import { schemaFetcher } from './fetchingSagas' import { getSelectedWorkspace, getSettings } from '../workspace/reducers' import { getSessionDocsState } from '../docs/selectors' import { getQueryTypes } from '../../components/Playground/util/getQueryTypes' import { parse } from 'graphql' import { Session } from './reducers' import { safely, prettify } from '../../utils' import * as queryString from 'query-string' import { parseHeaders } from '../../components/Playground/util/parseHeaders' function* setQueryFacts() { // debounce by 100 ms yield delay(100) const session: Session = yield select(getSelectedSession) const { schema } = yield schemaFetcher.fetch(session) try { const ast = parse(session.query) const queryFacts = getQueryFacts(schema, ast) if (queryFacts) { const immutableQueryFacts = fromJS(queryFacts) const operationName = getSelectedOperationName( session.operations, session.operationName, immutableQueryFacts.operations, ) if ( !is(immutableQueryFacts.get('variableToType'), session.variableToType) ) { // set variableToType yield put(setVariableToType(immutableQueryFacts.get('variableToType'))) } if (!is(immutableQueryFacts.get('operations'), session.operations)) { // set operations yield put(setOperations(immutableQueryFacts.get('operations'))) } if (operationName !== session.operationName) { yield put(setOperationName(operationName)) } } const queryTypes = getQueryTypes(ast) yield put(setQueryTypes(queryTypes)) } catch (e) { const queryTypes = getQueryTypes(null) yield put(setQueryTypes(queryTypes)) } } function* reflectQueryToUrl({ payload }) { // debounce by 100 ms yield delay(100) if (!location.search.includes('query')) { return } const params = queryString.parse(location.search) if (typeof params.query !== 'undefined') { const newSearch = queryString.stringify({ ...params, query: payload.query, }) const url = `${location.origin}${location.pathname}?${newSearch}` window.history.replaceState( {}, document.getElementsByTagName('title')[0].innerHTML, url, ) } } function* runQueryAtPosition(action) { const { position } = action.payload const session: Session = yield select(getSelectedSession) if (session.operations) { let operationName const operations = session.operations.toJS() operations.forEach((operation: any) => { if ( operation.loc && operation.loc.start <= position && operation.loc.end >= position ) { operationName = operation.name && operation.name.value } }) if (operationName) { yield put(runQuery(operationName)) } else { yield put(runQuery()) } } else { yield put(runQuery()) } } function* getSessionWithCredentials() { const session = yield select(getSelectedSession) const settings = yield select(getSettings) const combinedHeaders = { ...settings['request.globalHeaders'], ...parseHeaders(session.headers), } return { endpoint: session.endpoint, headers: JSON.stringify(combinedHeaders), credentials: settings['request.credentials'], } } function* fetchSchemaSaga() { const session: Session = yield getSessionWithCredentials() try { yield schemaFetcher.fetch(session) yield put( schemaFetchingSuccess( session.endpoint, null, yield select(getIsPollingSchema), ), ) } catch (e) { yield put(schemaFetchingError(session.endpoint)) yield delay(5000) yield put(fetchSchema()) } } function* refetchSchemaSaga() { const session: Session = yield getSessionWithCredentials() try { yield schemaFetcher.refetch(session) yield put( schemaFetchingSuccess( session.endpoint, null, yield select(getIsPollingSchema), ), ) } catch (e) { yield put(schemaFetchingError(session.endpoint)) yield delay(5000) yield put(refetchSchema()) } } let lastSchema function* renewStacks() { const session: Session = yield select(getSelectedSession) const fetchSession = yield getSessionWithCredentials() const docs: DocsSessionState = yield select(getSessionDocsState) const result = yield schemaFetcher.fetch(fetchSession) const { schema, tracingSupported } = result if (schema && (!lastSchema || lastSchema !== schema)) { const rootMap = getRootMap(schema) const stacks = docs.navStack .map(stack => getNewStack(rootMap, schema, stack)) .filter(s => s) yield put(setStacks(session.id, stacks)) yield put(setTracingSupported(tracingSupported)) lastSchema = schema } } function* addToHistory({ payload }) { const { sessionId } = payload const workspace = yield select(getSelectedWorkspace) const session = workspace.getIn(['sessions', sessionId]) const history: HistoryState = workspace.get('history') const exists = history.toKeyedSeq().find(item => is(item, session)) if (!exists) { yield put(addHistoryItem(session)) } } function* prettifyQuery() { const { query } = yield select(getSelectedSession) const settings = yield select(getSettings) try { const prettyQuery = prettify(query, { printWidth: settings['prettier.printWidth'], tabWidth: settings['prettier.tabWidth'], useTabs: settings['prettier.useTabs'], }) yield put(editQuery(prettyQuery)) } catch (e) { // TODO show errors somewhere // tslint:disable-next-line console.log(e) } } export const sessionsSagas = [ takeLatest('GET_QUERY_FACTS', safely(setQueryFacts)), takeLatest('SET_OPERATION_NAME', safely(setQueryFacts)), takeEvery('EDIT_QUERY', safely(setQueryFacts)), takeEvery('EDIT_QUERY', safely(reflectQueryToUrl)), takeEvery('RUN_QUERY_AT_POSITION', safely(runQueryAtPosition)), takeLatest('FETCH_SCHEMA', safely(fetchSchemaSaga)), takeLatest('REFETCH_SCHEMA', safely(refetchSchemaSaga)), takeLatest('SCHEMA_FETCHING_SUCCESS', safely(renewStacks)), takeEvery('QUERY_SUCCESS' as any, safely(addToHistory)), takeLatest('PRETTIFY_QUERY', safely(prettifyQuery)), ] // needed to fix typescript export { ForkEffect } ================================================ FILE: packages/graphql-playground-react/src/state/sessions/selectors.ts ================================================ import { createSelector } from 'reselect' import { makeWorkspace } from '../workspace/reducers' function getSelectedWorkspaceId(state) { return state.get('selectedWorkspace') } function getSelectedWorkspace(state) { return ( state.getIn(['workspaces', getSelectedWorkspaceId(state)]) || makeWorkspace('') ) } export const getSessionsState = createSelector( [getSelectedWorkspace], workspace => workspace.get('sessions'), ) export const getSelectedSession = createSelector([getSessionsState], state => { const id = getSelectedSessionId(state) const session = state.getIn(['sessions', id]) return session }) export const getSelectedSessionId = state => state.selectedSessionId && state.selectedSessionId !== '' ? state.selectedSessionId : state.sessions.first().id export const getSelectedSessionIdFromRoot = createSelector( [getSelectedSession], state => state.get('id'), ) const makeSessionSelector = prop => { return createSelector([getSelectedSession], session => session.get(prop)) } export const getScrollTop = makeSessionSelector('scrollTop') export const getEndpoint = makeSessionSelector('endpoint') export const getQuery = makeSessionSelector('query') export const getFile = makeSessionSelector('file') export const getVariables = makeSessionSelector('variables') export const getResponses = makeSessionSelector('responses') export const getOperationName = makeSessionSelector('operationName') export const getQueryRunning = makeSessionSelector('queryRunning') export const getSubscriptionActive = makeSessionSelector('subscriptionActive') export const getOperations = makeSessionSelector('operations') export const getVariableToType = makeSessionSelector('variableToType') export const getQueryTypes = makeSessionSelector('queryTypes') export const getDate = makeSessionSelector('date') export const getHasMutation = makeSessionSelector('hasMutation') export const getHasSubscription = makeSessionSelector('hasSubscription') export const getHasQuery = makeSessionSelector('hasQuery') export const getIsFile = makeSessionSelector('isFile') export const getStarred = makeSessionSelector('starred') export const getName = makeSessionSelector('name') export const getFilePath = makeSessionSelector('filePath') export const getSelectedUserToken = makeSessionSelector('selectedUserToken') export const getHeaders = makeSessionSelector('headers') export const getHasChanged = makeSessionSelector('hasChanged') export const getAbsolutePath = makeSessionSelector('absolutePath') export const getIsSettingsTab = makeSessionSelector('isSettingsTab') export const getIsConfigTab = makeSessionSelector('isConfigTab') export const getCurrentQueryStartTime = makeSessionSelector( 'currentQueryStartTime', ) export const getCurrentQueryEndTime = makeSessionSelector('currentQueryEndTime') export const getIsReloadingSchema = makeSessionSelector('isReloadingSchema') export const getIsPollingSchema = createSelector( [getEndpoint, getSettings], (endpoint, settings) => { const json = JSON.parse(settings) try { const isPolling = json['schema.polling.enable'] && endpoint.match(`/${json['schema.polling.endpointFilter']}`) && true return isPolling } catch (e) { return false } }, ) export const getResponseExtensions = makeSessionSelector('responseExtensions') export const getQueryVariablesActive = makeSessionSelector( 'queryVariablesActive', ) export const getEndpointUnreachable = makeSessionSelector('endpointUnreachable') export const getEditorFlex = makeSessionSelector('editorFlex') export const getVariableEditorOpen = makeSessionSelector('variableEditorOpen') export const getVariableEditorHeight = makeSessionSelector( 'variableEditorHeight', ) export const getResponseTracingOpen = makeSessionSelector('responseTracingOpen') export const getResponseTracingHeight = makeSessionSelector( 'responseTracingHeight', ) export const getDocExplorerWidth = makeSessionSelector('docExplorerWidth') export const getNextQueryStartTime = makeSessionSelector('nextQueryStartTime') export const getTracingSupported = makeSessionSelector('tracingSupported') function getSettings(state) { return state.getIn(['settingsString']) } export const getTabWidth = createSelector([getSettings], settings => { try { const json = JSON.parse(settings) return json['prettier.tabWidth'] || 2 } catch (e) { // } return 2 }) export const getUseTabs = createSelector([getSettings], settings => { try { const json = JSON.parse(settings) return json['prettier.useTabs'] || false } catch (e) { // } return false }) export const getHeadersCount = createSelector([getHeaders], headers => { try { const json = JSON.parse(headers) return Object.keys(json).length } catch (e) { // } return 0 }) export const getParsedHeaders = createSelector( [getSelectedSession], getParsedHeadersFromSession, ) export function getParsedHeadersFromSession(headers) { try { const json = JSON.parse(headers) return json } catch (e) { // } return {} } export const getParsedVariables = createSelector( [getSelectedSession], getParsedVariablesFromSession, ) export function getParsedVariablesFromSession(session) { const variables = session.variables try { const json = JSON.parse(variables) return json } catch (e) { // } return {} } export const getTracing = createSelector( [getResponseExtensions], extensions => extensions && extensions.tracing, ) export const getSessionsArray = createSelector([getSessionsState], state => { const array = state .get('sessions') .toArray() .map(arr => arr[1]) return array }) ================================================ FILE: packages/graphql-playground-react/src/state/sharing/actions.ts ================================================ import { createActions } from 'redux-actions' export const { share, toggleShareHistory, toggleShareHeaders, toggleShareAllTabs, setShareUrl, } = createActions({ TOGGLE_SHARE_HISTORY: () => ({}), TOGGLE_SHARE_HEADERS: () => ({}), TOGGLE_SHARE_ALL_TABS: () => ({}), SHARE: () => ({}), SET_SHARE_URL: shareUrl => ({ shareUrl }), }) ================================================ FILE: packages/graphql-playground-react/src/state/sharing/reducers.ts ================================================ import { Record } from 'immutable' import { handleActions } from 'redux-actions' export class SharingState extends Record({ history: false, headers: true, allTabs: true, shareUrl: null, }) { history: boolean headers: boolean allTabs: boolean shareUrl: any // go away typescript } export default handleActions( { TOGGLE_SHARE_HISTORY: state => state.set('history', !state.history), TOGGLE_SHARE_HEADERS: state => state.set('headers', !state.headers), TOGGLE_SHARE_ALL_TABS: state => state.set('allTabs', !state.allTabs), SET_SHARE_URL: (state, { payload: { shareUrl } }) => state.set('shareUrl', shareUrl), }, new SharingState(), ) ================================================ FILE: packages/graphql-playground-react/src/state/sharing/selectors.ts ================================================ import { createSelector } from 'reselect' import { getSelectedWorkspace } from '../workspace/reducers' export const getSharingState = createSelector([getSelectedWorkspace], state => { return state.sharing }) const makeSharingSelector = key => createSelector([getSharingState], state => { return state.get(key) }) export const getSharingHistory = makeSharingSelector('history') export const getSharingHeaders = makeSharingSelector('headers') export const getSharingAllTabs = makeSharingSelector('allTabs') export const getShareUrl = makeSharingSelector('shareUrl') ================================================ FILE: packages/graphql-playground-react/src/state/sharing/sharingSaga.ts ================================================ import { takeEvery, ForkEffect, select, put } from 'redux-saga/effects' import { getEndpoint } from '../sessions/selectors' import { setShareUrl } from './actions' import * as cuid from 'cuid' import { getSharingState } from './selectors' import { Map } from 'immutable' import { safely } from '../../utils' function* share() { const state = yield makeSharingState() const endpoint = yield select(getEndpoint) const res = yield fetch('https://api.graphqlbin.com/', { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: ` mutation ($session: String! $endpoint: String!) { addSession(session: $session endpoint: $endpoint) { id } } `, variables: { session: JSON.stringify(state), endpoint, }, }), }).then(data => data.json()) const shareUrl = `https://graphqlbin.com/v2/${res.data.addSession.id}` yield put(setShareUrl(shareUrl)) } function* makeSharingState() { let state = yield select() const sharing = yield select(getSharingState) const id = cuid() state = state .update('workspaces', w => w.filter((workspace, key) => key === state.selectedWorkspace), ) .set('selectedWorkspace', `${id}~${state.selectedWorkspace}`) .update('workspaces', w => w.mapKeys(k => `${id}~${k}`)) const selectedSessionId = state.workspaces.get(state.selectedWorkspace) .sessions.selectedSessionId if (!sharing.allTabs) { state = state .updateIn( ['workspaces', state.selectedWorkspace, 'sessions', 'sessions'], sessions => sessions.filter((value, key) => key === selectedSessionId), ) .setIn( ['workspaces', state.selectedWorkspace, 'sessions', 'sessionCount'], 1, ) } if (!sharing.headers) { state = state.updateIn( ['workspaces', state.selectedWorkspace, 'sessions', 'sessions'], sessions => sessions.map(session => session.set('headers', '')), ) } if (!sharing.history) { state = state.setIn( ['workspaces', state.selectedWorkspace, 'history'], Map(), ) } return state } export const sharingSagas = [takeEvery('SHARE', safely(share))] // needed to fix typescript export { ForkEffect } ================================================ FILE: packages/graphql-playground-react/src/state/workspace/actions.ts ================================================ import { createActions } from 'redux-actions' export const { selectWorkspace, initState, injectState, injectTabs, } = createActions({ SELECT_WORKSPACE: workspace => ({ workspace }), INIT_STATE: (workspaceId, endpoint) => ({ workspaceId, endpoint }), INJECT_STATE: state => ({ state }), INJECT_TABS: tabs => ({ tabs }), }) ================================================ FILE: packages/graphql-playground-react/src/state/workspace/deserialize.ts ================================================ import { DocsSession } from '../docs/reducers' import { Session, SessionState, ResponseRecord } from '../sessions/reducers' import { SharingState } from '../sharing/reducers' import { Map, OrderedMap, List, fromJS } from 'immutable' import { GeneralState } from '../general/reducers' import { mapValues } from 'lodash' import { RootState, Workspace, normalizeSettingsString } from './reducers' import { AppHistory, AppHistoryItem } from '../appHistory/reducers' export function deserializePersistedState(state) { return new RootState({ workspaces: deserializeWorkspaces(state.workspaces), selectedWorkspace: state.selectedWorkspace, settingsString: normalizeSettingsString(state.settingsString), appHistory: deserializeAppHistory(state.appHistory), general: deserializeGeneral(state.general), }) as any } function deserializeWorkspaces(workspaces): Map { return Map( mapValues(workspaces, (workspace, workspaceId) => { return new Workspace({ docs: deserializeDocs(workspace.docs), sessions: deserializeSessionsState(workspace.sessions), sharing: deserializeSharing(workspace.sharing), history: deserializeHistory(workspace.history), }) }), ) } function deserializeAppHistory(state) { return new AppHistory({ items: OrderedMap(mapValues(state.items, item => new AppHistoryItem(item))), }) } function deserializeDocs(state): Map { return Map( mapValues(state, docsSession => { return new DocsSession({ docsOpen: docsSession.docsOpen, keyMove: docsSession.keyMove, docsWidth: docsSession.docsWidth, navStack: deserializeNavstack(docsSession.navStack), }) }), ) } function deserializeNavstack(navStack) { // note that stacks are plain objects. could be refactored to Map later return List(navStack.map(s => Map(s))) as any } function deserializeSessionsState(state) { const sessions: any = deserializeSessions(state.sessions) const selectedSessionId = state.selectedSessionId && state.selectedSessionId !== '' ? state.selectedSessionId : // @ts-ignore sessions.first()!.id return new SessionState({ selectedSessionId, // @ts-ignore sessions, sessionCount: sessions.size, headers: state.headers, }) } function deserializeSessions(state) { return OrderedMap(mapValues(state, session => deserializeSession(session))) } function deserializeSession(session) { return new Session({ ...session, responses: deserializeResponses(session.responses), operations: fromJS(session.operations), variableToType: Map(session.variableToType), date: session.date ? new Date(session.date) : undefined, currentQueryStartTime: session.currentQueryStartTime ? new Date(session.currentQueryStartTime) : undefined, currentQueryEndTime: session.currentQueryEndTime ? new Date(session.currentQueryEndTime) : undefined, nextQueryStartTime: session.nextQueryStartTime ? new Date(session.nextQueryStartTime) : undefined, }) } function deserializeResponses(responses) { return List( responses .filter(r => r.isSchemaError) .map(response => deserializeResponse(response)), ) } function deserializeResponse(response) { return new ResponseRecord({ resultID: response.resultID, date: response.date, time: new Date(response.time), isSchemaError: response.isSchemaError || false, }) } function deserializeSharing({ shareUrl, ...state }) { // dont deserialize the shareUrl return new SharingState(state) } function deserializeHistory(state) { return deserializeSessions(state) } function deserializeGeneral(state) { return new GeneralState(state) } ================================================ FILE: packages/graphql-playground-react/src/state/workspace/reducers.ts ================================================ import { Reducer } from 'redux' import { combineReducers } from 'redux-immutable' import docs, { DocsSession, DocsState } from '../docs/reducers' import sessions, { makeSessionState, SessionState, Tab, sessionFromTab, Session, } from '../sessions/reducers' import sharing, { SharingState } from '../sharing/reducers' import history, { HistoryState } from '../history/reducers' import { Map, Record, OrderedMap } from 'immutable' import general, { GeneralState } from '../general/reducers' import { immutableMemoize } from '../../components/Playground/util/immutableMemoize' import { createSelector } from 'reselect' import { deserializePersistedState } from './deserialize' import appHistory, { AppHistory } from '../appHistory/reducers' // import { createSelector } from 'reselect' import { ISettings } from '../../types' export function getSelectedWorkspaceId(state) { return state.get('selectedWorkspace') } export function getSelectedWorkspace(state) { return state.getIn(['workspaces', getSelectedWorkspaceId(state)]) } export class Workspace extends Record({ docs: Map({}), sessions: makeSessionState(''), sharing: new SharingState(), history: OrderedMap(), }) { docs: DocsState sessions: SessionState sharing: SharingState history: HistoryState } export const defaultSettings: ISettings = { 'editor.cursorShape': 'line', 'editor.fontFamily': `'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace`, 'editor.fontSize': 14, 'editor.reuseHeaders': true, 'editor.theme': 'dark', 'general.betaUpdates': false, 'prettier.printWidth': 80, 'prettier.tabWidth': 2, 'prettier.useTabs': false, 'request.credentials': 'omit', 'request.globalHeaders': {}, 'schema.disableComments': true, 'schema.polling.enable': true, 'schema.polling.endpointFilter': '*localhost*', 'schema.polling.interval': 2000, 'tracing.hideTracingResponse': true, 'tracing.tracingSupported': true, 'subscriptions.protocol': 'subscription-transport-ws', } // tslint:disable-next-line:max-classes-per-file export class RootState extends Record({ workspaces: Map({ '': makeWorkspace('') }), selectedWorkspace: '', settingsString: JSON.stringify(defaultSettings, null, 2), stateInjected: false, appHistory: new AppHistory(), general: new GeneralState(), }) { workspaces: Map selectedWorkspace: string settingsString: string stateInjected: false appHistory: AppHistory general: GeneralState } const workspaceReducers: Reducer = combineReducers({ docs, sessions, sharing, history, general, appHistory, }) // todo: add lru-cache later when the localStorage is full export const rootReducer = (state = new RootState(), action) => { if (action.type === 'SELECT_WORKSPACE') { return state.set('selectedWorkspace', action.payload.workspace) } if (action.type === 'SET_SETTINGS_STRING') { return state.set('settingsString', action.payload.settingsString) } if (action.type === 'INIT_STATE' && !state.stateInjected) { const { workspaceId, endpoint } = action.payload if (!state.workspaces.get(workspaceId)) { const newState = state.setIn( ['workspaces', workspaceId], makeWorkspace(endpoint), ) return newState.set('selectedWorkspace', workspaceId) } return state.set('selectedWorkspace', workspaceId) } if (action.type === 'INJECT_STATE') { return deserializePersistedState(action.payload.state).set( 'stateInjected', true, ) } if (action.type === 'INJECT_TABS') { return makeStateFromTabs(action.payload.tabs) } if (action.type === 'SELECT_APP_HISTORY_ITEM') { return state.set('appHistory', appHistory(state.appHistory, action)) } const generalActions = { OPEN_HISTORY: true, CLOSE_HISTORY: true, SET_ENDPOINT_DISABLED: true, SET_CONFIG_STRING: true, } if (generalActions[action.type]) { return state.set('general', general(state.general, action)) } const selectedWorkspaceId = action.payload && action.payload.workspaceId ? action.payload.workspaceId : getSelectedWorkspaceId(state) const path = ['workspaces', selectedWorkspaceId] return state.setIn(path, workspaceReducers(state.getIn(path), action)) } function makeStateFromTabs(tabs: Tab[]): RootState { const endpoint = tabs[0].endpoint const tabSessions = OrderedMap( tabs.map(sessionFromTab).reduce( (acc, curr) => { return { ...acc, [curr.id]: curr } }, {} as OrderedMap, ), ) // @ts-ignore const selectedSessionId = tabSessions.first()!.id const workspace = makeWorkspace(endpoint) .setIn(['sessions', 'sessions'], tabSessions) .setIn(['sessions', 'selectedSessionId'], selectedSessionId) return new RootState() .setIn(['workspaces', endpoint], workspace) .set('selectedWorkspace', endpoint) } export function makeWorkspace(endpoint) { const sessionState = makeSessionState(endpoint) // weird typescript error return new Workspace({ docs: Map({ [sessionState.selectedSessionId]: new DocsSession(), }), sessions: sessionState, sharing: new SharingState(), history: OrderedMap(), }) as any } export default rootReducer export const getSessionCounts = immutableMemoize(state => { return state.workspaces.map(w => w.sessions.sessionCount) }) export const getSettingsString = state => state.settingsString export const getSettings = createSelector( [getSettingsString], parseSettingsString, ) function normalizeSettings(settings) { const theme = settings['editor.theme'] if (theme !== 'dark' && theme !== 'light') { settings['editor.theme'] = 'dark' } return { ...defaultSettings, ...settings, } } function parseSettingsString(settingsString) { try { return normalizeSettings(JSON.parse(settingsString)) } catch (e) { return defaultSettings } } export function normalizeSettingsString(settingsString) { return JSON.stringify(parseSettingsString(settingsString), null, 2) } export const getTheme = (state, customSettings) => { const settings = customSettings || getSettings(state) return settings['editor.theme'] || 'dark' } ================================================ FILE: packages/graphql-playground-react/src/styled/index.ts ================================================ export * from './styled' export { default as styled } from './styled' ================================================ FILE: packages/graphql-playground-react/src/styled/styled.ts ================================================ import * as styledComponents from 'styled-components' import { ThemedStyledComponentsModule } from 'styled-components' // tslint:disable-line import { ComponentClass } from 'react' // tslint:disable-line import { ThemeInterface, theme } from './theme' const { default: styled, css, injectGlobal, keyframes, ThemeProvider, withTheme, createGlobalStyle, } = styledComponents as ThemedStyledComponentsModule export { css, injectGlobal, keyframes, ThemeProvider, theme, withTheme, createGlobalStyle, ThemeInterface, } export default styled export { ComponentClass } ================================================ FILE: packages/graphql-playground-react/src/styled/theme.ts ================================================ import { ISettings } from '../types' import { defaultSettings } from '../state/workspace/reducers' export interface Colours { green: string darkBlue: string darkBlue50: string darkBlue60: string darkBlue80: string darkBlue30: string darkBlue20: string darkBlue10: string darkerBlue: string darkestBlue: string white80: string white70: string white60: string white30: string white20: string white10: string white: string black02: string black04: string black07: string black10: string black30: string black40: string black50: string paleText: string paleGrey: string red: string blue: string orange: string purple: string lightGrey: string lighterGrey: string // New dynamic styles text: string textInactive: string } 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 text: string textInactive: string background: string sidebarTop: string sidebar: string sidebarBottom: string sidebarItemActive: string sidebarItemSide: string sidebarItemSessions: string tab: string tabInactive: string tabText: string navigationBar: string navigationBarText: string editorBackground: string resultBackground: string leftDrawerBackground: string rightDrawerBackground: string drawerText: string drawerTextInactive: string executeButton: string executeButtonBorder: string executeButtonHover: string executeButtonSubscription: string executeButtonSubscriptionHover: string icon: string iconHover: string pollingIcon: string pollingIconShadow: string button: string buttonHover: string buttonText: string buttonWorkspace: string buttonWorkspaceHover: string buttonWorkspaceText: string circle: string subscriptionTimeBoaderTop: string subscriptionTimeText: string } export const darkColours: Colours = { green: '#27ae60', darkBlue: 'rgb(23, 42, 58)', darkBlue50: 'rgba(23, 42, 58, 0.5)', darkBlue80: 'rgba(23, 42, 58, 0.8)', darkBlue60: 'rgba(23, 42, 58, 0.6)', darkBlue30: 'rgba(23, 42, 58, 0.3)', darkBlue20: 'rgba(23, 42, 58, 0.2)', darkBlue10: 'rgba(23, 42, 58, 0.1)', darkerBlue: '#0F202D', darkestBlue: 'rgb(11,20,28)', white10: 'rgba(255, 255, 255, 0.1)', white20: 'rgba(255, 255, 255, 0.2)', white30: 'rgba(255, 255, 255, 0.3)', white60: 'rgba(255, 255, 255, 0.6)', white70: 'rgba(255, 255, 255, 0.7)', white80: 'rgba(255, 255, 255, 0.8)', white: 'rgba(255, 255, 255, 1)', black02: 'rgba(0, 0, 0, 0.02)', black07: 'rgba(0, 0, 0, 0.07)', black04: 'rgba(0, 0, 0, 0.04)', black10: 'rgba(0, 0, 0, 0.1)', black30: 'rgba(0, 0, 0, 0.3)', black40: 'rgba(0, 0, 0, 0.4)', black50: 'rgba(0, 0, 0, 0.5)', red: '#f25c54', 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', lighterGrey: '#f6f7f7', // New colors text: 'rgba(255,255,255,0.6)', textInactive: '#555e66', } export const lightColours: Colours = { green: '#27ae60', darkBlue: 'rgb(23, 42, 58)', darkBlue50: 'rgba(23, 42, 58, 0.5)', darkBlue80: 'rgba(23, 42, 58, 0.8)', darkBlue60: 'rgba(23, 42, 58, 0.6)', darkBlue30: 'rgba(23, 42, 58, 0.3)', darkBlue20: 'rgba(23, 42, 58, 0.2)', darkBlue10: 'rgba(23, 42, 58, 0.1)', darkerBlue: '#0F202D', darkestBlue: 'rgb(11,20,28)', white10: 'rgba(255, 255, 255, 0.1)', white20: 'rgba(255, 255, 255, 0.2)', white30: 'rgba(255, 255, 255, 0.3)', white60: 'rgba(255, 255, 255, 0.6)', white70: 'rgba(255, 255, 255, 0.7)', white80: 'rgba(255, 255, 255, 0.8)', white: 'rgba(255, 255, 255, 1)', black02: 'rgba(0, 0, 0, 0.02)', black04: 'rgba(0, 0, 0, 0.04)', black10: 'rgba(0, 0, 0, 0.1)', black07: 'rgba(0, 0, 0, 0.07)', black30: 'rgba(0, 0, 0, 0.3)', black40: 'rgba(0, 0, 0, 0.4)', black50: 'rgba(0, 0, 0, 0.5)', red: '#f25c54', 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', lighterGrey: '#f6f7f7', // New colors text: 'rgba(0,0,0,.7)', textInactive: 'rgba(0,0,0,.3)', } export const darkEditorColours: EditorColours = { property: 'rgb(41, 185, 115)', comment: 'rgba(255, 255, 255, 0.3)', punctuation: 'rgba(255, 255, 255, 0.4)', keyword: 'rgb(42, 126, 211)', def: 'rgb(56, 189, 193)', qualifier: '#1c92a9', attribute: 'rgb(247, 116, 102)', number: '#2882f9', string: '#d64292', builtin: '#d47509', string2: '#0b7fc7', variable: 'rgb(181, 34, 130)', meta: '#b33086', atom: 'rgb(249, 233, 34)', 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', sidebarTop: '#0f202d', sidebar: '#172b3a', sidebarBottom: '#172b3a', sidebarItemActive: 'rgb(23, 42, 58)', sidebarItemSide: '#27ae60', sidebarItemSessions: 'rgba(255, 255, 255, 0.05)', tab: '#172b3a', tabInactive: '#0f202d', tabText: '#fff', navigationBar: '#172b3a', navigationBarText: 'rgba(255, 255, 255, 0.6)', editorBackground: '#0f202d', resultBackground: '#172b3a', leftDrawerBackground: '#0b1924', rightDrawerBackground: '#0b1924', drawerText: 'rgba(255,255,255,0.6)', drawerTextInactive: '#555e66', executeButton: 'rgb(185, 191, 196)', executeButtonBorder: 'rgb(11, 20, 28)', executeButtonHover: 'rgb(195, 201, 206)', executeButtonSubscription: '#f25c54', executeButtonSubscriptionHover: '#f36c65', icon: 'rgb(74, 85, 95)', iconHover: 'rgba(255, 255, 255, 0.6)', pollingIcon: 'rgba(139, 149, 156, 1)', pollingIconShadow: 'rgba(139, 149, 156, 0.4)', button: '#0F202D', buttonHover: '#122535', buttonText: 'rgba(255,255,255,0.6)', buttonWorkspace: '#b9bfc4', buttonWorkspaceHover: '#a4acb2', buttonWorkspaceText: 'rgb(23, 42, 58)', circle: 'rgba(255, 255, 255, 0.4)', subscriptionTimeBoaderTop: 'rgba(255, 255, 255, 0.2)', subscriptionTimeText: 'rgba(255, 255, 255, 0.5)', } export const lightEditorColours: EditorColours = { property: '#328c8c', // comment: 'rgba(0, 0, 0, 0.3)', // punctuation: 'rgba(23,42,58,.8)', // keyword: '#366b6b', // def: 'rgb(56, 189, 193)', // qualifier: '#1c92a9', // attribute: '#b56531', // number: '#1f6ed6;', // string: '#d64292', // builtin: '#d47509', // string2: '#0b7fc7', // variable: 'rgb(236, 95, 103)', // meta: '#b33086', // atom: 'rgb(245, 160, 0)', // 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', sidebarTop: '#eeeff0', sidebar: '#eeeff0', sidebarBottom: '#f6f7f7', sidebarItemActive: '#f6f7f7', sidebarItemSide: '#27ae60', sidebarItemSessions: '#dbdee0', tab: '#eeeff0', tabInactive: '#e7eaec', tabText: 'rgba(23, 42, 58, .8)', navigationBar: '#eeeff0', navigationBarText: 'rgba(23, 42, 58, 0.8)', editorBackground: '#f6f7f7', resultBackground: '#eeeff0', leftDrawerBackground: '#e9eaea', rightDrawerBackground: '#e5e7e7', drawerText: 'rgba(0, 0, 0, 0.7)', drawerTextInactive: 'rgba(0, 0, 0, 0.3)', executeButton: 'rgb(115, 127, 136)', executeButtonBorder: '#eeeff0', executeButtonHover: '', executeButtonSubscription: '#f25c54', executeButtonSubscriptionHover: '#f36c65', icon: 'rgb(194, 200, 203)', iconHover: 'rgba(23, 42, 58, 0.6)', pollingIcon: 'rgba(139, 149, 156, 1)', pollingIconShadow: 'rgba(139, 149, 156, 0.4)', button: '#d8dbde', buttonHover: 'rgba(20, 37, 51, 0.2)', buttonText: 'rgba(23, 42, 58, 0.8)', buttonWorkspace: 'rgb(185, 191, 196)', buttonWorkspaceHover: 'rgb(157, 166, 173)', buttonWorkspaceText: 'rgb(238, 239, 240)', circle: 'rgba(23,42,58,.4)', subscriptionTimeBoaderTop: 'rgba(23, 42, 58, 0.2)', subscriptionTimeText: 'rgba(23, 42, 58, 0.5)', } export interface Sizes { small6: string small10: string small12: string small16: string medium25: string smallRadius: string fontLight: string fontSemiBold: string fontTiny: string fontSmall: string fontMedium: string } export const sizes: Sizes = { small6: '6px', small10: '10px', small12: '12px', small16: '16px', medium25: '25px', // font weights fontLight: '300', fontSemiBold: '600', // font sizes fontTiny: '12px', fontSmall: '14px', fontMedium: '20px', // others smallRadius: '2px', } export interface Shorthands { [x: string]: any } export const shorthands: Shorthands = {} export interface ThemeInterface { mode: 'light' | 'dark' colours: Colours sizes: Sizes shorthands: Shorthands editorColours: EditorColours settings: ISettings } export const theme: any = { mode: 'dark', colours: darkColours, sizes, shorthands, editorColours: darkEditorColours, settings: defaultSettings, } ================================================ FILE: packages/graphql-playground-react/src/types.ts ================================================ import { Observable, FetchResult } from 'apollo-link' export type ApolloLinkExecuteResponse = Observable export type HistoryFilter = 'HISTORY' | 'STARRED' export type Environment = 'Node' | 'Browser' | 'Cli' export type GraphQLClient = | 'fetch' | 'relay' | 'apollo' | 'graphql-request' | 'curl' export type Theme = 'dark' | 'light' export type CursorShape = 'line' | 'block' | 'underline' export interface ISettings { ['editor.cursorShape']: CursorShape ['editor.fontFamily']: string ['editor.fontSize']: number ['editor.reuseHeaders']: boolean ['editor.theme']: Theme ['general.betaUpdates']: boolean ['prettier.printWidth']: number ['prettier.tabWidth']: number ['prettier.useTabs']: boolean ['request.credentials']: 'omit' | 'include' | 'same-origin' ['request.globalHeaders']: { [key: string]: string } ['schema.disableComments']: boolean ['schema.polling.enable']: boolean ['schema.polling.endpointFilter']: string ['schema.polling.interval']: number ['tracing.hideTracingResponse']: boolean ['tracing.tracingSupported']: boolean ['subscriptions.protocol']: 'subscription-transport-ws' | 'graphql-ws' } ================================================ FILE: packages/graphql-playground-react/src/utils/performance.ts ================================================ let last: number | null = null // tslint:disable export function log(...messages) { if (messages.length === 0) { last = null } if (last) { console.log(...messages, `${performance.now() - last}ms`) } else { console.log(...messages) } last = performance.now() } ================================================ FILE: packages/graphql-playground-react/src/utils.ts ================================================ import * as prettier from 'prettier/standalone' import * as graphql from 'prettier/parser-graphql' // tslint:disable export function safely(cb: any) { return function*(...args) { try { yield cb(...args) } catch (e) { console.error(e) } } } interface PrettierOptions { printWidth: number tabWidth: number useTabs: boolean } export function prettify(query: string, options: PrettierOptions) { return prettier.format(query, { ...options, parser: 'graphql', plugins: [graphql], }) } export function isIframe() { try { return window.self !== window.top } catch (e) { return true } } ================================================ FILE: packages/graphql-playground-react/tests/schema.faker.graphql ================================================ type Company { name: String @fake(type:companyName) industry: String @examples(values: ["IT", "Manufacturing", "Medicine", "Media"]) employees: [Employee!] } # Employee type type Employee { firstName: String @fake(type: firstName, locale:en_CA) lastName: String @fake(type: lastName, locale:en_CA) address: String @fake(type:streetAddress, options: { useFullAddress: true }) subordinates: [Employee!]! company: Company } interface Node { id: ID! } interface Character { id: ID! name: String! } type Human implements Character, Node { id: ID! name: String! totalCredits: Int } union UNIONMASTER = Employee | User | Company enum Role { # Adimn role ADMIN USER @deprecated(reason: "not cool") # Jedi JEDI NO_DESCRIPTION DEP_NO_REASON @deprecated } type User { # User role lorem ipsum role: Role union: UNIONMASTER dep: String @deprecated(reason: "hey") hey: String! id: ID } type Query { # get first user user( # Hey i am the id id: ID ): User! human: Human employee(id: ID, name: String, lorem: String, Ipsum: Int): Employee company(id: ID): Company allCompanies: [Company!] } type Mutation { user: User } ================================================ FILE: packages/graphql-playground-react/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "lib", "noUnusedLocals": false } } ================================================ FILE: packages/graphql-playground-react/tsconfig.jest.json ================================================ { "extends": "./tsconfig", "compilerOptions": { "jsx": "react" } } ================================================ FILE: packages/graphql-playground-react/tsconfig.json ================================================ { "compilerOptions": { "outDir": "dist", "target": "es5", "lib": [ "esnext", "dom" ], "sourceMap": true, "jsx": "preserve", "rootDir": "src", "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "noImplicitThis": true, "noImplicitAny": false, "skipLibCheck": true, "strictNullChecks": false, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "declaration": true, "plugins": [ { "name": "typescript-styled-plugin" } ] }, "exclude": [ "node_modules", "lib", "dist", "examples", "build" ] } ================================================ FILE: packages/graphql-playground-react/tslint.json ================================================ { "extends": ["tslint-graphcool-frontend"], "rules": { "forin": false, "no-submodule-imports": false, "prefer-conditional-expression": false, "no-implicit-dependencies": false, "no-empty": false, "no-shadowed-variable": false, "prefer-for-of": false, "no-string-literal": false } } ================================================ FILE: packages/graphql-playground-react/typings/custom.d.ts ================================================ interface Window { GraphQLPlayground: any version: string } declare module '*.json' { const value: any export default value } ================================================ FILE: packages/graphql-playground-react/typings/styled-jsx.d.ts ================================================ import 'react' declare module 'react' { interface HTMLProps { jsx?: boolean global?: boolean } } declare module 'react' { interface StyleHTMLAttributes extends React.HTMLAttributes { jsx?: boolean global?: boolean } } ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base", "docker:disable" ], "ignoreDeps": [ "hapi" ] } ================================================ FILE: scripts/build.sh ================================================ #!/bin/bash set -e cd packages packages=( graphql-playground-html graphql-playground-react # graphql-playground-electron graphql-playground-middleware-express graphql-playground-middleware-hapi graphql-playground-middleware-koa graphql-playground-middleware-lambda ) for pkg in "${packages[@]}" do cd $pkg echo "Building ${pkg}" yarn build cd .. done ================================================ FILE: scripts/release-html.sh ================================================ #!/bin/bash if ! [ -x "$(command -v jq)" ]; then echo 'Error: jq is not installed.' >&2 exit 1 fi set -e cd packages middlewares=( graphql-playground-middleware-express graphql-playground-middleware-hapi graphql-playground-middleware-koa graphql-playground-middleware-lambda ) cd graphql-playground-html echo "Releasing graphql-playground-html..." yarn version --no-git-tag-version --new-version patch yarn publish --non-interactive version=$(cat package.json | jq -r '.version') cd .. for middleware in "${middlewares[@]}" do cd $middleware echo "Releasing ${middleware}..." yarn add graphql-playground-html@$version yarn version --no-git-tag-version --new-version patch yarn publish --non-interactive cd .. done ================================================ FILE: scripts/release-react.sh ================================================ #!/bin/bash if ! [ -x "$(command -v jq)" ]; then echo 'Error: jq is not installed.' >&2 exit 1 fi set -e cd packages middlewares=( graphql-playground-middleware-express graphql-playground-middleware-hapi graphql-playground-middleware-koa graphql-playground-middleware-lambda ) cd graphql-playground-react yarn install echo "Releasing graphql-playground-react..." yarn version --no-git-tag-version --new-version patch yarn publish --non-interactive export version=$(cat package.json | jq -r '.version') cd .. echo "Updating JSDeliver cache..." curl -X POST \ http://purge.jsdelivr.net/ \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "path": [ "/npm/graphql-playground-react/build/static/css/middleware.css", "/npm/graphql-playground-react/build/static/js/middleware.js" ] }' for middleware in "${middlewares[@]}" do cd $middleware yarn install echo "Releasing ${middleware}..." cat package.json | jq ".playgroundVersion = \"$version\"" > package.tmp.json mv package.tmp.json package.json npm version patch --no-git-tag-version npm publish cd .. done cd graphql-playground-electron echo "Updating dependency & version in graphql-playground-electron..." yarn add graphql-playground-react@$version yarn version --no-git-tag-version --new-version patch cd .. ================================================ FILE: scripts/versions.sh ================================================ #!/bin/bash set -e cd packages packages=( graphql-playground-react graphql-playground-html graphql-playground-electron graphql-playground-middleware-express graphql-playground-middleware-hapi graphql-playground-middleware-koa graphql-playground-middleware-lambda ) for pkg in "${packages[@]}" do cd $pkg version=$(cat package.json | jq -r '.version') echo "$pkg: $version" cd .. done