diff --git a/agent/app/api/v2/setting.go b/agent/app/api/v2/setting.go index 80f574d5a..c2a6d0632 100644 --- a/agent/app/api/v2/setting.go +++ b/agent/app/api/v2/setting.go @@ -155,3 +155,24 @@ func (b *BaseApi) GetSettingByKey(c *gin.Context) { value := settingService.GetSettingByKey(key) 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) +} diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index 8cef64d39..2358c8272 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -40,6 +40,9 @@ type ContainerInfo struct { AppName string `json:"appName"` AppInstallName string `json:"appInstallName"` Websites []string `json:"websites"` + + IsPinned bool `json:"isPinned"` + Description string `json:"description"` } type ContainerOptions struct { diff --git a/agent/app/dto/image.go b/agent/app/dto/image.go index 192e5974c..ad2bb8873 100644 --- a/agent/app/dto/image.go +++ b/agent/app/dto/image.go @@ -15,6 +15,9 @@ type ImageInfo struct { IsUsed bool `json:"isUsed"` Tags []string `json:"tags"` Size int64 `json:"size"` + + IsPinned bool `json:"isPinned"` + Description string `json:"description"` } type ImageLoad struct { diff --git a/agent/app/dto/setting.go b/agent/app/dto/setting.go index 17ccdd917..9714002f7 100644 --- a/agent/app/dto/setting.go +++ b/agent/app/dto/setting.go @@ -81,3 +81,11 @@ type SystemProxy struct { User string `json:"user"` 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"` +} diff --git a/agent/app/model/setting.go b/agent/app/model/setting.go index a21a08f26..22f12cf6c 100644 --- a/agent/app/model/setting.go +++ b/agent/app/model/setting.go @@ -7,6 +7,14 @@ type Setting struct { 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 { Scope string `json:"scope"` BaseDir string `json:"baseDir"` diff --git a/agent/app/repo/common.go b/agent/app/repo/common.go index 2b40e8417..36c2d4fe9 100644 --- a/agent/app/repo/common.go +++ b/agent/app/repo/common.go @@ -77,6 +77,11 @@ func WithByType(tp string) DBOption { 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 { return func(db *gorm.DB) *gorm.DB { diff --git a/agent/app/repo/setting.go b/agent/app/repo/setting.go index 9b766f445..af9db0c8c 100644 --- a/agent/app/repo/setting.go +++ b/agent/app/repo/setting.go @@ -26,6 +26,13 @@ type ISettingRepo interface { DelMonitorIO(timeForDelete time.Time) error DelMonitorNet(timeForDelete time.Time) 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 { @@ -108,3 +115,36 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) 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) + } +} diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 9177bd7fc..29d1e8eaa 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -96,10 +96,6 @@ func NewIContainerService() IContainerService { } func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) { - var ( - records []container.Summary - list []container.Summary - ) client, err := docker.NewDockerClient() if err != nil { return 0, nil, err @@ -117,114 +113,32 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro if err != nil { return 0, nil, err } - if req.ExcludeAppStore { - for _, item := range containers { - if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" { - continue - } - list = append(list, item) - } - } else { - list = containers - } + records := searchWithFilter(req, 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 - }) - } - - total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize + var backData []dto.ContainerInfo + total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize if start > total { - records = make([]container.Summary, 0) + backData = make([]dto.ContainerInfo, 0) } else { if end >= total { end = total } - records = list[start:end] + backData = records[start:end] } - backDatas := make([]dto.ContainerInfo, len(records)) - for i := 0; i < len(records); i++ { - 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)) + for i := 0; i < len(backData); i++ { + install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name)) if install.ID > 0 { - info.AppInstallName = install.Name - info.AppName = install.App.Name + backData[i].AppInstallName = install.Name + backData[i].AppName = install.App.Name websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(install.ID)) 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 { @@ -1771,3 +1685,110 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper { } 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 +} diff --git a/agent/app/service/image.go b/agent/app/service/image.go index 5389e190a..c0b38a4f3 100644 --- a/agent/app/service/image.go +++ b/agent/app/service/image.go @@ -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 if start > total { backDatas = make([]dto.ImageInfo, 0) diff --git a/agent/app/service/setting.go b/agent/app/service/setting.go index 6ed775431..91ccbba13 100644 --- a/agent/app/service/setting.go +++ b/agent/app/service/setting.go @@ -7,6 +7,7 @@ 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/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/ssh" @@ -24,6 +25,8 @@ type ISettingService interface { GetSystemProxy() (*dto.SystemProxy, error) GetLocalConn() dto.SSHConnData GetSettingByKey(key string) string + + SaveDescription(req dto.CommonDescription) error } func NewISettingService() ISettingService { @@ -170,3 +173,24 @@ func (u *SettingService) GetSettingByKey(key string) string { 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) +} diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 9adf68fb7..15c24199d 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -52,6 +52,7 @@ func InitAgentDB() { migrations.UpdateCronJob, migrations.UpdateTensorrtLLM, migrations.AddIptablesFilterRuleTable, + migrations.AddCommonDescription, migrations.UpdateDatabase, }) if err := m.Migrate(); err != nil { diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 03eb0f763..96e4abf3d 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -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{ ID: "20251117-update-database", Migrate: func(tx *gorm.DB) error { diff --git a/agent/router/ro_setting.go b/agent/router/ro_setting.go index a8cb5b46c..79ee7510a 100644 --- a/agent/router/ro_setting.go +++ b/agent/router/ro_setting.go @@ -16,6 +16,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { settingRouter.POST("/update", baseApi.UpdateSetting) settingRouter.GET("/get/:key", baseApi.GetSettingByKey) + settingRouter.POST("/description/save", baseApi.SaveDescription) + settingRouter.GET("/snapshot/load", baseApi.LoadSnapshotData) settingRouter.POST("/snapshot", baseApi.CreateSnapshot) settingRouter.POST("/snapshot/recreate", baseApi.RecreateSnapshot) diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index a82cf7e9e..5de510af9 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -128,6 +128,13 @@ export namespace Setting { code: string; interval: string; } + export interface CommonDescription { + id: string; + type: string; + detailType: string; + isPinned: boolean; + description: string; + } export interface SnapshotCreate { id: number; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index 275746e5a..f7504ff4b 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -71,6 +71,9 @@ export const getAgentSettingInfo = () => { export const getAgentSettingByKey = (key: string) => { return http.get(`/settings/get/${key}`); }; +export const updateCommonDescription = (param: Setting.CommonDescription) => { + return http.post(`/settings/description/save`, param); +}; // core export const getSettingInfo = () => { diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index 8c90ba9c6..96b18cf5b 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -80,6 +80,8 @@ :data="data" @sort-change="search" @search="search" + @cell-mouse-enter="showFavorite" + @cell-mouse-leave="hideFavorite" :row-style="{ height: '65px' }" style="width: 100%" :columns="columns" @@ -89,27 +91,41 @@ -