시나리오
This commit is contained in:
@@ -4,6 +4,8 @@ import MainContentArea from '@/components/MainContentArea.jsx';
|
||||
import {useAPI} from '@/providers/APIProvider.jsx';
|
||||
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';
|
||||
|
||||
const menuConfig = [
|
||||
{
|
||||
@@ -78,7 +80,9 @@ const APITester = () => {
|
||||
const [isApiMode, setIsApiMode] = useState(true);
|
||||
const [showAddCollection, setShowAddCollection] = useState(false);
|
||||
const [showAddApi, setShowAddApi] = useState(false);
|
||||
const [showAddScenario, setShowAddScenario] = useState(false);
|
||||
const { handleLogout, createCollection, createAPIRequest } = useAPI();
|
||||
const {createScenario} = useLoadTest();
|
||||
|
||||
// 모드 전환 핸들러
|
||||
const toggleMode = () => {
|
||||
@@ -104,6 +108,15 @@ const APITester = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddScenario = async (name) => {
|
||||
try {
|
||||
await createScenario(name);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('시나리오 생성에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleMenuClick = async (action) => {
|
||||
// Handle menu item clicks
|
||||
@@ -115,6 +128,9 @@ const APITester = () => {
|
||||
case 'collection-add':
|
||||
setShowAddCollection(true);
|
||||
break;
|
||||
case 'scenario-add':
|
||||
setShowAddScenario(true);
|
||||
break;
|
||||
case 'logout':
|
||||
const success = await handleLogout();
|
||||
if (success) {
|
||||
@@ -176,6 +192,11 @@ const APITester = () => {
|
||||
onOpenChange={setShowAddApi}
|
||||
onSubmit={handleAddApi}
|
||||
/>
|
||||
<AddScenarioDialog
|
||||
open={showAddScenario}
|
||||
onOpenChange={setShowAddScenario}
|
||||
onSubmit={handleAddScenario}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import ScenarioEditor from '@/components/api-tester/ScenarioEditor.jsx';
|
||||
|
||||
const MainContentArea = ({ isApiMode, toggleMode }) => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="h-[calc(100vh-57px)] w-full overflow-hidden">
|
||||
{isApiMode ? <APIMode isApiMode={isApiMode} onToggleMode={toggleMode}/> : <ScenarioMode isApiMode={isApiMode} onToggleMode={toggleMode}/>}
|
||||
</div>
|
||||
);
|
||||
@@ -128,22 +128,19 @@ const APIMode = ({ isApiMode, onToggleMode }) => {
|
||||
|
||||
// 시나리오 모드
|
||||
const ScenarioMode = ({ isApiMode, onToggleMode }) => (
|
||||
<div className="scenario-mode flex flex-1">
|
||||
<div className="scenario-mode flex flex-1 h-[calc(100vh-57px)]">
|
||||
{/* 왼쪽 고정: ScenarioNavigation */}
|
||||
<div className="navigation-sidebar min-w-[200px] border-r bg-gray-100">
|
||||
<ScenarioNavigation
|
||||
contextPath="/"
|
||||
side="left"
|
||||
onScenarioSelect={(scenarioId) => {
|
||||
// Handle scenario selection
|
||||
}}
|
||||
isApiMode={isApiMode}
|
||||
onToggleMode={onToggleMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 중앙: ScenarioEditor */}
|
||||
<div className="editor flex-1">
|
||||
<div className="editor flex-1 h-[calc(100vh-57px)] overflow-hidden">
|
||||
<ScenarioEditor/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import ErrorAlertDialog from '@/components/shared/ErrorAlertDialog.jsx';
|
||||
|
||||
const AddScenarioDialog = ({ open, onOpenChange, onSubmit }) => {
|
||||
const [scenarioName, setScenarioName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!scenarioName.trim()) {
|
||||
setError('시나리오 이름을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const success = await onSubmit(scenarioName.trim());
|
||||
if (success) {
|
||||
setScenarioName('');
|
||||
setError('');
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
setError('시나리오 생성에 실패했습니다');
|
||||
setShowErrorDialog(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message || '시나리오 생성에 실패했습니다');
|
||||
setShowErrorDialog(true);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (open) => {
|
||||
if (!open) {
|
||||
setScenarioName('');
|
||||
setError('');
|
||||
}
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>새 시나리오 추가</DialogTitle>
|
||||
<DialogDescription>
|
||||
새로운 시나리오의 이름을 입력해주세요
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">시나리오 이름</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={scenarioName}
|
||||
onChange={(e) => {
|
||||
setScenarioName(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
placeholder="시나리오 이름을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '생성 중...' : '생성'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ErrorAlertDialog
|
||||
open={showErrorDialog}
|
||||
onOpenChange={setShowErrorDialog}
|
||||
title="시나리오 생성 오류"
|
||||
description={error}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddScenarioDialog;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
const DeleteScenarioDialog = ({ open, onOpenChange, onConfirm, scenarioName }) => {
|
||||
const handleConfirm = async () => {
|
||||
await onConfirm();
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>시나리오 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
'{scenarioName}' 시나리오를 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없으며, 시나리오의 모든 구성요소가 함께 삭제됩니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirm}
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteScenarioDialog;
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, Position, Handle, ReactFlowProvider } from '@xyflow/react';
|
||||
import React, { useCallback, memo, useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { Play } from 'lucide-react';
|
||||
import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, Position, Handle, ReactFlowProvider} from '@xyflow/react';
|
||||
import React, {useCallback, memo, useMemo, useRef, useState, useEffect, forwardRef, useImperativeHandle} from 'react';
|
||||
import {Play} from 'lucide-react';
|
||||
import {useLoadTest} from '@/providers/LoadTestProvider.jsx';
|
||||
|
||||
// Optimized node components with proper prop types and memoization
|
||||
const StartNode = memo(({ data, isConnectable }) => {
|
||||
const StartNode = memo(({data, isConnectable}) => {
|
||||
// Memoize handles configuration
|
||||
const handles = useMemo(() => ([
|
||||
{ id: 'start-right', type: 'source', position: Position.Right }
|
||||
{id: 'start-right', type: 'source', position: Position.Right}
|
||||
]), []);
|
||||
|
||||
return (
|
||||
@@ -21,18 +22,18 @@ const StartNode = memo(({ data, isConnectable }) => {
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Play className="h-4 w-4 text-green-600" />
|
||||
<Play className="h-4 w-4 text-green-600"/>
|
||||
<span className="font-semibold text-sm text-green-700">Start</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ApiNode = memo(({ data, isConnectable }) => {
|
||||
const ApiNode = memo(({data, isConnectable}) => {
|
||||
// Memoize handles and colors
|
||||
const handles = useMemo(() => ([
|
||||
{ id: `${data.id}-left`, type: 'target', position: Position.Left },
|
||||
{ id: `${data.id}-right`, type: 'source', position: Position.Right }
|
||||
{id: `${data.id}-left`, type: 'target', position: Position.Left},
|
||||
{id: `${data.id}-right`, type: 'source', position: Position.Right}
|
||||
]), [data.id]);
|
||||
|
||||
const methodColor = useMemo(() => {
|
||||
@@ -59,7 +60,7 @@ const ApiNode = memo(({ data, isConnectable }) => {
|
||||
))}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${methodColor}`} />
|
||||
<div className={`w-2 h-2 rounded-full ${methodColor}`}/>
|
||||
<span className="font-bold text-sm">{data.method}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +72,7 @@ const ApiNode = memo(({ data, isConnectable }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const ConditionNode = memo(({ data, isConnectable }) => {
|
||||
const ConditionNode = memo(({data, isConnectable}) => {
|
||||
// Memoize handle styles
|
||||
const handleStyles = useMemo(() => ({
|
||||
true: {
|
||||
@@ -113,7 +114,7 @@ const ConditionNode = memo(({ data, isConnectable }) => {
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500" />
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500"/>
|
||||
<span className="font-bold text-sm">Condition</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,10 +125,10 @@ const ConditionNode = memo(({ data, isConnectable }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const DelayNode = memo(({ data, isConnectable }) => {
|
||||
const DelayNode = memo(({data, isConnectable}) => {
|
||||
const handles = useMemo(() => ([
|
||||
{ id: `${data.id}-left`, type: 'target', position: Position.Left },
|
||||
{ id: `${data.id}-right`, type: 'source', position: Position.Right }
|
||||
{id: `${data.id}-left`, type: 'target', position: Position.Left},
|
||||
{id: `${data.id}-right`, type: 'source', position: Position.Right}
|
||||
]), [data.id]);
|
||||
|
||||
return (
|
||||
@@ -143,7 +144,7 @@ const DelayNode = memo(({ data, isConnectable }) => {
|
||||
))}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500" />
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500"/>
|
||||
<span className="font-bold text-sm">Delay</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,18 +161,6 @@ ApiNode.displayName = 'ApiNode';
|
||||
ConditionNode.displayName = 'ConditionNode';
|
||||
DelayNode.displayName = 'DelayNode';
|
||||
|
||||
// Memoize initial nodes and edges
|
||||
const initialNodes = [
|
||||
{
|
||||
id: 'start',
|
||||
type: 'startNode',
|
||||
position: { x: 100, y: 50 },
|
||||
data: { id: 'start' }
|
||||
}
|
||||
];
|
||||
|
||||
const initialEdges = [];
|
||||
|
||||
// Memoize node types
|
||||
const nodeTypes = {
|
||||
startNode: StartNode,
|
||||
@@ -180,11 +169,40 @@ const nodeTypes = {
|
||||
delayNode: DelayNode
|
||||
};
|
||||
|
||||
const DiagramArea = () => {
|
||||
const DiagramArea = forwardRef(({onSave}, ref) => {
|
||||
const {currentFlowData} = useLoadTest();
|
||||
const reactFlowWrapper = useRef(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(currentFlowData?.edges || []);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(currentFlowData?.nodes || [
|
||||
{
|
||||
id: 'start',
|
||||
type: 'startNode',
|
||||
position: {x: 100, y: 50},
|
||||
data: {id: 'start'}
|
||||
}]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFlowData) {
|
||||
const {nodes: scenarioNodes, edges: scenarioEdges} = currentFlowData;
|
||||
setNodes(scenarioNodes?.length ? scenarioNodes : [
|
||||
{
|
||||
id: 'start',
|
||||
type: 'startNode',
|
||||
position: {x: 100, y: 50},
|
||||
data: {id: 'start'}
|
||||
}]);
|
||||
setEdges(scenarioEdges || []);
|
||||
}
|
||||
}, [currentFlowData, setNodes, setEdges]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getFlowData: () => ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport: reactFlowWrapper.current?.getViewport()
|
||||
})
|
||||
}));
|
||||
|
||||
// Click handlers for node selection
|
||||
const onNodeClick = useCallback((event, node) => {
|
||||
@@ -206,9 +224,11 @@ const DiagramArea = () => {
|
||||
);
|
||||
}, [selectedNodeId, setNodes]);
|
||||
|
||||
// Memoize callback functions
|
||||
const onConnect = useCallback((params) => {
|
||||
setEdges(prev => [...prev, { ...params, id: `e${prev.length + 1}` }]);
|
||||
setEdges(prev => {
|
||||
const newId = `edge-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
return [...prev, {...params, id: newId}];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onInit = useCallback((instance) => {
|
||||
@@ -223,25 +243,24 @@ const DiagramArea = () => {
|
||||
const onDrop = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!reactFlowWrapper.current) return;
|
||||
if (!reactFlowWrapper.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiData = JSON.parse(event.dataTransfer.getData('application/json'));
|
||||
const position = reactFlowWrapper.current.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
y: event.clientY
|
||||
});
|
||||
|
||||
const newNode = {
|
||||
id: `api-${Date.now()}`,
|
||||
id: `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'apiNode',
|
||||
position,
|
||||
data: {
|
||||
id: apiData.id,
|
||||
name: apiData.name,
|
||||
method: apiData.method,
|
||||
path: apiData.path,
|
||||
collectionId: apiData.collectionId
|
||||
...apiData,
|
||||
id: `api-${Date.now()}` // Ensure unique API ID
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,34 +271,35 @@ const DiagramArea = () => {
|
||||
}, [setNodes]);
|
||||
|
||||
// Default edge options memoized
|
||||
const defaultEdgeOptions = useMemo(() => ({ type: 'smoothstep' }), []);
|
||||
const defaultEdgeOptions = useMemo(() => ({type: 'smoothstep'}), []);
|
||||
|
||||
return (
|
||||
<div className="h-[50vh] border-b">
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={onInit}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
defaultZoom={1.0} // 초기 배율 설정 (1이 기본값)
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={onInit}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
proOptions={{hideAttribution: true}}
|
||||
>
|
||||
<Background/>
|
||||
<Controls/>
|
||||
<MiniMap/>
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
DiagramArea.displayName = 'DiagramArea';
|
||||
|
||||
export default memo(DiagramArea);
|
||||
|
||||
@@ -37,18 +37,7 @@ const DraggableApiItem = ({ api, collectionId, onApiAction }) => {
|
||||
|
||||
const handleDragStart = (event) => {
|
||||
// Prepare API data for drag operation
|
||||
const apiData = {
|
||||
id: api.id,
|
||||
name: api.name,
|
||||
method: api.method || 'GET',
|
||||
path: api.path || '/',
|
||||
collectionId: collectionId,
|
||||
headers: api.headers || [],
|
||||
parameters: api.parameters || [],
|
||||
body: api.body || '',
|
||||
description: api.description || '',
|
||||
type: api.type || 'HTTP'
|
||||
};
|
||||
const apiData = {...api};
|
||||
|
||||
// Create drag image for better visual feedback
|
||||
const dragImage = document.createElement('div');
|
||||
|
||||
@@ -1,48 +1,97 @@
|
||||
import React, {useState} from 'react';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import MonacoEditor from '@/components/shared/MonacoEditor';
|
||||
import ScenarioMenuBar from '@/components/api-tester/ScenarioMenuBar.jsx';
|
||||
import DiagramArea from '@/components/api-tester/DiagramArea.jsx';
|
||||
import {useLoadTest} from '@/providers/LoadTestProvider.jsx';
|
||||
import {useScenarioExecutor} from '@/services/ScenarioExecutor';
|
||||
import APIClient from '@/services/APIClient.js';
|
||||
import {Button} from '@/components/ui/button.jsx';
|
||||
|
||||
const apiClient = new APIClient();
|
||||
|
||||
// ConsoleArea Component
|
||||
const ConsoleArea = () => {
|
||||
const [consoleOutput, setConsoleOutput] = useState('');
|
||||
const ConsoleArea = ({value}) => (
|
||||
|
||||
return (
|
||||
<div className="h-[50vh]">
|
||||
<MonacoEditor
|
||||
id="scenario-console"
|
||||
value={consoleOutput}
|
||||
language="plaintext"
|
||||
readOnly={true}
|
||||
height="full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<MonacoEditor
|
||||
id="scenario-console"
|
||||
value={value}
|
||||
language="plaintext"
|
||||
readOnly={true}
|
||||
height="full"
|
||||
/>
|
||||
|
||||
);
|
||||
|
||||
// Main ScenarioEditor Component
|
||||
const ScenarioEditor = () => {
|
||||
const handleSave = () => {
|
||||
console.log('Saving scenario...');
|
||||
const diagramRef = useRef(null);
|
||||
const {currentScenario, currentFlowData, updateScenario} = useLoadTest();
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
const {
|
||||
logs,
|
||||
clearLogs,
|
||||
executeScenario,
|
||||
executeStep,
|
||||
resetExecutor,
|
||||
pauseExecution,
|
||||
isRunning
|
||||
} = useScenarioExecutor(
|
||||
currentFlowData?.nodes || [],
|
||||
currentFlowData?.edges || [],
|
||||
apiClient
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resetExecutor();
|
||||
clearLogs();
|
||||
}, [currentFlowData]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
console.log(currentScenario);
|
||||
const flowData = diagramRef.current?.getFlowData();
|
||||
await updateScenario({
|
||||
...currentScenario,
|
||||
scenario: JSON.stringify(flowData)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save scenario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStepRun = () => {
|
||||
console.log('Running step...');
|
||||
};
|
||||
const handleStepRun = useCallback(async () => {
|
||||
setIsExecuting(true);
|
||||
try {
|
||||
await executeStep();
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
}, [executeStep]);
|
||||
|
||||
const handleFullRun = () => {
|
||||
console.log('Running all steps...');
|
||||
};
|
||||
const handleFullRun = useCallback(async () => {
|
||||
setIsExecuting(true);
|
||||
try {
|
||||
await executeScenario();
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
}, [executeScenario]);
|
||||
|
||||
const handlePause = () => {
|
||||
console.log('Pausing execution...');
|
||||
};
|
||||
const handlePause = useCallback(() => {
|
||||
pauseExecution();
|
||||
}, [pauseExecution]);
|
||||
|
||||
const handleReset = () => {
|
||||
console.log('Resetting scenario...');
|
||||
};
|
||||
const handleReset = useCallback(() => {
|
||||
resetExecutor();
|
||||
clearLogs();
|
||||
}, [resetExecutor, clearLogs]);
|
||||
|
||||
if (!currentFlowData) {
|
||||
return <div className="flex items-center justify-center h-full">Select a scenario to edit</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -52,9 +101,35 @@ const ScenarioEditor = () => {
|
||||
onFullRun={handleFullRun}
|
||||
onPause={handlePause}
|
||||
onReset={handleReset}
|
||||
disabled={isExecuting}
|
||||
isRunning={isRunning}
|
||||
/>
|
||||
<DiagramArea/>
|
||||
<ConsoleArea/>
|
||||
<div className="flex-1 min-h-0 grid grid-rows-2">
|
||||
<div className="min-h-0 border-b">
|
||||
<DiagramArea
|
||||
ref={diagramRef}
|
||||
initialNodes={currentFlowData?.nodes || []}
|
||||
initialEdges={currentFlowData?.edges || []}
|
||||
isExecuting={isExecuting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 p-1 border-b">
|
||||
<div>Output: </div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm">
|
||||
숨기기
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm">
|
||||
보이기
|
||||
</Button>
|
||||
</div>
|
||||
<ConsoleArea value={logs}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import { FileText, MoreHorizontal } from 'lucide-react';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -23,6 +23,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useLoadTest } from '@/providers/LoadTestProvider';
|
||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||
import DeleteScenarioDialog from '@/components/api-tester/DeleteScenarioDialog.jsx';
|
||||
|
||||
const ScenarioDropdownMenu = ({ onOpen, onRename, onDelete }) => (
|
||||
<DropdownMenu>
|
||||
@@ -64,16 +65,21 @@ const ErrorMessage = ({ message }) => (
|
||||
|
||||
const ScenarioNavigation = ({
|
||||
contextPath = '/',
|
||||
onScenarioSelect,
|
||||
side,
|
||||
isApiMode,
|
||||
onToggleMode
|
||||
}) => {
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [scenarioToDelete, setScenarioToDelete] = useState(null);
|
||||
|
||||
const {
|
||||
scenarios,
|
||||
loading,
|
||||
error,
|
||||
loadScenarioList
|
||||
deleteScenario,
|
||||
loadScenarioList,
|
||||
selectScenario
|
||||
} = useLoadTest();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,14 +87,18 @@ const ScenarioNavigation = ({
|
||||
}, [loadScenarioList]);
|
||||
|
||||
const handleScenarioAction = {
|
||||
open: (scenarioId) => {
|
||||
if (onScenarioSelect) {
|
||||
onScenarioSelect(scenarioId);
|
||||
open: async (scenarioId) => {
|
||||
try {
|
||||
console.log({scenarioId});
|
||||
selectScenario(scenarioId);
|
||||
} catch (err) {
|
||||
console.error('Failed to select scenario:', err);
|
||||
}
|
||||
},
|
||||
delete: async (scenarioId) => {
|
||||
// TODO: Implement delete functionality
|
||||
console.log('Delete scenario:', scenarioId);
|
||||
delete: async (scenario) => {
|
||||
console.log('delete', scenario);
|
||||
setScenarioToDelete(scenario);
|
||||
setDeleteDialogOpen(true);
|
||||
},
|
||||
rename: (scenarioId) => {
|
||||
// TODO: Implement rename functionality
|
||||
@@ -96,6 +106,14 @@ const ScenarioNavigation = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (scenarioToDelete) {
|
||||
await deleteScenario(scenarioToDelete);
|
||||
setDeleteDialogOpen(false);
|
||||
setScenarioToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const renderScenarioItem = (scenario) => (
|
||||
<SidebarMenuItem key={scenario.id}>
|
||||
<SidebarMenuButton
|
||||
@@ -117,7 +135,7 @@ const ScenarioNavigation = ({
|
||||
<ScenarioDropdownMenu
|
||||
onOpen={() => handleScenarioAction.open(scenario.id)}
|
||||
onRename={() => handleScenarioAction.rename(scenario.id)}
|
||||
onDelete={() => handleScenarioAction.delete(scenario.id)}
|
||||
onDelete={() => handleScenarioAction.delete(scenario)}
|
||||
/>
|
||||
</SidebarMenuAction>
|
||||
</div>
|
||||
@@ -161,6 +179,12 @@ const ScenarioNavigation = ({
|
||||
</Button>
|
||||
</SidebarHeader>
|
||||
{renderContent()}
|
||||
<DeleteScenarioDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
scenarioName={scenarioToDelete?.name || ''}
|
||||
/>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import APIClient from '../../lib/api-client';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
|
||||
import {Card, CardContent} from '@/components/ui/card';
|
||||
import LayoutGrid from '../layout/LayoutGrid';
|
||||
import LayoutTreeGrid from '../layout/LayoutTreeGrid';
|
||||
import LayoutTreeDataGrid from '../layout/LayoutTreeDataGrid';
|
||||
import ApiLayoutItemTreeBuilder from '../../lib/ApiLayoutItemTreeBuilder';
|
||||
import ApiLayoutItemTreeDataBuilder from '../../lib/ApiLayoutItemTreeDataBuilder';
|
||||
import VariableSnippet from '../shared/VariableSnippet';
|
||||
import APIClient from '@/services/APIClient.js';
|
||||
|
||||
export const TCPClientView = ({ parent, serverManager, model, index }) => {
|
||||
const [apiClient] = useState(() => new APIClient());
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { cn } from "@/lib/utils";
|
||||
import EditableInput from '../shared/EditableInput';
|
||||
import APIClient from '@/lib/api-client';
|
||||
import _ from 'lodash';
|
||||
import APIClient from '@/services/APIClient.js';
|
||||
|
||||
// Helper function to get indentation for tree visualization
|
||||
const getIndent = (depth) => {
|
||||
|
||||
Reference in New Issue
Block a user