feat: Support favorite operations for containers and images (#11049)

Refs #10035 #3372
This commit is contained in:
ssongliu 2025-11-24 15:55:14 +08:00 committed by GitHub
parent 063ed23acb
commit d7c9b3b192
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 371 additions and 104 deletions

View file

@ -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)
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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"`
}

View file

@ -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"`

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

View file

@ -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;

View file

@ -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 = () => {

View file

@ -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 = [];

View file

@ -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);