Add prune function to image and container

This commit is contained in:
cuigh 2021-12-23 15:25:51 +08:00
parent 70837391d1
commit bb48beec82
17 changed files with 234 additions and 41 deletions

View File

@ -18,6 +18,7 @@ type ContainerHandler struct {
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"container.delete" desc:"delete container"` Delete web.HandlerFunc `path:"/delete" method:"post" auth:"container.delete" desc:"delete container"`
FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"container.logs" desc:"fetch logs of container"` FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"container.logs" desc:"fetch logs of container"`
Connect web.HandlerFunc `path:"/connect" auth:"container.execute" desc:"connect to a running container"` Connect web.HandlerFunc `path:"/connect" auth:"container.execute" desc:"connect to a running container"`
Prune web.HandlerFunc `path:"/prune" method:"post" auth:"container.delete" desc:"delete unused containers"`
} }
// NewContainer creates an instance of ContainerHandler // NewContainer creates an instance of ContainerHandler
@ -28,6 +29,7 @@ func NewContainer(b biz.ContainerBiz) *ContainerHandler {
Delete: containerDelete(b), Delete: containerDelete(b),
FetchLogs: containerFetchLogs(b), FetchLogs: containerFetchLogs(b),
Connect: containerConnect(b), Connect: containerConnect(b),
Prune: containerPrune(b),
} }
} }
@ -78,11 +80,12 @@ func containerDelete(b biz.ContainerBiz) web.HandlerFunc {
type Args struct { type Args struct {
Node string `json:"node"` Node string `json:"node"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"`
} }
return func(ctx web.Context) (err error) { return func(ctx web.Context) (err error) {
args := &Args{} args := &Args{}
if err = ctx.Bind(args); err == nil { if err = ctx.Bind(args); err == nil {
err = b.Delete(args.Node, args.ID, ctx.User()) err = b.Delete(args.Node, args.ID, args.Name, ctx.User())
} }
return ajax(ctx, err) return ajax(ctx, err)
} }
@ -214,3 +217,25 @@ func containerConnect(b biz.ContainerBiz) web.HandlerFunc {
return nil return nil
} }
} }
func containerPrune(b biz.ContainerBiz) web.HandlerFunc {
type Args struct {
Node string `json:"node"`
}
return func(ctx web.Context) (err error) {
args := &Args{}
if err = ctx.Bind(args); err != nil {
return err
}
count, size, err := b.Prune(args.Node, ctx.User())
if err != nil {
return err
}
return success(ctx, data.Map{
"count": count,
"size": size,
})
}
}

View File

@ -11,6 +11,7 @@ type ImageHandler struct {
Search web.HandlerFunc `path:"/search" auth:"image.view" desc:"search images"` Search web.HandlerFunc `path:"/search" auth:"image.view" desc:"search images"`
Find web.HandlerFunc `path:"/find" auth:"image.view" desc:"find image by id"` Find web.HandlerFunc `path:"/find" auth:"image.view" desc:"find image by id"`
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"image.delete" desc:"delete image"` Delete web.HandlerFunc `path:"/delete" method:"post" auth:"image.delete" desc:"delete image"`
Prune web.HandlerFunc `path:"/prune" method:"post" auth:"image.delete" desc:"delete unused images"`
} }
// NewImage creates an instance of ImageHandler // NewImage creates an instance of ImageHandler
@ -19,6 +20,7 @@ func NewImage(b biz.ImageBiz) *ImageHandler {
Search: imageSearch(b), Search: imageSearch(b),
Find: imageFind(b), Find: imageFind(b),
Delete: imageDelete(b), Delete: imageDelete(b),
Prune: imagePrune(b),
} }
} }
@ -77,3 +79,25 @@ func imageDelete(b biz.ImageBiz) web.HandlerFunc {
return ajax(ctx, err) return ajax(ctx, err)
} }
} }
func imagePrune(b biz.ImageBiz) web.HandlerFunc {
type Args struct {
Node string `json:"node"`
}
return func(ctx web.Context) (err error) {
args := &Args{}
if err = ctx.Bind(args); err != nil {
return err
}
count, size, err := b.Prune(args.Node, ctx.User())
if err != nil {
return err
}
return success(ctx, data.Map{
"count": count,
"size": size,
})
}
}

View File

@ -103,14 +103,14 @@ func volumePrune(b biz.VolumeBiz) web.HandlerFunc {
return err return err
} }
deletedVolumes, reclaimedSpace, err := b.Prune(args.Node, ctx.User()) count, size, err := b.Prune(args.Node, ctx.User())
if err != nil { if err != nil {
return err return err
} }
return success(ctx, data.Map{ return success(ctx, data.Map{
"deletedVolumes": deletedVolumes, "count": count,
"reclaimedSpace": reclaimedSpace, "size": size,
}) })
} }
} }

View File

@ -14,19 +14,21 @@ import (
type ContainerBiz interface { type ContainerBiz interface {
Search(node, name, status string, pageIndex, pageSize int) ([]*Container, int, error) Search(node, name, status string, pageIndex, pageSize int) ([]*Container, int, error)
Find(node, id string) (container *Container, raw string, err error) Find(node, id string) (container *Container, raw string, err error)
Delete(node, id string, user web.User) (err error) Delete(node, id, name string, user web.User) (err error)
FetchLogs(node, id string, lines int, timestamps bool) (stdout, stderr string, err error) FetchLogs(node, id string, lines int, timestamps bool) (stdout, stderr string, err error)
ExecCreate(node, id string, cmd string) (resp types.IDResponse, err error) ExecCreate(node, id string, cmd string) (resp types.IDResponse, err error)
ExecAttach(node, id string) (resp types.HijackedResponse, err error) ExecAttach(node, id string) (resp types.HijackedResponse, err error)
ExecStart(node, id string) error ExecStart(node, id string) error
Prune(node string, user web.User) (count int, size uint64, err error)
} }
func NewContainer(d *docker.Docker) ContainerBiz { func NewContainer(d *docker.Docker, eb EventBiz) ContainerBiz {
return &containerBiz{d: d} return &containerBiz{d: d, eb: eb}
} }
type containerBiz struct { type containerBiz struct {
d *docker.Docker d *docker.Docker
eb EventBiz
} }
func (b *containerBiz) Find(node, id string) (c *Container, raw string, err error) { func (b *containerBiz) Find(node, id string) (c *Container, raw string, err error) {
@ -58,11 +60,11 @@ func (b *containerBiz) Search(node, name, status string, pageIndex, pageSize int
return containers, total, nil return containers, total, nil
} }
func (b *containerBiz) Delete(node, id string, user web.User) (err error) { func (b *containerBiz) Delete(node, id, name string, user web.User) (err error) {
err = b.d.ContainerRemove(context.TODO(), node, id) err = b.d.ContainerRemove(context.TODO(), node, id)
//if err == nil { if err == nil {
// Event.CreateContainer(model.EventActionDelete, id, user) b.eb.CreateContainer(EventActionDelete, id, name, user)
//} }
return return
} }
@ -86,6 +88,15 @@ func (b *containerBiz) FetchLogs(node, id string, lines int, timestamps bool) (s
return stdout.String(), stderr.String(), nil return stdout.String(), stderr.String(), nil
} }
func (b *containerBiz) Prune(node string, user web.User) (count int, size uint64, err error) {
var report types.ContainersPruneReport
if report, err = b.d.ContainerPrune(context.TODO(), node); err == nil {
count, size = len(report.ContainersDeleted), report.SpaceReclaimed
b.eb.CreateContainer(EventActionPrune, "", "", user)
}
return
}
type Container struct { type Container struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -21,6 +21,8 @@ const (
EventTypeStack EventType = "Stack" EventTypeStack EventType = "Stack"
EventTypeConfig EventType = "Config" EventTypeConfig EventType = "Config"
EventTypeSecret EventType = "Secret" EventTypeSecret EventType = "Secret"
EventTypeImage EventType = "Image"
EventTypeContainer EventType = "Container"
EventTypeVolume EventType = "Volume" EventTypeVolume EventType = "Volume"
EventTypeUser EventType = "User" EventTypeUser EventType = "User"
EventTypeRole EventType = "Role" EventTypeRole EventType = "Role"
@ -54,6 +56,8 @@ type EventBiz interface {
CreateConfig(action EventAction, id, name string, user web.User) CreateConfig(action EventAction, id, name string, user web.User)
CreateSecret(action EventAction, id, name string, user web.User) CreateSecret(action EventAction, id, name string, user web.User)
CreateStack(action EventAction, name string, user web.User) CreateStack(action EventAction, name string, user web.User)
CreateImage(action EventAction, id string, user web.User)
CreateContainer(action EventAction, id, name string, user web.User)
CreateVolume(action EventAction, name string, user web.User) CreateVolume(action EventAction, name string, user web.User)
CreateUser(action EventAction, id, name string, user web.User) CreateUser(action EventAction, id, name string, user web.User)
CreateRole(action EventAction, id, name string, user web.User) CreateRole(action EventAction, id, name string, user web.User)
@ -110,6 +114,14 @@ func (b *eventBiz) CreateNode(action EventAction, id, name string, user web.User
b.create(EventTypeNode, action, id, name, user) b.create(EventTypeNode, action, id, name, user)
} }
func (b *eventBiz) CreateImage(action EventAction, id string, user web.User) {
b.create(EventTypeImage, action, id, "", user)
}
func (b *eventBiz) CreateContainer(action EventAction, id, name string, user web.User) {
b.create(EventTypeContainer, action, id, name, user)
}
func (b *eventBiz) CreateVolume(action EventAction, name string, user web.User) { func (b *eventBiz) CreateVolume(action EventAction, name string, user web.User) {
b.create(EventTypeVolume, action, name, name, user) b.create(EventTypeVolume, action, name, name, user)
} }

View File

@ -15,14 +15,16 @@ type ImageBiz interface {
Search(node, name string, pageIndex, pageSize int) ([]*Image, int, error) Search(node, name string, pageIndex, pageSize int) ([]*Image, int, error)
Find(node, name string) (image *Image, raw string, err error) Find(node, name string) (image *Image, raw string, err error)
Delete(node, id string, user web.User) (err error) Delete(node, id string, user web.User) (err error)
Prune(node string, user web.User) (count int, size uint64, err error)
} }
func NewImage(d *docker.Docker) ImageBiz { func NewImage(d *docker.Docker, eb EventBiz) ImageBiz {
return &imageBiz{d: d} return &imageBiz{d: d, eb: eb}
} }
type imageBiz struct { type imageBiz struct {
d *docker.Docker d *docker.Docker
eb EventBiz
} }
func (b *imageBiz) Find(node, id string) (img *Image, raw string, err error) { func (b *imageBiz) Find(node, id string) (img *Image, raw string, err error) {
@ -62,9 +64,18 @@ func (b *imageBiz) Search(node, name string, pageIndex, pageSize int) (images []
func (b *imageBiz) Delete(node, id string, user web.User) (err error) { func (b *imageBiz) Delete(node, id string, user web.User) (err error) {
err = b.d.ImageRemove(context.TODO(), node, id) err = b.d.ImageRemove(context.TODO(), node, id)
//if err == nil { if err == nil {
// Event.CreateImage(model.EventActionDelete, id, user) b.eb.CreateImage(EventActionDelete, id, user)
//} }
return
}
func (b *imageBiz) Prune(node string, user web.User) (count int, size uint64, err error) {
var report types.ImagesPruneReport
if report, err = b.d.ImagePrune(context.TODO(), node); err == nil {
count, size = len(report.ImagesDeleted), report.SpaceReclaimed
b.eb.CreateImage(EventActionPrune, "", user)
}
return return
} }

View File

@ -16,7 +16,7 @@ type VolumeBiz interface {
Find(node, name string) (volume *Volume, raw string, err error) Find(node, name string) (volume *Volume, raw string, err error)
Delete(node, name string, user web.User) (err error) Delete(node, name string, user web.User) (err error)
Create(volume *Volume, user web.User) (err error) Create(volume *Volume, user web.User) (err error)
Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) Prune(node string, user web.User) (count int, size uint64, err error)
} }
func NewVolume(d *docker.Docker, eb EventBiz) VolumeBiz { func NewVolume(d *docker.Docker, eb EventBiz) VolumeBiz {
@ -85,11 +85,11 @@ func (b *volumeBiz) Create(vol *Volume, user web.User) (err error) {
return return
} }
func (b *volumeBiz) Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) { func (b *volumeBiz) Prune(node string, user web.User) (count int, size uint64, err error) {
var report types.VolumesPruneReport var report types.VolumesPruneReport
report, err = b.d.VolumePrune(context.TODO(), node) report, err = b.d.VolumePrune(context.TODO(), node)
if err == nil { if err == nil {
deletedVolumes, reclaimedSpace = report.VolumesDeleted, report.SpaceReclaimed count, size = len(report.VolumesDeleted), report.SpaceReclaimed
b.eb.CreateVolume(EventActionPrune, "", user) b.eb.CreateVolume(EventActionPrune, "", user)
} }
return return

View File

@ -136,3 +136,12 @@ func (d *Docker) ContainerLogs(ctx context.Context, node, id string, lines int,
} }
return return
} }
// ContainerPrune remove all unused containers.
func (d *Docker) ContainerPrune(ctx context.Context, node string) (report types.ContainersPruneReport, err error) {
var c *client.Client
if c, err = d.agent(node); err == nil {
report, err = c.ContainersPrune(ctx, filters.NewArgs())
}
return
}

View File

@ -59,3 +59,12 @@ func (d *Docker) ImageRemove(ctx context.Context, node, id string) error {
} }
return err return err
} }
// ImagePrune remove all unused images.
func (d *Docker) ImagePrune(ctx context.Context, node string) (report types.ImagesPruneReport, err error) {
var c *client.Client
if c, err = d.agent(node); err == nil {
report, err = c.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "false")))
}
return
}

View File

@ -79,6 +79,13 @@ export class ContainerApi {
stderr: string; stderr: string;
}>('/container/fetch-logs', args) }>('/container/fetch-logs', args)
} }
prune(node: string) {
return ajax.post<{
count: number;
size: number;
}>('/container/prune', { node })
}
} }
export default new ContainerApi export default new ContainerApi

View File

@ -74,6 +74,13 @@ export class ImageApi {
delete(node: string, id: string, name: string) { delete(node: string, id: string, name: string) {
return ajax.post<Result<Object>>('/image/delete', { node, id, name }) return ajax.post<Result<Object>>('/image/delete', { node, id, name })
} }
prune(node: string) {
return ajax.post<{
count: number;
size: number;
}>('/image/prune', { node })
}
} }
export default new ImageApi export default new ImageApi

View File

@ -36,11 +36,6 @@ export interface FindResult {
raw: string; raw: string;
} }
export interface PruneResult {
deletedVolumes: string[];
reclaimedSpace: number;
}
export class VolumeApi { export class VolumeApi {
find(node: string, name: string) { find(node: string, name: string) {
return ajax.get<FindResult>('/volume/find', { node, name }) return ajax.get<FindResult>('/volume/find', { node, name })
@ -59,7 +54,10 @@ export class VolumeApi {
} }
prune(node: string) { prune(node: string) {
return ajax.post<PruneResult>('/volume/prune', { node }) return ajax.post<{
count: number;
size: number;
}>('/volume/prune', { node })
} }
} }

View File

@ -233,6 +233,14 @@ export default {
"export_chart": { "export_chart": {
"title": "Export chart", "title": "Export chart",
}, },
"prune_image": {
"title": "Prune image",
"body": "Are you sure you want to clean up unused images?",
},
"prune_container": {
"title": "Prune container",
"body": "Are you sure you want to clean up stopped containers?",
},
"prune_volume": { "prune_volume": {
"title": "Prune volume", "title": "Prune volume",
"body": "Are you sure you want to clean up unused data volumes?", "body": "Are you sure you want to clean up unused data volumes?",
@ -384,6 +392,8 @@ export default {
"service_notice": "This service belong to '%s' stack, usually you should modify original compose instead of updating it directly.", "service_notice": "This service belong to '%s' stack, usually you should modify original compose instead of updating it directly.",
"password_notice": "You are a LDAP user, can not modify password here.", "password_notice": "You are a LDAP user, can not modify password here.",
"setting_notice": "You must restart Swirl to activate modifications.", "setting_notice": "You must restart Swirl to activate modifications.",
"prune_image_success": "A total of {count} images are cleaned up to free up {size} of space.",
"prune_container_success": "A total of {count} containers are cleaned up to free up {size} of space.",
"prune_volume_success": "A total of {count} data volumes are cleaned up to free up {size} of space.", "prune_volume_success": "A total of {count} data volumes are cleaned up to free up {size} of space.",
"403": "Sorry, you don't have permission to access this page", "403": "Sorry, you don't have permission to access this page",
"404": "The page you are requesting has been moved, deleted, renamed, or may not exist", "404": "The page you are requesting has been moved, deleted, renamed, or may not exist",

View File

@ -233,6 +233,14 @@ export default {
"export_chart": { "export_chart": {
"title": "导出图表", "title": "导出图表",
}, },
"prune_image": {
"title": "清理镜像",
"body": "是否确实要清理未使用的镜像?",
},
"prune_container": {
"title": "清理容器",
"body": "是否确实要清理已停止的容器?",
},
"prune_volume": { "prune_volume": {
"title": "清理数据卷", "title": "清理数据卷",
"body": "是否确实要清理未使用的数据卷?", "body": "是否确实要清理未使用的数据卷?",
@ -384,6 +392,8 @@ export default {
"service_notice": "这个服务属于编排 {stack},通常你应该修改原始的编排而不是直接修改此服务信息,否则后续重新部署编排时这些修改将会丢失。", "service_notice": "这个服务属于编排 {stack},通常你应该修改原始的编排而不是直接修改此服务信息,否则后续重新部署编排时这些修改将会丢失。",
"password_notice": "你是 LDAP 用户,不能在这里修改密码。", "password_notice": "你是 LDAP 用户,不能在这里修改密码。",
"setting_notice": "你必须重启 Swirl 才能让设置生效。", "setting_notice": "你必须重启 Swirl 才能让设置生效。",
"prune_image_success": "共清理 {count} 个镜像,释放 {size} 空间。",
"prune_container_success": "共清理 {count} 个容器,释放 {size} 空间。",
"prune_volume_success": "共清理 {count} 个数据卷,释放 {size} 空间。", "prune_volume_success": "共清理 {count} 个数据卷,释放 {size} 空间。",
"403": "很抱歉,你没有权限访问此页面", "403": "很抱歉,你没有权限访问此页面",
"404": "您要请求的页面已被移动、删除、重命名或可能不存在", "404": "您要请求的页面已被移动、删除、重命名或可能不存在",

View File

@ -1,5 +1,16 @@
<template> <template>
<x-page-header /> <x-page-header>
<template #action>
<n-button secondary size="small" type="warning" @click="prune">
<template #icon>
<n-icon>
<close-icon />
</n-icon>
</template>
{{ t('buttons.prune') }}
</n-button>
</template>
</x-page-header>
<n-space class="page-body" vertical :size="12"> <n-space class="page-body" vertical :size="12">
<n-space :size="12"> <n-space :size="12">
<n-select <n-select
@ -38,13 +49,15 @@ import {
NDataTable, NDataTable,
NInput, NInput,
NSelect, NSelect,
NIcon,
} from "naive-ui"; } from "naive-ui";
import { CloseOutline as CloseIcon } from "@vicons/ionicons5";
import XPageHeader from "@/components/PageHeader.vue"; import XPageHeader from "@/components/PageHeader.vue";
import containerApi from "@/api/container"; import containerApi from "@/api/container";
import type { Container } from "@/api/container"; import type { Container } from "@/api/container";
import nodeApi from "@/api/node"; import nodeApi from "@/api/node";
import { useDataTable } from "@/utils/data-table"; import { useDataTable } from "@/utils/data-table";
import { renderButton, renderLink, renderTag } from "@/utils/render"; import { formatSize, renderButton, renderLink, renderTag } from "@/utils/render";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
@ -82,18 +95,35 @@ const columns = [
title: t('fields.actions'), title: t('fields.actions'),
key: "actions", key: "actions",
render(i: Container, index: number) { render(i: Container, index: number) {
return renderButton('error', t('buttons.delete'), () => deleteContainer(i, index), t('prompts.delete')) return renderButton('error', t('buttons.delete'), () => remove(i, index), t('prompts.delete'))
}, },
}, },
]; ];
const { state, pagination, fetchData, changePageSize } = useDataTable(containerApi.search, filter, false) const { state, pagination, fetchData, changePageSize } = useDataTable(containerApi.search, filter, false)
async function deleteContainer(c: Container, index: number) { async function remove(c: Container, index: number) {
const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id') const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id')
await containerApi.delete(node?.value || '', c.id, ''); await containerApi.delete(node?.value || '', c.id, c.name);
state.data.splice(index, 1) state.data.splice(index, 1)
} }
async function prune() {
window.dialog.warning({
title: t('dialogs.prune_container.title'),
content: t('dialogs.prune_container.body'),
positiveText: t('buttons.confirm'),
negativeText: t('buttons.cancel'),
onPositiveClick: async () => {
const r = await containerApi.prune(filter.node);
window.message.info(t('texts.prune_container_success', {
count: r.data?.count,
size: formatSize(r.data?.size as number),
}));
fetchData();
}
})
}
onMounted(async () => { onMounted(async () => {
const r = await nodeApi.list(true) const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id })) nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))

View File

@ -1,5 +1,16 @@
<template> <template>
<x-page-header /> <x-page-header>
<template #action>
<n-button secondary size="small" type="warning" @click="prune">
<template #icon>
<n-icon>
<close-icon />
</n-icon>
</template>
{{ t('buttons.prune') }}
</n-button>
</template>
</x-page-header>
<n-space class="page-body" vertical :size="12"> <n-space class="page-body" vertical :size="12">
<n-space :size="12"> <n-space :size="12">
<n-select <n-select
@ -38,7 +49,9 @@ import {
NDataTable, NDataTable,
NInput, NInput,
NSelect, NSelect,
NIcon,
} from "naive-ui"; } from "naive-ui";
import { CloseOutline as CloseIcon } from "@vicons/ionicons5";
import XPageHeader from "@/components/PageHeader.vue"; import XPageHeader from "@/components/PageHeader.vue";
import imageApi from "@/api/image"; import imageApi from "@/api/image";
import type { Image } from "@/api/image"; import type { Image } from "@/api/image";
@ -86,17 +99,34 @@ const columns = [
title: t('fields.actions'), title: t('fields.actions'),
key: "actions", key: "actions",
render(i: Image, index: number) { render(i: Image, index: number) {
return renderButton('error', t('buttons.delete'), () => deleteImage(i.id, index), t('prompts.delete')) return renderButton('error', t('buttons.delete'), () => remove(i.id, index), t('prompts.delete'))
}, },
}, },
]; ];
const { state, pagination, fetchData, changePageSize } = useDataTable(imageApi.search, filter, false) const { state, pagination, fetchData, changePageSize } = useDataTable(imageApi.search, filter, false)
async function deleteImage(id: string, index: number) { async function remove(id: string, index: number) {
await imageApi.delete(filter.node, id, ""); await imageApi.delete(filter.node, id, "");
state.data.splice(index, 1) state.data.splice(index, 1)
} }
async function prune() {
window.dialog.warning({
title: t('dialogs.prune_image.title'),
content: t('dialogs.prune_image.body'),
positiveText: t('buttons.confirm'),
negativeText: t('buttons.cancel'),
onPositiveClick: async () => {
const r = await imageApi.prune(filter.node);
window.message.info(t('texts.prune_image_success', {
count: r.data?.count,
size: formatSize(r.data?.size as number),
}));
fetchData();
}
})
}
onMounted(async () => { onMounted(async () => {
const r = await nodeApi.list(true) const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id })) nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))

View File

@ -1,7 +1,7 @@
<template> <template>
<x-page-header> <x-page-header>
<template #action> <template #action>
<n-button secondary size="small" type="warning" @click="pruneVolume"> <n-button secondary size="small" type="warning" @click="prune">
<template #icon> <template #icon>
<n-icon> <n-icon>
<close-icon /> <close-icon />
@ -107,18 +107,18 @@ const columns = [
title: t('fields.actions'), title: t('fields.actions'),
key: "actions", key: "actions",
render(v: Volume, index: number) { render(v: Volume, index: number) {
return renderButton('error', t('buttons.delete'), () => deleteVolume(v.name, index), t('prompts.delete')) return renderButton('error', t('buttons.delete'), () => remove(v.name, index), t('prompts.delete'))
}, },
}, },
]; ];
const { state, pagination, fetchData, changePageSize } = useDataTable(volumeApi.search, filter, false) const { state, pagination, fetchData, changePageSize } = useDataTable(volumeApi.search, filter, false)
async function deleteVolume(name: string, index: number) { async function remove(name: string, index: number) {
await volumeApi.delete(filter.node, name); await volumeApi.delete(filter.node, name);
state.data.splice(index, 1) state.data.splice(index, 1)
} }
async function pruneVolume() { async function prune() {
window.dialog.warning({ window.dialog.warning({
title: t('dialogs.prune_volume.title'), title: t('dialogs.prune_volume.title'),
content: t('dialogs.prune_volume.body'), content: t('dialogs.prune_volume.body'),
@ -127,8 +127,8 @@ async function pruneVolume() {
onPositiveClick: async () => { onPositiveClick: async () => {
const r = await volumeApi.prune(filter.node); const r = await volumeApi.prune(filter.node);
window.message.info(t('texts.prune_volume_success', { window.message.info(t('texts.prune_volume_success', {
count: r.data?.deletedVolumes.length, count: r.data?.count,
size: formatSize(r.data?.reclaimedSpace as number) size: formatSize(r.data?.size as number)
})); }));
fetchData(); fetchData();
} }