TCP 서버 모드 추가
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user