mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-29 16:39:35 +00:00
This bundles a set of group-related improvements that built up across one session and only make sense together. Terminology / API surface: - Rename "assign group" → "add to group" everywhere: i18n keys, callback names (bulkAddToGroup), component + file names (BulkAddToGroupModal, AddClientsToGroupModal), Go controller/struct names (bulkAddToGroup, AddToGroup), OpenAPI summaries. Nothing keeps the word "assign" anymore. - Move group routes under /panel/api/clients/groups/* (was /bulkAssignGroup at the clients root). - Split add and remove into two endpoints: /groups/bulkAdd now rejects empty group; new /groups/bulkRemove clears the label for the given emails. The old "submit empty to clear" UX is gone — Ungroup is its own action. UI affordances on Clients page: - Promote Group + Ungroup to visible bar buttons next to Attach + Detach. Group reuses BulkAddToGroupModal; Ungroup pops a danger confirm and calls bulkRemoveFromGroup. - Custom UngroupIcon (TagsOutlined with a diagonal strike) for the Ungroup button so the pairing reads at a glance. - Hide the Group column when no clients have a group label yet — removes a column of em-dashes on fresh installs. UI on Groups page: - New per-row Add clients… / Remove clients… actions backed by GroupAddClientsModal and GroupRemoveClientsModal: rich client picker (email / comment / current group / enable) with search and preserveSelectedRowKeys, mirroring the inbounds Attach modal UX. Controller split: - Move all /groups/* routes, handlers, and request bodies out of web/controller/client.go into a dedicated web/controller/group.go (GroupController with leaner clientService + xrayService dependencies). URLs are byte-identical because the new controller registers on the same parent gin.RouterGroup; api_docs_test.go gets a group.go → /panel/api/clients basePath entry so its route extraction keeps working. Invalidation dedup: - Removing a client from a group on the Groups page used to refetch /clients/groups and /clients/onlines three times: once from the mutation's onSuccess, once from a redundant invalidate() in the page's onSubmit, once from the WebSocket invalidate broadcast that the backend fires after every mutation. The manual invalidate() is gone, and a small invalidationTracker module lets websocketBridge skip WS-driven invalidates that arrive within 1.5s of a local invalidate — bringing the refetch count down to one. The WS path still works for changes made by another tab or user.
155 lines
3.8 KiB
Go
155 lines
3.8 KiB
Go
package controller
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/util/common"
|
|
"github.com/mhsanaei/3x-ui/v3/web/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type GroupController struct {
|
|
clientService service.ClientService
|
|
xrayService service.XrayService
|
|
}
|
|
|
|
func NewGroupController(g *gin.RouterGroup) *GroupController {
|
|
a := &GroupController{}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
func (a *GroupController) initRouter(g *gin.RouterGroup) {
|
|
g.GET("/groups", a.list)
|
|
g.GET("/groups/:name/emails", a.emails)
|
|
g.POST("/groups/create", a.create)
|
|
g.POST("/groups/rename", a.rename)
|
|
g.POST("/groups/delete", a.delete)
|
|
g.POST("/groups/bulkAdd", a.bulkAdd)
|
|
g.POST("/groups/bulkRemove", a.bulkRemove)
|
|
}
|
|
|
|
func (a *GroupController) list(c *gin.Context) {
|
|
rows, err := a.clientService.ListGroups()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, rows, nil)
|
|
}
|
|
|
|
func (a *GroupController) emails(c *gin.Context) {
|
|
name := c.Param("name")
|
|
emails, err := a.clientService.EmailsByGroup(name)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, emails, nil)
|
|
}
|
|
|
|
type groupCreateBody struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (a *GroupController) create(c *gin.Context) {
|
|
var body groupCreateBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if err := a.clientService.CreateGroup(body.Name); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"name": body.Name}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type groupRenameBody struct {
|
|
OldName string `json:"oldName"`
|
|
NewName string `json:"newName"`
|
|
}
|
|
|
|
func (a *GroupController) rename(c *gin.Context) {
|
|
var body groupRenameBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
affected, err := a.clientService.RenameGroup(body.OldName, body.NewName)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
a.xrayService.SetToNeedRestart()
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type groupDeleteBody struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (a *GroupController) delete(c *gin.Context) {
|
|
var body groupDeleteBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
affected, err := a.clientService.DeleteGroup(body.Name)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
a.xrayService.SetToNeedRestart()
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type bulkAddToGroupRequest struct {
|
|
Emails []string `json:"emails"`
|
|
Group string `json:"group"`
|
|
}
|
|
|
|
func (a *GroupController) bulkAdd(c *gin.Context) {
|
|
var req bulkAddToGroupRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Group) == "" {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("group name is required"))
|
|
return
|
|
}
|
|
affected, err := a.clientService.AddToGroup(req.Emails, req.Group)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
a.xrayService.SetToNeedRestart()
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type bulkRemoveFromGroupRequest struct {
|
|
Emails []string `json:"emails"`
|
|
}
|
|
|
|
func (a *GroupController) bulkRemove(c *gin.Context) {
|
|
var req bulkRemoveFromGroupRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
affected, err := a.clientService.RemoveFromGroup(req.Emails)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
a.xrayService.SetToNeedRestart()
|
|
notifyClientsChanged()
|
|
}
|