@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/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;