fix: Optimize container cleanup mechanism (#9795)

Refs #7444

涉及到容器、镜像、网络、存储卷、构建缓存清理
This commit is contained in:
ssongliu 2025-08-01 16:07:24 +08:00 committed by GitHub
parent ba4307c1dc
commit 9c292bebf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 280 additions and 169 deletions

View file

@ -315,7 +315,7 @@ func (b *BaseApi) ContainerUpgrade(c *gin.Context) {
// @Summary Clean container // @Summary Clean container
// @Accept json // @Accept json
// @Param request body dto.ContainerPrune true "request" // @Param request body dto.ContainerPrune true "request"
// @Success 200 {object} dto.ContainerPruneReport // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /containers/prune [post] // @Router /containers/prune [post]
@ -326,12 +326,11 @@ func (b *BaseApi) ContainerPrune(c *gin.Context) {
return return
} }
report, err := containerService.Prune(req) if err := containerService.Prune(req); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, report) helper.Success(c)
} }
// @Tags Container // @Tags Container

View file

@ -138,7 +138,7 @@ func (b *BaseApi) ImagePush(c *gin.Context) {
// @Summary Delete image // @Summary Delete image
// @Accept json // @Accept json
// @Param request body dto.BatchDelete true "request" // @Param request body dto.BatchDelete true "request"
// @Success 200 {object} dto.ContainerPruneReport // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /containers/image/remove [post] // @Router /containers/image/remove [post]
@ -149,13 +149,12 @@ func (b *BaseApi) ImageRemove(c *gin.Context) {
return return
} }
data, err := imageService.ImageRemove(req) if err := imageService.ImageRemove(req); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, data) helper.Success(c)
} }
// @Tags Container Image // @Tags Container Image

View file

@ -174,15 +174,11 @@ type ContainerCommit struct {
} }
type ContainerPrune struct { type ContainerPrune struct {
TaskID string `json:"taskID"`
PruneType string `json:"pruneType" validate:"required,oneof=container image volume network buildcache"` PruneType string `json:"pruneType" validate:"required,oneof=container image volume network buildcache"`
WithTagAll bool `json:"withTagAll"` WithTagAll bool `json:"withTagAll"`
} }
type ContainerPruneReport struct {
DeletedNumber int `json:"deletedNumber"`
SpaceReclaimed int `json:"spaceReclaimed"`
}
type Network struct { type Network struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -227,8 +223,9 @@ type VolumeCreate struct {
} }
type BatchDelete struct { type BatchDelete struct {
Force bool `json:"force"` TaskID string `json:"taskID"`
Names []string `json:"names" validate:"required"` Force bool `json:"force"`
Names []string `json:"names" validate:"required"`
} }
type ComposeInfo struct { type ComposeInfo struct {

View file

@ -82,7 +82,7 @@ type IContainerService interface {
CreateVolume(req dto.VolumeCreate) error CreateVolume(req dto.VolumeCreate) error
TestCompose(req dto.ComposeCreate) (bool, error) TestCompose(req dto.ComposeCreate) (bool, error)
ComposeUpdate(req dto.ComposeUpdate) error ComposeUpdate(req dto.ComposeUpdate) error
Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) Prune(req dto.ContainerPrune) error
LoadUsers(req dto.OperationWithName) []string LoadUsers(req dto.OperationWithName) []string
@ -417,66 +417,93 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
return string(bytes), nil return string(bytes), nil
} }
func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) { func (u *ContainerService) Prune(req dto.ContainerPrune) error {
report := dto.ContainerPruneReport{}
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return report, err return err
} }
defer client.Close() defer client.Close()
pruneFilters := filters.NewArgs() name := ""
if req.WithTagAll {
pruneFilters.Add("dangling", "false")
if req.PruneType != "image" {
pruneFilters.Add("until", "24h")
}
}
switch req.PruneType { switch req.PruneType {
case "container": case "container":
rep, err := client.ContainersPrune(context.Background(), pruneFilters) name = "Container"
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.ContainersDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
case "image": case "image":
rep, err := client.ImagesPrune(context.Background(), pruneFilters) name = "Image"
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.ImagesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
case "network":
rep, err := client.NetworksPrune(context.Background(), pruneFilters)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.NetworksDeleted)
case "volume": case "volume":
versions, err := client.ServerVersion(context.Background()) name = "Volume"
if err != nil {
return report, err
}
if common.ComparePanelVersion(versions.APIVersion, "1.42") {
pruneFilters.Add("all", "true")
}
rep, err := client.VolumesPrune(context.Background(), pruneFilters)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.VolumesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
case "buildcache": case "buildcache":
opts := build.CachePruneOptions{} name = "BuildCache"
opts.All = true case "network":
rep, err := client.BuildCachePrune(context.Background(), opts) name = "Network"
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.CachesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
} }
return report, nil taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey(name), task.TaskClean, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
pruneFilters := filters.NewArgs()
if req.WithTagAll {
pruneFilters.Add("dangling", "false")
if req.PruneType != "image" {
pruneFilters.Add("until", "24h")
}
}
DeletedNumber := 0
SpaceReclaimed := 0
switch req.PruneType {
case "container":
rep, err := client.ContainersPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.ContainersDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
case "image":
rep, err := client.ImagesPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.ImagesDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
case "network":
rep, err := client.NetworksPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.NetworksDeleted)
case "volume":
versions, err := client.ServerVersion(context.Background())
if err != nil {
return err
}
if common.ComparePanelVersion(versions.APIVersion, "1.42") {
pruneFilters.Add("all", "true")
}
rep, err := client.VolumesPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.VolumesDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
case "buildcache":
opts := build.CachePruneOptions{}
opts.All = true
rep, err := client.BuildCachePrune(context.Background(), opts)
if err != nil {
return err
}
DeletedNumber = len(rep.CachesDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
}
taskItem.Log(i18n.GetMsgWithMap("PruneHelper", map[string]interface{}{"name": i18n.GetMsgByKey(name), "count": DeletedNumber, "size": common.LoadSizeUnit2F(float64(SpaceReclaimed))}))
return nil
}, nil)
go func() {
_ = taskItem.Execute()
}()
return nil
} }
func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) { func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) {
@ -1241,29 +1268,29 @@ func checkImageExist(client *client.Client, imageItem string) bool {
} }
func checkImageLike(client *client.Client, imageName string) bool { func checkImageLike(client *client.Client, imageName string) bool {
if client == nil { if client == nil {
var err error var err error
client, err = docker.NewDockerClient() client, err = docker.NewDockerClient()
if err != nil { if err != nil {
return false return false
} }
} }
images, err := client.ImageList(context.Background(), image.ListOptions{}) images, err := client.ImageList(context.Background(), image.ListOptions{})
if err != nil { if err != nil {
return false return false
} }
for _, img := range images { for _, img := range images {
for _, tag := range img.RepoTags { for _, tag := range img.RepoTags {
parts := strings.Split(tag, "/") parts := strings.Split(tag, "/")
imageNameWithTag := parts[len(parts)-1] imageNameWithTag := parts[len(parts)-1]
if imageNameWithTag == imageName { if imageNameWithTag == imageName {
return true return true
} }
} }
} }
return false return false
} }
func pullImages(task *task.Task, client *client.Client, imageName string) error { func pullImages(task *task.Task, client *client.Client, imageName string) error {

View file

@ -21,6 +21,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@ -41,7 +42,7 @@ type IImageService interface {
ImageLoad(req dto.ImageLoad) error ImageLoad(req dto.ImageLoad) error
ImageSave(req dto.ImageSave) error ImageSave(req dto.ImageSave) error
ImagePush(req dto.ImagePush) error ImagePush(req dto.ImagePush) error
ImageRemove(req dto.BatchDelete) (dto.ContainerPruneReport, error) ImageRemove(req dto.BatchDelete) error
ImageTag(req dto.ImageTag) error ImageTag(req dto.ImageTag) error
} }
@ -386,34 +387,44 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
return nil return nil
} }
func (u *ImageService) ImageRemove(req dto.BatchDelete) (dto.ContainerPruneReport, error) { func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
report := dto.ContainerPruneReport{}
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return report, err return err
} }
defer client.Close() defer client.Close()
for _, id := range req.Names { taskItem, err := task.NewTaskWithOps(task.TaskScopeImage, task.TaskDelete, task.TaskScopeContainer, req.TaskID, 1)
imageItem, _, err := client.ImageInspectWithRaw(context.TODO(), id) if err != nil {
if err != nil { global.LOG.Errorf("new task for create container failed, err: %v", err)
return report, err return err
}
if _, err := client.ImageRemove(context.TODO(), id, image.RemoveOptions{Force: req.Force, PruneChildren: true}); err != nil {
if strings.Contains(err.Error(), "image is being used") || strings.Contains(err.Error(), "is using") {
if strings.Contains(id, "sha256:") {
return report, buserr.New("ErrObjectInUsed")
}
return report, buserr.WithDetail("ErrInUsed", id, nil)
}
if strings.Contains(err.Error(), "image has dependent") {
return report, buserr.New("ErrObjectBeDependent")
}
return report, err
}
report.DeletedNumber++
report.SpaceReclaimed += int(imageItem.Size)
} }
return report, nil
for _, id := range req.Names {
taskItem.AddSubTask(i18n.GetMsgByKey("TaskDelete")+id, func(t *task.Task) error {
imageItem, err := client.ImageInspect(context.TODO(), id)
if err != nil {
return err
}
if _, err := client.ImageRemove(context.TODO(), id, image.RemoveOptions{Force: req.Force, PruneChildren: true}); err != nil {
if strings.Contains(err.Error(), "image is being used") || strings.Contains(err.Error(), "is using") {
if strings.Contains(id, "sha256:") {
return buserr.New("ErrObjectInUsed")
}
return buserr.WithDetail("ErrInUsed", id, nil)
}
if strings.Contains(err.Error(), "image has dependent") {
return buserr.New("ErrObjectBeDependent")
}
return err
}
taskItem.Log(i18n.GetMsgWithMap("ImageRemoveHelper", map[string]interface{}{"name": id, "size": common.LoadSizeUnit2F(float64(imageItem.Size))}))
return nil
}, nil)
}
go func() {
_ = taskItem.Execute()
}()
return nil
} }
func formatFileSize(fileSize int64) (size string) { func formatFileSize(fileSize int64) (size string) {

View file

@ -65,6 +65,7 @@ const (
TaskPull = "TaskPull" TaskPull = "TaskPull"
TaskCommit = "TaskCommit" TaskCommit = "TaskCommit"
TaskPush = "TaskPush" TaskPush = "TaskPush"
TaskClean = "TaskClean"
TaskHandle = "TaskHandle" TaskHandle = "TaskHandle"
) )

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'The object is in use and cannot be deleted'
ErrObjectBeDependent: 'This image depends on other images and cannot be deleted' ErrObjectBeDependent: 'This image depends on other images and cannot be deleted'
ErrPortRules: 'Port number does not match, please re-enter!' ErrPortRules: 'Port number does not match, please re-enter!'
ErrPgImagePull: 'Image pull timed out, please configure image acceleration or manually pull the {{ .name }} image and try again' ErrPgImagePull: 'Image pull timed out, please configure image acceleration or manually pull the {{ .name }} image and try again'
PruneHelper: "This cleanup removed {{ .name }} {{ .count }} items, freeing {{ .size }} disk space"
ImageRemoveHelper: "Deleted image {{ .name }}, freeing {{ .size }} disk space"
BuildCache: "Build cache"
Volume: "Storage volume"
Network: "Network"
#runtime #runtime
ErrFileNotExist: '{{ .detail }} file does not exist! Please check the integrity of the source file!' ErrFileNotExist: '{{ .detail }} file does not exist! Please check the integrity of the source file!'
@ -290,6 +295,7 @@ TaskPull: 'Pull'
TaskCommit: 'Commit' TaskCommit: 'Commit'
TaskBuild: 'Build' TaskBuild: 'Build'
TaskPush: 'Push' TaskPush: 'Push'
TaskClean: "Cleanup"
TaskHandle: 'Execute' TaskHandle: 'Execute'
Website: 'Website' Website: 'Website'
App: 'Application' App: 'Application'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'オブジェクトは使用中のため削除できません'
ErrObjectBeDependent: 'このイメージは他のイメージに依存しているため、削除できません' ErrObjectBeDependent: 'このイメージは他のイメージに依存しているため、削除できません'
ErrPortRules: 'ポート番号が一致しません。再入力してください。' ErrPortRules: 'ポート番号が一致しません。再入力してください。'
ErrPgImagePull: 'イメージのプルがタイムアウトしました。イメージのアクセラレーションを設定するか、{{ .name }} イメージを手動でプルして再試行してください' ErrPgImagePull: 'イメージのプルがタイムアウトしました。イメージのアクセラレーションを設定するか、{{ .name }} イメージを手動でプルして再試行してください'
PruneHelper: "今回のクリーンアップで{{ .name }} {{ .count }}個を削除し、{{ .size }}のディスク領域を解放しました"
ImageRemoveHelper: "イメージ{{ .name }}を削除し、{{ .size }}のディスク領域を解放しました"
BuildCache: "ビルドキャッシュ"
Volume: "ストレージボリューム"
Network: "ネットワーク"
#runtime #runtime
ErrFileNotExist: '{{ .detail }} ファイルが存在しません。ソース ファイルの整合性を確認してください。' ErrFileNotExist: '{{ .detail }} ファイルが存在しません。ソース ファイルの整合性を確認してください。'
@ -290,6 +295,7 @@ TaskPull: 'プル'
TaskCommit: 'コミット' TaskCommit: 'コミット'
TaskBuild: 'ビルド' TaskBuild: 'ビルド'
TaskPush: 'プッシュ' TaskPush: 'プッシュ'
TaskClean: "クリーンアップ"
TaskHandle: '実行' TaskHandle: '実行'
Website: 'ウェブサイト' Website: 'ウェブサイト'
App: 'アプリケーション' App: 'アプリケーション'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: '개체가 사용 중이므로 삭제할 수 없습니다'
ErrObjectBeDependent: '이 이미지는 다른 이미지에 의존하므로 삭제할 수 없습니다' ErrObjectBeDependent: '이 이미지는 다른 이미지에 의존하므로 삭제할 수 없습니다'
ErrPortRules: '포트 번호가 일치하지 않습니다. 다시 입력하세요!' ErrPortRules: '포트 번호가 일치하지 않습니다. 다시 입력하세요!'
ErrPgImagePull: '이미지 풀링 시간이 초과되었습니다. 이미지 가속을 구성하거나 {{ .name }} 이미지를 수동으로 풀링한 다음 다시 시도하세요.' ErrPgImagePull: '이미지 풀링 시간이 초과되었습니다. 이미지 가속을 구성하거나 {{ .name }} 이미지를 수동으로 풀링한 다음 다시 시도하세요.'
PruneHelper: "이번 정리에서 {{ .name }} {{ .count }}개를 제거하여 {{ .size }} 디스크 공간을 확보했습니다"
ImageRemoveHelper: "이미지 {{ .name }} 삭제, {{ .size }} 디스크 공간 확보"
BuildCache: "빌드 캐시"
Volume: "스토리지 볼륨"
Network: "네트워크"
#실행 시간 #실행 시간
ErrFileNotExist: '{{ .detail }} 파일이 존재하지 않습니다! 소스 파일의 무결성을 확인하세요!' ErrFileNotExist: '{{ .detail }} 파일이 존재하지 않습니다! 소스 파일의 무결성을 확인하세요!'
@ -290,6 +295,7 @@ TaskPull: '당기기'
TaskCommit: '커밋' TaskCommit: '커밋'
TaskBuild: '빌드' TaskBuild: '빌드'
TaskPush: '푸시' TaskPush: '푸시'
TaskClean: "정리"
TaskHandle: '실행' TaskHandle: '실행'
Website: '웹사이트' Website: '웹사이트'
App: '애플리케이션' App: '애플리케이션'

View file

@ -172,6 +172,11 @@ ErrObjectInUsed: 'Objek sedang digunakan dan tidak boleh dipadamkan'
ErrObjectBeDependent: 'Imej ini bergantung pada imej lain dan tidak boleh dipadamkan' ErrObjectBeDependent: 'Imej ini bergantung pada imej lain dan tidak boleh dipadamkan'
ErrPortRules: 'Nombor port tidak sepadan, sila masukkan semula!' ErrPortRules: 'Nombor port tidak sepadan, sila masukkan semula!'
ErrPgImagePull: 'Tarikh imej tamat masa, sila konfigurasikan pecutan imej atau tarik imej {{ .name }} secara manual dan cuba lagi' ErrPgImagePull: 'Tarikh imej tamat masa, sila konfigurasikan pecutan imej atau tarik imej {{ .name }} secara manual dan cuba lagi'
PruneHelper: "Pembersihan ini membuang {{ .name }} {{ .count }} item, membebaskan {{ .size }} ruang cakera"
ImageRemoveHelper: "Padam imej {{ .name }}, membebaskan {{ .size }} ruang cakera"
BuildCache: "Cache binaan"
Volume: "Jilid storan"
Network: "Rangkaian"
#masa berjalan #masa berjalan
ErrFileNotExist: 'Fail {{ .detail }} tidak wujud! Sila semak integriti fail sumber!' ErrFileNotExist: 'Fail {{ .detail }} tidak wujud! Sila semak integriti fail sumber!'
@ -289,6 +294,7 @@ TaskPull: 'Tarik'
TaskCommit: 'Komit' TaskCommit: 'Komit'
TaskBuild: 'Bina' TaskBuild: 'Bina'
TaskPush: 'Tolak' TaskPush: 'Tolak'
TaskClean: "Pembersihan"
TaskHandle: 'Laksanakan' TaskHandle: 'Laksanakan'
Website: 'Laman web' Website: 'Laman web'
App: 'Aplikasi' App: 'Aplikasi'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'O objeto está em uso e não pode ser excluído'
ErrObjectBeDependent: 'Esta imagem depende de outras imagens e não pode ser excluída' ErrObjectBeDependent: 'Esta imagem depende de outras imagens e não pode ser excluída'
ErrPortRules: 'O número da porta não corresponde, digite novamente!' ErrPortRules: 'O número da porta não corresponde, digite novamente!'
ErrPgImagePull: 'Tempo limite para extração de imagem. Configure a aceleração de imagem ou extraia manualmente a imagem {{ .name }} e tente novamente' ErrPgImagePull: 'Tempo limite para extração de imagem. Configure a aceleração de imagem ou extraia manualmente a imagem {{ .name }} e tente novamente'
PruneHelper: "Esta limpeza removeu {{ .name }} {{ .count }} itens, liberando {{ .size }} de espaço em disco"
ImageRemoveHelper: "Excluída a imagem {{ .name }}, liberando {{ .size }} de espaço em disco"
BuildCache: "Cache de construção"
Volume: "Volume de armazenamento"
Network: "Rede"
#tempo de execução #tempo de execução
ErrFileNotExist: 'O arquivo {{ .detail }} não existe! Verifique a integridade do arquivo de origem!' ErrFileNotExist: 'O arquivo {{ .detail }} não existe! Verifique a integridade do arquivo de origem!'
@ -290,6 +295,7 @@ TaskPull: 'Puxar'
TaskCommit: 'Commit' TaskCommit: 'Commit'
TaskBuild: 'Construir' TaskBuild: 'Construir'
TaskPush: 'Empurrar' TaskPush: 'Empurrar'
TaskClean: "Limpeza"
TaskHandle: 'Executar' TaskHandle: 'Executar'
Website: 'Site' Website: 'Site'
App: 'Aplicativo' App: 'Aplicativo'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'Объект используется и не может бы
ErrObjectBeDependent: 'Это изображение зависит от других изображений и не может быть удалено' ErrObjectBeDependent: 'Это изображение зависит от других изображений и не может быть удалено'
ErrPortRules: 'Номер порта не совпадает, введите заново!' ErrPortRules: 'Номер порта не совпадает, введите заново!'
ErrPgImagePull: 'Время извлечения изображения истекло. Настройте ускорение изображения или вручную извлеките изображение {{ .name }} и повторите попытку' ErrPgImagePull: 'Время извлечения изображения истекло. Настройте ускорение изображения или вручную извлеките изображение {{ .name }} и повторите попытку'
PruneHelper: "Очистка удалила {{ .name }} в количестве {{ .count }}, освободив {{ .size }} дискового пространства"
ImageRemoveHelper: "Удалён образ {{ .name }}, освобождено {{ .size }} дискового пространства"
BuildCache: "Кэш сборки"
Volume: "Том хранилища"
Network: "Сеть"
#время выполнения #время выполнения
ErrFileNotExist: 'Файл {{ .detail }} не существует! Проверьте целостность исходного файла!' ErrFileNotExist: 'Файл {{ .detail }} не существует! Проверьте целостность исходного файла!'
@ -290,6 +295,7 @@ TaskPull: 'Вытянуть'
TaskCommit: 'Kоммит' TaskCommit: 'Kоммит'
ЗадачаСборка: 'Сборка' ЗадачаСборка: 'Сборка'
TaskPush: 'Push' TaskPush: 'Push'
TaskClean: "Очистка"
TaskHandle: 'Выполнить' TaskHandle: 'Выполнить'
Website: 'Be6-сайт' Website: 'Be6-сайт'
App: 'Приложение' App: 'Приложение'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'Nesne kullanımda ve silinemez'
ErrObjectBeDependent: 'Bu image diğer imagelere bağlı ve silinemez' ErrObjectBeDependent: 'Bu image diğer imagelere bağlı ve silinemez'
ErrPortRules: 'Port numarası eşleşmiyor, lütfen yeniden girin!' ErrPortRules: 'Port numarası eşleşmiyor, lütfen yeniden girin!'
ErrPgImagePull: 'Image çekme zaman aşımı, lütfen image hızlandırma yapılandırın veya manuel olarak {{ .name }} imageını çekin ve tekrar deneyin' ErrPgImagePull: 'Image çekme zaman aşımı, lütfen image hızlandırma yapılandırın veya manuel olarak {{ .name }} imageını çekin ve tekrar deneyin'
PruneHelper: "Bu temizleme {{ .name }} {{ .count }} öğe kaldırdı, {{ .size }} disk alanı boşalttı"
ImageRemoveHelper: "{{ .name }} imajı silindi, {{ .size }} disk alanı boşalttı"
BuildCache: "Derleme önbelleği"
Volume: "Depolama hacmi"
Network: "Ağ"
#runtime #runtime
ErrFileNotExist: '{{ .detail }} dosyası mevcut değil! Lütfen kaynak dosyanın bütünlüğünü kontrol edin!' ErrFileNotExist: '{{ .detail }} dosyası mevcut değil! Lütfen kaynak dosyanın bütünlüğünü kontrol edin!'
@ -288,6 +293,7 @@ TaskPull: 'Çek'
TaskCommit: 'işleme' TaskCommit: 'işleme'
TaskBuild: 'Yapı' TaskBuild: 'Yapı'
TaskPush: 'Gönder' TaskPush: 'Gönder'
TaskClean: "Temizleme"
TaskHandle: 'Yürüt' TaskHandle: 'Yürüt'
Website: 'Web Sitesi' Website: 'Web Sitesi'
App: 'Uygulama' App: 'Uygulama'

View file

@ -172,6 +172,11 @@ ErrObjectInUsed: '該物件正被使用,無法刪除'
ErrObjectBeDependent: '此鏡像依賴其他鏡像,無法刪除' ErrObjectBeDependent: '此鏡像依賴其他鏡像,無法刪除'
ErrPortRules: '連接埠數目不匹配,請重新輸入!' ErrPortRules: '連接埠數目不匹配,請重新輸入!'
ErrPgImagePull: '鏡像拉取逾時,請配置鏡像加速或手動拉取{{ .name }} 鏡像後重試' ErrPgImagePull: '鏡像拉取逾時,請配置鏡像加速或手動拉取{{ .name }} 鏡像後重試'
PruneHelper: "本次清理 {{ .name }} {{ .count }} 個,釋放磁盤空間 {{ .size }}"
ImageRemoveHelper: "刪除鏡像 {{ .name }} ,釋放磁盤空間 {{ .size }}"
BuildCache: "構建緩存"
Volume: "存儲卷"
Network: "網絡"
#runtime #runtime
ErrFileNotExist: '{{ .detail }} 檔案不存在!請檢查來源檔案完整性!' ErrFileNotExist: '{{ .detail }} 檔案不存在!請檢查來源檔案完整性!'
@ -289,6 +294,7 @@ TaskPull: '拉取'
TaskCommit: '制作' TaskCommit: '制作'
TaskBuild: '建置' TaskBuild: '建置'
TaskPush: '推送' TaskPush: '推送'
TaskClean: "清理"
TaskHandle: '執行' TaskHandle: '執行'
Website: '網站' Website: '網站'
App: '應用程式' App: '應用程式'

View file

@ -172,6 +172,11 @@ ErrObjectInUsed: "该对象正被使用,无法删除"
ErrObjectBeDependent: "该镜像依赖于其他镜像,无法删除" ErrObjectBeDependent: "该镜像依赖于其他镜像,无法删除"
ErrPortRules: "端口数目不匹配,请重新输入!" ErrPortRules: "端口数目不匹配,请重新输入!"
ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 {{ .name }} 镜像后重试" ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 {{ .name }} 镜像后重试"
PruneHelper: "本次清理 {{ .name }} {{ .count }} 个,释放磁盘空间 {{ .size }}"
ImageRemoveHelper: "删除镜像 {{ .name }} ,释放磁盘空间 {{ .size }}"
BuildCache: "构建缓存"
Volume: "存储卷"
Network: "网络"
#runtime #runtime
ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!" ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!"
@ -289,6 +294,7 @@ TaskPull: "拉取"
TaskCommit: "制作" TaskCommit: "制作"
TaskBuild: "构建" TaskBuild: "构建"
TaskPush: "推送" TaskPush: "推送"
TaskClean: "清理"
TaskHandle: "执行" TaskHandle: "执行"
Website: "网站" Website: "网站"
App: "应用" App: "应用"

View file

@ -1,6 +1,12 @@
import { ReqPage } from '.'; import { ReqPage } from '.';
export namespace Cronjob { export namespace Cronjob {
export interface Search extends ReqPage {
info: string;
groupIDs: Array<number>;
orderBy?: string;
order?: string;
}
export interface CronjobInfo { export interface CronjobInfo {
id: number; id: number;
name: string; name: string;

View file

@ -20,7 +20,6 @@ export interface ReqPage {
} }
export interface SearchWithPage { export interface SearchWithPage {
info: string; info: string;
groupIDs: Array<number>;
page: number; page: number;
pageSize: number; pageSize: number;
orderBy?: string; orderBy?: string;

View file

@ -53,7 +53,7 @@ export const containerOperator = (params: Container.ContainerOperate) => {
return http.post(`/containers/operate`, params); return http.post(`/containers/operate`, params);
}; };
export const containerPrune = (params: Container.ContainerPrune) => { export const containerPrune = (params: Container.ContainerPrune) => {
return http.post<Container.ContainerPruneReport>(`/containers/prune`, params); return http.post(`/containers/prune`, params);
}; };
export const inspect = (params: Container.ContainerInspect) => { export const inspect = (params: Container.ContainerInspect) => {
return http.post<string>(`/containers/inspect`, params); return http.post<string>(`/containers/inspect`, params);
@ -95,7 +95,7 @@ export const imageTag = (params: Container.ImageTag) => {
return http.post(`/containers/image/tag`, params); return http.post(`/containers/image/tag`, params);
}; };
export const imageRemove = (params: Container.BatchDelete) => { export const imageRemove = (params: Container.BatchDelete) => {
return http.post<Container.ContainerPruneReport>(`/containers/image/remove`, params); return http.post(`/containers/image/remove`, params);
}; };
// network // network

View file

@ -3,7 +3,7 @@ import { ResPage, SearchWithPage } from '../interface';
import { Cronjob } from '../interface/cronjob'; import { Cronjob } from '../interface/cronjob';
import { TimeoutEnum } from '@/enums/http-enum'; import { TimeoutEnum } from '@/enums/http-enum';
export const searchCronjobPage = (params: SearchWithPage) => { export const searchCronjobPage = (params: Cronjob.Search) => {
return http.post<ResPage<Cronjob.CronjobInfo>>(`/cronjobs/search`, params); return http.post<ResPage<Cronjob.CronjobInfo>>(`/cronjobs/search`, params);
}; };

View file

@ -18,42 +18,45 @@
</span> </span>
</template> </template>
</DialogPro> </DialogPro>
<TaskLog ref="taskLogRef" width="70%" @close="onSearch" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { containerPrune } from '@/api/modules/container'; import { containerPrune } from '@/api/modules/container';
import i18n from '@/lang'; import TaskLog from '@/components/log/task/index.vue';
import { MsgSuccess } from '@/utils/message';
import { ref } from 'vue'; import { ref } from 'vue';
import { computeSize } from '@/utils/util'; import { newUUID } from '@/utils/util';
const loading = ref(false); const loading = ref(false);
const open = ref<boolean>(false); const open = ref<boolean>(false);
const taskLogRef = ref();
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
const onClean = async () => { const onClean = async () => {
loading.value = true; loading.value = true;
let params = { let params = {
taskID: newUUID(),
pruneType: 'container', pruneType: 'container',
withTagAll: false, withTagAll: false,
}; };
await containerPrune(params) await containerPrune(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
open.value = false; open.value = false;
emit('search'); openTaskLog(params.taskID);
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}; };
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const onSearch = () => {
emit('search');
};
const acceptParams = (): void => { const acceptParams = (): void => {
open.value = true; open.value = true;

View file

@ -33,7 +33,7 @@
</template> </template>
<template #main> <template #main>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="300"> <ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="300">
<el-table-column label="ID" prop="id" width="140" show-overflow-tooltip> <el-table-column label="ID" prop="id" width="140">
<template #default="{ row }"> <template #default="{ row }">
<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) }}
@ -92,12 +92,13 @@
<Build ref="dialogBuildRef" @search="search" /> <Build ref="dialogBuildRef" @search="search" />
<Delete ref="dialogDeleteRef" @search="search" /> <Delete ref="dialogDeleteRef" @search="search" />
<Prune ref="dialogPruneRef" @search="search" /> <Prune ref="dialogPruneRef" @search="search" />
<TaskLog ref="taskLogRef" width="70%" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, computed } from 'vue'; import { reactive, ref, computed } from 'vue';
import { dateFormat } from '@/utils/util'; import { dateFormat, newUUID } from '@/utils/util';
import { Container } from '@/api/interface/container'; import { Container } from '@/api/interface/container';
import Pull from '@/views/container/image/pull/index.vue'; import Pull from '@/views/container/image/pull/index.vue';
import Tag from '@/views/container/image/tag/index.vue'; import Tag from '@/views/container/image/tag/index.vue';
@ -109,13 +110,14 @@ import Delete from '@/views/container/image/delete/index.vue';
import Prune from '@/views/container/image/prune/index.vue'; import Prune from '@/views/container/image/prune/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue'; import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import TaskLog from '@/components/log/task/index.vue';
import { searchImage, listImageRepo, imageRemove, inspect, containerPrune } from '@/api/modules/container'; import { searchImage, listImageRepo, imageRemove, inspect, containerPrune } from '@/api/modules/container';
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 { MsgSuccess } from '@/utils/message';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const taskLogRef = ref();
const mobile = computed(() => { const mobile = computed(() => {
return globalStore.isMobile(); return globalStore.isMobile();
}); });
@ -220,13 +222,14 @@ const onOpenBuildCache = () => {
}).then(async () => { }).then(async () => {
loading.value = true; loading.value = true;
let params = { let params = {
taskID: newUUID(),
pruneType: 'buildcache', pruneType: 'buildcache',
withTagAll: false, withTagAll: false,
}; };
await containerPrune(params) await containerPrune(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber])); openTaskLog(params.taskID);
search(); search();
}) })
.catch(() => { .catch(() => {
@ -234,6 +237,9 @@ const onOpenBuildCache = () => {
}); });
}); });
}; };
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const onOpenload = () => { const onOpenload = () => {
dialogLoadRef.value!.acceptParams(); dialogLoadRef.value!.acceptParams();

View file

@ -38,13 +38,14 @@
</span> </span>
</template> </template>
</DrawerPro> </DrawerPro>
<TaskLog ref="taskLogRef" width="70%" @close="onSearch" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { containerPrune, imageRemove, listAllImage } from '@/api/modules/container'; import { containerPrune, imageRemove, listAllImage } from '@/api/modules/container';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { newUUID } from '@/utils/util';
import { computeSize } from '@/utils/util';
import { ref } from 'vue'; import { ref } from 'vue';
const dialogVisible = ref(false); const dialogVisible = ref(false);
@ -54,6 +55,7 @@ const loading = ref();
const unTagList = ref([]); const unTagList = ref([]);
const unUsedList = ref([]); const unUsedList = ref([]);
const data = ref([]); const data = ref([]);
const taskLogRef = ref();
const checkAll = ref(false); const checkAll = ref(false);
const isIndeterminate = ref(false); const isIndeterminate = ref(false);
@ -120,6 +122,10 @@ const handleClose = () => {
dialogVisible.value = false; dialogVisible.value = false;
}; };
const onSearch = () => {
emit('search');
};
const onClean = async () => { const onClean = async () => {
loading.value = true; loading.value = true;
if (checkAll.value) { if (checkAll.value) {
@ -131,41 +137,34 @@ const onClean = async () => {
const prune = async () => { const prune = async () => {
let params = { let params = {
taskID: newUUID(),
pruneType: 'image', pruneType: 'image',
withTagAll: scope.value === 'unused', withTagAll: scope.value === 'unused',
}; };
await containerPrune(params) await containerPrune(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
dialogVisible.value = false; dialogVisible.value = false;
MsgSuccess( openTaskLog(params.taskID);
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
emit('search');
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}; };
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const removeImage = async () => { const removeImage = async () => {
let params = { let params = {
taskID: newUUID(),
names: checkedLists.value, names: checkedLists.value,
}; };
await imageRemove(params) await imageRemove(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
dialogVisible.value = false; dialogVisible.value = false;
MsgSuccess( openTaskLog(params.taskID);
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
emit('search');
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;

View file

@ -88,6 +88,7 @@
<OpDialog ref="opRef" @search="search" /> <OpDialog ref="opRef" @search="search" />
<CodemirrorDrawer ref="myDetail" /> <CodemirrorDrawer ref="myDetail" />
<CreateDialog @search="search" ref="dialogCreateRef" /> <CreateDialog @search="search" ref="dialogCreateRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div> </div>
</template> </template>
@ -95,16 +96,17 @@
import CreateDialog from '@/views/container/network/create/index.vue'; import CreateDialog from '@/views/container/network/create/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue'; import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { dateFormat } from '@/utils/util'; import { dateFormat, newUUID } from '@/utils/util';
import { deleteNetwork, searchNetwork, inspect, containerPrune } from '@/api/modules/container'; import { deleteNetwork, searchNetwork, inspect, containerPrune } from '@/api/modules/container';
import { Container } from '@/api/interface/container'; import { Container } from '@/api/interface/container';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import DockerStatus from '@/views/container/docker-status/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue';
const loading = ref(); const loading = ref();
const myDetail = ref(); const myDetail = ref();
const taskLogRef = ref();
const data = ref(); const data = ref();
const selects = ref<any>([]); const selects = ref<any>([]);
@ -136,20 +138,23 @@ const onClean = () => {
}).then(async () => { }).then(async () => {
loading.value = true; loading.value = true;
let params = { let params = {
taskID: newUUID(),
pruneType: 'network', pruneType: 'network',
withTagAll: false, withTagAll: false,
}; };
await containerPrune(params) await containerPrune(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber])); openTaskLog(params.taskID);
search();
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}); });
}; };
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
function selectable(row) { function selectable(row) {
return !row.isSystem; return !row.isSystem;

View file

@ -83,6 +83,7 @@
<CodemirrorDrawer ref="myDetail" /> <CodemirrorDrawer ref="myDetail" />
<CreateDialog @search="search" ref="dialogCreateRef" /> <CreateDialog @search="search" ref="dialogCreateRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div> </div>
</template> </template>
@ -91,16 +92,17 @@ import CreateDialog from '@/views/container/volume/create/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue'; import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import DockerStatus from '@/views/container/docker-status/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue';
import { reactive, ref, computed } from 'vue'; import { reactive, ref, computed } from 'vue';
import { computeSize, dateFormat } from '@/utils/util'; import { dateFormat, newUUID } from '@/utils/util';
import { deleteVolume, searchVolume, inspect, containerPrune } from '@/api/modules/container'; import { deleteVolume, searchVolume, inspect, containerPrune } from '@/api/modules/container';
import { Container } from '@/api/interface/container'; import { Container } from '@/api/interface/container';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import router from '@/routers'; import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const taskLogRef = ref();
const mobile = computed(() => { const mobile = computed(() => {
return globalStore.isMobile(); return globalStore.isMobile();
}); });
@ -174,25 +176,23 @@ const onClean = () => {
}).then(async () => { }).then(async () => {
loading.value = true; loading.value = true;
let params = { let params = {
taskID: newUUID(),
pruneType: 'volume', pruneType: 'volume',
withTagAll: false, withTagAll: false,
}; };
await containerPrune(params) await containerPrune(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess( openTaskLog(params.taskID);
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
search();
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}); });
}; };
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const batchDelete = async (row: Container.VolumeInfo | null) => { const batchDelete = async (row: Container.VolumeInfo | null) => {
let names = []; let names = [];

View file

@ -125,6 +125,7 @@
<Config ref="configRef" /> <Config ref="configRef" />
<Supervisor ref="supervisorRef" /> <Supervisor ref="supervisorRef" />
<Terminal ref="terminalRef" /> <Terminal ref="terminalRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div> </div>
</template> </template>
@ -132,10 +133,10 @@
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime'; import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, RuntimeDeleteCheck, SearchRuntimes } from '@/api/modules/runtime'; import { DeleteRuntime, RuntimeDeleteCheck, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat } from '@/utils/util'; import { dateFormat, newUUID } from '@/utils/util';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container'; import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message'; import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import ExtManagement from './extension-management/index.vue'; import ExtManagement from './extension-management/index.vue';
import Extensions from './extension-template/index.vue'; import Extensions from './extension-template/index.vue';
@ -158,6 +159,7 @@ const mobile = computed(() => {
return globalStore.isMobile(); return globalStore.isMobile();
}); });
const taskLogRef = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size', cacheSizeKey: 'runtime-page-size',
currentPage: 1, currentPage: 1,
@ -354,20 +356,23 @@ const onOpenBuildCache = () => {
}).then(async () => { }).then(async () => {
loading.value = true; loading.value = true;
let params = { let params = {
taskID: newUUID(),
pruneType: 'buildcache', pruneType: 'buildcache',
withTagAll: false, withTagAll: false,
}; };
await containerPrune(params) await containerPrune(params)
.then((res) => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber])); openTaskLog(params.taskID);
search();
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}); });
}; };
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const toFolder = (folder: string) => { const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } }); router.push({ path: '/hosts/files', query: { path: folder } });