mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-12-10 16:30:24 +08:00
[区域]增加区域管理
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
package teaconst
|
package teaconst
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "0.0.5"
|
Version = "0.0.6"
|
||||||
|
|
||||||
ProductName = "Edge Admin"
|
ProductName = "Edge Admin"
|
||||||
ProcessName = "edge-admin"
|
ProcessName = "edge-admin"
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ func (this *RPCClient) NodeGroupRPC() pb.NodeGroupServiceClient {
|
|||||||
return pb.NewNodeGroupServiceClient(this.pickConn())
|
return pb.NewNodeGroupServiceClient(this.pickConn())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *RPCClient) NodeRegionRPC() pb.NodeRegionServiceClient {
|
||||||
|
return pb.NewNodeRegionServiceClient(this.pickConn())
|
||||||
|
}
|
||||||
|
|
||||||
func (this *RPCClient) NodeIPAddressRPC() pb.NodeIPAddressServiceClient {
|
func (this *RPCClient) NodeIPAddressRPC() pb.NodeIPAddressServiceClient {
|
||||||
return pb.NewNodeIPAddressServiceClient(this.pickConn())
|
return pb.NewNodeIPAddressServiceClient(this.pickConn())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (this *DeleteAction) RunPost(params struct {
|
|||||||
GroupId int64
|
GroupId int64
|
||||||
}) {
|
}) {
|
||||||
// 检查是否正在使用
|
// 检查是否正在使用
|
||||||
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithGroupIdRequest{GroupId: params.GroupId})
|
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithNodeGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithNodeGroupIdRequest{NodeGroupId: params.GroupId})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
|
|
||||||
groupMaps := []maps.Map{}
|
groupMaps := []maps.Map{}
|
||||||
for _, group := range groupsResp.Groups {
|
for _, group := range groupsResp.Groups {
|
||||||
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithGroupIdRequest{GroupId: group.Id})
|
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithNodeGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithNodeGroupIdRequest{NodeGroupId: group.Id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, group := range groupsResp.Groups {
|
for _, group := range groupsResp.Groups {
|
||||||
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithGroupIdRequest{GroupId: group.Id})
|
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithNodeGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithNodeGroupIdRequest{NodeGroupId: group.Id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
|
|||||||
41
internal/web/actions/default/clusters/regions/createPopup.go
Normal file
41
internal/web/actions/default/clusters/regions/createPopup.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package regions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreatePopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreatePopupAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreatePopupAction) RunGet(params struct{}) {
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreatePopupAction) RunPost(params struct {
|
||||||
|
Name string
|
||||||
|
|
||||||
|
Must *actions.Must
|
||||||
|
CSRF *actionutils.CSRF
|
||||||
|
}) {
|
||||||
|
params.Must.
|
||||||
|
Field("name", params.Name).
|
||||||
|
Require("请输入区域名称")
|
||||||
|
|
||||||
|
createResp, err := this.RPC().NodeRegionRPC().CreateNodeRegion(this.AdminContext(), &pb.CreateNodeRegionRequest{Name: params.Name})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志
|
||||||
|
defer this.CreateLogInfo("创建节点区域 %d", createResp.NodeRegionId)
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
35
internal/web/actions/default/clusters/regions/delete.go
Normal file
35
internal/web/actions/default/clusters/regions/delete.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package regions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *DeleteAction) RunPost(params struct {
|
||||||
|
RegionId int64
|
||||||
|
}) {
|
||||||
|
defer this.CreateLogInfo("删除节点区域 %d", params.RegionId)
|
||||||
|
|
||||||
|
// 检查有无在使用
|
||||||
|
countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithNodeRegionId(this.AdminContext(), &pb.CountAllEnabledNodesWithNodeRegionIdRequest{NodeRegionId: params.RegionId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if countResp.Count > 0 {
|
||||||
|
this.Fail("此区域正在使用,不能删除")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行删除
|
||||||
|
_, err = this.RPC().NodeRegionRPC().DeleteNodeRegion(this.AdminContext(), &pb.DeleteNodeRegionRequest{NodeRegionId: params.RegionId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
41
internal/web/actions/default/clusters/regions/index.go
Normal file
41
internal/web/actions/default/clusters/regions/index.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package regions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *IndexAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *IndexAction) RunGet(params struct{}) {
|
||||||
|
regionsResp, err := this.RPC().NodeRegionRPC().FindAllEnabledNodeRegions(this.AdminContext(), &pb.FindAllEnabledNodeRegionsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
regionMaps := []maps.Map{}
|
||||||
|
for _, region := range regionsResp.NodeRegions {
|
||||||
|
countNodesResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithNodeRegionId(this.AdminContext(), &pb.CountAllEnabledNodesWithNodeRegionIdRequest{NodeRegionId: region.Id})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
regionMaps = append(regionMaps, maps.Map{
|
||||||
|
"id": region.Id,
|
||||||
|
"isOn": region.IsOn,
|
||||||
|
"name": region.Name,
|
||||||
|
"countNodes": countNodesResp.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.Data["regions"] = regionMaps
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
23
internal/web/actions/default/clusters/regions/init.go
Normal file
23
internal/web/actions/default/clusters/regions/init.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package regions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||||
|
"github.com/iwind/TeaGo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||||
|
server.
|
||||||
|
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNode)).
|
||||||
|
Data("teaMenu", "clusters").
|
||||||
|
Data("teaSubMenu", "region").
|
||||||
|
Prefix("/clusters/regions").
|
||||||
|
Get("", new(IndexAction)).
|
||||||
|
GetPost("/createPopup", new(CreatePopupAction)).
|
||||||
|
GetPost("/updatePopup", new(UpdatePopupAction)).
|
||||||
|
Post("/delete", new(DeleteAction)).
|
||||||
|
Post("/sort", new(SortAction)).
|
||||||
|
EndAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
24
internal/web/actions/default/clusters/regions/sort.go
Normal file
24
internal/web/actions/default/clusters/regions/sort.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package regions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *SortAction) RunPost(params struct {
|
||||||
|
RegionIds []int64
|
||||||
|
}) {
|
||||||
|
defer this.CreateLogInfo("修改节点区域排序")
|
||||||
|
|
||||||
|
_, err := this.RPC().NodeRegionRPC().UpdateNodeRegionOrders(this.AdminContext(), &pb.UpdateNodeRegionOrdersRequest{NodeRegionIds: params.RegionIds})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
67
internal/web/actions/default/clusters/regions/updatePopup.go
Normal file
67
internal/web/actions/default/clusters/regions/updatePopup.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package regions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdatePopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdatePopupAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdatePopupAction) RunGet(params struct {
|
||||||
|
RegionId int64
|
||||||
|
}) {
|
||||||
|
regionResp, err := this.RPC().NodeRegionRPC().FindEnabledNodeRegion(this.AdminContext(), &pb.FindEnabledNodeRegionRequest{NodeRegionId: params.RegionId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
region := regionResp.NodeRegion
|
||||||
|
if region == nil {
|
||||||
|
this.NotFound("nodeRegion", params.RegionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["region"] = maps.Map{
|
||||||
|
"id": region.Id,
|
||||||
|
"isOn": region.IsOn,
|
||||||
|
"name": region.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdatePopupAction) RunPost(params struct {
|
||||||
|
RegionId int64
|
||||||
|
|
||||||
|
Name string
|
||||||
|
IsOn bool
|
||||||
|
|
||||||
|
Must *actions.Must
|
||||||
|
CSRF *actionutils.CSRF
|
||||||
|
}) {
|
||||||
|
defer this.CreateLogInfo("修改节点区域 %d", params.RegionId)
|
||||||
|
|
||||||
|
params.Must.
|
||||||
|
Field("name", params.Name).
|
||||||
|
Require("请输入区域名称")
|
||||||
|
|
||||||
|
_, err := this.RPC().NodeRegionRPC().UpdateNodeRegion(this.AdminContext(), &pb.UpdateNodeRegionRequest{
|
||||||
|
NodeRegionId: params.RegionId,
|
||||||
|
Name: params.Name,
|
||||||
|
IsOn: params.IsOn,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -172,6 +172,11 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map {
|
|||||||
"url": "/clusters/grants",
|
"url": "/clusters/grants",
|
||||||
"code": "grant",
|
"code": "grant",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "区域设置",
|
||||||
|
"url": "/clusters/regions",
|
||||||
|
"code": "region",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants"
|
||||||
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/regions"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/csrf"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/csrf"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db"
|
||||||
|
|||||||
15
web/views/@default/clusters/regions/createPopup.html
Normal file
15
web/views/@default/clusters/regions/createPopup.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{$layout "layout_popup"}
|
||||||
|
|
||||||
|
<h3>创建区域</h3>
|
||||||
|
<form class="ui form" method="post" data-tea-success="success" data-tea-action="$">
|
||||||
|
<csrf-token></csrf-token>
|
||||||
|
<table class="ui table definition selectable">
|
||||||
|
<tr>
|
||||||
|
<td>区域名称 *</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="name" maxlength="100" ref="focus"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<submit-btn></submit-btn>
|
||||||
|
</form>
|
||||||
35
web/views/@default/clusters/regions/index.html
Normal file
35
web/views/@default/clusters/regions/index.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{$layout}
|
||||||
|
|
||||||
|
<first-menu>
|
||||||
|
<menu-item @click.prevent="createRegion()">创建</menu-item>
|
||||||
|
<span class="item">|</span>
|
||||||
|
<menu-item><tip-icon content="可以设置节点所属区域,从而利用区域设置进行不同的价格设定。"></tip-icon></menu-item>
|
||||||
|
</first-menu>
|
||||||
|
|
||||||
|
<p class="comment" v-if="regions.length == 0">暂时还没有区域。</p>
|
||||||
|
|
||||||
|
<table class="ui table selectable" v-if="regions.length > 0" style="width: 35em" id="sortable-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:3em"></th>
|
||||||
|
<th>区域名称</th>
|
||||||
|
<th class="width10 center">节点数</th>
|
||||||
|
<th class="width10">区域状态</th>
|
||||||
|
<th class="two op">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody v-for="region in regions" :v-id="region.id">
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center;"><i class="icon bars handle grey"></i> </td>
|
||||||
|
<td :class="{disabled: !region.isOn}">{{region.name}}</td>
|
||||||
|
<td class="center">{{region.countNodes}}</td>
|
||||||
|
<td>
|
||||||
|
<label-on :v-is-on="region.isOn"></label-on>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="" @click.prevent="updateRegion(region.id)">修改</a> <a href="" @click.prevent="deleteRegion(region.id)">删除</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-if="regions.length > 0" class="comment">可以拖动左侧的<i class="icon bars"></i>排序。</p>
|
||||||
45
web/views/@default/clusters/regions/index.js
Normal file
45
web/views/@default/clusters/regions/index.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
this.$delay(function () {
|
||||||
|
let that = this
|
||||||
|
sortTable(function (ids) {
|
||||||
|
that.$post(".sort")
|
||||||
|
.params({
|
||||||
|
regionIds: ids
|
||||||
|
})
|
||||||
|
.success(function () {
|
||||||
|
teaweb.successToast("排序保存成功")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.createRegion = function () {
|
||||||
|
teaweb.popup(Tea.url(".createPopup"), {
|
||||||
|
callback: function () {
|
||||||
|
teaweb.success("保存成功", function () {
|
||||||
|
teaweb.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRegion = function (regionId) {
|
||||||
|
teaweb.popup(Tea.url(".updatePopup?regionId=" + regionId), {
|
||||||
|
callback: function () {
|
||||||
|
teaweb.success("保存成功", function () {
|
||||||
|
teaweb.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deleteRegion = function (regionId) {
|
||||||
|
let that = this
|
||||||
|
teaweb.confirm("确定要删除这个区域吗?", function () {
|
||||||
|
that.$post(".delete")
|
||||||
|
.params({
|
||||||
|
regionId: regionId
|
||||||
|
})
|
||||||
|
.refresh()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
27
web/views/@default/clusters/regions/updatePopup.html
Normal file
27
web/views/@default/clusters/regions/updatePopup.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{$layout "layout_popup"}
|
||||||
|
|
||||||
|
<h3>修改区域</h3>
|
||||||
|
<form class="ui form" method="post" data-tea-success="success" data-tea-action="$">
|
||||||
|
<csrf-token></csrf-token>
|
||||||
|
<input type="hidden" name="regionId" :value="region.id"/>
|
||||||
|
<table class="ui table definition selectable">
|
||||||
|
<tr>
|
||||||
|
<td class="title">区域名称 *</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="name" maxlength="100" ref="focus" v-model="region.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><more-options-indicator></more-options-indicator></td>
|
||||||
|
</tr>
|
||||||
|
<tbody v-show="moreOptionsVisible">
|
||||||
|
<tr>
|
||||||
|
<td>是否启用</td>
|
||||||
|
<td>
|
||||||
|
<checkbox name="isOn" v-model="region.isOn"></checkbox>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<submit-btn></submit-btn>
|
||||||
|
</form>
|
||||||
Reference in New Issue
Block a user