From 1037d507c498b68ef3af38d8847eaffa1be34ed6 Mon Sep 17 00:00:00 2001 From: flav1o Date: Mon, 29 Dec 2025 14:43:33 +0000 Subject: [PATCH 1/6] chore: add network as a list --- .../renderer/pages/drawer-window/index.tsx | 87 ++++++++ .../timeline/components/better-network.tsx | 138 +++++++++++++ .../timeline/components/drawer.styles.ts | 186 ++++++++++++++++++ .../timeline/components/timeline.styles.ts | 72 +++++++ .../timeline/components/useDrawerResize.ts | 63 ++++++ .../timeline/components/virtualized-list.tsx | 60 ++++++ .../src/renderer/pages/timeline/constant.ts | 6 + .../src/renderer/pages/timeline/index.tsx | 154 ++++++--------- lib/reactotron-core-ui/package.json | 1 + 9 files changed, 677 insertions(+), 90 deletions(-) create mode 100644 apps/reactotron-app/src/renderer/pages/drawer-window/index.tsx create mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx create mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts create mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts create mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts create mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx create mode 100644 apps/reactotron-app/src/renderer/pages/timeline/constant.ts diff --git a/apps/reactotron-app/src/renderer/pages/drawer-window/index.tsx b/apps/reactotron-app/src/renderer/pages/drawer-window/index.tsx new file mode 100644 index 000000000..4ebd058b3 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/drawer-window/index.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from "react" +import { ipcRenderer } from "electron" +import styled from "styled-components" +import { Command } from "../timeline/components/better-network" + +const Container = styled.div` + display: flex; + flex-direction: column; + height: 100vh; + background-color: ${(props) => props.theme.background}; + color: ${(props) => props.theme.foreground}; + overflow-y: auto; +` + +const Header = styled.div` + padding: 16px; + border-bottom: 1px solid ${(props) => props.theme.border}; + font-size: 18px; + font-weight: bold; + -webkit-app-region: drag; +` + +const Content = styled.div` + padding: 16px; + flex: 1; + overflow-y: auto; +` + +const CommandItem = styled.div` + padding: 12px; + margin-bottom: 8px; + border: 1px solid ${(props) => props.theme.border}; + border-radius: 4px; + background-color: ${(props) => props.theme.chrome}; +` + +const CommandType = styled.div` + font-weight: bold; + margin-bottom: 8px; + color: ${(props) => props.theme.tag}; +` + +const CommandDetails = styled.div` + font-size: 12px; + color: ${(props) => props.theme.foregroundDark}; +` + +function DrawerWindow() { + const [commands, setCommands] = useState([]) + + useEffect(() => { + const handleUpdateCommands = (_event: any, newCommands: Command[]) => { + setCommands(newCommands) + } + + ipcRenderer.on("update-drawer-commands", handleUpdateCommands) + + return () => { + ipcRenderer.removeListener("update-drawer-commands", handleUpdateCommands) + } + }, []) + + return ( + +
Timeline Commands
+ + {commands.length === 0 ? ( +
No commands to display
+ ) : ( + commands.map((command) => ( + + {command.type} + +
Message ID: {command.messageId}
+
Connection ID: {command.connectionId}
+
Date: {new Date(command.date).toLocaleString()}
+ {command.clientId &&
Client ID: {command.clientId}
} +
+
+ )) + )} +
+
+ ) +} + +export default DrawerWindow diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx b/apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx new file mode 100644 index 000000000..172261607 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from "react" +import Styles from "./drawer.styles" +import { ContentView } from "reactotron-core-ui" +import { useDrawerResize } from "./useDrawerResize" +import { VirtualizedList } from "./virtualized-list" + +export interface Command { + clientId?: string + connectionId: number + date: Date + deltaTime: number + important: boolean + messageId: number + payload: any + type: string +} + +interface Props { + commands: Command[] +} + +const { + SDrawer, + RequestContainer, + RequestResponseContainer, + RequestItem, + RequestDataHeader, + RequestResponseContainerBody, + ResizeHandle, + RequestMethodStatus, + HttpMethod, + StatusCode, + StatusSeparator, +} = Styles + +const AvailableTabs = [ + "request headers", + "request params", + "request body", + "response headers", + "response", +] as const + +export const BetterNetwork = ({ commands }: Props) => { + const filteredCommands = commands.filter((command) => command.type === "api.response") + + const [currentCommandId, setCurrentCommandId] = useState() + const [currSelectedType, setCurrSelectedType] = useState("request headers") + + const { containerRef, leftPanelWidth, handleMouseDown } = useDrawerResize({ + initialLeftPanelWidth: 350, + minLeftPanelWidth: 200, + minRightPanelWidth: 200, + resizeHandleWidth: 10, + }) + + useEffect(() => { + if (!currentCommandId && !!commands.length) { + setCurrentCommandId(commands[0].messageId) + } + }, [commands, commands.length, currentCommandId]) + + const currentCommand = filteredCommands.find((command) => command.messageId === currentCommandId) + + const tabResolver = (tab: string) => { + switch (tab) { + case "response": + return currentCommand?.payload?.response?.body + case "response headers": + return currentCommand?.payload?.response?.headers + case "request headers": + return currentCommand?.payload?.request?.headers + case "request params": + return currentCommand?.payload?.request?.params + case "request body": + return currentCommand?.payload?.request?.data + } + } + + return ( + + + {containerRef.current && ( + item.messageId.toString()} + data={filteredCommands} + itemHeight={50} + height={containerRef.current?.offsetHeight} + renderItem={(command) => { + return ( + setCurrentCommandId(command?.messageId)} + className={currentCommandId === command?.messageId ? "active" : ""} + > +

{command.payload?.request?.url}

+
+ ) + }} + /> + )} +
+ + + + + + {currentCommand?.payload?.request?.method?.toUpperCase() || "N/A"} + + + + {currentCommand?.payload?.response?.status || "N/A"} + + + {AvailableTabs.map((tab) => { + const hasTab = tabResolver(tab) + if (!hasTab) return null + + return ( +
  • setCurrSelectedType(tab)} + className={currSelectedType === tab ? "active" : ""} + > + {tab} +
  • + ) + })} +
    + {currentCommandId && ( + + + + )} +
    +
    + ) +} diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts b/apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts new file mode 100644 index 000000000..d93a5e31e --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts @@ -0,0 +1,186 @@ +import styled from "styled-components"; + +const SDrawer = styled.div` + top: 60px; + right: 0; + height: calc(100vh - 60px); + background-color: ${(props) => props.theme.background}; + border-left: 1px solid ${(props) => props.theme.border}; + display: grid; + grid-template-columns: 350px 10px 1fr; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + overflow: hidden; +` + +const RequestContainer = styled.div` + pointer-events: auto; + overflow-y: auto; + background-color: ${(props) => props.theme.backgroundSubtleLight}; + border-right: 1px solid ${(props) => props.theme.border}; +`; + +const RequestItem = styled.div` + cursor: pointer; + pointer-events: auto; + height: 50px; + border-bottom: 1px solid ${(props) => props.theme.border}; + transition: all 0.2s ease; + color: ${(props) => props.theme.foreground}; + font-size: 13px; + position: relative; + display: flex; + align-items: center; + justify-content: flex-start; + padding-left: .5vw; + + &:hover { + background-color: ${(props) => props.theme.backgroundLighter}; + } + + &.active { + background-color: ${(props) => props.theme.backgroundLighter}; + border-left: 3px solid ${(props) => props.theme.tag}; + padding-left: 13px; + font-weight: 500; + } + + p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: small; + } +`; + +const RequestResponseContainer = styled.div` + width: 100%; + pointer-events: auto; + overflow-y: hidden; + overflow-x: auto; + display: flex; + flex-direction: column; + height: 100%; + position: relative; +`; + +const RequestDataHeader = styled.ul` + display: flex; + justify-content: flex-start; + align-items: center; + border-bottom: 1px solid ${(props) => props.theme.border}; + margin: 0; + padding: 0 16px; + pointer-events: auto; + background-color: ${(props) => props.theme.backgroundSubtleLight}; + gap: 8px; + + li { + list-style: none; + cursor: pointer; + pointer-events: auto; + padding: 12px 16px; + color: ${(props) => props.theme.foregroundDark}; + font-size: small; + font-weight: 500; + text-transform: capitalize; + transition: all 0.2s ease; + border-bottom: 2px solid transparent; + + &:hover { + color: ${(props) => props.theme.foreground}; + background-color: ${(props) => props.theme.backgroundLighter}; + } + + &.active { + color: ${(props) => props.theme.tag}; + border-bottom-color: ${(props) => props.theme.tag}; + } + } +`; + +const RequestMethodStatus = styled.div` + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-right: 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 600; +`; + +const HttpMethod = styled.span<{ method?: string }>` + color: ${(props) => { + const method = props.method?.toUpperCase(); + switch (method) { + case 'GET': return '#61affe'; + case 'POST': return '#49cc90'; + case 'PUT': return '#fca130'; + case 'PATCH': return '#50e3c2'; + case 'DELETE': return '#f93e3e'; + default: return props.theme.foreground; + } + }}; + font-weight: 700; + letter-spacing: 0.5px; +`; + +const StatusCode = styled.span<{ status?: number }>` + color: ${(props) => { + const status = props.status; + if (!status) return props.theme.foregroundDark; + if (status >= 200 && status < 300) return '#49cc90'; + if (status >= 300 && status < 400) return '#61affe'; + if (status >= 400 && status < 500) return '#fca130'; + if (status >= 500) return '#f93e3e'; + return props.theme.foregroundDark; + }}; + font-weight: 700; +`; + +const StatusSeparator = styled.span` + color: ${(props) => props.theme.foregroundDark}; + opacity: 0.5; +`; + +const RequestResponseContainerBody = styled.div` + overflow-y: auto; + padding: 20px; + flex: 1; + height: 0; +`; + +const ResizeHandle = styled.div` + width: 5px; + height: 100%; + background-color: ${(props) => props.theme.border}; + cursor: col-resize; + transition: background-color 0.2s ease; + position: absolute; + left: 0; + top: 0; + + &:hover { + background-color: ${(props) => props.theme.tag}; + } + + &:active { + background-color: ${(props) => props.theme.tag}; + } +`; + +const Styles = { + SDrawer, + RequestContainer, + RequestResponseContainer, + RequestItem, + RequestDataHeader, + RequestResponseContainerBody, + ResizeHandle, + RequestMethodStatus, + HttpMethod, + StatusCode, + StatusSeparator, +} + +export default Styles; \ No newline at end of file diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts b/apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts new file mode 100644 index 000000000..e960de8c3 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts @@ -0,0 +1,72 @@ +import styled from "styled-components" + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` +const TimelineContainer = styled.div` + height: 100%; + overflow-y: auto; + overflow-x: hidden; +` +const SearchContainer = styled.div` + display: flex; + align-items: center; + padding-bottom: 10px; + padding-top: 4px; + padding-right: 10px; +` +const SearchLabel = styled.p` + padding: 0 10px; + font-size: 14px; + color: ${(props) => props.theme.foregroundDark}; +` +const SearchInput = styled.input` + border-radius: 4px; + padding: 10px; + flex: 1; + background-color: ${(props) => props.theme.backgroundSubtleDark}; + border: none; + color: ${(props) => props.theme.foregroundDark}; + font-size: 14px; +` +const HelpMessage = styled.div` + margin: 0 40px; +` +const QuickStartButtonContainer = styled.div` + display: flex; + padding: 4px 8px; + margin: 30px 20px; + border-radius: 4px; + cursor: pointer; + background-color: ${(props) => props.theme.backgroundLighter}; + color: ${(props) => props.theme.foreground}; + align-items: center; + justify-content: center; + text-align: center; +` +const Divider = styled.div` + height: 1px; + background-color: ${(props) => props.theme.foregroundDark}; + margin: 40px 10px; +` + +export const ButtonContainer = styled.div` + padding: 10px; + cursor: pointer; +` + +const Styles = { + Container, + TimelineContainer, + Divider, + HelpMessage, + SearchInput, + QuickStartButtonContainer, + SearchLabel, + SearchContainer, + ButtonContainer, +} + +export default Styles; \ No newline at end of file diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts b/apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts new file mode 100644 index 000000000..12510fd68 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts @@ -0,0 +1,63 @@ +import { useEffect, useRef, useState } from "react" + +interface UsePanelResizeOptions { + initialLeftPanelWidth?: number + minLeftPanelWidth?: number + minRightPanelWidth?: number + resizeHandleWidth?: number +} + +export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { + const { + initialLeftPanelWidth = 350, + minLeftPanelWidth = 200, + minRightPanelWidth = 200, + resizeHandleWidth = 10, + } = options + + const [isResizing, setIsResizing] = useState(false) + const [leftPanelWidth, setLeftPanelWidth] = useState(initialLeftPanelWidth) + + const containerRef = useRef(null) + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + setIsResizing(true) + } + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing || !containerRef.current) return + + const containerRect = containerRef.current.getBoundingClientRect() + const newWidth = e.clientX - containerRect.left + + const maxWidth = containerRect.width - minRightPanelWidth - resizeHandleWidth + + if (newWidth >= minLeftPanelWidth && newWidth <= maxWidth) { + setLeftPanelWidth(newWidth) + } + } + + const handleMouseUp = () => { + setIsResizing(false) + } + + if (isResizing) { + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + }, [isResizing, minLeftPanelWidth, minRightPanelWidth, resizeHandleWidth]) + + return { + containerRef, + leftPanelWidth, + handleMouseDown, + } +} + diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx b/apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx new file mode 100644 index 000000000..805b96dcf --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx @@ -0,0 +1,60 @@ +import React, { useMemo, useRef, useState } from "react" + +interface VirtualizedListProps { + data: T[] + renderItem: (item: T) => React.ReactNode + itemHeight: number + height: number + getKey: (item: T) => string +} + +export const VirtualizedList = ({ + data, + renderItem, + itemHeight, + height, + getKey, +}: VirtualizedListProps) => { + const itemAmount = height / itemHeight + const totalHeight = data.length * itemHeight + + const [positions, setPositions] = useState({ head: 0, tail: itemAmount }) + const outerWrapperRef = useRef(null) + + const calculateNewIndexes = () => { + const el = outerWrapperRef.current + if (!el) return + + const scrollTop = el.scrollTop + const head = Math.max(0, Math.ceil(scrollTop / itemHeight)) + const tail = Math.min(data.length, head + itemAmount) + + if (head === positions.head && tail === positions.tail) return + setPositions({ head, tail }) + } + + const visibleItems = useMemo( + () => data.slice(positions.head, positions.tail), + [data, positions.head, positions.tail] + ) + + if (!data.length) return null + + return ( +
    +
    +
    + {visibleItems.map((item) => ( + {renderItem(item)} + ))} +
    +
    +
    + ) +} diff --git a/apps/reactotron-app/src/renderer/pages/timeline/constant.ts b/apps/reactotron-app/src/renderer/pages/timeline/constant.ts new file mode 100644 index 000000000..5cd507779 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/timeline/constant.ts @@ -0,0 +1,6 @@ +const STATIC_DATA = [] + +export default (() => { + const parsed = JSON.parse(JSON.stringify(STATIC_DATA)) + return parsed +})() diff --git a/apps/reactotron-app/src/renderer/pages/timeline/index.tsx b/apps/reactotron-app/src/renderer/pages/timeline/index.tsx index 18937d128..94fef075b 100644 --- a/apps/reactotron-app/src/renderer/pages/timeline/index.tsx +++ b/apps/reactotron-app/src/renderer/pages/timeline/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useMemo } from "react" +import React, { useCallback, useContext, useMemo, useState } from "react" import { clipboard, shell } from "electron" import fs from "fs" import os from "os" @@ -8,11 +8,11 @@ import { Header, filterCommands, TimelineFilterModal, - timelineCommandResolver, EmptyState, ReactotronContext, TimelineContext, RandomJoke, + timelineCommandResolver, } from "reactotron-core-ui" import { MdSearch, @@ -21,69 +21,28 @@ import { MdSwapVert, MdReorder, MdDownload, + MdConnectWithoutContact, } from "react-icons/md" import { FaTimes } from "react-icons/fa" -import styled from "styled-components" - -const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; -` -const TimelineContainer = styled.div` - height: 100%; - overflow-y: auto; - overflow-x: hidden; -` -const SearchContainer = styled.div` - display: flex; - align-items: center; - padding-bottom: 10px; - padding-top: 4px; - padding-right: 10px; -` -const SearchLabel = styled.p` - padding: 0 10px; - font-size: 14px; - color: ${(props) => props.theme.foregroundDark}; -` -const SearchInput = styled.input` - border-radius: 4px; - padding: 10px; - flex: 1; - background-color: ${(props) => props.theme.backgroundSubtleDark}; - border: none; - color: ${(props) => props.theme.foregroundDark}; - font-size: 14px; -` -const HelpMessage = styled.div` - margin: 0 40px; -` -const QuickStartButtonContainer = styled.div` - display: flex; - padding: 4px 8px; - margin: 30px 20px; - border-radius: 4px; - cursor: pointer; - background-color: ${(props) => props.theme.backgroundLighter}; - color: ${(props) => props.theme.foreground}; - align-items: center; - justify-content: center; - text-align: center; -` -const Divider = styled.div` - height: 1px; - background-color: ${(props) => props.theme.foregroundDark}; - margin: 40px 10px; -` - -export const ButtonContainer = styled.div` - padding: 10px; - cursor: pointer; -` +import Styles from "./components/timeline.styles" +import { Command, BetterNetwork } from "./components/better-network" + +const { + Container, + TimelineContainer, + SearchContainer, + SearchLabel, + SearchInput, + ButtonContainer, + HelpMessage, + QuickStartButtonContainer, + Divider, +} = Styles function Timeline() { const { sendCommand, clearCommands, commands, openDispatchModal } = useContext(ReactotronContext) + const [isNetworkModalOpen, setIsNetworkModalOpen] = useState(true) + const { isSearchOpen, toggleSearch, @@ -111,9 +70,9 @@ function Timeline() { filteredCommands = filteredCommands.reverse() } - const dispatchAction = (action: any) => { + const dispatchAction = useCallback((action: any) => { sendCommand("state.action.dispatch", { action }) - } + }, [sendCommand]) function openDocs() { shell.openExternal("https://docs.infinite.red/reactotron/quick-start/react-native/") @@ -132,12 +91,52 @@ function Timeline() { const { searchString, handleInputChange } = useDebouncedSearchInput(search, setSearch, 300) + const renderCommandList = useMemo(() => { + return filteredCommands.map((command) => { + const CommandComponent = timelineCommandResolver(command.type) + + if (CommandComponent) { + return ( + { + return new Promise((resolve, reject) => { + fs.readFile(path, "utf-8", (err, data) => { + if (err || !data) reject(new Error("Something failed")) + else resolve(data) + }) + }) + }} + sendCommand={sendCommand} + dispatchAction={dispatchAction} + openDispatchDialog={openDispatchModal} + /> + ) + } + + return null + }) + }, [dispatchAction, filteredCommands, openDispatchModal, sendCommand]) + + const shouldRenderNoContent = filteredCommands.length === 0 + const shouldRenderCustomNetwork = isNetworkModalOpen && filteredCommands.length > 0 + const shouldRenderCommandList = !isNetworkModalOpen && filteredCommands.length > 0 + return (
    { + setIsNetworkModalOpen((prev) => !prev) + }, + }, { tip: "Export Log", icon: MdDownload, @@ -194,7 +193,7 @@ function Timeline() { )}
    - {filteredCommands.length === 0 ? ( + {shouldRenderNoContent && ( Once your app connects and starts sending events, they will appear here. @@ -205,34 +204,9 @@ function Timeline() { - ) : ( - filteredCommands.map((command) => { - const CommandComponent = timelineCommandResolver(command.type) - - if (CommandComponent) { - return ( - { - return new Promise((resolve, reject) => { - fs.readFile(path, "utf-8", (err, data) => { - if (err || !data) reject(new Error("Something failed")) - else resolve(data) - }) - }) - }} - sendCommand={sendCommand} - dispatchAction={dispatchAction} - openDispatchDialog={openDispatchModal} - /> - ) - } - - return null - }) )} + {shouldRenderCustomNetwork && } + {shouldRenderCommandList && renderCommandList} Date: Mon, 29 Dec 2025 15:28:24 +0000 Subject: [PATCH 2/6] chore: revert chnages on timeline --- .../timeline/components/better-network.tsx | 138 ------------- .../timeline/components/drawer.styles.ts | 186 ------------------ .../timeline/components/timeline.styles.ts | 72 ------- .../timeline/components/useDrawerResize.ts | 63 ------ .../timeline/components/virtualized-list.tsx | 60 ------ .../src/renderer/pages/timeline/constant.ts | 6 - .../src/renderer/pages/timeline/index.tsx | 154 +++++++++------ 7 files changed, 90 insertions(+), 589 deletions(-) delete mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx delete mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts delete mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts delete mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts delete mode 100644 apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx delete mode 100644 apps/reactotron-app/src/renderer/pages/timeline/constant.ts diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx b/apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx deleted file mode 100644 index 172261607..000000000 --- a/apps/reactotron-app/src/renderer/pages/timeline/components/better-network.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useEffect, useState } from "react" -import Styles from "./drawer.styles" -import { ContentView } from "reactotron-core-ui" -import { useDrawerResize } from "./useDrawerResize" -import { VirtualizedList } from "./virtualized-list" - -export interface Command { - clientId?: string - connectionId: number - date: Date - deltaTime: number - important: boolean - messageId: number - payload: any - type: string -} - -interface Props { - commands: Command[] -} - -const { - SDrawer, - RequestContainer, - RequestResponseContainer, - RequestItem, - RequestDataHeader, - RequestResponseContainerBody, - ResizeHandle, - RequestMethodStatus, - HttpMethod, - StatusCode, - StatusSeparator, -} = Styles - -const AvailableTabs = [ - "request headers", - "request params", - "request body", - "response headers", - "response", -] as const - -export const BetterNetwork = ({ commands }: Props) => { - const filteredCommands = commands.filter((command) => command.type === "api.response") - - const [currentCommandId, setCurrentCommandId] = useState() - const [currSelectedType, setCurrSelectedType] = useState("request headers") - - const { containerRef, leftPanelWidth, handleMouseDown } = useDrawerResize({ - initialLeftPanelWidth: 350, - minLeftPanelWidth: 200, - minRightPanelWidth: 200, - resizeHandleWidth: 10, - }) - - useEffect(() => { - if (!currentCommandId && !!commands.length) { - setCurrentCommandId(commands[0].messageId) - } - }, [commands, commands.length, currentCommandId]) - - const currentCommand = filteredCommands.find((command) => command.messageId === currentCommandId) - - const tabResolver = (tab: string) => { - switch (tab) { - case "response": - return currentCommand?.payload?.response?.body - case "response headers": - return currentCommand?.payload?.response?.headers - case "request headers": - return currentCommand?.payload?.request?.headers - case "request params": - return currentCommand?.payload?.request?.params - case "request body": - return currentCommand?.payload?.request?.data - } - } - - return ( - - - {containerRef.current && ( - item.messageId.toString()} - data={filteredCommands} - itemHeight={50} - height={containerRef.current?.offsetHeight} - renderItem={(command) => { - return ( - setCurrentCommandId(command?.messageId)} - className={currentCommandId === command?.messageId ? "active" : ""} - > -

    {command.payload?.request?.url}

    -
    - ) - }} - /> - )} -
    - - - - - - {currentCommand?.payload?.request?.method?.toUpperCase() || "N/A"} - - - - {currentCommand?.payload?.response?.status || "N/A"} - - - {AvailableTabs.map((tab) => { - const hasTab = tabResolver(tab) - if (!hasTab) return null - - return ( -
  • setCurrSelectedType(tab)} - className={currSelectedType === tab ? "active" : ""} - > - {tab} -
  • - ) - })} -
    - {currentCommandId && ( - - - - )} -
    -
    - ) -} diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts b/apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts deleted file mode 100644 index d93a5e31e..000000000 --- a/apps/reactotron-app/src/renderer/pages/timeline/components/drawer.styles.ts +++ /dev/null @@ -1,186 +0,0 @@ -import styled from "styled-components"; - -const SDrawer = styled.div` - top: 60px; - right: 0; - height: calc(100vh - 60px); - background-color: ${(props) => props.theme.background}; - border-left: 1px solid ${(props) => props.theme.border}; - display: grid; - grid-template-columns: 350px 10px 1fr; - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); - overflow: hidden; -` - -const RequestContainer = styled.div` - pointer-events: auto; - overflow-y: auto; - background-color: ${(props) => props.theme.backgroundSubtleLight}; - border-right: 1px solid ${(props) => props.theme.border}; -`; - -const RequestItem = styled.div` - cursor: pointer; - pointer-events: auto; - height: 50px; - border-bottom: 1px solid ${(props) => props.theme.border}; - transition: all 0.2s ease; - color: ${(props) => props.theme.foreground}; - font-size: 13px; - position: relative; - display: flex; - align-items: center; - justify-content: flex-start; - padding-left: .5vw; - - &:hover { - background-color: ${(props) => props.theme.backgroundLighter}; - } - - &.active { - background-color: ${(props) => props.theme.backgroundLighter}; - border-left: 3px solid ${(props) => props.theme.tag}; - padding-left: 13px; - font-weight: 500; - } - - p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: small; - } -`; - -const RequestResponseContainer = styled.div` - width: 100%; - pointer-events: auto; - overflow-y: hidden; - overflow-x: auto; - display: flex; - flex-direction: column; - height: 100%; - position: relative; -`; - -const RequestDataHeader = styled.ul` - display: flex; - justify-content: flex-start; - align-items: center; - border-bottom: 1px solid ${(props) => props.theme.border}; - margin: 0; - padding: 0 16px; - pointer-events: auto; - background-color: ${(props) => props.theme.backgroundSubtleLight}; - gap: 8px; - - li { - list-style: none; - cursor: pointer; - pointer-events: auto; - padding: 12px 16px; - color: ${(props) => props.theme.foregroundDark}; - font-size: small; - font-weight: 500; - text-transform: capitalize; - transition: all 0.2s ease; - border-bottom: 2px solid transparent; - - &:hover { - color: ${(props) => props.theme.foreground}; - background-color: ${(props) => props.theme.backgroundLighter}; - } - - &.active { - color: ${(props) => props.theme.tag}; - border-bottom-color: ${(props) => props.theme.tag}; - } - } -`; - -const RequestMethodStatus = styled.div` - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - margin-right: 12px; - border-radius: 4px; - font-size: 13px; - font-weight: 600; -`; - -const HttpMethod = styled.span<{ method?: string }>` - color: ${(props) => { - const method = props.method?.toUpperCase(); - switch (method) { - case 'GET': return '#61affe'; - case 'POST': return '#49cc90'; - case 'PUT': return '#fca130'; - case 'PATCH': return '#50e3c2'; - case 'DELETE': return '#f93e3e'; - default: return props.theme.foreground; - } - }}; - font-weight: 700; - letter-spacing: 0.5px; -`; - -const StatusCode = styled.span<{ status?: number }>` - color: ${(props) => { - const status = props.status; - if (!status) return props.theme.foregroundDark; - if (status >= 200 && status < 300) return '#49cc90'; - if (status >= 300 && status < 400) return '#61affe'; - if (status >= 400 && status < 500) return '#fca130'; - if (status >= 500) return '#f93e3e'; - return props.theme.foregroundDark; - }}; - font-weight: 700; -`; - -const StatusSeparator = styled.span` - color: ${(props) => props.theme.foregroundDark}; - opacity: 0.5; -`; - -const RequestResponseContainerBody = styled.div` - overflow-y: auto; - padding: 20px; - flex: 1; - height: 0; -`; - -const ResizeHandle = styled.div` - width: 5px; - height: 100%; - background-color: ${(props) => props.theme.border}; - cursor: col-resize; - transition: background-color 0.2s ease; - position: absolute; - left: 0; - top: 0; - - &:hover { - background-color: ${(props) => props.theme.tag}; - } - - &:active { - background-color: ${(props) => props.theme.tag}; - } -`; - -const Styles = { - SDrawer, - RequestContainer, - RequestResponseContainer, - RequestItem, - RequestDataHeader, - RequestResponseContainerBody, - ResizeHandle, - RequestMethodStatus, - HttpMethod, - StatusCode, - StatusSeparator, -} - -export default Styles; \ No newline at end of file diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts b/apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts deleted file mode 100644 index e960de8c3..000000000 --- a/apps/reactotron-app/src/renderer/pages/timeline/components/timeline.styles.ts +++ /dev/null @@ -1,72 +0,0 @@ -import styled from "styled-components" - -const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; -` -const TimelineContainer = styled.div` - height: 100%; - overflow-y: auto; - overflow-x: hidden; -` -const SearchContainer = styled.div` - display: flex; - align-items: center; - padding-bottom: 10px; - padding-top: 4px; - padding-right: 10px; -` -const SearchLabel = styled.p` - padding: 0 10px; - font-size: 14px; - color: ${(props) => props.theme.foregroundDark}; -` -const SearchInput = styled.input` - border-radius: 4px; - padding: 10px; - flex: 1; - background-color: ${(props) => props.theme.backgroundSubtleDark}; - border: none; - color: ${(props) => props.theme.foregroundDark}; - font-size: 14px; -` -const HelpMessage = styled.div` - margin: 0 40px; -` -const QuickStartButtonContainer = styled.div` - display: flex; - padding: 4px 8px; - margin: 30px 20px; - border-radius: 4px; - cursor: pointer; - background-color: ${(props) => props.theme.backgroundLighter}; - color: ${(props) => props.theme.foreground}; - align-items: center; - justify-content: center; - text-align: center; -` -const Divider = styled.div` - height: 1px; - background-color: ${(props) => props.theme.foregroundDark}; - margin: 40px 10px; -` - -export const ButtonContainer = styled.div` - padding: 10px; - cursor: pointer; -` - -const Styles = { - Container, - TimelineContainer, - Divider, - HelpMessage, - SearchInput, - QuickStartButtonContainer, - SearchLabel, - SearchContainer, - ButtonContainer, -} - -export default Styles; \ No newline at end of file diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts b/apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts deleted file mode 100644 index 12510fd68..000000000 --- a/apps/reactotron-app/src/renderer/pages/timeline/components/useDrawerResize.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useRef, useState } from "react" - -interface UsePanelResizeOptions { - initialLeftPanelWidth?: number - minLeftPanelWidth?: number - minRightPanelWidth?: number - resizeHandleWidth?: number -} - -export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { - const { - initialLeftPanelWidth = 350, - minLeftPanelWidth = 200, - minRightPanelWidth = 200, - resizeHandleWidth = 10, - } = options - - const [isResizing, setIsResizing] = useState(false) - const [leftPanelWidth, setLeftPanelWidth] = useState(initialLeftPanelWidth) - - const containerRef = useRef(null) - - const handleMouseDown = (e: React.MouseEvent) => { - e.preventDefault() - setIsResizing(true) - } - - useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - if (!isResizing || !containerRef.current) return - - const containerRect = containerRef.current.getBoundingClientRect() - const newWidth = e.clientX - containerRect.left - - const maxWidth = containerRect.width - minRightPanelWidth - resizeHandleWidth - - if (newWidth >= minLeftPanelWidth && newWidth <= maxWidth) { - setLeftPanelWidth(newWidth) - } - } - - const handleMouseUp = () => { - setIsResizing(false) - } - - if (isResizing) { - document.addEventListener("mousemove", handleMouseMove) - document.addEventListener("mouseup", handleMouseUp) - } - - return () => { - document.removeEventListener("mousemove", handleMouseMove) - document.removeEventListener("mouseup", handleMouseUp) - } - }, [isResizing, minLeftPanelWidth, minRightPanelWidth, resizeHandleWidth]) - - return { - containerRef, - leftPanelWidth, - handleMouseDown, - } -} - diff --git a/apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx b/apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx deleted file mode 100644 index 805b96dcf..000000000 --- a/apps/reactotron-app/src/renderer/pages/timeline/components/virtualized-list.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useMemo, useRef, useState } from "react" - -interface VirtualizedListProps { - data: T[] - renderItem: (item: T) => React.ReactNode - itemHeight: number - height: number - getKey: (item: T) => string -} - -export const VirtualizedList = ({ - data, - renderItem, - itemHeight, - height, - getKey, -}: VirtualizedListProps) => { - const itemAmount = height / itemHeight - const totalHeight = data.length * itemHeight - - const [positions, setPositions] = useState({ head: 0, tail: itemAmount }) - const outerWrapperRef = useRef(null) - - const calculateNewIndexes = () => { - const el = outerWrapperRef.current - if (!el) return - - const scrollTop = el.scrollTop - const head = Math.max(0, Math.ceil(scrollTop / itemHeight)) - const tail = Math.min(data.length, head + itemAmount) - - if (head === positions.head && tail === positions.tail) return - setPositions({ head, tail }) - } - - const visibleItems = useMemo( - () => data.slice(positions.head, positions.tail), - [data, positions.head, positions.tail] - ) - - if (!data.length) return null - - return ( -
    -
    -
    - {visibleItems.map((item) => ( - {renderItem(item)} - ))} -
    -
    -
    - ) -} diff --git a/apps/reactotron-app/src/renderer/pages/timeline/constant.ts b/apps/reactotron-app/src/renderer/pages/timeline/constant.ts deleted file mode 100644 index 5cd507779..000000000 --- a/apps/reactotron-app/src/renderer/pages/timeline/constant.ts +++ /dev/null @@ -1,6 +0,0 @@ -const STATIC_DATA = [] - -export default (() => { - const parsed = JSON.parse(JSON.stringify(STATIC_DATA)) - return parsed -})() diff --git a/apps/reactotron-app/src/renderer/pages/timeline/index.tsx b/apps/reactotron-app/src/renderer/pages/timeline/index.tsx index 94fef075b..18937d128 100644 --- a/apps/reactotron-app/src/renderer/pages/timeline/index.tsx +++ b/apps/reactotron-app/src/renderer/pages/timeline/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useMemo, useState } from "react" +import React, { useCallback, useContext, useMemo } from "react" import { clipboard, shell } from "electron" import fs from "fs" import os from "os" @@ -8,11 +8,11 @@ import { Header, filterCommands, TimelineFilterModal, + timelineCommandResolver, EmptyState, ReactotronContext, TimelineContext, RandomJoke, - timelineCommandResolver, } from "reactotron-core-ui" import { MdSearch, @@ -21,28 +21,69 @@ import { MdSwapVert, MdReorder, MdDownload, - MdConnectWithoutContact, } from "react-icons/md" import { FaTimes } from "react-icons/fa" -import Styles from "./components/timeline.styles" -import { Command, BetterNetwork } from "./components/better-network" - -const { - Container, - TimelineContainer, - SearchContainer, - SearchLabel, - SearchInput, - ButtonContainer, - HelpMessage, - QuickStartButtonContainer, - Divider, -} = Styles +import styled from "styled-components" + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` +const TimelineContainer = styled.div` + height: 100%; + overflow-y: auto; + overflow-x: hidden; +` +const SearchContainer = styled.div` + display: flex; + align-items: center; + padding-bottom: 10px; + padding-top: 4px; + padding-right: 10px; +` +const SearchLabel = styled.p` + padding: 0 10px; + font-size: 14px; + color: ${(props) => props.theme.foregroundDark}; +` +const SearchInput = styled.input` + border-radius: 4px; + padding: 10px; + flex: 1; + background-color: ${(props) => props.theme.backgroundSubtleDark}; + border: none; + color: ${(props) => props.theme.foregroundDark}; + font-size: 14px; +` +const HelpMessage = styled.div` + margin: 0 40px; +` +const QuickStartButtonContainer = styled.div` + display: flex; + padding: 4px 8px; + margin: 30px 20px; + border-radius: 4px; + cursor: pointer; + background-color: ${(props) => props.theme.backgroundLighter}; + color: ${(props) => props.theme.foreground}; + align-items: center; + justify-content: center; + text-align: center; +` +const Divider = styled.div` + height: 1px; + background-color: ${(props) => props.theme.foregroundDark}; + margin: 40px 10px; +` + +export const ButtonContainer = styled.div` + padding: 10px; + cursor: pointer; +` function Timeline() { const { sendCommand, clearCommands, commands, openDispatchModal } = useContext(ReactotronContext) - const [isNetworkModalOpen, setIsNetworkModalOpen] = useState(true) - const { isSearchOpen, toggleSearch, @@ -70,9 +111,9 @@ function Timeline() { filteredCommands = filteredCommands.reverse() } - const dispatchAction = useCallback((action: any) => { + const dispatchAction = (action: any) => { sendCommand("state.action.dispatch", { action }) - }, [sendCommand]) + } function openDocs() { shell.openExternal("https://docs.infinite.red/reactotron/quick-start/react-native/") @@ -91,52 +132,12 @@ function Timeline() { const { searchString, handleInputChange } = useDebouncedSearchInput(search, setSearch, 300) - const renderCommandList = useMemo(() => { - return filteredCommands.map((command) => { - const CommandComponent = timelineCommandResolver(command.type) - - if (CommandComponent) { - return ( - { - return new Promise((resolve, reject) => { - fs.readFile(path, "utf-8", (err, data) => { - if (err || !data) reject(new Error("Something failed")) - else resolve(data) - }) - }) - }} - sendCommand={sendCommand} - dispatchAction={dispatchAction} - openDispatchDialog={openDispatchModal} - /> - ) - } - - return null - }) - }, [dispatchAction, filteredCommands, openDispatchModal, sendCommand]) - - const shouldRenderNoContent = filteredCommands.length === 0 - const shouldRenderCustomNetwork = isNetworkModalOpen && filteredCommands.length > 0 - const shouldRenderCommandList = !isNetworkModalOpen && filteredCommands.length > 0 - return (
    { - setIsNetworkModalOpen((prev) => !prev) - }, - }, { tip: "Export Log", icon: MdDownload, @@ -193,7 +194,7 @@ function Timeline() { )}
    - {shouldRenderNoContent && ( + {filteredCommands.length === 0 ? ( Once your app connects and starts sending events, they will appear here. @@ -204,9 +205,34 @@ function Timeline() { + ) : ( + filteredCommands.map((command) => { + const CommandComponent = timelineCommandResolver(command.type) + + if (CommandComponent) { + return ( + { + return new Promise((resolve, reject) => { + fs.readFile(path, "utf-8", (err, data) => { + if (err || !data) reject(new Error("Something failed")) + else resolve(data) + }) + }) + }} + sendCommand={sendCommand} + dispatchAction={dispatchAction} + openDispatchDialog={openDispatchModal} + /> + ) + } + + return null + }) )} - {shouldRenderCustomNetwork && } - {shouldRenderCommandList && renderCommandList} Date: Mon, 29 Dec 2025 15:30:40 +0000 Subject: [PATCH 3/6] chore: add network as a route --- apps/reactotron-app/src/renderer/App.tsx | 4 + .../renderer/components/SideBar/Sidebar.tsx | 3 + .../src/renderer/pages/network/index.tsx | 152 ++++++++++++++ .../renderer/pages/network/network.styles.ts | 196 ++++++++++++++++++ .../renderer/pages/network/useDrawerResize.ts | 63 ++++++ .../pages/network/virtualized-list.tsx | 61 ++++++ 6 files changed, 479 insertions(+) create mode 100644 apps/reactotron-app/src/renderer/pages/network/index.tsx create mode 100644 apps/reactotron-app/src/renderer/pages/network/network.styles.ts create mode 100644 apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts create mode 100644 apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx diff --git a/apps/reactotron-app/src/renderer/App.tsx b/apps/reactotron-app/src/renderer/App.tsx index bbab99b60..9697857bd 100644 --- a/apps/reactotron-app/src/renderer/App.tsx +++ b/apps/reactotron-app/src/renderer/App.tsx @@ -15,6 +15,7 @@ import Overlay from "./pages/reactNative/Overlay" import Storybook from "./pages/reactNative/Storybook" import CustomCommands from "./pages/customCommands" import Help from "./pages/help" +import { Network } from "./pages/network" const AppContainer = styled.div` position: absolute; @@ -67,6 +68,9 @@ function App() { {/* Custom Commands */} } /> + {/* Network */} + } /> + {/* Help */} } /> diff --git a/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx b/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx index 2ff34b503..22d722c03 100644 --- a/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx +++ b/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx @@ -7,6 +7,7 @@ import { MdWarning, MdOutlineMobileFriendly, MdMobiledataOff, + MdConnectWithoutContact, } from "react-icons/md" import { FaMagic } from "react-icons/fa" import styled from "styled-components" @@ -72,6 +73,8 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv /> + + { + const { commands } = useContext(ReactotronContext) + const filteredCommands = commands.filter((command) => command.type === "api.response") + + const [currentCommandId, setCurrentCommandId] = useState() + const [currSelectedType, setCurrSelectedType] = useState("request headers") + + const { containerRef, leftPanelWidth, handleMouseDown } = useDrawerResize({ + initialLeftPanelWidth: 350, + minLeftPanelWidth: 200, + minRightPanelWidth: 200, + resizeHandleWidth: 10, + }) + + useEffect(() => { + if (!currentCommandId && !!commands.length) { + setCurrentCommandId(commands[0].messageId) + } + }, [commands, commands.length, currentCommandId]) + + const currentCommand = filteredCommands.find((command) => command.messageId === currentCommandId) + + const tabResolver = (tab: string) => { + switch (tab) { + case "response": + return currentCommand?.payload?.response?.body + case "response headers": + return currentCommand?.payload?.response?.headers + case "request headers": + return currentCommand?.payload?.request?.headers + case "request params": + return currentCommand?.payload?.request?.params + case "request body": + return currentCommand?.payload?.request?.data + } + } + + if (filteredCommands.length === 0) { + return ( + +
    + +

    Network requests will appear here once your app starts making API calls.

    +
    + + ) + } + + return ( + +
    + + + {containerRef.current && ( + item.messageId.toString()} + data={filteredCommands} + itemHeight={50} + height={containerRef.current?.offsetHeight} + renderItem={(command) => { + return ( + setCurrentCommandId(command?.messageId)} + className={currentCommandId === command?.messageId ? "active" : ""} + > +

    {command.payload?.request?.url}

    +
    + ) + }} + /> + )} +
    + + + + + + {currentCommand?.payload?.request?.method?.toUpperCase() || "N/A"} + + + + {currentCommand?.payload?.response?.status || "N/A"} + + + {AvailableTabs.map((tab) => { + const hasTab = tabResolver(tab) + if (!hasTab) return null + + return ( +
  • setCurrSelectedType(tab)} + className={currSelectedType === tab ? "active" : ""} + > + {tab} +
  • + ) + })} +
    + {currentCommandId && ( + + + + )} +
    +
    + + ) +} + diff --git a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts new file mode 100644 index 000000000..ae7cfdf54 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts @@ -0,0 +1,196 @@ +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +` + +const SDrawer = styled.div` + top: 60px; + right: 0; + height: calc(100vh - 60px); + background-color: ${(props) => props.theme.background}; + border-left: 1px solid ${(props) => props.theme.border}; + display: grid; + grid-template-columns: 350px 10px 1fr; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + overflow: hidden; +` + +const RequestContainer = styled.div` + pointer-events: auto; + overflow-y: auto; + background-color: ${(props) => props.theme.backgroundSubtleLight}; + border-right: 1px solid ${(props) => props.theme.border}; +`; + +const RequestItem = styled.div` + cursor: pointer; + pointer-events: auto; + height: 50px; + border-bottom: 1px solid ${(props) => props.theme.border}; + transition: all 0.2s ease; + color: ${(props) => props.theme.foreground}; + font-size: 13px; + position: relative; + display: flex; + align-items: center; + justify-content: flex-start; + padding-left: .5vw; + + &:hover { + background-color: ${(props) => props.theme.backgroundLighter}; + } + + &.active { + background-color: ${(props) => props.theme.backgroundLighter}; + border-left: 3px solid ${(props) => props.theme.tag}; + padding-left: 13px; + font-weight: 500; + } + + p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: small; + } +`; + +const RequestResponseContainer = styled.div` + width: 100%; + pointer-events: auto; + overflow-y: hidden; + overflow-x: auto; + display: flex; + flex-direction: column; + height: 100%; + position: relative; +`; + +const RequestDataHeader = styled.ul` + display: flex; + justify-content: flex-start; + align-items: center; + border-bottom: 1px solid ${(props) => props.theme.border}; + margin: 0; + padding: 0 16px; + pointer-events: auto; + background-color: ${(props) => props.theme.backgroundSubtleLight}; + gap: 8px; + + li { + list-style: none; + cursor: pointer; + pointer-events: auto; + padding: 12px 16px; + color: ${(props) => props.theme.foregroundDark}; + font-size: small; + font-weight: 500; + text-transform: capitalize; + transition: all 0.2s ease; + border-bottom: 2px solid transparent; + + &:hover { + color: ${(props) => props.theme.foreground}; + background-color: ${(props) => props.theme.backgroundLighter}; + } + + &.active { + color: ${(props) => props.theme.tag}; + border-bottom-color: ${(props) => props.theme.tag}; + } + } +`; + +const RequestMethodStatus = styled.div` + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-right: 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 600; +`; + +const HttpMethod = styled.span<{ method?: string }>` + color: ${(props) => { + const method = props.method?.toUpperCase(); + switch (method) { + case 'GET': return '#61affe'; + case 'POST': return '#49cc90'; + case 'PUT': return '#fca130'; + case 'PATCH': return '#50e3c2'; + case 'DELETE': return '#f93e3e'; + default: return props.theme.foreground; + } + }}; + font-weight: 700; + letter-spacing: 0.5px; +`; + +const StatusCode = styled.span<{ status?: number }>` + color: ${(props) => { + const status = props.status; + if (!status) return props.theme.foregroundDark; + if (status >= 200 && status < 300) return '#49cc90'; + if (status >= 300 && status < 400) return '#61affe'; + if (status >= 400 && status < 500) return '#fca130'; + if (status >= 500) return '#f93e3e'; + return props.theme.foregroundDark; + }}; + font-weight: 700; +`; + +const StatusSeparator = styled.span` + color: ${(props) => props.theme.foregroundDark}; + opacity: 0.5; +`; + +const RequestResponseContainerBody = styled.div` + overflow-y: auto; + padding: 20px; + flex: 1; + height: 0; +`; + +const ResizeHandle = styled.div` + width: 5px; + height: 100%; + background-color: ${(props) => props.theme.border}; + cursor: col-resize; + transition: background-color 0.2s ease; + position: absolute; + left: 0; + top: 0; + + &:hover { + background-color: ${(props) => props.theme.tag}; + } + + &:active { + background-color: ${(props) => props.theme.tag}; + } +`; + +const Styles = { + Container, + SDrawer, + RequestContainer, + RequestResponseContainer, + RequestItem, + RequestDataHeader, + RequestResponseContainerBody, + ResizeHandle, + RequestMethodStatus, + HttpMethod, + StatusCode, + StatusSeparator, +} + +export default Styles; + diff --git a/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts b/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts new file mode 100644 index 000000000..12510fd68 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts @@ -0,0 +1,63 @@ +import { useEffect, useRef, useState } from "react" + +interface UsePanelResizeOptions { + initialLeftPanelWidth?: number + minLeftPanelWidth?: number + minRightPanelWidth?: number + resizeHandleWidth?: number +} + +export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { + const { + initialLeftPanelWidth = 350, + minLeftPanelWidth = 200, + minRightPanelWidth = 200, + resizeHandleWidth = 10, + } = options + + const [isResizing, setIsResizing] = useState(false) + const [leftPanelWidth, setLeftPanelWidth] = useState(initialLeftPanelWidth) + + const containerRef = useRef(null) + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + setIsResizing(true) + } + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing || !containerRef.current) return + + const containerRect = containerRef.current.getBoundingClientRect() + const newWidth = e.clientX - containerRect.left + + const maxWidth = containerRect.width - minRightPanelWidth - resizeHandleWidth + + if (newWidth >= minLeftPanelWidth && newWidth <= maxWidth) { + setLeftPanelWidth(newWidth) + } + } + + const handleMouseUp = () => { + setIsResizing(false) + } + + if (isResizing) { + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + }, [isResizing, minLeftPanelWidth, minRightPanelWidth, resizeHandleWidth]) + + return { + containerRef, + leftPanelWidth, + handleMouseDown, + } +} + diff --git a/apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx b/apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx new file mode 100644 index 000000000..dc4d812af --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx @@ -0,0 +1,61 @@ +import React, { useMemo, useRef, useState } from "react" + +interface VirtualizedListProps { + data: T[] + renderItem: (item: T) => React.ReactNode + itemHeight: number + height: number + getKey: (item: T) => string +} + +export const VirtualizedList = ({ + data, + renderItem, + itemHeight, + height, + getKey, +}: VirtualizedListProps) => { + const itemAmount = height / itemHeight + const totalHeight = data.length * itemHeight + + const [positions, setPositions] = useState({ head: 0, tail: itemAmount }) + const outerWrapperRef = useRef(null) + + const calculateNewIndexes = () => { + const el = outerWrapperRef.current + if (!el) return + + const scrollTop = el.scrollTop + const head = Math.max(0, Math.ceil(scrollTop / itemHeight)) + const tail = Math.min(data.length, head + itemAmount) + + if (head === positions.head && tail === positions.tail) return + setPositions({ head, tail }) + } + + const visibleItems = useMemo( + () => data.slice(positions.head, positions.tail), + [data, positions.head, positions.tail] + ) + + if (!data.length) return null + + return ( +
    +
    +
    + {visibleItems.map((item) => ( + {renderItem(item)} + ))} +
    +
    +
    + ) +} + From 2fb66be4d228ab6ddb04c60e7534a604331aff37 Mon Sep 17 00:00:00 2001 From: flav1o Date: Mon, 29 Dec 2025 20:32:22 +0000 Subject: [PATCH 4/6] chore: add load time to the pr --- .../components/NetworkRequestHeader.tsx | 70 ++++++++++++++ .../components/NetworkRequestsList.tsx | 42 +++++++++ .../src/renderer/pages/network/index.tsx | 91 ++++--------------- .../renderer/pages/network/network.styles.ts | 22 ++++- .../src/renderer/pages/network/types.ts | 11 +++ 5 files changed, 158 insertions(+), 78 deletions(-) create mode 100644 apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx create mode 100644 apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx create mode 100644 apps/reactotron-app/src/renderer/pages/network/types.ts diff --git a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx new file mode 100644 index 000000000..2060c5276 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx @@ -0,0 +1,70 @@ +import React from "react" +import { Command } from "../types" +import Styles from "../network.styles" + +interface NetworkRequestHeaderProps { + currentCommand?: Command + currSelectedType: string + onTabChange: (tab: string) => void + tabResolver: (tab: string) => any +} + +const AvailableTabs = [ + "request headers", + "request params", + "request body", + "response headers", + "response", +] as const + +const { + RequestDataHeader, + RequestMethodStatus, + HttpMethod, + StatusCode, + StatusSeparator, + RequestAvailableTabsContainer, + Duration, +} = Styles + +export const NetworkRequestHeader: React.FC = ({ + currentCommand, + currSelectedType, + onTabChange, + tabResolver, +}) => { + console.log(currentCommand) + return ( + + + + {currentCommand?.payload?.request?.method?.toUpperCase() || "N/A"} + + + + {currentCommand?.payload?.response?.status || "N/A"} + + + {currentCommand?.payload?.duration ? `${currentCommand.payload.duration.toFixed(3)}ms` : "N/A"} + + + {AvailableTabs.map((tab) => { + const hasTab = tabResolver(tab) + if (!hasTab) return null + + return ( +
  • onTabChange(tab)} + className={currSelectedType === tab ? "active" : ""} + > + {tab} +
  • + ) + })} +
    +
    + ) +} + +export default NetworkRequestHeader diff --git a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx new file mode 100644 index 000000000..cfcafe170 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx @@ -0,0 +1,42 @@ +import React from "react" +import { VirtualizedList } from "../virtualized-list" +import { Command } from "../types" +import Styles from "../network.styles" + +interface NetworkRequestsListProps { + filteredCommands: Command[] + containerHeight: number + currentCommandId?: number + onRequestClick: (messageId: number) => void +} + +const { RequestContainer, RequestItem } = Styles + +export const NetworkRequestsList: React.FC = ({ + filteredCommands, + containerHeight, + currentCommandId, + onRequestClick, +}) => { + return ( + + item.messageId.toString()} + data={filteredCommands} + itemHeight={50} + height={containerHeight} + renderItem={(command) => { + return ( + onRequestClick(command?.messageId)} + className={currentCommandId === command?.messageId ? "active" : ""} + > +

    {command.payload?.request?.url}

    +
    + ) + }} + /> +
    + ) +} diff --git a/apps/reactotron-app/src/renderer/pages/network/index.tsx b/apps/reactotron-app/src/renderer/pages/network/index.tsx index 18a9638c9..c64a9b476 100644 --- a/apps/reactotron-app/src/renderer/pages/network/index.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/index.tsx @@ -3,42 +3,17 @@ import Styles from "./network.styles" import { ContentView, ReactotronContext, Header, EmptyState } from "reactotron-core-ui" import { MdNetworkCheck } from "react-icons/md" import { useDrawerResize } from "./useDrawerResize" -import { VirtualizedList } from "./virtualized-list" - -export interface Command { - clientId?: string - connectionId: number - date: Date - deltaTime: number - important: boolean - messageId: number - payload: any - type: string -} +import { NetworkRequestsList } from "./components/NetworkRequestsList" +import { NetworkRequestHeader } from "./components/NetworkRequestHeader" const { Container, SDrawer, - RequestContainer, RequestResponseContainer, - RequestItem, - RequestDataHeader, RequestResponseContainerBody, ResizeHandle, - RequestMethodStatus, - HttpMethod, - StatusCode, - StatusSeparator, } = Styles -const AvailableTabs = [ - "request headers", - "request params", - "request body", - "response headers", - "response", -] as const - export const Network = () => { const { commands } = useContext(ReactotronContext) const filteredCommands = commands.filter((command) => command.type === "api.response") @@ -89,56 +64,24 @@ export const Network = () => { return ( -
    +
    - - {containerRef.current && ( - item.messageId.toString()} - data={filteredCommands} - itemHeight={50} - height={containerRef.current?.offsetHeight} - renderItem={(command) => { - return ( - setCurrentCommandId(command?.messageId)} - className={currentCommandId === command?.messageId ? "active" : ""} - > -

    {command.payload?.request?.url}

    -
    - ) - }} - /> - )} -
    + {containerRef.current && ( + + )} - - - - {currentCommand?.payload?.request?.method?.toUpperCase() || "N/A"} - - - - {currentCommand?.payload?.response?.status || "N/A"} - - - {AvailableTabs.map((tab) => { - const hasTab = tabResolver(tab) - if (!hasTab) return null - - return ( -
  • setCurrSelectedType(tab)} - className={currSelectedType === tab ? "active" : ""} - > - {tab} -
  • - ) - })} -
    + {currentCommandId && ( diff --git a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts index ae7cfdf54..67cfaf408 100644 --- a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts +++ b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts @@ -73,14 +73,14 @@ const RequestResponseContainer = styled.div` const RequestDataHeader = styled.ul` display: flex; - justify-content: flex-start; - align-items: center; + flex-direction: column; + justify-content: center; + align-items: flex-start; border-bottom: 1px solid ${(props) => props.theme.border}; margin: 0; - padding: 0 16px; + padding: 0; pointer-events: auto; background-color: ${(props) => props.theme.backgroundSubtleLight}; - gap: 8px; li { list-style: none; @@ -109,14 +109,20 @@ const RequestDataHeader = styled.ul` const RequestMethodStatus = styled.div` display: flex; align-items: center; + width: 100%; gap: 8px; padding: 8px 12px; + padding-left: 16px; margin-right: 12px; border-radius: 4px; font-size: 13px; font-weight: 600; `; +const RequestAvailableTabsContainer = styled.div` + display: flex; +` + const HttpMethod = styled.span<{ method?: string }>` color: ${(props) => { const method = props.method?.toUpperCase(); @@ -151,6 +157,12 @@ const StatusSeparator = styled.span` opacity: 0.5; `; +const Duration = styled.span` + color: ${(props) => props.theme.foregroundLight}; + font-weight: 600; + font-size: 12px; +`; + const RequestResponseContainerBody = styled.div` overflow-y: auto; padding: 20px; @@ -184,12 +196,14 @@ const Styles = { RequestResponseContainer, RequestItem, RequestDataHeader, + RequestAvailableTabsContainer, RequestResponseContainerBody, ResizeHandle, RequestMethodStatus, HttpMethod, StatusCode, StatusSeparator, + Duration, } export default Styles; diff --git a/apps/reactotron-app/src/renderer/pages/network/types.ts b/apps/reactotron-app/src/renderer/pages/network/types.ts new file mode 100644 index 000000000..b1b7c6451 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/network/types.ts @@ -0,0 +1,11 @@ +export interface Command { + clientId?: string + connectionId: number + date: Date + deltaTime: number + important: boolean + messageId: number + payload: any + type: string +} + From f6f6f2c569194830e5a5fc3cbcaad54b55fde9c2 Mon Sep 17 00:00:00 2001 From: flav1o Date: Mon, 29 Dec 2025 21:10:52 +0000 Subject: [PATCH 5/6] chore: add table with time method url and status --- .../components/NetworkRequestHeader.tsx | 27 +--- .../components/NetworkRequestsList.tsx | 43 +++++- .../VirtualizedList.tsx} | 12 +- .../src/renderer/pages/network/index.tsx | 41 ++---- .../renderer/pages/network/network.styles.ts | 130 ++++++++---------- 5 files changed, 126 insertions(+), 127 deletions(-) rename apps/reactotron-app/src/renderer/pages/network/{virtualized-list.tsx => components/VirtualizedList.tsx} (79%) diff --git a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx index 2060c5276..91d576108 100644 --- a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx @@ -6,7 +6,7 @@ interface NetworkRequestHeaderProps { currentCommand?: Command currSelectedType: string onTabChange: (tab: string) => void - tabResolver: (tab: string) => any + tabContent: Record } const AvailableTabs = [ @@ -17,39 +17,20 @@ const AvailableTabs = [ "response", ] as const -const { - RequestDataHeader, - RequestMethodStatus, - HttpMethod, - StatusCode, - StatusSeparator, - RequestAvailableTabsContainer, - Duration, -} = Styles +const { RequestDataHeader, RequestAvailableTabsContainer } = Styles export const NetworkRequestHeader: React.FC = ({ currentCommand, currSelectedType, onTabChange, - tabResolver, + tabContent, }) => { console.log(currentCommand) return ( - - - {currentCommand?.payload?.request?.method?.toUpperCase() || "N/A"} - - - - {currentCommand?.payload?.response?.status || "N/A"} - - - {currentCommand?.payload?.duration ? `${currentCommand.payload.duration.toFixed(3)}ms` : "N/A"} - {AvailableTabs.map((tab) => { - const hasTab = tabResolver(tab) + const hasTab = tabContent[tab] if (!hasTab) return null return ( diff --git a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx index cfcafe170..c4f3721f4 100644 --- a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx @@ -1,5 +1,5 @@ import React from "react" -import { VirtualizedList } from "../virtualized-list" +import { VirtualizedList } from "./VirtualizedList" import { Command } from "../types" import Styles from "../network.styles" @@ -8,31 +8,66 @@ interface NetworkRequestsListProps { containerHeight: number currentCommandId?: number onRequestClick: (messageId: number) => void + overscan?: number } -const { RequestContainer, RequestItem } = Styles +const { + RequestContainer, + RequestItem, + RequestTableHeader, + RequestTableHeaderCell, + RequestTableCell, +} = Styles export const NetworkRequestsList: React.FC = ({ filteredCommands, containerHeight, currentCommandId, onRequestClick, + overscan = 5, }) => { + const formatTime = (date: Date) => { + const d = new Date(date) + return d.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + } + return ( + + Time + Method + URL + Status + item.messageId.toString()} data={filteredCommands} itemHeight={50} - height={containerHeight} + height={containerHeight - 40} + overscan={overscan} renderItem={(command) => { + const shortenedUrl = command.payload?.request?.url?.split("://")[1] || command.payload?.request?.url || "N/A" + const method = command.payload?.request?.method?.toUpperCase() || "N/A" + const status = command.payload?.response?.status || "N/A" + const time = formatTime(command.date) + return ( onRequestClick(command?.messageId)} className={currentCommandId === command?.messageId ? "active" : ""} > -

    {command.payload?.request?.url}

    + {time} + {method} + + {shortenedUrl} + + {status}
    ) }} diff --git a/apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx b/apps/reactotron-app/src/renderer/pages/network/components/VirtualizedList.tsx similarity index 79% rename from apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx rename to apps/reactotron-app/src/renderer/pages/network/components/VirtualizedList.tsx index dc4d812af..307545d62 100644 --- a/apps/reactotron-app/src/renderer/pages/network/virtualized-list.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/components/VirtualizedList.tsx @@ -6,6 +6,7 @@ interface VirtualizedListProps { itemHeight: number height: number getKey: (item: T) => string + overscan?: number } export const VirtualizedList = ({ @@ -14,6 +15,8 @@ export const VirtualizedList = ({ itemHeight, height, getKey, + // extra items before and after visible area + overscan = 5, }: VirtualizedListProps) => { const itemAmount = height / itemHeight const totalHeight = data.length * itemHeight @@ -33,9 +36,12 @@ export const VirtualizedList = ({ setPositions({ head, tail }) } + const overscanHead = Math.max(0, positions.head - overscan) + const overscanTail = Math.min(data.length, positions.tail + overscan) + const visibleItems = useMemo( - () => data.slice(positions.head, positions.tail), - [data, positions.head, positions.tail] + () => data.slice(overscanHead, overscanTail), + [data, overscanHead, overscanTail] ) if (!data.length) return null @@ -47,7 +53,7 @@ export const VirtualizedList = ({ style={{ position: "absolute", width: "100%", - transform: `translateY(${positions.head * itemHeight}px)`, + transform: `translateY(${overscanHead * itemHeight}px)`, }} > {visibleItems.map((item) => ( diff --git a/apps/reactotron-app/src/renderer/pages/network/index.tsx b/apps/reactotron-app/src/renderer/pages/network/index.tsx index c64a9b476..892b05002 100644 --- a/apps/reactotron-app/src/renderer/pages/network/index.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/index.tsx @@ -6,13 +6,8 @@ import { useDrawerResize } from "./useDrawerResize" import { NetworkRequestsList } from "./components/NetworkRequestsList" import { NetworkRequestHeader } from "./components/NetworkRequestHeader" -const { - Container, - SDrawer, - RequestResponseContainer, - RequestResponseContainerBody, - ResizeHandle, -} = Styles +const { Container, SDrawer, RequestResponseContainer, RequestResponseContainerBody, ResizeHandle } = + Styles export const Network = () => { const { commands } = useContext(ReactotronContext) @@ -22,9 +17,9 @@ export const Network = () => { const [currSelectedType, setCurrSelectedType] = useState("request headers") const { containerRef, leftPanelWidth, handleMouseDown } = useDrawerResize({ - initialLeftPanelWidth: 350, - minLeftPanelWidth: 200, - minRightPanelWidth: 200, + initialLeftPanelWidth: 700, + minLeftPanelWidth: 600, + minRightPanelWidth: 700, resizeHandleWidth: 10, }) @@ -36,19 +31,12 @@ export const Network = () => { const currentCommand = filteredCommands.find((command) => command.messageId === currentCommandId) - const tabResolver = (tab: string) => { - switch (tab) { - case "response": - return currentCommand?.payload?.response?.body - case "response headers": - return currentCommand?.payload?.response?.headers - case "request headers": - return currentCommand?.payload?.request?.headers - case "request params": - return currentCommand?.payload?.request?.params - case "request body": - return currentCommand?.payload?.request?.data - } + const tabContent = { + response: currentCommand?.payload?.response?.body, + "response headers": currentCommand?.payload?.response?.headers, + "request headers": currentCommand?.payload?.request?.headers, + "request params": currentCommand?.payload?.request?.params, + "request body": currentCommand?.payload?.request?.data, } if (filteredCommands.length === 0) { @@ -64,7 +52,7 @@ export const Network = () => { return ( -
    +
    {containerRef.current && ( { currentCommand={currentCommand} currSelectedType={currSelectedType} onTabChange={setCurrSelectedType} - tabResolver={tabResolver} + tabContent={tabContent} /> {currentCommandId && ( - + )} @@ -92,4 +80,3 @@ export const Network = () => { ) } - diff --git a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts index 67cfaf408..2826f4a12 100644 --- a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts +++ b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts @@ -5,7 +5,6 @@ const Container = styled.div` flex-direction: column; width: 100%; height: 100%; - overflow: hidden; ` const SDrawer = styled.div` @@ -22,11 +21,31 @@ const SDrawer = styled.div` const RequestContainer = styled.div` pointer-events: auto; - overflow-y: auto; background-color: ${(props) => props.theme.backgroundSubtleLight}; border-right: 1px solid ${(props) => props.theme.border}; `; +const RequestTableHeader = styled.div` + display: flex; + align-items: center; + height: 40px; + background-color: ${(props) => props.theme.backgroundSubtleDark}; + border-bottom: 2px solid ${(props) => props.theme.border}; + padding: 0 12px; + font-weight: 600; + font-size: 12px; + color: ${(props) => props.theme.foregroundDark}; + text-transform: uppercase; + letter-spacing: 0.5px; +`; + +const RequestTableHeaderCell = styled.div` + padding: 0 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + const RequestItem = styled.div` cursor: pointer; pointer-events: auto; @@ -38,8 +57,7 @@ const RequestItem = styled.div` position: relative; display: flex; align-items: center; - justify-content: flex-start; - padding-left: .5vw; + padding: 0 12px; &:hover { background-color: ${(props) => props.theme.backgroundLighter}; @@ -48,16 +66,44 @@ const RequestItem = styled.div` &.active { background-color: ${(props) => props.theme.backgroundLighter}; border-left: 3px solid ${(props) => props.theme.tag}; - padding-left: 13px; + padding-left: 9px; font-weight: 500; } +`; + +const RequestTableCell = styled.div<{ method?: string; status?: number }>` + padding: 0 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 13px; - p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: small; - } + ${(props) => props.method && ` + color: ${(() => { + const method = props.method?.toUpperCase(); + switch (method) { + case 'GET': return '#61affe'; + case 'POST': return '#49cc90'; + case 'PUT': return '#fca130'; + case 'PATCH': return '#50e3c2'; + case 'DELETE': return '#f93e3e'; + default: return props.theme.foreground; + } + })()}; + font-weight: 600; + `} + + ${(props) => props.status && ` + color: ${(() => { + const status = props.status; + if (status >= 200 && status < 300) return '#49cc90'; + if (status >= 300 && status < 400) return '#61affe'; + if (status >= 400 && status < 500) return '#fca130'; + if (status >= 500) return '#f93e3e'; + return props.theme.foregroundDark; + })()}; + font-weight: 600; + `} `; const RequestResponseContainer = styled.div` @@ -106,65 +152,11 @@ const RequestDataHeader = styled.ul` } `; -const RequestMethodStatus = styled.div` - display: flex; - align-items: center; - width: 100%; - gap: 8px; - padding: 8px 12px; - padding-left: 16px; - margin-right: 12px; - border-radius: 4px; - font-size: 13px; - font-weight: 600; -`; - const RequestAvailableTabsContainer = styled.div` display: flex; ` -const HttpMethod = styled.span<{ method?: string }>` - color: ${(props) => { - const method = props.method?.toUpperCase(); - switch (method) { - case 'GET': return '#61affe'; - case 'POST': return '#49cc90'; - case 'PUT': return '#fca130'; - case 'PATCH': return '#50e3c2'; - case 'DELETE': return '#f93e3e'; - default: return props.theme.foreground; - } - }}; - font-weight: 700; - letter-spacing: 0.5px; -`; - -const StatusCode = styled.span<{ status?: number }>` - color: ${(props) => { - const status = props.status; - if (!status) return props.theme.foregroundDark; - if (status >= 200 && status < 300) return '#49cc90'; - if (status >= 300 && status < 400) return '#61affe'; - if (status >= 400 && status < 500) return '#fca130'; - if (status >= 500) return '#f93e3e'; - return props.theme.foregroundDark; - }}; - font-weight: 700; -`; - -const StatusSeparator = styled.span` - color: ${(props) => props.theme.foregroundDark}; - opacity: 0.5; -`; - -const Duration = styled.span` - color: ${(props) => props.theme.foregroundLight}; - font-weight: 600; - font-size: 12px; -`; - const RequestResponseContainerBody = styled.div` - overflow-y: auto; padding: 20px; flex: 1; height: 0; @@ -195,15 +187,13 @@ const Styles = { RequestContainer, RequestResponseContainer, RequestItem, + RequestTableHeader, + RequestTableHeaderCell, + RequestTableCell, RequestDataHeader, RequestAvailableTabsContainer, RequestResponseContainerBody, ResizeHandle, - RequestMethodStatus, - HttpMethod, - StatusCode, - StatusSeparator, - Duration, } export default Styles; From 7d76e78deeb285d8d4bc80718792c8135ffc47dc Mon Sep 17 00:00:00 2001 From: flav1o Date: Mon, 29 Dec 2025 22:33:36 +0000 Subject: [PATCH 6/6] chore: calculate panel positions on resize --- .../renderer/components/SideBar/Sidebar.tsx | 2 +- .../components/NetworkRequestHeader.tsx | 6 +- .../components/NetworkRequestsList.tsx | 67 ++-- .../src/renderer/pages/network/index.tsx | 68 +++- .../renderer/pages/network/network.styles.ts | 337 +++++++++--------- .../src/renderer/pages/network/types.ts | 11 - .../renderer/pages/network/useDrawerResize.ts | 11 +- 7 files changed, 285 insertions(+), 217 deletions(-) delete mode 100644 apps/reactotron-app/src/renderer/pages/network/types.ts diff --git a/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx b/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx index 22d722c03..429684f50 100644 --- a/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx +++ b/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx @@ -59,6 +59,7 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv + - diff --git a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx index 91d576108..4c243dc59 100644 --- a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx @@ -1,9 +1,7 @@ import React from "react" -import { Command } from "../types" import Styles from "../network.styles" interface NetworkRequestHeaderProps { - currentCommand?: Command currSelectedType: string onTabChange: (tab: string) => void tabContent: Record @@ -14,18 +12,16 @@ const AvailableTabs = [ "request params", "request body", "response headers", - "response", + "response body", ] as const const { RequestDataHeader, RequestAvailableTabsContainer } = Styles export const NetworkRequestHeader: React.FC = ({ - currentCommand, currSelectedType, onTabChange, tabContent, }) => { - console.log(currentCommand) return ( diff --git a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx index c4f3721f4..726e19581 100644 --- a/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx @@ -1,18 +1,37 @@ import React from "react" import { VirtualizedList } from "./VirtualizedList" -import { Command } from "../types" import Styles from "../network.styles" +import { Command, CommandTypeKey } from "reactotron-core-contract" interface NetworkRequestsListProps { - filteredCommands: Command[] + filteredCommands: Command[] containerHeight: number currentCommandId?: number onRequestClick: (messageId: number) => void overscan?: number } -const { - RequestContainer, +const formatTime = (date: Date) => { + const d = new Date(date) + return d.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) +} + +const formatSize = (bytes: number) => { + const kb = bytes / 1024 + if (kb < 1024) { + return `${kb.toFixed(2)} KB` + } + const mb = kb / 1024 + return `${mb.toFixed(2)} MB` +} + +const { + RequestContainer, RequestItem, RequestTableHeader, RequestTableHeaderCell, @@ -26,23 +45,15 @@ export const NetworkRequestsList: React.FC = ({ onRequestClick, overscan = 5, }) => { - const formatTime = (date: Date) => { - const d = new Date(date) - return d.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) - } return ( - Time - Method - URL - Status + Time + Method + URL + Status + Size item.messageId.toString()} @@ -51,7 +62,12 @@ export const NetworkRequestsList: React.FC = ({ height={containerHeight - 40} overscan={overscan} renderItem={(command) => { - const shortenedUrl = command.payload?.request?.url?.split("://")[1] || command.payload?.request?.url || "N/A" + const payloadBuffer = Buffer.from(JSON.stringify(command.payload), "utf8"); + const payloadSize = payloadBuffer.byteLength + + const shortenedUrl = + command.payload?.request?.url?.split("://")[1] || command.payload?.request?.url || "N/A" + const method = command.payload?.request?.method?.toUpperCase() || "N/A" const status = command.payload?.response?.status || "N/A" const time = formatTime(command.date) @@ -62,12 +78,19 @@ export const NetworkRequestsList: React.FC = ({ onClick={() => onRequestClick(command?.messageId)} className={currentCommandId === command?.messageId ? "active" : ""} > - {time} - {method} - + {time} + + {method} + + {shortenedUrl} - {status} + + {status} + + + {formatSize(payloadSize)} + ) }} diff --git a/apps/reactotron-app/src/renderer/pages/network/index.tsx b/apps/reactotron-app/src/renderer/pages/network/index.tsx index 892b05002..207475cf6 100644 --- a/apps/reactotron-app/src/renderer/pages/network/index.tsx +++ b/apps/reactotron-app/src/renderer/pages/network/index.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react" +import React, { useContext, useEffect, useRef, useState } from "react" import Styles from "./network.styles" import { ContentView, ReactotronContext, Header, EmptyState } from "reactotron-core-ui" import { MdNetworkCheck } from "react-icons/md" @@ -6,37 +6,73 @@ import { useDrawerResize } from "./useDrawerResize" import { NetworkRequestsList } from "./components/NetworkRequestsList" import { NetworkRequestHeader } from "./components/NetworkRequestHeader" -const { Container, SDrawer, RequestResponseContainer, RequestResponseContainerBody, ResizeHandle } = - Styles +const { + Container, + ResizableSectionWrapper, + RequestResponseContainer, + RequestResponseContainerBody, + ResizeHandle, +} = Styles export const Network = () => { + const hasUserResizedRef = useRef(false) + const resizeTimeoutRef = useRef() + const { commands } = useContext(ReactotronContext) - const filteredCommands = commands.filter((command) => command.type === "api.response") const [currentCommandId, setCurrentCommandId] = useState() const [currSelectedType, setCurrSelectedType] = useState("request headers") + const [screenWidth, setScreenWidth] = useState(window.innerWidth) + + const filteredCommands = commands.filter((command) => command.type === "api.response") + + useEffect(() => { + const handleResize = () => { + if (hasUserResizedRef.current) return + if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current) + + resizeTimeoutRef.current = setTimeout(() => { + setScreenWidth(window.innerWidth) + }, 150) + } + + window.addEventListener("resize", handleResize) + + return () => { + window.removeEventListener("resize", handleResize) + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current) + } + } + }, []) + + const initialLeftPanelWidth = Math.floor((screenWidth * 7) / 12) + const minRightPanelWidth = Math.floor(screenWidth / 12) const { containerRef, leftPanelWidth, handleMouseDown } = useDrawerResize({ - initialLeftPanelWidth: 700, - minLeftPanelWidth: 600, - minRightPanelWidth: 700, + initialLeftPanelWidth, + minLeftPanelWidth: 400, + minRightPanelWidth, resizeHandleWidth: 10, + onUserResize: () => { + hasUserResizedRef.current = true + }, }) useEffect(() => { - if (!currentCommandId && !!commands.length) { - setCurrentCommandId(commands[0].messageId) + if (!currentCommandId && filteredCommands.length > 0) { + setCurrentCommandId(filteredCommands[0].messageId) } - }, [commands, commands.length, currentCommandId]) + }, [currentCommandId, filteredCommands]) const currentCommand = filteredCommands.find((command) => command.messageId === currentCommandId) const tabContent = { - response: currentCommand?.payload?.response?.body, - "response headers": currentCommand?.payload?.response?.headers, "request headers": currentCommand?.payload?.request?.headers, "request params": currentCommand?.payload?.request?.params, "request body": currentCommand?.payload?.request?.data, + "response body": currentCommand?.payload?.response?.body, + "response headers": currentCommand?.payload?.response?.headers, } if (filteredCommands.length === 0) { @@ -53,7 +89,10 @@ export const Network = () => { return (
    - + {containerRef.current && ( { { )} - + ) } diff --git a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts index 2826f4a12..2f023f74d 100644 --- a/apps/reactotron-app/src/renderer/pages/network/network.styles.ts +++ b/apps/reactotron-app/src/renderer/pages/network/network.styles.ts @@ -1,200 +1,215 @@ -import styled from "styled-components"; +import styled from "styled-components" const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; - height: 100%; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; ` -const SDrawer = styled.div` - top: 60px; - right: 0; - height: calc(100vh - 60px); - background-color: ${(props) => props.theme.background}; - border-left: 1px solid ${(props) => props.theme.border}; - display: grid; - grid-template-columns: 350px 10px 1fr; - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); - overflow: hidden; +const ResizableSectionWrapper = styled.div` + top: 60px; + right: 0; + height: calc(100vh - 60px); + background-color: ${(props) => props.theme.background}; + border-left: 1px solid ${(props) => props.theme.border}; + display: grid; + grid-template-columns: 350px 10px 1fr; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + overflow: hidden; ` const RequestContainer = styled.div` - pointer-events: auto; - background-color: ${(props) => props.theme.backgroundSubtleLight}; - border-right: 1px solid ${(props) => props.theme.border}; -`; + pointer-events: auto; + background-color: ${(props) => props.theme.backgroundSubtleLight}; + border-right: 1px solid ${(props) => props.theme.border}; +` const RequestTableHeader = styled.div` - display: flex; - align-items: center; - height: 40px; - background-color: ${(props) => props.theme.backgroundSubtleDark}; - border-bottom: 2px solid ${(props) => props.theme.border}; - padding: 0 12px; - font-weight: 600; - font-size: 12px; - color: ${(props) => props.theme.foregroundDark}; - text-transform: uppercase; - letter-spacing: 0.5px; -`; + display: flex; + align-items: center; + height: 40px; + background-color: ${(props) => props.theme.backgroundSubtleDark}; + border-bottom: 2px solid ${(props) => props.theme.border}; + padding: 0 12px; + font-weight: 600; + font-size: 12px; + color: ${(props) => props.theme.foregroundDark}; + text-transform: uppercase; + letter-spacing: 0.5px; +` -const RequestTableHeaderCell = styled.div` - padding: 0 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; +const RequestTableHeaderCell = styled.div<{ width?: string }>` + padding: 0 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + ${(props) => props.width && `width: ${props.width};`} + ${(props) => props.width === "flex" && `flex: 1;`} +` const RequestItem = styled.div` - cursor: pointer; - pointer-events: auto; - height: 50px; - border-bottom: 1px solid ${(props) => props.theme.border}; - transition: all 0.2s ease; - color: ${(props) => props.theme.foreground}; - font-size: 13px; - position: relative; - display: flex; - align-items: center; - padding: 0 12px; - - &:hover { - background-color: ${(props) => props.theme.backgroundLighter}; - } - - &.active { - background-color: ${(props) => props.theme.backgroundLighter}; - border-left: 3px solid ${(props) => props.theme.tag}; - padding-left: 9px; - font-weight: 500; - } -`; - -const RequestTableCell = styled.div<{ method?: string; status?: number }>` - padding: 0 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 13px; + cursor: pointer; + pointer-events: auto; + height: 50px; + border-bottom: 1px solid ${(props) => props.theme.border}; + transition: all 0.2s ease; + color: ${(props) => props.theme.foreground}; + font-size: 13px; + position: relative; + display: flex; + align-items: center; + padding: 0 12px; + + &:hover { + background-color: ${(props) => props.theme.backgroundLighter}; + } + + &.active { + background-color: ${(props) => props.theme.backgroundLighter}; + border-left: 3px solid ${(props) => props.theme.tag}; + padding-left: 9px; + font-weight: 500; + } +` + +const RequestTableCell = styled.div<{ method?: string; status?: number; width?: string }>` + padding: 0 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 13px; + ${(props) => props.width && `width: ${props.width};`} + ${(props) => props.width === "flex" && `flex: 1;`} - ${(props) => props.method && ` + ${(props) => + props.method && + ` color: ${(() => { - const method = props.method?.toUpperCase(); - switch (method) { - case 'GET': return '#61affe'; - case 'POST': return '#49cc90'; - case 'PUT': return '#fca130'; - case 'PATCH': return '#50e3c2'; - case 'DELETE': return '#f93e3e'; - default: return props.theme.foreground; - } + const method = props.method?.toUpperCase() + switch (method) { + case "GET": + return "#61affe" + case "POST": + return "#49cc90" + case "PUT": + return "#fca130" + case "PATCH": + return "#50e3c2" + case "DELETE": + return "#f93e3e" + default: + return props.theme.foreground + } })()}; font-weight: 600; `} - ${(props) => props.status && ` + ${(props) => + props.status && + ` color: ${(() => { - const status = props.status; - if (status >= 200 && status < 300) return '#49cc90'; - if (status >= 300 && status < 400) return '#61affe'; - if (status >= 400 && status < 500) return '#fca130'; - if (status >= 500) return '#f93e3e'; - return props.theme.foregroundDark; + const status = props.status + if (status >= 200 && status < 300) return "#49cc90" + if (status >= 300 && status < 400) return "#61affe" + if (status >= 400 && status < 500) return "#fca130" + if (status >= 500) return "#f93e3e" + return props.theme.foregroundDark })()}; font-weight: 600; `} -`; +` const RequestResponseContainer = styled.div` - width: 100%; - pointer-events: auto; - overflow-y: hidden; - overflow-x: auto; - display: flex; - flex-direction: column; - height: 100%; - position: relative; -`; + width: 100%; + pointer-events: auto; + overflow-y: auto; + overflow-x: auto; + display: flex; + flex-direction: column; + height: 100%; + position: relative; +` const RequestDataHeader = styled.ul` - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - border-bottom: 1px solid ${(props) => props.theme.border}; - margin: 0; - padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + border-bottom: 1px solid ${(props) => props.theme.border}; + margin: 0; + padding: 0; + pointer-events: auto; + background-color: ${(props) => props.theme.backgroundSubtleLight}; + + li { + list-style: none; + cursor: pointer; pointer-events: auto; - background-color: ${(props) => props.theme.backgroundSubtleLight}; - - li { - list-style: none; - cursor: pointer; - pointer-events: auto; - padding: 12px 16px; - color: ${(props) => props.theme.foregroundDark}; - font-size: small; - font-weight: 500; - text-transform: capitalize; - transition: all 0.2s ease; - border-bottom: 2px solid transparent; - - &:hover { - color: ${(props) => props.theme.foreground}; - background-color: ${(props) => props.theme.backgroundLighter}; - } - - &.active { - color: ${(props) => props.theme.tag}; - border-bottom-color: ${(props) => props.theme.tag}; - } + padding: 12px 16px; + color: ${(props) => props.theme.foregroundDark}; + font-size: small; + font-weight: 500; + text-transform: capitalize; + transition: all 0.2s ease; + border-bottom: 2px solid transparent; + display: flex; + align-items: center; + + &:hover { + color: ${(props) => props.theme.foreground}; + background-color: ${(props) => props.theme.backgroundLighter}; + } + + &.active { + color: ${(props) => props.theme.tag}; + border-bottom-color: ${(props) => props.theme.tag}; } -`; + } +` const RequestAvailableTabsContainer = styled.div` - display: flex; + display: flex; ` const RequestResponseContainerBody = styled.div` - padding: 20px; - flex: 1; - height: 0; -`; + padding: 20px; + flex: 1; + height: 0; +` const ResizeHandle = styled.div` - width: 5px; - height: 100%; - background-color: ${(props) => props.theme.border}; - cursor: col-resize; - transition: background-color 0.2s ease; - position: absolute; - left: 0; - top: 0; - - &:hover { - background-color: ${(props) => props.theme.tag}; - } - - &:active { - background-color: ${(props) => props.theme.tag}; - } -`; + width: 5px; + height: 100%; + background-color: ${(props) => props.theme.border}; + cursor: col-resize; + transition: background-color 0.2s ease; + position: absolute; + left: 0; + top: 0; + + &:hover { + background-color: ${(props) => props.theme.tag}; + } + + &:active { + background-color: ${(props) => props.theme.tag}; + } +` const Styles = { - Container, - SDrawer, - RequestContainer, - RequestResponseContainer, - RequestItem, - RequestTableHeader, - RequestTableHeaderCell, - RequestTableCell, - RequestDataHeader, - RequestAvailableTabsContainer, - RequestResponseContainerBody, - ResizeHandle, + Container, + ResizableSectionWrapper, + RequestContainer, + RequestResponseContainer, + RequestItem, + RequestTableHeader, + RequestTableHeaderCell, + RequestTableCell, + RequestDataHeader, + RequestAvailableTabsContainer, + RequestResponseContainerBody, + ResizeHandle, } -export default Styles; - +export default Styles diff --git a/apps/reactotron-app/src/renderer/pages/network/types.ts b/apps/reactotron-app/src/renderer/pages/network/types.ts deleted file mode 100644 index b1b7c6451..000000000 --- a/apps/reactotron-app/src/renderer/pages/network/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface Command { - clientId?: string - connectionId: number - date: Date - deltaTime: number - important: boolean - messageId: number - payload: any - type: string -} - diff --git a/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts b/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts index 12510fd68..96c72d6f3 100644 --- a/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts +++ b/apps/reactotron-app/src/renderer/pages/network/useDrawerResize.ts @@ -5,6 +5,7 @@ interface UsePanelResizeOptions { minLeftPanelWidth?: number minRightPanelWidth?: number resizeHandleWidth?: number + onUserResize?: () => void } export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { @@ -13,6 +14,7 @@ export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { minLeftPanelWidth = 200, minRightPanelWidth = 200, resizeHandleWidth = 10, + onUserResize, } = options const [isResizing, setIsResizing] = useState(false) @@ -25,6 +27,10 @@ export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { setIsResizing(true) } + useEffect(() => { + setLeftPanelWidth(initialLeftPanelWidth) + }, [initialLeftPanelWidth]) + useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isResizing || !containerRef.current) return @@ -36,6 +42,8 @@ export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { if (newWidth >= minLeftPanelWidth && newWidth <= maxWidth) { setLeftPanelWidth(newWidth) + + if (onUserResize) onUserResize() } } @@ -52,7 +60,7 @@ export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { document.removeEventListener("mousemove", handleMouseMove) document.removeEventListener("mouseup", handleMouseUp) } - }, [isResizing, minLeftPanelWidth, minRightPanelWidth, resizeHandleWidth]) + }, [isResizing, minLeftPanelWidth, minRightPanelWidth, resizeHandleWidth, onUserResize]) return { containerRef, @@ -60,4 +68,3 @@ export const useDrawerResize = (options: UsePanelResizeOptions = {}) => { handleMouseDown, } } -