feat: unify pin actions (#13417)

This commit is contained in:
ssongliu
2026-07-30 09:39:47 +08:00
committed by GitHub
parent 6ac7a5f167
commit 33b3eecb95
36 changed files with 297 additions and 119 deletions
+20
View File
@@ -881,6 +881,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
helper.Success(c)
}
// @Tags Container Compose
// @Summary Pin compose
// @Accept json
// @Param request body dto.ComposePin true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/compose/pin [post]
func (b *BaseApi) ComposePin(c *gin.Context) {
var req dto.ComposePin
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.ComposePin(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Container Compose
// @Summary Load compose environment variables
// @Accept json
+5
View File
@@ -300,6 +300,7 @@ type ComposeInfo struct {
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`
ComposeFileExists bool `json:"composeFileExists"`
IsPinned bool `json:"isPinned"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env string `json:"env"`
@@ -337,6 +338,10 @@ type ComposeUpdate struct {
Env string `json:"env"`
ForcePull bool `json:"forcePull"`
}
type ComposePin struct {
Name string `json:"name" validate:"required"`
IsPinned bool `json:"isPinned"`
}
type ComposeLogClean struct {
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
+3 -2
View File
@@ -11,6 +11,7 @@ type ComposeTemplate struct {
type Compose struct {
BaseModel
Name string `json:"name"`
Path string `json:"path"`
Name string `json:"name"`
Path string `json:"path"`
IsPinned bool `json:"isPinned"`
}
+4
View File
@@ -71,6 +71,7 @@ type IContainerService interface {
ComposeOperation(req dto.ComposeOperation) error
TestCompose(req dto.ComposeCreate) (bool, error)
ComposeUpdate(req dto.ComposeUpdate) error
ComposePin(req dto.ComposePin) error
ComposeLogClean(req dto.ComposeLogClean) error
ContainerCreate(req dto.ContainerOperate, inThread bool) error
@@ -2025,6 +2026,9 @@ func loadComposeCount(client *client.Client) int {
}
}
for _, compose := range composeCreatedByLocal {
if len(compose.Path) == 0 {
continue
}
if _, has := composeMap[compose.Name]; !has {
composeMap[compose.Name] = struct{}{}
}
+35 -3
View File
@@ -52,7 +52,15 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
return 0, nil, err
}
composeCreatedByLocal, _ := composeRepo.ListRecord()
composeRecords, _ := composeRepo.ListRecord()
pinnedByName := make(map[string]bool, len(composeRecords))
composeCreatedByLocal := make([]model.Compose, 0, len(composeRecords))
for _, record := range composeRecords {
pinnedByName[record.Name] = record.IsPinned
if len(record.Path) != 0 {
composeCreatedByLocal = append(composeCreatedByLocal, record)
}
}
composeLocalMap := make(map[string]dto.ComposeInfo)
for _, localItem := range composeCreatedByLocal {
composeItemLocal := dto.ComposeInfo{
@@ -136,6 +144,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
for key, value := range mergedMap {
value.Name = key
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
value.IsPinned = pinnedByName[key]
records = append(records, value)
}
if len(req.Info) != 0 {
@@ -150,6 +159,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
}
}
sort.Slice(records, func(i, j int) bool {
if records[i].IsPinned != records[j].IsPinned {
return records[i].IsPinned
}
return records[i].CreatedAt > records[j].CreatedAt
})
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
@@ -193,7 +205,7 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
return false, buserr.New("ErrCmdIllegal")
}
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
if composeItem.ID != 0 {
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
return false, buserr.New("ErrRecordExist")
}
if err := u.loadPath(&req); err != nil {
@@ -235,7 +247,13 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
_, _ = compose.Down(req.Path)
return err
}
_ = composeRepo.CreateRecord(&model.Compose{Name: strings.ToLower(req.Name), Path: req.Path})
recordName := strings.ToLower(req.Name)
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
if record.ID == 0 {
_ = composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: req.Path})
} else {
_ = composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": req.Path})
}
return nil
}, nil)
_ = taskItem.Execute()
@@ -327,6 +345,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
return nil
}
func (u *ContainerService) ComposePin(req dto.ComposePin) error {
record, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
if record.ID == 0 {
if !req.IsPinned {
return nil
}
return composeRepo.CreateRecord(&model.Compose{Name: req.Name, IsPinned: true})
}
if !req.IsPinned && len(record.Path) == 0 {
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
}
return composeRepo.UpdateRecord(req.Name, map[string]interface{}{"is_pinned": req.IsPinned})
}
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
client, err := docker.NewDockerClient()
if err != nil {
+1
View File
@@ -95,6 +95,7 @@ func InitAgentDB() {
migrations.AddDatabaseUserTable,
migrations.AddBackupRecordArgs,
migrations.AddFtpIdentity,
migrations.AddComposePinned,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+7
View File
@@ -1698,3 +1698,10 @@ var AddFtpIdentity = &gormigrate.Migration{
}).Error
},
}
var AddComposePinned = &gormigrate.Migration{
ID: "20260729-add-compose-pinned",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Compose{})
},
}
+1
View File
@@ -55,6 +55,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/compose/operate", baseApi.OperatorCompose)
baRouter.POST("/compose/clean/log", baseApi.CleanComposeLog)
baRouter.POST("/compose/update", baseApi.ComposeUpdate)
baRouter.POST("/compose/pin", baseApi.ComposePin)
baRouter.GET("/template", baseApi.ListComposeTemplate)
baRouter.POST("/template/search", baseApi.SearchComposeTemplate)
+5
View File
@@ -337,6 +337,7 @@ export namespace Container {
configFile: string;
workdir: string;
composeFileExists: boolean;
isPinned: boolean;
path: string;
containers: Array<ComposeContainer>;
expand: boolean;
@@ -374,6 +375,10 @@ export namespace Container {
forcePull: boolean;
createdBy: string;
}
export interface ComposePin {
name: string;
isPinned: boolean;
}
export interface TemplateCreate {
name: string;
+3
View File
@@ -240,6 +240,9 @@ export const composeOperate = (params: Container.ComposeOperation) => {
export const composeUpdate = (params: Container.ComposeUpdate) => {
return http.post(`/containers/compose/update`, params, TimeoutEnum.T_10M);
};
export const composePin = (params: Container.ComposePin) => {
return http.post(`/containers/compose/pin`, params);
};
// docker
export const dockerOperate = (operation: string) => {
+8 -4
View File
@@ -1,9 +1,9 @@
@font-face {
font-family: "iconfont"; /* Project id 4776196 */
src: url('iconfont.woff2?t=1775716122874') format('woff2'),
url('iconfont.woff?t=1775716122874') format('woff'),
url('iconfont.ttf?t=1775716122874') format('truetype'),
url('iconfont.svg?t=1775716122874#iconfont') format('svg');
src: url('iconfont.woff2?t=1785320291556') format('woff2'),
url('iconfont.woff?t=1785320291556') format('woff'),
url('iconfont.ttf?t=1785320291556') format('truetype'),
url('iconfont.svg?t=1785320291556#iconfont') format('svg');
}
.iconfont {
@@ -14,6 +14,10 @@
-moz-osx-font-smoothing: grayscale;
}
.p-pushpin:before {
content: "\e7e3";
}
.p-qrcode:before {
content: "\e72a";
}
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
"css_prefix_text": "p-",
"description": "",
"glyphs": [
{
"icon_id": "4766969",
"name": "pushpin",
"font_class": "pushpin",
"unicode": "e7e3",
"unicode_decimal": 59363
},
{
"icon_id": "14679640",
"name": "qrcode",
@@ -13,6 +13,8 @@
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="pushpin" unicode="&#59363;" d="M878.3 503.9L631.9 750.3c-6.5 6.5-15 9.7-23.5 9.7s-17-3.2-23.5-9.7L423.8 589.1c-12.2 1.4-24.5 2-36.8 2-73.2 0-146.4-24.1-206.5-72.3-15.4-12.3-16.7-35.4-2.7-49.4l181.7-181.7-215.4-215.2c-2.6-2.6-4.3-6.1-4.6-9.8l-3.4-37.2c-0.9-9.4 6.6-17.4 15.9-17.4 0.5 0 1 0 1.5 0.1l37.2 3.4c3.7 0.3 7.2 2 9.8 4.6l215.4 215.4 181.7-181.7c6.5-6.5 15-9.7 23.5-9.7 9.7 0 19.3 4.2 25.9 12.4 56.3 70.3 79.7 158.3 70.2 243.4l161.1 161.1c12.9 12.8 12.9 33.8 0 46.8zM666.2 346.7l-24.5-24.5 3.8-34.4c3.7-33.7 1-67.2-8.2-99.7-5.4-19-12.8-37.1-22.2-54.2L262 487.2c12.9 7.1 26.3 13.1 40.3 17.9 27.2 9.4 55.7 14.1 84.7 14.1 9.6 0 19.3-0.5 28.9-1.6l34.4-3.8 24.5 24.5L608.5 672 800 480.5 666.2 346.7z" horiz-adv-x="1024" />
<glyph glyph-name="qrcode" unicode="&#59178;" d="M629.650286 419.364571h194.998857c54.857143 0 81.846857 27.428571 81.846857 83.986286V694.930286c0 56.576-26.989714 83.565714-81.846857 83.565714H629.650286c-54.436571 0-81.865143-26.989714-81.865143-83.565714v-191.579429c0-56.557714 27.428571-83.986286 81.865143-83.986286z m-430.299429 0H394.788571c54.418286 0 81.846857 27.428571 81.846858 83.986286V694.930286c0 56.576-27.428571 83.565714-81.846858 83.565714H199.350857c-54.418286 0-81.846857-26.989714-81.846857-83.565714v-191.579429c0-56.557714 27.428571-83.986286 81.846857-83.986286z m0.859429 60.416c-14.994286 0-22.290286 7.716571-22.290286 23.588572V694.912c0 15.433143 7.296 23.149714 22.308571 23.149714h193.28c14.994286 0 22.710857-7.716571 22.710858-23.149714v-191.579429c0-15.853714-7.716571-23.570286-22.710858-23.570285z m430.281143 0c-14.994286 0-22.272 7.716571-22.272 23.588572V694.912c0 15.433143 7.277714 23.149714 22.272 23.149714h193.718857c14.573714 0 21.869714-7.716571 21.869714-23.149714v-191.579429c0-15.853714-7.296-23.570286-21.869714-23.570285z m-370.285715 74.148572h73.289143c6.436571 0 8.996571 2.56 8.996572 9.856v71.131428c0 6.875429-2.56 9.435429-8.996572 9.435429H260.205714c-6.418286 0-8.137143-2.56-8.137143-9.417143v-71.149714c0-7.277714 1.718857-9.874286 8.137143-9.874286z m432.859429 0h72.868571c6.418286 0 8.996571 2.56 8.996572 9.856v71.131428c0 6.875429-2.56 9.435429-8.996572 9.435429h-72.868571c-6.418286 0-8.557714-2.56-8.557714-9.417143v-71.149714c0-7.277714 2.139429-9.874286 8.557714-9.874286z m-493.714286-564.425143H394.788571c54.418286 0 81.846857 26.989714 81.846858 83.565714v192c0 56.137143-27.428571 83.565714-81.846858 83.565715H199.350857c-54.418286 0-81.846857-27.428571-81.846857-83.565715v-192c0-56.576 27.428571-83.565714 81.846857-83.565714z m377.142857 248.137143h73.289143c6.436571 0 8.996571 2.56 8.996572 9.874286v71.131428c0 6.857143-2.56 9.417143-8.996572 9.417143h-73.289143c-6.418286 0-8.137143-2.56-8.137143-9.417143v-71.131428c0-7.314286 1.718857-9.874286 8.137143-9.874286z m227.584 0h73.270857c6.436571 0 9.014857 2.56 9.014858 9.874286v71.131428c0 6.857143-2.578286 9.417143-9.014858 9.417143h-73.270857c-6.436571 0-8.594286-2.56-8.594285-9.417143v-71.131428c0-7.314286 2.157714-9.874286 8.594285-9.874286zM200.210286 49.92c-14.994286 0-22.290286 7.716571-22.290286 23.149714V264.649143c0 15.853714 7.296 23.570286 22.308571 23.570286h193.28c14.994286 0 22.710857-7.716571 22.710858-23.588572v-191.561143c0-15.433143-7.716571-23.149714-22.710858-23.149714z m59.995428 73.728h73.289143c6.436571 0 8.996571 2.56 8.996572 10.276571v70.710858c0 6.857143-2.56 9.435429-8.996572 9.435428H260.205714c-6.418286 0-8.137143-2.56-8.137143-9.435428v-70.710858c0-7.716571 1.718857-10.276571 8.137143-10.276571z m431.158857 0h73.270858c6.436571 0 8.996571 2.56 8.996571 10.276571v70.710858c0 6.857143-2.56 9.435429-8.996571 9.435428h-73.289143c-6.418286 0-8.137143-2.56-8.137143-9.435428v-70.710858c0-7.716571 1.718857-10.276571 8.155428-10.276571z m-114.870857-113.572571h73.289143c6.436571 0 8.996571 2.56 8.996572 9.856v71.131428c0 6.875429-2.56 9.435429-8.996572 9.435429h-73.289143c-6.418286 0-8.137143-2.56-8.137143-9.435429v-71.131428c0-7.296 1.718857-9.874286 8.137143-9.874286z m227.584 0h73.270857c6.436571 0 9.014857 2.56 9.014858 9.856v71.131428c0 6.875429-2.578286 9.435429-9.014858 9.435429h-73.270857c-6.436571 0-8.594286-2.56-8.594285-9.435429v-71.131428c0-7.296 2.157714-9.874286 8.594285-9.874286z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -150,6 +150,8 @@ const message = {
manageGroup: 'Manage Groups',
backToList: 'Back to List',
keepEdit: 'Continue Editing',
pin: 'Pin to top',
unpin: 'Unpin',
},
loadingText: {
Upgrading: 'Upgrading system, please wait...',
@@ -3758,8 +3760,6 @@ const message = {
cacheWarn: 'Please turn off the cache switch in the reverse proxy first',
loadBalanceHelper:
'After creating the load balancing, go to "Reverse Proxy", add a proxy and set the backend address to: http://<load balancing name>',
favorite: 'Favorite',
cancelFavorite: 'Cancel Favorite',
useProxy: 'Use Proxy',
useProxyHelper: 'Use the proxy server address in the panel settings',
westCN: 'West Digital',
+2 -2
View File
@@ -147,6 +147,8 @@ const message = {
manageGroup: 'Gestionar grupos',
backToList: 'Volver a la lista',
keepEdit: 'Continuar Editando',
pin: 'Fijar arriba',
unpin: 'Desfijar',
creator: 'Creador',
updater: 'Actualizador',
},
@@ -3805,8 +3807,6 @@ const message = {
runtimePortWarn: 'Este runtime no tiene puertos, no se puede usar proxy',
cacheWarn: 'Desactiva la caché en proxy inverso primero',
loadBalanceHelper: 'Tras crear balanceo, ve a "Proxy inverso" y añade un backend: http://<nombre balanceo>',
favorite: 'Favorito',
cancelFavorite: 'Quitar favorito',
useProxy: 'Usar proxy',
useProxyHelper: 'Usa la dirección del proxy configurado en el panel',
westCN: 'West Digital',
+2 -2
View File
@@ -146,6 +146,8 @@ const message = {
manageGroup: 'مدیریت گروه‌ها',
backToList: 'بازگشت به لیست',
keepEdit: 'ادامه ویرایش',
pin: 'سنجاق کردن در بالا',
unpin: 'برداشتن سنجاق',
},
loadingText: {
Upgrading: 'در حال ارتقاء سیستم، لطفاً صبر کنید...',
@@ -3727,8 +3729,6 @@ const message = {
cacheWarn: 'لطفاً ابتدا کلید کش را در پراکسی معکوس خاموش کنید',
loadBalanceHelper:
'پس از ایجاد تعادل بار، به "پراکسی معکوس" بروید، یک پراکسی اضافه کرده و آدرس بک‌اند را به: http://<نام تعادل بار> تنظیم کنید',
favorite: 'نشان کردن',
cancelFavorite: 'لغو نشان کردن',
useProxy: 'استفاده از پراکسی',
useProxyHelper: 'از آدرس سرور پراکسی در تنظیمات پنل استفاده کنید',
westCN: 'وست دیجیتال',
+2 -2
View File
@@ -142,6 +142,8 @@ const message = {
manageGroup: 'グループ管理',
backToList: 'リストに戻る',
keepEdit: '編集を続ける',
pin: '上部に固定',
unpin: '固定を解除',
default: 'デフォルト',
noRefresh: '自動更新なし',
creator: '作成者',
@@ -3772,8 +3774,6 @@ const message = {
cacheWarn: 'まずリバースプロキシのキャッシュスイッチをオフにしてください',
loadBalanceHelper:
'負荷分散を作成した後リバースプロキシに移動しプロキシを追加してバックエンドアドレスを次のように設定してくださいhttp://<負荷分散名>。',
favorite: 'お気に入り',
cancelFavorite: 'お気に入りを解除',
useProxy: 'プロキシを使用',
useProxyHelper: 'パネル設定のプロキシサーバーアドレスを使用',
westCN: '西部デジタル',
+2 -2
View File
@@ -142,6 +142,8 @@ const message = {
manageGroup: '그룹 관리',
backToList: '목록으로 돌아가기',
keepEdit: '계속 편집',
pin: '상단 고정',
unpin: '고정 해제',
default: '기본값',
noRefresh: '자동 새로고침 ',
creator: '생성자',
@@ -3691,8 +3693,6 @@ const message = {
cacheWarn: '먼저 리버스 프록시의 캐시 스위치를 끄십시오',
loadBalanceHelper:
'로드 밸런싱을 생성한 , "리버스 프록시" 이동하여 프록시를 추가하고 백엔드 주소를 다음으로 설정하세요: http://<로드 밸런싱 이름>.',
favorite: '즐겨찾기',
cancelFavorite: '즐겨찾기 취소',
useProxy: '프록시 사용',
useProxyHelper: '패널 설정의 프록시 서버 주소 사용',
westCN: '서부 디지털',
+2 -2
View File
@@ -150,6 +150,8 @@ const message = {
manageGroup: 'ຈັດການກຸ່ມ',
backToList: 'ກັບຄືນໄປລາຍການ',
keepEdit: 'ແກ້ໄຂຕໍ່',
pin: 'ປັກໝຸດໄວ້ເທິງ',
unpin: 'ຍົກເລີກການປັກໝຸດ',
},
loadingText: {
Upgrading: 'ກຳລັງອັບເກຣດລະບົບ, ກະລຸນາຖ້າ...',
@@ -3672,8 +3674,6 @@ const message = {
cacheWarn: 'ກະລຸນາປິດແຄຊໃນ reverse proxy ກ່ອນ',
loadBalanceHelper:
'ຫຼັງຈາກສ້າງການກະຈາຍພາລະແລ້ວ, ໃຫ້ໄປທີ່ "Reverse Proxy", ເພີ່ມ proxy ແລະ ຕັ້ງທີ່ຢູ່ເບື້ອງຫຼັງເປັນ: http://<ຊື່ load balancing>',
favorite: 'ເພີ່ມໃນລາຍການທີ່ມັກ',
cancelFavorite: 'ຍົກເລີກລາຍການທີ່ມັກ',
useProxy: 'ໃຊ້ Proxy',
useProxyHelper: 'ໃຊ້ທີ່ຢູ່ເຊີບເວີ proxy ໃນການຕັ້ງຄ່າແຜງຄວບຄຸມ',
westCN: 'West Digital',
+2 -2
View File
@@ -142,6 +142,8 @@ const message = {
manageGroup: 'Urus Kumpulan',
backToList: 'Kembali ke Senarai',
keepEdit: 'Teruskan Mengedit',
pin: 'Semat ke atas',
unpin: 'Nyahsemat',
default: 'Lalai',
noRefresh: 'Tiada segar semula auto',
creator: 'Pencipta',
@@ -3825,8 +3827,6 @@ const message = {
cacheWarn: 'Sila matikan suis cache dalam pembalikan proksi terlebih dahulu',
loadBalanceHelper:
'Setelah mencipta pengimbang beban, sila pergi ke "Reverse Proxy", tambahkan proksi dan tetapkan alamat backend ke: http://<nama pengimbang beban>.',
favorite: 'Kegemaran',
cancelFavorite: 'Batalkan Kegemaran',
useProxy: 'Gunakan Proksi',
useProxyHelper: 'Gunakan alamat pelayan proksi dalam tetapan panel',
westCN: 'West Digital',
+2 -2
View File
@@ -142,6 +142,8 @@ const message = {
manageGroup: 'Gerenciar Grupos',
backToList: 'Voltar à Lista',
keepEdit: 'Continuar Editando',
pin: 'Fixar no topo',
unpin: 'Desafixar',
default: 'Padrão',
noRefresh: 'Sem atualização',
creator: 'Criador',
@@ -3961,8 +3963,6 @@ const message = {
cacheWarn: 'Por favor, desligue o interruptor de cache no proxy reverso primeiro',
loadBalanceHelper:
'Após criar o balanceamento de carga, para "Proxy Reverso", adicione um proxy e configure o endereço de backend para: http://<nome do balanceamento de carga>.',
favorite: 'Favorito',
cancelFavorite: 'Cancelar Favorito',
useProxy: 'Usar Proxy',
useProxyHelper: 'Usar o endereço do servidor proxy nas configurações do painel',
westCN: 'West Digital',
+2 -2
View File
@@ -142,6 +142,8 @@ const message = {
manageGroup: 'Управление группами',
backToList: 'Вернуться к списку',
keepEdit: 'Продолжить редактирование',
pin: 'Закрепить сверху',
unpin: 'Открепить',
default: 'По умолчанию',
noRefresh: 'Без автообновления',
creator: 'Создатель',
@@ -3814,8 +3816,6 @@ const message = {
cacheWarn: 'Пожалуйста, сначала выключите кэш в обратном прокси',
loadBalanceHelper:
'После создания балансировки нагрузки, пожалуйста, перейдите в "Обратный прокси", добавьте прокси и установите адрес бэкенда на: http://<название балансировки нагрузки>.',
favorite: 'Избранное',
cancelFavorite: 'Отменить избранное',
useProxy: 'Использовать прокси',
useProxyHelper: 'Использовать адрес прокси-сервера в настройках панели',
westCN: 'Западный цифровой',
+2 -2
View File
@@ -147,6 +147,8 @@ const message = {
manageGroup: 'Grupları Yönet',
backToList: 'Listeye Dön',
keepEdit: 'Düzenlemeye Devam Et',
pin: 'Üste sabitle',
unpin: 'Sabitlemeyi kaldır',
creator: 'Oluşturan',
updater: 'Güncelleyen',
},
@@ -3816,8 +3818,6 @@ const message = {
cacheWarn: 'Lütfen önce ters vekildeki önbellek anahtarını kapatın',
loadBalanceHelper:
'Bu bölüm yalnızca yük dengeleme kuralları oluşturur, kuralları kullanmak için lütfen http(s)://<yük dengeleme adı> adresine ters vekil yapın',
favorite: 'Favori',
cancelFavorite: 'Favoriyi İptal Et',
useProxy: 'Vekil Kullan',
useProxyHelper: 'Panel ayarlarındaki vekil sunucu adresini kullan',
westCN: 'Batı Dijital',
+2 -2
View File
@@ -143,6 +143,8 @@ const message = {
manageGroup: '管理群組',
backToList: '返回列表',
keepEdit: '繼續編輯',
pin: '置頂',
unpin: '取消置頂',
creator: '建立者',
updater: '更新者',
},
@@ -3490,8 +3492,6 @@ const message = {
runtimePortWarn: '目前執行環境沒有埠無法代理',
cacheWarn: '請先關閉反代中的快取開關',
loadBalanceHelper: '建立負載均衡後請前往反向代理新增代理並將後端地址設定為http://<負載均衡名稱>',
favorite: '收藏',
cancelFavorite: '取消收藏',
useProxy: '使用代理',
useProxyHelper: '使用面板設定中的代理伺服器地址',
westCN: '西部數碼',
+2 -2
View File
@@ -138,6 +138,8 @@ const message = {
manageGroup: '管理分组',
backToList: '返回列表',
keepEdit: '继续编辑',
pin: '置顶',
unpin: '取消置顶',
},
loadingText: {
Upgrading: '系统升级中请稍候...',
@@ -3491,8 +3493,6 @@ const message = {
runtimePortWarn: '当前运行环境没有端口无法代理',
cacheWarn: '请先关闭反代中的缓存开关',
loadBalanceHelper: '创建负载均衡后请前往反向代理添加代理并将后端地址设置为http://<负载均衡名称>。',
favorite: '收藏',
cancelFavorite: '取消收藏',
useProxy: '使用代理',
useProxyHelper: '使用面板设置中的代理服务器地址',
westCN: '西部数码',
@@ -21,9 +21,7 @@
<div v-for="row in group.items" :key="row.name" class="node-table__row">
<div class="node-table__cell node-table__cell--node">
<el-tooltip
:content="
row.isFavorite ? $t('website.cancelFavorite') : $t('website.favorite')
"
:content="row.isFavorite ? $t('commons.table.unpin') : $t('commons.table.pin')"
placement="left"
>
<el-button
@@ -31,10 +29,11 @@
link
v-if="isAdmin"
:type="row.isFavorite ? 'warning' : 'info'"
:icon="row.isFavorite ? 'StarFilled' : 'Star'"
:loading="favoriteLoadingIDs.includes(row.id)"
@click.stop="toggleFavorite(row)"
/>
>
<SvgIcon iconName="p-pushpin" className="node-pin-icon" />
</el-button>
</el-tooltip>
<div class="node-name-cell">
<SvgIcon class="icon" iconName="p-zhuji" />
@@ -309,6 +308,13 @@ watch(
height: 20px;
min-height: 20px;
padding: 0;
:deep(.node-pin-icon) {
width: 1em;
height: 1em;
padding: 0;
vertical-align: middle;
}
}
.node-name-cell {
@@ -83,29 +83,21 @@
<span class="ml-1" v-if="mode === 'installed'">
<el-tooltip
effect="dark"
:content="$t('website.cancelFavorite')"
:content="installed.favorite ? $t('commons.table.unpin') : $t('commons.table.pin')"
placement="top-start"
v-if="installed.favorite"
>
<el-button
v-permission
link
size="large"
icon="StarFilled"
type="warning"
:type="installed.favorite ? 'warning' : 'info'"
:disabled="sortMode"
@click="$emit('favoriteInstall')"
></el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('website.favorite')" placement="top-start" v-else>
<el-button
v-permission
link
icon="Star"
type="info"
:disabled="sortMode"
@click="$emit('favoriteInstall')"
></el-button>
>
<el-icon>
<SvgIcon iconName="p-pushpin" />
</el-icon>
</el-button>
</el-tooltip>
</span>
</div>
+64 -3
View File
@@ -38,7 +38,7 @@
:class="{ 'is-active': currentCompose?.name === row.name && !isOnCreate }"
>
<div class="font-medium text-base compose-title">
{{ row.name }}
<span class="compose-title__name">{{ row.name }}</span>
<el-tooltip
v-if="!row.composeFileExists"
:content="$t('container.composeFileMissing')"
@@ -47,6 +47,24 @@
<WarningFilled />
</el-button>
</el-tooltip>
<el-tooltip
:content="
row.isPinned
? $t('commons.table.unpin')
: $t('commons.table.pin')
"
>
<el-button
class="compose-pin-button"
:class="{ 'is-pinned': row.isPinned }"
link
:type="row.isPinned ? 'warning' : 'info'"
v-permission
@click.stop="changePinned(row)"
>
<svg-icon iconName="p-pushpin" className="compose-pin-icon" />
</el-button>
</el-tooltip>
</div>
<div class="mb-1">
<el-text class="w-12" link size="small" type="info">
@@ -222,7 +240,10 @@
</div>
<el-popover placement="right" width="500px" class="float-right">
<template #reference>
<svg-icon iconName="p-xiangqing" class="svg-icon"></svg-icon>
<svg-icon
iconName="p-xiangqing"
className="resource-detail-icon"
></svg-icon>
</template>
<template #default>
<el-descriptions
@@ -494,6 +515,7 @@ import Backups from '@/components/backup/index.vue';
import Uploads from '@/components/upload/index.vue';
import {
composeOperate,
composePin,
composeUpdate,
containerItemStats,
containerListStats,
@@ -653,6 +675,15 @@ const search = async (withRefreshDetail?: boolean) => {
});
};
const changePinned = async (row: Container.ComposeInfo) => {
await composePin({
name: row.name,
isPinned: !row.isPinned,
});
await search(true);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
const loadDetail = async (row: Container.ComposeInfo, withRefresh: boolean) => {
if (currentCompose.value?.name === row.name && withRefresh !== true) {
return;
@@ -958,9 +989,39 @@ const onOpenLog = (row: any) => {
}
.compose-title {
display: flex;
align-items: center;
gap: 4px;
transition: color 0.15s;
}
.compose-title__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compose-pin-button {
flex-shrink: 0;
margin-left: auto;
opacity: 0.72;
transition: opacity 0.2s;
&:hover,
&:focus-visible,
&.is-pinned {
opacity: 1;
}
:deep(.compose-pin-icon) {
width: 1em;
height: 1em;
padding: 0;
vertical-align: middle;
}
}
.compose-item.is-active .compose-title {
color: var(--el-color-primary);
}
@@ -980,7 +1041,7 @@ const onOpenLog = (row: any) => {
max-height: 40px;
}
.svg-icon {
.resource-detail-icon {
margin-top: -3px;
font-size: 6px;
cursor: pointer;
@@ -63,8 +63,6 @@
row-key="containerID"
@sort-change="search"
@search="search"
@cell-mouse-enter="showFavorite"
@cell-mouse-leave="hideFavorite"
:row-style="{ height: '65px' }"
style="width: 100%"
:columns="columns"
@@ -82,24 +80,26 @@
:fixed="isMobile ? false : 'left'"
show-overflow-tooltip
>
<template #default="{ row, $index }">
<template #default="{ row }">
<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"
:content="row.isPinned ? $t('commons.table.unpin') : $t('commons.table.pin')"
>
<el-button
class="container-pin-button"
:class="{ 'is-pinned': row.isPinned }"
link
size="large"
:icon="row.isPinned ? 'StarFilled' : 'Star'"
type="warning"
:type="row.isPinned ? 'warning' : 'info'"
v-permission
@click="changePinned(row, true)"
/>
>
<svg-icon iconName="p-pushpin" className="container-pin-icon" />
</el-button>
</el-tooltip>
</div>
</template>
@@ -183,7 +183,7 @@
</div>
<el-popover placement="right" width="500px" class="float-right">
<template #reference>
<svg-icon iconName="p-xiangqing" class="svg-icon"></svg-icon>
<svg-icon iconName="p-xiangqing" className="resource-detail-icon"></svg-icon>
</template>
<template #default>
<el-descriptions direction="vertical" border :column="3" size="small">
@@ -550,7 +550,6 @@ const taskLogRef = ref();
const tags = ref([]);
const activeTag = ref('all');
const hoveredRowIndex = ref(-1);
const activeDropdownContainerId = ref('');
const statFields = [
'cpuTotalUsage',
@@ -753,12 +752,6 @@ 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,
@@ -1102,7 +1095,7 @@ onMounted(() => {
.source-font {
font-size: 12px;
}
.svg-icon {
.resource-detail-icon {
margin-top: -3px;
font-size: 6px;
cursor: pointer;
@@ -1122,4 +1115,24 @@ onMounted(() => {
text-overflow: ellipsis;
white-space: nowrap;
}
.container-pin-button {
opacity: 0.72;
transition: opacity 0.2s;
&:hover,
&:focus-visible {
opacity: 1;
}
&.is-pinned {
opacity: 1;
}
:deep(.container-pin-icon) {
width: 1em;
height: 1em;
padding: 0;
vertical-align: middle;
}
}
</style>
+28 -16
View File
@@ -36,30 +36,30 @@
: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="180">
<template #default="{ row, $index }">
<template #default="{ row }">
<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"
:content="row.isPinned ? $t('commons.table.unpin') : $t('commons.table.pin')"
>
<el-button
class="image-pin-button"
:class="{ 'is-pinned': row.isPinned }"
link
size="large"
:icon="row.isPinned ? 'StarFilled' : 'Star'"
type="warning"
:type="row.isPinned ? 'warning' : 'info'"
v-permission
@click="changePinned(row, true)"
/>
>
<svg-icon iconName="p-pushpin" className="image-pin-icon" />
</el-button>
</el-tooltip>
</div>
</template>
@@ -204,8 +204,6 @@ const columns = ref([]);
const isActive = ref(false);
const isExist = ref(false);
const hoveredRowIndex = ref(-1);
const myDetail = ref();
const dialogPullRef = ref();
const dialogTagRef = ref();
@@ -282,12 +280,6 @@ const onSubmitDelete = async () => {
});
};
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:', ''),
@@ -493,3 +485,23 @@ const buttons = [
},
];
</script>
<style scoped lang="scss">
.image-pin-button {
opacity: 0.72;
transition: opacity 0.2s;
&:hover,
&:focus-visible,
&.is-pinned {
opacity: 1;
}
:deep(.image-pin-icon) {
width: 1em;
height: 1em;
padding: 0;
vertical-align: middle;
}
}
</style>
@@ -63,24 +63,22 @@
<el-button v-permission link icon="edit" class="ml-2.5" @click="startEdit" v-if="!isEditing"></el-button>
</div>
<div v-if="showFavorite">
<el-tooltip effect="dark" :content="$t('website.cancelFavorite')" placement="top-start" v-if="row.favorite">
<el-button
v-permission
link
:size="hideName ? 'default' : 'large'"
icon="StarFilled"
type="warning"
@click="favoriteWebsite(row)"
></el-button>
</el-tooltip>
<el-tooltip
effect="dark"
:content="$t('website.favorite')"
:content="row.favorite ? $t('commons.table.unpin') : $t('commons.table.pin')"
placement="top-start"
v-if="!row.favorite && isHovered"
>
<el-button v-permission link icon="Star" type="info" @click="favoriteWebsite(row)"></el-button>
<el-button
v-permission
class="website-pin-button"
:class="{ 'is-pinned': row.favorite }"
link
:size="hideName ? 'default' : 'large'"
:type="row.favorite ? 'warning' : 'info'"
@click="favoriteWebsite(row)"
>
<svg-icon iconName="p-pushpin" className="website-pin-icon" />
</el-button>
</el-tooltip>
</div>
</div>
@@ -95,7 +93,6 @@ import { Rules } from '@/global/form-rules';
import { GetPunyCodeDomain, isPunycoded } from '@/utils/misc';
interface Props {
row: Website.Website;
isHovered: boolean;
defaultHttpPort: number;
defaultHttpsPort: number;
hideName?: boolean;
@@ -287,4 +284,22 @@ const shouldShowDomainTooltip = (domain: string) => {
word-break: break-all;
line-height: 1.5;
}
.website-pin-button {
opacity: 0.72;
transition: opacity 0.2s;
}
.website-pin-button:hover,
.website-pin-button:focus-visible,
.website-pin-button.is-pinned {
opacity: 1;
}
.website-pin-button :deep(.website-pin-icon) {
width: 1em;
height: 1em;
padding: 0;
vertical-align: middle;
}
</style>
+1 -14
View File
@@ -75,8 +75,6 @@
:class="{ mask: disabledConfig }"
:heightDiff="310"
:columns="columns"
@cell-mouse-enter="showFavorite"
@cell-mouse-leave="hideFavorite"
localKey="websiteColumn"
v-model:selects="selects"
:tooltip-options="{
@@ -92,7 +90,7 @@
sortable
card-type="name"
>
<template #default="{ row, $index, viewMode: columnViewMode }">
<template #default="{ row, viewMode: columnViewMode }">
<div v-if="columnViewMode === 'card'" class="website-card-domain">
<el-text
type="primary"
@@ -104,7 +102,6 @@
<Domain
class="website-card-domain__actions"
:row="row"
:is-hovered="true"
:defaultHttpPort="appStatusRef?.getHttpPort?.() || 0"
:defaultHttpsPort="appStatusRef?.getHttpsPort?.() || 0"
:hide-name="true"
@@ -116,7 +113,6 @@
<Domain
v-else
:row="row"
:is-hovered="hoveredRowIndex === $index"
:defaultHttpPort="appStatusRef?.getHttpPort?.() || 0"
:defaultHttpsPort="appStatusRef?.getHttpsPort?.() || 0"
@favorite-change="favoriteWebsite"
@@ -415,7 +411,6 @@ const data = ref();
let groups = ref<Group.GroupInfo[]>([]);
const dataRef = ref();
const columns = ref([]);
const hoveredRowIndex = ref(-1);
const websiteDir = ref();
const selects = ref([]);
const batchReq = reactive({
@@ -459,14 +454,6 @@ const goRouter = async (key: string) => {
routerToNameWithQuery('AppAll', { install: key });
};
const showFavorite = (row: any) => {
hoveredRowIndex.value = data.value.findIndex((item) => item === row);
};
const hideFavorite = () => {
hoveredRowIndex.value = -1;
};
const favoriteWebsite = (row: Website.Website) => {
row.favorite = !row.favorite;
updateWebsitConfig(row);