@inspectr/ui 0.0.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 +674 -0
- package/index.d.ts +8 -0
- package/package.json +56 -0
- package/src/components/InspectrApp.jsx +113 -0
- package/src/components/RequestContent.jsx +113 -0
- package/src/components/RequestDetail.jsx +51 -0
- package/src/components/RequestDetailsPanel.jsx +52 -0
- package/src/components/RequestList.jsx +48 -0
- package/src/components/RequestListItem.jsx +52 -0
- package/src/components/ResponseContent.jsx +89 -0
- package/src/components/SSETest.jsx +22 -0
- package/src/components/SettingsPanel.jsx +93 -0
- package/src/components/index.js +10 -0
- package/src/index.js +2 -0
- package/src/styles/global.css +5 -0
- package/src/utils/getStatusClass.js +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inspectr/ui",
|
|
3
|
+
"description": "React UI components for Inspectr",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"author": "Tim Haselaars",
|
|
7
|
+
"homepage": "https://github.com/thim81/inspectr#readme",
|
|
8
|
+
"bugs": "https://github.com/thim81/inspectr-ui/issues",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/thim81/inspectr-ui.git"
|
|
12
|
+
},
|
|
13
|
+
"main": "src/index.js",
|
|
14
|
+
"module": "src/index.js",
|
|
15
|
+
"types": "index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"src/components",
|
|
18
|
+
"src/styles",
|
|
19
|
+
"src/utils",
|
|
20
|
+
"src/index.js",
|
|
21
|
+
"index.d.ts"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"storybook": "storybook dev -p 6006",
|
|
25
|
+
"build-storybook": "storybook build",
|
|
26
|
+
"release": "npx np --branch main",
|
|
27
|
+
"test": "echo 'test completed'"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@monaco-editor/react": "^4.7.0-rc.0",
|
|
31
|
+
"tailwindcss": "^4.0.5"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^19.0.0",
|
|
35
|
+
"react-dom": "^19.0.0",
|
|
36
|
+
"postcss": "^8.5.1",
|
|
37
|
+
"@tailwindcss/postcss": "^4.0.5"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.0.8",
|
|
41
|
+
"@types/react-dom": "^19.0.3",
|
|
42
|
+
"@chromatic-com/storybook": "^3.2.4",
|
|
43
|
+
"@storybook/addon-essentials": "^8.5.3",
|
|
44
|
+
"@storybook/addon-interactions": "^8.5.3",
|
|
45
|
+
"@storybook/addon-onboarding": "^8.5.3",
|
|
46
|
+
"@storybook/blocks": "^8.5.3",
|
|
47
|
+
"@storybook/react": "^8.5.3",
|
|
48
|
+
"@storybook/react-vite": "^8.5.3",
|
|
49
|
+
"@storybook/test": "^8.5.3",
|
|
50
|
+
"@tailwindcss/vite": "^4.0.5",
|
|
51
|
+
"storybook": "^8.5.3"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/components/InspectrApp.jsx
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import RequestList from './RequestList';
|
|
4
|
+
import RequestDetailsPanel from './RequestDetailsPanel';
|
|
5
|
+
import SettingsPanel from "./SettingsPanel";
|
|
6
|
+
|
|
7
|
+
const InspectrApp = ({ sseEndpoint: propSseEndpoint }) => {
|
|
8
|
+
const [requests, setRequests] = useState([]);
|
|
9
|
+
const [selectedRequest, setSelectedRequest] = useState(null);
|
|
10
|
+
const [currentTab, setCurrentTab] = useState('request');
|
|
11
|
+
const [sseEndpoint, setSseEndpoint] = useState('/api/sse');
|
|
12
|
+
const [isConnected, setIsConnected] = useState(false); // Track connection status
|
|
13
|
+
|
|
14
|
+
// Ensure localStorage is only accessed on the client
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (typeof window !== "undefined") {
|
|
17
|
+
const storedSseEndpoint = localStorage.getItem("sseEndpoint");
|
|
18
|
+
if (!propSseEndpoint && storedSseEndpoint) {
|
|
19
|
+
setSseEndpoint(storedSseEndpoint);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}, [sseEndpoint]);
|
|
23
|
+
|
|
24
|
+
// Connect to SSE when the component mounts.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const generateId = () => `req-${Math.random().toString(36).substr(2, 9)}`;
|
|
27
|
+
|
|
28
|
+
const eventSource = new EventSource(sseEndpoint);
|
|
29
|
+
console.log(`EventSource created with URL: ${sseEndpoint}`);
|
|
30
|
+
|
|
31
|
+
eventSource.onopen = () => {
|
|
32
|
+
console.log('SSE connection opened');
|
|
33
|
+
setIsConnected(true);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
eventSource.onmessage = (e) => {
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(e.data);
|
|
39
|
+
// console.log('Received event:', data);
|
|
40
|
+
// Update the list and, if it's the first event, select it.
|
|
41
|
+
if (!data.id) data.id = generateId();
|
|
42
|
+
setRequests((prev) => {
|
|
43
|
+
if (prev.length === 0) {
|
|
44
|
+
setSelectedRequest(data);
|
|
45
|
+
}
|
|
46
|
+
return [data, ...prev];
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error parsing SSE data:', error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
eventSource.onerror = (err) => {
|
|
54
|
+
console.error('SSE error:', err);
|
|
55
|
+
setIsConnected(false);
|
|
56
|
+
// The EventSource object will try to reconnect automatically.
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
console.log('Closing EventSource');
|
|
61
|
+
eventSource.close();
|
|
62
|
+
setIsConnected(false);
|
|
63
|
+
};
|
|
64
|
+
}, [sseEndpoint]); // Run only once on mount
|
|
65
|
+
|
|
66
|
+
const clearRequests = () => {
|
|
67
|
+
setRequests([]);
|
|
68
|
+
setSelectedRequest(null);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const removeRequest = (reqId) => {
|
|
72
|
+
setRequests((prev) =>
|
|
73
|
+
prev.filter((req, i) => (req.id ? req.id !== reqId : i !== reqId))
|
|
74
|
+
);
|
|
75
|
+
if (selectedRequest && (selectedRequest.id || '') === reqId) {
|
|
76
|
+
setSelectedRequest(null);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex flex-col h-screen">
|
|
82
|
+
<div className="flex flex-grow">
|
|
83
|
+
{/* Left Panel */}
|
|
84
|
+
<div className="w-1/3 border-r border-gray-300 overflow-y-auto">
|
|
85
|
+
<RequestList
|
|
86
|
+
requests={requests}
|
|
87
|
+
onSelect={setSelectedRequest}
|
|
88
|
+
onRemove={removeRequest}
|
|
89
|
+
clearRequests={clearRequests}
|
|
90
|
+
selectedRequest={selectedRequest}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Right Panel */}
|
|
95
|
+
<div className="w-2/3 p-4">
|
|
96
|
+
<RequestDetailsPanel
|
|
97
|
+
request={selectedRequest}
|
|
98
|
+
currentTab={currentTab}
|
|
99
|
+
setCurrentTab={setCurrentTab}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
{/* Bottom Panel */}
|
|
104
|
+
<SettingsPanel
|
|
105
|
+
sseEndpoint={sseEndpoint}
|
|
106
|
+
setSseEndpoint={setSseEndpoint}
|
|
107
|
+
isConnected={isConnected}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default InspectrApp;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/components/RequestContent.jsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import Editor from '@monaco-editor/react';
|
|
4
|
+
|
|
5
|
+
const RequestContent = ({ request }) => {
|
|
6
|
+
const [showQueryParams, setShowQueryParams] = useState(false);
|
|
7
|
+
const [showRequestHeaders, setShowRequestHeaders] = useState(false);
|
|
8
|
+
|
|
9
|
+
const renderTableRows = (data) =>
|
|
10
|
+
Object.entries(data || {}).map(([key, value]) => (
|
|
11
|
+
<tr key={key}>
|
|
12
|
+
<td className="border border-slate-200 px-2 py-1 font-mono text-slate-500 text-xs">
|
|
13
|
+
{key}
|
|
14
|
+
</td>
|
|
15
|
+
<td className="border border-slate-200 px-2 py-1 font-mono text-xs">{value}</td>
|
|
16
|
+
</tr>
|
|
17
|
+
));
|
|
18
|
+
|
|
19
|
+
// Check if the request body has content.
|
|
20
|
+
const payload = request.request.payload;
|
|
21
|
+
const isEmptyPayload =
|
|
22
|
+
!payload ||
|
|
23
|
+
(typeof payload === 'object' && Object.keys(payload).length === 0) ||
|
|
24
|
+
(typeof payload === 'string' &&
|
|
25
|
+
(payload.trim() === '' || payload.trim() === '{}'));
|
|
26
|
+
|
|
27
|
+
const formatPayload = (payload) => {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(payload);
|
|
30
|
+
return JSON.stringify(parsed, null, 2);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
{/* Query Parameters Section */}
|
|
39
|
+
<div className="mb-4">
|
|
40
|
+
<button
|
|
41
|
+
className="w-full p-2 text-left font-bold bg-gray-200"
|
|
42
|
+
onClick={() => setShowQueryParams(!showQueryParams)}
|
|
43
|
+
>
|
|
44
|
+
Query Parameters ({Object.keys(request.request.queryParams || {}).length})
|
|
45
|
+
</button>
|
|
46
|
+
{showQueryParams && (
|
|
47
|
+
<div className="p-0">
|
|
48
|
+
<table className="w-full border-collapse border border-gray-300">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr className="bg-gray-100">
|
|
51
|
+
<th className="border border-slate-200 px-2 py-1 w-1/4 text-left">Key</th>
|
|
52
|
+
<th className="border border-slate-200 px-2 py-1 text-left">Value</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>{renderTableRows(request.request.queryParams)}</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Request Headers Section */}
|
|
62
|
+
<div className="mb-4">
|
|
63
|
+
<button
|
|
64
|
+
className="w-full p-2 text-left font-bold bg-gray-200"
|
|
65
|
+
onClick={() => setShowRequestHeaders(!showRequestHeaders)}
|
|
66
|
+
>
|
|
67
|
+
Headers ({Object.keys(request.request.headers || {}).length})
|
|
68
|
+
</button>
|
|
69
|
+
{showRequestHeaders && (
|
|
70
|
+
<div className="p-0">
|
|
71
|
+
<table className="w-full border-collapse border border-gray-300">
|
|
72
|
+
<thead>
|
|
73
|
+
<tr className="bg-gray-100">
|
|
74
|
+
<th className="border border-slate-200 px-2 py-1 w-1/4 text-left">Header</th>
|
|
75
|
+
<th className="border border-slate-200 px-2 py-1 text-left">Value</th>
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody>{renderTableRows(request.request.headers)}</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Request Body Section */}
|
|
85
|
+
<div>
|
|
86
|
+
<button className="w-full p-2 text-left font-bold bg-gray-200">
|
|
87
|
+
Request Body
|
|
88
|
+
</button>
|
|
89
|
+
{isEmptyPayload ? (
|
|
90
|
+
<div className="hidden"></div>
|
|
91
|
+
) : (
|
|
92
|
+
<div className="bg-white rounded-b shadow p-0 h-100">
|
|
93
|
+
<Editor
|
|
94
|
+
height="100%"
|
|
95
|
+
defaultLanguage="json"
|
|
96
|
+
value={formatPayload(payload)}
|
|
97
|
+
options={{
|
|
98
|
+
readOnly: true,
|
|
99
|
+
minimap: { enabled: false },
|
|
100
|
+
automaticLayout: true,
|
|
101
|
+
fontFamily: '"Cascadia Code", "Jetbrains Mono", "Fira Code", "Menlo", "Consolas", monospace',
|
|
102
|
+
tabSize: 2,
|
|
103
|
+
scrollBeyondLastLine: false
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default RequestContent;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/components/RequestDetail.jsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {getStatusClass} from "../utils/getStatusClass.js";
|
|
4
|
+
|
|
5
|
+
const RequestDetail = ({ request }) => {
|
|
6
|
+
|
|
7
|
+
const formatTimestamp = (isoString) => {
|
|
8
|
+
if (!isoString) return "N/A"; // Handle missing timestamp
|
|
9
|
+
const date = new Date(isoString);
|
|
10
|
+
|
|
11
|
+
const formattedDate = date.toLocaleDateString("en-CA", { // YYYY-MM-DD format
|
|
12
|
+
year: "numeric",
|
|
13
|
+
month: "2-digit",
|
|
14
|
+
day: "2-digit",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const formattedTime = date.toLocaleTimeString([], { // HH:MM:SS in local time
|
|
18
|
+
hour: "2-digit",
|
|
19
|
+
minute: "2-digit",
|
|
20
|
+
second: "2-digit",
|
|
21
|
+
hour12: false, // 24-hour format
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return `${formattedDate} at ${formattedTime}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mb-4 p-4 bg-white rounded shadow">
|
|
29
|
+
<h2 className="font-bold text-lg mb-2">Request Details</h2>
|
|
30
|
+
<div className="flex flex-col space-y-1">
|
|
31
|
+
<div className="flex items-center space-x-2 font-mono text-lg">
|
|
32
|
+
<span className="font-bold">{request.request.method}</span>
|
|
33
|
+
<span className="text-blue-600">{request.endpoint}</span>
|
|
34
|
+
<span
|
|
35
|
+
className={`inline-flex px-3 py-1 text-xs font-semibold rounded-full ${getStatusClass(
|
|
36
|
+
request.response.status
|
|
37
|
+
)}`}
|
|
38
|
+
>
|
|
39
|
+
{request.response.status}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="text-gray-600">{request.url}</div>
|
|
43
|
+
<div className="text-gray-500 text-xs">
|
|
44
|
+
Received on {formatTimestamp(request.timestamp)} • Took {request.latency}ms to respond
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default RequestDetail;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/components/RequestDetailsPanel.jsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import RequestDetail from './RequestDetail';
|
|
4
|
+
import RequestContent from './RequestContent';
|
|
5
|
+
import ResponseContent from './ResponseContent';
|
|
6
|
+
|
|
7
|
+
const RequestDetailsPanel = ({ request, currentTab, setCurrentTab }) => {
|
|
8
|
+
if (!request) {
|
|
9
|
+
return <div className="text-gray-500">No request selected.</div>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<RequestDetail request={request} />
|
|
15
|
+
|
|
16
|
+
{/* Tabs for Request and Response */}
|
|
17
|
+
<div className="flex space-x-2">
|
|
18
|
+
<button
|
|
19
|
+
className={`px-4 py-2 rounded-t ${
|
|
20
|
+
currentTab === 'request'
|
|
21
|
+
? 'bg-blue-600 text-white'
|
|
22
|
+
: 'bg-gray-200 text-gray-700'
|
|
23
|
+
}`}
|
|
24
|
+
onClick={() => setCurrentTab('request')}
|
|
25
|
+
>
|
|
26
|
+
Request
|
|
27
|
+
</button>
|
|
28
|
+
<button
|
|
29
|
+
className={`px-4 py-2 rounded-t ${
|
|
30
|
+
currentTab === 'response'
|
|
31
|
+
? 'bg-blue-600 text-white'
|
|
32
|
+
: 'bg-gray-200 text-gray-700'
|
|
33
|
+
}`}
|
|
34
|
+
onClick={() => setCurrentTab('response')}
|
|
35
|
+
>
|
|
36
|
+
Response
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Tab Content */}
|
|
41
|
+
<div className="p-4 bg-white rounded-b shadow">
|
|
42
|
+
{currentTab === 'request' ? (
|
|
43
|
+
<RequestContent request={request} />
|
|
44
|
+
) : (
|
|
45
|
+
<ResponseContent request={request} />
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default RequestDetailsPanel;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/components/RequestList.jsx
|
|
2
|
+
import React, {useState} from 'react';
|
|
3
|
+
import RequestListItem from './RequestListItem';
|
|
4
|
+
|
|
5
|
+
const RequestList = ({requests, onSelect, onRemove, clearRequests, selectedRequest}) => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex flex-col h-full">
|
|
8
|
+
<div className="p-4 flex justify-between items-center">
|
|
9
|
+
<span className="font-bold text-xl">
|
|
10
|
+
Requests ({requests.length})
|
|
11
|
+
</span>
|
|
12
|
+
<button
|
|
13
|
+
className="px-3 py-1 bg-red-500 text-white rounded text-xs"
|
|
14
|
+
onClick={clearRequests}
|
|
15
|
+
>
|
|
16
|
+
Clear All
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
{/* Table Header */}
|
|
21
|
+
<div className="flex items-center bg-gray-200 p-2 border-b border-gray-300 text-sm font-bold">
|
|
22
|
+
<div className="w-16 text-center">Status</div>
|
|
23
|
+
<div className="w-20 text-center">Method</div>
|
|
24
|
+
<div className="flex-grow text-left">URL</div>
|
|
25
|
+
<div className="w-20 text-center">Duration</div>
|
|
26
|
+
<div className="w-10"></div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<ul className="overflow-y-auto flex-grow" style={{ maxHeight: "calc(100vh - 40px - 100px)" }}>
|
|
30
|
+
{requests.map((req, index) => {
|
|
31
|
+
const reqId = req.id || index;
|
|
32
|
+
return (
|
|
33
|
+
<RequestListItem
|
|
34
|
+
key={reqId}
|
|
35
|
+
reqId={reqId}
|
|
36
|
+
request={req}
|
|
37
|
+
onSelect={onSelect}
|
|
38
|
+
onRemove={onRemove}
|
|
39
|
+
selected={selectedRequest && selectedRequest.id === reqId}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default RequestList;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/components/RequestListItem.jsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {getStatusClass} from "../utils/getStatusClass.js";
|
|
4
|
+
|
|
5
|
+
const selectedClass = ["bg-blue-100", "border-l-4", "border-blue-700"].join(" ");
|
|
6
|
+
|
|
7
|
+
const RequestListItem = ({request, reqId, onSelect, onRemove, selected}) => {
|
|
8
|
+
const handleSelect = (request) => {
|
|
9
|
+
onSelect(request);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<li
|
|
14
|
+
className={`flex items-center cursor-pointer hover:bg-gray-200 ${selected ? selectedClass : ""}`}
|
|
15
|
+
onClick={() => {
|
|
16
|
+
handleSelect(request);
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<div className="flex items-center p-2 w-full">
|
|
20
|
+
<div className="w-16 flex justify-center">
|
|
21
|
+
<span
|
|
22
|
+
className={`inline-flex px-3 py-1 text-xs font-semibold rounded-full ${getStatusClass(
|
|
23
|
+
request.response.status
|
|
24
|
+
)}`}
|
|
25
|
+
>
|
|
26
|
+
{request.response.status || 'N/A'}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="w-20 text-center font-medium">
|
|
30
|
+
{request.request.method || 'GET'}
|
|
31
|
+
</div>
|
|
32
|
+
<div className="flex-grow truncate text-left">
|
|
33
|
+
{request.endpoint || request.url}
|
|
34
|
+
</div>
|
|
35
|
+
<div className="w-20 text-gray-500 text-center">
|
|
36
|
+
{request.latency}ms
|
|
37
|
+
</div>
|
|
38
|
+
<button
|
|
39
|
+
className="w-8 h-8 flex items-center justify-center text-red-500 hover:text-red-700"
|
|
40
|
+
onClick={(e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
onRemove(reqId);
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
✕
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</li>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default RequestListItem;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/components/ResponseContent.jsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import Editor from '@monaco-editor/react';
|
|
4
|
+
|
|
5
|
+
const ResponseContent = ({ request }) => {
|
|
6
|
+
const [showResponseHeaders, setShowResponseHeaders] = useState(false);
|
|
7
|
+
|
|
8
|
+
const renderTableRows = (data) =>
|
|
9
|
+
Object.entries(data || {}).map(([key, value]) => (
|
|
10
|
+
<tr key={key}>
|
|
11
|
+
<td className="border border-slate-200 px-2 py-1 font-mono text-slate-500 text-xs">
|
|
12
|
+
{key}
|
|
13
|
+
</td>
|
|
14
|
+
<td className="border border-slate-200 px-2 py-1 font-mono text-xs">{value}</td>
|
|
15
|
+
</tr>
|
|
16
|
+
));
|
|
17
|
+
|
|
18
|
+
// Check if the response body has content.
|
|
19
|
+
const payload = request.response.payload;
|
|
20
|
+
const isEmptyPayload =
|
|
21
|
+
!payload ||
|
|
22
|
+
(typeof payload === 'object' && Object.keys(payload).length === 0) ||
|
|
23
|
+
(typeof payload === 'string' &&
|
|
24
|
+
(payload.trim() === '' || payload.trim() === '{}'));
|
|
25
|
+
|
|
26
|
+
const formatPayload = (payload) => {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(payload);
|
|
29
|
+
return JSON.stringify(parsed, null, 2);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return payload;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div>
|
|
37
|
+
{/* Response Headers Section */}
|
|
38
|
+
<div className="mb-4">
|
|
39
|
+
<button
|
|
40
|
+
className="w-full p-2 text-left font-bold bg-gray-200"
|
|
41
|
+
onClick={() => setShowResponseHeaders(!showResponseHeaders)}
|
|
42
|
+
>
|
|
43
|
+
Headers ({Object.keys(request.response.headers || {}).length})
|
|
44
|
+
</button>
|
|
45
|
+
{showResponseHeaders && (
|
|
46
|
+
<div className="p-0">
|
|
47
|
+
<table className="w-full border-collapse border border-gray-300">
|
|
48
|
+
<thead>
|
|
49
|
+
<tr className="bg-gray-100">
|
|
50
|
+
<th className="border border-slate-200 px-2 py-1 w-1/4 text-left">Header</th>
|
|
51
|
+
<th className="border border-slate-200 px-2 py-1 text-left">Value</th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>{renderTableRows(request.response.headers)}</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Response Body Section */}
|
|
61
|
+
<div>
|
|
62
|
+
<button className="w-full p-2 text-left font-bold bg-gray-200">
|
|
63
|
+
Response Body
|
|
64
|
+
</button>
|
|
65
|
+
{isEmptyPayload ? (
|
|
66
|
+
<div className="hidden"></div>
|
|
67
|
+
) : (
|
|
68
|
+
<div className="bg-white rounded-b shadow p-0 h-100">
|
|
69
|
+
<Editor
|
|
70
|
+
height="100%"
|
|
71
|
+
defaultLanguage="json"
|
|
72
|
+
value={formatPayload(payload)}
|
|
73
|
+
options={{
|
|
74
|
+
readOnly: true,
|
|
75
|
+
minimap: { enabled: false },
|
|
76
|
+
automaticLayout: true,
|
|
77
|
+
fontFamily: '"Cascadia Code", "Jetbrains Mono", "Fira Code", "Menlo", "Consolas", monospace',
|
|
78
|
+
tabSize: 2,
|
|
79
|
+
scrollBeyondLastLine: false
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default ResponseContent;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import useSSE from '../hooks/useSSE';
|
|
3
|
+
|
|
4
|
+
const SSETest = () => {
|
|
5
|
+
const { data, error } = useSSE('http://localhost:4321/api/events');
|
|
6
|
+
|
|
7
|
+
console.log("test", "test")
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<h1>SSE Test Component</h1>
|
|
12
|
+
{error && <p style={{color: 'red'}}>Error: {error}</p>}
|
|
13
|
+
{data ? (
|
|
14
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
15
|
+
) : (
|
|
16
|
+
<p>No data received yet</p>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default SSETest;
|