mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-10 23:47:39 +08:00
fix: Fix issue where website group can still be deleted even if it contains other websites (#8049)
This commit is contained in:
parent
451b69072d
commit
adbf44a176
27 changed files with 619 additions and 13 deletions
|
@ -67,4 +67,5 @@ var (
|
||||||
|
|
||||||
websiteCAService = service.NewIWebsiteCAService()
|
websiteCAService = service.NewIWebsiteCAService()
|
||||||
taskService = service.NewITaskService()
|
taskService = service.NewITaskService()
|
||||||
|
groupService = service.NewIGroupService()
|
||||||
)
|
)
|
||||||
|
|
96
agent/app/api/v2/group.go
Normal file
96
agent/app/api/v2/group.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags System Group
|
||||||
|
// @Summary Create group
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.GroupCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /agent/groups [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建组 [name][type]","formatEN":"create group [name][type]"}
|
||||||
|
func (b *BaseApi) CreateGroup(c *gin.Context) {
|
||||||
|
var req dto.GroupCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := groupService.Create(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithOutData(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags System Group
|
||||||
|
// @Summary Delete group
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /agent/groups/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"name","output_value":"name"},{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"type","output_value":"type"}],"formatZH":"删除组 [type][name]","formatEN":"delete group [type][name]"}
|
||||||
|
func (b *BaseApi) DeleteGroup(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := groupService.Delete(req.ID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithOutData(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags System Group
|
||||||
|
// @Summary Update group
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.GroupUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /agent/groups/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新组 [name][type]","formatEN":"update group [name][type]"}
|
||||||
|
func (b *BaseApi) UpdateGroup(c *gin.Context) {
|
||||||
|
var req dto.GroupUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := groupService.Update(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithOutData(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags System Group
|
||||||
|
// @Summary List groups
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.GroupSearch true "request"
|
||||||
|
// @Success 200 {array} dto.OperateByType
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /agent/groups/search [post]
|
||||||
|
func (b *BaseApi) ListGroup(c *gin.Context) {
|
||||||
|
var req dto.OperateByType
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := groupService.List(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, list)
|
||||||
|
}
|
29
agent/app/dto/group.go
Normal file
29
agent/app/dto/group.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
type GroupCreate struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupSearch struct {
|
||||||
|
Type string `json:"type" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUpdate struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type" validate:"required"`
|
||||||
|
IsDefault bool `json:"isDefault"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IsDefault bool `json:"isDefault"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperateByType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
8
agent/app/model/group.go
Normal file
8
agent/app/model/group.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
BaseModel
|
||||||
|
IsDefault bool `json:"isDefault"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
56
agent/app/repo/group.go
Normal file
56
agent/app/repo/group.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupRepo struct{}
|
||||||
|
|
||||||
|
type IGroupRepo interface {
|
||||||
|
Get(opts ...DBOption) (model.Group, error)
|
||||||
|
GetList(opts ...DBOption) ([]model.Group, error)
|
||||||
|
Create(group *model.Group) error
|
||||||
|
Update(id uint, vars map[string]interface{}) error
|
||||||
|
Delete(opts ...DBOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIGroupRepo() IGroupRepo {
|
||||||
|
return &GroupRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GroupRepo) Get(opts ...DBOption) (model.Group, error) {
|
||||||
|
var group model.Group
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.First(&group).Error
|
||||||
|
return group, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GroupRepo) GetList(opts ...DBOption) ([]model.Group, error) {
|
||||||
|
var groups []model.Group
|
||||||
|
db := global.DB.Model(&model.Group{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.Find(&groups).Error
|
||||||
|
return groups, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GroupRepo) Create(group *model.Group) error {
|
||||||
|
return global.DB.Create(group).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GroupRepo) Update(id uint, vars map[string]interface{}) error {
|
||||||
|
return global.DB.Model(&model.Group{}).Where("id = ?", id).Updates(vars).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GroupRepo) Delete(opts ...DBOption) error {
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Delete(&model.Group{}).Error
|
||||||
|
}
|
|
@ -45,4 +45,6 @@ var (
|
||||||
favoriteRepo = repo.NewIFavoriteRepo()
|
favoriteRepo = repo.NewIFavoriteRepo()
|
||||||
|
|
||||||
taskRepo = repo.NewITaskRepo()
|
taskRepo = repo.NewITaskRepo()
|
||||||
|
|
||||||
|
groupRepo = repo.NewIGroupRepo()
|
||||||
)
|
)
|
||||||
|
|
87
agent/app/service/group.go
Normal file
87
agent/app/service/group.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupService struct{}
|
||||||
|
|
||||||
|
type IGroupService interface {
|
||||||
|
List(req dto.OperateByType) ([]dto.GroupInfo, error)
|
||||||
|
Create(req dto.GroupCreate) error
|
||||||
|
Update(req dto.GroupUpdate) error
|
||||||
|
Delete(id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIGroupService() IGroupService {
|
||||||
|
return &GroupService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *GroupService) List(req dto.OperateByType) ([]dto.GroupInfo, error) {
|
||||||
|
options := []repo.DBOption{
|
||||||
|
repo.WithOrderBy("is_default desc"),
|
||||||
|
repo.WithOrderBy("created_at desc"),
|
||||||
|
}
|
||||||
|
if len(req.Type) != 0 {
|
||||||
|
options = append(options, repo.WithByType(req.Type))
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
groups []model.Group
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
groups, err = groupRepo.GetList(options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, buserr.New("ErrRecordNotFound")
|
||||||
|
}
|
||||||
|
var dtoUsers []dto.GroupInfo
|
||||||
|
for _, group := range groups {
|
||||||
|
var item dto.GroupInfo
|
||||||
|
if err := copier.Copy(&item, &group); err != nil {
|
||||||
|
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
|
}
|
||||||
|
dtoUsers = append(dtoUsers, item)
|
||||||
|
}
|
||||||
|
return dtoUsers, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *GroupService) Create(req dto.GroupCreate) error {
|
||||||
|
group, _ := groupRepo.Get(repo.WithByName(req.Name), repo.WithByType(req.Type))
|
||||||
|
if group.ID != 0 {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
if err := copier.Copy(&group, &req); err != nil {
|
||||||
|
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
|
}
|
||||||
|
if err := groupRepo.Create(&group); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *GroupService) Delete(id uint) error {
|
||||||
|
group, _ := groupRepo.Get(repo.WithByID(id))
|
||||||
|
if group.ID == 0 {
|
||||||
|
return buserr.New("ErrRecordNotFound")
|
||||||
|
}
|
||||||
|
if group.IsDefault {
|
||||||
|
return buserr.New("ErrGroupIsDefault")
|
||||||
|
}
|
||||||
|
if group.Type == "website" {
|
||||||
|
websites, _ := websiteRepo.List(websiteRepo.WithGroupID(group.ID))
|
||||||
|
if len(websites) > 0 {
|
||||||
|
return buserr.New("ErrGroupIsInWebsiteUse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groupRepo.Delete(repo.WithByID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *GroupService) Update(req dto.GroupUpdate) error {
|
||||||
|
upMap := make(map[string]interface{})
|
||||||
|
upMap["name"] = req.Name
|
||||||
|
upMap["is_default"] = req.IsDefault
|
||||||
|
return groupRepo.Update(req.ID, upMap)
|
||||||
|
}
|
|
@ -19,6 +19,8 @@ ErrTypePort: "Port {{ .name }} format error"
|
||||||
Success: "Success"
|
Success: "Success"
|
||||||
Failed: "Failed"
|
Failed: "Failed"
|
||||||
SystemRestart: "System restart causes task interruption"
|
SystemRestart: "System restart causes task interruption"
|
||||||
|
ErrGroupIsDefault: "Default group, cannot be deleted"
|
||||||
|
ErrGroupIsInWebsiteUse: "Group is being used by other websites, cannot be deleted"
|
||||||
|
|
||||||
#backup
|
#backup
|
||||||
ErrBackupLocalDelete: "Deleting local server backup accounts is not currently supported."
|
ErrBackupLocalDelete: "Deleting local server backup accounts is not currently supported."
|
||||||
|
|
|
@ -28,6 +28,8 @@ ErrTypePortRange: 'ポートレンジは1-65535の間である必要がありま
|
||||||
Success: "成功"
|
Success: "成功"
|
||||||
Failed: "失敗した"
|
Failed: "失敗した"
|
||||||
SystemRestart: "システムの再起動により、タスクが中断されます"
|
SystemRestart: "システムの再起動により、タスクが中断されます"
|
||||||
|
ErrGroupIsDefault: "デフォルトグループ、削除できません"
|
||||||
|
ErrGroupIsInWebsiteUse: "グループは他のウェブサイトで使用されています、削除できません"
|
||||||
|
|
||||||
#app
|
#app
|
||||||
ErrPortInUsed: "{{.Detail}}ポートはすでに使用されています"
|
ErrPortInUsed: "{{.Detail}}ポートはすでに使用されています"
|
||||||
|
|
|
@ -29,6 +29,8 @@ ErrTypePortRange: '포트 범위는 1-65535 사이여야 합니다'
|
||||||
Success: "성공"
|
Success: "성공"
|
||||||
Failed: "실패"
|
Failed: "실패"
|
||||||
SystemRestart: "시스템 재시작으로 인해 작업이 중단되었습니다"
|
SystemRestart: "시스템 재시작으로 인해 작업이 중단되었습니다"
|
||||||
|
ErrGroupIsDefault: "기본 그룹, 삭제할 수 없습니다"
|
||||||
|
ErrGroupIsInWebsiteUse: "그룹이 다른 웹사이트에서 사용 중입니다, 삭제할 수 없습니다"
|
||||||
|
|
||||||
# 애플리케이션
|
# 애플리케이션
|
||||||
ErrPortInUsed: "{{ .detail }} 포트가 이미 사용 중입니다"
|
ErrPortInUsed: "{{ .detail }} 포트가 이미 사용 중입니다"
|
||||||
|
|
|
@ -29,6 +29,8 @@ ErrTypePortRange: 'Julat port perlu berada di antara 1-65535'
|
||||||
Success: "Berjaya"
|
Success: "Berjaya"
|
||||||
Failed: "Gagal"
|
Failed: "Gagal"
|
||||||
SystemRestart: "Mulakan semula sistem menyebabkan gangguan tugas"
|
SystemRestart: "Mulakan semula sistem menyebabkan gangguan tugas"
|
||||||
|
ErrGroupIsDefault: "Kumpulan lalai, tidak boleh dipadam"
|
||||||
|
ErrGroupIsInWebsiteUse: "Kumpulan sedang digunakan oleh laman web lain, tidak boleh dipadam"
|
||||||
|
|
||||||
#app
|
#app
|
||||||
ErrPortInUsed: "Port {{ .detail }} sudah digunakan"
|
ErrPortInUsed: "Port {{ .detail }} sudah digunakan"
|
||||||
|
|
|
@ -28,6 +28,8 @@ ErrTypePortRange: 'O intervalo da porta deve estar entre 1-65535'
|
||||||
Success: "Sucesso"
|
Success: "Sucesso"
|
||||||
Failed: "Falhou"
|
Failed: "Falhou"
|
||||||
SystemRestart: "A reinicialização do sistema causa interrupção da tarefa"
|
SystemRestart: "A reinicialização do sistema causa interrupção da tarefa"
|
||||||
|
ErrGroupIsDefault: "Grupo padrão, não pode ser excluído"
|
||||||
|
ErrGroupIsInWebsiteUse: "O grupo está sendo usado por outros sites, não pode ser excluído"
|
||||||
|
|
||||||
#app
|
#app
|
||||||
ErrPortInUsed: "A porta {{ .detail }} já está em uso"
|
ErrPortInUsed: "A porta {{ .detail }} já está em uso"
|
||||||
|
|
|
@ -29,6 +29,8 @@ ErrTypePortRange: "Диапазон портов должен быть межд
|
||||||
Success: "Успех"
|
Success: "Успех"
|
||||||
Failed: "Неудача"
|
Failed: "Неудача"
|
||||||
SystemRestart: "Перезагрузка системы приводит к прерыванию задачи"
|
SystemRestart: "Перезагрузка системы приводит к прерыванию задачи"
|
||||||
|
ErrGroupIsDefault: "Группа по умолчанию, невозможно удалить"
|
||||||
|
ErrGroupIsInWebsiteUse: "Группа используется другими сайтами, невозможно удалить"
|
||||||
|
|
||||||
#app
|
#app
|
||||||
ErrPortInUsed: "Порт {{ .detail }} уже используется"
|
ErrPortInUsed: "Порт {{ .detail }} уже используется"
|
||||||
|
|
|
@ -24,6 +24,8 @@ Success: "成功"
|
||||||
Failed: "失敗"
|
Failed: "失敗"
|
||||||
SystemRestart: "系統重啟導致任務中斷"
|
SystemRestart: "系統重啟導致任務中斷"
|
||||||
ErrInvalidChar: "禁止使用非法字元"
|
ErrInvalidChar: "禁止使用非法字元"
|
||||||
|
ErrGroupIsDefault: "預設分組,無法刪除"
|
||||||
|
ErrGroupIsInWebsiteUse: "分組正在被其他網站使用,無法刪除"
|
||||||
|
|
||||||
#backup
|
#backup
|
||||||
ErrBackupLocalDelete: "暫不支持刪除本地伺服器備份帳號"
|
ErrBackupLocalDelete: "暫不支持刪除本地伺服器備份帳號"
|
||||||
|
|
|
@ -30,6 +30,8 @@ ErrTypePortRange: '端口范围需要在 1-65535 之间'
|
||||||
Success: "成功"
|
Success: "成功"
|
||||||
Failed: "失败"
|
Failed: "失败"
|
||||||
SystemRestart: "系统重启导致任务中断"
|
SystemRestart: "系统重启导致任务中断"
|
||||||
|
ErrGroupIsDefault: "默认分组,无法删除"
|
||||||
|
ErrGroupIsInWebsiteUse: "分组正在被其他网站使用,无法删除"
|
||||||
|
|
||||||
#backup
|
#backup
|
||||||
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"
|
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"
|
||||||
|
|
|
@ -26,6 +26,7 @@ func InitAgentDB() {
|
||||||
migrations.UpdateApp,
|
migrations.UpdateApp,
|
||||||
migrations.AddOllamaModel,
|
migrations.AddOllamaModel,
|
||||||
migrations.UpdateSettingStatus,
|
migrations.UpdateSettingStatus,
|
||||||
|
migrations.InitDefault,
|
||||||
})
|
})
|
||||||
if err := m.Migrate(); err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
global.LOG.Error(err)
|
global.LOG.Error(err)
|
||||||
|
|
|
@ -57,6 +57,7 @@ var AddTable = &gormigrate.Migration{
|
||||||
&model.WebsiteDnsAccount{},
|
&model.WebsiteDnsAccount{},
|
||||||
&model.WebsiteDomain{},
|
&model.WebsiteDomain{},
|
||||||
&model.WebsiteSSL{},
|
&model.WebsiteSSL{},
|
||||||
|
&model.Group{},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -285,3 +286,13 @@ var UpdateSettingStatus = &gormigrate.Migration{
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var InitDefault = &gormigrate.Migration{
|
||||||
|
ID: "20250301-init-default",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(&model.Group{Name: "Default", Type: "website", IsDefault: true}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -22,5 +22,6 @@ func commonGroups() []CommonRouter {
|
||||||
&ProcessRouter{},
|
&ProcessRouter{},
|
||||||
&WebsiteCARouter{},
|
&WebsiteCARouter{},
|
||||||
&AIToolsRouter{},
|
&AIToolsRouter{},
|
||||||
|
&GroupRouter{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
agent/router/ro_group.go
Normal file
20
agent/router/ro_group.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupRouter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GroupRouter) InitRouter(Router *gin.RouterGroup) {
|
||||||
|
groupRouter := Router.Group("groups")
|
||||||
|
baseApi := v2.ApiGroupApp.BaseApi
|
||||||
|
{
|
||||||
|
groupRouter.POST("", baseApi.CreateGroup)
|
||||||
|
groupRouter.POST("/del", baseApi.DeleteGroup)
|
||||||
|
groupRouter.POST("/update", baseApi.UpdateGroup)
|
||||||
|
groupRouter.POST("/search", baseApi.ListGroup)
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ type IGroupRepo interface {
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
Delete(opts ...global.DBOption) error
|
Delete(opts ...global.DBOption) error
|
||||||
|
|
||||||
WithByDefault(isDefalut bool) global.DBOption
|
WithByDefault(isDefault bool) global.DBOption
|
||||||
CancelDefault(groupType string) error
|
CancelDefault(groupType string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,9 +23,9 @@ func NewIGroupRepo() IGroupRepo {
|
||||||
return &GroupRepo{}
|
return &GroupRepo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GroupRepo) WithByDefault(isDefalut bool) global.DBOption {
|
func (c *GroupRepo) WithByDefault(isDefault bool) global.DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("is_default = ?", isDefalut)
|
return g.Where("is_default = ?", isDefault)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,20 +166,20 @@ var InitSetting = &gormigrate.Migration{
|
||||||
var InitHost = &gormigrate.Migration{
|
var InitHost = &gormigrate.Migration{
|
||||||
ID: "20240816-init-host",
|
ID: "20240816-init-host",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
hostGroup := &model.Group{Name: "default", Type: "host", IsDefault: true}
|
hostGroup := &model.Group{Name: "Default", Type: "host", IsDefault: true}
|
||||||
if err := tx.Create(hostGroup).Error; err != nil {
|
if err := tx.Create(hostGroup).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tx.Create(&model.Group{Name: "default", Type: "node", IsDefault: true}).Error; err != nil {
|
if err := tx.Create(&model.Group{Name: "Default", Type: "node", IsDefault: true}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tx.Create(&model.Group{Name: "default", Type: "command", IsDefault: true}).Error; err != nil {
|
if err := tx.Create(&model.Group{Name: "Default", Type: "command", IsDefault: true}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tx.Create(&model.Group{Name: "default", Type: "website", IsDefault: true}).Error; err != nil {
|
if err := tx.Create(&model.Group{Name: "Default", Type: "website", IsDefault: true}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := tx.Create(&model.Group{Name: "default", Type: "redis", IsDefault: true}).Error; err != nil {
|
if err := tx.Create(&model.Group{Name: "Default", Type: "redis", IsDefault: true}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
host := model.Host{
|
host := model.Host{
|
||||||
|
|
|
@ -13,3 +13,16 @@ export const updateGroup = (params: Group.GroupUpdate) => {
|
||||||
export const deleteGroup = (id: number) => {
|
export const deleteGroup = (id: number) => {
|
||||||
return http.post(`/core/groups/del`, { id: id });
|
return http.post(`/core/groups/del`, { id: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAgentGroupList = (type: string) => {
|
||||||
|
return http.post<Array<Group.GroupInfo>>(`/groups/search`, { type: type });
|
||||||
|
};
|
||||||
|
export const createAgentGroup = (params: Group.GroupCreate) => {
|
||||||
|
return http.post<Group.GroupCreate>(`/groups`, params);
|
||||||
|
};
|
||||||
|
export const updateAgentGroup = (params: Group.GroupUpdate) => {
|
||||||
|
return http.post(`/groups/update`, params);
|
||||||
|
};
|
||||||
|
export const deleteAgentGroup = (id: number) => {
|
||||||
|
return http.post(`/groups/del`, { id: id });
|
||||||
|
};
|
||||||
|
|
83
frontend/src/components/agent-group/change.vue
Normal file
83
frontend/src/components/agent-group/change.vue
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<DrawerPro v-model="drawerVisible" :header="$t('terminal.groupChange')" :back="handleClose" size="small">
|
||||||
|
<el-form @submit.prevent ref="hostInfoRef" label-position="top" :model="dialogData" :rules="rules">
|
||||||
|
<el-form-item :label="$t('commons.table.group')" prop="group">
|
||||||
|
<el-select filterable v-model="dialogData.groupID" clearable style="width: 100%">
|
||||||
|
<div v-for="item in groupList" :key="item.id">
|
||||||
|
<el-option :label="item.name" :value="item.id" />
|
||||||
|
</div>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="onSubmit(hostInfoRef)">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</DrawerPro>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
import type { ElForm } from 'element-plus';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import { getGroupList } from '@/api/modules/group';
|
||||||
|
|
||||||
|
const loading = ref();
|
||||||
|
interface DialogProps {
|
||||||
|
group: string;
|
||||||
|
groupType: string;
|
||||||
|
}
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const dialogData = ref({
|
||||||
|
groupID: 0,
|
||||||
|
groupType: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupList = ref();
|
||||||
|
const acceptParams = (params: DialogProps): void => {
|
||||||
|
dialogData.value.groupType = params.groupType;
|
||||||
|
loadGroups(params.group);
|
||||||
|
drawerVisible.value = true;
|
||||||
|
};
|
||||||
|
const emit = defineEmits(['search', 'change']);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const hostInfoRef = ref<FormInstance>();
|
||||||
|
const rules = reactive({
|
||||||
|
groupID: [Rules.requiredSelect],
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadGroups = async (groupName: string) => {
|
||||||
|
const res = await getGroupList(dialogData.value.groupType);
|
||||||
|
groupList.value = res.data;
|
||||||
|
for (const group of groupList.value) {
|
||||||
|
if (group.name === groupName) {
|
||||||
|
dialogData.value.groupID = group.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
loading.value = true;
|
||||||
|
emit('change', Number(dialogData.value.groupID));
|
||||||
|
loading.value = false;
|
||||||
|
drawerVisible.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
179
frontend/src/components/agent-group/index.vue
Normal file
179
frontend/src/components/agent-group/index.vue
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<template>
|
||||||
|
<DrawerPro v-model="open" :header="$t('commons.table.group')" @close="handleClose" size="large" :back="handleClose">
|
||||||
|
<template #content>
|
||||||
|
<ComplexTable :data="data" @search="search()">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-button type="primary" @click="openCreate">{{ $t('website.createGroup') }}</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table-column :label="$t('commons.table.name')" prop="name">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="!row.edit">
|
||||||
|
<span v-if="row.name === 'default'">
|
||||||
|
{{ $t('commons.table.default') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="row.name !== 'default'">{{ row.name }}</span>
|
||||||
|
<el-tag v-if="row.isDefault" type="success" class="ml-2" size="small">
|
||||||
|
({{ $t('commons.table.default') }})
|
||||||
|
</el-tag>
|
||||||
|
|
||||||
|
<el-tag type="warning" size="small" class="ml-4" v-if="row.isDelete">
|
||||||
|
{{ $t('app.takeDown') }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form @submit.prevent ref="groupForm" v-if="row.edit" :model="row">
|
||||||
|
<el-form-item prop="name" v-if="row.edit" :rules="Rules.name">
|
||||||
|
<div style="margin-top: 20px; width: 100%"><el-input v-model="row.name" /></div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('commons.table.operate')">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<div>
|
||||||
|
<el-button link v-if="row.edit" type="primary" @click="saveGroup(groupForm, row)">
|
||||||
|
{{ $t('commons.button.save') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button link v-if="!row.edit" type="primary" @click="editGroup($index)">
|
||||||
|
{{ $t('commons.button.edit') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
v-if="!row.edit"
|
||||||
|
:disabled="row.isDefault"
|
||||||
|
type="primary"
|
||||||
|
@click="removeGroup($index)"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.delete') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button link v-if="row.edit" type="primary" @click="search()">
|
||||||
|
{{ $t('commons.button.cancel') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
v-if="!row.edit && !row.isDefault && !row.isDelete"
|
||||||
|
type="primary"
|
||||||
|
@click="setDefault(row)"
|
||||||
|
>
|
||||||
|
{{ $t('website.setDefault') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</ComplexTable>
|
||||||
|
</template>
|
||||||
|
</DrawerPro>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { createAgentGroup, deleteAgentGroup, getAgentGroupList, updateAgentGroup } from '@/api/modules/group';
|
||||||
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import { Group } from '@/api/interface/group';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const type = ref();
|
||||||
|
const data = ref();
|
||||||
|
const handleClose = () => {
|
||||||
|
open.value = false;
|
||||||
|
data.value = [];
|
||||||
|
emit('search');
|
||||||
|
};
|
||||||
|
interface DialogProps {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupForm = ref<FormInstance>();
|
||||||
|
const acceptParams = (params: DialogProps): void => {
|
||||||
|
type.value = params.type;
|
||||||
|
open.value = true;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
|
const search = () => {
|
||||||
|
getAgentGroupList(type.value).then((res) => {
|
||||||
|
data.value = res.data || [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveGroup = async (formEl: FormInstance, group: Group.GroupInfo) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
await formEl.validate((valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
group.type = type.value;
|
||||||
|
if (group.id == 0) {
|
||||||
|
createAgentGroup(group).then(() => {
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateAgentGroup(group).then(() => {
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDefault = (group: Group.GroupInfo) => {
|
||||||
|
group.isDefault = true;
|
||||||
|
group.type = type.value;
|
||||||
|
updateAgentGroup(group).then(() => {
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
for (const d of data.value) {
|
||||||
|
if (d.name == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.edit) {
|
||||||
|
d.edit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const g = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
isDefault: false,
|
||||||
|
edit: true,
|
||||||
|
status: 'Enable',
|
||||||
|
};
|
||||||
|
data.value.unshift(g);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeGroup = (index: number) => {
|
||||||
|
const group = data.value[index];
|
||||||
|
|
||||||
|
if (group.id > 0) {
|
||||||
|
deleteAgentGroup(group.id).then(() => {
|
||||||
|
data.value.splice(index, 1);
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
data.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const editGroup = (index: number) => {
|
||||||
|
for (const i in data.value) {
|
||||||
|
const d = data.value[i];
|
||||||
|
if (d.name == '') {
|
||||||
|
data.value.splice(Number(i), 1);
|
||||||
|
}
|
||||||
|
if (d.edit) {
|
||||||
|
d.edit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.value[index].edit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ acceptParams });
|
||||||
|
</script>
|
|
@ -42,6 +42,9 @@ router.beforeEach((to, from, next) => {
|
||||||
if (to.path === '/apps/all' && to.query.install != undefined) {
|
if (to.path === '/apps/all' && to.query.install != undefined) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
// if (to.query.uncached != undefined) {
|
||||||
|
// return next();
|
||||||
|
// }
|
||||||
|
|
||||||
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || '');
|
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || '');
|
||||||
const cachedRoute = localStorage.getItem(activeMenuKey);
|
const cachedRoute = localStorage.getItem(activeMenuKey);
|
||||||
|
|
|
@ -41,7 +41,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { FormInstance } from 'element-plus';
|
import { FormInstance } from 'element-plus';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
import { getGroupList } from '@/api/modules/group';
|
import { getAgentGroupList } from '@/api/modules/group';
|
||||||
import { Group } from '@/api/interface/group';
|
import { Group } from '@/api/interface/group';
|
||||||
|
|
||||||
const websiteForm = ref<FormInstance>();
|
const websiteForm = ref<FormInstance>();
|
||||||
|
@ -87,7 +87,7 @@ const submit = async (formEl: FormInstance | undefined) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
const res = await getGroupList('website');
|
const res = await getAgentGroupList('website');
|
||||||
groups.value = res.data;
|
groups.value = res.data;
|
||||||
|
|
||||||
getWebsite(websiteId.value).then((res) => {
|
getWebsite(websiteId.value).then((res) => {
|
||||||
|
|
|
@ -245,7 +245,7 @@ import DefaultHtml from '@/views/website/website/html/index.vue';
|
||||||
import CreateWebSite from '@/views/website/website/create/index.vue';
|
import CreateWebSite from '@/views/website/website/create/index.vue';
|
||||||
import DeleteWebsite from '@/views/website/website/delete/index.vue';
|
import DeleteWebsite from '@/views/website/website/delete/index.vue';
|
||||||
import NginxConfig from '@/views/website/website/nginx/index.vue';
|
import NginxConfig from '@/views/website/website/nginx/index.vue';
|
||||||
import GroupDialog from '@/components/group/index.vue';
|
import GroupDialog from '@/components/agent-group/index.vue';
|
||||||
import AppStatus from '@/components/app-status/index.vue';
|
import AppStatus from '@/components/app-status/index.vue';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import router from '@/routers';
|
import router from '@/routers';
|
||||||
|
@ -257,7 +257,7 @@ import { ElMessageBox } from 'element-plus';
|
||||||
import { dateFormatSimple } from '@/utils/util';
|
import { dateFormatSimple } from '@/utils/util';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { getGroupList } from '@/api/modules/group';
|
import { getAgentGroupList } from '@/api/modules/group';
|
||||||
import { Group } from '@/api/interface/group';
|
import { Group } from '@/api/interface/group';
|
||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
|
@ -359,7 +359,7 @@ const search = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const listGroup = async () => {
|
const listGroup = async () => {
|
||||||
const res = await getGroupList('website');
|
const res = await getAgentGroupList('website');
|
||||||
groups.value = res.data;
|
groups.value = res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue