layout grid
This commit is contained in:
+24
-78
@@ -20,36 +20,37 @@ const App = ({ options = { type: 'api', contextPath: '/' } }) => {
|
|||||||
|
|
||||||
const AppContent = ({ options, showLogin, setShowLogin }) => {
|
const AppContent = ({ options, showLogin, setShowLogin }) => {
|
||||||
const { isAuthenticated, setIsAuthenticated } = useAPI();
|
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'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
const checkAuth = async () => {
|
||||||
setShowLogin(true);
|
try {
|
||||||
setIsAuthenticated(false);
|
const response = await fetch('/api/account', {});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
if (response.status !== 200) {
|
||||||
setIsAuthenticated(!!data.authorities);
|
|
||||||
if (!data.authorities) {
|
|
||||||
setShowLogin(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Auth check failed:', error);
|
|
||||||
setShowLogin(true);
|
setShowLogin(true);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
checkAuth();
|
const data = await response.json();
|
||||||
}, [setIsAuthenticated]);
|
setIsAuthenticated(!!data.authorities);
|
||||||
|
if (!data.authorities) {
|
||||||
|
setShowLogin(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth check failed:', error);
|
||||||
|
setShowLogin(true);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitialized) {
|
||||||
|
setIsInitialized(true);
|
||||||
|
checkAuth();
|
||||||
|
}
|
||||||
|
}, [isInitialized]);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <LoginDialog open={showLogin} onOpenChange={setShowLogin} />;
|
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
|
export default App
|
||||||
|
|||||||
@@ -10,26 +10,13 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {getCSRFToken} from '@/services/token.js';
|
||||||
|
|
||||||
const LoginDialog = ({ open, onOpenChange }) => {
|
const LoginDialog = ({ open, onOpenChange }) => {
|
||||||
const [formData, setFormData] = useState({ id: '', password: '' });
|
const [formData, setFormData] = useState({ id: '', password: '' });
|
||||||
const [message, setMessage] = useState({ type: '', content: '' });
|
const [message, setMessage] = useState({ type: '', content: '' });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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) => {
|
const showMessage = (type, content) => {
|
||||||
setMessage({ type, content });
|
setMessage({ type, content });
|
||||||
};
|
};
|
||||||
@@ -45,7 +32,7 @@ const LoginDialog = ({ open, onOpenChange }) => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'X-XSRF-TOKEN': getCsrfToken()
|
'X-XSRF-TOKEN': getCSRFToken()
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
id: formData.id,
|
id: formData.id,
|
||||||
@@ -54,7 +41,7 @@ const LoginDialog = ({ open, onOpenChange }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
switch(data.status) {
|
switch(data.status) {
|
||||||
case 'SUCCESS':
|
case 'SUCCESS':
|
||||||
window.location.href = data.redirectUrl;
|
window.location.href = data.redirectUrl;
|
||||||
@@ -84,7 +71,7 @@ const LoginDialog = ({ open, onOpenChange }) => {
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>로그인</DialogTitle>
|
<DialogTitle>TestMaster 로그인</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ const MainContentArea = ({isApiMode, toggleMode}) => {
|
|||||||
const [apiRequests, setApiRequests] = useState([]);
|
const [apiRequests, setApiRequests] = useState([]);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
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 (
|
return (
|
||||||
<div className="h-[calc(100vh-57px)] w-full overflow-hidden">
|
<div className="h-[calc(100vh-57px)] w-full overflow-hidden">
|
||||||
{isApiMode ? (
|
{isApiMode ? (
|
||||||
@@ -93,7 +106,8 @@ const APIMode = ({
|
|||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
collectionName: collection.name,
|
collectionName: collection.name,
|
||||||
server: api.server || null,
|
server: api.server || null,
|
||||||
description: api.description || ''
|
description: api.description || '',
|
||||||
|
layout: api.layout || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingIndex = apiRequests.findIndex(
|
const existingIndex = apiRequests.findIndex(
|
||||||
@@ -113,12 +127,12 @@ const APIMode = ({
|
|||||||
|
|
||||||
const renderFallback = () => (
|
const renderFallback = () => (
|
||||||
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider cookieName="sidebar-right:state" shortcut="/">
|
||||||
<div className="api-mode flex flex-1 h-[calc(100vh-57px)]">
|
<div className="api-mode flex flex-1 h-[calc(100vh-57px)]">
|
||||||
<div className="navigation-sidebar min-w-[0px] border-r bg-gray-100">
|
<div className="navigation-sidebar min-w-[0px] border-r bg-gray-100">
|
||||||
<APINavigation
|
<APINavigation
|
||||||
@@ -159,30 +173,32 @@ const ScenarioMode = ({
|
|||||||
let handleApiSelect = () => {
|
let handleApiSelect = () => {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<div className="scenario-mode flex flex-1 h-[calc(100vh-57px)]">
|
||||||
<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">
|
<div className="navigation-sidebar min-w-[0px] border-r bg-gray-100">
|
||||||
<ScenarioNavigation
|
<ScenarioNavigation
|
||||||
contextPath="/"
|
contextPath="/"
|
||||||
side="left"
|
side="left"
|
||||||
isApiMode={isApiMode}
|
isApiMode={isApiMode}
|
||||||
onToggleMode={onToggleMode}
|
onToggleMode={onToggleMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="editor flex-1 h-[calc(100vh-57px)] overflow-hidden">
|
<div className="editor flex-1 h-[calc(100vh-57px)] overflow-hidden">
|
||||||
<ScenarioEditor/>
|
<ScenarioEditor/>
|
||||||
</div>
|
|
||||||
<div className="navigation-sidebar min-w-[0px] border-l bg-gray-100">
|
|
||||||
<APINavigation
|
|
||||||
contextPath="/"
|
|
||||||
side="right"
|
|
||||||
onApiSelect={handleApiSelect}
|
|
||||||
isApiMode={isApiMode}
|
|
||||||
onToggleMode={onToggleMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
<div className="navigation-sidebar min-w-[0px] border-l bg-gray-100">
|
||||||
|
<SidebarProvider cookieName="sidebar-right:state" shortcut="/">
|
||||||
|
<APINavigation
|
||||||
|
contextPath="/"
|
||||||
|
side="right"
|
||||||
|
onApiSelect={handleApiSelect}
|
||||||
|
isApiMode={isApiMode}
|
||||||
|
onToggleMode={onToggleMode}
|
||||||
|
/>
|
||||||
|
</SidebarProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ const APINavigation = ({
|
|||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<ScrollArea className="h-[calc(100vh-64px)]">
|
<ScrollArea className="h-[calc(100vh-64px)]">
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Collections</SidebarGroupLabel>
|
<SidebarGroupLabel>Collections (Ctrl + /)</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{collections.map(renderCollectionItem)}
|
{collections.map(renderCollectionItem)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const ScenarioEditor = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!currentFlowData) {
|
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 (
|
return (
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ const ScenarioNavigation = ({
|
|||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<ScrollArea className="h-[calc(100%-64px)]">
|
<ScrollArea className="h-[calc(100%-64px)]">
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Scenarios</SidebarGroupLabel>
|
<SidebarGroupLabel>Scenarios (Ctrl + .)</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{scenarios.map(renderScenarioItem)}
|
{scenarios.map(renderScenarioItem)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
@@ -1,457 +1,279 @@
|
|||||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
import * as monaco from 'monaco-editor';
|
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
|
||||||
import {Card, CardContent} from '@/components/ui/card';
|
import ServerManager from '@/components/ServerManager.jsx';
|
||||||
import LayoutGrid from '../layout/LayoutGrid';
|
import {useAPI} from '@/providers/APIProvider';
|
||||||
import LayoutTreeDataGrid from '../layout/LayoutTreeDataGrid';
|
import MonacoEditor from '@/components/shared/MonacoEditor';
|
||||||
import ApiLayoutItemTreeBuilder from '../../lib/ApiLayoutItemTreeBuilder';
|
|
||||||
import ApiLayoutItemTreeDataBuilder from '../../lib/ApiLayoutItemTreeDataBuilder';
|
|
||||||
import VariableSnippet from '../shared/VariableSnippet';
|
|
||||||
import APIClient from '@/services/APIClient.js';
|
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 }) => {
|
export const TCPClientView = ({model, index}) => {
|
||||||
const [apiClient] = useState(() => new APIClient());
|
const apiClient = new APIClient();
|
||||||
|
const {getServer, updateAPIRequest} = useAPI();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [editors, setEditors] = useState({
|
const [currentModel, setCurrentModel] = useState(model);
|
||||||
layoutJson: null,
|
const [currentTab, setCurrentTab] = useState('body');
|
||||||
requestBody: null,
|
const [currentResponseTab, setCurrentResponseTab] = useState('response-body');
|
||||||
responseBody: null,
|
|
||||||
console: null,
|
useEffect(() => {
|
||||||
preRequest: null,
|
setCurrentModel(prev => ({
|
||||||
postRequest: null
|
...prev,
|
||||||
|
requestBody: model.requestBody || '',
|
||||||
|
layout: model.layout || {},
|
||||||
|
contentType: 'plaintext'
|
||||||
|
}));
|
||||||
|
}, [model]);
|
||||||
|
|
||||||
|
|
||||||
|
const [layoutRoot, setLayoutRoot] = useState(() => {
|
||||||
|
const root = ApiLayoutItemTreeBuilder.createTree(model.layout.layoutItems);
|
||||||
|
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
|
||||||
|
return replicator.createReplicatedTree(root);
|
||||||
});
|
});
|
||||||
const [layoutData, setLayoutData] = useState(null);
|
|
||||||
|
|
||||||
// Refs
|
const [responseData, setResponseData] = useState({
|
||||||
const layoutJsonRef = useRef(null);
|
headers: [],
|
||||||
const requestBodyRef = useRef(null);
|
status: '',
|
||||||
const responseBodyRef = useRef(null);
|
time: '',
|
||||||
const consoleRef = useRef(null);
|
size: ''
|
||||||
const preRequestRef = useRef(null);
|
});
|
||||||
const postRequestRef = useRef(null);
|
|
||||||
const totalMessageLengthRef = useRef(null);
|
|
||||||
const layoutGridRef = useRef(null);
|
|
||||||
|
|
||||||
const handleResponseDisplay = useCallback((response) => {
|
const [editorContents, setEditorContents] = useState({
|
||||||
try {
|
requestBody: currentModel.requestBody || '',
|
||||||
editors.responseBody?.setValue(response.body || '');
|
responseBody: '',
|
||||||
if (response.time) {
|
consoleLog: '',
|
||||||
const responseTimeEl = document.querySelector('.response_time');
|
layout: JSON.stringify(currentModel.layout, null, 2) || '',
|
||||||
if (responseTimeEl) {
|
preRequestScript: currentModel.preRequestScript || '',
|
||||||
responseTimeEl.textContent = `time: ${response.time}ms`;
|
postRequestScript: currentModel.postRequestScript || ''
|
||||||
}
|
});
|
||||||
}
|
|
||||||
if (response.size) {
|
useEffect(() => {
|
||||||
const responseSizeEl = document.querySelector('.response_size');
|
if (currentModel.server) {
|
||||||
if (responseSizeEl) {
|
const selectedServer = getServer(currentModel.server);
|
||||||
responseSizeEl.textContent = `size: ${response.size}bytes`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error displaying response:', error);
|
|
||||||
}
|
}
|
||||||
}, [editors.responseBody]);
|
}, [currentModel.server]);
|
||||||
|
|
||||||
const handleLoadLayout = useCallback(() => {
|
const handleServerChange = (serverId) => {
|
||||||
try {
|
setCurrentModel(prev => ({
|
||||||
const newLayout = JSON.parse(editors.layoutJson?.getValue() || '{}');
|
...prev,
|
||||||
model.layout = newLayout;
|
server: serverId
|
||||||
if (newLayout.layoutItems) {
|
}));
|
||||||
newLayout.layoutItems.forEach((item) => {
|
};
|
||||||
if (item.value === undefined) {
|
|
||||||
item.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutRoot = ApiLayoutItemTreeBuilder.createTree(newLayout.layoutItems);
|
const handleSendRequest = async () => {
|
||||||
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
|
if (isLoading) {
|
||||||
const layoutDataRoot = replicator.createReplicatedTree(layoutRoot);
|
|
||||||
|
|
||||||
setLayoutData(layoutDataRoot);
|
|
||||||
|
|
||||||
if (layoutGridRef.current) {
|
|
||||||
layoutGridRef.current.rebuild(newLayout);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading layout:', error);
|
|
||||||
alert(error.message);
|
|
||||||
}
|
|
||||||
}, [model, editors.layoutJson]);
|
|
||||||
|
|
||||||
const handleSendRequest = useCallback(async () => {
|
|
||||||
if (!model.server) {
|
|
||||||
alert('서버를 등록해주세요.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
setEditorContents(prev => ({...prev, consoleLog: ''}));
|
||||||
editors.responseBody?.setValue('');
|
|
||||||
editors.console?.setValue('');
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
try {
|
||||||
const response = await apiClient.sendAPIRequest(
|
const response = await apiClient.sendAPIRequest(
|
||||||
model,
|
currentModel,
|
||||||
null,
|
(log) => setEditorContents(prev => ({
|
||||||
(message) => {
|
...prev,
|
||||||
const consoleEditor = editors.console;
|
consoleLog: prev.consoleLog + log + '\n'
|
||||||
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
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response) {
|
setResponseData({
|
||||||
const endTime = Date.now();
|
time: `${response.time}ms`,
|
||||||
response.time = endTime - startTime;
|
size: `Size: ${response.size}`
|
||||||
handleResponseDisplay(response);
|
});
|
||||||
}
|
|
||||||
|
setEditorContents(prev => ({
|
||||||
|
...prev,
|
||||||
|
responseBody: response.body
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending request:', error);
|
console.error(error);
|
||||||
alert(error.message);
|
setEditorContents(prev => ({
|
||||||
|
...prev,
|
||||||
|
consoleLog: prev.consoleLog + `Error: ${error.message}\n`
|
||||||
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [model, editors, apiClient, handleResponseDisplay]);
|
};
|
||||||
|
|
||||||
const handleResize = useCallback(() => {
|
const handleSave = async () => {
|
||||||
Object.values(editors).forEach(editor => {
|
try {
|
||||||
if (editor) {
|
const updatedModel = {
|
||||||
editor.layout({ width: 0 });
|
...currentModel,
|
||||||
editor.layout();
|
preRequestScript: editorContents.preRequestScript,
|
||||||
}
|
postRequestScript: editorContents.postRequestScript,
|
||||||
});
|
layout: JSON.parse(editorContents.layout),
|
||||||
}, [editors]);
|
requestBody: currentModel.requestBody
|
||||||
|
};
|
||||||
|
|
||||||
// Layout update handler
|
await updateAPIRequest(updatedModel);
|
||||||
const handleLayoutUpdate = useCallback(async (finalMessage, layoutDataTree) => {
|
} catch (error) {
|
||||||
const prefix = (model.server && model.server.serverOptions)
|
console.error('Failed to save API:', error);
|
||||||
? 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(() => {
|
|
||||||
try {
|
|
||||||
const content = layoutJsonEditor.getValue();
|
|
||||||
JSON.parse(content); // Validate JSON
|
|
||||||
} catch (e) {
|
|
||||||
// Invalid JSON - ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setEditors(prev => ({ ...prev, layoutJson: layoutJsonEditor }));
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (requestBodyRef.current && !editors.requestBody) {
|
const handleLayoutUpdate = useCallback((updatedLayout) => {
|
||||||
const requestBodyEditor = monaco.editor.create(requestBodyRef.current, {
|
const updatedRoot = ApiLayoutItemTreeBuilder.createTree(updatedLayout.layoutItems);
|
||||||
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 replicator = new ApiLayoutItemTreeDataBuilder(10000);
|
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
|
||||||
const layoutDataRoot = replicator.createReplicatedTree(layoutRoot);
|
const newRoot = replicator.createReplicatedTree(updatedRoot);
|
||||||
setLayoutData(layoutDataRoot);
|
setLayoutRoot(newRoot);
|
||||||
|
currentModel.layout = updatedLayout;
|
||||||
return () => {
|
}, [currentModel]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full p-4">
|
<div className="flex flex-col h-full p-4 gap-4">
|
||||||
{/* Header Section */}
|
{/* Request Header Section */}
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<SidebarTrigger className="-ml-1" />
|
||||||
{model.collectionName} > {model.name}
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
</span>
|
<div>
|
||||||
</div>
|
<span>{currentModel.collectionName}</span>
|
||||||
|
{' > '}
|
||||||
{/* Server Selection */}
|
<span>{currentModel.name}</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={handleSendRequest}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Sending...' : 'Send'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => parent?.handleSaveRequest?.(model)}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server Options */}
|
{/* Request Controls */}
|
||||||
<div className="server_options mb-4" />
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="shrink-0">
|
||||||
{/* Main Content */}
|
<ServerManager
|
||||||
<Tabs defaultValue="layout" className="flex-1">
|
onServerChange={handleServerChange}
|
||||||
<TabsList>
|
defaultValue={currentModel.server}
|
||||||
<TabsTrigger value="layout">Layout</TabsTrigger>
|
/>
|
||||||
<TabsTrigger value="layoutJson">Layout JSON</TabsTrigger>
|
</div>
|
||||||
<TabsTrigger value="layoutTree">Layout Tree</TabsTrigger>
|
<div className="flex gap-2 shrink-0">
|
||||||
<TabsTrigger value="layoutData">Layout Data</TabsTrigger>
|
<Button disabled={isLoading} onClick={handleSendRequest}>
|
||||||
<TabsTrigger value="preRequest">Pre-Request</TabsTrigger>
|
{isLoading ? 'Sending...' : 'Send'}
|
||||||
<TabsTrigger value="postRequest">Post-Request</TabsTrigger>
|
</Button>
|
||||||
</TabsList>
|
<Button variant="secondary" onClick={handleSave}>Save</Button>
|
||||||
|
</div>
|
||||||
<TabsContent value="layout">
|
|
||||||
<div ref={layoutGridRef}>
|
|
||||||
<LayoutGrid
|
|
||||||
layout={model.layout}
|
|
||||||
onChange={(updatedLayout) => {
|
|
||||||
model.layout = updatedLayout;
|
|
||||||
editors.layoutJson?.setValue(JSON.stringify(updatedLayout, null, 2));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Response Section */}
|
{/* Request/Response Container */}
|
||||||
<Card className="mt-4">
|
<div className="flex flex-col gap-4 flex-1 min-h-0">
|
||||||
<CardContent>
|
{/* Request Section */}
|
||||||
<div className="flex flex-col">
|
<div className="flex-1 min-h-0">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<Tabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-col h-full">
|
||||||
<span>Response</span>
|
<TabsList>
|
||||||
<span className="response_time" />
|
<TabsTrigger value="body">Layout</TabsTrigger>
|
||||||
<span className="response_size" />
|
<TabsTrigger value="layout-json">Layout JSON</TabsTrigger>
|
||||||
|
<TabsTrigger value="pre-request">Pre-Request</TabsTrigger>
|
||||||
|
<TabsTrigger value="post-request">Post-Request</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<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={currentModel.layout}
|
||||||
|
onChange={handleLayoutUpdate}
|
||||||
|
/>
|
||||||
|
<MonacoEditor
|
||||||
|
id={`request-body-${model.id}`}
|
||||||
|
value={currentModel.requestBody}
|
||||||
|
language="plaintext"
|
||||||
|
onChange={(newValue) => {
|
||||||
|
setCurrentModel(prev => ({
|
||||||
|
...prev,
|
||||||
|
requestBody: newValue
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="body">
|
</Tabs>
|
||||||
<TabsList>
|
</div>
|
||||||
<TabsTrigger value="body">Body</TabsTrigger>
|
|
||||||
<TabsTrigger value="console">Console</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="body">
|
{/* Response Section */}
|
||||||
<div ref={responseBodyRef} className="h-[250px] border" />
|
<div className="flex-1 min-h-0">
|
||||||
</TabsContent>
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span>Response</span>
|
||||||
<TabsContent value="console">
|
<span className="text-sm text-gray-500">{responseData.status}</span>
|
||||||
<div ref={consoleRef} className="h-[250px] border" />
|
<span className="text-sm text-gray-500">{responseData.time}</span>
|
||||||
</TabsContent>
|
<span className="text-sm text-gray-500">{responseData.size}</span>
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<Tabs value={currentResponseTab} onValueChange={setCurrentResponseTab} className="flex flex-col h-[calc(100%-2rem)]">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="response-body">Body</TabsTrigger>
|
||||||
|
<TabsTrigger value="console">Console</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<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" className="h-full m-0">
|
||||||
|
<MonacoEditor
|
||||||
|
id={`console-${model.id}`}
|
||||||
|
value={editorContents.consoleLog}
|
||||||
|
language="plaintext"
|
||||||
|
readOnly={true}
|
||||||
|
height="full"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select } from "@/components/ui/select";
|
import {
|
||||||
import EditableInput from '../shared/EditableInput';
|
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 LayoutGrid = ({ layout, onChange }) => {
|
||||||
const [items, setItems] = useState(layout?.layoutItems || []);
|
const [items, setItems] = useState(layout?.layoutItems || []);
|
||||||
|
|
||||||
const getLastId = useCallback(() => {
|
const getLastId = useCallback(() => {
|
||||||
let lastId = 0;
|
let lastId = 0;
|
||||||
items.forEach(item => {
|
items.forEach((item) => {
|
||||||
if (item.id > lastId) {
|
if (item.id > lastId) {
|
||||||
lastId = item.id;
|
lastId = item.id;
|
||||||
}
|
}
|
||||||
@@ -22,26 +39,26 @@ const LayoutGrid = ({ layout, onChange }) => {
|
|||||||
id: getLastId(),
|
id: getLastId(),
|
||||||
parent: 0,
|
parent: 0,
|
||||||
level: 1,
|
level: 1,
|
||||||
loutName: '',
|
loutName: "",
|
||||||
loutItemName: '',
|
loutItemName: "",
|
||||||
loutItemDesc: '',
|
loutItemDesc: "",
|
||||||
loutItemDepth: 1,
|
loutItemDepth: 1,
|
||||||
loutItemType: 'Field',
|
loutItemType: "Field",
|
||||||
loutItemOccCnt: '',
|
loutItemOccCnt: "",
|
||||||
loutItemOccRef: '',
|
loutItemOccRef: "",
|
||||||
loutItemDataType: 'String',
|
loutItemDataType: "String",
|
||||||
loutItemLength: 1,
|
loutItemLength: 1,
|
||||||
loutItemDecimal: 0,
|
loutItemDecimal: 0,
|
||||||
loutItemDefault: '',
|
loutItemDefault: "",
|
||||||
loutItemMaskYn: 'N',
|
loutItemMaskYn: "N",
|
||||||
loutItemMaskOffset: 0,
|
loutItemMaskOffset: 0,
|
||||||
loutItemMaskLength: 0,
|
loutItemMaskLength: 0,
|
||||||
parentLoutItemIndex: 0,
|
parentLoutItemIndex: 0,
|
||||||
expanded: true,
|
expanded: true,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
LOUTITEMPATH: '',
|
LOUTITEMPATH: "",
|
||||||
value: '',
|
value: "",
|
||||||
loutItemSerno: items.length + 1
|
loutItemSerno: items.length + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newItems = [...items, newItem];
|
const newItems = [...items, newItem];
|
||||||
@@ -50,201 +67,257 @@ const LayoutGrid = ({ layout, onChange }) => {
|
|||||||
onChange?.(layout);
|
onChange?.(layout);
|
||||||
}, [items, layout, onChange, getLastId]);
|
}, [items, layout, onChange, getLastId]);
|
||||||
|
|
||||||
const handleMove = useCallback((index, direction) => {
|
const handleMove = useCallback(
|
||||||
if ((direction === 'up' && index > 0) ||
|
(index, direction) => {
|
||||||
(direction === 'down' && index < items.length - 1)) {
|
if (
|
||||||
const newItems = [...items];
|
(direction === "up" && index > 0) ||
|
||||||
const swapIndex = direction === 'up' ? index - 1 : index + 1;
|
(direction === "down" && index < items.length - 1)
|
||||||
[newItems[index], newItems[swapIndex]] = [newItems[swapIndex], newItems[index]];
|
) {
|
||||||
|
const newItems = [...items];
|
||||||
|
const swapIndex = direction === "up" ? index - 1 : index + 1;
|
||||||
|
[newItems[index], newItems[swapIndex]] = [
|
||||||
|
newItems[swapIndex],
|
||||||
|
newItems[index],
|
||||||
|
];
|
||||||
|
|
||||||
// Update loutItemSerno values
|
// Update loutItemSerno values
|
||||||
newItems.forEach((item, idx) => {
|
newItems.forEach((item, idx) => {
|
||||||
item.loutItemSerno = idx + 1;
|
item.loutItemSerno = idx + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
setItems(newItems);
|
setItems(newItems);
|
||||||
layout.layoutItems = newItems;
|
layout.layoutItems = newItems;
|
||||||
onChange?.(layout);
|
onChange?.(layout);
|
||||||
}
|
}
|
||||||
}, [items, layout, onChange]);
|
},
|
||||||
|
[items, layout, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback((index) => {
|
const handleDelete = useCallback(
|
||||||
const newItems = items.filter((_, idx) => idx !== index);
|
(index) => {
|
||||||
|
const newItems = items.filter((_, idx) => idx !== index);
|
||||||
|
|
||||||
// Update loutItemSerno values
|
// Update loutItemSerno values
|
||||||
newItems.forEach((item, idx) => {
|
newItems.forEach((item, idx) => {
|
||||||
item.loutItemSerno = idx + 1;
|
item.loutItemSerno = idx + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
setItems(newItems);
|
setItems(newItems);
|
||||||
layout.layoutItems = newItems;
|
layout.layoutItems = newItems;
|
||||||
onChange?.(layout);
|
onChange?.(layout);
|
||||||
}, [items, layout, onChange]);
|
},
|
||||||
|
[items, layout, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
const handleFieldChange = useCallback((index, field, value) => {
|
const handleFieldChange = useCallback(
|
||||||
const newItems = [...items];
|
(index, field, value) => {
|
||||||
newItems[index][field] = value;
|
const newItems = [...items];
|
||||||
|
newItems[index][field] = value;
|
||||||
|
|
||||||
if (field === 'value' && newItems[index].value === undefined) {
|
if (field === "value" && newItems[index].value === undefined) {
|
||||||
newItems[index].value = '';
|
newItems[index].value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
setItems(newItems);
|
setItems(newItems);
|
||||||
layout.layoutItems = newItems;
|
layout.layoutItems = newItems;
|
||||||
onChange?.(layout);
|
onChange?.(layout);
|
||||||
}, [items, layout, onChange]);
|
},
|
||||||
|
[items, layout, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full border-collapse border">
|
<Table>
|
||||||
<thead>
|
<TableHeader>
|
||||||
<tr>
|
<TableRow>
|
||||||
<th className="border p-2 text-center w-[50px]">#</th>
|
<TableHead className="text-center w-[50px]">#</TableHead>
|
||||||
<th className="border p-2 text-center w-[80px]">ID</th>
|
<TableHead className="text-center w-[80px]">ID</TableHead>
|
||||||
<th className="border p-2 text-center w-[80px]">Parent</th>
|
<TableHead className="text-center w-[80px]">Parent</TableHead>
|
||||||
<th className="border p-2 text-center w-[120px]">항목명(영문)</th>
|
<TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
|
||||||
<th className="border p-2 text-center w-[150px]">항목설명</th>
|
<TableHead className="text-center w-[150px]">항목설명</TableHead>
|
||||||
<th className="border p-2 text-center w-[60px]">깊이</th>
|
<TableHead className="text-center w-[60px]">깊이</TableHead>
|
||||||
<th className="border p-2 text-center w-[110px]">아이템 유형</th>
|
<TableHead className="text-center w-[110px]">아이템 유형</TableHead>
|
||||||
<th className="border p-2 text-center w-[80px]">반복 횟수</th>
|
<TableHead className="text-center w-[80px]">반복 횟수</TableHead>
|
||||||
<th className="border p-2 text-center w-[100px]">반복 참조 필드</th>
|
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
|
||||||
<th className="border p-2 text-center w-[80px]">데이터 길이</th>
|
<TableHead className="text-center w-[80px]">데이터 길이</TableHead>
|
||||||
<th className="border p-2 text-center w-[120px]">값</th>
|
<TableHead className="text-center w-[120px]">
|
||||||
<th className="border p-2 text-center w-[120px]">
|
값
|
||||||
비고
|
</TableHead>
|
||||||
<Button
|
<TableHead className="text-center w-[120px]">
|
||||||
variant="outline"
|
비고
|
||||||
size="sm"
|
<Button
|
||||||
onClick={handleAddItem}
|
variant="outline"
|
||||||
className="ml-2"
|
size="sm"
|
||||||
>
|
onClick={handleAddItem}
|
||||||
<i className="fas fa-plus" />
|
className="ml-2"
|
||||||
</Button>
|
>
|
||||||
</th>
|
<Plus className="h-4 w-4" />
|
||||||
</tr>
|
</Button>
|
||||||
</thead>
|
</TableHead>
|
||||||
<tbody>
|
</TableRow>
|
||||||
{items.map((item, index) => (
|
</TableHeader>
|
||||||
<tr key={item.id} className="layout-grid-row">
|
<TableBody>
|
||||||
<td className="border p-2 text-center">
|
{items.map((item, index) => (
|
||||||
{item.loutItemSerno}
|
<TableRow key={item.id} className="layout-grid-row">
|
||||||
</td>
|
<TableCell className="text-center">
|
||||||
<td className="border p-2">
|
{item.loutItemSerno}
|
||||||
<Input
|
</TableCell>
|
||||||
type="text"
|
<TableCell>
|
||||||
value={item.id}
|
<Input
|
||||||
onChange={(e) => handleFieldChange(index, 'id', e.target.value)}
|
type="text"
|
||||||
className="w-full"
|
value={item.id}
|
||||||
/>
|
onChange={(e) =>
|
||||||
</td>
|
handleFieldChange(index, "id", e.target.value)
|
||||||
<td className="border p-2">
|
}
|
||||||
<Input
|
className="w-full"
|
||||||
type="text"
|
/>
|
||||||
value={item.parent}
|
</TableCell>
|
||||||
onChange={(e) => handleFieldChange(index, 'parent', e.target.value)}
|
<TableCell>
|
||||||
className="w-full"
|
<Input
|
||||||
/>
|
type="text"
|
||||||
</td>
|
value={item.parent}
|
||||||
<td className="border p-2">
|
onChange={(e) =>
|
||||||
<Input
|
handleFieldChange(index, "parent", e.target.value)
|
||||||
type="text"
|
}
|
||||||
value={item.loutItemName}
|
className="w-full"
|
||||||
onChange={(e) => handleFieldChange(index, 'loutItemName', e.target.value)}
|
/>
|
||||||
className="w-full"
|
</TableCell>
|
||||||
/>
|
<TableCell>
|
||||||
</td>
|
<Input
|
||||||
<td className="border p-2">
|
type="text"
|
||||||
<Input
|
value={item.loutItemName}
|
||||||
type="text"
|
onChange={(e) =>
|
||||||
value={item.loutItemDesc}
|
handleFieldChange(index, "loutItemName", e.target.value)
|
||||||
onChange={(e) => handleFieldChange(index, 'loutItemDesc', e.target.value)}
|
}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</td>
|
</TableCell>
|
||||||
<td className="border p-2">
|
<TableCell>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
value={item.loutItemDepth}
|
value={item.loutItemDesc}
|
||||||
onChange={(e) => handleFieldChange(index, 'loutItemDepth', parseInt(e.target.value))}
|
onChange={(e) =>
|
||||||
min={1}
|
handleFieldChange(index, "loutItemDesc", e.target.value)
|
||||||
className="w-full"
|
}
|
||||||
/>
|
className="w-full"
|
||||||
</td>
|
/>
|
||||||
<td className="border p-2">
|
</TableCell>
|
||||||
<Select
|
<TableCell>
|
||||||
value={item.loutItemType}
|
<Input
|
||||||
onValueChange={(value) => handleFieldChange(index, 'loutItemType', value)}
|
type="number"
|
||||||
className="w-full"
|
value={item.loutItemDepth}
|
||||||
>
|
onChange={(e) =>
|
||||||
<option value="Field">Field</option>
|
handleFieldChange(
|
||||||
<option value="Grid">Grid</option>
|
index,
|
||||||
<option value="Group">Group</option>
|
"loutItemDepth",
|
||||||
<option value="Attr">Attr</option>
|
parseInt(e.target.value)
|
||||||
</Select>
|
)
|
||||||
</td>
|
}
|
||||||
<td className="border p-2">
|
min={1}
|
||||||
<Input
|
className="w-full"
|
||||||
type="text"
|
/>
|
||||||
value={item.loutItemOccCnt}
|
</TableCell>
|
||||||
onChange={(e) => handleFieldChange(index, 'loutItemOccCnt', e.target.value)}
|
<TableCell>
|
||||||
className="w-full"
|
<Select
|
||||||
/>
|
value={item.loutItemType}
|
||||||
</td>
|
onValueChange={(value) =>
|
||||||
<td className="border p-2">
|
handleFieldChange(index, "loutItemType", value)
|
||||||
<Input
|
}
|
||||||
type="text"
|
|
||||||
value={item.loutItemOccRef}
|
|
||||||
onChange={(e) => handleFieldChange(index, 'loutItemOccRef', e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="border p-2">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={item.loutItemLength}
|
|
||||||
onChange={(e) => handleFieldChange(index, 'loutItemLength', parseInt(e.target.value))}
|
|
||||||
min={1}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="border p-2">
|
|
||||||
{item.loutItemType === 'Field' && (
|
|
||||||
<EditableInput
|
|
||||||
name={`items[${index}].value`}
|
|
||||||
value={item.value}
|
|
||||||
onChange={(value) => handleFieldChange(index, 'value', value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="border p-2 text-center">
|
|
||||||
<div className="flex justify-center space-x-1">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleMove(index, 'up')}
|
|
||||||
>
|
>
|
||||||
<i className="fas fa-arrow-up" />
|
<SelectTrigger className="w-full">
|
||||||
</Button>
|
<SelectValue placeholder="Select type" />
|
||||||
<Button
|
</SelectTrigger>
|
||||||
variant="outline"
|
<SelectContent>
|
||||||
size="sm"
|
<SelectItem value="Field">Field</SelectItem>
|
||||||
onClick={() => handleMove(index, 'down')}
|
<SelectItem value="Grid">Grid</SelectItem>
|
||||||
>
|
<SelectItem value="Group">Group</SelectItem>
|
||||||
<i className="fas fa-arrow-down" />
|
<SelectItem value="Attr">Attr</SelectItem>
|
||||||
</Button>
|
</SelectContent>
|
||||||
<Button
|
</Select>
|
||||||
variant="outline"
|
</TableCell>
|
||||||
size="sm"
|
<TableCell>
|
||||||
onClick={() => handleDelete(index)}
|
<Input
|
||||||
>
|
type="text"
|
||||||
<i className="fas fa-trash-alt" />
|
value={item.loutItemOccCnt}
|
||||||
</Button>
|
onChange={(e) =>
|
||||||
</div>
|
handleFieldChange(
|
||||||
</td>
|
index,
|
||||||
</tr>
|
"loutItemOccCnt",
|
||||||
))}
|
e.target.value
|
||||||
</tbody>
|
)
|
||||||
</table>
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={item.loutItemOccRef}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange(
|
||||||
|
index,
|
||||||
|
"loutItemOccRef",
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={item.loutItemLength}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFieldChange(
|
||||||
|
index,
|
||||||
|
"loutItemLength",
|
||||||
|
parseInt(e.target.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.loutItemType === "Field" && (
|
||||||
|
<EditableInput
|
||||||
|
name={`items[${index}].value`}
|
||||||
|
value={item.value}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleFieldChange(index, "value", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="flex justify-center space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleMove(index, "up")}
|
||||||
|
>
|
||||||
|
<ArrowUp className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleMove(index, "down")}
|
||||||
|
>
|
||||||
|
<ArrowDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ const MonacoEditor = memo(({
|
|||||||
onChange = () => {}
|
onChange = () => {}
|
||||||
}) => {
|
}) => {
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
const modelRef = useRef(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const subscriptionRef = useRef(null);
|
const subscriptionRef = useRef(null);
|
||||||
|
|
||||||
// Configure editor options based on language
|
|
||||||
const getEditorOptions = (lang) => {
|
const getEditorOptions = (lang) => {
|
||||||
const baseOptions = {
|
const baseOptions = {
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
@@ -31,7 +31,6 @@ const MonacoEditor = memo(({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Language-specific options
|
|
||||||
switch (lang) {
|
switch (lang) {
|
||||||
case 'json':
|
case 'json':
|
||||||
return {
|
return {
|
||||||
@@ -55,97 +54,59 @@ const MonacoEditor = memo(({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize or update editor
|
useEffect(() => {
|
||||||
const setupEditor = () => {
|
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
// Dispose existing editor if it exists
|
const modelUri = monaco.Uri.parse(`inmemory://model-${id}`);
|
||||||
if (editorRef.current) {
|
modelRef.current = monaco.editor.createModel(value, language, modelUri);
|
||||||
editorRef.current.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new editor with language-specific options
|
|
||||||
const options = getEditorOptions(language);
|
const options = getEditorOptions(language);
|
||||||
editorRef.current = monaco.editor.create(containerRef.current, {
|
editorRef.current = monaco.editor.create(containerRef.current, {
|
||||||
value,
|
...options,
|
||||||
language,
|
model: modelRef.current
|
||||||
...options
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up change event handler
|
|
||||||
if (subscriptionRef.current) {
|
|
||||||
subscriptionRef.current.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => {
|
subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => {
|
||||||
const newValue = editorRef.current.getValue();
|
onChange(editorRef.current.getValue());
|
||||||
onChange(newValue);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Format document if it's JSON
|
|
||||||
if (language === 'json' && value) {
|
if (language === 'json' && value) {
|
||||||
try {
|
try {
|
||||||
const modelUri = monaco.Uri.parse(`inmemory://model-${id}.json`);
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
const model = monaco.editor.createModel(value, 'json', modelUri);
|
validate: true,
|
||||||
|
allowComments: false,
|
||||||
|
schemas: []
|
||||||
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
const action = editorRef.current?.getAction('editor.action.formatDocument');
|
||||||
validate: true,
|
if (action) action.run();
|
||||||
allowComments: false,
|
|
||||||
schemas: []
|
|
||||||
});
|
|
||||||
editorRef.current.setModel(model);
|
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to format JSON:', e);
|
console.warn('Failed to format JSON:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Initial setup
|
|
||||||
useEffect(() => {
|
|
||||||
setupEditor();
|
|
||||||
|
|
||||||
// Set up resize observer
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
if (editorRef.current) {
|
editorRef.current?.layout();
|
||||||
editorRef.current.layout();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (containerRef.current) {
|
resizeObserver.observe(containerRef.current);
|
||||||
resizeObserver.observe(containerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
if (subscriptionRef.current) {
|
subscriptionRef.current?.dispose();
|
||||||
subscriptionRef.current.dispose();
|
editorRef.current?.dispose();
|
||||||
}
|
if (modelRef.current && !modelRef.current.isDisposed()) {
|
||||||
if (editorRef.current) {
|
modelRef.current.dispose();
|
||||||
editorRef.current.dispose();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [language]); // Reinitialize when language changes
|
}, [language]);
|
||||||
|
|
||||||
// Update value if it changes externally
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current && value !== editorRef.current.getValue()) {
|
if (modelRef.current && !modelRef.current.isDisposed()) {
|
||||||
editorRef.current.setValue(value);
|
modelRef.current.setValue(value);
|
||||||
|
|
||||||
// Add formatting for JSON
|
|
||||||
if (language === 'json' && value) {
|
|
||||||
try {
|
|
||||||
const action = editorRef.current.getAction('editor.action.formatDocument');
|
|
||||||
if (action) {
|
|
||||||
action.run();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to format JSON:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [value, language]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const SidebarProvider = React.forwardRef((
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
|
cookieName,
|
||||||
|
shortcut,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -63,7 +65,7 @@ const SidebarProvider = React.forwardRef((
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
// 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])
|
}, [setOpenProp, open])
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
// Helper to toggle the sidebar.
|
||||||
@@ -77,7 +79,7 @@ const SidebarProvider = React.forwardRef((
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
if (
|
if (
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
event.key === shortcut &&
|
||||||
(event.metaKey || event.ctrlKey)
|
(event.metaKey || event.ctrlKey)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class APIClient {
|
|||||||
return {
|
return {
|
||||||
body: data.body,
|
body: data.body,
|
||||||
status: data.status,
|
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
|
size: data.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user