@buoy-gg/network 7.0.3 → 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 +3 -2
- 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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The SAVED screen — the network tool's favorites list.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is read from the persisted saved store, never from the live
|
|
7
|
+
* event store. That is the whole point of the screen: these requests survive a
|
|
8
|
+
* Clear, the 500-event cap, and an app restart, so a failure you kept is still
|
|
9
|
+
* here tomorrow.
|
|
10
|
+
*
|
|
11
|
+
* Rows are the same two row components the live list uses, so a saved request
|
|
12
|
+
* looks exactly like the request it was. Long-press here UNSAVES rather than
|
|
13
|
+
* pins — in this list "get rid of it" is the action the row affords, and
|
|
14
|
+
* pinning is still available from the detail header.
|
|
15
|
+
*
|
|
16
|
+
* Stable-tree contract (rule4): hooks run unconditionally, then the component
|
|
17
|
+
* returns null when not visible.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { FlatList, StyleSheet, Text, TextInput, View, TouchableOpacity } from "react-native";
|
|
22
|
+
import { Bookmark, CopyButton, Search, macOSColors } from "@buoy-gg/shared-ui";
|
|
23
|
+
import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
|
|
24
|
+
import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
|
|
25
|
+
import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
|
|
26
|
+
import { filterNetworkEvents } from "../utils/filterNetworkEvents";
|
|
27
|
+
import { useNetworkSaved } from "../hooks/useNetworkSaved";
|
|
28
|
+
import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
|
|
29
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
30
|
+
const keyExtractor = item => item.id;
|
|
31
|
+
function EmptyState() {
|
|
32
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
33
|
+
style: styles.emptyState,
|
|
34
|
+
children: [/*#__PURE__*/_jsx(Bookmark, {
|
|
35
|
+
size: 32,
|
|
36
|
+
color: macOSColors.text.muted
|
|
37
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
38
|
+
style: styles.emptyTitle,
|
|
39
|
+
children: "No saved requests"
|
|
40
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
41
|
+
style: styles.emptyText,
|
|
42
|
+
children: "Open a request and tap the bookmark to keep it here \u2014 saved requests survive Clear and app restarts."
|
|
43
|
+
})]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function NetworkSavedScreen({
|
|
47
|
+
visible,
|
|
48
|
+
isPro,
|
|
49
|
+
copySettings,
|
|
50
|
+
onEventPress,
|
|
51
|
+
onUpgradePress
|
|
52
|
+
}) {
|
|
53
|
+
const {
|
|
54
|
+
savedRecords,
|
|
55
|
+
pinnedLiveIds,
|
|
56
|
+
toggleSave,
|
|
57
|
+
clearSaved
|
|
58
|
+
} = useNetworkSaved();
|
|
59
|
+
const [search, setSearch] = useState("");
|
|
60
|
+
if (!visible) return null;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A downgrade (or a limit change) can leave a free user holding more saves
|
|
64
|
+
* than the free tier allows. Rather than deleting their data, the overflow
|
|
65
|
+
* is LOCKED — oldest first, so the newest saves stay reachable — and the
|
|
66
|
+
* banner offers the upgrade that unlocks it.
|
|
67
|
+
*/
|
|
68
|
+
const limit = isPro ? MAX_SAVED : FREE_TIER_SAVED_LIMIT;
|
|
69
|
+
const unlocked = savedRecords.slice(0, limit);
|
|
70
|
+
const lockedCount = savedRecords.length - unlocked.length;
|
|
71
|
+
const events = unlocked.map(record => record.event);
|
|
72
|
+
const filtered = search ? filterNetworkEvents(events, {
|
|
73
|
+
searchText: search
|
|
74
|
+
}) : events;
|
|
75
|
+
const handleLongPress = event => {
|
|
76
|
+
toggleSave(event);
|
|
77
|
+
};
|
|
78
|
+
const renderItem = ({
|
|
79
|
+
item
|
|
80
|
+
}) => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
|
|
81
|
+
event: item,
|
|
82
|
+
onPress: onEventPress,
|
|
83
|
+
onLongPress: handleLongPress,
|
|
84
|
+
saved: true,
|
|
85
|
+
pinned: pinnedLiveIds.has(item.id)
|
|
86
|
+
});
|
|
87
|
+
const renderListFooter = () => {
|
|
88
|
+
if (lockedCount === 0) return null;
|
|
89
|
+
return /*#__PURE__*/_jsx(NetworkUpgradeBanner, {
|
|
90
|
+
title: `${lockedCount} saved ${lockedCount === 1 ? "request" : "requests"} locked`,
|
|
91
|
+
subtitle: `Free keeps ${FREE_TIER_SAVED_LIMIT}. Upgrade to Pro to unlock the rest.`,
|
|
92
|
+
onPress: onUpgradePress
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
96
|
+
style: styles.root,
|
|
97
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
98
|
+
style: styles.toolbar,
|
|
99
|
+
children: [/*#__PURE__*/_jsx(Bookmark, {
|
|
100
|
+
size: 11,
|
|
101
|
+
color: macOSColors.semantic.debug
|
|
102
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
103
|
+
style: styles.toolbarTitle,
|
|
104
|
+
children: ["SAVED \xB7 ", savedRecords.length, isPro ? "" : `/${FREE_TIER_SAVED_LIMIT}`]
|
|
105
|
+
}), isPro ? /*#__PURE__*/_jsx(CopyButton
|
|
106
|
+
// Lazy value, resolved at press time — no string is built per
|
|
107
|
+
// render, and it always reflects the current list.
|
|
108
|
+
, {
|
|
109
|
+
value: () => generateNetworkCopyText(filtered, copySettings),
|
|
110
|
+
size: 13,
|
|
111
|
+
buttonStyle: styles.toolbarButton,
|
|
112
|
+
colors: {
|
|
113
|
+
idle: macOSColors.text.secondary,
|
|
114
|
+
success: macOSColors.semantic.success,
|
|
115
|
+
error: macOSColors.semantic.error
|
|
116
|
+
},
|
|
117
|
+
disabled: filtered.length === 0
|
|
118
|
+
}) : null, /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
119
|
+
onPress: clearSaved,
|
|
120
|
+
"sentry-label": "ignore clear saved",
|
|
121
|
+
style: styles.toolbarClear,
|
|
122
|
+
disabled: savedRecords.length === 0,
|
|
123
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
124
|
+
style: [styles.toolbarClearText, savedRecords.length === 0 && styles.toolbarClearTextDisabled],
|
|
125
|
+
children: "Clear all"
|
|
126
|
+
})
|
|
127
|
+
})]
|
|
128
|
+
}), savedRecords.length > 0 ? /*#__PURE__*/_jsxs(View, {
|
|
129
|
+
style: styles.searchRow,
|
|
130
|
+
children: [/*#__PURE__*/_jsx(Search, {
|
|
131
|
+
size: 13,
|
|
132
|
+
color: macOSColors.text.secondary
|
|
133
|
+
}), /*#__PURE__*/_jsx(TextInput, {
|
|
134
|
+
style: styles.searchInput,
|
|
135
|
+
placeholder: "Search saved requests\u2026",
|
|
136
|
+
placeholderTextColor: macOSColors.text.muted,
|
|
137
|
+
value: search,
|
|
138
|
+
onChangeText: setSearch,
|
|
139
|
+
"sentry-label": "ignore saved search",
|
|
140
|
+
accessibilityLabel: "Search saved requests",
|
|
141
|
+
autoCapitalize: "none",
|
|
142
|
+
autoCorrect: false,
|
|
143
|
+
returnKeyType: "search"
|
|
144
|
+
})]
|
|
145
|
+
}) : null, savedRecords.length === 0 ? /*#__PURE__*/_jsx(EmptyState, {}) : /*#__PURE__*/_jsx(FlatList, {
|
|
146
|
+
data: filtered,
|
|
147
|
+
renderItem: renderItem,
|
|
148
|
+
keyExtractor: keyExtractor,
|
|
149
|
+
contentContainerStyle: styles.listContent,
|
|
150
|
+
showsVerticalScrollIndicator: true,
|
|
151
|
+
removeClippedSubviews: true,
|
|
152
|
+
scrollEnabled: true,
|
|
153
|
+
initialNumToRender: 12,
|
|
154
|
+
maxToRenderPerBatch: 8,
|
|
155
|
+
windowSize: 5,
|
|
156
|
+
extraData: pinnedLiveIds,
|
|
157
|
+
"sentry-label": "ignore saved requests list",
|
|
158
|
+
ListFooterComponent: renderListFooter
|
|
159
|
+
})]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const styles = StyleSheet.create({
|
|
163
|
+
root: {
|
|
164
|
+
flex: 1,
|
|
165
|
+
minHeight: 0
|
|
166
|
+
},
|
|
167
|
+
toolbar: {
|
|
168
|
+
flexDirection: "row",
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
gap: 6,
|
|
171
|
+
paddingHorizontal: 20,
|
|
172
|
+
paddingTop: 10,
|
|
173
|
+
paddingBottom: 6
|
|
174
|
+
},
|
|
175
|
+
toolbarTitle: {
|
|
176
|
+
flex: 1,
|
|
177
|
+
color: macOSColors.semantic.debug,
|
|
178
|
+
fontSize: 10,
|
|
179
|
+
fontWeight: "700",
|
|
180
|
+
letterSpacing: 0.6,
|
|
181
|
+
fontFamily: "monospace"
|
|
182
|
+
},
|
|
183
|
+
toolbarButton: {
|
|
184
|
+
width: 26,
|
|
185
|
+
height: 26,
|
|
186
|
+
borderRadius: 6,
|
|
187
|
+
alignItems: "center",
|
|
188
|
+
justifyContent: "center"
|
|
189
|
+
},
|
|
190
|
+
toolbarClear: {
|
|
191
|
+
paddingHorizontal: 6,
|
|
192
|
+
paddingVertical: 2
|
|
193
|
+
},
|
|
194
|
+
toolbarClearText: {
|
|
195
|
+
color: macOSColors.text.muted,
|
|
196
|
+
fontSize: 10
|
|
197
|
+
},
|
|
198
|
+
toolbarClearTextDisabled: {
|
|
199
|
+
color: macOSColors.text.disabled
|
|
200
|
+
},
|
|
201
|
+
searchRow: {
|
|
202
|
+
flexDirection: "row",
|
|
203
|
+
alignItems: "center",
|
|
204
|
+
gap: 6,
|
|
205
|
+
marginHorizontal: 12,
|
|
206
|
+
marginBottom: 8,
|
|
207
|
+
paddingHorizontal: 10,
|
|
208
|
+
paddingVertical: 5,
|
|
209
|
+
backgroundColor: macOSColors.background.input,
|
|
210
|
+
borderRadius: 10,
|
|
211
|
+
borderWidth: 1,
|
|
212
|
+
borderColor: macOSColors.border.default
|
|
213
|
+
},
|
|
214
|
+
searchInput: {
|
|
215
|
+
flex: 1,
|
|
216
|
+
color: macOSColors.text.primary,
|
|
217
|
+
fontSize: 13,
|
|
218
|
+
paddingVertical: 2
|
|
219
|
+
},
|
|
220
|
+
listContent: {
|
|
221
|
+
paddingTop: 4
|
|
222
|
+
},
|
|
223
|
+
emptyState: {
|
|
224
|
+
alignItems: "center",
|
|
225
|
+
paddingVertical: 40,
|
|
226
|
+
paddingHorizontal: 24
|
|
227
|
+
},
|
|
228
|
+
emptyTitle: {
|
|
229
|
+
color: macOSColors.text.primary,
|
|
230
|
+
fontSize: 14,
|
|
231
|
+
fontWeight: "600",
|
|
232
|
+
marginTop: 12,
|
|
233
|
+
marginBottom: 6
|
|
234
|
+
},
|
|
235
|
+
emptyText: {
|
|
236
|
+
color: macOSColors.text.muted,
|
|
237
|
+
fontSize: 12,
|
|
238
|
+
textAlign: "center",
|
|
239
|
+
lineHeight: 17
|
|
240
|
+
}
|
|
241
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The "N things are locked, upgrade to see them" banner.
|
|
5
|
+
*
|
|
6
|
+
* Extracted because there are now two of them — locked request history on the
|
|
7
|
+
* live list, and locked saves on the Saved screen — and they are the same
|
|
8
|
+
* offer. Two copies would drift the moment one of them gets a copy tweak.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
12
|
+
import { ProBadge, Zap, buoyColors } from "@buoy-gg/shared-ui";
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
export function NetworkUpgradeBanner({
|
|
15
|
+
title,
|
|
16
|
+
subtitle,
|
|
17
|
+
onPress
|
|
18
|
+
}) {
|
|
19
|
+
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
20
|
+
style: styles.banner,
|
|
21
|
+
onPress: onPress,
|
|
22
|
+
activeOpacity: 0.8,
|
|
23
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
24
|
+
style: styles.content,
|
|
25
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
26
|
+
style: styles.left,
|
|
27
|
+
children: [/*#__PURE__*/_jsx(Zap, {
|
|
28
|
+
size: 16,
|
|
29
|
+
color: buoyColors.primary
|
|
30
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
31
|
+
style: styles.title,
|
|
32
|
+
children: title
|
|
33
|
+
})]
|
|
34
|
+
}), /*#__PURE__*/_jsx(ProBadge, {})]
|
|
35
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
36
|
+
style: styles.subtitle,
|
|
37
|
+
children: subtitle
|
|
38
|
+
})]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
banner: {
|
|
43
|
+
backgroundColor: buoyColors.card,
|
|
44
|
+
borderRadius: 10,
|
|
45
|
+
borderWidth: 1,
|
|
46
|
+
borderColor: buoyColors.primary + "40",
|
|
47
|
+
padding: 12,
|
|
48
|
+
marginHorizontal: 12,
|
|
49
|
+
marginTop: 12,
|
|
50
|
+
marginBottom: 34
|
|
51
|
+
},
|
|
52
|
+
content: {
|
|
53
|
+
flexDirection: "row",
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "space-between",
|
|
56
|
+
marginBottom: 4
|
|
57
|
+
},
|
|
58
|
+
left: {
|
|
59
|
+
flexDirection: "row",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
gap: 8
|
|
62
|
+
},
|
|
63
|
+
title: {
|
|
64
|
+
color: buoyColors.text,
|
|
65
|
+
fontSize: 13,
|
|
66
|
+
fontWeight: "600"
|
|
67
|
+
},
|
|
68
|
+
subtitle: {
|
|
69
|
+
color: buoyColors.textMuted,
|
|
70
|
+
fontSize: 11
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a selected event ID to the LIVE event object in the store.
|
|
5
|
+
*
|
|
6
|
+
* The detail view used to be handed the `NetworkEvent` captured at press
|
|
7
|
+
* time. The store updates immutably — a response/error replaces the pending
|
|
8
|
+
* event with a NEW object — so a request opened while pending stayed
|
|
9
|
+
* "Pending" for as long as the detail view was open, and only showed its
|
|
10
|
+
* real status after going back and re-opening the row (which re-read the
|
|
11
|
+
* store). This hook closes that gap: callers keep the ID as UI state and the
|
|
12
|
+
* leaf that renders the detail subscribes for its current value.
|
|
13
|
+
*
|
|
14
|
+
* Subscribing here (not at the modal root) keeps the event stream out of the
|
|
15
|
+
* shell's render path — same rule the list/header leaves follow.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { networkEventStore } from "../utils/networkEventStore";
|
|
20
|
+
import { networkSavedStore } from "../utils/networkSavedStore";
|
|
21
|
+
/**
|
|
22
|
+
* Live store first, then the pinned/saved snapshots. The fallback is what
|
|
23
|
+
* makes a pinned request openable after a Clear, after it aged past the
|
|
24
|
+
* 500-event cap, or in a later app launch — cases where the live store has
|
|
25
|
+
* nothing and the persisted snapshot is the only copy left.
|
|
26
|
+
*/
|
|
27
|
+
function readEvent(eventId) {
|
|
28
|
+
return networkEventStore.getEvents().find(e => e.id === eventId) ?? networkSavedStore.getEventById(eventId);
|
|
29
|
+
}
|
|
30
|
+
export function useLiveNetworkEvent(eventId, options = {}) {
|
|
31
|
+
const {
|
|
32
|
+
paused = false
|
|
33
|
+
} = options;
|
|
34
|
+
const [entry, setEntry] = useState(null);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// `paused` is the power toggle's freeze-stream semantic: a frozen tool
|
|
37
|
+
// keeps showing the version it froze on.
|
|
38
|
+
if (!eventId || paused) return undefined;
|
|
39
|
+
// subscribeToEvents replays the current array immediately, so the open
|
|
40
|
+
// detail catches up on subscribe as well as on every later update.
|
|
41
|
+
return networkEventStore.subscribeToEvents(events => {
|
|
42
|
+
const next = events.find(e => e.id === eventId);
|
|
43
|
+
// Gone from the store (cleared, or aged past maxEvents) — keep the last
|
|
44
|
+
// known version rather than blanking an open detail view.
|
|
45
|
+
if (!next) return;
|
|
46
|
+
setEntry(prev => prev && prev.id === eventId && prev.event === next ? prev : {
|
|
47
|
+
id: eventId,
|
|
48
|
+
event: next
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}, [eventId, paused]);
|
|
52
|
+
if (!eventId) return null;
|
|
53
|
+
// A store read during render covers the frame where `eventId` just changed:
|
|
54
|
+
// the effect's first emission lands a commit later, and until then `entry`
|
|
55
|
+
// still describes the previously selected request.
|
|
56
|
+
if (entry && entry.id === eventId) return entry.event;
|
|
57
|
+
return readEvent(eventId);
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow subscription to the pinned/saved store.
|
|
5
|
+
*
|
|
6
|
+
* The store rebuilds its derived view once per mutation, so the object this
|
|
7
|
+
* returns keeps a stable identity between pins — memoized rows don't re-render
|
|
8
|
+
* because a network event arrived, only because the user pinned something.
|
|
9
|
+
*
|
|
10
|
+
* Mutations happen through module-scope callbacks (stable identities), so
|
|
11
|
+
* passing them into memoized rows costs nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { networkSavedStore } from "../utils/networkSavedStore";
|
|
16
|
+
const togglePin = event => networkSavedStore.togglePin(event);
|
|
17
|
+
const toggleSave = event => networkSavedStore.toggleSave(event);
|
|
18
|
+
const remove = key => networkSavedStore.remove(key);
|
|
19
|
+
const clearSaved = () => networkSavedStore.clearSaved();
|
|
20
|
+
const clearPinned = () => networkSavedStore.clearPinned();
|
|
21
|
+
export function useNetworkSaved() {
|
|
22
|
+
const [state, setState] = useState(() => networkSavedStore.getState());
|
|
23
|
+
useEffect(() => networkSavedStore.subscribe(() => {
|
|
24
|
+
setState(networkSavedStore.getState());
|
|
25
|
+
}), []);
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
togglePin,
|
|
29
|
+
toggleSave,
|
|
30
|
+
remove,
|
|
31
|
+
clearSaved,
|
|
32
|
+
clearPinned
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -5,17 +5,23 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Components
|
|
8
|
-
export { NetworkModal } from "./components/NetworkModal";
|
|
8
|
+
export { NetworkModal, NetworkToolView } from "./components/NetworkModal";
|
|
9
9
|
export { NetworkEventDetailView } from "./components/NetworkEventDetailView";
|
|
10
10
|
export { NetworkEventItemCompact } from "./components/NetworkEventItemCompact";
|
|
11
|
+
export { NetworkSavedScreen } from "./components/NetworkSavedScreen";
|
|
12
|
+
export { NetworkSavedButton, NetworkDetailPinActions } from "./components/NetworkHeaderLive";
|
|
11
13
|
|
|
12
14
|
// Hooks
|
|
13
15
|
export { useNetworkEvents } from "./hooks/useNetworkEvents";
|
|
16
|
+
export { useLiveNetworkEvent } from "./hooks/useLiveNetworkEvent";
|
|
17
|
+
export { useNetworkSaved } from "./hooks/useNetworkSaved";
|
|
14
18
|
export { TickProvider, useTickEveryMinute } from "./hooks/useTickEveryMinute";
|
|
15
19
|
|
|
16
20
|
// Utils
|
|
17
21
|
export { networkListener, startNetworkListener, stopNetworkListener, addNetworkListener, removeAllNetworkListeners, isNetworkListening, getNetworkListenerCount } from "./utils/networkListener";
|
|
18
22
|
export { networkEventStore } from "./utils/networkEventStore";
|
|
23
|
+
export { networkSavedStore, flagsForEventId, FREE_TIER_SAVED_LIMIT, MAX_PINNED, MAX_SAVED, SESSION_ID as NETWORK_SESSION_ID } from "./utils/networkSavedStore";
|
|
24
|
+
export { splitPinned } from "./utils/withPinnedFirst";
|
|
19
25
|
export { formatBytes, formatDuration, formatHttpStatus } from "./utils/formatting";
|
|
20
26
|
|
|
21
27
|
// Types
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { networkEventStore } from "../utils/networkEventStore";
|
|
4
|
+
import { flagsForEventId, networkSavedStore } from "../utils/networkSavedStore";
|
|
4
5
|
/**
|
|
5
6
|
* Request/response bodies larger than this (using the cached size proxy already
|
|
6
7
|
* on the event — no per-snapshot stringify) are NOT streamed in the list
|
|
@@ -26,6 +27,9 @@ const stripLargeBodies = event => {
|
|
|
26
27
|
if (stripResponse) stripped.responseData = undefined;
|
|
27
28
|
return stripped;
|
|
28
29
|
};
|
|
30
|
+
const resolveEvent = id => networkEventStore.getEventById(id) ?? networkSavedStore.getEventById(id);
|
|
31
|
+
const readId = params => params && typeof params === "object" ? params.id : undefined;
|
|
32
|
+
const readBool = (params, key) => !!(params && typeof params === "object" && params[key]);
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* Sync adapter for the network tool, consumed by @buoy-gg/external-sync's
|
|
@@ -37,29 +41,103 @@ const stripLargeBodies = event => {
|
|
|
37
41
|
*
|
|
38
42
|
* v2: large bodies are stripped from the snapshot and fetched lazily via
|
|
39
43
|
* `getEventBody` (see SNAPSHOT_BODY_INLINE_LIMIT).
|
|
44
|
+
*
|
|
45
|
+
* v3: the payload became an OBJECT — `{ events, saved }` — so pinned/saved
|
|
46
|
+
* requests reach the dashboard. They can't ride as flags on the events: a pin
|
|
47
|
+
* outlives the event that produced it (that is its whole purpose), so it has
|
|
48
|
+
* to be its own list. Dashboards that predate v3 read `payload` as an array;
|
|
49
|
+
* `NetworkPanel`/`NetworkInline` accept either shape.
|
|
40
50
|
*/
|
|
41
51
|
export const networkSyncAdapter = {
|
|
42
|
-
version:
|
|
43
|
-
getSnapshot: () =>
|
|
44
|
-
|
|
52
|
+
version: 3,
|
|
53
|
+
getSnapshot: () => ({
|
|
54
|
+
events: networkEventStore.getEvents().map(stripLargeBodies),
|
|
55
|
+
// Saved snapshots go through the same body strip. They are few (capped in
|
|
56
|
+
// the store) but they ride on EVERY snapshot, so a fat one would land in
|
|
57
|
+
// the same 200ms loop that the body-stripping exists to protect.
|
|
58
|
+
saved: networkSavedStore.getRecords().map(record => ({
|
|
59
|
+
...record,
|
|
60
|
+
event: stripLargeBodies(record.event)
|
|
61
|
+
}))
|
|
62
|
+
}),
|
|
63
|
+
subscribe: onChange => {
|
|
64
|
+
// Pinning with no traffic in flight still has to reach the dashboard, so
|
|
65
|
+
// both stores drive the snapshot.
|
|
66
|
+
const unsubscribeEvents = networkEventStore.subscribeToEvents(onChange);
|
|
67
|
+
const unsubscribeSaved = networkSavedStore.subscribe(onChange);
|
|
68
|
+
return () => {
|
|
69
|
+
unsubscribeEvents();
|
|
70
|
+
unsubscribeSaved();
|
|
71
|
+
};
|
|
72
|
+
},
|
|
45
73
|
actions: {
|
|
46
74
|
clearEvents: () => {
|
|
47
75
|
networkEventStore.clearEvents();
|
|
48
76
|
},
|
|
49
77
|
/**
|
|
50
78
|
* Return the full (un-stripped) request/response bodies for one event,
|
|
51
|
-
* fetched on demand by the dashboard when its detail view opens.
|
|
52
|
-
*
|
|
79
|
+
* fetched on demand by the dashboard when its detail view opens. Falls
|
|
80
|
+
* back to the saved store, which may hold the only remaining copy of a
|
|
81
|
+
* request that has since been cleared. Returns null if neither has it.
|
|
53
82
|
*/
|
|
54
83
|
getEventBody: params => {
|
|
55
|
-
const id = params
|
|
84
|
+
const id = readId(params);
|
|
56
85
|
if (!id) return null;
|
|
57
|
-
const event =
|
|
86
|
+
const event = resolveEvent(id);
|
|
58
87
|
if (!event) return null;
|
|
59
88
|
return {
|
|
60
89
|
requestData: event.requestData ?? null,
|
|
61
90
|
responseData: event.responseData ?? null
|
|
62
91
|
};
|
|
92
|
+
},
|
|
93
|
+
/** Pin/unpin a request from the dashboard. */
|
|
94
|
+
setPinned: params => {
|
|
95
|
+
const id = readId(params);
|
|
96
|
+
if (!id) return null;
|
|
97
|
+
const event = resolveEvent(id);
|
|
98
|
+
if (!event) return null;
|
|
99
|
+
const wanted = readBool(params, "pinned");
|
|
100
|
+
// `flagsForEventId`, not `isPinned` — the id may be a `saved:<key>`
|
|
101
|
+
// snapshot, which the live-id Sets deliberately don't carry.
|
|
102
|
+
const current = flagsForEventId(networkSavedStore.getState(), id).pinned;
|
|
103
|
+
if (current === wanted) return {
|
|
104
|
+
ok: true
|
|
105
|
+
};
|
|
106
|
+
return networkSavedStore.togglePin(event);
|
|
107
|
+
},
|
|
108
|
+
/** Save/unsave a request from the dashboard. */
|
|
109
|
+
setSaved: params => {
|
|
110
|
+
const id = readId(params);
|
|
111
|
+
if (!id) return null;
|
|
112
|
+
const event = resolveEvent(id);
|
|
113
|
+
if (!event) return null;
|
|
114
|
+
const wanted = readBool(params, "saved");
|
|
115
|
+
const current = flagsForEventId(networkSavedStore.getState(), id).saved;
|
|
116
|
+
if (current === wanted) return {
|
|
117
|
+
ok: true
|
|
118
|
+
};
|
|
119
|
+
return networkSavedStore.toggleSave(event);
|
|
120
|
+
},
|
|
121
|
+
/** Drop one pinned/saved record by its stable key. */
|
|
122
|
+
removeSavedRecord: params => {
|
|
123
|
+
const key = params && typeof params === "object" ? params.key : undefined;
|
|
124
|
+
if (!key) return null;
|
|
125
|
+
networkSavedStore.remove(key);
|
|
126
|
+
return {
|
|
127
|
+
ok: true
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
clearSavedRequests: () => {
|
|
131
|
+
networkSavedStore.clearSaved();
|
|
132
|
+
return {
|
|
133
|
+
ok: true
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
clearPinnedRequests: () => {
|
|
137
|
+
networkSavedStore.clearPinned();
|
|
138
|
+
return {
|
|
139
|
+
ok: true
|
|
140
|
+
};
|
|
63
141
|
}
|
|
64
142
|
}
|
|
65
143
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render a captured request as a runnable `curl` command.
|
|
5
|
+
*
|
|
6
|
+
* The point is to get a request OUT of the app and into a terminal, Postman or
|
|
7
|
+
* a bug report without retyping it — so the output has to actually run, which
|
|
8
|
+
* makes quoting the whole job.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Headers that describe the captured hop rather than the request, and would
|
|
12
|
+
* either break the replay or lie about it. curl sets these itself. */
|
|
13
|
+
const SKIPPED_HEADERS = new Set(["content-length",
|
|
14
|
+
// curl recomputes it; a stale value truncates the body
|
|
15
|
+
"host",
|
|
16
|
+
// implied by the URL
|
|
17
|
+
"connection", "accept-encoding" // curl would then have to decode what it asked for
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Single-quote for POSIX shells.
|
|
22
|
+
*
|
|
23
|
+
* Inside single quotes every byte is literal, so the ONLY character needing
|
|
24
|
+
* care is the single quote itself — closed, escaped, reopened. Newlines and
|
|
25
|
+
* `$`/backtick survive untouched, which is what makes this safe for JSON
|
|
26
|
+
* bodies and tokens.
|
|
27
|
+
*/
|
|
28
|
+
function shellQuote(value) {
|
|
29
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
function serializeBody(data) {
|
|
32
|
+
if (data === undefined || data === null) return null;
|
|
33
|
+
if (typeof data === "string") return data;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(data);
|
|
36
|
+
} catch {
|
|
37
|
+
// Circular or otherwise unserializable — better to emit no body than a
|
|
38
|
+
// command that silently posts "[object Object]".
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function generateCurl(event) {
|
|
43
|
+
const method = (event.method || "GET").toUpperCase();
|
|
44
|
+
const body = serializeBody(event.requestData);
|
|
45
|
+
|
|
46
|
+
// Flags are collected as whole `-X PATCH` strings rather than as a flat token
|
|
47
|
+
// list, so the line-wrap below can't mis-pair them — an earlier version
|
|
48
|
+
// destructured `[cmd, url, ...rest]` and put the URL in the wrong slot the
|
|
49
|
+
// moment an -X was present.
|
|
50
|
+
const flags = [];
|
|
51
|
+
|
|
52
|
+
// curl defaults to GET, and to POST once -d is present, so -X only carries
|
|
53
|
+
// information when it disagrees with that.
|
|
54
|
+
if (method !== "GET" && !(method === "POST" && body !== null)) {
|
|
55
|
+
flags.push(`-X ${method}`);
|
|
56
|
+
}
|
|
57
|
+
for (const [key, value] of Object.entries(event.requestHeaders || {})) {
|
|
58
|
+
if (SKIPPED_HEADERS.has(key.toLowerCase())) continue;
|
|
59
|
+
flags.push(`-H ${shellQuote(`${key}: ${value}`)}`);
|
|
60
|
+
}
|
|
61
|
+
if (body !== null) flags.push(`--data-raw ${shellQuote(body)}`);
|
|
62
|
+
const head = `curl ${shellQuote(event.url)}`;
|
|
63
|
+
if (flags.length === 0) return head;
|
|
64
|
+
// One flag per line — these get long, and a wrapped blob is unreadable in a
|
|
65
|
+
// terminal or pasted into a ticket.
|
|
66
|
+
return `${head} \\\n ${flags.join(" \\\n ")}`;
|
|
67
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { BaseEventStore } from "@buoy-gg/shared-ui";
|
|
25
25
|
import { extractOperationName } from "./extractOperationName";
|
|
26
26
|
import { networkListener, addNetworkListener } from "./networkListener";
|
|
27
|
+
import { networkSavedStore } from "./networkSavedStore";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Callback type for individual event notifications
|
|
@@ -155,7 +156,11 @@ class NetworkEventStore extends BaseEventStore {
|
|
|
155
156
|
if (index !== -1) {
|
|
156
157
|
const updatedEvent = {
|
|
157
158
|
...this.events[index],
|
|
158
|
-
duration: event.duration
|
|
159
|
+
duration: event.duration,
|
|
160
|
+
// Left undefined when the interceptor couldn't measure it (e.g. a
|
|
161
|
+
// request that failed before headers). Consumers hide the breakdown
|
|
162
|
+
// in that case rather than rendering zeroes.
|
|
163
|
+
timings: event.timings
|
|
159
164
|
};
|
|
160
165
|
if (event.response) {
|
|
161
166
|
updatedEvent.status = event.response.status;
|
|
@@ -171,6 +176,10 @@ class NetworkEventStore extends BaseEventStore {
|
|
|
171
176
|
}
|
|
172
177
|
this.events = this.events.map((e, i) => i === index ? updatedEvent : e);
|
|
173
178
|
this.pendingRequests.delete(request.id);
|
|
179
|
+
// Refresh any pinned/saved snapshot of this request — a request pinned
|
|
180
|
+
// while in flight would otherwise stay frozen as "Pending" forever.
|
|
181
|
+
// No-ops on a single Set check when nothing is pinned.
|
|
182
|
+
networkSavedStore.syncFromLive(updatedEvent);
|
|
174
183
|
this.notify(updatedEvent);
|
|
175
184
|
this.notifyArrayListeners();
|
|
176
185
|
}
|