react-native-network-lens 0.0.0-stage → 0.1.1
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/LICENSE +20 -0
- package/README.md +208 -2
- package/lib/module/backHandler.js +31 -0
- package/lib/module/backHandler.js.map +1 -0
- package/lib/module/components/BodyViewer.js +213 -0
- package/lib/module/components/BodyViewer.js.map +1 -0
- package/lib/module/components/CodeList.js +104 -0
- package/lib/module/components/CodeList.js.map +1 -0
- package/lib/module/components/JsonTree/JsonTreeView.js +206 -0
- package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -0
- package/lib/module/components/JsonTree/model.js +196 -0
- package/lib/module/components/JsonTree/model.js.map +1 -0
- package/lib/module/components/NetworkLens.js +110 -0
- package/lib/module/components/NetworkLens.js.map +1 -0
- package/lib/module/components/NetworkLensOverlay.js +158 -0
- package/lib/module/components/NetworkLensOverlay.js.map +1 -0
- package/lib/module/components/RawTextView.js +119 -0
- package/lib/module/components/RawTextView.js.map +1 -0
- package/lib/module/components/RequestDetails/RequestDetails.js +256 -0
- package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -0
- package/lib/module/components/RequestList/FilterBar.js +138 -0
- package/lib/module/components/RequestList/FilterBar.js.map +1 -0
- package/lib/module/components/RequestList/RequestList.js +163 -0
- package/lib/module/components/RequestList/RequestList.js.map +1 -0
- package/lib/module/components/RequestList/RequestRow.js +161 -0
- package/lib/module/components/RequestList/RequestRow.js.map +1 -0
- package/lib/module/components/RequestList/filters.js +57 -0
- package/lib/module/components/RequestList/filters.js.map +1 -0
- package/lib/module/components/ui/ActionSheet.js +94 -0
- package/lib/module/components/ui/ActionSheet.js.map +1 -0
- package/lib/module/components/ui/Button.js +104 -0
- package/lib/module/components/ui/Button.js.map +1 -0
- package/lib/module/components/ui/Pill.js +53 -0
- package/lib/module/components/ui/Pill.js.map +1 -0
- package/lib/module/components/ui/SearchInput.js +77 -0
- package/lib/module/components/ui/SearchInput.js.map +1 -0
- package/lib/module/components/ui/Section.js +147 -0
- package/lib/module/components/ui/Section.js.map +1 -0
- package/lib/module/components/ui/SegmentedControl.js +65 -0
- package/lib/module/components/ui/SegmentedControl.js.map +1 -0
- package/lib/module/components/ui/index.js +9 -0
- package/lib/module/components/ui/index.js.map +1 -0
- package/lib/module/core/Logger.js +230 -0
- package/lib/module/core/Logger.js.map +1 -0
- package/lib/module/core/NetworkRequest.js +155 -0
- package/lib/module/core/NetworkRequest.js.map +1 -0
- package/lib/module/core/XHRInterceptor.js +104 -0
- package/lib/module/core/XHRInterceptor.js.map +1 -0
- package/lib/module/core/har.js +72 -0
- package/lib/module/core/har.js.map +1 -0
- package/lib/module/core/loggerSingleton.js +12 -0
- package/lib/module/core/loggerSingleton.js.map +1 -0
- package/lib/module/hooks.js +42 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +34 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/theme.js +136 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/body.js +179 -0
- package/lib/module/utils/body.js.map +1 -0
- package/lib/module/utils/debounce.js +18 -0
- package/lib/module/utils/debounce.js.map +1 -0
- package/lib/module/utils/format.js +45 -0
- package/lib/module/utils/format.js.map +1 -0
- package/lib/module/utils/logger.js +6 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/search.js +70 -0
- package/lib/module/utils/search.js.map +1 -0
- package/lib/module/utils/url.js +50 -0
- package/lib/module/utils/url.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/backHandler.d.ts +15 -0
- package/lib/typescript/src/backHandler.d.ts.map +1 -0
- package/lib/typescript/src/components/BodyViewer.d.ts +16 -0
- package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -0
- package/lib/typescript/src/components/CodeList.d.ts +21 -0
- package/lib/typescript/src/components/CodeList.d.ts.map +1 -0
- package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts +21 -0
- package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -0
- package/lib/typescript/src/components/JsonTree/model.d.ts +53 -0
- package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -0
- package/lib/typescript/src/components/NetworkLens.d.ts +31 -0
- package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -0
- package/lib/typescript/src/components/NetworkLensOverlay.d.ts +32 -0
- package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/RawTextView.d.ts +14 -0
- package/lib/typescript/src/components/RawTextView.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts +9 -0
- package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/FilterBar.d.ts +12 -0
- package/lib/typescript/src/components/RequestList/FilterBar.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/RequestList.d.ts +18 -0
- package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/RequestRow.d.ts +11 -0
- package/lib/typescript/src/components/RequestList/RequestRow.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/filters.d.ts +17 -0
- package/lib/typescript/src/components/RequestList/filters.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ActionSheet.d.ts +15 -0
- package/lib/typescript/src/components/ui/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Button.d.ts +21 -0
- package/lib/typescript/src/components/ui/Button.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Pill.d.ts +11 -0
- package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SearchInput.d.ts +17 -0
- package/lib/typescript/src/components/ui/SearchInput.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Section.d.ts +16 -0
- package/lib/typescript/src/components/ui/Section.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SegmentedControl.d.ts +14 -0
- package/lib/typescript/src/components/ui/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/index.d.ts +7 -0
- package/lib/typescript/src/components/ui/index.d.ts.map +1 -0
- package/lib/typescript/src/core/Logger.d.ts +48 -0
- package/lib/typescript/src/core/Logger.d.ts.map +1 -0
- package/lib/typescript/src/core/NetworkRequest.d.ts +48 -0
- package/lib/typescript/src/core/NetworkRequest.d.ts.map +1 -0
- package/lib/typescript/src/core/XHRInterceptor.d.ts +29 -0
- package/lib/typescript/src/core/XHRInterceptor.d.ts.map +1 -0
- package/lib/typescript/src/core/har.d.ts +65 -0
- package/lib/typescript/src/core/har.d.ts.map +1 -0
- package/lib/typescript/src/core/loggerSingleton.d.ts +9 -0
- package/lib/typescript/src/core/loggerSingleton.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts +11 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +33 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/theme.d.ts +49 -0
- package/lib/typescript/src/theme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +80 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/body.d.ts +19 -0
- package/lib/typescript/src/utils/body.d.ts.map +1 -0
- package/lib/typescript/src/utils/debounce.d.ts +4 -0
- package/lib/typescript/src/utils/debounce.d.ts.map +1 -0
- package/lib/typescript/src/utils/format.d.ts +7 -0
- package/lib/typescript/src/utils/format.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +2 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/lib/typescript/src/utils/search.d.ts +17 -0
- package/lib/typescript/src/utils/search.d.ts.map +1 -0
- package/lib/typescript/src/utils/url.d.ts +15 -0
- package/lib/typescript/src/utils/url.d.ts.map +1 -0
- package/package.json +186 -4
- package/src/backHandler.ts +29 -0
- package/src/components/BodyViewer.tsx +250 -0
- package/src/components/CodeList.tsx +120 -0
- package/src/components/JsonTree/JsonTreeView.tsx +304 -0
- package/src/components/JsonTree/model.ts +266 -0
- package/src/components/NetworkLens.tsx +176 -0
- package/src/components/NetworkLensOverlay.tsx +187 -0
- package/src/components/RawTextView.tsx +163 -0
- package/src/components/RequestDetails/RequestDetails.tsx +315 -0
- package/src/components/RequestList/FilterBar.tsx +168 -0
- package/src/components/RequestList/RequestList.tsx +226 -0
- package/src/components/RequestList/RequestRow.tsx +150 -0
- package/src/components/RequestList/filters.ts +98 -0
- package/src/components/ui/ActionSheet.tsx +109 -0
- package/src/components/ui/Button.tsx +124 -0
- package/src/components/ui/Pill.tsx +53 -0
- package/src/components/ui/SearchInput.tsx +97 -0
- package/src/components/ui/Section.tsx +133 -0
- package/src/components/ui/SegmentedControl.tsx +79 -0
- package/src/components/ui/index.ts +6 -0
- package/src/core/Logger.ts +293 -0
- package/src/core/NetworkRequest.ts +216 -0
- package/src/core/XHRInterceptor.ts +179 -0
- package/src/core/har.ts +71 -0
- package/src/core/loggerSingleton.ts +14 -0
- package/src/hooks.ts +51 -0
- package/src/index.tsx +56 -0
- package/src/theme.ts +174 -0
- package/src/types.ts +83 -0
- package/src/utils/body.ts +194 -0
- package/src/utils/debounce.ts +18 -0
- package/src/utils/format.ts +50 -0
- package/src/utils/logger.ts +3 -0
- package/src/utils/search.ts +72 -0
- package/src/utils/url.ts +59 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StyleSheet,
|
|
3
|
+
Text,
|
|
4
|
+
View,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string;
|
|
11
|
+
color: string;
|
|
12
|
+
filled?: boolean;
|
|
13
|
+
small?: boolean;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Pill = ({ label, color, filled, small, style }: Props) => (
|
|
18
|
+
<View
|
|
19
|
+
style={[
|
|
20
|
+
styles.pill,
|
|
21
|
+
small && styles.pillSmall,
|
|
22
|
+
{ borderColor: color },
|
|
23
|
+
filled ? { backgroundColor: color } : styles.outline,
|
|
24
|
+
style,
|
|
25
|
+
]}
|
|
26
|
+
>
|
|
27
|
+
<Text
|
|
28
|
+
numberOfLines={1}
|
|
29
|
+
style={[
|
|
30
|
+
styles.text,
|
|
31
|
+
small && styles.textSmall,
|
|
32
|
+
filled ? styles.textFilled : { color },
|
|
33
|
+
]}
|
|
34
|
+
>
|
|
35
|
+
{label}
|
|
36
|
+
</Text>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const styles = StyleSheet.create({
|
|
41
|
+
pill: {
|
|
42
|
+
borderWidth: 1,
|
|
43
|
+
borderRadius: 6,
|
|
44
|
+
paddingHorizontal: 6,
|
|
45
|
+
paddingVertical: 2,
|
|
46
|
+
alignSelf: 'flex-start',
|
|
47
|
+
},
|
|
48
|
+
pillSmall: { paddingHorizontal: 4, paddingVertical: 1, borderRadius: 4 },
|
|
49
|
+
outline: { backgroundColor: 'transparent' },
|
|
50
|
+
text: { fontSize: 12, fontWeight: '700', letterSpacing: 0.3 },
|
|
51
|
+
textSmall: { fontSize: 11 },
|
|
52
|
+
textFilled: { color: '#ffffff' },
|
|
53
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StyleSheet,
|
|
3
|
+
Text,
|
|
4
|
+
TextInput,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { useTheme } from '../../theme';
|
|
10
|
+
import { IconButton } from './Button';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
value: string;
|
|
14
|
+
onChangeText: (text: string) => void;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
/** When provided, shows "n/m" and previous/next buttons. */
|
|
17
|
+
matches?: {
|
|
18
|
+
count: number;
|
|
19
|
+
index: number;
|
|
20
|
+
onPrev: () => void;
|
|
21
|
+
onNext: () => void;
|
|
22
|
+
};
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SearchInput = ({
|
|
27
|
+
value,
|
|
28
|
+
onChangeText,
|
|
29
|
+
placeholder = 'Search',
|
|
30
|
+
matches,
|
|
31
|
+
style,
|
|
32
|
+
}: Props) => {
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
return (
|
|
35
|
+
<View
|
|
36
|
+
style={[
|
|
37
|
+
styles.container,
|
|
38
|
+
{ backgroundColor: theme.colors.surfaceAlt },
|
|
39
|
+
style,
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
<TextInput
|
|
43
|
+
value={value}
|
|
44
|
+
onChangeText={onChangeText}
|
|
45
|
+
placeholder={placeholder}
|
|
46
|
+
placeholderTextColor={theme.colors.textMuted}
|
|
47
|
+
accessibilityLabel={placeholder}
|
|
48
|
+
autoCapitalize="none"
|
|
49
|
+
autoCorrect={false}
|
|
50
|
+
returnKeyType="search"
|
|
51
|
+
onSubmitEditing={matches?.onNext}
|
|
52
|
+
style={[styles.input, { color: theme.colors.text }]}
|
|
53
|
+
/>
|
|
54
|
+
{matches && value.length > 0 && (
|
|
55
|
+
<Text style={[styles.count, { color: theme.colors.textMuted }]}>
|
|
56
|
+
{matches.count === 0 ? '0' : `${matches.index + 1}/${matches.count}`}
|
|
57
|
+
</Text>
|
|
58
|
+
)}
|
|
59
|
+
{matches && (
|
|
60
|
+
<>
|
|
61
|
+
<IconButton
|
|
62
|
+
glyph="↑"
|
|
63
|
+
accessibilityLabel="Previous match"
|
|
64
|
+
onPress={matches.onPrev}
|
|
65
|
+
disabled={!matches.count}
|
|
66
|
+
/>
|
|
67
|
+
<IconButton
|
|
68
|
+
glyph="↓"
|
|
69
|
+
accessibilityLabel="Next match"
|
|
70
|
+
onPress={matches.onNext}
|
|
71
|
+
disabled={!matches.count}
|
|
72
|
+
/>
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
75
|
+
{value.length > 0 && (
|
|
76
|
+
<IconButton
|
|
77
|
+
glyph="✕"
|
|
78
|
+
accessibilityLabel="Clear search"
|
|
79
|
+
onPress={() => onChangeText('')}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
container: {
|
|
88
|
+
flexDirection: 'row',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
borderRadius: 10,
|
|
91
|
+
paddingLeft: 10,
|
|
92
|
+
paddingRight: 2,
|
|
93
|
+
minHeight: 38,
|
|
94
|
+
},
|
|
95
|
+
input: { flex: 1, fontSize: 14, paddingVertical: 6, paddingHorizontal: 0 },
|
|
96
|
+
count: { fontSize: 12, marginHorizontal: 4, fontVariant: ['tabular-nums'] },
|
|
97
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useTheme } from '../../theme';
|
|
4
|
+
|
|
5
|
+
export const Section = ({
|
|
6
|
+
title,
|
|
7
|
+
right,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
title: string;
|
|
11
|
+
right?: ReactNode;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}) => {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
return (
|
|
16
|
+
<View style={styles.section}>
|
|
17
|
+
<View style={styles.header}>
|
|
18
|
+
<Text style={[styles.title, { color: theme.colors.textMuted }]}>
|
|
19
|
+
{title.toUpperCase()}
|
|
20
|
+
</Text>
|
|
21
|
+
{right}
|
|
22
|
+
</View>
|
|
23
|
+
<View style={[styles.body, { backgroundColor: theme.colors.surface }]}>
|
|
24
|
+
{children}
|
|
25
|
+
</View>
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const KeyValueList = ({
|
|
31
|
+
entries,
|
|
32
|
+
emptyLabel = 'None',
|
|
33
|
+
mono,
|
|
34
|
+
}: {
|
|
35
|
+
entries: [string, string][];
|
|
36
|
+
emptyLabel?: string;
|
|
37
|
+
mono?: boolean;
|
|
38
|
+
}) => {
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
if (entries.length === 0) {
|
|
41
|
+
return (
|
|
42
|
+
<Text style={[styles.empty, { color: theme.colors.textMuted }]}>
|
|
43
|
+
{emptyLabel}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
<View>
|
|
49
|
+
{entries.map(([key, value], index) => (
|
|
50
|
+
<View
|
|
51
|
+
key={`${key}-${index}`}
|
|
52
|
+
style={[
|
|
53
|
+
styles.row,
|
|
54
|
+
index > 0 && {
|
|
55
|
+
borderTopColor: theme.colors.border,
|
|
56
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
>
|
|
60
|
+
<Text
|
|
61
|
+
selectable
|
|
62
|
+
style={[styles.key, { color: theme.colors.textMuted }]}
|
|
63
|
+
>
|
|
64
|
+
{key}
|
|
65
|
+
</Text>
|
|
66
|
+
<Text
|
|
67
|
+
selectable
|
|
68
|
+
style={[
|
|
69
|
+
styles.value,
|
|
70
|
+
{ color: theme.colors.text },
|
|
71
|
+
mono && [styles.mono, { fontFamily: theme.fonts.mono }],
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{value}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
))}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const EmptyState = ({
|
|
83
|
+
title,
|
|
84
|
+
subtitle,
|
|
85
|
+
}: {
|
|
86
|
+
title: string;
|
|
87
|
+
subtitle?: string;
|
|
88
|
+
}) => {
|
|
89
|
+
const theme = useTheme();
|
|
90
|
+
return (
|
|
91
|
+
<View style={styles.emptyState}>
|
|
92
|
+
<Text style={[styles.emptyTitle, { color: theme.colors.text }]}>
|
|
93
|
+
{title}
|
|
94
|
+
</Text>
|
|
95
|
+
{subtitle ? (
|
|
96
|
+
<Text style={[styles.emptySubtitle, { color: theme.colors.textMuted }]}>
|
|
97
|
+
{subtitle}
|
|
98
|
+
</Text>
|
|
99
|
+
) : null}
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const styles = StyleSheet.create({
|
|
105
|
+
section: { marginTop: 14, paddingHorizontal: 12 },
|
|
106
|
+
header: {
|
|
107
|
+
flexDirection: 'row',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
justifyContent: 'space-between',
|
|
110
|
+
marginBottom: 6,
|
|
111
|
+
paddingHorizontal: 2,
|
|
112
|
+
},
|
|
113
|
+
title: { fontSize: 11, fontWeight: '700', letterSpacing: 0.8 },
|
|
114
|
+
body: { borderRadius: 10, paddingHorizontal: 12, paddingVertical: 4 },
|
|
115
|
+
row: { paddingVertical: 8 },
|
|
116
|
+
key: { fontSize: 12, fontWeight: '600', marginBottom: 2 },
|
|
117
|
+
value: { fontSize: 14 },
|
|
118
|
+
mono: { fontSize: 12 },
|
|
119
|
+
empty: { fontSize: 13, paddingVertical: 10, fontStyle: 'italic' },
|
|
120
|
+
emptyState: {
|
|
121
|
+
flex: 1,
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
justifyContent: 'center',
|
|
124
|
+
padding: 32,
|
|
125
|
+
},
|
|
126
|
+
emptyTitle: { fontSize: 16, fontWeight: '600', textAlign: 'center' },
|
|
127
|
+
emptySubtitle: {
|
|
128
|
+
fontSize: 13,
|
|
129
|
+
textAlign: 'center',
|
|
130
|
+
marginTop: 6,
|
|
131
|
+
lineHeight: 18,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Pressable,
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { useTheme } from '../../theme';
|
|
10
|
+
|
|
11
|
+
interface Props<T extends string> {
|
|
12
|
+
options: { value: T; label: string; badge?: boolean }[];
|
|
13
|
+
value: T;
|
|
14
|
+
onChange: (value: T) => void;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SegmentedControl = <T extends string>({
|
|
19
|
+
options,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
style,
|
|
23
|
+
}: Props<T>) => {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
return (
|
|
26
|
+
<View
|
|
27
|
+
style={[
|
|
28
|
+
styles.container,
|
|
29
|
+
{ backgroundColor: theme.colors.surfaceAlt },
|
|
30
|
+
style,
|
|
31
|
+
]}
|
|
32
|
+
>
|
|
33
|
+
{options.map((option) => {
|
|
34
|
+
const selected = option.value === value;
|
|
35
|
+
return (
|
|
36
|
+
<Pressable
|
|
37
|
+
key={option.value}
|
|
38
|
+
accessibilityRole="tab"
|
|
39
|
+
accessibilityState={{ selected }}
|
|
40
|
+
onPress={() => onChange(option.value)}
|
|
41
|
+
style={[
|
|
42
|
+
styles.segment,
|
|
43
|
+
selected && { backgroundColor: theme.colors.surface },
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
<Text
|
|
47
|
+
numberOfLines={1}
|
|
48
|
+
style={[
|
|
49
|
+
styles.label,
|
|
50
|
+
{
|
|
51
|
+
color: selected ? theme.colors.text : theme.colors.textMuted,
|
|
52
|
+
},
|
|
53
|
+
selected && styles.labelSelected,
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
{option.label}
|
|
57
|
+
{option.badge && (
|
|
58
|
+
<Text style={{ color: theme.colors.primary }}> •</Text>
|
|
59
|
+
)}
|
|
60
|
+
</Text>
|
|
61
|
+
</Pressable>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const styles = StyleSheet.create({
|
|
69
|
+
container: { flexDirection: 'row', borderRadius: 10, padding: 3 },
|
|
70
|
+
segment: {
|
|
71
|
+
flex: 1,
|
|
72
|
+
paddingVertical: 7,
|
|
73
|
+
paddingHorizontal: 6,
|
|
74
|
+
borderRadius: 8,
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
},
|
|
77
|
+
label: { fontSize: 13, fontWeight: '500' },
|
|
78
|
+
labelSelected: { fontWeight: '700' },
|
|
79
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Pill } from './Pill';
|
|
2
|
+
export { TextButton, IconButton } from './Button';
|
|
3
|
+
export { SegmentedControl } from './SegmentedControl';
|
|
4
|
+
export { SearchInput } from './SearchInput';
|
|
5
|
+
export { Section, KeyValueList, EmptyState } from './Section';
|
|
6
|
+
export { ActionSheet, type ActionItem } from './ActionSheet';
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import NetworkRequest from './NetworkRequest';
|
|
2
|
+
import * as XHRInterceptor from './XHRInterceptor';
|
|
3
|
+
import type { NetworkLensOptions, RequestMethod } from '../types';
|
|
4
|
+
import debounce from '../utils/debounce';
|
|
5
|
+
import { extractHost } from '../utils/url';
|
|
6
|
+
import { byteLength } from '../utils/format';
|
|
7
|
+
import { warn } from '../utils/logger';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MAX_REQUESTS = 500;
|
|
10
|
+
|
|
11
|
+
/** Best-effort byte size when the server sent no Content-Length. */
|
|
12
|
+
const measureResponse = (response: unknown): number => {
|
|
13
|
+
if (typeof response === 'string') return byteLength(response);
|
|
14
|
+
if (response && typeof response === 'object') {
|
|
15
|
+
const sized = response as { size?: unknown; byteLength?: unknown };
|
|
16
|
+
if (typeof sized.size === 'number') return sized.size; // Blob
|
|
17
|
+
if (typeof sized.byteLength === 'number') return sized.byteLength; // ArrayBuffer
|
|
18
|
+
try {
|
|
19
|
+
return byteLength(JSON.stringify(response));
|
|
20
|
+
} catch {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
};
|
|
26
|
+
export const DEFAULT_REFRESH_RATE = 50;
|
|
27
|
+
|
|
28
|
+
type Listener = () => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Central store. Holds the captured requests (newest first) and publishes an
|
|
32
|
+
* immutable snapshot that React reads through `useSyncExternalStore`.
|
|
33
|
+
*/
|
|
34
|
+
export default class Logger {
|
|
35
|
+
private requests: NetworkRequest[] = [];
|
|
36
|
+
private snapshot: NetworkRequest[] = [];
|
|
37
|
+
private byXhrId = new Map<number, NetworkRequest>();
|
|
38
|
+
private listeners = new Set<Listener>();
|
|
39
|
+
private nextId = 0;
|
|
40
|
+
private maxRequests = DEFAULT_MAX_REQUESTS;
|
|
41
|
+
private refreshRate = DEFAULT_REFRESH_RATE;
|
|
42
|
+
private ignoredHosts?: Set<string>;
|
|
43
|
+
private ignoredUrls?: Set<string>;
|
|
44
|
+
private ignoredPatterns?: RegExp[];
|
|
45
|
+
private paused = false;
|
|
46
|
+
private dirty = false;
|
|
47
|
+
private publishDebounced = debounce(() => this.publish(), this.refreshRate);
|
|
48
|
+
|
|
49
|
+
/** True while XMLHttpRequest is being intercepted by this logger. */
|
|
50
|
+
enabled = false;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------
|
|
53
|
+
// Subscriptions
|
|
54
|
+
// ---------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
subscribe = (listener: Listener): (() => void) => {
|
|
57
|
+
this.listeners.add(listener);
|
|
58
|
+
return () => {
|
|
59
|
+
this.listeners.delete(listener);
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
getSnapshot = (): NetworkRequest[] => this.snapshot;
|
|
64
|
+
|
|
65
|
+
isPaused = (): boolean => this.paused;
|
|
66
|
+
|
|
67
|
+
private emit() {
|
|
68
|
+
this.listeners.forEach((listener) => listener());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private publish() {
|
|
72
|
+
if (this.paused) return;
|
|
73
|
+
if (!this.dirty) return;
|
|
74
|
+
this.dirty = false;
|
|
75
|
+
this.snapshot = this.requests.slice();
|
|
76
|
+
this.emit();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private schedulePublish() {
|
|
80
|
+
this.dirty = true;
|
|
81
|
+
this.publishDebounced();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------
|
|
85
|
+
// Interceptor callbacks
|
|
86
|
+
// ---------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
private isIgnored(method: string, url: string): boolean {
|
|
89
|
+
if (this.ignoredHosts?.has(extractHost(url))) return true;
|
|
90
|
+
if (this.ignoredUrls?.has(url)) return true;
|
|
91
|
+
if (
|
|
92
|
+
this.ignoredPatterns?.some((pattern) => pattern.test(`${method} ${url}`))
|
|
93
|
+
)
|
|
94
|
+
return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private onOpen = (
|
|
99
|
+
method: RequestMethod,
|
|
100
|
+
url: string,
|
|
101
|
+
xhr: XHRInterceptor.InterceptedXHR
|
|
102
|
+
) => {
|
|
103
|
+
if (this.isIgnored(method, url)) return;
|
|
104
|
+
const id = this.nextId++;
|
|
105
|
+
xhr.__lensId = id;
|
|
106
|
+
const request = new NetworkRequest(String(id), method, url);
|
|
107
|
+
this.byXhrId.set(id, request);
|
|
108
|
+
this.requests.unshift(request);
|
|
109
|
+
while (this.requests.length > this.maxRequests) {
|
|
110
|
+
const dropped = this.requests.pop();
|
|
111
|
+
if (dropped) this.byXhrId.delete(Number(dropped.id));
|
|
112
|
+
}
|
|
113
|
+
this.schedulePublish();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
private get = (xhr: XHRInterceptor.InterceptedXHR) =>
|
|
117
|
+
xhr.__lensId === undefined ? undefined : this.byXhrId.get(xhr.__lensId);
|
|
118
|
+
|
|
119
|
+
private onRequestHeader = (
|
|
120
|
+
header: string,
|
|
121
|
+
value: string,
|
|
122
|
+
xhr: XHRInterceptor.InterceptedXHR
|
|
123
|
+
) => {
|
|
124
|
+
const request = this.get(xhr);
|
|
125
|
+
if (!request) return;
|
|
126
|
+
request.requestHeaders[header] = value;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
private onSend = (body: unknown, xhr: XHRInterceptor.InterceptedXHR) => {
|
|
130
|
+
const request = this.get(xhr);
|
|
131
|
+
if (!request) return;
|
|
132
|
+
request.update({ startTime: Date.now(), dataSent: body });
|
|
133
|
+
this.schedulePublish();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
private onHeadersReceived = (
|
|
137
|
+
status: number,
|
|
138
|
+
contentType: string,
|
|
139
|
+
contentLength: number,
|
|
140
|
+
headers: Record<string, string>,
|
|
141
|
+
xhr: XHRInterceptor.InterceptedXHR
|
|
142
|
+
) => {
|
|
143
|
+
const request = this.get(xhr);
|
|
144
|
+
if (!request) return;
|
|
145
|
+
request.update({
|
|
146
|
+
status,
|
|
147
|
+
responseContentType: contentType,
|
|
148
|
+
responseSize: contentLength,
|
|
149
|
+
responseHeaders: headers,
|
|
150
|
+
});
|
|
151
|
+
this.schedulePublish();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
private onResponse = (
|
|
155
|
+
status: number,
|
|
156
|
+
timeout: number,
|
|
157
|
+
response: unknown,
|
|
158
|
+
responseURL: string,
|
|
159
|
+
responseType: string,
|
|
160
|
+
xhr: XHRInterceptor.InterceptedXHR
|
|
161
|
+
) => {
|
|
162
|
+
const request = this.get(xhr);
|
|
163
|
+
if (!request) return;
|
|
164
|
+
const failed = status <= 0;
|
|
165
|
+
request.update({
|
|
166
|
+
endTime: Date.now(),
|
|
167
|
+
status,
|
|
168
|
+
timeout,
|
|
169
|
+
response,
|
|
170
|
+
responseURL,
|
|
171
|
+
responseType,
|
|
172
|
+
state: failed ? 'failed' : 'done',
|
|
173
|
+
failureReason: failed ? (request.failureReason ?? 'error') : undefined,
|
|
174
|
+
});
|
|
175
|
+
if (!failed && !request.responseSize) {
|
|
176
|
+
request.responseSize = measureResponse(response);
|
|
177
|
+
}
|
|
178
|
+
this.schedulePublish();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
private onFailure = (
|
|
182
|
+
reason: 'error' | 'timeout' | 'abort',
|
|
183
|
+
xhr: XHRInterceptor.InterceptedXHR
|
|
184
|
+
) => {
|
|
185
|
+
const request = this.get(xhr);
|
|
186
|
+
if (!request) return;
|
|
187
|
+
request.update({
|
|
188
|
+
state: 'failed',
|
|
189
|
+
failureReason: reason,
|
|
190
|
+
endTime: request.endTime || Date.now(),
|
|
191
|
+
});
|
|
192
|
+
this.schedulePublish();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------
|
|
196
|
+
// Public API
|
|
197
|
+
// ---------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
start = (options: NetworkLensOptions = {}): void => {
|
|
200
|
+
if (options.enabled === false) {
|
|
201
|
+
if (this.enabled) this.stop();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (this.enabled) return;
|
|
205
|
+
|
|
206
|
+
if (options.maxRequests !== undefined) {
|
|
207
|
+
if (typeof options.maxRequests !== 'number' || options.maxRequests < 1) {
|
|
208
|
+
warn(
|
|
209
|
+
'`maxRequests` must be a number greater than 0. Using the default.'
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
this.maxRequests = options.maxRequests;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (options.refreshRate !== undefined) {
|
|
216
|
+
if (typeof options.refreshRate !== 'number' || options.refreshRate < 1) {
|
|
217
|
+
warn(
|
|
218
|
+
'`refreshRate` must be a number greater than 0. Using the default.'
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
this.refreshRate = options.refreshRate;
|
|
222
|
+
this.publishDebounced = debounce(
|
|
223
|
+
() => this.publish(),
|
|
224
|
+
this.refreshRate
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.ignoredHosts = options.ignoredHosts?.length
|
|
229
|
+
? new Set(options.ignoredHosts)
|
|
230
|
+
: undefined;
|
|
231
|
+
this.ignoredUrls = options.ignoredUrls?.length
|
|
232
|
+
? new Set(options.ignoredUrls)
|
|
233
|
+
: undefined;
|
|
234
|
+
this.ignoredPatterns = options.ignoredPatterns?.length
|
|
235
|
+
? options.ignoredPatterns
|
|
236
|
+
: undefined;
|
|
237
|
+
|
|
238
|
+
XHRInterceptor.setCallbacks({
|
|
239
|
+
onOpen: this.onOpen,
|
|
240
|
+
onRequestHeader: this.onRequestHeader,
|
|
241
|
+
onSend: this.onSend,
|
|
242
|
+
onHeadersReceived: this.onHeadersReceived,
|
|
243
|
+
onResponse: this.onResponse,
|
|
244
|
+
onFailure: this.onFailure,
|
|
245
|
+
});
|
|
246
|
+
XHRInterceptor.enableInterception();
|
|
247
|
+
this.enabled = true;
|
|
248
|
+
this.emit();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
stop = (): void => {
|
|
252
|
+
if (!this.enabled) return;
|
|
253
|
+
XHRInterceptor.disableInterception();
|
|
254
|
+
this.publishDebounced.cancel();
|
|
255
|
+
this.enabled = false;
|
|
256
|
+
this.paused = false;
|
|
257
|
+
this.requests = [];
|
|
258
|
+
this.byXhrId.clear();
|
|
259
|
+
this.nextId = 0;
|
|
260
|
+
this.maxRequests = DEFAULT_MAX_REQUESTS;
|
|
261
|
+
this.refreshRate = DEFAULT_REFRESH_RATE;
|
|
262
|
+
this.ignoredHosts = undefined;
|
|
263
|
+
this.ignoredUrls = undefined;
|
|
264
|
+
this.ignoredPatterns = undefined;
|
|
265
|
+
this.snapshot = [];
|
|
266
|
+
this.dirty = false;
|
|
267
|
+
this.emit();
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
getRequests = (): NetworkRequest[] => this.snapshot;
|
|
271
|
+
|
|
272
|
+
getRequest = (id: string): NetworkRequest | undefined =>
|
|
273
|
+
this.requests.find((request) => request.id === id);
|
|
274
|
+
|
|
275
|
+
clear = (): void => {
|
|
276
|
+
this.requests = [];
|
|
277
|
+
this.byXhrId.clear();
|
|
278
|
+
this.snapshot = [];
|
|
279
|
+
this.dirty = false;
|
|
280
|
+
this.emit();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
setPaused = (paused: boolean): void => {
|
|
284
|
+
if (this.paused === paused) return;
|
|
285
|
+
this.paused = paused;
|
|
286
|
+
if (!paused) {
|
|
287
|
+
this.dirty = true;
|
|
288
|
+
this.publish();
|
|
289
|
+
} else {
|
|
290
|
+
this.emit();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|