코드 정리
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
+8
-37
@@ -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'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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'}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user