From 43d3cd704fbf3ca7f4f6266409d6917a5ea4558f Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 12 Aug 2015 14:11:00 -0700 Subject: [PATCH 01/14] get things ready for plugins --- agent/Bridge.js | 2 +- frontend/Container.js | 1 + frontend/Panel.js | 14 ++++++++------ frontend/TabbedPane.js | 10 +++++++++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/agent/Bridge.js b/agent/Bridge.js index b80844402e..4d529eb237 100644 --- a/agent/Bridge.js +++ b/agent/Bridge.js @@ -153,7 +153,7 @@ class Bridge { }); } - call(name: string, args: any | Array, cb: (val: any) => any) { + call(name: string, args: Array, cb: (val: any) => any) { var _cid = this._cid++; this._cbs.set(_cid, cb); diff --git a/frontend/Container.js b/frontend/Container.js index 5aecc360c5..bf09266e75 100644 --- a/frontend/Container.js +++ b/frontend/Container.js @@ -34,6 +34,7 @@ class Container extends React.Component { store: Object ) => ?Array, }, + extraTabs: {[key: string]: () => ReactElement}, }; render(): ReactElement { diff --git a/frontend/Panel.js b/frontend/Panel.js index dfa1e13a7c..7e995cc572 100644 --- a/frontend/Panel.js +++ b/frontend/Panel.js @@ -15,6 +15,7 @@ var Container = require('./Container'); var Store = require('./Store'); var keyboardNav = require('./keyboardNav'); var invariant = require('./invariant'); +var assign = require('object-assign'); var Bridge = require('../agent/Bridge'); var NativeStyler = require('../plugins/ReactNativeStyle/ReactNativeStyle.js'); @@ -44,7 +45,7 @@ class Panel extends React.Component { _keyListener: ?(e: DOMEvent) => void; _checkTimeout: ?number; _unMounted: boolean; - _bridge: ?Bridge; + _bridge: Bridge; _store: Store; _unsub: ?() => void; @@ -55,6 +56,7 @@ class Panel extends React.Component { this.state = {loading: true, isReact: this.props.alreadyFoundReact}; this._unMounted = false; window.panel = this; + this.plugins = []; } getChildContext(): Object { @@ -129,6 +131,8 @@ class Panel extends React.Component { } teardown() { + this.plugins.forEach(p => p.teardown()); + this.plugins = []; if (this._keyListener) { window.removeEventListener('keydown', this._keyListener); this._keyListener = null; @@ -140,7 +144,6 @@ class Panel extends React.Component { this._teardownWall(); this._teardownWall = null; } - this._bridge = null; } inject() { @@ -150,10 +153,7 @@ class Panel extends React.Component { this._bridge = new Bridge(); this._bridge.attach(wall); - // xx FlowFixMe this._bridge is not null - if (this._bridge) { - this._store = new Store(this._bridge); - } + this._store = new Store(this._bridge); this._keyListener = keyboardNav(this._store, window); window.addEventListener('keydown', this._keyListener); @@ -201,6 +201,7 @@ class Panel extends React.Component { if (!this.state.isReact) { return

Looking for react...

; } + var extraTabs = assign.apply(null, [{}].concat(this.plugins.map(p => p.tabs()))); return ( ); } diff --git a/frontend/TabbedPane.js b/frontend/TabbedPane.js index 25335afcae..fdbc316cfe 100644 --- a/frontend/TabbedPane.js +++ b/frontend/TabbedPane.js @@ -35,7 +35,7 @@ class TabbedPane { style = assign({}, style, styles.lastTab); } return ( -
  • this.props.setSelectedTab(name)}> +
  • this.props.setSelectedTab(name)}> {name}
  • ); @@ -87,6 +87,14 @@ var styles = { module.exports = decorate({ listeners: () => ['selectedTab'], + shouldUpdate: (props, prevProps) => { + for (var name in props) { + if (props[name] !== prevProps[name]) { + return true; + } + } + return false; + }, props(store) { return { selected: store.selectedTab, From 65e1ba5216d47b642d54f3dc9713d51651dfb5e2 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 12 Aug 2015 14:11:20 -0700 Subject: [PATCH 02/14] skeleton relay plugin --- frontend/Container.js | 4 +--- frontend/Panel.js | 5 ++++ plugins/Relay/RelayPlugin.js | 46 ++++++++++++++++++++++++++++++++++++ plugins/Relay/backend.js | 4 ++++ shells/chrome/src/backend.js | 3 +++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 plugins/Relay/RelayPlugin.js create mode 100644 plugins/Relay/backend.js diff --git a/frontend/Container.js b/frontend/Container.js index bf09266e75..a8216ab1cd 100644 --- a/frontend/Container.js +++ b/frontend/Container.js @@ -50,9 +50,7 @@ class Container extends React.Component { }; return (
    - +
    ); diff --git a/frontend/Panel.js b/frontend/Panel.js index 7e995cc572..704b5d8ae0 100644 --- a/frontend/Panel.js +++ b/frontend/Panel.js @@ -19,6 +19,7 @@ var assign = require('object-assign'); var Bridge = require('../agent/Bridge'); var NativeStyler = require('../plugins/ReactNativeStyle/ReactNativeStyle.js'); +var RelayPlugin = require('../plugins/Relay/RelayPlugin'); var consts = require('../agent/consts'); @@ -160,6 +161,10 @@ class Panel extends React.Component { this._store.on('connected', () => { this.setState({loading: false}); + var refresh = () => this.forceUpdate(); + this.plugins = [ + new RelayPlugin(this._store, this._bridge, refresh), + ]; this.getNewSelection(); }); }); diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js new file mode 100644 index 0000000000..5435b855ce --- /dev/null +++ b/plugins/Relay/RelayPlugin.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +import type Bridge from '../../agent/Bridge'; +import type Store from '../../frontend/Store'; + +var React = require('react'); + +class RelayPlugin { + hasRelay: bool; + bridge: Bridge; + store: Store; + + constructor(store: Store, bridge: Bridge, refresh: () => void) { + this.bridge = bridge; + this.store = store; + this.hasRelay = false; + bridge.call('relay:check', [], hasRelay => { + this.hasRelay = hasRelay; + refresh(); + }); + } + + tabs(): ?{[key: string]: () => ReactElement} { + if (!this.hasRelay) { + return; + } + return { + Relay: () => { + return

    Relay is the win

    ; + }, + }; + } +} + +module.exports = RelayPlugin; + diff --git a/plugins/Relay/backend.js b/plugins/Relay/backend.js new file mode 100644 index 0000000000..8824526a05 --- /dev/null +++ b/plugins/Relay/backend.js @@ -0,0 +1,4 @@ + +module.exports = (bridge, agent, hook) => { + bridge.onCall('relay:check', () => !!hook._relayInternals); +}; diff --git a/shells/chrome/src/backend.js b/shells/chrome/src/backend.js index 5840c15cbe..1121e75373 100644 --- a/shells/chrome/src/backend.js +++ b/shells/chrome/src/backend.js @@ -14,6 +14,7 @@ var Agent = require('../../../agent/Agent'); var Bridge = require('../../../agent/Bridge'); var setupHighlighter = require('../../../frontend/Highlighter/setup'); var setupRNStyle = require('../../../plugins/ReactNativeStyle/setupBackend'); +var setupRelay = require('../../../plugins/Relay/backend'); var inject = require('../../../agent/inject'); @@ -71,6 +72,8 @@ function setup(hook) { setupRNStyle(bridge, agent, hook.resolveRNStyle); } + setupRelay(bridge, agent, hook); + agent.on('shutdown', () => { hook.emit('shutdown'); listeners.forEach(fn => { From e548bb1535a462653b70fd89755903a4b2fa1779 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 12 Aug 2015 16:06:24 -0700 Subject: [PATCH 03/14] no style, but relay queries + mutations are displayed! --- frontend/decorate.js | 24 ++++++------ frontend/provideStore.js | 36 ++++++++++++++++++ plugins/Relay/Query.js | 73 ++++++++++++++++++++++++++++++++++++ plugins/Relay/QueryList.js | 55 +++++++++++++++++++++++++++ plugins/Relay/RelayPlugin.js | 18 ++++++++- plugins/Relay/Store.js | 45 ++++++++++++++++++++++ plugins/Relay/backend.js | 70 +++++++++++++++++++++++++++++++++- 7 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 frontend/provideStore.js create mode 100644 plugins/Relay/Query.js create mode 100644 plugins/Relay/QueryList.js create mode 100644 plugins/Relay/Store.js diff --git a/frontend/decorate.js b/frontend/decorate.js index 5101471459..5f3a4dbf2a 100644 --- a/frontend/decorate.js +++ b/frontend/decorate.js @@ -51,6 +51,7 @@ type Options = { * }, MyComp); */ module.exports = function (options: Options, Component: any): any { + var storeKey = options.store || 'store'; class Wrapper extends React.Component { _listeners: Array; _update: () => void; @@ -61,25 +62,25 @@ module.exports = function (options: Options, Component: any): any { } componentWillMount() { - if (!this.context.store) { + if (!this.context[storeKey]) { return console.warn('no store on context...'); } this._update = () => this.forceUpdate(); if (!options.listeners) { return; } - this._listeners = options.listeners(this.props, this.context.store); + this._listeners = options.listeners(this.props, this.context[storeKey]); this._listeners.forEach(evt => { - this.context.store.on(evt, this._update); + this.context[storeKey].on(evt, this._update); }); } componentWillUnmount() { - if (!this.context.store) { + if (!this.context[storeKey]) { return console.warn('no store on context...'); } this._listeners.forEach(evt => { - this.context.store.off(evt, this._update); + this.context[storeKey].off(evt, this._update); }); } @@ -94,32 +95,33 @@ module.exports = function (options: Options, Component: any): any { } componentWillUpdate(nextProps, nextState) { - if (!this.context.store) { + if (!this.context[storeKey]) { return console.warn('no store on context...'); } if (!options.listeners) { return; } - var listeners = options.listeners(this.props, this.context.store); + var listeners = options.listeners(this.props, this.context[storeKey]); var diff = arrayDiff(listeners, this._listeners); diff.missing.forEach(name => { - this.context.store.off(name, this._update); + this.context[storeKey].off(name, this._update); }); diff.newItems.forEach(name => { - this.context.store.on(name, this._update); + this.context[storeKey].on(name, this._update); }); this._listeners = listeners; } render() { - var store = this.context.store; + var store = this.context[storeKey]; var props = store && options.props(store, this.props); return ; } } Wrapper.contextTypes = { - store: React.PropTypes.object, + // $FlowFixMe computed property + [storeKey]: React.PropTypes.object, }; Wrapper.displayName = 'Wrapper(' + Component.name + ')'; diff --git a/frontend/provideStore.js b/frontend/provideStore.js new file mode 100644 index 0000000000..fdacdaa2bc --- /dev/null +++ b/frontend/provideStore.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +var React = require('react'); + +module.exports = function (name: string): Object { + class Wrapper extends React.Component { + props: { + children: () => ReactElement, + store: Object, + }; + getChildContext() { + // $FlowFixMe computed property + return {[name]: this.props.store}; + } + render() { + return this.props.children(); + } + } + Wrapper.childContextTypes = { + // $FlowFixMe computed property + [name]: React.PropTypes.object, + }; + Wrapper.displayName = 'StoreProvider(' + name + ')'; + return Wrapper; +}; + diff --git a/plugins/Relay/Query.js b/plugins/Relay/Query.js new file mode 100644 index 0000000000..ccdddb31d2 --- /dev/null +++ b/plugins/Relay/Query.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var assign = require('object-assign'); + +class Query { + props: { + data: Map, + }; + render(): ReactElement { + var data = this.props.data; + var statusStyle = assign({}, styles.status, { + backgroundColor: statusColors[data.get('status')] || statusColors.error + }); + return ( +
  • +
    +
    + {data.get('name')} +
    +
    + {JSON.stringify(data.get('variables'), null, 2)} +
    +
    + {data.get('text')} +
    +
  • + ); + } +} + +var statusColors = { + pending: 'orange', + success: 'green', + failure: 'red', + error: '#aaa', +}; + +var styles = { + container: { + padding: '10px 20px', + display: 'flex', + }, + + name: { + }, + + status: { + width: 20, + height: 20, + margin: 10, + borderRadius: 25, + backgroundColor: '#aaa', + }, + + text: { + whiteSpace: 'pre', + fontFamily: 'monospace', + flex: 1, + }, +}; + +module.exports = Query; diff --git a/plugins/Relay/QueryList.js b/plugins/Relay/QueryList.js new file mode 100644 index 0000000000..1fd9215672 --- /dev/null +++ b/plugins/Relay/QueryList.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var decorate = require('../../frontend/decorate'); +var Query = require('./Query'); + +class QueryList { + props: Object; + + render() { + return ( +
      + {this.props.queries.map(q => + // $FlowFixMe react element + )} + {!this.props.queries.count() &&
    • No Relay Queries logged
    • } +
    + ); + } +} + +var styles = { + list: { + listStyle: 'none', + padding: 0, + margin: 0, + overflow: 'auto', + minHeight: 0, + }, + + empty: { + padding: 50, + textAlign: 'center', + }, +}; + +module.exports = decorate({ + store: 'relayStore', + listeners: () => ['queries'], + props(store, props) { + return { + queries: store.queries, + }; + }, +}, QueryList); diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js index 5435b855ce..5da409fc0d 100644 --- a/plugins/Relay/RelayPlugin.js +++ b/plugins/Relay/RelayPlugin.js @@ -14,16 +14,24 @@ import type Bridge from '../../agent/Bridge'; import type Store from '../../frontend/Store'; var React = require('react'); +var provideStore = require('../../frontend/provideStore'); + +var RelayStore = require('./Store'); +var QueryList = require('./QueryList'); + +var StoreWrapper = provideStore('relayStore'); class RelayPlugin { hasRelay: bool; bridge: Bridge; store: Store; + relayStore: RelayStore; constructor(store: Store, bridge: Bridge, refresh: () => void) { this.bridge = bridge; this.store = store; this.hasRelay = false; + this.relayStore = new RelayStore(bridge); bridge.call('relay:check', [], hasRelay => { this.hasRelay = hasRelay; refresh(); @@ -36,7 +44,15 @@ class RelayPlugin { } return { Relay: () => { - return

    Relay is the win

    ; + return ( + + {() => ( +
    + +
    + )} +
    + ); }, }; } diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js new file mode 100644 index 0000000000..dbe436011b --- /dev/null +++ b/plugins/Relay/Store.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +import type Bridge from '../../agent/Bridge'; + +var {EventEmitter} = require('events'); +var {Map} = require('immutable'); + +class Store extends EventEmitter { + queries: Map; + + constructor(bridge: Bridge) { + super(); + this.queries = new Map(); + bridge.on('relay:pending', data => { + this.queries = this.queries.set(data.id, new Map(data).set('status', 'pending')); + this.emit('queries'); + }); + bridge.on('relay:success', ({id, val}) => { + console.log('val', id, val); + this.queries = this.queries.setIn([id, 'status'], 'success'); + this.emit('queries'); + }); + bridge.on('relay:failure', ({id, err}) => { + console.log('err', id, err); + this.queries = this.queries.setIn([id, 'status'], 'failure'); + this.emit('queries'); + }); + } + + off(evt: string, fn: () => void) { + this.removeListener(evt, fn); + } +} + +module.exports = Store; diff --git a/plugins/Relay/backend.js b/plugins/Relay/backend.js index 8824526a05..e2d25dba26 100644 --- a/plugins/Relay/backend.js +++ b/plugins/Relay/backend.js @@ -1,4 +1,72 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; -module.exports = (bridge, agent, hook) => { +import type Bridge from '../../agent/Bridge'; +import type Agent from '../../agent/Agent'; + +function decorate(obj, attr, fn) { + var old = obj[attr]; + obj[attr] = function () { + var res = old.apply(this, arguments); + fn.apply(this, arguments); + return res; + }; + return () => (obj[attr] = old); +} + +function makeId() { + return Math.random().toString(16); +} + +module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { bridge.onCall('relay:check', () => !!hook._relayInternals); + if (!hook._relayInternals) { + return; + } + var NetworkLayer = hook._relayInternals.NetworkLayer; + var restore = [ + decorate(NetworkLayer, 'sendMutation', mut => { + var id = makeId(); + bridge.send('relay:pending', { + id, + type: 'mutation', + text: mut.getQueryString(), + variables: mut.getVariables(), + name: mut.getDebugName(), + }); + mut.then( + val => bridge.send('relay:success', {id, val}), + err => bridge.send('relay:failure', {id, err}) + ); + }), + + decorate(NetworkLayer, 'sendQueries', queries => { + bridge.send('relay:pending', queries.map(q => { + var id = makeId(); + q.then( + val => bridge.send('relay:success', {id, val}), + err => bridge.send('relay:failure', {id, err}) + ); + return { + id, + type: 'query', + text: q.getQueryString(), + variables: q.getVariables(), + name: q.getDebugName(), + } + })); + }) + ]; + hook.on('shutdown', () => { + restore.forEach(fn => fn()); + }); }; From 53a15d5815db32670890d0b377e9496e9fed2e1b Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 12 Aug 2015 22:49:22 -0700 Subject: [PATCH 04/14] generalize plain container to use in a relay example --- shells/plain/backend.js | 2 ++ shells/plain/container.js | 37 ++++++++++++++++++++++++++-------- shells/plain/global-hook.js | 15 ++++++++++++++ shells/plain/webpack.config.js | 1 + 4 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 shells/plain/global-hook.js diff --git a/shells/plain/backend.js b/shells/plain/backend.js index fd260f005b..d2a1a4fd72 100644 --- a/shells/plain/backend.js +++ b/shells/plain/backend.js @@ -13,6 +13,7 @@ var Agent = require('../../agent/Agent'); var Bridge = require('../../agent/Bridge'); var setupHighlighter = require('../../frontend/Highlighter/setup'); +var setupRelay = require('../../plugins/Relay/backend'); var inject = require('../../agent/inject'); @@ -33,3 +34,4 @@ agent.addBridge(bridge); inject(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, agent); setupHighlighter(agent); +setupRelay(bridge, agent, window.__REACT_DEVTOOLS_GLOBAL_HOOK__); diff --git a/shells/plain/container.js b/shells/plain/container.js index c38ef7cc63..f847735ec7 100644 --- a/shells/plain/container.js +++ b/shells/plain/container.js @@ -18,20 +18,22 @@ window.React = React; var Panel = require('../../frontend/Panel'); var target: Object = document.getElementById('target'); -function inject(src, done) { - var script = target.contentDocument.createElement('script'); - script.src = src; - script.onload = done; - target.contentDocument.body.appendChild(script); -} + +var appSrc = target.getAttribute('data-app-src') || '../../test/example/build/target.js'; +var devtoolsSrc = target.getAttribute('data-devtools-src') || './build/backend.js'; var win = target.contentWindow; globalHook(win); +var iframeSrc = document.getElementById('iframe-src'); +if (iframeSrc) { + win.document.documentElement.innerHTML = iframeSrc.textContent.replace(/>/g, '>'); +} + var config = { alreadyFoundReact: true, inject(done) { - inject('./build/backend.js', () => { + inject(devtoolsSrc, () => { var wall = { listen(fn) { win.parent.addEventListener('message', evt => fn(evt.data)); @@ -45,7 +47,26 @@ var config = { }, }; -inject('../../test/example/build/target.js', () => { +function inject(src, done) { + if (!src || src === 'false') { + return done(); + } + var script = target.contentDocument.createElement('script'); + script.src = src; + script.onload = done; + target.contentDocument.body.appendChild(script); +} + +function injectMany(sources, done) { + if (sources.length === 1) { + return inject(sources[0], done); + } + inject(sources[0], () => injectMany(sources.slice(1), done)) +} + +var sources = appSrc.split('|'); + +injectMany(sources, () => { var node = document.getElementById('container'); React.render(, node); }); diff --git a/shells/plain/global-hook.js b/shells/plain/global-hook.js new file mode 100644 index 0000000000..5a3b9d631c --- /dev/null +++ b/shells/plain/global-hook.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +var globalHook = require('../../backend/GlobalHook'); +globalHook(window); + diff --git a/shells/plain/webpack.config.js b/shells/plain/webpack.config.js index 59c059783c..9847433684 100644 --- a/shells/plain/webpack.config.js +++ b/shells/plain/webpack.config.js @@ -14,6 +14,7 @@ module.exports = { entry: { backend: './backend.js', container: './container.js', + globalHook: './global-hook.js', }, output: { path: __dirname + '/build', // eslint-disable-line no-path-concat From 6f65020a22fb5d81958149697b4ceecd5b356725 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 12 Aug 2015 22:50:11 -0700 Subject: [PATCH 05/14] flowtypes, small fixes --- agent/Bridge.js | 13 +++++++++++-- frontend/DataView/DataView.js | 11 ++++++++--- frontend/SplitPane.js | 15 +++++++++++++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/agent/Bridge.js b/agent/Bridge.js index 4d529eb237..2cd07e95c2 100644 --- a/agent/Bridge.js +++ b/agent/Bridge.js @@ -172,11 +172,20 @@ class Bridge { this._callers[name] = handler; } + setInspectable(id: string, data: Object) { + var prev = this._inspectables.get(id); + if (!prev) { + this._inspectables.set(id, data); + return; + } + this._inspectables.set(id, {...prev, ...data}); + } + sendOne(evt: string, data: any) { var cleaned = []; var san = dehydrate(data, cleaned); if (cleaned.length) { - this._inspectables.set(data.id, data); + this.setInspectable(data.id, data); } this._wall.send({type: 'event', evt, data: san, cleaned}); } @@ -203,7 +212,7 @@ class Bridge { var cleaned = []; var san = dehydrate(data, cleaned); if (cleaned.length) { - this._inspectables.set(data.id, data); + this.setInspectable(data.id, data); } return {type: 'event', evt, data: san, cleaned}; }); diff --git a/frontend/DataView/DataView.js b/frontend/DataView/DataView.js index 7f9faff120..bd1139fc64 100644 --- a/frontend/DataView/DataView.js +++ b/frontend/DataView/DataView.js @@ -10,6 +10,8 @@ */ 'use strict'; +import type {DOMEvent} from '../types'; + var React = require('react'); var Simple = require('./Simple'); @@ -20,8 +22,9 @@ class DataView { props: { data: Object, path: Array, - inspect: () => void, - showMenu: () => void, + inspect: (path: Array, cb: () => void) => void, + showMenu: (e: DOMEvent, val: any, path: Array, name: string) => void, + noSort?: boolean, readOnly: boolean, }; @@ -31,7 +34,9 @@ class DataView { return
    null
    ; } var names = Object.keys(data); - names.sort(); + if (!this.props.noSort) { + names.sort(); + } var path = this.props.path; if (!names.length) { return Empty object; diff --git a/frontend/SplitPane.js b/frontend/SplitPane.js index cbc94ddd87..2687de232d 100644 --- a/frontend/SplitPane.js +++ b/frontend/SplitPane.js @@ -14,8 +14,16 @@ var React = require('react'); var Draggable = require('./Draggable'); var assign = require('object-assign'); +type Props = { + style: ?{[key: string]: any}, + left: () => ReactElement, + right: () => ReactElement, + initialWidth: number, +}; + class SplitPane extends React.Component { - constructor(props: Object) { + props: Props; + constructor(props: Props) { super(props); this.state = { width: props.initialWidth, @@ -37,6 +45,10 @@ class SplitPane extends React.Component { var rightStyle = assign({}, styles.rightPane, { width: this.state.width }); + var containerStyle = styles.container; + if (this.props.style) { + containerStyle = assign(containerStyle, this.props.style); + } return
    {this.props.left()} @@ -58,7 +70,6 @@ class SplitPane extends React.Component { var styles = { container: { display: 'flex', - fontFamily: 'sans-serif', minWidth: 0, flex: 1, }, From a5e9ad8bf1aaef1251b885ea40cc9aeebe6e3d32 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Wed, 12 Aug 2015 22:51:18 -0700 Subject: [PATCH 06/14] Getting Relay query inspection going --- plugins/Relay/QueriesTab.js | 62 +++++++++++++++++++ plugins/Relay/Query.js | 38 ++++++++++-- plugins/Relay/QueryList.js | 27 ++++++-- plugins/Relay/QueryViewer.js | 115 +++++++++++++++++++++++++++++++++++ plugins/Relay/RelayPlugin.js | 12 ++-- plugins/Relay/Store.js | 45 +++++++++++--- plugins/Relay/backend.js | 13 ++-- 7 files changed, 281 insertions(+), 31 deletions(-) create mode 100644 plugins/Relay/QueriesTab.js create mode 100644 plugins/Relay/QueryViewer.js diff --git a/plugins/Relay/QueriesTab.js b/plugins/Relay/QueriesTab.js new file mode 100644 index 0000000000..869d03d6f5 --- /dev/null +++ b/plugins/Relay/QueriesTab.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var decorate = require('../../frontend/decorate'); +var QueryList = require('./QueryList'); +var QueryViewer = require('./QueryViewer'); +var SplitPane = require('../../frontend/SplitPane'); + +class QueriesTab { + props: { + isSplit: boolean, + }; + render() { + var contents; + if (!this.props.isSplit) { + contents = ; + } else { + contents = ( + } + right={() => } + /> + ); + } + + return ( +
    + {contents} +
    + ); + } +} + +var styles = { + container: { + fontFamily: 'Menlo, sans-serif', + fontSize: 12, + flex: 1, + display: 'flex', + }, +}; + +module.exports = decorate({ + store: 'relayStore', + listeners: () => ['selectedQuery'], + props(store) { + return { + isSplit: !!store.selectedQuery, + }; + }, +}, QueriesTab); diff --git a/plugins/Relay/Query.js b/plugins/Relay/Query.js index ccdddb31d2..c000b90e84 100644 --- a/plugins/Relay/Query.js +++ b/plugins/Relay/Query.js @@ -10,29 +10,40 @@ */ 'use strict'; +import type {Map} from 'immutable'; + var React = require('react'); var assign = require('object-assign'); class Query { props: { data: Map, + onSelect: () => void, }; render(): ReactElement { var data = this.props.data; + var containerStyle = styles.container; + if (this.props.isSelected) { + containerStyle = { + ...styles.container, + ...styles.selectedContainer, + }; + } var statusStyle = assign({}, styles.status, { backgroundColor: statusColors[data.get('status')] || statusColors.error }); + return ( -
  • +
  • {data.get('name')}
    -
    - {JSON.stringify(data.get('variables'), null, 2)} +
    + {new Date(data.get('start')).toLocaleTimeString()}
    -
    - {data.get('text')} +
    + {data.get('end') - data.get('start')}ms
  • ); @@ -49,10 +60,27 @@ var statusColors = { var styles = { container: { padding: '10px 20px', + cursor: 'pointer', display: 'flex', + fontSize: 14, + }, + + selectedContainer: { + backgroundColor: '#eef', }, name: { + flex: 1, + fontSize: 16, + padding: 10, + }, + + time: { + padding: 10, + }, + + duration: { + padding: 10, }, status: { diff --git a/plugins/Relay/QueryList.js b/plugins/Relay/QueryList.js index 1fd9215672..45a6407107 100644 --- a/plugins/Relay/QueryList.js +++ b/plugins/Relay/QueryList.js @@ -10,20 +10,34 @@ */ 'use strict'; +import type {OrderedMap} from 'immutable'; + var React = require('react'); var decorate = require('../../frontend/decorate'); var Query = require('./Query'); +var QueryViewer = require('./QueryViewer'); class QueryList { - props: Object; + props: { + queries: OrderedMap, + selectQuery: (id: string) => void, + selectedQuery: ?string, + }; render() { return (
      - {this.props.queries.map(q => + {this.props.queries.valueSeq().map(q => ( // $FlowFixMe react element - )} - {!this.props.queries.count() &&
    • No Relay Queries logged
    • } + this.props.selectQuery(q.get('id'))} + /> + )).toArray()} + {!this.props.queries.count() && +
    • No Relay Queries logged
    • }
    ); } @@ -36,6 +50,7 @@ var styles = { margin: 0, overflow: 'auto', minHeight: 0, + flex: 1, }, empty: { @@ -46,10 +61,12 @@ var styles = { module.exports = decorate({ store: 'relayStore', - listeners: () => ['queries'], + listeners: () => ['queries', 'selectedQuery'], props(store, props) { return { queries: store.queries, + selectQuery: id => store.selectQuery(id), + selectedQuery: store.selectedQuery, }; }, }, QueryList); diff --git a/plugins/Relay/QueryViewer.js b/plugins/Relay/QueryViewer.js new file mode 100644 index 0000000000..892246fd8e --- /dev/null +++ b/plugins/Relay/QueryViewer.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +import type {Map} from 'immutable'; + +var React = require('react'); +var assign = require('object-assign'); +var decorate = require('../../frontend/decorate'); +var DataView = require('../../frontend/DataView/DataView'); + +class QueryViewer { + props: { + data: Map, + }; + render(): ReactElement { + var data = this.props.data; + var info = { + }; + var type = data.get('type'); + if (type === 'mutation') { + info.mutation = data.get('mutation'); + } else { + info.query = data.get('query'); + } + info.variables = data.get('variables'); + var status = data.get('status'); + if (status === 'success') { + info.response = data.get('response'); + } else if (status === 'failure') { + info.error = data.get('error'); + } + return ( +
    +
    {data.get('name')}
    +
    + +
    +
    + ); + } +} + +var statusColors = { + pending: 'orange', + success: 'green', + failure: 'red', + error: '#aaa', +}; + +var styles = { + container: { + padding: '10px 20px', + display: 'flex', + flexDirection: 'column', + overflow: 'auto', + minHeight: 0, + flex: 1, + }, + + title: { + fontSize: 20, + color: '#666', + marginBottom: 15, + }, + + name: { + }, + + status: { + width: 20, + height: 20, + margin: 10, + borderRadius: 25, + backgroundColor: '#aaa', + }, + + variables: { + whiteSpace: 'pre', + fontFamily: 'monospace', + wordWrap: 'break-word', + }, + + text: { + whiteSpace: 'pre', + fontFamily: 'monospace', + wordWrap: 'break-word', + }, +}; + +module.exports = decorate({ + store: 'relayStore', + listeners: (props, store) => ['selectedQuery', store.selectedQuery], + props(store) { + return { + data: store.queries.get(store.selectedQuery), + inspect: store.inspect.bind(store, store.selectedQuery), + }; + }, +}, QueryViewer); diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js index 5da409fc0d..13b28208d9 100644 --- a/plugins/Relay/RelayPlugin.js +++ b/plugins/Relay/RelayPlugin.js @@ -17,7 +17,7 @@ var React = require('react'); var provideStore = require('../../frontend/provideStore'); var RelayStore = require('./Store'); -var QueryList = require('./QueryList'); +var QueriesTab = require('./QueriesTab'); var StoreWrapper = provideStore('relayStore'); @@ -38,6 +38,9 @@ class RelayPlugin { }); } + teardown() { + } + tabs(): ?{[key: string]: () => ReactElement} { if (!this.hasRelay) { return; @@ -46,11 +49,7 @@ class RelayPlugin { Relay: () => { return ( - {() => ( -
    - -
    - )} + {() => }
    ); }, @@ -59,4 +58,3 @@ class RelayPlugin { } module.exports = RelayPlugin; - diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js index dbe436011b..749219fc83 100644 --- a/plugins/Relay/Store.js +++ b/plugins/Relay/Store.js @@ -13,33 +13,58 @@ import type Bridge from '../../agent/Bridge'; var {EventEmitter} = require('events'); -var {Map} = require('immutable'); +var {OrderedMap, Map} = require('immutable'); +var assign = require('object-assign'); +var consts = require('../../agent/consts'); class Store extends EventEmitter { - queries: Map; + queries: OrderedMap; - constructor(bridge: Bridge) { + constructor(bridge: Bridge, mainStore: Object) { super(); - this.queries = new Map(); + this.selectedQuery = null; + this.queries = new OrderedMap(); + this._bridge = bridge; + this._mainStore = store; bridge.on('relay:pending', data => { this.queries = this.queries.set(data.id, new Map(data).set('status', 'pending')); this.emit('queries'); + this.emit(data.id); }); - bridge.on('relay:success', ({id, val}) => { - console.log('val', id, val); - this.queries = this.queries.setIn([id, 'status'], 'success'); + bridge.on('relay:success', ({id, response, end}) => { + console.log('response', id, response); + this.queries = this.queries.mergeIn([id], new Map({status: 'success', response, end})); this.emit('queries'); + this.emit(id); }); - bridge.on('relay:failure', ({id, err}) => { - console.log('err', id, err); - this.queries = this.queries.setIn([id, 'status'], 'failure'); + bridge.on('relay:failure', ({id, error, end}) => { + console.log('error', id, error); + this.queries = this.queries.mergeIn([id], new Map({status: 'failure', error, end})); this.emit('queries'); + this.emit(id); + }); + } + + inspect(id: string, path: Array, cb: () => void) { + this._bridge.inspect(id, path, value => { + var base = this.queries.get(id).get(path[0]); + var inspected = path.slice(1).reduce((obj, attr) => obj ? obj[attr] : null, base); + if (inspected) { + assign(inspected, value); + inspected[consts.inspected] = true; + } + cb(); }); } off(evt: string, fn: () => void) { this.removeListener(evt, fn); } + + selectQuery(id: string) { + this.selectedQuery = id; + this.emit('selectedQuery'); + } } module.exports = Store; diff --git a/plugins/Relay/backend.js b/plugins/Relay/backend.js index e2d25dba26..b9c26aee91 100644 --- a/plugins/Relay/backend.js +++ b/plugins/Relay/backend.js @@ -36,16 +36,19 @@ module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { var restore = [ decorate(NetworkLayer, 'sendMutation', mut => { var id = makeId(); + var start = Date.now(); bridge.send('relay:pending', { id, type: 'mutation', + start, + mutation: mut._mutation, text: mut.getQueryString(), variables: mut.getVariables(), name: mut.getDebugName(), }); mut.then( - val => bridge.send('relay:success', {id, val}), - err => bridge.send('relay:failure', {id, err}) + response => bridge.send('relay:success', {id, response: response.response, end: Date.now()}), + error => bridge.send('relay:failure', {id, error, end: Date.now()}) ); }), @@ -53,12 +56,14 @@ module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { bridge.send('relay:pending', queries.map(q => { var id = makeId(); q.then( - val => bridge.send('relay:success', {id, val}), - err => bridge.send('relay:failure', {id, err}) + response => bridge.send('relay:success', {id, response: response.response, end: Date.now()}), + error => bridge.send('relay:failure', {id, error, end: Date.now()}) ); return { id, type: 'query', + start: Date.now(), + query: q._query, text: q.getQueryString(), variables: q.getVariables(), name: q.getDebugName(), From ebf80af0fe73ee70ac2d373ff6f40dfcc0c82711 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Thu, 13 Aug 2015 08:24:13 -0700 Subject: [PATCH 07/14] initialize plugins earlier --- frontend/Panel.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/Panel.js b/frontend/Panel.js index 704b5d8ae0..1b2680e5ad 100644 --- a/frontend/Panel.js +++ b/frontend/Panel.js @@ -159,12 +159,13 @@ class Panel extends React.Component { window.addEventListener('keydown', this._keyListener); + var refresh = () => this.forceUpdate(); + this.plugins = [ + new RelayPlugin(this._store, this._bridge, refresh), + ]; + this._store.on('connected', () => { this.setState({loading: false}); - var refresh = () => this.forceUpdate(); - this.plugins = [ - new RelayPlugin(this._store, this._bridge, refresh), - ]; this.getNewSelection(); }); }); From 3ed3315dde90de155a2299bc885f4a45713740e3 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Fri, 14 Aug 2015 14:32:04 -0700 Subject: [PATCH 08/14] add basic relay pane --- frontend/DataView/DataView.js | 19 ++++++- frontend/DataView/Simple.js | 2 +- frontend/Panel.js | 14 +++-- plugins/Relay/ElementPanel.js | 75 +++++++++++++++++++++++++++ plugins/Relay/RelayPlugin.js | 29 ++++++++--- plugins/Relay/Store.js | 98 ++++++++++++++++++++++++++++++++++- plugins/Relay/StoreTab.js | 73 ++++++++++++++++++++++++++ plugins/Relay/backend.js | 2 + 8 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 plugins/Relay/ElementPanel.js create mode 100644 plugins/Relay/StoreTab.js diff --git a/frontend/DataView/DataView.js b/frontend/DataView/DataView.js index bd1139fc64..8626a05b52 100644 --- a/frontend/DataView/DataView.js +++ b/frontend/DataView/DataView.js @@ -15,6 +15,7 @@ import type {DOMEvent} from '../types'; var React = require('react'); var Simple = require('./Simple'); +var assign = require('object-assign'); var consts = require('../../agent/consts'); var previewComplex = require('./previewComplex'); @@ -24,6 +25,7 @@ class DataView { path: Array, inspect: (path: Array, cb: () => void) => void, showMenu: (e: DOMEvent, val: any, path: Array, name: string) => void, + startOpen: boolean, noSort?: boolean, readOnly: boolean, }; @@ -49,6 +51,7 @@ class DataView { name={'__proto__'} path={path.concat(['__proto__'])} key={'__proto__'} + startOpen={this.props.startOpen} inspect={this.props.inspect} showMenu={this.props.showMenu} readOnly={this.props.readOnly} @@ -60,6 +63,7 @@ class DataView { name={name} path={path.concat([name])} key={name} + startOpen={this.props.startOpen} inspect={this.props.inspect} showMenu={this.props.showMenu} readOnly={this.props.readOnly} @@ -74,7 +78,13 @@ class DataView { class DataItem extends React.Component { constructor(props) { super(props); - this.state = {open: false, loading: false}; + this.state = {open: this.props.startOpen, loading: false}; + } + + componentDidMount() { + if (this.state.open && this.props.value && this.props.value[consts.inspected] === false) { + this.inspect(); + } } componentWillReceiveProps(nextProps) { @@ -163,7 +173,8 @@ class DataItem extends React.Component {
    {opener}
    {this.props.name}:
    @@ -224,6 +235,10 @@ var styles = { margin: '2px 3px', }, + complexName: { + cursor: 'pointer', + }, + preview: { display: 'flex', margin: '2px 3px', diff --git a/frontend/DataView/Simple.js b/frontend/DataView/Simple.js index a4c49fb461..943946d88a 100644 --- a/frontend/DataView/Simple.js +++ b/frontend/DataView/Simple.js @@ -141,7 +141,7 @@ Simple.propTypes = { }; Simple.contextTypes = { - onChange: React.PropTypes.func.isRequired, + onChange: React.PropTypes.func, }; var styles = { diff --git a/frontend/Panel.js b/frontend/Panel.js index 1b2680e5ad..c5d16c4b01 100644 --- a/frontend/Panel.js +++ b/frontend/Panel.js @@ -155,14 +155,14 @@ class Panel extends React.Component { this._bridge.attach(wall); this._store = new Store(this._bridge); - this._keyListener = keyboardNav(this._store, window); - - window.addEventListener('keydown', this._keyListener); - var refresh = () => this.forceUpdate(); this.plugins = [ new RelayPlugin(this._store, this._bridge, refresh), ]; + this._keyListener = keyboardNav(this._store, window); + + window.addEventListener('keydown', this._keyListener); + this._store.on('connected', () => { this.setState({loading: false}); @@ -208,6 +208,10 @@ class Panel extends React.Component { return

    Looking for react...

    ; } var extraTabs = assign.apply(null, [{}].concat(this.plugins.map(p => p.tabs()))); + var extraPanes = [].concat(...this.plugins.map(p => p.panes())); + if (this._store.capabilities.rnStyle) { + extraPanes.push(panelRNStyle(this._bridge)); + } return ( ); diff --git a/plugins/Relay/ElementPanel.js b/plugins/Relay/ElementPanel.js new file mode 100644 index 0000000000..b2a27a8316 --- /dev/null +++ b/plugins/Relay/ElementPanel.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var decorate = require('../../frontend/decorate'); + +class ElementPanel { + render() { + return ( +
    +
      + {this.props.dataIDs.map(({id, queries}) => ( +
    • + ID: {id}
      +
        + {queries.map(query => ( +
      • + Query: {query.get('name')} +
      • + ))} +
      +
    • + ))} +
    +
    + ); + } +} + +var styles = { + dataIDs: { + listStyle: 'none', + padding: 0, + margin: 0, + }, + queries: { + listStyle: 'none', + padding: 0, + margin: 0, + }, +}; + +module.exports = decorate({ + store: 'relayStore', + listeners(props, store) { + return [props.id]; + }, + shouldUpdate(props, prevProps) { + return props.id !== prevProps.id; + }, + props(store, props) { + var dataIDs = []; + if (store.nodesToDataIDs[props.id]) { + for (var id of store.nodesToDataIDs[props.id]) { + dataIDs.push({ + id, + queries: (store.queriesByDataID[id] || []).map(qid => store.queries.get(qid)), + }); + } + } + return { + dataIDs, + jumpTo: dataID => store.jumpToDataID(dataID), + }; + }, +}, ElementPanel); diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js index 13b28208d9..22ca5788cb 100644 --- a/plugins/Relay/RelayPlugin.js +++ b/plugins/Relay/RelayPlugin.js @@ -18,6 +18,8 @@ var provideStore = require('../../frontend/provideStore'); var RelayStore = require('./Store'); var QueriesTab = require('./QueriesTab'); +var StoreTab = require('./StoreTab'); +var ElementPanel = require('./ElementPanel'); var StoreWrapper = provideStore('relayStore'); @@ -38,6 +40,16 @@ class RelayPlugin { }); } + panes(): Array<(node: Object, id: string) => ReactElement> { + return [ + (node, id) => ( + + {() => } + + ), + ]; + } + teardown() { } @@ -46,13 +58,16 @@ class RelayPlugin { return; } return { - Relay: () => { - return ( - - {() => } - - ); - }, + Relay: () => ( + + {() => } + + ), + RelayStore: () => ( + + {() => } + + ) }; } } diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js index 749219fc83..aec49fdebe 100644 --- a/plugins/Relay/Store.js +++ b/plugins/Relay/Store.js @@ -17,19 +17,47 @@ var {OrderedMap, Map} = require('immutable'); var assign = require('object-assign'); var consts = require('../../agent/consts'); +function getDataIDs(obj, collector) { + for (var name in obj) { + if (name === 'id' && 'string' === typeof obj[name]) { + collector.push(obj[name]); + } else if (typeof obj[name] == 'object') { + getDataIDs(obj[name], collector); + } + } +} + class Store extends EventEmitter { queries: OrderedMap; + storeData: Object; constructor(bridge: Bridge, mainStore: Object) { super(); + this.storeData = null; this.selectedQuery = null; this.queries = new OrderedMap(); this._bridge = bridge; this._mainStore = store; + // initial population of the store + bridge.on('relay:store', data => { + this.storeData = data; + this.emit('storeData'); + }); + this.queriesByDataID = {}; + // queries and mutations bridge.on('relay:pending', data => { this.queries = this.queries.set(data.id, new Map(data).set('status', 'pending')); this.emit('queries'); this.emit(data.id); + var dataIDs = []; + getDataIDs(data.variables, dataIDs); + dataIDs.forEach(id => { + if (!this.queriesByDataID[id]) { + this.queriesByDataID[id] = [data.id]; + } else { + this.queriesByDataID[id].push(data.id); + } + }); }); bridge.on('relay:success', ({id, response, end}) => { console.log('response', id, response); @@ -43,11 +71,79 @@ class Store extends EventEmitter { this.emit('queries'); this.emit(id); }); + this.dataIDsToNodes = {}; + this.nodesToDataIDs = {}; + // track nodes + bridge.on('mount', data => { + if (!data.props || (!data.props.relay && data.name.indexOf('Relay(') !== 0)) { + return; // not a relay child + } + this.nodesToDataIDs[data.id] = new window.Set(); + for (var name in data.props) { + var id = data.props[name].__dataID__; + if (!id) { + continue; + } + if (!this.dataIDsToNodes[id]) { + this.dataIDsToNodes[id] = new window.Set(); + } + this.dataIDsToNodes[id].add(data.id); + this.nodesToDataIDs[data.id].add(id); + } + }); + bridge.on('update', data => { + if (!data.props || !this.nodesToDataIDs[data.id]) { + return; + } + var newIds = new window.Set(); + for (var name in data.props) { + var id = data.props[name].__dataID__; + if (!id) { + continue; + } + newIds.add(id); + if (this.nodesToDataIDs[data.id].has(id)) { + continue; + } + if (!this.dataIDsToNodes[id]) { + this.dataIDsToNodes[id] = new window.Set(); + } + this.dataIDsToNodes[id].add(data.id); + // this.nodesToDataIDs[data.id].add(id); + } + + for (var item of this.nodesToDataIDs[data.id]) { + if (!newIds.has(item)) { + this.dataIDsToNodes[item].delete(data.id); + } + } + this.nodesToDataIDs[id] = newIds; + }); + bridge.on('unmount', id => { + if (!this.nodesToDataIDs[id]) { + return; + } + for (var item of this.nodesToDataIDs[id]) { + this.dataIDsToNodes[item].delete(id); + } + this.nodesToDataIDs[id] = null; + }); + } + + jumpToDataID(dataID) { + this._mainStore.setSelectedTab('RelayStore'); + this.selectedDataNode = dataID; + this.emit('selectedDataNode'); } inspect(id: string, path: Array, cb: () => void) { this._bridge.inspect(id, path, value => { - var base = this.queries.get(id).get(path[0]); + var base; + if (id === 'relay:store') { + base = this.storeData.nodes; + } else { + base = this.queries.get(id).get(path[0]); + } var inspected = path.slice(1).reduce((obj, attr) => obj ? obj[attr] : null, base); if (inspected) { assign(inspected, value); diff --git a/plugins/Relay/StoreTab.js b/plugins/Relay/StoreTab.js new file mode 100644 index 0000000000..c831271c18 --- /dev/null +++ b/plugins/Relay/StoreTab.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +import type {Map} from 'immutable'; + +var React = require('react'); +var assign = require('object-assign'); +var DataView = require('../../frontend/DataView/DataView'); +var decorate = require('../../frontend/decorate'); + +class StoreTab { + props: { + data: Map, + }; + render(): ReactElement { + if (!this.props.storeData) { + return ( +
    +

    Loading...

    +
    + ); + } + return ( +
    +

    Default Store

    + +
    + ); + } +} + +var styles = { + container: { + fontFamily: 'Menlo, sans-serif', + minHeight: 0, + flex: 1, + overflow: 'auto', + fontSize: 12, + padding: 30, + }, + loading: { + textAlign: 'center', + color: '#aaa', + } +}; + +module.exports = decorate({ + store: 'relayStore', + listeners: () => ['storeData'], + props(store) { + return { + storeData: store.storeData, + inspect: store.inspect.bind(store, 'relay:store'), + }; + }, +}, StoreTab); diff --git a/plugins/Relay/backend.js b/plugins/Relay/backend.js index b9c26aee91..d3306cdaa5 100644 --- a/plugins/Relay/backend.js +++ b/plugins/Relay/backend.js @@ -33,6 +33,8 @@ module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { return; } var NetworkLayer = hook._relayInternals.NetworkLayer; + + bridge.send('relay:store', {id: 'relay:store', nodes: hook._relayInternals.DefaultStoreData.getNodeData()}); var restore = [ decorate(NetworkLayer, 'sendMutation', mut => { var id = makeId(); From f48aaf9663eb599b3b56ca5e0c36494937c9de75 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Fri, 14 Aug 2015 15:09:57 -0700 Subject: [PATCH 09/14] some styling and jump to the query --- plugins/Relay/ElementPanel.js | 35 ++++++++++++++++++++++++++++++----- plugins/Relay/QueryViewer.js | 14 ++++++++++++++ plugins/Relay/RelayPlugin.js | 3 +++ plugins/Relay/Store.js | 7 +++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/plugins/Relay/ElementPanel.js b/plugins/Relay/ElementPanel.js index b2a27a8316..f341fd8d07 100644 --- a/plugins/Relay/ElementPanel.js +++ b/plugins/Relay/ElementPanel.js @@ -15,18 +15,25 @@ var decorate = require('../../frontend/decorate'); class ElementPanel { render() { + if (!this.props.dataIDs.length) { + return ; + } return (
    + Relay Nodes
      {this.props.dataIDs.map(({id, queries}) => ( -
    • - ID: {id}
      +
    • +
      this.props.jumpToData(id)}> + ID: {id} +
        {queries.map(query => ( -
      • - Query: {query.get('name')} +
      • this.props.jumpToQuery(query.get('id'))}> + {query.get('name')}
      • ))} + {!queries.length &&
      • No Queries
      • }
    • ))} @@ -37,6 +44,10 @@ class ElementPanel { } var styles = { + dataNode: { + marginBottom: 5, + border: '1px solid #ccc', + }, dataIDs: { listStyle: 'none', padding: 0, @@ -47,6 +58,19 @@ var styles = { padding: 0, margin: 0, }, + dataID: { + cursor: 'pointer', + padding: '2px 4px', + backgroundColor: '#ccc', + }, + queryID: { + cursor: 'pointer', + padding: '2px 4px', + }, + noQueries: { + color: '#999', + padding: '2px 4px', + }, }; module.exports = decorate({ @@ -69,7 +93,8 @@ module.exports = decorate({ } return { dataIDs, - jumpTo: dataID => store.jumpToDataID(dataID), + jumpToData: dataID => store.jumpToDataID(dataID), + jumpToQuery: queryID => store.jumpToQuery(queryID), }; }, }, ElementPanel); diff --git a/plugins/Relay/QueryViewer.js b/plugins/Relay/QueryViewer.js index 892246fd8e..2abee85663 100644 --- a/plugins/Relay/QueryViewer.js +++ b/plugins/Relay/QueryViewer.js @@ -41,6 +41,12 @@ class QueryViewer { return (
      {data.get('name')}
      +
      + {new Date(data.get('start')).toLocaleTimeString()} +
      +
      + {data.get('end') - data.get('start')}ms +
      ReactElement> { + if (!this.hasRelay) { + return []; + } return [ (node, id) => ( diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js index aec49fdebe..61acb5a37f 100644 --- a/plugins/Relay/Store.js +++ b/plugins/Relay/Store.js @@ -136,6 +136,13 @@ class Store extends EventEmitter { this.emit('selectedDataNode'); } + jumpToQuery(queryID) { + this._mainStore.setSelectedTab('Relay'); + this.selectedQuery = queryID; + this.emit('selectedQuery'); + this.emit('queries'); + } + inspect(id: string, path: Array, cb: () => void) { this._bridge.inspect(id, path, value => { var base; From 4da0d52880995bc3f18350f9897b35afab76ae24 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 8 Sep 2015 15:18:40 -0700 Subject: [PATCH 10/14] flow fixes --- plugins/Relay/ElementPanel.js | 6 ++++++ plugins/Relay/QueryViewer.js | 19 +++++++++---------- plugins/Relay/RelayPlugin.js | 2 +- plugins/Relay/Store.js | 21 +++++++++++++++++---- plugins/Relay/StoreTab.js | 1 + 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/plugins/Relay/ElementPanel.js b/plugins/Relay/ElementPanel.js index f341fd8d07..76b5deb8a5 100644 --- a/plugins/Relay/ElementPanel.js +++ b/plugins/Relay/ElementPanel.js @@ -14,6 +14,12 @@ var React = require('react'); var decorate = require('../../frontend/decorate'); class ElementPanel { + props: { + dataIDs: Array<{id: string, queries: Array}>, + jumpToData: (id: string) => void, + jumpToQuery: (queryID: string) => void, + }; + render() { if (!this.props.dataIDs.length) { return ; diff --git a/plugins/Relay/QueryViewer.js b/plugins/Relay/QueryViewer.js index 2abee85663..bb5784da11 100644 --- a/plugins/Relay/QueryViewer.js +++ b/plugins/Relay/QueryViewer.js @@ -20,6 +20,7 @@ var DataView = require('../../frontend/DataView/DataView'); class QueryViewer { props: { data: Map, + inspect: (path: Array, cb: () => void) => void, }; render(): ReactElement { var data = this.props.data; @@ -47,16 +48,14 @@ class QueryViewer {
      {data.get('end') - data.get('start')}ms
      -
      - -
      +
      ); } diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js index 58c17de1a1..a355cffc0f 100644 --- a/plugins/Relay/RelayPlugin.js +++ b/plugins/Relay/RelayPlugin.js @@ -33,7 +33,7 @@ class RelayPlugin { this.bridge = bridge; this.store = store; this.hasRelay = false; - this.relayStore = new RelayStore(bridge); + this.relayStore = new RelayStore(bridge, store); bridge.call('relay:check', [], hasRelay => { this.hasRelay = hasRelay; refresh(); diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js index 61acb5a37f..2f5a8e5497 100644 --- a/plugins/Relay/Store.js +++ b/plugins/Relay/Store.js @@ -29,7 +29,16 @@ function getDataIDs(obj, collector) { class Store extends EventEmitter { queries: OrderedMap; - storeData: Object; + storeData: ?{ + nodes: any, + }; + dataIDsToNodes: Map; + selectedDataNode: string; + nodesToDataIDs: Map; + _bridge: Bridge; + _mainStore: Object; + queriesByDataID: {[id: string]: Array}; + selectedQuery: ?string; constructor(bridge: Bridge, mainStore: Object) { super(); @@ -37,7 +46,7 @@ class Store extends EventEmitter { this.selectedQuery = null; this.queries = new OrderedMap(); this._bridge = bridge; - this._mainStore = store; + this._mainStore = mainStore; // initial population of the store bridge.on('relay:store', data => { this.storeData = data; @@ -130,13 +139,13 @@ class Store extends EventEmitter { }); } - jumpToDataID(dataID) { + jumpToDataID(dataID: string) { this._mainStore.setSelectedTab('RelayStore'); this.selectedDataNode = dataID; this.emit('selectedDataNode'); } - jumpToQuery(queryID) { + jumpToQuery(queryID: string) { this._mainStore.setSelectedTab('Relay'); this.selectedQuery = queryID; this.emit('selectedQuery'); @@ -147,6 +156,10 @@ class Store extends EventEmitter { this._bridge.inspect(id, path, value => { var base; if (id === 'relay:store') { + invariant( + this.storeData, + 'RelayStore.inspect: this.storeData should be defined.' + ); base = this.storeData.nodes; } else { base = this.queries.get(id).get(path[0]); diff --git a/plugins/Relay/StoreTab.js b/plugins/Relay/StoreTab.js index c831271c18..6254074bc8 100644 --- a/plugins/Relay/StoreTab.js +++ b/plugins/Relay/StoreTab.js @@ -20,6 +20,7 @@ var decorate = require('../../frontend/decorate'); class StoreTab { props: { data: Map, + inspect: (path: Array, cb: () => void) => void, }; render(): ReactElement { if (!this.props.storeData) { From 4d2923fc165c2c2bdfa589474855ea5a18e4374e Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 8 Sep 2015 16:14:04 -0700 Subject: [PATCH 11/14] lint fixes --- frontend/provideStore.js | 2 +- plugins/Relay/Query.js | 2 +- plugins/Relay/QueryList.js | 3 +-- plugins/Relay/QueryViewer.js | 8 -------- plugins/Relay/RelayPlugin.js | 4 ++-- plugins/Relay/Store.js | 5 +++-- plugins/Relay/StoreTab.js | 3 +-- plugins/Relay/backend.js | 6 +++--- shells/plain/container.js | 2 +- 9 files changed, 13 insertions(+), 22 deletions(-) diff --git a/frontend/provideStore.js b/frontend/provideStore.js index fdacdaa2bc..88bfbbe245 100644 --- a/frontend/provideStore.js +++ b/frontend/provideStore.js @@ -12,7 +12,7 @@ var React = require('react'); -module.exports = function (name: string): Object { +module.exports = function(name: string): Object { class Wrapper extends React.Component { props: { children: () => ReactElement, diff --git a/plugins/Relay/Query.js b/plugins/Relay/Query.js index c000b90e84..590a90cf38 100644 --- a/plugins/Relay/Query.js +++ b/plugins/Relay/Query.js @@ -30,7 +30,7 @@ class Query { }; } var statusStyle = assign({}, styles.status, { - backgroundColor: statusColors[data.get('status')] || statusColors.error + backgroundColor: statusColors[data.get('status')] || statusColors.error, }); return ( diff --git a/plugins/Relay/QueryList.js b/plugins/Relay/QueryList.js index 45a6407107..762c19796a 100644 --- a/plugins/Relay/QueryList.js +++ b/plugins/Relay/QueryList.js @@ -15,7 +15,6 @@ import type {OrderedMap} from 'immutable'; var React = require('react'); var decorate = require('../../frontend/decorate'); var Query = require('./Query'); -var QueryViewer = require('./QueryViewer'); class QueryList { props: { @@ -32,7 +31,7 @@ class QueryList { this.props.selectQuery(q.get('id'))} /> )).toArray()} diff --git a/plugins/Relay/QueryViewer.js b/plugins/Relay/QueryViewer.js index bb5784da11..6881a66212 100644 --- a/plugins/Relay/QueryViewer.js +++ b/plugins/Relay/QueryViewer.js @@ -13,7 +13,6 @@ import type {Map} from 'immutable'; var React = require('react'); -var assign = require('object-assign'); var decorate = require('../../frontend/decorate'); var DataView = require('../../frontend/DataView/DataView'); @@ -61,13 +60,6 @@ class QueryViewer { } } -var statusColors = { - pending: 'orange', - success: 'green', - failure: 'red', - error: '#aaa', -}; - var styles = { container: { padding: '10px 20px', diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js index a355cffc0f..6b0cf6ebd7 100644 --- a/plugins/Relay/RelayPlugin.js +++ b/plugins/Relay/RelayPlugin.js @@ -58,7 +58,7 @@ class RelayPlugin { tabs(): ?{[key: string]: () => ReactElement} { if (!this.hasRelay) { - return; + return null; } return { Relay: () => ( @@ -70,7 +70,7 @@ class RelayPlugin { {() => } - ) + ), }; } } diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js index 2f5a8e5497..a62f56eca9 100644 --- a/plugins/Relay/Store.js +++ b/plugins/Relay/Store.js @@ -16,12 +16,13 @@ var {EventEmitter} = require('events'); var {OrderedMap, Map} = require('immutable'); var assign = require('object-assign'); var consts = require('../../agent/consts'); +var invariant = require('../../frontend/invariant'); function getDataIDs(obj, collector) { for (var name in obj) { - if (name === 'id' && 'string' === typeof obj[name]) { + if (name === 'id' && typeof obj[name] === 'string') { collector.push(obj[name]); - } else if (typeof obj[name] == 'object') { + } else if (typeof obj[name] === 'object') { getDataIDs(obj[name], collector); } } diff --git a/plugins/Relay/StoreTab.js b/plugins/Relay/StoreTab.js index 6254074bc8..7bb4016e8f 100644 --- a/plugins/Relay/StoreTab.js +++ b/plugins/Relay/StoreTab.js @@ -13,7 +13,6 @@ import type {Map} from 'immutable'; var React = require('react'); -var assign = require('object-assign'); var DataView = require('../../frontend/DataView/DataView'); var decorate = require('../../frontend/decorate'); @@ -59,7 +58,7 @@ var styles = { loading: { textAlign: 'center', color: '#aaa', - } + }, }; module.exports = decorate({ diff --git a/plugins/Relay/backend.js b/plugins/Relay/backend.js index d3306cdaa5..fff4bfa854 100644 --- a/plugins/Relay/backend.js +++ b/plugins/Relay/backend.js @@ -15,7 +15,7 @@ import type Agent from '../../agent/Agent'; function decorate(obj, attr, fn) { var old = obj[attr]; - obj[attr] = function () { + obj[attr] = function() { var res = old.apply(this, arguments); fn.apply(this, arguments); return res; @@ -69,9 +69,9 @@ module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { text: q.getQueryString(), variables: q.getVariables(), name: q.getDebugName(), - } + }; })); - }) + }), ]; hook.on('shutdown', () => { restore.forEach(fn => fn()); diff --git a/shells/plain/container.js b/shells/plain/container.js index 842e1fac1f..bf14fa6bab 100644 --- a/shells/plain/container.js +++ b/shells/plain/container.js @@ -61,7 +61,7 @@ function injectMany(sources, done) { if (sources.length === 1) { return inject(sources[0], done); } - inject(sources[0], () => injectMany(sources.slice(1), done)) + inject(sources[0], () => injectMany(sources.slice(1), done)); } var sources = appSrc.split('|'); From 1615336e02465e81eb716de8180b555572ce1d67 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Wed, 9 Sep 2015 17:08:25 -0700 Subject: [PATCH 12/14] Hack to work around a race condition A better approach migtht be to implement a handshake on either the `Bridge` or `Wall` level that confirms that both sides are ready before sending anything. --- plugins/Relay/RelayPlugin.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/Relay/RelayPlugin.js b/plugins/Relay/RelayPlugin.js index 6b0cf6ebd7..0b029689fa 100644 --- a/plugins/Relay/RelayPlugin.js +++ b/plugins/Relay/RelayPlugin.js @@ -34,10 +34,15 @@ class RelayPlugin { this.store = store; this.hasRelay = false; this.relayStore = new RelayStore(bridge, store); - bridge.call('relay:check', [], hasRelay => { - this.hasRelay = hasRelay; - refresh(); - }); + // TODO (kassens): There's a race condition here. The Relay backend + // implements this call and is initialized from the injected script whereas + // this file is called from the Panel. + setTimeout(() => { + bridge.call('relay:check', [], hasRelay => { + this.hasRelay = hasRelay; + refresh(); + }); + }, 1000); } panes(): Array<(node: Object, id: string) => ReactElement> { From 09969204fe8d3ac8568b9209c9bcfd4e278a9528 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 15 Sep 2015 14:43:05 -0700 Subject: [PATCH 13/14] Hide Relay devtools behind URL flag Only show the Relay plugin for now as long as `location.hash` contains `relaydevtools`. --- plugins/Relay/backend.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Relay/backend.js b/plugins/Relay/backend.js index fff4bfa854..563889d895 100644 --- a/plugins/Relay/backend.js +++ b/plugins/Relay/backend.js @@ -28,8 +28,13 @@ function makeId() { } module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { - bridge.onCall('relay:check', () => !!hook._relayInternals); - if (!hook._relayInternals) { + var shouldEnable = !!( + hook._relayInternals && + window.location.hash.indexOf('relaydevtools') >= 0 + ); + + bridge.onCall('relay:check', () => shouldEnable); + if (!shouldEnable) { return; } var NetworkLayer = hook._relayInternals.NetworkLayer; From 95c9a93c5d816a74cd39601ee79b87d0e8578f2a Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 21 Sep 2015 21:42:19 -0700 Subject: [PATCH 14/14] Remove console.log from Relay plugin --- plugins/Relay/Store.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/Relay/Store.js b/plugins/Relay/Store.js index a62f56eca9..f24ed80ed4 100644 --- a/plugins/Relay/Store.js +++ b/plugins/Relay/Store.js @@ -70,13 +70,11 @@ class Store extends EventEmitter { }); }); bridge.on('relay:success', ({id, response, end}) => { - console.log('response', id, response); this.queries = this.queries.mergeIn([id], new Map({status: 'success', response, end})); this.emit('queries'); this.emit(id); }); bridge.on('relay:failure', ({id, error, end}) => { - console.log('error', id, error); this.queries = this.queries.mergeIn([id], new Map({status: 'failure', error, end})); this.emit('queries'); this.emit(id);