큐 관리 추가
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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'}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user