@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useMemo } from "react";
|
|
4
4
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
|
|
5
5
|
import { Clock, Upload, Download, AlertCircle, ChevronDown, ChevronUp, Lock, Unlock, FileJson, Filter, Globe, Link, Copy, InlineCopyButton, macOSColors, useFeatureGate, UpgradeModal } from "@buoy-gg/shared-ui";
|
|
6
6
|
import { formatBytes, formatDuration, formatHttpStatus } from "../utils/formatting";
|
|
@@ -8,13 +8,86 @@ import { formatRelativeTime } from "@buoy-gg/shared-ui";
|
|
|
8
8
|
import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
|
|
9
9
|
import { formatGraphQLDisplay } from "../utils/formatGraphQLVariables";
|
|
10
10
|
import { useNetworkBodyResolver } from "../sync/networkBodyResolver";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Colour for the icons that merely LABEL a section (Sent/Received, the JSON
|
|
14
|
+
* body glyphs). The Lock/Unlock pair keeps its colour because
|
|
15
|
+
* secure-vs-insecure is a genuine signal, as does the status pill.
|
|
16
|
+
*/
|
|
11
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
const NEUTRAL_ICON = macOSColors.semantic.success;
|
|
19
|
+
/**
|
|
20
|
+
* The measurable slice of a request's lifetime, as proportional bars.
|
|
21
|
+
*
|
|
22
|
+
* Renders NOTHING when the interceptor couldn't measure it — an older device
|
|
23
|
+
* talking to a newer dashboard, or a request that died before headers. A phase
|
|
24
|
+
* row reading `0ms` would claim we measured zero, which is a different and
|
|
25
|
+
* false statement from "we didn't measure this".
|
|
26
|
+
*
|
|
27
|
+
* Only two phases, on purpose. Queueing / DNS / TLS / request-send all happen
|
|
28
|
+
* below the JS layer with nothing to read them from — see NetworkTimings. The
|
|
29
|
+
* design this came from shows five, but derives the other three by multiplying
|
|
30
|
+
* the total by fixed fractions, i.e. they are invented. Two real ones are worth
|
|
31
|
+
* more than five where three are decoration.
|
|
32
|
+
*/
|
|
33
|
+
const TimingPhases = ({
|
|
34
|
+
timings,
|
|
35
|
+
total
|
|
36
|
+
}) => {
|
|
37
|
+
// Guard the divisor: a sub-millisecond request would otherwise divide by 0.
|
|
38
|
+
const span = Math.max(1, total);
|
|
39
|
+
const phases = [{
|
|
40
|
+
// Deliberately NOT called "server time". Measured from JS, this includes
|
|
41
|
+
// queueing, DNS, TLS and sending the request — everything before the
|
|
42
|
+
// headers land. The subtitle says so rather than implying Chrome-grade
|
|
43
|
+
// precision we don't have.
|
|
44
|
+
label: "Waiting (TTFB)",
|
|
45
|
+
hint: "incl. queue, DNS, TLS",
|
|
46
|
+
value: timings.ttfb,
|
|
47
|
+
color: macOSColors.semantic.warning
|
|
48
|
+
}, {
|
|
49
|
+
label: "Content download",
|
|
50
|
+
hint: undefined,
|
|
51
|
+
value: timings.download,
|
|
52
|
+
color: macOSColors.semantic.success
|
|
53
|
+
}];
|
|
54
|
+
return /*#__PURE__*/_jsx(View, {
|
|
55
|
+
style: styles.phaseList,
|
|
56
|
+
children: phases.map(phase => /*#__PURE__*/_jsxs(View, {
|
|
57
|
+
style: styles.phaseRow,
|
|
58
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
59
|
+
style: styles.phaseLabelGroup,
|
|
60
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
61
|
+
style: styles.phaseLabel,
|
|
62
|
+
children: phase.label
|
|
63
|
+
}), phase.hint ? /*#__PURE__*/_jsx(Text, {
|
|
64
|
+
style: styles.phaseHint,
|
|
65
|
+
children: phase.hint
|
|
66
|
+
}) : null]
|
|
67
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
68
|
+
style: styles.phaseTrack,
|
|
69
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
70
|
+
style: [styles.phaseBar, {
|
|
71
|
+
backgroundColor: phase.color,
|
|
72
|
+
// A floor so a 1ms phase is still a visible tick.
|
|
73
|
+
width: `${Math.max(1, phase.value / span * 100)}%`
|
|
74
|
+
}]
|
|
75
|
+
})
|
|
76
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
77
|
+
style: styles.phaseValue,
|
|
78
|
+
children: formatDuration(phase.value)
|
|
79
|
+
})]
|
|
80
|
+
}, phase.label))
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
12
84
|
// Component for collapsible sections matching Sentry style
|
|
13
85
|
const CollapsibleSection = ({
|
|
14
86
|
title,
|
|
15
87
|
icon,
|
|
16
88
|
children,
|
|
17
|
-
defaultOpen = false
|
|
89
|
+
defaultOpen = false,
|
|
90
|
+
count
|
|
18
91
|
}) => {
|
|
19
92
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
20
93
|
return /*#__PURE__*/_jsxs(View, {
|
|
@@ -27,7 +100,10 @@ const CollapsibleSection = ({
|
|
|
27
100
|
children: [icon, /*#__PURE__*/_jsx(Text, {
|
|
28
101
|
style: styles.collapsibleTitleText,
|
|
29
102
|
children: title
|
|
30
|
-
})
|
|
103
|
+
}), count !== undefined ? /*#__PURE__*/_jsx(Text, {
|
|
104
|
+
style: styles.collapsibleCount,
|
|
105
|
+
children: count
|
|
106
|
+
}) : null]
|
|
31
107
|
}), isOpen ? /*#__PURE__*/_jsx(ChevronUp, {
|
|
32
108
|
size: 16,
|
|
33
109
|
color: macOSColors.text.secondary
|
|
@@ -95,7 +171,23 @@ const UrlBreakdown = ({
|
|
|
95
171
|
};
|
|
96
172
|
}
|
|
97
173
|
};
|
|
98
|
-
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* MUST be memoized. `parseUrl` builds a fresh `params` object every call, and
|
|
177
|
+
* that object is handed to `DataViewer` as its `data` prop — whose flattening
|
|
178
|
+
* effect keys on `data` by IDENTITY. Computed inline, every render of this
|
|
179
|
+
* component produced a new object, so the effect re-fired and re-entered
|
|
180
|
+
* "Processing data…" forever. The detail view re-renders constantly on the
|
|
181
|
+
* dashboard (it tracks the live event as the device pushes snapshots ~4×/sec),
|
|
182
|
+
* so this pinned the query-parameter viewer in a permanent processing loop.
|
|
183
|
+
*
|
|
184
|
+
* `url` is a string, so this is stable by value.
|
|
185
|
+
*/
|
|
186
|
+
const urlParts = useMemo(() => parseUrl(url),
|
|
187
|
+
// parseUrl closes over the graphql/operation props, which are what change
|
|
188
|
+
// the derived pathname.
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
+
[url, requestClient, operationName, graphqlVariables]);
|
|
99
191
|
const [showParams, setShowParams] = useState(false);
|
|
100
192
|
return /*#__PURE__*/_jsxs(View, {
|
|
101
193
|
style: styles.urlBreakdown,
|
|
@@ -144,6 +236,9 @@ const UrlBreakdown = ({
|
|
|
144
236
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
145
237
|
style: styles.queryParamsToggleText,
|
|
146
238
|
children: "Query Parameters"
|
|
239
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
240
|
+
style: styles.collapsibleCount,
|
|
241
|
+
children: Object.keys(urlParts.params).length
|
|
147
242
|
}), showParams ? /*#__PURE__*/_jsx(ChevronUp, {
|
|
148
243
|
size: 14,
|
|
149
244
|
color: macOSColors.text.secondary
|
|
@@ -359,34 +454,41 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
359
454
|
}), /*#__PURE__*/_jsxs(Text, {
|
|
360
455
|
style: styles.timingExact,
|
|
361
456
|
children: ["(", new Date(event.timestamp).toLocaleTimeString(), ")"]
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
})]
|
|
457
|
+
}), event.requestSize || event.responseSize ? /*#__PURE__*/_jsxs(View, {
|
|
458
|
+
style: styles.sizeRow,
|
|
459
|
+
children: [event.requestSize !== undefined ? /*#__PURE__*/_jsxs(View, {
|
|
460
|
+
style: styles.sizeItem,
|
|
461
|
+
children: [/*#__PURE__*/_jsx(Upload, {
|
|
462
|
+
size: 10,
|
|
463
|
+
color: macOSColors.semantic.info
|
|
464
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
465
|
+
style: styles.sizeLabel,
|
|
466
|
+
children: "Sent:"
|
|
467
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
468
|
+
style: styles.sizeValue,
|
|
469
|
+
children: formatBytes(event.requestSize)
|
|
470
|
+
})]
|
|
471
|
+
}) : null, event.responseSize !== undefined ? /*#__PURE__*/_jsxs(View, {
|
|
472
|
+
style: styles.sizeItem,
|
|
473
|
+
children: [/*#__PURE__*/_jsx(Download, {
|
|
474
|
+
size: 10,
|
|
475
|
+
color: NEUTRAL_ICON
|
|
476
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
477
|
+
style: styles.sizeLabel,
|
|
478
|
+
children: "Received:"
|
|
479
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
480
|
+
style: styles.sizeValue,
|
|
481
|
+
children: formatBytes(event.responseSize)
|
|
482
|
+
})]
|
|
483
|
+
}) : null]
|
|
389
484
|
}) : null]
|
|
485
|
+
}), event.timings ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
486
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
487
|
+
style: styles.phaseDivider
|
|
488
|
+
}), /*#__PURE__*/_jsx(TimingPhases, {
|
|
489
|
+
timings: event.timings,
|
|
490
|
+
total: event.duration ?? event.timings.ttfb + event.timings.download
|
|
491
|
+
})]
|
|
390
492
|
}) : null]
|
|
391
493
|
}), /*#__PURE__*/_jsx(CollapsibleSection, {
|
|
392
494
|
title: "Request Headers",
|
|
@@ -395,6 +497,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
395
497
|
color: macOSColors.semantic.info
|
|
396
498
|
}),
|
|
397
499
|
defaultOpen: false,
|
|
500
|
+
count: Object.keys(event.requestHeaders).length,
|
|
398
501
|
children: Object.keys(event.requestHeaders).length > 0 ? /*#__PURE__*/_jsx(View, {
|
|
399
502
|
style: styles.dataViewerContainer,
|
|
400
503
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
@@ -413,9 +516,10 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
413
516
|
title: "Response Headers",
|
|
414
517
|
icon: /*#__PURE__*/_jsx(Download, {
|
|
415
518
|
size: 14,
|
|
416
|
-
color:
|
|
519
|
+
color: NEUTRAL_ICON
|
|
417
520
|
}),
|
|
418
521
|
defaultOpen: false,
|
|
522
|
+
count: Object.keys(event.responseHeaders).length,
|
|
419
523
|
children: Object.keys(event.responseHeaders).length > 0 ? /*#__PURE__*/_jsx(View, {
|
|
420
524
|
style: styles.dataViewerContainer,
|
|
421
525
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
@@ -463,9 +567,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
463
567
|
title: "Response Body",
|
|
464
568
|
icon: /*#__PURE__*/_jsx(FileJson, {
|
|
465
569
|
size: 14,
|
|
466
|
-
color:
|
|
467
|
-
})
|
|
468
|
-
|
|
570
|
+
color: NEUTRAL_ICON
|
|
571
|
+
})
|
|
572
|
+
// The body is what you opened the row FOR — the design has it
|
|
573
|
+
// expanded on arrival, everything else closed.
|
|
574
|
+
,
|
|
575
|
+
defaultOpen: true,
|
|
469
576
|
children: /*#__PURE__*/_jsx(View, {
|
|
470
577
|
style: styles.dataViewerContainer,
|
|
471
578
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
@@ -481,7 +588,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
481
588
|
title: "Response Body",
|
|
482
589
|
icon: /*#__PURE__*/_jsx(FileJson, {
|
|
483
590
|
size: 14,
|
|
484
|
-
color:
|
|
591
|
+
color: NEUTRAL_ICON
|
|
485
592
|
}),
|
|
486
593
|
defaultOpen: true,
|
|
487
594
|
children: /*#__PURE__*/_jsx(Text, {
|
|
@@ -707,6 +814,7 @@ const styles = StyleSheet.create({
|
|
|
707
814
|
marginBottom: 8
|
|
708
815
|
},
|
|
709
816
|
queryParamsToggleText: {
|
|
817
|
+
flexGrow: 0,
|
|
710
818
|
color: macOSColors.text.primary,
|
|
711
819
|
fontSize: 12,
|
|
712
820
|
fontWeight: "600",
|
|
@@ -725,7 +833,10 @@ const styles = StyleSheet.create({
|
|
|
725
833
|
timingRow: {
|
|
726
834
|
flexDirection: "row",
|
|
727
835
|
alignItems: "center",
|
|
728
|
-
gap: 6
|
|
836
|
+
gap: 6,
|
|
837
|
+
// Wraps rather than squashing when the card is narrow (a phone, or a
|
|
838
|
+
// half-width panel) — the sizes drop to their own line there.
|
|
839
|
+
flexWrap: "wrap"
|
|
729
840
|
},
|
|
730
841
|
timingLabel: {
|
|
731
842
|
color: macOSColors.text.secondary,
|
|
@@ -743,11 +854,11 @@ const styles = StyleSheet.create({
|
|
|
743
854
|
},
|
|
744
855
|
sizeRow: {
|
|
745
856
|
flexDirection: "row",
|
|
857
|
+
alignItems: "center",
|
|
746
858
|
gap: 16,
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
borderTopColor: macOSColors.border.default
|
|
859
|
+
// Pushed to the far end of the Started row. No top border/padding — it is
|
|
860
|
+
// no longer a separate strip.
|
|
861
|
+
marginLeft: "auto"
|
|
751
862
|
},
|
|
752
863
|
sizeItem: {
|
|
753
864
|
flexDirection: "row",
|
|
@@ -764,6 +875,50 @@ const styles = StyleSheet.create({
|
|
|
764
875
|
fontFamily: "monospace",
|
|
765
876
|
fontWeight: "600"
|
|
766
877
|
},
|
|
878
|
+
/* ---- Timing phases ---- */
|
|
879
|
+
phaseDivider: {
|
|
880
|
+
height: 1,
|
|
881
|
+
backgroundColor: macOSColors.border.default,
|
|
882
|
+
marginTop: 12,
|
|
883
|
+
marginBottom: 12
|
|
884
|
+
},
|
|
885
|
+
phaseList: {
|
|
886
|
+
gap: 8
|
|
887
|
+
},
|
|
888
|
+
phaseRow: {
|
|
889
|
+
flexDirection: "row",
|
|
890
|
+
alignItems: "center",
|
|
891
|
+
gap: 14
|
|
892
|
+
},
|
|
893
|
+
phaseLabelGroup: {
|
|
894
|
+
width: 150
|
|
895
|
+
},
|
|
896
|
+
phaseLabel: {
|
|
897
|
+
fontSize: 12,
|
|
898
|
+
color: macOSColors.text.secondary
|
|
899
|
+
},
|
|
900
|
+
phaseHint: {
|
|
901
|
+
fontSize: 10,
|
|
902
|
+
color: macOSColors.text.muted
|
|
903
|
+
},
|
|
904
|
+
phaseTrack: {
|
|
905
|
+
flex: 1,
|
|
906
|
+
height: 7,
|
|
907
|
+
borderRadius: 4,
|
|
908
|
+
backgroundColor: macOSColors.background.hover,
|
|
909
|
+
justifyContent: "center"
|
|
910
|
+
},
|
|
911
|
+
phaseBar: {
|
|
912
|
+
height: 7,
|
|
913
|
+
borderRadius: 4
|
|
914
|
+
},
|
|
915
|
+
phaseValue: {
|
|
916
|
+
width: 72,
|
|
917
|
+
textAlign: "right",
|
|
918
|
+
fontSize: 12,
|
|
919
|
+
fontFamily: "monospace",
|
|
920
|
+
color: macOSColors.text.primary
|
|
921
|
+
},
|
|
767
922
|
// Collapsible section styles
|
|
768
923
|
collapsibleSection: {
|
|
769
924
|
marginHorizontal: 12,
|
|
@@ -779,6 +934,8 @@ const styles = StyleSheet.create({
|
|
|
779
934
|
alignItems: "center",
|
|
780
935
|
justifyContent: "space-between",
|
|
781
936
|
padding: 12,
|
|
937
|
+
// `background.hover` is the SELECTION grey — heavy enough here that every
|
|
938
|
+
// section header read as a toolbar. The design uses a near-white strip.
|
|
782
939
|
backgroundColor: macOSColors.background.hover
|
|
783
940
|
},
|
|
784
941
|
collapsibleTitle: {
|
|
@@ -791,6 +948,12 @@ const styles = StyleSheet.create({
|
|
|
791
948
|
fontSize: 13,
|
|
792
949
|
fontWeight: "600"
|
|
793
950
|
},
|
|
951
|
+
/** The "how much is in here" number beside a section title. */
|
|
952
|
+
collapsibleCount: {
|
|
953
|
+
color: macOSColors.text.muted,
|
|
954
|
+
fontSize: 11.5,
|
|
955
|
+
fontFamily: "monospace"
|
|
956
|
+
},
|
|
794
957
|
collapsibleContent: {
|
|
795
958
|
padding: 12
|
|
796
959
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { StyleSheet, View, Text } from "react-native";
|
|
5
|
-
import { ChevronRight, Upload, Download, Clock, AlertCircle, DevToolsCard, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
|
|
5
|
+
import { ChevronRight, Upload, Download, Clock, AlertCircle, Bookmark, Pin, DevToolsCard, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
|
|
6
6
|
import { formatBytes, formatDuration } from "../utils/formatting";
|
|
7
7
|
import { formatRelativeTime } from "@buoy-gg/shared-ui";
|
|
8
8
|
import { useTickEveryMinute } from "../hooks/useTickEveryMinute";
|
|
@@ -32,6 +32,39 @@ function getContentTypeBadge(headers) {
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The transport chip (`fetch` / `GQL` / `gRPC` / `XHR`), one brand colour each.
|
|
37
|
+
*/
|
|
38
|
+
function getClientChipColors(requestClient) {
|
|
39
|
+
switch (requestClient) {
|
|
40
|
+
case "fetch":
|
|
41
|
+
return {
|
|
42
|
+
backgroundColor: "rgba(74, 144, 226, 0.15)",
|
|
43
|
+
color: "#4A90E2"
|
|
44
|
+
};
|
|
45
|
+
case "graphql":
|
|
46
|
+
return {
|
|
47
|
+
backgroundColor: "rgba(229, 53, 171, 0.15)",
|
|
48
|
+
color: "#E535AB"
|
|
49
|
+
};
|
|
50
|
+
case "grpc-web":
|
|
51
|
+
return {
|
|
52
|
+
backgroundColor: "rgba(16, 185, 129, 0.15)",
|
|
53
|
+
color: "#10B981"
|
|
54
|
+
};
|
|
55
|
+
case "xhr":
|
|
56
|
+
return {
|
|
57
|
+
backgroundColor: "rgba(245, 158, 11, 0.15)",
|
|
58
|
+
color: "#F59E0B"
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
return {
|
|
62
|
+
backgroundColor: "rgba(147, 51, 234, 0.15)",
|
|
63
|
+
color: "#9333EA"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
// Decomposed components following rule3 - Component Composition
|
|
36
69
|
|
|
37
70
|
// Status indicator component - single responsibility
|
|
@@ -134,7 +167,10 @@ export const NetworkEventItemCompact = /*#__PURE__*/memo(
|
|
|
134
167
|
// renders to this component instead of a compiled anonymous name.
|
|
135
168
|
function NetworkEventItemCompact({
|
|
136
169
|
event,
|
|
137
|
-
onPress
|
|
170
|
+
onPress,
|
|
171
|
+
pinned = false,
|
|
172
|
+
saved = false,
|
|
173
|
+
onLongPress
|
|
138
174
|
}) {
|
|
139
175
|
const statusColor = getStatusColor(event.status, event.error);
|
|
140
176
|
const isPending = !event.status && !event.error;
|
|
@@ -159,23 +195,30 @@ function NetworkEventItemCompact({
|
|
|
159
195
|
}
|
|
160
196
|
return /*#__PURE__*/_jsxs(DevToolsCard, {
|
|
161
197
|
onPress: () => onPress(event),
|
|
198
|
+
onLongPress: onLongPress ? () => onLongPress(event) : undefined,
|
|
162
199
|
style: styles.cardWrapper,
|
|
163
200
|
contentStyle: [styles.container, {
|
|
164
201
|
borderLeftColor: statusColor
|
|
165
202
|
}],
|
|
166
203
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
167
204
|
style: styles.topRightContainer,
|
|
168
|
-
children: [/*#__PURE__*/_jsx(
|
|
205
|
+
children: [pinned ? /*#__PURE__*/_jsx(Pin, {
|
|
206
|
+
size: 10,
|
|
207
|
+
color: macOSColors.semantic.info
|
|
208
|
+
}) : null, saved ? /*#__PURE__*/_jsx(Bookmark, {
|
|
209
|
+
size: 10,
|
|
210
|
+
color: macOSColors.semantic.debug
|
|
211
|
+
}) : null, /*#__PURE__*/_jsx(StatusIndicator, {
|
|
169
212
|
event: event,
|
|
170
213
|
isPending: isPending,
|
|
171
214
|
statusColor: statusColor
|
|
172
215
|
}), event.requestClient ? /*#__PURE__*/_jsx(View, {
|
|
173
216
|
style: [styles.topRightBadge, {
|
|
174
|
-
backgroundColor:
|
|
217
|
+
backgroundColor: getClientChipColors(event.requestClient).backgroundColor
|
|
175
218
|
}],
|
|
176
219
|
children: /*#__PURE__*/_jsx(Text, {
|
|
177
220
|
style: [styles.clientText, {
|
|
178
|
-
color: event.requestClient
|
|
221
|
+
color: getClientChipColors(event.requestClient).color
|
|
179
222
|
}],
|
|
180
223
|
children: event.requestClient === "graphql" ? "GQL" : event.requestClient === "grpc-web" ? "gRPC" : event.requestClient === "xhr" ? "XHR" : event.requestClient
|
|
181
224
|
})
|
|
@@ -220,13 +263,17 @@ function NetworkEventItemCompact({
|
|
|
220
263
|
NetworkEventItemCompact.displayName = "NetworkEventItemCompact";
|
|
221
264
|
const styles = StyleSheet.create({
|
|
222
265
|
cardWrapper: {
|
|
266
|
+
// Overrides DevToolsCard's own wrapper margin, so it has to opt into the
|
|
267
|
+
// edge-to-edge chat layout too or the rows keep their gutter.
|
|
223
268
|
marginHorizontal: 8
|
|
224
269
|
},
|
|
225
270
|
container: {
|
|
226
271
|
flexDirection: "row",
|
|
227
272
|
alignItems: "center",
|
|
228
273
|
borderLeftWidth: 3,
|
|
229
|
-
borderLeftColor: "transparent"
|
|
274
|
+
borderLeftColor: "transparent",
|
|
275
|
+
// One hairline per row — the same value DevToolsCard's border uses.
|
|
276
|
+
borderBottomWidth: 1
|
|
230
277
|
},
|
|
231
278
|
leftSection: {
|
|
232
279
|
marginRight: 8,
|
|
@@ -19,13 +19,15 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
22
|
-
import { CheckCircle, XCircle, Clock, Copy, Trash2, CopyButton, macOSColors } from "@buoy-gg/shared-ui";
|
|
22
|
+
import { Bookmark, CheckCircle, XCircle, Clock, Copy, Pin, Trash2, CopyButton, macOSColors } from "@buoy-gg/shared-ui";
|
|
23
23
|
import { networkEventStore } from "../utils/networkEventStore";
|
|
24
|
+
import { flagsForEventId, networkSavedStore, FREE_TIER_SAVED_LIMIT, MAX_PINNED, MAX_SAVED } from "../utils/networkSavedStore";
|
|
25
|
+
import { useNetworkSaved } from "../hooks/useNetworkSaved";
|
|
24
26
|
import { useNetworkBadgeCounts } from "../hooks/useNetworkBadgeCounts";
|
|
25
27
|
import { useNetworkHasEvents, useNetworkHasMatches } from "../hooks/useNetworkHasMatches";
|
|
26
28
|
import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
|
|
27
29
|
import { applyIgnoredPatterns, filterNetworkEvents } from "../utils/filterNetworkEvents";
|
|
28
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
30
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
29
31
|
/** Success / error / pending chips — tap to toggle the status filter. */
|
|
30
32
|
export function NetworkHeaderBadges({
|
|
31
33
|
activeStatus,
|
|
@@ -98,6 +100,104 @@ export function NetworkClearButton({
|
|
|
98
100
|
})
|
|
99
101
|
});
|
|
100
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Entry point to the Saved screen, with its count. A self-subscribing leaf so
|
|
105
|
+
* the badge tracks the store without the modal header re-rendering.
|
|
106
|
+
*/
|
|
107
|
+
export function NetworkSavedButton({
|
|
108
|
+
onPress
|
|
109
|
+
}) {
|
|
110
|
+
const {
|
|
111
|
+
savedRecords
|
|
112
|
+
} = useNetworkSaved();
|
|
113
|
+
const count = savedRecords.length;
|
|
114
|
+
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
115
|
+
"sentry-label": "ignore open saved",
|
|
116
|
+
onPress: onPress,
|
|
117
|
+
accessibilityLabel: "Saved requests",
|
|
118
|
+
style: styles.headerActionButton,
|
|
119
|
+
children: [/*#__PURE__*/_jsx(Bookmark, {
|
|
120
|
+
size: 14,
|
|
121
|
+
color: count > 0 ? macOSColors.semantic.debug : macOSColors.text.muted
|
|
122
|
+
}), count > 0 ? /*#__PURE__*/_jsx(View, {
|
|
123
|
+
style: styles.savedBadge,
|
|
124
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
125
|
+
style: styles.savedBadgeText,
|
|
126
|
+
children: count > 99 ? "99+" : count
|
|
127
|
+
})
|
|
128
|
+
}) : null]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Pin + Save toggles for the request-detail header.
|
|
133
|
+
*
|
|
134
|
+
* Lives here, as a self-subscribing leaf, for two reasons. First the usual
|
|
135
|
+
* one: the modal root stays free of subscriptions. Second, and the real
|
|
136
|
+
* constraint — these buttons deliberately do NOT go inside
|
|
137
|
+
* `NetworkEventDetailView`, because the Events tool renders that same detail
|
|
138
|
+
* view (UnifiedEventDetail) and has no saved store behind it. The header is
|
|
139
|
+
* the Network tool's own surface, so the feature stays where it belongs.
|
|
140
|
+
*
|
|
141
|
+
* The event object is resolved at PRESS time rather than held as a prop: the
|
|
142
|
+
* live store has the fresh copy while the request is in flight, and the saved
|
|
143
|
+
* store is the only copy once it has been cleared or aged out.
|
|
144
|
+
*/
|
|
145
|
+
export function NetworkDetailPinActions({
|
|
146
|
+
eventId,
|
|
147
|
+
isPro,
|
|
148
|
+
onUpgradePress
|
|
149
|
+
}) {
|
|
150
|
+
const state = useNetworkSaved();
|
|
151
|
+
const {
|
|
152
|
+
pinned,
|
|
153
|
+
saved
|
|
154
|
+
} = flagsForEventId(state, eventId);
|
|
155
|
+
const savedLimit = isPro ? MAX_SAVED : FREE_TIER_SAVED_LIMIT;
|
|
156
|
+
const atPinCap = !pinned && state.pinnedEvents.length >= MAX_PINNED;
|
|
157
|
+
const atSavedCap = !saved && state.savedRecords.length >= savedLimit;
|
|
158
|
+
const resolveEvent = () => {
|
|
159
|
+
if (!eventId) return null;
|
|
160
|
+
return networkEventStore.getEvents().find(event => event.id === eventId) ?? networkSavedStore.getEventById(eventId);
|
|
161
|
+
};
|
|
162
|
+
const handlePin = () => {
|
|
163
|
+
const event = resolveEvent();
|
|
164
|
+
if (event) state.togglePin(event);
|
|
165
|
+
};
|
|
166
|
+
const handleSave = () => {
|
|
167
|
+
// Hitting the free ceiling is an upgrade moment, not a dead button.
|
|
168
|
+
if (atSavedCap && !isPro) {
|
|
169
|
+
onUpgradePress();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const event = resolveEvent();
|
|
173
|
+
if (event) state.toggleSave(event);
|
|
174
|
+
};
|
|
175
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
176
|
+
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
177
|
+
"sentry-label": "ignore pin request",
|
|
178
|
+
onPress: handlePin,
|
|
179
|
+
disabled: atPinCap,
|
|
180
|
+
accessibilityLabel: pinned ? "Unpin request" : "Pin request",
|
|
181
|
+
accessibilityHint: atPinCap ? `Pin limit reached (${MAX_PINNED})` : undefined,
|
|
182
|
+
style: [styles.headerActionButton, pinned && styles.headerActionButtonActive, atPinCap && styles.headerActionButtonDisabled],
|
|
183
|
+
children: /*#__PURE__*/_jsx(Pin, {
|
|
184
|
+
size: 14,
|
|
185
|
+
color: pinned ? macOSColors.semantic.info : atPinCap ? macOSColors.text.disabled : macOSColors.text.secondary
|
|
186
|
+
})
|
|
187
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
188
|
+
"sentry-label": "ignore save request",
|
|
189
|
+
onPress: handleSave,
|
|
190
|
+
disabled: atSavedCap && isPro,
|
|
191
|
+
accessibilityLabel: saved ? "Remove from saved" : "Save request",
|
|
192
|
+
accessibilityHint: atSavedCap && isPro ? `Saved limit reached (${MAX_SAVED})` : undefined,
|
|
193
|
+
style: [styles.headerActionButton, saved && styles.headerActionButtonSaved, atSavedCap && isPro && styles.headerActionButtonDisabled],
|
|
194
|
+
children: /*#__PURE__*/_jsx(Bookmark, {
|
|
195
|
+
size: 14,
|
|
196
|
+
color: saved ? macOSColors.semantic.debug : atSavedCap && isPro ? macOSColors.text.disabled : macOSColors.text.secondary
|
|
197
|
+
})
|
|
198
|
+
})]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
101
201
|
/**
|
|
102
202
|
* Copy-all-requests button. Subscribes to a flip-guarded boolean (its
|
|
103
203
|
* enabled state); the copy text is generated at PRESS time from a live
|
|
@@ -149,6 +249,10 @@ const styles = StyleSheet.create({
|
|
|
149
249
|
alignItems: "center",
|
|
150
250
|
gap: 8
|
|
151
251
|
},
|
|
252
|
+
/**
|
|
253
|
+
* Counter chips and toolbar buttons carry a permanent outline; the fill marks
|
|
254
|
+
* the active filter.
|
|
255
|
+
*/
|
|
152
256
|
headerChip: {
|
|
153
257
|
flexDirection: "row",
|
|
154
258
|
alignItems: "center",
|
|
@@ -189,5 +293,31 @@ const styles = StyleSheet.create({
|
|
|
189
293
|
},
|
|
190
294
|
headerActionButtonDisabled: {
|
|
191
295
|
opacity: 0.55
|
|
296
|
+
},
|
|
297
|
+
headerActionButtonActive: {
|
|
298
|
+
backgroundColor: macOSColors.semantic.infoBackground,
|
|
299
|
+
borderColor: macOSColors.semantic.info + "50"
|
|
300
|
+
},
|
|
301
|
+
headerActionButtonSaved: {
|
|
302
|
+
backgroundColor: macOSColors.semantic.debug + "1A",
|
|
303
|
+
borderColor: macOSColors.semantic.debug + "50"
|
|
304
|
+
},
|
|
305
|
+
savedBadge: {
|
|
306
|
+
position: "absolute",
|
|
307
|
+
top: -3,
|
|
308
|
+
right: -3,
|
|
309
|
+
minWidth: 14,
|
|
310
|
+
height: 14,
|
|
311
|
+
paddingHorizontal: 3,
|
|
312
|
+
borderRadius: 7,
|
|
313
|
+
backgroundColor: macOSColors.semantic.debug,
|
|
314
|
+
alignItems: "center",
|
|
315
|
+
justifyContent: "center"
|
|
316
|
+
},
|
|
317
|
+
savedBadgeText: {
|
|
318
|
+
color: "#FFFFFF",
|
|
319
|
+
fontSize: 9,
|
|
320
|
+
fontWeight: "700",
|
|
321
|
+
fontFamily: "monospace"
|
|
192
322
|
}
|
|
193
323
|
});
|