mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-19 05:49:02 +08:00
feat: Support favorite operations for containers and images (#11049)
Refs #10035 #3372
This commit is contained in:
parent
063ed23acb
commit
d7c9b3b192
17 changed files with 371 additions and 104 deletions
|
|
@ -155,3 +155,24 @@ func (b *BaseApi) GetSettingByKey(c *gin.Context) {
|
||||||
value := settingService.GetSettingByKey(key)
|
value := settingService.GetSettingByKey(key)
|
||||||
helper.SuccessWithData(c, value)
|
helper.SuccessWithData(c, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags System Setting
|
||||||
|
// @Summary Save common description
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.CommonDescription true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /settings/description/save [post]
|
||||||
|
func (b *BaseApi) SaveDescription(c *gin.Context) {
|
||||||
|
var req dto.CommonDescription
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settingService.SaveDescription(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ type ContainerInfo struct {
|
||||||
AppName string `json:"appName"`
|
AppName string `json:"appName"`
|
||||||
AppInstallName string `json:"appInstallName"`
|
AppInstallName string `json:"appInstallName"`
|
||||||
Websites []string `json:"websites"`
|
Websites []string `json:"websites"`
|
||||||
|
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerOptions struct {
|
type ContainerOptions struct {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ type ImageInfo struct {
|
||||||
IsUsed bool `json:"isUsed"`
|
IsUsed bool `json:"isUsed"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageLoad struct {
|
type ImageLoad struct {
|
||||||
|
|
|
||||||
|
|
@ -81,3 +81,11 @@ type SystemProxy struct {
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommonDescription struct {
|
||||||
|
ID string `json:"id" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required"`
|
||||||
|
DetailType string `json:"detailType"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,14 @@ type Setting struct {
|
||||||
About string `json:"about"`
|
About string `json:"about"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommonDescription struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
DetailType string `json:"detailType"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
BaseDir string `json:"baseDir"`
|
BaseDir string `json:"baseDir"`
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,11 @@ func WithByType(tp string) DBOption {
|
||||||
return g.Where("`type` = ?", tp)
|
return g.Where("`type` = ?", tp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func WithByDetailType(tp string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("`detail_type` = ?", tp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithTypes(types []string) DBOption {
|
func WithTypes(types []string) DBOption {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,13 @@ type ISettingRepo interface {
|
||||||
DelMonitorIO(timeForDelete time.Time) error
|
DelMonitorIO(timeForDelete time.Time) error
|
||||||
DelMonitorNet(timeForDelete time.Time) error
|
DelMonitorNet(timeForDelete time.Time) error
|
||||||
UpdateOrCreate(key, value string) error
|
UpdateOrCreate(key, value string) error
|
||||||
|
|
||||||
|
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||||
|
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||||
|
CreateDescription(data *model.CommonDescription) error
|
||||||
|
UpdateDescription(id string, val map[string]interface{}) error
|
||||||
|
DelDescription(id string) error
|
||||||
|
WithByDescriptionID(id string) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewISettingRepo() ISettingRepo {
|
func NewISettingRepo() ISettingRepo {
|
||||||
|
|
@ -108,3 +115,36 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
||||||
}
|
}
|
||||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||||
|
var lists []model.CommonDescription
|
||||||
|
db := global.DB.Model(&model.CommonDescription{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.Find(&lists).Error
|
||||||
|
return lists, err
|
||||||
|
}
|
||||||
|
func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription, error) {
|
||||||
|
var data model.CommonDescription
|
||||||
|
db := global.DB.Model(&model.CommonDescription{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.First(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||||
|
return global.DB.Create(data).Error
|
||||||
|
}
|
||||||
|
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||||
|
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||||
|
}
|
||||||
|
func (s *SettingRepo) DelDescription(id string) error {
|
||||||
|
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||||
|
}
|
||||||
|
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("id = ?", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,6 @@ func NewIContainerService() IContainerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
||||||
var (
|
|
||||||
records []container.Summary
|
|
||||||
list []container.Summary
|
|
||||||
)
|
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
|
|
@ -117,114 +113,32 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
if req.ExcludeAppStore {
|
records := searchWithFilter(req, containers)
|
||||||
for _, item := range containers {
|
|
||||||
if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
list = append(list, item)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
list = containers
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.Name) != 0 {
|
var backData []dto.ContainerInfo
|
||||||
length, count := len(list), 0
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
for count < length {
|
|
||||||
if !strings.Contains(list[count].Names[0][1:], req.Name) && !strings.Contains(list[count].Image, req.Name) {
|
|
||||||
list = append(list[:count], list[(count+1):]...)
|
|
||||||
length--
|
|
||||||
} else {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.State != "all" {
|
|
||||||
length, count := len(list), 0
|
|
||||||
for count < length {
|
|
||||||
if list[count].State != req.State {
|
|
||||||
list = append(list[:count], list[(count+1):]...)
|
|
||||||
length--
|
|
||||||
} else {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch req.OrderBy {
|
|
||||||
case "name":
|
|
||||||
sort.Slice(list, func(i, j int) bool {
|
|
||||||
if req.Order == constant.OrderAsc {
|
|
||||||
return list[i].Names[0][1:] < list[j].Names[0][1:]
|
|
||||||
}
|
|
||||||
return list[i].Names[0][1:] > list[j].Names[0][1:]
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
sort.Slice(list, func(i, j int) bool {
|
|
||||||
if req.Order == constant.OrderAsc {
|
|
||||||
return list[i].Created < list[j].Created
|
|
||||||
}
|
|
||||||
return list[i].Created > list[j].Created
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
|
||||||
if start > total {
|
if start > total {
|
||||||
records = make([]container.Summary, 0)
|
backData = make([]dto.ContainerInfo, 0)
|
||||||
} else {
|
} else {
|
||||||
if end >= total {
|
if end >= total {
|
||||||
end = total
|
end = total
|
||||||
}
|
}
|
||||||
records = list[start:end]
|
backData = records[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
backDatas := make([]dto.ContainerInfo, len(records))
|
for i := 0; i < len(backData); i++ {
|
||||||
for i := 0; i < len(records); i++ {
|
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
|
||||||
item := records[i]
|
|
||||||
IsFromCompose := false
|
|
||||||
if _, ok := item.Labels[composeProjectLabel]; ok {
|
|
||||||
IsFromCompose = true
|
|
||||||
}
|
|
||||||
IsFromApp := false
|
|
||||||
if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" {
|
|
||||||
IsFromApp = true
|
|
||||||
}
|
|
||||||
|
|
||||||
exposePorts := transPortToStr(records[i].Ports)
|
|
||||||
info := dto.ContainerInfo{
|
|
||||||
ContainerID: item.ID,
|
|
||||||
CreateTime: time.Unix(item.Created, 0).Format(constant.DateTimeLayout),
|
|
||||||
Name: item.Names[0][1:],
|
|
||||||
ImageId: strings.Split(item.ImageID, ":")[1],
|
|
||||||
ImageName: item.Image,
|
|
||||||
State: item.State,
|
|
||||||
RunTime: item.Status,
|
|
||||||
Ports: exposePorts,
|
|
||||||
IsFromApp: IsFromApp,
|
|
||||||
IsFromCompose: IsFromCompose,
|
|
||||||
SizeRw: item.SizeRw,
|
|
||||||
SizeRootFs: item.SizeRootFs,
|
|
||||||
}
|
|
||||||
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(info.Name))
|
|
||||||
if install.ID > 0 {
|
if install.ID > 0 {
|
||||||
info.AppInstallName = install.Name
|
backData[i].AppInstallName = install.Name
|
||||||
info.AppName = install.App.Name
|
backData[i].AppName = install.App.Name
|
||||||
websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(install.ID))
|
websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(install.ID))
|
||||||
for _, website := range websites {
|
for _, website := range websites {
|
||||||
info.Websites = append(info.Websites, website.PrimaryDomain)
|
backData[i].Websites = append(backData[i].Websites, website.PrimaryDomain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
backDatas[i] = info
|
|
||||||
if item.NetworkSettings != nil && len(item.NetworkSettings.Networks) > 0 {
|
|
||||||
networks := make([]string, 0, len(item.NetworkSettings.Networks))
|
|
||||||
for key := range item.NetworkSettings.Networks {
|
|
||||||
networks = append(networks, item.NetworkSettings.Networks[key].IPAddress)
|
|
||||||
}
|
|
||||||
sort.Strings(networks)
|
|
||||||
backDatas[i].Network = networks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return int64(total), backDatas, nil
|
return int64(total), backData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) List() []dto.ContainerOptions {
|
func (u *ContainerService) List() []dto.ContainerOptions {
|
||||||
|
|
@ -1771,3 +1685,110 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
||||||
}
|
}
|
||||||
return exposedPorts
|
return exposedPorts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func searchWithFilter(req dto.PageContainer, containers []container.Summary) []dto.ContainerInfo {
|
||||||
|
var (
|
||||||
|
records []dto.ContainerInfo
|
||||||
|
list []container.Summary
|
||||||
|
)
|
||||||
|
|
||||||
|
if req.ExcludeAppStore {
|
||||||
|
for _, item := range containers {
|
||||||
|
if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list = containers
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Name) != 0 {
|
||||||
|
length, count := len(list), 0
|
||||||
|
for count < length {
|
||||||
|
if !strings.Contains(list[count].Names[0][1:], req.Name) && !strings.Contains(list[count].Image, req.Name) {
|
||||||
|
list = append(list[:count], list[(count+1):]...)
|
||||||
|
length--
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.State != "all" {
|
||||||
|
length, count := len(list), 0
|
||||||
|
for count < length {
|
||||||
|
if list[count].State != req.State {
|
||||||
|
list = append(list[:count], list[(count+1):]...)
|
||||||
|
length--
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch req.OrderBy {
|
||||||
|
case "name":
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
if req.Order == constant.OrderAsc {
|
||||||
|
return list[i].Names[0][1:] < list[j].Names[0][1:]
|
||||||
|
}
|
||||||
|
return list[i].Names[0][1:] > list[j].Names[0][1:]
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
if req.Order == constant.OrderAsc {
|
||||||
|
return list[i].Created < list[j].Created
|
||||||
|
}
|
||||||
|
return list[i].Created > list[j].Created
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, item := range list {
|
||||||
|
IsFromCompose := false
|
||||||
|
if _, ok := item.Labels[composeProjectLabel]; ok {
|
||||||
|
IsFromCompose = true
|
||||||
|
}
|
||||||
|
IsFromApp := false
|
||||||
|
if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" {
|
||||||
|
IsFromApp = true
|
||||||
|
}
|
||||||
|
exposePorts := transPortToStr(item.Ports)
|
||||||
|
info := dto.ContainerInfo{
|
||||||
|
ContainerID: item.ID,
|
||||||
|
CreateTime: time.Unix(item.Created, 0).Format(constant.DateTimeLayout),
|
||||||
|
Name: item.Names[0][1:],
|
||||||
|
Ports: exposePorts,
|
||||||
|
ImageId: strings.Split(item.ImageID, ":")[1],
|
||||||
|
ImageName: item.Image,
|
||||||
|
State: item.State,
|
||||||
|
RunTime: item.Status,
|
||||||
|
SizeRw: item.SizeRw,
|
||||||
|
SizeRootFs: item.SizeRootFs,
|
||||||
|
IsFromApp: IsFromApp,
|
||||||
|
IsFromCompose: IsFromCompose,
|
||||||
|
}
|
||||||
|
if item.NetworkSettings != nil && len(item.NetworkSettings.Networks) > 0 {
|
||||||
|
networks := make([]string, 0, len(item.NetworkSettings.Networks))
|
||||||
|
for key := range item.NetworkSettings.Networks {
|
||||||
|
networks = append(networks, item.NetworkSettings.Networks[key].IPAddress)
|
||||||
|
}
|
||||||
|
sort.Strings(networks)
|
||||||
|
info.Network = networks
|
||||||
|
}
|
||||||
|
records = append(records, info)
|
||||||
|
}
|
||||||
|
dscriptions, _ := settingRepo.GetDescriptionList(repo.WithByType("container"))
|
||||||
|
for i := 0; i < len(records); i++ {
|
||||||
|
for _, desc := range dscriptions {
|
||||||
|
if desc.ID == records[i].ContainerID {
|
||||||
|
records[i].Description = desc.Description
|
||||||
|
records[i].IsPinned = desc.IsPinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(records, func(i, j int) bool {
|
||||||
|
if records[i].IsPinned == records[j].IsPinned {
|
||||||
|
return list[i].Created > list[j].Created
|
||||||
|
}
|
||||||
|
return records[i].IsPinned
|
||||||
|
})
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,21 @@ func (u *ImageService) Page(req dto.PageImage) (int64, interface{}, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageDescriptions, _ := settingRepo.GetDescriptionList(repo.WithByType("image"))
|
||||||
|
for i := 0; i < len(list); i++ {
|
||||||
|
for _, desc := range imageDescriptions {
|
||||||
|
if "sha256:"+desc.ID == records[i].ID {
|
||||||
|
records[i].Description = desc.Description
|
||||||
|
records[i].IsPinned = desc.IsPinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(records, func(i, j int) bool {
|
||||||
|
if records[i].IsPinned == records[j].IsPinned {
|
||||||
|
return records[i].IsUsed
|
||||||
|
}
|
||||||
|
return records[i].IsPinned
|
||||||
|
})
|
||||||
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
if start > total {
|
if start > total {
|
||||||
backDatas = make([]dto.ImageInfo, 0)
|
backDatas = make([]dto.ImageInfo, 0)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"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/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
||||||
|
|
@ -24,6 +25,8 @@ type ISettingService interface {
|
||||||
GetSystemProxy() (*dto.SystemProxy, error)
|
GetSystemProxy() (*dto.SystemProxy, error)
|
||||||
GetLocalConn() dto.SSHConnData
|
GetLocalConn() dto.SSHConnData
|
||||||
GetSettingByKey(key string) string
|
GetSettingByKey(key string) string
|
||||||
|
|
||||||
|
SaveDescription(req dto.CommonDescription) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewISettingService() ISettingService {
|
func NewISettingService() ISettingService {
|
||||||
|
|
@ -170,3 +173,24 @@ func (u *SettingService) GetSettingByKey(key string) string {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *SettingService) SaveDescription(req dto.CommonDescription) error {
|
||||||
|
if len(req.Description) == 0 && !req.IsPinned {
|
||||||
|
_ = settingRepo.DelDescription(req.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, _ := settingRepo.GetDescription(settingRepo.WithByDescriptionID(req.ID), repo.WithByType(req.Type), repo.WithByDetailType(req.DetailType))
|
||||||
|
if data.ID == "" {
|
||||||
|
if err := copier.Copy(&data, &req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.CreateDescription(&data)
|
||||||
|
}
|
||||||
|
valMap := make(map[string]interface{})
|
||||||
|
valMap["type"] = req.Type
|
||||||
|
valMap["detail_type"] = req.DetailType
|
||||||
|
valMap["is_pinned"] = req.IsPinned
|
||||||
|
valMap["description"] = req.Description
|
||||||
|
|
||||||
|
return settingRepo.UpdateDescription(data.ID, valMap)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ func InitAgentDB() {
|
||||||
migrations.UpdateCronJob,
|
migrations.UpdateCronJob,
|
||||||
migrations.UpdateTensorrtLLM,
|
migrations.UpdateTensorrtLLM,
|
||||||
migrations.AddIptablesFilterRuleTable,
|
migrations.AddIptablesFilterRuleTable,
|
||||||
|
migrations.AddCommonDescription,
|
||||||
migrations.UpdateDatabase,
|
migrations.UpdateDatabase,
|
||||||
})
|
})
|
||||||
if err := m.Migrate(); err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -706,6 +706,13 @@ var UpdateTensorrtLLM = &gormigrate.Migration{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddCommonDescription = &gormigrate.Migration{
|
||||||
|
ID: "20251117-add-common-description",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
return tx.AutoMigrate(&model.CommonDescription{})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var UpdateDatabase = &gormigrate.Migration{
|
var UpdateDatabase = &gormigrate.Migration{
|
||||||
ID: "20251117-update-database",
|
ID: "20251117-update-database",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
|
||||||
settingRouter.POST("/update", baseApi.UpdateSetting)
|
settingRouter.POST("/update", baseApi.UpdateSetting)
|
||||||
settingRouter.GET("/get/:key", baseApi.GetSettingByKey)
|
settingRouter.GET("/get/:key", baseApi.GetSettingByKey)
|
||||||
|
|
||||||
|
settingRouter.POST("/description/save", baseApi.SaveDescription)
|
||||||
|
|
||||||
settingRouter.GET("/snapshot/load", baseApi.LoadSnapshotData)
|
settingRouter.GET("/snapshot/load", baseApi.LoadSnapshotData)
|
||||||
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
||||||
settingRouter.POST("/snapshot/recreate", baseApi.RecreateSnapshot)
|
settingRouter.POST("/snapshot/recreate", baseApi.RecreateSnapshot)
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,13 @@ export namespace Setting {
|
||||||
code: string;
|
code: string;
|
||||||
interval: string;
|
interval: string;
|
||||||
}
|
}
|
||||||
|
export interface CommonDescription {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
detailType: string;
|
||||||
|
isPinned: boolean;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SnapshotCreate {
|
export interface SnapshotCreate {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@ export const getAgentSettingInfo = () => {
|
||||||
export const getAgentSettingByKey = (key: string) => {
|
export const getAgentSettingByKey = (key: string) => {
|
||||||
return http.get<string>(`/settings/get/${key}`);
|
return http.get<string>(`/settings/get/${key}`);
|
||||||
};
|
};
|
||||||
|
export const updateCommonDescription = (param: Setting.CommonDescription) => {
|
||||||
|
return http.post(`/settings/description/save`, param);
|
||||||
|
};
|
||||||
|
|
||||||
// core
|
// core
|
||||||
export const getSettingInfo = () => {
|
export const getSettingInfo = () => {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@
|
||||||
:data="data"
|
:data="data"
|
||||||
@sort-change="search"
|
@sort-change="search"
|
||||||
@search="search"
|
@search="search"
|
||||||
|
@cell-mouse-enter="showFavorite"
|
||||||
|
@cell-mouse-leave="hideFavorite"
|
||||||
:row-style="{ height: '65px' }"
|
:row-style="{ height: '65px' }"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
|
@ -89,27 +91,41 @@
|
||||||
<el-table-column type="selection" />
|
<el-table-column type="selection" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('commons.table.name')"
|
:label="$t('commons.table.name')"
|
||||||
:width="mobile ? 300 : 200"
|
min-width="250"
|
||||||
min-width="100"
|
|
||||||
prop="name"
|
prop="name"
|
||||||
sortable
|
sortable
|
||||||
fix
|
fix
|
||||||
:fixed="mobile ? false : 'left'"
|
:fixed="mobile ? false : 'left'"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row, $index }">
|
||||||
<el-text type="primary" class="cursor-pointer" @click="onInspect(row)">
|
<el-text type="primary" class="cursor-pointer" @click="onInspect(row)">
|
||||||
{{ row.name }}
|
{{ row.name }}
|
||||||
</el-text>
|
</el-text>
|
||||||
|
|
||||||
|
<div class="float-right">
|
||||||
|
<el-tooltip
|
||||||
|
:content="row.isPinned ? $t('website.cancelFavorite') : $t('website.favorite')"
|
||||||
|
v-if="row.isPinned || hoveredRowIndex === $index"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
size="large"
|
||||||
|
:icon="row.isPinned ? 'StarFilled' : 'Star'"
|
||||||
|
type="warning"
|
||||||
|
@click="changePinned(row, true)"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('container.image')"
|
:label="$t('container.image')"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
min-width="150"
|
min-width="180"
|
||||||
prop="imageName"
|
prop="imageName"
|
||||||
/>
|
/>
|
||||||
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="state" sortable>
|
<el-table-column :label="$t('commons.table.status')" min-width="150" prop="state" sortable>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-dropdown placement="bottom">
|
<el-dropdown placement="bottom">
|
||||||
<Status :key="row.state" :status="row.state" :operate="true"></Status>
|
<Status :key="row.state" :status="row.state" :operate="true"></Status>
|
||||||
|
|
@ -303,6 +319,20 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
min-width="200"
|
||||||
|
:label="$t('commons.table.description')"
|
||||||
|
prop="description"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<fu-input-rw-switch
|
||||||
|
v-model="row.description"
|
||||||
|
@enter="changePinned(row, false)"
|
||||||
|
@blur="changePinned(row, false)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('container.upTime')"
|
:label="$t('container.upTime')"
|
||||||
min-width="200"
|
min-width="200"
|
||||||
|
|
@ -367,6 +397,7 @@ import { GlobalStore } from '@/store';
|
||||||
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
||||||
import router from '@/routers';
|
import router from '@/routers';
|
||||||
import { computeSize2, computeSizeForDocker, computeCPU, newUUID } from '@/utils/util';
|
import { computeSize2, computeSizeForDocker, computeCPU, newUUID } from '@/utils/util';
|
||||||
|
import { updateCommonDescription } from '@/api/modules/setting';
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
|
|
||||||
const mobile = computed(() => {
|
const mobile = computed(() => {
|
||||||
|
|
@ -402,6 +433,8 @@ const taskLogRef = ref();
|
||||||
const tags = ref([]);
|
const tags = ref([]);
|
||||||
const activeTag = ref('all');
|
const activeTag = ref('all');
|
||||||
|
|
||||||
|
const hoveredRowIndex = ref(-1);
|
||||||
|
|
||||||
const goDashboard = async (port: any) => {
|
const goDashboard = async (port: any) => {
|
||||||
if (port.indexOf('127.0.0.1') !== -1) {
|
if (port.indexOf('127.0.0.1') !== -1) {
|
||||||
MsgWarning(i18n.global.t('container.unExposedPort'));
|
MsgWarning(i18n.global.t('container.unExposedPort'));
|
||||||
|
|
@ -474,6 +507,29 @@ const searchWithAppShow = (item: any) => {
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showFavorite = (row: any) => {
|
||||||
|
hoveredRowIndex.value = data.value.findIndex((item) => item === row);
|
||||||
|
};
|
||||||
|
const hideFavorite = () => {
|
||||||
|
hoveredRowIndex.value = -1;
|
||||||
|
};
|
||||||
|
const changePinned = (row: any, isPinned: boolean) => {
|
||||||
|
let params = {
|
||||||
|
id: row.containerID,
|
||||||
|
type: 'container',
|
||||||
|
detailType: '',
|
||||||
|
isPinned: !row.isPinned,
|
||||||
|
description: row.description || '',
|
||||||
|
};
|
||||||
|
if (isPinned) {
|
||||||
|
params.isPinned = !row.isPinned;
|
||||||
|
}
|
||||||
|
updateCommonDescription(params).then(() => {
|
||||||
|
search();
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadContainerCount = async () => {
|
const loadContainerCount = async () => {
|
||||||
await loadContainerStatus().then((res) => {
|
await loadContainerStatus().then((res) => {
|
||||||
tags.value = [];
|
tags.value = [];
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,31 @@
|
||||||
:pagination-config="paginationConfig"
|
:pagination-config="paginationConfig"
|
||||||
:data="data"
|
:data="data"
|
||||||
@sort-change="search"
|
@sort-change="search"
|
||||||
|
@cell-mouse-enter="showFavorite"
|
||||||
|
@cell-mouse-leave="hideFavorite"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@search="search"
|
@search="search"
|
||||||
:heightDiff="300"
|
:heightDiff="300"
|
||||||
>
|
>
|
||||||
<el-table-column label="ID" prop="id" width="140">
|
<el-table-column label="ID" prop="id" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row, $index }">
|
||||||
<el-text type="primary" class="cursor-pointer" @click="onInspect(row.id)">
|
<el-text type="primary" class="cursor-pointer" @click="onInspect(row.id)">
|
||||||
{{ row.id.replaceAll('sha256:', '').substring(0, 12) }}
|
{{ row.id.replaceAll('sha256:', '').substring(0, 12) }}
|
||||||
</el-text>
|
</el-text>
|
||||||
|
<div class="float-right">
|
||||||
|
<el-tooltip
|
||||||
|
:content="row.isPinned ? $t('website.cancelFavorite') : $t('website.favorite')"
|
||||||
|
v-if="row.isPinned || hoveredRowIndex === $index"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
size="large"
|
||||||
|
:icon="row.isPinned ? 'StarFilled' : 'Star'"
|
||||||
|
type="warning"
|
||||||
|
@click="changePinned(row, true)"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('commons.table.status')" prop="isUsed" width="100" sortable>
|
<el-table-column :label="$t('commons.table.status')" prop="isUsed" width="100" sortable>
|
||||||
|
|
@ -128,6 +144,8 @@ import { searchImage, listImageRepo, imageRemove, inspect, containerPrune } from
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import { updateCommonDescription } from '@/api/modules/setting';
|
||||||
|
import { MsgSuccess } from '@/utils/message';
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
|
|
||||||
const taskLogRef = ref();
|
const taskLogRef = ref();
|
||||||
|
|
@ -155,6 +173,8 @@ const columns = ref([]);
|
||||||
const isActive = ref(false);
|
const isActive = ref(false);
|
||||||
const isExist = ref(false);
|
const isExist = ref(false);
|
||||||
|
|
||||||
|
const hoveredRowIndex = ref(-1);
|
||||||
|
|
||||||
const myDetail = ref();
|
const myDetail = ref();
|
||||||
const dialogPullRef = ref();
|
const dialogPullRef = ref();
|
||||||
const dialogTagRef = ref();
|
const dialogTagRef = ref();
|
||||||
|
|
@ -208,6 +228,29 @@ const onDelete = (row: Container.ImageInfo) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showFavorite = (row: any) => {
|
||||||
|
hoveredRowIndex.value = data.value.findIndex((item) => item === row);
|
||||||
|
};
|
||||||
|
const hideFavorite = () => {
|
||||||
|
hoveredRowIndex.value = -1;
|
||||||
|
};
|
||||||
|
const changePinned = (row: any, isPinned: boolean) => {
|
||||||
|
let params = {
|
||||||
|
id: row.id.replaceAll('sha256:', ''),
|
||||||
|
type: 'image',
|
||||||
|
detailType: '',
|
||||||
|
isPinned: !row.isPinned,
|
||||||
|
description: row.description || '',
|
||||||
|
};
|
||||||
|
if (isPinned) {
|
||||||
|
params.isPinned = !row.isPinned;
|
||||||
|
}
|
||||||
|
updateCommonDescription(params).then(() => {
|
||||||
|
search();
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onInspect = async (id: string) => {
|
const onInspect = async (id: string) => {
|
||||||
const res = await inspect({ id: id, type: 'image' });
|
const res = await inspect({ id: id, type: 'image' });
|
||||||
let detailInfo = JSON.stringify(JSON.parse(res.data), null, 2);
|
let detailInfo = JSON.stringify(JSON.parse(res.data), null, 2);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue