API시나리오 단계별 실행
This commit is contained in:
+12
-3
@@ -3,24 +3,33 @@ package com.eactive.httpmockserver.client.controller;
|
||||
import com.eactive.httpmockserver.client.dto.ApiRequestDTO;
|
||||
import com.eactive.httpmockserver.client.dto.ApiResponse;
|
||||
import com.eactive.httpmockserver.client.service.NettyApiClient;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@RestController
|
||||
public class ApiClientController {
|
||||
|
||||
private final NettyApiClient nettyApiClient;
|
||||
private final Validator validator;
|
||||
|
||||
public ApiClientController(NettyApiClient nettyApiClient) {
|
||||
public ApiClientController(NettyApiClient nettyApiClient, Validator validator) {
|
||||
this.nettyApiClient = nettyApiClient;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@PostMapping("/mgmt/api/test.do")
|
||||
public ApiResponse handleApiRequest(@RequestBody ApiRequestDTO request) throws InterruptedException, ExecutionException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException {
|
||||
public ApiResponse handleApiRequest(@RequestBody ApiRequestDTO request, BindingResult bindingResult) throws InterruptedException, ExecutionException {
|
||||
|
||||
validator.validate(request, bindingResult);
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new IllegalArgumentException(bindingResult.getAllErrors().get(0).getDefaultMessage());
|
||||
}
|
||||
|
||||
return nettyApiClient.handleRequest(request).get();
|
||||
}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ public class ApiScenarioMgmtController {
|
||||
}
|
||||
|
||||
|
||||
@PutMapping("/update.do")
|
||||
@PostMapping("/update.do")
|
||||
public ResponseEntity<ApiScenarioDTO> updateScenario(@RequestBody ApiScenarioDTO apiScenarioDto) {
|
||||
ApiScenario updatedApiScenario = apiScenarioMapper.map(apiScenarioDto);
|
||||
ApiScenario updatedScenario = apiScenarioMgmtService.updateApiScenario(apiScenarioDto.getId(), updatedApiScenario);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.eactive.httpmockserver.client.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -12,6 +13,7 @@ public class ApiRequestDTO {
|
||||
private String path;
|
||||
private String method;
|
||||
|
||||
@NotNull(message = "서버를 선택해주세요.")
|
||||
private Long server;
|
||||
|
||||
private List<ApiRequestKeyValueDTO> headers = new ArrayList<>();
|
||||
|
||||
@@ -20,14 +20,12 @@ import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import org.apache.commons.beanutils.BeanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
@@ -36,7 +34,7 @@ public class NettyApiClient {
|
||||
@Autowired
|
||||
ServerRepository serverRepository;
|
||||
|
||||
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO originalRequest) throws InterruptedException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException {
|
||||
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO originalRequest) {
|
||||
ApiRequestDTO apiRequest = originalRequest;
|
||||
Server server = serverRepository.findById(apiRequest.getServer()).orElseThrow(() -> new ServerNotFoundException("Server not found"));
|
||||
String host = server.getHostname();
|
||||
@@ -73,7 +71,7 @@ public class NettyApiClient {
|
||||
content = Unpooled.EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
String fullPath = server.getBasePath() + apiRequest.getPath();
|
||||
String fullPath = server.getBasePath() + apiRequest.getPath();
|
||||
|
||||
HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(apiRequest.getMethod()), fullPath, content);
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addResourceHandler("/img/**").addResourceLocations("/img/", "classpath:/static/img/").setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)).resourceChain(true).addResolver(new PathResourceResolver());
|
||||
registry.addResourceHandler("/js/**").addResourceLocations("/js/", "classpath:/static/js/").setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)).resourceChain(true).addResolver(new PathResourceResolver());
|
||||
registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/", "classpath:/static/plugins/").setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)).resourceChain(true).addResolver(new PathResourceResolver());
|
||||
registry.addResourceHandler("/min-maps/**").addResourceLocations("/min-maps/", "classpath:/static/plugins/").setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)).resourceChain(true).addResolver(new PathResourceResolver());
|
||||
registry.addResourceHandler("/favicon.ico").addResourceLocations("/favicon.ico", "classpath:/static/favicon.ico").setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)).resourceChain(true).addResolver(new PathResourceResolver());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,16 @@ class APIClient {
|
||||
window.globals = {};
|
||||
this.globalVariables = window.globals;
|
||||
this.variables = {};
|
||||
this.currentAjaxRequest = null;
|
||||
this.abortController = null;
|
||||
}
|
||||
|
||||
async sendAPIRequest(model) {
|
||||
async sendAPIRequest(model, callback) {
|
||||
try {
|
||||
const updatedModel = await this.executePreRequestScript(model);
|
||||
let processedRequest = this.replacePlaceholdersWithVariables(updatedModel);
|
||||
if (callback){
|
||||
callback(processedRequest);
|
||||
}
|
||||
return this.makeAjaxRequest(processedRequest, model);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -45,14 +47,17 @@ class APIClient {
|
||||
body: JSON.stringify(processedRequest),
|
||||
signal: signal // pass the signal to the fetch
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(async response => {
|
||||
const responseData = await response.json(); // Parse the JSON response
|
||||
if (!response.ok) {
|
||||
// If response is not OK, use the parsed error information
|
||||
const errorMessage = responseData.error || 'Unknown error';
|
||||
throw new Error(`API 호출 중 오류가 발생했습니다. 사유[${errorMessage}]`);
|
||||
}
|
||||
return responseData;
|
||||
})
|
||||
.then(data => {
|
||||
try {
|
||||
model.responseModel.status = data.status;
|
||||
model.responseModel.body = data.body;
|
||||
model.responseModel.headers = data.headers;
|
||||
model.responseModel.size = data.size;
|
||||
|
||||
let response = {};
|
||||
response.body = data.body;
|
||||
response.status = data.status;
|
||||
@@ -61,7 +66,8 @@ class APIClient {
|
||||
_.forEach(data.headers, (header) => {
|
||||
response.headers[header.key] = header.value;
|
||||
});
|
||||
return this.executePostRequestScript(model.postRequestScript, model.responseModel);
|
||||
|
||||
return this.executePostRequestScript(model.postRequestScript, response);
|
||||
} catch (error) {
|
||||
console.log('error1')
|
||||
$('.toast-body').text(error);
|
||||
@@ -69,14 +75,7 @@ class APIClient {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('error2')
|
||||
console.log(error.name)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('Fetch aborted');
|
||||
} else {
|
||||
// $('.toast-body').text(response.responseJSON.error);
|
||||
// $('.toast').toast('show');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ require.config({paths: {'vs': '/plugins/vs'}});
|
||||
|
||||
class APIScenario {
|
||||
|
||||
|
||||
constructor(apiTester) {
|
||||
this.scenarioList = [];
|
||||
this.scenarioTab = [];
|
||||
this.currentIndex = -1;
|
||||
this.currentScenario = null;
|
||||
this.apiTester = apiTester;
|
||||
this.dataflow = {drawflow: {Home: {data: {} }}};
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -31,14 +30,7 @@ class APIScenario {
|
||||
this.editor.force_first_input = true;
|
||||
this.editor.start();
|
||||
|
||||
this.editor.on('connectionCreated', (connection) => {
|
||||
console.log('Connection created');
|
||||
console.log(connection);
|
||||
});
|
||||
this.showOutput = true;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
loadScenarioList() {
|
||||
@@ -49,11 +41,24 @@ class APIScenario {
|
||||
contentType: 'application/json',
|
||||
success: function (data) {
|
||||
me.scenarioList = data;
|
||||
let drawflowData = {drawflow: {Home: {data: {}}}};
|
||||
data.forEach((scenario) => {
|
||||
me.dataflow.drawflow[scenario.id] = {data : JSON.parse(scenario.scenario)};
|
||||
let scenarioData = null;
|
||||
try {
|
||||
scenarioData = JSON.parse(scenario.scenario)
|
||||
} catch (e) {
|
||||
scenarioData = {};
|
||||
}
|
||||
drawflowData.drawflow[scenario.id] = {data: scenarioData};
|
||||
});
|
||||
console.log(JSON.stringify(me.dataflow));
|
||||
me.editor.import(me.dataflow);
|
||||
|
||||
me.editor.import(drawflowData);
|
||||
// if (data.length === 0) {
|
||||
me.editor.changeModule('Home');
|
||||
// } else {
|
||||
// me.editor.changeModule(data[0].id);
|
||||
// me.currentScenario = data[0].id;
|
||||
// }
|
||||
},
|
||||
error: function (response, textStatus, errorThrown) {
|
||||
me.hideLoadingOverlay();
|
||||
@@ -74,46 +79,19 @@ class APIScenario {
|
||||
openScenario(event) {
|
||||
event.preventDefault();
|
||||
let scenarioId = $(event.target).closest('li').data('id');
|
||||
var all = document.querySelectorAll(".api-scenario-list ul li");
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
let all = document.querySelectorAll(".api-scenario-list li");
|
||||
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
all[i].classList.remove('selected');
|
||||
}
|
||||
event.target.classList.add('selected');
|
||||
event.target.closest('li').classList.add('selected');
|
||||
|
||||
this.editor.changeModule(scenarioId);
|
||||
}
|
||||
|
||||
openTab(scenario) {
|
||||
console.log('openTab');
|
||||
console.log(scenario);
|
||||
// this.currentScenario = scenario;
|
||||
// let openTab = this.scenarioTab.find((tab) => tab.id === scenario.id);
|
||||
// if (openTab) {
|
||||
// this.currentIndex = this.scenarioTab.indexOf(openTab);
|
||||
// //$(`#api_scenario_tab_${id}`).tab('show'); // API request 탭 활성화
|
||||
// } else {
|
||||
// this.scenarioTab.push(scenario);
|
||||
// this.renderScenarioTab(scenario);
|
||||
// }
|
||||
}
|
||||
|
||||
renderScenarioTab(scenario) {
|
||||
this.renderTabHeader(scenario);
|
||||
this.renderTabContent(scenario);
|
||||
}
|
||||
|
||||
renderTabHeader(scenario) {
|
||||
console.log('renderTabHeader');
|
||||
|
||||
}
|
||||
|
||||
renderTabContent(scenario) {
|
||||
console.log('renderTabContent')
|
||||
|
||||
this.currentScenario = scenarioId;
|
||||
}
|
||||
|
||||
getScenario(scenarioId) {
|
||||
return this.getScenarioList().find((scenario) => scenario.id === scenarioId);
|
||||
|
||||
}
|
||||
|
||||
showLoadingOverlay() {
|
||||
@@ -129,7 +107,7 @@ class APIScenario {
|
||||
$('#new_scenario_modal').modal('show');
|
||||
}
|
||||
|
||||
saveScenario() {
|
||||
createScenario() {
|
||||
const name = $('#new_scenario_name').val();
|
||||
const me = this;
|
||||
me.showLoadingOverlay();
|
||||
@@ -137,7 +115,7 @@ class APIScenario {
|
||||
url: '/mgmt/api_scenario/create.do',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({name: name, scenario: "", description: ""}),
|
||||
data: JSON.stringify({name: name, scenario: "{}", description: ""}),
|
||||
error: function (response, textStatus, errorThrown) {
|
||||
me.hideLoadingOverlay();
|
||||
$('.toast-body').text(response.responseJSON.error);
|
||||
@@ -151,8 +129,63 @@ class APIScenario {
|
||||
});
|
||||
}
|
||||
|
||||
removeScenario(scenarioId) {
|
||||
saveScenario() {
|
||||
if (this.currentScenario !== null) {
|
||||
let data = this.editor.export();
|
||||
console.log(data.drawflow[this.currentScenario].data);
|
||||
let updatedScenario = this.getScenario(this.currentScenario);
|
||||
updatedScenario.scenario = JSON.stringify(data.drawflow[this.currentScenario].data);
|
||||
|
||||
const me = this;
|
||||
me.showLoadingOverlay();
|
||||
this.currentAjaxRequest = $.ajax({
|
||||
url: '/mgmt/api_scenario/update.do',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(updatedScenario),
|
||||
error: function (response, textStatus, errorThrown) {
|
||||
me.hideLoadingOverlay();
|
||||
$('.toast-body').text(response.responseJSON.error);
|
||||
$('.toast').toast('show');
|
||||
},
|
||||
complete: function () {
|
||||
me.hideLoadingOverlay();
|
||||
$('.toast-body').text('저장되었습니다.');
|
||||
$('.toast').toast('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showDeleteModal(event) {
|
||||
const id = $(event.currentTarget).closest('li').data('id');
|
||||
$('#confirm_delete_scenario_modal').find('.confirm_delete_scenario').data('id', id);
|
||||
$('#confirm_delete_scenario_modal').modal('show');
|
||||
}
|
||||
|
||||
removeScenario(event) {
|
||||
const id = $(event.currentTarget).data('id');
|
||||
const me = this;
|
||||
me.showLoadingOverlay();
|
||||
this.currentAjaxRequest = $.ajax({
|
||||
url: '/mgmt/api_scenario/delete.do',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({id: id}),
|
||||
error: function (response, textStatus, errorThrown) {
|
||||
me.hideLoadingOverlay();
|
||||
$('.toast-body').text(response.responseJSON.error);
|
||||
$('.toast').toast('show');
|
||||
},
|
||||
complete: function () {
|
||||
me.hideLoadingOverlay();
|
||||
$('#confirm_delete_scenario_modal').modal('hide');
|
||||
$('.toast-body').text('삭제되었습니다.');
|
||||
$('.toast').toast('show');
|
||||
me.editor.changeModule('Home');
|
||||
me.loadScenarioList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleOutput() {
|
||||
@@ -204,16 +237,19 @@ class APIScenario {
|
||||
|
||||
runScript() {
|
||||
console.log(this.editor.export().drawflow);
|
||||
let graph = APIScenarioExecutor.extractGraphFromStart(this.editor.export().drawflow.Home.data);
|
||||
this.executor = new APIScenarioExecutor(graph, this.apiTester);
|
||||
this.outputEditor.setValue(JSON.stringify(graph, null, 4));
|
||||
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();
|
||||
}
|
||||
this.executor.executeAll();
|
||||
if (!this.executor.isDone) {
|
||||
this.executor.executeAll();
|
||||
} else {
|
||||
this.executor.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,8 +257,13 @@ class APIScenario {
|
||||
if (!this.executor) {
|
||||
this.runScript();
|
||||
}
|
||||
this.executor.executeCurrentNode();
|
||||
this.executor.moveToNextNode();
|
||||
if (!this.executor.isDone) {
|
||||
this.executor.executeCurrentNode();
|
||||
this.executor.moveToNextNode();
|
||||
} else {
|
||||
this.executor.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resetScenario() {
|
||||
@@ -234,9 +275,11 @@ class APIScenario {
|
||||
}
|
||||
|
||||
stopScenario() {
|
||||
|
||||
if (this.executor)
|
||||
this.executor.reset();
|
||||
}
|
||||
addStart(){
|
||||
|
||||
addStart() {
|
||||
let start = `
|
||||
<div>
|
||||
<div><i class="fas fa-play"></i> Start </div>
|
||||
@@ -256,8 +299,12 @@ class APIScenario {
|
||||
{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) {
|
||||
@@ -296,6 +343,12 @@ class APIScenario {
|
||||
let scenarioList = this.getScenarioList();
|
||||
let scenarioListHtml = scenarioList.map((scenario) => this.scenarioListTemplate(scenario)).join('');
|
||||
$('.api-scenario-list').html(scenarioListHtml);
|
||||
let nodes = document.querySelectorAll(".api-scenario-list li");
|
||||
nodes.forEach((node) => {
|
||||
if ($(node).data('id') === this.currentScenario) {
|
||||
node.classList.add('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
class APIScenarioExecutor {
|
||||
constructor(graph, apiTester) {
|
||||
constructor(graph, apiTester, outputEditor) {
|
||||
this.graph = graph; // The graph data
|
||||
this.currentNodeId = this.findStartNodeId(); // Initialize at the 'start' node
|
||||
this.apiClient = new APIClient();
|
||||
this.apiTester = apiTester;
|
||||
this.visitedNodes = new Set(); // Keep track of visited nodes
|
||||
this.outputEditor = outputEditor;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
// Helper method to find the start node ID
|
||||
@@ -17,38 +17,108 @@ class APIScenarioExecutor {
|
||||
return null; // Return null if no start node is found
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.currentNodeId = this.findStartNodeId(); // Initialize at the 'start' node
|
||||
this.visitedNodes = new Set(); // Keep track of visited nodes
|
||||
this.isRunning = false;
|
||||
this.isDone = false;
|
||||
|
||||
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
|
||||
allNodes.forEach(node => {
|
||||
node.classList.remove('current-execution');
|
||||
node.classList.remove('result-success');
|
||||
node.classList.remove('result-error');
|
||||
node.classList.remove('result-abort');
|
||||
});
|
||||
}
|
||||
|
||||
// Method to execute the current node's API request if it's an 'api' node
|
||||
executeCurrentNode() {
|
||||
console.log(this.currentNodeId)
|
||||
|
||||
let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
|
||||
allNodes.forEach(node => {
|
||||
node.classList.remove('current-execution');
|
||||
});
|
||||
|
||||
if (!this.currentNodeId) {
|
||||
console.log('No current node to execute.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNode = this.graph[this.currentNodeId];
|
||||
if (currentNode && currentNode.class === 'api') {
|
||||
// get API model with currentNode.id
|
||||
let model = this.apiTester.getApiRequestModel(currentNode.id);
|
||||
this.apiClient.sendAPIRequest(model)
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.name)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('Fetch aborted');
|
||||
} else {
|
||||
console.log('error3')
|
||||
}
|
||||
});
|
||||
console.log(`Executing API request for node: ${currentNode.id} - ${currentNode.name}`);
|
||||
} else {
|
||||
console.log('Current node is not an API node or does not exist.');
|
||||
}
|
||||
this.visitedNodes.add(this.currentNodeId); // Mark this node as visited
|
||||
|
||||
if (currentNode) {
|
||||
this.isRunning = true;
|
||||
|
||||
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.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');
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
nodeElement.classList.add('result-abort');
|
||||
$('.toast-body').text(error.message);
|
||||
$('.toast').toast('show');
|
||||
this.appendLog(error.message);
|
||||
});
|
||||
console.log(`Executing API request for node: ${currentNode.id} - ${currentNode.name}`);
|
||||
} else if (currentNode.class === 'start') {
|
||||
this.appendLog('Starting API scenario execution...');
|
||||
nodeElement.classList.add('result-success');
|
||||
} else {
|
||||
console.log(`Executing script for node: ${currentNode.id} - ${currentNode.name}`);
|
||||
}
|
||||
this.visitedNodes.add(this.currentNodeId); // Mark this node as visited
|
||||
}
|
||||
}
|
||||
|
||||
appendLog(text) {
|
||||
var range = new monaco.Range(this.outputEditor.getModel().getLineCount(), 1, this.outputEditor.getModel().getLineCount(), 1);
|
||||
var id = {major: 1, minor: 1};
|
||||
var op = {identifier: id, range: range, text: text + '\n', forceMoveMarkers: true};
|
||||
this.outputEditor.executeEdits("my-source", [op]);
|
||||
}
|
||||
|
||||
logHttpRequest(model) {
|
||||
let log = `${model.method} ${model.path} HTTP/1.1\n${model.requestBody}`;
|
||||
this.appendLog(log);
|
||||
}
|
||||
|
||||
logHttpResponse(response) {
|
||||
let log = `HTTP/1.1 ${response.status}\n${this.parseHeaders(response.headers)}\nBody: ${response.body}`;
|
||||
this.appendLog(log);
|
||||
}
|
||||
|
||||
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];
|
||||
@@ -59,7 +129,6 @@ class APIScenarioExecutor {
|
||||
const nextNode = currentNode.connections.find(nodeId => !this.visitedNodes.has(nodeId));
|
||||
if (nextNode) {
|
||||
this.currentNodeId = nextNode;
|
||||
console.log(`Moved to node: ${this.currentNodeId}`);
|
||||
} else {
|
||||
console.log('No more unvisited connections to move to.');
|
||||
this.currentNodeId = null; // No more nodes to visit
|
||||
@@ -67,26 +136,36 @@ class APIScenarioExecutor {
|
||||
} else {
|
||||
console.log('Current node has no connections or does not exist.');
|
||||
this.currentNodeId = null; // No more nodes to visit
|
||||
this.isDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to execute the entire scenario from start to end
|
||||
executeAll() {
|
||||
async executeAll() {
|
||||
while (this.currentNodeId) {
|
||||
console.log(this.currentNodeId)
|
||||
this.executeCurrentNode();
|
||||
this.moveToNextNode();
|
||||
await this.moveToNextNodeWithDelay(500);
|
||||
}
|
||||
console.log('Completed execution of all API nodes.');
|
||||
}
|
||||
|
||||
static extractGraphFromStart(homeData) {
|
||||
console.log(homeData)
|
||||
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 = homeData[nodeId];
|
||||
const node = graphData[nodeId];
|
||||
if (!node || graph[nodeId]) {
|
||||
// If the node doesn't exist or has already been visited, stop the recursion
|
||||
return;
|
||||
@@ -97,6 +176,7 @@ class APIScenarioExecutor {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
class: node.class,
|
||||
data: node.data,
|
||||
connections: []
|
||||
};
|
||||
|
||||
@@ -112,8 +192,8 @@ class APIScenarioExecutor {
|
||||
}
|
||||
|
||||
// Find the start node
|
||||
for (let nodeId in homeData) {
|
||||
if (homeData[nodeId].class === 'start') {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -288,14 +288,14 @@ class APITester {
|
||||
`;
|
||||
}
|
||||
|
||||
responseHeaderTemplate(header, index) {
|
||||
responseHeaderTemplate(key, value) {
|
||||
return `
|
||||
<tr data-id="${index}">
|
||||
<tr>
|
||||
<td>
|
||||
<input class="form-control" type="text" name="model.responseModel.headers[${index}].key" value="${header.key}" readonly/>
|
||||
<input class="form-control" type="text" value="${key}" readonly/>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" name="model.responseModel.headers[${index}].value" value="${header.value}" readonly/>
|
||||
<input class="form-control" type="text" value="${value}" readonly/>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -663,7 +663,6 @@ class APITester {
|
||||
}
|
||||
|
||||
renderResponse(tabIndex) {
|
||||
console.log('renderResponse: ' + tabIndex);
|
||||
let model = this.apiRequests[tabIndex];
|
||||
let target = $('#api_request_' + model.id);
|
||||
target.find('.response_headers').empty();
|
||||
@@ -677,9 +676,10 @@ class APITester {
|
||||
this.responseEditors[tabIndex].setValue(model.responseModel.body);
|
||||
}
|
||||
if (model.responseModel && model.responseModel.headers) {
|
||||
for (let i = 0; i < model.responseModel.headers.length; i++) {
|
||||
target.find('.response_headers').append(this.responseHeaderTemplate(model.responseModel.headers[i], i));
|
||||
}
|
||||
const headers = model.responseModel.headers;
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
target.find('.response_headers').append(this.responseHeaderTemplate(key, value));
|
||||
});
|
||||
}
|
||||
|
||||
target.find(".response_status").text("status: " + model.responseModel.status);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
if (document.location.search.match(/type=embed/gi)) {
|
||||
window.parent.postMessage('resize', "*");
|
||||
}
|
||||
__bindToLinks();
|
||||
|
||||
$(window).scroll(function() {
|
||||
var navbarColor = "62,195,246";//color attr for rgba
|
||||
var smallLogoHeight = $('.small-logo').height();
|
||||
var bigLogoHeight = $('.big-logo').height();
|
||||
var navbarHeight = $('.navbar').height();
|
||||
|
||||
var smallLogoEndPos = 0;
|
||||
var smallSpeed = (smallLogoHeight / bigLogoHeight);
|
||||
|
||||
var ySmall = ($(window).scrollTop() * smallSpeed);
|
||||
|
||||
var smallPadding = navbarHeight - ySmall;
|
||||
if (smallPadding > navbarHeight) { smallPadding = navbarHeight; }
|
||||
if (smallPadding < smallLogoEndPos) { smallPadding = smallLogoEndPos; }
|
||||
if (smallPadding < 0) { smallPadding = 0; }
|
||||
|
||||
$('.small-logo-container ').css({ "padding-top": smallPadding});
|
||||
|
||||
var navOpacity = ySmall / smallLogoHeight;
|
||||
if (navOpacity > 1) { navOpacity = 1; }
|
||||
if (navOpacity < 0 ) { navOpacity = 0; }
|
||||
var navBackColor = 'rgba(' + navbarColor + ',' + navOpacity + ')';
|
||||
$('.navbar').css({"background-color": navBackColor});
|
||||
|
||||
var shadowOpacity = navOpacity * 0.4;
|
||||
if ( ySmall > 1) {
|
||||
$('.navbar').css({"box-shadow": "0 2px 3px rgba(0,0,0," + shadowOpacity + ")"});
|
||||
} else {
|
||||
$('.navbar').css({"box-shadow": "none"});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.de.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.de\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.de.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.es.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.es\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.es.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.fr.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.fr\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.fr.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.it.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.it\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.it.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.ja.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.ja\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.ja.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\n/*---------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n *--------------------------------------------------------*/\ndefine(\"vs/base/common/worker/simpleWorker.nls\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\"\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAUA,OAAO,yCAA0C,CAChD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.ko.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.ko\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.ko.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.ru.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.ru\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,4CAA6C,CACnD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.ru.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.zh-cn.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.zh-cn\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,+CAAgD,CACtD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.zh-cn.js"}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["out-editor/vs/base/common/worker/simpleWorker.nls.zh-tw.js"],"sourcesContent":["/*!-----------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Version: 0.38.0(0e330ae453813de4e6cf272460fb79c7117073d0)\n * Released under the MIT license\n * https://github.com/microsoft/vscode/blob/main/LICENSE.txt\n *-----------------------------------------------------------*/\n\ndefine(\"vs/base/common/worker/simpleWorker.nls.zh-tw\", {\n\t\"vs/base/common/platform\": [\n\t\t\"_\",\n\t]\n});"],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+DAOA,OAAO,+CAAgD,CACtD,0BAA2B,CAC1B,GACD,CACD,CAAC","names":[],"file":"simpleWorker.nls.zh-tw.js"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -18,6 +18,14 @@
|
||||
|
||||
<!-- csrf -->
|
||||
<input id="csrf" 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; right: 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="tester-toolbar">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="toggle_tester">시나리오 보기</button>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button type="button" class="btn btn-primary save_scenario">저장</button>
|
||||
<button type="button" class="btn btn-primary create_scenario">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,6 +115,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="confirm_delete_scenario_modal" tabindex="-1" aria-labelledby="confirm_delete_scenario_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_scenario">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="confirm_delete_api_modal" tabindex="-1" aria-labelledby="confirm_delete_api_modal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -139,32 +156,26 @@
|
||||
<script th:inline="javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
var showingScenario = true;
|
||||
var showingScenario = false;
|
||||
|
||||
$('#toggle_tester').on('click', function () {
|
||||
console.log(showingScenario);
|
||||
showingScenario = !showingScenario;
|
||||
if (showingScenario) {
|
||||
// Move .api-scenario off-screen to the left
|
||||
// $('.api-scenario').css('transform', 'translateX(0)');
|
||||
$('.api-scenario').show();
|
||||
$('.api-tester').hide();
|
||||
$('#toggle_tester').text('테스터 보기');
|
||||
} else {
|
||||
// Bring .api-scenario into view
|
||||
// $('.api-scenario').css('transform', 'translateX(-100%)');
|
||||
$('#toggle_tester').text('시나리오 보기');
|
||||
$('.api-scenario').hide();
|
||||
$('.api-tester').show();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (showingScenario){
|
||||
if (showingScenario) {
|
||||
$('.api-scenario').show();
|
||||
$('.api-tester').hide();
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
$('.api-scenario').hide();
|
||||
$('.api-tester').show();
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<div class="api-scenario-content">
|
||||
<div class="api-scenario-editor">
|
||||
<div class="api-scenario-editor-toolbar" style="height: 50px; border: 1px solid grey; padding: 5px;">
|
||||
<button type="button" class="btn btn-primary btn-sm save_scenario me-2">저장</button>
|
||||
<button type="button" class="btn btn-primary btn-sm run_scenario_step">단계 별 실행</button>
|
||||
<button type="button" class="btn btn-primary btn-sm run_scenario_all">전체 실행</button>
|
||||
<button type="button" class="btn btn-primary btn-sm pause_scenario">일시 정지</button>
|
||||
<button type="button" class="btn btn-primary btn-sm stop_scenario">정지</button>
|
||||
<button type="button" class="btn btn-primary btn-sm save">저장</button>
|
||||
<button type="button" class="btn btn-primary btn-sm export">내보내기</button>
|
||||
<button type="button" class="btn btn-primary btn-sm clear">초기화</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 clear me-2">초기화</button>-->
|
||||
<button type="button" class="btn btn-primary btn-sm add_start">시작 노드</button>
|
||||
</div>
|
||||
<div>시나리오 설명이 여기에 표시</div>
|
||||
|
||||
@@ -8,14 +8,6 @@
|
||||
require.config({paths: {'vs': '/plugins/vs'}});
|
||||
</script>
|
||||
<div class="container-fluid mt-2">
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; bottom: 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>
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<div class="card">
|
||||
<div class="card-header" style="min-height: 50px;">
|
||||
|
||||
Reference in New Issue
Block a user