@buoy-gg/network 7.0.12 → 7.0.14
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/lib/commonjs/index.js +52 -0
- package/lib/commonjs/network/components/NetworkDetailStepper.js +26 -14
- package/lib/commonjs/network/components/NetworkEventDetailView.js +86 -14
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
- package/lib/commonjs/network/components/NetworkHeaderLive.js +32 -7
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +134 -8
- package/lib/commonjs/network/components/NetworkModal.js +198 -51
- package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
- package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
- package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
- package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
- package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
- package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
- package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
- package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
- package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
- package/lib/commonjs/network/hooks/useNetworkSelection.js +53 -0
- package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
- package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
- package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
- package/lib/commonjs/network/overrides/engine.js +70 -0
- package/lib/commonjs/network/overrides/matchRule.js +81 -0
- package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
- package/lib/commonjs/network/overrides/presets.js +70 -0
- package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
- package/lib/commonjs/network/overrides/types.js +85 -0
- package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
- package/lib/commonjs/network/types/index.js +5 -1
- package/lib/commonjs/network/utils/networkEventStore.js +52 -6
- package/lib/commonjs/network/utils/networkListener.js +90 -10
- package/lib/commonjs/network/utils/networkSelectionStore.js +193 -0
- package/lib/commonjs/preset.js +11 -0
- package/lib/module/index.js +19 -0
- package/lib/module/network/components/NetworkDetailStepper.js +27 -15
- package/lib/module/network/components/NetworkEventDetailView.js +87 -15
- package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
- package/lib/module/network/components/NetworkHeaderLive.js +32 -7
- package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
- package/lib/module/network/components/NetworkListScreen.js +135 -9
- package/lib/module/network/components/NetworkModal.js +200 -53
- package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
- package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
- package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
- package/lib/module/network/components/NetworkOverridesButton.js +85 -0
- package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
- package/lib/module/network/components/response-editor/Explorer.js +972 -0
- package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
- package/lib/module/network/components/response-editor/editorContext.js +29 -0
- package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
- package/lib/module/network/hooks/useNetworkSelection.js +46 -0
- package/lib/module/network/hooks/useOverrideRules.js +32 -0
- package/lib/module/network/overrides/applyToFetch.js +72 -0
- package/lib/module/network/overrides/applyToXhr.js +185 -0
- package/lib/module/network/overrides/engine.js +64 -0
- package/lib/module/network/overrides/matchRule.js +75 -0
- package/lib/module/network/overrides/overrideRulesStore.js +547 -0
- package/lib/module/network/overrides/presets.js +65 -0
- package/lib/module/network/overrides/resolveOverride.js +152 -0
- package/lib/module/network/overrides/types.js +80 -0
- package/lib/module/network/sync/networkSyncAdapter.js +230 -1
- package/lib/module/network/types/index.js +3 -1
- package/lib/module/network/utils/networkEventStore.js +52 -6
- package/lib/module/network/utils/networkListener.js +91 -10
- package/lib/module/network/utils/networkSelectionStore.js +188 -0
- package/lib/module/preset.js +13 -1
- package/lib/typescript/index.d.ts +9 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkDetailStepper.d.ts +8 -5
- package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts +36 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts +13 -3
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
- package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
- package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
- package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
- package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
- package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
- package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
- package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
- package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
- package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
- package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkSelection.d.ts +26 -0
- package/lib/typescript/network/hooks/useNetworkSelection.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
- package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
- package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
- package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
- package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
- package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
- package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
- package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
- package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
- package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
- package/lib/typescript/network/overrides/engine.d.ts +47 -0
- package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
- package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
- package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
- package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
- package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
- package/lib/typescript/network/overrides/presets.d.ts +38 -0
- package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
- package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
- package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
- package/lib/typescript/network/overrides/types.d.ts +169 -0
- package/lib/typescript/network/overrides/types.d.ts.map +1 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +10 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +3 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSelectionStore.d.ts +117 -0
- package/lib/typescript/network/utils/networkSelectionStore.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { StyleSheet, Text, TouchableOpacity } from "react-native";
|
|
4
|
+
import { macOSColors } from "@buoy-gg/shared-ui";
|
|
5
|
+
import { useOverrideRules } from "../hooks/useOverrideRules";
|
|
6
|
+
import { useSelectedNetworkEventId } from "../hooks/useNetworkSelection";
|
|
7
|
+
import { ruleMatches } from "../overrides/matchRule";
|
|
8
|
+
import { draftFromEvent } from "../overrides/presets";
|
|
9
|
+
import { networkEventStore } from "../utils/networkEventStore";
|
|
10
|
+
import { networkSavedStore } from "../utils/networkSavedStore";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Override action, in the request-detail header.
|
|
14
|
+
*
|
|
15
|
+
* A labelled word rather than an icon: every other control here is a glyph
|
|
16
|
+
* (back, pin, bookmark) and glyphs are fine for things you already know. This
|
|
17
|
+
* one is the least familiar action in the tool, and "Override" spelled out is
|
|
18
|
+
* the entire explanation.
|
|
19
|
+
*
|
|
20
|
+
* A self-subscribing leaf, for the same two reasons `NetworkDetailPinActions`
|
|
21
|
+
* is one: the modal root stays free of subscriptions, and the selected id is
|
|
22
|
+
* subscribed here so stepping between requests doesn't rebuild the header
|
|
23
|
+
* around a new prop.
|
|
24
|
+
*
|
|
25
|
+
* It lights up while a rule covers the request you're looking at — the same
|
|
26
|
+
* warning tint the OVERRIDDEN strip and the toolbar flask use, because "you are
|
|
27
|
+
* not seeing the truth" is one idea and should look like one.
|
|
28
|
+
*
|
|
29
|
+
* Tapping it goes straight to the Overrides screen with the rule open: a new
|
|
30
|
+
* one prefilled from this request, or the existing one if something already
|
|
31
|
+
* covers it. One destination, so there's never a question of where the thing
|
|
32
|
+
* you just made went.
|
|
33
|
+
*/
|
|
34
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
35
|
+
export function NetworkOverrideHeaderButton({
|
|
36
|
+
onOpen
|
|
37
|
+
}) {
|
|
38
|
+
const eventId = useSelectedNetworkEventId();
|
|
39
|
+
const {
|
|
40
|
+
rules,
|
|
41
|
+
isRuleSpent
|
|
42
|
+
} = useOverrideRules();
|
|
43
|
+
|
|
44
|
+
// Resolved here rather than held as a prop: the live store has the fresh copy
|
|
45
|
+
// while a request is in flight, and the saved store is the only copy once it
|
|
46
|
+
// has been cleared or aged out.
|
|
47
|
+
const event = eventId ? networkEventStore.getEvents().find(e => e.id === eventId) ?? networkSavedStore.getEventById(eventId) : null;
|
|
48
|
+
const existing = event ? rules.find(rule => rule.enabled && !isRuleSpent(rule) && ruleMatches(rule, event.url, String(event.method || "GET"))) ?? null : null;
|
|
49
|
+
const active = existing !== null;
|
|
50
|
+
const handlePress = () => {
|
|
51
|
+
if (!event) return;
|
|
52
|
+
if (existing) {
|
|
53
|
+
// Editing what's already there, rather than stacking a second rule the
|
|
54
|
+
// first would shadow — first enabled match wins.
|
|
55
|
+
const {
|
|
56
|
+
id,
|
|
57
|
+
hits: _h,
|
|
58
|
+
seen: _s,
|
|
59
|
+
createdAt: _c,
|
|
60
|
+
...draft
|
|
61
|
+
} = existing;
|
|
62
|
+
onOpen(draft, id);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
onOpen(draftFromEvent(event));
|
|
66
|
+
};
|
|
67
|
+
return /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
68
|
+
onPress: handlePress,
|
|
69
|
+
"sentry-label": "ignore open overrides",
|
|
70
|
+
accessibilityLabel: active ? "Edit the override on this request" : "Override this request",
|
|
71
|
+
style: [styles.button, active && styles.buttonActive],
|
|
72
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
73
|
+
style: [styles.label, active && styles.labelActive],
|
|
74
|
+
children: "Override"
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const styles = StyleSheet.create({
|
|
79
|
+
button: {
|
|
80
|
+
paddingHorizontal: 10,
|
|
81
|
+
paddingVertical: 6,
|
|
82
|
+
borderRadius: 6,
|
|
83
|
+
borderWidth: 1,
|
|
84
|
+
borderColor: macOSColors.border.input,
|
|
85
|
+
backgroundColor: macOSColors.background.input
|
|
86
|
+
},
|
|
87
|
+
buttonActive: {
|
|
88
|
+
borderColor: macOSColors.semantic.warning + "77",
|
|
89
|
+
backgroundColor: macOSColors.semantic.warningBackground
|
|
90
|
+
},
|
|
91
|
+
label: {
|
|
92
|
+
fontSize: 11,
|
|
93
|
+
fontWeight: "700",
|
|
94
|
+
color: macOSColors.text.primary
|
|
95
|
+
},
|
|
96
|
+
labelActive: {
|
|
97
|
+
color: macOSColors.semantic.warning
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
4
|
+
import { FlaskConical, macOSColors } from "@buoy-gg/shared-ui";
|
|
5
|
+
import { useOverrideRules } from "../hooks/useOverrideRules";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Header button for the Overrides screen, and the always-visible "overrides
|
|
9
|
+
* are active" indicator.
|
|
10
|
+
*
|
|
11
|
+
* A self-subscribing LEAF, like the other live header pieces: the rule list
|
|
12
|
+
* changes on its own schedule (every hit bumps a counter) and the modal shell
|
|
13
|
+
* has no business re-rendering for it.
|
|
14
|
+
*
|
|
15
|
+
* The active tint is not decoration. A rule left enabled from yesterday makes
|
|
16
|
+
* the app behave wrongly in a way that looks exactly like a real bug, so the
|
|
17
|
+
* tool has to keep saying so from the top level — Chrome shows the same
|
|
18
|
+
* warning in its Overrides pane for the same reason.
|
|
19
|
+
*/
|
|
20
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
21
|
+
export function NetworkOverridesButton({
|
|
22
|
+
onPress
|
|
23
|
+
}) {
|
|
24
|
+
const {
|
|
25
|
+
activeCount
|
|
26
|
+
} = useOverrideRules();
|
|
27
|
+
const active = activeCount > 0;
|
|
28
|
+
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
29
|
+
onPress: onPress,
|
|
30
|
+
"sentry-label": "ignore open overrides",
|
|
31
|
+
style: [styles.button, active && styles.buttonActive],
|
|
32
|
+
accessibilityLabel: active ? `Response overrides — ${activeCount} active` : "Response overrides",
|
|
33
|
+
children: [/*#__PURE__*/_jsx(FlaskConical, {
|
|
34
|
+
size: 14,
|
|
35
|
+
color: active ? macOSColors.semantic.warning : macOSColors.text.secondary
|
|
36
|
+
}), active ? /*#__PURE__*/_jsx(View, {
|
|
37
|
+
style: styles.badge,
|
|
38
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
39
|
+
style: styles.badgeText,
|
|
40
|
+
children: activeCount
|
|
41
|
+
})
|
|
42
|
+
}) : null]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const styles = StyleSheet.create({
|
|
46
|
+
// The header's standard 32×32 action box — same shape, fill, border and
|
|
47
|
+
// radius as search/power/clear. It was a bare padded glyph, which read as a
|
|
48
|
+
// different CLASS of control than its neighbours when it is exactly the same
|
|
49
|
+
// kind of thing.
|
|
50
|
+
button: {
|
|
51
|
+
width: 32,
|
|
52
|
+
height: 32,
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
backgroundColor: macOSColors.background.hover,
|
|
57
|
+
borderWidth: 1,
|
|
58
|
+
borderColor: macOSColors.border.default
|
|
59
|
+
},
|
|
60
|
+
// Active fills the whole box, the way the power toggle goes green — the
|
|
61
|
+
// warning now reads at a glance instead of as a tinted glyph.
|
|
62
|
+
buttonActive: {
|
|
63
|
+
backgroundColor: macOSColors.semantic.warningBackground,
|
|
64
|
+
borderColor: macOSColors.semantic.warning + "77"
|
|
65
|
+
},
|
|
66
|
+
badge: {
|
|
67
|
+
position: "absolute",
|
|
68
|
+
// Inside the box now that there is one; a corner-hung badge would clip
|
|
69
|
+
// against the neighbouring buttons' 8pt gap.
|
|
70
|
+
top: 1,
|
|
71
|
+
right: 1,
|
|
72
|
+
minWidth: 12,
|
|
73
|
+
height: 12,
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
paddingHorizontal: 3,
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
justifyContent: "center",
|
|
78
|
+
backgroundColor: macOSColors.semantic.warning
|
|
79
|
+
},
|
|
80
|
+
badgeText: {
|
|
81
|
+
fontSize: 8,
|
|
82
|
+
fontWeight: "800",
|
|
83
|
+
color: macOSColors.background.base
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
5
|
+
import { AlertTriangle, EmptyState, FlaskConical, Plus, macOSColors } from "@buoy-gg/shared-ui";
|
|
6
|
+
import { useOverrideRules } from "../hooks/useOverrideRules";
|
|
7
|
+
import { NetworkOverrideEditor } from "./NetworkOverrideEditor";
|
|
8
|
+
import { overrideRulesStore } from "../overrides/overrideRulesStore";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The rule manager: what's armed, and how to stop all of it.
|
|
12
|
+
*
|
|
13
|
+
* One bar across the top is both the warning and the switch. It used to be two
|
|
14
|
+
* things — an ON/OFF pill beside a title, and a separate banner saying the same
|
|
15
|
+
* thing in different words — which left the pill reading as though it applied
|
|
16
|
+
* to the selected rule. A state pill is ambiguous; a verb button isn't.
|
|
17
|
+
*/
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
function blankDraft() {
|
|
20
|
+
return {
|
|
21
|
+
enabled: true,
|
|
22
|
+
urlPattern: "",
|
|
23
|
+
kind: "respond",
|
|
24
|
+
status: 500,
|
|
25
|
+
body: "",
|
|
26
|
+
times: null
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function toDraft(rule) {
|
|
30
|
+
const {
|
|
31
|
+
id: _id,
|
|
32
|
+
hits: _hits,
|
|
33
|
+
seen: _seen,
|
|
34
|
+
createdAt: _createdAt,
|
|
35
|
+
...draft
|
|
36
|
+
} = rule;
|
|
37
|
+
return draft;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Plain-language names for statuses, so a row never reads as a bare number. */
|
|
41
|
+
const STATUS_NAMES = {
|
|
42
|
+
200: "Success",
|
|
43
|
+
201: "Created",
|
|
44
|
+
400: "Bad request",
|
|
45
|
+
401: "Unauthorized",
|
|
46
|
+
403: "Forbidden",
|
|
47
|
+
404: "Not found",
|
|
48
|
+
429: "Rate limited",
|
|
49
|
+
500: "Server error",
|
|
50
|
+
503: "Unavailable"
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** What the rule DOES, in the same words the editor uses. */
|
|
54
|
+
function outcomeLine(rule) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (rule.kind === "respond") {
|
|
57
|
+
const status = rule.status ?? 200;
|
|
58
|
+
const name = STATUS_NAMES[status];
|
|
59
|
+
parts.push(name ? `${name} ${status}` : String(status));
|
|
60
|
+
} else if (rule.kind === "fail") {
|
|
61
|
+
parts.push(rule.failKind === "timeout" ? "Timeout" : "Offline");
|
|
62
|
+
} else {
|
|
63
|
+
parts.push("Real response");
|
|
64
|
+
}
|
|
65
|
+
if (rule.delayMs) parts.push(`${rule.delayMs / 1000}s`);
|
|
66
|
+
if (rule.alternate) parts.push("every other");else if (rule.times === 1) parts.push("once");else if (typeof rule.times === "number") parts.push(`${rule.times}×`);
|
|
67
|
+
return parts.join(" · ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where the rule applies — PATH only, not the whole URL.
|
|
72
|
+
*
|
|
73
|
+
* The host is the same for every rule in a typical app, so showing it pushes
|
|
74
|
+
* the part that differs off the end of the line.
|
|
75
|
+
*/
|
|
76
|
+
function scopeLine(rule) {
|
|
77
|
+
const method = rule.methods?.length ? rule.methods.join("/") : "Any";
|
|
78
|
+
let path = rule.urlPattern;
|
|
79
|
+
const schemeEnd = path.indexOf("://");
|
|
80
|
+
if (schemeEnd !== -1) {
|
|
81
|
+
const slash = path.indexOf("/", schemeEnd + 3);
|
|
82
|
+
path = slash === -1 ? path.slice(schemeEnd + 3) : path.slice(slash);
|
|
83
|
+
}
|
|
84
|
+
return `${method} ${path}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One rule. Toggle · two lines · hit count, and nothing else.
|
|
89
|
+
*
|
|
90
|
+
* The pencil and trash icons are gone: the row itself opens the editor (where
|
|
91
|
+
* Delete now lives), and three tap targets in a 44pt row is a config table, not
|
|
92
|
+
* a list. `hits` stays as the only right-hand element because "my glob never
|
|
93
|
+
* matched" is the most common rule bug and the count is how you find out.
|
|
94
|
+
*/
|
|
95
|
+
function RuleRow({
|
|
96
|
+
rule,
|
|
97
|
+
spent,
|
|
98
|
+
onToggle,
|
|
99
|
+
onEdit
|
|
100
|
+
}) {
|
|
101
|
+
const live = rule.enabled && !spent;
|
|
102
|
+
const accent = live ? macOSColors.semantic.warning : macOSColors.text.muted;
|
|
103
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
104
|
+
style: styles.row,
|
|
105
|
+
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
106
|
+
onPress: onToggle,
|
|
107
|
+
"sentry-label": "ignore toggle override rule",
|
|
108
|
+
accessibilityLabel: live ? "Turn rule off" : "Turn rule on",
|
|
109
|
+
style: [styles.toggle, live && {
|
|
110
|
+
borderColor: accent,
|
|
111
|
+
backgroundColor: `${accent}33`
|
|
112
|
+
}],
|
|
113
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
114
|
+
style: [styles.toggleDot, {
|
|
115
|
+
backgroundColor: accent
|
|
116
|
+
}]
|
|
117
|
+
})
|
|
118
|
+
}), /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
119
|
+
onPress: onEdit,
|
|
120
|
+
"sentry-label": "ignore edit override rule",
|
|
121
|
+
style: styles.rowBody,
|
|
122
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
123
|
+
style: [styles.rowTitle, !live && styles.rowTitleOff],
|
|
124
|
+
numberOfLines: 1,
|
|
125
|
+
children: outcomeLine(rule)
|
|
126
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
127
|
+
style: styles.rowScope,
|
|
128
|
+
numberOfLines: 1,
|
|
129
|
+
children: scopeLine(rule)
|
|
130
|
+
})]
|
|
131
|
+
}), rule.hits > 0 ? /*#__PURE__*/_jsx(Text, {
|
|
132
|
+
style: styles.rowHits,
|
|
133
|
+
children: rule.hits
|
|
134
|
+
}) : null]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Stable-router gate.
|
|
139
|
+
*
|
|
140
|
+
* The screens in `NetworkModal` are rendered as permanent siblings, and the
|
|
141
|
+
* convention there is that a HIDDEN screen must not subscribe to anything.
|
|
142
|
+
* Splitting the body out is how this one honours that: `useOverrideRules`
|
|
143
|
+
* subscribes to a store that ticks while rules are firing, and a hidden screen
|
|
144
|
+
* re-rendering for that would be pure waste. (The header's overrides button is
|
|
145
|
+
* a deliberate exception — it's always visible and its whole job is to react.)
|
|
146
|
+
*/
|
|
147
|
+
export function NetworkOverridesScreen(props) {
|
|
148
|
+
if (!props.visible) return null;
|
|
149
|
+
return /*#__PURE__*/_jsx(OverridesBody, {
|
|
150
|
+
...props
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function OverridesBody({
|
|
154
|
+
isPro,
|
|
155
|
+
onUpgradePress,
|
|
156
|
+
pendingDraft,
|
|
157
|
+
onPendingConsumed,
|
|
158
|
+
applySafeAreaInset
|
|
159
|
+
}) {
|
|
160
|
+
const {
|
|
161
|
+
enabled,
|
|
162
|
+
autoPaused,
|
|
163
|
+
rules,
|
|
164
|
+
activeCount,
|
|
165
|
+
setEnabled,
|
|
166
|
+
upsertRule,
|
|
167
|
+
setRuleEnabled,
|
|
168
|
+
removeRule,
|
|
169
|
+
isRuleSpent
|
|
170
|
+
} = useOverrideRules();
|
|
171
|
+
const [editing, setEditing] = useState(null);
|
|
172
|
+
|
|
173
|
+
// Reaching this screen proves the app renders, so the launch-safety streak
|
|
174
|
+
// has nothing to guard against — see `noteHealthyLaunch`.
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
overrideRulesStore.noteHealthyLaunch();
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Open a rule for editing, pulling its body over if this surface doesn't have
|
|
181
|
+
* it.
|
|
182
|
+
*
|
|
183
|
+
* On the device that's already the case and the fetch resolves synchronously.
|
|
184
|
+
* On the dashboard a large body is left out of the snapshot on purpose (see
|
|
185
|
+
* `stripLargeRuleBodies`), so it arrives a moment after the editor does — the
|
|
186
|
+
* `id` re-check keeps a slow reply from landing in a rule you've since moved
|
|
187
|
+
* on from.
|
|
188
|
+
*/
|
|
189
|
+
const hydrateBody = useCallback(id => {
|
|
190
|
+
void overrideRulesStore.fetchRuleBody(id).then(body => {
|
|
191
|
+
if (body == null) return;
|
|
192
|
+
setEditing(current => current?.id === id ? {
|
|
193
|
+
...current,
|
|
194
|
+
draft: {
|
|
195
|
+
...current.draft,
|
|
196
|
+
body,
|
|
197
|
+
bodyOmitted: undefined
|
|
198
|
+
}
|
|
199
|
+
} : current);
|
|
200
|
+
});
|
|
201
|
+
}, []);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!pendingDraft) return;
|
|
204
|
+
const {
|
|
205
|
+
id,
|
|
206
|
+
...draft
|
|
207
|
+
} = pendingDraft;
|
|
208
|
+
setEditing({
|
|
209
|
+
id,
|
|
210
|
+
draft
|
|
211
|
+
});
|
|
212
|
+
// Same reason as `openRule`: the detail page's Override button hands over an
|
|
213
|
+
// existing rule, and on the dashboard that rule's body may not be here yet.
|
|
214
|
+
if (id && draft.bodyOmitted) hydrateBody(id);
|
|
215
|
+
onPendingConsumed?.();
|
|
216
|
+
}, [pendingDraft, onPendingConsumed, hydrateBody]);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Leave the editor when the rule it's editing stops existing.
|
|
220
|
+
*
|
|
221
|
+
* Rules can be cleared from somewhere else — the dashboard, an MCP agent —
|
|
222
|
+
* while this editor is open. Without this, Save would upsert the old id and
|
|
223
|
+
* quietly RESURRECT a rule someone had just deleted. Only guards existing
|
|
224
|
+
* rules; a draft with no id yet is a new rule and has nothing to lose.
|
|
225
|
+
*/
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!editing?.id) return;
|
|
228
|
+
if (rules.some(rule => rule.id === editing.id)) return;
|
|
229
|
+
setEditing(null);
|
|
230
|
+
}, [editing?.id, rules]);
|
|
231
|
+
const openRule = rule => {
|
|
232
|
+
setEditing({
|
|
233
|
+
id: rule.id,
|
|
234
|
+
draft: toDraft(rule)
|
|
235
|
+
});
|
|
236
|
+
if (rule.bodyOmitted) hydrateBody(rule.id);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* On the free tier a new rule REPLACES the live one rather than being refused.
|
|
241
|
+
*
|
|
242
|
+
* The old behaviour put an upgrade modal in the action path: you tapped a
|
|
243
|
+
* thing and got a sales pitch instead of the thing. Nobody buys from a modal
|
|
244
|
+
* that just cancelled their action, and it hid the feature behind a wall
|
|
245
|
+
* instead of letting you feel its shape. "One override at a time" is a shape
|
|
246
|
+
* a developer understands.
|
|
247
|
+
*/
|
|
248
|
+
const enforceFreeTier = keepId => {
|
|
249
|
+
if (isPro) return;
|
|
250
|
+
rules.filter(rule => rule.enabled && !isRuleSpent(rule) && rule.id !== keepId).forEach(rule => setRuleEnabled(rule.id, false));
|
|
251
|
+
};
|
|
252
|
+
const handleSave = () => {
|
|
253
|
+
if (!editing) return;
|
|
254
|
+
const draft = {
|
|
255
|
+
...editing.draft,
|
|
256
|
+
urlPattern: editing.draft.urlPattern.trim()
|
|
257
|
+
};
|
|
258
|
+
if (!draft.urlPattern) return;
|
|
259
|
+
const saved = upsertRule({
|
|
260
|
+
...draft,
|
|
261
|
+
id: editing.id
|
|
262
|
+
});
|
|
263
|
+
if (draft.enabled) {
|
|
264
|
+
enforceFreeTier(saved?.id ?? editing.id);
|
|
265
|
+
// Arm the master switch. Deliberately creating a rule and having it sit
|
|
266
|
+
// inert because overrides happen to be paused is the silent-failure this
|
|
267
|
+
// feature can least afford — the preset path already does this, and the
|
|
268
|
+
// manager disagreeing with it was just a gap.
|
|
269
|
+
setEnabled(true);
|
|
270
|
+
}
|
|
271
|
+
setEditing(null);
|
|
272
|
+
};
|
|
273
|
+
const handleToggle = rule => {
|
|
274
|
+
const next = !rule.enabled;
|
|
275
|
+
setRuleEnabled(rule.id, next);
|
|
276
|
+
if (next) enforceFreeTier(rule.id);
|
|
277
|
+
};
|
|
278
|
+
if (editing) {
|
|
279
|
+
return /*#__PURE__*/_jsx(NetworkOverrideEditor, {
|
|
280
|
+
draft: editing.draft,
|
|
281
|
+
onChange: draft => setEditing({
|
|
282
|
+
...editing,
|
|
283
|
+
draft
|
|
284
|
+
}),
|
|
285
|
+
onSave: handleSave,
|
|
286
|
+
onCancel: () => setEditing(null),
|
|
287
|
+
saveLabel: editing.id ? "Save" : "Add",
|
|
288
|
+
applySafeAreaInset: applySafeAreaInset
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const hasRules = rules.length > 0;
|
|
292
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
293
|
+
style: styles.root,
|
|
294
|
+
children: [hasRules ? /*#__PURE__*/_jsxs(View, {
|
|
295
|
+
style: [styles.bar, enabled && activeCount > 0 && styles.barOn],
|
|
296
|
+
children: [enabled && activeCount > 0 ? /*#__PURE__*/_jsx(AlertTriangle, {
|
|
297
|
+
size: 12,
|
|
298
|
+
color: macOSColors.semantic.warning
|
|
299
|
+
}) : null, /*#__PURE__*/_jsx(Text, {
|
|
300
|
+
style: [styles.barText, enabled && activeCount > 0 && styles.barTextOn],
|
|
301
|
+
numberOfLines: 2,
|
|
302
|
+
children: autoPaused ? "Overrides paused — they'd been on for 3 launches" : enabled && activeCount > 0 ? `${activeCount} override${activeCount === 1 ? " is" : "s are"} on` : "Overrides are off"
|
|
303
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
304
|
+
onPress: () => setEnabled(!(enabled && activeCount > 0)),
|
|
305
|
+
"sentry-label": "ignore toggle overrides master",
|
|
306
|
+
style: styles.barButton,
|
|
307
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
308
|
+
style: styles.barButtonText,
|
|
309
|
+
children: enabled && activeCount > 0 ? "Turn all off" : autoPaused ? "Turn back on" : "Turn on"
|
|
310
|
+
})
|
|
311
|
+
})]
|
|
312
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
313
|
+
style: styles.listHeader,
|
|
314
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
315
|
+
style: styles.listHeaderText,
|
|
316
|
+
children: hasRules ? `${rules.length} rule${rules.length === 1 ? "" : "s"}` : ""
|
|
317
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
318
|
+
onPress: () => setEditing({
|
|
319
|
+
draft: blankDraft()
|
|
320
|
+
}),
|
|
321
|
+
"sentry-label": "ignore new override rule",
|
|
322
|
+
accessibilityLabel: "New override rule",
|
|
323
|
+
style: styles.newButton,
|
|
324
|
+
children: /*#__PURE__*/_jsx(Plus, {
|
|
325
|
+
size: 14,
|
|
326
|
+
color: macOSColors.semantic.info
|
|
327
|
+
})
|
|
328
|
+
})]
|
|
329
|
+
}), !hasRules ? /*#__PURE__*/_jsx(View, {
|
|
330
|
+
style: styles.emptyWrap,
|
|
331
|
+
children: /*#__PURE__*/_jsx(EmptyState, {
|
|
332
|
+
icon: FlaskConical,
|
|
333
|
+
title: "Nothing overridden"
|
|
334
|
+
// Names the control that actually exists. This said "tap Force 500"
|
|
335
|
+
// — a button from the deleted preset list, which sent anyone
|
|
336
|
+
// following it looking for something that isn't there.
|
|
337
|
+
,
|
|
338
|
+
description: "Open any request and tap Override, or + to write a rule."
|
|
339
|
+
})
|
|
340
|
+
}) : /*#__PURE__*/_jsxs(ScrollView, {
|
|
341
|
+
style: styles.list,
|
|
342
|
+
contentContainerStyle: styles.listContent,
|
|
343
|
+
keyboardShouldPersistTaps: "handled",
|
|
344
|
+
children: [rules.map(rule => /*#__PURE__*/_jsx(RuleRow, {
|
|
345
|
+
rule: rule,
|
|
346
|
+
spent: isRuleSpent(rule),
|
|
347
|
+
onToggle: () => handleToggle(rule),
|
|
348
|
+
onEdit: () => openRule(rule)
|
|
349
|
+
}, rule.id)), /*#__PURE__*/_jsx(Text, {
|
|
350
|
+
style: styles.footNote,
|
|
351
|
+
children: isPro ? "Dev builds only." : "Dev builds only. Free: one override at a time. Pro: up to 50."
|
|
352
|
+
})]
|
|
353
|
+
})]
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const styles = StyleSheet.create({
|
|
357
|
+
root: {
|
|
358
|
+
flex: 1
|
|
359
|
+
},
|
|
360
|
+
bar: {
|
|
361
|
+
flexDirection: "row",
|
|
362
|
+
alignItems: "center",
|
|
363
|
+
gap: 8,
|
|
364
|
+
paddingHorizontal: 12,
|
|
365
|
+
paddingVertical: 10,
|
|
366
|
+
borderBottomWidth: 1,
|
|
367
|
+
borderBottomColor: macOSColors.border.default,
|
|
368
|
+
backgroundColor: macOSColors.background.card
|
|
369
|
+
},
|
|
370
|
+
barOn: {
|
|
371
|
+
borderBottomColor: macOSColors.semantic.warning + "44",
|
|
372
|
+
backgroundColor: macOSColors.semantic.warningBackground
|
|
373
|
+
},
|
|
374
|
+
barText: {
|
|
375
|
+
flex: 1,
|
|
376
|
+
fontSize: 12,
|
|
377
|
+
fontWeight: "600",
|
|
378
|
+
color: macOSColors.text.secondary
|
|
379
|
+
},
|
|
380
|
+
barTextOn: {
|
|
381
|
+
color: macOSColors.semantic.warning
|
|
382
|
+
},
|
|
383
|
+
barButton: {
|
|
384
|
+
paddingHorizontal: 10,
|
|
385
|
+
paddingVertical: 6,
|
|
386
|
+
borderRadius: 5,
|
|
387
|
+
borderWidth: 1,
|
|
388
|
+
borderColor: macOSColors.border.hover,
|
|
389
|
+
backgroundColor: macOSColors.background.base
|
|
390
|
+
},
|
|
391
|
+
barButtonText: {
|
|
392
|
+
fontSize: 11,
|
|
393
|
+
fontWeight: "700",
|
|
394
|
+
color: macOSColors.text.primary
|
|
395
|
+
},
|
|
396
|
+
listHeader: {
|
|
397
|
+
flexDirection: "row",
|
|
398
|
+
alignItems: "center",
|
|
399
|
+
justifyContent: "space-between",
|
|
400
|
+
paddingHorizontal: 12,
|
|
401
|
+
paddingTop: 10,
|
|
402
|
+
paddingBottom: 4
|
|
403
|
+
},
|
|
404
|
+
listHeaderText: {
|
|
405
|
+
fontSize: 10,
|
|
406
|
+
fontWeight: "700",
|
|
407
|
+
letterSpacing: 0.6,
|
|
408
|
+
color: macOSColors.text.muted,
|
|
409
|
+
textTransform: "uppercase"
|
|
410
|
+
},
|
|
411
|
+
newButton: {
|
|
412
|
+
padding: 6,
|
|
413
|
+
borderRadius: 5,
|
|
414
|
+
borderWidth: 1,
|
|
415
|
+
borderColor: macOSColors.border.default,
|
|
416
|
+
backgroundColor: macOSColors.background.card
|
|
417
|
+
},
|
|
418
|
+
emptyWrap: {
|
|
419
|
+
flex: 1,
|
|
420
|
+
justifyContent: "center"
|
|
421
|
+
},
|
|
422
|
+
list: {
|
|
423
|
+
flex: 1
|
|
424
|
+
},
|
|
425
|
+
listContent: {
|
|
426
|
+
padding: 10,
|
|
427
|
+
gap: 6
|
|
428
|
+
},
|
|
429
|
+
row: {
|
|
430
|
+
flexDirection: "row",
|
|
431
|
+
alignItems: "center",
|
|
432
|
+
gap: 10,
|
|
433
|
+
paddingHorizontal: 10,
|
|
434
|
+
paddingVertical: 10,
|
|
435
|
+
borderRadius: 6,
|
|
436
|
+
borderWidth: 1,
|
|
437
|
+
borderColor: macOSColors.border.default,
|
|
438
|
+
backgroundColor: macOSColors.background.card
|
|
439
|
+
},
|
|
440
|
+
toggle: {
|
|
441
|
+
width: 22,
|
|
442
|
+
height: 22,
|
|
443
|
+
borderRadius: 11,
|
|
444
|
+
borderWidth: 1,
|
|
445
|
+
borderColor: macOSColors.border.default,
|
|
446
|
+
alignItems: "center",
|
|
447
|
+
justifyContent: "center"
|
|
448
|
+
},
|
|
449
|
+
toggleDot: {
|
|
450
|
+
width: 8,
|
|
451
|
+
height: 8,
|
|
452
|
+
borderRadius: 4
|
|
453
|
+
},
|
|
454
|
+
rowBody: {
|
|
455
|
+
flex: 1
|
|
456
|
+
},
|
|
457
|
+
rowTitle: {
|
|
458
|
+
fontSize: 12,
|
|
459
|
+
fontWeight: "600",
|
|
460
|
+
color: macOSColors.text.primary
|
|
461
|
+
},
|
|
462
|
+
rowTitleOff: {
|
|
463
|
+
color: macOSColors.text.muted
|
|
464
|
+
},
|
|
465
|
+
rowScope: {
|
|
466
|
+
fontSize: 10,
|
|
467
|
+
fontFamily: "monospace",
|
|
468
|
+
color: macOSColors.text.secondary,
|
|
469
|
+
marginTop: 2
|
|
470
|
+
},
|
|
471
|
+
rowHits: {
|
|
472
|
+
fontSize: 11,
|
|
473
|
+
fontWeight: "700",
|
|
474
|
+
color: macOSColors.semantic.warning,
|
|
475
|
+
minWidth: 16,
|
|
476
|
+
textAlign: "right"
|
|
477
|
+
},
|
|
478
|
+
footNote: {
|
|
479
|
+
fontSize: 10,
|
|
480
|
+
lineHeight: 14,
|
|
481
|
+
color: macOSColors.text.muted,
|
|
482
|
+
paddingHorizontal: 4,
|
|
483
|
+
paddingTop: 8
|
|
484
|
+
}
|
|
485
|
+
});
|