ApiTestManager 소스 추가
This commit is contained in:
Generated
+6547
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 = '​'; // Insert a zero-width space
|
||||
}
|
||||
hiddenInput.value = this.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
let contentHtml = this.value ? this.renderContent(this.parseContent(this.value)) : '​';
|
||||
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 = '​'; // 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;
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -41,7 +41,6 @@
|
||||
<script th:src="@{/js/jquery-validation/localization/messages_ko.min.js}"></script>
|
||||
<script th:src="@{/js/common.js}"></script>
|
||||
<script th:src="@{/js/dataTables/datatables.min.js}"></script>
|
||||
<script th:src="@{/plugins/vs/loader.js}"></script>
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<button type="button" class="btn btn-primary btn-sm stop_scenario me-2">정지</button>
|
||||
<!-- <button type="button" class="btn btn-primary btn-sm export">내보내기</button>-->
|
||||
<!-- <button type="button" class="btn btn-primary btn-sm clear me-2">초기화</button>-->
|
||||
<button type="button" class="btn btn-primary btn-sm add_start">시작 노드</button>
|
||||
<!-- <button type="button" class="btn btn-primary btn-sm add_start">시작 노드</button>-->
|
||||
</div>
|
||||
<div>시나리오 설명이 여기에 표시</div>
|
||||
<div class="api-scenario-editor-body" style="display: flex; flex-direction: row; flex: 1; border: 1px solid grey; padding: 5px;">
|
||||
|
||||
Reference in New Issue
Block a user