PropertyTable 수정

This commit is contained in:
현성필
2025-01-25 22:10:27 +09:00
parent 4d77faebf5
commit e58855e636
8 changed files with 214 additions and 51 deletions
+11 -1
View File
@@ -6,6 +6,7 @@ import AddCollectionDialog from '@/components/api-tester/AddCollectionDialog.jsx
import AddAPIDialog from '@/components/api-tester/AddAPIDialog.jsx';
import AddScenarioDialog from '@/components/api-tester/AddScenarioDialog.jsx';
import {useLoadTest} from '@/providers/LoadTestProvider.jsx';
import VariableModal from '@/components/api-tester/VariableModal.jsx';
const menuConfig = [
{
@@ -22,7 +23,8 @@ const menuConfig = [
items: [
{id: 'collection-add', label: '컬렉션 추가', action: 'collection-add'},
{id: 'request-add', label: '요청 추가', action: 'request-add'},
{id: 'scenario-add', label: '시나리오 추가', action: 'scenario-add'}
{id: 'scenario-add', label: '시나리오 추가', action: 'scenario-add'},
{id: 'variables-show', label: '변수 보기', action: 'variables-show'},
]
},
{
@@ -81,6 +83,7 @@ const APITester = () => {
const [showAddCollection, setShowAddCollection] = useState(false);
const [showAddApi, setShowAddApi] = useState(false);
const [showAddScenario, setShowAddScenario] = useState(false);
const [showVariableModal, setShowVariableModal] = useState(false);
const { handleLogout, createCollection, createAPIRequest } = useAPI();
const {createScenario} = useLoadTest();
@@ -131,6 +134,9 @@ const APITester = () => {
case 'scenario-add':
setShowAddScenario(true);
break;
case 'variables-show':
setShowVariableModal(true);
break;
case 'logout':
const success = await handleLogout();
if (success) {
@@ -197,6 +203,10 @@ const APITester = () => {
onOpenChange={setShowAddScenario}
onSubmit={handleAddScenario}
/>
<VariableModal
open={showVariableModal}
onOpenChange={setShowVariableModal}
/>
</div>
);
};
@@ -53,7 +53,7 @@ const DraggableApiItem = ({api, collectionId, onApiAction}) => {
<FileText className="h-4 w-4 flex-shrink-0"/>
<Tooltip>
<TooltipTrigger asChild className="block max-w-[150px]">
<span className="block truncate">s
<span className="block truncate">
{api.name}
</span>
</TooltipTrigger>
@@ -0,0 +1,114 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Trash } from 'lucide-react';
const VariableModal = ({ open, onOpenChange }) => {
const [variables, setVariables] = useState([]);
useEffect(() => {
if (open) {
const globals = window.globals || {};
setVariables(Object.entries(globals).map(([key, value]) => ({ key, value })));
}
}, [open]);
const handleKeyChange = (index, newKey) => {
const newVariables = [...variables];
const oldKey = newVariables[index].key;
newVariables[index].key = newKey;
setVariables(newVariables);
const globals = window.globals || {};
const value = globals[oldKey];
delete globals[oldKey];
globals[newKey] = value;
window.globals = globals;
};
const handleValueChange = (index, newValue) => {
const newVariables = [...variables];
newVariables[index].value = newValue;
setVariables(newVariables);
const globals = window.globals || {};
globals[newVariables[index].key] = newValue;
window.globals = globals;
};
const handleDelete = (index) => {
const key = variables[index].key;
const newVariables = variables.filter((_, i) => i !== index);
setVariables(newVariables);
const globals = window.globals || {};
delete globals[key];
window.globals = globals;
};
const handleAdd = () => {
setVariables([...variables, { key: '', value: '' }]);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>환경 변수</DialogTitle>
</DialogHeader>
<div>
<div className="font-medium mb-2">Globals</div>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>
<Button variant="secondary" size="sm" onClick={handleAdd}>
추가
</Button>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{variables.map((variable, index) => (
<TableRow key={index}>
<TableCell>
<Input
value={variable.key}
onChange={(e) => handleKeyChange(index, e.target.value)}
/>
</TableCell>
<TableCell>
<Input
value={variable.value}
onChange={(e) => handleValueChange(index, e.target.value)}
/>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(index)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<DialogFooter>
<Button variant="secondary" onClick={() => onOpenChange(false)}>
닫기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default VariableModal;
@@ -3,7 +3,6 @@ import {Button} from '@/components/ui/button';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
import {Input} from '@/components/ui/input';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
import {Label} from '@/components/ui/label';
import EditableInput from '../shared/EditableInput';
import PropertyTable from '../shared/PropertyTable';
import ServerManager from '@/components/ServerManager.jsx';
@@ -27,6 +26,19 @@ export const HTTPClientView = ({model, index}) => {
const [basePath, setBasePath] = useState('');
const [currentTab, setCurrentTab] = useState('body');
const [currentResponseTab, setCurrentResponseTab] = useState('response-body');
useEffect(() => {
setCurrentModel(prev => ({
...prev,
queryParams: model.queryParams || [],
headers: model.headers || [],
path: model.path || '',
requestBody: model.requestBody || '',
method: model.method || 'GET',
contentType: model.contentType || 'json'
}));
}, [model]);
const [responseData, setResponseData] = useState({
headers: [],
status: '',
@@ -52,16 +64,10 @@ export const HTTPClientView = ({model, index}) => {
}, [currentModel.server]);
const handleServerChange = (serverId) => {
// const selectedServer = getServer(serverId);
setCurrentModel(prev => ({
...prev,
server: serverId
}));
// if (selectedServer) {
// setBasePath(selectedServer.basePath || '');
// }
};
const handleHeadersChange = (headers) => {
@@ -71,7 +77,16 @@ export const HTTPClientView = ({model, index}) => {
}));
};
const handlePathChange = (path) => {
const handlePathChange = (e) => {
const path = e.target.value;
if (!path) {
setCurrentModel(prev => ({
...prev,
path: '',
queryParams: []
}));
return;
}
const queryParams = parseQueryParams(path);
setCurrentModel(prev => ({
...prev,
@@ -80,40 +95,50 @@ export const HTTPClientView = ({model, index}) => {
}));
};
const buildUrlFromPathAndParams = (path, queryParams) => {
const baseUrl = path.split('?')[0];
const enabledParams = queryParams.filter(p => p.enabled && p.key);
if (!enabledParams.length) return baseUrl;
const queryString = enabledParams
.map(p => `${p.key}=${p.value || ''}`)
.join('&');
return `${baseUrl}?${queryString}`;
};
const handleQueryParamsChange = (newParams) => {
const newPath = buildUrlFromPathAndParams(currentModel.path.split('?')[0], newParams);
setCurrentModel(prev => ({
...prev,
path: newPath,
queryParams: newParams
}));
};
const parseQueryParams = (path) => {
const queryParams = [];
const queryString = path.split('?')[1];
const [, queryString] = path.split('?');
if (queryString) {
queryString.split('&').forEach(param => {
const [key, value] = param.split('=');
queryParams.push({
enabled: true,
key,
value: value || ''
});
const [key, value] = param.split('=').map(decodeURIComponent);
if (key) {
queryParams.push({
enabled: true,
key,
value: value || ''
});
}
});
}
return queryParams;
};
const renderRequestVariables = () => (
<div className="p-4">
<h3 className="text-sm font-medium mb-2">Available Variables</h3>
<div className="space-y-2">
<div>
<Label>request</Label>
<div className="text-sm text-gray-500">Request object containing headers, body, etc.</div>
</div>
<div>
<Label>environment</Label>
<div className="text-sm text-gray-500">Current environment variables</div>
</div>
</div>
</div>
);
const handleSendRequest = async () => {
if (isLoading) {
return;
@@ -237,6 +262,7 @@ export const HTTPClientView = ({model, index}) => {
value={currentModel.path}
onChange={handlePathChange}
className="w-full"
inputMode="text"
/>
</div>
<div className="flex gap-2 shrink-0">
@@ -298,15 +324,15 @@ export const HTTPClientView = ({model, index}) => {
<TabsContent value="params" className="h-full m-0">
<PropertyTable
properties={currentModel.queryParams}
onChange={handlePathChange}
properties={currentModel.queryParams || []}
onChange={handleQueryParamsChange}
/>
</TabsContent>
{/* Headers Tab Content */}
<TabsContent value="headers" className="h-full m-0">
<PropertyTable
properties={currentModel.headers}
properties={currentModel.headers || []}
onChange={handleHeadersChange}
/>
</TabsContent>
@@ -106,10 +106,18 @@ const EditableInput = ({
return parts;
};
// useEffect(() => {
// if (value !== internalValue) {
// setInternalValue(value);
// }
// }, [value]);
const handleInput = (e) => {
console.log('handleInput', e);
const newValue = e.target.textContent;
const newSelection = saveSelection();
console.log(newValue);
setInternalValue(newValue);
setSelection(newSelection);
onChange(newValue);
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, {useEffect, useState} from 'react';
import {
Table,
TableBody,
@@ -20,6 +20,10 @@ const PropertyTable = ({
}) => {
const [localProperties, setLocalProperties] = useState(properties);
useEffect(() => {
setLocalProperties(properties);
}, [properties]);
// Handle property changes without causing infinite loops
const handlePropertyChange = (index, field, value) => {
const updatedProperties = localProperties.map((prop, i) => {
@@ -29,7 +33,9 @@ const PropertyTable = ({
return prop;
});
setLocalProperties(updatedProperties);
onChange?.(updatedProperties);
requestAnimationFrame(() => {
onChange?.(updatedProperties);
});
};
// Add new property
@@ -62,8 +68,8 @@ const PropertyTable = ({
<TableHeader>
<TableRow>
<TableHead className="w-14">Enable</TableHead>
<TableHead>Key</TableHead>
<TableHead>Value</TableHead>
<TableHead className="w-[40%]">Key</TableHead>
<TableHead className="w-[40%]">Value</TableHead>
<TableHead className="w-14">
<Button
onClick={handleAdd}
@@ -90,8 +96,8 @@ const PropertyTable = ({
<TableCell>
<EditableInput
value={property.key}
onChange={(e) =>
handlePropertyChange(index, 'key', e.target.value)
onChange={(value) =>
handlePropertyChange(index, 'key', value)
}
className="w-full"
/>
@@ -99,8 +105,8 @@ const PropertyTable = ({
<TableCell>
<EditableInput
value={property.value}
onChange={(e) =>
handlePropertyChange(index, 'value', e.target.value)
onChange={(value) =>
handlePropertyChange(index, 'value',value)
}
className="w-full"
/>
@@ -52,11 +52,9 @@ export const APIProvider = ({
try {
const validatedServers = await servers$.loadServers();
setServers(validatedServers);
console.log({validatedServers});
return validatedServers;
} catch (err) {
setError(err.message);
console.log({err});
return [];
} finally {
setLoading(false);
@@ -65,7 +63,6 @@ export const APIProvider = ({
const getServer = useCallback((serverId) => {
const server = servers.find(server => server.id === serverId);
console.log(server);
return server;
}, [servers]);
+6 -4
View File
@@ -2,7 +2,6 @@ import { getCSRFToken } from '@/services/token.js';
class APIClient {
constructor() {
this.globals = {};
this.abortController = null;
this.contextPath = document.querySelector('meta[name="context-path"]')?.getAttribute('content') || '/';
this.contextPath = this.contextPath.endsWith('/') ? this.contextPath : this.contextPath + '/';
@@ -24,8 +23,7 @@ class APIClient {
onLog(`Request: ${processedRequest.method} ${processedRequest.path}`);
const response = await this.makeRequest(model);
const response = await this.makeRequest(processedRequest);
const formattedResponse = this.formatResponse(response);
const finalResponse = await this.executePostRequestScript(
model.postRequestScript,
@@ -86,14 +84,18 @@ class APIClient {
replacePlaceholdersWithVariables(original, variables) {
const request = JSON.parse(JSON.stringify(original));
const mergedVariables = { ...this.globals, ...variables };
const mergedVariables = { ...window.globals, ...variables };
console.log(mergedVariables);
Object.entries(mergedVariables).forEach(([key, value]) => {
const placeholder = new RegExp(`{{${key}}}`, 'g');
console.log(placeholder);
request.path = request.path?.replace(placeholder, value);
request.requestBody = request.requestBody?.replace(placeholder, value);
console.log("REQUEST BODY:",request.requestBody);
if (request.headers) {
const headersStr = JSON.stringify(request.headers).replace(placeholder, value);
request.headers = JSON.parse(headersStr);