load tester beta
This commit is contained in:
Generated
+768
-32
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,11 @@
|
|||||||
"@babel/core": "^7.23.7",
|
"@babel/core": "^7.23.7",
|
||||||
"@babel/preset-env": "^7.23.8",
|
"@babel/preset-env": "^7.23.8",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.1.3",
|
||||||
"css-loader": "^6.9.0",
|
"css-loader": "^6.10.0",
|
||||||
"expose-loader": "^4.1.0",
|
"expose-loader": "^4.1.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"less-loader": "^12.2.0",
|
||||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"style-loader": "^3.3.4",
|
"style-loader": "^3.3.4",
|
||||||
@@ -28,9 +30,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@stomp/stompjs": "^7.0.0",
|
||||||
"drawflow": "^0.0.59",
|
"drawflow": "^0.0.59",
|
||||||
|
"dropzone": "^6.0.0-beta.2",
|
||||||
|
"golden-layout": "^2.6.0",
|
||||||
"jqueryui": "^1.11.1",
|
"jqueryui": "^1.11.1",
|
||||||
"monaco-editor": "^0.45.0",
|
"monaco-editor": "^0.45.0",
|
||||||
"monaco-editor-workers": "^0.45.0"
|
"monaco-editor-workers": "^0.45.0",
|
||||||
|
"sockjs-client": "^1.6.1",
|
||||||
|
"split-grid": "^1.0.11",
|
||||||
|
"x-data-spreadsheet": "^1.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-167
@@ -8,159 +8,156 @@
|
|||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
|
|
||||||
CLIENT_URL = 'mgmt/api/test.do';
|
CLIENT_URL = 'mgmt/api/test.do';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
window.globals = {};
|
window.globals = {};
|
||||||
this.variables = {};
|
this.variables = {};
|
||||||
this.abortController = null;
|
this.abortController = null;
|
||||||
this.contextPath = document.querySelector('meta[name="context-path"]').getAttribute('content');
|
this.contextPath = document.querySelector('meta[name="context-path"]').getAttribute('content');
|
||||||
|
|
||||||
if (!this.contextPath) {
|
if (!this.contextPath) {
|
||||||
this.contextPath = '/';
|
this.contextPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.contextPath.endsWith('/')) {
|
||||||
|
this.contextPath += '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientUrl() {
|
||||||
|
return this.contextPath + this.CLIENT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(this.getClientUrl(), {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.contextPath.endsWith('/')) {
|
window.temp_variable = this.variables;
|
||||||
this.contextPath += '/';
|
window.currentRequest = model;
|
||||||
}
|
window.consoleOutput = consoleOutput;
|
||||||
}
|
resultFrame.srcdoc = `<script>
|
||||||
|
|
||||||
getClientUrl() {
|
|
||||||
return this.contextPath + this.CLIENT_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(this.getClientUrl(), {
|
|
||||||
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;
|
var oldLog = console.log;
|
||||||
console.log = function(message) {
|
console.log = function(message) {
|
||||||
if(window.parent.consoleOutput)
|
if(window.parent.consoleOutput)
|
||||||
@@ -179,23 +176,23 @@ class APIClient {
|
|||||||
window.parent.preRequestComplete(null, error);
|
window.parent.preRequestComplete(null, error);
|
||||||
}
|
}
|
||||||
</script>`;
|
</script>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
executePostRequestScript(script, response, consoleOutput) {
|
executePostRequestScript(script, response, consoleOutput) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let resultFrame = document.getElementById('postRequest');
|
let resultFrame = document.getElementById('postRequest');
|
||||||
window.response = response;
|
window.response = response;
|
||||||
window.temp_variable = this.variables;
|
window.temp_variable = this.variables;
|
||||||
window.postRequestComplete = (response, error) => {
|
window.postRequestComplete = (response, error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.consoleOutput = consoleOutput;
|
window.consoleOutput = consoleOutput;
|
||||||
resultFrame.srcdoc = `<script>
|
resultFrame.srcdoc = `<script>
|
||||||
var oldLog = console.log;
|
var oldLog = console.log;
|
||||||
console.log = function(message) {
|
console.log = function(message) {
|
||||||
if(window.parent.consoleOutput)
|
if(window.parent.consoleOutput)
|
||||||
@@ -212,8 +209,8 @@ class APIClient {
|
|||||||
window.parent.postRequestComplete(window.response, error.toString());
|
window.parent.postRequestComplete(window.response, error.toString());
|
||||||
}
|
}
|
||||||
</script>`;
|
</script>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default APIClient;
|
export default APIClient;
|
||||||
|
|||||||
+321
-321
@@ -1,368 +1,367 @@
|
|||||||
// require.config({paths: {'vs': '/plugins/vs'}});
|
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
import APIScenarioExecutor from "./APIScenarioExecutor";
|
import APIScenarioExecutor from './APIScenarioExecutor';
|
||||||
import Drawflow from "drawflow";
|
import Drawflow from 'drawflow';
|
||||||
|
|
||||||
class APIScenario {
|
class APIScenario {
|
||||||
|
|
||||||
constructor(apiTester, contextPath) {
|
constructor(apiTester, contextPath) {
|
||||||
this.scenarioList = [];
|
this.scenarioList = [];
|
||||||
this.currentScenario = null;
|
this.currentScenario = null;
|
||||||
this.apiTester = apiTester;
|
this.apiTester = apiTester;
|
||||||
this.contextPath = contextPath || '/';
|
this.contextPath = contextPath || '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
getAPIScenarioListURL() {
|
getAPIScenarioListURL() {
|
||||||
return this.contextPath + 'mgmt/api_scenario/list.do';
|
return this.contextPath + 'mgmt/api_scenario/list.do';
|
||||||
}
|
}
|
||||||
|
|
||||||
getAPIScenarioCreateURL() {
|
getAPIScenarioCreateURL() {
|
||||||
return this.contextPath + 'mgmt/api_scenario/create.do';
|
return this.contextPath + 'mgmt/api_scenario/create.do';
|
||||||
}
|
}
|
||||||
|
|
||||||
getAPIScenarioUpdateURL() {
|
getAPIScenarioUpdateURL() {
|
||||||
return this.contextPath + 'mgmt/api_scenario/update.do';
|
return this.contextPath + 'mgmt/api_scenario/update.do';
|
||||||
}
|
}
|
||||||
|
|
||||||
getAPIScenarioDeleteURL() {
|
getAPIScenarioDeleteURL() {
|
||||||
return this.contextPath + 'mgmt/api_scenario/delete.do';
|
return this.contextPath + 'mgmt/api_scenario/delete.do';
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.loadScenarioList();
|
this.loadScenarioList();
|
||||||
this.outputEditor = monaco.editor.create(document.getElementById('output'), {
|
this.outputEditor = monaco.editor.create(document.getElementById('output'), {
|
||||||
value: '',
|
value: '',
|
||||||
automaticLayout: true
|
automaticLayout: true
|
||||||
});
|
});
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
|
||||||
let id = document.getElementById('drawflow');
|
let id = document.getElementById('drawflow');
|
||||||
this.editor = new Drawflow(id);
|
this.editor = new Drawflow(id);
|
||||||
this.editor.reroute = true;
|
this.editor.reroute = true;
|
||||||
this.editor.reroute_fix_curvature = true;
|
this.editor.reroute_fix_curvature = true;
|
||||||
this.editor.force_first_input = true;
|
this.editor.force_first_input = true;
|
||||||
this.editor.start();
|
this.editor.start();
|
||||||
// this.editor.on('contextmenu', (event) => {
|
// this.editor.on('contextmenu', (event) => {
|
||||||
// let node = this.editor.getNodeFromId(event.target.id);
|
// let node = this.editor.getNodeFromId(event.target.id);
|
||||||
// })
|
// })
|
||||||
|
|
||||||
this.showOutput = true;
|
this.showOutput = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScenarioList(callback) {
|
loadScenarioList(callback) {
|
||||||
const me = this;
|
const me = this;
|
||||||
this.currentAjaxRequest = $.ajax({
|
this.currentAjaxRequest = $.ajax({
|
||||||
url: this.getAPIScenarioListURL(),
|
url: this.getAPIScenarioListURL(),
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
success: function (data) {
|
success: function(data) {
|
||||||
me.scenarioList = data;
|
me.scenarioList = data;
|
||||||
let drawflowData = {drawflow: {Home: {data: {}}}};
|
let drawflowData = {drawflow: {Home: {data: {}}}};
|
||||||
data.forEach((scenario) => {
|
data.forEach((scenario) => {
|
||||||
let scenarioData = null;
|
let scenarioData = null;
|
||||||
try {
|
try {
|
||||||
scenarioData = JSON.parse(scenario.scenario)
|
scenarioData = JSON.parse(scenario.scenario);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
scenarioData = {};
|
scenarioData = {};
|
||||||
}
|
}
|
||||||
drawflowData.drawflow[scenario.id] = {data: 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
|
me.editor.import(drawflowData);
|
||||||
document.querySelector(`.api-scenario-list li[data-id="${scenarioId}"]`).classList.add('selected');
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.editor.changeModule(scenarioId);
|
getScenarioList() {
|
||||||
this.currentScenario = scenarioId;
|
return this.scenarioList;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScenario(scenarioId) {
|
openScenario(event) {
|
||||||
return this.getScenarioList().find((scenario) => scenario.id === scenarioId);
|
event.preventDefault();
|
||||||
}
|
let scenarioId = $(event.target).closest('li').data('id');
|
||||||
|
this.openScenarioById(scenarioId);
|
||||||
|
}
|
||||||
|
|
||||||
showLoadingOverlay() {
|
openScenarioById(scenarioId) {
|
||||||
$("#loading-overlay").show();
|
document.querySelectorAll('.api-scenario-list li').forEach((node) => {
|
||||||
}
|
node.classList.remove('selected');
|
||||||
|
});
|
||||||
|
|
||||||
hideLoadingOverlay() {
|
//find li by data-id and add class selected
|
||||||
$("#loading-overlay").hide();
|
document.querySelector(`.api-scenario-list li[data-id="${scenarioId}"]`).classList.add('selected');
|
||||||
}
|
|
||||||
|
|
||||||
showNewScenarioModal() {
|
this.editor.changeModule(scenarioId);
|
||||||
$('#new_scenario_modal').find('input').val('');
|
this.currentScenario = scenarioId;
|
||||||
$('#new_scenario_modal').modal('show');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
createScenario() {
|
getScenario(scenarioId) {
|
||||||
const name = $('#new_scenario_name').val();
|
return this.getScenarioList().find((scenario) => scenario.id === scenarioId);
|
||||||
const me = this;
|
}
|
||||||
me.showLoadingOverlay();
|
|
||||||
this.currentAjaxRequest = $.ajax({
|
showLoadingOverlay() {
|
||||||
url: this.getAPIScenarioCreateURL(),
|
$('#loading-overlay').show();
|
||||||
type: 'POST',
|
}
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify({name: name, scenario: "{}", description: ""}),
|
hideLoadingOverlay() {
|
||||||
success: function (data) {
|
$('#loading-overlay').hide();
|
||||||
me.loadScenarioList(() => {
|
}
|
||||||
me.openScenarioById(data.id);
|
|
||||||
let start = `
|
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: this.getAPIScenarioCreateURL(),
|
||||||
|
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>
|
||||||
<div><i class="fas fa-play"></i> Start </div>
|
<div><i class="fas fa-play"></i> Start </div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
me.editor.addNode('Start', 0, 1, 100, 100, 'start', {}, start);
|
me.editor.addNode('Start', 0, 1, 100, 100, 'start', {}, start);
|
||||||
me.saveScenario();
|
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');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
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() {
|
saveScenario() {
|
||||||
if (this.currentScenario !== null) {
|
if (this.currentScenario !== null) {
|
||||||
let data = this.editor.export();
|
let data = this.editor.export();
|
||||||
console.log(data.drawflow[this.currentScenario].data);
|
console.log(data.drawflow[this.currentScenario].data);
|
||||||
let updatedScenario = this.getScenario(this.currentScenario);
|
let updatedScenario = this.getScenario(this.currentScenario);
|
||||||
updatedScenario.scenario = JSON.stringify(data.drawflow[this.currentScenario].data);
|
updatedScenario.scenario = JSON.stringify(data.drawflow[this.currentScenario].data);
|
||||||
|
|
||||||
const me = this;
|
const me = this;
|
||||||
me.showLoadingOverlay();
|
me.showLoadingOverlay();
|
||||||
this.currentAjaxRequest = $.ajax({
|
this.currentAjaxRequest = $.ajax({
|
||||||
url: this.getAPIScenarioUpdateURL(),
|
url: this.getAPIScenarioUpdateURL(),
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify(updatedScenario),
|
data: JSON.stringify(updatedScenario),
|
||||||
error: function (response, textStatus, errorThrown) {
|
error: function(response, textStatus, errorThrown) {
|
||||||
me.hideLoadingOverlay();
|
me.hideLoadingOverlay();
|
||||||
$('.toast-body').text(response.responseJSON.error);
|
$('.toast-body').text(response.responseJSON.error);
|
||||||
$('.toast').toast('show');
|
$('.toast').toast('show');
|
||||||
},
|
},
|
||||||
complete: function () {
|
complete: function() {
|
||||||
me.hideLoadingOverlay();
|
me.hideLoadingOverlay();
|
||||||
$('.toast-body').text('저장되었습니다.');
|
$('.toast-body').text('저장되었습니다.');
|
||||||
$('.toast').toast('show');
|
$('.toast').toast('show');
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showDeleteModal(event) {
|
showDeleteModal(event) {
|
||||||
const id = $(event.currentTarget).closest('li').data('id');
|
const id = $(event.currentTarget).closest('li').data('id');
|
||||||
$('#confirm_delete_scenario_modal').find('.confirm_delete_scenario').data('id', id);
|
$('#confirm_delete_scenario_modal').find('.confirm_delete_scenario').data('id', id);
|
||||||
$('#confirm_delete_scenario_modal').modal('show');
|
$('#confirm_delete_scenario_modal').modal('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
removeScenario(event) {
|
removeScenario(event) {
|
||||||
const id = $(event.currentTarget).data('id');
|
const id = $(event.currentTarget).data('id');
|
||||||
const me = this;
|
const me = this;
|
||||||
me.showLoadingOverlay();
|
me.showLoadingOverlay();
|
||||||
this.currentAjaxRequest = $.ajax({
|
this.currentAjaxRequest = $.ajax({
|
||||||
url: this.getAPIScenarioDeleteURL(),
|
url: this.getAPIScenarioDeleteURL(),
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({id: id}),
|
data: JSON.stringify({id: id}),
|
||||||
error: function (response, textStatus, errorThrown) {
|
error: function(response, textStatus, errorThrown) {
|
||||||
me.hideLoadingOverlay();
|
me.hideLoadingOverlay();
|
||||||
$('.toast-body').text(response.responseJSON.error);
|
$('.toast-body').text(response.responseJSON.error);
|
||||||
$('.toast').toast('show');
|
$('.toast').toast('show');
|
||||||
},
|
},
|
||||||
complete: function () {
|
complete: function() {
|
||||||
me.hideLoadingOverlay();
|
me.hideLoadingOverlay();
|
||||||
$('#confirm_delete_scenario_modal').modal('hide');
|
$('#confirm_delete_scenario_modal').modal('hide');
|
||||||
$('.toast-body').text('삭제되었습니다.');
|
$('.toast-body').text('삭제되었습니다.');
|
||||||
$('.toast').toast('show');
|
$('.toast').toast('show');
|
||||||
me.editor.changeModule('Home');
|
me.editor.changeModule('Home');
|
||||||
me.loadScenarioList(() => {
|
me.loadScenarioList(() => {
|
||||||
if (me.getScenarioList().length > 0)
|
if (me.getScenarioList().length > 0) {
|
||||||
me.openScenarioById(me.getScenarioList()[0].id);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOutput() {
|
// Adjustments for when parentHeight isn't a valid number
|
||||||
// Define your selectors as constants for easy changes and better readability
|
if (isNaN(parentHeight)) {
|
||||||
const scenarioContentSelector = '.api-scenario-content';
|
console.error('parentHeight is not a number. Check the height of the api-scenario-content element.');
|
||||||
const scenarioOutputSelector = '.api-scenario-output';
|
// Fallback or additional error handling can be placed here
|
||||||
const scenarioOutputBodySelector = '.api-scenario-output-body';
|
}
|
||||||
const drawflowSelector = '#drawflow';
|
}
|
||||||
const scenarioEditorSelector = '.scenario_editor';
|
|
||||||
|
|
||||||
// Ensure parentHeight is a number
|
resizeEditor() {
|
||||||
let parentHeight = parseInt($(scenarioContentSelector).css('height'), 10);
|
//FIXME: 창 크기 변경시 자동으로 editor 크기가 조정되지 않아서, 0으로 강제 변경후 다시 layout() 호출
|
||||||
|
this.outputEditor.layout({
|
||||||
|
width: 0
|
||||||
|
});
|
||||||
|
this.outputEditor.layout({});
|
||||||
|
|
||||||
// Toggle the showOutput state
|
}
|
||||||
this.showOutput = !this.showOutput;
|
|
||||||
|
|
||||||
if (!this.showOutput) {
|
runScript() {
|
||||||
$(scenarioOutputSelector).css('flex', '0');
|
console.log(this.editor.export().drawflow);
|
||||||
$(scenarioOutputBodySelector).hide();
|
let graph = APIScenarioExecutor.extractGraphFromStart(this.editor.export().drawflow[this.currentScenario].data);
|
||||||
|
this.executor = new APIScenarioExecutor(graph, this.apiTester, this.outputEditor);
|
||||||
|
}
|
||||||
|
|
||||||
let editorHeight = $(scenarioEditorSelector).css('height');
|
runScenarioAll() {
|
||||||
$(drawflowSelector).css('height', editorHeight);
|
if (!this.executor) {
|
||||||
$(scenarioOutputSelector).removeClass('halfHeight');
|
this.runScript();
|
||||||
$(drawflowSelector).removeClass('halfHeight');
|
}
|
||||||
} else {
|
if (!this.executor.isDone) {
|
||||||
$(scenarioOutputSelector).css('flex', '1');
|
this.executor.executeAll();
|
||||||
$(scenarioOutputSelector).addClass('halfHeight');
|
} else {
|
||||||
$(scenarioOutputBodySelector).show();
|
this.executor.reset();
|
||||||
$(drawflowSelector).addClass('halfHeight');
|
}
|
||||||
$(drawflowSelector).css('height', parentHeight / 2);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Adjustments for when parentHeight isn't a valid number
|
runScenarioStep() {
|
||||||
if (isNaN(parentHeight)) {
|
if (!this.executor) {
|
||||||
console.error('parentHeight is not a number. Check the height of the api-scenario-content element.');
|
this.runScript();
|
||||||
// Fallback or additional error handling can be placed here
|
}
|
||||||
}
|
if (!this.executor.isDone) {
|
||||||
|
this.executor.executeCurrentNode();
|
||||||
|
this.executor.moveToNextNode();
|
||||||
|
} else {
|
||||||
|
this.executor.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
resizeEditor() {
|
resetScenario() {
|
||||||
//FIXME: 창 크기 변경시 자동으로 editor 크기가 조정되지 않아서, 0으로 강제 변경후 다시 layout() 호출
|
|
||||||
this.outputEditor.layout({
|
|
||||||
width: 0
|
|
||||||
});
|
|
||||||
this.outputEditor.layout({});
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseScenario() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
stopScenario() {
|
||||||
|
if (this.executor) {
|
||||||
|
this.executor.reset();
|
||||||
}
|
}
|
||||||
|
this.outputEditor.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
runScript() {
|
addStart() {
|
||||||
console.log(this.editor.export().drawflow);
|
let start = `
|
||||||
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>
|
||||||
<div><i class="fas fa-play"></i> Start </div>
|
<div><i class="fas fa-play"></i> Start </div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.editor.addNode('Start', 0, 1, 100, 100, 'start', {}, start);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
window.addEventListener('resize', this.resizeEditor.bind(this));
|
||||||
const events = [
|
var me = this;
|
||||||
{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) {
|
$('#drawflow').droppable({
|
||||||
$(document).on(event.type, event.selector, event.action);
|
drop: function(event, ui) {
|
||||||
}
|
const x = ui.offset.left - $(this).offset().left;
|
||||||
|
const y = ui.offset.top - $(this).offset().top;
|
||||||
|
|
||||||
window.addEventListener('resize', this.resizeEditor.bind(this));
|
const dataset = ui.draggable[0].dataset;
|
||||||
var me = this;
|
var node = `<div><div class="title-box">API</div><div class="box">${dataset.name}</div></div>`;
|
||||||
|
|
||||||
$('#drawflow').droppable({
|
me.editor.addNode(dataset.name, 1, 1, x, y, dataset.nodeType, dataset, node);
|
||||||
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;
|
scenarioListTemplate(scenario) {
|
||||||
var node = `<div><div class="title-box">API</div><div class="box">${dataset.name}</div></div>`;
|
return `
|
||||||
|
|
||||||
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}">
|
<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>
|
<a href="#" class="d-inline-flex align-items-center rounded open_scenario">${scenario.name}</a>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
@@ -371,19 +370,20 @@ class APIScenario {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderScenarioList() {
|
renderScenarioList() {
|
||||||
let scenarioList = this.getScenarioList();
|
let scenarioList = this.getScenarioList();
|
||||||
let scenarioListHtml = scenarioList.map((scenario) => this.scenarioListTemplate(scenario)).join('');
|
let scenarioListHtml = scenarioList.map((scenario) => this.scenarioListTemplate(scenario)).join('');
|
||||||
$('.api-scenario-list').html(scenarioListHtml);
|
$('.api-scenario-list').html(scenarioListHtml);
|
||||||
let nodes = document.querySelectorAll(".api-scenario-list li");
|
|
||||||
nodes.forEach((node) => {
|
let nodes = document.querySelectorAll('.api-scenario-list li');
|
||||||
if ($(node).data('id') === this.currentScenario) {
|
nodes.forEach((node) => {
|
||||||
node.classList.add('selected');
|
if ($(node).data('id') === this.currentScenario) {
|
||||||
}
|
node.classList.add('selected');
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default APIScenario;
|
export default APIScenario;
|
||||||
|
|||||||
@@ -2,232 +2,227 @@ import * as monaco from 'monaco-editor';
|
|||||||
import APIClient from './APIClient';
|
import APIClient from './APIClient';
|
||||||
|
|
||||||
class APIScenarioExecutor {
|
class APIScenarioExecutor {
|
||||||
constructor(graph, apiTester, outputEditor) {
|
constructor(graph, apiTester, outputEditor) {
|
||||||
this.graph = graph; // The graph data
|
this.graph = graph; // The graph data
|
||||||
this.apiClient = new APIClient();
|
this.apiClient = new APIClient();
|
||||||
this.apiTester = apiTester;
|
this.apiTester = apiTester;
|
||||||
this.outputEditor = outputEditor;
|
this.outputEditor = outputEditor;
|
||||||
this.reset();
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to find the start node ID
|
reset() {
|
||||||
findStartNodeId() {
|
this.currentNodeId = this.findStartNodeId(); // Initialize at the 'start' node
|
||||||
for (let nodeId in this.graph) {
|
this.visitedNodes = new Set(); // Keep track of visited nodes
|
||||||
if (this.graph[nodeId].class === 'start') {
|
this.isRunning = false;
|
||||||
return nodeId;
|
this.isDone = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
return null; // Return null if no start node is found
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
|
||||||
this.currentNodeId = this.findStartNodeId(); // Initialize at the 'start' node
|
allNodes.forEach(node => {
|
||||||
this.visitedNodes = new Set(); // Keep track of visited nodes
|
node.classList.remove('current-execution');
|
||||||
this.isRunning = false;
|
node.classList.remove('result-success');
|
||||||
this.isDone = false;
|
node.classList.remove('result-error');
|
||||||
|
node.classList.remove('result-abort');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
|
executeCurrentNode() {
|
||||||
allNodes.forEach(node => {
|
return new Promise((resolve, reject) => {
|
||||||
node.classList.remove('current-execution');
|
console.log(this.currentNodeId);
|
||||||
node.classList.remove('result-success');
|
|
||||||
node.classList.remove('result-error');
|
|
||||||
node.classList.remove('result-abort');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
executeCurrentNode() {
|
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
|
||||||
return new Promise((resolve, reject) => {
|
allNodes.forEach(node => {
|
||||||
console.log(this.currentNodeId)
|
node.classList.remove('current-execution');
|
||||||
|
});
|
||||||
|
|
||||||
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
|
if (!this.currentNodeId) {
|
||||||
allNodes.forEach(node => {
|
console.log('No current node to execute.');
|
||||||
node.classList.remove('current-execution');
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
if (!this.currentNodeId) {
|
const currentNode = this.graph[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
|
||||||
|
|
||||||
if (currentNode) {
|
let nodeElement = document.querySelector('#drawflow').querySelector(`.drawflow-node[id="node-${this.currentNodeId}"]`);
|
||||||
this.isRunning = true;
|
if (nodeElement) {
|
||||||
this.visitedNodes.add(this.currentNodeId); // Mark this node as visited
|
nodeElement.classList.add('current-execution');
|
||||||
|
|
||||||
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(`===============================================================\nExecuting 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(`===============================================================\nExecuting 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\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}\n`;
|
|
||||||
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 {
|
} else {
|
||||||
console.log('Current node has no connections or does not exist.');
|
console.error('Node with ID ' + this.currentNodeId + ' not found');
|
||||||
this.currentNodeId = null; // No more nodes to visit
|
|
||||||
this.isDone = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async executeAll() {
|
if (currentNode.class === 'api') {
|
||||||
while (this.currentNodeId) {
|
let model = JSON.parse(JSON.stringify(this.apiTester.getApiRequestModel(currentNode.data.id)));
|
||||||
console.log(this.currentNodeId);
|
model.responseModel = {};
|
||||||
try {
|
this.appendLog(`===============================================================\nExecuting API [${model.name}]`);
|
||||||
await this.executeCurrentNode();
|
this.apiClient.sendAPIRequest(model, this.logHttpRequest.bind(this)).then(response => {
|
||||||
} catch (error) {
|
this.isRunning = false;
|
||||||
console.error('Error executing node: ', error);
|
this.logHttpResponse(response);
|
||||||
// Handle error or break loop if necessary
|
if (response.status === 200 || response.status === 201) {
|
||||||
|
nodeElement.classList.add('result-success');
|
||||||
|
} else {
|
||||||
|
nodeElement.classList.add('result-error');
|
||||||
}
|
}
|
||||||
await this.moveToNextNodeWithDelay(250);
|
resolve();
|
||||||
|
}).catch(error => {
|
||||||
|
nodeElement.classList.add('result-abort');
|
||||||
|
$('.toast-body').text(error.message);
|
||||||
|
$('.toast').toast('show');
|
||||||
|
this.appendLog(error.message);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
console.log(`===============================================================\nExecuting 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();
|
||||||
}
|
}
|
||||||
console.log('Completed execution of all API nodes.');
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
moveToNextNodeWithDelay(delay) {
|
}
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(() => {
|
appendLog(text) {
|
||||||
this.moveToNextNode();
|
var range = new monaco.Range(this.outputEditor.getModel().getLineCount(), 1, this.outputEditor.getModel().getLineCount(), 1);
|
||||||
resolve();
|
var id = {major: 1, minor: 1};
|
||||||
}, delay);
|
var op = {identifier: id, range: range, text: text + '\n\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}\n`;
|
||||||
|
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
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static extractGraphFromStart(graphData) {
|
// Find the start node
|
||||||
console.log(graphData)
|
for (let nodeId in graphData) {
|
||||||
let graph = {};
|
if (graphData[nodeId].class === 'start') {
|
||||||
|
buildGraph(nodeId); // Start building the graph from the 'start' node
|
||||||
// Helper function to recursively build the graph
|
break; // Assuming there's only one start node
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default APIScenarioExecutor;
|
export default APIScenarioExecutor;
|
||||||
|
|||||||
+1065
-1061
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,261 @@
|
|||||||
|
import LoadTester from './components/LoadTester';
|
||||||
|
import LoadTestResult from './components/LoadTestResult';
|
||||||
|
|
||||||
|
class LoadTestManagerModel {
|
||||||
|
constructor() {
|
||||||
|
this.testList = [];
|
||||||
|
this.scenarios = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getTestList() {
|
||||||
|
return this.testList;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScenarioList() {
|
||||||
|
return this.scenarios;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestManagerView {
|
||||||
|
constructor(model, controller) {
|
||||||
|
this.model = model;
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(element) {
|
||||||
|
this.element = element;
|
||||||
|
this.element.innerHTML = `<div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
testListTemplate(test) {
|
||||||
|
return `
|
||||||
|
<li class="d-flex justify-content-between " data-id="${test.id}">
|
||||||
|
<a href="#" class="d-inline-flex align-items-center rounded open_load_test">${test.name}</a>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<button class="p-1 reset-button delete_load_test"><i class="fa fa-trash-can"></i></button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTestList() {
|
||||||
|
const testList = this.model.getTestList();
|
||||||
|
let testListHtml = testList.map((test) => this.testListTemplate(test)).join('');
|
||||||
|
$('.load_test_list').html(testListHtml);
|
||||||
|
let nodes = document.querySelectorAll('.load_test_list li');
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (this.controller.currentTest !== null && $(node).data('id') === this.controller.currentTest.id) {
|
||||||
|
node.classList.add('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestManagerController {
|
||||||
|
constructor(contextPath, model, testResult) {
|
||||||
|
this.contextPath = contextPath;
|
||||||
|
this.model = model;
|
||||||
|
this.view = new LoadTestManagerView(this.model, this);
|
||||||
|
this.testResult = testResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(element) {
|
||||||
|
console.log('LoadTestManagerController.init() called');
|
||||||
|
await this.loadScenarioList();
|
||||||
|
this.currentTest = new LoadTester(this.contextPath, this.model.scenarios);
|
||||||
|
this.currentTest.init(document.getElementById('load-tester-view'));
|
||||||
|
this.view.render(element);
|
||||||
|
this.bindEvents();
|
||||||
|
await this.loadTestList();
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoadingOverlay() {
|
||||||
|
$('#loading-overlay').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoadingOverlay() {
|
||||||
|
$('#loading-overlay').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadTestListURL() {
|
||||||
|
return this.contextPath + 'mgmt/load_test/list.do';
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadTestCreateURL() {
|
||||||
|
return this.contextPath + 'mgmt/load_test/create.do';
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadTestUpdateURL() {
|
||||||
|
return this.contextPath + 'mgmt/load_test/update.do';
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadTestDeleteURL() {
|
||||||
|
return this.contextPath + 'mgmt/load_test/delete.do';
|
||||||
|
}
|
||||||
|
|
||||||
|
openTest(event) {
|
||||||
|
console.log('openTest');
|
||||||
|
let testId = $(event.target).closest('li').data('id');
|
||||||
|
document.querySelectorAll('.load_test_list li').forEach((node) => {
|
||||||
|
node.classList.remove('selected');
|
||||||
|
});
|
||||||
|
document.querySelector(`.load_test_list li[data-id="${testId}"]`).classList.add('selected');
|
||||||
|
|
||||||
|
let currentTestModel = this.model.getTestList().find((test) => test.id === testId);
|
||||||
|
this.currentTest.openLoadTest(currentTestModel);
|
||||||
|
this.testResult.showResult(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
showNewTestModal() {
|
||||||
|
$('#new_load_test_modal').find('input').val('');
|
||||||
|
$('#new_load_test_modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
showDeleteModal(event) {
|
||||||
|
const id = $(event.currentTarget).closest('li').data('id');
|
||||||
|
$('#confirm_delete_modal').find('.confirm_delete_load_test').data('id', id);
|
||||||
|
$('#confirm_delete_modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTestList(callback) {
|
||||||
|
this.showLoadingOverlay();
|
||||||
|
const response = await fetch(this.getLoadTestListURL(), {
|
||||||
|
method: 'GET', headers: {
|
||||||
|
'Content-Type': 'application/json', 'X-XSRF-TOKEN': $('meta[name="_csrf"]').attr('content')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.model.testList = await response.json();
|
||||||
|
this.view.renderTestList();
|
||||||
|
this.hideLoadingOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadScenarioList() {
|
||||||
|
const response = await fetch(this.contextPath + 'mgmt/api_scenario/list.do', {
|
||||||
|
method: 'GET', headers: {
|
||||||
|
'Content-Type': 'application/json', 'X-XSRF-TOKEN': $('meta[name="_csrf"]').attr('content')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.model.scenarios = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
createLoadTest() {
|
||||||
|
const testName = $('#new_load_test_modal').find('input').val();
|
||||||
|
if (testName) {
|
||||||
|
const me = this;
|
||||||
|
me.showLoadingOverlay();
|
||||||
|
this.currentAjaxRequest = $.ajax({
|
||||||
|
url: this.getLoadTestCreateURL(),
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({name: testName}),
|
||||||
|
success: function(data) {
|
||||||
|
me.loadTestList(() => {
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(response, textStatus, errorThrown) {
|
||||||
|
me.hideLoadingOverlay();
|
||||||
|
$('.toast-body').text(response.responseJSON.error);
|
||||||
|
$('.toast').toast('show');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
me.hideLoadingOverlay();
|
||||||
|
$('#new_load_test_modal').modal('hide');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLoadTest() {
|
||||||
|
if (this.currentTest !== null) {
|
||||||
|
|
||||||
|
const me = this;
|
||||||
|
me.showLoadingOverlay();
|
||||||
|
this.currentAjaxRequest = $.ajax({
|
||||||
|
url: this.getLoadTestUpdateURL(),
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(this.currentTest.getLoadTest()),
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLoadTest(event) {
|
||||||
|
const id = $(event.currentTarget).data('id');
|
||||||
|
const me = this;
|
||||||
|
me.showLoadingOverlay();
|
||||||
|
this.currentAjaxRequest = $.ajax({
|
||||||
|
url: this.getLoadTestDeleteURL(),
|
||||||
|
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_modal').modal('hide');
|
||||||
|
$('.toast-body').text('삭제되었습니다.');
|
||||||
|
$('.toast').toast('show');
|
||||||
|
me.loadTestList(() => {
|
||||||
|
if (me.model.getTestList().length > 0) {
|
||||||
|
//me.openT(me.getScenarioList()[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLoadTest() {
|
||||||
|
this.testResult.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
showTestReport() {
|
||||||
|
this.testResult.showTestRpeort();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const events = [
|
||||||
|
{type: 'click', selector: '#new_load_test', action: this.showNewTestModal.bind(this)},
|
||||||
|
{type: 'click', selector: '.create_load_test', action: this.createLoadTest.bind(this)},
|
||||||
|
{type: 'click', selector: '.save_load_test', action: this.saveLoadTest.bind(this)},
|
||||||
|
{type: 'click', selector: '.reset_load_test', action: this.resetLoadTest.bind(this)},
|
||||||
|
{type: 'click', selector: '.show_test_report', action: this.showTestReport.bind(this)},
|
||||||
|
{type: 'click', selector: '.delete_load_test', action: this.showDeleteModal.bind(this)},
|
||||||
|
{type: 'click', selector: '.open_load_test', action: this.openTest.bind(this)},
|
||||||
|
{type: 'click', selector: '.confirm_delete_load_test', action: this.removeLoadTest.bind(this)}];
|
||||||
|
|
||||||
|
for (let event of events) {
|
||||||
|
$(document).on(event.type, event.selector, event.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestManager {
|
||||||
|
constructor(contextPath) {
|
||||||
|
this.contextPath = contextPath;
|
||||||
|
this.model = new LoadTestManagerModel();
|
||||||
|
this.testResult = new LoadTestResult(this.contextPath, document.getElementById('load-tester-result-view'));
|
||||||
|
this.testResult.init();
|
||||||
|
this.controller = new LoadTestManagerController(this.contextPath, this.model, this.testResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log('LoadTestManager.init() called');
|
||||||
|
let element = document.getElementById('load_test_menubar');
|
||||||
|
this.controller.init(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadTestManager;
|
||||||
@@ -16,9 +16,9 @@ class EditableInputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EditableInputView {
|
class EditableInputView {
|
||||||
constructor(model, editableInput) {
|
constructor(model, controller) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.editableInput = editableInput;
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(element) {
|
render(element) {
|
||||||
@@ -39,9 +39,9 @@ class EditableInputView {
|
|||||||
|
|
||||||
attachEventListeners() {
|
attachEventListeners() {
|
||||||
this.editableDiv.addEventListener('input', (event) => {
|
this.editableDiv.addEventListener('input', (event) => {
|
||||||
this.editableInput.controller.handleInput(this.editableDiv);
|
this.controller.handleInput(this.editableDiv);
|
||||||
if (this.editableInput.controller.additionalCallback) {
|
if (this.controller.additionalCallback) {
|
||||||
this.editableInput.controller.additionalCallback(this.model.getValue());
|
this.controller.additionalCallback(this.model.getValue());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,9 +99,9 @@ class EditableInputView {
|
|||||||
|
|
||||||
|
|
||||||
class EditableInputController {
|
class EditableInputController {
|
||||||
constructor(model, view) {
|
constructor(model) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.view = view;
|
this.view = new EditableInputView(this.model, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(element, callback) {
|
init(element, callback) {
|
||||||
@@ -208,16 +208,17 @@ class EditableInputController {
|
|||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
this.view.updateUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditableInput {
|
class EditableInput {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
// Initialize the model with the value
|
// Initialize the model with the value
|
||||||
this.model = new EditableInputModel(options.value || '');
|
this.model = new EditableInputModel(options.value || '');
|
||||||
|
this.controller = new EditableInputController(this.model);
|
||||||
// Create the view and controller, passing the model to them
|
|
||||||
this.view = new EditableInputView(this.model, this);
|
|
||||||
this.controller = new EditableInputController(this.model, this.view);
|
|
||||||
|
|
||||||
// Additional properties as required
|
// Additional properties as required
|
||||||
this.name = options.name || '';
|
this.name = options.name || '';
|
||||||
@@ -239,7 +240,7 @@ class EditableInput {
|
|||||||
setValue(newValue) {
|
setValue(newValue) {
|
||||||
// Delegate to the model and update the view
|
// Delegate to the model and update the view
|
||||||
this.model.setValue(newValue);
|
this.model.setValue(newValue);
|
||||||
this.view.updateUI();
|
this.controller.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any additional methods required for interaction with this component
|
// Any additional methods required for interaction with this component
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import {Stomp} from '@stomp/stompjs';
|
||||||
|
import SockJS from 'sockjs-client';
|
||||||
|
import LoadTestResultList from './LoadTestResultList';
|
||||||
|
import LoadTestResultDetail from './LoadTestResultDetail';
|
||||||
|
|
||||||
|
class LoadTestResultModel {
|
||||||
|
constructor(testId) {
|
||||||
|
this.testId = testId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultView {
|
||||||
|
constructor(model, element, controller) {
|
||||||
|
this.element = element;
|
||||||
|
this.model = model;
|
||||||
|
this.controller = controller;
|
||||||
|
|
||||||
|
this.testResultListElement = document.createElement('div');
|
||||||
|
this.testResultListElement.id = 'loadTestResultList';
|
||||||
|
this.testResultListElement.style.width = '250px';
|
||||||
|
this.testResultListElement.style.border = '1px solid #e0e0e0';
|
||||||
|
this.testResultListElement.style.padding = '5px';
|
||||||
|
this.testResultListElement.style.overflow = 'hidden';
|
||||||
|
this.testResultListElement.style.height = `${window.innerHeight - 160}px`;
|
||||||
|
this.testResultListElement.style.maxHeight = '100%';
|
||||||
|
this.testResultListElement.classList.add('load-test-result-container');
|
||||||
|
this.element.appendChild(this.testResultListElement);
|
||||||
|
|
||||||
|
this.testResultDetailElement = document.createElement('div');
|
||||||
|
this.testResultDetailElement.id = 'loadTestResultDetail';
|
||||||
|
this.testResultDetailElement.style.width = 'calc(100% - 250px)';
|
||||||
|
this.testResultDetailElement.style.border = '1px solid #e0e0e0';
|
||||||
|
this.testResultDetailElement.classList.add('load-test-result-detail');
|
||||||
|
this.testResultDetailElement.style.display = 'flex';
|
||||||
|
this.testResultDetailElement.style.height = `${window.innerHeight - 160}px`;
|
||||||
|
this.testResultDetailElement.style.maxHeight = '100%'; //it seems not working
|
||||||
|
this.element.appendChild(this.testResultDetailElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
this.testResultListElement.style.height = `${window.innerHeight - 160}px`;
|
||||||
|
this.testResultDetailElement.style.height = `${window.innerHeight - 160}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultController {
|
||||||
|
constructor(contextPath, element, model) {
|
||||||
|
this.contextPath = contextPath || '/';
|
||||||
|
this.model = model;
|
||||||
|
this.view = new LoadTestResultView(this.model, element, this);
|
||||||
|
this.subscription = null;
|
||||||
|
this.loadTestResultList = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log('LoadTestResultController init');
|
||||||
|
//initialize websocket
|
||||||
|
this.socket = new SockJS(this.contextPath + 'load-test-result');
|
||||||
|
this.client = Stomp.over(this.socket);
|
||||||
|
this.client.connect({}, () => {
|
||||||
|
console.log('connected');
|
||||||
|
});
|
||||||
|
this.client.activate();
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
window.addEventListener('resize', this.resize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
this.view.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult(testId) {
|
||||||
|
if (this.subscription) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
this.subscription = null; // Clear the subscription after unsubscribing
|
||||||
|
}
|
||||||
|
this.model.testId = testId;
|
||||||
|
this.subscription = this.client.subscribe(`/topic/${this.model.testId}`, (message) => {
|
||||||
|
this.loadTestResultList.addTestResult(JSON.parse(message.body));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResult {
|
||||||
|
constructor(contextPath, element) {
|
||||||
|
this.contextPath = contextPath || '/';
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.model = new LoadTestResultModel();
|
||||||
|
this.controller = new LoadTestResultController(this.contextPath, this.element, this.model);
|
||||||
|
this.controller.init();
|
||||||
|
this.view = this.controller.view;
|
||||||
|
this.loadTestResultDetail = new LoadTestResultDetail(this.contextPath, this.view.testResultDetailElement);
|
||||||
|
this.loadTestResultList = new LoadTestResultList(this.contextPath, this.view.testResultListElement, this.loadTestResultDetail);
|
||||||
|
this.controller.loadTestResultList = this.loadTestResultList;
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult(testId) {
|
||||||
|
this.controller.showResult(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.loadTestResultList.reset();
|
||||||
|
this.loadTestResultDetail.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
showTestReport() {
|
||||||
|
//show test report modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadTestResult;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import * as monaco from 'monaco-editor';
|
||||||
|
|
||||||
|
class LoadTestResultDetailModel {
|
||||||
|
constructor() {
|
||||||
|
this.testResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultDetailView {
|
||||||
|
constructor(element, model, controller) {
|
||||||
|
this.element = element;
|
||||||
|
this.editorContainer = document.createElement('div');
|
||||||
|
this.editorContainer.style.flex = '1';
|
||||||
|
this.element.appendChild(this.editorContainer);
|
||||||
|
|
||||||
|
this.model = model;
|
||||||
|
this.controller = controller;
|
||||||
|
this.outputEditor = monaco.editor.create(this.editorContainer, {
|
||||||
|
value: '',
|
||||||
|
automaticLayout: true,
|
||||||
|
quickSuggestions: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultDetailController {
|
||||||
|
constructor(element, model) {
|
||||||
|
this.element = element;
|
||||||
|
this.model = model;
|
||||||
|
this.view = new LoadTestResultDetailView(element, model, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
showDetail(result) {
|
||||||
|
this.view.outputEditor.setValue('');
|
||||||
|
this.logHttpRequest(result.request);
|
||||||
|
this.logHttpResponse(result.response);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(){
|
||||||
|
this.view.outputEditor.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLog(text) {
|
||||||
|
var range = new monaco.Range(this.view.outputEditor.getModel().getLineCount(), 1, this.view.outputEditor.getModel().getLineCount(), 1);
|
||||||
|
var id = {major: 1, minor: 1};
|
||||||
|
var op = {identifier: id, range: range, text: text + '\n\n', forceMoveMarkers: true};
|
||||||
|
this.view.outputEditor.executeEdits('my-source', [op]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logHttpRequest(request) {
|
||||||
|
console.log({request});
|
||||||
|
let log = `[API Request]\n${request.method} ${request.path} HTTP/1.1\n[Request Headers]\n${this.parseRequestHeaders(request.headers)}\n[Request Body]\n${request.requestBody}`;
|
||||||
|
this.appendLog(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
logHttpResponse(response) {
|
||||||
|
console.log({response});
|
||||||
|
let log = `[Response]\nHTTP/1.1 ${response.status}\n[Response Headers]\n${this.parseHeaders(response.headers)}\n[Response Body]\n${response.body}\n`;
|
||||||
|
this.appendLog(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseRequestHeaders(headers) {
|
||||||
|
let parsedHeaders = '';
|
||||||
|
headers.forEach(header => {
|
||||||
|
if (header.enabled) {
|
||||||
|
parsedHeaders += `${header.key}: ${header.value}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseHeaders(headers) {
|
||||||
|
let parsedHeaders = '';
|
||||||
|
headers.forEach(header => {
|
||||||
|
parsedHeaders += `${header.key}: ${header.value}\n`;
|
||||||
|
});
|
||||||
|
return parsedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeEditor() {
|
||||||
|
//FIXME: 창 크기 변경시 자동으로 editor 크기가 조정되지 않아서, 0으로 강제 변경후 다시 layout() 호출
|
||||||
|
this.view.outputEditor.layout({
|
||||||
|
width: 0
|
||||||
|
});
|
||||||
|
this.view.outputEditor.layout({});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
window.addEventListener('resize', this.resizeEditor.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultDetail {
|
||||||
|
constructor(contextPath, element) {
|
||||||
|
this.contextPath = contextPath || '/';
|
||||||
|
this.model = new LoadTestResultDetailModel();
|
||||||
|
this.controller = new LoadTestResultDetailController(element, this.model);
|
||||||
|
this.controller.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
showDetail(result) {
|
||||||
|
this.controller.showDetail(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.controller.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadTestResultDetail;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
class LoadTestResultListModel {
|
||||||
|
constructor() {
|
||||||
|
this.testResults = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultListView {
|
||||||
|
constructor(element, model, controller) {
|
||||||
|
this.element = element;
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.style.overflowY = 'scroll';
|
||||||
|
ul.style.padding = '0';
|
||||||
|
ul.style.height = 'inherit';
|
||||||
|
ul.style.maxHeight = '100%';
|
||||||
|
this.element.appendChild(ul);
|
||||||
|
this.model = model;
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTestResult(result) {
|
||||||
|
const ul = this.element.querySelector('ul');
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.style.border = '1px solid #e0e0e0';
|
||||||
|
li.style.marginBottom = '5px';
|
||||||
|
li.style.padding = '4px';
|
||||||
|
li.textContent = result.request.name; // Assuming `result` is a string or has a `.toString()` method that returns a meaningful representation
|
||||||
|
|
||||||
|
li.addEventListener('click', () =>{
|
||||||
|
const allLi = ul.querySelectorAll('li');
|
||||||
|
allLi.forEach(li => {
|
||||||
|
li.classList.remove('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add "selected" class to the clicked li
|
||||||
|
li.classList.add('selected');
|
||||||
|
this.controller.showDetail(result);
|
||||||
|
} );
|
||||||
|
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultListController {
|
||||||
|
constructor(model, element, detail) {
|
||||||
|
this.model = model;
|
||||||
|
this.view = new LoadTestResultListView(element, model, this);
|
||||||
|
this.detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
showDetail(result) {
|
||||||
|
this.detail.showDetail(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const events = [
|
||||||
|
// {type: 'click'}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let event of events) {
|
||||||
|
$(document).off(event.type, event.selector).on(event.type, event.selector, event.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTestResult(result) {
|
||||||
|
this.model.testResults.push(result);
|
||||||
|
this.view.addTestResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.model.testResults = [];
|
||||||
|
this.view.element.querySelector('ul').innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestResultList {
|
||||||
|
|
||||||
|
constructor(contextPath, element, detail) {
|
||||||
|
this.contextPath = contextPath || '/';
|
||||||
|
this.model = new LoadTestResultListModel();
|
||||||
|
this.detail = detail;
|
||||||
|
this.controller = new LoadTestResultListController(this.model, element, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.controller.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
addTestResult(result) {
|
||||||
|
this.controller.addTestResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.controller.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadTestResultList;
|
||||||
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
class LoadTestModel {
|
||||||
|
constructor(loadTest) {
|
||||||
|
this.scenarios = [];
|
||||||
|
|
||||||
|
if (loadTest) {
|
||||||
|
this.loadTest = loadTest;
|
||||||
|
} else {
|
||||||
|
this.loadTest = {
|
||||||
|
scenarioId: null, threadCount: 1, loopCount: 0, rampUpTime: 0, duration: 0, testData: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getScenarioList() {
|
||||||
|
return this.scenarios;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTestView {
|
||||||
|
constructor(model, controller) {
|
||||||
|
this.model = model;
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScenarioList() {
|
||||||
|
const scenarioList = this.model.getScenarioList();
|
||||||
|
return `<div class="form-floating me-2" style="width: 250px; height: 60px;">
|
||||||
|
<select class="form-select" name="scenarioId">
|
||||||
|
<option value="" selected>시나리오 선택</option>
|
||||||
|
${scenarioList.map(s => `<option value="${s.id}" ${s.id === this.model.loadTest.scenarioId ? 'selected' : ''}>${s.name}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<label for="scenario_list">시나리오 선택</label>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(element) {
|
||||||
|
this.element = element;
|
||||||
|
|
||||||
|
// Assuming the container's HTML is defined here as before
|
||||||
|
const html = `
|
||||||
|
${this.renderScenarioList()}
|
||||||
|
<div class="form-floating me-2" style="width: 100px; height: 60px;">
|
||||||
|
<input type="number" class="form-control" name="user_count" min="1" value="${this.model.loadTest.threadCount}">
|
||||||
|
<label for="user_count">사용자 수</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating me-2" style="width: 90px; height: 60px;">
|
||||||
|
<input type="number" class="form-control" name="repeat_count" min="0" value="${this.model.loadTest.loopCount}">
|
||||||
|
<label for="repeat_count">반복 횟수</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating me-2" style="width: 130px; height: 60px;">
|
||||||
|
<input type="number" class="form-control" name="ramp_up" min="0" value="${this.model.loadTest.rampUpTime}">
|
||||||
|
<label for="ramp_up">ramp-up (ms)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating me-2" style="width: 120px; height: 60px;">
|
||||||
|
<input type="number" class="form-control" name="duration" min="0" value="${this.model.loadTest.duration}">
|
||||||
|
<label for="duration">수행 시간(분)</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary me-2 start_test" style="height: 60px;">시작</button>
|
||||||
|
<button type="button" class="btn btn-primary me-2 stop_test" style="height: 60px;">정지</button>
|
||||||
|
<button type="button" class="btn btn-primary me-2 save_load_test" style="height: 60px;">저장</button>
|
||||||
|
<button type="button" class="btn btn-primary me-2 reset_load_test" style="height: 60px;">초기화</button>
|
||||||
|
<!-- <button type="button" class="btn btn-primary me-2 show_test_report" style="height: 60px;">통계</button>-->
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.element.innerHTML = html;
|
||||||
|
console.log('LoadTestView.render() called');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateView() {
|
||||||
|
document.querySelector('select[name="scenarioId"]').value = this.model.loadTest.scenarioId || '';
|
||||||
|
document.querySelector('input[name="user_count"]').value = this.model.loadTest.threadCount || 1;
|
||||||
|
document.querySelector('input[name="repeat_count"]').value = this.model.loadTest.loopCount || 0;
|
||||||
|
document.querySelector('input[name="ramp_up"]').value = this.model.loadTest.rampUpTime || 0;
|
||||||
|
document.querySelector('input[name="duration"]').value = this.model.loadTest.duration || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LoadTestController {
|
||||||
|
constructor(contextPath, model) {
|
||||||
|
this.contextPath = contextPath || '/';
|
||||||
|
this.model = model;
|
||||||
|
this.view = new LoadTestView(this.model, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStartLoadTestUrl() {
|
||||||
|
return this.contextPath + 'startLoadTest';
|
||||||
|
}
|
||||||
|
|
||||||
|
getStopLoadTestUrl() {
|
||||||
|
return this.contextPath + 'stopLoadTest';
|
||||||
|
}
|
||||||
|
|
||||||
|
init(element) {
|
||||||
|
// Initialize the view
|
||||||
|
this.view.render(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
startTest() {
|
||||||
|
this.updateLoadTest();
|
||||||
|
return fetch(this.getStartLoadTestUrl(), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(this.model.loadTest),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': $('meta[name="_csrf"]').attr('content')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTest() {
|
||||||
|
return fetch(this.getStopLoadTestUrl(), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(this.model.loadTest),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': $('meta[name="_csrf"]').attr('content')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLoadTest() {
|
||||||
|
this.model.loadTest.scenarioId = document.querySelector('select[name="scenarioId"]').value;
|
||||||
|
this.model.loadTest.threadCount = document.querySelector('input[name="user_count"]').value;
|
||||||
|
this.model.loadTest.loopCount = document.querySelector('input[name="repeat_count"]').value;
|
||||||
|
this.model.loadTest.rampUpTime = document.querySelector('input[name="ramp_up"]').value;
|
||||||
|
this.model.loadTest.duration = document.querySelector('input[name="duration"]').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoadTest(loadTest) {
|
||||||
|
this.model.loadTest = loadTest;
|
||||||
|
this.view.updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTester {
|
||||||
|
constructor(contextPath, scenarios) {
|
||||||
|
this.contextPath = contextPath || '/';
|
||||||
|
this.model = new LoadTestModel();
|
||||||
|
|
||||||
|
if (scenarios) {
|
||||||
|
this.model.scenarios = scenarios;
|
||||||
|
} else {
|
||||||
|
this.model.scenarios = [];
|
||||||
|
}
|
||||||
|
this.controller = new LoadTestController(this.contextPath, this.model);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init(element) {
|
||||||
|
this.controller.init(element);
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const events = [
|
||||||
|
{type: 'click', selector: '.start_test', action: this.controller.startTest.bind(this.controller)},
|
||||||
|
{type: 'click', selector: '.stop_test', action: this.controller.stopTest.bind(this.controller)}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let event of events) {
|
||||||
|
$(document).off(event.type, event.selector).on(event.type, event.selector, event.action);
|
||||||
|
}
|
||||||
|
console.log('LoadTester.bindEvents() called');
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoadTest() {
|
||||||
|
//set current value from input
|
||||||
|
this.controller.updateLoadTest();
|
||||||
|
return this.model.loadTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
openLoadTest(loadTest) {
|
||||||
|
this.controller.showLoadTest(loadTest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadTester;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const column = [
|
||||||
|
{name: 'name', type: 'text', label: '이름'},
|
||||||
|
{name: 'numberOfSamples', type: 'text', label: '#'},
|
||||||
|
{name: 'average', type: 'text', label: '평균'},
|
||||||
|
{name: 'min', type: 'text', label: '최소'},
|
||||||
|
{name: 'max', type: 'text', label: '최대'},
|
||||||
|
{name: 'errorRate', type: 'text', label: '에러 %'},
|
||||||
|
{name: 'throughput', type: 'text', label: 'Throughput'}
|
||||||
|
];
|
||||||
|
|
||||||
@@ -23,9 +23,9 @@ class PropertyTableModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PropertyTableView {
|
class PropertyTableView {
|
||||||
constructor(model, propertyTable) {
|
constructor(model, controller) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.propertyTable = propertyTable;
|
this.controller = controller;
|
||||||
this.modelViewList = [];
|
this.modelViewList = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +138,9 @@ class PropertyTableView {
|
|||||||
|
|
||||||
|
|
||||||
class PropertyTableController {
|
class PropertyTableController {
|
||||||
constructor(model, view) {
|
constructor(model) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.view = view;
|
this.view = new PropertyTableView(this.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(element, callback) {
|
init(element, callback) {
|
||||||
@@ -176,18 +176,16 @@ class PropertyTableController {
|
|||||||
properties[index][key] = value;
|
properties[index][key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
this.view.updateUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PropertyTable {
|
class PropertyTable {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
// Initialize the model with properties
|
|
||||||
this.model = new PropertyTableModel(options.properties || []);
|
this.model = new PropertyTableModel(options.properties || []);
|
||||||
|
this.controller = new PropertyTableController(this.model);
|
||||||
// Create the view and controller, passing the model to them
|
|
||||||
this.view = new PropertyTableView(this.model, this);
|
|
||||||
this.controller = new PropertyTableController(this.model, this.view);
|
|
||||||
|
|
||||||
// Additional properties
|
|
||||||
this.propertyName = options.propertyName || '';
|
this.propertyName = options.propertyName || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +203,7 @@ class PropertyTable {
|
|||||||
setProperties(newProperties) {
|
setProperties(newProperties) {
|
||||||
// Update the model and refresh the view
|
// Update the model and refresh the view
|
||||||
this.model.setProperties(newProperties);
|
this.model.setProperties(newProperties);
|
||||||
this.view.updateUI();
|
this.controller.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
addProperty(property) {
|
addProperty(property) {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
class Column {
|
||||||
|
constructor(name, label, style = {}) {
|
||||||
|
this.name = name;
|
||||||
|
this.label = label;
|
||||||
|
this.style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to convert style object to string; can be overridden or extended
|
||||||
|
styleToString() {
|
||||||
|
return Object.entries(this.style).map(([key, value]) => `${key}: ${value};`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render header cell (th)
|
||||||
|
renderHeader() {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.textContent = this.name;
|
||||||
|
// Apply styles
|
||||||
|
Object.entries(this.style).forEach(([key, value]) => {
|
||||||
|
th.style[key] = value;
|
||||||
|
});
|
||||||
|
return th;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render data cell (td)
|
||||||
|
renderCell(data) {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
td.textContent = data;
|
||||||
|
// Apply styles
|
||||||
|
Object.entries(this.style).forEach(([key, value]) => {
|
||||||
|
td.style[key] = value;
|
||||||
|
});
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumberColumn extends Column {
|
||||||
|
constructor(header) {
|
||||||
|
super(header, {'text-align': 'right'}); // Default style for number columns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Columns {
|
||||||
|
constructor(columnJson) {
|
||||||
|
this.columns = columnJson.map(def => {
|
||||||
|
let style = def.style || {};
|
||||||
|
switch (def.type) {
|
||||||
|
case 'number':
|
||||||
|
return new NumberColumn(def.name, def.label || def.name, style);
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
return new Column(def.name, def.label || def.name, style);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumns() {
|
||||||
|
return this.columns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Columns;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import Columns from './Columns';
|
||||||
|
|
||||||
|
class TableModel {
|
||||||
|
constructor(columns, rows) {
|
||||||
|
this.columns = columns; // Array of Columns objects
|
||||||
|
this.rows = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRows(newRows) {
|
||||||
|
this.rows = newRows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableView {
|
||||||
|
constructor(model) {
|
||||||
|
this.model = model;
|
||||||
|
this.table = document.createElement('table');
|
||||||
|
this.table.style.border = '1px solid black';
|
||||||
|
this.tbody = document.createElement('tbody'); // Keep tbody as a member for updates
|
||||||
|
this.table.appendChild(this.renderTableHeader());
|
||||||
|
this.table.appendChild(this.tbody); // Append tbody to the table
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.renderTableBody(); // Initial rendering of the table body
|
||||||
|
return this.table;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTableHeader() {
|
||||||
|
const thead = document.createElement('thead');
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
thead.appendChild(tr);
|
||||||
|
|
||||||
|
this.model.columns.getColumns().forEach(column => {
|
||||||
|
tr.appendChild(column.renderHeader());
|
||||||
|
});
|
||||||
|
|
||||||
|
return thead;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTableBody() {
|
||||||
|
// Clear existing tbody content
|
||||||
|
while (this.tbody.firstChild) {
|
||||||
|
this.tbody.removeChild(this.tbody.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate tbody with new rows
|
||||||
|
this.model.rows.forEach(row => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
this.model.columns.getColumns().forEach(column => {
|
||||||
|
tr.appendChild(column.renderCell(row[column.header]));
|
||||||
|
});
|
||||||
|
this.tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableController {
|
||||||
|
constructor(model) {
|
||||||
|
this.model = model;
|
||||||
|
this.view = new TableView(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateView() {
|
||||||
|
const tableDiv = document.getElementById('tableDiv');
|
||||||
|
// Clear the previous table view
|
||||||
|
while (tableDiv.firstChild) {
|
||||||
|
tableDiv.removeChild(tableDiv.firstChild);
|
||||||
|
}
|
||||||
|
tableDiv.appendChild(this.view.render());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRows(newRows) {
|
||||||
|
this.model.updateRows(newRows);
|
||||||
|
this.view.renderTableBody(); // Re-render the table body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Table {
|
||||||
|
constructor(columnJson) {
|
||||||
|
const columns = new Columns(columnJson);
|
||||||
|
this.model = new TableModel(columns, []);
|
||||||
|
this.controller = new TableController(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRows(data) {
|
||||||
|
this.controller.updateRows(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Table;
|
||||||
+41
-28
@@ -1,39 +1,52 @@
|
|||||||
import APITester from './APITester';
|
import APITester from './APITester';
|
||||||
import APIScenario from "./APIScenario";
|
import APIScenario from './APIScenario';
|
||||||
|
import LoadTestManager from './LoadTestManager';
|
||||||
|
|
||||||
window.MonacoEnvironment = {
|
window.MonacoEnvironment = {
|
||||||
getWorkerUrl: function (moduleId, label) {
|
getWorkerUrl: function(moduleId, label) {
|
||||||
let contextPath = document.querySelector('meta[name="context-path"]').getAttribute('content');
|
let contextPath = document.querySelector(
|
||||||
if (!contextPath) {
|
'meta[name="context-path"]').getAttribute('content');
|
||||||
contextPath = '/';
|
if (!contextPath) {
|
||||||
}
|
contextPath = '/';
|
||||||
if (!contextPath.endsWith('/')) {
|
|
||||||
contextPath += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label === 'json') {
|
|
||||||
return contextPath + 'plugins/apitestmanager/json.worker.bundle.js';
|
|
||||||
}
|
|
||||||
if (label === 'css' || label === 'scss' || label === 'less') {
|
|
||||||
return contextPath + 'plugins/apitestmanager/css.worker.bundle.js';
|
|
||||||
}
|
|
||||||
if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
|
||||||
return contextPath + 'plugins/apitestmanager/html.worker.bundle.js';
|
|
||||||
}
|
|
||||||
if (label === 'typescript' || label === 'javascript') {
|
|
||||||
return contextPath + 'plugins/apitestmanager/ts.worker.bundle.js';
|
|
||||||
}
|
|
||||||
return contextPath + 'plugins/apitestmanager/editor.worker.bundle.js';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!contextPath.endsWith('/')) {
|
||||||
|
contextPath += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === 'json') {
|
||||||
|
return contextPath + 'plugins/apitestmanager/json.worker.bundle.js';
|
||||||
|
}
|
||||||
|
if (label === 'css' || label === 'scss' || label === 'less') {
|
||||||
|
return contextPath + 'plugins/apitestmanager/css.worker.bundle.js';
|
||||||
|
}
|
||||||
|
if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
||||||
|
return contextPath + 'plugins/apitestmanager/html.worker.bundle.js';
|
||||||
|
}
|
||||||
|
if (label === 'typescript' || label === 'javascript') {
|
||||||
|
return contextPath + 'plugins/apitestmanager/ts.worker.bundle.js';
|
||||||
|
}
|
||||||
|
return contextPath + 'plugins/apitestmanager/editor.worker.bundle.js';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class APITestManager {
|
export default class APITestManager {
|
||||||
|
|
||||||
constructor(servers, options = {}) {
|
constructor(options = {}) {
|
||||||
this.apiTester = new APITester(options.contextPath);
|
if (options.type === undefined) {
|
||||||
this.apiTester.init(servers);
|
options.type = 'api';
|
||||||
this.apiScenario = new APIScenario(this.apiTester, options.contextPath);
|
|
||||||
this.apiScenario.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.type === 'api') {
|
||||||
|
this.apiTester = new APITester(options.contextPath);
|
||||||
|
this.apiTester.init(options.servers);
|
||||||
|
this.apiScenario = new APIScenario(this.apiTester, options.contextPath);
|
||||||
|
this.apiScenario.init();
|
||||||
|
} else if (options.type === 'load') {
|
||||||
|
this.loadTesterManager = new LoadTestManager(options.contextPath);
|
||||||
|
this.loadTesterManager.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.less$/,
|
||||||
|
use: [
|
||||||
|
'style-loader', // Creates `style` nodes from JS strings
|
||||||
|
'css-loader', // Translates CSS into CommonJS
|
||||||
|
'less-loader' // Compiles Less to CSS
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: ['style-loader', 'css-loader']
|
use: ['style-loader', 'css-loader']
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package com.eactive.testmaster.client.dto;
|
package com.eactive.testmaster.client.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ApiRequestDTO {
|
public class ApiRequestDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
@@ -28,6 +31,8 @@ public class ApiRequestDTO {
|
|||||||
|
|
||||||
private String postRequestScript;
|
private String postRequestScript;
|
||||||
|
|
||||||
|
private long sentBytes; //estimated size of the request
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -115,4 +120,12 @@ public class ApiRequestDTO {
|
|||||||
public void setPostRequestScript(String postRequestScript) {
|
public void setPostRequestScript(String postRequestScript) {
|
||||||
this.postRequestScript = postRequestScript;
|
this.postRequestScript = postRequestScript;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getSentBytes() {
|
||||||
|
return sentBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSentBytes(long sentBytes) {
|
||||||
|
this.sentBytes = sentBytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
package com.eactive.testmaster.client.dto;
|
package com.eactive.testmaster.client.dto;
|
||||||
|
|
||||||
public class ApiRequestKeyValueDTO {
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class ApiRequestKeyValueDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
private String key;
|
private String key;
|
||||||
private String value;
|
private String value;
|
||||||
|
|
||||||
|
|
||||||
|
public ApiRequestKeyValueDTO(boolean enabled, String key, String value) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.eactive.testmaster.client.dto.internal;
|
||||||
|
|
||||||
|
public class ScenarioConnection {
|
||||||
|
|
||||||
|
private String node;
|
||||||
|
private String input;
|
||||||
|
private String output;
|
||||||
|
|
||||||
|
public String getNode() {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNode(String node) {
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInput() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInput(String input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOutput() {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOutput(String output) {
|
||||||
|
this.output = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.eactive.testmaster.client.dto.internal;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ScenarioConnections {
|
||||||
|
|
||||||
|
private List<ScenarioConnection> connections;
|
||||||
|
|
||||||
|
public List<ScenarioConnection> getConnections() {
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnections(List<ScenarioConnection> connections) {
|
||||||
|
this.connections = connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.eactive.testmaster.client.dto.internal;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ScenarioNode {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private Map<String, String> data;
|
||||||
|
|
||||||
|
@JsonProperty("class")
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
private String html;
|
||||||
|
private Map<String, ScenarioConnections> inputs;
|
||||||
|
private Map<String, ScenarioConnections> outputs;
|
||||||
|
|
||||||
|
private List<ScenarioNode> connections;
|
||||||
|
|
||||||
|
private ApiRequestDTO apiRequest;
|
||||||
|
|
||||||
|
public ScenarioNode() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(Map<String, String> data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClassName() {
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClassName(String className) {
|
||||||
|
this.className = className;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHtml() {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHtml(String html) {
|
||||||
|
this.html = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, ScenarioConnections> getInputs() {
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputs(Map<String, ScenarioConnections> inputs) {
|
||||||
|
this.inputs = inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, ScenarioConnections> getOutputs() {
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOutputs(Map<String, ScenarioConnections> outputs) {
|
||||||
|
this.outputs = outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScenarioNode> getConnections() {
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnections(List<ScenarioNode> connections) {
|
||||||
|
this.connections = connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNode(ScenarioNode node) {
|
||||||
|
if (this.connections == null)
|
||||||
|
this.connections = new ArrayList<>();
|
||||||
|
|
||||||
|
this.connections.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiRequestDTO getApiRequest() {
|
||||||
|
return apiRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApiRequest(ApiRequestDTO apiRequest) {
|
||||||
|
this.apiRequest = apiRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import com.eactive.testmaster.client.dto.ApiCollectionDTO;
|
|||||||
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||||
import com.eactive.testmaster.client.entity.ApiCollection;
|
import com.eactive.testmaster.client.entity.ApiCollection;
|
||||||
import com.eactive.testmaster.client.entity.ApiRequestInfo;
|
import com.eactive.testmaster.client.entity.ApiRequestInfo;
|
||||||
|
import com.eactive.testmaster.client.repository.ApiRequestRepository;
|
||||||
import com.eactive.testmaster.common.mapper.CommonMapper;
|
import com.eactive.testmaster.common.mapper.CommonMapper;
|
||||||
import com.eactive.testmaster.server.mapper.ServerMapper;
|
import com.eactive.testmaster.server.mapper.ServerMapper;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.ReportingPolicy;
|
import org.mapstruct.ReportingPolicy;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -17,17 +19,25 @@ import java.util.List;
|
|||||||
uses = {CommonMapper.class, ServerMapper.class},
|
uses = {CommonMapper.class, ServerMapper.class},
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||||
@Component
|
@Component
|
||||||
public interface ApiRequestMapper {
|
public abstract class ApiRequestMapper {
|
||||||
ApiCollection map(ApiCollectionDTO dto);
|
|
||||||
|
|
||||||
ApiRequestInfo map(ApiRequestDTO dto);
|
@Autowired
|
||||||
|
private ApiRequestRepository apiRequestRepository;
|
||||||
|
|
||||||
|
public abstract ApiCollection map(ApiCollectionDTO dto);
|
||||||
|
|
||||||
ApiRequestDTO map(ApiRequestInfo entity);
|
public abstract ApiRequestInfo map(ApiRequestDTO dto);
|
||||||
|
|
||||||
|
public abstract ApiRequestDTO map(ApiRequestInfo entity);
|
||||||
|
|
||||||
|
public ApiRequestInfo map(String id) {
|
||||||
|
if(id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return apiRequestRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("No ApiRequest found for ID: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
List<ApiRequestInfo> mapApis(List<ApiRequestDTO> apis);
|
public abstract List<ApiRequestInfo> mapApis(List<ApiRequestDTO> apis);
|
||||||
|
|
||||||
List<ApiCollectionDTO> mapCollections(List<ApiCollection> apis);
|
public abstract List<ApiCollectionDTO> mapCollections(List<ApiCollection> apis);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.eactive.testmaster.client.mapper;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||||
|
import com.eactive.testmaster.client.repository.ApiScenarioRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class ApiScenarioHelper {
|
||||||
|
|
||||||
|
private final ApiScenarioRepository apiScenarioRepository;
|
||||||
|
|
||||||
|
public ApiScenarioHelper(ApiScenarioRepository apiScenarioRepository) {
|
||||||
|
this.apiScenarioRepository = apiScenarioRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiScenario getApiScenarioById(String id) {
|
||||||
|
// Assuming the repository has a method findById that returns an Optional<ApiScenario>
|
||||||
|
return apiScenarioRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("No ApiScenario found for ID: " + id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,20 +2,39 @@ package com.eactive.testmaster.client.mapper;
|
|||||||
|
|
||||||
import com.eactive.testmaster.client.dto.ApiScenarioDTO;
|
import com.eactive.testmaster.client.dto.ApiScenarioDTO;
|
||||||
import com.eactive.testmaster.client.entity.ApiScenario;
|
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||||
|
import com.eactive.testmaster.client.repository.ApiScenarioRepository;
|
||||||
|
import org.mapstruct.Context;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.ReportingPolicy;
|
import org.mapstruct.ReportingPolicy;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper(componentModel = "spring",
|
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
|
||||||
@Component
|
@Component
|
||||||
public interface ApiScenarioMapper {
|
public abstract class ApiScenarioMapper {
|
||||||
|
|
||||||
ApiScenario map(ApiScenarioDTO dto);
|
@Autowired
|
||||||
|
private ApiScenarioRepository apiScenarioRepository;
|
||||||
|
|
||||||
ApiScenarioDTO map(ApiScenario scenario);
|
public abstract ApiScenario map(ApiScenarioDTO dto);
|
||||||
|
|
||||||
List<ApiScenarioDTO> toDtoList(List<ApiScenario> scenarios);
|
public abstract ApiScenarioDTO map(ApiScenario scenario);
|
||||||
|
|
||||||
|
public ApiScenario map(String id) {
|
||||||
|
if(id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return apiScenarioRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("No ApiScenario found for ID: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String mapToId(ApiScenario scenario) {
|
||||||
|
if(scenario == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scenario.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract List<ApiScenarioDTO> toDtoList(List<ApiScenario> scenarios);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ public class HttpHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
|
|||||||
public List<ApiRequestKeyValueDTO> convertHttpHeadersToList(HttpHeaders headers) {
|
public List<ApiRequestKeyValueDTO> convertHttpHeadersToList(HttpHeaders headers) {
|
||||||
List<ApiRequestKeyValueDTO> headerList = new ArrayList<>();
|
List<ApiRequestKeyValueDTO> headerList = new ArrayList<>();
|
||||||
for (Map.Entry<String, String> entry : headers.entries()) {
|
for (Map.Entry<String, String> entry : headers.entries()) {
|
||||||
ApiRequestKeyValueDTO header = new ApiRequestKeyValueDTO();
|
ApiRequestKeyValueDTO header = new ApiRequestKeyValueDTO(true, entry.getKey(), entry.getValue());
|
||||||
header.setKey(entry.getKey());
|
|
||||||
header.setValue(entry.getValue());
|
|
||||||
headerList.add(header);
|
headerList.add(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import io.netty.handler.ssl.SslContextBuilder;
|
|||||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -29,12 +31,12 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class NettyApiClient {
|
public class NettyApiClient {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(NettyApiClient.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ServerRepository serverRepository;
|
ServerRepository serverRepository;
|
||||||
|
|
||||||
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO originalRequest) {
|
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO apiRequest) {
|
||||||
ApiRequestDTO apiRequest = originalRequest;
|
|
||||||
Server server = serverRepository.findById(apiRequest.getServer()).orElseThrow(() -> new ServerNotFoundException("Server not found"));
|
Server server = serverRepository.findById(apiRequest.getServer()).orElseThrow(() -> new ServerNotFoundException("Server not found"));
|
||||||
String host = server.getHostname();
|
String host = server.getHostname();
|
||||||
int port = server.getPort();
|
int port = server.getPort();
|
||||||
@@ -92,6 +94,8 @@ public class NettyApiClient {
|
|||||||
request.headers().add(customHeaders);
|
request.headers().add(customHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiRequest.setSentBytes(request.toString().length());
|
||||||
|
|
||||||
ch.writeAndFlush(request);
|
ch.writeAndFlush(request);
|
||||||
ch.closeFuture().sync();
|
ch.closeFuture().sync();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Mapper(componentModel = "spring",
|
@Mapper(componentModel = "spring",
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||||
public class CommonMapper {
|
public class CommonMapper {
|
||||||
|
|
||||||
public String map(boolean value) {
|
public String map(boolean value) {
|
||||||
@@ -22,17 +22,23 @@ public class CommonMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean map(String value) {
|
public boolean map(String value) {
|
||||||
if (value == null) return false;
|
if (value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return value.equals("Y");
|
return value.equals("Y");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String map(List<String> values) {
|
public String map(List<String> values) {
|
||||||
if (values == null) return "";
|
if (values == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return String.join(",", values);
|
return String.join(",", values);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> toList(String value) {
|
public List<String> toList(String value) {
|
||||||
if (StringUtils.hasLength(value)) return new ArrayList<>();
|
if (StringUtils.hasLength(value)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
return Arrays.asList(value.split(","));
|
return Arrays.asList(value.split(","));
|
||||||
}
|
}
|
||||||
@@ -44,7 +50,9 @@ public class CommonMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String mapKeyValue(List<ApiRequestKeyValueDTO> list) {
|
public String mapKeyValue(List<ApiRequestKeyValueDTO> list) {
|
||||||
if (list == null) return "";
|
if (list == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
for (ApiRequestKeyValueDTO dto : list) {
|
for (ApiRequestKeyValueDTO dto : list) {
|
||||||
result.append(dto.toString()).append("||");
|
result.append(dto.toString()).append("||");
|
||||||
@@ -58,22 +66,19 @@ public class CommonMapper {
|
|||||||
public List<ApiRequestKeyValueDTO> keyValueToList(String keyValue) {
|
public List<ApiRequestKeyValueDTO> keyValueToList(String keyValue) {
|
||||||
List<ApiRequestKeyValueDTO> list = new ArrayList<>();
|
List<ApiRequestKeyValueDTO> list = new ArrayList<>();
|
||||||
|
|
||||||
if (!StringUtils.hasLength(keyValue)) return list;
|
if (!StringUtils.hasLength(keyValue)) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
String[] pairs = keyValue.split("\\|\\|");
|
String[] pairs = keyValue.split("\\|\\|");
|
||||||
for (String pair : pairs) {
|
for (String pair : pairs) {
|
||||||
ApiRequestKeyValueDTO dto = new ApiRequestKeyValueDTO();
|
boolean isEnabled = pair.startsWith("//");
|
||||||
if (pair.startsWith("//")) {
|
if (isEnabled) {
|
||||||
dto.setEnabled(false);
|
|
||||||
pair = pair.substring(2);
|
pair = pair.substring(2);
|
||||||
} else {
|
|
||||||
dto.setEnabled(true);
|
|
||||||
}
|
}
|
||||||
String[] keyValueArray = pair.split("::");
|
String[] keyValueArray = pair.split("::");
|
||||||
if (keyValueArray.length == 2) {
|
if (keyValueArray.length == 2) {
|
||||||
dto.setKey(keyValueArray[0]);
|
list.add(new ApiRequestKeyValueDTO(isEnabled, keyValueArray[0], keyValueArray[1]));
|
||||||
dto.setValue(keyValueArray[1]);
|
|
||||||
list.add(dto);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -9,20 +9,21 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class ServletConfig {
|
public class ServletConfig {
|
||||||
@Value("${server.port.http}")
|
|
||||||
private int serverPortHttp;
|
|
||||||
|
|
||||||
@Bean
|
@Value("${server.port.http}")
|
||||||
public ServletWebServerFactory serverFactory() {
|
private int serverPortHttp;
|
||||||
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
|
|
||||||
tomcat.addAdditionalTomcatConnectors(createStandardConnector());
|
|
||||||
return tomcat;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Connector createStandardConnector() {
|
@Bean
|
||||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
public ServletWebServerFactory serverFactory() {
|
||||||
connector.setPort(serverPortHttp);
|
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
|
||||||
return connector;
|
tomcat.addAdditionalTomcatConnectors(createStandardConnector());
|
||||||
}
|
return tomcat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Connector createStandardConnector() {
|
||||||
|
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||||
|
connector.setPort(serverPortHttp);
|
||||||
|
return connector;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.eactive.testmaster.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||||
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
|
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||||
|
import org.springframework.messaging.support.ChannelInterceptor;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||||
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
registry.addEndpoint("/load-test-result").withSockJS();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||||
|
registry.enableSimpleBroker("/topic");
|
||||||
|
registry.setApplicationDestinationPrefixes("/app");
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||||
|
registration.interceptors(new ChannelInterceptor() {
|
||||||
|
@Override
|
||||||
|
public void postSend(org.springframework.messaging.Message<?> message, org.springframework.messaging.MessageChannel channel, boolean sent) {
|
||||||
|
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||||||
|
switch (accessor.getCommand()) {
|
||||||
|
case CONNECT:
|
||||||
|
// Log or handle session connect
|
||||||
|
System.out.println("STOMP Connect [sessionId: " + accessor.getSessionId() + "]");
|
||||||
|
break;
|
||||||
|
case DISCONNECT:
|
||||||
|
// Log or handle session disconnect
|
||||||
|
System.out.println("STOMP Disconnect [sessionId: " + accessor.getSessionId() + "]");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.controller;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||||
|
import com.eactive.testmaster.loadtest.service.LoadTestService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Scope("session")
|
||||||
|
public class LoadTestController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LoadTestService loadTestService;
|
||||||
|
|
||||||
|
@PostMapping("/startLoadTest")
|
||||||
|
public ResponseEntity<String> startLoadTest(@RequestBody LoadTestDTO loadTestDTO) {
|
||||||
|
loadTestService.startLoadTest(loadTestDTO);
|
||||||
|
return ResponseEntity.ok("Load Test Started");
|
||||||
|
}
|
||||||
|
|
||||||
|
//stop
|
||||||
|
@PostMapping("/stopLoadTest")
|
||||||
|
public ResponseEntity<String> stopLoadTest(@RequestBody LoadTestDTO loadTestDTO) {
|
||||||
|
loadTestService.stopLoadTest(loadTestDTO.getId());
|
||||||
|
return ResponseEntity.ok("Load Test Stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.controller;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.common.util.SecurityUtil;
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||||
|
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||||
|
import com.eactive.testmaster.loadtest.mapper.LoadTestMapper;
|
||||||
|
import com.eactive.testmaster.loadtest.service.LoadTestMgmtService;
|
||||||
|
import com.eactive.testmaster.user.entity.StaffUser;
|
||||||
|
import com.eactive.testmaster.user.service.UserService;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/mgmt/load_test")
|
||||||
|
public class LoadTestMgmtController {
|
||||||
|
|
||||||
|
private final LoadTestMgmtService loadTestMgmtService;
|
||||||
|
|
||||||
|
private final LoadTestMapper loadTestMapper;
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public LoadTestMgmtController(LoadTestMgmtService loadTestMgmtService, LoadTestMapper loadTestMapper, UserService userService) {
|
||||||
|
this.loadTestMgmtService = loadTestMgmtService;
|
||||||
|
this.loadTestMapper = loadTestMapper;
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list.do")
|
||||||
|
public ResponseEntity<List<LoadTestDTO>> list() {
|
||||||
|
List<LoadTest> list = loadTestMgmtService.findAll();
|
||||||
|
return ResponseEntity.ok(loadTestMapper.toDtoList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/create.do")
|
||||||
|
public ResponseEntity<LoadTestDTO> createScenario(@RequestBody LoadTestDTO loadTestDto) {
|
||||||
|
LoadTest loadTest = loadTestMapper.map(loadTestDto);
|
||||||
|
StaffUser user = userService.findByEsntlId(SecurityUtil.getCurrentUserEsntlId());
|
||||||
|
loadTest.setOwner(user);
|
||||||
|
LoadTest createdScenario = loadTestMgmtService.createLoadTest(loadTest);
|
||||||
|
return new ResponseEntity<>(loadTestMapper.map(createdScenario), HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/update.do")
|
||||||
|
public ResponseEntity<LoadTestDTO> updateScenario(@RequestBody LoadTestDTO loadTestDTO) {
|
||||||
|
LoadTest updatedLoadTest = loadTestMapper.map(loadTestDTO);
|
||||||
|
LoadTest updatedScenario = loadTestMgmtService.updateLoadTest(loadTestDTO.getId(), updatedLoadTest);
|
||||||
|
return ResponseEntity.ok(loadTestMapper.map(updatedScenario));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete.do")
|
||||||
|
public ResponseEntity<Void> deleteScenario(@RequestBody LoadTestDTO loadTestDto) {
|
||||||
|
loadTestMgmtService.deleteLoadTest(loadTestDto.getId());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class LoadTestPageController {
|
||||||
|
|
||||||
|
@GetMapping("/mgmt/load_tester.do")
|
||||||
|
public String testView(ModelMap model) {
|
||||||
|
return "page/tester/loadTester";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.dto;
|
||||||
|
|
||||||
|
public class ApiRequestEvent {
|
||||||
|
private String testId;
|
||||||
|
private LoadTestRequestAndResponse requestAndResponse;
|
||||||
|
|
||||||
|
public String getTestId() {
|
||||||
|
return testId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTestId(String testId) {
|
||||||
|
this.testId = testId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoadTestRequestAndResponse getRequestAndResponse() {
|
||||||
|
return requestAndResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestAndResponse(LoadTestRequestAndResponse requestAndResponse) {
|
||||||
|
this.requestAndResponse = requestAndResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.dto;
|
||||||
|
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||||
|
import com.eactive.testmaster.user.entity.StaffUser;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
|
||||||
|
public class LoadTestDTO {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String scenarioId;
|
||||||
|
|
||||||
|
private int threadCount; //min 1
|
||||||
|
|
||||||
|
private int loopCount; //0 means infinite
|
||||||
|
|
||||||
|
private int rampUpTime; //in milliseconds
|
||||||
|
|
||||||
|
private int duration; //in minutes
|
||||||
|
|
||||||
|
private String status; //Running, Completed, Stopped, Waiting
|
||||||
|
|
||||||
|
private String testData; //csv format, first row is header
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScenarioId() {
|
||||||
|
return scenarioId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScenarioId(String scenarioId) {
|
||||||
|
this.scenarioId = scenarioId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getThreadCount() {
|
||||||
|
return threadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThreadCount(int threadCount) {
|
||||||
|
this.threadCount = threadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLoopCount() {
|
||||||
|
return loopCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoopCount(int loopCount) {
|
||||||
|
this.loopCount = loopCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRampUpTime() {
|
||||||
|
return rampUpTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRampUpTime(int rampUpTime) {
|
||||||
|
this.rampUpTime = rampUpTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDuration(int duration) {
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTestData() {
|
||||||
|
return testData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTestData(String testData) {
|
||||||
|
this.testData = testData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.dto;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||||
|
import com.eactive.testmaster.client.dto.ApiResponse;
|
||||||
|
|
||||||
|
public class LoadTestRequestAndResponse {
|
||||||
|
|
||||||
|
private ApiRequestDTO request;
|
||||||
|
|
||||||
|
private ApiResponse response;
|
||||||
|
|
||||||
|
private long responseTime;
|
||||||
|
|
||||||
|
|
||||||
|
public LoadTestRequestAndResponse(ApiRequestDTO request, ApiResponse response, long responseTime) {
|
||||||
|
this.request = request;
|
||||||
|
this.response = response;
|
||||||
|
this.responseTime = responseTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiRequestDTO getRequest() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequest(ApiRequestDTO request) {
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse getResponse() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResponse(ApiResponse response) {
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getResponseTime() {
|
||||||
|
return responseTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResponseTime(long responseTime) {
|
||||||
|
this.responseTime = responseTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.entity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class APILoadTestResult {
|
||||||
|
|
||||||
|
private String testName;
|
||||||
|
private long timestamp;
|
||||||
|
private double averageResponseTime;
|
||||||
|
private double maxResponseTime;
|
||||||
|
private double minResponseTime;
|
||||||
|
private int totalRequests;
|
||||||
|
private double throughput; // requests per second
|
||||||
|
private double errorRate; // percentage of requests that resulted in errors
|
||||||
|
private int concurrentUsers;
|
||||||
|
private double cpuUsagePercentage;
|
||||||
|
private double memoryUsagePercentage;
|
||||||
|
private Map<Integer, Integer> statusCodeDistribution; // HTTP status code -> count
|
||||||
|
private Map<String, Double> percentileResponseTimes; // e.g., "P95" -> 95th percentile response time
|
||||||
|
private double networkIO; // Network I/O usage
|
||||||
|
private double diskIO; // Disk I/O usage
|
||||||
|
private List<String> errors; // List of error messages or types encountered during the test
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.entity;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||||
|
import com.eactive.testmaster.user.entity.StaffUser;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "LOAD_TEST")
|
||||||
|
public class LoadTest {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "USER_ID")
|
||||||
|
private StaffUser owner;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "API_SCENARIO_ID")
|
||||||
|
private ApiScenario scenario;
|
||||||
|
|
||||||
|
private int threadCount; //min 1
|
||||||
|
|
||||||
|
private int loopCount; //0 means infinite
|
||||||
|
|
||||||
|
private int rampUpTime; //in milliseconds
|
||||||
|
|
||||||
|
private int duration; //in minutes
|
||||||
|
|
||||||
|
private String status; //Running, Completed, Stopped, Waiting
|
||||||
|
|
||||||
|
private String testData; //csv format, first row is header
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StaffUser getOwner() {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwner(StaffUser owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiScenario getScenario() {
|
||||||
|
return scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScenario(ApiScenario scenario) {
|
||||||
|
this.scenario = scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getThreadCount() {
|
||||||
|
return threadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThreadCount(int threadCount) {
|
||||||
|
this.threadCount = threadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLoopCount() {
|
||||||
|
return loopCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoopCount(int loopCount) {
|
||||||
|
this.loopCount = loopCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRampUpTime() {
|
||||||
|
return rampUpTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRampUpTime(int rampUpTime) {
|
||||||
|
this.rampUpTime = rampUpTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDuration(int duration) {
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTestData() {
|
||||||
|
return testData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTestData(String testData) {
|
||||||
|
this.testData = testData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.entity;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "LOAD_TEST_RESULT")
|
||||||
|
public class LoadTestResult {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "USER_ID")
|
||||||
|
private ApiScenario scenario;
|
||||||
|
|
||||||
|
private LocalDateTime startDate;
|
||||||
|
private LocalDateTime endDate;
|
||||||
|
private String status; //Running, Completed, Stopped
|
||||||
|
private String result; // api result in csv format.
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiScenario getScenario() {
|
||||||
|
return scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScenario(ApiScenario scenario) {
|
||||||
|
this.scenario = scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getStartDate() {
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartDate(LocalDateTime startDate) {
|
||||||
|
this.startDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getEndDate() {
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndDate(LocalDateTime endDate) {
|
||||||
|
this.endDate = endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResult(String result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.mapper;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.mapper.ApiScenarioMapper;
|
||||||
|
import com.eactive.testmaster.common.mapper.CommonMapper;
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||||
|
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||||
|
import com.eactive.testmaster.server.mapper.ServerMapper;
|
||||||
|
import java.util.List;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.ReportingPolicy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring",
|
||||||
|
uses = {CommonMapper.class, ServerMapper.class, ApiScenarioMapper.class},
|
||||||
|
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||||
|
@Component
|
||||||
|
public interface LoadTestMapper {
|
||||||
|
|
||||||
|
@Mapping(target = "scenario", source = "scenarioId")
|
||||||
|
LoadTest map(LoadTestDTO dto);
|
||||||
|
|
||||||
|
@Mapping(target = "scenarioId", source = "scenario")
|
||||||
|
LoadTestDTO map(LoadTest entity);
|
||||||
|
|
||||||
|
List<LoadTestDTO> toDtoList(List<LoadTest> list);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.repository;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface LoadTestRepository extends JpaRepository<LoadTest, String>, JpaSpecificationExecutor<LoadTest> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.repository;
|
||||||
|
|
||||||
|
public class LoadTestResultRepository {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
public class ApiPerformanceAggregation {
|
||||||
|
|
||||||
|
private String apiId;
|
||||||
|
|
||||||
|
private String apiName;
|
||||||
|
|
||||||
|
private long numberOfSamples;
|
||||||
|
|
||||||
|
private double average;
|
||||||
|
|
||||||
|
private double median;
|
||||||
|
|
||||||
|
private double line90;
|
||||||
|
|
||||||
|
private double line95;
|
||||||
|
|
||||||
|
private double line99;
|
||||||
|
|
||||||
|
private double min;
|
||||||
|
|
||||||
|
private double max;
|
||||||
|
|
||||||
|
private double errorRate;
|
||||||
|
|
||||||
|
private double throughput;
|
||||||
|
|
||||||
|
private double receivedKbPerSec;
|
||||||
|
|
||||||
|
private double sentKbPerSec;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
public class ApiPerformanceSummary {
|
||||||
|
|
||||||
|
private String apiId;
|
||||||
|
|
||||||
|
private String apiName;
|
||||||
|
|
||||||
|
private long numberOfSamples;
|
||||||
|
|
||||||
|
private double average;
|
||||||
|
|
||||||
|
private double min;
|
||||||
|
|
||||||
|
private double max;
|
||||||
|
|
||||||
|
private double stdDev;
|
||||||
|
|
||||||
|
private double errorRate;
|
||||||
|
|
||||||
|
private double throughput;
|
||||||
|
|
||||||
|
private double receivedKbPerSec;
|
||||||
|
|
||||||
|
private double sentKbPerSec;
|
||||||
|
|
||||||
|
private double avgBytes;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Scope("session")
|
||||||
|
public class CallableLoadTestService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SimpMessagingTemplate template;
|
||||||
|
|
||||||
|
private ScheduledExecutorService scheduledExecutorService;
|
||||||
|
private List<ScheduledFuture<?>> scheduledFutures;
|
||||||
|
|
||||||
|
public CallableLoadTestService() {
|
||||||
|
this.scheduledFutures = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendLoadTestUpdate(String topic, Object update) {
|
||||||
|
template.convertAndSend("/topic/" + topic, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startLoadTest(int numberOfUsers, long rampUpTimeMillis, long taskDurationMillis) {
|
||||||
|
if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) {
|
||||||
|
scheduledExecutorService.shutdownNow();
|
||||||
|
}
|
||||||
|
scheduledExecutorService = Executors.newScheduledThreadPool(numberOfUsers);
|
||||||
|
|
||||||
|
for (int i = 0; i < numberOfUsers; i++) {
|
||||||
|
final CallableVirtualUser virtualUser = new CallableVirtualUser(this);
|
||||||
|
|
||||||
|
// Calculate delay for each user to simulate ramp-up
|
||||||
|
long delay = i * rampUpTimeMillis;
|
||||||
|
|
||||||
|
// Schedule the task with the calculated delay
|
||||||
|
ScheduledFuture<?> future = scheduledExecutorService.schedule(() -> {
|
||||||
|
try {
|
||||||
|
// Execute the user task and wait for its completion or interruption
|
||||||
|
String result = virtualUser.call();
|
||||||
|
System.out.println("Task completed with result: " + result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}, delay, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
scheduledFutures.add(future);
|
||||||
|
|
||||||
|
// Schedule a stop action for each task after its duration expires
|
||||||
|
scheduledExecutorService.schedule(() -> {
|
||||||
|
if (!future.isDone()) {
|
||||||
|
future.cancel(true); // Attempt to interrupt the task
|
||||||
|
System.out.println("Task was cancelled after its duration limit.");
|
||||||
|
}
|
||||||
|
}, delay + taskDurationMillis, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopLoadTest() {
|
||||||
|
if (scheduledFutures != null) {
|
||||||
|
for (ScheduledFuture<?> future : scheduledFutures) {
|
||||||
|
if (!future.isDone()) {
|
||||||
|
future.cancel(true); // Attempt to interrupt if running
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduledFutures.clear();
|
||||||
|
}
|
||||||
|
if (scheduledExecutorService != null) {
|
||||||
|
scheduledExecutorService.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void cleanup() {
|
||||||
|
stopLoadTest(); // Ensure all tasks are stopped and executor service is shut down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class CallableVirtualUser implements Callable<String> {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(VirtualUser.class);
|
||||||
|
|
||||||
|
private final CallableLoadTestService loadTestService;
|
||||||
|
|
||||||
|
private LoadTestDTO loadTest;
|
||||||
|
private Map<String, String> data;
|
||||||
|
|
||||||
|
public CallableVirtualUser(CallableLoadTestService loadTestService) {
|
||||||
|
this.loadTestService = loadTestService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String call() throws Exception {
|
||||||
|
try {
|
||||||
|
// Simulate user action
|
||||||
|
Thread.sleep(1000); // Replace with actual user action
|
||||||
|
loadTestService.sendLoadTestUpdate(loadTest.getId(), "Virtual user completed");
|
||||||
|
return "Completed";
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt(); // Preserve interrupt status
|
||||||
|
logger.debug("Task interrupted");
|
||||||
|
return "Interrupted";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class Console {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Console.class);
|
||||||
|
|
||||||
|
public void log(String message) {
|
||||||
|
logger.info(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class LoadTestGraphBuilder {
|
||||||
|
|
||||||
|
public static ScenarioNode getTopNode(String data) {
|
||||||
|
try {
|
||||||
|
Map<String, ScenarioNode> graphData = loadData(data);
|
||||||
|
return getHead(extractGraphFromStart(graphData));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, ScenarioNode> loadData(String data) throws JsonProcessingException {
|
||||||
|
Map<String, ScenarioNode> graphData;
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
graphData = objectMapper.readValue(data, new TypeReference<>() {
|
||||||
|
});
|
||||||
|
return graphData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static ScenarioNode getHead(Map<String, ScenarioNode> graphData) {
|
||||||
|
for (ScenarioNode node : graphData.values()) {
|
||||||
|
if ("start".equals(node.getClassName())) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, ScenarioNode> extractGraphFromStart(Map<String, ScenarioNode> graphData) {
|
||||||
|
Map<String, ScenarioNode> graph = new HashMap<>();
|
||||||
|
|
||||||
|
// Helper function to recursively build the graph
|
||||||
|
graphData.forEach((nodeId, node) -> {
|
||||||
|
if ("start".equals(node.getClassName())) {
|
||||||
|
buildGraph(nodeId, graph, graphData);
|
||||||
|
return; // Break the forEach loop assuming there's only one start node
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void buildGraph(String nodeId, Map<String, ScenarioNode> graph, Map<String, ScenarioNode> graphData) {
|
||||||
|
ScenarioNode node = graphData.get(nodeId);
|
||||||
|
if (node == null || graph.containsKey(nodeId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.put(nodeId, node);
|
||||||
|
|
||||||
|
if (node.getOutputs() != null) {
|
||||||
|
node.getOutputs().values().forEach(outputList -> {
|
||||||
|
outputList.getConnections().forEach(output -> {
|
||||||
|
ScenarioNode v = graphData.get(output.getNode());
|
||||||
|
graph.get(nodeId).addNode(v);
|
||||||
|
buildGraph(output.getNode(), graph, graphData); // Recurse
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
Map<String, ScenarioNode> graphData = loadData(testData);
|
||||||
|
LoadTestGraphBuilder graphBuilder = new LoadTestGraphBuilder();
|
||||||
|
Map<String, ScenarioNode> graph = graphBuilder.extractGraphFromStart(graphData);
|
||||||
|
ScenarioNode start = graphBuilder.getHead(graph);
|
||||||
|
|
||||||
|
System.out.println(start);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String testData = "{\n"
|
||||||
|
+ " \"1\": {\n"
|
||||||
|
+ " \"id\": 1,\n"
|
||||||
|
+ " \"name\": \"Start\",\n"
|
||||||
|
+ " \"data\": {},\n"
|
||||||
|
+ " \"class\": \"start\",\n"
|
||||||
|
+ " \"html\": \"\\n <div>\\n <div><i class=\\\"fas fa-play\\\"></i> Start </div>\\n </div>\\n \",\n"
|
||||||
|
+ " \"typenode\": false,\n"
|
||||||
|
+ " \"inputs\": {},\n"
|
||||||
|
+ " \"outputs\": {\n"
|
||||||
|
+ " \"output_1\": {\n"
|
||||||
|
+ " \"connections\": [\n"
|
||||||
|
+ " {\n"
|
||||||
|
+ " \"node\": \"2\",\n"
|
||||||
|
+ " \"output\": \"input_1\"\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"pos_x\": 100,\n"
|
||||||
|
+ " \"pos_y\": 100\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"2\": {\n"
|
||||||
|
+ " \"id\": 2,\n"
|
||||||
|
+ " \"name\": \"잔액조회\",\n"
|
||||||
|
+ " \"data\": {\n"
|
||||||
|
+ " \"collection\": \"b9748a16-bbb7-4dcc-8521-88e76a0b2ee8\",\n"
|
||||||
|
+ " \"id\": \"51351280-a72f-4886-8426-70d2932a2f48\",\n"
|
||||||
|
+ " \"index\": \"2\",\n"
|
||||||
|
+ " \"nodeType\": \"api\",\n"
|
||||||
|
+ " \"name\": \"잔액조회\"\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"class\": \"api\",\n"
|
||||||
|
+ " \"html\": \"<div><div class=\\\"title-box\\\">API</div><div class=\\\"box\\\">잔액조회</div></div>\",\n"
|
||||||
|
+ " \"typenode\": false,\n"
|
||||||
|
+ " \"inputs\": {\n"
|
||||||
|
+ " \"input_1\": {\n"
|
||||||
|
+ " \"connections\": [\n"
|
||||||
|
+ " {\n"
|
||||||
|
+ " \"node\": \"1\",\n"
|
||||||
|
+ " \"input\": \"output_1\"\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"outputs\": {\n"
|
||||||
|
+ " \"output_1\": {\n"
|
||||||
|
+ " \"connections\": [\n"
|
||||||
|
+ " {\n"
|
||||||
|
+ " \"node\": \"3\",\n"
|
||||||
|
+ " \"output\": \"input_1\"\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"pos_x\": 328,\n"
|
||||||
|
+ " \"pos_y\": 184\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"3\": {\n"
|
||||||
|
+ " \"id\": 3,\n"
|
||||||
|
+ " \"name\": \"거래내역조회\",\n"
|
||||||
|
+ " \"data\": {\n"
|
||||||
|
+ " \"collection\": \"b9748a16-bbb7-4dcc-8521-88e76a0b2ee8\",\n"
|
||||||
|
+ " \"id\": \"3f156dc1-f71b-4a26-a778-64a2ddd2b54f\",\n"
|
||||||
|
+ " \"index\": \"3\",\n"
|
||||||
|
+ " \"nodeType\": \"api\",\n"
|
||||||
|
+ " \"name\": \"거래내역조회\"\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"class\": \"api\",\n"
|
||||||
|
+ " \"html\": \"<div><div class=\\\"title-box\\\">API</div><div class=\\\"box\\\">거래내역조회</div></div>\",\n"
|
||||||
|
+ " \"typenode\": false,\n"
|
||||||
|
+ " \"inputs\": {\n"
|
||||||
|
+ " \"input_1\": {\n"
|
||||||
|
+ " \"connections\": [\n"
|
||||||
|
+ " {\n"
|
||||||
|
+ " \"node\": \"2\",\n"
|
||||||
|
+ " \"input\": \"output_1\"\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"outputs\": {\n"
|
||||||
|
+ " \"output_1\": {\n"
|
||||||
|
+ " \"connections\": []\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " },\n"
|
||||||
|
+ " \"pos_x\": 654,\n"
|
||||||
|
+ " \"pos_y\": 301\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ "}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.loadtest.entity.LoadTest;
|
||||||
|
import com.eactive.testmaster.loadtest.repository.LoadTestRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class LoadTestMgmtService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
LoadTestRepository loadTestRepository;
|
||||||
|
|
||||||
|
public List<LoadTest> findAll() {
|
||||||
|
return loadTestRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoadTest createLoadTest(LoadTest loadTest) {
|
||||||
|
loadTest.setId(UUID.randomUUID().toString());
|
||||||
|
return loadTestRepository.save(loadTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoadTest updateLoadTest(String id, LoadTest updatedLoadTest) {
|
||||||
|
return loadTestRepository.findById(id)
|
||||||
|
.map(loadTest -> {
|
||||||
|
loadTest.setName(updatedLoadTest.getName());
|
||||||
|
loadTest.setDescription(updatedLoadTest.getDescription());
|
||||||
|
loadTest.setScenario(updatedLoadTest.getScenario());
|
||||||
|
loadTest.setThreadCount(updatedLoadTest.getThreadCount());
|
||||||
|
loadTest.setLoopCount(updatedLoadTest.getLoopCount());
|
||||||
|
loadTest.setRampUpTime(updatedLoadTest.getRampUpTime());
|
||||||
|
loadTest.setDuration(updatedLoadTest.getDuration());
|
||||||
|
loadTest.setTestData(updatedLoadTest.getTestData());
|
||||||
|
|
||||||
|
return loadTestRepository.save(loadTest);
|
||||||
|
}).orElseGet(() -> {
|
||||||
|
// If the LoadTest with the given id doesn't exist, create a new one
|
||||||
|
updatedLoadTest.setId(id);
|
||||||
|
return loadTestRepository.save(updatedLoadTest);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteLoadTest(String id) {
|
||||||
|
loadTestRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class LoadTestMonitorMap {
|
||||||
|
|
||||||
|
private final Map<String, List<ScheduledFuture<?>>> futures = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, List<VirtualUser>> virtualUsers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Map<String, ThreadPoolTaskScheduler> schedulers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public void put(String testId, List<ScheduledFuture<?>> futures, List<VirtualUser> users, ThreadPoolTaskScheduler scheduler) {
|
||||||
|
this.futures.put(testId, futures);
|
||||||
|
virtualUsers.put(testId, users);
|
||||||
|
schedulers.put(testId, scheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(String testId) {
|
||||||
|
futures.remove(testId);
|
||||||
|
virtualUsers.remove(testId);
|
||||||
|
schedulers.remove(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThreadPoolTaskScheduler getScheduler(String testId) {
|
||||||
|
return schedulers.get(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScheduledFuture<?>> getFutures(String testId) {
|
||||||
|
return futures.get(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VirtualUser> getVirtualUsers(String testId) {
|
||||||
|
return virtualUsers.get(testId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void cleanup() {
|
||||||
|
// Perform cleanup logic here
|
||||||
|
//get all the keys and stop them all
|
||||||
|
futures.keySet().forEach(key -> {
|
||||||
|
List<ScheduledFuture<?>> value = futures.get(key);
|
||||||
|
value.forEach(future -> {
|
||||||
|
future.cancel(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
virtualUsers.keySet().forEach(key -> {
|
||||||
|
List<VirtualUser> value = virtualUsers.get(key);
|
||||||
|
value.forEach(VirtualUser::stop);
|
||||||
|
});
|
||||||
|
|
||||||
|
schedulers.keySet().forEach(key -> {
|
||||||
|
ThreadPoolTaskScheduler value = schedulers.get(key);
|
||||||
|
value.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||||
|
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
|
||||||
|
import com.eactive.testmaster.client.entity.ApiRequestInfo;
|
||||||
|
import com.eactive.testmaster.client.entity.ApiScenario;
|
||||||
|
import com.eactive.testmaster.client.mapper.ApiRequestMapper;
|
||||||
|
import com.eactive.testmaster.client.mapper.ApiScenarioMapper;
|
||||||
|
import com.eactive.testmaster.client.service.NettyApiClient;
|
||||||
|
import com.eactive.testmaster.loadtest.dto.ApiRequestEvent;
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestRequestAndResponse;
|
||||||
|
import com.lmax.disruptor.RingBuffer;
|
||||||
|
import com.lmax.disruptor.dsl.Disruptor;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
//@Scope("session")
|
||||||
|
public class LoadTestService extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
//logger
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LoadTestService.class);
|
||||||
|
private static long MINUTE = 10 * 1000L;
|
||||||
|
|
||||||
|
private SimpMessagingTemplate template;
|
||||||
|
|
||||||
|
private LoadTestMonitorMap loadTestMonitorMap;
|
||||||
|
|
||||||
|
private NettyApiClient nettyApiClient;
|
||||||
|
|
||||||
|
private ApiScenarioMapper apiScenarioMapper;
|
||||||
|
|
||||||
|
private ApiRequestMapper apiRequestMapper;
|
||||||
|
|
||||||
|
private final RingBuffer<ApiRequestEvent> ringBuffer;
|
||||||
|
|
||||||
|
public LoadTestService(SimpMessagingTemplate template, LoadTestMonitorMap loadTestMonitorMap, NettyApiClient nettyApiClient, ApiScenarioMapper apiScenarioMapper, ApiRequestMapper apiRequestMapper) {
|
||||||
|
this.template = template;
|
||||||
|
this.loadTestMonitorMap = loadTestMonitorMap;
|
||||||
|
this.nettyApiClient = nettyApiClient;
|
||||||
|
this.apiScenarioMapper = apiScenarioMapper;
|
||||||
|
this.apiRequestMapper = apiRequestMapper;
|
||||||
|
|
||||||
|
Disruptor<ApiRequestEvent> disruptor = new Disruptor<>(ApiRequestEvent::new, 1024, Executors.defaultThreadFactory());
|
||||||
|
disruptor.handleEventsWith(this::processApiRequestEvent);
|
||||||
|
ringBuffer = disruptor.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processApiRequestEvent(ApiRequestEvent event, long sequence, boolean endOfBatch) {
|
||||||
|
LoadTestRequestAndResponse requestAndResponse = event.getRequestAndResponse();
|
||||||
|
String testId = event.getTestId();
|
||||||
|
List<Long> responseTimes = new ArrayList<>();
|
||||||
|
insertSorted(responseTimes, requestAndResponse.getResponseTime());
|
||||||
|
|
||||||
|
template.convertAndSend("/topic/" + testId, requestAndResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void insertSorted(List<Long> list, long value) {
|
||||||
|
if (list.isEmpty() || value > list.get(list.size() - 1)) {
|
||||||
|
list.add(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int insertionPoint = findInsertionPoint(list, value);
|
||||||
|
list.add(insertionPoint, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findInsertionPoint(List<Long> list, long value) {
|
||||||
|
int low = 0;
|
||||||
|
int high = list.size() - 1;
|
||||||
|
|
||||||
|
while (low <= high) {
|
||||||
|
int mid = low + (high - low) / 2;
|
||||||
|
if (value == list.get(mid)) {
|
||||||
|
return mid; // Optional: Handle duplicate values as needed
|
||||||
|
} else if (value > list.get(mid)) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||||
|
String clientMessage = message.getPayload();
|
||||||
|
System.out.println("Received message: " + clientMessage);
|
||||||
|
session.sendMessage(new TextMessage("Hello from server!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void sendLoadTestUpdate(String topic, LoadTestRequestAndResponse requestAndResponse) {
|
||||||
|
long sequence = ringBuffer.next();
|
||||||
|
ApiRequestEvent event = ringBuffer.get(sequence);
|
||||||
|
event.setTestId(topic);
|
||||||
|
event.setRequestAndResponse(requestAndResponse);
|
||||||
|
ringBuffer.publish(sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendLoadTestError(String topic, String errorMessage) {
|
||||||
|
template.convertAndSend("/topic/" + topic, errorMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startLoadTest(LoadTestDTO loadTestDTO) {
|
||||||
|
int numberOfUsers = loadTestDTO.getThreadCount();
|
||||||
|
int maxCount = loadTestDTO.getLoopCount();
|
||||||
|
long rampUpTimeMillis = loadTestDTO.getRampUpTime();
|
||||||
|
long taskDurationMillis = loadTestDTO.getDuration() * MINUTE;
|
||||||
|
CountDownLatch latch = new CountDownLatch(numberOfUsers);
|
||||||
|
|
||||||
|
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||||
|
taskScheduler.setPoolSize(numberOfUsers + 1); // Add extra space for cancellation tasks
|
||||||
|
taskScheduler.initialize();
|
||||||
|
ApiScenario apiScenario = apiScenarioMapper.map(loadTestDTO.getScenarioId());
|
||||||
|
ScenarioNode scenario = LoadTestGraphBuilder.getTopNode(apiScenario.getScenario());
|
||||||
|
traverseDFS(scenario);
|
||||||
|
|
||||||
|
List<ScheduledFuture<?>> scheduledFutures = new ArrayList<>();
|
||||||
|
List<VirtualUser> virtualUsers = new ArrayList<>();
|
||||||
|
for (int i = 0; i < numberOfUsers; i++) {
|
||||||
|
final VirtualUser virtualUser = new VirtualUser(loadTestDTO.getId(), maxCount, scenario, this, nettyApiClient, latch);
|
||||||
|
virtualUsers.add(virtualUser);
|
||||||
|
|
||||||
|
Instant startTime = Instant.now().plusMillis(i * rampUpTimeMillis);
|
||||||
|
ScheduledFuture<?> userTask = taskScheduler.schedule(virtualUser, Date.from(startTime));
|
||||||
|
scheduledFutures.add(userTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskDurationMillis > 0) {
|
||||||
|
taskScheduler.schedule(() -> {
|
||||||
|
stopLoadTest(loadTestDTO.getId());
|
||||||
|
}, new Date(Instant.now().toEpochMilli() + taskDurationMillis)); // Schedule cancellation task at startTime + taskDurationMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTestMonitorMap.put(loadTestDTO.getId(), scheduledFutures, virtualUsers, taskScheduler);
|
||||||
|
|
||||||
|
try {
|
||||||
|
latch.await();
|
||||||
|
logger.debug("All virtual users have completed");
|
||||||
|
stopLoadTest(loadTestDTO.getId());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void traverseDFS(ScenarioNode node) {
|
||||||
|
if (node == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.getClassName() != null && node.getClassName().equalsIgnoreCase("api")) {
|
||||||
|
ApiRequestInfo apiRequestInfo = apiRequestMapper.map(node.getData().get("id"));
|
||||||
|
ApiRequestDTO apiRequestDTO = apiRequestMapper.map(apiRequestInfo);
|
||||||
|
node.setApiRequest(apiRequestDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visited set to avoid cycles
|
||||||
|
Set<String> visited = new HashSet<>();
|
||||||
|
visited.add(node.getId());
|
||||||
|
|
||||||
|
dfsHelper(node, visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dfsHelper(ScenarioNode node, Set<String> visited) {
|
||||||
|
if (node.getConnections() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ScenarioNode adjacentNode : node.getConnections()) {
|
||||||
|
if (!visited.contains(adjacentNode.getId())) {
|
||||||
|
// Process the adjacent node
|
||||||
|
System.out.println(adjacentNode.getName());
|
||||||
|
if (adjacentNode.getClassName() != null && adjacentNode.getClassName().equalsIgnoreCase("api")) {
|
||||||
|
ApiRequestInfo apiRequestInfo = apiRequestMapper.map(adjacentNode.getData().get("id"));
|
||||||
|
ApiRequestDTO apiRequestDTO = apiRequestMapper.map(apiRequestInfo);
|
||||||
|
adjacentNode.setApiRequest(apiRequestDTO);
|
||||||
|
}
|
||||||
|
visited.add(adjacentNode.getId());
|
||||||
|
|
||||||
|
// Recurse into adjacent node
|
||||||
|
dfsHelper(adjacentNode, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void stopLoadTest(String id) {
|
||||||
|
logger.debug("Stopping load test with id: " + id);
|
||||||
|
List<VirtualUser> virtualUsers = loadTestMonitorMap.getVirtualUsers(id);
|
||||||
|
if (virtualUsers != null) {
|
||||||
|
virtualUsers.forEach(VirtualUser::stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ScheduledFuture<?>> futures = loadTestMonitorMap.getFutures(id);
|
||||||
|
if (futures != null) {
|
||||||
|
futures.forEach(future -> {
|
||||||
|
future.cancel(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPoolTaskScheduler scheduler = loadTestMonitorMap.getScheduler(id);
|
||||||
|
if (scheduler != null) {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTestMonitorMap.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package com.eactive.testmaster.loadtest.service;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.client.dto.ApiRequestDTO;
|
||||||
|
import com.eactive.testmaster.client.dto.ApiRequestKeyValueDTO;
|
||||||
|
import com.eactive.testmaster.client.dto.ApiResponse;
|
||||||
|
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
|
||||||
|
import com.eactive.testmaster.client.service.NettyApiClient;
|
||||||
|
import com.eactive.testmaster.loadtest.dto.LoadTestRequestAndResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.script.ScriptEngine;
|
||||||
|
import javax.script.ScriptEngineManager;
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
import org.apache.commons.lang3.SerializationUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class VirtualUser implements Runnable {
|
||||||
|
|
||||||
|
//logger
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(VirtualUser.class);
|
||||||
|
private final Object pauseLock = new Object();
|
||||||
|
private boolean paused = false;
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
|
private int maxCount;
|
||||||
|
|
||||||
|
private Thread runningThread;
|
||||||
|
|
||||||
|
private final LoadTestService loadTestService;
|
||||||
|
private final NettyApiClient nettyApiClient;
|
||||||
|
|
||||||
|
private static Map<String, String> globals = new ConcurrentHashMap<>();
|
||||||
|
private Map<String, String> variables = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private String testId;
|
||||||
|
|
||||||
|
private ScenarioNode scenario;
|
||||||
|
|
||||||
|
private Set<ScenarioNode> visitedNodes = new HashSet<>();
|
||||||
|
|
||||||
|
private CountDownLatch latch;
|
||||||
|
|
||||||
|
public VirtualUser(String testId, int maxCount, ScenarioNode scenarioDTO, LoadTestService loadTestService, NettyApiClient nettyApiClient, CountDownLatch latch) {
|
||||||
|
this.testId = testId;
|
||||||
|
this.scenario = scenarioDTO;
|
||||||
|
this.loadTestService = loadTestService;
|
||||||
|
this.nettyApiClient = nettyApiClient;
|
||||||
|
this.maxCount = maxCount;
|
||||||
|
this.latch = latch;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
synchronized (pauseLock) {
|
||||||
|
paused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resume() {
|
||||||
|
synchronized (pauseLock) {
|
||||||
|
paused = false;
|
||||||
|
pauseLock.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
running = false;
|
||||||
|
latch.countDown();
|
||||||
|
resume(); // To ensure the user exits if it's paused
|
||||||
|
if (runningThread != null) {
|
||||||
|
logger.debug("Virtual User Interrupting thread");
|
||||||
|
runningThread.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
logger.debug("Virtual user started");
|
||||||
|
runningThread = Thread.currentThread();
|
||||||
|
int count = 0;
|
||||||
|
while (maxCount == 0 || running && count < maxCount) {
|
||||||
|
synchronized (pauseLock) {
|
||||||
|
if (paused) {
|
||||||
|
try {
|
||||||
|
pauseLock.wait();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt(); // Preserve interrupt status
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!running) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//clear visited nodes
|
||||||
|
visitedNodes.clear();
|
||||||
|
performAction();
|
||||||
|
logger.debug("Count: " + (count + 1) + " of " + maxCount);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performAction() {
|
||||||
|
ScenarioNode currentNode = scenario;
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (currentNode != null) {
|
||||||
|
if (visitedNodes.contains(currentNode)) {
|
||||||
|
logger.debug("Loop detected, stopping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
visitedNodes.add(currentNode);
|
||||||
|
|
||||||
|
if (currentNode.getClassName().equalsIgnoreCase("api")) {
|
||||||
|
processApi(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNode.getConnections() != null && !currentNode.getConnections().isEmpty()) {
|
||||||
|
currentNode = currentNode.getConnections().get(0); //현재는 1개의 connection만 지원
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Action performed or interrupted");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processApi(ScenarioNode currentNode) {
|
||||||
|
try {
|
||||||
|
if (currentNode.getApiRequest() != null) {
|
||||||
|
ApiRequestDTO cloned = SerializationUtils.clone(currentNode.getApiRequest());
|
||||||
|
|
||||||
|
executePreRequestScript(cloned);
|
||||||
|
ApiRequestDTO replaced = replacePlaceHoldersWithVariables(cloned);
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
ApiResponse response = nettyApiClient.handleRequest(replaced).get();
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long responseTime = endTime - startTime;
|
||||||
|
|
||||||
|
executePostRequestScript(replaced, response);
|
||||||
|
LoadTestRequestAndResponse requestAndResponse = new LoadTestRequestAndResponse(replaced, response, responseTime);
|
||||||
|
loadTestService.sendLoadTestUpdate(testId, requestAndResponse);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
loadTestService.sendLoadTestError(testId, "An error occurred: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executePreRequestScript(ApiRequestDTO apiRequest) {
|
||||||
|
ScriptEngineManager manager = new ScriptEngineManager();
|
||||||
|
ScriptEngine engine = manager.getEngineByName("nashorn");
|
||||||
|
engine.put("console", new Console());
|
||||||
|
engine.put("globals", globals);
|
||||||
|
engine.put("variables", variables);
|
||||||
|
engine.put("request", apiRequest);
|
||||||
|
try {
|
||||||
|
engine.eval(apiRequest.getPreRequestScript());
|
||||||
|
} catch (ScriptException e) {
|
||||||
|
logger.error("Error executing pre-request script", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executePostRequestScript(ApiRequestDTO apiRequest, ApiResponse response) {
|
||||||
|
ScriptEngineManager manager = new ScriptEngineManager();
|
||||||
|
ScriptEngine engine = manager.getEngineByName("nashorn");
|
||||||
|
engine.put("console", new Console());
|
||||||
|
engine.put("globals", globals);
|
||||||
|
engine.put("variables", variables);
|
||||||
|
engine.put("request", apiRequest);
|
||||||
|
engine.put("response", response);
|
||||||
|
try {
|
||||||
|
engine.eval(apiRequest.getPostRequestScript());
|
||||||
|
} catch (ScriptException e) {
|
||||||
|
logger.error("Error executing post-request script", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiRequestDTO replacePlaceHoldersWithVariables(ApiRequestDTO apiRequest) {
|
||||||
|
Map<String, String> mergedVariables = new HashMap<>();
|
||||||
|
|
||||||
|
// Merge globals and variables
|
||||||
|
if (globals != null) {
|
||||||
|
mergedVariables.putAll(globals);
|
||||||
|
}
|
||||||
|
if (variables != null) {
|
||||||
|
mergedVariables.putAll(variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : mergedVariables.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
String placeholder = "\\{\\{" + key + "\\}\\}"; // Escape braces for regex
|
||||||
|
|
||||||
|
apiRequest.setPath(apiRequest.getPath().replaceAll(placeholder, value));
|
||||||
|
|
||||||
|
if (apiRequest.getRequestBody() != null) {
|
||||||
|
apiRequest.setRequestBody(apiRequest.getRequestBody().replaceAll(placeholder, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiRequest.getHeaders() != null) {
|
||||||
|
apiRequest.setHeaders(replaceInList(apiRequest.getHeaders(), placeholder, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiRequest.getQueryParams() != null) {
|
||||||
|
apiRequest.setQueryParams(replaceInList(apiRequest.getQueryParams(), placeholder, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ApiRequestKeyValueDTO> replaceInList(List<ApiRequestKeyValueDTO> originalList, String placeholder, String value) {
|
||||||
|
List<ApiRequestKeyValueDTO> replacedList = new ArrayList<>();
|
||||||
|
Pattern pattern = Pattern.compile(placeholder);
|
||||||
|
|
||||||
|
originalList.forEach(kv -> {
|
||||||
|
String replacedKey = pattern.matcher(kv.getKey()).replaceAll(value);
|
||||||
|
String replacedValue = pattern.matcher(kv.getValue()).replaceAll(value);
|
||||||
|
replacedList.add(new ApiRequestKeyValueDTO(kv.isEnabled(), replacedKey, replacedValue));
|
||||||
|
});
|
||||||
|
|
||||||
|
return replacedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.eactive.testmaster.server.controller;
|
||||||
|
|
||||||
|
import com.eactive.testmaster.server.dto.ServerDTO;
|
||||||
|
import com.eactive.testmaster.server.dto.ServerSearch;
|
||||||
|
import com.eactive.testmaster.server.entity.Server;
|
||||||
|
import com.eactive.testmaster.server.mapper.ServerMapper;
|
||||||
|
import com.eactive.testmaster.server.service.ServerMgmtService;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.ui.ModelMap;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/mgmt/servers")
|
||||||
|
public class ServerRestController {
|
||||||
|
|
||||||
|
private final ServerMgmtService serverMgmtService;
|
||||||
|
|
||||||
|
private final ServerMapper serverMapper;
|
||||||
|
|
||||||
|
public ServerRestController(ServerMgmtService serverMgmtService, ServerMapper serverMapper) {
|
||||||
|
this.serverMgmtService = serverMgmtService;
|
||||||
|
this.serverMapper = serverMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list.do")
|
||||||
|
public ResponseEntity<List<ServerDTO>> listView(@ModelAttribute("serverSearch") ServerSearch serverSearch, ModelMap model, Pageable pageable) {
|
||||||
|
Page<Server> page = serverMgmtService.findAll(serverSearch.buildSpecification(), pageable);
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.add("X-Total-Count", String.valueOf(page.getTotalElements()));
|
||||||
|
headers.add("X-Total-Pages", String.valueOf(page.getTotalPages()));
|
||||||
|
headers.add("X-Current-Page", String.valueOf(page.getNumber()));
|
||||||
|
headers.add("X-Page-Size", String.valueOf(page.getSize()));
|
||||||
|
return new ResponseEntity<>(serverMapper.map(page.getContent()), headers, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,15 @@ import com.eactive.testmaster.common.exception.NotFoundException;
|
|||||||
import com.eactive.testmaster.server.dto.ServerDTO;
|
import com.eactive.testmaster.server.dto.ServerDTO;
|
||||||
import com.eactive.testmaster.server.entity.Server;
|
import com.eactive.testmaster.server.entity.Server;
|
||||||
import com.eactive.testmaster.server.entity.ServerRepository;
|
import com.eactive.testmaster.server.entity.ServerRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.ReportingPolicy;
|
import org.mapstruct.ReportingPolicy;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Mapper(componentModel = "spring",
|
@Mapper(componentModel = "spring",
|
||||||
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||||
@Component
|
@Component
|
||||||
public class ServerMapper {
|
public class ServerMapper {
|
||||||
|
|
||||||
@@ -19,17 +21,23 @@ public class ServerMapper {
|
|||||||
ServerRepository serverRepository;
|
ServerRepository serverRepository;
|
||||||
|
|
||||||
public Server mapById(Long id) {
|
public Server mapById(Long id) {
|
||||||
if (id == null || id == 0) return null;
|
if (id == null || id == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return serverRepository.findById(id).orElseThrow(() -> new NotFoundException("Server[" + id + "] not found"));
|
return serverRepository.findById(id).orElseThrow(() -> new NotFoundException("Server[" + id + "] not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long mapById(Server server) {
|
public Long mapById(Server server) {
|
||||||
if (server == null) return null;
|
if (server == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return server.getId();
|
return server.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerDTO map(Server server) {
|
public ServerDTO map(Server server) {
|
||||||
if (server == null) return null;
|
if (server == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
ServerDTO dto = new ServerDTO();
|
ServerDTO dto = new ServerDTO();
|
||||||
|
|
||||||
dto.setId(server.getId());
|
dto.setId(server.getId());
|
||||||
@@ -45,7 +53,9 @@ public class ServerMapper {
|
|||||||
|
|
||||||
|
|
||||||
public Server map(ServerDTO dto) {
|
public Server map(ServerDTO dto) {
|
||||||
if (dto == null) return null;
|
if (dto == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Server server = new Server();
|
Server server = new Server();
|
||||||
|
|
||||||
@@ -59,4 +69,8 @@ public class ServerMapper {
|
|||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ServerDTO> map(List<Server> servers) {
|
||||||
|
return servers.stream().map(this::map).collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<script th:src="@{/js/jquery-validation/localization/messages_ko.min.js}"></script>
|
<script th:src="@{/js/jquery-validation/localization/messages_ko.min.js}"></script>
|
||||||
<script th:src="@{/js/common.js}"></script>
|
<script th:src="@{/js/common.js}"></script>
|
||||||
<script th:src="@{/js/dataTables/datatables.min.js}"></script>
|
<script th:src="@{/js/dataTables/datatables.min.js}"></script>
|
||||||
<script th:src="@{/plugins/apitestmanager/app.bundle.js}" defer></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<div class="text-center flex-fill" sec:authorize="isAuthenticated()">
|
<div class="text-center flex-fill" sec:authorize="isAuthenticated()">
|
||||||
<a class="nav-link" th:href="@{/mgmt/api_tester.do}">요청 관리</a>
|
<a class="nav-link" th:href="@{/mgmt/api_tester.do}">요청 관리</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-center flex-fill" sec:authorize="isAuthenticated()">
|
||||||
|
<a class="nav-link" th:href="@{/mgmt/load_tester.do}">부하 테스트</a>
|
||||||
|
</div>
|
||||||
<div class="text-center flex-fill" sec:authorize="hasRole('ROLE_ADMIN')">
|
<div class="text-center flex-fill" sec:authorize="hasRole('ROLE_ADMIN')">
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<div class="nav-item dropdown">
|
<div class="nav-item dropdown">
|
||||||
|
|||||||
@@ -51,8 +51,8 @@
|
|||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
<link th:href="@{/css/resizable.css}" rel="stylesheet" />
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
const container = document.querySelector('.api-tester-layout');
|
const container = document.querySelector('.api-tester-layout');
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer th:replace="fragment/footer :: footerFragment"></footer>
|
<!--<footer th:replace="fragment/footer :: footerFragment"></footer>-->
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</th:block>
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer th:replace="fragment/footer :: footerFragment"></footer>
|
<!--<footer th:replace="fragment/footer :: footerFragment"></footer>-->
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" name="id" id="id" maxlength="20" required>
|
<input type="text" class="form-control" name="id" id="id" maxlength="20" required style="border-bottom-left-radius: 0; border-bottom-right-radius: 0">
|
||||||
<label for="id" class="form-label">ID</label>
|
<label for="id" class="form-label">ID</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
<button type="button" class="btn btn-primary btn-sm stop_scenario me-2">정지</button>
|
<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 export">내보내기</button>-->
|
||||||
<!-- <button type="button" class="btn btn-primary btn-sm clear me-2">초기화</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>-->
|
|
||||||
</div>
|
</div>
|
||||||
<div>시나리오 설명이 여기에 표시</div>
|
<div>시나리오 설명이 여기에 표시</div>
|
||||||
<div class="api-scenario-editor-body" style="display: flex; flex-direction: row; flex: 1; border: 1px solid grey; padding: 5px;">
|
<div class="api-scenario-editor-body" style="display: flex; flex-direction: row; flex: 1; border: 1px solid grey; padding: 5px;">
|
||||||
|
|||||||
@@ -4,58 +4,47 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<section layout:fragment="contentFragment">
|
<section layout:fragment="contentFragment">
|
||||||
<!-- <script>-->
|
<div class="container-fluid mt-2" style="height: 100%">
|
||||||
<!-- require.config({paths: {'vs': '/plugins/vs'}});-->
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<!-- </script>-->
|
<div class="card" style="height: 100%">
|
||||||
<div class="container-fluid mt-2">
|
<div class="card-header" style="min-height: 50px;">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<ul class="nav nav-tabs card-header-tabs" role="tablist" id="apiRequestTabTitle">
|
||||||
<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 id="popperContent" class="variable_popper" style="display: none;">
|
|
||||||
<div class="variable_popper">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: none">
|
|
||||||
<ul id="servers">
|
|
||||||
<li th:each="server : ${servers}" th:data-id="${server.id}" th:data-name="${server.name}" th:data-path="${server.basePath}"></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="min-height: 800px;">
|
||||||
|
<div class="tab-content" id="apiRequestTabContent" style="height: 100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="preRequest" class="preRequest"></iframe>
|
</div>
|
||||||
<iframe id="postRequest" class="postRequest"></iframe>
|
<div id="popperContent" class="variable_popper" style="display: none;">
|
||||||
|
<div class="variable_popper">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<iframe id="preRequest" class="preRequest"></iframe>
|
||||||
|
<iframe id="postRequest" class="postRequest"></iframe>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
<script>
|
<script th:src="@{/plugins/apitestmanager/app.bundle.js}" defer></script>
|
||||||
$(document).ajaxError(function (event, jqxhr, settings, exception) {
|
<script>
|
||||||
if (jqxhr.status === 401) {
|
$(document).ajaxError(function(event, jqxhr, settings, exception) {
|
||||||
window.location.href = '[[@{/}]]';
|
if (jqxhr.status === 401) {
|
||||||
}
|
window.location.href = '[[@{/}]]';
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
let servers = [];
|
fetch('[[@{/mgmt/servers/list.do}]]').then(response => response.json()).then(data => {
|
||||||
$('#servers li').each(function () {
|
let options = {};
|
||||||
servers.push({
|
options.contextPath = '[[@{/}]]';
|
||||||
id: $(this).data('id'),
|
options.servers = data;
|
||||||
name: $(this).data('name'),
|
const apiTester = new APITestManager(options);
|
||||||
basePath: $(this).data('path')
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
let options = {};
|
</script>
|
||||||
options.contextPath = '[[@{/}]]';
|
|
||||||
const apiTester = new APITestManager(servers, options);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</th:block>
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/base_layout}">
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<section layout:fragment="contentFragment">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<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>
|
||||||
|
<div class="api-tester-layout">
|
||||||
|
<div class="modal fade" id="new_load_test_modal" tabindex="-1" aria-labelledby="new_load_test_modal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">새로운 테스트 추가</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="text" id="new_load_test_name" class="form-control" placeholder="이름을 입력하세요" aria-label="이름을 입력하세요" name="test_name" required/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||||
|
<button type="button" class="btn btn-primary create_load_test">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="confirm_delete_modal" tabindex="-1" aria-labelledby="confirm_delete_load_test_modal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">테스트 삭제</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div>삭제 하시겠습니까?</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||||
|
<button type="button" class="btn btn-primary confirm_delete_load_test">삭제</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tester">
|
||||||
|
<section class="d-flex flex-row" style="width: 100%">
|
||||||
|
<div class="load-test-sidebar border" style="width: 200px; ">
|
||||||
|
<div style="background: rgba(0,0,0,.03); padding: 2px;">
|
||||||
|
테스트 목록
|
||||||
|
<button type="button" class="btn btn-primary me-2" id="new_load_test"><i class="fa fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="pt-links">
|
||||||
|
<ul class="list-unstyled load_test_list" id="load_test_menubar">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column" style="flex: 1;">
|
||||||
|
<div class="d-flex" style="width: 100%; height: 80px; background: rgba(0, 0, 0, .03); padding: 5px;" id="load-tester-view">
|
||||||
|
</div>
|
||||||
|
<div class="test-log d-flex flex-row" style="flex: 1" id="load-tester-result-view">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<th:block layout:fragment="contentScript">
|
||||||
|
<link th:href="@{/css/goldenlayout-base.css}" rel="stylesheet"/>
|
||||||
|
<link th:href="@{/css/goldenlayout-light-theme.css}" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/xspreadsheet.css}">
|
||||||
|
<script th:src="@{/plugins/apitestmanager/app.bundle.js}"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
$(document).ajaxError(function(event, jqxhr, settings, exception) {
|
||||||
|
if (jqxhr.status === 401) {
|
||||||
|
window.location.href = '[[@{/}]]';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
let options = {};
|
||||||
|
options.contextPath = '[[@{/}]]';
|
||||||
|
options.type = 'load';
|
||||||
|
new APITestManager(options);
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user