Add agent support for image and volume

This commit is contained in:
cuigh
2021-12-20 14:28:43 +08:00
parent cb2cb4ab86
commit dfe15524a2
25 changed files with 253 additions and 139 deletions

View File

@@ -46,6 +46,7 @@ export interface Image {
}
export interface SearchArgs {
node?: string;
name?: string;
pageIndex: number;
pageSize: number;
@@ -62,16 +63,16 @@ export interface FindResult {
}
export class ImageApi {
find(id: string) {
return ajax.get<FindResult>('/image/find', { id })
find(node: string, id: string) {
return ajax.get<FindResult>('/image/find', { node, id })
}
search(args: SearchArgs) {
return ajax.get<SearchResult>('/image/search', args)
}
delete(id: string, name: string) {
return ajax.post<Result<Object>>('/image/delete', { id, name })
delete(node: string, id: string, name: string) {
return ajax.post<Result<Object>>('/image/delete', { node, id, name })
}
}

View File

@@ -37,8 +37,8 @@ export class NodeApi {
return ajax.get<FindResult>('/node/find', { id })
}
list() {
return ajax.get<Node[]>('/node/list')
list(agent: boolean) {
return ajax.get<Node[]>('/node/list', { agent })
}
search() {

View File

@@ -10,6 +10,7 @@ export interface Task {
serviceId: string;
serviceName: string;
nodeId: string;
nodeName: string;
containerId?: string;
pid?: number;
exitCode?: number;

View File

@@ -20,6 +20,7 @@ export interface Volume {
}
export interface SearchArgs {
node?: string;
name?: string;
pageIndex: number;
pageSize: number;
@@ -41,24 +42,24 @@ export interface PruneResult {
}
export class VolumeApi {
find(name: string) {
return ajax.get<FindResult>('/volume/find', { name })
find(node: string, name: string) {
return ajax.get<FindResult>('/volume/find', { node, name })
}
search(args: SearchArgs) {
return ajax.get<SearchResult>('/volume/search', args)
}
delete(name: string) {
return ajax.post<Result<Object>>('/volume/delete', { name })
delete(node: string, name: string) {
return ajax.post<Result<Object>>('/volume/delete', { node, name })
}
save(v: Volume) {
return ajax.post<Result<Object>>('/volume/save', v)
}
prune() {
return ajax.post<PruneResult>('/volume/prune')
prune(node: string) {
return ajax.post<PruneResult>('/volume/prune', { node })
}
}

View File

@@ -3,7 +3,9 @@
<n-space class="page-body" vertical :size="12">
<n-space :size="12">
<n-select
filterable
size="small"
:consistent-menu-width="false"
:placeholder="t('objects.node')"
v-model:value="filter.node"
:options="nodes"
@@ -58,7 +60,7 @@ const columns = [
fixed: "left" as const,
render: (c: Container) => {
const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id')
return renderLink({ name: 'container_detail', params: { id: c.id, node: node?.value || '@' } }, c.name)
return renderLink({ name: 'container_detail', params: { id: c.id, node: node?.value || '-' } }, c.name)
},
},
{
@@ -93,7 +95,7 @@ async function deleteContainer(c: Container, index: number) {
}
onMounted(async () => {
const r = await nodeApi.list()
const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
if (r.data?.length) {
filter.node = r.data[0].id

View File

@@ -2,6 +2,16 @@
<x-page-header />
<n-space class="page-body" vertical :size="12">
<n-space :size="12">
<n-select
filterable
size="small"
:consistent-menu-width="false"
:placeholder="t('objects.node')"
v-model:value="filter.node"
:options="nodes"
style="width: 200px"
v-if="nodes && nodes.length"
/>
<n-input size="small" v-model:value="filter.name" :placeholder="t('fields.name')" clearable />
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
</n-space>
@@ -21,30 +31,34 @@
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { onMounted, reactive, ref } from "vue";
import {
NSpace,
NButton,
NDataTable,
NInput,
NSelect,
} from "naive-ui";
import XPageHeader from "@/components/PageHeader.vue";
import imageApi from "@/api/image";
import type { Image } from "@/api/image";
import nodeApi from "@/api/node";
import { useDataTable } from "@/utils/data-table";
import { formatSize, renderButton, renderLink, renderTags } from "@/utils/render";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const filter = reactive({
name: "",
node: '',
name: '',
});
const nodes: any = ref([])
const columns = [
{
title: t('fields.id'),
key: "id",
fixed: "left" as const,
render: (i: Image) => renderLink(`/local/images/${i.id}`, i.id.substring(7, 19)),
render: (i: Image) => renderLink({ name: 'image_detail', params: { node: filter.node || '-', id: i.id } }, i.id.substring(7, 19)),
},
{
title: t('fields.tags'),
@@ -76,10 +90,19 @@ const columns = [
},
},
];
const { state, pagination, fetchData, changePageSize } = useDataTable(imageApi.search, filter)
const { state, pagination, fetchData, changePageSize } = useDataTable(imageApi.search, filter, false)
async function deleteImage(id: string, index: number) {
await imageApi.delete(id, "");
await imageApi.delete(filter.node, id, "");
state.data.splice(index, 1)
}
onMounted(async () => {
const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
if (r.data?.length) {
filter.node = r.data[0].id
}
fetchData()
})
</script>

View File

@@ -92,6 +92,7 @@ const { t } = useI18n()
const route = useRoute();
const model = ref({} as Image);
const raw = ref('');
const node = route.params.node as string || '';
const columns = [
{
title: t('fields.sn'),
@@ -137,7 +138,7 @@ const columns = [
async function fetchData() {
const id = route.params.id as string;
let r = await imageApi.find(id);
let r = await imageApi.find(node, id);
model.value = r.data?.image as Image;
raw.value = r.data?.raw as string;
}

View File

@@ -444,7 +444,7 @@
<tbody>
<tr v-for="t in tasks">
<td>
<x-anchor :url="`/swarm/tasks/${t.id}`">{{ t.id }}</x-anchor>
<x-anchor :url="{ name: 'task_detail', params: { id: t.id } }">{{ t.id }}</x-anchor>
</td>
<td>
<n-tag
@@ -454,7 +454,7 @@
>{{ t.state }}</n-tag>
</td>
<td>
<x-anchor :url="`/swarm/nodes/${t.nodeId}`">{{ t.nodeId }}</x-anchor>
<x-anchor :url="{ name: 'node_detail', params: { id: t.nodeId } }">{{ t.nodeName }}</x-anchor>
</td>
<td>
<n-space :size="4">
@@ -481,7 +481,7 @@
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, h, computed } from "vue";
import { onMounted, ref, h, computed } from "vue";
import {
NButton,
NTag,

View File

@@ -2,7 +2,12 @@
<x-page-header />
<n-space class="page-body" vertical :size="12">
<n-space :size="12">
<n-input size="small" v-model:value="filter.service" :placeholder="t('fields.name')" clearable />
<n-input
size="small"
v-model:value="filter.service"
:placeholder="t('fields.name')"
clearable
/>
<n-select
size="small"
:placeholder="t('fields.state')"
@@ -60,12 +65,12 @@ const columns = [
title: t('fields.id'),
key: "id",
fixed: "left" as const,
render: (s: Task) => renderLink(`/swarm/tasks/${s.id}`, s.id),
render: (s: Task) => renderLink({ name: 'task_detail', params: { id: s.id } }, s.id),
},
{
title: t('fields.service_id'),
key: "service",
render: (s: Task) => renderLink(`/swarm/services/${s.serviceId}`, s.serviceId),
render: (s: Task) => renderLink({ name: 'service_detail', params: { name: s.serviceId } }, s.serviceId),
},
{
title: t('objects.image'),
@@ -74,7 +79,7 @@ const columns = [
{
title: t('fields.node_id'),
key: "image",
render: (s: Task) => renderLink(`/swarm/nodes/${s.nodeId}`, s.nodeId),
render: (s: Task) => renderLink({ name: 'node_detail', params: { id: s.nodeId } }, s.nodeName),
},
{
title: t('fields.state'),

View File

@@ -18,14 +18,20 @@
<x-description label-placement="left" label-align="right" :label-width="90">
<x-description-item :label="t('fields.id')">{{ model.id }}</x-description-item>
<x-description-item :label="t('objects.image')">{{ model.image }}</x-description-item>
<x-description-item :label="t('objects.service')" :span="2">
<x-anchor :url="`/swarm/services/${model.serviceName}`">{{ model.serviceName }}</x-anchor>
<x-description-item :label="t('objects.service')" :span="2" v-if="model.serviceName">
<x-anchor
:url="{ name: 'service_detail', params: { name: model.serviceName } }"
>{{ model.serviceName }}</x-anchor>
</x-description-item>
<x-description-item :label="t('objects.container')" :span="2">
<x-anchor :url="`/local/containers/${model.nodeId}/${model.containerId}`">{{ model.containerId }}</x-anchor>
<x-description-item :label="t('objects.container')" :span="2" v-if="model.containerId">
<x-anchor
:url="{ name: 'container_detail', params: { id: model.containerId, node: model.nodeId || '-' } }"
>{{ model.containerId }}</x-anchor>
</x-description-item>
<x-description-item :label="t('objects.node')" :span="2">
<x-anchor :url="`/swarm/nodes/${model.nodeId}`">{{ model.nodeId }}</x-anchor>
<x-description-item :label="t('objects.node')" :span="2" v-if="model.nodeId">
<x-anchor
:url="{ name: 'node_detail', params: { id: model.nodeId } }"
>{{ model.nodeName }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.created_at')">{{ model.createdAt }}</x-description-item>
<x-description-item :label="t('fields.updated_at')">{{ model.updatedAt }}</x-description-item>
@@ -70,7 +76,7 @@
<tbody>
<tr v-for="n in model.networks">
<td>
<x-anchor :url="`/swarm/networks/${n.name}`">{{ n.name }}</x-anchor>
<x-anchor :url="{ name: 'network_detail', params: { name: n.name } }">{{ n.name }}</x-anchor>
</td>
<td>
<n-space :size="4">

View File

@@ -9,7 +9,7 @@
</template>
{{ t('buttons.prune') }}
</n-button>
<n-button secondary size="small" @click="$router.push('/local/volumes/new')">
<n-button secondary size="small" @click="$router.push({name: 'volume_new', params: {node: filter.node || '-'}})">
<template #icon>
<n-icon>
<add-icon />
@@ -21,6 +21,16 @@
</x-page-header>
<n-space class="page-body" vertical :size="12">
<n-space :size="12">
<n-select
filterable
size="small"
:consistent-menu-width="false"
:placeholder="t('objects.node')"
v-model:value="filter.node"
:options="nodes"
style="width: 200px"
v-if="nodes && nodes.length"
/>
<n-input size="small" v-model:value="filter.name" :placeholder="t('fields.name')" clearable />
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
</n-space>
@@ -40,32 +50,36 @@
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { onMounted, reactive, ref } from "vue";
import {
NSpace,
NButton,
NDataTable,
NInput,
NIcon,
NSelect,
} from "naive-ui";
import { AddOutline as AddIcon, CloseOutline as CloseIcon } from "@vicons/ionicons5";
import XPageHeader from "@/components/PageHeader.vue";
import volumeApi from "@/api/volume";
import type { Volume } from "@/api/volume";
import nodeApi from "@/api/node";
import { useDataTable } from "@/utils/data-table";
import { formatSize, renderButton, renderLink, renderTag } from "@/utils/render";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const filter = reactive({
name: "",
node: '',
name: '',
});
const nodes: any = ref([])
const columns = [
{
title: t('fields.name'),
key: "name",
fixed: "left" as const,
render: (v: Volume) => renderLink(`/local/volumes/${v.name}`, v.name),
render: (v: Volume) => renderLink({ name: 'volume_detail', params: { node: filter.node || '-', name: v.name } }, v.name),
},
{
title: t('fields.driver'),
@@ -97,10 +111,10 @@ const columns = [
},
},
];
const { state, pagination, fetchData, changePageSize } = useDataTable(volumeApi.search, filter)
const { state, pagination, fetchData, changePageSize } = useDataTable(volumeApi.search, filter, false)
async function deleteVolume(name: string, index: number) {
await volumeApi.delete(name);
await volumeApi.delete(filter.node, name);
state.data.splice(index, 1)
}
@@ -111,7 +125,7 @@ async function pruneVolume() {
positiveText: t('buttons.confirm'),
negativeText: t('buttons.cancel'),
onPositiveClick: async () => {
const r = await volumeApi.prune();
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)
@@ -120,4 +134,13 @@ async function pruneVolume() {
}
})
}
onMounted(async () => {
const r = await nodeApi.list(true)
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
if (r.data?.length) {
filter.node = r.data[0].id
}
fetchData()
})
</script>

View File

@@ -97,10 +97,11 @@ const { t } = useI18n()
const route = useRoute();
const model = ref({} as Volume);
const raw = ref('');
const node = route.params.node as string || '';
async function fetchData() {
const name = route.params.name as string;
let r = await volumeApi.find(name);
let r = await volumeApi.find(node, name);
model.value = r.data?.volume as Volume;
raw.value = r.data?.raw as string;
}

View File

@@ -203,7 +203,7 @@ const routes: RouteRecordRaw[] = [
},
{
name: "image_detail",
path: "/local/images/:id",
path: "/local/images/:node/:id",
component: () => import('../pages/image/View.vue'),
},
{
@@ -223,12 +223,12 @@ const routes: RouteRecordRaw[] = [
},
{
name: "volume_detail",
path: "/local/volumes/:name",
path: "/local/volumes/:node/:name",
component: () => import('../pages/volume/View.vue'),
},
{
name: "volume_new",
path: "/local/volumes/new",
path: "/local/volumes/:node/new",
component: () => import('../pages/volume/New.vue'),
},
{