Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/reactotron-app/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,6 +68,9 @@ function App() {
{/* Custom Commands */}
<Route path="/customCommands" element={<CustomCommands />} />

{/* Network */}
<Route path="/network" element={<Network />} />

{/* Help */}
<Route path="/help" element={<Help />} />
</Routes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MdWarning,
MdOutlineMobileFriendly,
MdMobiledataOff,
MdConnectWithoutContact,
} from "react-icons/md"
import { FaMagic } from "react-icons/fa"
import styled from "styled-components"
Expand Down Expand Up @@ -58,6 +59,7 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv
<SideBarContainer $isOpen={isOpen}>
<SideBarButton image={reactotronLogo} path="/" text="Home" hideTopBar />
<SideBarButton icon={MdReorder} path="/timeline" text="Timeline" />
<SideBarButton icon={MdConnectWithoutContact} path="/network" text="Network" />
<SideBarButton
icon={MdAssignment}
path="/state/subscriptions"
Expand All @@ -72,6 +74,7 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv
/>
<SideBarButton icon={FaMagic} path="/customCommands" text="Custom Commands" iconSize={25} />


<Spacer />

<SideBarButton
Expand Down
87 changes: 87 additions & 0 deletions apps/reactotron-app/src/renderer/pages/drawer-window/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Command[]>([])

useEffect(() => {
const handleUpdateCommands = (_event: any, newCommands: Command[]) => {
setCommands(newCommands)
}

ipcRenderer.on("update-drawer-commands", handleUpdateCommands)

return () => {
ipcRenderer.removeListener("update-drawer-commands", handleUpdateCommands)
}
}, [])

return (
<Container>
<Header>Timeline Commands</Header>
<Content>
{commands.length === 0 ? (
<div>No commands to display</div>
) : (
commands.map((command) => (
<CommandItem key={command.messageId}>
<CommandType>{command.type}</CommandType>
<CommandDetails>
<div>Message ID: {command.messageId}</div>
<div>Connection ID: {command.connectionId}</div>
<div>Date: {new Date(command.date).toLocaleString()}</div>
{command.clientId && <div>Client ID: {command.clientId}</div>}
</CommandDetails>
</CommandItem>
))
)}
</Content>
</Container>
)
}

export default DrawerWindow
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react"
import Styles from "../network.styles"

interface NetworkRequestHeaderProps {
currSelectedType: string
onTabChange: (tab: string) => void
tabContent: Record<string, object>
}

const AvailableTabs = [
"request headers",
"request params",
"request body",
"response headers",
"response body",
] as const

const { RequestDataHeader, RequestAvailableTabsContainer } = Styles

export const NetworkRequestHeader: React.FC<NetworkRequestHeaderProps> = ({
currSelectedType,
onTabChange,
tabContent,
}) => {
return (
<RequestDataHeader>
<RequestAvailableTabsContainer>
{AvailableTabs.map((tab) => {
const hasTab = tabContent[tab]
if (!hasTab) return null

return (
<li
key={tab}
onClick={() => onTabChange(tab)}
className={currSelectedType === tab ? "active" : ""}
>
{tab}
</li>
)
})}
</RequestAvailableTabsContainer>
</RequestDataHeader>
)
}

export default NetworkRequestHeader
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from "react"
import { VirtualizedList } from "./VirtualizedList"
import Styles from "../network.styles"
import { Command, CommandTypeKey } from "reactotron-core-contract"

interface NetworkRequestsListProps {
filteredCommands: Command<CommandTypeKey, any>[]
containerHeight: number
currentCommandId?: number
onRequestClick: (messageId: number) => void
overscan?: number
}

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,
RequestTableCell,
} = Styles

export const NetworkRequestsList: React.FC<NetworkRequestsListProps> = ({
filteredCommands,
containerHeight,
currentCommandId,
onRequestClick,
overscan = 5,
}) => {

return (
<RequestContainer>
<RequestTableHeader>
<RequestTableHeaderCell width="80px">Time</RequestTableHeaderCell>
<RequestTableHeaderCell width="70px">Method</RequestTableHeaderCell>
<RequestTableHeaderCell width="flex">URL</RequestTableHeaderCell>
<RequestTableHeaderCell width="70px">Status</RequestTableHeaderCell>
<RequestTableHeaderCell width="100px">Size</RequestTableHeaderCell>
</RequestTableHeader>
<VirtualizedList
getKey={(item) => item.messageId.toString()}
data={filteredCommands}
itemHeight={50}
height={containerHeight - 40}
overscan={overscan}
renderItem={(command) => {
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)

return (
<RequestItem
key={command?.messageId}
onClick={() => onRequestClick(command?.messageId)}
className={currentCommandId === command?.messageId ? "active" : ""}
>
<RequestTableCell width="80px">{time}</RequestTableCell>
<RequestTableCell width="70px" method={method}>
{method}
</RequestTableCell>
<RequestTableCell width="flex" title={command.payload?.request?.url}>
{shortenedUrl}
</RequestTableCell>
<RequestTableCell width="70px" status={status}>
{status}
</RequestTableCell>
<RequestTableCell width="100px" title={`${payloadSize} bytes`}>
{formatSize(payloadSize)}
</RequestTableCell>
</RequestItem>
)
}}
/>
</RequestContainer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useMemo, useRef, useState } from "react"

interface VirtualizedListProps<T> {
data: T[]
renderItem: (item: T) => React.ReactNode
itemHeight: number
height: number
getKey: (item: T) => string
overscan?: number
}

export const VirtualizedList = <T,>({
data,
renderItem,
itemHeight,
height,
getKey,
// extra items before and after visible area
overscan = 5,
}: VirtualizedListProps<T>) => {
const itemAmount = height / itemHeight
const totalHeight = data.length * itemHeight

const [positions, setPositions] = useState({ head: 0, tail: itemAmount })
const outerWrapperRef = useRef<HTMLDivElement>(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 overscanHead = Math.max(0, positions.head - overscan)
const overscanTail = Math.min(data.length, positions.tail + overscan)

const visibleItems = useMemo(
() => data.slice(overscanHead, overscanTail),
[data, overscanHead, overscanTail]
)

if (!data.length) return null

return (
<div ref={outerWrapperRef} style={{ height, overflowY: "auto" }} onScroll={calculateNewIndexes}>
<div style={{ height: totalHeight, position: "relative" }}>
<div
style={{
position: "absolute",
width: "100%",
transform: `translateY(${overscanHead * itemHeight}px)`,
}}
>
{visibleItems.map((item) => (
<React.Fragment key={getKey(item)}>{renderItem(item)}</React.Fragment>
))}
</div>
</div>
</div>
)
}

Loading