layout grid

This commit is contained in:
현성필
2025-01-27 08:59:12 +09:00
parent 97648bab40
commit ef11d6eaa4
11 changed files with 604 additions and 797 deletions
+8 -62
View File
@@ -20,15 +20,12 @@ const App = ({ options = { type: 'api', contextPath: '/' } }) => {
const AppContent = ({ options, showLogin, setShowLogin }) => {
const { isAuthenticated, setIsAuthenticated } = useAPI();
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const checkAuth = async () => {
try {
const response = await fetch('/api/account', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const response = await fetch('/api/account', {});
if (response.status !== 200) {
setShowLogin(true);
@@ -48,8 +45,12 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
}
};
useEffect(() => {
if (!isInitialized) {
setIsInitialized(true);
checkAuth();
}, [setIsAuthenticated]);
}
}, [isInitialized]);
if (!isAuthenticated) {
return <LoginDialog open={showLogin} onOpenChange={setShowLogin} />;
@@ -68,59 +69,4 @@ const AppContent = ({ options, showLogin, setShowLogin }) => {
);
};
// const App = ({ options = { type: 'api', contextPath: '/' } }) => {
//
// const [isAuthenticated, setIsAuthenticated] = useState(false);
// const [showLogin, setShowLogin] = useState(false);
//
// useEffect(() => {
// // Check authentication status
// const checkAuth = async () => {
// try {
// const response = await fetch('/api/accouont', {
// headers: {
// 'X-Requested-With': 'XMLHttpRequest'
// }
// });
//
// if (response.status !== 200) {
// setShowLogin(true);
// return;
// }
//
// const data = await response.json();
// console.log(data);
// setIsAuthenticated(data.authorities);
// if (!data.authorities) {
// setShowLogin(true);
// }
// } catch (error) {
// console.error('Auth check failed:', error);
// setShowLogin(true);
// }
// };
//
// checkAuth();
// }, []);
//
// if (!isAuthenticated) {
// return <LoginDialog open={showLogin} onOpenChange={setShowLogin} />;
// }
//
// return (
// <APIProvider contextPath={options.contextPath}>
// <LoadTestProvider contextPath={options.contextPath}>
// <div className="flex flex-col h-screen bg-background">
// {options.type === 'api' ? (
// <APITester />
// ) : (
// <LoadTestManager />
// )}
// </div>
// </LoadTestProvider>
// </APIProvider>
// );
// };
export default App
+4 -17
View File
@@ -10,26 +10,13 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Label } from '@/components/ui/label';
import {getCSRFToken} from '@/services/token.js';
const LoginDialog = ({ open, onOpenChange }) => {
const [formData, setFormData] = useState({ id: '', password: '' });
const [message, setMessage] = useState({ type: '', content: '' });
const [isLoading, setIsLoading] = useState(false);
const getCsrfToken = () => {
const name = 'XSRF-TOKEN=';
const decodedCookie = decodeURIComponent(document.cookie);
const cookieArray = decodedCookie.split(';');
for (let cookie of cookieArray) {
cookie = cookie.trim();
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length, cookie.length);
}
}
return null;
};
const showMessage = (type, content) => {
setMessage({ type, content });
};
@@ -45,7 +32,7 @@ const LoginDialog = ({ open, onOpenChange }) => {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'X-XSRF-TOKEN': getCsrfToken()
'X-XSRF-TOKEN': getCSRFToken()
},
body: new URLSearchParams({
id: formData.id,
@@ -54,7 +41,7 @@ const LoginDialog = ({ open, onOpenChange }) => {
});
const data = await response.json();
console.log(data);
switch(data.status) {
case 'SUCCESS':
window.location.href = data.redirectUrl;
@@ -84,7 +71,7 @@ const LoginDialog = ({ open, onOpenChange }) => {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>로그인</DialogTitle>
<DialogTitle>TestMaster 로그인</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
@@ -11,6 +11,19 @@ const MainContentArea = ({isApiMode, toggleMode}) => {
const [apiRequests, setApiRequests] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'm' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleMode();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleMode]);
return (
<div className="h-[calc(100vh-57px)] w-full overflow-hidden">
{isApiMode ? (
@@ -93,7 +106,8 @@ const APIMode = ({
collectionId: collectionId,
collectionName: collection.name,
server: api.server || null,
description: api.description || ''
description: api.description || '',
layout: api.layout || {},
};
const existingIndex = apiRequests.findIndex(
@@ -113,12 +127,12 @@ const APIMode = ({
const renderFallback = () => (
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
<p>No APIs opened. Select an API to test from the sidebar.</p>
<p>왼쪽에서 API를 선택해 주세요</p>
</div>
);
return (
<SidebarProvider>
<SidebarProvider cookieName="sidebar-right:state" shortcut="/">
<div className="api-mode flex flex-1 h-[calc(100vh-57px)]">
<div className="navigation-sidebar min-w-[0px] border-r bg-gray-100">
<APINavigation
@@ -159,8 +173,8 @@ const ScenarioMode = ({
let handleApiSelect = () => {
};
return (
<SidebarProvider>
<div className="scenario-mode flex flex-1 h-[calc(100vh-57px)]">
<SidebarProvider cookieName="sidebar-left:state" shortcut=".">
<div className="navigation-sidebar min-w-[0px] border-r bg-gray-100">
<ScenarioNavigation
contextPath="/"
@@ -172,7 +186,9 @@ const ScenarioMode = ({
<div className="editor flex-1 h-[calc(100vh-57px)] overflow-hidden">
<ScenarioEditor/>
</div>
</SidebarProvider>
<div className="navigation-sidebar min-w-[0px] border-l bg-gray-100">
<SidebarProvider cookieName="sidebar-right:state" shortcut="/">
<APINavigation
contextPath="/"
side="right"
@@ -180,9 +196,9 @@ const ScenarioMode = ({
isApiMode={isApiMode}
onToggleMode={onToggleMode}
/>
</div>
</div>
</SidebarProvider>
</div>
</div>
);
};
@@ -239,7 +239,7 @@ const APINavigation = ({
<SidebarContent>
<ScrollArea className="h-[calc(100vh-64px)]">
<SidebarGroup>
<SidebarGroupLabel>Collections</SidebarGroupLabel>
<SidebarGroupLabel>Collections (Ctrl + /)</SidebarGroupLabel>
<SidebarMenu>
{collections.map(renderCollectionItem)}
</SidebarMenu>
@@ -102,7 +102,7 @@ const ScenarioEditor = () => {
}, []);
if (!currentFlowData) {
return <div className="flex items-center justify-center h-full">Select a scenario to edit</div>;
return <div className="flex items-center justify-center h-full">시나리오를 선택하세요 (Ctrl + .)</div>;
}
return (
@@ -156,7 +156,7 @@ const ScenarioNavigation = ({
<SidebarContent>
<ScrollArea className="h-[calc(100%-64px)]">
<SidebarGroup>
<SidebarGroupLabel>Scenarios</SidebarGroupLabel>
<SidebarGroupLabel>Scenarios (Ctrl + .)</SidebarGroupLabel>
<SidebarMenu>
{scenarios.map(renderScenarioItem)}
</SidebarMenu>
@@ -1,457 +1,279 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import * as monaco from 'monaco-editor';
import React, {useCallback, useEffect, useState} from 'react';
import {Button} from '@/components/ui/button';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
import {Card, CardContent} from '@/components/ui/card';
import LayoutGrid from '../layout/LayoutGrid';
import LayoutTreeDataGrid from '../layout/LayoutTreeDataGrid';
import ApiLayoutItemTreeBuilder from '../../lib/ApiLayoutItemTreeBuilder';
import ApiLayoutItemTreeDataBuilder from '../../lib/ApiLayoutItemTreeDataBuilder';
import VariableSnippet from '../shared/VariableSnippet';
import ServerManager from '@/components/ServerManager.jsx';
import {useAPI} from '@/providers/APIProvider';
import MonacoEditor from '@/components/shared/MonacoEditor';
import APIClient from '@/services/APIClient.js';
import {SidebarTrigger} from '@/components/ui/sidebar.jsx';
import {Separator} from '@/components/ui/separator.jsx';
import LayoutGrid from '@/components/layout/LayoutGrid.jsx';
import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js';
import ApiLayoutItemTreeDataBuilder from '@/lib/ApiLayoutItemTreeDataBuilder.js';
export const TCPClientView = ({ parent, serverManager, model, index }) => {
const [apiClient] = useState(() => new APIClient());
export const TCPClientView = ({model, index}) => {
const apiClient = new APIClient();
const {getServer, updateAPIRequest} = useAPI();
const [isLoading, setIsLoading] = useState(false);
const [editors, setEditors] = useState({
layoutJson: null,
requestBody: null,
responseBody: null,
console: null,
preRequest: null,
postRequest: null
});
const [layoutData, setLayoutData] = useState(null);
const [currentModel, setCurrentModel] = useState(model);
const [currentTab, setCurrentTab] = useState('body');
const [currentResponseTab, setCurrentResponseTab] = useState('response-body');
// Refs
const layoutJsonRef = useRef(null);
const requestBodyRef = useRef(null);
const responseBodyRef = useRef(null);
const consoleRef = useRef(null);
const preRequestRef = useRef(null);
const postRequestRef = useRef(null);
const totalMessageLengthRef = useRef(null);
const layoutGridRef = useRef(null);
useEffect(() => {
setCurrentModel(prev => ({
...prev,
requestBody: model.requestBody || '',
layout: model.layout || {},
contentType: 'plaintext'
}));
}, [model]);
const handleResponseDisplay = useCallback((response) => {
try {
editors.responseBody?.setValue(response.body || '');
if (response.time) {
const responseTimeEl = document.querySelector('.response_time');
if (responseTimeEl) {
responseTimeEl.textContent = `time: ${response.time}ms`;
}
}
if (response.size) {
const responseSizeEl = document.querySelector('.response_size');
if (responseSizeEl) {
responseSizeEl.textContent = `size: ${response.size}bytes`;
}
}
} catch (error) {
console.error('Error displaying response:', error);
}
}, [editors.responseBody]);
const handleLoadLayout = useCallback(() => {
try {
const newLayout = JSON.parse(editors.layoutJson?.getValue() || '{}');
model.layout = newLayout;
if (newLayout.layoutItems) {
newLayout.layoutItems.forEach((item) => {
if (item.value === undefined) {
item.value = '';
}
});
}
const layoutRoot = ApiLayoutItemTreeBuilder.createTree(newLayout.layoutItems);
const [layoutRoot, setLayoutRoot] = useState(() => {
const root = ApiLayoutItemTreeBuilder.createTree(model.layout.layoutItems);
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
const layoutDataRoot = replicator.createReplicatedTree(layoutRoot);
return replicator.createReplicatedTree(root);
});
setLayoutData(layoutDataRoot);
const [responseData, setResponseData] = useState({
headers: [],
status: '',
time: '',
size: ''
});
if (layoutGridRef.current) {
layoutGridRef.current.rebuild(newLayout);
const [editorContents, setEditorContents] = useState({
requestBody: currentModel.requestBody || '',
responseBody: '',
consoleLog: '',
layout: JSON.stringify(currentModel.layout, null, 2) || '',
preRequestScript: currentModel.preRequestScript || '',
postRequestScript: currentModel.postRequestScript || ''
});
useEffect(() => {
if (currentModel.server) {
const selectedServer = getServer(currentModel.server);
}
} catch (error) {
console.error('Error loading layout:', error);
alert(error.message);
}
}, [model, editors.layoutJson]);
}, [currentModel.server]);
const handleSendRequest = useCallback(async () => {
if (!model.server) {
alert('서버를 등록해주세요.');
const handleServerChange = (serverId) => {
setCurrentModel(prev => ({
...prev,
server: serverId
}));
};
const handleSendRequest = async () => {
if (isLoading) {
return;
}
setIsLoading(true);
try {
editors.responseBody?.setValue('');
editors.console?.setValue('');
setEditorContents(prev => ({...prev, consoleLog: ''}));
const startTime = Date.now();
try {
const response = await apiClient.sendAPIRequest(
model,
null,
(message) => {
const consoleEditor = editors.console;
if (consoleEditor) {
const model = consoleEditor.getModel();
if (model) {
const lastLine = model.getLineCount();
const range = new monaco.Range(lastLine, 1, lastLine, 1);
const text = typeof message === 'object' ? JSON.stringify(message, null, 2) : message;
consoleEditor.executeEdits('console', [{
identifier: { major: 1, minor: 1 },
range,
text: text + '\n',
forceMoveMarkers: true
}]);
}
}
}
currentModel,
(log) => setEditorContents(prev => ({
...prev,
consoleLog: prev.consoleLog + log + '\n'
}))
);
if (response) {
const endTime = Date.now();
response.time = endTime - startTime;
handleResponseDisplay(response);
}
setResponseData({
time: `${response.time}ms`,
size: `Size: ${response.size}`
});
setEditorContents(prev => ({
...prev,
responseBody: response.body
}));
} catch (error) {
console.error('Error sending request:', error);
alert(error.message);
console.error(error);
setEditorContents(prev => ({
...prev,
consoleLog: prev.consoleLog + `Error: ${error.message}\n`
}));
} finally {
setIsLoading(false);
}
}, [model, editors, apiClient, handleResponseDisplay]);
};
const handleResize = useCallback(() => {
Object.values(editors).forEach(editor => {
if (editor) {
editor.layout({ width: 0 });
editor.layout();
}
});
}, [editors]);
// Layout update handler
const handleLayoutUpdate = useCallback(async (finalMessage, layoutDataTree) => {
const prefix = (model.server && model.server.serverOptions)
? await serverManager.renderServerPrefix(model.server.serverOptions, model)
: '';
editors.requestBody?.setValue(prefix + finalMessage);
model.layout.computedLayoutItemRoot = layoutDataTree;
}, [model, editors.requestBody, serverManager]);
// Initialize Monaco editors
useEffect(() => {
if (layoutJsonRef.current && !editors.layoutJson) {
const layoutJson = JSON.stringify(model.layout, null, 2);
const layoutJsonEditor = monaco.editor.create(layoutJsonRef.current, {
value: layoutJson,
language: 'json',
automaticLayout: true,
quickSuggestions: false
});
layoutJsonEditor.onDidChangeModelContent(() => {
const handleSave = async () => {
try {
const content = layoutJsonEditor.getValue();
JSON.parse(content); // Validate JSON
} catch (e) {
// Invalid JSON - ignore
const updatedModel = {
...currentModel,
preRequestScript: editorContents.preRequestScript,
postRequestScript: editorContents.postRequestScript,
layout: JSON.parse(editorContents.layout),
requestBody: currentModel.requestBody
};
await updateAPIRequest(updatedModel);
} catch (error) {
console.error('Failed to save API:', error);
}
});
};
setEditors(prev => ({ ...prev, layoutJson: layoutJsonEditor }));
}
if (requestBodyRef.current && !editors.requestBody) {
const requestBodyEditor = monaco.editor.create(requestBodyRef.current, {
value: model.requestBody || '',
language: 'text',
automaticLayout: true,
quickSuggestions: false
});
requestBodyEditor.onDidChangeModelContent(() => {
model.requestBody = requestBodyEditor.getValue();
});
requestBodyEditor.onMouseMove((e) => {
const position = e.target.position;
if (position) {
const editorModel = requestBodyEditor.getModel();
if (!editorModel) return;
const word = editorModel.getWordAtPosition(position);
const lineContent = editorModel.getLineContent(position.lineNumber);
if (word && lineContent.includes(`{{${word.word}}}`)) {
parent?.showPopper?.(e.target.element, word.word);
} else {
parent?.hidePopper?.(e.target.element);
}
}
});
setEditors(prev => ({ ...prev, requestBody: requestBodyEditor }));
}
if (responseBodyRef.current && !editors.responseBody) {
const responseBodyEditor = monaco.editor.create(responseBodyRef.current, {
value: '',
language: 'text',
automaticLayout: true,
quickSuggestions: false,
readOnly: true
});
setEditors(prev => ({ ...prev, responseBody: responseBodyEditor }));
}
if (consoleRef.current && !editors.console) {
const consoleEditor = monaco.editor.create(consoleRef.current, {
value: '',
automaticLayout: true,
quickSuggestions: false,
readOnly: true
});
setEditors(prev => ({ ...prev, console: consoleEditor }));
}
if (preRequestRef.current && !editors.preRequest) {
const preRequestEditor = monaco.editor.create(preRequestRef.current, {
value: model.preRequestScript || '',
language: 'javascript',
automaticLayout: true,
quickSuggestions: false
});
preRequestEditor.onDidChangeModelContent(() => {
model.preRequestScript = preRequestEditor.getValue();
});
setEditors(prev => ({ ...prev, preRequest: preRequestEditor }));
}
if (postRequestRef.current && !editors.postRequest) {
const postRequestEditor = monaco.editor.create(postRequestRef.current, {
value: model.postRequestScript || '',
language: 'javascript',
automaticLayout: true,
quickSuggestions: false
});
postRequestEditor.onDidChangeModelContent(() => {
model.postRequestScript = postRequestEditor.getValue();
});
setEditors(prev => ({ ...prev, postRequest: postRequestEditor }));
}
// Initialize layout data
const layoutRoot = ApiLayoutItemTreeBuilder.createTree(model.layout.layoutItems);
const handleLayoutUpdate = useCallback((updatedLayout) => {
const updatedRoot = ApiLayoutItemTreeBuilder.createTree(updatedLayout.layoutItems);
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
const layoutDataRoot = replicator.createReplicatedTree(layoutRoot);
setLayoutData(layoutDataRoot);
return () => {
Object.values(editors).forEach(editor => editor?.dispose());
};
}, []);
// Handle window resize
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
// Tab change handlers
useEffect(() => {
const handleTabChange = (tab) => {
const currentEditor = {
'requestBodyTab': editors.requestBody,
'preRequestTab': editors.preRequest,
'postRequestTab': editors.postRequest
}[tab];
if (currentEditor) {
currentEditor.layout();
}
};
const tabs = ['requestBodyTab', 'preRequestTab', 'postRequestTab'];
tabs.forEach(tab => {
const element = document.querySelector(`#api_request_${model.id} .btn_${tab}`);
if (element) {
const handler = () => handleTabChange(tab);
element.addEventListener('shown.bs.tab', handler);
return () => element.removeEventListener('shown.bs.tab', handler);
}
});
}, [editors, model.id]);
// Server options effect
useEffect(() => {
if (!model.server) return;
if (model.server.headers === undefined) {
model.server.headers = [];
}
if (model.server.serverOptions) {
serverManager.renderTcpServerOptions(
'.server_options',
model.server.serverOptions,
model.headers,
parent
);
}
}, [model.server, model.headers, parent, serverManager]);
const newRoot = replicator.createReplicatedTree(updatedRoot);
setLayoutRoot(newRoot);
currentModel.layout = updatedLayout;
}, [currentModel]);
return (
<div className="flex flex-col h-full p-4">
{/* Header Section */}
<div className="flex items-center space-x-2 mb-4">
<span className="text-sm text-muted-foreground">
{model.collectionName} &gt; {model.name}
</span>
<div className="flex flex-col h-full p-4 gap-4">
{/* Request Header Section */}
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<div>
<span>{currentModel.collectionName}</span>
{' > '}
<span>{currentModel.name}</span>
</div>
</div>
{/* Server Selection */}
<div className="flex items-center space-x-2 mb-4">
<div className="form-floating">
{serverManager.renderTcpServers(model.server)}
<label className="text-sm text-muted-foreground">Server</label>
{/* Request Controls */}
<div className="flex items-center gap-2">
<div className="shrink-0">
<ServerManager
onServerChange={handleServerChange}
defaultValue={currentModel.server}
/>
</div>
<Button
onClick={handleSendRequest}
disabled={isLoading}
>
<div className="flex gap-2 shrink-0">
<Button disabled={isLoading} onClick={handleSendRequest}>
{isLoading ? 'Sending...' : 'Send'}
</Button>
<Button
onClick={() => parent?.handleSaveRequest?.(model)}
disabled={isLoading}
>
Save
</Button>
<Button variant="secondary" onClick={handleSave}>Save</Button>
</div>
</div>
{/* Server Options */}
<div className="server_options mb-4" />
{/* Main Content */}
<Tabs defaultValue="layout" className="flex-1">
{/* Request/Response Container */}
<div className="flex flex-col gap-4 flex-1 min-h-0">
{/* Request Section */}
<div className="flex-1 min-h-0">
<Tabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-col h-full">
<TabsList>
<TabsTrigger value="layout">Layout</TabsTrigger>
<TabsTrigger value="layoutJson">Layout JSON</TabsTrigger>
<TabsTrigger value="layoutTree">Layout Tree</TabsTrigger>
<TabsTrigger value="layoutData">Layout Data</TabsTrigger>
<TabsTrigger value="preRequest">Pre-Request</TabsTrigger>
<TabsTrigger value="postRequest">Post-Request</TabsTrigger>
<TabsTrigger value="body">Layout</TabsTrigger>
<TabsTrigger value="layout-json">Layout JSON</TabsTrigger>
<TabsTrigger value="pre-request">Pre-Request</TabsTrigger>
<TabsTrigger value="post-request">Post-Request</TabsTrigger>
</TabsList>
<TabsContent value="layout">
<div ref={layoutGridRef}>
<div className="flex-1 min-h-0 mt-2">
{/* Body Tab Content */}
<TabsContent value="body" className="h-full m-0">
<div className="flex flex-col h-full gap-2">
<div className="h-[calc(100%-40px)]">
<LayoutGrid
layout={model.layout}
onChange={(updatedLayout) => {
model.layout = updatedLayout;
editors.layoutJson?.setValue(JSON.stringify(updatedLayout, null, 2));
layout={currentModel.layout}
onChange={handleLayoutUpdate}
/>
<MonacoEditor
id={`request-body-${model.id}`}
value={currentModel.requestBody}
language="plaintext"
onChange={(newValue) => {
setCurrentModel(prev => ({
...prev,
requestBody: newValue
}));
}}
/>
</div>
</TabsContent>
<TabsContent value="layoutJson">
<div className="flex flex-col gap-4">
<div ref={layoutJsonRef} className="h-[300px] border" />
<Button
onClick={handleLoadLayout}
disabled={isLoading}
>
레이아웃 불러오기
</Button>
</div>
</TabsContent>
<TabsContent value="layoutData">
<div className="flex flex-col gap-4">
<div className="border h-[400px] overflow-auto">
{layoutData && (
<LayoutTreeDataGrid
root={layoutData}
apiRequest={model}
options={layoutDataTreeOptions}
totalMessageLengthSelector={totalMessageLengthRef.current}
onUpdate={handleLayoutUpdate}
<TabsContent value="layout-json" className="h-full m-0">
<MonacoEditor
id={`layout-json-${model.id}`}
value={editorContents.layout}
language="json"
onChange={(value) => {
setEditorContents(prev => ({...prev, layout: value}));
setCurrentModel(prev => ({...prev, layout: JSON.parse(value)}));
}}
height="full"
/>
</TabsContent>
<TabsContent value="pre-request" className="h-full m-0">
<MonacoEditor
id={`pre-request-${model.id}`}
value={editorContents.preRequestScript}
language="javascript"
onChange={(value) => {
setEditorContents(prev => ({...prev, preRequestScript: value}));
setCurrentModel(prev => ({...prev, preRequestScript: value}));
}}
height="full"
/>
)}
</div>
</div>
</TabsContent>
<TabsContent value="preRequest">
<div className="flex gap-4">
<div className="flex-1">
<div ref={preRequestRef} className="h-[300px] border" />
</div>
<div className="w-48">
<VariableSnippet editor={editors.preRequest} />
</div>
</div>
{/* Post-Request Tab Content */}
<TabsContent value="post-request" className="h-full m-0">
<MonacoEditor
id={`post-request-${model.id}`}
value={editorContents.postRequestScript}
language="javascript"
onChange={(value) => {
setEditorContents(prev => ({...prev, postRequestScript: value}));
setCurrentModel(prev => ({...prev, postRequestScript: value}));
}}
height="full"
/>
</TabsContent>
<TabsContent value="postRequest">
<div className="flex gap-4">
<div className="flex-1">
<div ref={postRequestRef} className="h-[300px] border" />
</div>
<div className="w-48">
<VariableSnippet editor={editors.postRequest} />
</div>
</div>
</TabsContent>
</Tabs>
{/* Message Length */}
<div className="mt-4 flex items-center space-x-2">
<span>전체 메시지 길이:</span>
<span ref={totalMessageLengthRef} />
</div>
{/* Response Section */}
<Card className="mt-4">
<CardContent>
<div className="flex flex-col">
<div className="flex-1 min-h-0">
<div className="flex items-center gap-2 mb-2">
<span>Response</span>
<span className="response_time" />
<span className="response_size" />
<span className="text-sm text-gray-500">{responseData.status}</span>
<span className="text-sm text-gray-500">{responseData.time}</span>
<span className="text-sm text-gray-500">{responseData.size}</span>
</div>
<Tabs defaultValue="body">
<Tabs value={currentResponseTab} onValueChange={setCurrentResponseTab} className="flex flex-col h-[calc(100%-2rem)]">
<TabsList>
<TabsTrigger value="body">Body</TabsTrigger>
<TabsTrigger value="response-body">Body</TabsTrigger>
<TabsTrigger value="console">Console</TabsTrigger>
</TabsList>
<TabsContent value="body">
<div ref={responseBodyRef} className="h-[250px] border" />
<div className="flex-1 min-h-0 mt-2">
<TabsContent value="response-body" className="h-full m-0">
<MonacoEditor
id={`response-body-${model.id}`}
value={editorContents.responseBody}
readOnly={true}
height="full"
/>
</TabsContent>
<TabsContent value="console">
<div ref={consoleRef} className="h-[250px] border" />
<TabsContent value="console" className="h-full m-0">
<MonacoEditor
id={`console-${model.id}`}
value={editorContents.consoleLog}
language="plaintext"
readOnly={true}
height="full"
/>
</TabsContent>
</div>
</Tabs>
</div>
</CardContent>
</Card>
</div>
</div>
);
};
// export default TCPClientView;
export default TCPClientView;
+170 -97
View File
@@ -1,15 +1,32 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import EditableInput from '../shared/EditableInput';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ArrowUp, ArrowDown, Trash2, Plus } from "lucide-react";
import EditableInput from "../shared/EditableInput";
// shadcn table imports
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const LayoutGrid = ({ layout, onChange }) => {
const [items, setItems] = useState(layout?.layoutItems || []);
const getLastId = useCallback(() => {
let lastId = 0;
items.forEach(item => {
items.forEach((item) => {
if (item.id > lastId) {
lastId = item.id;
}
@@ -22,26 +39,26 @@ const LayoutGrid = ({ layout, onChange }) => {
id: getLastId(),
parent: 0,
level: 1,
loutName: '',
loutItemName: '',
loutItemDesc: '',
loutName: "",
loutItemName: "",
loutItemDesc: "",
loutItemDepth: 1,
loutItemType: 'Field',
loutItemOccCnt: '',
loutItemOccRef: '',
loutItemDataType: 'String',
loutItemType: "Field",
loutItemOccCnt: "",
loutItemOccRef: "",
loutItemDataType: "String",
loutItemLength: 1,
loutItemDecimal: 0,
loutItemDefault: '',
loutItemMaskYn: 'N',
loutItemDefault: "",
loutItemMaskYn: "N",
loutItemMaskOffset: 0,
loutItemMaskLength: 0,
parentLoutItemIndex: 0,
expanded: true,
isLeaf: true,
LOUTITEMPATH: '',
value: '',
loutItemSerno: items.length + 1
LOUTITEMPATH: "",
value: "",
loutItemSerno: items.length + 1,
};
const newItems = [...items, newItem];
@@ -50,12 +67,18 @@ const LayoutGrid = ({ layout, onChange }) => {
onChange?.(layout);
}, [items, layout, onChange, getLastId]);
const handleMove = useCallback((index, direction) => {
if ((direction === 'up' && index > 0) ||
(direction === 'down' && index < items.length - 1)) {
const handleMove = useCallback(
(index, direction) => {
if (
(direction === "up" && index > 0) ||
(direction === "down" && index < items.length - 1)
) {
const newItems = [...items];
const swapIndex = direction === 'up' ? index - 1 : index + 1;
[newItems[index], newItems[swapIndex]] = [newItems[swapIndex], newItems[index]];
const swapIndex = direction === "up" ? index - 1 : index + 1;
[newItems[index], newItems[swapIndex]] = [
newItems[swapIndex],
newItems[index],
];
// Update loutItemSerno values
newItems.forEach((item, idx) => {
@@ -66,9 +89,12 @@ const LayoutGrid = ({ layout, onChange }) => {
layout.layoutItems = newItems;
onChange?.(layout);
}
}, [items, layout, onChange]);
},
[items, layout, onChange]
);
const handleDelete = useCallback((index) => {
const handleDelete = useCallback(
(index) => {
const newItems = items.filter((_, idx) => idx !== index);
// Update loutItemSerno values
@@ -79,38 +105,45 @@ const LayoutGrid = ({ layout, onChange }) => {
setItems(newItems);
layout.layoutItems = newItems;
onChange?.(layout);
}, [items, layout, onChange]);
},
[items, layout, onChange]
);
const handleFieldChange = useCallback((index, field, value) => {
const handleFieldChange = useCallback(
(index, field, value) => {
const newItems = [...items];
newItems[index][field] = value;
if (field === 'value' && newItems[index].value === undefined) {
newItems[index].value = '';
if (field === "value" && newItems[index].value === undefined) {
newItems[index].value = "";
}
setItems(newItems);
layout.layoutItems = newItems;
onChange?.(layout);
}, [items, layout, onChange]);
},
[items, layout, onChange]
);
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse border">
<thead>
<tr>
<th className="border p-2 text-center w-[50px]">#</th>
<th className="border p-2 text-center w-[80px]">ID</th>
<th className="border p-2 text-center w-[80px]">Parent</th>
<th className="border p-2 text-center w-[120px]">항목명(영문)</th>
<th className="border p-2 text-center w-[150px]">항목설명</th>
<th className="border p-2 text-center w-[60px]">깊이</th>
<th className="border p-2 text-center w-[110px]">아이템 유형</th>
<th className="border p-2 text-center w-[80px]">반복 횟수</th>
<th className="border p-2 text-center w-[100px]">반복 참조 필드</th>
<th className="border p-2 text-center w-[80px]">데이터 길이</th>
<th className="border p-2 text-center w-[120px]"></th>
<th className="border p-2 text-center w-[120px]">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-center w-[50px]">#</TableHead>
<TableHead className="text-center w-[80px]">ID</TableHead>
<TableHead className="text-center w-[80px]">Parent</TableHead>
<TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
<TableHead className="text-center w-[150px]">항목설명</TableHead>
<TableHead className="text-center w-[60px]">깊이</TableHead>
<TableHead className="text-center w-[110px]">아이템 유형</TableHead>
<TableHead className="text-center w-[80px]">반복 횟수</TableHead>
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
<TableHead className="text-center w-[80px]">데이터 길이</TableHead>
<TableHead className="text-center w-[120px]">
</TableHead>
<TableHead className="text-center w-[120px]">
비고
<Button
variant="outline"
@@ -118,133 +151,173 @@ const LayoutGrid = ({ layout, onChange }) => {
onClick={handleAddItem}
className="ml-2"
>
<i className="fas fa-plus" />
<Plus className="h-4 w-4" />
</Button>
</th>
</tr>
</thead>
<tbody>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item, index) => (
<tr key={item.id} className="layout-grid-row">
<td className="border p-2 text-center">
<TableRow key={item.id} className="layout-grid-row">
<TableCell className="text-center">
{item.loutItemSerno}
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="text"
value={item.id}
onChange={(e) => handleFieldChange(index, 'id', e.target.value)}
onChange={(e) =>
handleFieldChange(index, "id", e.target.value)
}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="text"
value={item.parent}
onChange={(e) => handleFieldChange(index, 'parent', e.target.value)}
onChange={(e) =>
handleFieldChange(index, "parent", e.target.value)
}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemName}
onChange={(e) => handleFieldChange(index, 'loutItemName', e.target.value)}
onChange={(e) =>
handleFieldChange(index, "loutItemName", e.target.value)
}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemDesc}
onChange={(e) => handleFieldChange(index, 'loutItemDesc', e.target.value)}
onChange={(e) =>
handleFieldChange(index, "loutItemDesc", e.target.value)
}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="number"
value={item.loutItemDepth}
onChange={(e) => handleFieldChange(index, 'loutItemDepth', parseInt(e.target.value))}
onChange={(e) =>
handleFieldChange(
index,
"loutItemDepth",
parseInt(e.target.value)
)
}
min={1}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Select
value={item.loutItemType}
onValueChange={(value) => handleFieldChange(index, 'loutItemType', value)}
className="w-full"
onValueChange={(value) =>
handleFieldChange(index, "loutItemType", value)
}
>
<option value="Field">Field</option>
<option value="Grid">Grid</option>
<option value="Group">Group</option>
<option value="Attr">Attr</option>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Field">Field</SelectItem>
<SelectItem value="Grid">Grid</SelectItem>
<SelectItem value="Group">Group</SelectItem>
<SelectItem value="Attr">Attr</SelectItem>
</SelectContent>
</Select>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemOccCnt}
onChange={(e) => handleFieldChange(index, 'loutItemOccCnt', e.target.value)}
onChange={(e) =>
handleFieldChange(
index,
"loutItemOccCnt",
e.target.value
)
}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemOccRef}
onChange={(e) => handleFieldChange(index, 'loutItemOccRef', e.target.value)}
onChange={(e) =>
handleFieldChange(
index,
"loutItemOccRef",
e.target.value
)
}
className="w-full"
/>
</td>
<td className="border p-2">
</TableCell>
<TableCell>
<Input
type="number"
value={item.loutItemLength}
onChange={(e) => handleFieldChange(index, 'loutItemLength', parseInt(e.target.value))}
onChange={(e) =>
handleFieldChange(
index,
"loutItemLength",
parseInt(e.target.value)
)
}
min={1}
className="w-full"
/>
</td>
<td className="border p-2">
{item.loutItemType === 'Field' && (
</TableCell>
<TableCell>
{item.loutItemType === "Field" && (
<EditableInput
name={`items[${index}].value`}
value={item.value}
onChange={(value) => handleFieldChange(index, 'value', value)}
onChange={(value) =>
handleFieldChange(index, "value", value)
}
/>
)}
</td>
<td className="border p-2 text-center">
</TableCell>
<TableCell className="text-center">
<div className="flex justify-center space-x-1">
<Button
variant="outline"
size="sm"
onClick={() => handleMove(index, 'up')}
onClick={() => handleMove(index, "up")}
>
<i className="fas fa-arrow-up" />
<ArrowUp className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleMove(index, 'down')}
onClick={() => handleMove(index, "down")}
>
<i className="fas fa-arrow-down" />
<ArrowDown className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(index)}
>
<i className="fas fa-trash-alt" />
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
</TableCell>
</TableRow>
))}
</tbody>
</table>
</TableBody>
</Table>
</div>
);
};
@@ -10,10 +10,10 @@ const MonacoEditor = memo(({
onChange = () => {}
}) => {
const editorRef = useRef(null);
const modelRef = useRef(null);
const containerRef = useRef(null);
const subscriptionRef = useRef(null);
// Configure editor options based on language
const getEditorOptions = (lang) => {
const baseOptions = {
automaticLayout: true,
@@ -31,7 +31,6 @@ const MonacoEditor = memo(({
}
};
// Language-specific options
switch (lang) {
case 'json':
return {
@@ -55,97 +54,59 @@ const MonacoEditor = memo(({
}
};
// Initialize or update editor
const setupEditor = () => {
useEffect(() => {
if (!containerRef.current) return;
// Dispose existing editor if it exists
if (editorRef.current) {
editorRef.current.dispose();
}
const modelUri = monaco.Uri.parse(`inmemory://model-${id}`);
modelRef.current = monaco.editor.createModel(value, language, modelUri);
// Create new editor with language-specific options
const options = getEditorOptions(language);
editorRef.current = monaco.editor.create(containerRef.current, {
value,
language,
...options
...options,
model: modelRef.current
});
// Set up change event handler
if (subscriptionRef.current) {
subscriptionRef.current.dispose();
}
subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => {
const newValue = editorRef.current.getValue();
onChange(newValue);
onChange(editorRef.current.getValue());
});
// Format document if it's JSON
if (language === 'json' && value) {
try {
const modelUri = monaco.Uri.parse(`inmemory://model-${id}.json`);
const model = monaco.editor.createModel(value, 'json', modelUri);
setTimeout(() => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
allowComments: false,
schemas: []
});
editorRef.current.setModel(model);
setTimeout(() => {
const action = editorRef.current?.getAction('editor.action.formatDocument');
if (action) action.run();
}, 100);
} catch (e) {
console.warn('Failed to format JSON:', e);
}
}
};
// Initial setup
useEffect(() => {
setupEditor();
// Set up resize observer
const resizeObserver = new ResizeObserver(() => {
if (editorRef.current) {
editorRef.current.layout();
}
editorRef.current?.layout();
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
// Cleanup
return () => {
resizeObserver.disconnect();
if (subscriptionRef.current) {
subscriptionRef.current.dispose();
}
if (editorRef.current) {
editorRef.current.dispose();
subscriptionRef.current?.dispose();
editorRef.current?.dispose();
if (modelRef.current && !modelRef.current.isDisposed()) {
modelRef.current.dispose();
}
};
}, [language]); // Reinitialize when language changes
}, [language]);
// Update value if it changes externally
useEffect(() => {
if (editorRef.current && value !== editorRef.current.getValue()) {
editorRef.current.setValue(value);
// Add formatting for JSON
if (language === 'json' && value) {
try {
const action = editorRef.current.getAction('editor.action.formatDocument');
if (action) {
action.run();
if (modelRef.current && !modelRef.current.isDisposed()) {
modelRef.current.setValue(value);
}
} catch (e) {
console.warn('Failed to format JSON:', e);
}
}
}
}, [value, language]);
}, [value]);
return (
<div
+4 -2
View File
@@ -43,6 +43,8 @@ const SidebarProvider = React.forwardRef((
className,
style,
children,
cookieName,
shortcut,
...props
},
ref
@@ -63,7 +65,7 @@ const SidebarProvider = React.forwardRef((
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
document.cookie = `${cookieName}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}, [setOpenProp, open])
// Helper to toggle the sidebar.
@@ -77,7 +79,7 @@ const SidebarProvider = React.forwardRef((
React.useEffect(() => {
const handleKeyDown = (event) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
event.key === shortcut &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
+1 -1
View File
@@ -73,7 +73,7 @@ class APIClient {
return {
body: data.body,
status: data.status,
headers: Object.fromEntries(data.headers.map(h => [h.key, h.value])),
headers: data.headers ? Object.fromEntries(data.headers.map(h => [h.key, h.value])) : {},
size: data.size
};
}