├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── assets └── demo.gif ├── dist └── index.html ├── package.json ├── src ├── devTools.jsx └── index.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hz/ 2 | dist/bundle.js 3 | node_modules/ 4 | rethinkdb_data/ 5 | *.log 6 | index.js 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .hz 2 | dist 3 | rethinkdb_data 4 | src 5 | .babelrc 6 | webpack.config.js 7 | *.log 8 | assets 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ryan Delaney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Horizon Devtools 2 | 3 | > A better dev experience for Horizon users 4 | 5 | ## Demo 6 | 7 | ![Demo](https://github.com/rrdelaney/horizon-devtools/blob/master/assets/demo.gif) 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save horizon-devtools 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | // Require the dependencies 19 | import React from 'react' 20 | import ReactDOM from 'react-dom' 21 | import { createDevTools } from 'horizon-devtools' 22 | import App from './App' 23 | 24 | // Create your horizon instance 25 | let horizon = Horizon() 26 | horizon.connect() 27 | 28 | // Run the `createDevTools` function on your horizon instance 29 | // This returns a `DevTools` component to render into your react app 30 | let DevTools = createDevTools(horizon) 31 | 32 | // Render your app! 33 | ReactDOM.render(
34 | 35 | 36 |
, document.getElementById('root')) 37 | ``` 38 | 39 | ## Guide 40 | 41 | The devtools will track any query you make through Horizon after the tools are 42 | initialized with `createDevTools`. If the query ends with `fetch()` the devtools 43 | will display the result of that query. If `watch()` is used 🔄 will 44 | show next to the query and be live updated with the results. 45 | 46 | `⌃ + Q` will show/hide the devtools. 47 | 48 | `⌃ + W` will change the position of the devtools. 49 | 50 | You can run custom queries using the query editor in the devtools. Type in a 51 | query into the bottom text box and press `⌃ + Enter` to run it. The horizon 52 | instance will be bound to `horizon`. If a query excludes `fetch()` or `watch()`, 53 | `fetch()` will be automatically appended. If a query excludes `subscribe()`, 54 | `subscribe()` will be automatically appended. For example, if `horizon('users')` 55 | is entered, `horizon('users').fetch().subscribe()` will be run. 56 | 57 | ## API 58 | 59 | ### `createDevTools(horizon): DevTools` 60 | 61 | Attaches instrumentation for monitoring on the horizon instance. It returns 62 | a `DevTools` component hooked up to the instrumentation that automatically 63 | updates. 64 | 65 | ### `` 66 | 67 | This renders the developer tools into the window. If `defaultVisible` is set 68 | to false, the tools will not be open at first. `defaultPosition` can be either 69 | `left`, `right`, `top`, or `bottom`. 70 | 71 | ## Contributing 72 | 73 | To run the example use `npm run dev` and go to `localhost:8181`. 74 | 75 | To build for publishing run `npm run build`. 76 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrdelaney/horizon-devtools/7aacba90a3c06a9e8840eabc8b6fa0ae9ac23a8e/assets/demo.gif -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horizon-devtools", 3 | "version": "1.1.0", 4 | "description": "Devtools for horizon", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "concurrently -c green,blue --names \"Horizon,Webpack\" --prefix \"[{name}]\" \"hz serve --dev\" \"webpack --watch\"", 8 | "build": "babel src/devTools.jsx > index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rrdelaney/horizon-devtools" 13 | }, 14 | "keywords": [ 15 | "horizon", 16 | "react", 17 | "devtools", 18 | "hz" 19 | ], 20 | "author": "Ryan Delaney ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/rrdelaney/horizon-devtools/issues" 24 | }, 25 | "homepage": "https://github.com/rrdelaney/horizon-devtools", 26 | "devDependencies": { 27 | "babel-cli": "^6.11.4", 28 | "babel-preset-es2015": "^6.9.0", 29 | "babel-preset-react": "^6.11.1", 30 | "concurrently": "^2.2.0", 31 | "horizon": "^2.0.0", 32 | "react": "^15.2.1", 33 | "react-dom": "^15.2.1", 34 | "snazzy": "^4.0.0", 35 | "standard": "^7.1.2", 36 | "webpack": "^1.13.1" 37 | }, 38 | "dependencies": { 39 | "react-dock": "^0.2.3", 40 | "react-json-tree": "^0.10.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/devTools.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import JSONTree from 'react-json-tree' 3 | import Dock from 'react-dock' 4 | 5 | // This should be replaced with Horizon 2's `query.toString()` method 6 | const ast2string = (ast, live) => { 7 | // All queries except `find` are an array, so we normalize `find` here to be an array 8 | if (ast.find) ast.find = [ast.find] 9 | return ['find', 'find_all', 'above', 'below', 'order', 'limit'].reduce( 10 | (res, key) => res + (!ast[key] ? '' : `.${key}(${JSON.stringify(ast[key][0]).replace(/[\[\]"\{\}]/g, '')})`), 11 | (live === 'watch' ? '🔄 ' : '') + ast.collection 12 | ) 13 | } 14 | 15 | let positions = ['left', 'top', 'right', 'bottom'] 16 | 17 | let nextPosition = c => positions[(positions.indexOf(c) + 1) % 4] 18 | 19 | const theme = { 20 | base00: '#1B2B34', 21 | base01: '#EC5F67', 22 | base03: '#99C794', 23 | base06: '#FAC863', 24 | base05: '#6699CC', 25 | base04: '#C594C5', 26 | base02: '#5FB3B3', 27 | base07: '#A7ADBA', 28 | base08: '#4F5B66', 29 | base09: '#EC5F67', 30 | base0A: '#99C794', 31 | base0B: '#FAC863', 32 | base0D: '#6699CC', 33 | base0C: '#C594C5', 34 | base0E: '#5FB3B3', 35 | base0F: '#D8DEE9' 36 | } 37 | 38 | function instrument (horizon) { 39 | let observ = { 40 | queries: {}, 41 | update: () => null 42 | } 43 | 44 | Object.getPrototypeOf(Object.getPrototypeOf(horizon('users'))).fetch = 45 | new Proxy(Object.getPrototypeOf(Object.getPrototypeOf(horizon('users'))).fetch, { 46 | apply (_fetch, thisArg) { 47 | return _fetch.bind(thisArg)().map(c => { 48 | observ.queries[ast2string(thisArg._query, 'fetch')] = c 49 | observ.update() 50 | return c 51 | }) 52 | } 53 | }) 54 | 55 | Object.getPrototypeOf(Object.getPrototypeOf(horizon('users'))).watch = 56 | new Proxy(Object.getPrototypeOf(Object.getPrototypeOf(horizon('users'))).watch, { 57 | apply (_watch, thisArg, args) { 58 | return _watch.bind(thisArg)(...args).map(c => { 59 | observ.queries[ast2string(thisArg._query, 'watch')] = c 60 | observ.update() 61 | return c 62 | }) 63 | } 64 | }) 65 | 66 | return observ 67 | } 68 | 69 | const Button = ({ onClick, children }) => 70 | 81 | {children} 82 | 83 | 84 | const ButtonTabs = ({ onClick }) => 85 |
86 | 87 | 88 | {/* */} 89 |
90 | 91 | const Logo = () => 92 | 93 | 94 | [DEV TOOLS] 95 | 96 | 97 | const QueryView = ({ queries }) => 98 | 104 | 105 | const UserView = ({ user }) => 106 | user === null 107 | ?
No user exists
108 | : 114 | 115 | export function createDevTools (horizon) { 116 | let devtools = instrument(horizon) 117 | if (typeof window !== 'undefined') window.devtools = devtools 118 | 119 | return class HzDevTools extends Component { 120 | constructor (props) { 121 | super(props) 122 | 123 | this.handleKeyDown = this.handleKeyDown.bind(this) 124 | this.handleQueryTextUpdate = this.handleQueryTextUpdate.bind(this) 125 | this.shouldRunQuery = this.shouldRunQuery.bind(this) 126 | 127 | this.state = { 128 | visible: props.defaultVisible || true, 129 | position: props.defaultPosition || 'right', 130 | queries: devtools.queries, 131 | user: {}, 132 | queryText: '', 133 | queryTextError: false, 134 | currentTab: 'query' 135 | } 136 | } 137 | 138 | componentDidMount () { 139 | if (typeof window !== 'undefined') window.addEventListener('keydown', this.handleKeyDown) 140 | 141 | horizon.currentUser().watch().subscribe(user => { 142 | this.setState({ user }) 143 | }, () => { 144 | this.setState({ user: null }) 145 | }) 146 | 147 | devtools.update = () => { 148 | this.setState({ queries: devtools.queries }) 149 | } 150 | } 151 | 152 | componentWillUnmount () { 153 | if (typeof window !== 'undefined') window.removeEventListener('keydown', this.handleKeyDown) 154 | devtools.update = () => null 155 | } 156 | 157 | handleKeyDown (e) { 158 | let char = String.fromCharCode(e.keyCode || e.which) 159 | if (char.toUpperCase() === 'Q' && e.ctrlKey) this.setState({ visible: !this.state.visible }) 160 | if (char.toUpperCase() === 'W' && e.ctrlKey) this.setState({ position: nextPosition(this.state.position) }) 161 | } 162 | 163 | handleQueryTextUpdate (e) { 164 | this.setState({ queryText: e.target.value, queryTextError: false }) 165 | } 166 | 167 | shouldRunQuery (e) { 168 | if (e.keyCode === 13 && e.ctrlKey) { 169 | this.setState({ queryText: '' }) 170 | this.runQuery(this.state.queryText) 171 | 172 | return false 173 | } 174 | } 175 | 176 | runQuery (query) { 177 | try { 178 | let q = eval(query) 179 | if (q.fetch) { q = q.fetch() } 180 | if (q.subscribe) { q = q.subscribe() } 181 | this.setState({ queries: devtools.queries }) 182 | } catch (e) { 183 | this.setState({ queryTextError: true, queryText: e }) 184 | } 185 | } 186 | 187 | render () { 188 | return 189 |
190 |           
191 |           
192 | 193 | this.setState({ currentTab })} /> 194 | 195 |
196 | {this.state.currentTab === 'query' 197 | ? 198 | : this.state.currentTab === 'user' 199 | ? 200 | : null 201 | } 202 |
203 |
204 |