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

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

View File

@@ -74,6 +74,13 @@ export class ImageApi {
delete(node: string, id: string, name: string) {
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

View File

@@ -36,11 +36,6 @@ export interface FindResult {
raw: string;
}
export interface PruneResult {
deletedVolumes: string[];
reclaimedSpace: number;
}
export class VolumeApi {
find(node: string, name: string) {
return ajax.get<FindResult>('/volume/find', { node, name })
@@ -59,7 +54,10 @@ export class VolumeApi {
}
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": {
"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": {
"title": "Prune volume",
"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.",
"password_notice": "You are a LDAP user, can not modify password here.",
"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.",
"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",

View File

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

View File

@@ -1,5 +1,16 @@
<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 :size="12">
<n-select
@@ -38,13 +49,15 @@ import {
NDataTable,
NInput,
NSelect,
NIcon,
} from "naive-ui";
import { CloseOutline as CloseIcon } from "@vicons/ionicons5";
import XPageHeader from "@/components/PageHeader.vue";
import containerApi from "@/api/container";
import type { Container } from "@/api/container";
import nodeApi from "@/api/node";
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'
const { t } = useI18n()
@@ -82,18 +95,35 @@ const columns = [
title: t('fields.actions'),
key: "actions",
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)
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')
await containerApi.delete(node?.value || '', c.id, '');
await containerApi.delete(node?.value || '', c.id, c.name);
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 () => {
const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))

View File

@@ -1,5 +1,16 @@
<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 :size="12">
<n-select
@@ -38,7 +49,9 @@ import {
NDataTable,
NInput,
NSelect,
NIcon,
} from "naive-ui";
import { CloseOutline as CloseIcon } from "@vicons/ionicons5";
import XPageHeader from "@/components/PageHeader.vue";
import imageApi from "@/api/image";
import type { Image } from "@/api/image";
@@ -86,17 +99,34 @@ const columns = [
title: t('fields.actions'),
key: "actions",
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)
async function deleteImage(id: string, index: number) {
async function remove(id: string, index: number) {
await imageApi.delete(filter.node, id, "");
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 () => {
const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))

View File

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