큐 관리 추가

This commit is contained in:
현성필
2025-01-31 14:30:00 +09:00
parent c7b6e29607
commit d52dadab4d
13 changed files with 928 additions and 56 deletions
+16 -6
View File
@@ -29,18 +29,22 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
showAddScenario: false,
showVariableModal: false,
showMockRouteEdit: false,
showTCPServerEdit: false,
showQueueEdit: false,
selectedItem: null,
});
const handleMenuClick = async (action, screen, item = null) => {
console.log(action);
console.log(item);
if (screen) {
console.log('Screen:', screen);
setCurrentScreen(screen);
return;
}
console.log(item);
// Handle modal actions
switch (action) {
case 'request-add':
@@ -49,11 +53,15 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
case 'response-add':
case 'response-edit':
case 'response-clone':
setDialogState(prev => ({
...prev,
showMockRouteEdit: true,
selectedItem: item // Pass the selected item
}));
setDialogState(prev => ({...prev, showMockRouteEdit: true, selectedItem: item }));
break;
case 'queue-add':
case 'queue-edit':
setDialogState(prev => ({ ...prev, showQueueEdit: true , selectedItem: item}));
break;
case 'socket-add':
case 'socket-edit':
setDialogState(prev => ({ ...prev, showTCPServerEdit: true , selectedItem: item}));
break;
case 'collection-add':
setDialogState(prev => ({ ...prev, showAddCollection: true }));
@@ -122,6 +130,8 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
showAddScenario={dialogState.showAddScenario}
showVariableModal={dialogState.showVariableModal}
showMockRouteEdit={dialogState.showMockRouteEdit}
showTCPServerEdit={dialogState.showTCPServerEdit}
showQueueEdit={dialogState.showQueueEdit}
selectedItem={dialogState.selectedItem}
onDialogChange={handleDialogChange}
/>
@@ -229,7 +229,7 @@ const MockRouteEditDialog = ({open, onOpenChange, item}) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="fixed translate-x-0 translate-y-0 overflow-y-auto inset-0 h-screen w-screen max-w-none">
<DialogContent className="max-w-full h-screen">
<DialogHeader>
<DialogTitle>
{mockRoute.id == null ? 'API 정보 등록' : 'API 정보 수정'}
@@ -0,0 +1,173 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import MonacoEditor from '@/components/shared/MonacoEditor';
import {getCSRFToken} from '@/services/token.js';
const defaultScript = `
//message를 사용 가능
var URL = Java.type('java.net.URL');
var HttpURLConnection = Java.type('java.net.HttpURLConnection');
var BufferedReader = Java.type('java.io.BufferedReader');
var InputStreamReader = Java.type('java.io.InputStreamReader');
var StringBuilder = Java.type('java.lang.StringBuilder');
try {
var url = new URL('http://localhost:38080/');
var connection = url.openConnection();
connection.setRequestMethod('POST');
connection.setDoOutput(true);
// Write the body
var outputStream = connection.getOutputStream();
var body = 'async_result' + message;
outputStream.write(new java.lang.String(body).getBytes('UTF-8'));
outputStream.close();
console.log(body);
// Read the response
var responseCode = connection.getResponseCode();
if (responseCode === 200) {
var inputStream = connection.getInputStream();
var reader = new BufferedReader(new InputStreamReader(inputStream));
var builder = new StringBuilder();
var line;
while ((line = reader.readLine()) !== null) {
builder.append(line);
builder.append('\\n');
}
reader.close();
console.log('POST Response:' + builder.toString());
} else {
console.log('HTTP Error! Status:' + responseCode);
}
} catch (error) {
console.log('Error making POST request:' + error);
}
`;
const QueueEditDialog = ({ open, onOpenChange, item }) => {
const [queueHandler, setQueueHandler] = useState({
queueName: '',
handlerScript: defaultScript
});
// Initialize form when item changes
useEffect(() => {
if (item) {
setQueueHandler(item);
} else {
// Reset to default state when creating new
setQueueHandler({
queueName: '',
handlerScript: defaultScript
});
}
}, [item]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!queueHandler.queueName || !queueHandler.handlerScript) {
return;
}
try {
const url = item ? `/api/queue-handlers/update/${item.queueName}` : '/api/queue-handlers';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCSRFToken()
},
body: JSON.stringify(queueHandler)
});
if (!response.ok) {
throw new Error('Failed to save queue handler');
}
onOpenChange(false);
} catch (error) {
console.error('Failed to save queue handler:', error);
alert('Failed to save queue handler');
}
};
const handleChange = (field, value) => {
setQueueHandler(prev => ({ ...prev, [field]: value }));
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-full">
<DialogHeader>
<DialogTitle>
{item ? '큐 핸들러 수정' : '큐 핸들러 등록'}
</DialogTitle>
<DialogDescription>
핸들러의 정보를 입력하거나 수정할 있습니다. 필수 항목을 모두 입력해주세요.
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<form id="queueForm" onSubmit={handleSubmit} className="space-y-4 p-1">
{/* Queue Name */}
<div className="space-y-2">
<Label htmlFor="queueName" className="must">
이름
</Label>
<Input
id="queueName"
required
value={queueHandler.queueName}
onChange={(e) => handleChange('queueName', e.target.value)}
disabled={!!item} // Disable editing of queue name for existing handlers
/>
</div>
{/* Handler Script */}
<Card>
<CardHeader>
<CardTitle>핸들러 스크립트</CardTitle>
</CardHeader>
<CardContent className="h-[400px]">
<MonacoEditor
value={queueHandler.handlerScript}
language="javascript"
onChange={(value) => handleChange('handlerScript', value)}
height="full"
/>
</CardContent>
</Card>
</form>
</ScrollArea>
<DialogFooter className="flex justify-end space-x-2">
<Button variant="secondary" onClick={() => onOpenChange(false)}>
닫기
</Button>
<Button type="submit" variant="default" onClick={handleSubmit}>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default QueueEditDialog;
@@ -0,0 +1,126 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import DataTable from '@/components/shared/DataTable';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { useAPI } from '@/providers/APIProvider';
const QueueList = ({ onMenuClick }) => {
const { queues, loading, loadQueues, deleteQueues, startQueue, stopQueue } = useAPI();
const [searchTerm, setSearchTerm] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
const [page, setPage] = useState(0);
useEffect(() => {
loadQueues({ page });
}, [page, loadQueues]);
const handleSearch = () => {
setPage(0);
loadQueues({ page: 0, queueName: searchTerm });
};
const handleDelete = async () => {
if (!selectedIds.length) return;
const success = await deleteQueues(selectedIds);
if (success) {
setSelectedIds([]);
}
};
const columns = [
{ key: 'queueName', label: '큐 이름' },
{
key: 'active',
label: '상태',
render: (value) => (
<Badge variant={value ? 'success' : 'destructive'}>
{value ? 'Active' : 'Inactive'}
</Badge>
)
}
];
const renderRowActions = (row) => (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => startQueue(row.queueName)}
>
시작
</Button>
<Button
variant="outline"
size="sm"
onClick={() => stopQueue(row.queueName)}
>
정지
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onMenuClick('queue-edit', null, row)}
>
수정
</Button>
</div>
);
const tableActions = (
<>
<Button variant="destructive" onClick={handleSearch}>조회</Button>
<Button
variant="destructive"
onClick={handleDelete}
disabled={selectedIds.length === 0}
>
삭제
</Button>
<Button onClick={() => onMenuClick('queue-add')}>등록</Button>
</>
);
return (
<div className="container mx-auto p-4">
<Card>
<CardHeader>
<CardTitle> 목록</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4 mb-4">
<div className="flex-1 max-w-sm">
<Input
placeholder="큐 이름으로 검색"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
/>
</div>
{tableActions}
</div>
<DataTable
data={queues}
columns={columns}
loading={loading}
selectable
selectedIds={selectedIds}
onSelectIds={setSelectedIds}
onPageChange={setPage}
onDelete={handleDelete}
renderRowActions={renderRowActions}
deleteDialogTitle="큐 삭제"
deleteDialogDescription="선택한 큐를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
/>
</CardContent>
</Card>
</div>
);
};
export default QueueList;
@@ -1,9 +1,18 @@
import React from 'react';
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table';
import {Button} from '@/components/ui/button';
import {Card, CardContent} from '@/components/ui/card';
import {Checkbox} from '@/components/ui/checkbox';
import {AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle} from '@/components/ui/alert-dialog';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog';
const DataTable = ({
// Data and columns
@@ -28,34 +37,33 @@ const DataTable = ({
deleteDialogDescription = 'Are you sure you want to delete the selected items? This action cannot be undone.',
onDelete
}) => {
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false);
const handleSelectAll = (checked) => {
const handleSelectAll = React.useCallback((checked) => {
if (!onSelectIds) return;
if (checked) {
onSelectIds(data.content.map(item => item.id));
onSelectIds(data.content.map(item => item.id || item.queueName));
} else {
onSelectIds([]);
}
};
}, [data.content, onSelectIds]);
const handleSelectItem = (id, checked) => {
const handleSelectItem = React.useCallback((id, checked) => {
if (!onSelectIds) return;
if (checked) {
onSelectIds([...selectedIds, id]);
} else {
onSelectIds(selectedIds.filter(selectedId => selectedId !== id));
}
};
}, [selectedIds, onSelectIds]);
const handlePageChange = (page) => {
const handlePageChange = React.useCallback((page) => {
// Reset selections when changing pages
if (onSelectIds) {
onSelectIds([]);
}
onPageChange(page);
};
}, [onSelectIds, onPageChange]);
if (loading) {
return (
@@ -82,34 +90,37 @@ const DataTable = ({
</TableHead>
)}
{columns.map((column) => (
<TableHead key={column.key}>{column.label}</TableHead>
<TableHead key={column.id || column.key}>{column.label}</TableHead>
))}
{renderRowActions && <TableHead className="text-right">Actions</TableHead>}
{renderRowActions && <TableHead className="text-center">Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{data.content.map((row) => (
<TableRow key={row.id}>
{selectable && (
<TableCell>
<Checkbox
checked={selectedIds.includes(row.id)}
onCheckedChange={(checked) => handleSelectItem(row.id, checked)}
/>
</TableCell>
)}
{columns.map((column) => (
<TableCell key={column.key}>
{column.render ? column.render(row[column.key], row) : row[column.key]}
</TableCell>
))}
{renderRowActions && (
<TableCell className="text-right">
{renderRowActions(row)}
</TableCell>
)}
</TableRow>
))}
{data.content.map((row) => {
const rowId = row.id || row.queueName;
return (
<TableRow key={rowId}>
{selectable && (
<TableCell>
<Checkbox
checked={selectedIds.includes(rowId)}
onCheckedChange={(checked) => handleSelectItem(rowId, checked)}
/>
</TableCell>
)}
{columns.map((column) => (
<TableCell key={`${rowId}-${column.id || column.key}`}>
{column.render ? column.render(row[column.key], row) : row[column.key]}
</TableCell>
))}
{renderRowActions && (
<TableCell className="text-center">
{renderRowActions(row)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
</div>
@@ -117,7 +128,7 @@ const DataTable = ({
{onPageChange && (
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-500">
Showing {data.numberOfElements} of {data.totalElements} results
전체 {data.totalElements} {data.numberOfElements} 표시
</div>
<div className="flex items-center gap-2">
<Button
@@ -134,12 +145,12 @@ const DataTable = ({
disabled={data.first}
size="sm"
>
Previous
이전
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: data.totalPages }, (_, i) => (
<Button
key={i}
key={`page-${i}`}
variant={i === data.number ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(i)}
@@ -155,7 +166,7 @@ const DataTable = ({
disabled={data.last}
size="sm"
>
Next
다음
</Button>
<Button
variant="outline"
@@ -4,6 +4,8 @@ import AddCollectionDialog from '@/components/api-tester/AddCollectionDialog.jsx
import AddScenarioDialog from '@/components/api-tester/AddScenarioDialog.jsx';
import VariableModal from '@/components/api-tester/VariableModal.jsx';
import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx';
import TCPServerEditDialog from '@/components/tcp-servers/TCPServerEditDialog.jsx';
import QueueEditDialog from '@/components/queues/QueueEditDialog.jsx';
const DialogManager = ({
showAddApi,
@@ -11,6 +13,8 @@ const DialogManager = ({
showAddScenario,
showVariableModal,
showMockRouteEdit,
showTCPServerEdit,
showQueueEdit,
selectedItem,
onDialogChange
}) => {
@@ -37,6 +41,16 @@ const DialogManager = ({
onOpenChange={(isOpen) => onDialogChange('showMockRouteEdit', isOpen)}
item={selectedItem}
/>
<TCPServerEditDialog
open={showTCPServerEdit}
onOpenChange={(isOpen) => onDialogChange('showTCPServerEdit', isOpen)}
item={selectedItem}
/>
<QueueEditDialog
open={showQueueEdit}
onOpenChange={(isOpen) => onDialogChange('showQueueEdit', isOpen)}
item={selectedItem}
/>
</>;
}
@@ -142,9 +142,19 @@ const MonacoEditor = memo(({
}, [normalizedValue]);
// Convert height to pixel value if it's a number
const containerHeight = typeof height === 'number'
? `${height}px`
: height.endsWith('px') ? height : `${height}px`;
// const containerHeight = typeof height === 'number'
// ? `${height}px`
// : height.endsWith('px') ? height : `${height}px`;
const getHeightStyle = (height) => {
if (height === 'full') {
return '100%';
}
if (typeof height === 'number') {
return `${height}px`;
}
return height.endsWith('px') ? height : `${height}px`;
};
return (
<div
@@ -152,7 +162,7 @@ const MonacoEditor = memo(({
ref={containerRef}
style={{
minHeight: '100px',
height: containerHeight,
height: getHeightStyle(height),
border: '1px solid #e2e8f0',
marginTop: '0.5rem'
}}
@@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import MonacoEditor from '@/components/shared/MonacoEditor';
const defaultScript = `function processJson(message) {
//process message;
return "response_message";
}`;
const TCPServerEditDialog = ({ open, onOpenChange, item }) => {
const [server, setServer] = useState({
id: null,
name: '',
port: 1024,
active: true,
script: defaultScript
});
// Initialize form when item changes
useEffect(() => {
if (item) {
setServer(item);
} else {
// Reset to default state when creating new
setServer({
id: null,
name: '',
port: 1024,
active: true,
script: defaultScript
});
}
}, [item]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!server.name || !server.port) {
return;
}
try {
const url = server.id ? `/api/socket/update/${server.id}` : '/api/socket';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify(server)
});
if (!response.ok) {
throw new Error('Failed to save server');
}
onOpenChange(false);
} catch (error) {
console.error('Failed to save server:', error);
alert('Failed to save server');
}
};
const handleChange = (field, value) => {
setServer(prev => ({ ...prev, [field]: value }));
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>
{server.id == null ? 'TCP 서버 등록' : 'TCP 서버 수정'}
</DialogTitle>
<DialogDescription>
TCP 서버의 정보를 입력하거나 수정할 있습니다. 필수 항목을 모두 입력해주세요.
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<form id="serverForm" onSubmit={handleSubmit} className="space-y-4 p-1">
{/* Server Name */}
<div className="space-y-2">
<Label htmlFor="name" className="must">
서버 이름
</Label>
<Input
id="name"
required
value={server.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
</div>
{/* Port Number */}
<div className="space-y-2">
<Label htmlFor="port" className="must">
포트
</Label>
<Input
id="port"
type="number"
required
min="1024"
max="65535"
value={server.port}
onChange={(e) => handleChange('port', parseInt(e.target.value))}
/>
</div>
{/* Active Status */}
<div className="flex items-center space-x-2">
<Checkbox
id="active"
checked={server.active}
onCheckedChange={(checked) => handleChange('active', checked)}
/>
<Label htmlFor="active">Active</Label>
</div>
{/* Script Editor */}
<Card>
<CardHeader>
<CardTitle>서버 스크립트</CardTitle>
</CardHeader>
<CardContent className="h-[400px]">
<MonacoEditor
value={server.script}
language="javascript"
onChange={(value) => handleChange('script', value)}
height="full"
/>
</CardContent>
</Card>
</form>
</ScrollArea>
<DialogFooter className="flex justify-end space-x-2">
<Button variant="secondary" onClick={() => onOpenChange(false)}>
닫기
</Button>
<Button type="submit" variant="default" onClick={handleSubmit}>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default TCPServerEditDialog;
@@ -0,0 +1,209 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import DataTable from '@/components/shared/DataTable';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns';
const TCPServerList = ({ onMenuClick }) => {
const [servers, setServers] = useState({
content: [],
totalPages: 0,
number: 0,
first: true,
last: true,
numberOfElements: 0,
totalElements: 0
});
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
const [page, setPage] = useState(0);
const loadServers = async (params = { page: 0 }) => {
setLoading(true);
try {
const searchParams = new URLSearchParams({
page: params.page,
size: 10,
...(searchTerm && { name: searchTerm })
});
const response = await fetch(`/api/socket?${searchParams}`);
const data = await response.json();
setServers(data);
} catch (error) {
console.error('Error loading servers:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadServers({ page });
}, [page]);
const handleSearch = () => {
setPage(0);
loadServers({ page: 0 });
};
const handleDelete = async () => {
if (!selectedIds.length) return;
try {
const response = await fetch('/api/socket/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify(selectedIds)
});
if (response.ok) {
setSelectedIds([]);
loadServers({ page });
}
} catch (error) {
console.error('Error deleting servers:', error);
}
};
const handleStartServer = async (id) => {
try {
const response = await fetch(`/api/socket/${id}/start`, {
method: 'POST',
headers: {
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
}
});
if (response.ok) {
loadServers({ page });
}
} catch (error) {
console.error('Error starting server:', error);
}
};
const handleStopServer = async (id) => {
try {
const response = await fetch(`/api/socket/${id}/stop`, {
method: 'POST',
headers: {
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
}
});
if (response.ok) {
loadServers({ page });
}
} catch (error) {
console.error('Error stopping server:', error);
}
};
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'port', label: 'Port' },
{
key: 'active',
label: 'Status',
render: (value) => (
<Badge variant={value ? 'success' : 'destructive'}>
{value ? 'Active' : 'Inactive'}
</Badge>
)
},
{
key: 'lastModified',
label: 'Last Modified',
render: (value) => format(new Date(value), 'yyyy-MM-dd HH:mm:ss')
}
];
const renderRowActions = (row) => (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={!row.active}
onClick={() => handleStartServer(row.id)}
>
Start
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleStopServer(row.id)}
>
Stop
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onMenuClick('socket-edit', null, row)}
>
Edit
</Button>
</div>
);
const tableActions = (
<>
<Button variant="destructive" onClick={handleSearch}>조회</Button>
<Button
variant="destructive"
onClick={handleDelete}
disabled={selectedIds.length === 0}
>
삭제
</Button>
<Button onClick={() => onMenuClick('socket-add')}>등록</Button>
</>
);
return (
<div className="container mx-auto p-4">
<Card>
<CardHeader>
<CardTitle>TCP Server List</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4 mb-4">
<div className="flex-1 max-w-sm">
<Input
placeholder="서버 이름으로 검색"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
/>
</div>
{tableActions}
</div>
<DataTable
data={servers}
columns={columns}
loading={loading}
selectable
selectedIds={selectedIds}
onSelectIds={setSelectedIds}
onPageChange={setPage}
onDelete={handleDelete}
renderRowActions={renderRowActions}
deleteDialogTitle="서버 삭제"
deleteDialogDescription="선택한 서버를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
/>
</CardContent>
</Card>
</div>
);
};
export default TCPServerList;
+3 -3
View File
@@ -7,8 +7,8 @@ export const menuConfig = [
{id: 'response-list', label: 'API 응답 목록', action: 'response-list', screen: 'response-list'},
{id: 'response-add', label: 'API 응답 추가', action: 'response-add'},
{type: 'separator'},
{id: 'tcp-list', label: 'TCP 서버 목록', action: 'tcp-list'},
{id: 'tcp-add', label: 'TCP 서버 추가', action: 'tcp-add'},
{id: 'socket-list', label: 'TCP 서버 목록', action: 'socket-list', screen: 'socket-list'},
{id: 'socket-add', label: 'TCP 서버 추가', action: 'socket-add'},
]
},
{
@@ -29,7 +29,7 @@ export const menuConfig = [
label: '큐 관리',
items: [
{id: 'queue-list', label: '큐 목록', action: 'queue-list', screen: 'queue-list'},
{id: 'queue-add', label: '큐 추가', action: 'queue-add', screen: 'queue-add'}
{id: 'queue-add', label: '큐 추가', action: 'queue-add'}
]
},
{
+4
View File
@@ -2,9 +2,13 @@
import APITester from '../components/api-tester/APITester';
import LoadTestManager from '../components/load-tester/LoadTestManager.jsx';
import MockRouteList from '../components/mock-route/MockRouteList.jsx';
import TCPServerList from '@/components/tcp-servers/TCPServerList.jsx';
import QueueList from '@/components/queues/QueueList.jsx';
export const SCREENS = {
'api-tester': APITester,
'load-test': LoadTestManager,
'response-list': MockRouteList,
'socket-list': TCPServerList,
'queue-list': QueueList,
};
+78 -1
View File
@@ -4,6 +4,7 @@ import {apiRequestService} from '@/services/apiRequestService';
import {serverService} from '@/services/serverService';
import {authService} from '@/services/authService';
import APIClient from '@/services/APIClient.js';
import {queueService} from '@/services/queueService.js';
const APIContext = createContext(null);
@@ -34,11 +35,21 @@ export const APIProvider = ({
last: true
});
const [queues, setQueues] = useState({
content: [],
totalPages: 0,
number: 0,
first: true,
last: true
});
// Initialize services
const collections$ = collectionService(contextPath);
const apiRequests$ = apiRequestService(contextPath);
const servers$ = serverService();
const auth$ = authService();
const queues$ = queueService(contextPath);
const handleLogout = useCallback(async () => {
try {
@@ -292,6 +303,67 @@ export const APIProvider = ({
}, [loadMockRoutes]);
const loadQueues = useCallback(async (params = { page: 0 }) => {
setLoading(true);
setError(null);
try {
const data = await queues$.loadQueues(params);
setQueues(data);
return data;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, []);
const deleteQueues = useCallback(async (ids) => {
setLoading(true);
setError(null);
try {
await queues$.deleteQueues(ids);
await loadQueues();
return true;
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
}, [loadQueues]);
const startQueue = useCallback(async (id) => {
setLoading(true);
setError(null);
try {
await queues$.startQueue(id);
await loadQueues();
return true;
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
}, [loadQueues]);
const stopQueue = useCallback(async (id) => {
setLoading(true);
setError(null);
try {
await queues$.stopQueue(id);
await loadQueues();
return true;
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
}, [loadQueues]);
const contextValue = {
apiClient,
collections,
@@ -316,7 +388,12 @@ export const APIProvider = ({
loadMockRoutes,
deleteMockRoutes,
createMockRoute,
updateMockRoute
updateMockRoute,
queues,
loadQueues,
deleteQueues,
startQueue,
stopQueue
};
return (
+73
View File
@@ -0,0 +1,73 @@
// services/queueService.js
import { getCSRFToken } from '@/services/token.js';
export const queueService = (contextPath = '/') => {
const headers = {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCSRFToken()
};
return {
async loadQueues(params = { page: 0 }) {
const searchParams = new URLSearchParams({
page: params.page,
size: 10,
...(params.queueName && { queueName: params.queueName })
});
const response = await fetch(`${contextPath}api/queue-handlers?${searchParams}`);
if (!response.ok) throw new Error('Failed to fetch queues');
return await response.json();
},
async createQueue(queue) {
const response = await fetch(`${contextPath}api/queue-handlers`, {
method: 'POST',
headers,
body: JSON.stringify(queue)
});
if (!response.ok) throw new Error('Failed to create queue');
return await response.json();
},
async updateQueue(id, queue) {
const response = await fetch(`${contextPath}api/queue-handlers/update/${id}`, {
method: 'POST',
headers,
body: JSON.stringify(queue)
});
if (!response.ok) throw new Error('Failed to update queue');
return await response.json();
},
async deleteQueues(ids) {
const response = await fetch(`${contextPath}api/queue-handlers/delete`, {
method: 'POST',
headers,
body: JSON.stringify(ids)
});
if (!response.ok) throw new Error('Failed to delete queues');
},
async startQueue(id) {
const response = await fetch(`${contextPath}api/queue-handlers/${id}/start`, {
method: 'POST',
headers
});
if (!response.ok) throw new Error('Failed to start queue');
},
async stopQueue(id) {
const response = await fetch(`${contextPath}api/queue-handlers/${id}/stop`, {
method: 'POST',
headers
});
if (!response.ok) throw new Error('Failed to stop queue');
}
};
};