170 lines
5.7 KiB
React
170 lines
5.7 KiB
React
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { Client } from '@stomp/stompjs';
|
|
import SockJS from 'sockjs-client';
|
|
import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
import { cn } from '@/lib/utils';
|
|
import { useMonaco } from '@monaco-editor/react';
|
|
|
|
const LoadTestResult = ({ contextPath = '/', testId }) => {
|
|
const [results, setResults] = useState([]);
|
|
const [selectedResult, setSelectedResult] = useState(null);
|
|
const [activeTab, setActiveTab] = useState('body');
|
|
const stompClientRef = useRef(null);
|
|
const monaco = useMonaco();
|
|
const editorRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
// Initialize Monaco editor
|
|
if (monaco && !editorRef.current) {
|
|
editorRef.current = monaco.editor.create(document.getElementById('test-output'), {
|
|
value: '',
|
|
language: 'json',
|
|
automaticLayout: true,
|
|
readOnly: true,
|
|
minimap: { enabled: false },
|
|
theme: 'vs-dark'
|
|
});
|
|
}
|
|
|
|
return () => {
|
|
if (editorRef.current) {
|
|
editorRef.current.dispose();
|
|
editorRef.current = null;
|
|
}
|
|
};
|
|
}, [monaco]);
|
|
|
|
useEffect(() => {
|
|
if (!testId) return;
|
|
|
|
// Initialize WebSocket connection
|
|
const socket = new SockJS(`${contextPath}load-test-result`);
|
|
const client = new Client({
|
|
webSocketFactory: () => socket,
|
|
onConnect: () => {
|
|
client.subscribe(`/topic/${testId}`, (message) => {
|
|
const result = JSON.parse(message.body);
|
|
setResults(prev => [...prev, result]);
|
|
});
|
|
},
|
|
onStompError: (error) => {
|
|
console.error('STOMP error:', error);
|
|
}
|
|
});
|
|
|
|
stompClientRef.current = client;
|
|
client.activate();
|
|
|
|
return () => {
|
|
if (client.connected) {
|
|
client.deactivate();
|
|
}
|
|
};
|
|
}, [testId, contextPath]);
|
|
|
|
const reset = useCallback(() => {
|
|
setResults([]);
|
|
setSelectedResult(null);
|
|
if (editorRef.current) {
|
|
editorRef.current.setValue('');
|
|
}
|
|
}, []);
|
|
|
|
const formatResponse = useCallback((response) => {
|
|
if (!response) return '';
|
|
|
|
if (response.headers) {
|
|
const sortedHeaders = Object.entries(response.headers)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join('\n');
|
|
return sortedHeaders;
|
|
}
|
|
|
|
try {
|
|
return JSON.stringify(JSON.parse(response.body || '{}'), null, 2);
|
|
} catch {
|
|
return response.body || '';
|
|
}
|
|
}, []);
|
|
|
|
const handleResultSelect = useCallback((result) => {
|
|
setSelectedResult(result);
|
|
if (editorRef.current) {
|
|
const content = result.type === 'TCP' ?
|
|
`[TCP Response]\n${result.response.body}` :
|
|
`[HTTP Response]\nStatus: ${result.response.status}\n\n[Headers]\n${formatResponse(result.response.headers)}\n\n[Body]\n${result.response.body}`;
|
|
editorRef.current.setValue(content);
|
|
}
|
|
}, [formatResponse]);
|
|
|
|
return (
|
|
<div className="flex h-full gap-4">
|
|
{/* Results List */}
|
|
<Card className="w-64">
|
|
<CardHeader>Test Results</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[600px]">
|
|
<div className="space-y-2">
|
|
{results.map((result, index) => (
|
|
<div
|
|
key={index}
|
|
onClick={() => handleResultSelect(result)}
|
|
className={cn(
|
|
"p-2 rounded cursor-pointer hover:bg-accent",
|
|
selectedResult === result && "bg-accent"
|
|
)}
|
|
>
|
|
<div className="font-medium truncate">{result.request.name}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
Status: {result.response.status || 'N/A'}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Result Details */}
|
|
<Card className="flex-1">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>Result Details</div>
|
|
<Button variant="outline" onClick={reset}>
|
|
Clear
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList>
|
|
<TabsTrigger value="body">Body</TabsTrigger>
|
|
<TabsTrigger value="console">Console</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="body" className="h-[600px]">
|
|
<div id="test-output" className="h-full w-full border rounded-md"></div>
|
|
</TabsContent>
|
|
<TabsContent value="console" className="h-[600px]">
|
|
<div id="test-console" className="h-full w-full border rounded-md bg-zinc-950 text-zinc-50 p-4 font-mono overflow-auto">
|
|
{results.map((result, index) => (
|
|
<div key={index} className="whitespace-pre-wrap mb-2">
|
|
{`[${new Date(result.timestamp).toISOString()}] ${result.request.name}\n`}
|
|
{`Status: ${result.response.status}\n`}
|
|
{`Time: ${result.response.time}ms\n`}
|
|
{`Size: ${result.response.size} bytes\n`}
|
|
{'='.repeat(50)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LoadTestResult;
|