feat: 增加分组、快速命令等接口

This commit is contained in:
ssongliu 2022-08-30 18:49:07 +08:00 committed by ssongliu
parent c5b2eddd3f
commit 89432bc1b8
44 changed files with 1150 additions and 389 deletions

View File

@ -37,6 +37,7 @@ sqlite:
log:
level: info
time_zone: Asia/Shanghai
path: /opt/1Panel/log
log_name: 1Panel
log_suffix: .log

View File

@ -0,0 +1,98 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateCommand(c *gin.Context) {
var req dto.CommandCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := commandService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) SearchCommand(c *gin.Context) {
var req dto.SearchWithPage
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := commandService.SearchWithPage(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
func (b *BaseApi) ListCommand(c *gin.Context) {
list, err := commandService.Search()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
func (b *BaseApi) DeleteCommand(c *gin.Context) {
var req dto.DeleteByName
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := commandService.Delete(req.Name); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateCommand(c *gin.Context) {
var req dto.CommandUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
id, err := helper.GetParamID(c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name
if err := commandService.Update(id, upMap); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -11,5 +11,7 @@ var ApiGroupApp = new(ApiGroup)
var (
userService = service.ServiceGroupApp.UserService
hostService = service.ServiceGroupApp.HostService
groupService = service.ServiceGroupApp.GroupService
commandService = service.ServiceGroupApp.CommandService
operationService = service.ServiceGroupApp.OperationService
)

View File

@ -0,0 +1,79 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateGroup(c *gin.Context) {
var req dto.GroupCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := groupService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) DeleteGroup(c *gin.Context) {
var req dto.DeleteByName
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := groupService.Delete(req.Name); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateGroup(c *gin.Context) {
var req dto.GroupUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
id, err := helper.GetParamID(c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name
if err := groupService.Update(id, upMap); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) ListGroup(c *gin.Context) {
list, err := groupService.Search()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}

View File

@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
)
func (b *BaseApi) Create(c *gin.Context) {
func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
@ -26,23 +26,20 @@ func (b *BaseApi) Create(c *gin.Context) {
helper.SuccessWithData(c, host)
}
func (b *BaseApi) PageHosts(c *gin.Context) {
var req dto.SearchWithPage
func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := hostService.Search(req)
data, err := hostService.SearchForTree(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
helper.SuccessWithData(c, data)
}
func (b *BaseApi) DeleteHost(c *gin.Context) {
@ -81,6 +78,7 @@ func (b *BaseApi) UpdateHost(c *gin.Context) {
upMap := make(map[string]interface{})
upMap["name"] = req.Name
upMap["group"] = req.Group
upMap["addr"] = req.Addr
upMap["port"] = req.Port
upMap["user"] = req.User

View File

@ -68,7 +68,7 @@ func (b *BaseApi) Register(c *gin.Context) {
}
func (b *BaseApi) PageUsers(c *gin.Context) {
var req dto.UserPage
var req dto.SearchWithPage
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return

View File

@ -0,0 +1,15 @@
package dto
type CommandCreate struct {
Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"`
}
type CommandUpdate struct {
Name string `json:"name" validate:"required"`
}
type CommandInfo struct {
Name string `json:"name"`
Command string `json:"command"`
}

View File

@ -1,5 +1,10 @@
package dto
type SearchWithPage struct {
PageInfo
Name string `json:"name" validate:"required"`
}
type PageInfo struct {
Page int `json:"page" validate:"required,number"`
PageSize int `json:"pageSize" validate:"required,number"`
@ -13,6 +18,10 @@ type BatchDeleteReq struct {
Ids []uint `json:"ids" validate:"required"`
}
type DeleteByName struct {
Name string `json:"name" validate:"required"`
}
type OperationWithNameAndType struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`

10
backend/app/dto/group.go Normal file
View File

@ -0,0 +1,10 @@
package dto
type GroupCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
}
type GroupUpdate struct {
Name string `json:"name" validate:"required"`
}

View File

@ -1,8 +1,11 @@
package dto
import "time"
import (
"time"
)
type HostCreate struct {
Group string `json:"group" validate:"required"`
Name string `json:"name" validate:"required"`
Addr string `json:"addr" validate:"required,ip"`
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
@ -14,14 +17,14 @@ type HostCreate struct {
Description string `json:"description"`
}
type SearchWithPage struct {
PageInfo
type SearchForTree struct {
Info string `json:"info"`
}
type HostInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Group string `json:"group"`
Name string `json:"name"`
Addr string `json:"addr"`
Port uint `json:"port"`
@ -31,7 +34,17 @@ type HostInfo struct {
Description string `json:"description"`
}
type HostTree struct {
Label string `json:"label"`
Children []TreeChild `json:"children"`
}
type TreeChild struct {
Label string `json:"label"`
}
type HostUpdate struct {
Group string `json:"group" validate:"required"`
Name string `json:"name" validate:"required"`
Addr string `json:"addr" validate:"required,ip"`
Port uint `json:"port" validate:"required,number,max=65535,min=1"`

View File

@ -10,11 +10,6 @@ type UserCreate struct {
Email string `json:"email" validate:"required,email"`
}
type UserPage struct {
PageInfo
Name string `json:"name" validate:"required"`
}
type CaptchaResponse struct {
CaptchaID string `json:"captchaID"`
ImagePath string `json:"imagePath"`

View File

@ -0,0 +1,9 @@
package model
import "gorm.io/gorm"
type Command struct {
gorm.Model
Name string `gorm:"type:varchar(64));unique;not null" json:"name"`
Command string `gorm:"type:varchar(256);unique;not null" json:"command"`
}

View File

@ -0,0 +1,9 @@
package model
import "gorm.io/gorm"
type Group struct {
gorm.Model
Name string `gorm:"type:varchar(64);not null" json:"name"`
Type string `gorm:"type:varchar(16);unique;not null" json:"type"`
}

View File

@ -4,6 +4,7 @@ import "gorm.io/gorm"
type Host struct {
gorm.Model
Group string `gorm:"type:varchar(64);not null" json:"group"`
Name string `gorm:"type:varchar(64);unique;not null" json:"name"`
Addr string `gorm:"type:varchar(16);unique;not null" json:"addr"`
Port int `gorm:"type:varchar(5);not null" json:"port"`

View File

@ -0,0 +1,80 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
"gorm.io/gorm"
)
type CommandRepo struct{}
type ICommandRepo interface {
GetList(opts ...DBOption) ([]model.Command, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Command, error)
WithByInfo(info string) DBOption
Create(command *model.Command) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
}
func NewICommandService() ICommandRepo {
return &CommandRepo{}
}
func (u *CommandRepo) Get(opts ...DBOption) (model.Command, error) {
var command model.Command
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&command).Error
return command, err
}
func (u *CommandRepo) Page(page, size int, opts ...DBOption) (int64, []model.Command, error) {
var users []model.Command
db := global.DB.Model(&model.Command{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (u *CommandRepo) GetList(opts ...DBOption) ([]model.Command, error) {
var commands []model.Command
db := global.DB.Model(&model.Command{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&commands).Error
return commands, err
}
func (c *CommandRepo) WithByInfo(info string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(info) == 0 {
return g
}
infoStr := "%" + info + "%"
return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr)
}
}
func (u *CommandRepo) Create(command *model.Command) error {
return global.DB.Create(command).Error
}
func (u *CommandRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Command{}).Where("id = ?", id).Updates(vars).Error
}
func (u *CommandRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Command{}).Error
}

View File

@ -3,6 +3,8 @@ package repo
type RepoGroup struct {
UserRepo
HostRepo
GroupRepo
CommandRepo
OperationRepo
CommonRepo
}

56
backend/app/repo/group.go Normal file
View File

@ -0,0 +1,56 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/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 NewIGroupService() IGroupRepo {
return &GroupRepo{}
}
func (u *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 (u *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 (u *GroupRepo) Create(group *model.Group) error {
return global.DB.Create(group).Error
}
func (u *GroupRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Group{}).Where("id = ?", id).Updates(vars).Error
}
func (u *GroupRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Group{}).Error
}

View File

@ -10,7 +10,7 @@ type HostRepo struct{}
type IHostRepo interface {
Get(opts ...DBOption) (model.Host, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error)
GetList(opts ...DBOption) ([]model.Host, error)
WithByInfo(info string) DBOption
Create(host *model.Host) error
Update(id uint, vars map[string]interface{}) error
@ -31,20 +31,21 @@ func (u *HostRepo) Get(opts ...DBOption) (model.Host, error) {
return host, err
}
func (u *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) {
func (u *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) {
var hosts []model.Host
db := global.DB.Model(&model.Host{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&hosts).Error
return count, hosts, err
err := db.Find(&hosts).Error
return hosts, err
}
func (c *HostRepo) WithByInfo(info string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(info) == 0 {
return g
}
infoStr := "%" + info + "%"
return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr)
}

View File

@ -0,0 +1,70 @@
package service
import (
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type CommandService struct{}
type ICommandService interface {
Search() ([]model.Command, error)
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Create(commandDto dto.CommandCreate) error
Update(id uint, upMap map[string]interface{}) error
Delete(name string) error
}
func NewICommandService() ICommandService {
return &CommandService{}
}
func (u *CommandService) Search() ([]model.Command, error) {
commands, err := commandRepo.GetList()
if err != nil {
return nil, constant.ErrRecordNotFound
}
return commands, err
}
func (u *CommandService) SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) {
total, commands, err := commandRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Name))
var dtoCommands []dto.CommandInfo
for _, command := range commands {
var item dto.CommandInfo
if err := copier.Copy(&item, &command); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtoCommands = append(dtoCommands, item)
}
return total, dtoCommands, err
}
func (u *CommandService) Create(commandDto dto.CommandCreate) error {
command, _ := commandRepo.Get(commonRepo.WithByName(commandDto.Name))
if command.ID != 0 {
return constant.ErrRecordExist
}
if err := copier.Copy(&command, &commandDto); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := commandRepo.Create(&command); err != nil {
return err
}
return nil
}
func (u *CommandService) Delete(name string) error {
command, _ := commandRepo.Get(commonRepo.WithByName(name))
if command.ID == 0 {
return constant.ErrRecordNotFound
}
return commandRepo.Delete(commonRepo.WithByID(command.ID))
}
func (u *CommandService) Update(id uint, upMap map[string]interface{}) error {
return commandRepo.Update(id, upMap)
}

View File

@ -5,6 +5,8 @@ import "github.com/1Panel-dev/1Panel/app/repo"
type ServiceGroup struct {
UserService
HostService
GroupService
CommandService
OperationService
}
@ -13,6 +15,8 @@ var ServiceGroupApp = new(ServiceGroup)
var (
userRepo = repo.RepoGroupApp.UserRepo
hostRepo = repo.RepoGroupApp.HostRepo
groupRepo = repo.RepoGroupApp.GroupRepo
commandRepo = repo.RepoGroupApp.CommandRepo
operationRepo = repo.RepoGroupApp.OperationRepo
commonRepo = repo.RepoGroupApp.CommonRepo
)

View File

@ -0,0 +1,56 @@
package service
import (
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type GroupService struct{}
type IGroupService interface {
Search() ([]model.Group, error)
Create(groupDto dto.GroupCreate) error
Update(id uint, upMap map[string]interface{}) error
Delete(name string) error
}
func NewIGroupService() IGroupService {
return &GroupService{}
}
func (u *GroupService) Search() ([]model.Group, error) {
groups, err := groupRepo.GetList()
if err != nil {
return nil, constant.ErrRecordNotFound
}
return groups, err
}
func (u *GroupService) Create(groupDto dto.GroupCreate) error {
group, _ := groupRepo.Get(commonRepo.WithByName(groupDto.Name), commonRepo.WithByName(groupDto.Name))
if group.ID != 0 {
return constant.ErrRecordExist
}
if err := copier.Copy(&group, &groupDto); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := groupRepo.Create(&group); err != nil {
return err
}
return nil
}
func (u *GroupService) Delete(name string) error {
group, _ := groupRepo.Get(commonRepo.WithByName(name))
if group.ID == 0 {
return constant.ErrRecordNotFound
}
return groupRepo.Delete(commonRepo.WithByID(group.ID))
}
func (u *GroupService) Update(id uint, upMap map[string]interface{}) error {
return groupRepo.Update(id, upMap)
}

View File

@ -1,6 +1,8 @@
package service
import (
"fmt"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
@ -12,7 +14,7 @@ type HostService struct{}
type IHostService interface {
GetConnInfo(id uint) (*model.Host, error)
Search(search dto.SearchWithPage) (int64, interface{}, error)
SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error)
Create(hostDto dto.HostCreate) (*dto.HostInfo, error)
Update(id uint, upMap map[string]interface{}) error
BatchDelete(ids []uint) error
@ -30,17 +32,25 @@ func (u *HostService) GetConnInfo(id uint) (*model.Host, error) {
return &host, err
}
func (u *HostService) Search(search dto.SearchWithPage) (int64, interface{}, error) {
total, hosts, err := hostRepo.Page(search.Page, search.PageSize, hostRepo.WithByInfo(search.Info))
var dtoHosts []dto.HostInfo
func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error) {
hosts, err := hostRepo.GetList(hostRepo.WithByInfo(search.Info))
distinctMap := make(map[string][]string)
for _, host := range hosts {
var item dto.HostInfo
if err := copier.Copy(&item, &host); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
if _, ok := distinctMap[host.Group]; !ok {
distinctMap[host.Group] = []string{fmt.Sprintf("%s@%s:%d", host.User, host.Addr, host.Port)}
} else {
distinctMap[host.Group] = append(distinctMap[host.Group], fmt.Sprintf("%s@%s:%d", host.User, host.Addr, host.Port))
}
dtoHosts = append(dtoHosts, item)
}
return total, dtoHosts, err
var data []dto.HostTree
for key, value := range distinctMap {
var children []dto.TreeChild
for _, label := range value {
children = append(children, dto.TreeChild{Label: label})
}
data = append(data, dto.HostTree{Label: key, Children: children})
}
return data, err
}
func (u *HostService) Create(hostDto dto.HostCreate) (*dto.HostInfo, error) {

View File

@ -5,7 +5,6 @@ import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/init/session/psession"
"github.com/1Panel-dev/1Panel/utils/encrypt"
"github.com/1Panel-dev/1Panel/utils/jwt"
"github.com/gin-gonic/gin"
@ -18,7 +17,7 @@ type UserService struct{}
type IUserService interface {
Get(id uint) (*dto.UserInfo, error)
Page(search dto.UserPage) (int64, interface{}, error)
Page(search dto.SearchWithPage) (int64, interface{}, error)
Register(userDto dto.UserCreate) error
Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error)
LogOut(c *gin.Context) error
@ -44,7 +43,7 @@ func (u *UserService) Get(id uint) (*dto.UserInfo, error) {
return &dtoUser, err
}
func (u *UserService) Page(search dto.UserPage) (int64, interface{}, error) {
func (u *UserService) Page(search dto.SearchWithPage) (int64, interface{}, error) {
total, users, err := userRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Name))
var dtoUsers []dto.UserInfo
for _, user := range users {
@ -92,13 +91,9 @@ func (u *UserService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo,
}
return &dto.UserLoginInfo{Name: user.Name, Token: token}, err
}
sessionUser := psession.SessionUser{
ID: user.ID,
Name: user.Name,
}
lifeTime := global.CONF.Session.ExpiresTime
sID, _ := c.Cookie(global.CONF.Session.SessionName)
sessionUser, err = global.SESSION.Get(sID)
sessionUser, err := global.SESSION.Get(sID)
if err != nil {
sID = uuid.NewV4().String()
c.SetCookie(global.CONF.Session.SessionName, sID, lifeTime, "", "", false, false)

Binary file not shown.

View File

@ -2,6 +2,7 @@ package configs
type LogConfig struct {
Level string `mapstructure:"level"`
TimeZone string `mapstructure:"timeZone"`
Path string `mapstructure:"path"`
LogName string `mapstructure:"log_name"`
LogSuffix string `mapstructure:"log_suffix"`

View File

@ -1,7 +1,10 @@
package log
import (
"fmt"
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/configs"
"github.com/1Panel-dev/1Panel/global"
@ -20,10 +23,10 @@ func setOutput(log *logrus.Logger, config configs.LogConfig) {
filePath := path.Join(config.Path, config.LogName+config.LogSuffix)
logPrint := &lumberjack.Logger{
Filename: filePath,
MaxSize: config.LogSize, // 日志文件大小,单位是 MB
MaxBackups: config.LogBackup, // 最大过期日志保留个数
MaxAge: config.LogData, // 保留过期文件最大时间,单位 天
Compress: true, // 是否压缩日志默认是不压缩。这里设置为true压缩日志
MaxSize: config.LogSize,
MaxBackups: config.LogBackup,
MaxAge: config.LogData,
Compress: true,
}
level, err := logrus.ParseLevel(config.Level)
if err != nil {
@ -31,4 +34,24 @@ func setOutput(log *logrus.Logger, config configs.LogConfig) {
}
log.SetOutput(logPrint)
log.SetLevel(level)
log.SetFormatter(new(MineFormatter))
}
type MineFormatter struct{}
const TimeFormat = "2006-01-02 15:04:05"
func (s *MineFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var cstSh, _ = time.LoadLocation(global.CONF.LogConfig.TimeZone)
detailInfo := ""
if entry.Caller != nil {
funcion := strings.ReplaceAll(entry.Caller.Function, "github.com/1Panel-dev/1Panel/", "")
detailInfo = fmt.Sprintf("(%s: %d)", funcion, entry.Caller.Line)
}
if len(entry.Data) == 0 {
msg := fmt.Sprintf("[%s] [%s] %s %s \n", time.Now().In(cstSh).Format(TimeFormat), strings.ToUpper(entry.Level.String()), entry.Message, detailInfo)
return []byte(msg), nil
}
msg := fmt.Sprintf("[%s] [%s] %s %s {%v} \n", time.Now().In(cstSh).Format(TimeFormat), strings.ToUpper(entry.Level.String()), entry.Message, detailInfo, entry.Data)
return []byte(msg), nil
}

View File

@ -18,5 +18,5 @@ func Init() {
global.LOG.Error(err)
panic(err)
}
global.LOG.Infof("Migration did run successfully")
global.LOG.Info("Migration did run successfully")
}

24
backend/router/command.go Normal file
View File

@ -0,0 +1,24 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
"github.com/1Panel-dev/1Panel/middleware"
"github.com/gin-gonic/gin"
)
type CommandRouter struct{}
func (s *CommandRouter) InitCommandRouter(Router *gin.RouterGroup) {
userRouter := Router.Group("commands")
userRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth())
withRecordRouter := userRouter.Use(middleware.OperationRecord())
baseApi := v1.ApiGroupApp.BaseApi
{
withRecordRouter.POST("", baseApi.CreateCommand)
withRecordRouter.POST("/del", baseApi.DeleteCommand)
userRouter.POST("/search", baseApi.SearchCommand)
userRouter.GET("", baseApi.ListCommand)
userRouter.PUT(":id", baseApi.UpdateCommand)
}
}

23
backend/router/group.go Normal file
View File

@ -0,0 +1,23 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
"github.com/1Panel-dev/1Panel/middleware"
"github.com/gin-gonic/gin"
)
type GroupRouter struct{}
func (s *GroupRouter) InitGroupRouter(Router *gin.RouterGroup) {
userRouter := Router.Group("group")
userRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth())
withRecordRouter := userRouter.Use(middleware.OperationRecord())
baseApi := v1.ApiGroupApp.BaseApi
{
withRecordRouter.POST("", baseApi.CreateGroup)
withRecordRouter.POST("/del", baseApi.DeleteGroup)
userRouter.GET("", baseApi.ListGroup)
userRouter.PUT(":id", baseApi.UpdateGroup)
}
}

View File

@ -15,9 +15,9 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) {
withRecordRouter := userRouter.Use(middleware.OperationRecord())
baseApi := v1.ApiGroupApp.BaseApi
{
withRecordRouter.POST("", baseApi.Create)
withRecordRouter.POST("", baseApi.CreateHost)
withRecordRouter.POST("/del", baseApi.DeleteHost)
userRouter.POST("/search", baseApi.PageHosts)
userRouter.POST("/search", baseApi.HostTree)
userRouter.PUT(":id", baseApi.UpdateHost)
}
}

View File

@ -80,9 +80,7 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
return
}
msgObj := wsMsg{}
if err := json.Unmarshal(wsData, &msgObj); err != nil {
global.LOG.Errorf("unmarshal websocket message %s failed, err: %v", wsData, err)
}
_ = json.Unmarshal(wsData, &msgObj)
switch msgObj.Type {
case wsMsgResize:
if msgObj.Cols > 0 && msgObj.Rows > 0 {

View File

@ -54,7 +54,7 @@ type LogicSshWsSession struct {
session *ssh.Session
wsConn *websocket.Conn
isAdmin bool
IsFlagged bool `comment:"当前session是否包含禁止命令"`
IsFlagged bool
}
func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
@ -127,9 +127,7 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
return
}
msgObj := wsMsg{}
if err := json.Unmarshal(wsData, &msgObj); err != nil {
global.LOG.Errorf("unmarshal websocket message %s failed, err: %v", wsData, err)
}
_ = json.Unmarshal(wsData, &msgObj)
switch msgObj.Type {
case wsMsgResize:
if msgObj.Cols > 0 && msgObj.Rows > 0 {

View File

@ -1,6 +1,10 @@
import { CommonModel, ReqPage } from '.';
import { CommonModel } from '.';
export namespace Host {
export interface HostTree {
label: string;
children: Array<string>;
}
export interface Host extends CommonModel {
name: string;
addr: string;
@ -21,7 +25,7 @@ export namespace Host {
description: string;
}
export interface ReqSearchWithPage extends ReqPage {
export interface ReqSearch {
info?: string;
}
}

View File

@ -1,9 +1,8 @@
import http from '@/api';
import { ResPage } from '../interface';
import { Host } from '../interface/host';
export const getHostList = (params: Host.ReqSearchWithPage) => {
return http.post<ResPage<Host.Host>>(`/hosts/search`, params);
export const getHostList = (params: Host.ReqSearch) => {
return http.post<Array<Host.HostTree>>(`/hosts/search`, params);
};
export const addHost = (params: Host.HostOperate) => {

View File

@ -5,7 +5,3 @@ import { ResOperationLog } from '../interface/operation-log';
export const getOperationList = (info: ReqPage) => {
return http.post<ResPage<ResOperationLog>>(`/operations`, info);
};
export const deleteOperation = (params: { ids: number[] }) => {
return http.post(`/operations/del`, params);
};

View File

@ -91,8 +91,9 @@ export default {
logout: '退出登录',
},
terminal: {
connHistory: '历史连接',
hostHistory: '历史主机信息',
conn: '连接',
hostList: '主机信息',
quickCmd: '快捷命令',
addHost: '添加主机',
localhost: '本地服务器',
name: '名称',

View File

@ -15,9 +15,6 @@ const demoRouter = {
path: '/demos/table',
name: 'Table',
component: () => import('@/views/demos/table/index.vue'),
meta: {
keepAlive: true,
},
},
{
path: '/demos/table/:op/:id?',
@ -27,7 +24,6 @@ const demoRouter = {
component: () => import('@/views/demos/table/operate/index.vue'),
meta: {
activeMenu: '/demos/table',
keepAlive: true,
},
},
],

View File

@ -15,7 +15,6 @@ const operationRouter = {
name: 'OperationLog',
component: () => import('@/views/operation-log/index.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
key: 'OperationLog',
},

View File

@ -1,4 +1,5 @@
import { Layout } from '@/routers/constant';
import i18n from '@/lang';
const terminalRouter = {
sort: 2,
@ -6,18 +7,44 @@ const terminalRouter = {
component: Layout,
redirect: '/terminal',
meta: {
title: 'menu.terminal',
title: i18n.global.t('menu.terminal'),
icon: 'monitor',
},
children: [
{
path: '/terminal',
path: '/terminals/terminal',
name: 'Terminal',
component: () => import('@/views/terminal/index.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
key: 'Terminal',
title: i18n.global.t('terminal.conn'),
icon: 'connection',
activeMenu: '/terminals',
},
},
{
path: '/terminals/host',
name: 'Host',
component: () => import('@/views/terminal/host/index.vue'),
meta: {
requiresAuth: true,
key: 'Host',
title: i18n.global.t('terminal.hostList'),
icon: 'platform',
activeMenu: '/terminals',
},
},
{
path: '/terminals/command',
name: 'Command',
component: () => import('@/views/terminal/command/index.vue'),
meta: {
requiresAuth: true,
key: 'Command',
title: i18n.global.t('terminal.quickCmd'),
icon: 'reading',
activeMenu: '/terminals',
},
},
],

View File

@ -178,3 +178,15 @@
}
border-radius: 5px;
}
.row-box {
display: flex;
flex-flow: wrap;
}
.row-box .el-card {
min-width: 100%;
height: 100%;
margin-right: 20px;
border: 0;
// box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}

View File

@ -1,80 +1,74 @@
<template>
<LayoutContent :header="$t('menu.operations')">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<el-table-column type="selection" fix />
<el-table-column :label="$t('operations.operatoin')" fix>
<template #default="{ row }">
{{ fmtOperation(row) }}
</template>
</el-table-column>
<el-table-column :label="$t('operations.status')" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status == '200'" class="ml-2" type="success">{{ row.status }}</el-tag>
<div v-else>
<el-popover
placement="top-start"
:title="$t('commons.table.message')"
:width="400"
trigger="hover"
:content="row.errorMessage"
>
<template #reference>
<el-tag class="ml-2" type="warning">{{ row.status }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column label="IP" prop="ip" />
<el-table-column align="left" :label="$t('operations.request')" prop="path">
<template #default="{ row }">
<div>
<el-popover :width="500" v-if="row.body" placement="left-start" trigger="click">
<div style="word-wrap: break-word; font-size: 12px; white-space: normal">
<pre class="pre">{{ fmtBody(row.body) }}</pre>
</div>
<template #reference>
<el-icon style="cursor: pointer"><warning /></el-icon>
</template>
</el-popover>
<span v-else></span>
</div>
</template>
</el-table-column>
<el-table-column align="left" :label="$t('operations.response')" prop="path">
<template #default="{ row }">
<div>
<el-popover :width="500" v-if="row.resp" placement="left-start" trigger="click">
<div style="word-wrap: break-word; font-size: 12px; white-space: normal">
<pre class="pre">{{ fmtBody(row.resp) }}</pre>
</div>
<template #reference>
<el-icon style="cursor: pointer"><warning /></el-icon>
</template>
</el-popover>
<span v-else></span>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</LayoutContent>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column :label="$t('operations.operatoin')" fix>
<template #default="{ row }">
{{ fmtOperation(row) }}
</template>
</el-table-column>
<el-table-column :label="$t('operations.status')" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status == '200'" class="ml-2" type="success">{{ row.status }}</el-tag>
<div v-else>
<el-popover
placement="top-start"
:title="$t('commons.table.message')"
:width="400"
trigger="hover"
:content="row.errorMessage"
>
<template #reference>
<el-tag class="ml-2" type="warning">{{ row.status }}</el-tag>
</template>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column label="IP" prop="ip" />
<el-table-column align="left" :label="$t('operations.request')" prop="path">
<template #default="{ row }">
<div>
<el-popover :width="500" v-if="row.body" placement="left-start" trigger="click">
<div style="word-wrap: break-word; font-size: 12px; white-space: normal">
<pre class="pre">{{ fmtBody(row.body) }}</pre>
</div>
<template #reference>
<el-icon style="cursor: pointer"><warning /></el-icon>
</template>
</el-popover>
<span v-else></span>
</div>
</template>
</el-table-column>
<el-table-column align="left" :label="$t('operations.response')" prop="path">
<template #default="{ row }">
<div>
<el-popover :width="500" v-if="row.resp" placement="left-start" trigger="click">
<div style="word-wrap: break-word; font-size: 12px; white-space: normal">
<pre class="pre">{{ fmtBody(row.resp) }}</pre>
</div>
<template #reference>
<el-icon style="cursor: pointer"><warning /></el-icon>
</template>
</el-popover>
<span v-else></span>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
</ComplexTable>
</template>
<script setup lang="ts">
import LayoutContent from '@/layout/layout-content.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import { dateFromat } from '@/utils/util';
import { getOperationList, deleteOperation } from '@/api/modules/operation-log';
import { getOperationList } from '@/api/modules/operation-log';
import { onMounted, reactive, ref } from '@vue/runtime-core';
import { ResOperationLog } from '@/api/interface/operation-log';
import { useDeleteData } from '@/hooks/use-delete-data';
import i18n from '@/lang';
const data = ref();
@ -89,29 +83,6 @@ const logSearch = reactive({
pageSize: 5,
});
const selects = ref<any>([]);
const batchDelete = async (row: ResOperationLog | null) => {
let ids: Array<number> = [];
if (row === null) {
selects.value.forEach((item: ResOperationLog) => {
ids.push(item.id);
});
} else {
ids.push(row.id);
}
await useDeleteData(deleteOperation, { ids: ids }, 'commons.msg.delete');
search();
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
type: 'danger',
click: batchDelete,
},
];
const search = async () => {
logSearch.page = paginationConfig.currentPage;
logSearch.pageSize = paginationConfig.pageSize;

View File

@ -0,0 +1,144 @@
<template>
<el-row style="margin: 20px; margin-left: 20px" class="row-box" :gutter="20">
<el-col :span="8">
<el-card class="el-card">
<el-button icon="Plus" @click="readyForCreate" size="small" />
<el-button icon="FolderAdd" @click="(folderCreate = true), (newGroupName = '')" size="small" />
<el-button icon="Expand" @click="setTreeStatus(true)" size="small" />
<el-button icon="Fold" @click="setTreeStatus(false)" size="small" />
<el-input size="small" @input="loadHost" clearable style="margin-top: 5px" v-model="searcConfig.info">
<template #append><el-button icon="search" @click="loadHost" /></template>
</el-input>
<el-input size="small" v-if="folderCreate" clearable style="margin-top: 5px" v-model="newGroupName">
<template #append>
<el-button-group>
<el-button icon="Check" @click="loadHost" />
<el-button icon="Close" @click="folderCreate = false" />
</el-button-group>
</template>
</el-input>
<el-tree ref="tree" :default-expand-all="true" :data="hostTree" :props="defaultProps" />
</el-card>
</el-col>
<el-col :span="16">
<el-card class="el-card">
<el-form ref="hostInfoRef" label-width="100px" label-position="left" :model="hostInfo" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model="hostInfo.name" />
</el-form-item>
<el-form-item label="IP" prop="addr">
<el-input clearable v-model="hostInfo.addr" />
</el-form-item>
<el-form-item :label="$t('terminal.port')" prop="port">
<el-input clearable v-model.number="hostInfo.port" />
</el-form-item>
<el-form-item :label="$t('terminal.user')" prop="user">
<el-input clearable v-model="hostInfo.user" />
</el-form-item>
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
<el-radio-group v-model="hostInfo.authMode">
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="$t('terminal.password')"
v-if="hostInfo.authMode === 'password'"
prop="password"
>
<el-input clearable show-password type="password" v-model="hostInfo.password" />
</el-form-item>
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
<el-input clearable type="textarea" v-model="hostInfo.privateKey" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitAddHost(hostInfoRef)">
{{ $t('commons.button.create') }}
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import type { ElForm } from 'element-plus';
import { Rules } from '@/global/form-rues';
import { Host } from '@/api/interface/host';
import { getHostList, addHost } from '@/api/modules/host';
import { ElMessage } from 'element-plus';
import i18n from '@/lang';
type FormInstance = InstanceType<typeof ElForm>;
const hostInfoRef = ref<FormInstance>();
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
addr: [Rules.requiredInput, Rules.ip],
port: [Rules.requiredInput, Rules.port],
user: [Rules.requiredInput],
authMode: [Rules.requiredSelect],
password: [Rules.requiredInput],
privateKey: [Rules.requiredInput],
});
let hostInfo = reactive<Host.HostOperate>({
id: 0,
name: '',
addr: '',
port: 22,
user: '',
authMode: 'password',
password: '',
privateKey: '',
description: '',
});
let searcConfig = reactive<Host.ReqSearch>({
info: '',
});
const tree = ref<any>(null);
const hostTree = ref<Array<Host.HostTree>>();
const defaultProps = {
children: 'children',
label: 'label',
};
const newGroupName = ref();
const folderCreate = ref<boolean>(false);
const loadHost = async () => {
const res = await getHostList(searcConfig);
hostTree.value = res.data;
};
function setTreeStatus(expend: boolean) {
for (let i = 0; i < tree.value.store._getAllNodes().length; i++) {
tree.value.store._getAllNodes()[i].expanded = expend;
}
}
function readyForCreate() {
if (hostInfoRef.value) {
hostInfoRef.value.resetFields();
}
}
const submitAddHost = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
try {
await addHost(hostInfo);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
// loadHost();
} catch (error) {
ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
}
});
};
onMounted(() => {
loadHost();
});
</script>

View File

@ -1,173 +1,189 @@
<template>
<LayoutContent :header="$t('menu.terminal')">
<el-button class="drawer-container" icon="arrowLeftBold" @click="hostDrawer = true">
{{ $t('terminal.connHistory') }}
</el-button>
<!-- <el-button class="drawer-container" icon="arrowLeftBold" @click="hostDrawer = true">
{{ $t('terminal.connHistory') }}
</el-button> -->
<div>
<el-tabs
type="card"
editable
style="background-color: #efefef"
v-model="terminalValue"
@edit="handleTabsEdit"
>
<el-tab-pane :key="item.key" v-for="item in terminalTabs" :label="item.title" :name="item.key">
<template #label>
<span class="custom-tabs-label">
<el-icon color="#67C23A" v-if="item.status === 'online'"><circleCheck /></el-icon>
<el-icon color="#F56C6C" v-if="item.status === 'closed'"><circleClose /></el-icon>
<span> &nbsp;{{ item.title }}&nbsp;&nbsp;</span>
</span>
</template>
<Terminal
style="height: calc(100vh - 265px); background-color: #000"
:ref="'Ref' + item.key"
:wsID="item.wsID"
:terminalID="item.key"
></Terminal>
</el-tab-pane>
<div v-if="terminalTabs.length === 0">
<el-empty
style="background-color: #000; height: calc(100vh - 265px)"
:description="$t('terminal.emptyTerminal')"
></el-empty>
</div>
</el-tabs>
</div>
<el-drawer :size="320" v-model="hostDrawer" :title="$t('terminal.hostHistory')" direction="rtl">
<el-input size="small" clearable style="margin-top: 5px" v-model="searcConfig.info">
<template #prepend>
<el-button icon="plus" @click="onAddHost">{{ $t('commons.button.add') }}</el-button>
<div>
<el-tabs
type="card"
editable
class="terminal-tabs"
style="background-color: #efefef"
v-model="terminalValue"
@edit="handleTabsEdit"
>
<el-tab-pane :key="item.key" v-for="item in terminalTabs" :label="item.title" :name="item.key">
<template #label>
<span class="custom-tabs-label">
<el-icon color="#67C23A" v-if="item.status === 'online'"><circleCheck /></el-icon>
<el-icon color="#F56C6C" v-if="item.status === 'closed'"><circleClose /></el-icon>
<span> &nbsp;{{ item.title }}&nbsp;&nbsp;</span>
</span>
</template>
<template #append><el-button icon="search" @click="loadHost" /></template>
</el-input>
<div v-infinite-scroll="nextPage" style="overflow: auto">
<el-card
@click="onConnLocal()"
style="margin-top: 5px; cursor: pointer"
:title="$t('terminal.localhost')"
shadow="hover"
>
<Terminal
style="height: calc(100vh - 210px); background-color: #000"
:ref="'Ref' + item.key"
:wsID="item.wsID"
:terminalID="item.key"
></Terminal>
<div style="background-color: #000">
<el-select v-model="quickCmd" style="width: 25%" class="m-2" placeholder="Select" size="small">
<el-option v-for="cmd in quickCmds" :key="cmd.value" :label="cmd.label" :value="cmd.value" />
</el-select>
<el-input v-model="batchInput" class="terminal-input" size="small">
<template #append>
<el-switch size="small" v-model="isBatch" class="ml-2" />
</template>
</el-input>
</div>
</el-tab-pane>
<div v-if="terminalTabs.length === 0">
<el-empty
style="background-color: #000; height: calc(100vh - 210px)"
:description="$t('terminal.emptyTerminal')"
></el-empty>
</div>
</el-tabs>
</div>
<!-- <el-drawer :size="320" v-model="hostDrawer" :title="$t('terminal.hostHistory')" direction="rtl">
<el-input size="small" clearable style="margin-top: 5px" v-model="searcConfig.info">
<template #prepend>
<el-button icon="plus" @click="onAddHost">{{ $t('commons.button.add') }}</el-button>
</template>
<template #append><el-button icon="search" @click="loadHost" /></template>
</el-input>
<div v-infinite-scroll="nextPage" style="overflow: auto">
<el-card
@click="onConnLocal()"
style="margin-top: 5px; cursor: pointer"
:title="$t('terminal.localhost')"
shadow="hover"
>
<div :inline="true">
<div>
<span>{{ $t('terminal.localhost') }}</span>
</div>
<span style="font-size: 14px; line-height: 25px"> [ 127.0.0.1 ]</span>
</div>
</el-card>
<div v-for="(item, index) in data" :key="item.id" @mouseover="hover = index" @mouseleave="hover = null">
<el-card @click="onConn(item)" style="margin-top: 5px; cursor: pointer" shadow="hover">
<div :inline="true">
<div>
<span>{{ $t('terminal.localhost') }}</span>
<span>{{ item.name }}</span>
</div>
<span style="font-size: 14px; line-height: 25px"> [ 127.0.0.1 ]</span>
<span style="font-size: 14px; line-height: 25px">
[ {{ item.addr + ':' + item.port }} ]
<el-button
style="float: right; margin-left: 5px"
size="small"
circle
@click="onDeleteHost(item)"
v-if="hover === index"
icon="delete"
></el-button>
<el-button
style="float: right; margin-left: 5px"
size="small"
circle
@click="onEditHost(item)"
v-if="hover === index"
icon="edit"
></el-button>
<div v-if="item.description && hover === index">
<span style="font-size: 12px">{{ item.description }}</span>
</div>
</span>
</div>
</el-card>
<div v-for="(item, index) in data" :key="item.id" @mouseover="hover = index" @mouseleave="hover = null">
<el-card @click="onConn(item)" style="margin-top: 5px; cursor: pointer" shadow="hover">
<div :inline="true">
<div>
<span>{{ item.name }}</span>
</div>
<span style="font-size: 14px; line-height: 25px">
[ {{ item.addr + ':' + item.port }} ]
<el-button
style="float: right; margin-left: 5px"
size="small"
circle
@click="onDeleteHost(item)"
v-if="hover === index"
icon="delete"
></el-button>
<el-button
style="float: right; margin-left: 5px"
size="small"
circle
@click="onEditHost(item)"
v-if="hover === index"
icon="edit"
></el-button>
<div v-if="item.description && hover === index">
<span style="font-size: 12px">{{ item.description }}</span>
</div>
</span>
</div>
</el-card>
</div>
</div>
</el-drawer>
</div>
</el-drawer> -->
<el-dialog v-model="connVisiable" :title="$t('terminal.addHost')" width="30%">
<el-form ref="hostInfoRef" label-width="80px" :model="hostInfo" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model="hostInfo.name" style="width: 80%" />
</el-form-item>
<el-form-item label="IP" prop="addr">
<el-input clearable v-model="hostInfo.addr" style="width: 80%" />
</el-form-item>
<el-form-item :label="$t('terminal.port')" prop="port">
<el-input clearable v-model.number="hostInfo.port" style="width: 80%" />
</el-form-item>
<el-form-item :label="$t('terminal.user')" prop="user">
<el-input clearable v-model="hostInfo.user" style="width: 80%" />
</el-form-item>
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
<el-radio-group v-model="hostInfo.authMode">
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('terminal.password')" v-if="hostInfo.authMode === 'password'" prop="password">
<el-input clearable show-password type="password" v-model="hostInfo.password" style="width: 80%" />
</el-form-item>
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
<el-input clearable type="textarea" v-model="hostInfo.privateKey" style="width: 80%" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="connVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-if="operation === 'conn'" type="primary" @click="submitAddHost(hostInfoRef)">
{{ $t('commons.button.conn') }}
</el-button>
<el-button v-else type="primary" @click="submitAddHost(hostInfoRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</LayoutContent>
<el-dialog v-model="connVisiable" :title="$t('terminal.addHost')" width="30%">
<el-form ref="hostInfoRef" label-width="100px" label-position="left" :model="hostInfo" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model="hostInfo.name" />
</el-form-item>
<el-form-item label="IP" prop="addr">
<el-input clearable v-model="hostInfo.addr" />
</el-form-item>
<el-form-item :label="$t('terminal.port')" prop="port">
<el-input clearable v-model.number="hostInfo.port" />
</el-form-item>
<el-form-item :label="$t('terminal.user')" prop="user">
<el-input clearable v-model="hostInfo.user" />
</el-form-item>
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
<el-radio-group v-model="hostInfo.authMode">
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('terminal.password')" v-if="hostInfo.authMode === 'password'" prop="password">
<el-input clearable show-password type="password" v-model="hostInfo.password" />
</el-form-item>
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
<el-input clearable type="textarea" v-model="hostInfo.privateKey" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="connVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<!-- <el-button v-if="operation === 'conn'" type="primary" @click="submitAddHost(hostInfoRef)">
{{ $t('commons.button.conn') }}
</el-button>
<el-button v-else type="primary" @click="submitAddHost(hostInfoRef)">
{{ $t('commons.button.confirm') }}
</el-button> -->
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { onMounted, onBeforeMount, ref, nextTick, reactive, getCurrentInstance } from 'vue';
import { Rules } from '@/global/form-rues';
import { getHostList, addHost, editHost, deleteHost } from '@/api/modules/host';
import { useDeleteData } from '@/hooks/use-delete-data';
import LayoutContent from '@/layout/layout-content.vue';
import i18n from '@/lang';
// import { getHostList, addHost, editHost, deleteHost } from '@/api/modules/host';
// import { useDeleteData } from '@/hooks/use-delete-data';
// import i18n from '@/lang';
import type { ElForm } from 'element-plus';
import { Host } from '@/api/interface/host';
import { ElMessage } from 'element-plus';
import Terminal from '@/views/terminal/terminal.vue';
// import { ElMessage } from 'element-plus';
import Terminal from '@/views/terminal/terminal/index.vue';
let timer: NodeJS.Timer | null = null;
const terminalValue = ref();
const terminalTabs = ref([]) as any;
let tabIndex = 0;
const data = ref();
const hostDrawer = ref(false);
// const data = ref();
// const hostDrawer = ref(false);
let searcConfig = reactive<Host.ReqSearchWithPage>({
info: '',
page: 1,
pageSize: 8,
});
// let searcConfig = reactive<Host.ReqSearchWithPage>({
// info: '',
// page: 1,
// pageSize: 8,
// });
const paginationConfig = reactive({
currentPage: 1,
pageSize: 8,
total: 0,
});
// const paginationConfig = reactive({
// currentPage: 1,
// pageSize: 8,
// total: 0,
// });
let quickCmd = ref();
const quickCmds = [
{ label: 'ls', value: 'ls -l' },
{ label: 'pwd', value: 'pwd' },
];
let batchInput = ref();
let isBatch = ref<boolean>(false);
const connVisiable = ref<boolean>(false);
const operation = ref();
const hover = ref();
// const hover = ref();
type FormInstance = InstanceType<typeof ElForm>;
const hostInfoRef = ref<FormInstance>();
const rules = reactive({
@ -222,85 +238,84 @@ const handleTabsEdit = (targetName: string, action: 'remove' | 'add') => {
}
};
const loadHost = async () => {
searcConfig.page = paginationConfig.currentPage;
searcConfig.pageSize = paginationConfig.pageSize;
const res = await getHostList(searcConfig);
data.value = res.data.items;
paginationConfig.total = res.data.total;
};
// const loadHost = async () => {
// searcConfig.page = paginationConfig.currentPage;
// searcConfig.pageSize = paginationConfig.pageSize;
// const res = await getHostList(searcConfig);
// data.value = res.data.items;
// paginationConfig.total = res.data.total;
// };
const nextPage = () => {
if (paginationConfig.pageSize >= paginationConfig.total) {
return;
}
paginationConfig.pageSize = paginationConfig.pageSize + 3;
loadHost();
};
// const nextPage = () => {
// if (paginationConfig.pageSize >= paginationConfig.total) {
// return;
// }
// paginationConfig.pageSize = paginationConfig.pageSize + 3;
// loadHost();
// };
function onAddHost() {
connVisiable.value = true;
operation.value = 'create';
if (hostInfoRef.value) {
hostInfoRef.value.resetFields();
}
}
// function onAddHost() {
// connVisiable.value = true;
// operation.value = 'create';
// if (hostInfoRef.value) {
// hostInfoRef.value.resetFields();
// }
// }
function onEditHost(row: Host.Host) {
hostInfo.id = row.id;
hostInfo.name = row.name;
hostInfo.addr = row.addr;
hostInfo.port = row.port;
hostInfo.user = row.user;
hostInfo.authMode = row.authMode;
hostInfo.password = '';
hostInfo.privateKey = '';
operation.value = 'update';
connVisiable.value = true;
}
// function onEditHost(row: Host.Host) {
// hostInfo.id = row.id;
// hostInfo.name = row.name;
// hostInfo.addr = row.addr;
// hostInfo.port = row.port;
// hostInfo.user = row.user;
// hostInfo.authMode = row.authMode;
// hostInfo.password = '';
// hostInfo.privateKey = '';
// operation.value = 'update';
// connVisiable.value = true;
// }
const submitAddHost = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
try {
switch (operation.value) {
case 'create':
await addHost(hostInfo);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
break;
case 'update':
await editHost(hostInfo);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
break;
case 'conn':
const res = await addHost(hostInfo);
terminalTabs.value.push({
key: `${res.data.addr}-${++tabIndex}`,
title: res.data.addr,
wsID: res.data.id,
status: 'online',
});
terminalValue.value = `${res.data.addr}-${tabIndex}`;
}
connVisiable.value = false;
loadHost();
} catch (error) {
ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
}
});
};
// const submitAddHost = (formEl: FormInstance | undefined) => {
// if (!formEl) return;
// formEl.validate(async (valid) => {
// if (!valid) return;
// try {
// switch (operation.value) {
// case 'create':
// await addHost(hostInfo);
// ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
// break;
// case 'update':
// await editHost(hostInfo);
// ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
// break;
// case 'conn':
// const res = await addHost(hostInfo);
// terminalTabs.value.push({
// key: `${res.data.addr}-${++tabIndex}`,
// title: res.data.addr,
// wsID: res.data.id,
// status: 'online',
// });
// terminalValue.value = `${res.data.addr}-${tabIndex}`;
// }
// connVisiable.value = false;
// // loadHost();
// } catch (error) {
// ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
// }
// });
// };
const onConn = (row: Host.Host) => {
terminalTabs.value.push({
key: `${row.addr}-${++tabIndex}`,
title: row.addr,
wsID: row.id,
status: 'online',
});
terminalValue.value = `${row.addr}-${tabIndex}`;
hostDrawer.value = false;
};
// const onConn = (row: Host.Host) => {
// terminalTabs.value.push({
// key: `${row.addr}-${++tabIndex}`,
// title: row.addr,
// wsID: row.id,
// status: 'online',
// });
// terminalValue.value = `${row.addr}-${tabIndex}`;
// };
const onConnLocal = () => {
terminalTabs.value.push({
@ -310,14 +325,13 @@ const onConnLocal = () => {
status: 'online',
});
terminalValue.value = `127.0.0.1-${tabIndex}`;
hostDrawer.value = false;
};
const onDeleteHost = async (row: Host.Host) => {
let ids: Array<number> = [row.id];
await useDeleteData(deleteHost, { ids: ids }, 'commons.msg.delete');
loadHost();
};
// const onDeleteHost = async (row: Host.Host) => {
// let ids: Array<number> = [row.id];
// await useDeleteData(deleteHost, { ids: ids }, 'commons.msg.delete');
// loadHost();
// };
function changeFrameHeight() {
let ifm = document.getElementById('iframeTerminal') as HTMLInputElement | null;
@ -340,7 +354,7 @@ onMounted(() => {
changeFrameHeight();
window.addEventListener('resize', changeFrameHeight);
});
loadHost();
// loadHost();
timer = setInterval(() => {
syncTerminal();
}, 1000 * 8);
@ -367,7 +381,13 @@ onBeforeMount(() => {
border-radius: 4px 0 0 4px;
cursor: pointer;
}
.el-tabs {
.terminal-input {
&:hover {
width: 75%;
}
width: 25%;
}
.terminal-tabs {
:deep .el-tabs__header {
padding: 0;
position: relative;
@ -406,4 +426,16 @@ onBeforeMount(() => {
transition: all 0.15s;
}
}
.vertical-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
.el-tabs--right .el-tabs__content,
.el-tabs--left .el-tabs__content {
height: 100%;
}
</style>