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

@ -8,22 +8,23 @@ import (
// ImageHandler encapsulates image related handlers.
type ImageHandler struct {
Search web.HandlerFunc `path:"/search" auth:"image.view" desc:"search images"`
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"`
Search web.HandlerFunc `path:"/search" auth:"image.view" desc:"search images"`
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"`
}
// NewImage creates an instance of ImageHandler
func NewImage(b biz.ImageBiz) *ImageHandler {
return &ImageHandler{
Search: imageSearch(b),
Find: imageFind(b),
Delete: imageDelete(b),
Search: imageSearch(b),
Find: imageFind(b),
Delete: imageDelete(b),
}
}
func imageSearch(b biz.ImageBiz) web.HandlerFunc {
type Args struct {
Node string `json:"node" bind:"node"`
Name string `json:"name" bind:"name"`
PageIndex int `json:"pageIndex" bind:"pageIndex"`
PageSize int `json:"pageSize" bind:"pageSize"`
@ -37,7 +38,7 @@ func imageSearch(b biz.ImageBiz) web.HandlerFunc {
)
if err = ctx.Bind(args); err == nil {
images, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
images, total, err = b.Search(args.Node, args.Name, args.PageIndex, args.PageSize)
}
if err != nil {
@ -53,8 +54,9 @@ func imageSearch(b biz.ImageBiz) web.HandlerFunc {
func imageFind(b biz.ImageBiz) web.HandlerFunc {
return func(ctx web.Context) error {
node := ctx.Query("node")
id := ctx.Query("id")
image, raw, err := b.Find(id)
image, raw, err := b.Find(node, id)
if err != nil {
return err
}
@ -64,12 +66,13 @@ func imageFind(b biz.ImageBiz) web.HandlerFunc {
func imageDelete(b biz.ImageBiz) web.HandlerFunc {
type Args struct {
Node string `json:"node"`
ID string `json:"id"`
}
return func(ctx web.Context) (err error) {
args := &Args{}
if err = ctx.Bind(args); err == nil {
err = b.Delete(args.ID, ctx.User())
err = b.Delete(args.Node, args.ID, ctx.User())
}
return ajax(ctx, err)
}

View File

@ -33,6 +33,17 @@ func nodeList(nb biz.NodeBiz) web.HandlerFunc {
return err
}
if ctx.Query("agent") == "true" {
i := 0
for j := 0; j < len(nodes); j++ {
if nodes[j].Agent != "" {
nodes[i] = nodes[j]
i++
}
}
nodes = nodes[:i]
}
return success(ctx, nodes)
}
}

View File

@ -28,6 +28,7 @@ func NewVolume(b biz.VolumeBiz) *VolumeHandler {
func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
type Args struct {
Node string `json:"node" bind:"node"`
Name string `json:"name" bind:"name"`
PageIndex int `json:"pageIndex" bind:"pageIndex"`
PageSize int `json:"pageSize" bind:"pageSize"`
@ -41,7 +42,7 @@ func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
)
if err = ctx.Bind(args); err == nil {
volumes, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
volumes, total, err = b.Search(args.Node, args.Name, args.PageIndex, args.PageSize)
}
if err != nil {
@ -57,8 +58,9 @@ func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
func volumeFind(b biz.VolumeBiz) web.HandlerFunc {
return func(ctx web.Context) error {
node := ctx.Query("node")
name := ctx.Query("name")
volume, raw, err := b.Find(name)
volume, raw, err := b.Find(node, name)
if err != nil {
return err
}
@ -68,12 +70,13 @@ func volumeFind(b biz.VolumeBiz) web.HandlerFunc {
func volumeDelete(b biz.VolumeBiz) web.HandlerFunc {
type Args struct {
Node string `json:"node"`
Name string `json:"name"`
}
return func(ctx web.Context) (err error) {
args := &Args{}
if err = ctx.Bind(args); err == nil {
err = b.Delete(args.Name, ctx.User())
err = b.Delete(args.Node, args.Name, ctx.User())
}
return ajax(ctx, err)
}
@ -91,11 +94,20 @@ func volumeSave(b biz.VolumeBiz) web.HandlerFunc {
}
func volumePrune(b biz.VolumeBiz) web.HandlerFunc {
return func(ctx web.Context) error {
deletedVolumes, reclaimedSpace, err := b.Prune(ctx.User())
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
}
deletedVolumes, reclaimedSpace, err := b.Prune(args.Node, ctx.User())
if err != nil {
return err
}
return success(ctx, data.Map{
"deletedVolumes": deletedVolumes,
"reclaimedSpace": reclaimedSpace,

View File

@ -12,9 +12,9 @@ import (
)
type ImageBiz interface {
Search(name string, pageIndex, pageSize int) ([]*Image, int, error)
Find(name string) (image *Image, raw string, err error)
Delete(id string, user web.User) (err error)
Search(node, name string, pageIndex, pageSize int) ([]*Image, int, error)
Find(node, name string) (image *Image, raw string, err error)
Delete(node, id string, user web.User) (err error)
}
func NewImage(d *docker.Docker) ImageBiz {
@ -25,7 +25,7 @@ type imageBiz struct {
d *docker.Docker
}
func (b *imageBiz) Find(id string) (img *Image, raw string, err error) {
func (b *imageBiz) Find(node, id string) (img *Image, raw string, err error) {
var (
i types.ImageInspect
r []byte
@ -33,12 +33,12 @@ func (b *imageBiz) Find(id string) (img *Image, raw string, err error) {
ctx = context.TODO()
)
if i, r, err = b.d.ImageInspect(ctx, id); err == nil {
if i, r, err = b.d.ImageInspect(ctx, node, id); err == nil {
raw, err = indentJSON(r)
}
if err == nil {
histories, err = b.d.ImageHistory(ctx, id)
histories, err = b.d.ImageHistory(ctx, node, id)
}
if err == nil {
@ -47,8 +47,8 @@ func (b *imageBiz) Find(id string) (img *Image, raw string, err error) {
return
}
func (b *imageBiz) Search(name string, pageIndex, pageSize int) (images []*Image, total int, err error) {
list, total, err := b.d.ImageList(context.TODO(), name, pageIndex, pageSize)
func (b *imageBiz) Search(node, name string, pageIndex, pageSize int) (images []*Image, total int, err error) {
list, total, err := b.d.ImageList(context.TODO(), node, name, pageIndex, pageSize)
if err != nil {
return nil, 0, err
}
@ -60,8 +60,8 @@ func (b *imageBiz) Search(name string, pageIndex, pageSize int) (images []*Image
return images, total, nil
}
func (b *imageBiz) Delete(id string, user web.User) (err error) {
err = b.d.ImageRemove(context.TODO(), id)
func (b *imageBiz) Delete(node, id string, user web.User) (err error) {
err = b.d.ImageRemove(context.TODO(), node, id)
//if err == nil {
// Event.CreateImage(model.EventActionDelete, id, user)
//}

View File

@ -2,6 +2,7 @@ package biz
import (
"context"
"sort"
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/net/web"
@ -42,7 +43,19 @@ func (b *nodeBiz) Find(id string) (node *Node, raw string, err error) {
}
func (b *nodeBiz) List() ([]*docker.Node, error) {
return b.d.NodeListCache()
m, err := b.d.NodeMap()
if err != nil {
return nil, err
}
nodes := make([]*docker.Node, 0, len(m))
for _, n := range m {
nodes = append(nodes, n)
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Name < nodes[j].Name
})
return nodes, nil
}
func (b *nodeBiz) Search() ([]*Node, error) {

View File

@ -28,12 +28,17 @@ func (b *taskBiz) Find(id string) (task *Task, raw string, err error) {
s swarm.Service
r []byte
)
t, r, err = b.d.TaskInspect(context.TODO(), id)
if err == nil {
raw, err = indentJSON(r)
}
if err == nil {
task = newTask(&t)
m, _ := b.d.NodeMap()
task = newTask(&t, m)
// Fill service name
if s, _, _ = b.d.ServiceInspect(context.TODO(), t.ServiceID, false); s.Spec.Name == "" {
task.ServiceName = task.ServiceID
} else {
@ -50,9 +55,15 @@ func (b *taskBiz) Search(node, service, state string, pageIndex, pageSize int) (
return
}
m, _ := b.d.NodeMap()
tasks = make([]*Task, len(list))
for i, t := range list {
tasks[i] = newTask(&t)
tasks[i] = newTask(&t, m)
if m != nil {
if n, ok := m[t.NodeID]; ok {
tasks[i].NodeName = n.Name
}
}
}
return
}
@ -75,6 +86,7 @@ type Task struct {
ServiceID string `json:"serviceId"`
ServiceName string `json:"serviceName"`
NodeID string `json:"nodeId"`
NodeName string `json:"nodeName"`
ContainerID string `json:"containerId"`
PID int `json:"pid"`
ExitCode int `json:"exitCode"`
@ -93,7 +105,7 @@ type TaskNetwork struct {
IPs []string `json:"ips"`
}
func newTask(t *swarm.Task) *Task {
func newTask(t *swarm.Task, nodes map[string]*docker.Node) *Task {
task := &Task{
ID: t.ID,
Name: t.Name,
@ -103,6 +115,7 @@ func newTask(t *swarm.Task) *Task {
State: t.Status.State,
ServiceID: t.ServiceID,
NodeID: t.NodeID,
NodeName: t.NodeID,
Message: t.Status.Message,
Error: t.Status.Err,
Env: envToOptions(t.Spec.ContainerSpec.Env),
@ -122,5 +135,11 @@ func newTask(t *swarm.Task) *Task {
IPs: n.Addresses,
})
}
// Fill node name
if nodes != nil {
if n, ok := nodes[t.NodeID]; ok {
task.NodeName = n.Name
}
}
return task
}

View File

@ -12,11 +12,11 @@ import (
)
type VolumeBiz interface {
Search(name string, pageIndex, pageSize int) ([]*Volume, int, error)
Find(name string) (volume *Volume, raw string, err error)
Delete(name string, user web.User) (err error)
Search(node, name string, pageIndex, pageSize int) ([]*Volume, int, error)
Find(node, name string) (volume *Volume, raw string, err error)
Delete(node, name string, user web.User) (err error)
Create(volume *Volume, user web.User) (err error)
Prune(user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error)
Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error)
}
func NewVolume(d *docker.Docker, eb EventBiz) VolumeBiz {
@ -28,13 +28,13 @@ type volumeBiz struct {
eb EventBiz
}
func (b *volumeBiz) Find(name string) (volume *Volume, raw string, err error) {
func (b *volumeBiz) Find(node, name string) (volume *Volume, raw string, err error) {
var (
v types.Volume
r []byte
)
if v, r, err = b.d.VolumeInspect(context.TODO(), name); err == nil {
if v, r, err = b.d.VolumeInspect(context.TODO(), node, name); err == nil {
raw, err = indentJSON(r)
}
@ -44,8 +44,8 @@ func (b *volumeBiz) Find(name string) (volume *Volume, raw string, err error) {
return
}
func (b *volumeBiz) Search(name string, pageIndex, pageSize int) (volumes []*Volume, total int, err error) {
list, total, err := b.d.VolumeList(context.TODO(), name, pageIndex, pageSize)
func (b *volumeBiz) Search(node, name string, pageIndex, pageSize int) (volumes []*Volume, total int, err error) {
list, total, err := b.d.VolumeList(context.TODO(), node, name, pageIndex, pageSize)
if err != nil {
return nil, 0, err
}
@ -57,8 +57,8 @@ func (b *volumeBiz) Search(name string, pageIndex, pageSize int) (volumes []*Vol
return volumes, total, nil
}
func (b *volumeBiz) Delete(name string, user web.User) (err error) {
err = b.d.VolumeRemove(context.TODO(), name)
func (b *volumeBiz) Delete(node, name string, user web.User) (err error) {
err = b.d.VolumeRemove(context.TODO(), node, name)
if err == nil {
b.eb.CreateVolume(EventActionDelete, name, user)
}
@ -78,16 +78,16 @@ func (b *volumeBiz) Create(vol *Volume, user web.User) (err error) {
options.Driver = vol.Driver
}
err = b.d.VolumeCreate(context.TODO(), options)
err = b.d.VolumeCreate(context.TODO(), vol.Node, options)
if err != nil {
b.eb.CreateVolume(EventActionDelete, vol.Name, user)
}
return
}
func (b *volumeBiz) Prune(user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) {
func (b *volumeBiz) Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) {
var report types.VolumesPruneReport
report, err = b.d.VolumePrune(context.TODO())
report, err = b.d.VolumePrune(context.TODO(), node)
if err == nil {
deletedVolumes, reclaimedSpace = report.VolumesDeleted, report.SpaceReclaimed
b.eb.CreateVolume(EventActionPrune, "", user)
@ -96,6 +96,7 @@ func (b *volumeBiz) Prune(user web.User) (deletedVolumes []string, reclaimedSpac
}
type Volume struct {
Node string `json:"node"`
Name string `json:"name"`
Driver string `json:"driver,omitempty"`
CustomDriver string `json:"customDriver,omitempty"`

View File

@ -4,10 +4,8 @@ import (
"context"
"strings"
"sync"
"time"
"github.com/cuigh/auxo/app/container"
"github.com/cuigh/auxo/cache"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/util/lazy"
@ -91,11 +89,11 @@ func (d *Docker) agent(node string) (*client.Client, error) {
}
func (d *Docker) getAgent(node string) (agent string, err error) {
if node == "" || node == "@" {
if node == "" || node == "-" {
return "", nil
}
nodes, err := d.getNodes()
nodes, err := d.NodeMap()
if err != nil {
return
}
@ -106,18 +104,6 @@ func (d *Docker) getAgent(node string) (agent string, err error) {
return
}
func (d *Docker) getNodes() (map[string]*Node, error) {
v := cache.Value{
TTL: 30 * time.Minute,
Load: func() (interface{}, error) { return d.loadCache() },
}
value, err := v.Get(true)
if err != nil {
return nil, err
}
return value.(map[string]*Node), nil
}
func (d *Docker) loadCache() (interface{}, error) {
c, err := d.client()
if err != nil {

View File

@ -11,8 +11,8 @@ import (
)
// ImageList return images on the host.
func (d *Docker) ImageList(ctx context.Context, name string, pageIndex, pageSize int) (images []types.ImageSummary, total int, err error) {
c, err := d.client()
func (d *Docker) ImageList(ctx context.Context, node, name string, pageIndex, pageSize int) (images []types.ImageSummary, total int, err error) {
c, err := d.agent(node)
if err != nil {
return nil, 0, err
}
@ -34,27 +34,28 @@ func (d *Docker) ImageList(ctx context.Context, name string, pageIndex, pageSize
}
// ImageInspect returns image information.
func (d *Docker) ImageInspect(ctx context.Context, id string) (image types.ImageInspect, raw []byte, err error) {
func (d *Docker) ImageInspect(ctx context.Context, node, id string) (image types.ImageInspect, raw []byte, err error) {
var c *client.Client
if c, err = d.client(); err == nil {
if c, err = d.agent(node); err == nil {
return c.ImageInspectWithRaw(ctx, id)
}
return
}
// ImageHistory returns the changes in an image in history format.
func (d *Docker) ImageHistory(ctx context.Context, id string) (histories []image.HistoryResponseItem, err error) {
func (d *Docker) ImageHistory(ctx context.Context, node, id string) (histories []image.HistoryResponseItem, err error) {
var c *client.Client
if c, err = d.client(); err == nil {
if c, err = d.agent(node); err == nil {
return c.ImageHistory(ctx, id)
}
return
}
// ImageRemove remove a image.
func (d *Docker) ImageRemove(ctx context.Context, id string) error {
return d.call(func(c *client.Client) (err error) {
func (d *Docker) ImageRemove(ctx context.Context, node, id string) error {
c, err := d.agent(node)
if err == nil {
_, err = c.ImageRemove(ctx, id, types.ImageRemoveOptions{})
return
})
}
return err
}

View File

@ -3,7 +3,9 @@ package docker
import (
"context"
"sort"
"time"
"github.com/cuigh/auxo/cache"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
@ -60,15 +62,14 @@ func (d *Docker) NodeInspect(ctx context.Context, id string) (node swarm.Node, r
return
}
func (d *Docker) NodeListCache() ([]*Node, error) {
m, err := d.getNodes()
func (d *Docker) NodeMap() (map[string]*Node, error) {
v := cache.Value{
TTL: 30 * time.Minute,
Load: func() (interface{}, error) { return d.loadCache() },
}
value, err := v.Get(true)
if err != nil {
return nil, err
}
nodes := make([]*Node, 0, len(m))
for _, n := range m {
nodes = append(nodes, n)
}
return nodes, nil
return value.(map[string]*Node), nil
}

View File

@ -82,7 +82,7 @@ func (d *Docker) fillStatus(ctx context.Context, c *client.Client, services []sw
opts = types.TaskListOptions{Filters: filters.NewArgs()}
)
nodes, err = d.getNodes()
nodes, err = d.NodeMap()
if err != nil {
return
}

View File

@ -12,13 +12,13 @@ import (
)
// VolumeList return volumes on the host.
func (d *Docker) VolumeList(ctx context.Context, name string, pageIndex, pageSize int) (volumes []*types.Volume, total int, err error) {
func (d *Docker) VolumeList(ctx context.Context, node, name string, pageIndex, pageSize int) (volumes []*types.Volume, total int, err error) {
var (
c *client.Client
resp volume.VolumeListOKBody
)
c, err = d.client()
c, err = d.agent(node)
if err != nil {
return
}
@ -44,34 +44,37 @@ func (d *Docker) VolumeList(ctx context.Context, name string, pageIndex, pageSiz
}
// VolumeCreate create a volume.
func (d *Docker) VolumeCreate(ctx context.Context, options *volume.VolumeCreateBody) error {
return d.call(func(c *client.Client) (err error) {
func (d *Docker) VolumeCreate(ctx context.Context, node string, options *volume.VolumeCreateBody) (err error) {
var c *client.Client
if c, err = d.agent(node); err == nil {
_, err = c.VolumeCreate(ctx, *options)
return
})
}
return
}
// VolumeRemove remove a volume.
func (d *Docker) VolumeRemove(ctx context.Context, name string) error {
return d.call(func(c *client.Client) (err error) {
return c.VolumeRemove(ctx, name, false)
})
func (d *Docker) VolumeRemove(ctx context.Context, node, name string) (err error) {
c, err := d.agent(node)
if err == nil {
err = c.VolumeRemove(ctx, name, false)
}
return err
}
// VolumePrune remove all unused volumes.
func (d *Docker) VolumePrune(ctx context.Context, ) (report types.VolumesPruneReport, err error) {
err = d.call(func(c *client.Client) (err error) {
func (d *Docker) VolumePrune(ctx context.Context, node string) (report types.VolumesPruneReport, err error) {
var c *client.Client
if c, err = d.agent(node); err == nil {
report, err = c.VolumesPrune(ctx, filters.NewArgs())
return
})
}
return
}
// VolumeInspect return volume raw information.
func (d *Docker) VolumeInspect(ctx context.Context, name string) (vol types.Volume, raw []byte, err error) {
err = d.call(func(c *client.Client) error {
func (d *Docker) VolumeInspect(ctx context.Context, node, name string) (vol types.Volume, raw []byte, err error) {
var c *client.Client
if c, err = d.agent(node); err == nil {
vol, raw, err = c.VolumeInspectWithRaw(ctx, name)
return err
})
}
return
}

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'),
},
{