mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-13 16:16:06 +00:00
refactor(clients): coherent group management — rename, split, extract
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.
This commit is contained in:
+1
-107
@@ -47,22 +47,15 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/bulkAdjust", a.bulkAdjust)
|
||||
g.POST("/bulkDel", a.bulkDelete)
|
||||
g.POST("/bulkCreate", a.bulkCreate)
|
||||
g.POST("/bulkAssignGroup", a.bulkAssignGroup)
|
||||
g.POST("/bulkAttach", a.bulkAttach)
|
||||
g.POST("/bulkDetach", a.bulkDetach)
|
||||
g.POST("/bulkResetTraffic", a.bulkResetTraffic)
|
||||
g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
|
||||
g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
|
||||
g.POST("/ips/:email", a.getIps)
|
||||
g.POST("/clearIps/:email", a.clearIps)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/lastOnline", a.lastOnline)
|
||||
|
||||
g.GET("/groups", a.listGroups)
|
||||
g.GET("/groups/:name/emails", a.groupEmails)
|
||||
g.POST("/groups/create", a.createGroup)
|
||||
g.POST("/groups/rename", a.renameGroup)
|
||||
g.POST("/groups/delete", a.deleteGroup)
|
||||
g.POST("/bulkResetTraffic", a.bulkResetTraffic)
|
||||
}
|
||||
|
||||
func (a *ClientController) list(c *gin.Context) {
|
||||
@@ -220,27 +213,6 @@ type bulkDeleteRequest struct {
|
||||
KeepTraffic bool `json:"keepTraffic"`
|
||||
}
|
||||
|
||||
type bulkAssignGroupRequest struct {
|
||||
Emails []string `json:"emails"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
func (a *ClientController) bulkAssignGroup(c *gin.Context) {
|
||||
var req bulkAssignGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
affected, err := a.clientService.AssignGroup(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 bulkAttachRequest struct {
|
||||
Emails []string `json:"emails"`
|
||||
InboundIds []int `json:"inboundIds"`
|
||||
@@ -471,25 +443,6 @@ func (a *ClientController) detach(c *gin.Context) {
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
func (a *ClientController) listGroups(c *gin.Context) {
|
||||
rows, err := a.clientService.ListGroups()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, rows, nil)
|
||||
}
|
||||
|
||||
func (a *ClientController) groupEmails(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 bulkResetRequest struct {
|
||||
Emails []string `json:"emails"`
|
||||
}
|
||||
@@ -509,62 +462,3 @@ func (a *ClientController) bulkResetTraffic(c *gin.Context) {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
type groupCreateBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (a *ClientController) createGroup(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 *ClientController) renameGroup(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 *ClientController) deleteGroup(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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user