@mimicai/explorer 0.7.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/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +190 -0
- package/dist/client/assets/index-6jl2Igpx.css +1 -0
- package/dist/client/assets/index-DrcjTHum.js +55 -0
- package/dist/client/index.html +14 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.js +210 -0
- package/index.html +13 -0
- package/package.json +45 -0
- package/postcss.config.js +6 -0
- package/server/index.ts +285 -0
- package/src/App.tsx +116 -0
- package/src/components/explorer/json-viewer.tsx +241 -0
- package/src/components/layout/header.tsx +57 -0
- package/src/components/layout/sidebar.tsx +132 -0
- package/src/index.css +69 -0
- package/src/lib/api.ts +57 -0
- package/src/lib/group-endpoints.ts +35 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/adapter-view.tsx +128 -0
- package/src/pages/dashboard.tsx +134 -0
- package/src/pages/data-view.tsx +202 -0
- package/src/pages/endpoint-tester.tsx +239 -0
- package/tailwind.config.ts +49 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +11 -0
- package/vite.config.ts +22 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import type { AdapterInfo } from '@/lib/api';
|
|
4
|
+
import { RawJsonViewer } from '@/components/explorer/json-viewer';
|
|
5
|
+
import { groupEndpoints } from '@/lib/group-endpoints';
|
|
6
|
+
|
|
7
|
+
interface EndpointTesterProps {
|
|
8
|
+
adapter: AdapterInfo;
|
|
9
|
+
initialEndpoint?: { method: string; path: string };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const METHOD_COLORS: Record<string, string> = {
|
|
13
|
+
GET: 'bg-blue-500/10 text-blue-500 border-blue-500/30',
|
|
14
|
+
POST: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/30',
|
|
15
|
+
PUT: 'bg-amber-500/10 text-amber-500 border-amber-500/30',
|
|
16
|
+
PATCH: 'bg-orange-500/10 text-orange-500 border-orange-500/30',
|
|
17
|
+
DELETE: 'bg-red-500/10 text-red-500 border-red-500/30',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function EndpointTester({ adapter, initialEndpoint }: EndpointTesterProps) {
|
|
21
|
+
const [selectedEndpoint, setSelectedEndpoint] = useState(
|
|
22
|
+
initialEndpoint ?? adapter.endpoints[0] ?? null,
|
|
23
|
+
);
|
|
24
|
+
const [requestBody, setRequestBody] = useState('{}');
|
|
25
|
+
const [response, setResponse] = useState<{
|
|
26
|
+
status: number;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
body: unknown;
|
|
29
|
+
duration: number;
|
|
30
|
+
} | null>(null);
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState<Record<string, boolean>>({});
|
|
34
|
+
const groups = useMemo(() => groupEndpoints(adapter.endpoints, adapter.basePath), [adapter]);
|
|
35
|
+
|
|
36
|
+
// Find which group the initial/selected endpoint belongs to, keep that expanded
|
|
37
|
+
const initialGroup = useMemo(() => {
|
|
38
|
+
if (!selectedEndpoint) return null;
|
|
39
|
+
return groups.find((g) =>
|
|
40
|
+
g.endpoints.some((ep) => ep.method === selectedEndpoint.method && ep.path === selectedEndpoint.path),
|
|
41
|
+
)?.label ?? null;
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
// Start with all groups collapsed except the one containing the selected endpoint
|
|
45
|
+
const [sidebarInited] = useState(() => {
|
|
46
|
+
const init: Record<string, boolean> = {};
|
|
47
|
+
for (const g of groups) {
|
|
48
|
+
init[g.label] = g.label !== initialGroup;
|
|
49
|
+
}
|
|
50
|
+
return init;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const getCollapsed = (label: string) => sidebarCollapsed[label] ?? sidebarInited[label] ?? true;
|
|
54
|
+
const toggleSidebar = (label: string) =>
|
|
55
|
+
setSidebarCollapsed((prev) => ({ ...prev, [label]: !getCollapsed(label) }));
|
|
56
|
+
|
|
57
|
+
const handleSend = async () => {
|
|
58
|
+
if (!selectedEndpoint || !adapter.port) return;
|
|
59
|
+
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
setResponse(null);
|
|
63
|
+
|
|
64
|
+
const start = performance.now();
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(`http://localhost:${adapter.port}${selectedEndpoint.path}`, {
|
|
67
|
+
method: selectedEndpoint.method,
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
...(selectedEndpoint.method !== 'GET' && requestBody.trim()
|
|
70
|
+
? { body: requestBody }
|
|
71
|
+
: {}),
|
|
72
|
+
});
|
|
73
|
+
const duration = Math.round(performance.now() - start);
|
|
74
|
+
const body = await res.json().catch(() => null);
|
|
75
|
+
setResponse({
|
|
76
|
+
status: res.status,
|
|
77
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
78
|
+
body,
|
|
79
|
+
duration,
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
setError(
|
|
83
|
+
err instanceof Error ? err.message : 'Request failed. Is mimic host running?',
|
|
84
|
+
);
|
|
85
|
+
} finally {
|
|
86
|
+
setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-6">
|
|
92
|
+
<div>
|
|
93
|
+
<h1 className="text-2xl font-bold">{adapter.name} — Endpoint Tester</h1>
|
|
94
|
+
<p className="mt-1 text-muted-foreground">
|
|
95
|
+
Send requests to the running mock server on port {adapter.port ?? 'N/A'}
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="flex gap-6">
|
|
100
|
+
{/* Endpoint list (grouped) */}
|
|
101
|
+
<div className="w-72 shrink-0 space-y-0.5 overflow-y-auto max-h-[calc(100vh-12rem)]">
|
|
102
|
+
{groups.map(({ label, endpoints }) => {
|
|
103
|
+
const isCollapsed = getCollapsed(label);
|
|
104
|
+
const hasSelected = endpoints.some(
|
|
105
|
+
(ep) => ep.method === selectedEndpoint?.method && ep.path === selectedEndpoint?.path,
|
|
106
|
+
);
|
|
107
|
+
return (
|
|
108
|
+
<div key={label}>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => toggleSidebar(label)}
|
|
111
|
+
className={cn(
|
|
112
|
+
'flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-xs transition-colors hover:bg-accent',
|
|
113
|
+
hasSelected && isCollapsed && 'text-accent-foreground',
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
<span className={cn('text-[10px] text-muted-foreground transition-transform', !isCollapsed && 'rotate-90')}>
|
|
117
|
+
▶
|
|
118
|
+
</span>
|
|
119
|
+
<span className="font-medium truncate">{label}</span>
|
|
120
|
+
<span className="ml-auto text-[10px] text-muted-foreground">{endpoints.length}</span>
|
|
121
|
+
</button>
|
|
122
|
+
{!isCollapsed &&
|
|
123
|
+
endpoints.map((ep, i) => (
|
|
124
|
+
<button
|
|
125
|
+
key={i}
|
|
126
|
+
onClick={() => {
|
|
127
|
+
setSelectedEndpoint(ep);
|
|
128
|
+
setResponse(null);
|
|
129
|
+
setError(null);
|
|
130
|
+
}}
|
|
131
|
+
className={cn(
|
|
132
|
+
'flex w-full items-center gap-2 rounded-md px-3 pl-6 py-1.5 text-sm transition-colors',
|
|
133
|
+
'hover:bg-accent',
|
|
134
|
+
selectedEndpoint?.path === ep.path && selectedEndpoint?.method === ep.method
|
|
135
|
+
? 'bg-accent text-accent-foreground'
|
|
136
|
+
: 'text-muted-foreground',
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<span
|
|
140
|
+
className={cn(
|
|
141
|
+
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold',
|
|
142
|
+
METHOD_COLORS[ep.method] ?? 'bg-muted',
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
{ep.method}
|
|
146
|
+
</span>
|
|
147
|
+
<span className="truncate font-mono text-xs">{ep.path}</span>
|
|
148
|
+
</button>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Request / Response */}
|
|
156
|
+
<div className="flex-1 space-y-4">
|
|
157
|
+
{selectedEndpoint && (
|
|
158
|
+
<>
|
|
159
|
+
{/* Request bar */}
|
|
160
|
+
<div className="flex items-center gap-3">
|
|
161
|
+
<span
|
|
162
|
+
className={cn(
|
|
163
|
+
'shrink-0 rounded border px-3 py-1.5 text-sm font-bold',
|
|
164
|
+
METHOD_COLORS[selectedEndpoint.method] ?? 'bg-muted',
|
|
165
|
+
)}
|
|
166
|
+
>
|
|
167
|
+
{selectedEndpoint.method}
|
|
168
|
+
</span>
|
|
169
|
+
<div className="flex-1 rounded-md border bg-muted/50 px-3 py-1.5 font-mono text-sm">
|
|
170
|
+
http://localhost:{adapter.port}{selectedEndpoint.path}
|
|
171
|
+
</div>
|
|
172
|
+
<button
|
|
173
|
+
onClick={handleSend}
|
|
174
|
+
disabled={loading || !adapter.port}
|
|
175
|
+
className={cn(
|
|
176
|
+
'rounded-md bg-primary px-6 py-1.5 text-sm font-medium text-primary-foreground',
|
|
177
|
+
'hover:bg-primary/90 transition-colors',
|
|
178
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
{loading ? 'Sending...' : 'Send'}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Request body (for POST/PUT/PATCH) */}
|
|
186
|
+
{selectedEndpoint.method !== 'GET' &&
|
|
187
|
+
selectedEndpoint.method !== 'DELETE' && (
|
|
188
|
+
<div>
|
|
189
|
+
<div className="mb-2 text-xs font-medium text-muted-foreground">
|
|
190
|
+
Request Body
|
|
191
|
+
</div>
|
|
192
|
+
<textarea
|
|
193
|
+
value={requestBody}
|
|
194
|
+
onChange={(e) => setRequestBody(e.target.value)}
|
|
195
|
+
className="w-full rounded-md border bg-muted/30 p-3 font-mono text-sm resize-y min-h-[100px] focus:outline-none focus:ring-1 focus:ring-ring"
|
|
196
|
+
placeholder="{}"
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Error */}
|
|
202
|
+
{error && (
|
|
203
|
+
<div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-500">
|
|
204
|
+
{error}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Response */}
|
|
209
|
+
{response && (
|
|
210
|
+
<div className="space-y-3">
|
|
211
|
+
<div className="flex items-center gap-4">
|
|
212
|
+
<span
|
|
213
|
+
className={cn(
|
|
214
|
+
'rounded px-2 py-0.5 text-sm font-bold',
|
|
215
|
+
response.status < 300
|
|
216
|
+
? 'bg-emerald-500/10 text-emerald-500'
|
|
217
|
+
: response.status < 500
|
|
218
|
+
? 'bg-amber-500/10 text-amber-500'
|
|
219
|
+
: 'bg-red-500/10 text-red-500',
|
|
220
|
+
)}
|
|
221
|
+
>
|
|
222
|
+
{response.status}
|
|
223
|
+
</span>
|
|
224
|
+
<span className="text-xs text-muted-foreground">
|
|
225
|
+
{response.duration}ms
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="rounded-lg border bg-card overflow-auto max-h-[600px] p-4">
|
|
229
|
+
<RawJsonViewer data={response.body} />
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
darkMode: 'class',
|
|
5
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
colors: {
|
|
9
|
+
border: 'hsl(var(--border))',
|
|
10
|
+
input: 'hsl(var(--input))',
|
|
11
|
+
ring: 'hsl(var(--ring))',
|
|
12
|
+
background: 'hsl(var(--background))',
|
|
13
|
+
foreground: 'hsl(var(--foreground))',
|
|
14
|
+
primary: {
|
|
15
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
16
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
17
|
+
},
|
|
18
|
+
secondary: {
|
|
19
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
20
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
21
|
+
},
|
|
22
|
+
muted: {
|
|
23
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
24
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
25
|
+
},
|
|
26
|
+
accent: {
|
|
27
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
28
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
29
|
+
},
|
|
30
|
+
destructive: {
|
|
31
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
32
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
33
|
+
},
|
|
34
|
+
card: {
|
|
35
|
+
DEFAULT: 'hsl(var(--card))',
|
|
36
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
borderRadius: {
|
|
40
|
+
lg: 'var(--radius)',
|
|
41
|
+
md: 'calc(var(--radius) - 2px)',
|
|
42
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
plugins: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default config;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"outDir": "dist",
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"paths": {
|
|
19
|
+
"@/*": ["./src/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["src", "server"]
|
|
23
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['server/index.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
outDir: 'dist/server',
|
|
7
|
+
dts: true,
|
|
8
|
+
clean: false,
|
|
9
|
+
target: 'node22',
|
|
10
|
+
external: ['@mimicai/core', 'fastify', '@fastify/static', '@fastify/cors'],
|
|
11
|
+
});
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
root: '.',
|
|
8
|
+
build: {
|
|
9
|
+
outDir: 'dist/client',
|
|
10
|
+
emptyDirOnly: true,
|
|
11
|
+
},
|
|
12
|
+
resolve: {
|
|
13
|
+
alias: {
|
|
14
|
+
'@': resolve(__dirname, './src'),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
server: {
|
|
18
|
+
proxy: {
|
|
19
|
+
'/_api': 'http://localhost:7879',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|