├── .DS_Store ├── .gitignore ├── README.md ├── __test__ ├── ActionLog.test.js ├── ActionLogBtn.test.js ├── Store.test.js ├── StoreBtn.test.js ├── TreeBtn.test.js └── functionUtility.js ├── babel.config.js ├── extension ├── .DS_Store ├── dist │ └── bundle.js ├── html │ ├── devtools.html │ └── panel.html ├── icons │ ├── .DS_Store │ ├── 128.png │ ├── 16.png │ └── 32.png ├── manifest.json └── scripts │ ├── background.js │ ├── contentScript.js │ ├── devtools.js │ └── injectedScript.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── .DS_Store └── client │ ├── App.jsx │ ├── components │ ├── ActionLog.jsx │ ├── ActionLogBtn.jsx │ ├── Diff.jsx │ ├── NavBar.jsx │ ├── StateSnapshots.jsx │ ├── Store.jsx │ ├── StoreBtn.jsx │ └── TreeBtn.jsx │ ├── d3hierarchy │ └── ReactD3Tree.jsx │ ├── index.html │ ├── index.js │ ├── store │ └── store.js │ └── styles.css ├── tailwind.config.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | extension/dist/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Zusty 3 | 4 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 5 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 6 | ![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) 7 | ![Webpack](https://img.shields.io/badge/webpack-%238DD6F9.svg?style=for-the-badge&logo=webpack&logoColor=black) 8 | ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) 9 | ![D3](https://img.shields.io/badge/d3-red?style=for-the-badge&logo=d3.js) 10 | ![Babel](https://img.shields.io/badge/Babel-F9DC3e?style=for-the-badge&logo=babel&logoColor=black) 11 | ![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) 12 | ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) 13 | ![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white) 14 | 15 | Zusty is a Zustand Dev Tool designed to facilitate the debugging of state management in your applications. It provides state snapshots, action logs, render time metrics, and a component tree showing which components are using state. With this information at hand debugging your Zustand code is easy! 16 | 17 | ## Installation 18 | 19 | Use the package manager to install our middleware - zustymiddleware. 20 | 21 | For JavaScript: 22 | 23 | ```bash 24 | npm i zustymiddleware 25 | ``` 26 | 27 | For TypeScript follow the below command and refer to the instructions here: https://www.npmjs.com/package/zustymiddlewarets?activeTab=readme. 28 | 29 | ```bash 30 | npm i zustymiddlewarets 31 | ``` 32 | 33 | ## Usage 34 | 35 | 1. Import zustymiddleware at the top of your store file 36 | 2. Wrap your store in the middleware 37 | 3. Before you export your store, add window.store = < your store name> 38 | 39 | ```javascript 40 | import zustymiddleware from 'zustymiddleware'; 41 | 42 | const useStore = create( 43 | zustymiddleware((set) => ({ 44 | bears: 0, 45 | increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 46 | removeAllBears: () => set({ bears: 0 }), 47 | })) 48 | ); 49 | 50 | window.store = useStore; 51 | export default useStore; 52 | ``` 53 | 54 | Next, download the Zusty Chrome extension: [link here](https://chromewebstore.google.com/detail/zusty/ckdnkkilcbkocfdpcaohdehnbeaefndo) 55 | 56 | You are now ready to use our tool! 57 | 58 | ## Features 59 | 60 | Upon opening Zusty you will see the action log, as well as state snapshots. Our state snapshot section features a toggle which allows users to switch between a JSON view and a list view depending on what is most convenient. The JSON view allows for easy visualization for nested objects, as well as the ability to collapse items to focus on what is being affected. A timestamp has been added to each snapshot so users can understand when each state change is occurring for debugging: 61 | 62 | ![StateSnapShot](https://github.com/oslabs-beta/Zusty/assets/44410674/b1831e67-0b11-4ac6-b246-e99b43ad33a9) 63 | 64 | 65 | A tree button is available, displaying all the components in the user's application in a convenient tree-like view, enabling a comprehensive overview of the application's state management structure and facilitating efficient debugging and identification of data flow patterns: 66 | 67 | ![Tree](https://github.com/oslabs-beta/Zusty/assets/44410674/73aea79b-dbe5-477e-ad01-de9f46645561) 68 | 69 | 70 | To visualize the action log, time travel, and metrics select the action log button. 71 | 72 | Here you can see each action that is changing state, the state before and after that action was completed, and the render time to update state after this action. Zusty adds dynamic highlighting to make it clear which action is being inspected when viewing previous and current state: 73 | 74 | ![ActionLogTimeTravel](https://github.com/oslabs-beta/Zusty/assets/44410674/611f61f0-e50c-4159-84f9-6e797adc4791) 75 | 76 | 77 | On each action we display either a green, yellow, or red dot indicating the performance of rendering times. We have a toggle option which users can select to see the exact render time to understand which actions are leading to issues if any: 78 | 79 | ![ActionLogMetrics](https://github.com/oslabs-beta/Zusty/assets/44410674/ed336c59-b0af-421b-bc67-80a8deee9727) 80 | 81 | 82 | There is also a store button that allows the user to conveniently see thier zustand store directly in the dev tool, as to see how these actions are actually affecting the state: 83 | 84 | ![Store](https://github.com/oslabs-beta/Zusty/assets/44410674/f082d5f8-a8d4-4853-bbd7-7bb1fc4bd893) 85 | 86 | 87 | 88 | If you would like to test out these features using the demo application you can access the github repository here! 89 | https://github.com/DanaKaplan944/zustand-demo 90 | 91 | 92 | ## Contributing 93 | 94 | Pull requests are welcome. For major changes, please open an issue first 95 | to discuss what you would like to change. 96 | 97 | ## Our Team 98 | 99 | - Adrian Insingo | Github: [@adrianinsingo](https://github.com/adrianinsingo) 100 | - Brian JaeKook Lee | Github: [@JaeBrian](https://github.com/JaeBrian) 101 | - Dana Kaplan | Github: [@DanaKaplan944](https://github.com/DanaKaplan944) 102 | - Nancy Huang | Github: [@itsnancyhuang](https://github.com/itsnancyhuang) 103 | - William Kil | Github: [@shinykoin](https://github.com/shinykoin) 104 | 105 | ## Check Out Our Website and Node Module 106 | - http://zustydev.com/ 107 | - https://www.npmjs.com/package/zustymiddleware 108 | 109 | ## License 110 | 111 | [MIT](https://choosealicense.com/licenses/mit/) 112 | -------------------------------------------------------------------------------- /__test__/ActionLog.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, getByText } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import useStore from '../src/client/store/store'; 5 | import ActionLog from '../src/client/components/ActionLog'; 6 | import { renderTimeCheck } from '../__test__/functionUtility'; 7 | import { handleToggleChange } from '../__test__/functionUtility'; 8 | 9 | describe('testing if ActionLog component renders correctly', () => { 10 | it('should correctly call the handleToggleChange function', () => { 11 | const ActionLog = ({ onChange }) => { 12 | return ( 13 | 14 | ); 15 | }; 16 | 17 | const setShowRenderTimes = jest.fn(); 18 | //check the input if onChange is invoked it sets prev to be true 19 | const handleToggleChange = () => { 20 | setShowRenderTimes((prev) => !prev); 21 | }; 22 | 23 | render(); 24 | 25 | const checkbox = screen.getByRole('checkbox'); 26 | fireEvent.click(checkbox); 27 | expect(setShowRenderTimes).toHaveBeenCalledWith(expect.any(Function)); 28 | fireEvent.click(checkbox); 29 | fireEvent.click(checkbox); 30 | expect(setShowRenderTimes).toHaveBeenCalledTimes(3); 31 | }); 32 | 33 | it('the input tag correctly starts with an empty string', () => { 34 | render(); 35 | const input = screen.getByRole('checkbox'); 36 | expect(input.value).toEqual(''); 37 | expect(input.className).toEqual('sr-only peer'); 38 | }); 39 | 40 | it('should render the ActionLog heading', () => { 41 | render(); 42 | expect(screen.getByText('Action Log')).toBeInTheDocument; 43 | }); 44 | 45 | it('should render the State Before Action heading', () => { 46 | render(); 47 | expect(screen.getByText('State Before Action:')).toBeInTheDocument; 48 | }); 49 | 50 | it('should render the State After Action heading', () => { 51 | render(); 52 | expect(screen.getByText('State After Action:')).toBeInTheDocument; 53 | }); 54 | }); 55 | 56 | describe('renderTimeCheck should return the correct class depending on input', () => { 57 | it('returns bg-red-500 if input is greater than 750ms', () => { 58 | const mockRenderTimeCheck = () => { 59 | return renderTimeCheck({ actionCompleteTime: 800 }); 60 | }; 61 | expect(mockRenderTimeCheck()).toEqual('bg-red-500'); 62 | }); 63 | 64 | it('returns bg-green-500 if input is less than 350ms', () => { 65 | const mockRenderTimeCheck = () => { 66 | return renderTimeCheck({ actionCompleteTime: 100 }); 67 | }; 68 | expect(mockRenderTimeCheck()).toEqual('bg-green-500'); 69 | }); 70 | 71 | it('returns bg-yellow-500 if input is between 350ms and 750ms', () => { 72 | const mockRenderTimeCheck = () => { 73 | return renderTimeCheck({ actionCompleteTime: 500 }); 74 | }; 75 | expect(mockRenderTimeCheck()).toEqual('bg-yellow-500'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /__test__/ActionLogBtn.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | 5 | import '@testing-library/jest-dom'; 6 | import ActionLogBtn from '../src/client/components/ActionLogBtn'; 7 | 8 | describe('ActionLogBtn', () => { 9 | it('should redner a button with the text Action Log', () => { 10 | render( {}} className="" />); 11 | 12 | const button = screen.getByRole('button', { name: 'Action Log' }); 13 | expect(button).toHaveTextContent('Action Log'); 14 | }); 15 | 16 | it('calls onClick prop when clicked', () => { 17 | const handleClick = jest.fn(); 18 | render(); 19 | const button = screen.getByRole('button', { name: 'Action Log' }); 20 | fireEvent.click(button); 21 | expect(handleClick).toHaveBeenCalledTimes(1); 22 | }); 23 | 24 | it('sets the activeTab to TreeBtn when clicked', () => { 25 | const setActiveTab = jest.fn(); 26 | 27 | const handleClick = () => { 28 | setActiveTab('actionLog'); 29 | }; 30 | render(); 31 | 32 | const button = screen.getByRole('button', { name: 'Action Log' }); 33 | fireEvent.click(button); 34 | 35 | expect(setActiveTab).toHaveBeenCalledWith('actionLog'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__test__/Store.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import useStore from '../src/client/store/store'; 5 | import Store from '../src/client/components/Store'; 6 | 7 | describe('Store Component', () => { 8 | it('renders the store correctly', () => { 9 | render(); 10 | expect(screen.getByText('Store:')).toBeInTheDocument; 11 | }); 12 | 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /__test__/StoreBtn.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import StoreBtn from '../src/client/components/StoreBtn'; 5 | import useStore from '../src/client/store/store'; 6 | 7 | describe('StoreBtn', () => { 8 | it('renders a button with the text Store', () => { 9 | render( {}} className="" />); 10 | const button = screen.getByRole('button', { name: 'Store' }); 11 | expect(button).toHaveTextContent('Store'); 12 | }); 13 | 14 | it('calls onClick prop when clicked', () => { 15 | const handleClick = jest.fn(); 16 | render(); 17 | const button = screen.getByRole('button', { name: 'Store' }); 18 | fireEvent.click(button); 19 | expect(handleClick).toHaveBeenCalledTimes(1); 20 | }); 21 | 22 | it('sets the activeTab to TreeBtn when clicked', () => { 23 | const setActiveTab = jest.fn(); 24 | 25 | const handleClick = () => { 26 | setActiveTab('storeBtn'); 27 | }; 28 | render(); 29 | 30 | const button = screen.getByRole('button', { name: 'Store' }); 31 | fireEvent.click(button); 32 | 33 | expect(setActiveTab).toHaveBeenCalledWith('storeBtn'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__test__/TreeBtn.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import TreeBtn from '../src/client/components/TreeBtn'; 5 | import useStore from '../src/client/store/store'; 6 | import { __esModule } from 'url-loader/dist'; 7 | 8 | describe('Testing TreeBtn', () => { 9 | it('renders a button with the text "Tree"', () => { 10 | render( {}} className="" />); 11 | 12 | const button = screen.getByRole('button', { name: 'Tree' }); 13 | expect(button).toHaveTextContent('Tree'); 14 | }); 15 | 16 | it('calls onClick prop when clicked', () => { 17 | const handleClick = jest.fn(); 18 | render(); 19 | const button = screen.getByRole('button', { name: 'Tree' }); 20 | fireEvent.click(button); 21 | expect(handleClick).toHaveBeenCalledTimes(1); 22 | }); 23 | 24 | it('sets the activeTab to TreeBtn when clicked', () => { 25 | const setActiveTab = jest.fn(); 26 | 27 | const handleClick = () => { 28 | setActiveTab('tree'); 29 | }; 30 | render(); 31 | 32 | const button = screen.getByRole('button', { name: 'Tree' }); 33 | fireEvent.click(button); 34 | 35 | expect(setActiveTab).toHaveBeenCalledWith('tree'); 36 | }); 37 | }); 38 | 39 | //act - more accurately implement react env for async functions 40 | //maybe wrap around act 41 | -------------------------------------------------------------------------------- /__test__/functionUtility.js: -------------------------------------------------------------------------------- 1 | export const renderTimeCheck = (diffObj) => { 2 | if (diffObj.actionCompleteTime >= 750) { 3 | return 'bg-red-500'; 4 | } else if ( 5 | diffObj.actionCompleteTime < 750 && 6 | diffObj.actionCompleteTime > 300 7 | ) { 8 | return 'bg-yellow-500'; 9 | } else { 10 | return 'bg-green-500'; 11 | } 12 | }; 13 | 14 | export const handleToggleChange = () => { 15 | setShowRenderTimes((prev) => !prev); 16 | }; 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ['@babel/preset-react', { runtime: 'automatic' }], 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /extension/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/extension/.DS_Store -------------------------------------------------------------------------------- /extension/html/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /extension/html/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /extension/icons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/extension/icons/.DS_Store -------------------------------------------------------------------------------- /extension/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/extension/icons/128.png -------------------------------------------------------------------------------- /extension/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/extension/icons/16.png -------------------------------------------------------------------------------- /extension/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/extension/icons/32.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "Zusty", 5 | "version": "6.0", 6 | 7 | "description": "Zustand Developer Tool for Debugging", 8 | "author": "Adrian Insingo, Jaekook Brian Lee, Dana Kaplan, Nancy Huang, William Kil", 9 | 10 | "devtools_page": "/html/devtools.html", 11 | 12 | "background": { 13 | "service_worker": "/scripts/background.js" 14 | }, 15 | "content_scripts": [ 16 | { 17 | "matches": [""], 18 | "js": ["/scripts/contentScript.js"], 19 | "all_frames": true 20 | } 21 | ], 22 | "web_accessible_resources": [ 23 | { 24 | "resources": [ 25 | "/scripts/contentScript.js", 26 | "/scripts/injectedScript.js", 27 | "styles.css" 28 | ], 29 | "matches": [""] 30 | } 31 | ], 32 | "permissions": ["storage", "tabs", "activeTab", "scripting"], 33 | "host_permissions": [""], 34 | "externally_connectable": { 35 | "matches": [""] 36 | }, 37 | "icons": { 38 | "128": "./icons/128.png", 39 | "32": "./icons/32.png", 40 | "16": "./icons/16.png" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /extension/scripts/background.js: -------------------------------------------------------------------------------- 1 | // declare a background port 2 | let backgroundPort; 3 | 4 | // getting message from content script 5 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 6 | if (request.type === 'REACT_COMPONENTS' && backgroundPort) { 7 | backgroundPort.postMessage({ 8 | body: 'treeComponents', 9 | type: request.type, 10 | data: request.data, 11 | }); 12 | } 13 | 14 | // getting message from zustymiddleware 15 | if (request.body === 'actionAndStateSnapshot' && backgroundPort) { 16 | backgroundPort.postMessage({ 17 | body: request.body, 18 | action: request.action, 19 | actionCompleteTime: request.actionCompleteTime, 20 | prevState: request.prevState, 21 | nextState: request.nextState, 22 | store: request.store, 23 | }); 24 | } 25 | }); 26 | 27 | // Chrome method to connect port (from App.jsx) to the port Chrome is running on 28 | chrome.runtime.onConnect.addListener((port) => { 29 | backgroundPort = port; 30 | 31 | backgroundPort.onMessage.addListener((message, sender, sendResponse) => { 32 | if (message.body === 'runContent') { 33 | chrome.scripting.executeScript({ 34 | target: { tabId: tabs[0].id }, 35 | files: ['./scripts/contentScript.js'], 36 | }); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /extension/scripts/contentScript.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('message', (event) => { 2 | chrome.runtime.sendMessage(event.data); 3 | }); 4 | 5 | // inject the injectedScript.js 6 | const injectScript = (file, node) => { 7 | const body0 = document.getElementsByTagName(node)[0]; 8 | const s0 = document.createElement('script'); 9 | s0.setAttribute('type', 'text/javascript'); 10 | s0.setAttribute('src', chrome.runtime.getURL(file)); 11 | body0.appendChild(s0); 12 | }; 13 | 14 | //content scripts allow you to grab data from the current web page you are viewing 15 | injectScript('./scripts/injectedScript.js', 'body'); 16 | -------------------------------------------------------------------------------- /extension/scripts/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Zusty', null, '/html/panel.html', null); 2 | -------------------------------------------------------------------------------- /extension/scripts/injectedScript.js: -------------------------------------------------------------------------------- 1 | // grab the users application store 2 | const store = window.store; 3 | 4 | const findReactComponents = (element) => { 5 | const components = []; 6 | 7 | const findComponentNames = (fiberNode) => { 8 | // Check if the node is a React component and has a name, excluding 'div' and 'h1' 9 | const isComponent = 10 | fiberNode && fiberNode.elementType && fiberNode.elementType.name; 11 | const isNotHtmlElement = 12 | fiberNode && fiberNode.type && typeof fiberNode.type === 'function'; 13 | 14 | if (isComponent || isNotHtmlElement) { 15 | // Only push component names that are not 'div' or 'h1' 16 | const name = fiberNode.elementType.name || fiberNode.type.name; 17 | components.push(name); 18 | } 19 | 20 | if (fiberNode && fiberNode.stateNode) { 21 | let childFiber = fiberNode.child; 22 | while (childFiber) { 23 | findComponentNames(childFiber); 24 | childFiber = childFiber.sibling; 25 | } 26 | } 27 | }; 28 | 29 | Array.from(element.children).forEach((child) => { 30 | // Find the internal React fiber node 31 | const key = Object.keys(child).find((key) => 32 | key.startsWith('__reactFiber$') 33 | ); 34 | const fiberNode = child[key]; 35 | if (fiberNode) { 36 | findComponentNames(fiberNode); 37 | } 38 | }); 39 | 40 | return components; 41 | }; 42 | 43 | const rootElement = document.getElementById('root'); 44 | const reactComponents = findReactComponents(rootElement); 45 | 46 | const hierarchyConv = (state) => { 47 | const rootNode = { 48 | name: 'STATE', 49 | children: [], 50 | }; 51 | 52 | const addNodeToTree = (key, value, parent) => { 53 | // Check if the value is an object and not null 54 | if (typeof value === 'object' && value !== null) { 55 | // Create a new node for the object 56 | const newNode = { name: key, children: [] }; 57 | // recurse add children for each entry in the object 58 | 59 | // If the new node has no children, don't add an empty children array 60 | if (newNode.children.length > 0) { 61 | parent.children.push(newNode); 62 | } else { 63 | // If an object is empty, represent it as a node with its name 64 | parent.children.push({ name: key }); 65 | } 66 | } else { 67 | // If the value is not an object, represent it as a node with the value as its name 68 | parent.children.push({ name: `${String(value)}` }); 69 | } 70 | }; 71 | 72 | // Initialize the tree construction 73 | Object.entries(state).forEach(([key, value]) => { 74 | addNodeToTree(key, value, rootNode); 75 | }); 76 | 77 | return rootNode; 78 | }; 79 | 80 | const d3hierarchy2 = hierarchyConv(reactComponents); 81 | 82 | setInterval(() => { 83 | window.postMessage({ 84 | type: 'REACT_COMPONENTS', 85 | data: JSON.stringify(d3hierarchy2), 86 | }); 87 | }, 1000); 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js", 8 | "watch": "webpack -w --config webpack.config.js", 9 | "client": "webpack serve --open ", 10 | "dev": "npm run client", 11 | "test": "jest" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@babel/core": "^7.23.2", 18 | "@babel/plugin-transform-runtime": "^7.23.3", 19 | "@babel/preset-env": "^7.23.5", 20 | "@babel/preset-react": "^7.23.3", 21 | "@testing-library/jest-dom": "^6.1.5", 22 | "@testing-library/react": "^14.1.2", 23 | "@testing-library/user-event": "^14.5.1", 24 | "babel-jest": "^29.7.0", 25 | "babel-loader": "^9.1.3", 26 | "css-loader": "^6.8.1", 27 | "file-loader": "^6.2.0", 28 | "html-webpack-plugin": "^5.5.3", 29 | "jest": "^29.7.0", 30 | "jest-environment-jsdom": "^29.7.0", 31 | "postcss": "^8.4.31", 32 | "postcss-import": "^15.1.0", 33 | "postcss-loader": "^7.3.3", 34 | "postcss-preset-env": "^9.3.0", 35 | "react-test-renderer": "^18.2.0", 36 | "sass": "^1.69.5", 37 | "sass-loader": "^13.3.2", 38 | "style-loader": "^3.3.3", 39 | "tailwindcss": "^3.3.5", 40 | "webpack": "^5.89.0", 41 | "webpack-bundle-analyzer": "^4.9.1", 42 | "webpack-cli": "^5.1.4", 43 | "webpack-dev-server": "^4.15.1" 44 | }, 45 | "dependencies": { 46 | "@microlink/react-json-view": "^1.23.0", 47 | "autoprefixer": "^10.4.16", 48 | "d3": "^7.8.5", 49 | "dotenv": "^16.3.1", 50 | "node-fetch": "^3.3.2", 51 | "react": "^18.2.0", 52 | "react-d3-tree": "^3.6.1", 53 | "react-dom": "^18.2.0", 54 | "react-router-dom": "^6.18.0", 55 | "react-uuid": "^2.0.0", 56 | "url-loader": "^4.1.1", 57 | "zustand": "^4.4.6" 58 | }, 59 | "browser": { 60 | "fs": false, 61 | "os": false, 62 | "path": false 63 | }, 64 | "jest": { 65 | "testEnvironment": "jsdom", 66 | "moduleFileExtensions": [ 67 | "js", 68 | "jsx" 69 | ], 70 | "transform": { 71 | "^.+\\.jsx?$": "babel-jest" 72 | }, 73 | "testRegex": "(/test/.*|(\\.|/)(test|spec))\\.jsx?$", 74 | "presets": [ 75 | "@babel/preset-react" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss/nesting': {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | 'postcss-preset-env': { 8 | features: { 'nesting-rules': false }, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Zusty/083dec0f5b5f692c7e4a7b693c64dff93eaab5b7/src/.DS_Store -------------------------------------------------------------------------------- /src/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import useStore from './store/store'; 3 | import Navigation from './components/NavBar'; 4 | import ActionLog from './components/ActionLog'; 5 | import StateSnapshots from './components/StateSnapshots'; 6 | import Store from './components/Store'; 7 | import ReactD3Tree from './d3hierarchy/ReactD3Tree'; 8 | 9 | const App = () => { 10 | const activeTab = useStore((state) => state.activeTab); 11 | const { addStateSnapshot, addDiffSnapshot, setStore, setD3data } = useStore(); 12 | 13 | let connected = false; 14 | let port; 15 | let count = 0; 16 | 17 | // listening to messages from background.js 18 | const setUpExtensionListener = () => { 19 | if (!connected) { 20 | port = chrome.runtime.connect(); 21 | connected = true; 22 | } 23 | 24 | if (connected) { 25 | port.onMessage.addListener((message, sender, sendResponse) => { 26 | if (message.body === 'actionAndStateSnapshot') { 27 | const store = JSON.parse(message.store); 28 | setStore(store); 29 | 30 | const timestamp = new Date().toLocaleString(); 31 | let currentStateSnapshot = JSON.parse(message.nextState); 32 | const currentStateWithTimestamp = { 33 | timestamp, 34 | stateSnapshot: currentStateSnapshot, 35 | }; 36 | addStateSnapshot(currentStateWithTimestamp); 37 | 38 | let prevState = JSON.parse(message.prevState); 39 | let nextState = JSON.parse(message.nextState); 40 | const currentDiffWithTimestamp = { 41 | action: message.action, 42 | actionCompleteTime: message.actionCompleteTime, 43 | prevState, 44 | nextState, 45 | }; 46 | 47 | addDiffSnapshot(currentDiffWithTimestamp); 48 | } 49 | 50 | if (message.body === 'treeComponents' && count < 1) { 51 | let data = JSON.parse(message.data); 52 | setD3data(data); 53 | count++; 54 | } 55 | }); 56 | } 57 | }; 58 | 59 | // run the set up extension listner when the page loads 60 | useEffect(() => { 61 | setUpExtensionListener(); 62 | }, []); 63 | 64 | return ( 65 |
66 |
67 | 68 | 69 |
70 |
71 | {activeTab === 'tree' && } 72 | {activeTab === 'actionLog' && } 73 | {activeTab === 'timeTravel' && } 74 | {activeTab === 'storeBtn' && } 75 |
76 |
77 | ); 78 | }; 79 | 80 | export default App; 81 | -------------------------------------------------------------------------------- /src/client/components/ActionLog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import uuid from 'react-uuid'; 3 | import useStore from '../store/store'; 4 | import Diff from './Diff'; 5 | 6 | const ActionLog = () => { 7 | const { diffArray, setPrevState, setNextState } = useStore(); 8 | // selectedDiv for dyamically highlighting elements when diff button is clicked 9 | const [selectedDiv, setSelectedDiv] = useState(null); 10 | // showRenderTimes for toggle to show or hide metrics 11 | const [showRenderTimes, setShowRenderTimes] = useState(false); 12 | 13 | const handleToggleChange = () => { 14 | setShowRenderTimes((prev) => !prev); 15 | }; 16 | 17 | const handleDiffButtonClick = (diffObj) => { 18 | // Setting the previous and next state based on the component where diff is clicked 19 | setPrevState(diffObj.prevState); 20 | setNextState(diffObj.nextState); 21 | 22 | // set the selected div to be diff Obj for dynamic highlighting 23 | setSelectedDiv(diffObj); 24 | }; 25 | 26 | const renderTimeCheck = (diffObj) => { 27 | if (diffObj.actionCompleteTime >= 750) { 28 | return 'bg-red-500'; 29 | } else if ( 30 | diffObj.actionCompleteTime < 750 && 31 | diffObj.actionCompleteTime > 300 32 | ) { 33 | return 'bg-yellow-500'; 34 | } else { 35 | return 'bg-green-500'; 36 | } 37 | }; 38 | 39 | return ( 40 |
41 | {/* Toggle Switch */} 42 | 54 | {/* Action Log Section */} 55 | 56 |
57 |
66 |

67 | Action Log 68 |

69 | 70 |
71 |
76 | {/* Mapping over diffArray to display each diff */} 77 | {diffArray.map((diffObj) => ( 78 |
87 | {/* Metrics status circle */} 88 |
93 |

94 | {diffObj.action} 95 |

96 | {/* Showing render times if toggle switched */} 97 | {showRenderTimes && ( 98 |

99 | {diffObj.actionCompleteTime < 1 100 | ? `< 1 ms` 101 | : `${diffObj.actionCompleteTime.toFixed(2)} ms`} 102 |

103 | )} 104 | 113 |
114 | ))} 115 |
116 |
117 |
118 | {/* Rendering the Diff component */} 119 |
120 | 121 |
122 |
123 |
124 | ); 125 | }; 126 | 127 | export default ActionLog; 128 | -------------------------------------------------------------------------------- /src/client/components/ActionLogBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useStore from '../store/store'; 3 | 4 | const ActionLogBtn = ({ onClick, className }) => { 5 | const { setActiveTab } = useStore(); 6 | 7 | const handleClick = () => { 8 | setActiveTab('actionLog'); 9 | onClick(); 10 | }; 11 | 12 | return ( 13 |
14 | 20 |
21 | ); 22 | }; 23 | 24 | export default ActionLogBtn; 25 | -------------------------------------------------------------------------------- /src/client/components/Diff.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from '@microlink/react-json-view'; 3 | import useStore from '../store/store'; 4 | 5 | const Diff = () => { 6 | const { prevState, nextState } = useStore(); 7 | 8 | const renderObjectProperties = (obj) => { 9 | if (obj && typeof obj === 'object') { 10 | return ( 11 | 19 | ); 20 | } 21 | }; 22 | 23 | const containerStyle = { 24 | height: 'calc(47vh - 3rem)', 25 | overflow: 'auto', 26 | }; 27 | 28 | return ( 29 |
30 |

31 | State Before Action: 32 |

33 |
37 | {renderObjectProperties(prevState)} 38 |
39 |

40 | State After Action: 41 |

42 |
46 | {renderObjectProperties(nextState)} 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Diff; 53 | -------------------------------------------------------------------------------- /src/client/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeBtn from './TreeBtn'; 3 | import StoreBtn from './StoreBtn'; 4 | import ActionLogBtn from './ActionLogBtn'; 5 | import useStore from '../store/store'; 6 | 7 | const Navigation = () => { 8 | const { storeButton, treeButton, actionButton, setActiveButton } = useStore(); 9 | //props being passed down to their respective button components for highlighting 10 | return ( 11 |
12 | setActiveButton('actionButton')} 14 | className={`${ 15 | actionButton ? 'bg-orange-500' : 'bg-light-codebg' 16 | } flex-grow`} 17 | /> 18 | setActiveButton('treeButton')} 20 | className={`${ 21 | treeButton ? 'bg-orange-500' : 'light-codebg' 22 | } flex-grow`} 23 | /> 24 | setActiveButton('storeButton')} 26 | className={`${ 27 | storeButton ? 'bg-orange-500' : 'bg-light-codebg' 28 | } flex-grow`} 29 | /> 30 |
31 | ); 32 | }; 33 | 34 | export default Navigation; 35 | -------------------------------------------------------------------------------- /src/client/components/StateSnapshots.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactJson from '@microlink/react-json-view'; 3 | import useStore from '../store/store'; 4 | import uuid from 'react-uuid'; 5 | 6 | const StateSnapshots = () => { 7 | const { stateSnapshotArray } = useStore(); 8 | 9 | // To handle toggling to JSON format display 10 | const [showAsJSON, setShowAsJSON] = useState(true); // Set to true for default JSON view 11 | 12 | const handleToggleChange = () => { 13 | setShowAsJSON((prev) => !prev); 14 | }; 15 | 16 | return ( 17 |
21 | {/* Toggle Switch */} 22 | 34 | {/* State Snapshot Section */} 35 |

36 | State Snapshots 37 |

38 | {/* Map through snapshots to display in either JSON format or list view depending on toggle switch */} 39 |
40 | {stateSnapshotArray.map((el) => ( 41 |
45 |
    46 |
  • 50 | Timestamp: 51 | {`${new Date(el.timestamp).toLocaleString()}`} 52 |
  • 53 | {showAsJSON ? ( 54 | 62 | ) : ( 63 | Object.keys(el.stateSnapshot).map((key) => ( 64 |
  • 68 | {`${key}:`} 69 | {`${JSON.stringify(el.stateSnapshot[key])}`} 70 |
  • 71 | )) 72 | )} 73 |
74 |
75 | ))} 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default StateSnapshots; 82 | -------------------------------------------------------------------------------- /src/client/components/Store.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useStore from '../store/store'; 3 | import ReactJson from '@microlink/react-json-view'; 4 | 5 | const Store = () => { 6 | const { store } = useStore(); 7 | 8 | const textChange = () => { 9 | for (let key in store) { 10 | if (store[key] === '[object Object]') { 11 | store[key] = '{}'; 12 | } 13 | } 14 | }; 15 | textChange(); 16 | 17 | return ( 18 | <> 19 |

20 | Store: 21 |

22 |
26 | 34 |
35 | 36 | ); 37 | }; 38 | 39 | export default Store; 40 | -------------------------------------------------------------------------------- /src/client/components/StoreBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useStore from '../store/store'; 3 | 4 | const StoreBtn = ({ onClick, className }) => { 5 | const { setActiveTab } = useStore(); 6 | 7 | const handleClick = () => { 8 | setActiveTab('storeBtn'); 9 | onClick(); 10 | }; 11 | return ( 12 |
13 | 19 |
20 | ); 21 | }; 22 | 23 | export default StoreBtn; 24 | -------------------------------------------------------------------------------- /src/client/components/TreeBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useStore from '../store/store'; 3 | 4 | const TreeBtn = ({ onClick, className }) => { 5 | const { setActiveTab } = useStore(); 6 | 7 | const handleClick = () => { 8 | setActiveTab('tree'); 9 | onClick(); 10 | }; 11 | 12 | return ( 13 |
14 | 20 |
21 | ); 22 | }; 23 | 24 | export default TreeBtn; 25 | -------------------------------------------------------------------------------- /src/client/d3hierarchy/ReactD3Tree.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tree from 'react-d3-tree'; 3 | import useStore from '../store/store'; 4 | 5 | const ReactD3Tree = () => { 6 | const { d3data } = useStore(); 7 | 8 | return ( 9 |
13 | 26 |
27 | ); 28 | }; 29 | 30 | export default ReactD3Tree; 31 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Zusty 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './styles.css'; 4 | import App from './App.jsx'; 5 | 6 | const root = createRoot(document.getElementById('root')); 7 | root.render(); 8 | -------------------------------------------------------------------------------- /src/client/store/store.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | const useStore = create((set) => ({ 4 | prevState: null, 5 | nextState: null, 6 | stateSnapshotArray: [], 7 | actionSnapshotArray: [], 8 | diffArray: [], 9 | activeTab: 'actionLog', 10 | store: {}, 11 | storeButton: false, 12 | treeButton: false, 13 | actionButton: true, 14 | d3data: {}, 15 | setActiveButton: (buttonName) => { 16 | set(() => ({ 17 | storeButton: buttonName === 'storeButton', 18 | treeButton: buttonName === 'treeButton', 19 | actionButton: buttonName === 'actionButton', 20 | })); 21 | }, 22 | setActiveTab: (activeTab) => set({ activeTab }), 23 | addStateSnapshot: (snapshot) => 24 | set((state) => ({ 25 | stateSnapshotArray: [...state.stateSnapshotArray, snapshot], 26 | })), 27 | addActionSnapshot: (action) => 28 | set((state) => ({ 29 | actionSnapshotArray: [...state.actionSnapshotArray, action], 30 | })), 31 | addDiffSnapshot: (diff) => 32 | set((state) => ({ 33 | diffArray: [...state.diffArray, diff], 34 | })), 35 | setPrevState: (pState) => set({ prevState: pState }), 36 | setNextState: (nState) => set({ nextState: nState }), 37 | setStore: (inputStore) => set({ store: inputStore }), 38 | setD3data: (data) => set({ d3data: data }), 39 | })); 40 | 41 | export default useStore; 42 | -------------------------------------------------------------------------------- /src/client/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* D3 link */ 6 | .link { 7 | stroke: #999; 8 | stroke-width: 2px; 9 | } 10 | 11 | .diffClass { 12 | position: sticky; 13 | top: 0px; 14 | overflow: scroll; 15 | } 16 | 17 | .node__root > circle { 18 | fill: #fcc48d; 19 | stroke: none; 20 | r: 15; 21 | } 22 | 23 | #treeWrapper path { 24 | stroke: white; 25 | } 26 | #treeWrapper text { 27 | fill: white; 28 | } 29 | .node__branch > circle { 30 | fill: palevioletred; 31 | } 32 | 33 | .node__leaf > circle { 34 | fill: #fd8b18; 35 | stroke: none; 36 | r: 15; 37 | } 38 | .custom-width { 39 | width: 580px; 40 | } 41 | 42 | ::-webkit-scrollbar { 43 | width: 10px; 44 | background-color: #332930; 45 | } 46 | 47 | ::-webkit-scrollbar-track { 48 | background-color: #332930; 49 | } 50 | 51 | ::-webkit-scrollbar-thumb { 52 | background-color: #5b5459; 53 | border-radius: 3px; 54 | border: 2px solid #332930; 55 | } 56 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const colors = require('tailwindcss/colors'); 3 | 4 | module.exports = { 5 | content: ['./src/client/**/*.{html,js,jsx}'], 6 | theme: { 7 | colors: { 8 | ...colors, 9 | 'code-bg': '#332930', 10 | 'light-codebg': '#5B5459', 11 | 'code-o': '#fd8b18', 12 | 'dk-navy': '#0f172a', 13 | 'lt-grey': '#94a3b8', 14 | 'dk-white': '#ededed', 15 | white: '#FFFFFF', 16 | blue: '#38bdf8', 17 | }, 18 | 19 | screens: {}, 20 | extend: {}, 21 | }, 22 | plugins: ['postcss-preset-env'], 23 | }; 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const config = { 5 | mode: 'development', 6 | entry: { bundle: path.resolve(__dirname, 'src/client/index.js') }, 7 | output: { 8 | path: path.join(__dirname, 'extension/dist'), 9 | filename: 'bundle.js', 10 | publicPath: '/', 11 | }, 12 | devtool: 'inline-source-map', 13 | devServer: { 14 | static: { 15 | directory: path.join(__dirname, 'dist'), 16 | publicPath: '/', 17 | }, 18 | port: 3000, 19 | open: true, 20 | hot: true, 21 | compress: true, 22 | historyApiFallback: true, 23 | }, 24 | resolve: { 25 | extensions: ['.js', '.jsx'], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.css$/i, 31 | use: ['style-loader', 'css-loader', 'postcss-loader'], 32 | }, 33 | { 34 | test: /\.(js|jsx)$/, 35 | exclude: /node_modules/, 36 | use: { 37 | loader: 'babel-loader', 38 | options: { 39 | presets: ['@babel/preset-env', '@babel/preset-react'], 40 | plugins: ['@babel/plugin-transform-runtime'], 41 | }, 42 | }, 43 | }, 44 | { 45 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 46 | 47 | type: 'asset/resource', 48 | }, 49 | ], 50 | }, 51 | plugins: [ 52 | new HtmlWebpackPlugin({ 53 | title: 'Development', 54 | template: path.join(__dirname, './src/client/index.html'), 55 | }), 56 | ], 57 | }; 58 | 59 | module.exports = config; 60 | --------------------------------------------------------------------------------