diff --git a/internal/rpc/rpc_client.go b/internal/rpc/rpc_client.go
index db45ac7f..abf30325 100644
--- a/internal/rpc/rpc_client.go
+++ b/internal/rpc/rpc_client.go
@@ -82,6 +82,10 @@ func (this *RPCClient) NodeClusterRPC() pb.NodeClusterServiceClient {
 	return pb.NewNodeClusterServiceClient(this.pickConn())
 }
 
+func (this *RPCClient) NodeGroupRPC() pb.NodeGroupServiceClient {
+	return pb.NewNodeGroupServiceClient(this.pickConn())
+}
+
 func (this *RPCClient) NodeIPAddressRPC() pb.NodeIPAddressServiceClient {
 	return pb.NewNodeIPAddressServiceClient(this.pickConn())
 }
diff --git a/internal/web/actions/default/clusters/cluster/groups/createPopup.go b/internal/web/actions/default/clusters/cluster/groups/createPopup.go
new file mode 100644
index 00000000..bb5bf73e
--- /dev/null
+++ b/internal/web/actions/default/clusters/cluster/groups/createPopup.go
@@ -0,0 +1,44 @@
+package groups
+
+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 {
+	ClusterId int64
+	Name      string
+
+	Must *actions.Must
+}) {
+	if params.ClusterId <= 0 {
+		this.Fail("请选择集群")
+	}
+
+	params.Must.
+		Field("name", params.Name).
+		Require("请输入分组名称")
+	_, err := this.RPC().NodeGroupRPC().CreateNodeGroup(this.AdminContext(), &pb.CreateNodeGroupRequest{
+		ClusterId: params.ClusterId,
+		Name:      params.Name,
+	})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+
+	this.Success()
+}
diff --git a/internal/web/actions/default/clusters/cluster/groups/delete.go b/internal/web/actions/default/clusters/cluster/groups/delete.go
new file mode 100644
index 00000000..3fd04763
--- /dev/null
+++ b/internal/web/actions/default/clusters/cluster/groups/delete.go
@@ -0,0 +1,33 @@
+package groups
+
+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 {
+	GroupId int64
+}) {
+	// 检查是否正在使用
+	countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithGroupIdRequest{GroupId: params.GroupId})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+
+	if countResp.Count > 0 {
+		this.Fail("此分组正在被使用不能删除,请修改节点后再删除")
+	}
+
+	_, err = this.RPC().NodeGroupRPC().DeleteNodeGroup(this.AdminContext(), &pb.DeleteNodeGroupRequest{GroupId: params.GroupId})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+
+	this.Success()
+}
diff --git a/internal/web/actions/default/clusters/cluster/groups/index.go b/internal/web/actions/default/clusters/cluster/groups/index.go
new file mode 100644
index 00000000..2e54ce78
--- /dev/null
+++ b/internal/web/actions/default/clusters/cluster/groups/index.go
@@ -0,0 +1,47 @@
+package groups
+
+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("", "node", "group")
+	this.SecondMenu("nodes")
+}
+
+func (this *IndexAction) RunGet(params struct {
+	ClusterId int64
+}) {
+	groupsResp, err := this.RPC().NodeGroupRPC().FindAllEnabledNodeGroupsWithClusterId(this.AdminContext(), &pb.FindAllEnabledNodeGroupsWithClusterIdRequest{
+		ClusterId: params.ClusterId,
+	})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+
+	groupMaps := []maps.Map{}
+	for _, group := range groupsResp.Groups {
+		countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesWithGroupId(this.AdminContext(), &pb.CountAllEnabledNodesWithGroupIdRequest{GroupId: group.Id})
+		if err != nil {
+			this.ErrorPage(err)
+			return
+		}
+		countNodes := countResp.Count
+
+		groupMaps = append(groupMaps, maps.Map{
+			"id":         group.Id,
+			"name":       group.Name,
+			"countNodes": countNodes,
+		})
+	}
+	this.Data["groups"] = groupMaps
+
+	this.Show()
+}
diff --git a/internal/web/actions/default/clusters/cluster/groups/sort.go b/internal/web/actions/default/clusters/cluster/groups/sort.go
new file mode 100644
index 00000000..b673f81d
--- /dev/null
+++ b/internal/web/actions/default/clusters/cluster/groups/sort.go
@@ -0,0 +1,22 @@
+package groups
+
+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 {
+	GroupIds []int64
+}) {
+	_, err := this.RPC().NodeGroupRPC().UpdateNodeGroupOrders(this.AdminContext(), &pb.UpdateNodeGroupOrdersRequest{GroupIds: params.GroupIds})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+
+	this.Success()
+}
diff --git a/internal/web/actions/default/clusters/cluster/groups/updatePopup.go b/internal/web/actions/default/clusters/cluster/groups/updatePopup.go
new file mode 100644
index 00000000..4a48c172
--- /dev/null
+++ b/internal/web/actions/default/clusters/cluster/groups/updatePopup.go
@@ -0,0 +1,59 @@
+package groups
+
+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 {
+	GroupId int64
+}) {
+	groupResp, err := this.RPC().NodeGroupRPC().FindEnabledNodeGroup(this.AdminContext(), &pb.FindEnabledNodeGroupRequest{GroupId: params.GroupId})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+	group := groupResp.Group
+	if group == nil {
+		this.NotFound("nodeGroup", params.GroupId)
+		return
+	}
+
+	this.Data["group"] = maps.Map{
+		"id":   group.Id,
+		"name": group.Name,
+	}
+
+	this.Show()
+}
+
+func (this *UpdatePopupAction) RunPost(params struct {
+	GroupId int64
+	Name    string
+
+	Must *actions.Must
+}) {
+	params.Must.
+		Field("name", params.Name).
+		Require("请输入分组名称")
+	_, err := this.RPC().NodeGroupRPC().UpdateNodeGroup(this.AdminContext(), &pb.UpdateNodeGroupRequest{
+		GroupId: params.GroupId,
+		Name:    params.Name,
+	})
+	if err != nil {
+		this.ErrorPage(err)
+		return
+	}
+
+	this.Success()
+}
diff --git a/internal/web/actions/default/clusters/cluster/index.go b/internal/web/actions/default/clusters/cluster/index.go
index 8d806bd7..9a0b2f3b 100644
--- a/internal/web/actions/default/clusters/cluster/index.go
+++ b/internal/web/actions/default/clusters/cluster/index.go
@@ -25,14 +25,17 @@ func (this *IndexAction) RunGet(params struct {
 	ClusterId      int64
 	InstalledState int
 	ActiveState    int
+	Keyword        string
 }) {
 	this.Data["installState"] = params.InstalledState
 	this.Data["activeState"] = params.ActiveState
+	this.Data["keyword"] = params.Keyword
 
 	countResp, err := this.RPC().NodeRPC().CountAllEnabledNodesMatch(this.AdminContext(), &pb.CountAllEnabledNodesMatchRequest{
 		ClusterId:    params.ClusterId,
 		InstallState: types.Int32(params.InstalledState),
 		ActiveState:  types.Int32(params.ActiveState),
+		Keyword:      params.Keyword,
 	})
 	if err != nil {
 		this.ErrorPage(err)
@@ -48,6 +51,7 @@ func (this *IndexAction) RunGet(params struct {
 		ClusterId:    params.ClusterId,
 		InstallState: types.Int32(params.InstalledState),
 		ActiveState:  types.Int32(params.ActiveState),
+		Keyword:      params.Keyword,
 	})
 	nodeMaps := []maps.Map{}
 	for _, node := range nodesResp.Nodes {
diff --git a/internal/web/actions/default/clusters/cluster/init.go b/internal/web/actions/default/clusters/cluster/init.go
index a8a8c56d..7465317e 100644
--- a/internal/web/actions/default/clusters/cluster/init.go
+++ b/internal/web/actions/default/clusters/cluster/init.go
@@ -1,6 +1,7 @@
 package cluster
 
 import (
+	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/groups"
 	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/node"
 	clusters "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/clusterutils"
 	"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
@@ -34,6 +35,14 @@ func init() {
 			Get("/node/logs", new(node.LogsAction)).
 			Post("/node/start", new(node.StartAction)).
 			Post("/node/stop", new(node.StopAction)).
+
+			// 分组相关
+			Get("/groups", new(groups.IndexAction)).
+			GetPost("/groups/createPopup", new(groups.CreatePopupAction)).
+			GetPost("/groups/updatePopup", new(groups.UpdatePopupAction)).
+			Post("/groups/delete", new(groups.DeleteAction)).
+			Post("/groups/sort", new(groups.SortAction)).
+
 			EndAll()
 	})
 }
diff --git a/internal/web/actions/default/clusters/cluster/upgradeRemote.go b/internal/web/actions/default/clusters/cluster/upgradeRemote.go
index 6a0574b3..c7d58c04 100644
--- a/internal/web/actions/default/clusters/cluster/upgradeRemote.go
+++ b/internal/web/actions/default/clusters/cluster/upgradeRemote.go
@@ -13,7 +13,8 @@ type UpgradeRemoteAction struct {
 }
 
 func (this *UpgradeRemoteAction) Init() {
-	this.Nav("", "", "")
+	this.Nav("", "node", "install")
+	this.SecondMenu("nodes")
 }
 
 func (this *UpgradeRemoteAction) RunGet(params struct {
diff --git a/web/views/@default/clusters/cluster/@menu.html b/web/views/@default/clusters/cluster/@menu.html
index fdbf1e34..76659ff6 100644
--- a/web/views/@default/clusters/cluster/@menu.html
+++ b/web/views/@default/clusters/cluster/@menu.html
@@ -2,4 +2,5 @@
 	
暂时还没有分组。
+| + | 分组名称 | +节点数 | +操作 | +
|---|---|---|---|
| + | {{group.name}} | ++ {{group.countNodes}} + 0 + | ++ 修改 删除 + | +
可以拖动左侧的排序。
\ No newline at end of file diff --git a/web/views/@default/clusters/cluster/groups/index.js b/web/views/@default/clusters/cluster/groups/index.js new file mode 100644 index 00000000..1da619f8 --- /dev/null +++ b/web/views/@default/clusters/cluster/groups/index.js @@ -0,0 +1,53 @@ +Tea.context(function () { + this.$delay(function () { + let that = this + sortTable(function () { + let groupIds = [] + document.querySelectorAll("*[data-group-id]").forEach(function (element) { + groupIds.push(element.getAttribute("data-group-id")) + }) + that.$post("/clusters/cluster/groups/sort") + .params({ + groupIds: groupIds + }) + .success(function () { + teaweb.successToast("保存成功") + }) + }) + }) + + this.createGroup = function () { + teaweb.popup("/clusters/cluster/groups/createPopup?clusterId=" + this.clusterId, { + callback: function () { + teaweb.success("保存成功", function () { + teaweb.reload() + }) + } + }) + } + + this.updateGroup = function (groupId) { + teaweb.popup("/clusters/cluster/groups/updatePopup?groupId=" + groupId, { + callback: function () { + teaweb.success("保存成功", function () { + teaweb.reload() + }) + } + }) + } + + this.deleteGroup = function (groupId) { + let that = this + teaweb.confirm("确定要删除这个分组吗?", function () { + that.$post("/clusters/cluster/groups/delete") + .params({ + groupId: groupId + }) + .success(function () { + teaweb.success("删除成功", function () { + teaweb.reload() + }) + }) + }) + } +}) \ No newline at end of file diff --git a/web/views/@default/clusters/cluster/groups/updatePopup.html b/web/views/@default/clusters/cluster/groups/updatePopup.html new file mode 100644 index 00000000..8286cbb4 --- /dev/null +++ b/web/views/@default/clusters/cluster/groups/updatePopup.html @@ -0,0 +1,15 @@ +{$layout "layout_popup"} + +