@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,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils/cn';
|
|
5
|
+
|
|
6
|
+
interface Toast {
|
|
7
|
+
id: string;
|
|
8
|
+
message: string;
|
|
9
|
+
type: 'success' | 'error' | 'info';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let addToastFn: ((message: string, type?: 'success' | 'error' | 'info') => void) | null = null;
|
|
13
|
+
|
|
14
|
+
export function toast(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
|
15
|
+
addToastFn?.(message, type);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
19
|
+
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
20
|
+
|
|
21
|
+
const addToast = useCallback((message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
|
22
|
+
const id = Math.random().toString(36).slice(2);
|
|
23
|
+
setToasts((prev) => [...prev, { id, message, type }]);
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
26
|
+
}, 3000);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
addToastFn = addToast;
|
|
31
|
+
return () => { addToastFn = null; };
|
|
32
|
+
}, [addToast]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
{children}
|
|
37
|
+
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
|
38
|
+
{toasts.map((t) => (
|
|
39
|
+
<div
|
|
40
|
+
key={t.id}
|
|
41
|
+
className={cn(
|
|
42
|
+
'px-4 py-2 rounded-lg text-sm shadow-lg animate-slide-in',
|
|
43
|
+
t.type === 'success' && 'bg-green-500/90 text-white',
|
|
44
|
+
t.type === 'error' && 'bg-destructive/90 text-destructive-foreground',
|
|
45
|
+
t.type === 'info' && 'bg-primary/90 text-primary-foreground',
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{t.message}
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils/cn';
|
|
5
|
+
|
|
6
|
+
interface RequestPanelProps {
|
|
7
|
+
activeTab: string;
|
|
8
|
+
onTabChange: (tab: string) => void;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const REQUEST_TABS = ['params', 'headers', 'body', 'auth', 'scripts'];
|
|
13
|
+
|
|
14
|
+
export function RequestPanel({ activeTab, onTabChange, children }: RequestPanelProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-col h-full">
|
|
17
|
+
<div className="flex border-b border-border">
|
|
18
|
+
{REQUEST_TABS.map((tab) => (
|
|
19
|
+
<button
|
|
20
|
+
key={tab}
|
|
21
|
+
onClick={() => onTabChange(tab)}
|
|
22
|
+
className={cn(
|
|
23
|
+
'px-4 py-2 text-sm capitalize transition-colors',
|
|
24
|
+
activeTab === tab
|
|
25
|
+
? 'text-primary border-b-2 border-primary'
|
|
26
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{tab}
|
|
30
|
+
</button>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
<div className="flex-1 overflow-auto p-4">
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils/cn';
|
|
4
|
+
|
|
5
|
+
interface ResponsePanelProps {
|
|
6
|
+
status: number | null;
|
|
7
|
+
statusText: string | null;
|
|
8
|
+
duration: number | null;
|
|
9
|
+
size: number | null;
|
|
10
|
+
body: string | null;
|
|
11
|
+
error?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ResponsePanel({ status, statusText, duration, size, body, error }: ResponsePanelProps) {
|
|
15
|
+
if (error) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex-1 flex items-center justify-center p-4">
|
|
18
|
+
<div className="text-destructive text-sm">{error}</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (status === null) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
|
26
|
+
<div className="text-center">
|
|
27
|
+
<p className="text-lg mb-2">No response yet</p>
|
|
28
|
+
<p className="text-sm">Enter a URL and click Send</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-col h-full">
|
|
36
|
+
{/* Status bar */}
|
|
37
|
+
<div className="flex items-center gap-4 p-3 border-b border-border">
|
|
38
|
+
<span className={cn(
|
|
39
|
+
'px-2 py-1 rounded text-sm font-mono font-medium',
|
|
40
|
+
status < 300 && 'bg-green-500/10 text-green-400',
|
|
41
|
+
status >= 300 && status < 400 && 'bg-blue-500/10 text-blue-400',
|
|
42
|
+
status >= 400 && status < 500 && 'bg-orange-500/10 text-orange-400',
|
|
43
|
+
status >= 500 && 'bg-red-500/10 text-red-400',
|
|
44
|
+
)}>
|
|
45
|
+
{status} {statusText}
|
|
46
|
+
</span>
|
|
47
|
+
{duration != null && (
|
|
48
|
+
<span className="text-sm text-muted-foreground">{duration}ms</span>
|
|
49
|
+
)}
|
|
50
|
+
{size != null && (
|
|
51
|
+
<span className="text-sm text-muted-foreground">{formatBytes(size)}</span>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Body */}
|
|
56
|
+
<div className="flex-1 overflow-auto p-4">
|
|
57
|
+
{body && (
|
|
58
|
+
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap break-all">
|
|
59
|
+
{tryFormatJson(body)}
|
|
60
|
+
</pre>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatBytes(bytes: number): string {
|
|
68
|
+
if (bytes === 0) return '0 B';
|
|
69
|
+
const k = 1024;
|
|
70
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
71
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
72
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i] ?? 'B'}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function tryFormatJson(body: string): string {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(JSON.parse(body), null, 2);
|
|
78
|
+
} catch {
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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' },
|
|
9
|
+
{ href: '/collections', label: 'Collections' },
|
|
10
|
+
{ href: '/environments', label: 'Environments' },
|
|
11
|
+
{ href: '/history', label: 'History' },
|
|
12
|
+
{ href: '/ai', label: 'AI' },
|
|
13
|
+
{ href: '/import', label: 'Import' },
|
|
14
|
+
{ href: '/settings', label: 'Settings' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
interface SidebarProps {
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Sidebar({ className }: SidebarProps) {
|
|
22
|
+
const pathname = usePathname();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<aside className={cn('w-56 border-r border-border flex flex-col shrink-0', className)}>
|
|
26
|
+
<div className="p-4 border-b border-border">
|
|
27
|
+
<Link href="/" className="flex items-center gap-2">
|
|
28
|
+
<div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
|
|
29
|
+
<span className="text-primary-foreground font-bold text-xs">AT</span>
|
|
30
|
+
</div>
|
|
31
|
+
<span className="font-bold text-foreground">APITester</span>
|
|
32
|
+
</Link>
|
|
33
|
+
</div>
|
|
34
|
+
<nav className="flex-1 p-2">
|
|
35
|
+
{navItems.map((item) => (
|
|
36
|
+
<Link
|
|
37
|
+
key={item.href}
|
|
38
|
+
href={item.href}
|
|
39
|
+
className={cn(
|
|
40
|
+
'flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors mb-1',
|
|
41
|
+
pathname === item.href || pathname?.startsWith(item.href + '/')
|
|
42
|
+
? 'bg-primary/10 text-primary'
|
|
43
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent',
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{item.label}
|
|
47
|
+
</Link>
|
|
48
|
+
))}
|
|
49
|
+
</nav>
|
|
50
|
+
</aside>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface SplitPaneProps {
|
|
6
|
+
left: ReactNode;
|
|
7
|
+
right: ReactNode;
|
|
8
|
+
defaultSplit?: number;
|
|
9
|
+
minSize?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SplitPane({ left, right, defaultSplit = 50, minSize = 200, className }: SplitPaneProps) {
|
|
14
|
+
const [split, setSplit] = useState(defaultSplit);
|
|
15
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
const dragging = useRef(false);
|
|
17
|
+
|
|
18
|
+
const onMouseDown = useCallback(() => {
|
|
19
|
+
dragging.current = true;
|
|
20
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
21
|
+
if (!dragging.current || !containerRef.current) return;
|
|
22
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
23
|
+
const pct = ((e.clientX - rect.left) / rect.width) * 100;
|
|
24
|
+
setSplit(Math.max(minSize / rect.width * 100, Math.min(100 - minSize / rect.width * 100, pct)));
|
|
25
|
+
};
|
|
26
|
+
const onMouseUp = () => {
|
|
27
|
+
dragging.current = false;
|
|
28
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
29
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
32
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
33
|
+
}, [minSize]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div ref={containerRef} className="flex h-full">
|
|
37
|
+
<div style={{ width: `${split}%` }} className="overflow-hidden">
|
|
38
|
+
{left}
|
|
39
|
+
</div>
|
|
40
|
+
<div
|
|
41
|
+
className="w-1 cursor-col-resize bg-border hover:bg-primary/30 transition-colors"
|
|
42
|
+
onMouseDown={onMouseDown}
|
|
43
|
+
/>
|
|
44
|
+
<div style={{ width: `${100 - split}%` }} className="overflow-hidden">
|
|
45
|
+
{right}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const AUTH_TYPES = ['none', 'basic', 'bearer', 'apikey', 'oauth2', 'aws'];
|
|
6
|
+
|
|
7
|
+
interface AuthTabProps {
|
|
8
|
+
auth: Record<string, string> | null;
|
|
9
|
+
onChange: (auth: Record<string, string> | null) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AuthTab({ auth, onChange }: AuthTabProps) {
|
|
13
|
+
const authType = auth?.['type'] ?? 'none';
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<div className="flex items-center gap-2 mb-3">
|
|
18
|
+
<span className="text-sm text-muted-foreground">Type:</span>
|
|
19
|
+
<select
|
|
20
|
+
value={authType}
|
|
21
|
+
onChange={(e) => {
|
|
22
|
+
const type = e.target.value;
|
|
23
|
+
if (type === 'none') onChange(null);
|
|
24
|
+
else onChange({ type, ...getDefaultAuthFields(type) });
|
|
25
|
+
}}
|
|
26
|
+
className="px-2 py-1 rounded border border-border bg-background text-sm text-foreground"
|
|
27
|
+
>
|
|
28
|
+
{AUTH_TYPES.map((t) => (
|
|
29
|
+
<option key={t} value={t}>{t}</option>
|
|
30
|
+
))}
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{authType === 'basic' && (
|
|
35
|
+
<div className="space-y-2">
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={auth?.['username'] ?? ''}
|
|
39
|
+
onChange={(e) => onChange({ ...auth!, username: e.target.value })}
|
|
40
|
+
placeholder="Username"
|
|
41
|
+
className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
|
|
42
|
+
/>
|
|
43
|
+
<input
|
|
44
|
+
type="password"
|
|
45
|
+
value={auth?.['password'] ?? ''}
|
|
46
|
+
onChange={(e) => onChange({ ...auth!, password: e.target.value })}
|
|
47
|
+
placeholder="Password"
|
|
48
|
+
className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{authType === 'bearer' && (
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
value={auth?.['token'] ?? ''}
|
|
57
|
+
onChange={(e) => onChange({ ...auth!, token: e.target.value })}
|
|
58
|
+
placeholder="Token"
|
|
59
|
+
className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{authType === 'apikey' && (
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
<input
|
|
66
|
+
type="text"
|
|
67
|
+
value={auth?.['key'] ?? ''}
|
|
68
|
+
onChange={(e) => onChange({ ...auth!, key: e.target.value })}
|
|
69
|
+
placeholder="Key name"
|
|
70
|
+
className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
|
|
71
|
+
/>
|
|
72
|
+
<input
|
|
73
|
+
type="text"
|
|
74
|
+
value={auth?.['value'] ?? ''}
|
|
75
|
+
onChange={(e) => onChange({ ...auth!, value: e.target.value })}
|
|
76
|
+
placeholder="Key value"
|
|
77
|
+
className="w-full px-2 py-1.5 rounded border border-border bg-background text-foreground text-sm"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getDefaultAuthFields(type: string): Record<string, string> {
|
|
86
|
+
switch (type) {
|
|
87
|
+
case 'basic': return { username: '', password: '' };
|
|
88
|
+
case 'bearer': return { token: '', prefix: 'Bearer' };
|
|
89
|
+
case 'apikey': return { key: '', value: '', addTo: 'header' };
|
|
90
|
+
case 'oauth2': return { grantType: 'authorization_code', clientId: '', clientSecret: '' };
|
|
91
|
+
case 'aws': return { accessKey: '', secretKey: '', region: '', service: '' };
|
|
92
|
+
default: return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CodeEditor } from '@/components/ui/code-editor';
|
|
5
|
+
|
|
6
|
+
const BODY_TYPES = ['none', 'json', 'urlencoded', 'raw', 'graphql'];
|
|
7
|
+
|
|
8
|
+
interface BodyTabProps {
|
|
9
|
+
body: string;
|
|
10
|
+
bodyType: string;
|
|
11
|
+
onBodyChange: (body: string) => void;
|
|
12
|
+
onBodyTypeChange: (bodyType: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function BodyTab({ body, bodyType, onBodyChange, onBodyTypeChange }: BodyTabProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div>
|
|
18
|
+
<div className="flex items-center gap-2 mb-3">
|
|
19
|
+
<span className="text-sm text-muted-foreground">Type:</span>
|
|
20
|
+
<select
|
|
21
|
+
value={bodyType}
|
|
22
|
+
onChange={(e) => onBodyTypeChange(e.target.value)}
|
|
23
|
+
className="px-2 py-1 rounded border border-border bg-background text-sm text-foreground"
|
|
24
|
+
>
|
|
25
|
+
{BODY_TYPES.map((t) => (
|
|
26
|
+
<option key={t} value={t}>{t}</option>
|
|
27
|
+
))}
|
|
28
|
+
</select>
|
|
29
|
+
</div>
|
|
30
|
+
{bodyType !== 'none' && (
|
|
31
|
+
<CodeEditor
|
|
32
|
+
value={body}
|
|
33
|
+
onChange={onBodyChange}
|
|
34
|
+
language={bodyType === 'json' || bodyType === 'graphql' ? 'json' : 'text'}
|
|
35
|
+
placeholder={`Enter ${bodyType} body...`}
|
|
36
|
+
className="h-64"
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { KeyValueEditor } from '@/components/ui/key-value-editor';
|
|
4
|
+
import type { KeyValue } from '@/lib/utils/validation';
|
|
5
|
+
|
|
6
|
+
interface HeadersTabProps {
|
|
7
|
+
headers: KeyValue[];
|
|
8
|
+
onChange: (headers: KeyValue[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function HeadersTab({ headers, onChange }: HeadersTabProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<h3 className="text-sm font-medium text-foreground mb-2">Request Headers</h3>
|
|
15
|
+
<KeyValueEditor
|
|
16
|
+
pairs={headers}
|
|
17
|
+
onChange={onChange}
|
|
18
|
+
keyPlaceholder="Header name"
|
|
19
|
+
valuePlaceholder="Header value"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { KeyValueEditor } from '@/components/ui/key-value-editor';
|
|
4
|
+
import type { KeyValue } from '@/lib/utils/validation';
|
|
5
|
+
|
|
6
|
+
interface ParamsTabProps {
|
|
7
|
+
params: KeyValue[];
|
|
8
|
+
onChange: (params: KeyValue[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ParamsTab({ params, onChange }: ParamsTabProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<h3 className="text-sm font-medium text-foreground mb-2">Query Parameters</h3>
|
|
15
|
+
<KeyValueEditor
|
|
16
|
+
pairs={params}
|
|
17
|
+
onChange={onChange}
|
|
18
|
+
keyPlaceholder="Parameter name"
|
|
19
|
+
valuePlaceholder="Parameter value"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CodeEditor } from '@/components/ui/code-editor';
|
|
4
|
+
|
|
5
|
+
interface PreRequestTabProps {
|
|
6
|
+
script: string;
|
|
7
|
+
onChange: (script: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PreRequestTab({ script, onChange }: PreRequestTabProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div>
|
|
13
|
+
<h3 className="text-sm font-medium text-foreground mb-2">Pre-request Script</h3>
|
|
14
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
15
|
+
JavaScript code that runs before the request is sent.
|
|
16
|
+
</p>
|
|
17
|
+
<CodeEditor
|
|
18
|
+
value={script}
|
|
19
|
+
onChange={onChange}
|
|
20
|
+
language="javascript"
|
|
21
|
+
placeholder="// Write your pre-request script here"
|
|
22
|
+
className="h-64"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils/cn';
|
|
4
|
+
import type { HttpMethod } from '@/lib/utils/validation';
|
|
5
|
+
|
|
6
|
+
const METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
7
|
+
|
|
8
|
+
interface UrlBarProps {
|
|
9
|
+
method: HttpMethod;
|
|
10
|
+
url: string;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
onMethodChange: (method: HttpMethod) => void;
|
|
13
|
+
onUrlChange: (url: string) => void;
|
|
14
|
+
onSend: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function UrlBar({ method, url, loading, onMethodChange, onUrlChange, onSend }: UrlBarProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center gap-2 p-3 border-b border-border">
|
|
20
|
+
<select
|
|
21
|
+
value={method}
|
|
22
|
+
onChange={(e) => onMethodChange(e.target.value as HttpMethod)}
|
|
23
|
+
className={cn(
|
|
24
|
+
'px-3 py-2 rounded-md border border-border bg-background text-sm font-medium font-mono',
|
|
25
|
+
method === 'GET' && 'text-method-get',
|
|
26
|
+
method === 'POST' && 'text-method-post',
|
|
27
|
+
method === 'PUT' && 'text-method-put',
|
|
28
|
+
method === 'DELETE' && 'text-method-delete',
|
|
29
|
+
method === 'PATCH' && 'text-method-patch',
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{METHODS.map((m) => (
|
|
33
|
+
<option key={m} value={m}>{m}</option>
|
|
34
|
+
))}
|
|
35
|
+
</select>
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={url}
|
|
39
|
+
onChange={(e) => onUrlChange(e.target.value)}
|
|
40
|
+
onKeyDown={(e) => e.key === 'Enter' && onSend()}
|
|
41
|
+
placeholder="Enter request URL"
|
|
42
|
+
className="flex-1 px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm font-mono placeholder:text-muted-foreground"
|
|
43
|
+
/>
|
|
44
|
+
<button
|
|
45
|
+
onClick={onSend}
|
|
46
|
+
disabled={loading || !url.trim()}
|
|
47
|
+
className="px-6 py-2 rounded-md bg-primary text-primary-foreground font-medium text-sm hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
48
|
+
>
|
|
49
|
+
{loading ? 'Sending...' : 'Send'}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
interface UseAiReturn {
|
|
6
|
+
loading: boolean;
|
|
7
|
+
result: string | null;
|
|
8
|
+
error: string | null;
|
|
9
|
+
generateTests: (response: unknown) => Promise<void>;
|
|
10
|
+
discoverEndpoints: (code: string, framework?: string) => Promise<void>;
|
|
11
|
+
explainResponse: (response: unknown, question?: string) => Promise<void>;
|
|
12
|
+
suggestImprovements: (request: unknown, response?: unknown) => Promise<void>;
|
|
13
|
+
clear: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useAi(): UseAiReturn {
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [result, setResult] = useState<string | null>(null);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
const generateTests = useCallback(async (response: unknown) => {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch('/api/ai/generate-tests', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ requestId: 'manual', response }),
|
|
29
|
+
});
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
if (data.success) {
|
|
32
|
+
setResult(typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2));
|
|
33
|
+
} else {
|
|
34
|
+
setError(data.error?.message ?? 'Failed to generate tests');
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setError(err instanceof Error ? err.message : 'Failed to generate tests');
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const discoverEndpoints = useCallback(async (code: string, framework: string = 'auto') => {
|
|
44
|
+
setLoading(true);
|
|
45
|
+
setError(null);
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch('/api/ai/discover', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ code, framework, workspaceId: 'default' }),
|
|
51
|
+
});
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
if (data.success) {
|
|
54
|
+
setResult(JSON.stringify(data.data, null, 2));
|
|
55
|
+
} else {
|
|
56
|
+
setError(data.error?.message ?? 'Failed to discover endpoints');
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
setError(err instanceof Error ? err.message : 'Failed to discover endpoints');
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const explainResponse = useCallback(async (response: unknown, question?: string) => {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
setError(null);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch('/api/ai/explain', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ response, question }),
|
|
73
|
+
});
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
if (data.success) {
|
|
76
|
+
setResult(typeof data.data === 'string' ? data.data : JSON.stringify(data.data, null, 2));
|
|
77
|
+
} else {
|
|
78
|
+
setError(data.error?.message ?? 'Failed to explain response');
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setError(err instanceof Error ? err.message : 'Failed to explain response');
|
|
82
|
+
} finally {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const suggestImprovements = useCallback(async (request: unknown, response?: unknown) => {
|
|
88
|
+
setLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('/api/ai/suggest', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ request, response }),
|
|
95
|
+
});
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
if (data.success) {
|
|
98
|
+
setResult(JSON.stringify(data.data, null, 2));
|
|
99
|
+
} else {
|
|
100
|
+
setError(data.error?.message ?? 'Failed to get suggestions');
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
setError(err instanceof Error ? err.message : 'Failed to get suggestions');
|
|
104
|
+
} finally {
|
|
105
|
+
setLoading(false);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const clear = useCallback(() => {
|
|
110
|
+
setResult(null);
|
|
111
|
+
setError(null);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
return { loading, result, error, generateTests, discoverEndpoints, explainResponse, suggestImprovements, clear };
|
|
115
|
+
}
|