mirror of
https://github.com/cuigh/swirl
synced 2025-06-26 18:16:50 +00:00
Add agent support for image and volume
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Task {
|
||||
serviceId: string;
|
||||
serviceName: string;
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
containerId?: string;
|
||||
pid?: number;
|
||||
exitCode?: number;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user