├── 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 | 
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 | 
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 |
38 | 1
39 | 2
40 | 3
41 | `;
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 |
61 |
62 | Array
63 |
64 | 1
65 | 2
66 |
67 |
68 |
69 |
70 |
71 | Array
72 |
73 | 3
74 | 4
75 |
76 |
77 |
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 |
126 | a: 1
127 | b:
128 |
129 | Object
130 |
133 |
134 |
135 | `;
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 |
150 |
151 |
152 | Array
153 |
154 | 1
155 | 2
156 | 3
157 |
158 |
159 |
160 | `;
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 |
175 |
176 |
177 | Array
178 |
179 | a
180 | b
181 | c
182 |
183 |
184 |
185 | 2
186 | 3
187 | `;
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 |
200 | a: 1
201 | b: 2
202 | `;
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 |
217 | 1
218 | hello
219 | `;
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 |
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 |
--------------------------------------------------------------------------------