APITester 기본 기능 완료
This commit is contained in:
+33
-5
@@ -1,28 +1,56 @@
|
|||||||
package com.eactive.httpmockserver.client.controller;
|
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.entity.ApiCollection;
|
||||||
import com.eactive.httpmockserver.client.entity.Server;
|
import com.eactive.httpmockserver.client.entity.Server;
|
||||||
import com.eactive.httpmockserver.client.repository.ServerRepository;
|
import com.eactive.httpmockserver.client.repository.ServerRepository;
|
||||||
|
import com.eactive.httpmockserver.client.service.ApiTesterService;
|
||||||
|
import com.eactive.httpmockserver.client.service.NettyApiClient;
|
||||||
|
import com.eactive.httpmockserver.common.security.CurrentUser;
|
||||||
|
import com.eactive.httpmockserver.user.entity.StaffUser;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/mgmt")
|
|
||||||
public class ApiClientController {
|
public class ApiClientController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ServerRepository serverRepository;
|
private ServerRepository serverRepository;
|
||||||
|
|
||||||
@GetMapping("/api_tester.do")
|
@Autowired
|
||||||
public String testView(ModelMap model){
|
private ApiTesterService apiTesterService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NettyApiClient nettyApiClient;
|
||||||
|
|
||||||
|
@PostMapping("/api/test.do")
|
||||||
|
@ResponseBody
|
||||||
|
public ApiResponse handleApiRequest(@RequestBody ApiRequestDTO request) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
return nettyApiClient.handleRequest(request).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mgmt/api_tester.do")
|
||||||
|
public String testView(ModelMap model,
|
||||||
|
@CurrentUser StaffUser user) {
|
||||||
|
|
||||||
List<Server> servers = serverRepository.findAll();
|
List<Server> servers = serverRepository.findAll();
|
||||||
|
List<ApiCollection> collections = apiTesterService.findMyCollections(user.getEsntlId());
|
||||||
|
|
||||||
model.addAttribute("servers", servers);
|
model.addAttribute("servers", servers);
|
||||||
|
model.addAttribute("collections", collections);
|
||||||
|
|
||||||
return "page/apiTester";
|
return "page/apiTester";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.eactive.httpmockserver.client.dto;
|
||||||
|
|
||||||
|
public class ApiHeaderDTO {
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.eactive.httpmockserver.client.dto;
|
|
||||||
|
|
||||||
public class ApiRequest {
|
|
||||||
|
|
||||||
private String uri;
|
|
||||||
private String method;
|
|
||||||
|
|
||||||
private String body;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.eactive.httpmockserver.client.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ApiRequestDTO {
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
private Long server;
|
||||||
|
|
||||||
|
private List<ApiHeaderDTO> headers;
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServer(Long server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMethod(String method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ApiHeaderDTO> getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeaders(List<ApiHeaderDTO> headers) {
|
||||||
|
this.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,46 @@
|
|||||||
package com.eactive.httpmockserver.client.dto;
|
package com.eactive.httpmockserver.client.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ApiResponse {
|
public class ApiResponse {
|
||||||
|
|
||||||
|
private int status;
|
||||||
|
|
||||||
|
private List<ApiHeaderDTO> headers;
|
||||||
|
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public int getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(int status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ApiHeaderDTO> getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeaders(List<ApiHeaderDTO> headers) {
|
||||||
|
this.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(int size) {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.eactive.httpmockserver.client.service;
|
||||||
|
|
||||||
|
import com.eactive.httpmockserver.client.dto.ApiHeaderDTO;
|
||||||
|
import com.eactive.httpmockserver.client.dto.ApiResponse;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
|
import io.netty.util.CharsetUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class HttptHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
|
||||||
|
|
||||||
|
private final CompletableFuture<ApiResponse> responseFuture;
|
||||||
|
|
||||||
|
public HttptHandler(CompletableFuture<ApiResponse> responseFuture) {
|
||||||
|
this.responseFuture = responseFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) {
|
||||||
|
|
||||||
|
ApiResponse apiResponse = new ApiResponse();
|
||||||
|
apiResponse.setStatus(response.status().code());
|
||||||
|
apiResponse.setHeaders(convertHttpHeadersToList(response.headers()));
|
||||||
|
apiResponse.setBody(response.content().toString(CharsetUtil.UTF_8));
|
||||||
|
|
||||||
|
int headerSize = calculateHeaderSize(response.headers());
|
||||||
|
int contentSize = response.content().readableBytes();
|
||||||
|
int totalSize = headerSize + contentSize;
|
||||||
|
apiResponse.setSize(totalSize);
|
||||||
|
|
||||||
|
responseFuture.complete(apiResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateHeaderSize(HttpHeaders headers) {
|
||||||
|
int headerSize = 0;
|
||||||
|
for (Map.Entry<String, String> entry : headers.entries()) {
|
||||||
|
headerSize += entry.getKey().length() + entry.getValue().length() + 4; // Add 2 for ': ' and 2 for '\r\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 2 for the final '\r\n' after the headers
|
||||||
|
headerSize += 2;
|
||||||
|
|
||||||
|
return headerSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ApiHeaderDTO> convertHttpHeadersToList(HttpHeaders headers) {
|
||||||
|
List<ApiHeaderDTO> headerList = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, String> entry : headers.entries()) {
|
||||||
|
ApiHeaderDTO header = new ApiHeaderDTO();
|
||||||
|
header.setKey(entry.getKey());
|
||||||
|
header.setValue(entry.getValue());
|
||||||
|
headerList.add(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||||
|
responseFuture.completeExceptionally(cause);
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.eactive.httpmockserver.client.service;
|
||||||
|
|
||||||
|
import com.eactive.httpmockserver.client.dto.ApiRequestDTO;
|
||||||
|
import com.eactive.httpmockserver.client.dto.ApiResponse;
|
||||||
|
import com.eactive.httpmockserver.client.entity.Server;
|
||||||
|
import com.eactive.httpmockserver.client.repository.ServerRepository;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.handler.codec.http.*;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NettyApiClient {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ServerRepository serverRepository;
|
||||||
|
|
||||||
|
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO apiRequest) throws Exception {
|
||||||
|
|
||||||
|
Server server = serverRepository.findById(apiRequest.getServer()).orElseThrow(() -> new Exception("Server not found"));
|
||||||
|
String host = server.getHostname();
|
||||||
|
int port = server.getPort();
|
||||||
|
|
||||||
|
CompletableFuture<ApiResponse> responseFuture = new CompletableFuture<>();
|
||||||
|
EventLoopGroup group = new NioEventLoopGroup();
|
||||||
|
try {
|
||||||
|
Bootstrap b = new Bootstrap()
|
||||||
|
.group(group)
|
||||||
|
.channel(NioSocketChannel.class)
|
||||||
|
.option(ChannelOption.TCP_NODELAY, true)
|
||||||
|
.handler(new ChannelInitializer<Channel>() {
|
||||||
|
@Override
|
||||||
|
public void initChannel(Channel ch) {
|
||||||
|
ch.pipeline().addLast(new HttpClientCodec());
|
||||||
|
ch.pipeline().addLast(new HttpObjectAggregator(65536));
|
||||||
|
ch.pipeline().addLast(new HttpContentDecompressor());
|
||||||
|
ch.pipeline().addLast(new HttptHandler(responseFuture));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Channel ch = b.connect(host, port).sync().channel();
|
||||||
|
|
||||||
|
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(apiRequest.getMethod()),
|
||||||
|
apiRequest.getPath(), Unpooled.EMPTY_BUFFER);
|
||||||
|
|
||||||
|
request.headers().set("Host", host);
|
||||||
|
request.headers().set("Connection", "close");
|
||||||
|
request.headers().set("Accept-Encoding", "gzip");
|
||||||
|
|
||||||
|
ch.writeAndFlush(request);
|
||||||
|
ch.closeFuture().sync();
|
||||||
|
} finally {
|
||||||
|
group.shutdownGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseFuture;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1507,9 +1507,9 @@ figure div span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.CodeMirror {
|
/*.CodeMirror {*/
|
||||||
margin-bottom: 0.1rem !important;
|
/* margin-bottom: 0.1rem !important;*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
.CodeMirror-fullscreen {
|
.CodeMirror-fullscreen {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="d-flex" id="navbarNav">
|
<div class="d-flex" id="navbarNav">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item" sec:authorize="isAuthenticated()">
|
||||||
<a class="nav-link" href="/mgmt/routes/list_view.do">API 관리</a>
|
<a class="nav-link" href="/mgmt/routes/list_view.do">API 관리</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" sec:authorize="isAuthenticated()">
|
||||||
<a class="nav-link" href="/mgmt/api_tester.do">API Tester</a>
|
<a class="nav-link" href="/mgmt/api_tester.do">API Tester</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown" sec:authorize="hasRole('ROLE_ADMIN')">
|
<li class="nav-item dropdown" sec:authorize="hasRole('ROLE_ADMIN')">
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="mgmtDropDown">
|
<ul class="dropdown-menu" aria-labelledby="mgmtDropDown">
|
||||||
<li><a class="dropdown-item" href="/mgmt/users/list_view.do">사용자 관리</a></li>
|
<li><a class="dropdown-item" href="/mgmt/users/list_view.do">사용자 관리</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/mgmt/groups/list_view.do">그룹 관리</a></li>
|
||||||
<li><a class="dropdown-item" href="/mgmt/servers/list_view.do">서버 관리</a></li>
|
<li><a class="dropdown-item" href="/mgmt/servers/list_view.do">서버 관리</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org"
|
||||||
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||||
|
|
||||||
|
|
||||||
|
<head th:replace="fragment/head :: headFragment">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- header -->
|
||||||
|
<header th:replace="fragment/header :: headerFragment"></header>
|
||||||
|
<th:block th:replace="fragment/header :: headerScript"></th:block>
|
||||||
|
<nav th:replace="fragment/header :: navFragment"></nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- csrf -->
|
||||||
|
<input id="csrf" type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<div class="pt-layout">
|
||||||
|
<aside th:replace="page/apiCollection :: collectionFragment"></aside>
|
||||||
|
<th:block th:replace="page/apiCollection :: collectionScript"></th:block>
|
||||||
|
|
||||||
|
<section class="pt-main order-1" layout:fragment="contentFragment">
|
||||||
|
</section>
|
||||||
|
<th:block layout:fragment="contentScript">
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer th:replace="fragment/footer :: footerFragment"></footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<aside class="pt-sidebar" th:fragment="collectionFragment">
|
||||||
|
<div class="input-group p-1">
|
||||||
|
<input type="text" class="form-control" placeholder="검색어를 입력하세요" aria-label="검색어를 입력하세요"
|
||||||
|
aria-describedby="button-addon2">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="button-addon2">검색</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- new collection button -->
|
||||||
|
<div class="pt-new-collection p-1">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="new_collection"><i class="fa fa-plus"></i> 새 컬렉션</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse pt-links" id="side_menubar" aria-label="sidebar menu">
|
||||||
|
<ul class="list-unstyled mb-0 py-3 pt-md-1">
|
||||||
|
<li th:each="collection, status: *{collections}">
|
||||||
|
<a href="#" class="d-inline-flex align-items-center rounded">기본</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="new_collection_modal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exampleModalLabel">새로운 컬렉션 추가</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="text" class="form-control" placeholder="이름을 입력하세요" aria-label="이름을 입력하세요" aria-describedby="button-addon2" name="collection_name"/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||||
|
<button type="button" class="btn btn-primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- menu script -->
|
||||||
|
<th:block th:fragment="collectionScript">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
$(function () {
|
||||||
|
//when new_collection button clicked show modal popup for entering name
|
||||||
|
$('#new_collection').click(function () {
|
||||||
|
$('#new_collection_modal').modal('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,123 +1,149 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
<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/default_layout}">
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/layout/api_tester_layout}">
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<section layout:fragment="contentFragment">
|
<section layout:fragment="contentFragment">
|
||||||
<div class="container mt-1">
|
<div id="loading-overlay" style="display: none;">
|
||||||
<div class="card col-md">
|
<div class="d-flex justify-content-center align-items-center" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 9999;">
|
||||||
<div class="card-header">
|
<div class="text-center">
|
||||||
<h3>API Tester</h3>
|
<span class="sr-only">Loading...</span>
|
||||||
|
<br/>
|
||||||
|
<button id="cancel-ajax" class="btn btn-danger mt-3">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="container">
|
||||||
<div class="form-floating col-md-2 me-1">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<select name="method" class="form-select" id="method">
|
<div class="d-flex align-items-center">
|
||||||
<option value="POST">POST</option>
|
<div class="form-floating col-md-2 me-1">
|
||||||
<option value="GET">GET</option>
|
<select name="method" class="form-select" id="method">
|
||||||
<option value="PUT">PUT</option>
|
<option value="POST">POST</option>
|
||||||
<option value="DELETE">DELETE</option>
|
<option value="GET">GET</option>
|
||||||
<option value="CONNECT">CONNECT</option>
|
<option value="PUT">PUT</option>
|
||||||
<option value="HEAD">HEAD</option>
|
<option value="DELETE">DELETE</option>
|
||||||
<option value="OPTIONS">OPTIONS</option>
|
<option value="CONNECT">CONNECT</option>
|
||||||
<option value="TRACE">TRACE</option>
|
<option value="HEAD">HEAD</option>
|
||||||
<option value="PATCH">PATCH</option>
|
<option value="OPTIONS">OPTIONS</option>
|
||||||
</select>
|
<option value="TRACE">TRACE</option>
|
||||||
<label for="method" class="form-label must">Method</label>
|
<option value="PATCH">PATCH</option>
|
||||||
</div>
|
</select>
|
||||||
<div class="form-floating col-md-3 me-1">
|
<label for="method" class="form-label must">Method</label>
|
||||||
<select name="server" class="form-select">
|
|
||||||
<option th:each="server : ${servers}" th:value="${server.id}"
|
|
||||||
th:text="${server.name}"></option>
|
|
||||||
</select>
|
|
||||||
<label for="method" class="form-label must">Server</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-group col-md">
|
|
||||||
<div class="form-floating col-md">
|
|
||||||
<input type="text" name="path" class="form-control" id="path" placeholder="호출할 경로 (/부터 입력)"
|
|
||||||
required>
|
|
||||||
<label for="path" class="must">Path</label>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" id="send">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-floating col-md-3 me-1">
|
||||||
<div class="card-body">
|
<select name="server" class="form-select" id="server">
|
||||||
<div class="row">
|
<option th:each="server : ${servers}" th:value="${server.id}"
|
||||||
<div class="col-md">
|
th:text="${server.name}"></option>
|
||||||
<div class="card">
|
</select>
|
||||||
<div class="card-header">
|
<label for="method" class="form-label must">Server</label>
|
||||||
<h4>요청</h4>
|
</div>
|
||||||
</div>
|
<div class="input-group col-md">
|
||||||
<div class="card-body">
|
<div class="form-floating col-md">
|
||||||
<table class="table">
|
<input type="text" name="path" class="form-control" id="path" placeholder="호출할 경로 (/부터 입력)" required>
|
||||||
<thead>
|
<label for="path" class="must">Path</label>
|
||||||
<tr>
|
|
||||||
<th scope="col">Header 이름</th>
|
|
||||||
<th scope="col">값</th>
|
|
||||||
<th>
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm btn_add_header">추가
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="headers">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table" id="requestMatcherParamTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Query 파라미터 이름</th>
|
|
||||||
<th>값</th>
|
|
||||||
<th>
|
|
||||||
<button type="button" class="btn btn-secondary btn-sm btn_add_query">추가
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="queryParams">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<label class="form-label must">Request Body</label>
|
|
||||||
<div class="">
|
|
||||||
<textarea class="form-control" style="height: 200px;" id="requestBody">
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4>응답</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Header 이름</th>
|
|
||||||
<th scope="col">값</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="responseHeaders">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<label class="form-label must">Response Body</label>
|
|
||||||
<div class="form-floating">
|
|
||||||
<textarea class="form-control" style="height: 200px;" id="responseBody"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-primary" id="send">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="nav nav-tabs mt-1" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#requestParamTab" type="button" role="tab" aria-controls="paramTab"
|
||||||
|
aria-selected="true">Param
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#requestHeaderTab" type="button" role="tab"
|
||||||
|
aria-controls="requestHeader"
|
||||||
|
aria-selected="false">Header
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#requestBodyTab" type="button" role="tab" aria-controls="requestBody"
|
||||||
|
aria-selected="false">Body
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content border-bottom" style="min-height: 300px;">
|
||||||
|
<div class="tab-pane fade show active" id="requestParamTab" role="tabpanel"
|
||||||
|
aria-labelledby="requestParams-tab">
|
||||||
|
<table class="table" id="requestMatcherParamTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">이름</th>
|
||||||
|
<th>값</th>
|
||||||
|
<th>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm btn_add_query">추가
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="queryParams">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="requestHeaderTab" role="tabpanel"
|
||||||
|
aria-labelledby="requestHeader-tab">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">이름</th>
|
||||||
|
<th scope="col">값</th>
|
||||||
|
<th>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm btn_add_header">추가
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="headers">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="requestBodyTab" role="tabpanel"
|
||||||
|
aria-labelledby="requestBody-tab">
|
||||||
|
<textarea class="form-control" style="height: 200px;" id="requestBody"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<span>Response</span>
|
||||||
|
<span class="response_status"></span>
|
||||||
|
<span class="response_time"></span>
|
||||||
|
<span class="response_size"></span>
|
||||||
|
</div>
|
||||||
|
<ul class="nav nav-tabs mt-1" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#responseBodyTab" type="button" role="tab" aria-controls="responseBody"
|
||||||
|
aria-selected="false">Body
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#responseHeaderTab" type="button" role="tab"
|
||||||
|
aria-controls="responseHeader"
|
||||||
|
aria-selected="false">Header
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content border-bottom" style="min-height: 250px;">
|
||||||
|
<div class="tab-pane fade show active" id="responseBodyTab" role="tabpanel"
|
||||||
|
aria-labelledby="requestParams-tab">
|
||||||
|
<textarea style="height: 200px;" id="responseBody"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="responseHeaderTab" role="tabpanel"
|
||||||
|
aria-labelledby="requestParams-tab">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">이름</th>
|
||||||
|
<th scope="col">값</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="responseHeaders">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,10 +169,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input class="form-control" type="text" name="key"/>
|
<input class="form-control" type="text" name="key" readonly/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input class="form-control" type="text" name="value"/>
|
<input class="form-control" type="text" name="value" readonly/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -166,14 +192,30 @@
|
|||||||
queryParams: [],
|
queryParams: [],
|
||||||
requestBody: ''
|
requestBody: ''
|
||||||
};
|
};
|
||||||
|
let responseModel = {
|
||||||
|
status: 0,
|
||||||
|
time: 0,
|
||||||
|
size: 0,
|
||||||
|
headers: [],
|
||||||
|
body: ''
|
||||||
|
};
|
||||||
|
|
||||||
let view = {
|
let view = {
|
||||||
|
requestBody: null,
|
||||||
|
responseBody: null,
|
||||||
init: function () {
|
init: function () {
|
||||||
$('.btn_add_header').click(function () {
|
$('.btn_add_header').click(function () {
|
||||||
controller.addHeader();
|
controller.addHeader();
|
||||||
});
|
});
|
||||||
$('.btn_add_query').click(function () {
|
$('.btn_add_query').click(function () {
|
||||||
controller.addQueryParam('','');
|
controller.addQueryParam('', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#cancel-ajax").on("click", function () {
|
||||||
|
if (controller.currentAjaxRequest) {
|
||||||
|
controller.currentAjaxRequest.abort();
|
||||||
|
controller.hideLoadingOverlay();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#requestBody').on('input', function () {
|
$('#requestBody').on('input', function () {
|
||||||
@@ -182,21 +224,22 @@
|
|||||||
model.server = $('#server').val();
|
model.server = $('#server').val();
|
||||||
model.method = $('#method').val();
|
model.method = $('#method').val();
|
||||||
|
|
||||||
$('#server').on('change', function(){
|
$('#server').on('change', function () {
|
||||||
model.server = $(this).val();
|
model.server = $(this).val();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#method').on('change', function(){
|
|
||||||
|
$('#method').on('change', function () {
|
||||||
model.method = $(this).val();
|
model.method = $(this).val();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#path').on('input', function(){
|
$('#path').on('input', function () {
|
||||||
model.path = $(this).val();
|
model.path = $(this).val();
|
||||||
model.queryParams = [];
|
model.queryParams = [];
|
||||||
let queryString = model.path.split('?')[1];
|
let queryString = model.path.split('?')[1];
|
||||||
if(queryString){
|
if (queryString) {
|
||||||
let queryParamArray = queryString.split('&');
|
let queryParamArray = queryString.split('&');
|
||||||
for(let i = 0; i < queryParamArray.length; i++){
|
for (let i = 0; i < queryParamArray.length; i++) {
|
||||||
let queryParam = queryParamArray[i].split('=');
|
let queryParam = queryParamArray[i].split('=');
|
||||||
controller.addQueryParam(queryParam[0], queryParam[1]);
|
controller.addQueryParam(queryParam[0], queryParam[1]);
|
||||||
}
|
}
|
||||||
@@ -219,6 +262,34 @@
|
|||||||
controller.send();
|
controller.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.requestBody = CodeMirror.fromTextArea(document.getElementById('requestBody'), {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: "application/json",
|
||||||
|
extraKeys: {
|
||||||
|
"Ctrl-Enter": function (cm) {
|
||||||
|
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
|
||||||
|
},
|
||||||
|
"Esc": function (cm) {
|
||||||
|
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.responseBody = CodeMirror.fromTextArea(document.getElementById('responseBody'), {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: "application/json",
|
||||||
|
readOnly: true,
|
||||||
|
noCursor: true,
|
||||||
|
extraKeys: {
|
||||||
|
"Ctrl-Enter": function (cm) {
|
||||||
|
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
|
||||||
|
},
|
||||||
|
"Esc": function (cm) {
|
||||||
|
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
render: function () {
|
render: function () {
|
||||||
@@ -249,11 +320,11 @@
|
|||||||
template.find('input[name="value"]').val(model.queryParams[i].value);
|
template.find('input[name="value"]').val(model.queryParams[i].value);
|
||||||
template.find('button').attr("data-id", i);
|
template.find('button').attr("data-id", i);
|
||||||
|
|
||||||
template.find('input[name="key"]').on('input', function () {
|
template.find('input[name="key"]').on('focusout', function () {
|
||||||
model.queryParams[i].key = $(this).val();
|
model.queryParams[i].key = $(this).val();
|
||||||
controller.buildPath();
|
controller.buildPath();
|
||||||
});
|
});
|
||||||
template.find('input[name="value"]').on('input', function () {
|
template.find('input[name="value"]').on('focusout', function () {
|
||||||
model.queryParams[i].value = $(this).val();
|
model.queryParams[i].value = $(this).val();
|
||||||
controller.buildPath();
|
controller.buildPath();
|
||||||
});
|
});
|
||||||
@@ -261,10 +332,29 @@
|
|||||||
$('#queryParams').append(template);
|
$('#queryParams').append(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
renderResponse: function(){
|
||||||
|
$('#responseHeaders').empty();
|
||||||
|
$(".response_status").empty();
|
||||||
|
|
||||||
|
view.responseBody.setValue(responseModel.body);
|
||||||
|
|
||||||
|
for (let i = 0; i < responseModel.headers.length; i++) {
|
||||||
|
let template = $('#responseHeaderTemplate').children().find('tr').clone();
|
||||||
|
template.find('input[name="key"]').val(responseModel.headers[i].key);
|
||||||
|
template.find('input[name="value"]').val(responseModel.headers[i].value);
|
||||||
|
$('#responseHeaders').append(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
//show response status
|
||||||
|
$(".response_status").text("status: " + responseModel.status);
|
||||||
|
$(".response_time").text("time: " + responseModel.time + "ms");
|
||||||
|
$(".response_size").text("size: " + responseModel.size + "bytes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = {
|
let controller = {
|
||||||
|
currentAjaxRequest: null,
|
||||||
init: function () {
|
init: function () {
|
||||||
view.init();
|
view.init();
|
||||||
},
|
},
|
||||||
@@ -273,7 +363,6 @@
|
|||||||
key: '',
|
key: '',
|
||||||
value: ''
|
value: ''
|
||||||
});
|
});
|
||||||
console.log(model.headers);
|
|
||||||
view.render();
|
view.render();
|
||||||
},
|
},
|
||||||
removeHeader: function (idx) {
|
removeHeader: function (idx) {
|
||||||
@@ -287,14 +376,14 @@
|
|||||||
});
|
});
|
||||||
view.render();
|
view.render();
|
||||||
},
|
},
|
||||||
buildPath : function(){
|
buildPath: function () {
|
||||||
let queryString = model.path.split('?')[0];
|
let queryString = model.path.split('?')[0];
|
||||||
if (model.queryParams.length>0){
|
if (model.queryParams.length > 0) {
|
||||||
queryString += '?';
|
queryString += '?';
|
||||||
}
|
}
|
||||||
for(let i = 0; i < model.queryParams.length; i++){
|
for (let i = 0; i < model.queryParams.length; i++) {
|
||||||
queryString += model.queryParams[i].key + '=' + model.queryParams[i].value;
|
queryString += model.queryParams[i].key + '=' + model.queryParams[i].value;
|
||||||
if(i < model.queryParams.length - 1){
|
if (i < model.queryParams.length - 1) {
|
||||||
queryString += '&';
|
queryString += '&';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,56 +395,47 @@
|
|||||||
this.buildPath();
|
this.buildPath();
|
||||||
view.render();
|
view.render();
|
||||||
},
|
},
|
||||||
|
showLoadingOverlay: function() {
|
||||||
|
$("#loading-overlay").show();
|
||||||
|
},
|
||||||
|
hideLoadingOverlay: function(){
|
||||||
|
$("#loading-overlay").hide();
|
||||||
|
},
|
||||||
send: function () {
|
send: function () {
|
||||||
$('#responseHeaders').empty();
|
$('#responseHeaders').empty();
|
||||||
$('#responseBody').val('');
|
view.responseBody.setValue('');
|
||||||
console.log(model);
|
let startTime = new Date().getTime();
|
||||||
// $.ajax({
|
if (model.path !== '') {
|
||||||
// url: '/api/test',
|
controller.showLoadingOverlay();
|
||||||
// type: 'POST',
|
this.currentAjaxRequest = $.ajax({
|
||||||
// contentType: 'application/json',
|
url: '/api/test.do',
|
||||||
// data: JSON.stringify(model),
|
type: 'POST',
|
||||||
// success: function (data) {
|
contentType: 'application/json',
|
||||||
// console.log(data);
|
data: JSON.stringify(model),
|
||||||
// $('#responseBody').val(JSON.stringify(data, null, 4));
|
success: function (data) {
|
||||||
// for (let key in data.headers) {
|
responseModel.status = data.status;
|
||||||
// let template = $('#responseHeaderTemplate').children().find('tr').clone();
|
responseModel.body = data.body;
|
||||||
// template.find('input[name="key"]').val(key);
|
responseModel.headers = data.headers;
|
||||||
// template.find('input[name="value"]').val(data.headers[key]);
|
responseModel.size = data.size;
|
||||||
// $('#responseHeaders').append(template);
|
},
|
||||||
// }
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
// }
|
console.error(textStatus, errorThrown);
|
||||||
// });
|
controller.hideLoadingOverlay();
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
let endTime = new Date().getTime();
|
||||||
|
responseModel.time = endTime - startTime;
|
||||||
|
view.renderResponse();
|
||||||
|
controller.hideLoadingOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.init();
|
controller.init();
|
||||||
|
|
||||||
CodeMirror.fromTextArea(document.getElementById('requestBody'), {
|
|
||||||
lineNumbers: true,
|
|
||||||
mode: "application/json",
|
|
||||||
extraKeys: {
|
|
||||||
"Ctrl-Enter": function (cm) {
|
|
||||||
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
|
|
||||||
},
|
|
||||||
"Esc": function (cm) {
|
|
||||||
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.fromTextArea(document.getElementById('responseBody'), {
|
|
||||||
lineNumbers: true,
|
|
||||||
mode: "application/json",
|
|
||||||
extraKeys: {
|
|
||||||
"Ctrl-Enter": function (cm) {
|
|
||||||
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
|
|
||||||
},
|
|
||||||
"Esc": function (cm) {
|
|
||||||
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|||||||
Reference in New Issue
Block a user