Skip to content

Commit c1d4a6b

Browse files
enhance network tab in timeline
1 parent 597ac4f commit c1d4a6b

File tree

7 files changed

+666
-60
lines changed

7 files changed

+666
-60
lines changed

apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv
5959
<SideBarContainer $isOpen={isOpen}>
6060
<SideBarButton image={reactotronLogo} path="/" text="Home" hideTopBar />
6161
<SideBarButton icon={MdReorder} path="/timeline" text="Timeline" />
62-
<SideBarButton icon={MdConnectWithoutContact} path="/network" text="Network" />
6362
<SideBarButton
6463
icon={MdAssignment}
6564
path="/state/subscriptions"

apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestHeader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react"
2+
import styled from "styled-components"
23
import Styles from "../network.styles"
34

45
interface NetworkRequestHeaderProps {

apps/reactotron-app/src/renderer/pages/network/components/NetworkRequestsList.tsx

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import React from "react"
2+
import styled from "styled-components"
23
import { VirtualizedList } from "./VirtualizedList"
34
import Styles from "../network.styles"
45
import { Command, CommandTypeKey } from "reactotron-core-contract"
6+
import { MdCallReceived, MdReceipt, MdContentCopy } from "react-icons/md"
7+
import { clipboard } from "electron"
8+
import { apiToMarkdown, apiRequestToCurl } from "../utils/api-helpers"
59

610
interface NetworkRequestsListProps {
711
filteredCommands: Command<CommandTypeKey, any>[]
@@ -11,6 +15,52 @@ interface NetworkRequestsListProps {
1115
overscan?: number
1216
}
1317

18+
const ActionButtons = styled.div`
19+
display: flex;
20+
gap: 4px;
21+
position: absolute;
22+
left: 170px;
23+
top: 50%;
24+
transform: translateY(-50%);
25+
opacity: 0;
26+
transition: opacity 0.2s;
27+
background: ${(props) => props.theme.backgroundSubtleLight};
28+
padding: 4px;
29+
border-radius: 4px;
30+
z-index: 10;
31+
`
32+
33+
const IconButton = styled.button`
34+
display: flex;
35+
align-items: center;
36+
justify-content: center;
37+
width: 24px;
38+
height: 24px;
39+
border: none;
40+
background: ${(props) => props.theme.backgroundLighter};
41+
color: ${(props) => props.theme.foregroundDark};
42+
border-radius: 4px;
43+
cursor: pointer;
44+
padding: 0;
45+
transition: all 0.2s;
46+
47+
&:hover {
48+
background: ${(props) => props.theme.tag};
49+
color: ${(props) => props.theme.background};
50+
transform: scale(1.1);
51+
}
52+
53+
svg {
54+
font-size: 14px;
55+
}
56+
`
57+
58+
const RequestItemWrapper = styled.div`
59+
&:hover ${ActionButtons} {
60+
opacity: 1;
61+
}
62+
`
63+
1464
const formatTime = (date: Date) => {
1565
const d = new Date(date)
1666
return d.toLocaleTimeString("en-US", {
@@ -71,26 +121,56 @@ export const NetworkRequestsList: React.FC<NetworkRequestsListProps> = ({
71121
const status = command.payload?.response?.status || "N/A"
72122
const time = formatTime(command.date)
73123

124+
const handleCopyResponse = (e: React.MouseEvent) => {
125+
e.stopPropagation()
126+
const text = JSON.stringify(command.payload.response.body, null, 2)
127+
clipboard.writeText(text)
128+
}
129+
130+
const handleCopyMarkdown = (e: React.MouseEvent) => {
131+
e.stopPropagation()
132+
const text = apiToMarkdown(command.payload)
133+
clipboard.writeText(text)
134+
}
135+
136+
const handleCopyCurl = (e: React.MouseEvent) => {
137+
e.stopPropagation()
138+
const text = apiRequestToCurl(command.payload)
139+
clipboard.writeText(text)
140+
}
141+
74142
return (
75-
<RequestItem
76-
key={command?.messageId}
77-
onClick={() => onRequestClick(command?.messageId)}
78-
className={currentCommandId === command?.messageId ? "active" : ""}
79-
>
80-
<RequestTableCell width="80px">{time}</RequestTableCell>
81-
<RequestTableCell width="70px" method={method}>
82-
{method}
83-
</RequestTableCell>
84-
<RequestTableCell width="flex" title={command.payload?.request?.url}>
85-
{shortenedUrl}
86-
</RequestTableCell>
87-
<RequestTableCell width="70px" status={status}>
88-
{status}
89-
</RequestTableCell>
90-
<RequestTableCell width="100px" title={`${payloadSize} bytes`}>
91-
{formatSize(payloadSize)}
92-
</RequestTableCell>
93-
</RequestItem>
143+
<RequestItemWrapper key={command?.messageId}>
144+
<RequestItem
145+
onClick={() => onRequestClick(command?.messageId)}
146+
className={currentCommandId === command?.messageId ? "active" : ""}
147+
>
148+
<RequestTableCell width="80px">{time}</RequestTableCell>
149+
<RequestTableCell width="70px" method={method}>
150+
{method}
151+
</RequestTableCell>
152+
<RequestTableCell width="flex" title={command.payload?.request?.url}>
153+
{shortenedUrl}
154+
</RequestTableCell>
155+
<RequestTableCell width="70px" status={status}>
156+
{status}
157+
</RequestTableCell>
158+
<RequestTableCell width="100px" title={`${payloadSize} bytes`}>
159+
{formatSize(payloadSize)}
160+
</RequestTableCell>
161+
<ActionButtons>
162+
<IconButton onClick={handleCopyResponse} title="Copy JSON response to clipboard">
163+
<MdCallReceived />
164+
</IconButton>
165+
<IconButton onClick={handleCopyMarkdown} title="Copy as markdown to clipboard">
166+
<MdReceipt />
167+
</IconButton>
168+
<IconButton onClick={handleCopyCurl} title="Copy JSON request as cURL">
169+
<MdContentCopy />
170+
</IconButton>
171+
</ActionButtons>
172+
</RequestItem>
173+
</RequestItemWrapper>
94174
)
95175
}}
96176
/>

apps/reactotron-app/src/renderer/pages/network/index.tsx

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useContext, useEffect, useRef, useState } from "react"
2+
import styled from "styled-components"
23
import Styles from "./network.styles"
34
import { ContentView, ReactotronContext, Header, EmptyState } from "reactotron-core-ui"
45
import { MdNetworkCheck } from "react-icons/md"
@@ -14,6 +15,71 @@ const {
1415
ResizeHandle,
1516
} = Styles
1617

18+
const RawJsonToggleContainer = styled.div`
19+
display: flex;
20+
align-items: center;
21+
gap: 8px;
22+
padding: 16px 20px;
23+
border-bottom: 1px solid ${(props) => props.theme.border};
24+
background-color: ${(props) => props.theme.backgroundSubtleDark};
25+
font-size: 13px;
26+
color: ${(props) => props.theme.foregroundDark};
27+
user-select: none;
28+
`
29+
30+
const ToggleSwitch = styled.label`
31+
position: relative;
32+
display: inline-block;
33+
width: 44px;
34+
height: 24px;
35+
36+
input {
37+
opacity: 0;
38+
width: 0;
39+
height: 0;
40+
}
41+
42+
.slider {
43+
position: absolute;
44+
cursor: pointer;
45+
top: 0;
46+
left: 0;
47+
right: 0;
48+
bottom: 0;
49+
background-color: ${(props) => props.theme.backgroundSubtleDark};
50+
transition: 0.3s;
51+
border-radius: 24px;
52+
border: 1px solid ${(props) => props.theme.border};
53+
}
54+
55+
.slider:before {
56+
position: absolute;
57+
content: "";
58+
height: 16px;
59+
width: 16px;
60+
left: 3px;
61+
bottom: 3px;
62+
background-color: ${(props) => props.theme.foregroundDark};
63+
transition: 0.3s;
64+
border-radius: 50%;
65+
}
66+
67+
input:checked + .slider {
68+
background-color: ${(props) => props.theme.tag};
69+
}
70+
71+
input:checked + .slider:before {
72+
transform: translateX(20px);
73+
background-color: ${(props) => props.theme.background};
74+
}
75+
`
76+
77+
const ContentWrapper = styled.div`
78+
flex: 1;
79+
overflow-y: auto;
80+
padding: 20px;
81+
`
82+
1783
export const Network = () => {
1884
const hasUserResizedRef = useRef(false)
1985
const resizeTimeoutRef = useRef<NodeJS.Timeout>()
@@ -23,6 +89,7 @@ export const Network = () => {
2389
const [currentCommandId, setCurrentCommandId] = useState<number>()
2490
const [currSelectedType, setCurrSelectedType] = useState<string>("request headers")
2591
const [screenWidth, setScreenWidth] = useState(window.innerWidth)
92+
const [showRawJson, setShowRawJson] = useState(false)
2693

2794
const filteredCommands = commands.filter((command) => command.type === "api.response")
2895

@@ -105,12 +172,50 @@ export const Network = () => {
105172
<ResizeHandle onMouseDown={handleMouseDown} />
106173
<NetworkRequestHeader
107174
currSelectedType={currSelectedType}
108-
onTabChange={setCurrSelectedType}
175+
onTabChange={(tab) => {
176+
setCurrSelectedType(tab)
177+
if (tab !== "response body") {
178+
setShowRawJson(false)
179+
}
180+
}}
109181
tabContent={tabContent}
110182
/>
111183
{currentCommandId && (
112184
<RequestResponseContainerBody key={currentCommandId}>
113-
<ContentView value={tabContent[currSelectedType]} />
185+
{currSelectedType === "response body" && (
186+
<RawJsonToggleContainer>
187+
<span>Raw JSON</span>
188+
<ToggleSwitch>
189+
<input
190+
type="checkbox"
191+
checked={showRawJson}
192+
onChange={() => setShowRawJson(!showRawJson)}
193+
/>
194+
<span className="slider"></span>
195+
</ToggleSwitch>
196+
</RawJsonToggleContainer>
197+
)}
198+
<ContentWrapper>
199+
{showRawJson && currSelectedType === "response body" ? (
200+
<pre
201+
style={{
202+
margin: 0,
203+
fontFamily: "Monaco, Menlo, Consolas, monospace",
204+
fontSize: "13px",
205+
lineHeight: "1.5",
206+
userSelect: "text",
207+
cursor: "text",
208+
whiteSpace: "pre-wrap",
209+
wordBreak: "break-word",
210+
color: "#cbd5e0",
211+
}}
212+
>
213+
{JSON.stringify(tabContent[currSelectedType], null, 2)}
214+
</pre>
215+
) : (
216+
<ContentView value={tabContent[currSelectedType]} />
217+
)}
218+
</ContentWrapper>
114219
</RequestResponseContainerBody>
115220
)}
116221
</RequestResponseContainer>

0 commit comments

Comments
 (0)