PropertyTable 수정
This commit is contained in:
@@ -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]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user