@buoy-gg/network 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +381 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/network/components/NetworkCopySettingsView.js +867 -0
- package/lib/commonjs/network/components/NetworkEventDetailView.js +837 -0
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +323 -0
- package/lib/commonjs/network/components/NetworkFilterViewV3.js +297 -0
- package/lib/commonjs/network/components/NetworkModal.js +937 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +320 -0
- package/lib/commonjs/network/hooks/useTickEveryMinute.js +34 -0
- package/lib/commonjs/network/index.js +102 -0
- package/lib/commonjs/network/types/index.js +1 -0
- package/lib/commonjs/network/utils/extractOperationName.js +80 -0
- package/lib/commonjs/network/utils/formatGraphQLVariables.js +219 -0
- package/lib/commonjs/network/utils/formatting.js +30 -0
- package/lib/commonjs/network/utils/networkEventStore.js +269 -0
- package/lib/commonjs/network/utils/networkListener.js +801 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +83 -0
- package/lib/module/index.js +7 -0
- package/lib/module/network/components/NetworkCopySettingsView.js +862 -0
- package/lib/module/network/components/NetworkEventDetailView.js +834 -0
- package/lib/module/network/components/NetworkEventItemCompact.js +320 -0
- package/lib/module/network/components/NetworkFilterViewV3.js +293 -0
- package/lib/module/network/components/NetworkModal.js +933 -0
- package/lib/module/network/hooks/useNetworkEvents.js +316 -0
- package/lib/module/network/hooks/useTickEveryMinute.js +29 -0
- package/lib/module/network/index.js +20 -0
- package/lib/module/network/types/index.js +1 -0
- package/lib/module/network/utils/extractOperationName.js +76 -0
- package/lib/module/network/utils/formatGraphQLVariables.js +213 -0
- package/lib/module/network/utils/formatting.js +9 -0
- package/lib/module/network/utils/networkEventStore.js +265 -0
- package/lib/module/network/utils/networkListener.js +791 -0
- package/lib/module/preset.js +79 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts +26 -0
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts +13 -0
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts +22 -0
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkModal.d.ts +14 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +72 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useTickEveryMinute.d.ts +9 -0
- package/lib/typescript/network/hooks/useTickEveryMinute.d.ts.map +1 -0
- package/lib/typescript/network/index.d.ts +12 -0
- package/lib/typescript/network/index.d.ts.map +1 -0
- package/lib/typescript/network/types/index.d.ts +88 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -0
- package/lib/typescript/network/utils/extractOperationName.d.ts +41 -0
- package/lib/typescript/network/utils/extractOperationName.d.ts.map +1 -0
- package/lib/typescript/network/utils/formatGraphQLVariables.d.ts +79 -0
- package/lib/typescript/network/utils/formatGraphQLVariables.d.ts.map +1 -0
- package/lib/typescript/network/utils/formatting.d.ts +6 -0
- package/lib/typescript/network/utils/formatting.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts +81 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkListener.d.ts +191 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +76 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { StyleSheet, View, Text } from "react-native";
|
|
5
|
+
import { ChevronRight, Upload, Download, Clock, AlertCircle, ListItem, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
|
|
6
|
+
import { formatBytes, formatDuration } from "../utils/formatting";
|
|
7
|
+
import { formatRelativeTime } from "@buoy-gg/shared-ui";
|
|
8
|
+
import { useTickEveryMinute } from "../hooks/useTickEveryMinute";
|
|
9
|
+
import { formatGraphQLDisplay } from "../utils/formatGraphQLVariables";
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
// Get color based on status
|
|
12
|
+
function getStatusColor(status, error) {
|
|
13
|
+
if (error) return macOSColors.semantic.error;
|
|
14
|
+
if (!status) return macOSColors.semantic.warning;
|
|
15
|
+
if (status >= 200 && status < 300) return macOSColors.semantic.success;
|
|
16
|
+
if (status >= 300 && status < 400) return macOSColors.semantic.info;
|
|
17
|
+
if (status >= 400) return macOSColors.semantic.error;
|
|
18
|
+
return macOSColors.text.muted;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get content type badge with color
|
|
22
|
+
function getContentTypeBadge(headers) {
|
|
23
|
+
const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
|
|
24
|
+
if (contentType.includes("json")) return "JSON";
|
|
25
|
+
if (contentType.includes("xml")) return "XML";
|
|
26
|
+
if (contentType.includes("html")) return "HTML";
|
|
27
|
+
if (contentType.includes("text")) return "TEXT";
|
|
28
|
+
if (contentType.includes("image")) return "IMG";
|
|
29
|
+
if (contentType.includes("video")) return "VIDEO";
|
|
30
|
+
if (contentType.includes("audio")) return "AUDIO";
|
|
31
|
+
if (contentType.includes("form")) return "FORM";
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Decomposed components following rule3 - Component Composition
|
|
36
|
+
|
|
37
|
+
// Status indicator component - single responsibility
|
|
38
|
+
function StatusIndicator({
|
|
39
|
+
event,
|
|
40
|
+
isPending,
|
|
41
|
+
statusColor
|
|
42
|
+
}) {
|
|
43
|
+
if (isPending) {
|
|
44
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
45
|
+
style: styles.pendingBadge,
|
|
46
|
+
children: [/*#__PURE__*/_jsx(Clock, {
|
|
47
|
+
size: 10,
|
|
48
|
+
color: macOSColors.semantic.warning
|
|
49
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
50
|
+
style: styles.pendingText,
|
|
51
|
+
children: "..."
|
|
52
|
+
})]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (event.error) {
|
|
56
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
57
|
+
style: styles.errorBadge,
|
|
58
|
+
children: [/*#__PURE__*/_jsx(AlertCircle, {
|
|
59
|
+
size: 10,
|
|
60
|
+
color: macOSColors.semantic.error
|
|
61
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
62
|
+
style: styles.errorText,
|
|
63
|
+
children: "ERR"
|
|
64
|
+
})]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return /*#__PURE__*/_jsx(View, {
|
|
68
|
+
style: styles.statusBadge,
|
|
69
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
70
|
+
style: [styles.statusText, {
|
|
71
|
+
color: statusColor
|
|
72
|
+
}],
|
|
73
|
+
children: String(event.status)
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Size indicators component - single responsibility
|
|
79
|
+
function SizeIndicators({
|
|
80
|
+
requestSize,
|
|
81
|
+
responseSize
|
|
82
|
+
}) {
|
|
83
|
+
if (!requestSize && !responseSize) return null;
|
|
84
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
85
|
+
style: styles.sizeRow,
|
|
86
|
+
children: [requestSize ? /*#__PURE__*/_jsxs(View, {
|
|
87
|
+
style: styles.sizeItem,
|
|
88
|
+
children: [/*#__PURE__*/_jsx(Upload, {
|
|
89
|
+
size: 8,
|
|
90
|
+
color: macOSColors.semantic.info
|
|
91
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
92
|
+
style: styles.sizeText,
|
|
93
|
+
children: formatBytes(requestSize)
|
|
94
|
+
})]
|
|
95
|
+
}) : null, responseSize ? /*#__PURE__*/_jsxs(View, {
|
|
96
|
+
style: styles.sizeItem,
|
|
97
|
+
children: [/*#__PURE__*/_jsx(Download, {
|
|
98
|
+
size: 8,
|
|
99
|
+
color: macOSColors.semantic.success
|
|
100
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
101
|
+
style: styles.sizeText,
|
|
102
|
+
children: formatBytes(responseSize)
|
|
103
|
+
})]
|
|
104
|
+
}) : null]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compact list-row representation of a network event. Optimized for large lists with memoization
|
|
110
|
+
* and periodic refresh via `useTickEveryMinute` to keep relative timestamps accurate.
|
|
111
|
+
*/
|
|
112
|
+
export const NetworkEventItemCompact = /*#__PURE__*/memo(({
|
|
113
|
+
event,
|
|
114
|
+
onPress
|
|
115
|
+
}) => {
|
|
116
|
+
const tick = useTickEveryMinute();
|
|
117
|
+
const statusColor = getStatusColor(event.status, event.error);
|
|
118
|
+
const isPending = !event.status && !event.error;
|
|
119
|
+
const contentType = getContentTypeBadge(event.responseHeaders);
|
|
120
|
+
|
|
121
|
+
// Format URL for display (max 2 lines)
|
|
122
|
+
let displayUrl = event.path || event.url.replace(/^https?:\/\/[^/]+/, "");
|
|
123
|
+
|
|
124
|
+
// If this is a GraphQL request, show operation name with variables using arrow notation
|
|
125
|
+
// Matches React Query pattern: ["pokemon", "Sandshrew"] → "pokemon › Sandshrew"
|
|
126
|
+
if (event.requestClient === "graphql") {
|
|
127
|
+
if (event.operationName) {
|
|
128
|
+
// Format: GetPokemon › Sandshrew (matches React Query pattern)
|
|
129
|
+
displayUrl = formatGraphQLDisplay(event.operationName, event.graphqlVariables);
|
|
130
|
+
} else {
|
|
131
|
+
// If no operation name found, just remove the redundant /graphql path
|
|
132
|
+
displayUrl = displayUrl.replace(/\/graphql[^?]*/, "/graphql");
|
|
133
|
+
}
|
|
134
|
+
} else if (event.operationName) {
|
|
135
|
+
// For non-GraphQL requests with operation names (e.g., gRPC)
|
|
136
|
+
displayUrl = `${displayUrl}\n(${event.operationName})`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Format time with both absolute and relative
|
|
140
|
+
const timeString = new Date(event.timestamp).toLocaleTimeString("en-US", {
|
|
141
|
+
hour: "numeric",
|
|
142
|
+
minute: "2-digit",
|
|
143
|
+
second: "2-digit",
|
|
144
|
+
hour12: true
|
|
145
|
+
});
|
|
146
|
+
const relativeTime = formatRelativeTime(event.timestamp, tick);
|
|
147
|
+
return /*#__PURE__*/_jsxs(ListItem, {
|
|
148
|
+
onPress: () => onPress(event),
|
|
149
|
+
style: [styles.container, {
|
|
150
|
+
borderLeftColor: statusColor
|
|
151
|
+
}],
|
|
152
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
153
|
+
style: styles.leftSection,
|
|
154
|
+
children: [/*#__PURE__*/_jsx(MethodBadge, {
|
|
155
|
+
method: event.method,
|
|
156
|
+
size: "small"
|
|
157
|
+
}), event.requestClient && /*#__PURE__*/_jsx(View, {
|
|
158
|
+
style: [styles.clientBadge, {
|
|
159
|
+
backgroundColor: event.requestClient === "fetch" ? "rgba(74, 144, 226, 0.15)" : event.requestClient === "graphql" ? "rgba(229, 53, 171, 0.15)" : event.requestClient === "grpc-web" ? "rgba(16, 185, 129, 0.15)" : "rgba(147, 51, 234, 0.15)"
|
|
160
|
+
}],
|
|
161
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
162
|
+
style: [styles.clientText, {
|
|
163
|
+
color: event.requestClient === "fetch" ? "#4A90E2" : event.requestClient === "graphql" ? "#E535AB" : event.requestClient === "grpc-web" ? "#10B981" : "#9333EA"
|
|
164
|
+
}],
|
|
165
|
+
children: event.requestClient === "graphql" ? "GQL" : event.requestClient === "grpc-web" ? "gRPC" : event.requestClient
|
|
166
|
+
})
|
|
167
|
+
}), /*#__PURE__*/_jsx(SizeIndicators, {
|
|
168
|
+
requestSize: event.requestSize,
|
|
169
|
+
responseSize: event.responseSize
|
|
170
|
+
})]
|
|
171
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
172
|
+
style: styles.middleSection,
|
|
173
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
174
|
+
style: styles.urlText,
|
|
175
|
+
numberOfLines: 2,
|
|
176
|
+
children: displayUrl
|
|
177
|
+
})
|
|
178
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
179
|
+
style: styles.rightSection,
|
|
180
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
181
|
+
style: styles.rightTopRow,
|
|
182
|
+
children: [/*#__PURE__*/_jsx(StatusIndicator, {
|
|
183
|
+
event: event,
|
|
184
|
+
isPending: isPending,
|
|
185
|
+
statusColor: statusColor
|
|
186
|
+
}), event.duration ? /*#__PURE__*/_jsx(Text, {
|
|
187
|
+
style: styles.durationText,
|
|
188
|
+
children: formatDuration(event.duration)
|
|
189
|
+
}) : null, contentType ? /*#__PURE__*/_jsx(TypeBadge, {
|
|
190
|
+
type: contentType,
|
|
191
|
+
size: "small"
|
|
192
|
+
}) : null]
|
|
193
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
194
|
+
style: styles.rightBottomRow,
|
|
195
|
+
children: /*#__PURE__*/_jsxs(ListItem.Metadata, {
|
|
196
|
+
children: [timeString, " (", relativeTime, ")"]
|
|
197
|
+
})
|
|
198
|
+
})]
|
|
199
|
+
}), /*#__PURE__*/_jsx(ChevronRight, {
|
|
200
|
+
size: 14,
|
|
201
|
+
color: macOSColors.text.muted
|
|
202
|
+
})]
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
const styles = StyleSheet.create({
|
|
206
|
+
container: {
|
|
207
|
+
flexDirection: "row",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
backgroundColor: macOSColors.background.card,
|
|
210
|
+
borderRadius: 6,
|
|
211
|
+
paddingVertical: 8,
|
|
212
|
+
paddingHorizontal: 10,
|
|
213
|
+
paddingLeft: 8,
|
|
214
|
+
marginBottom: 4,
|
|
215
|
+
marginHorizontal: 12,
|
|
216
|
+
minHeight: 44,
|
|
217
|
+
borderLeftWidth: 3,
|
|
218
|
+
borderLeftColor: "transparent"
|
|
219
|
+
},
|
|
220
|
+
leftSection: {
|
|
221
|
+
marginRight: 8,
|
|
222
|
+
alignItems: "flex-start",
|
|
223
|
+
paddingTop: 2
|
|
224
|
+
},
|
|
225
|
+
middleSection: {
|
|
226
|
+
flex: 1,
|
|
227
|
+
justifyContent: "center",
|
|
228
|
+
paddingRight: 8
|
|
229
|
+
},
|
|
230
|
+
urlText: {
|
|
231
|
+
fontSize: 12,
|
|
232
|
+
color: macOSColors.text.primary,
|
|
233
|
+
lineHeight: 16,
|
|
234
|
+
fontFamily: "monospace"
|
|
235
|
+
},
|
|
236
|
+
rightSection: {
|
|
237
|
+
alignItems: "flex-end",
|
|
238
|
+
justifyContent: "center",
|
|
239
|
+
marginRight: 4
|
|
240
|
+
},
|
|
241
|
+
rightTopRow: {
|
|
242
|
+
flexDirection: "row",
|
|
243
|
+
alignItems: "center",
|
|
244
|
+
gap: 6,
|
|
245
|
+
marginBottom: 2
|
|
246
|
+
},
|
|
247
|
+
rightBottomRow: {
|
|
248
|
+
flexDirection: "row",
|
|
249
|
+
alignItems: "center",
|
|
250
|
+
gap: 4
|
|
251
|
+
},
|
|
252
|
+
statusBadge: {
|
|
253
|
+
paddingHorizontal: 4,
|
|
254
|
+
paddingVertical: 1,
|
|
255
|
+
borderRadius: 3
|
|
256
|
+
},
|
|
257
|
+
statusText: {
|
|
258
|
+
fontSize: 10,
|
|
259
|
+
fontWeight: "600"
|
|
260
|
+
},
|
|
261
|
+
pendingBadge: {
|
|
262
|
+
flexDirection: "row",
|
|
263
|
+
alignItems: "center",
|
|
264
|
+
gap: 2,
|
|
265
|
+
paddingHorizontal: 4,
|
|
266
|
+
paddingVertical: 1,
|
|
267
|
+
backgroundColor: macOSColors.semantic.warning + "26",
|
|
268
|
+
borderRadius: 3
|
|
269
|
+
},
|
|
270
|
+
pendingText: {
|
|
271
|
+
fontSize: 10,
|
|
272
|
+
color: macOSColors.semantic.warning,
|
|
273
|
+
fontWeight: "600"
|
|
274
|
+
},
|
|
275
|
+
errorBadge: {
|
|
276
|
+
flexDirection: "row",
|
|
277
|
+
alignItems: "center",
|
|
278
|
+
gap: 2,
|
|
279
|
+
paddingHorizontal: 4,
|
|
280
|
+
paddingVertical: 1,
|
|
281
|
+
backgroundColor: macOSColors.semantic.error + "26",
|
|
282
|
+
borderRadius: 3
|
|
283
|
+
},
|
|
284
|
+
errorText: {
|
|
285
|
+
fontSize: 10,
|
|
286
|
+
color: macOSColors.semantic.error,
|
|
287
|
+
fontWeight: "600"
|
|
288
|
+
},
|
|
289
|
+
durationText: {
|
|
290
|
+
fontSize: 9,
|
|
291
|
+
color: macOSColors.text.secondary
|
|
292
|
+
},
|
|
293
|
+
sizeRow: {
|
|
294
|
+
flexDirection: "row",
|
|
295
|
+
gap: 4,
|
|
296
|
+
marginTop: 4
|
|
297
|
+
},
|
|
298
|
+
sizeItem: {
|
|
299
|
+
flexDirection: "row",
|
|
300
|
+
alignItems: "center",
|
|
301
|
+
gap: 2
|
|
302
|
+
},
|
|
303
|
+
sizeText: {
|
|
304
|
+
fontSize: 8,
|
|
305
|
+
color: macOSColors.text.secondary,
|
|
306
|
+
fontFamily: "monospace"
|
|
307
|
+
},
|
|
308
|
+
clientBadge: {
|
|
309
|
+
paddingHorizontal: 4,
|
|
310
|
+
paddingVertical: 2,
|
|
311
|
+
borderRadius: 3,
|
|
312
|
+
marginTop: 4
|
|
313
|
+
},
|
|
314
|
+
clientText: {
|
|
315
|
+
fontSize: 8,
|
|
316
|
+
fontWeight: "700",
|
|
317
|
+
letterSpacing: 0.3,
|
|
318
|
+
textTransform: "uppercase"
|
|
319
|
+
}
|
|
320
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle, Clock, DynamicFilterView, Film, FileJson, FileText, Filter, Globe, Image, macOSColors, Music, XCircle } from "@buoy-gg/shared-ui";
|
|
4
|
+
import { useCallback, useMemo } from "react";
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
function getContentType(event) {
|
|
7
|
+
const headers = event.responseHeaders || event.requestHeaders;
|
|
8
|
+
const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
|
|
9
|
+
if (contentType.includes("json")) return {
|
|
10
|
+
type: "JSON",
|
|
11
|
+
color: macOSColors.semantic.info
|
|
12
|
+
};
|
|
13
|
+
if (contentType.includes("xml")) return {
|
|
14
|
+
type: "XML",
|
|
15
|
+
color: macOSColors.semantic.success
|
|
16
|
+
};
|
|
17
|
+
if (contentType.includes("html")) return {
|
|
18
|
+
type: "HTML",
|
|
19
|
+
color: macOSColors.semantic.warning
|
|
20
|
+
};
|
|
21
|
+
if (contentType.includes("text")) return {
|
|
22
|
+
type: "TEXT",
|
|
23
|
+
color: macOSColors.semantic.success
|
|
24
|
+
};
|
|
25
|
+
if (contentType.includes("image")) return {
|
|
26
|
+
type: "IMAGE",
|
|
27
|
+
color: macOSColors.semantic.error
|
|
28
|
+
};
|
|
29
|
+
if (contentType.includes("video")) return {
|
|
30
|
+
type: "VIDEO",
|
|
31
|
+
color: macOSColors.semantic.error
|
|
32
|
+
};
|
|
33
|
+
if (contentType.includes("audio")) return {
|
|
34
|
+
type: "AUDIO",
|
|
35
|
+
color: macOSColors.semantic.debug
|
|
36
|
+
};
|
|
37
|
+
if (contentType.includes("form")) return {
|
|
38
|
+
type: "FORM",
|
|
39
|
+
color: macOSColors.semantic.info
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
type: "OTHER",
|
|
43
|
+
color: macOSColors.text.muted
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function getContentTypeIcon(type) {
|
|
47
|
+
switch (type) {
|
|
48
|
+
case "JSON":
|
|
49
|
+
return FileJson;
|
|
50
|
+
case "HTML":
|
|
51
|
+
case "XML":
|
|
52
|
+
case "TEXT":
|
|
53
|
+
return FileText;
|
|
54
|
+
case "IMAGE":
|
|
55
|
+
return Image;
|
|
56
|
+
case "VIDEO":
|
|
57
|
+
return Film;
|
|
58
|
+
case "AUDIO":
|
|
59
|
+
return Music;
|
|
60
|
+
default:
|
|
61
|
+
return Globe;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getMethodColor(method) {
|
|
65
|
+
switch (method) {
|
|
66
|
+
case "GET":
|
|
67
|
+
return macOSColors.semantic.success;
|
|
68
|
+
case "POST":
|
|
69
|
+
return macOSColors.semantic.info;
|
|
70
|
+
case "PUT":
|
|
71
|
+
return macOSColors.semantic.warning;
|
|
72
|
+
case "DELETE":
|
|
73
|
+
return macOSColors.semantic.error;
|
|
74
|
+
case "PATCH":
|
|
75
|
+
return macOSColors.semantic.success;
|
|
76
|
+
default:
|
|
77
|
+
return macOSColors.text.muted;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Control panel for filtering captured network events by status, method, host, and content type.
|
|
83
|
+
* Provides quick toggles, stats, and ignored-pattern management reminiscent of desktop tooling.
|
|
84
|
+
*/
|
|
85
|
+
export function NetworkFilterViewV3({
|
|
86
|
+
events,
|
|
87
|
+
filter,
|
|
88
|
+
onFilterChange,
|
|
89
|
+
ignoredPatterns = new Set(),
|
|
90
|
+
onTogglePattern = () => {},
|
|
91
|
+
onAddPattern = () => {}
|
|
92
|
+
}) {
|
|
93
|
+
const statusCounts = useMemo(() => {
|
|
94
|
+
const counts = {
|
|
95
|
+
all: events.length,
|
|
96
|
+
success: 0,
|
|
97
|
+
error: 0,
|
|
98
|
+
pending: 0
|
|
99
|
+
};
|
|
100
|
+
events.forEach(event => {
|
|
101
|
+
if (event.error || event.status && event.status >= 400) {
|
|
102
|
+
counts.error += 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.status && event.status >= 200 && event.status < 300) {
|
|
106
|
+
counts.success += 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!event.status && !event.error) {
|
|
110
|
+
counts.pending += 1;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return counts;
|
|
114
|
+
}, [events]);
|
|
115
|
+
const methodCounts = useMemo(() => {
|
|
116
|
+
return events.reduce((acc, event) => {
|
|
117
|
+
if (!event.method) return acc;
|
|
118
|
+
acc[event.method] = (acc[event.method] || 0) + 1;
|
|
119
|
+
return acc;
|
|
120
|
+
}, {});
|
|
121
|
+
}, [events]);
|
|
122
|
+
const contentTypeCounts = useMemo(() => {
|
|
123
|
+
return events.reduce((acc, event) => {
|
|
124
|
+
const {
|
|
125
|
+
type
|
|
126
|
+
} = getContentType(event);
|
|
127
|
+
acc[type] = (acc[type] || 0) + 1;
|
|
128
|
+
return acc;
|
|
129
|
+
}, {});
|
|
130
|
+
}, [events]);
|
|
131
|
+
const availableDomains = useMemo(() => {
|
|
132
|
+
const domains = new Set();
|
|
133
|
+
events.forEach(event => {
|
|
134
|
+
if (event.host) {
|
|
135
|
+
domains.add(event.host);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
if (event.url) {
|
|
140
|
+
const parsed = new URL(event.url);
|
|
141
|
+
const host = String(parsed.host || "");
|
|
142
|
+
if (host) domains.add(host);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Ignore relative URLs
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return Array.from(domains).filter(domain => domain && domain !== "").sort((a, b) => a.localeCompare(b));
|
|
149
|
+
}, [events]);
|
|
150
|
+
const availableUrls = useMemo(() => {
|
|
151
|
+
const urls = new Set();
|
|
152
|
+
events.forEach(event => {
|
|
153
|
+
if (!event.url) return;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = new URL(event.url);
|
|
156
|
+
const origin = String(parsed.origin || "");
|
|
157
|
+
const pathname = String(parsed.pathname || "");
|
|
158
|
+
const normalized = `${origin}${pathname}`;
|
|
159
|
+
urls.add(normalized);
|
|
160
|
+
} catch {
|
|
161
|
+
// URL constructor fails for relative paths - fall back to raw value
|
|
162
|
+
urls.add(event.url);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return Array.from(urls).filter(url => url && url !== "").sort((a, b) => a.localeCompare(b));
|
|
166
|
+
}, [events]);
|
|
167
|
+
const suggestionItems = useMemo(() => {
|
|
168
|
+
const maxItemsPerGroup = 30;
|
|
169
|
+
const domainSuggestions = availableDomains.slice(0, maxItemsPerGroup);
|
|
170
|
+
const urlSuggestions = availableUrls.slice(0, maxItemsPerGroup);
|
|
171
|
+
return [...domainSuggestions, ...urlSuggestions];
|
|
172
|
+
}, [availableDomains, availableUrls]);
|
|
173
|
+
const handleDynamicFilterChange = useCallback((optionId, value) => {
|
|
174
|
+
const [group] = optionId.split("::");
|
|
175
|
+
if (group === "status") {
|
|
176
|
+
const nextStatus = value === "all" ? undefined : value;
|
|
177
|
+
onFilterChange({
|
|
178
|
+
...filter,
|
|
179
|
+
status: nextStatus
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (group === "method") {
|
|
184
|
+
const methodValue = String(value);
|
|
185
|
+
const currentMethods = filter.method || [];
|
|
186
|
+
const hasMethod = currentMethods.includes(methodValue);
|
|
187
|
+
const updatedMethods = hasMethod ? currentMethods.filter(method => method !== methodValue) : [methodValue];
|
|
188
|
+
onFilterChange({
|
|
189
|
+
...filter,
|
|
190
|
+
method: updatedMethods.length > 0 ? updatedMethods : undefined
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (group === "contentType") {
|
|
195
|
+
const typeValue = String(value);
|
|
196
|
+
const currentTypes = filter.contentType || [];
|
|
197
|
+
const hasType = currentTypes.includes(typeValue);
|
|
198
|
+
const updatedTypes = hasType ? currentTypes.filter(type => type !== typeValue) : [typeValue];
|
|
199
|
+
onFilterChange({
|
|
200
|
+
...filter,
|
|
201
|
+
contentType: updatedTypes.length > 0 ? updatedTypes : undefined
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}, [filter, onFilterChange]);
|
|
205
|
+
const filterSections = useMemo(() => {
|
|
206
|
+
const sections = [];
|
|
207
|
+
sections.push({
|
|
208
|
+
id: "status",
|
|
209
|
+
title: "Status",
|
|
210
|
+
type: "status",
|
|
211
|
+
data: ["all", "success", "error", "pending"].map(status => ({
|
|
212
|
+
id: `status::${status}`,
|
|
213
|
+
label: status.charAt(0).toUpperCase() + status.slice(1),
|
|
214
|
+
count: statusCounts[status],
|
|
215
|
+
icon: status === "success" ? CheckCircle : status === "error" ? XCircle : status === "pending" ? Clock : Globe,
|
|
216
|
+
color: status === "success" ? macOSColors.semantic.success : status === "error" ? macOSColors.semantic.error : status === "pending" ? macOSColors.semantic.warning : macOSColors.semantic.info,
|
|
217
|
+
isActive: filter.status === status || !filter.status && status === "all",
|
|
218
|
+
value: status
|
|
219
|
+
}))
|
|
220
|
+
});
|
|
221
|
+
const methodEntries = Object.entries(methodCounts);
|
|
222
|
+
if (methodEntries.length > 0) {
|
|
223
|
+
sections.push({
|
|
224
|
+
id: "method",
|
|
225
|
+
title: "Method",
|
|
226
|
+
type: "method",
|
|
227
|
+
data: methodEntries.map(([method, count]) => ({
|
|
228
|
+
id: `method::${method}`,
|
|
229
|
+
label: method,
|
|
230
|
+
count,
|
|
231
|
+
color: getMethodColor(method),
|
|
232
|
+
isActive: filter.method?.includes(method) ?? false,
|
|
233
|
+
value: method
|
|
234
|
+
}))
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const contentTypeEntries = Object.entries(contentTypeCounts);
|
|
238
|
+
if (contentTypeEntries.length > 0) {
|
|
239
|
+
sections.push({
|
|
240
|
+
id: "contentType",
|
|
241
|
+
title: "Content Type",
|
|
242
|
+
type: "contentType",
|
|
243
|
+
data: contentTypeEntries.map(([type, count]) => {
|
|
244
|
+
const representativeEvent = events.find(event => getContentType(event).type === type);
|
|
245
|
+
const {
|
|
246
|
+
color
|
|
247
|
+
} = representativeEvent ? getContentType(representativeEvent) : {
|
|
248
|
+
color: macOSColors.text.muted
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
id: `contentType::${type}`,
|
|
252
|
+
label: type,
|
|
253
|
+
count,
|
|
254
|
+
icon: getContentTypeIcon(type),
|
|
255
|
+
color,
|
|
256
|
+
isActive: filter.contentType?.includes(type) ?? false,
|
|
257
|
+
value: type
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return sections;
|
|
263
|
+
}, [contentTypeCounts, events, filter.contentType, filter.method, filter.status, methodCounts, statusCounts]);
|
|
264
|
+
const dynamicFilterConfig = useMemo(() => ({
|
|
265
|
+
sections: filterSections,
|
|
266
|
+
addFilterSection: {
|
|
267
|
+
enabled: true,
|
|
268
|
+
placeholder: "Enter domain or URL pattern...",
|
|
269
|
+
title: "ACTIVE FILTERS",
|
|
270
|
+
icon: Filter
|
|
271
|
+
},
|
|
272
|
+
availableItemsSection: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
title: "AVAILABLE DOMAINS & URLS",
|
|
275
|
+
emptyMessage: "No network events captured yet. Domains and URLs will appear here.",
|
|
276
|
+
items: suggestionItems
|
|
277
|
+
},
|
|
278
|
+
howItWorksSection: {
|
|
279
|
+
enabled: true,
|
|
280
|
+
title: "HOW NETWORK FILTERS WORK",
|
|
281
|
+
description: "Patterns hide matching requests from the network event list. Filters match if the domain or URL contains the provided text.",
|
|
282
|
+
examples: ["• example.com → filters any request whose host includes example.com", "• https://api.example.com/v1/users → filters that exact endpoint", "• /health → filters any URL path containing /health"],
|
|
283
|
+
icon: Filter
|
|
284
|
+
},
|
|
285
|
+
onFilterChange: handleDynamicFilterChange,
|
|
286
|
+
onPatternAdd: onAddPattern,
|
|
287
|
+
onPatternToggle: onTogglePattern,
|
|
288
|
+
activePatterns: ignoredPatterns
|
|
289
|
+
}), [filterSections, handleDynamicFilterChange, ignoredPatterns, onAddPattern, onTogglePattern, suggestionItems]);
|
|
290
|
+
return /*#__PURE__*/_jsx(DynamicFilterView, {
|
|
291
|
+
...dynamicFilterConfig
|
|
292
|
+
});
|
|
293
|
+
}
|