시나리오

This commit is contained in:
현성필
2025-01-25 09:51:11 +09:00
parent 097b2c79c2
commit bca5840712
17 changed files with 989 additions and 515 deletions
+21
View File
@@ -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) => {
-272
View File
@@ -1,272 +0,0 @@
export const fillPadding = async (apiRequest, value, dataLength) => {
let paddedValue = value; // In real app, this would be processed by APIClient
while (paddedValue.length < dataLength) {
paddedValue = ' ' + paddedValue;
}
return paddedValue;
};
class APIClient {
constructor() {
// Initialize global variables store
window.globals = window.globals || {};
this.abortController = null;
this.contextPath = document.querySelector('meta[name="context-path"]')?.getAttribute('content') || '/';
if (!this.contextPath.endsWith('/')) {
this.contextPath += '/';
}
}
getClientUrl() {
return `${this.contextPath}mgmt/api/test.do`;
}
// Format response for consistency
formatResponse(data) {
return {
body: data.body,
status: data.status,
headers: data.headers.reduce((acc, header) => {
acc[header.key] = header.value;
return acc;
}, {}),
size: data.size
};
}
// Abort ongoing requests
abort() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}
// Make the actual API request
async makeAjaxRequest(processedRequest) {
this.abortController = new AbortController();
try {
const response = await fetch(this.getClientUrl(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify(processedRequest),
signal: this.abortController.signal
});
if (!response.ok) {
throw new Error('Network request failed');
}
return response;
} catch (error) {
if (error.name === 'AbortError') {
// Handle abort silently
return null;
}
throw error;
}
}
// Process pre-script and variables
async processPreScriptAndVariables(model, value, consoleOutput) {
const variables = {};
const updatedModel = await this.executePreRequestScript(model, variables, consoleOutput);
return this.replacePlaceholderValueWithVariables(updatedModel, value, variables);
}
// Replace placeholder values with actual variables
replacePlaceholderValueWithVariables(model, originalValue, variables = {}) {
const mergedVariables = {
...window.globals,
...variables
};
return Object.entries(mergedVariables).reduce((value, [key, replacement]) => {
const placeholder = `{{${key}}}`;
return value.replace(new RegExp(placeholder, 'g'), replacement);
}, originalValue);
}
// Main method to send API requests
async sendAPIRequest(model, preProcessCallback, consoleOutput) {
try {
// Execute pre-request script and get variables
const variables = {};
const updatedModel = await this.executePreRequestScript(model, variables, consoleOutput);
// Process request with variables
const processedRequest = this.replacePlaceholdersWithVariables(updatedModel, variables);
// Allow pre-processing if callback provided
if (preProcessCallback) {
preProcessCallback(processedRequest);
}
// Make the request
const response = await this.makeAjaxRequest(processedRequest, consoleOutput);
if (!response) return null;
const responseData = await response.json();
if (!response.ok) {
throw new Error(responseData.error || 'Unknown error');
}
// Format response
const finalResponse = this.formatResponse(responseData);
// Execute post-request script
return this.executePostRequestScript(
model.postRequestScript,
variables,
processedRequest,
finalResponse,
consoleOutput
).catch(error => {
console.error('Post-request script error:', error);
return finalResponse;
});
} catch (error) {
throw new Error(`API 호출 중 오류가 발생했습니다. 사유[${error.message}]`);
}
}
// Replace placeholders in request with variables
replacePlaceholdersWithVariables(original, variables = {}) {
const request = JSON.parse(JSON.stringify(original));
const mergedVariables = {
...window.globals,
...variables
};
Object.entries(mergedVariables).forEach(([key, value]) => {
const placeholder = new RegExp(`{{${key}}}`, 'g');
// Replace in path
request.path = request.path.replace(placeholder, value);
// Replace in request body
if (request.requestBody) {
request.requestBody = request.requestBody.replace(placeholder, value);
}
// Replace in headers
if (request.headers?.length) {
request.headers = JSON.parse(
JSON.stringify(request.headers).replace(placeholder, value)
);
}
// Replace in query params
if (request.queryParams?.length) {
request.queryParams = JSON.parse(
JSON.stringify(request.queryParams).replace(placeholder, value)
);
}
});
return request;
}
// Execute pre-request script in an iframe
executePreRequestScript(model, variables, consoleOutput) {
return new Promise((resolve, reject) => {
const resultFrame = document.getElementById('preRequest');
if (!resultFrame) {
console.warn('preRequest iframe not found');
resolve(model);
return;
}
window.preRequestComplete = (updatedModel, error) => {
if (error) {
reject(error);
} else {
resolve(updatedModel);
}
};
window.temp_variable = variables;
window.currentRequest = model;
window.consoleOutput = consoleOutput;
resultFrame.srcdoc = `
<script>
var oldLog = console.log;
console.log = function(message) {
if(window.parent.consoleOutput)
window.parent.consoleOutput(message);
oldLog.apply(console, arguments);
};
window.globals = window.parent.globals;
window.variables = window.parent.temp_variable;
window.request = window.parent.currentRequest;
try {
${model.preRequestScript || ''}
window.parent.preRequestComplete(window.request, null);
} catch (error) {
window.parent.preRequestComplete(null, error);
}
</script>
`;
});
}
// Execute post-request script in an iframe
executePostRequestScript(script, variables, request, response, consoleOutput) {
return new Promise((resolve, reject) => {
const resultFrame = document.getElementById('postRequest');
if (!resultFrame) {
console.warn('postRequest iframe not found');
resolve(response);
return;
}
window.request = request;
window.response = response;
window.temp_variable = variables;
window.postRequestComplete = (response, error) => {
if (error) {
reject(error);
} else {
resolve(response);
}
};
window.consoleOutput = consoleOutput;
resultFrame.srcdoc = `
<script>
var oldLog = console.log;
console.log = function(message) {
if(window.parent.consoleOutput)
window.parent.consoleOutput(message);
oldLog.apply(console, arguments);
};
window.globals = window.parent.globals;
window.variables = window.parent.temp_variable;
window.request = window.parent.request;
window.response = window.parent.response;
try {
${script || ''}
window.parent.postRequestComplete(window.response, null);
} catch (error) {
window.parent.postRequestComplete(window.response, error.toString());
}
</script>
`;
});
}
}
export default APIClient;
+1 -1
View File
@@ -1,9 +1,9 @@
import React, {createContext, useState, useContext, useCallback, useEffect} from 'react';
import APIClient from '../lib/api-client';
import {collectionService} from '@/services/collectionService';
import {apiRequestService} from '@/services/apiRequestService';
import {serverService} from '@/services/serverService';
import {authService} from '@/services/authService';
import APIClient from '@/services/APIClient.js';
const APIContext = createContext(null);
+109 -109
View File
@@ -1,51 +1,110 @@
// providers/LoadTestProvider.jsx
import React, { createContext, useState, useContext, useCallback } from 'react';
import { loadTestService } from '@/services/loadTestService';
import { scenarioService } from '@/services/scenarioService';
// Create the context
const LoadTestContext = createContext();
// Provider component
export const LoadTestProvider = ({ children, contextPath = '/' }) => {
const [tests, setTests] = useState([]);
const [scenarios, setScenarios] = useState([]);
const [currentTest, setCurrentTest] = useState(null);
const [currentScenario, setCurrentScenario] = useState(null);
const [currentFlowData, setCurrentFlowData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Load scenario list
// Initialize services
const loadTests$ = loadTestService(contextPath);
const scenarios$ = scenarioService(contextPath);
const loadScenarioList = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}mgmt/api_scenario/list.do`, {
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
}
});
if (!response.ok) throw new Error('Failed to load scenarios');
const data = await response.json();
const data = await scenarios$.loadScenarios();
setScenarios(data);
return data;
} catch (err) {
setError(err.message);
return [];
} finally {
setLoading(false);
}
}, [contextPath]);
}, []);
const selectScenario = useCallback((scenarioId) => {
const scenario = scenarios.find(s => s.id === scenarioId);
if (!scenario) {
setError('Scenario not found');
return null;
}
let flowData = { nodes: [], edges: [] };
if (scenario.scenario) {
try {
flowData = JSON.parse(scenario.scenario);
} catch (e) {
console.warn('Failed to parse scenario data:', e);
}
}
console.log(scenario);
console.log(flowData);
setCurrentScenario(scenario);
setCurrentFlowData(flowData);
return flowData;
}, [scenarios]);
const createScenario = useCallback(async (name) => {
setLoading(true);
setError(null);
try {
const newScenario = await scenarios$.createScenario(name);
await loadScenarioList();
return newScenario;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, [loadScenarioList]);
const updateScenario = useCallback(async (scenario) => {
setLoading(true);
setError(null);
try {
const updated = await scenarios$.updateScenario(scenario);
await loadScenarioList();
return updated;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, [loadScenarioList]);
const deleteScenario = useCallback(async (scenario) => {
setLoading(true);
setError(null);
try {
const newScenario = await scenarios$.deleteScenario(scenario);
await loadScenarioList();
return newScenario;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, [loadScenarioList]);
// Load test list
const loadTestList = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}mgmt/load_test/list.do`, {
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
}
});
if (!response.ok) throw new Error('Failed to load tests');
const data = await response.json();
const data = await loadTests$.loadTests();
setTests(data);
return data;
} catch (err) {
@@ -54,24 +113,13 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
} finally {
setLoading(false);
}
}, [contextPath]);
}, []);
// Create a new test
const createTest = useCallback(async (testName) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}mgmt/load_test/create.do`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify({ name: testName })
});
if (!response.ok) throw new Error('Failed to create test');
const newTest = await response.json();
const newTest = await loadTests$.createTest(testName);
await loadTestList();
return newTest;
} catch (err) {
@@ -80,60 +128,32 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
} finally {
setLoading(false);
}
}, [contextPath, loadTestList]);
}, [loadTestList]);
// Update a test
const updateTest = useCallback(async (test) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}mgmt/load_test/update.do`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify(test)
});
if (!response.ok) throw new Error('Failed to update test');
// Update local state
setTests(prevTests =>
prevTests.map(t => t.id === test.id ? { ...t, ...test } : t)
);
return test;
const updatedTest = await loadTests$.updateTest(test);
setTests(prev => prev.map(t => t.id === test.id ? updatedTest : t));
return updatedTest;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
}, [contextPath]);
}, []);
// Delete a test
const deleteTest = useCallback(async (testId) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}mgmt/load_test/delete.do`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify({ id: testId })
});
if (!response.ok) throw new Error('Failed to delete test');
// Remove test from local state
setTests(prevTests => prevTests.filter(t => t.id !== testId));
// Clear current test if deleted
await loadTests$.deleteTest(testId);
setTests(prev => prev.filter(t => t.id !== testId));
if (currentTest?.id === testId) {
setCurrentTest(null);
}
return true;
} catch (err) {
setError(err.message);
@@ -141,60 +161,43 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
} finally {
setLoading(false);
}
}, [contextPath, currentTest]);
}, [currentTest?.id]);
// Start a load test
const startTest = useCallback(async (test) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}startLoadTest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify(test)
});
if (!response.ok) throw new Error('Failed to start test');
return true;
return await loadTests$.startTest(test);
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
}, [contextPath]);
}, []);
// Stop a load test
const stopTest = useCallback(async (test) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${contextPath}stopLoadTest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
},
body: JSON.stringify(test)
});
if (!response.ok) throw new Error('Failed to stop test');
return true;
return await loadTests$.stopTest(test);
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
}, [contextPath]);
}, []);
// Prepare context value
const contextValue = {
tests,
scenarios,
currentScenario,
currentFlowData,
createScenario,
updateScenario,
deleteScenario,
selectScenario,
currentTest,
loading,
error,
@@ -216,14 +219,11 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
);
};
// Custom hook to use the LoadTestContext
export const useLoadTest = () => {
const context = useContext(LoadTestContext);
if (!context) {
throw new Error('useLoadTest must be used within a LoadTestProvider');
}
return context;
};
+71
View File
@@ -0,0 +1,71 @@
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 + '/';
this.CLIENT_URL = 'mgmt/api/test.do';
}
abort() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}
async sendAPIRequest(model, onLog = () => {}) {
try {
onLog(`Request: ${model.method} ${model.path}`);
const response = await this.makeRequest(model);
const formattedResponse = this.formatResponse(response);
return formattedResponse;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request was cancelled');
}
throw new Error(`API Error: ${error.message}`);
} finally {
this.abortController = null;
}
}
async makeRequest(request) {
this.abortController = new AbortController();
const response = await fetch(this.getClientUrl(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCSRFToken()
},
body: JSON.stringify(request),
signal: this.abortController.signal
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Unknown error');
}
return response.json();
}
formatResponse(data) {
return {
body: data.body,
status: data.status,
headers: Object.fromEntries(data.headers.map(h => [h.key, h.value])),
size: data.size
};
}
getClientUrl() {
return this.contextPath + this.CLIENT_URL;
}
}
export default APIClient;
@@ -0,0 +1,273 @@
import { useCallback, useRef, useState } from 'react';
export class ScenarioExecutor {
constructor(nodes, edges, apiClient, onLogAppend, onNodeStatusChange) {
this.nodes = nodes;
this.edges = edges;
this.apiClient = apiClient;
this.onLogAppend = onLogAppend;
this.onNodeStatusChange = onNodeStatusChange;
this.reset();
}
reset() {
this.currentNodeId = this.findStartNode()?.id;
this.visitedNodes = new Set();
this.isRunning = false;
this.isPaused = false;
this.isDone = false;
this.nodes.forEach(node => {
this.onNodeStatusChange?.(node.id, 'default');
});
}
pause() {
this.isPaused = true;
this.isRunning = false;
}
resume() {
this.isPaused = false;
this.isRunning = true;
}
findStartNode() {
return this.nodes.find(node => node.type === 'startNode');
}
findNextNodes(nodeId) {
return this.edges
.filter(edge => edge.source === nodeId)
.map(edge => ({
nodeId: edge.target,
condition: edge.sourceHandle
}));
}
async executeCurrentNode() {
if (!this.currentNodeId || this.isPaused) return;
console.log(this.currentNodeId);
const currentNode = this.nodes.find(n => n.id === this.currentNodeId);
console.log({currentNode});
if (!currentNode) return;
this.isRunning = true;
this.visitedNodes.add(this.currentNodeId);
this.onNodeStatusChange?.(this.currentNodeId, 'running');
try {
let result;
switch (currentNode.type) {
case 'startNode':
this.appendLog('Starting API scenario\n');
break;
case 'apiNode':
result = await this.executeApiNode(currentNode);
break;
case 'delayNode':
await this.executeDelayNode(currentNode);
break;
case 'conditionNode':
result = await this.executeConditionNode(currentNode);
break;
}
this.onNodeStatusChange?.(this.currentNodeId, 'success');
return result;
} catch (error) {
console.error(error);
this.onNodeStatusChange?.(this.currentNodeId, 'error');
this.appendLog(`Error executing node ${currentNode.id}: ${error.message}`);
throw error;
}
}
async executeApiNode(node) {
const { data } = node;
this.appendLog(`Executing API [${data.name}]`);
try {
const response = await this.apiClient.sendAPIRequest(data, this.appendLog.bind(this));
console.log({response});
this.logApiResponse(response, data.type);
return response;
} catch (error) {
throw new Error(`API Error: ${error.message}`);
}
}
async executeDelayNode(node) {
const duration = node.data.duration || 1000;
this.appendLog(`Delay: ${duration}ms`);
await new Promise(resolve => setTimeout(resolve, duration));
}
async executeConditionNode(node) {
const { condition, script } = node.data;
this.appendLog(`Evaluating condition: ${condition}`);
try {
const result = await this.evaluateCondition(script || condition);
this.appendLog(`Condition result: ${result}`);
return result;
} catch (error) {
throw new Error(`Condition evaluation failed: ${error.message}`);
}
}
async evaluateCondition(script) {
return new Promise((resolve, reject) => {
const scriptExecutor = document.createElement('iframe');
scriptExecutor.style.display = 'none';
document.body.appendChild(scriptExecutor);
window.conditionComplete = (result, error) => {
document.body.removeChild(scriptExecutor);
if (error) reject(error);
else resolve(!!result);
};
const scriptContent = `
<script>
try {
const result = (function() { ${script} })();
window.parent.conditionComplete(result);
} catch (error) {
window.parent.conditionComplete(null, error);
}
</script>
`;
scriptExecutor.srcdoc = scriptContent;
});
}
appendLog(text) {
this.onLogAppend(text + '\n\n');
}
logApiResponse(response, type) {
const responseLog = type === 'HTTP'
? `Response:\nStatus: ${response.status}\nHeaders:\n${this.formatHeaders(response.headers)}\nBody:\n${response.body}`
: `Response:\n${response.body}`;
this.appendLog(responseLog);
}
formatHeaders(headers) {
return Object.entries(headers)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
}
async executeAll() {
while (this.currentNodeId && !this.isDone && !this.isPaused) {
try {
await this.executeCurrentNode();
await this.moveToNextNode();
} catch (error) {
this.appendLog(`Execution stopped: ${error.message}`);
break;
}
}
if (this.isDone) {
this.appendLog('Scenario execution completed');
} else if (this.isPaused) {
this.appendLog('Scenario execution paused');
}
}
async moveToNextNode() {
const nextNodes = this.findNextNodes(this.currentNodeId);
if (nextNodes.length === 0) {
this.isDone = true;
this.currentNodeId = null;
return;
}
const currentNode = this.nodes.find(n => n.id === this.currentNodeId);
if (currentNode.type === 'conditionNode') {
const result = await this.evaluateCondition(currentNode.data.script || currentNode.data.condition);
const nextNode = nextNodes.find(n => n.condition === (result ? 'true' : 'false'));
this.currentNodeId = nextNode?.nodeId;
} else {
const nextNode = nextNodes.find(n => !this.visitedNodes.has(n.nodeId));
this.currentNodeId = nextNode?.nodeId;
}
if (!this.currentNodeId) {
this.isDone = true;
}
}
}
export const useScenarioExecutor = (nodes, edges, apiClient) => {
const [logs, setLogs] = useState('');
const [nodeStatuses, setNodeStatuses] = useState({});
const executorRef = useRef(null);
const appendLog = useCallback((text) => {
setLogs(prev => prev + text);
}, []);
const updateNodeStatus = useCallback((nodeId, status) => {
setNodeStatuses(prev => ({ ...prev, [nodeId]: status }));
}, []);
const initExecutor = useCallback(() => {
executorRef.current = new ScenarioExecutor(
nodes,
edges,
apiClient,
appendLog,
updateNodeStatus
);
}, [nodes, edges, apiClient]);
const executeStep = useCallback(async () => {
if (!executorRef.current) initExecutor();
if (!executorRef.current.isDone) {
await executorRef.current.executeCurrentNode();
await executorRef.current.moveToNextNode();
}
}, [initExecutor]);
const executeScenario = useCallback(async () => {
if (!executorRef.current) initExecutor();
await executorRef.current.executeAll();
}, [initExecutor]);
const pauseExecution = useCallback(() => {
if (executorRef.current) {
executorRef.current.pause();
}
}, []);
const resumeExecution = useCallback(async () => {
if (executorRef.current) {
executorRef.current.resume();
await executorRef.current.executeAll();
}
}, []);
return {
logs,
nodeStatuses,
clearLogs: () => setLogs(''),
executeScenario,
executeStep,
pauseExecution,
resumeExecution,
resetExecutor: initExecutor,
isRunning: executorRef.current?.isRunning ?? false,
isPaused: executorRef.current?.isPaused ?? false,
isDone: executorRef.current?.isDone ?? false
};
};
@@ -0,0 +1,72 @@
// services/loadTestService.js
import { getCSRFToken } from '@/services/token.js';
export const loadTestService = (contextPath = '/') => {
const headers = {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCSRFToken()
};
return {
async loadTests() {
const response = await fetch(`${contextPath}mgmt/load_test/list.do`, { headers });
if (!response.ok) throw new Error('Failed to fetch load tests');
return await response.json();
},
async createTest(testName) {
const response = await fetch(`${contextPath}mgmt/load_test/create.do`, {
method: 'POST',
headers,
body: JSON.stringify({ name: testName })
});
if (!response.ok) throw new Error('Failed to create test');
return await response.json();
},
async updateTest(test) {
const response = await fetch(`${contextPath}mgmt/load_test/update.do`, {
method: 'POST',
headers,
body: JSON.stringify(test)
});
if (!response.ok) throw new Error('Failed to update test');
return test;
},
async deleteTest(testId) {
const response = await fetch(`${contextPath}mgmt/load_test/delete.do`, {
method: 'POST',
headers,
body: JSON.stringify({ id: testId })
});
if (!response.ok) throw new Error('Failed to delete test');
return true;
},
async startTest(test) {
const response = await fetch(`${contextPath}startLoadTest`, {
method: 'POST',
headers,
body: JSON.stringify(test)
});
if (!response.ok) throw new Error('Failed to start test');
return true;
},
async stopTest(test) {
const response = await fetch(`${contextPath}stopLoadTest`, {
method: 'POST',
headers,
body: JSON.stringify(test)
});
if (!response.ok) throw new Error('Failed to stop test');
return true;
}
};
};
@@ -0,0 +1,50 @@
// services/scenarioService.js
import { getCSRFToken } from '@/services/token.js';
export const scenarioService = (contextPath = '/') => {
const headers = {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCSRFToken()
};
return {
async loadScenarios() {
const response = await fetch(`${contextPath}mgmt/api_scenario/list.do`, { headers });
if (!response.ok) throw new Error('Failed to fetch scenarios');
return await response.json();
},
async createScenario(name) {
const response = await fetch(`${contextPath}mgmt/api_scenario/create.do`, {
method: 'POST',
headers,
body: JSON.stringify({ name })
});
if (!response.ok) throw new Error('Failed to create scenario');
return await response.json();
},
async updateScenario(scenario) {
const response = await fetch(`${contextPath}mgmt/api_scenario/update.do`, {
method: 'POST',
headers,
body: JSON.stringify(scenario)
});
if (!response.ok) throw new Error('Failed to update scenario');
return scenario;
},
async deleteScenario(scenario) {
const response = await fetch(`${contextPath}mgmt/api_scenario/delete.do`, {
method: 'POST',
headers,
body: JSON.stringify(scenario)
});
if (!response.ok) throw new Error('Failed to delete scenario');
return true;
}
};
};