mirror of
https://gitee.com/durcframework/SOP.git
synced 2025-08-11 21:57:56 +08:00
服务列表查询、上下线
This commit is contained in:
@@ -644,6 +644,11 @@ table th, table td {
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layui-table-cell .layui-btn {
|
||||||
|
height: 27px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.x-admin-sm .layui-btn-lg{
|
.x-admin-sm .layui-btn-lg{
|
||||||
height: 38px;
|
height: 38px;
|
||||||
line-height: 38px;
|
line-height: 38px;
|
||||||
|
@@ -55,11 +55,11 @@ var ApiUtil = (function () {
|
|||||||
, getUrl: function () {
|
, getUrl: function () {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
, createUrl: function (uri) {
|
, createUrl: function (uri, params) {
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
throw new Error('name不能为空');
|
throw new Error('uri不能为空');
|
||||||
}
|
}
|
||||||
return url + formatUri(uri);
|
return url + formatUri(uri) + (params ? '?data=' + encodeURIComponent(JSON.stringify(params)) : '');
|
||||||
}
|
}
|
||||||
, getParam: function (paramName) {
|
, getParam: function (paramName) {
|
||||||
return params[paramName];
|
return params[paramName];
|
||||||
|
@@ -43,7 +43,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="layui-hide" id="routeTable" lay-filter="routeTable"></table>
|
<table class="layui-hide" id="routeTable" lay-filter="routeTableFilter"></table>
|
||||||
<script type="text/html" id="toolbar">
|
<script type="text/html" id="toolbar">
|
||||||
<div class="layui-btn-container">
|
<div class="layui-btn-container">
|
||||||
<button class="layui-btn layui-btn-ms layui-btn-normal" lay-event="add">
|
<button class="layui-btn layui-btn-ms layui-btn-normal" lay-event="add">
|
||||||
@@ -51,9 +51,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
<script type="text/html" id="optBar" >
|
|
||||||
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="edit">修改</a>
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -128,12 +128,16 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
|
|||||||
return ROUTE_STATUS[row.status + ''];
|
return ROUTE_STATUS[row.status + ''];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
, {fixed: 'right', title: '操作', toolbar: '#optBar', width: 100}
|
, {
|
||||||
|
fixed: 'right', title: '操作', width: 100, templet: function (row) {
|
||||||
|
return '<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="edit">修改</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
]]
|
]]
|
||||||
});
|
});
|
||||||
|
|
||||||
//监听单元格事件
|
//监听单元格事件
|
||||||
table.on('tool(routeTable)', function(obj) {
|
table.on('tool(routeTableFilter)', function(obj) {
|
||||||
var data = obj.data;
|
var data = obj.data;
|
||||||
if(obj.event === 'edit'){
|
if(obj.event === 'edit'){
|
||||||
//表单初始赋值
|
//表单初始赋值
|
||||||
@@ -142,8 +146,6 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
|
|||||||
|
|
||||||
updateForm.setData(data);
|
updateForm.setData(data);
|
||||||
|
|
||||||
// form.val('updateWinFilter', data);
|
|
||||||
|
|
||||||
layer.open({
|
layer.open({
|
||||||
type: 1
|
type: 1
|
||||||
,title: '修改路由' + smTitle
|
,title: '修改路由' + smTitle
|
||||||
|
@@ -32,11 +32,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="layui-hide" id="treeTable"></table>
|
<table class="layui-hide" id="treeTable" lay-filter="treeTableFilter"></table>
|
||||||
<script type="text/html" id="optCol">
|
|
||||||
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="edit">修改</a>
|
|
||||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">下线</a>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="../../assets/js/lib.js"></script>
|
<script type="text/javascript" src="../../assets/js/lib.js"></script>
|
||||||
|
@@ -3,23 +3,23 @@ lib.config({
|
|||||||
}).extend({
|
}).extend({
|
||||||
treetable: 'treetable-lay/treetable'
|
treetable: 'treetable-lay/treetable'
|
||||||
}).use(['element', 'table', 'form', 'treetable'], function () {
|
}).use(['element', 'table', 'form', 'treetable'], function () {
|
||||||
|
var table = layui.table;
|
||||||
var layer = layui.layer;
|
var layer = layui.layer;
|
||||||
var form = layui.form;
|
var form = layui.form;
|
||||||
var treetable = layui.treetable;
|
var treetable = layui.treetable;
|
||||||
var serverTable;
|
|
||||||
|
|
||||||
// 渲染表格
|
// 渲染表格
|
||||||
var renderTable = function () {
|
var renderTable = function (params) {
|
||||||
layer.load(2);
|
layer.load(2);
|
||||||
serverTable = treetable.render({
|
treetable.render({
|
||||||
|
elem: '#treeTable',
|
||||||
treeColIndex: 1,
|
treeColIndex: 1,
|
||||||
treeSpid: 0,
|
treeSpid: 0,
|
||||||
treeIdName: 'id',
|
treeIdName: 'id',
|
||||||
treePidName: 'parentId',
|
treePidName: 'parentId',
|
||||||
treeDefaultClose: true,
|
treeDefaultClose: false,
|
||||||
treeLinkage: false,
|
treeLinkage: false,
|
||||||
elem: '#treeTable',
|
url: ApiUtil.createUrl('service.instance.list', params),
|
||||||
url: ApiUtil.createUrl('service.instance.list'),
|
|
||||||
page: false,
|
page: false,
|
||||||
cols: [[
|
cols: [[
|
||||||
{type: 'numbers'},
|
{type: 'numbers'},
|
||||||
@@ -29,24 +29,61 @@ lib.config({
|
|||||||
{field: 'serverPort', title: '端口号', width: 100},
|
{field: 'serverPort', title: '端口号', width: 100},
|
||||||
{field: 'status', title: 'status', width: 100},
|
{field: 'status', title: 'status', width: 100},
|
||||||
{field: 'updateTime', title: '最后更新时间', width: 150},
|
{field: 'updateTime', title: '最后更新时间', width: 150},
|
||||||
{fixed: 'right', templet: '#optCol', title: '操作', width: 200}
|
{fixed: 'right', title: '操作', width: 200, templet: function (row) {
|
||||||
|
if (row.parentId > 0) {
|
||||||
|
var html = [];
|
||||||
|
if (row.status === 'UP') {
|
||||||
|
html.push('<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="offline">下线</a>');
|
||||||
|
}
|
||||||
|
if (row.status === 'OUT_OF_SERVICE') {
|
||||||
|
html.push('<a class="layui-btn layui-btn-xs" lay-event="online">上线</a>');
|
||||||
|
}
|
||||||
|
return html.join('');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}}
|
||||||
]],
|
]],
|
||||||
done: function () {
|
done: function () {
|
||||||
layer.closeAll('loading');
|
layer.closeAll('loading');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//监听单元格事件
|
||||||
|
table.on('tool(treeTableFilter)', function(obj) {
|
||||||
|
if (obj.event === 'offline') {
|
||||||
|
var data = obj.data;
|
||||||
|
layer.confirm('确定要下线【'+data.name+'】吗?', {icon: 3, title:'提示'}, function(index){
|
||||||
|
var params = {
|
||||||
|
serviceId: data.name
|
||||||
|
, instanceId: data.instanceId
|
||||||
|
};
|
||||||
|
ApiUtil.post('service.instance.offline', params, function (resp) {
|
||||||
|
layer.alert('修改成功');
|
||||||
|
});
|
||||||
|
layer.close(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (obj.event === 'online') {
|
||||||
|
var data = obj.data;
|
||||||
|
layer.confirm('确定要上线【'+data.name+'】吗?', {icon: 3, title:'提示'}, function(index){
|
||||||
|
var params = {
|
||||||
|
serviceId: data.name
|
||||||
|
, instanceId: data.instanceId
|
||||||
|
};
|
||||||
|
ApiUtil.post('service.instance.online', params, function (resp) {
|
||||||
|
layer.alert('修改成功');
|
||||||
|
});
|
||||||
|
layer.close(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderTable();
|
renderTable();
|
||||||
|
|
||||||
|
|
||||||
form.on('submit(searchFilter)', function(data){
|
form.on('submit(searchFilter)', function(data){
|
||||||
var param = data.field;
|
var param = data.field;
|
||||||
serverTable.reload({
|
renderTable(param)
|
||||||
where: {
|
|
||||||
data: JSON.stringify(param)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import com.gitee.easyopen.annotation.ApiService;
|
|||||||
import com.gitee.easyopen.doc.annotation.ApiDoc;
|
import com.gitee.easyopen.doc.annotation.ApiDoc;
|
||||||
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
|
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
|
||||||
import com.gitee.easyopen.exception.ApiException;
|
import com.gitee.easyopen.exception.ApiException;
|
||||||
|
import com.gitee.sop.adminserver.api.service.param.ServiceInstanceParam;
|
||||||
import com.gitee.sop.adminserver.api.service.param.ServiceSearchParam;
|
import com.gitee.sop.adminserver.api.service.param.ServiceSearchParam;
|
||||||
import com.gitee.sop.adminserver.api.service.result.ServiceInfo;
|
import com.gitee.sop.adminserver.api.service.result.ServiceInfo;
|
||||||
import com.gitee.sop.adminserver.api.service.result.ServiceInfoVo;
|
import com.gitee.sop.adminserver.api.service.result.ServiceInfoVo;
|
||||||
@@ -50,12 +51,8 @@ public class ServiceApi {
|
|||||||
// eureka.client.serviceUrl.defaultZone
|
// eureka.client.serviceUrl.defaultZone
|
||||||
|
|
||||||
@Api(name = "service.list")
|
@Api(name = "service.list")
|
||||||
@ApiDocMethod(description = "服务列表", elementClass = ServiceInfo.class)
|
@ApiDocMethod(description = "服务列表(旧)", elementClass = ServiceInfo.class)
|
||||||
List<ServiceInfo> listServiceInfo(ServiceSearchParam param) throws Exception {
|
List<ServiceInfo> listServiceInfo(ServiceSearchParam param) throws Exception {
|
||||||
String json = this.requestEurekaServer(EurekaUri.QUERY_APPS);
|
|
||||||
EurekaApps eurekaApps = JSON.parseObject(json, EurekaApps.class);
|
|
||||||
|
|
||||||
|
|
||||||
String routeRootPath = ZookeeperContext.getSopRouteRootPath(param.getProfile());
|
String routeRootPath = ZookeeperContext.getSopRouteRootPath(param.getProfile());
|
||||||
List<ChildData> childDataList = ZookeeperContext.getChildrenData(routeRootPath);
|
List<ChildData> childDataList = ZookeeperContext.getChildrenData(routeRootPath);
|
||||||
List<ServiceInfo> serviceInfoList = childDataList.stream()
|
List<ServiceInfo> serviceInfoList = childDataList.stream()
|
||||||
@@ -78,7 +75,7 @@ public class ServiceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Api(name = "service.instance.list")
|
@Api(name = "service.instance.list")
|
||||||
@ApiDocMethod(description = "服务列表", elementClass = ServiceInfo.class)
|
@ApiDocMethod(description = "服务列表", elementClass = ServiceInfoVo.class)
|
||||||
List<ServiceInfoVo> listService(ServiceSearchParam param) throws Exception {
|
List<ServiceInfoVo> listService(ServiceSearchParam param) throws Exception {
|
||||||
String json = this.requestEurekaServer(EurekaUri.QUERY_APPS);
|
String json = this.requestEurekaServer(EurekaUri.QUERY_APPS);
|
||||||
EurekaApps eurekaApps = JSON.parseObject(json, EurekaApps.class);
|
EurekaApps eurekaApps = JSON.parseObject(json, EurekaApps.class);
|
||||||
@@ -87,6 +84,12 @@ public class ServiceApi {
|
|||||||
List<EurekaApplication> applicationList = eurekaApps.getApplications().getApplication();
|
List<EurekaApplication> applicationList = eurekaApps.getApplications().getApplication();
|
||||||
AtomicInteger idGen = new AtomicInteger(1);
|
AtomicInteger idGen = new AtomicInteger(1);
|
||||||
applicationList.stream()
|
applicationList.stream()
|
||||||
|
.filter(eurekaApplication -> {
|
||||||
|
if (StringUtils.isBlank(param.getServiceId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return StringUtils.containsIgnoreCase(eurekaApplication.getName(), param.getServiceId());
|
||||||
|
})
|
||||||
.forEach(eurekaApplication -> {
|
.forEach(eurekaApplication -> {
|
||||||
int pid = idGen.getAndIncrement();
|
int pid = idGen.getAndIncrement();
|
||||||
String name = eurekaApplication.getName();
|
String name = eurekaApplication.getName();
|
||||||
@@ -107,29 +110,33 @@ public class ServiceApi {
|
|||||||
serviceInfoVoList.add(vo);
|
serviceInfoVoList.add(vo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceInfoVoList;
|
return serviceInfoVoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String requestEurekaServer(EurekaUri uri) throws IOException {
|
@Api(name = "service.instance.offline")
|
||||||
Request request = this.buildRequest(EurekaUri.QUERY_APPS);
|
@ApiDocMethod(description = "服务下线")
|
||||||
|
void serviceOffline(ServiceInstanceParam param) throws IOException {
|
||||||
|
this.requestEurekaServer(EurekaUri.OFFLINE_SERVICE, param.getServiceId(), param.getInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Api(name = "service.instance.online")
|
||||||
|
@ApiDocMethod(description = "服务上线")
|
||||||
|
void serviceOnline(ServiceInstanceParam param) throws IOException {
|
||||||
|
this.requestEurekaServer(EurekaUri.ONLINE_SERVICE, param.getServiceId(), param.getInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requestEurekaServer(EurekaUri eurekaUri, String... args) throws IOException {
|
||||||
|
Request request = eurekaUri.getRequest(this.eurekaUrl, args);
|
||||||
Response response = client.newCall(request).execute();
|
Response response = client.newCall(request).execute();
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
return response.body().string();
|
return response.body().string();
|
||||||
} else {
|
} else {
|
||||||
return "{}";
|
log.error("操作失败,url:{}, msg:{}, code:{}", eurekaUri.getUri(args), response.message(), response.code());
|
||||||
|
throw new ApiException("操作失败", String.valueOf(response.code()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Request buildRequest(EurekaUri uri) {
|
|
||||||
String apiUrl = this.eurekaUrl + uri.getUri();
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(apiUrl)
|
|
||||||
.addHeader("Content-Type", "application/json")
|
|
||||||
.addHeader("Accept", "application/json")
|
|
||||||
.build();
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
protected void after() {
|
protected void after() {
|
||||||
String eurekaUrls = environment.getProperty("eureka.client.serviceUrl.defaultZone");
|
String eurekaUrls = environment.getProperty("eureka.client.serviceUrl.defaultZone");
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.gitee.sop.adminserver.api.service.param;
|
package com.gitee.sop.adminserver.api.service.param;
|
||||||
|
|
||||||
|
import com.gitee.easyopen.doc.annotation.ApiDocField;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -9,5 +10,6 @@ import lombok.Setter;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class RouteSearchParam extends ServiceSearchParam {
|
public class RouteSearchParam extends ServiceSearchParam {
|
||||||
|
@ApiDocField(description = "id")
|
||||||
private String id;
|
private String id;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
package com.gitee.sop.adminserver.api.service.param;
|
||||||
|
|
||||||
|
import com.gitee.easyopen.doc.annotation.ApiDocField;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ServiceInstanceParam {
|
||||||
|
@ApiDocField(description = "serviceId")
|
||||||
|
@NotBlank(message = "serviceId不能为空")
|
||||||
|
private String serviceId;
|
||||||
|
|
||||||
|
@ApiDocField(description = "instanceId")
|
||||||
|
@NotBlank(message = "instanceId不能为空")
|
||||||
|
private String instanceId;
|
||||||
|
}
|
@@ -15,7 +15,7 @@ public class ServiceInfoVo {
|
|||||||
@ApiDocField(description = "id")
|
@ApiDocField(description = "id")
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
@ApiDocField(description = "服务名称")
|
@ApiDocField(description = "服务名称(serviceId)")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@ApiDocField(description = "instanceId")
|
@ApiDocField(description = "instanceId")
|
||||||
@@ -42,6 +42,10 @@ public class ServiceInfoVo {
|
|||||||
@ApiDocField(description = "parentId")
|
@ApiDocField(description = "parentId")
|
||||||
private Integer parentId;
|
private Integer parentId;
|
||||||
|
|
||||||
|
public String getServiceId() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||||
public Date getUpdateTime() {
|
public Date getUpdateTime() {
|
||||||
if (StringUtils.isBlank(lastUpdatedTimestamp)) {
|
if (StringUtils.isBlank(lastUpdatedTimestamp)) {
|
||||||
|
@@ -1,20 +1,70 @@
|
|||||||
package com.gitee.sop.adminserver.bean;
|
package com.gitee.sop.adminserver.bean;
|
||||||
|
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.internal.http.HttpMethod;
|
||||||
|
import org.apache.commons.lang.ArrayUtils;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
|
* https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
|
||||||
|
*
|
||||||
* @author tanghc
|
* @author tanghc
|
||||||
*/
|
*/
|
||||||
public enum EurekaUri {
|
public enum EurekaUri {
|
||||||
|
|
||||||
/** Query for all instances */
|
/**
|
||||||
QUERY_APPS("/apps"),
|
* 查询所有实例 Query for all instances
|
||||||
|
*/
|
||||||
|
QUERY_APPS(RequestMethod.GET, "/apps"),
|
||||||
|
/**
|
||||||
|
* 下线 Take instance out of service
|
||||||
|
*/
|
||||||
|
OFFLINE_SERVICE(RequestMethod.PUT, "/apps/%s/%s/status?value=OUT_OF_SERVICE"),
|
||||||
|
/**
|
||||||
|
* 上线 Move instance back into service (remove override)
|
||||||
|
*/
|
||||||
|
ONLINE_SERVICE(RequestMethod.DELETE, "/apps/%s/%s/status?value=UP"),
|
||||||
;
|
;
|
||||||
String uri;
|
public static final String URL_PREFIX = "/";
|
||||||
|
|
||||||
EurekaUri(String uri) {
|
String uri;
|
||||||
|
RequestMethod requestMethod;
|
||||||
|
|
||||||
|
EurekaUri(RequestMethod httpMethod, String uri) {
|
||||||
|
if (!uri.startsWith(URL_PREFIX)) {
|
||||||
|
uri = "/" + uri;
|
||||||
|
}
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
|
this.requestMethod = httpMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUri() {
|
public String getUri(String... args) {
|
||||||
|
if (ArrayUtils.isEmpty(args)) {
|
||||||
return uri;
|
return uri;
|
||||||
}}
|
}
|
||||||
|
Object[] param = ArrayUtils.clone(args);
|
||||||
|
return String.format(uri, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request getRequest(String url, String... args) {
|
||||||
|
String requestUrl = url + getUri(args);
|
||||||
|
Request request = this.getBuilder()
|
||||||
|
.url(requestUrl)
|
||||||
|
.addHeader("Content-Type", "application/json")
|
||||||
|
.addHeader("Accept", "application/json")
|
||||||
|
.build();
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request.Builder getBuilder() {
|
||||||
|
String method = requestMethod.name();
|
||||||
|
RequestBody requestBody = null;
|
||||||
|
if (HttpMethod.requiresRequestBody(method)) {
|
||||||
|
MediaType contentType = MediaType.parse(org.springframework.http.MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
requestBody = RequestBody.create(contentType, "{}");
|
||||||
|
}
|
||||||
|
return new Request.Builder().method(requestMethod.name(), requestBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user