@buoy-gg/network 7.0.4 → 7.0.5
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 +2 -1
- package/lib/commonjs/index.js +85 -0
- package/lib/commonjs/network/components/NetworkEventDetailView.js +201 -40
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +52 -5
- package/lib/commonjs/network/components/NetworkHeaderLive.js +132 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +134 -56
- package/lib/commonjs/network/components/NetworkModal.js +154 -36
- package/lib/commonjs/network/components/NetworkSavedScreen.js +245 -0
- package/lib/commonjs/network/components/NetworkUpgradeBanner.js +76 -0
- package/lib/commonjs/network/hooks/useLiveNetworkEvent.js +62 -0
- package/lib/commonjs/network/hooks/useNetworkSaved.js +38 -0
- package/lib/commonjs/network/index.js +84 -0
- package/lib/commonjs/network/sync/networkSyncAdapter.js +85 -7
- package/lib/commonjs/network/utils/generateCurl.js +71 -0
- package/lib/commonjs/network/utils/networkEventStore.js +10 -1
- package/lib/commonjs/network/utils/networkListener.js +54 -1
- package/lib/commonjs/network/utils/networkSavedStore.js +575 -0
- package/lib/commonjs/network/utils/withPinnedFirst.js +32 -0
- package/lib/module/index.js +17 -1
- package/lib/module/network/components/NetworkEventDetailView.js +204 -41
- package/lib/module/network/components/NetworkEventItemCompact.js +53 -6
- package/lib/module/network/components/NetworkHeaderLive.js +132 -2
- package/lib/module/network/components/NetworkListScreen.js +136 -58
- package/lib/module/network/components/NetworkModal.js +156 -39
- package/lib/module/network/components/NetworkSavedScreen.js +241 -0
- package/lib/module/network/components/NetworkUpgradeBanner.js +72 -0
- package/lib/module/network/hooks/useLiveNetworkEvent.js +58 -0
- package/lib/module/network/hooks/useNetworkSaved.js +34 -0
- package/lib/module/network/index.js +7 -1
- package/lib/module/network/sync/networkSyncAdapter.js +85 -7
- package/lib/module/network/utils/generateCurl.js +67 -0
- package/lib/module/network/utils/networkEventStore.js +10 -1
- package/lib/module/network/utils/networkListener.js +54 -1
- package/lib/module/network/utils/networkSavedStore.js +569 -0
- package/lib/module/network/utils/withPinnedFirst.js +28 -0
- package/lib/typescript/index.d.ts +13 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts +28 -0
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts +34 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkSavedScreen.d.ts +29 -0
- package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts +17 -0
- package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts +19 -0
- package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkSaved.d.ts +21 -0
- package/lib/typescript/network/hooks/useNetworkSaved.d.ts.map +1 -0
- package/lib/typescript/network/index.d.ts +8 -1
- package/lib/typescript/network/index.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +52 -3
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +43 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/generateCurl.d.ts +3 -0
- package/lib/typescript/network/utils/generateCurl.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +4 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +186 -0
- package/lib/typescript/network/utils/networkSavedStore.d.ts.map +1 -0
- package/lib/typescript/network/utils/withPinnedFirst.d.ts +24 -0
- package/lib/typescript/network/utils/withPinnedFirst.d.ts.map +1 -0
- package/package.json +2 -2
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
* `paused || !visible` so a hidden list doesn't subscribe at all.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { useCallback } from "react";
|
|
15
16
|
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
16
|
-
import { Globe,
|
|
17
|
+
import { Globe, Pin, Power, macOSColors } from "@buoy-gg/shared-ui";
|
|
17
18
|
import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
|
|
19
|
+
import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
|
|
18
20
|
import { useNetworkEventList } from "../hooks/useNetworkEventList";
|
|
19
|
-
import {
|
|
21
|
+
import { useNetworkSaved } from "../hooks/useNetworkSaved";
|
|
22
|
+
import { splitPinned } from "../utils/withPinnedFirst";
|
|
23
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
20
24
|
function EmptyState({
|
|
21
25
|
isEnabled
|
|
22
26
|
}) {
|
|
@@ -35,6 +39,54 @@ function EmptyState({
|
|
|
35
39
|
});
|
|
36
40
|
}
|
|
37
41
|
const keyExtractor = item => item.id;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The PINNED strip above the live list.
|
|
45
|
+
*
|
|
46
|
+
* It renders OUTSIDE the FlatList on purpose. The list's window/batch values
|
|
47
|
+
* are a measured configuration (see the FlatList below), and merging pins into
|
|
48
|
+
* `data` would mean sentinel items in `keyExtractor`/`renderItem` and a
|
|
49
|
+
* section boundary that moves as events arrive. Keeping pins in their own
|
|
50
|
+
* bounded strip (capped at MAX_PINNED in the store) leaves that tuning
|
|
51
|
+
* untouched — and makes "pins ignore the filters" fall out for free, because
|
|
52
|
+
* this strip is never handed the filter.
|
|
53
|
+
*/
|
|
54
|
+
function PinnedSection({
|
|
55
|
+
events,
|
|
56
|
+
onEventPress,
|
|
57
|
+
onLongPress,
|
|
58
|
+
savedLiveIds,
|
|
59
|
+
onClearPinned
|
|
60
|
+
}) {
|
|
61
|
+
if (events.length === 0) return null;
|
|
62
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
63
|
+
style: styles.pinnedSection,
|
|
64
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
65
|
+
style: styles.pinnedHeader,
|
|
66
|
+
children: [/*#__PURE__*/_jsx(Pin, {
|
|
67
|
+
size: 11,
|
|
68
|
+
color: macOSColors.semantic.info
|
|
69
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
70
|
+
style: styles.pinnedTitle,
|
|
71
|
+
children: ["PINNED \xB7 ", events.length]
|
|
72
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
73
|
+
onPress: onClearPinned,
|
|
74
|
+
"sentry-label": "ignore unpin all",
|
|
75
|
+
style: styles.pinnedClear,
|
|
76
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
77
|
+
style: styles.pinnedClearText,
|
|
78
|
+
children: "Unpin all"
|
|
79
|
+
})
|
|
80
|
+
})]
|
|
81
|
+
}), events.map(event => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
|
|
82
|
+
event: event,
|
|
83
|
+
onPress: onEventPress,
|
|
84
|
+
onLongPress: onLongPress,
|
|
85
|
+
pinned: true,
|
|
86
|
+
saved: savedLiveIds.has(event.id)
|
|
87
|
+
}, event.id))]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
38
90
|
export function NetworkListScreen({
|
|
39
91
|
visible,
|
|
40
92
|
paused,
|
|
@@ -55,42 +107,48 @@ export function NetworkListScreen({
|
|
|
55
107
|
paused: paused || !visible,
|
|
56
108
|
isPro
|
|
57
109
|
});
|
|
110
|
+
|
|
111
|
+
// Pins come from the persisted saved store, NOT from the live list — that is
|
|
112
|
+
// what lets a pinned failure survive Clear, the 500-event cap and a reload.
|
|
113
|
+
const {
|
|
114
|
+
pinnedEvents,
|
|
115
|
+
pinnedLiveIds,
|
|
116
|
+
savedLiveIds,
|
|
117
|
+
togglePin,
|
|
118
|
+
clearPinned
|
|
119
|
+
} = useNetworkSaved();
|
|
120
|
+
const {
|
|
121
|
+
pinned,
|
|
122
|
+
rest
|
|
123
|
+
} = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
124
|
+
|
|
125
|
+
// Stable identity: handed to memoized rows.
|
|
126
|
+
const handleLongPress = useCallback(event => {
|
|
127
|
+
togglePin(event);
|
|
128
|
+
}, [togglePin]);
|
|
58
129
|
if (!visible) return null;
|
|
59
130
|
const isEnabled = !paused;
|
|
60
131
|
const renderItem = ({
|
|
61
132
|
item
|
|
62
133
|
}) => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
|
|
63
134
|
event: item,
|
|
64
|
-
onPress: onEventPress
|
|
135
|
+
onPress: onEventPress,
|
|
136
|
+
onLongPress: handleLongPress,
|
|
137
|
+
saved: savedLiveIds.has(item.id)
|
|
65
138
|
});
|
|
66
139
|
const renderListFooter = () => {
|
|
67
140
|
if (!hasLockedEvents) return null;
|
|
68
141
|
const isSearching = searchText.length > 0;
|
|
69
142
|
const bannerText = isSearching ? `${lockedEventCount} matching ${lockedEventCount === 1 ? "request" : "requests"} in locked history` : `${lockedEventCount} requests locked`;
|
|
70
143
|
const subtextMessage = isSearching ? "Upgrade to Pro to search your full request history" : "Upgrade to Pro to unlock full request history";
|
|
71
|
-
return /*#__PURE__*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
children: [/*#__PURE__*/_jsxs(View, {
|
|
76
|
-
style: styles.limitBannerContent,
|
|
77
|
-
children: [/*#__PURE__*/_jsxs(View, {
|
|
78
|
-
style: styles.limitBannerLeft,
|
|
79
|
-
children: [/*#__PURE__*/_jsx(Zap, {
|
|
80
|
-
size: 16,
|
|
81
|
-
color: "#20C997"
|
|
82
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
83
|
-
style: styles.limitBannerText,
|
|
84
|
-
children: bannerText
|
|
85
|
-
})]
|
|
86
|
-
}), /*#__PURE__*/_jsx(ProBadge, {})]
|
|
87
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
88
|
-
style: styles.limitBannerSubtext,
|
|
89
|
-
children: subtextMessage
|
|
90
|
-
})]
|
|
144
|
+
return /*#__PURE__*/_jsx(NetworkUpgradeBanner, {
|
|
145
|
+
title: bannerText,
|
|
146
|
+
subtitle: subtextMessage,
|
|
147
|
+
onPress: onUpgradePress
|
|
91
148
|
});
|
|
92
149
|
};
|
|
93
|
-
return /*#__PURE__*/_jsxs(
|
|
150
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
151
|
+
style: styles.root,
|
|
94
152
|
children: [!isEnabled ? /*#__PURE__*/_jsxs(View, {
|
|
95
153
|
style: styles.disabledBanner,
|
|
96
154
|
children: [/*#__PURE__*/_jsx(Power, {
|
|
@@ -100,8 +158,14 @@ export function NetworkListScreen({
|
|
|
100
158
|
style: styles.disabledText,
|
|
101
159
|
children: "Network interception is disabled"
|
|
102
160
|
})]
|
|
103
|
-
}) : null,
|
|
104
|
-
|
|
161
|
+
}) : null, /*#__PURE__*/_jsx(PinnedSection, {
|
|
162
|
+
events: pinned,
|
|
163
|
+
onEventPress: onEventPress,
|
|
164
|
+
onLongPress: handleLongPress,
|
|
165
|
+
savedLiveIds: savedLiveIds,
|
|
166
|
+
onClearPinned: clearPinned
|
|
167
|
+
}), rest.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
|
|
168
|
+
data: rest,
|
|
105
169
|
renderItem: renderItem,
|
|
106
170
|
keyExtractor: keyExtractor,
|
|
107
171
|
contentContainerStyle: styles.listContent,
|
|
@@ -121,15 +185,59 @@ export function NetworkListScreen({
|
|
|
121
185
|
initialNumToRender: 12,
|
|
122
186
|
maxToRenderPerBatch: 8,
|
|
123
187
|
windowSize: 5,
|
|
124
|
-
updateCellsBatchingPeriod: 50
|
|
188
|
+
updateCellsBatchingPeriod: 50
|
|
189
|
+
// Saving a request from the detail view changes a row's glyph
|
|
190
|
+
// without changing `data`. VirtualizedList is a PureComponent, so
|
|
191
|
+
// the Set has to be declared as a re-render marker rather than
|
|
192
|
+
// relying on `renderItem` getting a fresh identity.
|
|
193
|
+
,
|
|
194
|
+
extraData: savedLiveIds,
|
|
125
195
|
"sentry-label": "ignore network events list",
|
|
126
196
|
ListFooterComponent: renderListFooter
|
|
127
|
-
}) : /*#__PURE__*/_jsx(EmptyState, {
|
|
197
|
+
}) : pinned.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
|
|
128
198
|
isEnabled: isEnabled
|
|
129
199
|
})]
|
|
130
200
|
});
|
|
131
201
|
}
|
|
132
202
|
const styles = StyleSheet.create({
|
|
203
|
+
root: {
|
|
204
|
+
flex: 1,
|
|
205
|
+
minHeight: 0
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* The strip is separated from the live list by a real rule, not just space:
|
|
209
|
+
* these rows do NOT obey the filters, and the boundary is what tells the
|
|
210
|
+
* user why a request they just filtered out is still on screen.
|
|
211
|
+
*/
|
|
212
|
+
pinnedSection: {
|
|
213
|
+
borderBottomWidth: 1,
|
|
214
|
+
borderBottomColor: macOSColors.border.default,
|
|
215
|
+
paddingBottom: 4
|
|
216
|
+
},
|
|
217
|
+
pinnedHeader: {
|
|
218
|
+
flexDirection: "row",
|
|
219
|
+
alignItems: "center",
|
|
220
|
+
gap: 6,
|
|
221
|
+
paddingHorizontal: 20,
|
|
222
|
+
paddingTop: 10,
|
|
223
|
+
paddingBottom: 6
|
|
224
|
+
},
|
|
225
|
+
pinnedTitle: {
|
|
226
|
+
flex: 1,
|
|
227
|
+
color: macOSColors.semantic.info,
|
|
228
|
+
fontSize: 10,
|
|
229
|
+
fontWeight: "700",
|
|
230
|
+
letterSpacing: 0.6,
|
|
231
|
+
fontFamily: "monospace"
|
|
232
|
+
},
|
|
233
|
+
pinnedClear: {
|
|
234
|
+
paddingHorizontal: 6,
|
|
235
|
+
paddingVertical: 2
|
|
236
|
+
},
|
|
237
|
+
pinnedClearText: {
|
|
238
|
+
color: macOSColors.text.muted,
|
|
239
|
+
fontSize: 10
|
|
240
|
+
},
|
|
133
241
|
disabledBanner: {
|
|
134
242
|
flexDirection: "row",
|
|
135
243
|
alignItems: "center",
|
|
@@ -165,35 +273,5 @@ const styles = StyleSheet.create({
|
|
|
165
273
|
color: macOSColors.text.muted,
|
|
166
274
|
fontSize: 12,
|
|
167
275
|
textAlign: "center"
|
|
168
|
-
},
|
|
169
|
-
limitBanner: {
|
|
170
|
-
backgroundColor: "#1A1A1A",
|
|
171
|
-
borderRadius: 10,
|
|
172
|
-
borderWidth: 1,
|
|
173
|
-
borderColor: "#20C997" + "40",
|
|
174
|
-
padding: 12,
|
|
175
|
-
marginHorizontal: 12,
|
|
176
|
-
marginTop: 12,
|
|
177
|
-
marginBottom: 34
|
|
178
|
-
},
|
|
179
|
-
limitBannerContent: {
|
|
180
|
-
flexDirection: "row",
|
|
181
|
-
alignItems: "center",
|
|
182
|
-
justifyContent: "space-between",
|
|
183
|
-
marginBottom: 4
|
|
184
|
-
},
|
|
185
|
-
limitBannerLeft: {
|
|
186
|
-
flexDirection: "row",
|
|
187
|
-
alignItems: "center",
|
|
188
|
-
gap: 8
|
|
189
|
-
},
|
|
190
|
-
limitBannerText: {
|
|
191
|
-
color: "#E0E0E0",
|
|
192
|
-
fontSize: 13,
|
|
193
|
-
fontWeight: "600"
|
|
194
|
-
},
|
|
195
|
-
limitBannerSubtext: {
|
|
196
|
-
color: "#888888",
|
|
197
|
-
fontSize: 11
|
|
198
276
|
}
|
|
199
277
|
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useState, useRef, useEffect } from "react";
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
4
|
import { View, StyleSheet, TouchableOpacity, TextInput } from "react-native";
|
|
5
5
|
import { Search, Filter, X, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, persistentStorage, TabSelector, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns } from "@buoy-gg/shared-ui";
|
|
6
6
|
import { NetworkFilterViewV3 } from "./NetworkFilterViewV3";
|
|
7
7
|
import { TickProvider } from "../hooks/useTickEveryMinute";
|
|
8
8
|
import { NetworkEventDetailView } from "./NetworkEventDetailView";
|
|
9
9
|
import { NetworkCopySettingsView, DEFAULT_COPY_SETTINGS } from "./NetworkCopySettingsView";
|
|
10
|
-
import { NetworkHeaderBadges, NetworkClearButton, NetworkHeaderCopyButton } from "./NetworkHeaderLive";
|
|
10
|
+
import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton, NetworkSavedButton } from "./NetworkHeaderLive";
|
|
11
11
|
import { NetworkListScreen } from "./NetworkListScreen";
|
|
12
|
+
import { NetworkSavedScreen } from "./NetworkSavedScreen";
|
|
12
13
|
import { useNetworkEventList } from "../hooks/useNetworkEventList";
|
|
13
|
-
import {
|
|
14
|
+
import { useLiveNetworkEvent } from "../hooks/useLiveNetworkEvent";
|
|
15
|
+
import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
|
|
16
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
14
17
|
// ============================================================================
|
|
15
18
|
// Screens (rule4 stable router: each gates itself with `return null`; the
|
|
16
19
|
// root renders all four as stable siblings instead of a nested ternary).
|
|
@@ -20,10 +23,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
23
|
|
|
21
24
|
function NetworkDetailScreen({
|
|
22
25
|
visible,
|
|
23
|
-
|
|
26
|
+
eventId,
|
|
27
|
+
paused,
|
|
24
28
|
ignoredPatternValues,
|
|
25
29
|
onTogglePattern
|
|
26
30
|
}) {
|
|
31
|
+
// Resolve the ID to the live event so a request opened while pending picks
|
|
32
|
+
// up its response/error in place instead of freezing on the snapshot taken
|
|
33
|
+
// when the row was tapped.
|
|
34
|
+
const event = useLiveNetworkEvent(eventId, {
|
|
35
|
+
paused: paused || !visible
|
|
36
|
+
});
|
|
27
37
|
if (!visible || !event) return null;
|
|
28
38
|
return /*#__PURE__*/_jsx(NetworkEventDetailView, {
|
|
29
39
|
event: event,
|
|
@@ -101,18 +111,31 @@ function NetworkModalInner({
|
|
|
101
111
|
onClose,
|
|
102
112
|
onBack,
|
|
103
113
|
onMinimize,
|
|
104
|
-
enableSharedModalDimensions = false
|
|
114
|
+
enableSharedModalDimensions = false,
|
|
115
|
+
inline = false,
|
|
116
|
+
paused: pausedProp,
|
|
117
|
+
onPausedChange
|
|
105
118
|
}) {
|
|
106
119
|
const {
|
|
107
120
|
isPro
|
|
108
121
|
} = useFeatureGate();
|
|
109
122
|
|
|
110
123
|
// Capture pause (the power toggle's freeze-stream semantic) — gates
|
|
111
|
-
// every subscriber in this modal's tree via props.
|
|
112
|
-
|
|
124
|
+
// every subscriber in this modal's tree via props. Controlled when the host
|
|
125
|
+
// supplies both halves; otherwise this local state is the source of truth.
|
|
126
|
+
const [pausedState, setPausedState] = useState(false);
|
|
127
|
+
const controlledPause = pausedProp != null && onPausedChange != null;
|
|
128
|
+
const paused = controlledPause ? pausedProp : pausedState;
|
|
129
|
+
const setPaused = next => {
|
|
130
|
+
const value = typeof next === "function" ? next(paused) : next;
|
|
131
|
+
if (controlledPause) onPausedChange(value);else setPausedState(value);
|
|
132
|
+
};
|
|
113
133
|
const [filter, setFilter] = useState({});
|
|
114
|
-
|
|
134
|
+
// The SELECTION is UI state (an ID); the event object it points at is read
|
|
135
|
+
// live from the store by the detail screen — see useLiveNetworkEvent.
|
|
136
|
+
const [selectedEventId, setSelectedEventId] = useState(null);
|
|
115
137
|
const [showFilterView, setShowFilterView] = useState(false);
|
|
138
|
+
const [showSavedView, setShowSavedView] = useState(false);
|
|
116
139
|
const [activeTab, setActiveTab] = useState("filters");
|
|
117
140
|
const [searchText, setSearchText] = useState("");
|
|
118
141
|
const [isSearchActive, setIsSearchActive] = useState(false);
|
|
@@ -130,6 +153,13 @@ function NetworkModalInner({
|
|
|
130
153
|
const [copySettings, setCopySettings] = useState(DEFAULT_COPY_SETTINGS);
|
|
131
154
|
const hasLoadedCopySettings = useRef(false);
|
|
132
155
|
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
|
156
|
+
// Which ceiling the user hit — the upgrade sheet has to argue for the thing
|
|
157
|
+
// they were actually trying to do, not for exporting.
|
|
158
|
+
const [upgradeReason, setUpgradeReason] = useState("export");
|
|
159
|
+
const showUpgrade = reason => {
|
|
160
|
+
setUpgradeReason(reason);
|
|
161
|
+
setShowUpgradeModal(true);
|
|
162
|
+
};
|
|
133
163
|
|
|
134
164
|
// Auto-close upgrade modal when user becomes Pro
|
|
135
165
|
useEffect(() => {
|
|
@@ -173,8 +203,13 @@ function NetworkModalInner({
|
|
|
173
203
|
|
|
174
204
|
// Simple handlers - no useCallback needed per rule2
|
|
175
205
|
const handleBack = () => {
|
|
176
|
-
|
|
206
|
+
setSelectedEventId(null);
|
|
177
207
|
};
|
|
208
|
+
|
|
209
|
+
// Stable identity: this is passed down to the memoized list rows.
|
|
210
|
+
const handleEventPress = useCallback(event => {
|
|
211
|
+
setSelectedEventId(event.id);
|
|
212
|
+
}, []);
|
|
178
213
|
const handleSearch = text => {
|
|
179
214
|
setSearchText(text);
|
|
180
215
|
setFilter(prev => ({
|
|
@@ -226,13 +261,32 @@ function NetworkModalInner({
|
|
|
226
261
|
}
|
|
227
262
|
|
|
228
263
|
// Event detail view header
|
|
229
|
-
if (
|
|
264
|
+
if (selectedEventId) {
|
|
230
265
|
return /*#__PURE__*/_jsxs(ModalHeader, {
|
|
231
266
|
children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
|
|
232
267
|
onBack: handleBack
|
|
233
268
|
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
234
269
|
title: "Request Details",
|
|
235
270
|
centered: true
|
|
271
|
+
}), /*#__PURE__*/_jsx(ModalHeader.Actions, {
|
|
272
|
+
children: /*#__PURE__*/_jsx(NetworkDetailPinActions, {
|
|
273
|
+
eventId: selectedEventId,
|
|
274
|
+
isPro: isPro,
|
|
275
|
+
onUpgradePress: () => showUpgrade("saved")
|
|
276
|
+
})
|
|
277
|
+
})]
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Saved (favorites) header. Checked AFTER the detail branch so opening a
|
|
282
|
+
// saved request still gets the detail header — and its pin/save toggles.
|
|
283
|
+
if (showSavedView) {
|
|
284
|
+
return /*#__PURE__*/_jsxs(ModalHeader, {
|
|
285
|
+
children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
|
|
286
|
+
onBack: () => setShowSavedView(false)
|
|
287
|
+
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
288
|
+
title: "Saved Requests",
|
|
289
|
+
centered: true
|
|
236
290
|
})]
|
|
237
291
|
});
|
|
238
292
|
}
|
|
@@ -301,13 +355,15 @@ function NetworkModalInner({
|
|
|
301
355
|
size: 14,
|
|
302
356
|
color: filter.status || filter.method || filter.contentType ? macOSColors.semantic.info : macOSColors.text.muted
|
|
303
357
|
})
|
|
358
|
+
}), /*#__PURE__*/_jsx(NetworkSavedButton, {
|
|
359
|
+
onPress: () => setShowSavedView(true)
|
|
304
360
|
}), /*#__PURE__*/_jsx(NetworkHeaderCopyButton, {
|
|
305
361
|
isPro: isPro,
|
|
306
362
|
filter: filter,
|
|
307
363
|
ignoredPatterns: ignoredPatterns,
|
|
308
364
|
copySettings: copySettings,
|
|
309
365
|
paused: paused,
|
|
310
|
-
onUpgradePress: () =>
|
|
366
|
+
onUpgradePress: () => showUpgrade("export")
|
|
311
367
|
}), /*#__PURE__*/_jsx(PowerToggleButton, {
|
|
312
368
|
isEnabled: !paused,
|
|
313
369
|
onToggle: () => setPaused(prev => !prev),
|
|
@@ -324,32 +380,20 @@ function NetworkModalInner({
|
|
|
324
380
|
// The LIST screen owns its scrolling (FlatList virtualization requires
|
|
325
381
|
// it — inside JsModal's ScrollView wrapper every row mounts, which at
|
|
326
382
|
// the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
|
|
327
|
-
// NetworkListScreen).
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
onClose: onClose,
|
|
332
|
-
onMinimize: onMinimize,
|
|
333
|
-
persistenceKey: persistenceKey,
|
|
334
|
-
header: {
|
|
335
|
-
showToggleButton: true,
|
|
336
|
-
customContent: renderHeaderContent()
|
|
337
|
-
},
|
|
338
|
-
onModeChange: handleModeChange,
|
|
339
|
-
enablePersistence: true,
|
|
340
|
-
initialMode: "bottomSheet",
|
|
341
|
-
enableGlitchEffects: true,
|
|
342
|
-
disableScrollWrapper: listOwnsScroll,
|
|
343
|
-
styles: {},
|
|
383
|
+
// NetworkListScreen). The Saved screen is a FlatList too, so it takes the
|
|
384
|
+
// same deal. Detail/filter/copy still use the wrapper.
|
|
385
|
+
const listOwnsScroll = selectedEventId == null && !showFilterView;
|
|
386
|
+
const body = /*#__PURE__*/_jsxs(_Fragment, {
|
|
344
387
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
345
388
|
style: styles.container,
|
|
346
389
|
children: [/*#__PURE__*/_jsx(NetworkDetailScreen, {
|
|
347
|
-
visible:
|
|
348
|
-
|
|
390
|
+
visible: selectedEventId != null,
|
|
391
|
+
eventId: selectedEventId,
|
|
392
|
+
paused: paused,
|
|
349
393
|
ignoredPatternValues: ignoredPatternValues,
|
|
350
394
|
onTogglePattern: toggleIgnoredPattern
|
|
351
395
|
}), /*#__PURE__*/_jsx(NetworkFilterScreen, {
|
|
352
|
-
visible: showFilterView && activeTab === "filters" && !
|
|
396
|
+
visible: showFilterView && activeTab === "filters" && !selectedEventId,
|
|
353
397
|
paused: paused,
|
|
354
398
|
isPro: isPro,
|
|
355
399
|
filter: filter,
|
|
@@ -359,37 +403,86 @@ function NetworkModalInner({
|
|
|
359
403
|
onAddPattern: addIgnoredPattern,
|
|
360
404
|
onTogglePatternMode: toggleIgnoredPatternMode
|
|
361
405
|
}), /*#__PURE__*/_jsx(NetworkCopyScreen, {
|
|
362
|
-
visible: showFilterView && activeTab === "copy" && !
|
|
406
|
+
visible: showFilterView && activeTab === "copy" && !selectedEventId,
|
|
363
407
|
paused: paused,
|
|
364
408
|
isPro: isPro,
|
|
365
409
|
filter: filter,
|
|
366
410
|
ignoredPatterns: ignoredPatterns,
|
|
367
411
|
settings: copySettings,
|
|
368
412
|
onSettingsChange: setCopySettings
|
|
413
|
+
}), /*#__PURE__*/_jsx(NetworkSavedScreen, {
|
|
414
|
+
visible: showSavedView && selectedEventId == null && !showFilterView,
|
|
415
|
+
isPro: isPro,
|
|
416
|
+
copySettings: copySettings,
|
|
417
|
+
onEventPress: handleEventPress,
|
|
418
|
+
onUpgradePress: () => showUpgrade("saved")
|
|
369
419
|
}), /*#__PURE__*/_jsx(NetworkListScreen, {
|
|
370
|
-
visible:
|
|
420
|
+
visible: selectedEventId == null && !showFilterView && !showSavedView,
|
|
371
421
|
paused: paused,
|
|
372
422
|
filter: filter,
|
|
373
423
|
ignoredPatterns: ignoredPatterns,
|
|
374
424
|
isPro: isPro,
|
|
375
425
|
searchText: searchText,
|
|
376
|
-
onEventPress:
|
|
377
|
-
onUpgradePress: () =>
|
|
426
|
+
onEventPress: handleEventPress,
|
|
427
|
+
onUpgradePress: () => showUpgrade("export")
|
|
378
428
|
})]
|
|
379
429
|
}), /*#__PURE__*/_jsx(UpgradeModal, {
|
|
380
430
|
visible: showUpgradeModal,
|
|
381
431
|
onClose: () => setShowUpgradeModal(false),
|
|
382
|
-
featureName: "Network Export",
|
|
383
|
-
description: "Export network requests to share with your team, debug APIs, or feed into AI tools.",
|
|
384
|
-
benefits: ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
|
|
432
|
+
featureName: upgradeReason === "saved" ? "Saved Requests" : "Network Export",
|
|
433
|
+
description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
|
|
434
|
+
benefits: upgradeReason === "saved" ? [`Save up to ${MAX_SAVED} requests (free keeps ${FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
|
|
385
435
|
})]
|
|
386
436
|
});
|
|
437
|
+
|
|
438
|
+
// The ONLY thing that differs between the phone and the desktop dashboard is
|
|
439
|
+
// which shell the header and body go in. Everything above this line is shared.
|
|
440
|
+
if (inline) {
|
|
441
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
442
|
+
style: styles.inlineRoot,
|
|
443
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
444
|
+
style: styles.inlineHeader,
|
|
445
|
+
children: renderHeaderContent()
|
|
446
|
+
}), body]
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return /*#__PURE__*/_jsx(JsModal, {
|
|
450
|
+
visible: visible,
|
|
451
|
+
onClose: onClose,
|
|
452
|
+
onMinimize: onMinimize,
|
|
453
|
+
persistenceKey: persistenceKey,
|
|
454
|
+
header: {
|
|
455
|
+
showToggleButton: true,
|
|
456
|
+
customContent: renderHeaderContent()
|
|
457
|
+
},
|
|
458
|
+
onModeChange: handleModeChange,
|
|
459
|
+
enablePersistence: true,
|
|
460
|
+
initialMode: "bottomSheet",
|
|
461
|
+
enableGlitchEffects: true,
|
|
462
|
+
disableScrollWrapper: listOwnsScroll,
|
|
463
|
+
styles: {},
|
|
464
|
+
children: body
|
|
465
|
+
});
|
|
387
466
|
}
|
|
388
467
|
const styles = StyleSheet.create({
|
|
389
468
|
container: {
|
|
390
469
|
flex: 1,
|
|
391
470
|
backgroundColor: macOSColors.background.base
|
|
392
471
|
},
|
|
472
|
+
/** Inline shell — fills the host panel; `minHeight: 0` so the list's own
|
|
473
|
+
* FlatList can scroll instead of stretching the column. */
|
|
474
|
+
inlineRoot: {
|
|
475
|
+
flex: 1,
|
|
476
|
+
minHeight: 0,
|
|
477
|
+
backgroundColor: macOSColors.background.base
|
|
478
|
+
},
|
|
479
|
+
inlineHeader: {
|
|
480
|
+
paddingHorizontal: 12,
|
|
481
|
+
paddingVertical: 8,
|
|
482
|
+
borderBottomWidth: 1,
|
|
483
|
+
borderBottomColor: macOSColors.border.default,
|
|
484
|
+
backgroundColor: macOSColors.background.card
|
|
485
|
+
},
|
|
393
486
|
headerSearchContainer: {
|
|
394
487
|
flexDirection: "row",
|
|
395
488
|
alignItems: "center",
|
|
@@ -437,4 +530,28 @@ export function NetworkModal(props) {
|
|
|
437
530
|
...props
|
|
438
531
|
})
|
|
439
532
|
});
|
|
440
|
-
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The same network tool as `NetworkModal`, rendered INLINE — header bar, then
|
|
536
|
+
* body — for hosts that place tools in a panel rather than a modal. Buoy Desktop
|
|
537
|
+
* is the only one today.
|
|
538
|
+
*
|
|
539
|
+
* This exists so there is exactly ONE network tool. The desktop dashboard used
|
|
540
|
+
* to ship its own 1,258-line copy of the modal's internals, which is how the two
|
|
541
|
+
* surfaces drifted apart in the first place; anything added here now shows up on
|
|
542
|
+
* both without a second edit.
|
|
543
|
+
*/
|
|
544
|
+
export function NetworkToolView(props = {}) {
|
|
545
|
+
return /*#__PURE__*/_jsx(TickProvider, {
|
|
546
|
+
children: /*#__PURE__*/_jsx(NetworkModalInner, {
|
|
547
|
+
...props,
|
|
548
|
+
inline: true,
|
|
549
|
+
visible: true
|
|
550
|
+
// Inline has no chrome to drive: there is no close button, no minimize
|
|
551
|
+
// and no outer back — the host panel owns all of that.
|
|
552
|
+
,
|
|
553
|
+
onClose: noop
|
|
554
|
+
})
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const noop = () => {};
|