diff --git a/README.md b/README.md index 7534d0384..b12b4a9f0 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ You can view your application's file structure and click on a snapshot to view your app's state. State can be visualized in a Component Graph, JSON Tree, or Performance Graph. Snapshot history can be visualized in the History tab. -The Web Metrics tab provides some useful metrics for site performance. The accessibility tab +The Web Metrics tab provides some useful metrics for site performance. The accessibility tab visualizes an app's accessibility tree per state change. Snapshots can be compared with the previous snapshot, which can be viewed in Diff mode.
@@ -89,7 +89,7 @@ Download the recorded snapshots as a JSON file and upload them to access state t


-### 🔹 Reconnect and Status +### 🔹 and Status If Reactime loses its connection to the tab you're monitoring, simply click the "reconnect" button to resume your work. You'll notice a circle located to the right of the button, which will appear as either red (indicating disconnection) or green (signifying a successful reconnection).
diff --git a/demo-app-next/next-env.d.ts b/demo-app-next/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/demo-app-next/next-env.d.ts +++ b/demo-app-next/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/demo-app-next/tsconfig.json b/demo-app-next/tsconfig.json index 093985aaf..1203d7cb2 100644 --- a/demo-app-next/tsconfig.json +++ b/demo-app-next/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -12,8 +16,15 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/demo-app/package.json b/demo-app/package.json index 17ed638c0..4003e82fa 100644 --- a/demo-app/package.json +++ b/demo-app/package.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@babel/core": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.16.7", "@babel/preset-react": "^7.16.7", "@types/express": "^4.17.13", @@ -17,6 +18,7 @@ "@types/react-dom": "^17.0.19", "babel-loader": "^8.2.3", "copy-webpack-plugin": "^10.2.0", + "core-js": "^3.39.0", "css-loader": "^6.5.1", "html-webpack-plugin": "^5.5.0", "node": "^16.0.0", diff --git a/demo-app/src/client/Components/Buttons.tsx b/demo-app/src/client/Components/Buttons.tsx index b27abfe24..60235aa92 100644 --- a/demo-app/src/client/Components/Buttons.tsx +++ b/demo-app/src/client/Components/Buttons.tsx @@ -1,22 +1,82 @@ -import React from 'react'; -import Increment from './Increment'; +import React, { Component, useState } from 'react'; -function Buttons(): JSX.Element { - const buttons = []; - for (let i = 0; i < 4; i++) { - buttons.push(); +type ButtonProps = { + id: string; + label: string; + color?: string; + initialCount?: number; +}; + +type IncrementClassState = { + count: number; +}; + +class IncrementClass extends Component { + state = { + count: this.props.initialCount || 0, + }; + + handleClick = (): void => { + this.setState((prevState: IncrementClassState) => ({ + count: prevState.count + 1, + })); + }; + + render(): JSX.Element { + return ( +
+ +
+ ); } +} + +const IncrementFunction = (props: ButtonProps): JSX.Element => { + const [count, setCount] = useState(props.initialCount || 0); + + const handleClick = (): void => { + setCount((prev) => prev + 1); + }; return ( -
-

Stateful Buttons

-

- These buttons are functional components that each manage their own state with the useState - hook. -

- {buttons} +
+
); +}; + +class Buttons extends Component { + render(): JSX.Element { + return ( +
+

Mixed State Counter

+

First two buttons use class components, last two use function components.

+ + + + +
+ ); + } } export default Buttons; diff --git a/demo-app/src/client/Components/FunctionalReducerCounter.tsx b/demo-app/src/client/Components/FunctionalReducerCounter.tsx new file mode 100644 index 000000000..85effd454 --- /dev/null +++ b/demo-app/src/client/Components/FunctionalReducerCounter.tsx @@ -0,0 +1,231 @@ +import React, { useState, useReducer } from 'react'; + +type CounterProps = { + initialCount?: number; + step?: number; + title?: string; + theme?: { + backgroundColor?: string; + textColor?: string; + }; +}; + +type CounterState = { + count: number; + history: number[]; + lastAction: string; +}; + +type CounterAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'DOUBLE' } + | { type: 'RESET' } + | { type: 'ADD'; payload: number } + | { type: 'SET_STATE'; payload: CounterState }; + +type SecondaryCounterState = { + count: number; + multiplier: number; + lastOperation: string; + history: number[]; +}; + +type SecondaryCounterAction = + | { type: 'MULTIPLY' } + | { type: 'DIVIDE' } + | { type: 'SET_MULTIPLIER'; payload: number } + | { type: 'RESET' } + | { type: 'SET_STATE'; payload: SecondaryCounterState }; + +function counterReducer(state: CounterState, action: CounterAction, step: number): CounterState { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + step, + history: [...state.history, state.count + step], + lastAction: 'INCREMENT', + }; + case 'DECREMENT': + return { + ...state, + count: state.count - step, + history: [...state.history, state.count - step], + lastAction: 'DECREMENT', + }; + case 'DOUBLE': + return { + ...state, + count: state.count * 2, + history: [...state.history, state.count * 2], + lastAction: 'DOUBLE', + }; + case 'RESET': + return { + count: 0, + history: [], + lastAction: 'RESET', + }; + case 'ADD': + return { + ...state, + count: state.count + action.payload, + history: [...state.history, state.count + action.payload], + lastAction: `ADD ${action.payload}`, + }; + case 'SET_STATE': + return { + ...action.payload, + lastAction: 'SET_STATE', + }; + default: + return state; + } +} + +function secondaryCounterReducer( + state: SecondaryCounterState, + action: SecondaryCounterAction, +): SecondaryCounterState { + switch (action.type) { + case 'MULTIPLY': + return { + ...state, + count: state.count * state.multiplier, + history: [...state.history, state.count * state.multiplier], + lastOperation: `Multiplied by ${state.multiplier}`, + }; + case 'DIVIDE': + return { + ...state, + count: state.count / state.multiplier, + history: [...state.history, state.count / state.multiplier], + lastOperation: `Divided by ${state.multiplier}`, + }; + case 'SET_MULTIPLIER': + return { + ...state, + multiplier: action.payload, + history: [...state.history], + lastOperation: `Set multiplier to ${action.payload}`, + }; + case 'RESET': + return { + count: 0, + multiplier: 2, + history: [], + lastOperation: 'Reset', + }; + case 'SET_STATE': + return { + ...action.payload, + lastOperation: 'SET_STATE', + }; + default: + return state; + } +} + +function FunctionalReducerCounter({ + initialCount = 0, + step = 1, + title = 'Function-based Reducer Counter', + theme = { + backgroundColor: '#ffffff', + textColor: '#330002', + }, +}: CounterProps): JSX.Element { + const [clickCount, setClickCount] = useState(0); + const [lastClickTime, setLastClickTime] = useState(null); + const [averageTimeBetweenClicks, setAverageTimeBetweenClicks] = useState(0); + + const [state, dispatch] = useReducer( + (state: CounterState, action: CounterAction) => counterReducer(state, action, step), + { + count: initialCount, + history: [], + lastAction: 'none', + }, + ); + + const [secondaryState, secondaryDispatch] = useReducer(secondaryCounterReducer, { + count: initialCount, + multiplier: 2, + history: [], + lastOperation: 'none', + }); + + return ( +
+

{title}

+ +
+

Primary Counter: {state.count}

+
+ +
+ + + + + +
+ +
+

History:

+
+ {state.history.map((value, index) => ( + + {value} + {index < state.history.length - 1 ? ' → ' : ''} + + ))} +
+
+ +
+

Secondary Counter: {secondaryState.count}

+
+ + + + +
+
+

Current Multiplier: {secondaryState.multiplier}

+

History:

+
+ {secondaryState.history.map((value, index) => ( + + {value} + {index < secondaryState.history.length - 1 ? ' → ' : ''} + + ))} +
+
+
+
+ ); +} + +export default FunctionalReducerCounter; diff --git a/demo-app/src/client/Components/FunctionalStateCounter.tsx b/demo-app/src/client/Components/FunctionalStateCounter.tsx new file mode 100644 index 000000000..a49916177 --- /dev/null +++ b/demo-app/src/client/Components/FunctionalStateCounter.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; + +type CounterProps = { + initialCount?: number; + step?: number; + title?: string; + theme?: { + backgroundColor?: string; + textColor?: string; + }; +}; + +function FunctionalStateCounter({ + initialCount = 0, + step = 1, + title = 'Function-based State Counter', + theme = { + backgroundColor: '#ffffff', + textColor: '#330002', + }, +}: CounterProps): JSX.Element { + const [count, setCount] = useState(initialCount); + const [history, setHistory] = useState([]); + const [lastAction, setLastAction] = useState('none'); + + const handleAction = (type: string, payload?: number) => { + let newCount = count; + switch (type) { + case 'INCREMENT': + newCount = count + step; + setCount(newCount); + setHistory([...history, newCount]); + setLastAction('INCREMENT'); + break; + case 'DECREMENT': + newCount = count - step; + setCount(newCount); + setHistory([...history, newCount]); + setLastAction('DECREMENT'); + break; + case 'DOUBLE': + newCount = count * 2; + setCount(newCount); + setHistory([...history, newCount]); + setLastAction('DOUBLE'); + break; + case 'ADD': + newCount = count + (payload || 0); + setCount(newCount); + setHistory([...history, newCount]); + setLastAction(`ADD ${payload}`); + break; + case 'RESET': + setCount(0); + setHistory([]); + setLastAction('RESET'); + break; + } + }; + + return ( +
+

{title}

+
+

Current Count: {count}

+
+ +
+ + + + + +
+ +
+

History:

+
+ {history.map((value, index) => ( + + {value} + {index < history.length - 1 ? ' → ' : ''} + + ))} +
+
+
+ ); +} + +export default FunctionalStateCounter; diff --git a/demo-app/src/client/Components/Home.tsx b/demo-app/src/client/Components/Home.tsx index cafff52ef..22f2d1f4d 100644 --- a/demo-app/src/client/Components/Home.tsx +++ b/demo-app/src/client/Components/Home.tsx @@ -1,22 +1,74 @@ import React from 'react'; +import { useTheme } from '../../contexts/ThemeContext'; +import { useAuth } from '../../contexts/AuthContext'; function Home(): JSX.Element { + const { theme } = useTheme(); + const { user, login, logout } = useAuth(); + return ( -
+

REACTIME - DEMO APP

+ + {user ? ( +
+

Welcome, {user.username}!

+ +
+ ) : ( +
+

Please log in:

+ + +
+ )} +

"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." -

-

- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ut labore et dolore magna aliqua..."

); diff --git a/demo-app/src/client/Components/Increment.tsx b/demo-app/src/client/Components/Increment.tsx deleted file mode 100644 index 3836152a7..000000000 --- a/demo-app/src/client/Components/Increment.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { useState } from 'react'; -function Increment(): JSX.Element { - const [count, setCount] = useState(0); - return ( -
- -
- ); -} - -export default Increment; diff --git a/demo-app/src/client/Components/Nav.tsx b/demo-app/src/client/Components/Nav.tsx index c289f8d81..53e12d553 100644 --- a/demo-app/src/client/Components/Nav.tsx +++ b/demo-app/src/client/Components/Nav.tsx @@ -11,7 +11,10 @@ function Nav(): JSX.Element { Tic-Tac-Toe - Counter + State Counter + + + Reducer Counter
); diff --git a/demo-app/src/client/Components/ReducerCounter.tsx b/demo-app/src/client/Components/ReducerCounter.tsx new file mode 100644 index 000000000..65b5d6155 --- /dev/null +++ b/demo-app/src/client/Components/ReducerCounter.tsx @@ -0,0 +1,141 @@ +import React, { Component } from 'react'; + +type CounterProps = { + initialCount?: number; + step?: number; + title?: string; + theme?: { + backgroundColor?: string; + textColor?: string; + }; +}; + +type CounterState = { + count: number; + history: number[]; + lastAction: string; +}; + +type CounterAction = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'DOUBLE' } + | { type: 'RESET' } + | { type: 'ADD'; payload: number }; + +class ReducerCounter extends Component { + static defaultProps = { + initialCount: 0, + step: 1, + title: 'Class-based Reducer Counter', + theme: { + backgroundColor: '#ffffff', + textColor: '#330002', + }, + }; + + static initialState(initialCount: number): CounterState { + return { + count: initialCount, + history: [], + lastAction: 'none', + }; + } + + static reducer(state: CounterState, action: CounterAction, step: number): CounterState { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + step, + history: [...state.history, state.count + step], + lastAction: 'INCREMENT', + }; + case 'DECREMENT': + return { + ...state, + count: state.count - step, + history: [...state.history, state.count - step], + lastAction: 'DECREMENT', + }; + case 'DOUBLE': + return { + ...state, + count: state.count * 2, + history: [...state.history, state.count * 2], + lastAction: 'DOUBLE', + }; + case 'RESET': + return { + ...ReducerCounter.initialState(0), + lastAction: 'RESET', + }; + case 'ADD': + return { + ...state, + count: state.count + action.payload, + history: [...state.history, state.count + action.payload], + lastAction: `ADD ${action.payload}`, + }; + default: + return state; + } + } + + constructor(props: CounterProps) { + super(props); + this.state = ReducerCounter.initialState(props.initialCount || 0); + this.dispatch = this.dispatch.bind(this); + } + + dispatch(action: CounterAction): void { + this.setState((currentState) => + ReducerCounter.reducer(currentState, action, this.props.step || 1), + ); + } + + render(): JSX.Element { + const { title, theme } = this.props; + + return ( +
+

{title}

+
+

Current Count: {this.state.count}

+
+ +
+ + + + + +
+ +
+

History:

+
+ {this.state.history.map((value, index) => ( + + {value} + {index < this.state.history.length - 1 ? ' → ' : ''} + + ))} +
+
+
+ ); + } +} + +export default ReducerCounter; diff --git a/demo-app/src/client/Components/ThemeToggle.tsx b/demo-app/src/client/Components/ThemeToggle.tsx new file mode 100644 index 000000000..58d05abeb --- /dev/null +++ b/demo-app/src/client/Components/ThemeToggle.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTheme } from '../../contexts/ThemeContext'; + +const ThemeToggle = (): JSX.Element => { + const { theme, toggleTheme } = useTheme(); + + return ( + + ); +}; + +export default ThemeToggle; diff --git a/demo-app/src/client/Router.tsx b/demo-app/src/client/Router.tsx index 47dc6e14b..8c2196639 100644 --- a/demo-app/src/client/Router.tsx +++ b/demo-app/src/client/Router.tsx @@ -1,31 +1,62 @@ +// src/client/Router.tsx import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { ThemeProvider } from '../contexts/ThemeContext'; +import { AuthProvider } from '../contexts/AuthContext'; import Nav from './Components/Nav'; import Board from './Components/Board'; import Home from './Components/Home'; import Buttons from './Components/Buttons'; -// import ButtonsWithMoreHooks from './Components/ButtonsWithMoreHooks'; +import ReducerCounter from './Components/ReducerCounter'; +import FunctionalReducerCounter from './Components/FunctionalReducerCounter'; +import FunctionalStateCounter from './Components/FunctionalStateCounter'; +import ThemeToggle from './Components/ThemeToggle'; const domNode = document.getElementById('root'); +if (!domNode) throw new Error('Root element not found'); const root = createRoot(domNode); -root.render( - -
- + {isCurrIndex ? ( + + ) : ( + + )} {isCurrIndex ? ( ); diff --git a/src/app/components/DiffRoute/Diff.tsx b/src/app/components/DiffRoute/Diff.tsx deleted file mode 100644 index 340c88790..000000000 --- a/src/app/components/DiffRoute/Diff.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import { diff, formatters } from 'jsondiffpatch'; -// const jsondiffpatch = require('jsondiffpatch'); -import Parse from 'html-react-parser'; -import { - CurrentTab, - DiffProps, - MainState, - RootState, - StatelessCleaning, -} from '../../FrontendTypes'; -import { useSelector } from 'react-redux'; - -/** - * Displays tree showing two specific versions of tree: - * one with specific state changes, the other the whole tree - * @param props props from maincontainer - * @returns a diff tree or a string stating no state changes have happened - */ - -function Diff(props: DiffProps): JSX.Element { - const { - snapshot, // snapshot from 'tabs[currentTab]' object in 'MainContainer' - show, // boolean that is dependent on the 'Route' path; true if 'Route' path === '/diffRaw' - } = props; - const { currentTab, tabs }: MainState = useSelector((state: RootState) => state.main); - const { snapshots, viewIndex, sliderIndex }: Partial = tabs[currentTab]; - - let previous: unknown; // = (viewIndex !== -1) ? snapshots[viewIndex - 1] : previous = snapshots[sliderIndex - 1] - - if (viewIndex !== -1 && snapshots && viewIndex) { - // snapshots should not have any property < 0. A viewIndex of '-1' means that we had a snapshot index that occurred before the initial snapshot of the application state... which is impossible. '-1' therefore means reset to the last/most recent snapshot. - previous = snapshots[viewIndex - 1]; // set previous to the snapshot that is before the one we are currently viewing - } else if (snapshots && sliderIndex) { - previous = snapshots[sliderIndex - 1]; // if viewIndex was an impossible index, we will get our snapshots index using 'sliderIndex.' sliderIndex should have already been reset to the latest snapshot index. Previous is then set to the snapshot that occurred immediately before our most recent snapshot. - } - - /* - State snapshot objects have the following structure: - { - children: array of objects - componentData: object - isExpanded: Boolean - name: string - route: object - state: string - } - - // cleaning preview from stateless data - */ - const statelessCleaning = (obj: StatelessCleaning) => { - const newObj = { ...obj }; // duplicate our input object into a new object - - if (newObj.name === 'nameless') { - // if our new object's name is nameless - delete newObj.name; // delete the name property - } - if (newObj.componentData) { - // if our new object has a componentData property - delete newObj.componentData; // delete the componentData property - } - if (newObj.state === 'stateless') { - // if if our new object's state is stateless - delete newObj.state; // delete the state property - } - - if (newObj.stateSnaphot) { - // if our new object has a stateSnaphot property - newObj.stateSnaphot = statelessCleaning(obj.stateSnaphot); // run statelessCleaning on the stateSnapshot - } - - if (newObj.children) { - // if our new object has a children property - newObj.children = []; - if (obj.children.length > 0) { - // and if our input object's children property is non-empty, go through each children object from our input object and determine, if the object being iterated on either has a stateless state or has a children array with a non-zero amount of objects. Objects that fulfill the above that need to be cleaned through statelessCleaning. Those that are cleaned through this process are then pushed to the new object's children array. - obj.children.forEach( - (element: { state?: Record | string; children?: [] }) => { - if (element.state !== 'stateless' || element.children.length > 0) { - const clean = statelessCleaning(element); - newObj.children.push(clean); - } - }, - ); - } - } - return newObj; // return the cleaned state snapshot(s) - }; - - const previousDisplay: StatelessCleaning = statelessCleaning(previous); // displays stateful data from the first snapshot that was taken before our current snapshot. - - const delta = diff(previousDisplay, snapshot); // diff function from 'jsondiffpatch' returns the difference in state between 'previousDisplay' and 'snapshot' - - const html = formatters.html.format(delta, previousDisplay); // formatters function from 'jsondiffpatch' returns an html string that shows the difference between delta and the previousDisplay - // console.log(html); - - if (show) - formatters.html.showUnchanged(); // shows unchanged values if we're on the '/diffRaw' path - else formatters.html.hideUnchanged(); // hides unchanged values - - if (previous === undefined || delta === undefined) { - // if there has been no state changes on the target/hooked application, previous and delta would be undefined. - return ( -
- {' '} - Make state changes and click on a Snapshot to see the difference between that snapshot and - the previous one.{' '} -
- ); - } - return
{Parse(html)}
; // HTMLReactParser from 'html-react-parser' package converts the HTML string into a react component. -} - -export default Diff; diff --git a/src/app/components/DiffRoute/DiffRoute.tsx b/src/app/components/DiffRoute/DiffRoute.tsx deleted file mode 100644 index 081f7ebd1..000000000 --- a/src/app/components/DiffRoute/DiffRoute.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { MemoryRouter as Router, Route, NavLink, Routes } from 'react-router-dom'; -import Diff from './Diff'; -import { DiffRouteProps } from '../../FrontendTypes'; - -/* - Loads the appropriate DiffRoute view and renders the 'Tree' and 'Raw' navbar buttons after clicking on the 'Diff' button located near the top rightmost corner. -*/ - -// 'DiffRoute' only passed in prop is 'snapshot' from 'tabs[currentTab]' object in 'MainContainer' -const DiffRoute = (props: DiffRouteProps): JSX.Element => ( -
-
- (navData.isActive ? 'is-active router-link' : 'router-link')} - to='/diff/tree' - > - Tree - - (navData.isActive ? 'is-active router-link' : 'router-link')} - to='/diff/diffRaw' - > - Raw - -
-
- - } /> - } /> - -
-
-); - -export default DiffRoute; diff --git a/src/app/components/StateRoute/AxMap/Ax.tsx b/src/app/components/StateRoute/AxMap/Ax.tsx index 6093e8293..4c0385b61 100644 --- a/src/app/components/StateRoute/AxMap/Ax.tsx +++ b/src/app/components/StateRoute/AxMap/Ax.tsx @@ -3,16 +3,13 @@ import { useDispatch, useSelector } from 'react-redux'; import { Group } from '@visx/group'; import { hierarchy, Tree } from '@visx/hierarchy'; import { LinearGradient } from '@visx/gradient'; -import { pointRadial } from 'd3-shape'; import LinkControls from './axLinkControls'; import getLinkComponent from './getAxLinkComponents'; import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'; import ToolTipDataDisplay from './ToolTipDataDisplay'; import { ToolTipStyles } from '../../../FrontendTypes'; import { localPoint } from '@visx/event'; -import AxLegend from './axLegend'; -import { renderAxLegend } from '../../../slices/AxSlices/axLegendSlice'; -import type { RootState } from '../../../store'; +import { toggleExpanded, setCurrentTabInApp } from '../../../slices/mainSlice'; const defaultMargin = { top: 30, @@ -52,7 +49,7 @@ export default function AxTree(props) { showTooltip, // function to set tooltip state hideTooltip, // function to close a tooltip } = useTooltip(); // returns an object with several properties that you can use to manage the tooltip state of your component - + const { containerRef, // Access to the container's bounding box. This will be empty on first render. TooltipInPortal, // TooltipWithBounds in a Portal, outside of your component DOM tree @@ -65,20 +62,20 @@ export default function AxTree(props) { const tooltipStyles: ToolTipStyles = { ...defaultStyles, minWidth: 60, - maxWidth: 300, - backgroundColor: 'rgb(15,15,15)', - color: 'white', - fontSize: '16px', + maxWidth: 250, + maxHeight: '300px', lineHeight: '18px', - fontFamily: 'Roboto', - zIndex: 100, pointerEvents: 'all !important', + margin: 0, + padding: 0, + borderRadius: '8px', + overflowY: 'auto', + overflowX: 'auto', }; - const [layout, setLayout] = useState('cartesian'); const [orientation, setOrientation] = useState('horizontal'); - const [linkType, setLinkType] = useState('diagonal'); - const [stepPercent, setStepPercent] = useState(0.5); + const [linkType, setLinkType] = useState('step'); + const [stepPercent, setStepPercent] = useState(0.0); const innerWidth: number = totalWidth - margin.left - margin.right; const innerHeight: number = totalHeight - margin.top - margin.bottom - 60; @@ -87,32 +84,23 @@ export default function AxTree(props) { let sizeWidth: number; let sizeHeight: number; - if (layout === 'polar') { - origin = { - x: innerWidth / 2, - y: innerHeight / 2, - }; - sizeWidth = 2 * Math.PI; - sizeHeight = Math.min(innerWidth, innerHeight) / 2; + origin = { x: 0, y: 0 }; + if (orientation === 'vertical') { + sizeWidth = innerWidth; + sizeHeight = innerHeight; } else { - origin = { x: 0, y: 0 }; - if (orientation === 'vertical') { - sizeWidth = innerWidth; - sizeHeight = innerHeight; - } else { - sizeWidth = innerHeight; - sizeHeight = innerWidth; - } + sizeWidth = innerHeight; + sizeHeight = innerWidth; } - const LinkComponent = getLinkComponent({ layout, linkType, orientation }); + const LinkComponent = getLinkComponent({ linkType, orientation }); const currAxSnapshot = JSON.parse(JSON.stringify(axSnapshots[currLocation.index])); // root node of currAxSnapshot const rootAxNode = JSON.parse(JSON.stringify(currAxSnapshot[0])); - // array that holds each ax tree node with children property + // array that holds each ax tree node with children property const nodeAxArr = []; // populates ax nodes with children property; visx recognizes 'children' in order to properly render a nested tree @@ -157,29 +145,22 @@ export default function AxTree(props) { populateNodeAxArr(rootAxNode); // Conditionally render ax legend component (RTK) - const { axLegendButtonClicked } = useSelector((state: RootState) => state.axLegend); const dispatch = useDispatch(); return totalWidth < 10 ? null : (
-
+
- -
- + { + onClick={() => { hideTooltip(); - }}/> + }} + /> (d.isExpanded ? null : d.children))} @@ -208,7 +190,6 @@ export default function AxTree(props) { fill='none' /> ))} - // code relating to each node in tree {tree.descendants().map((node, key) => { const widthFunc = (name): number => { // returns a number that is related to the length of the name. Used for determining the node width. @@ -223,11 +204,7 @@ export default function AxTree(props) { let top: number; let left: number; - if (layout === 'polar') { - const [radialX, radialY] = pointRadial(node.x, node.y); - top = radialY; - left = radialX; - } else if (orientation === 'vertical') { + if (orientation === 'vertical') { top = node.y; left = node.x; } else { @@ -299,7 +276,7 @@ export default function AxTree(props) { } } } else { - aspect = Math.max(aspect, 0.2); + aspect = Math.max(aspect, 0.5); } const handleMouseAndClickOver = (event): void => { const coords = localPoint(event.target.ownerSVGElement, event); @@ -318,17 +295,16 @@ export default function AxTree(props) { {node.depth === 0 && ( { - node.data.isExpanded = !node.data.isExpanded; + dispatch(toggleExpanded(node.data)); hideTooltip(); }} /> @@ -340,12 +316,11 @@ export default function AxTree(props) { width={width} y={-height / 2} x={-width / 2} - fill="url('#parent-gradient')" strokeWidth={1.5} strokeOpacity='1' - rx={node.children ? 4 : 10} + rx={8} onClick={() => { - node.data.isExpanded = !node.data.isExpanded; + dispatch(toggleExpanded(node.data)); hideTooltip(); }} // Mouse Enter Rect (Component Node) ----------------------------------------------------------------------- @@ -428,10 +403,9 @@ export default function AxTree(props) { }} >
-
- {/*tooltipData['name'].value cannot be referred to using dot notation so using brackets here overrides typescript's strict data typing which was interfering with accessiccing this property */} - {JSON.stringify(tooltipData['name'].value)} -
+
+

{tooltipData['name'].value}

+
{/* Ax Node Info below names the tooltip title because of how its passed to the ToolTipDataDisplay container*/} @@ -439,13 +413,6 @@ export default function AxTree(props) {
)} - -
- { axLegendButtonClicked ? - : '' - } -
-
); } diff --git a/src/app/components/StateRoute/AxMap/AxMap.tsx b/src/app/components/StateRoute/AxMap/AxContainer.tsx similarity index 99% rename from src/app/components/StateRoute/AxMap/AxMap.tsx rename to src/app/components/StateRoute/AxMap/AxContainer.tsx index 12e2ca801..719d57645 100644 --- a/src/app/components/StateRoute/AxMap/AxMap.tsx +++ b/src/app/components/StateRoute/AxMap/AxContainer.tsx @@ -37,5 +37,3 @@ const AxContainer = (props: AxContainer) => { }; export default AxContainer; - - diff --git a/src/app/components/StateRoute/AxMap/ToolTipDataDisplay.tsx b/src/app/components/StateRoute/AxMap/ToolTipDataDisplay.tsx index c0f4507ec..5580e7636 100644 --- a/src/app/components/StateRoute/AxMap/ToolTipDataDisplay.tsx +++ b/src/app/components/StateRoute/AxMap/ToolTipDataDisplay.tsx @@ -18,7 +18,6 @@ const colors = { base07: '#e7e9db', base08: '#ef6155', base09: '#824508', //base09 is orange for booleans and numbers. This base in particular fails to match the entered color. - // base09: '#592bad', // alternative purple base0A: '#fec418', base0B: '#48b685', base0C: '#5bc4bf', @@ -68,7 +67,7 @@ const ToolTipDataDisplay = ({ containerName, dataObj }) => { } return ( -
+
({ className: `tooltipData-JSONTree` }) }} // theme set to a base16 theme that has been extended to include "className: 'json-tree'" diff --git a/src/app/components/StateRoute/AxMap/axLegend.tsx b/src/app/components/StateRoute/AxMap/axLegend.tsx deleted file mode 100644 index 9ebe139a5..000000000 --- a/src/app/components/StateRoute/AxMap/axLegend.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -const AxLegend = () => { - return ( -
- Nodes from the accessibility tree have either a role role or internalRole -
    -
  • - Role refers to ARIA roles, which indicate the purpose of the element to assistive technologies, like screen readers. - All of the nodes rendered in this tree have a role of 'role' -
  • -
  • - internalRole refers to browser-specific roles Chrome for its own accessibility processing -
  • -
- -

Each node is given a property labeled ignored. Nodes read by the screen reader have their ignored property evaluate to false. - Nodes not read by the screen reader evaluate to true.

- -

Nodes labeled as no name are visible to a screen reader, but were not given a name label.

-
- ); -} - -export default AxLegend; \ No newline at end of file diff --git a/src/app/components/StateRoute/AxMap/axLinkControls.tsx b/src/app/components/StateRoute/AxMap/axLinkControls.tsx index 433936919..04347a693 100644 --- a/src/app/components/StateRoute/AxMap/axLinkControls.tsx +++ b/src/app/components/StateRoute/AxMap/axLinkControls.tsx @@ -1,78 +1,61 @@ import React from 'react'; -const controlStyles = { fontSize: 10 }; - -type Props = { - layout: string; - orientation: string; - linkType: string; - stepPercent: number; - setLayout: (layout: string) => void; - setOrientation: (orientation: string) => void; - setLinkType: (linkType: string) => void; - setStepPercent: (percent: number) => void; -}; - -export default function LinkControls({ - layout, +const AxLinkControls = ({ orientation, linkType, stepPercent, - setLayout, setOrientation, setLinkType, setStepPercent, -}: Props) { +}) => { return ( -
-   - -    -   - -    -   - - {linkType === 'step' && layout !== 'polar' && ( - <> -    -   +
+
+ + +
+ +
+ + +
+ + {linkType === 'step' && ( +
+ e.stopPropagation()} - type="range" + type='range' min={0} max={1} step={0.1} onChange={(e) => setStepPercent(Number(e.target.value))} value={stepPercent} - disabled={linkType !== 'step' || layout === 'polar'} + disabled={linkType !== 'step'} + className='control-range' /> - +
)}
); -} +}; + +export default AxLinkControls; diff --git a/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx b/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx index 33316542f..26ce0426f 100644 --- a/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx +++ b/src/app/components/StateRoute/AxMap/getAxLinkComponents.tsx @@ -15,27 +15,15 @@ import { } from '@visx/shape'; export default function getLinkComponent({ - layout, linkType, orientation, }: { - layout: string; linkType: string; orientation: string; }) { let LinkComponent; - if (layout === 'polar') { - if (linkType === 'step') { - LinkComponent = LinkRadialStep; - } else if (linkType === 'curve') { - LinkComponent = LinkRadialCurve; - } else if (linkType === 'line') { - LinkComponent = LinkRadialLine; - } else { - LinkComponent = LinkRadial; - } - } else if (orientation === 'vertical') { + if (orientation === 'vertical') { if (linkType === 'step') { LinkComponent = LinkVerticalStep; } else if (linkType === 'curve') { diff --git a/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx b/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx index ed52beff5..1436468d3 100644 --- a/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx +++ b/src/app/components/StateRoute/ComponentMap/ComponentMap.tsx @@ -22,30 +22,23 @@ import { toggleExpanded, setCurrentTabInApp } from '../../../slices/mainSlice'; import { useDispatch } from 'react-redux'; import { LinkTypesProps, DefaultMargin, ToolTipStyles } from '../../../FrontendTypes'; -const linkStroke = '#F00008'; //#F00008 original -const rootStroke = '#F00008'; //#F00008 original -const nodeParentFill = '#161521'; //#161521 original -const nodeChildFill = '#62d6fb'; //#62d6fb original -const nodeParentStroke = '#F00008'; //#F00008 original -const nodeChildStroke = '#4D4D4D'; //#4D4D4D original -let stroke = ''; - -/* Heat Map Colors (for links) */ -const lightOrange = '#F1B476'; -const darkOrange = '#E4765B'; -const red = '#C64442'; -const plum = '#8C2743'; +let stroke = ''; + +const lightWeight = '#94a3b8'; // Lightest gray for minimal props +const mediumWeight = '#64748b'; // Medium gray for light prop load +const heavyWeight = '#556579'; +const veryHeavy = '#475569'; // Darker gray for medium load const defaultMargin: DefaultMargin = { top: 30, - left: 30, - right: 55, + left: 20, + right: 20, bottom: 70, }; const nodeCoords: object = {}; let count: number = 0; -let aspect: number = 1; // aspect resizes the component map container to accommodate large node trees on complex sites +let aspect: number = 1; let nodeCoordTier = 0; let nodeOneLeft = 0; let nodeTwoLeft = 2; @@ -57,11 +50,12 @@ export default function ComponentMap({ margin = defaultMargin, currentSnapshot, // from 'tabs[currentTab].stateSnapshot object in 'MainContainer' }: LinkTypesProps): JSX.Element { - const [layout, setLayout] = useState('cartesian'); // We create a local state "layout" and set it to a string 'cartesian' const [orientation, setOrientation] = useState('vertical'); // We create a local state "orientation" and set it to a string 'vertical'. - const [linkType, setLinkType] = useState('diagonal'); // We create a local state "linkType" and set it to a string 'diagonal'. - const [stepPercent, setStepPercent] = useState(0.5); // We create a local state "stepPercent" and set it to a number '0.5'. This will be used to scale the Map component's link: Step to 50% + const [linkType, setLinkType] = useState('step'); // We create a local state "linkType" and set it to a string 'step'. + const [stepPercent, setStepPercent] = useState(0.0); // We create a local state "stepPercent" and set it to a number '0.0'. This will be used to scale the Map component's link: Step to 0% const [selectedNode, setSelectedNode] = useState('root'); // We create a local state "selectedNode" and set it to a string 'root'. + const [forceUpdate, setForceUpdate] = useState(false); + const dispatch = useDispatch(); const toolTipTimeoutID = useRef(null); //useRef stores stateful data that’s not needed for rendering. @@ -70,6 +64,14 @@ export default function ComponentMap({ dispatch(setCurrentTabInApp('map')); // dispatch sent at initial page load allowing changing "immer's" draft.currentTabInApp to 'map' to facilitate render. }, [dispatch]); + // force app to re-render to accurately calculate aspect ratio upon initial load + useEffect(() => { + const timer = setTimeout(() => { + setForceUpdate((prev) => !prev); + }, 100); + return () => clearTimeout(timer); + }, []); + // setting the margins for the Map to render in the tab window. const innerWidth: number = totalWidth - margin.left - margin.right; const innerHeight: number = totalHeight - margin.top - margin.bottom - 60; @@ -80,33 +82,19 @@ export default function ComponentMap({ /* We begin setting the starting position for the root node on the maps display. - The 'polar layout' sets the root node to the relative center of the display box based on the size of the browser window. - The 'cartesian layout' (else conditional) sets the root nodes location either in the left middle *or top middle of the browser window relative to the size of the browser. + The default view sets the root nodes location either in the left middle *or top middle of the browser window relative to the size of the browser. */ - if (layout === 'polar') { - // 'polar layout' option - origin = { - x: innerWidth / 2, - y: innerHeight / 2, - }; - - // set the sizeWidth and sizeHeight - sizeWidth = 2 * Math.PI; - sizeHeight = Math.min(innerWidth, innerHeight) / 2; + origin = { x: 0, y: 0 }; + if (orientation === 'vertical') { + sizeWidth = innerWidth; + sizeHeight = innerHeight; } else { - // 'cartesian layout' option - origin = { x: 0, y: 0 }; - if (orientation === 'vertical') { - sizeWidth = innerWidth; - sizeHeight = innerHeight; - } else { - // if the orientation isn't vertical, swap the width and the height - sizeWidth = innerHeight; - sizeHeight = innerWidth; - } + // if the orientation isn't vertical, swap the width and the height + sizeWidth = innerHeight; + sizeHeight = innerWidth; } - + //} const { tooltipData, // value/data that tooltip may need to render tooltipLeft, // number used for tooltip positioning @@ -128,29 +116,14 @@ export default function ComponentMap({ const tooltipStyles: ToolTipStyles = { ...defaultStyles, minWidth: 60, - maxWidth: 300, - backgroundColor: 'rgb(15,15,15)', - color: 'white', - fontSize: '16px', + maxWidth: 250, + maxHeight: '300px', lineHeight: '18px', - fontFamily: 'Roboto', - zIndex: 100, pointerEvents: 'all !important', - }; - - const scrollStyle: {} = { - minWidth: '60', - maxWidth: '300', - minHeight: '20px', - maxHeight: '200px', - overflowY: 'scroll', - overflowWrap: 'break-word', - }; - - const formatRenderTime: string = (time: number): string => { - if (!time) return 'No time information'; - const renderTime = time.toFixed(3); - return `${renderTime} ms `; + margin: 0, + padding: 0, + borderRadius: '8px', + overflowY: 'auto', }; const nodeList: [] = []; // create a nodeList array to store our nodes as a flat array @@ -172,7 +145,69 @@ export default function ComponentMap({ } }; - collectNodes(currentSnapshot); + // check if any data should be displayed in tool tip display + const hasDisplayableData = (nodeData) => { + // Check if the node has props + const hasProps = + nodeData.componentData?.props && Object.keys(nodeData.componentData.props).length > 0; + + // Check if the node has state + const hasState = + (nodeData.componentData?.state && Object.keys(nodeData.componentData.state).length > 0) || + (nodeData.componentData?.hooksState && + Object.keys(nodeData.componentData.hooksState).length > 0); + + // Check if the node has reducer states + const hasReducers = + nodeData.componentData?.reducerStates && nodeData.componentData.reducerStates.length > 0; + + return hasProps || hasState || hasReducers; + }; + + const shouldIncludeNode = (node) => { + // Return false if node has any context properties + if (node?.componentData?.context && Object.keys(node.componentData.context).length > 0) { + return false; + } + // Return false if node name ends with 'Provider' + if (node?.name && node.name.endsWith('Provider')) { + return false; + } + return true; + }; + + const processTreeData = (node) => { + if (!node) return null; + + // Create a new node + const newNode = { ...node }; + + if (node.children) { + // Process all children first + const processedChildren = node.children + .map((child) => processTreeData(child)) + .filter(Boolean); // Remove null results + + // For each child that shouldn't be included, replace it with its children + newNode.children = processedChildren.reduce((acc, child) => { + if (shouldIncludeNode(child)) { + // If child should be included, add it directly + acc.push(child); + } else { + // If child should be filtered out, add its children instead + if (child.children) { + acc.push(...child.children); + } + } + return acc; + }, []); + } + + return newNode; + }; + // filter out Conext Providers + let filtered = processTreeData(currentSnapshot); + collectNodes(filtered); // @ts // find the node that has been selected and use it as the root @@ -188,24 +223,21 @@ export default function ComponentMap({ if (startNode === null) startNode = rootNode; }; - findSelectedNode(); // locates the rootNode... do we really need this? This function is only used once... it's here. + findSelectedNode(); // locates the rootNode // controls for the map const LinkComponent: React.ComponentType = getLinkComponent({ - layout, linkType, orientation, }); - return totalWidth < 10 ? null : ( + return totalWidth < 10 ? null : (
- {/* */} (a.parent === b.parent ? 0.5 : 0.5) / a.depth} > - {(tree) => ( - + {tree.links().map((link, i) => { - const linkName = link.source.data.name; + const linkName = link.source.data.name; const propsObj = link.source.data.componentData.props; const childPropsObj = link.target.data.componentData.props; let propsLength; @@ -246,65 +276,103 @@ export default function ComponentMap({ } if (childPropsObj) { childPropsLength = Object.keys(childPropsObj).length; - } - // go to https://en.wikipedia.org/wiki/Logistic_function + // go to https://en.wikipedia.org/wiki/Logistic_function // for an explanation of Logistic functions and parameters used const yshift = -3; const x0 = 5; const L = 25; - const k = .4; - const strokeWidthIndex = yshift + L / (1 + Math.exp(-k * (childPropsLength - x0))); + const k = 0.4; + const strokeWidthIndex = + yshift + L / (1 + Math.exp(-k * (childPropsLength - x0))); if (strokeWidthIndex <= 1) { stroke = '#808080'; } else { if (childPropsLength <= 1) { - stroke = lightOrange; + stroke = lightWeight; } else if (childPropsLength <= 2) { - stroke = darkOrange; + stroke = mediumWeight; } else if (childPropsLength <= 3) { - stroke = red; + stroke = heavyWeight; } else { - stroke = plum; + stroke = veryHeavy; } - // stroke = '#df6f37' } return ( - - ) - }) - } + + ); + })} {tree.descendants().map((node, key) => { - const widthFunc: number = (name) => { - //returns a number that is related to the length of the name. Used for determining the node width. - const nodeLength = name.length; - //return nodeLength * 7 + 20; //uncomment this line if we want each node to be directly proportional to the name.length (instead of nodes of similar sizes to snap to the same width) + const calculateNodeWidth = (text: string): number => { + const nodeLength = text.length; if (nodeLength <= 5) return nodeLength + 50; if (nodeLength <= 10) return nodeLength + 120; return nodeLength + 140; }; - const width: number = widthFunc(node.data.name); // the width is determined by the length of the node.name - const height: number = 25; + // Find the maximum width for any node + const findMaxNodeWidth = (nodeData: any): number => { + // If no children, return current node width + if (!nodeData.children) { + return calculateNodeWidth(nodeData.name); + } + + // Get width of current node + const currentWidth = calculateNodeWidth(nodeData.name); + + // Get max width from children + const childrenWidths = nodeData.children.map((child) => + findMaxNodeWidth(child), + ); + + // Return the maximum width found + return Math.max(currentWidth, ...childrenWidths); + }; + + // Truncate text for nodes that exceed a certain length + const truncateText = (text: string, width: number, maxWidth: number): string => { + // Calculate approximate text width + const estimatedTextWidth = text.length * 8; + + // If this node's width is close to the max width (within 10%), truncate it + if (width >= maxWidth * 0.9) { + const maxChars = Math.floor((width - 30) / 8); // -30 for padding + ellipsis + return `${text.slice(0, maxChars)}...`; + } + + return text; + }; + + const getNodeDimensions = ( + name: string, + rootNode: any, + ): { width: number; displayText: string } => { + const width = calculateNodeWidth(name); + const maxWidth = findMaxNodeWidth(rootNode); + const displayText = truncateText(name, width, maxWidth); + + return { width, displayText }; + }; + + // Usage in your render function: + const { width, displayText } = getNodeDimensions(node.data.name, startNode); + + const height: number = 35; let top: number; let left: number; - if (layout === 'polar') { - const [radialX, radialY] = pointRadial(node.x, node.y); - top = radialY; - left = radialX; - } else if (orientation === 'vertical') { + if (orientation === 'vertical') { top = node.y; left = node.x; } else { @@ -376,38 +444,59 @@ export default function ComponentMap({ } } } else { - aspect = Math.max(aspect, 0.2); + aspect = Math.max(aspect, 1); } // mousing controls & Tooltip display logic - const handleMouseAndClickOver: void = (event) => { - const coords = localPoint(event.target.ownerSVGElement, event); - const tooltipObj = { ...node.data }; - - showTooltip({ - tooltipLeft: coords.x, - tooltipTop: coords.y, - tooltipData: tooltipObj, - // this is where the data for state and render time is displayed - // but does not show props functions and etc - }); + const handleMouseAndClickOver = (event, nodeData) => { + // Only show tooltip if the node has data to display + if (hasDisplayableData(nodeData)) { + const coords = localPoint(event.target.ownerSVGElement, event); + const tooltipObj = { ...nodeData }; + + showTooltip({ + tooltipLeft: coords.x, + tooltipTop: coords.y, + tooltipData: tooltipObj, + }); + } }; return ( + // Replace the root node rect rendering block with this: {node.depth === 0 && ( - { dispatch(toggleExpanded(node.data)); hideTooltip(); }} + onMouseEnter={(event) => { + if (hasDisplayableData(node.data)) { + if (toolTipTimeoutID.current !== null) { + clearTimeout(toolTipTimeoutID.current); + hideTooltip(); + } + toolTipTimeoutID.current = null; + handleMouseAndClickOver(event, node.data); + } + }} + onMouseLeave={() => { + if (hasDisplayableData(node.data)) { + toolTipTimeoutID.current = setTimeout(() => { + hideTooltip(); + toolTipTimeoutID.current = null; + }, 300); + } + }} /> )} - {/* This creates the rectangle boxes for each component and sets it relative position to other parent nodes of the same level. */} {node.depth !== 0 && ( @@ -417,57 +506,31 @@ export default function ComponentMap({ width={width} y={-height / 2} x={-width / 2} - fill="url('#parent-gradient')" - //color={'#ff0000'} - //fill={node.children ? nodeParentFill : nodeChildFill} - //stroke={ - // node.data.isExpanded && node.data.children.length > 0 - // ? nodeParentStroke - // : nodeChildStroke - // } - strokeWidth={1.5} - strokeOpacity='1' - rx={node.children ? 4 : 10} + rx={10} onClick={() => { dispatch(toggleExpanded(node.data)); hideTooltip(); }} - // Mouse Enter Rect (Component Node) ----------------------------------------------------------------------- - /** This onMouseEnter event fires when the mouse first moves/hovers over a component node. - * The supplied event listener callback produces a Tooltip element for the current node. */ - onMouseEnter={(event) => { - /** This 'if' statement block checks to see if you've just left another component node - * by seeing if there's a current setTimeout waiting to close that component node's - * tooltip (see onMouseLeave immediately below). If so it clears the tooltip generated - * from that component node so a new tooltip for the node you've just entered can render. */ - if (toolTipTimeoutID.current !== null) { - clearTimeout(toolTipTimeoutID.current); - hideTooltip(); + if (hasDisplayableData(node.data)) { + if (toolTipTimeoutID.current !== null) { + clearTimeout(toolTipTimeoutID.current); + hideTooltip(); + } + toolTipTimeoutID.current = null; + handleMouseAndClickOver(event, node.data); } - // Removes the previous timeoutID to avoid errors - toolTipTimeoutID.current = null; - //This generates a tooltip for the component node the mouse has entered. - handleMouseAndClickOver(event); }} - // Mouse Leave Rect (Component Node) -------------------------------------------------------------------------- - /** This onMouseLeave event fires when the mouse leaves a component node. - * The supplied event listener callback generates a setTimeout call which gives the - * mouse a certain amount of time between leaving the current component node and - * closing the tooltip for that node. - * If the mouse enters the tooltip before the timeout delay has passed, the - * setTimeout event will be canceled. */ onMouseLeave={() => { - // Store setTimeout ID so timeout can be cleared if necessary - toolTipTimeoutID.current = setTimeout(() => { - // hideTooltip unmounts the tooltip - hideTooltip(); - toolTipTimeoutID.current = null; - }, 300); + if (hasDisplayableData(node.data)) { + toolTipTimeoutID.current = setTimeout(() => { + hideTooltip(); + toolTipTimeoutID.current = null; + }, 300); + } }} /> )} - {/* Display text inside of each component node */} - {node.data.name} + {displayText} ); @@ -496,43 +556,24 @@ export default function ComponentMap({ {tooltipOpen && tooltipData && ( { clearTimeout(toolTipTimeoutID.current); toolTipTimeoutID.current = null; }} - //------------- Mouse Leave TooltipInPortal ----------------------------------------------------------------- - /** When the mouse leaves the tooltip, the tooltip unmounts */ onMouseLeave={() => { hideTooltip(); }} >
-
- {tooltipData.name} +
+

{tooltipData.name}

-
- Key: {tooltipData.componentData.key !== null ? tooltipData.componentData.key : 'null'} -
-
Render time: {formatRenderTime(tooltipData.componentData.actualDuration)}
-
- - +
diff --git a/src/app/components/StateRoute/ComponentMap/LinkControls.tsx b/src/app/components/StateRoute/ComponentMap/LinkControls.tsx index 69b80447f..ceaa948fd 100644 --- a/src/app/components/StateRoute/ComponentMap/LinkControls.tsx +++ b/src/app/components/StateRoute/ComponentMap/LinkControls.tsx @@ -1,119 +1,118 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ import React from 'react'; -import { LinkControlProps, ControlStyles, DropDownStyle, Node } from '../../../FrontendTypes'; -// Font size of the Controls label and Dropdowns -const controlStyles: ControlStyles = { - //fontSize: '16px', - padding: '10px', -}; -const dropDownStyle: DropDownStyle = { - margin: '0.1em', - //fontSize: '16px', - fontFamily: 'Roboto, sans-serif', - borderRadius: '4px', - borderStyle: 'solid', - borderWidth: '1px', - backgroundColor: '#d9d9d9', - color: '#161617', - padding: '2px', -}; +const LinkControls = ({ + linkType, + stepPercent, + setOrientation, + setLinkType, + setStepPercent, + setSelectedNode, + snapShots, +}) => { + const collectNodes = (node) => { + const nodeList = []; + nodeList.push(node); + for (let i = 0; i < nodeList.length; i += 1) { + const cur = nodeList[i]; + if (cur.children?.length > 0) { + cur.children.forEach((child) => nodeList.push(child)); + } + } + return nodeList; + }; + + const shouldIncludeNode = (node) => { + // Return false if node has any context properties + if (node?.componentData?.context && Object.keys(node.componentData.context).length > 0) { + return false; + } + // Return false if node name ends with 'Provider' + if (node?.name && node.name.endsWith('Provider')) { + return false; + } + return true; + }; + + const processTreeData = (node) => { + if (!node) return null; -// use BFS to put all the nodes under snapShots(which is the tree node) into an array -const nodeList: Node[] = []; + // Create a new node + const newNode = { ...node }; -const collectNodes = (node: Node): void => { - nodeList.splice(0, nodeList.length); - /* We used the .splice method here to ensure that nodeList - did not accumulate with page refreshes */ - nodeList.push(node); - for (let i = 0; i < nodeList.length; i += 1) { - const cur = nodeList[i]; - if (cur.children?.length > 0) { - cur.children.forEach((child) => nodeList.push(child)); + if (node.children) { + // Process all children first + const processedChildren = node.children + .map((child) => processTreeData(child)) + .filter(Boolean); // Remove null results + + // For each child that shouldn't be included, replace it with its children + newNode.children = processedChildren.reduce((acc, child) => { + if (shouldIncludeNode(child)) { + // If child should be included, add it directly + acc.push(child); + } else { + // If child should be filtered out, add its children instead + if (child.children) { + acc.push(...child.children); + } + } + return acc; + }, []); } - } -}; -export default function LinkControls({ - layout, // from the layout local state (initially 'cartesian') in 'ComponentMap' - linkType, // from linkType local state (initially 'vertical') in 'ComponentMap' - stepPercent, // from stepPercent local state (initially '0.5') in 'ComponentMap' - setLayout, // from the layout local state in 'ComponentMap' - setOrientation, // from the orientation local state in 'ComponentMap' - setLinkType, // from the linkType local state in 'ComponentMap' - setStepPercent, // from the stepPercent local state in 'ComponentMap' - setSelectedNode, // from the selectedNode local state in 'ComponentMap' - snapShots, -}: LinkControlProps): JSX.Element { - collectNodes(snapShots); + return newNode; + }; + const filtered = processTreeData(snapShots); + const nodeList = collectNodes(filtered); return ( -
- {' '} - {/* Controls for the layout selection */} - -  {' '} - {/* This is a non-breaking space - Prevents an automatic line break at this position */} - -    - {' '} - {/* Toggle record button to pause state changes on target application */} - {/* Controls for the Orientation selection, this dropdown will be disabled when the polar layout is selected as it is not needed */} -   - -    - {/* Controls for the link selections. */} -   - - {/* Controls for the select selections. */} -   - e.stopPropagation()} + onChange={(e) => setOrientation(e.target.value)} + className='control-select' + > + + + +
+ +
+ + +
+ +
+ + - {/* This is the slider control for the step option */} - {linkType === 'step' && layout !== 'polar' && ( - <> -    - -   + ) : null, + )} + +
+ + {linkType === 'step' && ( +
+ e.stopPropagation()} type='range' @@ -122,10 +121,13 @@ export default function LinkControls({ step={0.1} onChange={(e) => setStepPercent(Number(e.target.value))} value={stepPercent} - disabled={linkType !== 'step' || layout === 'polar'} + disabled={linkType !== 'step'} + className='control-range' /> - +
)}
); -} +}; + +export default LinkControls; diff --git a/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx b/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx index 452eb8c12..bf19f5307 100644 --- a/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx +++ b/src/app/components/StateRoute/ComponentMap/ToolTipDataDisplay.tsx @@ -1,70 +1,110 @@ import React from 'react'; import { JSONTree } from 'react-json-tree'; -/* - Code that show's the tooltip of our JSON tree -*/ +const ToolTipDataDisplay = ({ data }) => { + if (!data) return null; -const colors = { - scheme: 'paraiso', - author: 'jan t. sott', - base00: '#2f1e2e', - base01: '#41323f', - base02: '#4f424c', - base03: '#776e71', - base04: '#8d8687', - base05: '#a39e9b', - base06: '#b9b6b0', - base07: '#e7e9db', - base08: '#ef6155', - base09: '#824508', //base09 is orange for booleans and numbers. This base in particular fails to match the entered color. - // base09: '#592bad', // alternative purple - base0A: '#fec418', - base0B: '#48b685', - base0C: '#5bc4bf', - base0D: '#06b6ef', - base0E: '#815ba4', - base0F: '#e96ba8', -}; + const jsonTheme = { + scheme: 'custom', + base00: 'transparent', + base0B: '#1f2937', // dark navy for strings + base0D: '#60a5fa', // Keys + base09: '#f59e0b', // Numbers + base0C: '#EF4444', // Null values + }; -const ToolTipDataDisplay = ({ containerName, dataObj }) => { - const printableObject = {}; // The key:value properties of printableObject will be rendered in the JSON Tree + // Helper function to parse stringified JSON in object values + const parseStringifiedValues = (obj) => { + if (!obj || typeof obj !== 'object') return obj; - if (!dataObj) { - // If state is null rather than an object, print "State: null" in tooltip - printableObject[containerName] = dataObj; - } else { - /* - Props often contain circular references. - Messages from the backend must be sent as JSON objects (strings). - JSON objects can't contain circular ref's, so the backend filters out problematic values by stringifying the values of object properties and ignoring any values that fail the conversion due to a circular ref. The following logic converts these values back to JS so they display clearly and are collapsible. - */ - const data = {}; - for (const key in dataObj) { - if (typeof dataObj[key] === 'string') { + const parsed = { ...obj }; + for (const key in parsed) { + if (typeof parsed[key] === 'string') { try { - data[key] = JSON.parse(dataObj[key]); - } catch { - data[key] = dataObj[key]; + // Check if the string looks like JSON + if (parsed[key].startsWith('{') || parsed[key].startsWith('[')) { + const parsedValue = JSON.parse(parsed[key]); + parsed[key] = parsedValue; + } + } catch (e) { + // If parsing fails, keep original value + continue; } - } else { - data[key] = dataObj[key]; + } else if (typeof parsed[key] === 'object') { + parsed[key] = parseStringifiedValues(parsed[key]); + } + } + return parsed; + }; + + const formatReducerData = (reducerStates) => { + // Check if reducerStates exists and is an object + if (!reducerStates || typeof reducerStates !== 'object') { + return {}; + } + + // Handle both array and object formats + const statesArray = Array.isArray(reducerStates) ? reducerStates : Object.values(reducerStates); + + return statesArray.reduce((acc, reducer) => { + // Add additional type checking for reducer object + if (reducer && typeof reducer === 'object') { + acc[reducer.hookName || 'Reducer'] = reducer.state; } + return acc; + }, {}); + }; + + const renderSection = (title, content, isReducer = false) => { + if ( + !content || + (Array.isArray(content) && content.length === 0) || + Object.keys(content).length === 0 + ) { + return null; } - /* - Adds container name (State, Props, future different names for hooks) at top of object so everything nested in it will collapse when you click on it. - */ - printableObject[containerName] = data; - } + + // Parse any stringified JSON before displaying + const parsedContent = parseStringifiedValues(content); + + if (isReducer && parsedContent) { + // Only try to format if we have valid content + const formattedData = formatReducerData(parsedContent); + + // Check if we have any formatted data to display + if (Object.keys(formattedData).length === 0) { + return null; + } + + return ( +
+ {Object.entries(formattedData).map(([hookName, state]) => ( +
+
{hookName}
+
+ true} /> +
+
+ ))} +
+ ); + } + + return ( +
+
{title}
+
+ true} /> +
+
+ ); + }; return ( -
- ({ className: `tooltipData-JSONTree` }) }} // theme set to a base16 theme that has been extended to include "className: 'json-tree'" - shouldExpandNodeInitially={() => true} // determines if node should be expanded when it first renders (root is expanded by default) - hideRoot={true} // hides the root node - /> +
+ {renderSection('Props', data.componentData?.props)} + {renderSection('State', data.componentData?.state || data.componentData?.hooksState)} + {renderSection(null, data.componentData?.reducerStates, true)}
); }; diff --git a/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts b/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts index 979141275..600c884fa 100644 --- a/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts +++ b/src/app/components/StateRoute/ComponentMap/getLinkComponent.ts @@ -15,29 +15,17 @@ import { import { LinkComponent } from '../../../FrontendTypes'; /* - Changes the shape of the LinkComponent based on the layout, linkType, and orientation + Changes the shape of the LinkComponent based on the linkType, and orientation */ export default function getLinkComponent({ - layout, linkType, orientation, }: LinkComponent): React.ComponentType { let LinkComponent: React.ComponentType; - if (layout === 'polar') { - // if the layout is polar, linkType can be either step, curve, line, or a plain LinkRadial. - if (linkType === 'step') { - LinkComponent = LinkRadialStep; - } else if (linkType === 'curve') { - LinkComponent = LinkRadialCurve; - } else if (linkType === 'line') { - LinkComponent = LinkRadialLine; - } else { - LinkComponent = LinkRadial; - } - } else if (orientation === 'vertical') { - // if the layout isn't polar and the orientation is vertical, linkType can be either step, curve, line, or a plain LinkVertical +if (orientation === 'vertical') { + // if the orientation is vertical, linkType can be either step, curve, line, or a plain LinkVertical if (linkType === 'step') { LinkComponent = LinkVerticalStep; } else if (linkType === 'curve') { diff --git a/src/app/components/StateRoute/ComponentMap/heatMapLegend.tsx b/src/app/components/StateRoute/ComponentMap/heatMapLegend.tsx deleted file mode 100644 index 1440ad2a6..000000000 --- a/src/app/components/StateRoute/ComponentMap/heatMapLegend.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -export default function HeatMapLegend() { - return ( -
-
-
-
-
-
- ); -} \ No newline at end of file diff --git a/src/app/components/StateRoute/History.tsx b/src/app/components/StateRoute/History.tsx index a836fb1c4..9cfc79bf3 100644 --- a/src/app/components/StateRoute/History.tsx +++ b/src/app/components/StateRoute/History.tsx @@ -14,12 +14,16 @@ import { changeView, changeSlider, setCurrentTabInApp } from '../../slices/mainS */ const defaultMargin: DefaultMargin = { - top: 30, + top: 60, left: 30, right: 55, bottom: 70, }; +// Fixed node separation distances +const FIXED_NODE_HEIGHT = 150; // Vertical distance between nodes +const FIXED_NODE_WIDTH = 200; // Horizontal distance between nodes + // main function exported to StateRoute // below we destructure the props function History(props: Record): JSX.Element { @@ -36,126 +40,86 @@ function History(props: Record): JSX.Element { const dispatch = useDispatch(); const svgRef = React.useRef(null); - const root = JSON.parse(JSON.stringify(hierarchy)); // why do we stringify and then parse our hierarchy back to JSON? (asked 7/31/23) + const root = JSON.parse(JSON.stringify(hierarchy)); // setting the margins for the Map to render in the tab window. const innerWidth: number = totalWidth - margin.left - margin.right; const innerHeight: number = totalHeight - margin.top - margin.bottom - 60; - function labelCurrentNode(d3root) { - // iterates through the parents of a node and applies a color property - if (d3root.data.index === currLocation.index) { - // node.data aka d3root.data allows us to access associated node data. So if node.index === currLocation.index... - - let currNode = d3root; // make our input the currNode - - while (currNode.parent) { - // while there are parent nodes - currNode.color = '#999'; // change or give the node a color property - currNode = currNode.parent; // change currNode to the parent - } - - currNode.color = '#999'; // when there are no more parent nodes, change or give the last node a color property - - return d3root; // return the modified d3root - } - - let found; - - if (!d3root.children) { - // if root has no children array - return found; // return undefined - } - - d3root.children.forEach((child) => { - // for each child node within the children array - if (!found) { - // if found is undefined - found = labelCurrentNode(child); // - } - }); - return found; // return's the found child node - } - function findDiff(index) { - // determines the difference between our current index and the index-1 snapshot and produces an html string const statelessCleaning = (obj: { - //'statelessCleaning' functions in the same way as the 'statelessCleaning' function in Diff.tsx name?: string; componentData?: object; state?: string | any; stateSnaphot?: object; children?: any[]; }) => { - const newObj = { ...obj }; // duplicate our input object into a new object + if (!obj) return {}; // Add null check - if (newObj.name === 'nameless') { - // if our new object's name is nameless - delete newObj.name; // delete the name property - } - if (newObj.componentData) { - // if our new object has a componentData property - delete newObj.componentData; // delete the componentData property - } - if (newObj.state === 'stateless') { - // if if our new object's state is stateless - delete newObj.state; // delete the state property - } + const newObj = { ...obj }; + if (newObj.name === 'nameless') delete newObj.name; + if (newObj.componentData) delete newObj.componentData; + if (newObj.state === 'stateless') delete newObj.state; if (newObj.stateSnaphot) { - // if our new object has a stateSnaphot property - newObj.stateSnaphot = statelessCleaning(obj.stateSnaphot); // run statelessCleaning on the stateSnapshot + newObj.stateSnaphot = statelessCleaning(obj.stateSnaphot); } - if (newObj.children) { - // if our new object has a children property newObj.children = []; - if (obj.children.length > 0) { - // and if our input object's children property is non-empty, go through each children object from our input object and determine, if the object being iterated on either has a stateless state or has a children array with a non-zero amount of objects. Objects that fulfill the above that need to be cleaned through statelessCleaning. Those that are cleaned through this process are then pushed to the new object's children array. + // Add null check for children array + if (Array.isArray(obj.children) && obj.children.length > 0) { obj.children.forEach((element: { state?: object | string; children?: [] }) => { - if (element.state !== 'stateless' || element.children.length > 0) { + // Add null check for element + if ( + element && + ((element.state && element.state !== 'stateless') || + (element.children && element.children.length > 0)) + ) { const clean = statelessCleaning(element); newObj.children.push(clean); } }); } } - return newObj; // return the cleaned state snapshot(s) + return newObj; }; function findStateChangeObj(delta, changedState = []) { - // function determines whether delta has resulted in a changedState. Function would return an empty array if there were no changes to state and an array of objects that changed state. - if (!delta.children && !delta.state) { - // if delta doesn't have a children property or a state property - return changedState; // returns an empty array - } - + if (!delta) return changedState; // Add null check + if (!delta.children && !delta.state) return changedState; if (delta.state && delta.state[0] !== 'stateless') { - // ignore stateless delta objects - changedState.push(delta.state); // and push stateful delta objects to changedState - } - - if (!delta.children) { - // if the delta doesn't have any children - return changedState; // return the changedState array with any and all stateful delta objects. + changedState.push(delta.state); } - + if (!delta.children) return changedState; Object.keys(delta.children).forEach((child) => { - // but if the delta object did have children, we iterate through each child object - // if (isNaN(child) === false) { - changedState.push(...findStateChangeObj(delta.children[child])); // recursively call this function on each child object. Push every 'stateful' child into the changedState array. - // } + if (delta.children[child]) { + // Add null check + changedState.push(...findStateChangeObj(delta.children[child])); + } }); + return changedState; + } - return changedState; // return the changedState array with any and all stateful delta objects. + if (index === 0) return 'Initial State'; + + // Add null checks for snapshots + if (!snapshots || !snapshots[index] || !snapshots[index - 1]) { + return 'No state changes'; } - const delta = diff( - // 'diff' function from 'jsondiffpatch' returns the difference in state between the (snapshot that occurred before the indexed snapshot) and the (indexed snapshot). - statelessCleaning(snapshots[index - 1]), - statelessCleaning(snapshots[index]), - ); - const changedState = findStateChangeObj(delta); // determines if delta had any stateful changes - const html = formatters.html.format(changedState[0]); // formats the difference into html string - return html; // return html string + try { + const delta = diff( + statelessCleaning(snapshots[index - 1]), + statelessCleaning(snapshots[index]), + ); + + if (!delta) return 'No state changes'; + + const changedState = findStateChangeObj(delta); + console.log('changed state', formatters.html.format(changedState[0])); + return changedState.length > 0 ? formatters.html.format(changedState[0]) : 'No state changes'; + } catch (error) { + console.error('Error in findDiff:', error); + return 'Error comparing states'; + } } /** @@ -163,145 +127,119 @@ function History(props: Record): JSX.Element { */ const makeD3Tree = () => { - const svg = d3.select(svgRef.current); // d3.select Selects the first element/node that matches svgRef.current. If no element/node match returns an empty selection. If multiple elements/nodes match the selector, only the first matching element/node (in document order) will be selected. - svg.selectAll('*').remove(); // Selects all elements. The elements will be selected in document order (top-to-bottom). We then remove the selected elements/nodes from the DOM. This is important as to ensure that the SVG is empty before rendering the D3 based visualization to avoid interference/overlap with any previously rendered content. - const tree = (data) => { - // function that takes in data and turns it into a d3 tree. - const treeRoot = d3.hierarchy(data); // 'd3.hierarchy' constructs a root node from the specified hierarchical data. - return d3.tree().size([innerWidth, innerHeight])(treeRoot); // d3.tree creates a new tree layout with a size option of innerWidth (~line 41) and innerHeight (~line 42). We specify our new tree layout's root as 'treeRoot' which assigns an x and y property to each node to represent an [x, y] coordinate system. - }; + const svg = d3.select(svgRef.current); + svg.selectAll('*').remove(); + + // Create tree layout with fixed node size + const treeLayout = d3 + .tree() + .nodeSize([FIXED_NODE_WIDTH, FIXED_NODE_HEIGHT]) + .separation((a, b) => { + // Increase separation between unrelated subtrees + return a.parent === b.parent ? 1.2 : 2; + }); - const d3root = tree(root); // create a d3. tree from our root - const currNode = labelCurrentNode(d3root); // iterate through our nodes and apply a color property + // Create hierarchy and compute initial layout + const d3root = d3.hierarchy(root); + treeLayout(d3root); + + // Calculate the bounds of the tree + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + d3root.each((d) => { + minX = Math.min(minX, d.x); + maxX = Math.max(maxX, d.x); + minY = Math.min(minY, d.y); + maxY = Math.max(maxY, d.y); + }); - const g = svg //serves as a container for the nodes and links within the D3 Visualization of the tree - .append('g') // create an element 'g' on svg - .attr( - 'transform', - `translate(${margin.left},${d3root.height === 0 ? totalHeight / 2 : margin.top})`, //Set the position of the group 'g' by translating it horizontally by 'margin.left' pixels and vertically based on the conditional expression. - ); + // Calculate the actual dimensions needed + const actualWidth = maxX - minX + FIXED_NODE_WIDTH; + const actualHeight = maxY - minY + FIXED_NODE_HEIGHT; + + // Set SVG dimensions to accommodate the tree + const svgWidth = Math.max(actualWidth + margin.left + margin.right, totalWidth); + svg + .attr('width', svgWidth) + .attr('height', Math.max(actualHeight + margin.top + margin.bottom, totalHeight)); + + // Calculate center offset to keep root centered + const rootOffset = -d3root.x; + const horizontalCenter = svgWidth / 2; + + // Create container group and apply transforms + const g = svg + .append('g') + .attr('transform', `translate(${horizontalCenter + rootOffset},${margin.top})`); - const link = g //responsible for rendering the links or connectors between the nodes in the d3 Tree - .selectAll('.link') // select all elements that contain the string '.link' and return a selection + // Draw links + const link = g + .selectAll('.link') .data(d3root.descendants().slice(1)) .enter() .append('path') - .attr('class', 'link') - .attr('stroke', '#161617') - .attr('fill', 'none') - .attr( - //defines the path attribute (d) for each link (edge) between nodes, using a Bézier curve (C) to connect the source node's coordinates (d.x, d.y) to the midpoint between the source and target nodes and then to the target node's coordinates (d.parent.x, d.parent.y) - 'd', - (d) => - `M${d.x},${d.y}C${d.x},${(d.y + d.parent.y) / 2} ${d.parent.x},${ - (d.y + d.parent.y) / 2 - } ${d.parent.x},${d.parent.y}`, - ) .attr('class', (d) => { - // Adding a class based on the current node's data - if (d.data.index === currLocation.index) { - return 'link current-link'; // Apply both 'link' and 'current-link' classes - } - return 'link'; // Apply only the 'link' class + return d.data.index === currLocation.index ? 'link current-link' : 'link'; + }) + .attr('d', (d) => { + return `M${d.x},${d.y} + C${d.x},${(d.y + d.parent.y) / 2} + ${d.parent.x},${(d.y + d.parent.y) / 2} + ${d.parent.x},${d.parent.y}`; }); - const node = g //responsible for rendering nodes in d3 visualization tree + // Create node groups + const node = g .selectAll('.node') .data(d3root.descendants()) .enter() .append('g') - .style('cursor', 'pointer') - .attr('class', `snapshotNode`) + .attr('class', (d) => { + const baseClass = 'node'; + const internalClass = d.children ? ' node--internal' : ''; + const activeClass = d.data.index === currLocation.index ? ' active' : ''; + return baseClass + internalClass + activeClass; + }) + .attr('transform', (d) => `translate(${d.x},${d.y})`) .on('click', (event, d) => { dispatch(changeView(d.data.index)); dispatch(changeSlider(d.data.index)); - /* - created popup div and appended it to display div(returned in this function) - - D3 doesn't utilize z-index for priority, rather decides on placement by order of rendering needed to define the return div with a className to have a target to append to with the correct level of priority - */ - function renderToolTip() { - const [x, y] = d3.pointer(event); - const div = d3 - .select('.display:first-child') - .append('div') - .attr('class', `tooltip`) - .attr('id', `tt-${d.data.index}`) - .style('left', `${event.clientX - 10}px`) - .style('top', `${event.clientY - 10}px`) - .style('max-height', `25%`) - .style('overflow', `scroll`); - d3.selectAll('.tooltip').html(findDiff(d.data.index)); - } - - if (d3.selectAll('.tooltip')._groups['0'].length === 0) { - renderToolTip(); //if there are no tooltips left in the doc, we call the function to create a new tooltip - } else { - if (d3.selectAll(`#tt-${d.data.index}`)._groups['0'].length === 0) { - // if there is no tooltip with the specific id - d3.selectAll('.tooltip').remove(); //remove any existing tooltips - renderToolTip(); //call the function again to create a new tooltip - } - } - }) - .on('mouseenter', function (event, d) { - const [x, y] = d3.pointer(event); - if (d3.selectAll('.tooltip')._groups['0'].length === 0) { - const div = d3 - .select('.display:first-child') - .append('div') - .attr('class', `tooltip`) - .attr('id', `tt-${d.data.index}`) - .style('left', `${event.clientX + 0}px`) - .style('top', `${event.clientY + 0}px`) - .style('max-height', `25%`) - .style('overflow', `auto`) - .on('mouseenter', function (event, d) {}) - .on('mouseleave', function (event, d) { - d3.selectAll('.tooltip').remove().style('display', 'hidden'); - }); - - d3.selectAll('.tooltip').html(findDiff(d.data.index)); - } - }) - .on('mouseleave', function (event, d) { - if (event.relatedTarget.id !== `tt-${d.data.index}`) { - d3.selectAll('.tooltip').transition().delay(100).remove(); - } - }) - - .attr('transform', function (d) { - return `translate(${d.x},${d.y})`; - }); - - const tooltip = d3 - .select('.tooltip') - .on('mousemove', function (event, d) { - d3.select('.tooltip').style('opacity', '1'); - }) - .on('mouseleave', function (event, d) { - d3.selectAll('.tooltip').remove(); }); + // Add rectangles for nodes with consistent size node - .append('circle') - .attr('fill', (d) => { - if (d.data.index === currLocation.index) { - return '#284b63'; - } - return d.color ? d.color : '#555'; - }) - .attr('r', 18); - + .append('rect') + .attr('width', 200) + .attr('height', 120) + .attr('x', -100) + .attr('y', -40) + .attr('rx', 8) + .attr('ry', 8); + + // Add snapshot titles node .append('text') - .attr('dy', '0.31em') + .attr('dy', '-20') .attr('text-anchor', 'middle') - .attr('fill', 'white') - .text((d) => `${d.data.name}.${d.data.branch}`) - .clone(true) - .lower() - .attr('stroke', 'white'); + .attr('class', 'snapshot-title') + .text((d) => `Snapshot ${d.data.index + 1}`); + + // Add state changes text + node + .append('foreignObject') + .attr('x', -85) + .attr('y', -15) + .attr('width', 170) + .attr('height', 90) + .append('xhtml:div') + .style('font-size', '12px') + .style('text-align', 'left') + .style('padding-left', '8px') + .html((d) => findDiff(d.data.index)); + return svg.node(); }; diff --git a/src/app/components/StateRoute/PerformanceVisx/BarGraph.tsx b/src/app/components/StateRoute/PerformanceVisx/BarGraph.tsx index 1b07f9223..3966b7e68 100644 --- a/src/app/components/StateRoute/PerformanceVisx/BarGraph.tsx +++ b/src/app/components/StateRoute/PerformanceVisx/BarGraph.tsx @@ -24,7 +24,7 @@ const margin = { top: 30, right: 30, bottom: 0, - left: 50, + left: 70, }; const axisColor = '#161617'; const axisTickLabelColor = '#363638'; @@ -32,11 +32,10 @@ const axisLabelColor = '#363638'; const tooltipStyles = { ...defaultStyles, minWidth: 60, - //backgroundColor: 'rgba(0,0,0,0.9)', //defaults to white - //color: 'white', //defaults to a gray - fontSize: '16px', lineHeight: '18px', - fontFamily: 'Roboto', + pointerEvents: 'all !important', + padding: '8px', + borderRadius: '8px', }; const BarGraph = (props: BarGraphProps): JSX.Element => { @@ -75,7 +74,7 @@ const BarGraph = (props: BarGraphProps): JSX.Element => { const keys = Object.keys(data.componentData); const getSnapshotId = (d: snapshot) => d.snapshotId; // data accessor (used to generate scales) and formatter (add units for on hover box). d comes from data.barstack post filtered data - const formatSnapshotId = (id) => `Snapshot ID: ${id}`; // returns snapshot id when invoked in tooltip section + const formatSnapshotId = (id) => `ID: ${id}`; // returns snapshot id when invoked in tooltip section const formatRenderTime = (time) => `${time} ms `; // returns render time when invoked in tooltip section const snapshotIdScale = scaleBand({ @@ -91,16 +90,16 @@ const BarGraph = (props: BarGraphProps): JSX.Element => { }); const LMcolorScale = [ - '#a0c1d6', - '#669bbc', - '#105377', - '#003049', - '#55a8ac', - '#3c6e71', - '#1c494b', - '#c1676d', - '#c1121f', - '#780000', + '#14b8a6', // Teal (matching existing accent) + '#0d9488', // Darker teal (matching existing accent) + '#3c6e71', // Primary strong teal + '#284b63', // Primary blue + '#2c5282', // Deeper blue + '#1a365d', // Navy + '#2d3748', // Blue gray + '#4a5568', // Darker blue gray + '#718096', // Medium blue gray + '#a0aec0', // Light blue gray ]; const colorScale = scaleOrdinal({ @@ -121,53 +120,11 @@ const BarGraph = (props: BarGraphProps): JSX.Element => { data, }; - useEffect(() => { - // Animates the save series button. - const saveButtons = document.getElementsByClassName('save-series-button'); // finds the buttom in the DOM - for (let i = 0; i < saveButtons.length; i++) { - if (tabs[currentTab].seriesSavedStatus === 'saved') { - saveButtons[i].classList.add('animate'); - saveButtons[i].innerHTML = 'Saved!'; - } else { - saveButtons[i].innerHTML = 'Save Series'; - saveButtons[i].classList.remove('animate'); - } - } - }); - - const saveSeriesClickHandler = () => { - // function to save the currently selected series - if (tabs[currentTab].seriesSavedStatus === 'inputBoxOpen') { - const actionNames = document.getElementsByClassName('actionname'); - for (let i = 0; i < actionNames.length; i += 1) { - toStorage.data.barStack[i].name = actionNames[i].value; - } - dispatch(save({ newSeries: toStorage, newSeriesName: seriesNameInput })); // saves the series under seriesName - setSeriesNameInput(`Series ${comparison.length}`); // sends a reducer that saves the series/toStorage object the user wants to chrome local storage - return; - } - //if for some reason, code doesn't hit in first conditional, we have error handling below to account it - dispatch(save({ newSeries: toStorage, newSeriesName: '' })); // or use a default value for newSeriesName - }; - - const textbox = // Need to change so textbox isn't empty before saving - tabs[currentTab].seriesSavedStatus === 'inputBoxOpen' ? ( - setSeriesNameInput(e.target.value)} - /> - ) : null; return (
- {textbox} -
- +
- +
@@ -299,17 +260,18 @@ const BarGraph = (props: BarGraphProps): JSX.Element => { fontSize: 11, textAnchor: 'middle', })} + tickFormat={() => ''} // Add this line to hide tick labels /> - + Rendering Time (ms)
{snapshot === 'All Snapshots' ? ( - + Snapshot ID ) : ( - + Components )} @@ -324,11 +286,15 @@ const BarGraph = (props: BarGraphProps): JSX.Element => { left={tooltipLeft} style={tooltipStyles} > -
+
{' '} {tooltipData.key}{' '}
-
{'State: ' + data.componentData[tooltipData.key].stateType}
{'Render time: ' + formatRenderTime(tooltipData.bar.data[tooltipData.key])}
{' '} diff --git a/src/app/components/StateRoute/PerformanceVisx/BarGraphComparison.tsx b/src/app/components/StateRoute/PerformanceVisx/BarGraphComparison.tsx deleted file mode 100644 index 82b8ce6c5..000000000 --- a/src/app/components/StateRoute/PerformanceVisx/BarGraphComparison.tsx +++ /dev/null @@ -1,485 +0,0 @@ -// @ts-nocheck -/// -/* eslint-disable no-param-reassign */ -import React, { useEffect, useState } from 'react'; -import { BarStack } from '@visx/shape'; -import { Group } from '@visx/group'; -import { Grid } from '@visx/grid'; -import { AxisBottom, AxisLeft } from '@visx/axis'; -import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale'; -import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'; -import { Text } from '@visx/text'; -import { schemeTableau10 } from 'd3-scale-chromatic'; -import Select from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import { useTheme } from '@mui/material/styles'; -import { Button, InputLabel } from '@mui/material'; -import { onHover, onHoverExit, deleteSeries, setCurrentTabInApp } from '../../../slices/mainSlice'; -import { useSelector, useDispatch } from 'react-redux'; -import { - snapshot, - TooltipData, - Margin, - BarGraphComparisonProps, - ActionObj, - Series, -} from '../../../FrontendTypes'; - -/* DEFAULTS */ -const margin: Margin = { - top: 30, - right: 30, - bottom: 0, - left: 50, -}; -const axisColor = '#161617'; -const axisTickLabelColor = '#363638'; -const axisLabelColor = '#363638'; -const background = '#ffffff'; -const tooltipStyles = { - ...defaultStyles, - minWidth: 60, - // backgroundColor: 'rgba(0,0,0,0.9)', //defaults to white - // color: 'white', //defaults to gray - fontSize: '14px', - lineHeight: '18px', - fontFamily: 'Roboto', -}; - -const BarGraphComparison = (props: BarGraphComparisonProps): JSX.Element => { - const dispatch = useDispatch(); - const tabs = useSelector((state) => state.main.tabs); - const currentTab = useSelector((state) => state.main.currentTab); - - const { - width, // from ParentSize provided in StateRoute - height, // from ParentSize provided in StateRoute - data, // Acquired from getPerfMetrics(snapshots, getSnapshotIds(hierarchy)) in 'PerformanceVisx' - comparison, // result from invoking 'allStorage' in 'PerformanceVisx' - setSeries, // setter function to update the state located in 'PerfomanceVisx' - series, // initialized as boolean, can be an object, from state set in 'PerformanceVisx' - setAction, // setter function to update the state located in 'PerfomanceVisx' - } = props; - const [snapshots] = useState(0); // creates a local state snapshots and sets it to a value of 0 (why is there no setter function? Also, why use state when it's only referenced once and never changed? 08/03/2023) - const [open, setOpen] = useState(false); // creates a local state setOpen and sets it to false (why is there no setter function? 08/03/2023) - const [picOpen, setPicOpen] = useState(false); // creates a local state setPicOpen and sets it to false (why is there no setter function? 08/03/2023) - const [buttonLoad, setButtonLoad] = useState(false); //tracking whether or not the clear series button is clicked - const theme = useTheme(); // MUI hook that allows access to theme variables inside your functional React components - - useEffect(() => { - dispatch(setCurrentTabInApp('performance-comparison')); // dispatch sent at initial page load allowing changing "immer's" draft.currentTabInApp to 'performance-comparison' to facilitate render. - }, [dispatch]); - - const currentIndex: number = tabs[currentTab].sliderIndex; - - const { - tooltipData, // value/data that tooltip may need to render - tooltipLeft, // number used for tooltip positioning - tooltipTop, // number used for tooltip positioning - tooltipOpen, // boolean whether the tooltip state is open or closed - showTooltip, // function to set tooltip state - hideTooltip, // function to close a tooltip - } = useTooltip(); // returns an object with several properties that you can use to manage the tooltip state of your component - let tooltipTimeout: number; - - const { - containerRef, // Access to the container's bounding box. This will be empty on first render. - TooltipInPortal, // Visx component that renders Tooltip or TooltipWithBounds in a Portal, outside of your component DOM tree - } = useTooltipInPortal(); - - const keys: string[] = Object.keys(data.componentData); - - // data accessor (used to generate scales) and formatter (add units for on hover box) - const getSnapshotId = (d: snapshot) => d.snapshotId; - const formatSnapshotId = (id: string): string => `Snapshot ID: ${id}`; - const formatRenderTime = (time: string): string => `${time} ms`; - const getCurrentTab = (storedSeries: ActionObj) => storedSeries.currentTab; - - // create visualization SCALES with cleaned data - const xAxisPoints: string[] = ['currentTab', 'comparison']; - const snapshotIdScale = scaleBand({ - domain: xAxisPoints, // the domain array/xAxisPoints elements will place the bars along the x-axis - padding: 0.2, - }); - - const calculateMaxTotalRender = (serie: number): number => { - // This function will iterate through the snapshots of the series, and grab the highest render times (sum of all component times). We'll then use it in the renderingScale function and compare with the render time of the current tab. The max render time will determine the Y-axis's highest number. - const currentSeriesBarStacks: ActionObj[] = !comparison[serie] - ? [] - : comparison[serie].data.barStack; - if (currentSeriesBarStacks.length === 0) return 0; - - let currentMax = -Infinity; - - for (let i = 0; i < currentSeriesBarStacks.length; i += 1) { - const renderTimes: number[] = Object.values(currentSeriesBarStacks[i]).slice(1); - const renderTotal: number = renderTimes.reduce((acc, curr) => acc + curr); - - if (renderTotal > currentMax) currentMax = renderTotal; - } - return currentMax; - }; - - const renderingScale = scaleLinear({ - // this function will use the domain array to assign each key a different color to make rectangle boxes and use range to set the color scheme each bar - domain: [0, Math.max(calculateMaxTotalRender(series), data.maxTotalRender)], // [minY, maxY] the domain array on rendering scale will set the coordinates for Y-axis points. - nice: true, - }); - - const LMcolorScale = ['#a0c1d6','#669bbc','#105377','#003049','#55a8ac','#3c6e71','#1c494b','#c1676d','#c1121f','#780000'] - const colorScale = scaleOrdinal({ - domain: keys, // the domain array will assign each key a different color to make rectangle boxes - range: LMcolorScale, // and use range to set the color scheme each bar - }); - - // setting max dimensions and scale ranges - const xMax = width - margin.left - margin.right; - const yMax = height - margin.top - 200; - snapshotIdScale.rangeRound([0, xMax]); - renderingScale.range([yMax, 0]); - - const handleSeriesChange = (event: Event) => { - if (!event) { - return; - } - const target = event.target as HTMLInputElement; - if (target) { - setSeries(target.value); - setAction(false); - } - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleOpen = () => { - setOpen(true); - }; - - const handleActionChange = (event: Event) => { - const target = event.target as HTMLInputElement; - if (!target.value) return; - if (target) { - setAction(target.value); - setSeries(false); - } - }; - - const picHandleClose = () => { - setPicOpen(false); - }; - - const picHandleOpen = () => { - setPicOpen(true); - }; - - function setXpointsComparison() { - // manually assigning X -axis points with tab ID. - comparison[series].data.barStack.forEach((elem: ActionObj) => { - elem.currentTab = 'comparison'; - }); - return comparison[series].data.barStack; - } - - function setXpointsCurrentTab() { - data.barStack.forEach((element) => { - element.currentTab = 'currentTab'; - }); - return data.barStack; - } - - const seriesList: ActionObj[][] = comparison.map((action: Series) => action.data.barStack); - const actionsList: ActionObj[] = seriesList.flat(); - const testList: string[] = actionsList.map((elem: ActionObj) => elem.name); - - const finalList = []; - for (let i = 0; i < testList.length; i += 1) { - if (testList[i] !== '' && !finalList.includes(testList[i])) finalList.push(testList[i]); - } - - return ( -
-
-
- {/*'Clear Series' MUI button that clears any saved series*/} - - - - - COMPARE SERIES - - - - {/* Mui 'Compare Series Dropdown ENDS here */} - - {/*==============================================================================================================================*/} - {/*commented the below portion out, as bargraphComparisonActions.tsx is not currently functional, can re implement later on */} - {/*==============================================================================================================================*/} - - {/* {

Compare Actions

- - {' '} - {/* MUI styled 'FormControl' component */} - {/* - Compare Actions - - - {!comparison[snapshots] ? ( - No snapshots available - ) : ( - finalList.map((elem) => {elem}) - )} - - - */} - {/*==============================================================================================================================*/} - {/*==============================================================================================================================*/} -
-
- - - - - - - {( - barStacks, // overides render function which is past the configured stack generator - ) => - barStacks.map((barStack, idx) => { - // Uses map method to iterate through all components, creating a rect component, from visx, for each iteration. height, width, etc are calculated by visx to set X and Y scale. The scaler will used the passed in function and will run it on the array thats outputted by data - const bar = barStack.bars[currentIndex]; - if (Number.isNaN(bar.bar[1]) || bar.height < 0) { - bar.height = 0; - } - return ( - { - // Hides tool tip once cursor moves off the current rect - dispatch( - onHoverExit(data.componentData[bar.key].rtid), - (tooltipTimeout = window.setTimeout(() => { - hideTooltip(); - }, 300)), - ); - }} - // Cursor position in window updates position of the tool tip - onMouseMove={(event) => { - // Cursor position in window updates position of the tool tip - dispatch(onHover(data.componentData[bar.key].rtid)); - if (tooltipTimeout) clearTimeout(tooltipTimeout); - const top = event.clientY - margin.top - bar.height; - const left = bar.x + bar.width / 2; - showTooltip({ - tooltipData: bar, - tooltipTop: top, - tooltipLeft: left, - }); - }} - /> - ); - }) - } - - - ({ - fill: axisTickLabelColor, - fontSize: 11, - textAnchor: 'middle', - })} - /> - - Rendering Time (ms) - - - Series ID - - - {/* FOR HOVER OVER DISPLAY */} - {tooltipOpen && tooltipData && ( - -
- {' '} - {tooltipData.key}{' '} -
-
{'State: ' + data.componentData[tooltipData.key].stateType}
-
{'Render time: ' + formatRenderTime(tooltipData.bar.data[tooltipData.key])}
-
- {' '} - {formatSnapshotId(getSnapshotId(tooltipData.bar.data))} -
-
- )} -
- ); -}; - -export default BarGraphComparison; diff --git a/src/app/components/StateRoute/PerformanceVisx/BarGraphComparisonActions.tsx b/src/app/components/StateRoute/PerformanceVisx/BarGraphComparisonActions.tsx deleted file mode 100644 index 837e4ba03..000000000 --- a/src/app/components/StateRoute/PerformanceVisx/BarGraphComparisonActions.tsx +++ /dev/null @@ -1,332 +0,0 @@ -// @ts-nocheck -import React, { useEffect, useState } from 'react'; -import { BarStack } from '@visx/shape'; -import { Group } from '@visx/group'; -import { Grid } from '@visx/grid'; -import { AxisBottom, AxisLeft } from '@visx/axis'; -import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale'; -import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'; -import { Text } from '@visx/text'; -import { schemeSet3 } from 'd3-scale-chromatic'; -import { styled } from '@mui/system'; -import Select from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import { useTheme } from '@mui/material/styles'; -import { Button } from '@mui/material'; -import { deleteSeries, setCurrentTabInApp } from '../../../slices/mainSlice'; -import { useDispatch } from 'react-redux'; -import { TooltipData, Margin, BarGraphComparisonAction, ActionObj } from '../../../FrontendTypes'; - -/* DEFAULTS */ -const margin: Margin = { - top: 30, - right: 30, - bottom: 0, - left: 50, -}; -const axisColor = '#ff0000'; //#62d6fb -const background = '#242529'; -const tooltipStyles = { - ...defaultStyles, - minWidth: 60, - backgroundColor: 'rgba(0,0,0,0.9)', - color: 'white', - fontSize: '14px', - lineHeight: '18px', - fontFamily: 'Roboto', -}; - -const BarGraphComparisonActions = (props: BarGraphComparisonAction) => { - const dispatch = useDispatch(); - const { - width, // from ParentSize provided in StateRoute - height, // from ParentSize provided in StateRoute - data, - comparison, // returned value from invoking 'allStorage()' in 'PerformanceVisx' which is an array of objects - setSeries, // setter function to update the state located in 'PerfomanceVisx' - series, // boolean from state set in 'PerformanceVisx' - setAction, // setter function to update the state located in 'PerfomanceVisx' - action, // boolean from state set in 'PerformanceVisx' - } = props; - const [snapshots] = React.useState(0); // creates a local state snapshots and sets it to a value of 0 (why is there no setter function? 08/03/2023) - const [setOpen] = React.useState(false); // creates a local state setOpen and sets it to false (why is there no setter function? Also this is never used in this file... 08/03/2023) - const [setPicOpen] = React.useState(false); // creates a local state setPicOpen and sets it to false (why is there no setter function? Also this is never used in this file... 08/03/2023) - - useEffect(() => { - // send dispatch only on initial page load - dispatch(setCurrentTabInApp('performance-comparison')); // dispatch sent at initial page load allowing changing "immer's" draft.currentTabInApp to 'performance-comparison' to facilitate render. - }, []); - - const { - tooltipOpen, // boolean whether the tooltip state is open or closed - tooltipLeft, // number used for tooltip positioning - tooltipTop, // number used for tooltip positioning - tooltipData, // value/data that tooltip may need to render - hideTooltip, // function to close a tooltip - showTooltip, // function to set tooltip state - } = useTooltip(); // returns an object with several properties that you can use to manage the tooltip state of your component - let tooltipTimeout: number; - - const { - containerRef, // Access to the container's bounding box. This will be empty on first render. - TooltipInPortal, // TooltipWithBounds in a Portal, outside of your component DOM tree - } = useTooltipInPortal(); // Visx hook - - const keys = Object.keys(data[0]).filter( - (componentName) => - componentName !== 'name' && componentName !== 'seriesName' && componentName !== 'snapshotId', - ); - - const getSeriesName = (action: ActionObj): string => action.seriesName; // data accessor (used to generate scales) and formatter (add units for on hover box) - const seriesNameScale = scaleBand({ - // create visualization SCALES with cleaned data. - domain: data.map(getSeriesName), // the domain array/xAxisPoints elements will place the bars along the x-axis - padding: 0.2, - }); - - const calculateMaxTotalRender = () => { - // This function will iterate through the snapshots of the series, and grab the highest render times (the sum of all component times). We'll then use it in the renderingScale function and compare with the render time of the current tab. The max render time will determine the Y-axis's highest number. - let currentMax = -Infinity; - for (let i = 0; i < data.length; i += 1) { - let currentSum = 0; - for (const key of keys) if (data[i][key]) currentSum += data[i][key]; - if (currentSum > currentMax) currentMax = currentSum; - } - return currentMax; - }; - - // the domain array on rendering scale will set the coordinates for Y-aix points. - const renderingScale = scaleLinear({ - domain: [0, calculateMaxTotalRender()], // [minY, maxY] the domain array on rendering scale will set the coordinates for Y-axis points. - nice: true, // boolean on whether to round extreme values - }); - - const colorScale = scaleOrdinal({ - // the domain array will assign each key a different color to make rectangle boxes and use range to set the color scheme each bar - domain: keys, - range: schemeSet3, - }); - - // setting max dimensions and scale ranges - const xMax = width - margin.left - margin.right; - const yMax = height - margin.top - 200; - seriesNameScale.rangeRound([0, xMax]); - renderingScale.range([yMax, 0]); - - const StyledFormControl = styled(FormControl)(({ theme }) => ({ - margin: theme.spacing(1), - minWidth: 80, - height: 30, - })); - - const StyledSelect = styled(Select)({ - minWidth: 80, - fontSize: '.75rem', - fontWeight: 200, - height: 30, - }); - - const handleSeriesChange = (event) => { - if (!event) return; - setSeries(event.target.value); - setAction(false); - }; - - const handleActionChange = (event) => { - if (!event) return; - setAction(event.target.value); - setSeries(false); - }; - - const seriesList = comparison.map((elem) => elem.data.barStack); - const actionsList = seriesList.flat(); - const testList = actionsList.map((elem) => elem.name); - - const finalList = []; - for (let i = 0; i < testList.length; i += 1) { - if (testList[i] !== '' && !finalList.includes(testList[i])) finalList.push(testList[i]); - } - - return ( -
-
-
- -

Compare Series:

- - - {!comparison.length ? ( - No series available - ) : ( - comparison.map((tabElem, index) => ( - - {tabElem.name} - - )) - )} - - -

Compare Actions

- - - {!comparison[snapshots] ? ( - No snapshots available - ) : ( - finalList.map((elem) => ( - - {elem} - - )) - )} - - -
-
- - - - - - - {(barStacks) => - barStacks.map((barStack) => - barStack.bars.map((bar) => ( - { - tooltipTimeout = window.setTimeout(() => { - hideTooltip(); - }, 300); - }} - // Cursor position in window updates position of the tool tip. - onMouseMove={(event) => { - if (tooltipTimeout) clearTimeout(tooltipTimeout); - const top = event.clientY - margin.top - bar.height; - const left = bar.x + bar.width / 2; - showTooltip({ - tooltipData: bar, - tooltipTop: top, - tooltipLeft: left, - }); - }} - /> - )), - ) - } - - {/* Insert Action Comparison Barstack here */} - - ({ - fill: 'rgb(231, 231, 231)', - fontSize: 11, - verticalAnchor: 'middle', - textAnchor: 'end', - })} - /> - ({ - fill: 'rgb(231, 231, 231)', - fontSize: 11, - textAnchor: 'middle', - })} - /> - - Rendering Time (ms) - - - Series Name - - - {/* FOR HOVER OVER DISPLAY */} - {tooltipOpen && tooltipData && ( - -
- {tooltipData.key} -
-
{`${tooltipData.bar.data[tooltipData.key]} ms`}
-
- {tooltipData.bar.data.seriesName} -
-
- )} -
- ); -}; - -export default BarGraphComparisonActions; diff --git a/src/app/components/StateRoute/PerformanceVisx/PerformanceVisx.tsx b/src/app/components/StateRoute/PerformanceVisx/PerformanceVisx.tsx index 24ffbe82f..e13f649e7 100644 --- a/src/app/components/StateRoute/PerformanceVisx/PerformanceVisx.tsx +++ b/src/app/components/StateRoute/PerformanceVisx/PerformanceVisx.tsx @@ -4,10 +4,7 @@ /* eslint-disable max-len */ import React, { useState, useEffect } from 'react'; import { MemoryRouter as Router, Route, NavLink, Routes, Navigate } from 'react-router-dom'; -import RenderingFrequency from './RenderingFrequency'; import BarGraph from './BarGraph'; -import BarGraphComparison from './BarGraphComparison'; -import BarGraphComparisonActions from './BarGraphComparisonActions'; import { useDispatch, useSelector } from 'react-redux'; import { setCurrentTabInApp } from '../../../slices/mainSlice'; import { @@ -186,10 +183,7 @@ const PerformanceVisx = (props: PerformanceVisxProps): JSX.Element => { } = props; const dispatch = useDispatch(); const { currentTabInApp }: MainState = useSelector((state: RootState) => state.main); - const NO_STATE_MSG = 'No state change detected. Trigger an event to change state'; const data = getPerfMetrics(snapshots, getSnapshotIds(hierarchy)); - const [series, setSeries] = useState(true); - const [action, setAction] = useState(false); const [route, setRoute] = useState('All Routes'); const [snapshot, setSnapshot] = useState('All Snapshots'); @@ -252,74 +246,7 @@ const PerformanceVisx = (props: PerformanceVisxProps): JSX.Element => { return ( <> -
- - navData.isActive ? 'is-active router-link-performance' : 'router-link-performance' - } - end - to='/performance/' - > - Snapshots View - - - navData.isActive ? 'is-active router-link-performance' : 'router-link-performance' - } - id='router-link-performance-comparison' - to='/performance/comparison' - > - Comparison View - - - navData.isActive ? 'is-active router-link-performance' : 'router-link-performance' - } - to='/performance/componentdetails' - > - Component Details - -
- - {/* {renderForTutorial()} */} - - - ) : ( - - ) - } - /> - - ) : ( -
{NO_STATE_MSG}
- ) - } - /> { - const perfData = props.data; - const dispatch = useDispatch(); - useEffect(() => { - dispatch(setCurrentTabInApp('performance-comparison')); // dispatch sent at initial page load allowing changing "immer's" draft.currentTabInApp to 'performance-comparison' to facilitate render. - }, []); - - return ( -
- {Object.keys(perfData).map((componentName) => { - const currentComponent = perfData[componentName]; - return ( - - ); - })} -
- ); -}; - -const ComponentCard = (props): JSX.Element => { - const { componentName, stateType, averageRenderTime, renderFrequency, information } = props; - const [expand, setExpand] = useState(false); - - // render time for each component from each snapshot - // differences in state change that happened prior; - - const dataComponentArray = []; - for (let i = 0; i < information.length; i++) { - dataComponentArray.push( - , - ); - } - - return ( -
-
-
-

{componentName}

-

{stateType}

-

average time: {averageRenderTime} ms

-
-
{ - if (expand === true) { - setExpand(false); - } else { - setExpand(true); - } - }} - className='RenderRight' - > -

{renderFrequency}

-
-
-
- {expand === true ? dataComponentArray : null} -
-
- ); -}; - -const DataComponent = (props) => { - const { header, paragraphs } = props; - - return ( -
-

{header}

-

{`renderTime: ${paragraphs[0].rendertime}`}

-
- ); -}; - -export default RenderingFrequency; diff --git a/src/app/components/StateRoute/StateRoute.tsx b/src/app/components/StateRoute/StateRoute.tsx index 82aa0a8a4..c6911e79d 100644 --- a/src/app/components/StateRoute/StateRoute.tsx +++ b/src/app/components/StateRoute/StateRoute.tsx @@ -15,24 +15,20 @@ import { useDispatch, useSelector } from 'react-redux'; import PerformanceVisx from './PerformanceVisx/PerformanceVisx'; import WebMetricsContainer from './WebMetrics/WebMetricsContainer'; import { MainState, RootState, StateRouteProps } from '../../FrontendTypes'; -import type AxContainer from './AxMap/AxMap'; - -/* - Loads the appropriate StateRoute view and renders the Map, Performance, History, Webmetrics, and Tree navbar buttons after clicking on the 'State' button located near the top rightmost corner. -*/ +import AxContainer from './AxMap/AxContainer'; const History = require('./History').default; -const NO_STATE_MSG = 'No state change detected. Trigger an event to change state'; // message to be returned if there has been no state change detected in our hooked/target app +const NO_STATE_MSG = 'No state change detected. Trigger an event to change state'; const StateRoute = (props: StateRouteProps) => { const { - axSnapshots, // from 'tabs[currentTab]' object in 'MainContainer' - snapshot, // from 'tabs[currentTab]' object in 'MainContainer' - hierarchy: propsHierarchy, // from 'tabs[currentTab]' object in 'MainContainer' - snapshots, // from 'tabs[currentTab].snapshotDisplay' object in 'MainContainer' - viewIndex: propsViewIndex, // from 'tabs[currentTab]' object in 'MainContainer' - webMetrics, // from 'tabs[currentTab]' object in 'MainContainer' - currLocation, // from 'tabs[currentTab]' object in 'MainContainer' + axSnapshots, + snapshot, + hierarchy: propsHierarchy, + snapshots, + viewIndex: propsViewIndex, + webMetrics, + currLocation, } = props; const { tabs, currentTab }: MainState = useSelector((state: RootState) => state.main); @@ -40,158 +36,117 @@ const StateRoute = (props: StateRouteProps) => { const hierarchy = propsHierarchy || tabsHierarchy; const viewIndex = propsViewIndex || tabsViewIndex; - - // lines 45 - 63 contains functionality to disable the accessibility features in Reactime. const dispatch = useDispatch(); const [showTree, setShowTree] = useState(false); - const [selectedValue, setSelectedValue] = useState('disable'); const [showParagraph, setShowParagraph] = useState(true); const enableAxTreeButton = () => { dispatch(toggleAxTree('toggleAxRecord')); dispatch(setCurrentTabInApp('AxTree')); - setSelectedValue('enable'); setShowParagraph(false); setShowTree(true); }; - const disableAxTree = () => { - dispatch(toggleAxTree('toggleAxRecord')); - setSelectedValue('disable'); - setShowParagraph(true); - setShowTree(false); - }; - return (
-
- {/* all classnames below are functionally defined for styling purposes */} - - navData.isActive ? 'is-active router-link map-tab map-tab1' : 'router-link map-tab map-tab1' - } - end - > - Map - - - navData.isActive ? 'is-active router-link map-tab' : 'router-link map-tab' - } - to='/performance' - > - Performance - - - navData.isActive ? 'is-active router-link history-tab' : 'router-link history-tab' - } - to='/history' - > - History - - - navData.isActive - ? 'is-active router-link web-metrics-tab' - : 'router-link web-metrics-tab' - } - to='/webMetrics' - > - Web Metrics - - - navData.isActive ? 'is-active router-link tree-tab' : 'router-link tree-tab' - } - to='/tree' - > - Tree - - - navData.isActive - ? 'is-active router-link accessibility-tab' - : 'router-link accessibility-tab' - } - to='/accessibility' - > - Accessibility - +
+
+ + navData.isActive ? 'is-active router-link map-tab' : 'router-link map-tab' + } + end + > + Map + + + navData.isActive ? 'is-active router-link performance' : 'router-link performance-tab' + } + to='/performance' + > + Performance + + + navData.isActive ? 'is-active router-link history-tab' : 'router-link history-tab' + } + to='/history' + > + History + + + navData.isActive + ? 'is-active router-link web-metrics-tab' + : 'router-link web-metrics-tab' + } + to='/webMetrics' + > + Web Metrics + + + navData.isActive ? 'is-active router-link tree-tab' : 'router-link tree-tab' + } + to='/tree' + > + Tree + + + navData.isActive + ? 'is-active router-link accessibility-tab' + : 'router-link accessibility-tab' + } + to='/accessibility' + > + Accessibility + +
+
- { - enableAxTreeButton(); - }} - /> - - { - disableAxTree(); - }} - /> - - {showTree && } +
) : (
{showParagraph && ( -

- A Note to Developers: Reactime is using the Chrome Debugger API in order to - grab the Accessibility Tree. Enabling this option will allow you to record - Accessibility Tree snapshots, but will result in the Chrome browser notifying - you that the Chrome Debugger has started. -

+
+

+ A Note to Developers: Reactime is using the Chrome Debugger API in order to + grab the Accessibility Tree. Enabling this option will allow you to record + Accessibility Tree snapshots, but will result in the Chrome browser + notifying you that the Chrome Debugger has started. +

+
)} -
- { - { - enableAxTreeButton(); - }} - /> - } +
+ - { - { - disableAxTree(); - }} - /> - } -
) } - > + /> { width={width} height={height} hierarchy={hierarchy} - // Commented out dispatch that was prop drilled as conversion to RTK might invalidate need for prop drilling to access dispatch - // dispatch={dispatch} sliderIndex={sliderIndex} viewIndex={viewIndex} currLocation={currLocation} @@ -221,13 +174,12 @@ const StateRoute = (props: StateRouteProps) => { element={ hierarchy ? (
- + {({ width, height }) => ( { hierarchy ? ( {({ width, height }) => { - // eslint-disable-next-line react/prop-types - const maxHeight: number = 1200; + const maxHeight = 1200; const h = Math.min(height, maxHeight); return ( { }; export default StateRoute; - diff --git a/src/app/components/StateRoute/Tree.tsx b/src/app/components/StateRoute/Tree.tsx index 6c1ba84ef..7d83c30d5 100644 --- a/src/app/components/StateRoute/Tree.tsx +++ b/src/app/components/StateRoute/Tree.tsx @@ -55,20 +55,22 @@ const Tree = (props: TreeProps) => { return ( <> - {snapshot && ( - // @ts-ignore - + {snapshot && ( // @ts-ignore - data={snapshots[currLocation.index] || snapshot} // data to be rendered, a snapshot object - theme={{ extend: colors, tree: () => ({ className: 'json-tree' }) }} // theme set to a base16 theme that has been extended to include "className: 'json-tree'" - shouldExpandNodeInitially={() => true} // determines if node should be expanded when it first renders (root is expanded by default) - getItemString={getItemString} // allows the customization of how arrays, objects, and iterable nodes are displayed. - labelRenderer={(raw: any[]) => { - // renders a label if the first element of raw is a number. - return typeof raw[0] !== 'number' ? {raw[0]} : null; - }} - /> - )} + ({ className: 'json-tree' }) }} // theme set to a base16 theme that has been extended to include "className: 'json-tree'" + shouldExpandNodeInitially={() => true} // determines if node should be expanded when it first renders (root is expanded by default) + getItemString={getItemString} // allows the customization of how arrays, objects, and iterable nodes are displayed. + labelRenderer={(raw: any[]) => { + // renders a label if the first element of raw is a number. + return typeof raw[0] !== 'number' ? {raw[0]} : null; + }} + /> + )} +
); }; diff --git a/src/app/components/StateRoute/WebMetrics/WebMetrics.tsx b/src/app/components/StateRoute/WebMetrics/WebMetrics.tsx index 164530fc3..4bad170e0 100644 --- a/src/app/components/StateRoute/WebMetrics/WebMetrics.tsx +++ b/src/app/components/StateRoute/WebMetrics/WebMetrics.tsx @@ -30,7 +30,6 @@ const radialGraph = (props) => { margin: 0, size: '75%', background: 'transparent', - // background: '#242529', image: props.overLimit ? 'https://static.vecteezy.com/system/resources/thumbnails/012/042/301/small/warning-sign-icon-transparent-background-free-png.png' : undefined, @@ -51,7 +50,7 @@ const radialGraph = (props) => { track: { background: '#161617', strokeWidth: '3%', - margin: 0, // margin is in pixels + margin: 0, dropShadow: { enabled: true, top: -3, @@ -111,18 +110,18 @@ const radialGraph = (props) => {
-
+
-
+

{props.name}

diff --git a/src/app/components/TimeTravel/MainSlider.tsx b/src/app/components/TimeTravel/VerticalSlider.tsx similarity index 73% rename from src/app/components/TimeTravel/MainSlider.tsx rename to src/app/components/TimeTravel/VerticalSlider.tsx index 7af7c7673..58dc63916 100644 --- a/src/app/components/TimeTravel/MainSlider.tsx +++ b/src/app/components/TimeTravel/VerticalSlider.tsx @@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { HandleProps, MainSliderProps, MainState, RootState } from '../../FrontendTypes'; const { Handle } = Slider; // component constructor of Slider that allows customization of the handle - +//takes in snapshot length const handle = (props: HandleProps): JSX.Element => { const { value, dragging, index, ...restProps } = props; @@ -27,9 +27,9 @@ const handle = (props: HandleProps): JSX.Element => { ); }; -function MainSlider(props: MainSliderProps): JSX.Element { +function VerticalSlider(props: MainSliderProps): JSX.Element { const dispatch = useDispatch(); - const { snapshotsLength } = props; // destructure props to get our total number of snapshots + const { snapshots } = props; // destructure props to get our total number of snapshots const [sliderIndex, setSliderIndex] = useState(0); // create a local state 'sliderIndex' and set it to 0. const { tabs, currentTab }: MainState = useSelector((state: RootState) => state.main); const { currLocation } = tabs[currentTab]; // we destructure the currentTab object @@ -37,8 +37,15 @@ function MainSlider(props: MainSliderProps): JSX.Element { useEffect(() => { if (currLocation) { // if we have a 'currLocation' - //@ts-ignore - setSliderIndex(currLocation.index); // set our slider thumb position to the 'currLocation.index' + let correctedSliderIndex; + + for (let i = 0; i < snapshots.length; i++) { + //@ts-ignore -- ignores the errors on the next line + if (snapshots[i].props.index === currLocation.index) { + correctedSliderIndex = i; + } + } + setSliderIndex(correctedSliderIndex); } else { setSliderIndex(0); // just set the thumb position to the beginning } @@ -47,22 +54,20 @@ function MainSlider(props: MainSliderProps): JSX.Element { return ( { // when the slider position changes setSliderIndex(index); // update the sliderIndex - }} - onAfterChange={() => { - // after updating the sliderIndex - dispatch(changeSlider(sliderIndex)); // updates currentTab's 'sliderIndex' and replaces our snapshot with the appropriate snapshot[sliderIndex] - dispatch(pause()); // pauses playing and sets currentTab object'a intervalId to null + dispatch(changeSlider(snapshots[index].props.index)); }} handle={handle} /> ); } -export default MainSlider; +export default VerticalSlider; diff --git a/src/app/containers/ActionContainer.tsx b/src/app/containers/ActionContainer.tsx index d81e995a1..e1892f537 100644 --- a/src/app/containers/ActionContainer.tsx +++ b/src/app/containers/ActionContainer.tsx @@ -1,28 +1,25 @@ /* eslint-disable max-len */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import Action from '../components/Actions/Action'; -import SwitchAppDropdown from '../components/Actions/SwitchApp'; import { emptySnapshots, changeView, changeSlider } from '../slices/mainSlice'; import { useDispatch, useSelector } from 'react-redux'; import RouteDescription from '../components/Actions/RouteDescription'; +import DropDown from '../components/Actions/DropDown'; +import ProvConContainer from './ProvConContainer'; import { ActionContainerProps, CurrentTab, MainState, Obj, RootState } from '../FrontendTypes'; import { Button, Switch } from '@mui/material'; +import RecordButton from '../components/Actions/RecordButton'; /* This file renders the 'ActionContainer'. The action container is the leftmost column in the application. It includes the button that shrinks and expands the action container, a dropdown to select the active site, a clear button, the current selected Route, and a list of selectable snapshots with timestamps. */ // resetSlider locates the rc-slider elements on the document and resets it's style attributes -const resetSlider = () => { - const slider = document.querySelector('.rc-slider-handle'); - const sliderTrack = document.querySelector('.rc-slider-track'); - if (slider && sliderTrack) { - slider.setAttribute('style', 'left: 0'); - sliderTrack.setAttribute('style', 'width: 0'); - } -}; function ActionContainer(props: ActionContainerProps): JSX.Element { + const [dropdownSelection, setDropdownSelection] = useState('Time Jump'); + const actionsEndRef = useRef(null as unknown as HTMLDivElement); + const dispatch = useDispatch(); const { currentTab, tabs, port }: MainState = useSelector((state: RootState) => state.main); @@ -30,28 +27,20 @@ function ActionContainer(props: ActionContainerProps): JSX.Element { const { toggleActionContainer, // function that handles Time Jump Sidebar view from MainContainer actionView, // local state declared in MainContainer - setActionView, // function to update actionView state declared in MainContainer + setActionView, // function to update actionView state declared in MainContainer, + snapshots, } = props; const [recordingActions, setRecordingActions] = useState(true); // We create a local state 'recordingActions' and set it to true let actionsArr: JSX.Element[] = []; // we create an array 'actionsArr' that will hold elements we create later on // we create an array 'hierarchyArr' that will hold objects and numbers const hierarchyArr: (number | {})[] = []; - /* - function to traverse state from hierarchy and also getting information on display name and component name - - the obj parameter is an object with the following structure: - { - stateSnapshot: { - route: any; - children: any[]; - }; - name: number; - branch: number; - index: number; - children?: []; + // Auto scroll when snapshots change + useEffect(() => { + if (actionsEndRef.current) { + actionsEndRef.current.scrollIntoView({ behavior: 'smooth' }); } - */ + }, [snapshots]); // Dependency on snapshots array const displayArray = (obj: Obj): void => { if ( @@ -65,7 +54,7 @@ function ActionContainer(props: ActionContainerProps): JSX.Element { //This utility can be used to map the properties of a type to another type) and populate it's properties with //relevant values from our argument 'obj'. index: obj.index, - displayName: `${obj.name}.${obj.branch}`, + displayName: `${obj.index + 1}`, state: obj.stateSnapshot.children[0].state, componentName: obj.stateSnapshot.children[0].name, routePath: obj.stateSnapshot.route.url, @@ -80,6 +69,7 @@ function ActionContainer(props: ActionContainerProps): JSX.Element { if (obj.children) { // if argument has a 'children' array, we iterate through it and run 'displayArray' on each element obj.children.forEach((element): void => { + //recursive call displayArray(element); }); } @@ -127,6 +117,7 @@ function ActionContainer(props: ActionContainerProps): JSX.Element { const selected = index === viewIndex; // boolean on whether the current index is the same as the viewIndex const last = viewIndex === -1 && index === hierarchyArr.length - 1; // boolean on whether the view index is less than 0 and if the index is the same as the last snapshot's index value in hierarchyArr const isCurrIndex = index === currLocation.index; + return ( { - // setActionView(true); - // }, [setActionView]); - // Function sends message to background.js which sends message to the content script const toggleRecord = (): void => { port.postMessage({ @@ -191,54 +177,49 @@ function ActionContainer(props: ActionContainerProps): JSX.Element { className='toggle' > {' '} - {/* JR: updating onClick to stop propagation so that it detects the click only on the arrow and not the parent*/}
Collapse
- {/* - -
- Record -
- {recordingActions ? : } -
*/}
{actionView ? (
- - -
- Record -
- {recordingActions ? : } -
- -
+ { + toggleRecord(); + setRecordingActions(!recordingActions); + }} + /> + +
- {/* Rendering of route description components */} - {Object.keys(routes).map((route, i) => ( - - ))} +
+ {dropdownSelection === 'Providers / Consumers' && ( + + )} + {dropdownSelection === 'Time Jump' && + Object.keys(routes).map((route, i) => ( + + ))} + {/* Add ref for scrolling */} +
+
) : null}
diff --git a/src/app/containers/ButtonsContainer.tsx b/src/app/containers/ButtonsContainer.tsx index 2eadc0e76..9af8f1ea8 100644 --- a/src/app/containers/ButtonsContainer.tsx +++ b/src/app/containers/ButtonsContainer.tsx @@ -5,17 +5,13 @@ import { Button } from '@mui/material'; //importing necesary material UI components for dialogue popup import { Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; import Tutorial from '../components/Buttons/Tutorial'; -import LockIcon from '@mui/icons-material/Lock'; -import LockOpenIcon from '@mui/icons-material/LockOpen'; -import FileDownloadIcon from '@mui/icons-material/FileDownload'; -import FileUploadIcon from '@mui/icons-material/FileUpload'; import { toggleMode, importSnapshots, startReconnect } from '../slices/mainSlice'; import { useDispatch, useSelector } from 'react-redux'; import StatusDot from '../components/Buttons/StatusDot'; -import LoopIcon from '@mui/icons-material/Loop'; import CloseIcon from '@mui/icons-material/Close'; import WarningIcon from '@mui/icons-material/Warning'; import { MainState, RootState } from '../FrontendTypes'; +import { Lock, Unlock, Download, Upload, RefreshCw, X, AlertTriangle } from 'lucide-react'; function exportHandler(snapshots: []): void { // function that takes in our tabs[currentTab] object to be exported as a JSON file. NOTE: TypeScript needs to be updated @@ -91,115 +87,106 @@ function ButtonsContainer(): JSX.Element { return (
- - - - {/* The component below renders a button for the tutorial walkthrough of Reactime */} - - {/* adding a button for reconnection functionality 10/5/2023 */} - - -
- handleReconnectCancel()} - className='close-icon-pop-up' - /> -
-
- - WARNING -
- -

Status: {connectionStatus ? 'Connected' : 'Disconnected'}

- {connectionStatus ? ( - <> - Reconnecting while Reactime is still connected to the application may cause unforeseen - issues. Are you sure you want to proceed with the reconnection? - +
+ - {!connectionStatus && ( + {paused ? 'Locked' : 'Unlocked'} + + + + {/* The component below renders a button for the tutorial walkthrough of Reactime */} + + {/* adding a button for reconnection functionality 10/5/2023 */} + + +
+ handleReconnectCancel()} + className='close-icon-pop-up' + /> +
+
+ + WARNING +
+ +

Status: {connectionStatus ? 'Connected' : 'Disconnected'}

+ {connectionStatus ? ( + <> + Reconnecting while Reactime is still connected to the application may cause + unforeseen issues. Are you sure you want to proceed with the reconnection? + + ) : ( + <> + Reactime has unexpectedly disconnected from your application. To continue using + Reactime, please reconnect. +
+
+ WARNING: Reconnecting can sometimes cause unforeseen issues, consider downloading + the data before proceeding with the reconnection, if needed. + + )} +
+ + - )} - - -
+ + +
+
); } diff --git a/src/app/containers/MainContainer.tsx b/src/app/containers/MainContainer.tsx index 9f91bd980..4fee77640 100644 --- a/src/app/containers/MainContainer.tsx +++ b/src/app/containers/MainContainer.tsx @@ -19,7 +19,6 @@ import { } from '../slices/mainSlice'; import { useDispatch, useSelector } from 'react-redux'; import { MainState, RootState } from '../FrontendTypes'; -import HeatMapLegend from '../components/StateRoute/ComponentMap/heatMapLegend'; /* This is the main container where everything in our application is rendered @@ -33,7 +32,7 @@ function MainContainer(): JSX.Element { const { connectionStatus }: MainState = useSelector((state: RootState) => state.main); // JR 12.22.23: so far this log always returns true - // console.log('MainContainer connectionStatus at initialization: ', connectionStatus); + console.log('MainContainer connectionStatus at initialization: ', connectionStatus); const [actionView, setActionView] = useState(true); // We create a local state 'actionView' and set it to true @@ -41,6 +40,9 @@ function MainContainer(): JSX.Element { const toggleActionContainer = () => { setActionView(!actionView); // sets actionView to the opposite boolean value + const bodyContainer = document.getElementById('bodyContainer'); + bodyContainer.classList.toggle('collapsed'); + const toggleElem = document.querySelector('aside'); // aside is like an added text that appears "on the side" aside some text. toggleElem.classList.toggle('no-aside'); // toggles the addition or the removal of the 'no-aside' class @@ -212,6 +214,8 @@ function MainContainer(): JSX.Element { actionView={actionView} setActionView={setActionView} toggleActionContainer={toggleActionContainer} + snapshots={snapshots} + currLocation={currLocation} /> {/* @ts-ignore */} {snapshots.length ? ( @@ -231,9 +235,11 @@ function MainContainer(): JSX.Element { />
) : null} - {/* @ts-ignore */} - - +
+ {/* @ts-ignore */} + + +
); diff --git a/src/app/containers/ProvConContainer.tsx b/src/app/containers/ProvConContainer.tsx new file mode 100644 index 000000000..e2137116d --- /dev/null +++ b/src/app/containers/ProvConContainer.tsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { ProvConContainerProps, FilteredNode } from '../FrontendTypes'; +import { JSONTree } from 'react-json-tree'; + +const ProvConContainer = (props: ProvConContainerProps): JSX.Element => { + const jsonTheme = { + scheme: 'custom', + base00: 'transparent', + base0B: '#1f2937', // dark navy for strings + base0D: '#60a5fa', // Keys + base09: '#f59e0b', // Numbers + base0C: '#EF4444', // Null values + }; + + //deconstruct props + const { currentSnapshot } = props; + + //parse through node + const keepContextAndProviderNodes = (node) => { + if (!node) return null; + + // Check if this node should be kept + const hasContext = + node?.componentData?.context && Object.keys(node.componentData.context).length > 0; + const isProvider = node?.name && node.name.endsWith('Provider'); + const shouldKeepNode = hasContext || isProvider; + + // Process children first + let processedChildren = []; + if (node.children) { + processedChildren = node.children + .map((child) => keepContextAndProviderNodes(child)) + .filter(Boolean); // Remove null results + } + + // If this node should be kept or has kept children, return it + if (shouldKeepNode || processedChildren.length > 0) { + return { + ...node, + children: processedChildren, + }; + } + + // If neither the node should be kept nor it has kept children, filter it out + return null; + }; + const contextProvidersOnly = keepContextAndProviderNodes(currentSnapshot); + + const filterComponentProperties = (node: any): FilteredNode | null => { + if (!node) return null; + + // Helper function to check if an object is empty (including nested objects) + const isEmptyObject = (obj: any): boolean => { + if (!obj) return true; + if (Array.isArray(obj)) return obj.length === 0; + if (typeof obj !== 'object') return false; + + // Check each property recursively + for (const key in obj) { + const value = obj[key]; + if (typeof value === 'object') { + if (!isEmptyObject(value)) return false; + } else if (value !== undefined && value !== null) { + return false; + } + } + return true; + }; + + // Create a new object for the filtered node + const filteredNode: FilteredNode = {}; + + // Flatten root level props if they exist + if (node.props && !isEmptyObject(node.props)) { + Object.entries(node.props).forEach(([key, value]) => { + if (!isEmptyObject(value)) { + filteredNode[`prop_${key}`] = value; + } + }); + } + + // Flatten componentData properties into root level if they exist + if (node.componentData.context && !isEmptyObject(node.componentData.context)) { + // Add context directly if it exists + Object.entries(node.componentData.context).forEach(([key, value]) => { + if (!isEmptyObject(value)) { + filteredNode[`${key}`] = value; + } + }); + + // Flatten componentData.props if they exist + if (node.componentData.props && !isEmptyObject(node.componentData.props)) { + Object.entries(node.componentData.props).forEach(([key, value]) => { + if (!isEmptyObject(value)) { + filteredNode[`${key}`] = value; + } + }); + } + + // Flatten componentData.hooksState if it exists + if (node.componentData.hooksState && !isEmptyObject(node.componentData.hooksState)) { + Object.entries(node.componentData.hooksState).forEach(([key, value]) => { + if (!isEmptyObject(value)) { + filteredNode[`${key}`] = value; + } + }); + } + } + + // Flatten root level hooksState if it exists + if (node.hooksState && !isEmptyObject(node.hooksState)) { + Object.entries(node.hooksState).forEach(([key, value]) => { + if (!isEmptyObject(value)) { + filteredNode[`hook_${key}`] = value; + } + }); + } + + // Process children and add them using the node's name as the key + if (node.hasOwnProperty('children') && Array.isArray(node.children)) { + for (const child of node.children) { + const filteredChild = filterComponentProperties(child); + if (filteredChild && !isEmptyObject(filteredChild) && child.name) { + filteredNode[child.name] = filteredChild; + } + } + } + + // Only return the node if it has at least one non-empty property + return isEmptyObject(filteredNode) ? null : filteredNode; + }; + const filteredProviders = filterComponentProperties(contextProvidersOnly); + console.log('filtered', filteredProviders); + + const parseStringifiedValues = (obj) => { + if (!obj || typeof obj !== 'object') return obj; + + const parsed = { ...obj }; + for (const key in parsed) { + if (typeof parsed[key] === 'string') { + try { + // Check if the string looks like JSON + if (parsed[key].startsWith('{') || parsed[key].startsWith('[')) { + const parsedValue = JSON.parse(parsed[key]); + parsed[key] = parsedValue; + } + } catch (e) { + // If parsing fails, keep original value + continue; + } + } else if (typeof parsed[key] === 'object') { + parsed[key] = parseStringifiedValues(parsed[key]); + } + } + return parsed; + }; + + const renderSection = (title, content, isReducer = false) => { + if ( + !content || + (Array.isArray(content) && content.length === 0) || + Object.keys(content).length === 0 + ) { + return null; + } + + // Parse any stringified JSON before displaying + const parsedContent = parseStringifiedValues(content); + + return ( +
+
{title}
+
+ true} + shouldExpandNodeInitially={() => true} + /> +
+
+ ); + }; + + return ( +
+
Providers / Consumers
+ {filteredProviders ? ( +
{renderSection(null, filteredProviders)}
+ ) : ( +
+

No providers or consumers found in the current component tree.

+
+ )} +
+ ); +}; + +export default ProvConContainer; diff --git a/src/app/containers/StateContainer.tsx b/src/app/containers/StateContainer.tsx index 9dc9136bd..06f1a58b7 100644 --- a/src/app/containers/StateContainer.tsx +++ b/src/app/containers/StateContainer.tsx @@ -8,7 +8,6 @@ import StateRoute from '../components/StateRoute/StateRoute'; import DiffRoute from '../components/DiffRoute/DiffRoute'; import { StateContainerProps } from '../FrontendTypes'; import { Outlet } from 'react-router'; -import HeatMapLegend from '../components/StateRoute/ComponentMap/heatMapLegend'; // eslint-disable-next-line react/prop-types const StateContainer = (props: StateContainerProps): JSX.Element => { @@ -19,48 +18,17 @@ const StateContainer = (props: StateContainerProps): JSX.Element => { viewIndex, // from 'tabs[currentTab]' object in 'MainContainer' webMetrics, // from 'tabs[currentTab]' object in 'MainContainer' currLocation, // from 'tabs[currentTab]' object in 'MainContainer' - axSnapshots,// from 'tabs[currentTab]' object in 'MainContainer' + axSnapshots, // from 'tabs[currentTab]' object in 'MainContainer' } = props; return ( <> -
-
-
-
- - navData.isActive ? 'is-active main-router-link' : 'main-router-link' - } - to='/' - > - State - - - navData.isActive ? 'is-active main-router-link' : 'main-router-link' - } - to='/diff' - > - Diff - -
-
- - - - - {/* */} -
- } - /> - +
+
+ + { snapshots={snapshots} currLocation={currLocation} /> - {/* */} - {/* */} -
- } - /> - -
+ } + /> + +
); }; diff --git a/src/app/containers/TravelContainer.tsx b/src/app/containers/TravelContainer.tsx index 7fc2506a2..9bda1a623 100644 --- a/src/app/containers/TravelContainer.tsx +++ b/src/app/containers/TravelContainer.tsx @@ -1,6 +1,5 @@ /* eslint-disable max-len */ import React, { useState } from 'react'; -import MainSlider from '../components/TimeTravel/MainSlider'; import Dropdown from '../components/TimeTravel/Dropdown'; import { playForward, @@ -85,7 +84,6 @@ function TravelContainer(props: TravelContainerProps): JSX.Element { > {playing ? 'Pause' : 'Play'} -