├── devtools.html ├── .babelrc ├── icons ├── icon128.png └── icon48.png ├── screenshots ├── 1.jpg ├── 2.jpg ├── 3.png └── 4.jpg ├── .gitignore ├── .eslintrc ├── manifest.json ├── src ├── components │ ├── Raw.js │ ├── Response.js │ ├── Computed.js │ ├── Entry.js │ ├── Request.js │ ├── CollapsableArray.js │ ├── Value.js │ ├── LongInformation.js │ ├── CollapsableObject.js │ ├── Collapsable.js │ └── DevToolsPanel.js ├── panel.js └── lib │ └── utils.js ├── package.js ├── webpack.config.js ├── package.json ├── README.md └── panel.html /devtools.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | } 4 | -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ghirro/graphql-network/HEAD/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ghirro/graphql-network/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /screenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ghirro/graphql-network/HEAD/screenshots/1.jpg -------------------------------------------------------------------------------- /screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ghirro/graphql-network/HEAD/screenshots/2.jpg -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ghirro/graphql-network/HEAD/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ghirro/graphql-network/HEAD/screenshots/4.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /package 4 | 5 | # OS generated files 6 | .DS_Store 7 | *.swp 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: "eslint-config-lens" 2 | parser: "babel-eslint" 3 | rules: 4 | max-len: 0 5 | arrow-body-style: 0 6 | consistent-return: 0 7 | react/jsx-no-bind: 0 8 | array-callback-return: 0 9 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GraphQL Network", 3 | "author": "Ben Stephenson ", 4 | "version": "1.5", 5 | "manifest_version": 2, 6 | "minimum_chrome_version": "43", 7 | "devtools_page": "devtools.html", 8 | "icons": { 9 | "48": "icons/icon48.png", 10 | "128": "icons/icon128.png" 11 | }, 12 | "permissions": [ 13 | "tabs", 14 | "http://*/*", 15 | "https://*/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Raw.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Raw({ 4 | query, 5 | queryVariables, 6 | }) { 7 | return ( 8 |
9 |

Raw Query Data

10 |
11 |         {query}
12 |       
13 |

Query Variables

14 |
15 |         {JSON.stringify(queryVariables, null, 4)}
16 |       
17 |
18 | ); 19 | } 20 | 21 | Raw.propTypes = { 22 | query: React.PropTypes.string.isRequired, 23 | queryVariables: React.PropTypes.object, 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Response.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CollapsableObject from './CollapsableObject'; 3 | 4 | export default function Response({ 5 | response, 6 | }) { 7 | return ( 8 |
9 |

Response

10 |
11 | 16 |
17 |
18 | ); 19 | } 20 | 21 | Response.propTypes = { 22 | response: React.PropTypes.string.isRequired, 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Computed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Collapsable from './Collapsable'; 3 | 4 | export default function Computed({ 5 | request, 6 | fragments, 7 | }) { 8 | const { operations } = request; 9 | return ( 10 |
11 | {operations.map(x => ( 12 |
13 | 20 |
21 | ))} 22 |
23 | ); 24 | } 25 | 26 | Computed.propTypes = { 27 | request: React.PropTypes.object.isRequired, 28 | fragments: React.PropTypes.array.isRequired, 29 | }; 30 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var file_system = require('fs'); 2 | var archiver = require('archiver'); 3 | 4 | var fileName = './package/graphql-network.zip' 5 | 6 | var output = file_system.createWriteStream(fileName); 7 | var archive = archiver('zip'); 8 | 9 | output.on('close', function () { 10 | console.log(archive.pointer() + ' total bytes'); 11 | console.log(`${fileName} was created successfully`); 12 | }); 13 | 14 | archive.on('error', function(err){ 15 | throw err; 16 | }); 17 | 18 | archive.pipe(output); 19 | 20 | archive.file('manifest.json'); 21 | archive.file('devtools.html'); 22 | archive.file('panel.html'); 23 | archive.directory('build'); 24 | archive.directory('icons'); 25 | 26 | // archive.bulk([ 27 | // { expand: true, cwd: 'source', src: ['**'], dest: 'source'} 28 | // ]); 29 | archive.finalize(); -------------------------------------------------------------------------------- /src/panel.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import DevToolsPanel from './components/DevToolsPanel'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | let alreadyShown = false; 7 | 8 | function createPanel() { 9 | const theme = chrome.devtools.panels.themeName || 'default'; 10 | chrome.devtools.panels.create('GraphQL Network', 11 | './icons/icon48.png', 12 | './panel.html', 13 | (panel) => { 14 | panel.onShown.addListener((panelWindow) => { 15 | if (!alreadyShown) { 16 | ReactDOM.render( 17 | , 22 | panelWindow.document.getElementById('results'), 23 | ); 24 | } 25 | alreadyShown = true; 26 | }); 27 | }, 28 | ); 29 | } 30 | 31 | createPanel(); 32 | -------------------------------------------------------------------------------- /src/components/Entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Request from './Request'; 3 | 4 | export default function Entry({ 5 | entry, 6 | onClick, 7 | isSelected, 8 | }) { 9 | return ( 10 |
11 |
12 | {entry.url} {entry.response ? entry.response.status : 'Error'} 13 |
14 | {entry.data && entry.data.map((request, i) => { 15 | if (request.kind !== 'FragmentDefinition') { 16 | return ; 17 | } 18 | })} 19 | {!entry.data && ( 20 |

{entry}

21 | )} 22 |
23 | ); 24 | } 25 | 26 | Entry.propTypes = { 27 | entry: React.PropTypes.object.isRequired, 28 | onClick: React.PropTypes.func.isRequired, 29 | isSelected: React.PropTypes.bool.isRequired, 30 | }; 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | const sourceReg = /src\//i; 4 | function matchSources(sourcePath) { 5 | return sourceReg.test(path.relative(__dirname, sourcePath)); 6 | } 7 | 8 | const babelLoader = { 9 | test: /\.js$/, 10 | loader: 'babel', 11 | //include: matchSources, 12 | exclude: '/node_modules/', 13 | query: { 14 | presets: ['react', 'es2015', 'stage-0'], 15 | }, 16 | }; 17 | 18 | module.exports = { 19 | entry: { 20 | panel: './src/panel.js', 21 | }, 22 | 23 | postcss: [ 24 | require('postcss-import'), 25 | ], 26 | 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.(png|jpg|gif|eot|svg|ttf|woff|woff2)$/, 31 | loader: 'file?name=[name].[hash].[ext]', 32 | exclude: /node_modules/, 33 | }, 34 | babelLoader, 35 | { test: /\.css$/, loaders: ['style', 'css?module&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss'] }, 36 | ], 37 | }, 38 | 39 | output: { 40 | path: __dirname + '/build', 41 | filename: '[name].js', 42 | }, 43 | 44 | devtool: 'cheap-module-source-map', 45 | debug: true, 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Request.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const quickDisplayParams = (x) => { 4 | if (!x) return 'None'; 5 | return x.map(y => `${y.name} = ${y.value}`).join(', '); 6 | }; 7 | 8 | const quickDisplayFields = (x) => { 9 | if (!x) return 'None'; 10 | return x.map(y => `${y.name}`).join(', '); 11 | }; 12 | 13 | function Operation({ 14 | operation, 15 | }) { 16 | const { name, fields, params } = operation; 17 | 18 | return ( 19 |
20 |

{`${name}`}

21 |

{quickDisplayParams(params)}

22 |

{quickDisplayFields(fields)}

23 |
24 | ); 25 | } 26 | 27 | Operation.propTypes = { 28 | operation: React.PropTypes.object.isRequired, 29 | }; 30 | 31 | 32 | // TODO: Change this and filename to "Definition" 33 | export default function Request({ 34 | request, 35 | }) { 36 | const { name, operations } = request; 37 | return ( 38 |
39 | {`- ${name}`} 40 | {operations.map(x => ( 41 | 42 | ))} 43 |
44 | ); 45 | } 46 | 47 | Request.propTypes = { 48 | request: React.PropTypes.object.isRequired, 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/CollapsableArray.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Value from './Value'; 3 | 4 | export default class CollapsableArray extends React.Component { 5 | static propTypes = { 6 | arr: React.PropTypes.array.isRequired, 7 | } 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | isOpen: false, 13 | }; 14 | } 15 | 16 | toggleOpen = () => { 17 | this.setState({ 18 | isOpen: !this.state.isOpen, 19 | }); 20 | } 21 | 22 | render() { 23 | const { arr } = this.props; 24 | const { isOpen } = this.state; 25 | return ( 26 |
27 | {!isOpen && ( 28 |
[{arr.length > 0 ? '...' : ''}]
29 | )} 30 | {isOpen && ( 31 | 32 | [ 33 |
34 | {arr.map((x, i) => ( 35 | 36 | ))} 37 |
38 | ] 39 |
40 | )} 41 |
42 | ); 43 | } 44 | } 45 | 46 | CollapsableArray.propTypes = { 47 | arr: React.PropTypes.array.isRequired, 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-network", 3 | "version": "1.0.0", 4 | "description": "A chrome extension for devtools to better display graphql requests", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "package": "npm run build && node package.js" 10 | }, 11 | "keywords": [ 12 | "Graphql", 13 | "chrome" 14 | ], 15 | "author": "Ben Stephenson ", 16 | "license": "ISC", 17 | "dependencies": { 18 | "babel": "^6.5.2", 19 | "babel-eslint": "^5.0.0", 20 | "babel-loader": "^6.2.4", 21 | "babel-preset-es2015": "^6.6.0", 22 | "babel-preset-react": "^6.5.0", 23 | "babel-preset-stage-0": "^6.5.0", 24 | "bluebird": "^3.3.4", 25 | "classnames": "^2.2.3", 26 | "css-loader": "^0.23.1", 27 | "eslint": "^2.3.0", 28 | "eslint-config-airbnb": "^6.1.0", 29 | "eslint-config-lens": "^2.3.0", 30 | "eslint-plugin-import": "^1.0.3", 31 | "eslint-plugin-react": "^4.2.1", 32 | "express": "^4.13.4", 33 | "file-loader": "^0.8.5", 34 | "graphql": "^0.4.18", 35 | "gulp": "^3.9.1", 36 | "imports-loader": "^0.6.5", 37 | "lodash": "^4.17.13", 38 | "open": "0.0.5", 39 | "postcss-import": "^8.0.2", 40 | "postcss-loader": "^0.8.1", 41 | "react": "^0.14.7", 42 | "react-dom": "^0.14.7", 43 | "require-dir": "^0.3.0", 44 | "style-loader": "^0.13.0", 45 | "webpack": "^1.12.14" 46 | }, 47 | "devDependencies": { 48 | "archiver": "^2.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Value.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CollapsableArray from './CollapsableArray'; 3 | import isPlainObject from 'lodash/isPlainObject'; 4 | import CollapsableObject from './CollapsableObject'; 5 | 6 | // TODO: Check Object.isObject 7 | export default function Value({ 8 | value, 9 | field, 10 | kind, 11 | index, 12 | requestOpen, 13 | openChildren, 14 | }) { 15 | field = index ? `${field}-${index}` : field; 16 | 17 | if (Array.isArray(value)) { 18 | return ( 19 | 25 | ); 26 | } else if (isPlainObject(value)) { 27 | return ( 28 | requestOpen(field)} 31 | open={openChildren.indexOf(field) !== -1} 32 | /> 33 | ); 34 | } else { 35 | if (value && value.length > 200) { 36 | value = `${value.slice(0, 200)}...`; 37 | } 38 | 39 | if ( 40 | kind === 'ObjectValue' || 41 | kind === 'EnumValue' || 42 | kind === 'Variable' || 43 | typeof value === 'boolean' || 44 | typeof value === 'undefined' || 45 | typeof value === 'number' || 46 | value === null 47 | ) { 48 | value = `${value}`; 49 | } else { 50 | value = value.charAt && value.charAt(0) === '$' ? `${value}` : `"${value}"`; 51 | } 52 | return
{value}
; 53 | } 54 | } 55 | 56 | Value.propTypes = { 57 | value: React.PropTypes.any.isRequired, 58 | field: React.PropTypes.string, 59 | kind: React.PropTypes.string, 60 | index: React.PropTypes.number, 61 | requestOpen: React.PropTypes.func, 62 | openChildren: React.PropTypes.array, 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/LongInformation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Raw from './Raw'; 3 | import Computed from './Computed'; 4 | import Response from './Response'; 5 | 6 | export default class LongInformation extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | view: 'raw', 11 | }; 12 | } 13 | 14 | setView = (view) => this.setState({ view }); 15 | render() { 16 | const { view } = this.state; 17 | const { entry, onRequestClose } = this.props; 18 | 19 | return ( 20 |
21 |
22 |

x

23 |
this.setView('raw')}>Raw Query
24 |
this.setView('computed')}>Computed Query
25 |
this.setView('response')}>Response
26 |
27 |
28 | {view === 'raw' && ( 29 | 33 | )} 34 | {view === 'computed' && entry.data && entry.data.map((request, i) => { 35 | if (request.kind !== 'FragmentDefinition') { 36 | return ( 37 |
38 |

{request.name}

39 | 40 |
41 | ); 42 | } 43 | })} 44 | {view === 'computed' && !entry.data && ( 45 |

{entry}

46 | )} 47 | {view === 'response' && ( 48 | 51 | )} 52 |
53 |
54 | ); 55 | } 56 | } 57 | LongInformation.propTypes = { 58 | entry: React.PropTypes.object.isRequired, 59 | onRequestClose: React.PropTypes.func.isRequired, 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/CollapsableObject.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Value from './Value'; 3 | 4 | export default class CollapsableObject extends React.Component { 5 | static propTypes = { 6 | topLevel: React.PropTypes.bool, 7 | object: React.PropTypes.object.isRequired, 8 | open: React.PropTypes.bool, 9 | requestOpen: React.PropTypes.func, 10 | }; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | openChildren: [], 16 | }; 17 | } 18 | 19 | openIsRequested = (name) => { 20 | if (this.state.openChildren.indexOf(name) === -1) { 21 | this.setState({ 22 | openChildren: [ 23 | ...this.state.openChildren, 24 | name, 25 | ], 26 | }); 27 | } else { 28 | this.setState({ 29 | openChildren: this.state.openChildren.filter(x => x !== name), 30 | }); 31 | } 32 | }; 33 | 34 | render() { 35 | const { topLevel, object, open } = this.props; 36 | const { openChildren } = this.state; 37 | 38 | return ( 39 |
40 | {open && ( 41 |
42 | {'{'} 43 |
44 | {Object.keys(object).map((key, i) => { 45 | return ( 46 |
47 |
48 | {key} 49 |
50 |
51 | 57 |
58 |
59 | ); 60 | })} 61 |
62 | {'}'} 63 |
64 | )} 65 | {!open && ( 66 |
67 |
68 | {'{'} 69 | {Object.keys(object).join(', ')} 70 | {'}'} 71 |
72 |
73 | )} 74 |
75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Collapsable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Value from './Value'; 3 | import Computed from './Computed'; 4 | 5 | export default class Collapsable extends React.Component { 6 | 7 | static propTypes = { 8 | object: React.PropTypes.object.isRequired, 9 | topLevel: React.PropTypes.bool, 10 | opened: React.PropTypes.bool, 11 | requestOpen: React.PropTypes.func.isRequired, 12 | fragments: React.PropTypes.array.isRequired, 13 | closable: React.PropTypes.bool.isRequired, 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | openChildren: [], 20 | }; 21 | } 22 | 23 | openIsRequested = (name) => { 24 | if (this.state.openChildren.indexOf(name) === -1) { 25 | this.setState({ 26 | openChildren: [ 27 | ...this.state.openChildren, 28 | name, 29 | ], 30 | }); 31 | } else { 32 | this.setState({ 33 | openChildren: this.state.openChildren.filter(x => x !== name), 34 | }); 35 | } 36 | }; 37 | 38 | render() { 39 | const { object, opened, requestOpen, fragments, closable } = this.props; 40 | const { openChildren } = this.state; 41 | 42 | const fields = object.fields; 43 | return ( 44 |
45 |
requestOpen(object.name)}> 46 |

requestOpen(object.name)}> 47 | {object.kind !== 'FragmentSpread' && object.name} 48 | {object.kind === 'FragmentSpread' && ( 49 | x.name === object.name)[0]} 51 | fragments={fragments} 52 | /> 53 | )} 54 |

55 | {object.params && object.params.length > 0 && ( 56 |
57 | {object.params.map(param => ( 58 |
59 | {param.name} 60 | 61 |
62 | ))} 63 |
64 | )} 65 |
66 | {fields && opened && ( 67 |
68 | {fields.map(field => ( 69 | 76 | ))} 77 |
78 | )} 79 |
80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/DevToolsPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Entry from './Entry'; 3 | import LongInformation from './LongInformation'; 4 | 5 | import { 6 | isGraphQL, 7 | parseEntry, 8 | } from '../lib/utils'; 9 | 10 | export default class DevToolsPanel extends React.Component { 11 | static propTypes = { 12 | requestFinished: React.PropTypes.object.isRequired, 13 | getHAR: React.PropTypes.func.isRequired, 14 | theme: React.PropTypes.string, 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | data: [], 21 | entryOpen: false, 22 | openIndex: null, 23 | }; 24 | } 25 | 26 | parseLogToState = (log) => { 27 | if (!isGraphQL(log)) return null; 28 | return parseEntry(log) 29 | .then(data => { 30 | this.setState({ 31 | data: [...this.state.data, ...data], 32 | }); 33 | }); 34 | }; 35 | 36 | requestHandler = (request) => { 37 | this.parseLogToState(request); 38 | }; 39 | 40 | setEntry = (entry, i) => this.setState({ entryOpen: entry, openIndex: i }); 41 | onRequestClose = () => this.setState({ entryOpen: false, openIndex: null }); 42 | 43 | clearEntries = () => { 44 | this.setState({ 45 | data: [], 46 | entryOpen: false 47 | }); 48 | } 49 | 50 | componentDidMount() { 51 | this.props.requestFinished.addListener(this.requestHandler); 52 | } 53 | 54 | render() { 55 | const { theme = 'default' } = this.props; 56 | const { data, entryOpen } = this.state; 57 | return ( 58 |
59 |
60 |
61 |
62 | Operation Name 63 | Params 64 |
65 | Selection 66 | {data.length > 0 && 67 | 68 | } 69 |
70 |
71 |
72 | {data.map((entry, i) => { 73 | return ( 74 | this.setEntry(entry, i)} 77 | entry={entry} 78 | isSelected={entryOpen && entry.id === entryOpen.id} 79 | /> 80 | ); 81 | })} 82 | 83 |
84 |
85 | {entryOpen && ( 86 | 90 | )} 91 |
92 |
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-network 2 | 3 | Chrome Devtool that provides a "network"-style tab for GraphQL requests to allow developers to debug more easily. 4 | 5 | [Get it here](https://chrome.google.com/webstore/detail/igbmhmnkobkjalekgiehijefpkdemocm) 6 | 7 | ## Why? 8 | 9 | GraphQL is fantastic but if you're using GraphQL you've probably bumped into how horrible it is to monitor requests via the network tab: 10 | 11 | ![Which one do I click?](http://bwes.co/whichone.png) 12 | 13 | ![How do I read that?](http://bwes.co/errr.png) 14 | 15 | GraphQL network allows you to actually monitor and debug network requests again, just like the good old days. 16 | 17 | ## What does it do? 18 | 19 | * Gives you a concise list of all GraphQL requests that have been sent. Easy to track what you're requesting. 20 | * Gives you a raw view of the string of GraphQL being sent. 21 | * Gives you a computed view of the request as your server will interpret it. So it's easy to debug fragments. 22 | * Displays a nicely formatted response. 23 | 24 | ### Screenshots 25 | 26 | #### Looking through GraphQL requests. 27 | ![Easy to navigate list](http://bwes.co/easy-to-navigate.png) 28 | 29 | 30 | 31 | #### Viewing the Raw Query 32 | ![Post Body](http://bwes.co/post-body.png) 33 | 34 | 35 | 36 | #### Viewing the Computed Query 37 | ![Computed Fragments](http://bwes.co/compute-fragments.png) 38 | 39 | 40 | 41 | #### Viewing the Response 42 | ![Response Data](http://bwes.co/response-data.png) 43 | 44 | ## I want to give it a try but don't have a GraphQL app 45 | 46 | After installing the app, why not head over to [GraphQLHub](http://graphqlhub.com). 47 | 48 | ## Troubleshooting 49 | 50 | ### Not Picking Up Requests 51 | 52 | Because of the way Chrome Devtool extensions work, you'll need to have the GraphQL tab open at the time the request is made in order for it to be displayed, it won't pick up requests in the background. 53 | 54 | Additionally, the extension will only pick up requests that send the `Content-Type` header with: 55 | * `application/graphql` 56 | * `application/json` where the GraphQL query is in an object parameter called `query` 57 | * `application/x-www-form-urlencoded` where the GraphQL query is in a parameter called `query` 58 | 59 | Since GraphQL is fairly new, consensus hasn't exactly been reached on the best way to make queries, if you think another way should be supported, send a PR or open an issue. 60 | 61 | ### Request is being listed as a "GraphQL Error" 62 | 63 | It's likely that your GraphQL is invalid. If you've double checked this, open up an issue. 64 | 65 | ### Request is being listed as an "Internal Error" 66 | 67 | It's likely that there's a bug in the extension. Open an issue. 68 | 69 | 70 | ## Contributing 71 | 72 | Hacking on the extension is really easy. 73 | 74 | * Clone the repo 75 | * `npm install` 76 | * Make your changes 77 | * `webpack` in the top-level directory. 78 | * Load it into `chrome://extensions` in the normal way. 79 | 80 | ## Roadmap 81 | 82 | * Redo approach to CSS. Haven't yet had time to implement something proper. 83 | * Include variable digestion. 84 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql'; 2 | import Promise from 'bluebird'; 3 | 4 | function getValue(value) { 5 | if (value.kind === 'ListValue') { 6 | return value.values.map(x => getValue(x)); 7 | } else if (value.kind === 'Variable') { 8 | return `$${value.name.value}`; 9 | } else if (value.kind === 'ObjectValue') { 10 | const out = {}; 11 | value.fields.forEach(field => { 12 | out[field.name.value] = getValue(field.value); 13 | }); 14 | return JSON.stringify(out); 15 | } else { 16 | return value.value; 17 | } 18 | } 19 | 20 | function parseArguments(arr) { 21 | return arr.filter(x => x.name).map(x => ({ 22 | name: x.name.value, 23 | value: getValue(x.value), 24 | kind: x.value.kind, 25 | })); 26 | } 27 | 28 | function parseFields(arr) { 29 | return arr.selections.map(x => parseOperation(x)); 30 | } 31 | 32 | function getName(definition) { 33 | if (definition.kind === 'InlineFragment') { 34 | return `InlineFragment if ${definition.typeCondition.name.value}`; 35 | } else if (definition.alias && definition.name) { 36 | return `${definition.alias.value}: ${definition.name.value}`; 37 | } else if (definition.name) { 38 | return definition.name.value; 39 | } else { 40 | return 'Anonymous'; 41 | } 42 | } 43 | 44 | function parseOperation(definition) { 45 | return { 46 | kind: definition.kind, 47 | name: getName(definition), 48 | params: definition.arguments ? parseArguments(definition.arguments) : null, 49 | fields: definition.selectionSet ? parseFields(definition.selectionSet) : null, 50 | }; 51 | } 52 | 53 | function internalParse(requestData) { 54 | const { definitions } = requestData; 55 | return definitions.map(definition => { 56 | return { 57 | name: definition.name ? definition.name.value : (definition.operation || 'request'), 58 | kind: definition.kind, 59 | operations: definition.selectionSet.selections.map(operation => { 60 | return { 61 | ...parseOperation(operation), 62 | type: definition.operation || operation.kind, 63 | }; 64 | }), 65 | }; 66 | }); 67 | } 68 | 69 | function isContentType(entry, contentType) { 70 | return entry.request.headers.some(({ name, value }) => { 71 | return name.toLowerCase() === 'content-type' && value.split(';')[0].toLowerCase() === contentType.toLowerCase(); 72 | }); 73 | } 74 | 75 | function getQueryFromParams(params = []) { 76 | return decodeURIComponent(params.find(param => param.name === 'query').value); 77 | } 78 | 79 | export function isGraphQL(entry) { 80 | try { 81 | if (isContentType(entry, 'application/graphql')) { 82 | return true; 83 | } 84 | 85 | if (isContentType(entry, 'application/json')) { 86 | const json = JSON.parse(entry.request.postData.text); 87 | return json.query || json[0].query; 88 | } 89 | 90 | if (isContentType(entry, 'application/x-www-form-urlencoded') && getQueryFromParams(entry.request.postData.params)) { 91 | return true; 92 | } 93 | } catch (e) { 94 | return false; 95 | } 96 | } 97 | 98 | export function parseEntry(entry) { 99 | const parsedQueries = []; 100 | 101 | if (isContentType(entry, 'application/graphql')) { 102 | parsedQueries.push(parseQuery( 103 | entry.request.postData.text, 104 | entry.request.postData.variables 105 | )); 106 | } else if (isContentType(entry, 'application/x-www-form-urlencoded')) { 107 | parsedQueries.push(parseQuery(getQueryFromParams(entry.request.postData.params))); 108 | } else { 109 | let json; 110 | 111 | try { 112 | json = JSON.parse(entry.request.postData.text); 113 | } catch (e) { 114 | return Promise.resolve(`Internal Error Parsing: ${entry}. Message: ${e.message}. Stack: ${e.stack}`); 115 | } 116 | 117 | if (!Array.isArray(json)) { 118 | json = [json]; 119 | } 120 | 121 | for (let batchItem of json) { 122 | const { query } = batchItem; 123 | let { variables } = batchItem; 124 | 125 | try { 126 | variables = typeof variables === 'string' ? JSON.parse(variables) : variables; 127 | } catch (e) { 128 | return Promise.resolve(`Internal Error Parsing: ${entry}. Message: ${e.message}. Stack: ${e.stack}`); 129 | } 130 | 131 | parsedQueries.push(parseQuery(query, variables)); 132 | } 133 | } 134 | 135 | return new Promise(resolve => { 136 | entry.getContent(responseBody => { 137 | const parsedResponseBody = JSON.parse(responseBody); 138 | 139 | resolve(parsedQueries.map((parsedQuery, i) => { 140 | return { 141 | responseBody: Array.isArray(parsedResponseBody) ? parsedResponseBody[i] : parsedResponseBody, 142 | url: entry.request.url, 143 | response: entry.response, 144 | ...parsedQuery 145 | }; 146 | })); 147 | }); 148 | }); 149 | } 150 | 151 | export function parseQuery(query, variables={}) { 152 | let requestData, 153 | rawParse; 154 | 155 | try { 156 | rawParse = parse(query); 157 | } catch (e) { 158 | return Promise.resolve(`GraphQL Error Parsing: ${query}. Message ${e.message}. Stack: ${e.stack}`); 159 | } 160 | 161 | try { 162 | requestData = internalParse(rawParse); 163 | } catch (e) { 164 | return Promise.resolve(`Internal Error Parsing: ${query}. Message: ${e.message}. Stack: ${e.stack}`); 165 | } 166 | 167 | const fragments = requestData 168 | .filter(x => x.kind === 'FragmentDefinition'); 169 | 170 | return { 171 | queryVariables: variables, 172 | fragments, 173 | id: `${Date.now() + Math.random()}`, 174 | bareQuery: query, 175 | data: requestData, 176 | rawParse: JSON.stringify(rawParse), 177 | }; 178 | } 179 | -------------------------------------------------------------------------------- /panel.html: -------------------------------------------------------------------------------- 1 | 301 |
Waiting for GraphQL
302 | --------------------------------------------------------------------------------