코드 정리

This commit is contained in:
현성필
2025-01-31 11:10:11 +09:00
parent c2d7d852d4
commit c7b6e29607
39 changed files with 1403 additions and 255 deletions
+1 -2
View File
@@ -53,7 +53,6 @@
"globals": "^15.14.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.5",
"vite-plugin-monaco-editor": "^1.1.0"
"vite": "^6.0.5"
}
}
+36 -119
View File
@@ -1,16 +1,15 @@
import React, { useEffect, useState } from 'react';
import { APIProvider, useAPI } from './providers/APIProvider';
import { LoadTestProvider } from './providers/LoadTestProvider';
import APITester from './components/APITester';
import LoadTestManager from './components/LoadTestManager';
import DialogManager from './components/DialogManager';
import DialogManager from './components/shared/DialogManager';
import { monacoConfig } from '@/config/monaco.js';
import LoginDialog from '@/components/LoginDialog.jsx';
import {Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger} from '@/components/ui/menubar.jsx';
import LoginDialog from '@/components/account/LoginDialog.jsx';
import AppMenu from './components/app-menu/AppMenu';
import { SCREENS } from './config/screenConfig';
window.MonacoEnvironment = monacoConfig;
const App = ({ options = { type: 'api', contextPath: '/' } }) => {
const App = ({ options = { contextPath: '/' } }) => {
const [showLogin, setShowLogin] = useState(false);
return (
@@ -20,102 +19,50 @@ const App = ({ options = { type: 'api', contextPath: '/' } }) => {
);
};
const menuConfig = [
{
id: 'response',
label: '응답관리',
items: [
{id: 'response-list', label: '응답목록', action: 'response-list'},
{id: 'response-add', label: '응답추가', action: 'response-add'}
]
},
{
id: 'request',
label: '요청관리',
items: [
{id: 'collection-add', label: '컬렉션 추가', action: 'collection-add'},
{id: 'request-add', label: '요청 추가', action: 'request-add'},
{id: 'scenario-add', label: '시나리오 추가', action: 'scenario-add'},
{id: 'variables-show', label: '변수 보기', action: 'variables-show'},
]
},
{
id: 'queue',
label: '큐관리',
items: [
{id: 'queue-status', label: '큐상태', action: 'queue-status'},
{id: 'queue-settings', label: '큐설정', action: 'queue-settings'}
]
},
{
id: 'loadtest',
label: '부하테스트',
items: [
{id: 'load-test-new', label: '새 테스트', action: 'load-test-new'},
{id: 'load-test-history', label: '테스트 이력', action: 'load-test-history'},
{id: 'load-test-reports', label: '결과 리포트', action: 'load-test-reports'}
]
},
{
id: 'system',
label: '시스템관리',
items: [
{id: 'system-settings', label: '시스템설정', action: 'system-settings'},
{id: 'user-management', label: '사용자관리', action: 'user-management'},
{type: 'separator'},
{id: 'system-logs', label: '시스템로그', action: 'system-logs'}
]
},
{
id: 'account',
label: '계정',
items: [
{id: 'change-password', label: '비밀번호변경', action: 'change-password'},
{type: 'separator'},
{id: 'logout', label: '로그아웃', action: 'logout'}
]
}
];
const MenuItem = ({item, onMenuClick}) => {
if (item.type === 'separator') {
return <MenubarSeparator/>;
}
return (
<MenubarItem onClick={() => onMenuClick(item.action)}>
{item.label}
</MenubarItem>
);
};
const AppContent = ({ options, showLogin, setShowLogin }) => {
const { isAuthenticated, setIsAuthenticated, handleLogout } = useAPI();
const [isInitialized, setIsInitialized] = useState(false);
const [currentScreen, setCurrentScreen] = useState('api-tester');
const [dialogState, setDialogState] = useState({
showAddApi: false,
showAddCollection: false,
showAddScenario: false,
showVariableModal: false
showVariableModal: false,
showMockRouteEdit: false,
selectedItem: null,
});
const handleMenuClick = async (action) => {
const handleMenuClick = async (action, screen, item = null) => {
console.log('Menu action:', action);
if (screen) {
console.log('Screen:', screen);
setCurrentScreen(screen);
return;
}
console.log(item);
// Handle modal actions
switch (action) {
case 'request-add':
setDialogState(prev => ({ ...prev, showAddApi: true }));
break;
case 'response-add':
case 'response-edit':
case 'response-clone':
setDialogState(prev => ({
...prev,
showMockRouteEdit: true,
selectedItem: item // Pass the selected item
}));
break;
case 'collection-add':
setDialogState(prev => ({ ...prev, showAddCollection: true }));
break;
case 'scenario-add':
setDialogState(prev => ({ ...prev, showAddScenario: true }));
break;
case 'variables-show':
setDialogState(prev => ({ ...prev, showVariableModal: true }));
break;
case 'logout':
const success = await handleLogout();
@@ -123,10 +70,6 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
window.location.href = "/actionLogout.do";
}
break;
case 'change-password':
// Handle password change
break;
// Add other cases as needed
}
};
@@ -167,53 +110,27 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
return <LoginDialog open={showLogin} onOpenChange={setShowLogin} />;
}
const CurrentScreenComponent = SCREENS[currentScreen] || SCREENS['api-tester'];
return (
<LoadTestProvider contextPath={options.contextPath}>
<div className="flex flex-col h-screen bg-background">
<div className="fixed top-0 left-0 right-0 z-50 bg-background border-b">
<div className="flex items-center h-14">
<div className="px-4 py-2 border-r h-full flex items-center">
<img
src="/img/logo.png"
alt="testmaster"
className="h-8 w-auto"
/>
</div>
<Menubar className="border-none flex-1">
{menuConfig.map((menu) => (
<MenubarMenu key={menu.id}>
<MenubarTrigger className="font-bold">
{menu.label}
</MenubarTrigger>
<MenubarContent>
{menu.items.map((item, index) => (
<MenuItem
key={item.id || `separator-${index}`}
item={item}
onMenuClick={handleMenuClick}
/>
))}
</MenubarContent>
</MenubarMenu>
))}
</Menubar>
</div>
</div>
<AppMenu onMenuClick={handleMenuClick} />
<DialogManager
showAddApi={dialogState.showAddApi}
showAddCollection={dialogState.showAddCollection}
showAddScenario={dialogState.showAddScenario}
showVariableModal={dialogState.showVariableModal}
showMockRouteEdit={dialogState.showMockRouteEdit}
selectedItem={dialogState.selectedItem}
onDialogChange={handleDialogChange}
/>
{options.type === 'api' ? (
<APITester />
) : (
<LoadTestManager />
)}
<div className="flex-1 mt-14">
<CurrentScreenComponent onMenuClick={handleMenuClick} />
</div>
</div>
</LoadTestProvider>
);
};
export default App
export default App;
@@ -1,5 +1,5 @@
import React, {useState} from 'react';
import MainContentArea from '@/components/MainContentArea.jsx';
import MainContentArea from '@/components/api-tester/MainContentArea.jsx';
const APITester = () => {
// API
@@ -13,7 +13,7 @@ const APITester = () => {
return (
<div className="flex flex-col h-screen">
{/* Main content area with top padding for menubar */}
<div className="flex-1 pt-14">
<div className="flex-1">
<MainContentArea isApiMode={isApiMode} toggleMode={toggleMode}/>
</div>
</div>
@@ -7,8 +7,8 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
import ErrorAlertDialog from '@/components/shared/ErrorAlertDialog';
import {useAPI} from '@/providers/APIProvider';
const AddAPIDialog = ({open, onOpenChange, onSubmit, selectedCollection}) => {
const {collections, loadCollections} = useAPI();
const AddAPIDialog = ({open, onOpenChange, selectedCollection}) => {
const {collections, loadCollections, createAPIRequest} = useAPI();
const [formData, setFormData] = useState({
name: '',
type: '',
@@ -19,6 +19,15 @@ const AddAPIDialog = ({open, onOpenChange, onSubmit, selectedCollection}) => {
const [showErrorDialog, setShowErrorDialog] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const handleAddApi = async (collectionId, apiData) => {
try {
await createAPIRequest(collectionId, apiData);
return true;
} catch (error) {
console.error('Failed to create API:', error);
return false;
}
};
useEffect(() => {
if (selectedCollection?.id) {
@@ -56,7 +65,7 @@ const AddAPIDialog = ({open, onOpenChange, onSubmit, selectedCollection}) => {
setIsSubmitting(true);
try {
const success = await onSubmit(formData.collectionId, {
const success = await handleAddApi(formData.collectionId, {
name: formData.name.trim(),
type: formData.type
});
@@ -1,23 +1,26 @@
// /components/AddCollectionDialog.jsx
import React, {useState} from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import ErrorAlertDialog from '@/components/shared/ErrorAlertDialog.jsx';
import {useAPI} from '@/providers/APIProvider.jsx';
const AddCollectionDialog = ({ open, onOpenChange, onSubmit }) => {
const AddCollectionDialog = ({ open, onOpenChange}) => {
const [collectionName, setCollectionName] = useState('');
const [error, setError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [showErrorDialog, setShowErrorDialog] = useState(false);
const {createCollection} = useAPI();
const handleAddCollection = async (name) => {
try {
await createCollection(name);
return true;
} catch (error) {
throw new Error('컬렉션 생성에 실패했습니다');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
@@ -29,7 +32,7 @@ const AddCollectionDialog = ({ open, onOpenChange, onSubmit }) => {
setIsSubmitting(true);
try {
const success = await onSubmit(collectionName.trim());
const success = await handleAddCollection(collectionName.trim());
if (success) {
setCollectionName('');
setError('');
@@ -0,0 +1,40 @@
import React from 'react';
import { Menubar, MenubarContent, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar';
import MenuItem from './MenuItem';
import { menuConfig } from '@/config/menuConfig';
const AppMenu = ({ onMenuClick }) => {
return (
<div className="fixed top-0 left-0 right-0 z-50 bg-background border-b">
<div className="flex items-center h-14">
<div className="px-4 py-2 border-r h-full flex items-center">
<img
src="/img/logo.png"
alt="testmaster"
className="h-8 w-auto"
/>
</div>
<Menubar className="border-none flex-1">
{menuConfig.map((menu) => (
<MenubarMenu key={menu.id}>
<MenubarTrigger className="font-bold">
{menu.label}
</MenubarTrigger>
<MenubarContent>
{menu.items.map((item, index) => (
<MenuItem
key={item.id || `separator-${index}`}
item={item}
onMenuClick={onMenuClick}
/>
))}
</MenubarContent>
</MenubarMenu>
))}
</Menubar>
</div>
</div>
);
};
export default AppMenu;
@@ -0,0 +1,16 @@
import React from 'react';
import { MenubarItem, MenubarSeparator } from '@/components/ui/menubar';
const MenuItem = ({ item, onMenuClick }) => {
if (item.type === 'separator') {
return <MenubarSeparator />;
}
return (
<MenubarItem onClick={() => onMenuClick(item.action, item.screen)}>
{item.label}
</MenubarItem>
);
};
export default MenuItem;
@@ -5,7 +5,7 @@ import {Input} from '@/components/ui/input';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
import EditableInput from '../shared/EditableInput';
import PropertyTable from '../shared/PropertyTable';
import ServerManager from '@/components/ServerManager.jsx';
import ServerManager from '@/components/shared/ServerManager';
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table.jsx';
import {useAPI} from '@/providers/APIProvider';
import MonacoEditor from '@/components/shared/MonacoEditor';
@@ -1,10 +1,10 @@
import React, {useCallback, useEffect, useState} from 'react';
import {Button} from '@/components/ui/button';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
import ServerManager from '@/components/ServerManager.jsx';
import ServerManager from '@/components/shared/ServerManager';
import {useAPI} from '@/providers/APIProvider';
import MonacoEditor from '@/components/shared/MonacoEditor';
import APIClient from '@/services/APIClient.js';
import APIClient from '@/services/APIClient';
import {SidebarTrigger} from '@/components/ui/sidebar.jsx';
import {Separator} from '@/components/ui/separator.jsx';
import LayoutGrid from '@/components/layout/LayoutGrid.jsx';
@@ -233,6 +233,7 @@ export const TCPClientView = ({model, index}) => {
</Tabs>
</div>
<Separator/>
{/* Response Section */}
<div className="flex-1 min-h-0">
<div className="flex items-center gap-2 mb-2">
@@ -242,13 +243,13 @@ export const TCPClientView = ({model, index}) => {
<span className="text-sm text-gray-500">{responseData.size}</span>
</div>
<Tabs value={currentResponseTab} onValueChange={setCurrentResponseTab} className="flex flex-col h-[calc(100%-2rem)]">
<Tabs value={currentResponseTab} onValueChange={setCurrentResponseTab} className="flex flex-col h-[calc(100%-1rem)]">
<TabsList>
<TabsTrigger value="response-body">Body</TabsTrigger>
<TabsTrigger value="console">Console</TabsTrigger>
</TabsList>
<div className="flex-1 min-h-0 mt-2">
<div className="flex-1 min-h-0 mt-1">
<TabsContent value="response-body" className="h-full m-0">
<MonacoEditor
id={`response-body-${model.id}`}
@@ -0,0 +1,277 @@
import React, {useEffect, useState} 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 MockRouteRequest from '@/components/mock-route/MockRouteRequest';
import MockRouteResponses from '@/components/mock-route/MockRouteResponses';
import {useAPI} from '@/providers/APIProvider';
import {Separator} from '@/components/ui/separator.jsx';
const MockRouteEditDialog = ({open, onOpenChange, item}) => {
const { createMockRoute, updateMockRoute } = useAPI();
const [mockRoute, setMockRoute] = useState({
id: null,
method: 'POST',
pathPattern: '',
name: '',
description: '',
requestScript: '',
responses: [
{
statusCode: 200,
delay: 0,
encodingCharset: '',
defaultResponse: 'true',
conditions: [],
headers: [],
body: ''
}
]
});
// Initialize form when item changes
useEffect(() => {
if (item) {
setMockRoute(item);
} else {
// Reset to default state when creating new
setMockRoute({
id: null,
method: 'POST',
pathPattern: '',
name: '',
description: '',
requestScript: '',
responses: [
{
statusCode: 200,
delay: 0,
encodingCharset: '',
defaultResponse: 'true',
conditions: [],
headers: [],
body: ''
}
]
});
}
}, [item]);
const handleChange = (field, value) => {
setMockRoute((prev) => ({...prev, [field]: value}));
};
const handleScriptChange = (value) => {
setMockRoute((prev) => ({...prev, requestScript: value}));
};
const addResponse = () => {
setMockRoute((prev) => ({
...prev,
responses: [
...prev.responses,
{
statusCode: 200,
delay: 0,
encodingCharset: '',
defaultResponse: 'false',
conditions: [],
headers: [],
body: ''
}
]
}));
};
const handleOpenChange = (open) => {
if (!open) {
setMockRoute({
id: null,
method: 'POST',
pathPattern: '',
name: '',
description: '',
requestScript: '',
responses: [
{
statusCode: 200,
delay: 0,
encodingCharset: '',
defaultResponse: 'true',
conditions: [],
headers: [],
body: ''
}
]
});
}
onOpenChange(open);
};
const removeResponse = (index) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
if (newResponses[index].defaultResponse === 'true') {
alert('기본 응답은 삭제할 수 없습니다.');
return prev;
}
newResponses.splice(index, 1);
return {...prev, responses: newResponses};
});
};
const updateResponseField = (index, field, value) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
newResponses[index] = {...newResponses[index], [field]: value};
return {...prev, responses: newResponses};
});
};
const addCondition = (respIndex) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
newResponses[respIndex] = {
...newResponses[respIndex],
conditions: [
...newResponses[respIndex].conditions,
{type: 'query', key: '', value: ''}
]
};
return {...prev, responses: newResponses};
});
};
const removeCondition = (respIndex, condIndex) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
const newConds = [...newResponses[respIndex].conditions];
newConds.splice(condIndex, 1);
newResponses[respIndex] = {
...newResponses[respIndex],
conditions: newConds
};
return {...prev, responses: newResponses};
});
};
const updateConditionField = (respIndex, condIndex, field, value) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
const newConds = [...newResponses[respIndex].conditions];
newConds[condIndex] = {...newConds[condIndex], [field]: value};
newResponses[respIndex] = {
...newResponses[respIndex],
conditions: newConds
};
return {...prev, responses: newResponses};
});
};
const addHeader = (respIndex) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
newResponses[respIndex] = {
...newResponses[respIndex],
headers: [...newResponses[respIndex].headers, {key: '', value: ''}]
};
return {...prev, responses: newResponses};
});
};
const removeHeader = (respIndex, hdrIndex) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
const newHeaders = [...newResponses[respIndex].headers];
newHeaders.splice(hdrIndex, 1);
newResponses[respIndex] = {
...newResponses[respIndex],
headers: newHeaders
};
return {...prev, responses: newResponses};
});
};
const updateHeaderField = (respIndex, hdrIndex, field, value) => {
setMockRoute((prev) => {
const newResponses = [...prev.responses];
const newHeaders = [...newResponses[respIndex].headers];
newHeaders[hdrIndex] = {...newHeaders[hdrIndex], [field]: value};
newResponses[respIndex] = {
...newResponses[respIndex],
headers: newHeaders
};
return {...prev, responses: newResponses};
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (mockRoute.id) {
await updateMockRoute(mockRoute);
} else {
await createMockRoute(mockRoute);
}
onOpenChange(false);
} catch (error) {
console.error('Failed to save mock route:', error);
alert('Failed to save mock route');
}
};
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">
<DialogHeader>
<DialogTitle>
{mockRoute.id == null ? 'API 정보 등록' : 'API 정보 수정'}
</DialogTitle>
<DialogDescription>
API 라우트의 정보를 입력하거나 수정할 있습니다. 필수 항목을 모두 입력해주세요.
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[100vh] w-full pr-2">
<MockRouteRequest
name={mockRoute.name}
method={mockRoute.method}
pathPattern={mockRoute.pathPattern}
requestScript={mockRoute.requestScript}
onChange={handleChange}
onScriptChange={handleScriptChange}
/>
<Separator className="mb-2 mt-2"/>
<MockRouteResponses
responses={mockRoute.responses}
onAddResponse={addResponse}
onRemoveResponse={removeResponse}
onAddCondition={addCondition}
onRemoveCondition={removeCondition}
onUpdateConditionField={updateConditionField}
onUpdateResponseField={updateResponseField}
onAddHeader={addHeader}
onRemoveHeader={removeHeader}
onUpdateHeaderField={updateHeaderField}
/>
</ScrollArea>
<DialogFooter className="flex justify-end space-x-2">
<Button variant="secondary" onClick={() => handleOpenChange(false)}>
닫기
</Button>
<Button type="submit" variant="default" onClick={handleSubmit}>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default MockRouteEditDialog;
@@ -0,0 +1,132 @@
import React from 'react';
import {Button} from '@/components/ui/button';
import DataTable from '@/components/shared/DataTable';
import {useAPI} from '@/providers/APIProvider';
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card.jsx';
import {Input} from '@/components/ui/input.jsx';
const MockRouteList = ({onMenuClick}) => {
const {
mockRoutes,
loadMockRoutes,
deleteMockRoutes,
loading
} = useAPI();
const [searchTerm, setSearchTerm] = React.useState('');
const [selectedIds, setSelectedIds] = React.useState([]);
const [page, setPage] = React.useState(0);
React.useEffect(() => {
loadMockRoutes({page});
}, [page, loadMockRoutes]);
const columns = [
{key: 'method', label: 'Method'},
{key: 'pathPattern', label: 'Path'},
{key: 'name', label: '이름'}
];
const handleSearch = () => {
setPage(0);
loadMockRoutes({name: searchTerm, page: 0});
};
const handleDelete = async () => {
const success = await deleteMockRoutes(selectedIds);
if (success) {
setSelectedIds([]);
}
};
const handleEdit = (item) => {
setSelectedItem(item);
setDialogState(prev => ({...prev, showMockRouteEdit: true}));
};
const handleCreate = () => {
setSelectedItem(null);
setDialogState(prev => ({...prev, showMockRouteEdit: true}));
};
const handleDialogChange = (key, value) => {
setDialogState(prev => ({...prev, [key]: value}));
if (!value) {
setSelectedItem(null);
}
};
const renderRowActions = (row) => (
<>
<Button
variant="outline"
size="sm"
className="mr-2"
onClick={() => onMenuClick('response-edit', null, row)}
>
수정
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onMenuClick('response-clone', null, {...row, id: null})}
>
복제
</Button>
</>
);
const tableActions = (
<>
<Button variant="destructive" onClick={handleSearch}>조회</Button>
<Button variant="destructive" onClick={handleDelete} disabled={selectedIds.length === 0}>삭제</Button>
<Button onClick={() => onMenuClick('response-add')}>등록</Button>
</>
);
return (
<div className="container mx-auto p-4">
<Card>
<CardHeader>
<CardTitle>응답 API 목록</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={mockRoutes}
columns={columns}
loading={loading}
selectable
selectedIds={selectedIds}
onSelectIds={setSelectedIds}
onPageChange={setPage}
onSearch={handleSearch}
onDelete={handleDelete}
actions={tableActions}
renderRowActions={renderRowActions}
deleteDialogTitle="Mock Route 삭제"
deleteDialogDescription="선택한 Mock Route를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
/>
</CardContent>
</Card>
</div>
);
};
export default MockRouteList;
@@ -0,0 +1,86 @@
import React from 'react'
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.jsx'
const MockRouteRequest=({
name,
method,
pathPattern,
requestScript,
onChange,
onScriptChange
}) => {
return (
<div className="space-y-4 m-1">
{/* Name */}
<div>
<Label htmlFor="name" className="must">
이름
</Label>
<Input
id="name"
required
value={name}
onChange={(e) => onChange('name', e.target.value)}
/>
</div>
{/* Method & Path */}
<div className="flex gap-2">
<div className="flex-1 max-w-[120px]">
<Label htmlFor="method" className="must">
Method
</Label>
<select
id="method"
required
className="border rounded-md p-2 w-full"
value={method}
onChange={(e) => onChange('method', e.target.value)}
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="CONNECT">CONNECT</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
<option value="TRACE">TRACE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div className="flex-1">
<Label htmlFor="path" className="must">
Path
</Label>
<Input
id="path"
required
placeholder="서버에 노출되는 경로 (/부터 입력)"
value={pathPattern}
onChange={(e) => onChange('pathPattern', e.target.value)}
/>
</div>
</div>
{/* Request Script */}
<Card>
<CardHeader>
<CardTitle>요청 스크립트</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<MonacoEditor
value={requestScript}
language="javascript"
onChange={onScriptChange}
height="full"
/>
</CardContent>
</Card>
</div>
)
}
export default MockRouteRequest;
@@ -0,0 +1,225 @@
import React from 'react';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Card, CardContent, CardHeader} from '@/components/ui/card';
import {Label} from '@/components/ui/label';
import {Trash2} from 'lucide-react';
import MonacoEditor from '@/components/shared/MonacoEditor.jsx';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
const MockRouteResponse = ({
resp,
idx,
onRemoveResponse,
onAddCondition,
onRemoveCondition,
onUpdateConditionField,
onUpdateResponseField,
onAddHeader,
onRemoveHeader,
onUpdateHeaderField
}) => {
return (
<div className="w-full">
<div className="flex gap-4">
{/* Left side - Condition card */}
<div className={`${resp.defaultResponse === 'false' ? 'w-1/2' : 'hidden'}`}>
<Card className="h-full">
<CardHeader>
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">조건 항목</h3>
<Button variant="destructive" onClick={() => onRemoveResponse(idx)}>
<Trash2 className="w-4 h-4 mr-1"/>
케이스 제거
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">종류</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>
<Button
variant="secondary"
size="sm"
onClick={() => onAddCondition(idx)}
>
추가
</Button>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{resp.conditions.map((cond, cIndex) => (
<TableRow key={cIndex}>
<TableCell>
<select
className="border p-1 rounded w-full"
value={cond.type}
onChange={(e) =>
onUpdateConditionField(idx, cIndex, 'type', e.target.value)
}
>
<option value="query">Query</option>
<option value="header">Header</option>
<option value="body">Body</option>
</select>
</TableCell>
<TableCell>
<Input
value={cond.key}
onChange={(e) =>
onUpdateConditionField(idx, cIndex, 'key', e.target.value)
}
/>
</TableCell>
<TableCell>
<Input
value={cond.value}
onChange={(e) =>
onUpdateConditionField(idx, cIndex, 'value', e.target.value)
}
/>
</TableCell>
<TableCell>
<Button
variant="destructive"
size="sm"
onClick={() => onRemoveCondition(idx, cIndex)}
>
제거
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
{/* Right side - Response Content */}
<div className={`${resp.defaultResponse === 'false' ? 'w-1/2' : 'w-full'}`}>
<Card className="h-full">
<CardHeader>
<h3 className="text-lg font-semibold">응답 내용</h3>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label className="must">Status</Label>
<Input
type="number"
required
value={resp.statusCode}
onChange={(e) => onUpdateResponseField(idx, 'statusCode', e.target.value)}
/>
</div>
<div>
<Label className="must">응답 지연 시간(ms)</Label>
<Input
type="number"
required
value={resp.delay}
onChange={(e) => onUpdateResponseField(idx, 'delay', e.target.value)}
/>
</div>
<div>
<Label className="must">문자 Set</Label>
<Input
type="text"
value={resp.encodingCharset}
onChange={(e) =>
onUpdateResponseField(idx, 'encodingCharset', e.target.value)
}
/>
</div>
<div>
<Label>응답 Body</Label>
<div className="h-64">
<MonacoEditor
value={resp.body}
language="javascript"
onChange={(v) => onUpdateResponseField(idx, 'body', v || '')}
height="100%"
/>
</div>
</div>
<div>
<Label>Headers</Label>
<div className="overflow-x-auto mt-2">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[150px]">Header 이름</TableHead>
<TableHead></TableHead>
<TableHead>
<Button
variant="secondary"
size="sm"
onClick={() => onAddHeader(idx)}
>
추가
</Button>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{resp.headers.map((header, hIndex) => (
<TableRow key={hIndex}>
<TableCell>
<Input
value={header.key}
onChange={(e) =>
onUpdateHeaderField(idx, hIndex, 'key', e.target.value)
}
/>
</TableCell>
<TableCell>
<Input
value={header.value}
onChange={(e) =>
onUpdateHeaderField(idx, hIndex, 'value', e.target.value)
}
/>
</TableCell>
<TableCell>
<Button
variant="destructive"
size="sm"
onClick={() => onRemoveHeader(idx, hIndex)}
>
제거
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default MockRouteResponse
@@ -0,0 +1,62 @@
import React from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Plus } from 'lucide-react'
import { Button } from '@/components/ui/button'
import MockRouteResponse from './MockRouteResponse.jsx'
const MockRouteResponses = ({
responses,
onAddResponse,
onRemoveResponse,
onAddCondition,
onRemoveCondition,
onUpdateConditionField,
onUpdateResponseField,
onAddHeader,
onRemoveHeader,
onUpdateHeaderField
}) => {
return (
<Tabs defaultValue="0">
<TabsList className="space-x-2">
{responses.map((resp, idx) => {
const tabValue = idx.toString()
return (
<TabsTrigger key={idx} value={tabValue}>
{resp.defaultResponse === 'true'
? '기본 응답'
: `조건 ${idx + 1}`}
</TabsTrigger>
)
})}
<Button variant="secondary" type="button" onClick={onAddResponse}>
<Plus className="w-4 h-4 mr-1" />
케이스 추가
</Button>
</TabsList>
{responses.map((resp, idx) => {
const tabValue = idx.toString()
return (
<TabsContent key={idx} value={tabValue} className="mt-2">
{/* Render a single response with all its fields and actions */}
<MockRouteResponse
resp={resp}
idx={idx}
onRemoveResponse={onRemoveResponse}
onAddCondition={onAddCondition}
onRemoveCondition={onRemoveCondition}
onUpdateConditionField={onUpdateConditionField}
onUpdateResponseField={onUpdateResponseField}
onAddHeader={onAddHeader}
onRemoveHeader={onRemoveHeader}
onUpdateHeaderField={onUpdateHeaderField}
/>
</TabsContent>
)
})}
</Tabs>
)
}
export default MockRouteResponses
@@ -0,0 +1,195 @@
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';
const DataTable = ({
// Data and columns
data,
columns,
// Selection
selectable = false,
selectedIds = [],
onSelectIds,
renderRowActions,
// Pagination
onPageChange,
// Loading state
loading = false,
// Delete dialog
deleteDialogTitle = 'Delete Items',
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) => {
if (!onSelectIds) return;
if (checked) {
onSelectIds(data.content.map(item => item.id));
} else {
onSelectIds([]);
}
};
const handleSelectItem = (id, checked) => {
if (!onSelectIds) return;
if (checked) {
onSelectIds([...selectedIds, id]);
} else {
onSelectIds(selectedIds.filter(selectedId => selectedId !== id));
}
};
const handlePageChange = (page) => {
// Reset selections when changing pages
if (onSelectIds) {
onSelectIds([]);
}
onPageChange(page);
};
if (loading) {
return (
<Card>
<CardContent className="flex justify-center items-center h-48">
Loading...
</CardContent>
</Card>
);
}
return (
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{selectable && (
<TableHead className="w-[50px]">
<Checkbox
checked={selectedIds.length === data.content.length && data.content.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
)}
{columns.map((column) => (
<TableHead key={column.key}>{column.label}</TableHead>
))}
{renderRowActions && <TableHead className="text-right">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>
))}
</TableBody>
</Table>
</div>
{onPageChange && (
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-500">
Showing {data.numberOfElements} of {data.totalElements} results
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => handlePageChange(0)}
disabled={data.first}
size="sm"
>
{'<<'}
</Button>
<Button
variant="outline"
onClick={() => handlePageChange(data.number - 1)}
disabled={data.first}
size="sm"
>
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: data.totalPages }, (_, i) => (
<Button
key={i}
variant={i === data.number ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(i)}
className="w-8"
>
{i + 1}
</Button>
))}
</div>
<Button
variant="outline"
onClick={() => handlePageChange(data.number + 1)}
disabled={data.last}
size="sm"
>
Next
</Button>
<Button
variant="outline"
onClick={() => handlePageChange(data.totalPages - 1)}
disabled={data.last}
size="sm"
>
{'>>'}
</Button>
</div>
</div>
)}
{onDelete && (
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{deleteDialogTitle}</AlertDialogTitle>
<AlertDialogDescription>
{deleteDialogDescription}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => {
onDelete?.();
setShowDeleteDialog(false);
}}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
);
};
export default DataTable;
@@ -3,69 +3,40 @@ import React from 'react';
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 {useLoadTest} from '@/providers/LoadTestProvider.jsx';
import {useAPI} from '@/providers/APIProvider.jsx';
import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx';
const DialogManager = ({
showAddApi,
showAddCollection,
showAddScenario,
showVariableModal,
showMockRouteEdit,
selectedItem,
onDialogChange
}) => {
const {createCollection, createAPIRequest} = useAPI();
const {createScenario} = useLoadTest();
const handleAddCollection = async (name) => {
try {
await createCollection(name);
return true;
} catch (error) {
throw new Error('컬렉션 생성에 실패했습니다');
}
};
const handleAddApi = async (collectionId, apiData) => {
try {
await createAPIRequest(collectionId, apiData);
return true;
} catch (error) {
console.error('Failed to create API:', error);
return false;
}
};
const handleAddScenario = async (name) => {
try {
await createScenario(name);
return true;
} catch (error) {
console.error('시나리오 생성에 실패했습니다.');
}
}
return <>
<AddCollectionDialog
open={showAddCollection}
onOpenChange={(isOpen) => onDialogChange('showAddCollection', isOpen)}
onSubmit={handleAddCollection}
/>
{/* Add these at the bottom of your return statement */}
<AddAPIDialog
open={showAddApi}
onOpenChange={(isOpen) => onDialogChange('showAddApi', isOpen)}
onSubmit={handleAddApi}
/>
<AddScenarioDialog
open={showAddScenario}
onOpenChange={(isOpen) => onDialogChange('showAddScenario', isOpen)}
onSubmit={handleAddScenario}
/>
<VariableModal
open={showVariableModal}
onOpenChange={(isOpen) => onDialogChange('showVariableModal', isOpen)}
/>
<MockRouteEditDialog
open={showMockRouteEdit}
onOpenChange={(isOpen) => onDialogChange('showMockRouteEdit', isOpen)}
item={selectedItem}
/>
</>;
}
@@ -1,5 +1,17 @@
import React, { useEffect, useRef, memo } from 'react';
import * as monaco from 'monaco-editor';
import {loader} from '@monaco-editor/react';
loader.config({
paths: {
vs: '/monaco-editor/min/vs'
},
'vs/nls': {
availableLanguages: {},
},
// Specify which languages to load
languages: ['javascript', 'json', 'xml','plaintext']
});
const MonacoEditor = memo(({
id,
@@ -10,10 +22,12 @@ const MonacoEditor = memo(({
onChange = () => {}
}) => {
const editorRef = useRef(null);
const modelRef = useRef(null);
const containerRef = useRef(null);
const subscriptionRef = useRef(null);
// Ensure value is always a string
const normalizedValue = value ?? '';
const getEditorOptions = (lang) => {
const baseOptions = {
automaticLayout: true,
@@ -57,63 +71,91 @@ const MonacoEditor = memo(({
useEffect(() => {
if (!containerRef.current) return;
const modelUri = monaco.Uri.parse(`inmemory://model-${id}`);
modelRef.current = monaco.editor.createModel(value, language, modelUri);
const options = getEditorOptions(language);
editorRef.current = monaco.editor.create(containerRef.current, {
const editor = monaco.editor.create(containerRef.current, {
...options,
model: modelRef.current
value: normalizedValue,
language: language
});
subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => {
onChange(editorRef.current.getValue());
editorRef.current = editor;
const subscription = editor.onDidChangeModelContent(() => {
const newValue = editor.getValue();
onChange(newValue);
});
if (language === 'json' && value) {
subscriptionRef.current = subscription;
if (language === 'json' && normalizedValue) {
try {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
allowComments: false,
schemas: []
});
const action = editor.getAction('editor.action.formatDocument');
if (action) {
setTimeout(() => {
const action = editorRef.current?.getAction('editor.action.formatDocument');
if (action) action.run();
action.run();
}, 100);
}
} catch (e) {
console.warn('Failed to format JSON:', e);
}
}
const resizeObserver = new ResizeObserver(() => {
editorRef.current?.layout();
editor.layout();
});
resizeObserver.observe(containerRef.current);
return () => {
resizeObserver.disconnect();
subscriptionRef.current?.dispose();
editorRef.current?.dispose();
if (modelRef.current && !modelRef.current.isDisposed()) {
modelRef.current.dispose();
subscription.dispose();
// Ensure proper cleanup
if (editor) {
try {
const model = editor.getModel();
if (model) {
model.dispose();
}
editor.dispose();
} catch (e) {
console.warn('Editor disposal error:', e);
}
}
};
}, [language]);
}, [language]); // Only recreate when language changes
// Handle value updates
useEffect(() => {
if (modelRef.current && !modelRef.current.isDisposed()) {
modelRef.current.setValue(value);
if (editorRef.current) {
const currentValue = editorRef.current.getValue();
if (currentValue !== normalizedValue) {
editorRef.current.setValue(normalizedValue);
}
}, [value]);
}
}, [normalizedValue]);
// Convert height to pixel value if it's a number
const containerHeight = typeof height === 'number'
? `${height}px`
: height.endsWith('px') ? height : `${height}px`;
return (
<div
id={id}
ref={containerRef}
className={`h-${height} border mt-2`}
style={{ minHeight: '100px' }}
style={{
minHeight: '100px',
height: containerHeight,
border: '1px solid #e2e8f0',
marginTop: '0.5rem'
}}
/>
);
});
+64
View File
@@ -0,0 +1,64 @@
// config/menuConfig.js
export const menuConfig = [
{
id: 'response',
label: '응답관리',
items: [
{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: 'request',
label: '요청 관리',
items: [
{id: 'api-tester', label: 'API 테스터', action: 'api-tester', screen: 'api-tester'},
{type: 'separator'},
{id: 'collection-add', label: '컬렉션 추가', action: 'collection-add'},
{id: 'request-add', label: '요청 추가', action: 'request-add'},
{id: 'scenario-add', label: '시나리오 추가', action: 'scenario-add'},
{type: 'separator'},
{id: 'variables-show', label: '변수 보기', action: 'variables-show'}
]
},
{
id: 'queue',
label: '큐 관리',
items: [
{id: 'queue-list', label: '큐 목록', action: 'queue-list', screen: 'queue-list'},
{id: 'queue-add', label: '큐 추가', action: 'queue-add', screen: 'queue-add'}
]
},
{
id: 'loadtest',
label: '부하 테스트',
items: [
{id: 'load-test', label: '새 테스트', action: 'load-test', screen: 'load-test'},
{type: 'separator'},
{id: 'load-test-history', label: '테스트 이력', action: 'load-test-history', screen: 'load-test-history'},
{id: 'load-test-reports', label: '결과 리포트', action: 'load-test-reports', screen: 'load-test-reports'}
]
},
{
id: 'system',
label: '시스템 관리',
items: [
{id: 'user-management', label: '사용자 관리', action: 'user-management', screen: 'user-management'},
{type: 'separator'},
{id: 'server-management', label: '서버 관리', action: 'server-management', screen: 'server-management'},
{id: 'server-add', label: '서버 추가', action: 'server-add', screen: 'server-add'},
]
},
{
id: 'account',
label: '계정',
items: [
{id: 'change-password', label: '비밀번호 변경', action: 'change-password'},
{type: 'separator'},
{id: 'logout', label: '로그 아웃', action: 'logout'}
]
}
];
+10
View File
@@ -0,0 +1,10 @@
// config/screenConfig.js
import APITester from '../components/api-tester/APITester';
import LoadTestManager from '../components/load-tester/LoadTestManager.jsx';
import MockRouteList from '../components/mock-route/MockRouteList.jsx';
export const SCREENS = {
'api-tester': APITester,
'load-test': LoadTestManager,
'response-list': MockRouteList,
};
+2 -1
View File
@@ -44,7 +44,7 @@ h1 {
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
/*padding: 0.6em 1.2em;*/
font-size: 1em;
font-weight: 500;
font-family: inherit;
@@ -52,6 +52,7 @@ button {
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
+74 -1
View File
@@ -26,6 +26,13 @@ export const APIProvider = ({
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [mockRoutes, setMockRoutes] = useState({
content: [],
totalPages: 0,
number: 0,
first: true,
last: true
});
// Initialize services
const collections$ = collectionService(contextPath);
@@ -224,6 +231,67 @@ export const APIProvider = ({
}
}, [apiClient]);
const loadMockRoutes = useCallback(async (params = { name: '', page: 0 }) => {
setLoading(true);
setError(null);
try {
const data = await apiRequests$.getMockRoutes(params);
setMockRoutes(data);
return data;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, []);
const deleteMockRoutes = useCallback(async (ids) => {
setLoading(true);
setError(null);
try {
await apiRequests$.deleteMockRoutes(ids);
await loadMockRoutes(); // Reload the list after deletion
return true;
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
}, [loadMockRoutes]);
const createMockRoute = useCallback(async (mockRoute) => {
setLoading(true);
setError(null);
try {
const newMockRoute = await apiRequests$.createMockRoute(mockRoute);
await loadMockRoutes();
return newMockRoute;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, [loadMockRoutes]);
const updateMockRoute = useCallback(async (mockRoute) => {
setLoading(true);
setError(null);
try {
const updatedMockRoute = await apiRequests$.updateMockRoute(mockRoute);
await loadMockRoutes();
return updatedMockRoute;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, [loadMockRoutes]);
const contextValue = {
apiClient,
collections,
@@ -243,7 +311,12 @@ export const APIProvider = ({
sendAPIRequest,
isAuthenticated,
setIsAuthenticated,
handleLogout
handleLogout,
mockRoutes,
loadMockRoutes,
deleteMockRoutes,
createMockRoute,
updateMockRoute
};
return (
@@ -52,6 +52,53 @@ export const apiRequestService = (contextPath = '/') => {
if (!response.ok) throw new Error('Failed to delete API request');
return true;
},
async getMockRoutes(params = { name: '', page: 0 }) {
const searchParams = new URLSearchParams();
if (params.name) searchParams.append('name', params.name);
if (typeof params.page !== 'undefined') searchParams.append('page', params.page.toString());
const queryString = searchParams.toString();
const url = `${contextPath}api/mock-routes${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, { headers });
if (!response.ok) throw new Error('Failed to fetch mock routes');
return response.json();
},
async deleteMockRoutes(ids) {
const response = await fetch(`${contextPath}api/mock-routes/delete`, {
method: 'POST',
headers,
body: JSON.stringify(ids)
});
if (!response.ok) throw new Error('Failed to delete mock routes');
return true;
},
async createMockRoute(mockRoute) {
const response = await fetch(`${contextPath}api/mock-routes`, {
method: 'POST',
headers,
body: JSON.stringify(mockRoute)
});
if (!response.ok) throw new Error('Failed to create mock route');
return response.json();
},
async updateMockRoute(mockRoute) {
const response = await fetch(`${contextPath}api/mock-routes/update/${mockRoute.id}`, {
method: 'POST',
headers,
body: JSON.stringify(mockRoute)
});
if (!response.ok) throw new Error('Failed to update mock route');
return response.json();
}
};
};
+20 -22
View File
@@ -1,63 +1,61 @@
import path from "path"
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import monacoEditorPlugin from './vite.monaco-editor';
import path from 'path';
import react from '@vitejs/plugin-react';
import {defineConfig} from 'vite';
export default defineConfig({
define: {
global: {},
global: {}
},
plugins: [react(),monacoEditorPlugin()],
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
'@': path.resolve(__dirname, './src')
}
},
server: {
proxy: {
'/mgmt': {
target: 'http://localhost:38080',
changeOrigin: true,
secure: false,
secure: false
},
'/api': {
target: 'http://localhost:38080',
changeOrigin: true,
secure: false,
secure: false
},
'/actionLogin.do': {
target: 'http://localhost:38080',
changeOrigin: true,
secure: false,
secure: false
},
'/actionLogout.do': {
target: 'http://localhost:38080',
changeOrigin: true,
secure: false,
secure: false
},
'/img': {
target: 'http://localhost:38080',
changeOrigin: true,
secure: false,
secure: false
},
'/servers': {
target: 'http://localhost:38080',
changeOrigin: true,
secure: false,
secure: false
}
}
},
optimizeDeps: {
include: ['@monaco-editor/react'],
include: ['@monaco-editor/react']
},
build: {
rollupOptions: {
output: {
manualChunks: {
// Split Monaco Editor into a separate chunk
'monaco-editor': ['monaco-editor'],
},
},
},
},
})
'monaco-editor': ['monaco-editor/esm/vs/editor/editor.api']
}
}
}
}
});
-20
View File
@@ -1,20 +0,0 @@
// vite.monaco-editor.js
export default function monacoEditorPlugin() {
return {
name: 'monaco-editor-plugin',
config(config) {
config.define = {
...config.define,
'process.platform': JSON.stringify('darwin'),
'process.env': {},
};
},
configureServer(server) {
return () => {
server.middlewares.use('/_monaco', (req, res) => {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
});
};
}
};
}