mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-18 13:29:03 +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)
|
||||
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"`
|
||||
AppInstallName string `json:"appInstallName"`
|
||||
Websites []string `json:"websites"`
|
||||
|
||||
IsPinned bool `json:"isPinned"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ContainerOptions struct {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ func InitAgentDB() {
|
|||
migrations.UpdateCronJob,
|
||||
migrations.UpdateTensorrtLLM,
|
||||
migrations.AddIptablesFilterRuleTable,
|
||||
migrations.AddCommonDescription,
|
||||
migrations.UpdateDatabase,
|
||||
})
|
||||
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{
|
||||
ID: "20251117-update-database",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ export const getAgentSettingInfo = () => {
|
|||
export const getAgentSettingByKey = (key: string) => {
|
||||
return http.get<string>(`/settings/get/${key}`);
|
||||
};
|
||||
export const updateCommonDescription = (param: Setting.CommonDescription) => {
|
||||
return http.post(`/settings/description/save`, param);
|
||||
};
|
||||
|
||||
// core
|
||||
export const getSettingInfo = () => {
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<el-table-column type="selection" />
|
||||
<el-table-column
|
||||
:label="$t('commons.table.name')"
|
||||
:width="mobile ? 300 : 200"
|
||||
min-width="100"
|
||||
min-width="250"
|
||||
prop="name"
|
||||
sortable
|
||||
fix
|
||||
:fixed="mobile ? false : 'left'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template #default="{ row, $index }">
|
||||
<el-text type="primary" class="cursor-pointer" @click="onInspect(row)">
|
||||
{{ row.name }}
|
||||
</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>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('container.image')"
|
||||
show-overflow-tooltip
|
||||
min-width="150"
|
||||
min-width="180"
|
||||
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 }">
|
||||
<el-dropdown placement="bottom">
|
||||
<Status :key="row.state" :status="row.state" :operate="true"></Status>
|
||||
|
|
@ -303,6 +319,20 @@
|
|||
</div>
|
||||
</template>
|
||||
</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
|
||||
:label="$t('container.upTime')"
|
||||
min-width="200"
|
||||
|
|
@ -367,6 +397,7 @@ import { GlobalStore } from '@/store';
|
|||
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
||||
import router from '@/routers';
|
||||
import { computeSize2, computeSizeForDocker, computeCPU, newUUID } from '@/utils/util';
|
||||
import { updateCommonDescription } from '@/api/modules/setting';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const mobile = computed(() => {
|
||||
|
|
@ -402,6 +433,8 @@ const taskLogRef = ref();
|
|||
const tags = ref([]);
|
||||
const activeTag = ref('all');
|
||||
|
||||
const hoveredRowIndex = ref(-1);
|
||||
|
||||
const goDashboard = async (port: any) => {
|
||||
if (port.indexOf('127.0.0.1') !== -1) {
|
||||
MsgWarning(i18n.global.t('container.unExposedPort'));
|
||||
|
|
@ -474,6 +507,29 @@ const searchWithAppShow = (item: any) => {
|
|||
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 () => {
|
||||
await loadContainerStatus().then((res) => {
|
||||
tags.value = [];
|
||||
|
|
|
|||
|
|
@ -36,15 +36,31 @@
|
|||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@sort-change="search"
|
||||
@cell-mouse-enter="showFavorite"
|
||||
@cell-mouse-leave="hideFavorite"
|
||||
:columns="columns"
|
||||
@search="search"
|
||||
:heightDiff="300"
|
||||
>
|
||||
<el-table-column label="ID" prop="id" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-table-column label="ID" prop="id" width="180">
|
||||
<template #default="{ row, $index }">
|
||||
<el-text type="primary" class="cursor-pointer" @click="onInspect(row.id)">
|
||||
{{ row.id.replaceAll('sha256:', '').substring(0, 12) }}
|
||||
</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>
|
||||
</el-table-column>
|
||||
<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 { GlobalStore } from '@/store';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { updateCommonDescription } from '@/api/modules/setting';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const taskLogRef = ref();
|
||||
|
|
@ -155,6 +173,8 @@ const columns = ref([]);
|
|||
const isActive = ref(false);
|
||||
const isExist = ref(false);
|
||||
|
||||
const hoveredRowIndex = ref(-1);
|
||||
|
||||
const myDetail = ref();
|
||||
const dialogPullRef = 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 res = await inspect({ id: id, type: 'image' });
|
||||
let detailInfo = JSON.stringify(JSON.parse(res.data), null, 2);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue