layout grid
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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} > {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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user