shokupan 0.10.4 → 0.11.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/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
- package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
- package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
- package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
- package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
- package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
- package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
- package/dist/cli.cjs +206 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +206 -18
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +6 -1
- package/dist/index.cjs +2405 -1008
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2402 -1006
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +423 -30
- package/dist/plugins/application/api-explorer/static/style.css +351 -10
- package/dist/plugins/application/api-explorer/static/theme.css +7 -2
- package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
- package/dist/plugins/application/asyncapi/static/style.css +24 -8
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
- package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
- package/dist/plugins/application/dashboard/static/charts.js +127 -62
- package/dist/plugins/application/dashboard/static/client.js +160 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
- package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
- package/dist/plugins/application/dashboard/static/registry.js +112 -8
- package/dist/plugins/application/dashboard/static/requests.js +868 -58
- package/dist/plugins/application/dashboard/static/styles.css +186 -14
- package/dist/plugins/application/dashboard/static/tabs.js +44 -9
- package/dist/plugins/application/dashboard/static/theme.css +7 -2
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
- package/dist/plugins/application/openapi/openapi.d.ts +3 -0
- package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
- package/dist/router.d.ts +55 -16
- package/dist/shokupan.d.ts +7 -2
- package/dist/util/adapter/adapters.d.ts +19 -0
- package/dist/util/adapter/filesystem.d.ts +20 -0
- package/dist/util/controller-scanner.d.ts +4 -0
- package/dist/util/cpu-monitor.d.ts +2 -0
- package/dist/util/middleware-tracker.d.ts +10 -0
- package/dist/util/types.d.ts +37 -0
- package/package.json +5 -5
- package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
- package/dist/http-server-BEMPIs33.cjs +0 -85
- package/dist/http-server-BEMPIs33.cjs.map +0 -1
- package/dist/http-server-CCeagTyU.js +0 -68
- package/dist/http-server-CCeagTyU.js.map +0 -1
- package/dist/plugins/application/dashboard/static/poll.js +0 -146
|
@@ -1,118 +1,928 @@
|
|
|
1
1
|
|
|
2
2
|
// Initialize Requests Table
|
|
3
|
-
|
|
3
|
+
window.requestsTable = null;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Filter State
|
|
6
|
+
// Initialize Filter State
|
|
7
|
+
let filterText = '';
|
|
8
|
+
let filterType = 'all';
|
|
9
|
+
let filterDirection = 'all';
|
|
10
|
+
|
|
11
|
+
function initRequests() {
|
|
12
|
+
console.log('[requests.js] Initializing...');
|
|
13
|
+
|
|
14
|
+
// Initialize Filter Listeners
|
|
15
|
+
const txtFilter = document.getElementById('network-filter-text');
|
|
16
|
+
const typeFilter = document.getElementById('network-filter-type');
|
|
17
|
+
const directionButtons = document.querySelectorAll('.filter-direction');
|
|
18
|
+
|
|
19
|
+
if (directionButtons) {
|
|
20
|
+
directionButtons.forEach(btn => {
|
|
21
|
+
btn.onclick = () => {
|
|
22
|
+
// Update active state
|
|
23
|
+
directionButtons.forEach(b => {
|
|
24
|
+
b.style.background = 'transparent';
|
|
25
|
+
b.style.color = 'var(--text-secondary)';
|
|
26
|
+
b.classList.remove('active');
|
|
27
|
+
});
|
|
28
|
+
btn.style.background = 'var(--bg-primary)';
|
|
29
|
+
btn.style.color = 'var(--text-primary)';
|
|
30
|
+
btn.classList.add('active');
|
|
31
|
+
|
|
32
|
+
filterDirection = btn.dataset.value;
|
|
33
|
+
if (window.requestsTable) window.requestsTable.setFilter(customFilter);
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (txtFilter) {
|
|
39
|
+
txtFilter.addEventListener('keyup', (e) => {
|
|
40
|
+
filterText = e.target.value.toLowerCase();
|
|
41
|
+
window.requestsTable.setFilter(customFilter);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeFilter) {
|
|
46
|
+
typeFilter.addEventListener('change', (e) => {
|
|
47
|
+
filterType = e.target.value;
|
|
48
|
+
window.requestsTable.setFilter(customFilter);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// specific check for Tabulator
|
|
53
|
+
if (typeof Tabulator === 'undefined') {
|
|
54
|
+
console.error('Tabulator is not defined. Ensure it is loaded before requests.js');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.requestsTable = new Tabulator("#requests-list-container", {
|
|
7
59
|
layout: "fitColumns",
|
|
8
60
|
placeholder: "No requests found",
|
|
9
61
|
selectable: 1,
|
|
62
|
+
resizableColumnFit: true,
|
|
63
|
+
height: "100%", // Fill container
|
|
64
|
+
index: "id",
|
|
65
|
+
rowHeight: 32, // Dense rows
|
|
66
|
+
initialSort: [
|
|
67
|
+
{ column: "timestamp", dir: "desc" }
|
|
68
|
+
],
|
|
10
69
|
columns: [
|
|
11
|
-
{ title: "Method", field: "method", width: 100 },
|
|
12
|
-
{ title: "URL", field: "url" },
|
|
13
70
|
{
|
|
14
71
|
title: "Status",
|
|
15
72
|
field: "status",
|
|
16
|
-
width:
|
|
73
|
+
width: 80,
|
|
17
74
|
formatter: function (cell) {
|
|
18
75
|
const status = cell.getValue();
|
|
19
|
-
|
|
20
|
-
|
|
76
|
+
if (!status) return '<span style="color: var(--text-secondary)">Pending</span>';
|
|
77
|
+
const color = status >= 500 ? '#ef4444' : status >= 400 ? '#f59e0b' : '#10b981';
|
|
78
|
+
return `<span style="display: inline-block; width: 10px; height: 10px; background: ${color}; border-radius: 50%; margin-right: 6px;"></span>${status}`;
|
|
21
79
|
}
|
|
22
80
|
},
|
|
23
|
-
{ title: "Duration (ms)", field: "duration", width: 150, formatter: (cell) => printDuration(cell.getValue()) },
|
|
24
81
|
{
|
|
25
|
-
title: "
|
|
26
|
-
field: "
|
|
27
|
-
width:
|
|
82
|
+
title: "Method",
|
|
83
|
+
field: "method",
|
|
84
|
+
width: 80,
|
|
85
|
+
visible: true
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: "Name",
|
|
89
|
+
field: "url",
|
|
90
|
+
widthGrow: 2, // Take up more space
|
|
28
91
|
formatter: function (cell) {
|
|
29
|
-
|
|
92
|
+
const url = cell.getValue();
|
|
93
|
+
// Extract name from URL
|
|
94
|
+
let name = url;
|
|
95
|
+
try {
|
|
96
|
+
const u = new URL(url, 'http://localhost');
|
|
97
|
+
name = u.pathname;
|
|
98
|
+
if (name === '/') name = 'localhost';
|
|
99
|
+
const parts = name.split('/');
|
|
100
|
+
const last = parts[parts.length - 1];
|
|
101
|
+
if (last) name = last;
|
|
102
|
+
} catch (e) { }
|
|
103
|
+
|
|
104
|
+
return `<div style="display: flex; flex-direction: column; line-height: 1.2;">
|
|
105
|
+
<span style="color: var(--text-secondary);">${name}</span>
|
|
106
|
+
</div>`;
|
|
30
107
|
}
|
|
31
108
|
},
|
|
32
109
|
{
|
|
33
|
-
title: "",
|
|
110
|
+
title: "Domain",
|
|
111
|
+
field: "domain",
|
|
112
|
+
width: 80,
|
|
113
|
+
visible: true
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: "Path",
|
|
117
|
+
field: "path",
|
|
118
|
+
width: 80,
|
|
119
|
+
visible: true
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
title: "URL",
|
|
123
|
+
field: "url",
|
|
34
124
|
width: 80,
|
|
125
|
+
visible: true
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
title: "Protocol",
|
|
129
|
+
field: "protocol",
|
|
130
|
+
width: 80,
|
|
131
|
+
visible: true,
|
|
35
132
|
formatter: function (cell) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
133
|
+
const row = cell.getData();
|
|
134
|
+
// Prefer explicit protocol version (e.g. 1.1, h2) if available
|
|
135
|
+
if (row.protocol && row.protocol !== 'http' && row.protocol !== 'https') return row.protocol;
|
|
136
|
+
return row.scheme || row.protocol || '-';
|
|
40
137
|
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: "Scheme",
|
|
141
|
+
field: "scheme",
|
|
142
|
+
width: 80,
|
|
143
|
+
visible: true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
title: "Remote IP",
|
|
147
|
+
field: "remoteIP",
|
|
148
|
+
width: 80,
|
|
149
|
+
visible: true
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: "Initiator",
|
|
153
|
+
field: "direction",
|
|
154
|
+
width: 80,
|
|
155
|
+
formatter: (cell) => {
|
|
156
|
+
const dir = cell.getValue();
|
|
157
|
+
return dir === 'outbound' ? 'Server' : 'Client';
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
title: "Type",
|
|
162
|
+
field: "type",
|
|
163
|
+
width: 80,
|
|
164
|
+
formatter: (cell) => {
|
|
165
|
+
const r = cell.getData();
|
|
166
|
+
if (r.type === 'fetch') return 'fetch';
|
|
167
|
+
if (r.type === 'xhr') return 'xhr';
|
|
168
|
+
if (r.type === 'ws') return 'ws';
|
|
169
|
+
return r.contentType || 'document';
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
title: "Cookies",
|
|
174
|
+
field: "cookies",
|
|
175
|
+
width: 80,
|
|
176
|
+
visible: true
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
title: "Transferred",
|
|
180
|
+
field: "transferred",
|
|
181
|
+
width: 80,
|
|
182
|
+
visible: true
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
title: "Size",
|
|
186
|
+
field: "size",
|
|
187
|
+
width: 80,
|
|
188
|
+
formatter: (cell) => formatBytes(cell.getValue())
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
title: "Time",
|
|
192
|
+
field: "duration",
|
|
193
|
+
width: 80,
|
|
194
|
+
formatter: (cell) => cell.getValue() ? Math.round(cell.getValue()) + ' ms' : 'Pending'
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
title: "Waterfall",
|
|
198
|
+
field: "timestamp",
|
|
199
|
+
widthGrow: 1,
|
|
200
|
+
formatter: waterfallFormatter,
|
|
201
|
+
headerSort: false
|
|
41
202
|
}
|
|
42
203
|
],
|
|
43
|
-
data: []
|
|
204
|
+
data: [],
|
|
205
|
+
rowContextMenu: [
|
|
206
|
+
{
|
|
207
|
+
label: "Copy as fetch",
|
|
208
|
+
action: function (e, row) {
|
|
209
|
+
const data = row.getData();
|
|
210
|
+
const fetchCode = generateFetchCode(data);
|
|
211
|
+
copyToClipboard(fetchCode);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
label: "Export as HAR",
|
|
216
|
+
action: function (e, row) {
|
|
217
|
+
const data = row.getData();
|
|
218
|
+
const har = generateHAR([data]);
|
|
219
|
+
downloadString(JSON.stringify(har, null, 2), `request-${data.id}.har`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
label: "Export All as HAR",
|
|
224
|
+
action: function (e, row) {
|
|
225
|
+
const allData = window.requestsTable.getData("active"); // get filtered data
|
|
226
|
+
const har = generateHAR(allData);
|
|
227
|
+
downloadString(JSON.stringify(har, null, 2), `requests-export.har`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Row selection handler
|
|
234
|
+
window.requestsTable.on("rowClick", function (e, row) {
|
|
235
|
+
showRequestDetails(row.getData());
|
|
44
236
|
});
|
|
45
237
|
|
|
46
|
-
// Auto-fetch on load
|
|
238
|
+
// Auto-fetch on load
|
|
47
239
|
fetchRequests();
|
|
48
|
-
});
|
|
49
240
|
|
|
50
|
-
|
|
241
|
+
// Resize Logic
|
|
242
|
+
initResizeHandle();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function initResizeHandle() {
|
|
246
|
+
const handle = document.getElementById('details-drag-handle');
|
|
247
|
+
const container = document.getElementById('request-details-container');
|
|
248
|
+
const listContainer = document.getElementById('requests-list-container');
|
|
249
|
+
if (!handle || !container) return;
|
|
250
|
+
|
|
251
|
+
let isResizing = false;
|
|
252
|
+
let startX, startWidth;
|
|
253
|
+
|
|
254
|
+
handle.addEventListener('mousedown', (e) => {
|
|
255
|
+
isResizing = true;
|
|
256
|
+
startX = e.clientX;
|
|
257
|
+
startWidth = container.offsetWidth;
|
|
258
|
+
document.body.style.cursor = 'col-resize';
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
document.addEventListener('mousemove', (e) => {
|
|
263
|
+
if (!isResizing) return;
|
|
264
|
+
// Calculate new width: It's expanding to the left, so moving mouse left increases width
|
|
265
|
+
const dx = startX - e.clientX;
|
|
266
|
+
const newWidth = Math.max(300, Math.min(window.innerWidth - 100, startWidth + dx));
|
|
267
|
+
container.style.width = `${newWidth}px`;
|
|
268
|
+
|
|
269
|
+
// Optional: trigger tabulator redraw if list container size changed significantly (it flexes)
|
|
270
|
+
if (window.requestsTable) window.requestsTable.redraw();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
document.addEventListener('mouseup', () => {
|
|
274
|
+
if (isResizing) {
|
|
275
|
+
isResizing = false;
|
|
276
|
+
document.body.style.cursor = '';
|
|
277
|
+
// Save width preference?
|
|
278
|
+
if (window.requestsTable) window.requestsTable.redraw();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function generateFetchCode(req) {
|
|
285
|
+
const headers = req.requestHeaders || {};
|
|
286
|
+
let code = `fetch("${req.url}", {\n`;
|
|
287
|
+
code += ` "method": "${req.method}",\n`;
|
|
288
|
+
code += ` "headers": ${JSON.stringify(headers, null, 2).replace(/\n/g, '\n ')},\n`;
|
|
289
|
+
|
|
290
|
+
if (req.requestBody) {
|
|
291
|
+
if (typeof req.requestBody === 'object') {
|
|
292
|
+
code += ` "body": JSON.stringify(${JSON.stringify(req.requestBody)}),\n`;
|
|
293
|
+
} else {
|
|
294
|
+
code += ` "body": ${JSON.stringify(req.requestBody)},\n`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
code += `});`;
|
|
298
|
+
return code;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function generateHAR(requests) {
|
|
302
|
+
return {
|
|
303
|
+
log: {
|
|
304
|
+
version: "1.2",
|
|
305
|
+
creator: { name: "Shokupan Dashboard", version: "1.0" },
|
|
306
|
+
entries: requests.map(req => ({
|
|
307
|
+
startedDateTime: new Date(req.timestamp).toISOString(),
|
|
308
|
+
time: req.duration,
|
|
309
|
+
request: {
|
|
310
|
+
method: req.method,
|
|
311
|
+
url: req.url,
|
|
312
|
+
httpVersion: req.protocol || "HTTP/1.1",
|
|
313
|
+
cookies: [], // Todo parse
|
|
314
|
+
headers: Object.entries(req.requestHeaders || {}).map(([name, value]) => ({ name, value })),
|
|
315
|
+
queryString: [], // Todo parse from url
|
|
316
|
+
postData: req.requestBody ? { mimeType: req.contentType || "application/json", text: JSON.stringify(req.requestBody) } : undefined,
|
|
317
|
+
headersSize: -1,
|
|
318
|
+
bodySize: -1
|
|
319
|
+
},
|
|
320
|
+
response: {
|
|
321
|
+
status: req.status,
|
|
322
|
+
statusText: "",
|
|
323
|
+
httpVersion: req.protocol || "HTTP/1.1",
|
|
324
|
+
cookies: [],
|
|
325
|
+
headers: Object.entries(req.responseHeaders || {}).map(([name, value]) => ({ name, value })),
|
|
326
|
+
content: {
|
|
327
|
+
size: req.size || 0,
|
|
328
|
+
mimeType: req.contentType || "",
|
|
329
|
+
text: typeof req.body === 'string' ? req.body : JSON.stringify(req.body)
|
|
330
|
+
},
|
|
331
|
+
redirectURL: "",
|
|
332
|
+
headersSize: -1,
|
|
333
|
+
bodySize: -1
|
|
334
|
+
},
|
|
335
|
+
cache: {},
|
|
336
|
+
timings: {
|
|
337
|
+
send: 0,
|
|
338
|
+
wait: req.duration,
|
|
339
|
+
receive: 0
|
|
340
|
+
}
|
|
341
|
+
}))
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function purgeRequests() {
|
|
347
|
+
if (!confirm("Are you sure you want to purge all captured requests?")) return;
|
|
348
|
+
|
|
51
349
|
const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
|
|
350
|
+
const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
|
|
351
|
+
// Need to handle if we are mounted at /dashboard vs /dashboard/ so stripping slice(-1) might be wrong if it wasn't there
|
|
352
|
+
// Safer:
|
|
353
|
+
let base = window.location.pathname;
|
|
354
|
+
if (base.endsWith('/')) base = base.slice(0, -1);
|
|
355
|
+
|
|
356
|
+
fetch(base + '/requests', {
|
|
357
|
+
method: 'DELETE',
|
|
358
|
+
headers
|
|
359
|
+
})
|
|
360
|
+
.then(res => res.json())
|
|
361
|
+
.then(data => {
|
|
362
|
+
if (data.success) {
|
|
363
|
+
console.log("Purge successful");
|
|
364
|
+
if (window.requestsTable) window.requestsTable.clearData();
|
|
365
|
+
closeRequestDetails();
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
.catch(console.error);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Robust initialization
|
|
372
|
+
let initAttempts = 0;
|
|
373
|
+
function tryInit() {
|
|
374
|
+
if (document.getElementById('requests-list-container') && typeof Tabulator !== 'undefined') {
|
|
375
|
+
try {
|
|
376
|
+
initRequests();
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.error('Failed to initialize requests table:', e);
|
|
379
|
+
const el = document.getElementById('requests-list-container');
|
|
380
|
+
if (el) el.innerHTML = `<div style="padding: 2rem; color: #ef4444;">Failed to initialize: ${e.message}</div>`;
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
initAttempts++;
|
|
384
|
+
if (initAttempts > 50) { // 5 seconds timeout
|
|
385
|
+
console.error('Request table initialization timed out. Tabulator is:', typeof Tabulator);
|
|
386
|
+
const el = document.getElementById('requests-list-container');
|
|
387
|
+
if (el) el.innerHTML = `<div style="padding: 2rem; color: #ef4444;">
|
|
388
|
+
Failed to load dependencies. <br>
|
|
389
|
+
Tabulator: ${typeof Tabulator}
|
|
390
|
+
</div>`;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
setTimeout(tryInit, 100);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
tryInit();
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
function customFilter(data) {
|
|
401
|
+
// Type Filter
|
|
402
|
+
if (filterType !== 'all') {
|
|
403
|
+
const type = data.type || 'xhr'; // default to xhr if missing
|
|
404
|
+
if (filterType === 'fetch' && type !== 'fetch') return false;
|
|
405
|
+
if (filterType === 'xhr' && type !== 'xhr') return false;
|
|
406
|
+
if (filterType === 'ws' && type !== 'ws') return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Direction Filter
|
|
410
|
+
if (filterDirection !== 'all') {
|
|
411
|
+
const dir = data.direction || 'inbound';
|
|
412
|
+
if (filterDirection !== dir) return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Text Filter (Regex-ish)
|
|
416
|
+
if (filterText) {
|
|
417
|
+
const text = (data.url + ' ' + data.method).toLowerCase();
|
|
418
|
+
return text.includes(filterText);
|
|
419
|
+
}
|
|
52
420
|
|
|
53
|
-
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function waterfallFormatter(cell) {
|
|
425
|
+
const data = cell.getData();
|
|
426
|
+
// We need a reference start time for the waterfall.
|
|
427
|
+
// For now, let's use the oldest timestamp in the current page/view or relative to 10 seconds ago?
|
|
428
|
+
// A better approach for "live" view is to just show bar width proportional to duration?
|
|
429
|
+
// Or relative to the start of the trace session.
|
|
430
|
+
|
|
431
|
+
// Simpler: Show duration bar relative to a fixed max (e.g. 1s or 5s).
|
|
432
|
+
// Or just a simple bar representing execution time.
|
|
433
|
+
|
|
434
|
+
// Let's do a "Time/Duration" visual.
|
|
435
|
+
const duration = data.duration || 0;
|
|
436
|
+
const maxDuration = 2000; // 2s baseline for full width
|
|
437
|
+
const pct = Math.min(100, (duration / maxDuration) * 100);
|
|
438
|
+
|
|
439
|
+
// Color based on duration
|
|
440
|
+
const color = duration > 1000 ? '#ef4444' : duration > 500 ? '#f59e0b' : '#3b82f6';
|
|
441
|
+
|
|
442
|
+
return `<div style="width: 100%; height: 100%; display: flex; align-items: center;">
|
|
443
|
+
<div style="height: 6px; width: ${pct}%; background: ${color}; border-radius: 3px; min-width: 2px;"></div>
|
|
444
|
+
</div>`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
function fetchRequests() {
|
|
450
|
+
const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
|
|
54
451
|
const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
|
|
55
|
-
const url = basePath + '/';
|
|
452
|
+
const url = basePath + '/requests';
|
|
56
453
|
|
|
57
|
-
fetch(url
|
|
454
|
+
fetch(url, { headers })
|
|
58
455
|
.then(res => res.json())
|
|
59
456
|
.then(data => {
|
|
60
|
-
if (requestsTable) {
|
|
61
|
-
requestsTable.setData(data.requests);
|
|
457
|
+
if (window.requestsTable) {
|
|
458
|
+
window.requestsTable.setData(data.requests || []);
|
|
459
|
+
window.requestsTable.setFilter(customFilter);
|
|
62
460
|
}
|
|
63
461
|
})
|
|
64
|
-
.catch(err =>
|
|
462
|
+
.catch(err => {
|
|
463
|
+
console.error("Failed to fetch requests", err);
|
|
464
|
+
});
|
|
65
465
|
}
|
|
66
466
|
|
|
467
|
+
|
|
468
|
+
|
|
67
469
|
function showRequestDetails(request) {
|
|
68
470
|
const container = document.getElementById('request-details-container');
|
|
69
471
|
const content = document.getElementById('request-details-content');
|
|
70
|
-
const traceContainer = document.getElementById('middleware-trace-container');
|
|
71
472
|
|
|
72
473
|
container.style.display = 'block';
|
|
474
|
+
if (window.requestsTable) window.requestsTable.redraw();
|
|
475
|
+
|
|
476
|
+
// Tab Headers
|
|
477
|
+
const tabs = [
|
|
478
|
+
{ id: 'headers', label: 'Headers' },
|
|
479
|
+
{ id: 'cookies', label: 'Cookies' },
|
|
480
|
+
{ id: 'request', label: 'Request' },
|
|
481
|
+
{ id: 'response', label: 'Response' },
|
|
482
|
+
{ id: 'timings', label: 'Timings' },
|
|
483
|
+
// { id: 'security', label: 'Security' } // Enable if we have data
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
if (request.scheme === 'https' || request.scheme === 'wss') {
|
|
487
|
+
tabs.push({ id: 'security', label: 'Security' });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let activeTab = 'headers';
|
|
491
|
+
|
|
492
|
+
function renderTabs() {
|
|
493
|
+
return `
|
|
494
|
+
<div class="tabs-header" style="display: flex; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem;">
|
|
495
|
+
${tabs.map(tab => `
|
|
496
|
+
<div class="tab-item ${tab.id === activeTab ? 'active' : ''}"
|
|
497
|
+
data-tab="${tab.id}"
|
|
498
|
+
style="padding: 8px 16px; cursor: pointer; border-bottom: 2px solid ${tab.id === activeTab ? 'var(--primary-color, #3b82f6)' : 'transparent'}; color: ${tab.id === activeTab ? 'var(--text-primary)' : 'var(--text-secondary)'};">
|
|
499
|
+
${tab.label}
|
|
500
|
+
</div>
|
|
501
|
+
`).join('')}
|
|
502
|
+
</div>
|
|
503
|
+
<div id="tab-content" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column;">
|
|
504
|
+
${renderTabContent(activeTab, request)}
|
|
505
|
+
</div>
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
content.innerHTML = renderTabs();
|
|
510
|
+
|
|
511
|
+
// Event Delegation for Tabs
|
|
512
|
+
content.onclick = (e) => {
|
|
513
|
+
const tabItem = e.target.closest('.tab-item');
|
|
514
|
+
if (tabItem) {
|
|
515
|
+
const newTab = tabItem.dataset.tab;
|
|
516
|
+
if (newTab !== activeTab) {
|
|
517
|
+
activeTab = newTab;
|
|
518
|
+
content.innerHTML = renderTabs();
|
|
519
|
+
// Re-initialize editors if needed
|
|
520
|
+
if (activeTab === 'response') initResponseEditor(request);
|
|
521
|
+
if (activeTab === 'request') initRequestEditor(request);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// Initial Editor Load
|
|
527
|
+
if (activeTab === 'response') initResponseEditor(request);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function renderTabContent(tabId, request) {
|
|
531
|
+
switch (tabId) {
|
|
532
|
+
case 'headers':
|
|
533
|
+
return renderHeadersTab(request);
|
|
534
|
+
case 'cookies':
|
|
535
|
+
return renderCookiesTab(request);
|
|
536
|
+
case 'request':
|
|
537
|
+
return renderRequestTab(request);
|
|
538
|
+
case 'response':
|
|
539
|
+
return renderResponseTab(request);
|
|
540
|
+
case 'timings':
|
|
541
|
+
return renderTimingsTab(request);
|
|
542
|
+
case 'security':
|
|
543
|
+
return renderSecurityTab(request);
|
|
544
|
+
default:
|
|
545
|
+
return '';
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderHeadersTab(request) {
|
|
550
|
+
const formatHeaderSection = (title, headers) => {
|
|
551
|
+
if (!headers || Object.keys(headers).length === 0) return '';
|
|
552
|
+
const rows = Object.entries(headers).map(([k, v]) => `
|
|
553
|
+
<tr>
|
|
554
|
+
<td style="font-weight: 500; color: var(--text-secondary); padding: 4px 8px; vertical-align: top;">${k}:</td>
|
|
555
|
+
<td style="word-break: break-all; padding: 4px 8px;">${v}</td>
|
|
556
|
+
</tr>
|
|
557
|
+
`).join('');
|
|
558
|
+
return `
|
|
559
|
+
<details open style="margin-bottom: 1rem;">
|
|
560
|
+
<summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">${title}</summary>
|
|
561
|
+
<table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
|
|
562
|
+
${rows}
|
|
563
|
+
</table>
|
|
564
|
+
</details>
|
|
565
|
+
`;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
return `
|
|
569
|
+
<div style="padding: 0 0.5rem;">
|
|
570
|
+
<details open style="margin-bottom: 1rem;">
|
|
571
|
+
<summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">General</summary>
|
|
572
|
+
<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9em; padding-left: 8px;">
|
|
573
|
+
<div style="color: var(--text-secondary);">Request URL:</div><div style="word-break: break-all;">${request.url}</div>
|
|
574
|
+
<div style="color: var(--text-secondary);">Request Method:</div><div>${request.method}</div>
|
|
575
|
+
<div style="color: var(--text-secondary);">Status Code:</div><div>${request.status}</div>
|
|
576
|
+
<div style="color: var(--text-secondary);">Remote Address:</div><div>${request.remoteIP || '-'}</div>
|
|
577
|
+
<div style="color: var(--text-secondary);">Referrer Policy:</div><div>${request.requestHeaders?.['referrer-policy'] || 'strict-origin-when-cross-origin'}</div>
|
|
578
|
+
</div>
|
|
579
|
+
</details>
|
|
580
|
+
${formatHeaderSection('Response Headers', request.responseHeaders)}
|
|
581
|
+
${formatHeaderSection('Request Headers', request.requestHeaders)}
|
|
582
|
+
</div>
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function renderCookiesTab(request) {
|
|
587
|
+
// Parse Cookies
|
|
588
|
+
const reqCookies = request.requestHeaders?.['cookie']
|
|
589
|
+
? request.requestHeaders['cookie'].split(';').map(c => {
|
|
590
|
+
const [k, v] = c.trim().split('=');
|
|
591
|
+
return { name: k, value: v };
|
|
592
|
+
})
|
|
593
|
+
: [];
|
|
594
|
+
|
|
595
|
+
// Naive Set-Cookie parsing (often an array, but we might have it merged or as single string depending on collection)
|
|
596
|
+
// If headers are just Record<string, string>, Set-Cookie might be joined by comma, which is bad for automated parsing if values contain commas.
|
|
597
|
+
// For now, let's assume one or basic parsing.
|
|
598
|
+
let resCookies = [];
|
|
599
|
+
if (request.responseHeaders?.['set-cookie']) {
|
|
600
|
+
// This is tricky if multiple set-cookies are merged.
|
|
601
|
+
// Assuming a simple array or single string for now.
|
|
602
|
+
// If generic Record<string,string> was used, multiple set-cookies might be lost or merged.
|
|
603
|
+
// We'll display what we have.
|
|
604
|
+
resCookies = [{ name: 'Set-Cookie', value: request.responseHeaders['set-cookie'] }];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const renderTable = (cookies) => {
|
|
608
|
+
if (!cookies.length) return '<div style="padding: 8px; color: var(--text-secondary);">No cookies found</div>';
|
|
609
|
+
return `
|
|
610
|
+
<table style="width: 100%; text-align: left; border-collapse: collapse; font-size: 0.9em;">
|
|
611
|
+
<thead>
|
|
612
|
+
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
613
|
+
<th style="padding: 4px 8px;">Name</th>
|
|
614
|
+
<th style="padding: 4px 8px;">Value</th>
|
|
615
|
+
</tr>
|
|
616
|
+
</thead>
|
|
617
|
+
<tbody>
|
|
618
|
+
${cookies.map(c => `
|
|
619
|
+
<tr style="border-bottom: 1px solid var(--border-color-dim, #33333333);">
|
|
620
|
+
<td style="padding: 4px 8px; font-weight: 500;">${c.name}</td>
|
|
621
|
+
<td style="padding: 4px 8px; word-break: break-all;">${c.value}</td>
|
|
622
|
+
</tr>
|
|
623
|
+
`).join('')}
|
|
624
|
+
</tbody>
|
|
625
|
+
</table>
|
|
626
|
+
`;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
return `
|
|
630
|
+
<div style="padding: 0 0.5rem; display: flex; flex-direction: column; gap: 1rem;">
|
|
631
|
+
<div>
|
|
632
|
+
<div style="font-weight: bold; margin-bottom: 0.5rem;">Request Cookies</div>
|
|
633
|
+
${renderTable(reqCookies)}
|
|
634
|
+
</div>
|
|
635
|
+
<div>
|
|
636
|
+
<div style="font-weight: bold; margin-bottom: 0.5rem;">Response Cookies</div>
|
|
637
|
+
${renderTable(resCookies)}
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function renderRequestTab(request) {
|
|
644
|
+
if (!request.requestBody && !request.body) return '<div style="padding: 1rem; color: var(--text-secondary);">No payload</div>';
|
|
645
|
+
return `
|
|
646
|
+
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
647
|
+
<div style="display: flex; justify-content: flex-end; padding: 4px;">
|
|
648
|
+
<button class="btn-action" onclick="copyToClipboard(currentRequestBody)">Copy</button>
|
|
649
|
+
</div>
|
|
650
|
+
<div id="request-body-editor" style="flex: 1; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; min-height: 200px;"></div>
|
|
651
|
+
</div>
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function renderResponseTab(request) {
|
|
656
|
+
if (!request.responseBody && !request.body) return '<div style="padding: 1rem; color: var(--text-secondary);">No content</div>';
|
|
657
|
+
|
|
658
|
+
return `
|
|
659
|
+
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
660
|
+
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px; border-bottom: 1px solid var(--border-color);">
|
|
661
|
+
<div style="font-size: 0.8em; color: var(--text-secondary);">${formatBytes(request.size || 0)}</div>
|
|
662
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
663
|
+
<label style="display: flex; align-items: center; gap: 4px; font-size: 0.8rem; cursor: pointer; user-select: none;">
|
|
664
|
+
<input type="checkbox" id="auto-format-check" ${window.autoFormatEnabled !== false ? 'checked' : ''}> Format
|
|
665
|
+
</label>
|
|
666
|
+
<div style="width: 1px; height: 16px; background: var(--border-color); margin: 0 4px;"></div>
|
|
667
|
+
<button id="btn-copy-body" class="btn-action" title="Copy Body">Copy</button>
|
|
668
|
+
<button id="btn-download-body" class="btn-action" title="Download Body">Download</button>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
<div id="response-body-editor" style="flex: 1; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; min-height: 200px;"></div>
|
|
672
|
+
</div>
|
|
673
|
+
`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function renderTimingsTab(request) {
|
|
677
|
+
// Placeholder for timings visualization
|
|
678
|
+
return `
|
|
679
|
+
<div style="padding: 1rem;">
|
|
680
|
+
<div style="display: grid; grid-template-columns: 1fr auto; gap: 8px; max-width: 400px; font-size: 0.9em;">
|
|
681
|
+
<div>Started At:</div><div>${new Date(request.timestamp).toLocaleString()}</div>
|
|
682
|
+
<div>Duration:</div><div>${request.duration.toFixed(2)} ms</div>
|
|
683
|
+
<div style="border-top: 1px solid var(--border-color); margin-top:8px; padding-top:8px; font-weight:bold;">Total Transferred:</div><div style="border-top: 1px solid var(--border-color); margin-top:8px; padding-top:8px; font-weight:bold;">${formatBytes(request.transferred || request.size || 0)}</div>
|
|
684
|
+
</div>
|
|
685
|
+
<!-- Future: detailed breakdown -->
|
|
686
|
+
</div>
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
73
689
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<div style="
|
|
77
|
-
<div
|
|
78
|
-
<div
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
690
|
+
function renderSecurityTab(request) {
|
|
691
|
+
return `
|
|
692
|
+
<div style="padding: 1rem;">
|
|
693
|
+
<div style="margin-bottom: 1rem; font-weight: bold;">Connection</div>
|
|
694
|
+
<div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9em;">
|
|
695
|
+
<div style="color: var(--text-secondary);">Protocol:</div><div>${request.protocol || request.scheme || 'tls'}</div>
|
|
696
|
+
<div style="color: var(--text-secondary);">Remote Address:</div><div>${request.remoteIP || 'Unknown'}</div>
|
|
697
|
+
</div>
|
|
698
|
+
<div style="margin-top: 1rem; color: var(--text-secondary); font-style: italic;">
|
|
699
|
+
Detailed certificate information is not currently captured by the interceptor.
|
|
700
|
+
</div>
|
|
82
701
|
</div>
|
|
83
702
|
`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
// Attach event listeners
|
|
707
|
+
// document.getElementById('auto-format-check').onchange = (e) => {
|
|
708
|
+
// window.autoFormatEnabled = e.target.checked;
|
|
709
|
+
// renderMonacoEditor(request);
|
|
710
|
+
// };
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
function closeRequestDetails() {
|
|
714
|
+
document.getElementById('request-details-container').style.display = 'none';
|
|
715
|
+
if (window.requestsTable) window.requestsTable.redraw();
|
|
716
|
+
}
|
|
717
|
+
window.closeRequestDetails = closeRequestDetails;
|
|
84
718
|
|
|
85
|
-
|
|
719
|
+
function renderTrace(request, container) {
|
|
86
720
|
if (request.handlerStack && request.handlerStack.length > 0) {
|
|
721
|
+
const totalDuration = request.duration || 1;
|
|
87
722
|
let html = '<div style="display: flex; flex-direction: column; gap: 4px;">';
|
|
88
723
|
|
|
89
724
|
request.handlerStack.forEach((item, index) => {
|
|
90
|
-
const duration = item.duration
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
html += `<div style="align-self: center">⬇︎</div>`;
|
|
94
|
-
}
|
|
725
|
+
const duration = item.duration > 0 ? item.duration : 0.01;
|
|
726
|
+
const percent = Math.min(100, Math.max(1, (duration / totalDuration) * 100));
|
|
727
|
+
const isSlow = percent > 15;
|
|
95
728
|
|
|
96
729
|
html += `
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</div>
|
|
102
|
-
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
|
103
|
-
${item.file}:${item.line}
|
|
104
|
-
</div>
|
|
105
|
-
${item.stateChanges ? `<div style="font-size: 0.8rem; margin-top: 4px; color: #aaa;">State Changes: ${Object.keys(item.stateChanges).join(', ')}</div>` : ''}
|
|
730
|
+
<div style="padding: 8px; border-radius: 4px; background: var(--bg-primary); border-left: 3px solid ${isSlow ? 'var(--color-warning)' : 'var(--color-success)'};">
|
|
731
|
+
<div style="display: flex; justify-content: space-between; font-size: 0.9em;">
|
|
732
|
+
<span style="font-weight: 500;">${item.name}</span>
|
|
733
|
+
<span style="font-family: monospace;">${printDuration(duration)}</span>
|
|
106
734
|
</div>
|
|
107
|
-
|
|
108
|
-
|
|
735
|
+
<div style="height: 3px; background: var(--bg-secondary); margin-top: 4px; border-radius: 2px; overflow: hidden;">
|
|
736
|
+
<div style="height: 100%; width: ${percent}%; background: ${isSlow ? 'var(--color-warning)' : 'var(--color-success)'}; opacity: 0.8;"></div>
|
|
737
|
+
</div>
|
|
738
|
+
</div>`;
|
|
109
739
|
|
|
740
|
+
if (index < request.handlerStack.length - 1) {
|
|
741
|
+
html += `<div style="display: flex; justify-content: center; height: 10px;"><div style="width: 1px; background: var(--border-color); opacity: 0.5;"></div></div>`;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
110
744
|
html += '</div>';
|
|
111
|
-
|
|
745
|
+
container.innerHTML = html;
|
|
112
746
|
} else {
|
|
113
|
-
|
|
747
|
+
container.innerHTML = `<div style="padding: 2rem; text-align: center; color: var(--text-secondary);">No trace data</div>`;
|
|
114
748
|
}
|
|
749
|
+
}
|
|
115
750
|
|
|
116
|
-
|
|
117
|
-
|
|
751
|
+
function getExtension(contentType) {
|
|
752
|
+
if (!contentType) return 'txt';
|
|
753
|
+
if (contentType.includes('json')) return 'json';
|
|
754
|
+
if (contentType.includes('html')) return 'html';
|
|
755
|
+
if (contentType.includes('xml')) return 'xml';
|
|
756
|
+
if (contentType.includes('javascript')) return 'js';
|
|
757
|
+
if (contentType.includes('css')) return 'css';
|
|
758
|
+
return 'txt';
|
|
118
759
|
}
|
|
760
|
+
|
|
761
|
+
function getRequestBody(request) {
|
|
762
|
+
let value = request.body || '';
|
|
763
|
+
if (typeof value === 'object') {
|
|
764
|
+
try {
|
|
765
|
+
value = JSON.stringify(value, null, 2);
|
|
766
|
+
} catch (e) {
|
|
767
|
+
value = String(value);
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
value = String(value);
|
|
771
|
+
}
|
|
772
|
+
return value;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let currentRequestBody = ''; // Global/Module scope tracking for request body
|
|
776
|
+
|
|
777
|
+
function initRequestEditor(request) {
|
|
778
|
+
const el = document.getElementById('request-body-editor');
|
|
779
|
+
if (!el) return;
|
|
780
|
+
|
|
781
|
+
let content = request.requestBody || request.body || '';
|
|
782
|
+
let language = 'plaintext';
|
|
783
|
+
const contentType = (request.requestHeaders?.['content-type'] || '').toLowerCase();
|
|
784
|
+
|
|
785
|
+
if (contentType.includes('json')) language = 'json';
|
|
786
|
+
else if (contentType.includes('html')) language = 'html';
|
|
787
|
+
else if (contentType.includes('xml')) language = 'xml';
|
|
788
|
+
else if (contentType.includes('javascript') || contentType.includes('application/x-javascript')) language = 'javascript';
|
|
789
|
+
else if (contentType.includes('css')) language = 'css';
|
|
790
|
+
else if (contentType.includes('typescript')) language = 'typescript';
|
|
791
|
+
else if (contentType.includes('markdown')) language = 'markdown';
|
|
792
|
+
else if (contentType.includes('sql')) language = 'sql';
|
|
793
|
+
else if (contentType.includes('yaml')) language = 'yaml';
|
|
794
|
+
|
|
795
|
+
if (typeof content === 'object') {
|
|
796
|
+
content = JSON.stringify(content, null, 2);
|
|
797
|
+
language = 'json';
|
|
798
|
+
} else if (typeof content === 'string') {
|
|
799
|
+
// Auto-detect JSON if content looks like JSON but header is wrong
|
|
800
|
+
if (language === 'plaintext' && (content.trim().startsWith('{') || content.trim().startsWith('['))) {
|
|
801
|
+
try {
|
|
802
|
+
JSON.parse(content);
|
|
803
|
+
language = 'json';
|
|
804
|
+
} catch (e) { /* not json */ }
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
currentRequestBody = content; // store for copy
|
|
809
|
+
|
|
810
|
+
renderMonacoEditor(el, content, language, false); // Request body usually not auto-formatted
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function initResponseEditor(request) {
|
|
814
|
+
const el = document.getElementById('response-body-editor');
|
|
815
|
+
if (!el) return;
|
|
816
|
+
|
|
817
|
+
let content = request.body || request.responseBody;
|
|
818
|
+
let contentType = request.contentType || '';
|
|
819
|
+
|
|
820
|
+
if (!content) content = '';
|
|
821
|
+
|
|
822
|
+
// Auto-Format Logic
|
|
823
|
+
let language = 'plaintext';
|
|
824
|
+
if (contentType.includes('json')) language = 'json';
|
|
825
|
+
else if (contentType.includes('html')) language = 'html';
|
|
826
|
+
else if (contentType.includes('xml')) language = 'xml';
|
|
827
|
+
else if (contentType.includes('javascript') || contentType.includes('application/x-javascript')) language = 'javascript';
|
|
828
|
+
else if (contentType.includes('css')) language = 'css';
|
|
829
|
+
else if (contentType.includes('typescript')) language = 'typescript';
|
|
830
|
+
else if (contentType.includes('markdown')) language = 'markdown';
|
|
831
|
+
else if (contentType.includes('sql')) language = 'sql';
|
|
832
|
+
else if (contentType.includes('yaml')) language = 'yaml';
|
|
833
|
+
|
|
834
|
+
if (typeof content === 'object') {
|
|
835
|
+
content = JSON.stringify(content, null, 2);
|
|
836
|
+
language = 'json';
|
|
837
|
+
} else if (window.autoFormatEnabled !== false && typeof content === 'string') {
|
|
838
|
+
// Try auto-detect JSON if string
|
|
839
|
+
if ((content.trim().startsWith('{') || content.trim().startsWith('[')) && content.length < 524288) {
|
|
840
|
+
try {
|
|
841
|
+
const parsed = JSON.parse(content);
|
|
842
|
+
content = JSON.stringify(parsed, null, 2);
|
|
843
|
+
language = 'json';
|
|
844
|
+
} catch (e) { /* not json */ }
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
renderMonacoEditor(el, content, language, window.autoFormatEnabled !== false);
|
|
849
|
+
|
|
850
|
+
// Attach button listeners
|
|
851
|
+
const btnCopy = document.getElementById('btn-copy-body');
|
|
852
|
+
const btnDownload = document.getElementById('btn-download-body');
|
|
853
|
+
if (btnCopy) btnCopy.onclick = () => copyToClipboard(getRequestBody(request));
|
|
854
|
+
if (btnDownload) btnDownload.onclick = () => downloadString(getRequestBody(request), `body-${request.timestamp}.${getExtension(request.contentType)}`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
let currentMonacoEditor = null; // To manage the active editor instance
|
|
858
|
+
|
|
859
|
+
function renderMonacoEditor(containerElement, value, language, shouldFormat = false) {
|
|
860
|
+
if (!window.monaco) {
|
|
861
|
+
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
|
|
862
|
+
require(['vs/editor/editor.main'], function () { renderMonacoEditor(containerElement, value, language, shouldFormat); });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
window.currentEditor = monaco.editor.create(containerElement, {
|
|
867
|
+
value: value,
|
|
868
|
+
language: language,
|
|
869
|
+
theme: 'vs-dark',
|
|
870
|
+
readOnly: true,
|
|
871
|
+
minimap: { enabled: false },
|
|
872
|
+
scrollBeyondLastLine: false,
|
|
873
|
+
automaticLayout: true,
|
|
874
|
+
wordWrap: 'on'
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
if (shouldFormat) {
|
|
878
|
+
setTimeout(() => {
|
|
879
|
+
if (window.currentEditor) {
|
|
880
|
+
window.currentEditor.getAction('editor.action.formatDocument').run();
|
|
881
|
+
}
|
|
882
|
+
}, 100);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function copyToClipboard(text) {
|
|
887
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
888
|
+
const btn = document.activeElement;
|
|
889
|
+
if (btn && btn.tagName === 'BUTTON') {
|
|
890
|
+
const original = btn.innerText;
|
|
891
|
+
btn.innerText = 'Copied!';
|
|
892
|
+
setTimeout(() => btn.innerText = original, 1500);
|
|
893
|
+
}
|
|
894
|
+
}).catch(err => console.error('Failed to copy', err));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function downloadString(text, filename) {
|
|
898
|
+
const blob = new Blob([text], { type: 'text/plain' });
|
|
899
|
+
const url = URL.createObjectURL(blob);
|
|
900
|
+
const a = document.createElement('a');
|
|
901
|
+
a.href = url;
|
|
902
|
+
a.download = filename;
|
|
903
|
+
a.click();
|
|
904
|
+
URL.revokeObjectURL(url);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function formatBytes(bytes, decimals = 2) {
|
|
908
|
+
if (!+bytes) return '0 B';
|
|
909
|
+
const k = 1024;
|
|
910
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
911
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
912
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
913
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
window.updateRequestsList = function (newRequests) {
|
|
917
|
+
if (!window.requestsTable || !newRequests || newRequests.length === 0) return;
|
|
918
|
+
|
|
919
|
+
// console.log('[requests.js] Adding/Updating', newRequests.length, 'rows');
|
|
920
|
+
window.requestsTable.updateOrAddData(newRequests)
|
|
921
|
+
.then(() => {
|
|
922
|
+
// Force redraw/filter application
|
|
923
|
+
window.requestsTable.recalc();
|
|
924
|
+
window.requestsTable.redraw();
|
|
925
|
+
// console.log('[requests.js] Table updated');
|
|
926
|
+
})
|
|
927
|
+
.catch(err => console.error("Failed to update table data", err));
|
|
928
|
+
};
|