layout grid

This commit is contained in:
현성필
2025-01-27 08:59:12 +09:00
parent 97648bab40
commit ef11d6eaa4
11 changed files with 604 additions and 797 deletions
+24 -78
View File
@@ -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
+4 -17
View File
@@ -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">
+35 -19
View File
@@ -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} &gt; {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;
+272 -199
View File
@@ -1,15 +1,32 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from "react";
import { Button } from "@/components/ui/button"; import { 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
+4 -2
View File
@@ -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()
+1 -1
View File
@@ -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
}; };
} }