├── .babelrc ├── .gitignore ├── .markdownlint.json ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── devtools.gif ├── index.d.ts ├── jsconfig.json ├── package-lock.json ├── package.json ├── publish.js ├── src ├── Controls │ ├── GraphControl.jsx │ ├── LogControl.jsx │ └── UpdatesControl.jsx ├── DevTool.js ├── Graph │ ├── index.js │ └── styles.js ├── Highlighter │ ├── index.js │ └── styles.js ├── ModalContainer │ ├── index.jsx │ └── styles.js ├── Panel │ ├── PanelButton.jsx │ ├── index.jsx │ └── styles │ │ ├── graph-active.svg │ │ ├── graph.svg │ │ ├── index.js │ │ ├── inspect.svg │ │ ├── log-active.svg │ │ ├── log.svg │ │ ├── updates-active.svg │ │ └── updates.svg ├── RenderingMonitor.js ├── consoleLogChange.js ├── deduplicateDependencies.js ├── globalStore.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-class-properties"], 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /index.js 3 | /npm-debug.log 4 | *.iml 5 | *.ipr 6 | *.iws 7 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "blanks-around-lists": false, 3 | "line-length": false 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /node_modules/ 3 | webpack.config.js 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 6.1.1 2 | 3 | * Fixed `Uncaught ReferenceError: propTypes is not defined` [#111](https://github.com/mobxjs/mobx-react-devtools/issues/111) through [#110](https://github.com/mobxjs/mobx-react-devtools/pull/110) by [mpedersen15](https://github.com/mpedersen15) 4 | 5 | # 6.1.0 6 | 7 | * Added more styling options to the panels, see [#100](https://github.com/mobxjs/mobx-react-devtools/pull/100) by [@janaagaard75](https://github.com/janaagaard75) and [#103](https://github.com/mobxjs/mobx-react-devtools/pull/103) by [@rokoroku](https://github.com/rokoroku) 8 | * Firefox now supports nested spy logging as well. [#105](https://github.com/mobxjs/mobx-react-devtools/pull/105) by [@wkillerud](https://github.com/wkillerud) 9 | 10 | # 6.0.3 11 | 12 | * Fixed #101: `window` not defined on node environments 13 | * Made border uniform, PR [#99](https://github.com/mobxjs/mobx-react-devtools/pull/99) by [@janaagaard75](https://github.com/janaagaard75) 14 | 15 | # 6.0.2 16 | 17 | * Fixed issue where an exception was thrown when an observer component returns a text node. Fixes [#80](https://github.com/mobxjs/mobx-react-devtools/issue/80) 18 | * Fixed issue where `isObservableMap` was undefined when logging map transtions. Through [#98](https://github.com/mobxjs/mobx-react-devtools/pull/98) by [@AMilassin](https://github.com/AMilassin) 19 | 20 | # 6.0.1 21 | 22 | * Corrected peer dependency 23 | 24 | # 6.0.0 25 | 26 | * Added compatibility with MobX 5. See [#96](https://github.com/mobxjs/mobx-react-devtools/pull/96) by [max9599](https://github.com/max9599) 27 | * Added support for tree-shaking / dead code elimination when the package is required but not rendered. [#95](https://github.com/mobxjs/mobx-react-devtools/pull/95) by [rifler](https://github.com/rifler) 28 | * Stack traces are now automatically collapsed if the browser supports it. [#79](https://github.com/mobxjs/mobx-react-devtools/pull/78) by [will-stone](https://github.com/will-stone) 29 | * Fixed several console output issues where `undefined` was printed incorrectly [#94](https://github.com/mobxjs/mobx-react-devtools/pull/94) by [srg-kostyrko](https://github.com/srg-kostyrko) 30 | * Webpack 4 is now used to build the package. See [#87](https://github.com/mobxjs/mobx-react-devtools/pull/87) by [hiroppy](https://github.com/hiroppy) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michel Weststrate 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 | # mobx-react-devtools 2 | 3 | _:warning: Note: This package is deprecated. Use the [browser plugin](https://github.com/mobxjs/mobx-devtools) instead. Also note that with mobx-react@6 and higher the package should no longer be needed, see [changelog](https://github.com/mobxjs/mobx-react/blob/master/CHANGELOG.md#600) :warning:_ 4 | 5 | DevTools for MobX to track the rendering behavior and data dependencies of your app. 6 | 7 | ![MobX DevTools](devtools.gif) 8 | 9 | *The default position of the panel has been changed to bottom right. If you prefer top right like in the gif above, add `position="topRight"` to ``.* 10 | 11 | ## Installation 12 | 13 | `npm install --save-dev mobx-react-devtools` 14 | 15 | or 16 | 17 | `` 18 | 19 | ## Usage 20 | 21 | Somewhere in your application, create a DevTools component: 22 | 23 | ```js 24 | import DevTools from 'mobx-react-devtools'; 25 | 26 | class MyApp extends React.Component { 27 | render() { 28 | return ( 29 |
30 | ... 31 | 32 |
33 | ); 34 | } 35 | } 36 | ``` 37 | 38 | or 39 | 40 | `React.createElement(mobxDevtools.default)` 41 | 42 | Supported props: 43 | * `highlightTimeout` — number, default: 1500 44 | * `noPanel` — boolean, if set, do not render control panel, default: false 45 | * `position` — string (or object), `topRight`, `bottomRight`, `bottomLeft` or `topLeft`, default: `bottomRight` 46 | * `className` — string, className of control panel, default: not defined 47 | * `style` — object, inline style object of control panel, default: not defined 48 | 49 | In order to be compatible with earlier versions of `mobx-react-devtools` it is also possible to assign `position` to an object containing inline styles. Using the dedicated `style` property is however recommended. 50 | 51 | From there on, after each rendering a reactive components logs the following three metrics: 52 | 1. Number of times the component did render so far 53 | 2. The time spend in the `render()` method of a component 54 | 3. The time spend from the start of the `render()` method until the changes are flushed to the DOM 55 | 56 | For each component the color indicates roughly how long the coloring took. Rendering times are cumulative; they include time spend in the children 57 | * Green: less then 25 ms 58 | * Orange: less then 100 ms 59 | * Red: rendering for this component took more than 100ms 60 | 61 | ### About log groups 62 | 63 | Note that if logging is enabled, MobX actions and reactions will appear as collapsible groups inside the browsers console. 64 | Mind that any log statements that are printed during these (re)actions will appear inside those groups as well, so that you can exactly trace when they are triggered. 65 | 66 | ### Configuration 67 | 68 | ```js 69 | import { configureDevtool } from 'mobx-react-devtools'; 70 | 71 | // Any configurations are optional 72 | configureDevtool({ 73 | // Turn on logging changes button programmatically: 74 | logEnabled: true, 75 | // Turn off displaying components updates button programmatically: 76 | updatesEnabled: false, 77 | // Log only changes of type `reaction` 78 | // (only affects top-level messages in console, not inside groups) 79 | logFilter: change => change.type === 'reaction', 80 | }); 81 | 82 | ``` 83 | 84 | There are also aliases for turning on/off devtools buttons: 85 | 86 | ```js 87 | import { setLogEnabled, setUpdatesEnabled, setGraphEnabled } from 'mobx-react-devtools'; 88 | 89 | setLogEnabled(true); // same as configureDevtool({ logEnabled: true }); 90 | setUpdatesEnabled(false); // same as configureDevtool({ updatesEnabled: false }); 91 | setGraphEnabled(false); // same as configureDevtool({ graphEnabled: false }); 92 | ``` 93 | 94 | ### Custom panel design 95 | 96 | ```js 97 | import DevTools, { GraphControl, LogControl, UpdatesControl } from 'mobx-react-devtools'; 98 | 99 | class MyNiceButton extends React.Component { 100 | render() { 101 | const { active, onToggle, children } = this.props; 102 | return ( 103 | 107 | ); 108 | } 109 | } 110 | 111 | class MyApp extends React.Component { 112 | render() { 113 | return ( 114 |
115 | 116 | {/* Include somewhere with `noPanel` prop. Is needed to display updates and modals */} 117 | 118 | 119 |
120 | 121 | {/* Must have only one child that takes props: `active` (bool), `onToggle` (func) */} 122 | Graph 123 | 124 | 125 | {/* Must have only one child that takes props: `active` (bool), `onToggle` (func) */} 126 | Log 127 | 128 | 129 | {/* Must have only one child that takes props: `active` (bool), `onToggle` (func) */} 130 | Updates 131 | 132 |
133 |
134 | ); 135 | } 136 | } 137 | ``` 138 | 139 | ## Roadmap 140 | 141 | * ~~Be able to turn dev-tools on and off at runtime~~ 142 | * ~~Select and log dependency tree of components~~ 143 | * Visualize observer tree values 144 | * ~~Be able to enable state change tracking from the extras module~~ 145 | 146 | ## Changelog 147 | 148 | 5.0.1 149 | 150 | * Updated peer dependencies for mobx-react@5.0.0 151 | 152 | 5.0.0 153 | 154 | * Upgraded to MobX 4.0.0 155 | 156 | 4.2.15 157 | 158 | * Fixed error on logging & expr 159 | 160 | 4.2.14 161 | 162 | * Stopped using mobx default export (#1043) 163 | 164 | 4.2.13 165 | 166 | * Fixed warning about calling PropTypes validators directly (#62) 167 | 168 | 4.2.12 169 | 170 | * Added react 15.5/16 support 171 | 172 | 4.2.11 173 | 174 | * Added MobX 3 support 175 | 176 | 4.2.9 177 | * Fixed typescript typings (#42) 178 | 179 | 4.2.8 180 | * Fixed typescript typings (#36) 181 | 182 | 4.2.7 183 | * Fixed passing highlightTimeout from DevTools (#41) 184 | 185 | 4.2.6 186 | * Fixed “max event listeners” warning when rendering in node.js () 187 | 188 | 4.2.5 189 | * Added ability to filter displaying changes in console 190 | * Fixed submitting forms by DevTools panel buttons (#29) 191 | 192 | 4.2.4 193 | * Added ability to change buttons state programmatically(#27) 194 | 195 | 4.2.3 196 | * Made console colors lighter (#25) 197 | 198 | 4.2.2 199 | * Added modular devtools controls (#21) 200 | 201 | 4.0.5 202 | * Added Object.assign polyfill to avoid issues with server side rendering on old node vesions 203 | 204 | 4.0.2 205 | * Make sure AMD / root imports work (#12) 206 | * DevTools should now 'work' (not do anything) when used in Isomorphic rendering (#11) 207 | * Highlighting boxes now show up at the proper coordinates when using complex stacking contexts 208 | 209 | 4.0.1 210 | * Added typescript typings (see #6) 211 | * Use (fix) uglify, by @evoyy 212 | * Added option to customize the position of the toolbar (by @evoyy) 213 | 214 | 4.0.0 215 | * Upgraded to MobX 2.0 / MobX React 3.0 216 | -------------------------------------------------------------------------------- /devtools.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mobxjs/mobx-react-devtools/bcd0298108655c5d271e9300cf008b9f53eba07a/devtools.gif -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Turns a React component or stateless render function into a reactive component. 3 | */ 4 | import React = require("react") 5 | 6 | export interface IDevToolProps { 7 | highlightTimeout?: number 8 | position?: "topRight" | "bottomRight" | "bottomLeft" | "topLeft" | 9 | { 10 | top?: number | string 11 | right?: number | string 12 | bottom?: number | string 13 | left?: number | string 14 | } 15 | noPanel?: boolean; 16 | className?: string; 17 | style?: React.CSSProperties; 18 | } 19 | 20 | export default class DevTools extends React.Component {} 21 | export class GraphControl extends React.Component<{}, {}> {} 22 | export class LogControl extends React.Component<{}, {}> {} 23 | export class UpdatesControl extends React.Component<{ highlightTimeout?: number }, {}> {} 24 | 25 | export function configureDevtool(options: { 26 | logEnabled?: boolean 27 | updatesEnabled?: boolean 28 | graphEnabled?: boolean 29 | logFilter?: (p: any) => boolean 30 | }): void 31 | 32 | export function setUpdatesEnabled(enabled: boolean): void 33 | export function setGraphEnabled(enabled: boolean): void 34 | export function setLogEnabled(enabled: boolean): void 35 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs" 5 | }, 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx-react-devtools", 3 | "version": "6.1.1", 4 | "description": "Dev-tools for MobX and React", 5 | "main": "index.js", 6 | "typings": "index", 7 | "sideEffects": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/mobxjs/mobx-react-devtools.git" 11 | }, 12 | "scripts": { 13 | "prettier": "prettier --write --print-width 100 --tab-width 4 --no-semi \"**/*.js\" \"**/*.ts\"", 14 | "build:dev": "cross-env NODE_ENV=development webpack", 15 | "build:prod": "cross-env NODE_ENV=production webpack", 16 | "prepublish": "npm run build:prod", 17 | "precommit": "lint-staged" 18 | }, 19 | "author": "Michel Weststrate", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/mobxjs/mobx/issues" 23 | }, 24 | "homepage": "https://mobxjs.github.io/mobx", 25 | "peerDependencies": { 26 | "mobx": "^4.3.1 || ^5.0.0", 27 | "mobx-react": "^4.0.0 || ^5.0.0" 28 | }, 29 | "devDependencies": { 30 | "babel-core": "^6.5.1", 31 | "babel-loader": "^7.1.4", 32 | "babel-plugin-transform-class-properties": "^6.5.0", 33 | "babel-preset-env": "^1.7.0", 34 | "babel-preset-react": "^6.5.0", 35 | "cross-env": "^5.1.4", 36 | "file-loader": "^0.8.5", 37 | "lint-staged": "^4.1.3", 38 | "prettier": "^1.6.1", 39 | "prop-types": "^15.5.10", 40 | "url-loader": "^1.0.1", 41 | "webpack": "^4.1.1", 42 | "webpack-cli": "^2.0.12" 43 | }, 44 | "keywords": [ 45 | "mobx", 46 | "mobservable", 47 | "react-component", 48 | "react", 49 | "reactjs", 50 | "reactive", 51 | "devtools" 52 | ], 53 | "lint-staged": { 54 | "*.{ts,tsx,js,jsx}": [ 55 | "prettier --write --print-width 100 --tab-width 4 --no-semi", 56 | "git add" 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* Publish.js, publish a new version of the npm package as found in the current directory */ 3 | /* Run this file from the root of the repository */ 4 | 5 | const shell = require("shelljs") 6 | const fs = require("fs") 7 | const readline = require("readline") 8 | 9 | const rl = readline.createInterface({ 10 | input: process.stdin, 11 | output: process.stdout 12 | }) 13 | 14 | function run(command, options) { 15 | const continueOnErrors = options && options.continueOnErrors 16 | const ret = shell.exec(command, options) 17 | if (!continueOnErrors && ret.code !== 0) { 18 | shell.exit(1) 19 | } 20 | return ret 21 | } 22 | 23 | function exit(code, msg) { 24 | console.error(msg) 25 | shell.exit(code) 26 | } 27 | 28 | async function prompt(question, defaultValue) { 29 | return new Promise(resolve => { 30 | rl.question(`${question} [${defaultValue}]: `, answer => { 31 | answer = answer && answer.trim() 32 | resolve(answer ? answer : defaultValue) 33 | }) 34 | }) 35 | } 36 | 37 | async function main() { 38 | const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")) 39 | 40 | // Bump version number 41 | let nrs = pkg.version.split(".") 42 | nrs[2] = 1 + parseInt(nrs[2], 10) 43 | const version = (pkg.version = await prompt( 44 | "Please specify the new package version of '" + pkg.name + "' (Ctrl^C to abort)", 45 | nrs.join(".") 46 | )) 47 | if (!version.match(/^\d+\.\d+\.\d+$/)) { 48 | exit(1, "Invalid semantic version: " + version) 49 | } 50 | 51 | // Check registry data 52 | const npmInfoRet = run(`npm info ${pkg.name} --json`, { 53 | continueOnErrors: true, 54 | silent: true 55 | }) 56 | if (npmInfoRet.code === 0) { 57 | //package is registered in npm? 58 | var publishedPackageInfo = JSON.parse(npmInfoRet.stdout) 59 | if ( 60 | publishedPackageInfo.versions == version || 61 | publishedPackageInfo.versions.includes(version) 62 | ) { 63 | exit(2, "Version " + pkg.version + " is already published to npm") 64 | } 65 | 66 | fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2), "utf8") 67 | 68 | // Finally, commit and publish! 69 | run("npm publish") 70 | run(`git commit -am "Published version ${version}"`) 71 | run(`git tag ${version}`) 72 | 73 | run("git push") 74 | run("git push --tags") 75 | console.log("Published!") 76 | exit(0) 77 | } else { 78 | exit(1, pkg.name + " is not an existing npm package") 79 | } 80 | } 81 | 82 | main().catch(e => { 83 | throw e 84 | }) 85 | -------------------------------------------------------------------------------- /src/Controls/GraphControl.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { trackComponents } from 'mobx-react'; 3 | import { getGlobalState, setGlobalState, eventEmitter, _handleMouseMove, _handleClick } from '../globalStore'; 4 | 5 | export default class GraphControl extends Component { 6 | 7 | componentWillMount() { 8 | this.setState({}); 9 | } 10 | 11 | componentDidMount() { 12 | trackComponents(); 13 | 14 | eventEmitter.on('update', this.handleUpdate); 15 | 16 | if (typeof window !== 'undefined') { 17 | if (typeof document !== 'undefined') { 18 | document.body.addEventListener('mousemove', _handleMouseMove, true); 19 | document.body.addEventListener('click', _handleClick, true); 20 | } 21 | } 22 | } 23 | 24 | componentWillUnmount() { 25 | eventEmitter.removeListener('update', this.handleUpdate) 26 | if (typeof document !== 'undefined') { 27 | document.body.removeEventListener('mousemove', _handleMouseMove, true); 28 | document.body.removeEventListener('click', _handleMouseMove, true); 29 | } 30 | } 31 | 32 | handleUpdate = () => this.setState({}); 33 | 34 | handleToggleGraph = () => { 35 | const { graphEnabled } = getGlobalState(); 36 | setGlobalState({ 37 | hoverBoxes: [], 38 | graphEnabled: !graphEnabled, 39 | }); 40 | }; 41 | 42 | render() { 43 | const { graphEnabled } = getGlobalState(); 44 | const { children } = this.props; 45 | return React.cloneElement(children, { 46 | onToggle: this.handleToggleGraph, 47 | active: graphEnabled, 48 | }); 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Controls/LogControl.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { getGlobalState, setGlobalState, eventEmitter, restoreLogFromLocalstorage } from '../globalStore'; 3 | 4 | export default class LogControl extends Component { 5 | 6 | componentDidMount() { 7 | eventEmitter.on('update', this.handleUpdate); 8 | restoreLogFromLocalstorage(); 9 | } 10 | 11 | componentWillUnmount() { 12 | eventEmitter.removeListener('update', this.handleUpdate) 13 | } 14 | 15 | handleUpdate = () => { 16 | this.setState({}); 17 | }; 18 | 19 | handleToggleLog = () => { 20 | const { logEnabled } = getGlobalState(); 21 | setGlobalState({ logEnabled: !logEnabled }) 22 | }; 23 | 24 | render() { 25 | const { logEnabled } = getGlobalState(); 26 | const { children } = this.props; 27 | return React.cloneElement(children, { 28 | onToggle: this.handleToggleLog, 29 | active: logEnabled, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Controls/UpdatesControl.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import RenderingMonitor from '../RenderingMonitor'; 4 | import { getGlobalState, setGlobalState, eventEmitter, restoreUpdatesFromLocalstorage } from '../globalStore'; 5 | 6 | export default class UpdatesControl extends Component { 7 | 8 | static propTypes = { 9 | highlightTimeout: PropTypes.number, 10 | }; 11 | 12 | static defaultProps = { 13 | highlightTimeout: 1500, 14 | }; 15 | 16 | componentDidMount() { 17 | eventEmitter.on('update', this.handleUpdate); 18 | const { highlightTimeout } = this.props; 19 | this.renderingMonitor = new RenderingMonitor({ highlightTimeout }); 20 | restoreUpdatesFromLocalstorage(); 21 | } 22 | 23 | componentWillUnmount() { 24 | eventEmitter.removeListener('update', this.handleUpdate) 25 | this.renderingMonitor.dispose(); 26 | } 27 | 28 | handleUpdate = () => this.setState({}); 29 | 30 | handleToggleUpdates = () => { 31 | const { updatesEnabled } = getGlobalState(); 32 | setGlobalState({ updatesEnabled: !updatesEnabled }); 33 | }; 34 | 35 | render() { 36 | const { updatesEnabled } = getGlobalState(); 37 | const { children } = this.props; 38 | return React.cloneElement(children, { 39 | onToggle: this.handleToggleUpdates, 40 | active: updatesEnabled, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DevTool.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import { getGlobalState, restoreState, eventEmitter } from "./globalStore" 4 | import Panel from "./Panel" 5 | import Highlighter from "./Highlighter" 6 | import Graph from "./Graph" 7 | 8 | export default class DevTool extends Component { 9 | static propTypes = { 10 | highlightTimeout: PropTypes.number, 11 | noPanel: PropTypes.bool, 12 | className: PropTypes.string, 13 | style: PropTypes.object, 14 | position: PropTypes.oneOfType( 15 | PropTypes.oneOf(['topRight', 'bottomRight', 'bottomLeft', 'topLeft']), 16 | PropTypes.shape({ 17 | top: PropTypes.string, 18 | right: PropTypes.string, 19 | bottom: PropTypes.string, 20 | left: PropTypes.string, 21 | }) 22 | ) 23 | } 24 | 25 | static defaultProps = { 26 | noPanel: false, 27 | className: '' 28 | } 29 | 30 | componentWillMount() { 31 | this.setState(getGlobalState()) 32 | } 33 | 34 | componentDidMount() { 35 | eventEmitter.on("update", this.handleUpdate) 36 | } 37 | 38 | componentWillUnmount() { 39 | eventEmitter.removeListener("update", this.handleUpdate) 40 | } 41 | 42 | handleUpdate = () => this.setState(getGlobalState()) 43 | 44 | handleToggleGraph = () => { 45 | this.setState({ 46 | hoverBoxes: [], 47 | graphEnabled: !this.state.graphEnabled 48 | }) 49 | } 50 | 51 | render() { 52 | const { noPanel, highlightTimeout, className, style } = this.props 53 | const { renderingBoxes, hoverBoxes } = this.state 54 | return ( 55 |
56 | {noPanel !== true && ( 57 | 63 | )} 64 | 65 | 66 |
67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Graph/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import ModalContainer from "../ModalContainer" 3 | import { getGlobalState, setGlobalState, eventEmitter } from "../globalStore" 4 | import * as styles from "./styles.js" 5 | 6 | export default class Graph extends Component { 7 | componentDidMount() { 8 | eventEmitter.on("update", this.handleUpdate) 9 | } 10 | 11 | componentWillUnmount() { 12 | eventEmitter.removeListener("update", this.handleUpdate) 13 | } 14 | 15 | handleUpdate = () => this.setState({}) 16 | 17 | handleClose = () => setGlobalState({ dependencyTree: undefined }) 18 | 19 | renderTreeItem({ name, dependencies }, isLast, isRoot) { 20 | return ( 21 |
22 | {name} 23 | {dependencies && ( 24 |
25 | {dependencies.map((dependency, i) => 26 | this.renderTreeItem( 27 | dependency, 28 | /*isLast:*/ i == dependencies.length - 1 29 | ) 30 | )} 31 |
32 | )} 33 | {!isRoot && } 34 | {!isRoot && ( 35 | 42 | )} 43 |
44 | ) 45 | } 46 | 47 | render() { 48 | const { dependencyTree } = getGlobalState() 49 | return ( 50 | 51 | {dependencyTree && ( 52 |
53 | 54 | × 55 | 56 |
57 | {this.renderTreeItem( 58 | dependencyTree, 59 | /*isLast:*/ true, 60 | /*isRoot:*/ true 61 | )} 62 |
63 |
64 | )} 65 |
66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Graph/styles.js: -------------------------------------------------------------------------------- 1 | export const graph = { 2 | background: "white", 3 | padding: "40px" 4 | } 5 | 6 | export const close = { 7 | color: "rgba(0, 0, 0, 0.2)", 8 | fontSize: "36px", 9 | position: "absolute", 10 | top: "5px", 11 | right: "5px", 12 | width: "40px", 13 | height: "40px", 14 | lineHeight: "34px", 15 | textAlign: "center", 16 | cursor: "pointer", 17 | ":hover": { 18 | color: "rgba(0, 0, 0, 0.5)" 19 | } 20 | } 21 | 22 | /* TREE */ 23 | 24 | export const tree = { 25 | position: "relative", 26 | paddingLeft: "25px" 27 | } 28 | 29 | export const item = { 30 | position: "relative" 31 | } 32 | 33 | export const box = { 34 | padding: "4px 10px", 35 | background: "rgba(0, 0, 0, 0.05)", 36 | display: "inline-block", 37 | marginBottom: "8px", 38 | color: "#000", 39 | root: { 40 | fontSize: "15px", 41 | fontWeight: "bold", 42 | padding: "6px 13px" 43 | } 44 | } 45 | 46 | export const itemHorisontalDash = { 47 | position: "absolute", 48 | left: "-12px", 49 | borderTop: "1px solid rgba(0, 0, 0, 0.2)", 50 | top: "14px", 51 | width: "12px", 52 | height: "0" 53 | } 54 | 55 | export const itemVericalStick = { 56 | position: "absolute", 57 | left: "-12px", 58 | borderLeft: "1px solid rgba(0, 0, 0, 0.2)", 59 | height: "100%", 60 | width: 0, 61 | top: "-8px", 62 | short: { 63 | height: "23px" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Highlighter/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import PropTypes from "prop-types" 3 | import * as styles from "./styles" 4 | 5 | export default class Highlighter extends Component { 6 | static propTypes = { 7 | boxes: PropTypes.arrayOf( 8 | PropTypes.shape({ 9 | type: PropTypes.oneOf(["rendering", "hover"]).isRequired, 10 | x: PropTypes.number.isRequired, 11 | y: PropTypes.number.isRequired, 12 | width: PropTypes.number.isRequired, 13 | height: PropTypes.number.isRequired, 14 | renderInfo: PropTypes.shape({ 15 | count: PropTypes.number.isRequired, 16 | renderTime: PropTypes.number.isRequired, 17 | totalTime: PropTypes.number.isRequired, 18 | cost: PropTypes.oneOf(["cheap", "acceptable", "expensive"]).isRequired 19 | }), 20 | lifeTime: PropTypes.number.isRequired 21 | }) 22 | ).isRequired 23 | } 24 | 25 | renderBox(box) { 26 | switch (box.type) { 27 | case "rendering": 28 | let renderingCostStyle = styles.rendering[box.renderInfo.cost] || {} 29 | return ( 30 |
34 | setTimeout(() => { 35 | if (el) el.style.opacity = 0 36 | }, box.lifeTime - 500)} 37 | style={Object.assign({}, styles.box, styles.rendering, renderingCostStyle, { 38 | left: box.x, 39 | top: box.y, 40 | width: box.width, 41 | height: box.height 42 | })} 43 | > 44 | 45 | {box.renderInfo.count}x | {box.renderInfo.renderTime} /{" "} 46 | {box.renderInfo.totalTime} ms 47 | 48 |
49 | ) 50 | 51 | case "hover": 52 | return ( 53 |
62 | ) 63 | 64 | default: 65 | throw new Error() 66 | } 67 | } 68 | 69 | render() { 70 | const { boxes } = this.props 71 | 72 | return
{boxes.map(box => this.renderBox(box))}
73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Highlighter/styles.js: -------------------------------------------------------------------------------- 1 | export const box = { 2 | display: "block", 3 | position: "fixed", 4 | zIndex: "64998", 5 | minWidth: "60px", 6 | outline: "3px solid", 7 | pointerEvents: "none", 8 | transition: "opacity 500ms ease-in" 9 | } 10 | 11 | export const text = { 12 | fontFamily: "verdana, sans-serif", 13 | padding: "0 4px 2px", 14 | color: "rgba(0, 0, 0, 0.6)", 15 | fontSize: "10px", 16 | lineHeight: "12px", 17 | pointerEvents: "none", 18 | float: "right", 19 | borderBottomRightRadius: "2px", 20 | maxWidth: "100%", 21 | maxHeight: "100%", 22 | overflow: "hidden", 23 | whiteSpace: "nowrap", 24 | textOverflow: "ellipsis" 25 | } 26 | 27 | export const rendering = { 28 | cheap: { 29 | outlineColor: "rgba(182, 218, 146, 0.75)", 30 | text: { 31 | backgroundColor: "rgba(182, 218, 146, 0.75)" 32 | } 33 | }, 34 | acceptable: { 35 | outlineColor: "rgba(228, 195, 66, 0.85)", 36 | text: { 37 | backgroundColor: "rgba(228, 195, 66, 0.85)" 38 | } 39 | }, 40 | expensive: { 41 | outlineColor: "rgba(228, 171, 171, 0.95)", 42 | text: { 43 | backgroundColor: "rgba(228, 171, 171, 0.95)" 44 | } 45 | } 46 | } 47 | 48 | export const hover = { 49 | outlineColor: "rgba(128, 128, 255, 0.5)" 50 | } 51 | -------------------------------------------------------------------------------- /src/ModalContainer/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import * as styles from './styles'; 4 | 5 | export default class ModalContainer extends Component { 6 | 7 | static propTypes = { 8 | children: PropTypes.node, 9 | onOverlayClick: PropTypes.func.isRequired, 10 | }; 11 | 12 | componentDidUpdate(prevProps) { 13 | const html = document.body.parentNode; 14 | if (prevProps.children && !this.props.children) { 15 | // Disappeared 16 | html.style.borderRight = null; 17 | html.style.overflow = null; 18 | } else if (!prevProps.children && this.props.children) { 19 | // Appeared 20 | const prevTotalWidth = html.offsetWidth; 21 | html.style.overflow = 'hidden'; 22 | const nextTotalWidth = html.offsetWidth; 23 | const rightOffset = Math.max(0, nextTotalWidth - prevTotalWidth); 24 | html.style.borderRight = `${rightOffset}px solid transparent` 25 | } 26 | } 27 | 28 | stopPropagation = e => e.stopPropagation(); 29 | 30 | render() { 31 | const { children, onOverlayClick } = this.props; 32 | if (!children) return null; 33 | return ( 34 |
38 |
43 | {children} 44 |
45 |
46 | ); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/ModalContainer/styles.js: -------------------------------------------------------------------------------- 1 | export const overlay = { 2 | position: "fixed", 3 | top: 0, 4 | right: 0, 5 | bottom: 0, 6 | left: 0, 7 | zIndex: 66000, 8 | overflow: "auto", 9 | WebkitOverflowScrolling: "touch", 10 | outline: 0, 11 | backgroundColor: "rgba(40, 40, 50, 0.5)", 12 | transformOrigin: "50% 25%" 13 | } 14 | 15 | export const modal = { 16 | position: "relative", 17 | width: "auto", 18 | margin: "5% 10%", 19 | zIndex: 1060 20 | } 21 | -------------------------------------------------------------------------------- /src/Panel/PanelButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import * as styles from './styles'; 4 | 5 | export default class PanelButton extends Component { 6 | 7 | static props = { 8 | onToggle: PropTypes.bool.isRequired, 9 | active: PropTypes.bool.isRequired, 10 | name: PropTypes.oneOf(['buttonUpdates', 'buttonGraph', 'buttonLog']).isRequired, 11 | }; 12 | 13 | state = { 14 | hovered: false, 15 | }; 16 | 17 | handleMouseOver = () => this.setState({ hovered: true }); 18 | handleMouseOut = () => this.setState({ hovered: false }); 19 | 20 | render() { 21 | const { active, id, onToggle } = this.props; 22 | const { hovered } = this.state; 23 | 24 | const additionalStyles = (() => { 25 | switch (id) { 26 | case 'buttonUpdates': return active ? styles.buttonUpdatesActive : styles.buttonUpdates; 27 | case 'buttonGraph': return active ? styles.buttonGraphActive : styles.buttonGraph; 28 | case 'buttonLog': return active ? styles.buttonLogActive : styles.buttonLog; 29 | } 30 | })(); 31 | 32 | const title = (() => { 33 | switch (id) { 34 | case 'buttonUpdates': return 'Visualize component re-renders'; 35 | case 'buttonGraph': return 'Select a component and show its dependency tree'; 36 | case 'buttonLog': return 'Log all MobX state changes and reactions to the browser console (use F12 to show / hide the console). Use Chrome / Chromium for an optimal experience'; 37 | } 38 | })(); 39 | 40 | const finalSyles = Object.assign( 41 | {}, 42 | styles.button, 43 | additionalStyles, 44 | active && styles.button.active, 45 | hovered && styles.button.hover 46 | ); 47 | 48 | return ( 49 |