@theihtisham/devtools-with-cloud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function AiPage() {
|
|
6
|
+
const [activeFeature, setActiveFeature] = useState<'generate' | 'discover' | 'explain' | 'suggest'>('generate');
|
|
7
|
+
const [input, setInput] = useState('');
|
|
8
|
+
const [output, setOutput] = useState('');
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
|
|
11
|
+
const handleSubmit = async () => {
|
|
12
|
+
if (!input.trim()) return;
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setOutput('');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
let url = '';
|
|
18
|
+
let body: Record<string, unknown> = {};
|
|
19
|
+
|
|
20
|
+
switch (activeFeature) {
|
|
21
|
+
case 'generate':
|
|
22
|
+
url = '/api/ai/generate-tests';
|
|
23
|
+
body = { requestId: 'manual', response: input };
|
|
24
|
+
break;
|
|
25
|
+
case 'discover':
|
|
26
|
+
url = '/api/ai/discover';
|
|
27
|
+
body = { code: input, framework: 'auto', workspaceId: 'default' };
|
|
28
|
+
break;
|
|
29
|
+
case 'explain':
|
|
30
|
+
url = '/api/ai/explain';
|
|
31
|
+
body = { response: input };
|
|
32
|
+
break;
|
|
33
|
+
case 'suggest':
|
|
34
|
+
url = '/api/ai/suggest';
|
|
35
|
+
body = { request: input };
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
if (data.success) {
|
|
47
|
+
setOutput(typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2));
|
|
48
|
+
} else {
|
|
49
|
+
setOutput(`Error: ${data.error?.message ?? 'Unknown error'}`);
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
setOutput(`Error: ${err instanceof Error ? err.message : 'Request failed'}`);
|
|
53
|
+
} finally {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const features = [
|
|
59
|
+
{ key: 'generate' as const, label: 'Generate Tests', description: 'Generate test assertions from API responses' },
|
|
60
|
+
{ key: 'discover' as const, label: 'Discover Endpoints', description: 'Auto-discover API endpoints from code' },
|
|
61
|
+
{ key: 'explain' as const, label: 'Explain Response', description: 'Get AI explanations of API responses' },
|
|
62
|
+
{ key: 'suggest' as const, label: 'Suggest Improvements', description: 'Get suggestions for API improvements' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="p-6">
|
|
67
|
+
<h1 className="text-2xl font-bold text-foreground mb-6">AI Assistant</h1>
|
|
68
|
+
|
|
69
|
+
{/* Feature Selection */}
|
|
70
|
+
<div className="grid grid-cols-4 gap-3 mb-6">
|
|
71
|
+
{features.map((feature) => (
|
|
72
|
+
<button
|
|
73
|
+
key={feature.key}
|
|
74
|
+
onClick={() => { setActiveFeature(feature.key); setInput(''); setOutput(''); }}
|
|
75
|
+
className={`p-3 rounded-lg border text-left transition-colors ${
|
|
76
|
+
activeFeature === feature.key
|
|
77
|
+
? 'border-primary bg-primary/10'
|
|
78
|
+
: 'border-border hover:border-primary/50'
|
|
79
|
+
}`}
|
|
80
|
+
>
|
|
81
|
+
<div className="font-medium text-foreground text-sm">{feature.label}</div>
|
|
82
|
+
<div className="text-xs text-muted-foreground mt-1">{feature.description}</div>
|
|
83
|
+
</button>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Input */}
|
|
88
|
+
<div className="mb-4">
|
|
89
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
90
|
+
{activeFeature === 'generate' && 'Paste a JSON response'}
|
|
91
|
+
{activeFeature === 'discover' && 'Paste source code to scan'}
|
|
92
|
+
{activeFeature === 'explain' && 'Paste a response to explain'}
|
|
93
|
+
{activeFeature === 'suggest' && 'Paste a request to analyze'}
|
|
94
|
+
</label>
|
|
95
|
+
<textarea
|
|
96
|
+
value={input}
|
|
97
|
+
onChange={(e) => setInput(e.target.value)}
|
|
98
|
+
className="w-full h-40 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-y"
|
|
99
|
+
placeholder={activeFeature === 'discover' ? '// Paste Express/FastAPI/Next.js code here...' : '{ "key": "value" }'}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<button
|
|
104
|
+
onClick={handleSubmit}
|
|
105
|
+
disabled={loading || !input.trim()}
|
|
106
|
+
className="px-6 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
|
107
|
+
>
|
|
108
|
+
{loading ? 'Processing...' : 'Run'}
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
{/* Output */}
|
|
112
|
+
{output && (
|
|
113
|
+
<div className="mt-6">
|
|
114
|
+
<label className="block text-sm font-medium text-foreground mb-2">Result</label>
|
|
115
|
+
<pre className="p-4 rounded-lg border border-border bg-muted/50 text-foreground font-mono text-sm whitespace-pre-wrap overflow-auto max-h-96">
|
|
116
|
+
{output}
|
|
117
|
+
</pre>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
interface Collection {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
parentId?: string;
|
|
11
|
+
sortOrder: number;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function CollectionsPage() {
|
|
16
|
+
const [collections, setCollections] = useState<Collection[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
19
|
+
const [newName, setNewName] = useState('');
|
|
20
|
+
const [newDescription, setNewDescription] = useState('');
|
|
21
|
+
|
|
22
|
+
const fetchCollections = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch('/api/collections');
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
if (data.success) {
|
|
27
|
+
setCollections(data.data);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Collections may not be available without DB
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchCollections();
|
|
38
|
+
}, [fetchCollections]);
|
|
39
|
+
|
|
40
|
+
const createCollection = async () => {
|
|
41
|
+
if (!newName.trim()) return;
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch('/api/collections', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
name: newName,
|
|
48
|
+
description: newDescription || undefined,
|
|
49
|
+
workspaceId: 'default',
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
if (data.success) {
|
|
54
|
+
setCollections((prev) => [...prev, data.data]);
|
|
55
|
+
setShowCreate(false);
|
|
56
|
+
setNewName('');
|
|
57
|
+
setNewDescription('');
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Handle error
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const deleteCollection = async (id: string) => {
|
|
65
|
+
try {
|
|
66
|
+
await fetch(`/api/collections/${id}`, { method: 'DELETE' });
|
|
67
|
+
setCollections((prev) => prev.filter((c) => c.id !== id));
|
|
68
|
+
} catch {
|
|
69
|
+
// Handle error
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="p-6">
|
|
75
|
+
<div className="flex items-center justify-between mb-6">
|
|
76
|
+
<h1 className="text-2xl font-bold text-foreground">Collections</h1>
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => setShowCreate(true)}
|
|
79
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors"
|
|
80
|
+
>
|
|
81
|
+
New Collection
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{showCreate && (
|
|
86
|
+
<div className="border border-border rounded-lg p-4 mb-6">
|
|
87
|
+
<h2 className="text-lg font-semibold text-foreground mb-3">Create Collection</h2>
|
|
88
|
+
<div className="space-y-3">
|
|
89
|
+
<input
|
|
90
|
+
type="text"
|
|
91
|
+
value={newName}
|
|
92
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
93
|
+
placeholder="Collection name"
|
|
94
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm"
|
|
95
|
+
/>
|
|
96
|
+
<textarea
|
|
97
|
+
value={newDescription}
|
|
98
|
+
onChange={(e) => setNewDescription(e.target.value)}
|
|
99
|
+
placeholder="Description (optional)"
|
|
100
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm"
|
|
101
|
+
rows={2}
|
|
102
|
+
/>
|
|
103
|
+
<div className="flex gap-2">
|
|
104
|
+
<button
|
|
105
|
+
onClick={createCollection}
|
|
106
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm"
|
|
107
|
+
>
|
|
108
|
+
Create
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
onClick={() => { setShowCreate(false); setNewName(''); setNewDescription(''); }}
|
|
112
|
+
className="px-4 py-2 border border-border rounded-md text-sm text-foreground"
|
|
113
|
+
>
|
|
114
|
+
Cancel
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{loading ? (
|
|
122
|
+
<div className="text-muted-foreground text-center py-8">Loading collections...</div>
|
|
123
|
+
) : collections.length === 0 ? (
|
|
124
|
+
<div className="text-center py-12">
|
|
125
|
+
<p className="text-muted-foreground mb-2">No collections yet</p>
|
|
126
|
+
<p className="text-sm text-muted-foreground">Create a collection to organize your API requests</p>
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div className="space-y-3">
|
|
130
|
+
{collections.map((collection) => (
|
|
131
|
+
<div
|
|
132
|
+
key={collection.id}
|
|
133
|
+
className="border border-border rounded-lg p-4 hover:border-primary/50 transition-colors"
|
|
134
|
+
>
|
|
135
|
+
<div className="flex items-center justify-between">
|
|
136
|
+
<div>
|
|
137
|
+
<h3 className="font-medium text-foreground">{collection.name}</h3>
|
|
138
|
+
{collection.description && (
|
|
139
|
+
<p className="text-sm text-muted-foreground mt-1">{collection.description}</p>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => deleteCollection(collection.id)}
|
|
144
|
+
className="text-sm text-destructive hover:text-destructive/80 transition-colors"
|
|
145
|
+
>
|
|
146
|
+
Delete
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
interface Environment {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
workspaceId: string;
|
|
9
|
+
isDefault: boolean;
|
|
10
|
+
variables: Array<{
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
type: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function EnvironmentsPage() {
|
|
19
|
+
const [environments, setEnvironments] = useState<Environment[]>([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
|
|
22
|
+
const fetchEnvironments = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch('/api/environments');
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
if (data.success) {
|
|
27
|
+
setEnvironments(data.data);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Environments may not be available without DB
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchEnvironments();
|
|
38
|
+
}, [fetchEnvironments]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="p-6">
|
|
42
|
+
<div className="flex items-center justify-between mb-6">
|
|
43
|
+
<h1 className="text-2xl font-bold text-foreground">Environments</h1>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{loading ? (
|
|
47
|
+
<div className="text-muted-foreground text-center py-8">Loading environments...</div>
|
|
48
|
+
) : environments.length === 0 ? (
|
|
49
|
+
<div className="text-center py-12">
|
|
50
|
+
<p className="text-muted-foreground mb-2">No environments configured</p>
|
|
51
|
+
<p className="text-sm text-muted-foreground">
|
|
52
|
+
Create environments to manage variables for different stages (dev, staging, prod)
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
{environments.map((env) => (
|
|
58
|
+
<div
|
|
59
|
+
key={env.id}
|
|
60
|
+
className="border border-border rounded-lg p-4"
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center gap-2 mb-3">
|
|
63
|
+
<h3 className="font-medium text-foreground">{env.name}</h3>
|
|
64
|
+
{env.isDefault && (
|
|
65
|
+
<span className="px-2 py-0.5 bg-primary/10 text-primary text-xs rounded-full">
|
|
66
|
+
Default
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
{env.variables.length > 0 ? (
|
|
71
|
+
<div className="space-y-1">
|
|
72
|
+
{env.variables
|
|
73
|
+
.filter((v) => v.enabled)
|
|
74
|
+
.map((variable, i) => (
|
|
75
|
+
<div key={i} className="flex items-center gap-2 text-sm font-mono">
|
|
76
|
+
<span className="text-primary">{variable.key}</span>
|
|
77
|
+
<span className="text-muted-foreground">=</span>
|
|
78
|
+
<span className="text-foreground">
|
|
79
|
+
{variable.type === 'secret' ? '********' : variable.value}
|
|
80
|
+
</span>
|
|
81
|
+
{variable.type !== 'string' && (
|
|
82
|
+
<span className="text-xs text-muted-foreground">({variable.type})</span>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<p className="text-sm text-muted-foreground">No variables defined</p>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils/cn';
|
|
5
|
+
|
|
6
|
+
interface HistoryEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
method: string;
|
|
9
|
+
url: string;
|
|
10
|
+
status?: number;
|
|
11
|
+
statusText?: string;
|
|
12
|
+
duration?: number;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function HistoryPage() {
|
|
17
|
+
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
|
|
20
|
+
const fetchHistory = useCallback(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/requests');
|
|
23
|
+
const data = await res.json();
|
|
24
|
+
if (data.success && Array.isArray(data.data)) {
|
|
25
|
+
// Flatten all requests' history
|
|
26
|
+
const allHistory: HistoryEntry[] = [];
|
|
27
|
+
for (const req of data.data) {
|
|
28
|
+
const histRes = await fetch(`/api/requests/${req.id}/history`);
|
|
29
|
+
const histData = await histRes.json();
|
|
30
|
+
if (histData.success && Array.isArray(histData.data)) {
|
|
31
|
+
allHistory.push(...histData.data);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
allHistory.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
35
|
+
setHistory(allHistory);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// History may not be available without DB
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
fetchHistory();
|
|
46
|
+
}, [fetchHistory]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="p-6">
|
|
50
|
+
<div className="flex items-center justify-between mb-6">
|
|
51
|
+
<h1 className="text-2xl font-bold text-foreground">Request History</h1>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{loading ? (
|
|
55
|
+
<div className="text-muted-foreground text-center py-8">Loading history...</div>
|
|
56
|
+
) : history.length === 0 ? (
|
|
57
|
+
<div className="text-center py-12">
|
|
58
|
+
<p className="text-muted-foreground mb-2">No request history</p>
|
|
59
|
+
<p className="text-sm text-muted-foreground">
|
|
60
|
+
Send requests from the workspace to see them here
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
{history.map((entry) => (
|
|
66
|
+
<div
|
|
67
|
+
key={entry.id}
|
|
68
|
+
className="flex items-center gap-4 p-3 border border-border rounded-lg hover:bg-accent/50 transition-colors"
|
|
69
|
+
>
|
|
70
|
+
<span className={cn(
|
|
71
|
+
'px-2 py-0.5 rounded text-xs font-mono font-medium',
|
|
72
|
+
entry.method === 'GET' && 'text-method-get',
|
|
73
|
+
entry.method === 'POST' && 'text-method-post',
|
|
74
|
+
entry.method === 'PUT' && 'text-method-put',
|
|
75
|
+
entry.method === 'DELETE' && 'text-method-delete',
|
|
76
|
+
entry.method === 'PATCH' && 'text-method-patch',
|
|
77
|
+
)}>
|
|
78
|
+
{entry.method}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="text-sm text-foreground font-mono flex-1 truncate">
|
|
81
|
+
{entry.url}
|
|
82
|
+
</span>
|
|
83
|
+
{entry.status && (
|
|
84
|
+
<span className={cn(
|
|
85
|
+
'text-sm font-mono',
|
|
86
|
+
entry.status < 300 && 'text-green-400',
|
|
87
|
+
entry.status >= 400 && 'text-red-400',
|
|
88
|
+
entry.status >= 300 && entry.status < 400 && 'text-blue-400',
|
|
89
|
+
)}>
|
|
90
|
+
{entry.status}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{entry.duration != null && (
|
|
94
|
+
<span className="text-sm text-muted-foreground">
|
|
95
|
+
{entry.duration}ms
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
<span className="text-xs text-muted-foreground">
|
|
99
|
+
{new Date(entry.createdAt).toLocaleString()}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
type ImportFormat = 'openapi' | 'postman' | 'curl' | 'har';
|
|
6
|
+
|
|
7
|
+
export default function ImportPage() {
|
|
8
|
+
const [format, setFormat] = useState<ImportFormat>('openapi');
|
|
9
|
+
const [input, setInput] = useState('');
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [result, setResult] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
const handleImport = async () => {
|
|
14
|
+
if (!input.trim()) return;
|
|
15
|
+
setLoading(true);
|
|
16
|
+
setResult(null);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`/api/import/${format}`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(
|
|
23
|
+
format === 'openapi'
|
|
24
|
+
? { spec: input, workspaceId: 'default' }
|
|
25
|
+
: format === 'postman'
|
|
26
|
+
? { collection: input, workspaceId: 'default' }
|
|
27
|
+
: format === 'curl'
|
|
28
|
+
? { command: input, collectionId: 'default' }
|
|
29
|
+
: { har: input, collectionId: 'default' },
|
|
30
|
+
),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
if (data.success) {
|
|
35
|
+
setResult(`Successfully imported: ${JSON.stringify(data.data, null, 2).slice(0, 500)}`);
|
|
36
|
+
} else {
|
|
37
|
+
setResult(`Error: ${data.error?.message ?? 'Import failed'}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setResult(`Error: ${err instanceof Error ? err.message : 'Import failed'}`);
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const formats: { key: ImportFormat; label: string; placeholder: string }[] = [
|
|
47
|
+
{ key: 'openapi', label: 'OpenAPI / Swagger', placeholder: 'Paste your OpenAPI 3.x JSON or YAML spec...' },
|
|
48
|
+
{ key: 'postman', label: 'Postman Collection', placeholder: 'Paste your Postman Collection v2.1 JSON...' },
|
|
49
|
+
{ key: 'curl', label: 'cURL Command', placeholder: 'curl -X GET https://api.example.com/users -H "Accept: application/json"' },
|
|
50
|
+
{ key: 'har', label: 'HAR File', placeholder: 'Paste your HAR (HTTP Archive) JSON...' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="p-6 max-w-4xl">
|
|
55
|
+
<h1 className="text-2xl font-bold text-foreground mb-6">Import</h1>
|
|
56
|
+
|
|
57
|
+
{/* Format Selection */}
|
|
58
|
+
<div className="flex gap-2 mb-6">
|
|
59
|
+
{formats.map((f) => (
|
|
60
|
+
<button
|
|
61
|
+
key={f.key}
|
|
62
|
+
onClick={() => { setFormat(f.key); setInput(''); setResult(null); }}
|
|
63
|
+
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
64
|
+
format === f.key
|
|
65
|
+
? 'bg-primary text-primary-foreground'
|
|
66
|
+
: 'border border-border text-foreground hover:bg-accent'
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
{f.label}
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Input */}
|
|
75
|
+
<div className="mb-4">
|
|
76
|
+
<textarea
|
|
77
|
+
value={input}
|
|
78
|
+
onChange={(e) => setInput(e.target.value)}
|
|
79
|
+
className="w-full h-64 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-y"
|
|
80
|
+
placeholder={formats.find((f) => f.key === format)?.placeholder}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<button
|
|
85
|
+
onClick={handleImport}
|
|
86
|
+
disabled={loading || !input.trim()}
|
|
87
|
+
className="px-6 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
|
88
|
+
>
|
|
89
|
+
{loading ? 'Importing...' : 'Import'}
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
{/* Result */}
|
|
93
|
+
{result && (
|
|
94
|
+
<div className="mt-6">
|
|
95
|
+
<pre className="p-4 rounded-lg border border-border bg-muted/50 text-foreground font-mono text-sm whitespace-pre-wrap overflow-auto max-h-64">
|
|
96
|
+
{result}
|
|
97
|
+
</pre>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { cn } from '@/lib/utils/cn';
|
|
6
|
+
|
|
7
|
+
const navItems = [
|
|
8
|
+
{ href: '/workspace', label: 'Workspace', icon: '{ }' },
|
|
9
|
+
{ href: '/collections', label: 'Collections', icon: '#' },
|
|
10
|
+
{ href: '/environments', label: 'Environments', icon: '$' },
|
|
11
|
+
{ href: '/history', label: 'History', icon: '<>' },
|
|
12
|
+
{ href: '/ai', label: 'AI Assistant', icon: '*' },
|
|
13
|
+
{ href: '/import', label: 'Import', icon: '+' },
|
|
14
|
+
{ href: '/settings', label: 'Settings', icon: '@' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|
18
|
+
const pathname = usePathname();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex h-screen bg-background">
|
|
22
|
+
{/* Sidebar */}
|
|
23
|
+
<aside className="w-56 border-r border-border flex flex-col shrink-0">
|
|
24
|
+
<div className="p-4 border-b border-border">
|
|
25
|
+
<Link href="/" className="flex items-center gap-2">
|
|
26
|
+
<div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
|
|
27
|
+
<span className="text-primary-foreground font-bold text-xs">AT</span>
|
|
28
|
+
</div>
|
|
29
|
+
<span className="font-bold text-foreground">APITester</span>
|
|
30
|
+
</Link>
|
|
31
|
+
</div>
|
|
32
|
+
<nav className="flex-1 p-2">
|
|
33
|
+
{navItems.map((item) => (
|
|
34
|
+
<Link
|
|
35
|
+
key={item.href}
|
|
36
|
+
href={item.href}
|
|
37
|
+
className={cn(
|
|
38
|
+
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors mb-1',
|
|
39
|
+
pathname === item.href || pathname?.startsWith(item.href + '/')
|
|
40
|
+
? 'bg-primary/10 text-primary'
|
|
41
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent',
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<span className="font-mono text-xs w-5 text-center">{item.icon}</span>
|
|
45
|
+
{item.label}
|
|
46
|
+
</Link>
|
|
47
|
+
))}
|
|
48
|
+
</nav>
|
|
49
|
+
<div className="p-4 border-t border-border text-xs text-muted-foreground">
|
|
50
|
+
v1.0.0
|
|
51
|
+
</div>
|
|
52
|
+
</aside>
|
|
53
|
+
|
|
54
|
+
{/* Main Content */}
|
|
55
|
+
<main className="flex-1 overflow-auto">
|
|
56
|
+
{children}
|
|
57
|
+
</main>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|