lynx-console 0.0.0
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/dist/assets/src/components/BottomSheet.css.ts.vanilla-D-1A77Ik.css +83 -0
- package/dist/assets/src/components/ConsolePanel.css.ts.vanilla-B3avfSlI.css +246 -0
- package/dist/assets/src/components/FloatingButton.css.ts.vanilla-rPj35oLW.css +55 -0
- package/dist/assets/src/components/NetworkPanel.css.ts.vanilla-DFMduT0T.css +247 -0
- package/dist/assets/src/components/PerformancePanel.css.ts.vanilla-D35LuXlW.css +216 -0
- package/dist/assets/src/components/Tabs.css.ts.vanilla-DD7L2oXt.css +50 -0
- package/dist/index.cjs +1058 -0
- package/dist/index.css +466 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +17 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1059 -0
- package/dist/index.mjs.map +1 -0
- package/dist/setup.cjs +377 -0
- package/dist/setup.d.cts +15 -0
- package/dist/setup.d.cts.map +1 -0
- package/dist/setup.d.mts +15 -0
- package/dist/setup.d.mts.map +1 -0
- package/dist/setup.mjs +374 -0
- package/dist/setup.mjs.map +1 -0
- package/package.json +51 -0
- package/src/components/BottomSheet.css.ts +93 -0
- package/src/components/BottomSheet.tsx +142 -0
- package/src/components/ConsolePanel.css.ts +261 -0
- package/src/components/ConsolePanel.tsx +41 -0
- package/src/components/FloatingButton.css.ts +62 -0
- package/src/components/FloatingButton.tsx +37 -0
- package/src/components/LogPanel.tsx +241 -0
- package/src/components/NetworkDetailSection.tsx +42 -0
- package/src/components/NetworkPanel.css.ts +280 -0
- package/src/components/NetworkPanel.tsx +222 -0
- package/src/components/PerformancePanel.css.ts +224 -0
- package/src/components/PerformancePanel.tsx +209 -0
- package/src/components/Tabs.css.ts +66 -0
- package/src/components/Tabs.tsx +81 -0
- package/src/globals.d.ts +9 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useConsole.ts +35 -0
- package/src/hooks/useNetwork.ts +36 -0
- package/src/hooks/usePerformance.ts +39 -0
- package/src/index.tsx +110 -0
- package/src/setup/_setupMainThreadConsole.ts +80 -0
- package/src/setup/index.ts +4 -0
- package/src/setup/setupLogMonitor.ts +78 -0
- package/src/setup/setupMainThreadConsole.ts +34 -0
- package/src/setup/setupNetworkMonitor.ts +247 -0
- package/src/setup/setupPerformanceMonitor.ts +70 -0
- package/src/shared/ensureConsoleStructure.ts +20 -0
- package/src/styles/getDimensionValue.ts +7 -0
- package/src/styles/global.css.ts +10 -0
- package/src/styles/tokens.json +80 -0
- package/src/styles/typography.ts +25 -0
- package/src/styles/vars/color.ts +228 -0
- package/src/styles/vars/dimension.ts +79 -0
- package/src/styles/vars/index.css +463 -0
- package/src/styles/vars/index.ts +22 -0
- package/src/styles/vars/radius.ts +12 -0
- package/src/types.ts +96 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useState } from "@lynx-js/react";
|
|
2
|
+
import type { NetworkEntry } from "../types";
|
|
3
|
+
import { NetworkDetailSection } from "./NetworkDetailSection";
|
|
4
|
+
import * as css from "./NetworkPanel.css";
|
|
5
|
+
|
|
6
|
+
interface NetworkPanelProps {
|
|
7
|
+
networks: NetworkEntry[];
|
|
8
|
+
clearNetworks: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type TabType = "general" | "request" | "response";
|
|
12
|
+
|
|
13
|
+
export const NetworkPanel = ({
|
|
14
|
+
networks,
|
|
15
|
+
clearNetworks,
|
|
16
|
+
}: NetworkPanelProps) => {
|
|
17
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
18
|
+
const [activeTab, setActiveTab] = useState<TabType>("general");
|
|
19
|
+
|
|
20
|
+
const formatDuration = (duration?: number): string => {
|
|
21
|
+
if (!duration) return "-";
|
|
22
|
+
if (duration < 1000) return `${duration}ms`;
|
|
23
|
+
return `${(duration / 1000).toFixed(2)}s`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const extractPath = (url: string): string => {
|
|
27
|
+
const pathMatch = url.match(/^https?:\/\/[^/]+(.*)$/);
|
|
28
|
+
if (pathMatch?.[1]) {
|
|
29
|
+
return pathMatch[1].startsWith("/")
|
|
30
|
+
? pathMatch[1].slice(1)
|
|
31
|
+
: pathMatch[1];
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getGeneralInfo = (network: NetworkEntry) => {
|
|
37
|
+
return [
|
|
38
|
+
{ key: "URL", value: network.url },
|
|
39
|
+
{ key: "Method", value: network.method },
|
|
40
|
+
network.statusCode
|
|
41
|
+
? { key: "Status", value: String(network.statusCode) }
|
|
42
|
+
: null,
|
|
43
|
+
{
|
|
44
|
+
key: "Request Time",
|
|
45
|
+
value: new Date(network.startTime).toISOString(),
|
|
46
|
+
},
|
|
47
|
+
network.endTime
|
|
48
|
+
? {
|
|
49
|
+
key: "Response Time",
|
|
50
|
+
value: new Date(network.endTime).toISOString(),
|
|
51
|
+
}
|
|
52
|
+
: null,
|
|
53
|
+
network.duration
|
|
54
|
+
? { key: "Duration", value: formatDuration(network.duration) }
|
|
55
|
+
: null,
|
|
56
|
+
].filter((item) => item !== null);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getStatusCodeVariant = (
|
|
60
|
+
status: string,
|
|
61
|
+
statusCode?: number,
|
|
62
|
+
): "success" | "error" | "pending" => {
|
|
63
|
+
if (status === "pending") return "pending";
|
|
64
|
+
if (status === "error") return "error";
|
|
65
|
+
if (statusCode && statusCode >= 200 && statusCode < 300) return "success";
|
|
66
|
+
return "error";
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<view className={css.container}>
|
|
71
|
+
<view className={css.header}>
|
|
72
|
+
<text className={css.count}>Total: {networks.length} requests</text>
|
|
73
|
+
<view className={css.clearButton} bindtap={clearNetworks}>
|
|
74
|
+
<text className={css.clearButtonText}>Clear</text>
|
|
75
|
+
</view>
|
|
76
|
+
</view>
|
|
77
|
+
|
|
78
|
+
{networks.length === 0 ? (
|
|
79
|
+
<view className={css.placeholder}>
|
|
80
|
+
<text className={css.placeholderText}>No network requests yet</text>
|
|
81
|
+
</view>
|
|
82
|
+
) : (
|
|
83
|
+
<list className={css.list}>
|
|
84
|
+
{networks.map((network) => (
|
|
85
|
+
<list-item key={network.id} item-key={network.id}>
|
|
86
|
+
<view className={css.item({ status: network.status })}>
|
|
87
|
+
<view
|
|
88
|
+
className={css.itemHeader}
|
|
89
|
+
bindtap={() =>
|
|
90
|
+
setSelectedId(selectedId === network.id ? null : network.id)
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
<text
|
|
94
|
+
className={css.method({
|
|
95
|
+
type: network.method as
|
|
96
|
+
| "GET"
|
|
97
|
+
| "POST"
|
|
98
|
+
| "PUT"
|
|
99
|
+
| "PATCH"
|
|
100
|
+
| "DELETE",
|
|
101
|
+
})}
|
|
102
|
+
>
|
|
103
|
+
{network.method}
|
|
104
|
+
</text>
|
|
105
|
+
{network.statusCode && (
|
|
106
|
+
<text
|
|
107
|
+
className={css.statusCode({
|
|
108
|
+
type: getStatusCodeVariant(
|
|
109
|
+
network.status,
|
|
110
|
+
network.statusCode,
|
|
111
|
+
),
|
|
112
|
+
})}
|
|
113
|
+
>
|
|
114
|
+
{network.statusCode}
|
|
115
|
+
</text>
|
|
116
|
+
)}
|
|
117
|
+
{network.status === "pending" && (
|
|
118
|
+
<text className={css.statusCode({ type: "pending" })}>
|
|
119
|
+
Pending...
|
|
120
|
+
</text>
|
|
121
|
+
)}
|
|
122
|
+
<text className={css.time}>
|
|
123
|
+
{formatDuration(network.duration)}
|
|
124
|
+
</text>
|
|
125
|
+
<text className={css.time}>
|
|
126
|
+
{new Date(network.startTime).toISOString()}
|
|
127
|
+
</text>
|
|
128
|
+
</view>
|
|
129
|
+
|
|
130
|
+
<text
|
|
131
|
+
className={css.path}
|
|
132
|
+
bindtap={() =>
|
|
133
|
+
setSelectedId(selectedId === network.id ? null : network.id)
|
|
134
|
+
}
|
|
135
|
+
>
|
|
136
|
+
{extractPath(network.url)}
|
|
137
|
+
</text>
|
|
138
|
+
|
|
139
|
+
{selectedId === network.id && (
|
|
140
|
+
<view className={css.detailsContainer}>
|
|
141
|
+
{/* Tabs */}
|
|
142
|
+
<view className={css.tabs}>
|
|
143
|
+
<view
|
|
144
|
+
className={css.tab({ active: activeTab === "general" })}
|
|
145
|
+
bindtap={() => setActiveTab("general")}
|
|
146
|
+
>
|
|
147
|
+
<text
|
|
148
|
+
className={css.tabText({
|
|
149
|
+
active: activeTab === "general",
|
|
150
|
+
})}
|
|
151
|
+
>
|
|
152
|
+
General
|
|
153
|
+
</text>
|
|
154
|
+
</view>
|
|
155
|
+
<view
|
|
156
|
+
className={css.tab({ active: activeTab === "request" })}
|
|
157
|
+
bindtap={() => setActiveTab("request")}
|
|
158
|
+
>
|
|
159
|
+
<text
|
|
160
|
+
className={css.tabText({
|
|
161
|
+
active: activeTab === "request",
|
|
162
|
+
})}
|
|
163
|
+
>
|
|
164
|
+
Request
|
|
165
|
+
</text>
|
|
166
|
+
</view>
|
|
167
|
+
<view
|
|
168
|
+
className={css.tab({
|
|
169
|
+
active: activeTab === "response",
|
|
170
|
+
})}
|
|
171
|
+
bindtap={() => setActiveTab("response")}
|
|
172
|
+
>
|
|
173
|
+
<text
|
|
174
|
+
className={css.tabText({
|
|
175
|
+
active: activeTab === "response",
|
|
176
|
+
})}
|
|
177
|
+
>
|
|
178
|
+
Response
|
|
179
|
+
</text>
|
|
180
|
+
</view>
|
|
181
|
+
</view>
|
|
182
|
+
|
|
183
|
+
{/* Tab Content */}
|
|
184
|
+
<view className={css.tabContent}>
|
|
185
|
+
{activeTab === "general" && (
|
|
186
|
+
<view className={css.table}>
|
|
187
|
+
{getGeneralInfo(network).map((item) => (
|
|
188
|
+
<view key={item.key} className={css.tableRow}>
|
|
189
|
+
<text className={css.tableKey}>{item.key}</text>
|
|
190
|
+
<text className={css.tableValue}>
|
|
191
|
+
{item.value}
|
|
192
|
+
</text>
|
|
193
|
+
</view>
|
|
194
|
+
))}
|
|
195
|
+
</view>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{activeTab === "request" && (
|
|
199
|
+
<NetworkDetailSection
|
|
200
|
+
headers={network.requestHeaders}
|
|
201
|
+
body={network.requestBody}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{activeTab === "response" && (
|
|
206
|
+
<NetworkDetailSection
|
|
207
|
+
headers={network.responseHeaders}
|
|
208
|
+
body={network.responseBody}
|
|
209
|
+
error={network.error}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</view>
|
|
213
|
+
</view>
|
|
214
|
+
)}
|
|
215
|
+
</view>
|
|
216
|
+
</list-item>
|
|
217
|
+
))}
|
|
218
|
+
</list>
|
|
219
|
+
)}
|
|
220
|
+
</view>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { style } from "@vanilla-extract/css";
|
|
2
|
+
import { recipe } from "@vanilla-extract/recipes";
|
|
3
|
+
import { typography } from "../styles/typography";
|
|
4
|
+
import { vars } from "../styles/vars";
|
|
5
|
+
|
|
6
|
+
export const container = style({
|
|
7
|
+
display: "flex",
|
|
8
|
+
flexDirection: "column",
|
|
9
|
+
flex: 1,
|
|
10
|
+
paddingTop: 4,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const header = style({
|
|
14
|
+
display: "flex",
|
|
15
|
+
flexDirection: "row",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
justifyContent: "space-between",
|
|
18
|
+
marginBottom: 8,
|
|
19
|
+
paddingBottom: 4,
|
|
20
|
+
borderBottomWidth: 1,
|
|
21
|
+
borderBottomColor: vars.$color.stroke.neutralSubtle,
|
|
22
|
+
borderBottomStyle: "solid",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const count = style({
|
|
26
|
+
...typography("t3", "regular"),
|
|
27
|
+
color: vars.$color.fg.neutralSubtle,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const clearButton = style({
|
|
31
|
+
padding: "6px 12px",
|
|
32
|
+
backgroundColor: vars.$color.bg.neutralWeak,
|
|
33
|
+
borderRadius: 4,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const clearButtonText = style({
|
|
37
|
+
...typography("t3", "medium"),
|
|
38
|
+
color: vars.$color.fg.neutral,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const list = style({
|
|
42
|
+
flex: 1,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const placeholder = style({
|
|
46
|
+
display: "flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
height: "100%",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const placeholderText = style({
|
|
53
|
+
...typography("t4", "regular"),
|
|
54
|
+
color: vars.$color.fg.disabled,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const item = style({
|
|
58
|
+
padding: "8px",
|
|
59
|
+
borderBottomWidth: 1,
|
|
60
|
+
borderBottomColor: vars.$color.stroke.neutralWeak,
|
|
61
|
+
borderBottomStyle: "solid",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const itemHeader = style({
|
|
65
|
+
display: "flex",
|
|
66
|
+
flexDirection: "row",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
marginBottom: 4,
|
|
69
|
+
gap: 8,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const entryType = recipe({
|
|
73
|
+
base: {
|
|
74
|
+
...typography("t2", "bold"),
|
|
75
|
+
padding: "0 6px",
|
|
76
|
+
borderRadius: 2,
|
|
77
|
+
color: vars.$color.fg.neutral,
|
|
78
|
+
backgroundColor: vars.$color.bg.neutralWeak,
|
|
79
|
+
},
|
|
80
|
+
variants: {
|
|
81
|
+
type: {
|
|
82
|
+
init: {
|
|
83
|
+
color: vars.$color.palette.blue600,
|
|
84
|
+
backgroundColor: vars.$color.palette.blue100,
|
|
85
|
+
},
|
|
86
|
+
metric: {
|
|
87
|
+
color: vars.$color.palette.green600,
|
|
88
|
+
backgroundColor: vars.$color.palette.green100,
|
|
89
|
+
},
|
|
90
|
+
pipeline: {
|
|
91
|
+
color: vars.$color.palette.purple600,
|
|
92
|
+
backgroundColor: vars.$color.palette.purple100,
|
|
93
|
+
},
|
|
94
|
+
resource: {
|
|
95
|
+
color: vars.$color.palette.yellow600,
|
|
96
|
+
backgroundColor: vars.$color.palette.yellow100,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const entryName = style({
|
|
103
|
+
...typography("t2", "medium"),
|
|
104
|
+
color: vars.$color.fg.neutral,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export const timestamp = style({
|
|
108
|
+
...typography("t2", "regular"),
|
|
109
|
+
color: vars.$color.fg.neutralSubtle,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const fcpMetricHeader = style({
|
|
113
|
+
display: "flex",
|
|
114
|
+
flexDirection: "row",
|
|
115
|
+
alignItems: "center",
|
|
116
|
+
gap: 8,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const fcpHighlight = style({
|
|
120
|
+
...typography("t3", "bold"),
|
|
121
|
+
color: vars.$color.palette.blue600,
|
|
122
|
+
backgroundColor: vars.$color.palette.blue100,
|
|
123
|
+
padding: "4px 8px",
|
|
124
|
+
borderRadius: 4,
|
|
125
|
+
marginTop: 4,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const metrics = style({
|
|
129
|
+
marginTop: 8,
|
|
130
|
+
display: "flex",
|
|
131
|
+
flexDirection: "column",
|
|
132
|
+
gap: 4,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const metric = style({
|
|
136
|
+
display: "flex",
|
|
137
|
+
flexDirection: "row",
|
|
138
|
+
alignItems: "center",
|
|
139
|
+
gap: 8,
|
|
140
|
+
padding: "4px 8px",
|
|
141
|
+
backgroundColor: vars.$color.bg.neutralWeak,
|
|
142
|
+
borderRadius: 2,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const metricName = style({
|
|
146
|
+
...typography("t3", "medium"),
|
|
147
|
+
color: vars.$color.fg.neutralSubtle,
|
|
148
|
+
minWidth: 100,
|
|
149
|
+
flexShrink: 0,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const metricValue = style({
|
|
153
|
+
...typography("t3", "bold"),
|
|
154
|
+
color: vars.$color.palette.green600,
|
|
155
|
+
flex: 1,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const detailsContainer = style({
|
|
159
|
+
marginTop: 12,
|
|
160
|
+
display: "flex",
|
|
161
|
+
flexDirection: "column",
|
|
162
|
+
gap: 12,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const fcpSection = style({
|
|
166
|
+
display: "flex",
|
|
167
|
+
flexDirection: "column",
|
|
168
|
+
gap: 8,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const fcpSectionDescription = style({
|
|
172
|
+
...typography("t3", "regular"),
|
|
173
|
+
color: vars.$color.fg.neutralSubtle,
|
|
174
|
+
marginBottom: 4,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const fcpMetric = style({
|
|
178
|
+
backgroundColor: vars.$color.bg.layerDefault,
|
|
179
|
+
borderRadius: 4,
|
|
180
|
+
display: "flex",
|
|
181
|
+
flexDirection: "column",
|
|
182
|
+
gap: 4,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
export const fcpMetricName = style({
|
|
186
|
+
...typography("t2", "bold"),
|
|
187
|
+
color: vars.$color.fg.neutral,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export const fcpMetricValue = style({
|
|
191
|
+
...typography("t1", "bold"),
|
|
192
|
+
color: vars.$color.palette.blue600,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const fcpMetricDescription = style({
|
|
196
|
+
...typography("t3", "regular"),
|
|
197
|
+
color: vars.$color.fg.neutralSubtle,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export const fcpMetricFormula = style({
|
|
201
|
+
...typography("t4", "regular"),
|
|
202
|
+
color: vars.$color.fg.disabled,
|
|
203
|
+
fontFamily: "monospace",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export const rawEntrySection = style({
|
|
207
|
+
padding: 12,
|
|
208
|
+
backgroundColor: vars.$color.bg.neutralWeak,
|
|
209
|
+
borderRadius: 4,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
export const detailTitle = style({
|
|
213
|
+
...typography("t3", "bold"),
|
|
214
|
+
color: vars.$color.fg.neutral,
|
|
215
|
+
marginBottom: 8,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export const rawEntry = style({
|
|
219
|
+
...typography("t3", "regular"),
|
|
220
|
+
color: vars.$color.fg.neutralSubtle,
|
|
221
|
+
fontFamily: "monospace",
|
|
222
|
+
whiteSpace: "pre-wrap",
|
|
223
|
+
wordBreak: "break-all",
|
|
224
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useState } from "@lynx-js/react";
|
|
2
|
+
import { stringify } from "javascript-stringify";
|
|
3
|
+
import type { PerformanceEntryData } from "../types";
|
|
4
|
+
import * as css from "./PerformancePanel.css";
|
|
5
|
+
|
|
6
|
+
interface PerformancePanelProps {
|
|
7
|
+
performances: PerformanceEntryData[];
|
|
8
|
+
clearPerformances: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FcpMetric {
|
|
12
|
+
name: string;
|
|
13
|
+
duration: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MetricFcpEntry {
|
|
17
|
+
totalFcp?: FcpMetric;
|
|
18
|
+
lynxFcp?: FcpMetric;
|
|
19
|
+
fcp?: FcpMetric;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isMetricFcpEntry = (entry: PerformanceEntryData): boolean => {
|
|
23
|
+
return entry.entryType === "metric" && entry.name === "fcp";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const extractFcpMetrics = (entry: PerformanceEntryData) => {
|
|
27
|
+
if (!isMetricFcpEntry(entry) || !entry.rawEntry) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const metricEntry = entry.rawEntry as MetricFcpEntry;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
totalFcp: metricEntry.totalFcp ?? undefined,
|
|
35
|
+
lynxFcp: metricEntry.lynxFcp ?? undefined,
|
|
36
|
+
fcp: metricEntry.fcp ?? undefined,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const formatDuration = (ms?: number): string => {
|
|
41
|
+
if (ms === undefined) return "-";
|
|
42
|
+
return `${ms.toFixed(2)}ms`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getPrimaryFcpLabel = (entry: PerformanceEntryData): string => {
|
|
46
|
+
const fcpMetrics = extractFcpMetrics(entry);
|
|
47
|
+
if (!fcpMetrics) return "";
|
|
48
|
+
|
|
49
|
+
const { totalFcp, lynxFcp, fcp } = fcpMetrics;
|
|
50
|
+
|
|
51
|
+
if (totalFcp?.duration !== undefined) {
|
|
52
|
+
return `totalFcp: ${formatDuration(totalFcp.duration)}`;
|
|
53
|
+
}
|
|
54
|
+
if (lynxFcp?.duration !== undefined) {
|
|
55
|
+
return `lynxFcp: ${formatDuration(lynxFcp.duration)}`;
|
|
56
|
+
}
|
|
57
|
+
if (fcp?.duration !== undefined) {
|
|
58
|
+
return `fcp: ${formatDuration(fcp.duration)}`;
|
|
59
|
+
}
|
|
60
|
+
return "";
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const PerformancePanel = ({
|
|
64
|
+
performances,
|
|
65
|
+
clearPerformances,
|
|
66
|
+
}: PerformancePanelProps) => {
|
|
67
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
68
|
+
|
|
69
|
+
if (performances.length === 0) {
|
|
70
|
+
return (
|
|
71
|
+
<view className={css.container}>
|
|
72
|
+
<view className={css.header}>
|
|
73
|
+
<text className={css.count}>0 entries</text>
|
|
74
|
+
<view
|
|
75
|
+
bindtap={() => {
|
|
76
|
+
console.log("[PerformancePanel] performances", performances);
|
|
77
|
+
}}
|
|
78
|
+
style={{ padding: "10px", backgroundColor: "red" }}
|
|
79
|
+
>
|
|
80
|
+
<text>Log</text>
|
|
81
|
+
</view>
|
|
82
|
+
<view bindtap={clearPerformances} className={css.clearButton}>
|
|
83
|
+
<text className={css.clearButtonText}>Clear</text>
|
|
84
|
+
</view>
|
|
85
|
+
</view>
|
|
86
|
+
<view className={css.placeholder}>
|
|
87
|
+
<text className={css.placeholderText}>
|
|
88
|
+
No performance data yet...
|
|
89
|
+
</text>
|
|
90
|
+
</view>
|
|
91
|
+
</view>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<view className={css.container}>
|
|
97
|
+
<view className={css.header}>
|
|
98
|
+
<text className={css.count}>{performances.length} entries</text>
|
|
99
|
+
<view bindtap={clearPerformances} className={css.clearButton}>
|
|
100
|
+
<text className={css.clearButtonText}>Clear</text>
|
|
101
|
+
</view>
|
|
102
|
+
</view>
|
|
103
|
+
|
|
104
|
+
<list className={css.list}>
|
|
105
|
+
{performances.map((perf) => {
|
|
106
|
+
const isMetricFcp = isMetricFcpEntry(perf);
|
|
107
|
+
const fcpMetrics = extractFcpMetrics(perf);
|
|
108
|
+
const primaryFcp = getPrimaryFcpLabel(perf);
|
|
109
|
+
const { totalFcp, lynxFcp, fcp } = fcpMetrics ?? {};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<list-item key={perf.id} item-key={perf.id}>
|
|
113
|
+
<view className={css.item}>
|
|
114
|
+
<view
|
|
115
|
+
className={css.itemHeader}
|
|
116
|
+
bindtap={() =>
|
|
117
|
+
setSelectedId(selectedId === perf.id ? null : perf.id)
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
<text className={css.entryType({ type: perf.entryType })}>
|
|
121
|
+
{perf.entryType}
|
|
122
|
+
</text>
|
|
123
|
+
<text className={css.entryName}>{perf.name}</text>
|
|
124
|
+
<text className={css.timestamp}>
|
|
125
|
+
{new Date(perf.timestamp).toISOString()}
|
|
126
|
+
</text>
|
|
127
|
+
</view>
|
|
128
|
+
|
|
129
|
+
<view
|
|
130
|
+
bindtap={() =>
|
|
131
|
+
setSelectedId(selectedId === perf.id ? null : perf.id)
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
{isMetricFcp && primaryFcp && (
|
|
135
|
+
<text className={css.fcpHighlight}>{primaryFcp}</text>
|
|
136
|
+
)}
|
|
137
|
+
</view>
|
|
138
|
+
|
|
139
|
+
{selectedId === perf.id && (
|
|
140
|
+
<view className={css.detailsContainer}>
|
|
141
|
+
{isMetricFcp && fcpMetrics && (
|
|
142
|
+
<view className={css.fcpSection}>
|
|
143
|
+
{totalFcp !== undefined && (
|
|
144
|
+
<view className={css.fcpMetric}>
|
|
145
|
+
<view className={css.fcpMetricHeader}>
|
|
146
|
+
<text className={css.fcpMetricName}>
|
|
147
|
+
전체 FCP
|
|
148
|
+
</text>
|
|
149
|
+
<text className={css.fcpMetricValue}>
|
|
150
|
+
{formatDuration(totalFcp.duration)}
|
|
151
|
+
</text>
|
|
152
|
+
</view>
|
|
153
|
+
<text className={css.fcpMetricDescription}>
|
|
154
|
+
PrepareTemplate Start부터 Paint End 까지 걸리는
|
|
155
|
+
시간
|
|
156
|
+
</text>
|
|
157
|
+
</view>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{lynxFcp !== undefined && (
|
|
161
|
+
<view className={css.fcpMetric}>
|
|
162
|
+
<view className={css.fcpMetricHeader}>
|
|
163
|
+
<text className={css.fcpMetricName}>LynxFCP</text>
|
|
164
|
+
<text className={css.fcpMetricValue}>
|
|
165
|
+
{formatDuration(lynxFcp.duration)}
|
|
166
|
+
</text>
|
|
167
|
+
</view>
|
|
168
|
+
<text className={css.fcpMetricDescription}>
|
|
169
|
+
Bundle Load 시작부터 Paint End 까지 걸리는 시간
|
|
170
|
+
</text>
|
|
171
|
+
</view>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{fcp !== undefined && (
|
|
175
|
+
<view className={css.fcpMetric}>
|
|
176
|
+
<view className={css.fcpMetricHeader}>
|
|
177
|
+
<text className={css.fcpMetricName}>
|
|
178
|
+
렌더링 FCP
|
|
179
|
+
</text>
|
|
180
|
+
<text className={css.fcpMetricValue}>
|
|
181
|
+
{formatDuration(fcp.duration)}
|
|
182
|
+
</text>
|
|
183
|
+
</view>
|
|
184
|
+
<text className={css.fcpMetricDescription}>
|
|
185
|
+
TemplateBundle 준비부터 Paint End 까지 걸리는 시간
|
|
186
|
+
</text>
|
|
187
|
+
</view>
|
|
188
|
+
)}
|
|
189
|
+
</view>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{!!perf.rawEntry && (
|
|
193
|
+
<view className={css.rawEntrySection}>
|
|
194
|
+
<text className={css.detailTitle}>Raw Entry</text>
|
|
195
|
+
<text className={css.rawEntry}>
|
|
196
|
+
{String(stringify(perf.rawEntry, null, 2, { references: true }))}
|
|
197
|
+
</text>
|
|
198
|
+
</view>
|
|
199
|
+
)}
|
|
200
|
+
</view>
|
|
201
|
+
)}
|
|
202
|
+
</view>
|
|
203
|
+
</list-item>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
</list>
|
|
207
|
+
</view>
|
|
208
|
+
);
|
|
209
|
+
};
|