├── LICENSE ├── README.md ├── __tests__ └── demo.test.js ├── assets ├── LogoAnimSmall.gif ├── logo150.png ├── realizeLogoPNG.png └── treeVisCropped1.gif ├── extension ├── 128.png ├── 16.png ├── 32.png ├── 48.png ├── backend │ ├── background.js │ ├── content_script.js │ └── hook.ts ├── devtools │ ├── create-panel.js │ ├── devtools-root.html │ └── panel │ │ ├── componentDisplay.js │ │ ├── createTree.js │ │ ├── data-example.js │ │ ├── interactions.js │ │ ├── panel.html │ │ ├── panel.js │ │ ├── search.js │ │ └── styles.css ├── libraries │ ├── d3.js │ └── d3.min.js └── manifest.json ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](./assets/LogoAnimSmall.gif) 2 | # Realize For React 3 | 4 | As React applications scale, it becomes more difficult to track state and to have a holistic overview of the component hierarchy. Realize is a tool to help developers visualize the structure and state flow of their React applications, especially when they are growing in scale and complexity. It currently supports React v.16.8. 5 | 6 | ![Logo](./assets/treeVisCropped1.gif) 7 | 8 | ## 👩‍💻 How to use it 9 | 1. Install the extension from the [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/realizeforreact/) store & [Chrome](https://chrome.google.com/webstore/detail/realize-for-react/llondniabnmnappjekpflmgcikaiilmh?authuser=0&hl=en) store 10 | 2. Navigate to your React website 11 | 3. Open the dev tools window and select the Realize Panel 12 | 4. Trigger a state change to see the component tree populate 13 | 14 | 15 | **Prerequisites** 16 | - Realize requires React Dev Tools to be installed before use. 17 | - Realize is best used on non-deployed applications. This uglification of deployed websites makes the component structure pretty unreadable. 18 | 19 | ## 🔥 Key Features 20 | **Zoom & Pan** - Hold down shift to enable dragging and zooming on the tree (to recenter just click the center button) 21 | **Component Focus** - Click on a node to view state, props and children in the right and panel 22 | **State Flow** - Click the 'state' toggle to show state flow on the tree. Stateful components have blue nodes and state flow is show by blue links 23 | **Search and Highlight** - Enter a component name in the search bar to see all matching nodes pulsate 24 | 25 | ## 💻 Installing locally 26 | 1. Clone the repo onto your computer `git clone https://github.com/oslabs-beta/Realize` 27 | 2. Run `npm i` from inside the root directory 28 | 3. Run `npm build` 29 | 4. Load the extension from the `build/extension` folder into your browser of choice: 30 |       For Firefox, navigate to `about:debugging#/runtime/this-firefox` and click Load Temporary Addon 31 |       For Chrome, navigate to `chrome://extensions/` toggle developer mode on and click Load Unpacked 32 | 5. Follow steps 2 onwards from the 'How to use it' section 33 | 34 | 35 | ## Authors 36 | Fan Shao - [Github](https://github.com/fan-shao) | [LinkedIn](https://www.linkedin.com/in/fan-shao/) 37 | Harry Clifford - [Github](https://github.com/HpwClifford/) | [LinkedIn](https://www.linkedin.com/in/harry-clifford-3788951a9/) 38 | Henry Black - [Github](https://github.com/blackhaj) | [LinkedIn](https://www.linkedin.com/in/henryblack1/) 39 | Horatiu Mitrea - [Github](https://github.com/hmitrea) | [LinkedIn](https://www.linkedin.com/in/horatiu-mitrea-515704137/) 40 | 41 | ## Contact 42 | You can contact us personally through our LinkedIn accounts (links above) or as a team via [realizeforreact@gmail.com](mailto:realizeforreact@gmail.com) 43 | 44 | ## Contributing 45 | We would love for you to test out our extensions and submit any issues you encounter. Feel free to fork to your own repo and submit PRs. Some features we would like to add: 46 | 1. Performance data on render times 47 | 2. Expanding/collapsing nodes 48 | 3. Autocomplete on search 49 | 50 | 51 | ### License 52 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 53 | -------------------------------------------------------------------------------- /__tests__/demo.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-env jest */ 3 | /* eslint-env browser */ 4 | const ComponentDisplay = require('../extension/devtools/panel/componentDisplay'); 5 | const searchData = require('../temp/search-example'); 6 | const search = require('../extension/devtools/panel/search'); 7 | 8 | describe('ComponentDisplay class testing', () => { 9 | let CD; 10 | const testName = 'test'; 11 | let parent; 12 | 13 | beforeEach(() => { 14 | parent = document.createElement('div'); 15 | CD = new ComponentDisplay({ name: testName }, parent); 16 | }); 17 | 18 | it('class instantiates', () => { 19 | expect(!!CD).toBe(true); 20 | }); 21 | 22 | it('displays component name', () => { 23 | const test = 'Name'; 24 | const result = CD.displayName(test); 25 | const target = document.createElement('span'); 26 | target.classList.add('component-display-name'); 27 | target.innerHTML = formatHTML``; 28 | }); 29 | 30 | xit('displays children', () => { 31 | const testArr = [{ name: 1 }, { name: 2 }, { name: 3 }]; 32 | 33 | const result = CD.displayChildren(testArr); 34 | 35 | const target = document.createElement('details'); 36 | target.innerHTML = formatHTML`Children 37 | `; 42 | 43 | expect(result.isEqualNode(target)).toBe(true); 44 | }); 45 | 46 | it('displays arrays', () => { 47 | // set up environment 48 | const testArr = [ 49 | [1, 2], 50 | [3, 4], 51 | ]; 52 | 53 | // receive result from targetted function 54 | const result = CD.displayData(testArr); 55 | 56 | // create target node 57 | const target = document.createElement('details'); 58 | target.innerHTML = formatHTML`Array 59 |
    60 |
  1. 61 |
    62 | Array 63 |
      64 |
    1. 1
    2. 65 |
    3. 2
    4. 66 |
    67 |
    68 |
  2. 69 |
  3. 70 |
    71 | Array 72 |
      73 |
    1. 3
    2. 74 |
    3. 4
    4. 75 |
    76 |
    77 |
  4. 78 |
`; 79 | 80 | // compare nodes 81 | expect(result.innerHTML === target.innerHTML).toBe(true); 82 | // dom elements being tested against each other 83 | expect(result.isEqualNode(target)).toBe(true); 84 | }); 85 | 86 | // bug here - not sure why 87 | xit('displays objects', () => { 88 | // set up input 89 | const testObj = { 90 | a: 1, 91 | b: 2, 92 | }; 93 | 94 | // execute function 95 | const result = CD.displayData(testObj); 96 | 97 | // create target node 98 | const target = document.createElement('details'); 99 | target.innerHTML = formatHTML`Object 100 | `; 104 | 105 | console.log('target: ', target.innerHTML); 106 | console.log('result: ', result.innerHTML); 107 | console.log(typeof result.children[1].children[0].innerHTML); 108 | expect(result.isEqualNode(target)).toBe(true); 109 | }); 110 | 111 | it('displays nested objects', () => { 112 | // set up input 113 | const testObj = { 114 | a: 1, 115 | b: { 116 | c: 3, 117 | }, 118 | }; 119 | 120 | // execute function 121 | const result = CD.displayData(testObj); 122 | 123 | const target = document.createElement('details'); 124 | target.innerHTML = formatHTML`Object 125 | `; 136 | 137 | console.log('result: ', result.innerHTML); 138 | console.log('target', target.innerHTML); 139 | expect(1).toBe(1); 140 | }); 141 | 142 | it('displays state', () => { 143 | const state = [1, 2, 3]; 144 | 145 | const result = CD.displayState(state, false); 146 | 147 | const target = document.createElement('details'); 148 | target.innerHTML = formatHTML`State 149 | `; 161 | 162 | console.log('target: ', target.innerHTML); 163 | console.log('result: ', result.innerHTML); 164 | expect(target.isEqualNode(result)).toBe(true); 165 | }); 166 | 167 | xit('displays useState state properly', () => { 168 | const state = [['a', 'b', 'c'], 2, 3]; 169 | 170 | const result = CD.displayState(state, true); 171 | 172 | const target = document.createElement('details'); 173 | target.innerHTML = formatHTML`State 174 | `; 188 | 189 | expect(target.isEqualNode(result)).toBe(true); 190 | }); 191 | 192 | it('displays props', () => { 193 | const props = { a: 1, b: 2 }; 194 | 195 | const result = CD.displayProps(props); 196 | 197 | const target = document.createElement('details'); 198 | target.innerHTML = formatHTML`Props 199 | `; 203 | 204 | console.log('target: ', target.innerHTML); 205 | console.log('result: ', result.innerHTML); 206 | expect(target.isEqualNode(result)).toBe(true); 207 | }); 208 | 209 | xit('displays state hooks', () => { 210 | const hooks = [1, 'hello']; 211 | 212 | const result = CD.displayHooks(hooks); 213 | 214 | const target = document.createElement('details'); 215 | target.innerHTML = formatHTML`Hooks 216 | `; 220 | console.log('result :', result.innerHTML); 221 | console.log('target :', target.innerHTML); 222 | expect(result.isEqualNode(target)).toBe(true); 223 | }); 224 | }); 225 | // To fix search testing to accomodate for the new function 226 | describe('Search functionality', () => { 227 | it('finds App', () => { 228 | const result = search(searchData, 'App'); 229 | 230 | expect(result.length).toBe(1); 231 | }); 232 | 233 | // it('returns -1 when none found', () => { 234 | // const result = search(searchData, 'afjasdnflnaslfmsad'); 235 | 236 | // expect(result).toBe(-1); 237 | // }); 238 | }); 239 | 240 | xit('panel display', () => { 241 | // create environment 242 | const infoPanel = document.createElement('div'); 243 | infoPanel.id = 'info-panel'; 244 | }); 245 | 246 | xit('isEqualNode test', () => { 247 | const test1 = document.createElement('ul'); 248 | test1.innerHTML = formatHTML`
  • a: 1
  • `; 249 | const li = document.createElement('li'); 250 | li.append(`a: `, '1'); 251 | test1.append(li); 252 | 253 | const test2 = document.createElement('ul'); 254 | test2.innerHTML = formatHTML`
  • a: 1
  • `; 255 | 256 | console.log(test1.innerHTML); 257 | console.log(test2.innerHTML); 258 | expect(test1.isEqualNode(test2)).toBe(true); 259 | }); 260 | 261 | function formatHTML(strings) { 262 | return strings[0] 263 | .split('\n') 264 | .map((s) => s.trim()) 265 | .join(''); 266 | } 267 | 268 | module.exports = { 269 | verbose: true, 270 | }; 271 | -------------------------------------------------------------------------------- /assets/LogoAnimSmall.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/assets/LogoAnimSmall.gif -------------------------------------------------------------------------------- /assets/logo150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/assets/logo150.png -------------------------------------------------------------------------------- /assets/realizeLogoPNG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/assets/realizeLogoPNG.png -------------------------------------------------------------------------------- /assets/treeVisCropped1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/assets/treeVisCropped1.gif -------------------------------------------------------------------------------- /extension/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/extension/128.png -------------------------------------------------------------------------------- /extension/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/extension/16.png -------------------------------------------------------------------------------- /extension/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/extension/32.png -------------------------------------------------------------------------------- /extension/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Realize/ef7173fdf6580ab36b588190e69998da7578eb7d/extension/48.png -------------------------------------------------------------------------------- /extension/backend/background.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | const connectedTabs = {}; 3 | 4 | chrome.runtime.onConnect.addListener((port) => { 5 | const panelListener = (request, sender, sendResponse) => { 6 | if (request.name === 'connect' && request.tabID) { 7 | console.log('tab connected, tabs: ', connectedTabs); 8 | connectedTabs[request.tabID] = port; 9 | // Injects the content-script when panel connection made 10 | chrome.tabs.executeScript({ 11 | file: "./content_script.js" 12 | }); 13 | } 14 | }; 15 | 16 | port.onMessage.addListener(panelListener); 17 | port.onDisconnect.addListener(function (port) { 18 | port.onMessage.removeListener(panelListener); 19 | 20 | const tabs = Object.keys(connectedTabs); 21 | for (let k = 0; k < tabs.length; k++) { 22 | if (connectedTabs[tabs[k]] === port) { 23 | delete connectedTabs[tabs[k]]; 24 | break; 25 | } 26 | } 27 | }); 28 | 29 | }); 30 | 31 | function handleMessage(request, sender, sendResponse) { 32 | // if from panel 33 | 34 | if (sender.tab) { 35 | const tabID = sender.tab.id; 36 | if (tabID in connectedTabs) { 37 | connectedTabs[tabID].postMessage(request); 38 | console.log('message sent to tab: ', tabID); 39 | console.log("MESSAGE PAYLOAD: ",request) 40 | } 41 | } 42 | 43 | return Promise.resolve('Dummy resolution for browser happiness'); 44 | } 45 | 46 | // Listen for messages from devtools 47 | chrome.runtime.onMessage.addListener(handleMessage); 48 | 49 | // Re-injects the script on refresh 50 | chrome.tabs.onUpdated.addListener(function(tabId,changeInfo,tab){ 51 | if (connectedTabs[tabId]){ 52 | chrome.tabs.executeScript({ 53 | file: "./content_script.js" 54 | }); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /extension/backend/content_script.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-disable no-undef */ 3 | 4 | const time = 500; 5 | 6 | setTimeout(() => { 7 | const script = document.createElement('script'); 8 | script.src = chrome.extension.getURL('hook.js'); 9 | document.head.appendChild(script); 10 | }, time); 11 | 12 | const sendMessage = (tree) => { 13 | chrome.runtime.sendMessage(tree); 14 | }; 15 | 16 | function handleMessage(request, sender, sendResponse) { 17 | if (request.data && request.data.tree) { 18 | sendMessage(request.data.tree); 19 | } 20 | } 21 | 22 | window.addEventListener('message', handleMessage); 23 | -------------------------------------------------------------------------------- /extension/backend/hook.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable no-undef */ 3 | /* eslint-disable no-use-before-define */ 4 | /* eslint-disable func-names */ 5 | /* eslint-disable no-underscore-dangle */ 6 | 7 | const throttle = require('lodash.throttle'); 8 | 9 | // need to define types here 10 | declare global { 11 | interface devTools { 12 | renderers: { size?: number }; 13 | onCommitFiberRoot(any?); 14 | } 15 | 16 | interface Window { 17 | __REACT_DEVTOOLS_GLOBAL_HOOK__: devTools; 18 | } 19 | 20 | interface component { 21 | name: any; 22 | node?: any; 23 | state?: object; 24 | stateType?: { stateful: boolean; receiving: boolean; sending: any } | -1; 25 | hooks?: [string]; 26 | children?: [string] | []; 27 | props?: object; 28 | } 29 | } 30 | 31 | function hook() { 32 | const devTools = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; 33 | 34 | // if devtools not activated 35 | if (!devTools) { 36 | sendToContentScript("Looks like you don't have react devtools activated"); 37 | return; 38 | } 39 | 40 | // if hook can't find react 41 | if (devTools.renderers && devTools.renderers.size < 1) { 42 | sendToContentScript("Looks like this page doesn't use React. Go to a React page and trigger a state change"); 43 | return; 44 | } 45 | 46 | // patch react devtools function called on render 47 | devTools.onCommitFiberRoot = (function (original) { 48 | return function (...args) { 49 | const fiberDOM = args[1]; 50 | const rootNode = fiberDOM.current.stateNode.current; 51 | const arr = []; 52 | try { 53 | throttledRecurse(rootNode.child, arr); 54 | if (arr.length > 0) sendToContentScript(arr[0]); 55 | } catch (error) { 56 | sendToContentScript( 57 | "we're sorry, there was an error on our end. To contribute: https://github.com/oslabs-beta/Realize" 58 | ); 59 | } 60 | 61 | return original(...args); 62 | }; 63 | })(devTools.onCommitFiberRoot); 64 | } 65 | 66 | // message sending function 67 | function sendToContentScript(tree) { 68 | // for debugging: 69 | // console.log(tree); 70 | window.postMessage({ tree }, '*'); 71 | } 72 | 73 | const clean = (item, depth = 0): any => { 74 | // base case 75 | if (depth > 10) return 'max recursion depth reached'; 76 | if (typeof item !== 'object' && typeof item !== 'function') return item; 77 | 78 | // if item is composite 79 | if (item === null) return null; 80 | if (typeof item === 'object') { 81 | let result; 82 | if (item.$$typeof && typeof item.$$typeof === 'symbol') { 83 | return item.type && typeof item.type !== 'string' 84 | ? `<${item.type.name} />` 85 | : 'React component'; 86 | } 87 | if (Array.isArray(item)) { 88 | result = []; 89 | item.forEach((elem, idx) => { 90 | result[idx] = clean(elem, depth + 1); 91 | }); 92 | } else { 93 | result = {}; 94 | Object.keys(item).forEach((key) => { 95 | result[key] = clean(item[key], depth + 1); 96 | }); 97 | } 98 | return result; 99 | } 100 | if (typeof item === 'function') { 101 | return `function: ${item.name}()`; 102 | } 103 | }; 104 | 105 | const getName = (node, component, parentArr): void | -1 => { 106 | if (!node.type || !node.type.name) { 107 | // this is a misc fiber node or html element, continue without appending 108 | if (node.child) recurse(node.child, parentArr); 109 | if (node.sibling) recurse(node.sibling, parentArr); 110 | return -1; 111 | } else { 112 | // if valid, extract component name 113 | component.name = node.type.name; 114 | } 115 | }; 116 | 117 | const getState = (node, component): void => { 118 | // for linked list recursion 119 | const llRecurse = (stateNode, arr): any => { 120 | arr.push(clean(stateNode.memoizedState)); 121 | 122 | if ( 123 | stateNode.next && 124 | stateNode.memoizedState !== stateNode.next.memoizedState 125 | ) 126 | llRecurse(stateNode.next, arr); 127 | }; 128 | 129 | // if no state, exit 130 | if (!node.memoizedState) return; 131 | // if state stored in linked list 132 | if (node.memoizedState.memoizedState !== undefined) { 133 | if (node.memoizedState.next === null) { 134 | component.state = clean(node.memoizedState.memoizedState); 135 | return; 136 | } 137 | component.state = []; 138 | llRecurse(node.memoizedState, component.state); 139 | return; 140 | } 141 | 142 | // not linked list 143 | component.state = clean(node.memoizedState); 144 | }; 145 | 146 | const getProps = (node, component): void => { 147 | if (node.memoizedProps && Object.keys(node.memoizedProps).length > 0) { 148 | const props = {}; 149 | Object.keys(node.memoizedProps).forEach((prop) => { 150 | props[prop] = clean(node.memoizedProps[prop]); 151 | }); 152 | 153 | component.props = props; 154 | } 155 | }; 156 | 157 | const getHooks = (node, component): void => { 158 | if (node._debugHookTypes) component.hooks = node._debugHookTypes; 159 | }; 160 | 161 | const getChildren = (node, component, parentArr): void => { 162 | const children = []; 163 | 164 | if (node.child) { 165 | recurse(node.child, children); 166 | } 167 | if (node.sibling) recurse(node.sibling, parentArr); 168 | 169 | // console.log(children.length); 170 | if (children.length > 0) component.children = children; 171 | }; 172 | 173 | const getStateType = (component): void => { 174 | const stateType = { 175 | stateful: !(component.state === undefined), 176 | receiving: !(component.props === undefined), 177 | sending: 178 | component.children && 179 | component.children.some((child) => child.props !== undefined), 180 | }; 181 | 182 | if (Object.values(stateType).some((isTrue) => isTrue)) { 183 | component.stateType = stateType; 184 | } 185 | }; 186 | 187 | const throttledRecurse = throttle(recurse, 300); 188 | 189 | // function for fiber tree traversal 190 | function recurse(node: any, parentArr) { 191 | const component: component = { 192 | name: '', 193 | // for debugging: 194 | // node, 195 | }; 196 | 197 | // if invalid component, recursion will contine, exit here 198 | if (getName(node, component, parentArr) === -1) return; 199 | getState(node, component); 200 | getProps(node, component); 201 | getHooks(node, component); 202 | // insert component into parent's children array 203 | parentArr.push(component); 204 | // below functions must execute after inner recursion 205 | getChildren(node, component, parentArr); 206 | getStateType(component); 207 | } 208 | 209 | hook(); 210 | 211 | export { clean }; 212 | -------------------------------------------------------------------------------- /extension/devtools/create-panel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | // This creates the dev tools panel using the panel.html file as the template 3 | 4 | chrome.devtools.panels.create( 5 | 'Realize', // title 6 | '', // icon 7 | './panel.html' // content 8 | ); 9 | -------------------------------------------------------------------------------- /extension/devtools/devtools-root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Title 7 | 8 | 9 | 10 | 11 | 12 |

    MAKE ME THE HTML

    13 | 14 | 15 | -------------------------------------------------------------------------------- /extension/devtools/panel/componentDisplay.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-disable class-methods-use-this */ 3 | /* eslint-env browser */ 4 | 5 | // parent: reference to infopanel dom node 6 | // obj: component being sent by the tree (node) 7 | class ComponentDisplay { 8 | constructor(parent) { 9 | this.parent = parent; 10 | } 11 | 12 | update(component) { 13 | // clear 14 | this.parent.innerHTML = ''; 15 | const compArr = []; 16 | 17 | // add name of component 18 | compArr.push(this.displayName(component.name)); 19 | 20 | // conditionals to load compArr based on component properties 21 | if (component.state !== undefined) { 22 | // if functional state 23 | if ( 24 | component.hooks && 25 | component.hooks.some((hook) => hook === 'useState') 26 | ) { 27 | compArr.push(this.displayState(component.state, true)); 28 | } else { 29 | compArr.push(this.displayState(component.state, false)); 30 | } 31 | } 32 | // add the hook 33 | if (component.hooks) compArr.push(this.displayHooks(component.hooks)); 34 | if (component.props) compArr.push(this.displayProps(component.props)); 35 | if (component.children) 36 | compArr.push(this.displayChildren(component.children)); 37 | 38 | // append node/nodes from compArr 39 | this.parent.append(...compArr); 40 | } 41 | 42 | displayName(name) { 43 | const div = document.createElement('div'); 44 | div.classList.add('component-display-name'); 45 | div.textContent = name; 46 | return div; 47 | } 48 | 49 | displayChildren(arr) { 50 | const details = document.createElement('details'); 51 | const summary = document.createElement('summary'); 52 | const list = document.createElement('ul'); 53 | summary.textContent = 'Children'; 54 | 55 | arr.forEach((child) => { 56 | const item = document.createElement('li'); 57 | item.textContent = child.name; 58 | list.append(item); 59 | }); 60 | 61 | details.append(summary, list); 62 | 63 | return details; 64 | } 65 | 66 | displayState(input, usedHooks) { 67 | const details = document.createElement('details'); 68 | const summary = document.createElement('summary'); 69 | const list = document.createElement('ul'); 70 | summary.textContent = 'State'; 71 | summary.id = 'state'; 72 | 73 | if (usedHooks && Array.isArray(input)) { 74 | input.forEach((stateValue) => { 75 | const li = document.createElement('li'); 76 | li.append(this.displayData(stateValue)); 77 | list.appendChild(li); 78 | }); 79 | } else { 80 | const li = document.createElement('li'); 81 | li.append(this.displayData(input)); 82 | list.appendChild(li); 83 | } 84 | 85 | details.append(summary, list); 86 | return details; 87 | } 88 | 89 | displayProps(input) { 90 | const details = document.createElement('details'); 91 | const summary = document.createElement('summary'); 92 | const list = document.createElement('ul'); 93 | summary.textContent = 'Props'; 94 | 95 | Object.keys(input).forEach((prop) => { 96 | const li = document.createElement('li'); 97 | li.append(`${prop}: `, this.displayData(input[prop])); 98 | list.appendChild(li); 99 | }); 100 | 101 | details.append(summary, list); 102 | return details; 103 | } 104 | 105 | // recursive function for composite data types 106 | displayData(input) { 107 | // base case 108 | if (typeof input !== 'object') return input; 109 | if (input === null) return 'null'; 110 | 111 | const details = document.createElement('details'); 112 | const summary = document.createElement('summary'); 113 | let list; 114 | 115 | if (Array.isArray(input)) { 116 | // if array 117 | summary.textContent = 'Array'; 118 | // if empty 119 | if (input.length === 0) { 120 | list = document.createElement('ul'); 121 | const li = document.createElement('li'); 122 | li.textContent = 'empty array'; 123 | list.appendChild(li); 124 | } else { 125 | list = document.createElement('ol'); 126 | list.start = '0'; 127 | input.forEach((elem) => { 128 | const li = document.createElement('li'); 129 | li.append(this.displayData(elem)); 130 | list.appendChild(li); 131 | }); 132 | } 133 | } else { 134 | // if object 135 | summary.textContent = 'Object'; 136 | const keys = Object.keys(input); 137 | list = document.createElement('ul'); 138 | if (keys.length > 0) { 139 | keys.forEach((key) => { 140 | const li = document.createElement('li'); 141 | li.append(`${key}: `, this.displayData(input[key])); 142 | list.appendChild(li); 143 | }); 144 | } else { 145 | const li = document.createElement('li'); 146 | li.append('empty object'); 147 | list.appendChild(li); 148 | } 149 | } 150 | 151 | details.append(summary, list); 152 | return details; 153 | } 154 | 155 | displayHooks(input) { 156 | const details = document.createElement('details'); 157 | const summary = document.createElement('summary'); 158 | const list = document.createElement('ul'); 159 | summary.textContent = 'Hooks'; 160 | input.forEach((hook) => { 161 | const li = document.createElement('li'); 162 | li.innerHTML = hook; 163 | list.appendChild(li); 164 | }); 165 | 166 | details.append(summary, list); 167 | return details; 168 | } 169 | } 170 | 171 | module.exports = ComponentDisplay; 172 | -------------------------------------------------------------------------------- /extension/devtools/panel/createTree.js: -------------------------------------------------------------------------------- 1 | import * as d3 from '../../libraries/d3.js'; 2 | import { addInteractionsListeners } from './interactions'; 3 | import addSearchListener from './search'; 4 | 5 | // ########################################## BUILDING THE TREE 6 | function createTree(inputData, panelInstance) { 7 | 8 | 9 | // Clear any previous tree data to avoid overlap 10 | // d3.selectAll("circle.node").remove() 11 | // d3.selectAll("line.link").remove() 12 | // d3.selectAll("text.label").remove() 13 | d3.select('#error-message') 14 | .style('display', 'none') 15 | d3.select('#button-bar') 16 | .style('display', 'block') 17 | d3.selectAll("svg g.links").html("") 18 | d3.selectAll("svg g.nodes").html("") 19 | 20 | // Creates a heirarchical data structure based on the object passed into it 21 | let root = d3.hierarchy(inputData); // using fake data here 22 | // // Can check out what the structure looks like 23 | // console.log('Nodes',root.descendants()) // -> shows the nested object of nodes 24 | // console.log(root.links()) // -> shows the array on links which connect the nodes 25 | 26 | // Store 66% of the users screen width for creating the tree 27 | const panelWidth = Math.floor(screen.width * 0.66); 28 | 29 | // Find out the height of the tree and size the svg accordingly (each level havin 95px) 30 | const dataHeight = root.height; 31 | const treeHeight = dataHeight * 95; 32 | const svgHeight = Math.max(window.innerHeight, treeHeight) 33 | 34 | // Creates a function which will later create the co-ordinates for the tree structure 35 | let treeLayout = d3.tree().size([panelWidth - 80, treeHeight]); 36 | d3.select('#tree') 37 | .attr('height', svgHeight + 80) 38 | 39 | // Creates x and y values on each node of root. 40 | // We will later use this x and y values to: 41 | // 1. position the circles (after joining the root.descendents data to svg circles) 42 | // 2. create links between the circles (after creating lines using root.links that go from x to y) 43 | treeLayout(root); 44 | 45 | // create additional nodes for interactions 46 | d3.select('svg g.nodes') 47 | .selectAll('circle.background-node') // select ALL circle objects with nodes as class (there are none) 48 | .data(root.descendants()) // attach the data to each of the nodes 49 | .enter() // as there are no nodes we will make them using enter (and attach the data) 50 | .append('circle') // add all the circle objects 51 | .classed('background-node', true) // add classes of node to each of them 52 | .attr('cx', function (d) { 53 | // set its x coordinates 54 | return d.x; 55 | }) 56 | .attr('cy', function (d) { 57 | // set its y coordinate 58 | return d.y; 59 | }) 60 | .attr('r', 6); // set radius of the circle size 61 | 62 | 63 | // SELECT a g object with nodes as their class 64 | d3.select('svg g.nodes') 65 | .selectAll('circle.node') // select ALL circle objects with nodes as class (there are none) 66 | .data(root.descendants()) // attach the data to each of the nodes 67 | .enter() // as there are no nodes we will make them using enter (and attach the data) 68 | .append('circle') // add all the circle objects 69 | .classed('node', true) // add classes of node to each of them 70 | .attr('cx', function (d) { 71 | // set its x coordinates 72 | return d.x; 73 | }) 74 | .attr('cy', function (d) { 75 | // set its y coordinate 76 | return d.y; 77 | }) 78 | .attr('r', 7); // set radius of the circle size 79 | 80 | 81 | 82 | // Add text nodes:",t labels at the same x / y co-ordinates as the nodes 83 | d3.selectAll('svg g.nodes') 84 | .selectAll('text.label') 85 | .data(root.descendants()) 86 | .enter() 87 | .append('text') 88 | .classed('label', true) 89 | .style('fill', 'white') 90 | .style('text-anchor', 'middle') 91 | .text((d) => d.data.name) 92 | .attr('x', (d) => d.x) 93 | .attr('y', (d) => d.y - 10); 94 | 95 | // Links with straight lines 96 | // d3.select('svg g.links') // select the g object with class links 97 | // .selectAll('line.link') // select all the line objects with class link - ain't any so we gunna create them 98 | // .data(root.links()) // attach the links data 99 | // .enter() // add the nodes that are missing 100 | // .append('line') // by creating a line object 101 | // .classed('link', true) // set the class 102 | // .attr('x1', function (d) { 103 | // return d.source.x; 104 | // }) // set the source x and y coordinates 105 | // .attr('y1', function (d) { 106 | // return d.source.y; 107 | // }) 108 | // .attr('x2', function (d) { 109 | // return d.target.x; 110 | // }) // set the target x and y coordinates 111 | // .attr('y2', function (d) { 112 | // return d.target.y; 113 | // }); 114 | 115 | // Links with Curves 116 | // Link for potentially making curves more extreme - https://stackoverflow.com/questions/44958789/d3-v4-how-to-use-linkradial-to-draw-a-link-between-two-points 117 | d3.select('svg g.links') // select the g object with class links 118 | .selectAll('path.link') // select all the line objects with class link - ain't any so we gunna create them 119 | .data(root.links()) 120 | .join("path") 121 | .classed('link', true) 122 | // .attr("d", function(d) { 123 | // var x0 = d.source.x; 124 | // var y0 = d.source.y; 125 | // var y1 = d.target.y; 126 | // var x1 = d.target.x; 127 | // var k = 10; 128 | 129 | // var path = d3.path() 130 | // path.moveTo(x0, y0) 131 | // path.bezierCurveTo(x1,y0-k,x0,y1-k,x1,y1); 132 | // path.lineTo(x1,y1); 133 | 134 | // return path.toString(); 135 | // }) 136 | // OR, nicer curves 137 | .attr("d", d3.linkVertical() 138 | .x(d => d.x) 139 | .y(d => d.y)) 140 | .attr("fill", "none") 141 | .attr("stroke", "#e8e888") 142 | .attr("stroke-opacity", 0.4) 143 | .attr("stroke-width", 1.5) 144 | 145 | let namesArray = [] 146 | d3.selectAll('circle.node') 147 | .each(function(d){ 148 | namesArray.push(d.data.name) 149 | }) 150 | 151 | let uniqueNamesArray = [...new Set(namesArray)]; 152 | addInteractionsListeners(panelInstance) 153 | addSearchListener(uniqueNamesArray); 154 | //addSearchListener(namesArray) // If we want multpile components with the same name 155 | } 156 | 157 | export {createTree}; -------------------------------------------------------------------------------- /extension/devtools/panel/data-example.js: -------------------------------------------------------------------------------- 1 | export const data = [ 2 | { 3 | "name": "App", 4 | "state": null, 5 | "children": [ 6 | { 7 | "name": "BrowserRouter", 8 | "props": {}, 9 | "children": [ 10 | { 11 | "name": "Router", 12 | "state": { 13 | "location": { 14 | "pathname": "/", 15 | "search": "", 16 | "hash": "", 17 | "key": "pwbb3b" 18 | } 19 | }, 20 | "props": { 21 | "history": { 22 | "length": 5, 23 | "action": "PUSH", 24 | "location": { 25 | "pathname": "/", 26 | "search": "", 27 | "hash": "", 28 | "key": "pwbb3b" 29 | } 30 | } 31 | }, 32 | "children": [ 33 | { 34 | "name": "Switch", 35 | "props": {}, 36 | "children": [ 37 | { 38 | "name": "ProtectedRoute", 39 | "props": { 40 | "exact": true, 41 | "path": "/", 42 | "component": "f component()", 43 | "location": { 44 | "pathname": "/", 45 | "search": "", 46 | "hash": "", 47 | "key": "pwbb3b" 48 | }, 49 | "computedMatch": { 50 | "path": "/", 51 | "url": "/", 52 | "isExact": true, 53 | "params": {} 54 | } 55 | }, 56 | "children": [ 57 | { 58 | "name": "Route", 59 | "props": { 60 | "exact": true, 61 | "path": "/", 62 | "location": { 63 | "pathname": "/", 64 | "search": "", 65 | "hash": "", 66 | "key": "pwbb3b" 67 | }, 68 | "computedMatch": { 69 | "path": "/", 70 | "url": "/", 71 | "isExact": true, 72 | "params": {} 73 | }, 74 | "render": "f render()" 75 | }, 76 | "children": [ 77 | { 78 | "name": "Home", 79 | "state": { 80 | "graphData": { 81 | "dates": [ 82 | "2019-01-01", 83 | "2019-02-01", 84 | "2019-03-01", 85 | "2019-04-01", 86 | "2019-05-01", 87 | "2019-06-01", 88 | "2019-07-01", 89 | "2019-08-01", 90 | "2019-09-01", 91 | "2019-10-01", 92 | "2019-11-01", 93 | "2019-12-01" 94 | ], 95 | "balances": [ 96 | "1000", 97 | "1077.8292851314", 98 | "1056.9707208628106", 99 | "1011.4230105318165", 100 | "1280.4601643568963", 101 | "1246.5236049556274", 102 | "1594.2490714050964", 103 | "1397.9523718173732", 104 | "1035.1826098757933", 105 | "1138.2455309109264", 106 | "1911.4746773973525", 107 | "1002.9841051342213" 108 | ] 109 | } 110 | }, 111 | "props": { 112 | "history": { 113 | "length": 5, 114 | "action": "PUSH", 115 | "location": { 116 | "pathname": "/", 117 | "search": "", 118 | "hash": "", 119 | "key": "pwbb3b" 120 | } 121 | }, 122 | "location": { 123 | "pathname": "/", 124 | "search": "", 125 | "hash": "", 126 | "key": "pwbb3b" 127 | }, 128 | "match": { 129 | "path": "/", 130 | "url": "/", 131 | "isExact": true, 132 | "params": {} 133 | } 134 | }, 135 | "children": [ 136 | { 137 | "name": "TitleBar", 138 | "props": { 139 | "title": "Home" 140 | }, 141 | "children": [ 142 | { 143 | "name": "Title", 144 | "props": { 145 | "title": "Home" 146 | }, 147 | "stateType": { 148 | "stateful": false, 149 | "receiving": true, 150 | "sending": false 151 | } 152 | }, 153 | { 154 | "name": "AccountIcon" 155 | } 156 | ], 157 | "stateType": { 158 | "stateful": false, 159 | "receiving": true, 160 | "sending": true 161 | } 162 | }, 163 | { 164 | "name": "Card", 165 | "props": { 166 | "data": { 167 | "dates": [ 168 | "2019-01-01", 169 | "2019-02-01", 170 | "2019-03-01", 171 | "2019-04-01", 172 | "2019-05-01", 173 | "2019-06-01", 174 | "2019-07-01", 175 | "2019-08-01", 176 | "2019-09-01", 177 | "2019-10-01", 178 | "2019-11-01", 179 | "2019-12-01" 180 | ], 181 | "balances": [ 182 | "1000", 183 | "1077.8292851314", 184 | "1056.9707208628106", 185 | "1011.4230105318165", 186 | "1280.4601643568963", 187 | "1246.5236049556274", 188 | "1594.2490714050964", 189 | "1397.9523718173732", 190 | "1035.1826098757933", 191 | "1138.2455309109264", 192 | "1911.4746773973525", 193 | "1002.9841051342213" 194 | ] 195 | } 196 | }, 197 | "children": [ 198 | { 199 | "name": "PlotlyComponent", 200 | "props": { 201 | "data": [ 202 | { 203 | "x": [ 204 | "2019-01-01", 205 | "2019-02-01", 206 | "2019-03-01", 207 | "2019-04-01", 208 | "2019-05-01", 209 | "2019-06-01", 210 | "2019-07-01", 211 | "2019-08-01", 212 | "2019-09-01", 213 | "2019-10-01", 214 | "2019-11-01", 215 | "2019-12-01" 216 | ], 217 | "y": [ 218 | "1000", 219 | "1077.8292851314", 220 | "1056.9707208628106", 221 | "1011.4230105318165", 222 | "1280.4601643568963", 223 | "1246.5236049556274", 224 | "1594.2490714050964", 225 | "1397.9523718173732", 226 | "1035.1826098757933", 227 | "1138.2455309109264", 228 | "1911.4746773973525", 229 | "1002.9841051342213" 230 | ], 231 | "mode": "none", 232 | "type": "scattergl", 233 | "fill": "tozeroy", 234 | "fillcolor": "#4BA4F4" 235 | } 236 | ], 237 | "layout": { 238 | "width": 320, 239 | "height": 240, 240 | "margin": { 241 | "l": 30, 242 | "r": 10, 243 | "b": 30, 244 | "t": 10 245 | }, 246 | "yaxis": { 247 | "range": [ 248 | 500, 249 | 2000 250 | ], 251 | "type": "linear" 252 | }, 253 | "xaxis": { 254 | "type": "date" 255 | } 256 | }, 257 | "config": { 258 | "displayModeBar": false 259 | }, 260 | "debug": false, 261 | "useResizeHandler": false, 262 | "style": { 263 | "position": "relative", 264 | "display": "inline-block" 265 | } 266 | }, 267 | "stateType": { 268 | "stateful": false, 269 | "receiving": true, 270 | "sending": false 271 | } 272 | } 273 | ], 274 | "stateType": { 275 | "stateful": false, 276 | "receiving": true, 277 | "sending": true 278 | } 279 | } 280 | ], 281 | "stateType": { 282 | "stateful": true, 283 | "receiving": true, 284 | "sending": true 285 | } 286 | }, 287 | { 288 | "name": "Navbar" 289 | } 290 | ], 291 | "stateType": { 292 | "stateful": false, 293 | "receiving": true, 294 | "sending": true 295 | } 296 | } 297 | ], 298 | "stateType": { 299 | "stateful": false, 300 | "receiving": true, 301 | "sending": true 302 | } 303 | } 304 | ], 305 | "stateType": { 306 | "stateful": false, 307 | "receiving": true, 308 | "sending": true 309 | } 310 | } 311 | ], 312 | "stateType": { 313 | "stateful": true, 314 | "receiving": true, 315 | "sending": true 316 | } 317 | } 318 | ], 319 | "stateType": { 320 | "stateful": false, 321 | "receiving": true, 322 | "sending": true 323 | } 324 | } 325 | ], 326 | "stateType": { 327 | "stateful": false, 328 | "receiving": false, 329 | "sending": true 330 | } 331 | } 332 | ] -------------------------------------------------------------------------------- /extension/devtools/panel/interactions.js: -------------------------------------------------------------------------------- 1 | import * as d3 from '../../libraries/d3.js'; 2 | 3 | // ########################################## OVERALL FUNCTION 4 | function addInteractionsListeners(panelInstance) { 5 | const zoom = d3.zoom(); 6 | addZoomListener(zoom); 7 | addCenterTreeListener(zoom); 8 | addShowStateListener(); 9 | addClickListeners(panelInstance) 10 | } 11 | 12 | // Utility function for transitions 13 | let t = d3.transition() 14 | .duration(750) 15 | .ease(d3.easeLinear); 16 | 17 | let tSlow = d3.transition() 18 | .duration(0) 19 | .ease(d3.easeLinear); 20 | 21 | // ########################################## TREE ZOOMING / PANNING 22 | const zoom = d3.zoom(); 23 | 24 | function addZoomListener(zoom) { 25 | // Grab body element 26 | let bodyElement = document.getElementsByTagName('body')[0]; 27 | 28 | // Attach eventlistener for 'option' keydown and trigger startZoom() 29 | bodyElement.addEventListener('keydown', (event) => { 30 | if (event.keyCode === 16) { 31 | startZoom(zoom); 32 | } 33 | }); 34 | 35 | // Remove zoom on key release 36 | bodyElement.addEventListener('keyup', (event) => { 37 | if (event.keyCode === 16) { 38 | endZoom(); 39 | } 40 | }); 41 | } 42 | 43 | // ZOOM Utility functions 44 | // Updates the g position based on user interactions (gets invoked inside startZoom()) 45 | function zoomed() { 46 | const g = d3.select('#treeG'); 47 | g.attr('transform', d3.event.transform); 48 | } 49 | 50 | // Start and end zoom functions for event listener 51 | function startZoom(zoom) { 52 | // Set zoom event listener on svg 53 | const svg = d3.select('#tree'); 54 | svg.call(zoom.on('zoom', zoomed)); 55 | } 56 | 57 | function endZoom() { 58 | // remove zoom listener 59 | const svg = d3.select('#tree'); 60 | svg.on('.zoom', null); 61 | } 62 | 63 | 64 | // ########################################## CENTER TREE 65 | // Centering the tree (resource which might help - http://bl.ocks.org/robschmuecker/7926762) 66 | 67 | function addCenterTreeListener(zoom) { 68 | // Function to reset svg so tree is centered 69 | function centerTree() { 70 | const svg = d3.select('#tree'); 71 | svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity); 72 | } 73 | 74 | // Add event listener to the center tree button 75 | document.getElementById('center-tree').addEventListener('click', centerTree); 76 | } 77 | 78 | // ########################################## STATE FLOW 79 | 80 | // Updating to show state 81 | function closedUpdateNodes() { 82 | // Set up transition 83 | // create closure for stateShown 84 | let stateShown = false; 85 | return function updateNodes(){ 86 | let nodes = d3.selectAll('circle.background-node') 87 | let color; 88 | let linkColor; 89 | let size; 90 | if (stateShown) { 91 | // default colors 92 | color = '#707070' 93 | linkColor = '#606060' 94 | size = 6 95 | stateShown = false 96 | } else { 97 | // state showns colors 98 | color = '#1eabd5' 99 | linkColor = '#1eabd5' 100 | size = 10 101 | stateShown = true 102 | }; 103 | nodes.each(function(d) { 104 | if (d.data.stateType){ 105 | if(d.data.stateType.stateful) { 106 | d3.select(this).transition(t) 107 | .style("fill", color) 108 | .attr('r', size) 109 | } 110 | } 111 | }) 112 | let button = d3.select('#show-state') 113 | stateShown ? button.transition(t).style('color', color) : button.transition(t).style('color', 'white') 114 | 115 | let links = d3.selectAll('path.link'); 116 | links.each(function(d){ 117 | console.log(d) 118 | if (d.source.data.stateType){ 119 | if(d.source.data.stateType.sending && d.target.data.stateType.receiving){ 120 | d3.select(this) 121 | .transition(t) 122 | .style('stroke', linkColor) 123 | } 124 | } 125 | }) 126 | } 127 | } 128 | 129 | // invokes closedUpdateNodes so returned function is passed to event listener 130 | function addShowStateListener() { 131 | document.getElementById('show-state').addEventListener('click', closedUpdateNodes()) 132 | } 133 | 134 | // Grabs all nodes and adds 'click' event listener 135 | function addClickListeners(panelInstance){ 136 | let nodes = d3.selectAll('circle.node'); 137 | let selected; 138 | let originalColor; 139 | nodes.on('click', function (datum, index, nodes) { 140 | if(d3.event.shiftKey) console.log("SHIFT PRESSED") // FOR USE WITH NESTING CHILDREN 141 | if (selected) { 142 | selected.interrupt() 143 | selected.style('fill', originalColor) 144 | } 145 | selected = d3.select(this); 146 | originalColor = selected.attr('fill') 147 | selected.style("fill", '#eee') 148 | // function repeat(){ 149 | // selected.style("fill", '#F6CF63') 150 | // .transition(t) 151 | // .attr('r', 8) 152 | // .transition(t) 153 | // .attr('r', 7) 154 | // .on('end', repeat) 155 | // } 156 | // repeat() 157 | panelInstance.update(datum.data); 158 | }); 159 | } 160 | 161 | 162 | //#################################### Search Function 163 | function highlightNodes(lowerCaseInput) { 164 | let selected = d3.selectAll('circle.node') 165 | .filter(function(d){ 166 | console.log('d data name',d.data.name.toLowerCase()) 167 | console.log('input', lowerCaseInput) 168 | return d.data.name.toLowerCase() === lowerCaseInput; 169 | }) 170 | console.log('selected nodes', selected) 171 | 172 | selected.transition(t) 173 | .attr('r', 10) 174 | .transition(t) 175 | .attr('r', 7) 176 | .transition(t) 177 | .attr('r', 10) 178 | .transition(t) 179 | .attr('r', 7) 180 | .transition(t) 181 | .attr('r', 10) 182 | .transition(t) 183 | .attr('r', 7) 184 | } 185 | 186 | 187 | export { addInteractionsListeners, highlightNodes }; 188 | 189 | // Resources for collapsable trees: 190 | // - http://bl.ocks.org/robschmuecker/7926762 191 | // - https://www.codeproject.com/tips/1021936/creating-vertical-collapsible-tree-with-d-js 192 | -------------------------------------------------------------------------------- /extension/devtools/panel/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Titleeeeee 7 | 8 | 9 | 10 | 11 | 12 |
    13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 |
    23 |
    Center
    24 |
    State
    25 |
    26 | 27 |
    28 | 29 | 30 |
    31 | 32 |
    33 | 34 |
    35 | 36 |
    Realize - State and Hierarchical Visualizer for React
    37 |
    38 | 39 | 40 |
    41 | 42 |
    43 | 44 | 45 |
    46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /extension/devtools/panel/panel.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { data } from './data-example.js'; 3 | import ComponentDisplay from './componentDisplay'; 4 | import { createTree } from './createTree'; 5 | import * as d3 from '../../libraries/d3.js'; 6 | // import search from './search'; 7 | import { highlightNodes } from './interactions'; 8 | 9 | // Instantiate the Panel 10 | 11 | const theInfoPanel = document.getElementById('info-panel'); 12 | const CompDisplay = new ComponentDisplay(theInfoPanel); 13 | 14 | // ########################################## CREATE PORT CONNECTION WITH BACKGROUND.JS 15 | const port = chrome.runtime.connect({ name: 'test' }); 16 | port.postMessage({ 17 | name: 'connect', 18 | tabID: chrome.devtools.inspectedWindow.tabId, 19 | }); 20 | 21 | port.onMessage.addListener((message) => { 22 | // if (!message.data) return; 23 | console.log('message received by panel ', message); 24 | if (typeof message === 'object') { 25 | createTree(message, CompDisplay); 26 | } else { 27 | d3.select('#error-message') 28 | .style('display', 'block') 29 | .text(message) 30 | 31 | } 32 | 33 | }); 34 | 35 | chrome.runtime.sendMessage({ 36 | name: 'inject-script', 37 | tabID: chrome.devtools.inspectedWindow.tabId, 38 | }); 39 | 40 | // For testing 41 | // createTree(data[0], CompDisplay) 42 | let search = document.getElementById('searchInput') 43 | console.log(search) 44 | search.addEventListener("keyup", function(event) { 45 | console.log("Inside event listener") 46 | console.log("event", event) 47 | if (event.keyCode === 13){ 48 | console.log('hit'); 49 | let input = search.value.toLowerCase() 50 | console.log('input', input) 51 | highlightNodes(input) 52 | search.textContent = '' 53 | } 54 | }) 55 | 56 | 57 | // ##################################################### ATTEMPT TO IMPORT FONT!!!! 58 | let font = new FontFace("Ubuntu", "url('ubuntu.woff2')"); 59 | // document.fonts.add(font); 60 | 61 | font.load().then(function(loadedFont) 62 | { 63 | document.fonts.add(loadedFont); 64 | //do something after the font is loaded 65 | console.log('good job the font loaded') 66 | }).catch(function(error) { 67 | // error occurred 68 | }); -------------------------------------------------------------------------------- /extension/devtools/panel/search.js: -------------------------------------------------------------------------------- 1 | // It works with Common JS File 2 | var d3 = require('../../libraries/d3.js'); 3 | var result = document.querySelector('.result'); 4 | // make it all lowercase 5 | function addSearchListener(valuesArray) { 6 | 7 | function autoComplete(input) { 8 | // Grab all nodes use d3.selectalcll 9 | // replicate filter using d3 method -> d3 object of filters 10 | return valuesArray.filter(e =>e.toLowerCase().includes(input.toLowerCase())); 11 | } 12 | 13 | function getValue(val){ 14 | 15 | // if no value, have an empty page, 16 | if(!val){ 17 | result.innerHTML=''; 18 | return 19 | } 20 | 21 | // search goes here 22 | let data = autoComplete(val); 23 | 24 | // append list data 25 | let res = ''; 26 | data.forEach(e=>{ 27 | res += '
  • '+e+'
  • '; 28 | }) 29 | 30 | 31 | result.innerHTML = res; 32 | } 33 | 34 | let searchInput = document.getElementById('searchInput'); 35 | searchInput.addEventListener('keyup', () => { 36 | const HTMLInputElement = document.getElementById('searchInput') 37 | const value= searchInput.value 38 | 39 | getValue(value) 40 | }) 41 | 42 | 43 | } 44 | 45 | 46 | export default addSearchListener; 47 | -------------------------------------------------------------------------------- /extension/devtools/panel/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #242424; 3 | } 4 | 5 | #info-panel { 6 | font-family: 'Helvetica'; 7 | font-size: 1.5em; 8 | } 9 | /* GENERAL STYLES */ 10 | * { 11 | box-sizing: border-box; 12 | text-decoration: none; 13 | margin: 0; 14 | color: white; 15 | } 16 | 17 | /* Main Window and Panels */ 18 | #main-container { 19 | display: flex; 20 | width: 100%; 21 | height: 100%; 22 | background-color: #242424; 23 | } 24 | 25 | #left-panel { 26 | width: 66%; 27 | min-width: 800px; 28 | display: flex; 29 | padding: 20px 5px; 30 | } 31 | 32 | #right-panel { 33 | width: 34%; 34 | min-width: 300px; 35 | height: 100vw; 36 | position: fixed; 37 | right: 0; 38 | top: 0; 39 | display: flex; 40 | border-left: 1px solid grey; 41 | padding: 20px 5px; 42 | flex-direction: column; 43 | } 44 | 45 | #title-panel { 46 | color: white; 47 | } 48 | 49 | #info-title { 50 | color: white; 51 | font-size: 1.5em; 52 | text-align: center; 53 | letter-spacing: 0.05em; 54 | padding: 10px; 55 | opacity: .8; 56 | } 57 | 58 | #info-panel { 59 | display: block; 60 | overflow-y: scroll; 61 | flex-direction: column; 62 | padding: 10px; 63 | background-color: #30353e; 64 | opacity: 0.8; 65 | color: rgb(234, 242, 250); 66 | border-radius: 9px; 67 | /* border: 1px solid rgb(200, 200, 200); */ 68 | scrollbar-width: thin; 69 | scrollbar-color: rgb(150, 150, 150) #30353e; 70 | width: 99%; 71 | min-width: 350px; 72 | min-height: 200px; 73 | max-height: 20%; 74 | /* box-shadow: 0 0 5px #1eabd5; */ 75 | } 76 | 77 | ::-webkit-scrollbar { 78 | width: 12px; /* for vertical scrollbars */ 79 | height: 12px; /* for horizontal scrollbars */ 80 | } 81 | 82 | ::-webkit-scrollbar-track { 83 | background: rgba(0, 0, 0, 0.1); 84 | } 85 | 86 | ::-webkit-scrollbar-thumb { 87 | background: rgba(0, 0, 0, 0.5); 88 | } 89 | 90 | .component-title-bar { 91 | font-size: 2.2em; 92 | font-weight: bold; 93 | text-align: center; 94 | letter-spacing: 0.2em; 95 | line-height: 0.5; 96 | padding-top: 15px; 97 | } 98 | 99 | #state-type { 100 | font-size: 1em; 101 | font-weight: normal; 102 | padding-left: 25px; 103 | } 104 | 105 | .component-display-name { 106 | text-align: center; 107 | letter-spacing: 0.2em; 108 | font-weight: bold; 109 | color: rgb(210, 215, 240); 110 | } 111 | 112 | #state { 113 | color: #1eabd5; 114 | } 115 | 116 | summary { 117 | outline: none; 118 | } 119 | 120 | #searchInput { 121 | border-radius: 9px; 122 | background-color: #30353e; 123 | color: rgb(234, 242, 250); 124 | opacity: 0.8; 125 | outline: none; 126 | border: 0px; 127 | padding: 5px; 128 | } 129 | 130 | /* TREE */ 131 | 132 | #tree { 133 | margin: auto; 134 | overflow: visible; 135 | padding-top: 15px; 136 | } 137 | 138 | .nodes { 139 | fill: #707070; 140 | } 141 | .link { 142 | stroke: #606060; 143 | } 144 | .node text { 145 | font: 12px sans-serif; 146 | fill: white; 147 | } 148 | 149 | .button-bar { 150 | top: 0px; 151 | right: 0px; 152 | } 153 | 154 | #center-tree, 155 | #show-state { 156 | /* font-size: 1.2rem; */ 157 | color: white; 158 | font-size: 1em; 159 | text-align: center; 160 | letter-spacing: 0.2em; 161 | padding: 3px; 162 | opacity: 0.8; 163 | } 164 | 165 | #error-message { 166 | color: #E06C75; 167 | /* margin-left: 30px; 168 | margin-top: 30px; */ 169 | margin-right: 400px; 170 | margin-top:100px; 171 | width: 400px; 172 | height: 400px; 173 | background-color: #242424; 174 | font-size: large; 175 | } 176 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Realize for React", 4 | "version": "1.0.0.0", 5 | "description": "A React component tree visualizer", 6 | 7 | "icons": { 8 | "16": "16.png", 9 | "32": "32.png", 10 | "48": "48.png", 11 | "128": "128.png" 12 | }, 13 | 14 | "permissions": [ 15 | "activeTab", 16 | "" 17 | ], 18 | 19 | "background": { 20 | "scripts": ["background.js"] 21 | }, 22 | 23 | "web_accessible_resources": ["hook.js"], 24 | 25 | "devtools_page": "devtools-root.html" 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactionary", 3 | "version": "1.0.0", 4 | "description": "A React component visualizer and performance optimizer", 5 | "private": true, 6 | "scripts": { 7 | "test": "jest", 8 | "build": "webpack", 9 | "dev": "NODE_ENV=development webpack --mode=development --watch" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/oslabs-beta/REACTionary.git" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/oslabs-beta/REACTionary/issues" 19 | }, 20 | "homepage": "https://github.com/oslabs-beta/REACTionary#readme", 21 | "devDependencies": { 22 | "@babel/preset-env": "^7.10.2", 23 | "babel-jest": "^26.0.1", 24 | "copy-webpack-plugin": "^6.0.1", 25 | "css-loader": "^3.5.3", 26 | "jest": "^26.0.1", 27 | "sass": "^1.26.7", 28 | "sass-loader": "^8.0.2", 29 | "ts-loader": "^7.0.5", 30 | "typescript": "^3.9.5", 31 | "webpack": "^4.43.0", 32 | "webpack-cli": "^3.3.11", 33 | "webpack-dev-server": "^3.11.0", 34 | "webpack-extension-reloader": "^1.1.4" 35 | }, 36 | "dependencies": { 37 | "accessible-autocomplete": "^2.0.2", 38 | "jquery": "^3.5.1", 39 | "jquery-ui-dist": "^1.12.1", 40 | "jsdom": "^16.2.2", 41 | "lodash.throttle": "^4.1.1", 42 | "node-sass": "^4.14.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const ExtensionReloader = require('webpack-extension-reloader'); 4 | 5 | module.exports = { 6 | // Files to bundle 7 | entry: { 8 | bundle: './extension/devtools/panel/panel.js', 9 | // "create-panel": './extension/devtools/create-panel.js' 10 | background: './extension/backend/background.js', 11 | hook: './extension/backend/hook.ts', 12 | }, 13 | // Location to bundle them to 14 | output: { 15 | filename: '[name].js', 16 | path: path.resolve(__dirname, 'build/extension'), 17 | }, 18 | // Modules to load non-jacvascript files 19 | module: { 20 | rules: [ 21 | // CSS Loader 22 | { 23 | test: /\.css$/i, 24 | use: ['style-loader', 'css-loader'], 25 | }, 26 | // SASS Loader 27 | { 28 | test: /\.s[ac]ss$/i, 29 | use: [ 30 | // Creates `style` nodes from JS strings 31 | 'style-loader', 32 | // Translates CSS into CommonJS 33 | 'css-loader', 34 | // Compiles Sass to CSS 35 | 'sass-loader', 36 | ], 37 | }, 38 | { 39 | rules: [ 40 | { 41 | test: /\.tsx?$/, 42 | use: 'ts-loader', 43 | exclude: /node_modules/, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | 50 | plugins: [ 51 | // Copies files to 'build' folder without bundling them 52 | new CopyPlugin({ 53 | patterns: [ 54 | { from: 'extension/manifest.json', to: '../extension/manifest.json' }, 55 | { 56 | from: 'extension/backend/background.js', 57 | to: '../extension/background.js', 58 | }, 59 | { 60 | from: 'extension/devtools/devtools-root.html', 61 | to: '../extension/devtools-root.html', 62 | }, 63 | { 64 | from: 'extension/devtools/create-panel.js', 65 | to: '../extension/create-panel.js', 66 | }, 67 | { 68 | from: 'extension/devtools/panel/panel.html', 69 | to: '../extension/panel.html', 70 | }, 71 | // { from: 'extension/backend/hook.js', to: '../extension/hook.js' }, 72 | { 73 | from: 'extension/backend/content_script.js', 74 | to: '../extension/content_script.js', 75 | }, 76 | { 77 | from: 'extension/devtools/panel/styles.css', 78 | to: '../extension/styles.css', 79 | }, 80 | { 81 | from: 'extension/128.png', 82 | to: '../extension/128.png', 83 | }, 84 | { 85 | from: 'extension/32.png', 86 | to: '../extension/32.png', 87 | }, 88 | { 89 | from: 'extension/16.png', 90 | to: '../extension/16.png', 91 | }, 92 | { 93 | from: 'extension/48.png', 94 | to: '../extension/48.png', 95 | }, 96 | ], 97 | }), 98 | // Enables hot reloading - use npm run dev command 99 | new ExtensionReloader({ 100 | manifest: path.resolve(__dirname, './extension/manifest.json'), 101 | entries: { 102 | bundle: 'bundle', 103 | background: '`background', 104 | }, 105 | }), 106 | ], 107 | 108 | optimization: { 109 | minimize: false 110 | }, 111 | 112 | // devtool: 'cheap-module-source-map', // Needed as to stop Chrome eval errors when using dev server 113 | }; 114 | --------------------------------------------------------------------------------