ApiTestManager 소스 추가

This commit is contained in:
현성필
2024-01-17 18:04:16 +09:00
parent 8a53e4dd23
commit dd4a24aa59
14 changed files with 9162 additions and 2 deletions
+6547
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "apitestmanager",
"version": "1.0.0",
"description": "API Test Manager UI",
"main": "dist/apitestmanager.js",
"scripts": {
"dev": "webpack-dev-server --mode development --open --host 127.0.0.1",
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1",
"clean": "rimraf ./dist",
"deploy": "cp ./dist/* ../src/main/resources/static/plugins/apitestmanager/"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/preset-env": "^7.23.8",
"babel-loader": "^9.1.3",
"css-loader": "^6.9.0",
"expose-loader": "^4.1.0",
"file-loader": "^6.2.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"rimraf": "^5.0.5",
"style-loader": "^3.3.4",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"dependencies": {
"drawflow": "^0.0.59",
"jqueryui": "^1.11.1",
"monaco-editor": "^0.45.0",
"monaco-editor-workers": "^0.45.0"
}
}
+64
View File
@@ -0,0 +1,64 @@
<html>
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta content="max-age=0, public" http-equiv="Cache-Control"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta content="index, follow" name="robots"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta content="https://www.eactive.co.kr/" property="og:url"/>
<title>API Tester</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<sciprt src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></sciprt>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<script src="/apitestmanager.js"></script>
<script>
$(document).ready(function () {
let servers = [];
const apiTester = new APITestManager(servers);
});
</script>
<body>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 0; left: 0;">
<div class="toast-header">
<strong class="mr-auto">알림</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
</div>
</div>
<sectio>
<!-- <script>-->
<!-- require.config({paths: {'vs': '/plugins/vs'}});-->
<!-- </script>-->
<div class="container-fluid mt-2">
<div class="card">
<div class="card-header" style="min-height: 50px;">
<ul class="nav nav-tabs card-header-tabs" role="tablist" id="apiRequestTabTitle">
</ul>
</div>
<div class="card-body" style="min-height: 800px;">
<div class="tab-content" id="apiRequestTabContent">
</div>
</div>
</div>
</div>
<div style="display: none">
<ul id="servers">
</ul>
</div>
<iframe id="preRequest" class="preRequest"></iframe>
<iframe id="postRequest" class="postRequest"></iframe>
</sectio>
</body>
</html>
+203
View File
@@ -0,0 +1,203 @@
/**
* 처리 순서
* 1. preRequestScript 실행 -> executePreRequestScript
* 2. 변수 치환 -> replacePlaceholdersWithVariables
* 3. API 호출 -> sendAPIRequest
* 4. postRequestScript 실행 -> executePostRequestScript
*/
class APIClient {
constructor() {
window.globals = {};
this.variables = {};
this.abortController = null;
}
async sendAPIRequest(model, preProcessCallback, consoleOutput) {
try {
const updatedModel = await this.executePreRequestScript(model, consoleOutput);
let processedRequest = this.replacePlaceholdersWithVariables(updatedModel);
if (preProcessCallback) {
preProcessCallback(processedRequest);
}
const response = await this.makeAjaxRequest(processedRequest, model, consoleOutput);
const responseData = await response.json(); // Parse the JSON response
if (!response.ok) {
const errorMessage = responseData.error || 'Unknown error';
throw new Error(`API 호출 중 오류가 발생했습니다. 사유[${errorMessage}]`);
}
const finalResponse = this.formatResponse(responseData);
return this.executePostRequestScript(model.postRequestScript, finalResponse, consoleOutput)
.catch(error => {
$('.toast-body').text(error);
$('.toast').toast('show');
return finalResponse; // Return the original response even if there's an error in the post-request script
});
} catch (error) {
throw error;
}
}
formatResponse(data) {
// Format response as needed before post-request script
let response = {};
response.body = data.body;
response.status = data.status;
response.headers = {};
response.size = data.size;
_.forEach(data.headers, (header) => {
response.headers[header.key] = header.value;
});
return response;
}
abort() {
if (this.abortController) {
this.abortController.abort(); // abort the fetch
this.abortController = null; // reset the controller
}
}
makeAjaxRequest(processedRequest, model) {
this.abortController = new AbortController();
const {signal} = this.abortController;
try {
return fetch('/mgmt/api/test.do', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': $('meta[name="_csrf"]').attr('content')
},
body: JSON.stringify(processedRequest),
signal: signal // pass the signal to the fetch
});
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Network request failed:', error);
}
throw error;
}
}
replacePlaceholdersWithVariables(original) {
let request = JSON.parse(JSON.stringify(original));
let mergedVariables = {};
_.forEach(window.globals, (value, key) => {
mergedVariables[key] = value;
});
if (this.variables) {
_.forEach(this.variables, (value, key) => {
mergedVariables[key] = value;
});
}
_.forEach(mergedVariables, (value, key) => {
const placeholder = `{{${key}}}`;
// Replace in path
request.path = request.path.replace(new RegExp(placeholder, 'g'), value);
// Replace in requestBody
if (request.requestBody) {
request.requestBody = request.requestBody.replace(new RegExp(placeholder, 'g'), value);
}
// Replace in headers
if (request.headers) {
let replaced = JSON.stringify(request.headers).replace(new RegExp(placeholder, 'g'), value);
request.headers = JSON.parse(replaced);
}
// Replace in queryParams
if (request.queryParams) {
let replaced = JSON.stringify(request.queryParams).replace(new RegExp(placeholder, 'g'), value);
request.queryParams = JSON.parse(replaced);
}
});
return request;
}
executePreRequestScript(model, consoleOutput) {
return new Promise((resolve, reject) => {
let script = model.preRequestScript;
let resultFrame = document.getElementById('preRequest');
this.variables = {};
window.preRequestComplete = (updatedModel, error) => {
if (error) {
reject(error);
} else {
resolve(updatedModel);
}
};
window.temp_variable = this.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 {
${script}
window.parent.preRequestComplete(window.request, null);
} catch (error) {
window.parent.preRequestComplete(null, error);
}
</script>`;
});
}
executePostRequestScript(script, response, consoleOutput) {
return new Promise((resolve, reject) => {
let resultFrame = document.getElementById('postRequest');
window.response = response;
window.temp_variable = this.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.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;
+380
View File
@@ -0,0 +1,380 @@
// require.config({paths: {'vs': '/plugins/vs'}});
import * as monaco from 'monaco-editor';
import APIScenarioExecutor from "./APIScenarioExecutor";
import Drawflow from "drawflow";
class APIScenario {
constructor(apiTester) {
this.scenarioList = [];
this.currentIndex = -1;
this.currentScenario = null;
this.apiTester = apiTester;
console.log('constructor');
}
init() {
console.log('init');
this.loadScenarioList();
this.outputEditor = monaco.editor.create(document.getElementById('output'), {
value: '',
automaticLayout: true
});
this.bindEvents();
let id = document.getElementById('drawflow');
this.editor = new Drawflow(id);
this.editor.reroute = true;
this.editor.reroute_fix_curvature = true;
this.editor.force_first_input = true;
this.editor.start();
this.editor.on('contextmenu', (event)=> {
console.log(event);
console.log('contextmenu');
//get class of target node
console.log(event.target.id);
let node = this.editor.getNodeFromId(event.target.id);
console.log(node);
})
this.showOutput = true;
}
loadScenarioList(callback) {
const me = this;
this.currentAjaxRequest = $.ajax({
url: '/mgmt/api_scenario/list.do',
type: 'GET',
contentType: 'application/json',
success: function (data) {
me.scenarioList = data;
let drawflowData = {drawflow: {Home: {data: {}}}};
data.forEach((scenario) => {
let scenarioData = null;
try {
scenarioData = JSON.parse(scenario.scenario)
} catch (e) {
scenarioData = {};
}
drawflowData.drawflow[scenario.id] = {data: scenarioData};
});
me.editor.import(drawflowData);
me.editor.changeModule('Home');
},
error: function (response, textStatus, errorThrown) {
me.hideLoadingOverlay();
$('.toast-body').text(response.responseJSON.error);
$('.toast').toast('show');
},
complete: function () {
me.renderScenarioList();
if (callback) {
callback();
}
me.hideLoadingOverlay();
}
});
}
getScenarioList() {
return this.scenarioList;
}
openScenario(event) {
event.preventDefault();
let scenarioId = $(event.target).closest('li').data('id');
this.openScenarioById(scenarioId);
}
openScenarioById(scenarioId) {
document.querySelectorAll(".api-scenario-list li").forEach((node) => {
node.classList.remove('selected');
});
//find li by data-id and add class selected
document.querySelector(`.api-scenario-list li[data-id="${scenarioId}"]`).classList.add('selected');
this.editor.changeModule(scenarioId);
this.currentScenario = scenarioId;
}
getScenario(scenarioId) {
return this.getScenarioList().find((scenario) => scenario.id === scenarioId);
}
showLoadingOverlay() {
$("#loading-overlay").show();
}
hideLoadingOverlay() {
$("#loading-overlay").hide();
}
showNewScenarioModal() {
$('#new_scenario_modal').find('input').val('');
$('#new_scenario_modal').modal('show');
}
createScenario() {
const name = $('#new_scenario_name').val();
const me = this;
me.showLoadingOverlay();
this.currentAjaxRequest = $.ajax({
url: '/mgmt/api_scenario/create.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({name: name, scenario: "{}", description: ""}),
success: function (data) {
me.loadScenarioList(() => {
me.openScenarioById(data.id);
let start = `
<div>
<div><i class="fas fa-play"></i> Start </div>
</div>
`;
me.editor.addNode('Start', 0, 1, 100, 100, 'start', {}, start);
me.saveScenario();
});
},
error: function (response, textStatus, errorThrown) {
me.hideLoadingOverlay();
$('.toast-body').text(response.responseJSON.error);
$('.toast').toast('show');
},
complete: function () {
me.hideLoadingOverlay();
$('#new_scenario_modal').modal('hide');
}
});
}
saveScenario() {
if (this.currentScenario !== null) {
let data = this.editor.export();
console.log(data.drawflow[this.currentScenario].data);
let updatedScenario = this.getScenario(this.currentScenario);
updatedScenario.scenario = JSON.stringify(data.drawflow[this.currentScenario].data);
const me = this;
me.showLoadingOverlay();
this.currentAjaxRequest = $.ajax({
url: '/mgmt/api_scenario/update.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(updatedScenario),
error: function (response, textStatus, errorThrown) {
me.hideLoadingOverlay();
$('.toast-body').text(response.responseJSON.error);
$('.toast').toast('show');
},
complete: function () {
me.hideLoadingOverlay();
$('.toast-body').text('저장되었습니다.');
$('.toast').toast('show');
}
});
}
}
showDeleteModal(event) {
const id = $(event.currentTarget).closest('li').data('id');
$('#confirm_delete_scenario_modal').find('.confirm_delete_scenario').data('id', id);
$('#confirm_delete_scenario_modal').modal('show');
}
removeScenario(event) {
const id = $(event.currentTarget).data('id');
const me = this;
me.showLoadingOverlay();
this.currentAjaxRequest = $.ajax({
url: '/mgmt/api_scenario/delete.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: id}),
error: function (response, textStatus, errorThrown) {
me.hideLoadingOverlay();
$('.toast-body').text(response.responseJSON.error);
$('.toast').toast('show');
},
complete: function () {
me.hideLoadingOverlay();
$('#confirm_delete_scenario_modal').modal('hide');
$('.toast-body').text('삭제되었습니다.');
$('.toast').toast('show');
me.editor.changeModule('Home');
me.loadScenarioList(() => {
if (me.getScenarioList().length > 0)
me.openScenarioById(me.getScenarioList()[0].id);
});
}
});
}
toggleOutput() {
// Define your selectors as constants for easy changes and better readability
const scenarioContentSelector = '.api-scenario-content';
const scenarioOutputSelector = '.api-scenario-output';
const scenarioOutputBodySelector = '.api-scenario-output-body';
const drawflowSelector = '#drawflow';
const scenarioEditorSelector = '.scenario_editor';
// Ensure parentHeight is a number
let parentHeight = parseInt($(scenarioContentSelector).css('height'), 10);
// Toggle the showOutput state
this.showOutput = !this.showOutput;
if (!this.showOutput) {
$(scenarioOutputSelector).css('flex', '0');
$(scenarioOutputBodySelector).hide();
let editorHeight = $(scenarioEditorSelector).css('height');
$(drawflowSelector).css('height', editorHeight);
$(scenarioOutputSelector).removeClass('halfHeight');
$(drawflowSelector).removeClass('halfHeight');
} else {
$(scenarioOutputSelector).css('flex', '1');
$(scenarioOutputSelector).addClass('halfHeight');
$(scenarioOutputBodySelector).show();
$(drawflowSelector).addClass('halfHeight');
$(drawflowSelector).css('height', parentHeight / 2);
}
// Adjustments for when parentHeight isn't a valid number
if (isNaN(parentHeight)) {
console.error('parentHeight is not a number. Check the height of the api-scenario-content element.');
// Fallback or additional error handling can be placed here
}
}
resizeEditor() {
//FIXME: 창 크기 변경시 자동으로 editor 크기가 조정되지 않아서, 0으로 강제 변경후 다시 layout() 호출
this.outputEditor.layout({
width: 0
});
this.outputEditor.layout({});
}
runScript() {
console.log(this.editor.export().drawflow);
let graph = APIScenarioExecutor.extractGraphFromStart(this.editor.export().drawflow[this.currentScenario].data);
this.executor = new APIScenarioExecutor(graph, this.apiTester, this.outputEditor);
}
runScenarioAll() {
if (!this.executor) {
this.runScript();
}
if (!this.executor.isDone) {
this.executor.executeAll();
} else {
this.executor.reset();
}
}
runScenarioStep() {
if (!this.executor) {
this.runScript();
}
if (!this.executor.isDone) {
this.executor.executeCurrentNode();
this.executor.moveToNextNode();
} else {
this.executor.reset();
}
}
resetScenario() {
}
pauseScenario() {
}
stopScenario() {
if (this.executor)
this.executor.reset();
this.outputEditor.setValue('');
}
addStart() {
let start = `
<div>
<div><i class="fas fa-play"></i> Start </div>
</div>
`;
this.editor.addNode('Start', 0, 1, 100, 100, 'start', {}, start);
}
bindEvents() {
const events = [
{type: 'click', selector: '.open_scenario', action: this.openScenario.bind(this)},
{type: 'click', selector: '.export', action: this.runScript.bind(this)},
{type: 'click', selector: '.toggle_output', action: this.toggleOutput.bind(this)},
{type: 'click', selector: '.run_scenario_step', action: this.runScenarioStep.bind(this)},
{type: 'click', selector: '.run_scenario_all', action: this.runScenarioAll.bind(this)},
{type: 'click', selector: '.reset_scenario', action: this.resetScenario.bind(this)},
{type: 'click', selector: '.pause_scenario', action: this.pauseScenario.bind(this)},
{type: 'click', selector: '.stop_scenario', action: this.stopScenario.bind(this)},
{type: 'click', selector: '#new_scenario', action: this.showNewScenarioModal.bind(this)},
{type: 'click', selector: '.create_scenario', action: this.createScenario.bind(this)},
{type: 'click', selector: '.save_scenario', action: this.saveScenario.bind(this)},
// {type: 'click', selector: '.add_start', action: this.addStart.bind(this)},
{type: 'click', selector: '.delete_scenario', action: this.showDeleteModal.bind(this)},
{type: 'click', selector: '.confirm_delete_scenario', action: this.removeScenario.bind(this)},
{type: 'click', selector: '.stop_scenario', action: this.stopScenario.bind(this)}
];
for (let event of events) {
$(document).on(event.type, event.selector, event.action);
}
window.addEventListener('resize', this.resizeEditor.bind(this));
var me = this;
$('#drawflow').droppable({
drop: function (event, ui) {
const x = ui.offset.left - $(this).offset().left;
const y = ui.offset.top - $(this).offset().top;
const dataset = ui.draggable[0].dataset;
var node = `<div><div class="title-box">API</div><div class="box">${dataset.name}</div></div>`;
me.editor.addNode(dataset.name, 1, 1, x, y, dataset.nodeType, dataset, node);
}
});
}
scenarioListTemplate(scenario) {
return `
<li class="d-flex justify-content-between " data-id="${scenario.id}">
<a href="#" class="d-inline-flex align-items-center rounded open_scenario">${scenario.name}</a>
<div class="d-flex justify-content-between">
<button class="p-1 reset-button edit_scenario"><i class="fa fa-sharp fa-file-edit"></i></button>
<button class="p-1 reset-button delete_scenario"><i class="fa fa-trash-can"></i></button>
</div>
</li>
`;
}
renderScenarioList() {
let scenarioList = this.getScenarioList();
let scenarioListHtml = scenarioList.map((scenario) => this.scenarioListTemplate(scenario)).join('');
$('.api-scenario-list').html(scenarioListHtml);
let nodes = document.querySelectorAll(".api-scenario-list li");
nodes.forEach((node) => {
if ($(node).data('id') === this.currentScenario) {
node.classList.add('selected');
}
});
}
}
export default APIScenario;
+233
View File
@@ -0,0 +1,233 @@
import * as monaco from 'monaco-editor';
import APIClient from './APIClient';
class APIScenarioExecutor {
constructor(graph, apiTester, outputEditor) {
this.graph = graph; // The graph data
this.apiClient = new APIClient();
this.apiTester = apiTester;
this.outputEditor = outputEditor;
this.reset();
}
// Helper method to find the start node ID
findStartNodeId() {
for (let nodeId in this.graph) {
if (this.graph[nodeId].class === 'start') {
return nodeId;
}
}
return null; // Return null if no start node is found
}
reset() {
this.currentNodeId = this.findStartNodeId(); // Initialize at the 'start' node
this.visitedNodes = new Set(); // Keep track of visited nodes
this.isRunning = false;
this.isDone = false;
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
allNodes.forEach(node => {
node.classList.remove('current-execution');
node.classList.remove('result-success');
node.classList.remove('result-error');
node.classList.remove('result-abort');
});
}
executeCurrentNode() {
return new Promise((resolve, reject) => {
console.log(this.currentNodeId)
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
allNodes.forEach(node => {
node.classList.remove('current-execution');
});
if (!this.currentNodeId) {
console.log('No current node to execute.');
return;
}
const currentNode = this.graph[this.currentNodeId];
if (currentNode) {
this.isRunning = true;
this.visitedNodes.add(this.currentNodeId); // Mark this node as visited
let nodeElement = document.querySelector('#drawflow').querySelector(`.drawflow-node[id="node-${this.currentNodeId}"]`);
if (nodeElement) {
nodeElement.classList.add('current-execution');
} else {
console.error('Node with ID ' + this.currentNodeId + ' not found');
}
if (currentNode.class === 'api') {
let model = JSON.parse(JSON.stringify(this.apiTester.getApiRequestModel(currentNode.data.id)));
model.responseModel = {};
this.appendLog(`Executing API [${model.name}]`);
this.apiClient.sendAPIRequest(model, this.logHttpRequest.bind(this))
.then(response => {
this.isRunning = false;
this.logHttpResponse(response);
if (response.status === 200 || response.status === 201) {
nodeElement.classList.add('result-success');
} else {
nodeElement.classList.add('result-error');
}
resolve();
})
.catch(error => {
nodeElement.classList.add('result-abort');
$('.toast-body').text(error.message);
$('.toast').toast('show');
this.appendLog(error.message);
reject(error);
});
console.log(`Executing API request for node: ${currentNode.id} - ${currentNode.name}`);
} else if (currentNode.class === 'start') {
this.appendLog('Starting API scenario\n');
nodeElement.classList.add('result-success');
resolve();
} else {
console.log(`Executing script for node: ${currentNode.id} - ${currentNode.name}`);
resolve();
}
}
});
}
appendLog(text) {
var range = new monaco.Range(this.outputEditor.getModel().getLineCount(), 1, this.outputEditor.getModel().getLineCount(), 1);
var id = {major: 1, minor: 1};
var op = {identifier: id, range: range, text: text + '\n', forceMoveMarkers: true};
this.outputEditor.executeEdits("my-source", [op]);
}
logHttpRequest(model) {
console.log(model);
let log = `[API Request]\n${model.method} ${model.path} HTTP/1.1\n[Request Headers]\n${this.parseRequestHeaders(model.headers)}\n[Request Body]\n${model.requestBody}`;
this.appendLog(log);
}
logHttpResponse(response) {
let log = `[Response]\nHTTP/1.1 ${response.status}\n[Response Headers]\n${this.parseHeaders(response.headers)}\n[Response Body]\n${response.body}`;
this.appendLog(log);
}
parseRequestHeaders(headers) {
let parsedHeaders = '';
headers.forEach(header => {
if (header.enabled){
parsedHeaders += `${header.key}: ${header.value}\n`;
}
});
return parsedHeaders;
}
parseHeaders(headers) {
//convert object to string with \n separation
//sort header by key
let parsedHeaders = '';
const sortedKeys = Object.keys(headers).sort(); // Sorting the keys alphabetically
sortedKeys.forEach(key => {
parsedHeaders += `${key}: ${headers[key]}\n`;
});
return parsedHeaders;
}
// Method to move to the next node
moveToNextNode() {
const currentNode = this.graph[this.currentNodeId];
console.log(currentNode);
if (currentNode && currentNode.connections.length > 0) {
// Find the next unvisited node
const nextNode = currentNode.connections.find(nodeId => !this.visitedNodes.has(nodeId));
if (nextNode) {
this.currentNodeId = nextNode;
} else {
console.log('No more unvisited connections to move to.');
this.currentNodeId = null; // No more nodes to visit
}
} else {
console.log('Current node has no connections or does not exist.');
this.currentNodeId = null; // No more nodes to visit
this.isDone = true;
}
}
async executeAll() {
while (this.currentNodeId) {
console.log(this.currentNodeId);
try {
await this.executeCurrentNode();
} catch (error) {
console.error('Error executing node: ', error);
// Handle error or break loop if necessary
}
await this.moveToNextNodeWithDelay(250);
}
console.log('Completed execution of all API nodes.');
}
moveToNextNodeWithDelay(delay) {
return new Promise(resolve => {
setTimeout(() => {
this.moveToNextNode();
resolve();
}, delay);
});
}
static extractGraphFromStart(graphData) {
console.log(graphData)
let graph = {};
// Helper function to recursively build the graph
function buildGraph(nodeId) {
const node = graphData[nodeId];
if (!node || graph[nodeId]) {
// If the node doesn't exist or has already been visited, stop the recursion
return;
}
// Initialize the node in the graph
graph[nodeId] = {
id: node.id,
name: node.name,
class: node.class,
data: node.data,
connections: []
};
// If the node has outputs, recursively add connected nodes
if (node.outputs) {
Object.values(node.outputs).forEach(output => {
output.connections.forEach(connection => {
graph[nodeId].connections.push(connection.node);
buildGraph(connection.node); // Recurse
});
});
}
}
// Find the start node
for (let nodeId in graphData) {
if (graphData[nodeId].class === 'start') {
buildGraph(nodeId); // Start building the graph from the 'start' node
break; // Assuming there's only one start node
}
}
return graph;
}
}
export default APIScenarioExecutor;
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
class LayoutUtil {
static flattenObject(obj, prefix = '') {
return _.reduce(obj, (acc, value, key) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (_.isObject(value) && !_.isArray(value)) {
_.assign(acc, this.flattenObject(value, fullKey));
} else if (_.isArray(value)) {
// Process each item in the array
value.forEach((item, index) => {
// If the item is an object, recurse, otherwise assign directly
if (_.isObject(item)) {
_.assign(acc, this.flattenObject(item, `${fullKey}[${index}]`));
} else {
acc[`${fullKey}[${index}]`] = item;
}
});
} else {
acc[fullKey] = value;
}
return acc;
}, {});
}
static createGroup(groupModel) {
let groupHtml = `<div><h6>${groupModel.groupTitle}</h6>`;
groupModel.fields.forEach(item => {
groupHtml += this.createFormElement(item);
});
groupHtml += '</div>';
return groupHtml;
}
static createTextInput(elementModel) {
return `
<div class="form-floating mb-2">
<input type="text" name="${elementModel.path}" class="form-control mb-2" id="${elementModel.path}" placeholder="${elementModel.placeholder}">
<label for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>
`;
}
static createBooleanInput(elementModel) {
return `
<div class="form-floating mb-2">
<div class="form-check form-check-inline">
<input type="checkbox" name="${elementModel.path}" class="form-check-input mb-2" id="${elementModel.path}" placeholder="${elementModel.placeholder}">
<label class="form-check-label" for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>
</div>`;
}
static createSelectInput(elementModel) {
const optionsHtml = elementModel.options.map(optionValue => `<option value="${optionValue}">${optionValue}</option>`).join('');
return `
<div class="form-floating mb-2">
<select class="form-select mb-2" id="${elementModel.path}" name="${elementModel.path}">
${optionsHtml}
</select>
<label for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>
`;
}
static createTextAreaInput(elementModel) {
return `
<div class="form-floating mb-2">
<textarea class="form-control mb-2" name="${elementModel.path}" id="${elementModel.path}" placeholder="${elementModel.placeholder}"></textarea>
<label for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>`;
}
static createFormElement(elementModel) {
switch (elementModel.type) {
case "boolean":
return this.createBooleanInput(elementModel);
case "text":
return this.createTextInput(elementModel);
case "select":
return this.createSelectInput(elementModel);
case "textarea":
return this.createTextAreaInput(elementModel);
default:
console.error("Unsupported element type: " + elementModel.type);
return '';
}
}
static createForm(model) {
let formHtml = '';
model.forEach(group => {
formHtml += this.createGroup(group);
});
return formHtml;
}
static getFieldValue(element) {
if ($(element).is('select') || $(element).is('input[type="text"]') || $(element).is('textarea')) {
return $(element).val();
}
if ($(element).is('input[type="checkbox"]')) {
return $(element).is(':checked');
}
}
static echartReplacer(key, value) {
if (key === 'echart' || key === 'series') {
return undefined;
}
return value;
}
}
export default LayoutUtil;
@@ -0,0 +1,173 @@
class EditableInput {
constructor(options = {}) {
this.value = options.value || '';
this.name = options.name || '';
this.id = options.id || '';
this.class = options.class || 'form-control';
this.placeholder = options.placeholder || '';
this.label = options.label || '';
}
init(element, callback) {
this.element = element;
this.element.innerHTML = this.render();
this.attachEventListeners(callback);
}
val(newValue) {
if (newValue !== undefined) {
this.value = newValue;
this.updateUI();
}
return this.value;
}
updateUI() {
const editableDiv = this.element.querySelector('.editableInputContent');
const hiddenInput = this.element.querySelector('input[type="hidden"]');
if (this.value) {
editableDiv.innerHTML = this.renderContent(this.parseContent(this.value));
} else {
editableDiv.innerHTML = '&#8203;'; // Insert a zero-width space
}
hiddenInput.value = this.value;
}
render() {
let contentHtml = this.value ? this.renderContent(this.parseContent(this.value)) : '&#8203;';
return `
<div contenteditable="true" class="editableInputContent ${this.class}" style="border-bottom-left-radius: 0; border-top-left-radius: 0;" placeholder="${this.placeholder}">
${contentHtml}
</div>
<input type="hidden" name="${this.name}" value="${this.value}" />
<label>${this.label}</label>
`;
}
attachEventListeners(callback) {
const editableDiv = this.element.querySelector('.editableInputContent');
const hiddenInput = this.element.querySelector('input[type="hidden"]');
editableDiv.addEventListener('input', (event) => {
this.handleInput(event, editableDiv, hiddenInput);
if (callback)
callback(event);
});
}
handleInput(event, editableDiv, hiddenInput) {
let cursor = this.getCurrentCursor(editableDiv);
let mergedString = Array.from(editableDiv.childNodes)
.filter(function(node) {
// Keep only SPAN elements and text nodes
return node.nodeName.toUpperCase() === 'SPAN' || node.nodeType === 3;
})
.map(node => {
return node.nodeType === 3 ? node.data.trim() : node.innerText.trim();
})
.join('');
let content = this.parseContent(mergedString);
this.value = content.join('');
hiddenInput.value = this.value;
if (!this.value) {
editableDiv.innerHTML = '&#8203;'; // Insert a zero-width space
cursor = 0;
} else {
editableDiv.innerHTML = this.renderContent(content);
}
this.restoreCursor(editableDiv, cursor);
}
renderContent(contentArray) {
let content = '';
contentArray.forEach((item) => {
if (item.startsWith('{{') && item.endsWith('}}')) {
content += `<span class="variable">${item}</span>`;
} else {
content += `<span class="text">${item}</span>`;
}
});
return content;
}
parseContent(inputString) {
let returnStringArray = [];
let regex = /{{.*?}}/g;
let lastIndex = 0;
inputString.replace(regex, (match, index) => {
// Add the string before the match
if (index > lastIndex) {
returnStringArray.push(inputString.slice(lastIndex, index));
}
// Add the matched string including the delimiters
returnStringArray.push(match);
// Update lastIndex for the next iteration
lastIndex = index + match.length;
});
// Add any remaining string after the last match
if (lastIndex < inputString.length) {
returnStringArray.push(inputString.slice(lastIndex));
}
return returnStringArray;
}
getCurrentCursor(element) {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().trim().length;
}
restoreCursor(element, position) {
const selection = window.getSelection();
const range = document.createRange();
range.setStart(element, 0);
range.collapse(true);
const nodeStack = [element];
let node, foundStart = false, stop = false;
let charIndex = 0;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType === 3) {
const nextCharIndex = charIndex + node.length;
if (!foundStart && position >= charIndex && position <= nextCharIndex) {
range.setStart(node, position - charIndex);
foundStart = true;
}
if (foundStart && position >= charIndex && position <= nextCharIndex) {
range.setEnd(node, position - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
let i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
selection.removeAllRanges();
selection.addRange(range);
}
}
export default EditableInput;
@@ -0,0 +1,130 @@
class EditableInput {
constructor(options = {}) {
this.value = options.value || '';
this.name = options.name || '';
this.id = options.id || '';
this.class = options.class || '';
this.placeholder = options.placeholder || '';
}
val(newValue) {
if (newValue !== undefined) {
this.value = newValue;
}
return this.value;
}
init(element, callback) {
const editableDiv = element.querySelector(`.editableInputContent`);
const hiddenInput = element.querySelector(`.editableInputTarget`);
editableDiv.innerHTML = this.renderContent(this.parseContent(hiddenInput.value)); // Set the initial content
editableDiv.addEventListener('input', (event) => {
let cursor = this.getCurrentCursor(editableDiv);
let mergedString = Array.from(editableDiv.children)
.map(span => span.textContent)
.join('');
let content = this.parseContent(mergedString);
editableDiv.innerHTML = this.renderContent(content); // Update the content
hiddenInput.value = content.join(''); // Update the hidden input value
this.value = content.join(''); // Update the value of the EditableInput object
this.restoreCursor(editableDiv, cursor);
if (callback)
callback(event);
});
}
renderContent(contentArray) {
let content = '';
contentArray.forEach((item) => {
if (item.startsWith('{{')) {
content += `<span class="variable">${item}</span>`;
} else {
content += `<span class="text">${item}</span>`;
}
});
return content;
}
parseContent(inputString) {
let returnStringArray = [];
let regex = /{{.*?}}/g;
let lastIndex = 0;
inputString.replace(regex, (match, index) => {
// Add the string before the match
if (index > lastIndex) {
returnStringArray.push(inputString.slice(lastIndex, index));
}
// Add the matched string including the delimiters
returnStringArray.push(match);
// Update lastIndex for the next iteration
lastIndex = index + match.length;
});
// Add any remaining string after the last match
if (lastIndex < inputString.length) {
returnStringArray.push(inputString.slice(lastIndex));
}
return returnStringArray;
}
getCurrentCursor(element) {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
restoreCursor(element, position) {
const selection = window.getSelection();
const range = document.createRange();
range.setStart(element, 0);
range.collapse(true);
const nodeStack = [element];
let node, foundStart = false, stop = false;
let charIndex = 0;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType === 3) {
const nextCharIndex = charIndex + node.length;
if (!foundStart && position >= charIndex && position <= nextCharIndex) {
range.setStart(node, position - charIndex);
foundStart = true;
}
if (foundStart && position >= charIndex && position <= nextCharIndex) {
range.setEnd(node, position - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
let i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
selection.removeAllRanges();
selection.addRange(range);
}
}
export default EditableInput;
+35
View File
@@ -0,0 +1,35 @@
import APITester from './APITester';
import APIScenario from "./APIScenario";
// import { buildWorkerDefinition } from "monaco-editor-workers";
//
// buildWorkerDefinition('./node_modules/monaco-editor-workers/dist/workers', import.meta.url, false);
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
if (label === 'json') {
return '/plugins/apitestmanager/json.worker.js';
}
if (label === 'css' || label === 'scss' || label === 'less') {
return '/plugins/apitestmanager/css.worker.bundle.js';
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return '/plugins/apitestmanager/html.worker.bundle.js';
}
if (label === 'typescript' || label === 'javascript') {
return '/plugins/apitestmanager/ts.worker.bundle.js';
}
return '/plugins/apitestmanager/editor.worker.bundle.js';
}
};
export default class APITestManager {
constructor(servers) {
this.apiTester = new APITester();
this.apiTester.init(servers);
this.apiScenario = new APIScenario(this.apiTester);
this.apiScenario.init();
console.log("APITestManager initialized");
}
}
+69
View File
@@ -0,0 +1,69 @@
const path = require('path');
// const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
'json.worker': 'monaco-editor/esm/vs/language/json/json.worker',
'css.worker': 'monaco-editor/esm/vs/language/css/css.worker',
'html.worker': 'monaco-editor/esm/vs/language/html/html.worker',
'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
},
},
],
}
]
},
externals: {
jquery: 'jQuery',
bootstrap: 'bootstrap',
lodash: '_',
monaco : 'monaco'
},
output: {
library: {
name: 'APITestManager',
type: 'umd',
export: 'default'
},
globalObject: `(typeof self !== 'undefined' ? self : this)`,
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
mode: 'development',
devServer: {
proxy : {
'/mgmt': {
target: 'http://localhost:38080',
changeOrigin: true,
}
}
}
};