fix: container list (#14156)

* fix: container list

* fix: container list
This commit is contained in:
Yuki Guo
2026-10-23 18:19:52 +08:00
committed by GitHub
parent 87828b03ae
commit c925184bf6
4 changed files with 146 additions and 94 deletions
+9 -3
View File
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
return
}
total, list, err := containerService.Page(req)
total, list, err := containerService.Page(c.Request.Context(), req)
if err != nil {
helper.InternalServer(c, err)
return
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
// @Success 200 {object} dto.ContainerStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Param containersOnly query boolean false "Only count containers"
// @Router /containers/status [get]
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
data, err := containerService.LoadStatus()
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
if err != nil {
helper.InternalServer(c, err)
return
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
// @Success 200 {array} dto.ContainerListStats
// @Security ApiKeyAuth
// @Security Timestamp
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
// @Router /containers/list/stats [get]
func (b *BaseApi) ContainerListStats(c *gin.Context) {
data, err := containerService.ContainerListStats()
var ids []string
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
}
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
if err != nil {
helper.InternalServer(c, err)
return
+56 -30
View File
@@ -58,10 +58,10 @@ type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions
ListByImage(imageName string) []dto.ContainerOptions
LoadStatus() (dto.ContainerStatus, error)
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
@@ -80,7 +80,7 @@ type IContainerService interface {
ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
ContainerListStats() ([]dto.ContainerListStats, error)
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error
@@ -112,7 +112,7 @@ func NewIContainerService() IContainerService {
return &ContainerService{}
}
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
@@ -123,7 +123,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
options.Filters = filters.NewArgs()
options.Filters.Add("label", req.Filters)
}
containers, err := client.ContainerList(context.Background(), options)
containers, err := client.ContainerList(ctx, options)
if err != nil {
return 0, nil, err
}
@@ -206,27 +206,30 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
return options
}
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
var data dto.ContainerStatus
client, err := docker.NewDockerClient()
if err != nil {
return data, err
}
defer client.Close()
c := context.Background()
images, _ := client.ImageList(c, image.ListOptions{All: true})
data.ImageCount = len(images)
repo, _ := imageRepoRepo.List()
data.RepoCount = len(repo)
templates, _ := composeRepo.List()
data.ComposeTemplateCount = len(templates)
networks, _ := client.NetworkList(c, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(c, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(client)
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
if !containersOnly {
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
data.ImageCount = len(images)
repo, _ := imageRepoRepo.List()
data.RepoCount = len(repo)
templates, _ := composeRepo.List()
data.ComposeTemplateCount = len(templates)
networks, _ := client.NetworkList(ctx, network.ListOptions{})
data.NetworkCount = len(networks)
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
data.VolumeCount = len(volumes.Volumes)
data.ComposeCount = loadComposeCount(client)
}
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return data, err
}
data.ContainerCount = len(containers)
for _, item := range containers {
switch item.State {
@@ -292,25 +295,46 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
}
return data, nil
}
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
options := container.ListOptions{All: true}
if ids != nil {
if len(ids) == 0 {
return []dto.ContainerListStats{}, nil
}
options.Filters = filters.NewArgs()
for _, id := range ids {
options.Filters.Add("id", id)
}
}
list, err := client.ContainerList(ctx, options)
if err != nil {
return nil, err
}
datas := make([]dto.ContainerListStats, len(list))
jobs := make(chan int)
var wg sync.WaitGroup
wg.Add(len(list))
for i := 0; i < len(list); i++ {
go func(index int, item container.Summary) {
datas[index] = loadCpuAndMem(client, item.ID)
wg.Done()
}(i, list[i])
for worker := 0; worker < min(8, len(list)); worker++ {
wg.Add(1)
go func() {
defer wg.Done()
for index := range jobs {
datas[index].ContainerID = list[index].ID
if ctx.Err() != nil || list[index].State != "running" {
continue
}
datas[index] = loadCpuAndMem(ctx, client, list[index].ID)
}
}()
}
for index := range list {
jobs <- index
}
close(jobs)
wg.Wait()
return datas, nil
}
@@ -1722,11 +1746,13 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
return selected
}
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
data := dto.ContainerListStats{
ContainerID: containerItem,
}
res, err := client.ContainerStats(context.Background(), containerItem, false)
res, err := client.ContainerStats(ctx, containerItem, false)
if err != nil {
return data
}
+16 -19
View File
@@ -4,13 +4,12 @@ import { ResPage, SearchWithPage } from '../interface';
import { Container } from '../interface/container';
import { TimeoutEnum } from '@/enums/http-enum';
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string) => {
return http.post<ResPage<Container.ContainerInfo>>(
`/containers/search`,
params,
TimeoutEnum.T_40S,
currentNode ? { CurrentNode: currentNode } : undefined,
);
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string, signal?: AbortSignal) => {
return http.postWithConfig<ResPage<Container.ContainerInfo>>(`/containers/search`, params, {
timeout: TimeoutEnum.T_40S,
headers: currentNode ? { CurrentNode: currentNode } : undefined,
signal,
});
};
export const listContainer = () => {
return http.post<Array<Container.ContainerOption>>(`/containers/list`, {});
@@ -46,12 +45,11 @@ export const downloadContainerFile = (params: { containerID: string; path: strin
timeout: TimeoutEnum.T_40S,
});
};
export const loadContainerStatus = (currentNode?: string) => {
return http.get<Container.ContainerStatus>(
`/containers/status`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
export const loadContainerStatus = (currentNode?: string, containersOnly = false, signal?: AbortSignal) => {
return http.get<Container.ContainerStatus>(`/containers/status`, containersOnly ? { containersOnly: true } : {}, {
headers: currentNode ? { CurrentNode: currentNode } : undefined,
signal,
});
};
export const loadResourceLimit = () => {
return http.get<Container.ResourceLimit>(`/containers/limit`);
@@ -92,12 +90,11 @@ export const containerItemStats = (containerID: string, currentNode?: string) =>
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const containerListStats = (currentNode?: string) => {
return http.get<Array<Container.ContainerListStats>>(
`/containers/list/stats`,
{},
currentNode ? { headers: { CurrentNode: currentNode } } : {},
);
export const containerListStats = (currentNode?: string, ids?: string[], signal?: AbortSignal) => {
return http.get<Array<Container.ContainerListStats>>(`/containers/list/stats`, ids ? { ids: ids.join(',') } : {}, {
headers: currentNode ? { CurrentNode: currentNode } : undefined,
signal,
});
};
export const containerStats = (id: string) => {
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
@@ -177,7 +177,8 @@
min-width="120"
>
<template #default="{ row }">
<div v-if="row.hasLoad">
<span v-if="row.state !== 'running'">—</span>
<div v-else-if="row.hasLoad">
<div class="source-font">CPU: {{ row.cpuPercent.toFixed(2) }}%</div>
<div class="float-left source-font">
{{ $t('monitor.memory') }}: {{ row.memoryPercent.toFixed(2) }}%
@@ -243,7 +244,7 @@
</template>
</el-popover>
</div>
<div v-if="!row.hasLoad">
<div v-else>
<el-button link loading></el-button>
</div>
</template>
@@ -504,7 +505,7 @@ import Uploads from '@/components/upload/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
import Status from '@/components/status/index.vue';
import { toRef, computed, reactive, ref } from 'vue';
import { toRef, computed, onBeforeUnmount, onDeactivated, reactive, ref } from 'vue';
import {
containerItemStats,
containerListStats,
@@ -729,49 +730,85 @@ const dialogContainerLogRef = ref();
const dialogRenameRef = ref();
const dialogPruneRef = ref();
const search = async (column?: any) => {
if (!isActive.value || !isExist.value) {
let requestController: AbortController | undefined;
const cancelRequests = () => {
requestController?.abort();
requestController = undefined;
loading.value = false;
};
onBeforeUnmount(cancelRequests);
onDeactivated(cancelRequests);
const loadContainers = async (background = false, column?: any) => {
if (!isActive.value || !isExist.value || (background && requestController)) {
return;
}
requestController?.abort();
const controller = new AbortController();
requestController = controller;
loading.value = !background;
localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false');
let filterItem = (router.currentRoute.value.query?.filters as string) || '';
if (column) {
paginationConfig.orderBy = column.order ? column.prop : defaultSort.prop;
paginationConfig.order = column.order || defaultSort.order;
}
let params = {
const params = {
name: searchName.value,
state: paginationConfig.state || 'all',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
filters: filterItem,
filters: props.filters || (router.currentRoute.value.query?.filters as string) || '',
orderBy: paginationConfig.orderBy,
order: paginationConfig.order,
excludeAppStore: !includeAppStore.value,
};
loading.value = true;
const [containerResult, statsResult, statusResult] = await Promise.allSettled([
searchContainer(params),
containerListStats(),
loadContainerStatus(),
]);
loading.value = false;
if (containerResult.status === 'fulfilled') {
const containers = containerResult.value.data.items || [];
const statusRequest = loadContainerStatus(undefined, true, controller.signal)
.then((res) => {
if (!controller.signal.aborted) {
updateTags(res.data || {});
}
})
.catch(() => {}); // Request errors are reported by the HTTP layer.
try {
const res = await searchContainer(params, undefined, controller.signal);
if (controller.signal.aborted) {
return;
}
const containers = res.data.items || [];
syncContainerRows(containers);
paginationConfig.total = containerResult.value.data.total;
}
if (statsResult.status === 'fulfilled') {
applyStatsToRows(statsResult.value.data || []);
}
if (statusResult.status === 'fulfilled') {
updateTags(statusResult.value.data || {});
paginationConfig.total = res.data.total;
loading.value = false;
// Stopped containers no longer produce samples; discard their previous usage.
for (const row of data.value) {
if (row.state !== 'running') {
for (const field of statFields) {
row[field] = 0;
}
}
}
const ids = containers.filter((item) => item.state === 'running').map((item) => item.containerID);
if (ids.length > 0) {
const stats = await containerListStats(undefined, ids, controller.signal);
if (!controller.signal.aborted) {
applyStatsToRows(stats.data || []);
}
}
} catch {
// Preserve the last successful rows; the HTTP layer reports the failure.
} finally {
if (!controller.signal.aborted) {
loading.value = false;
}
await statusRequest;
if (requestController === controller) {
requestController = undefined;
}
}
};
const search = (column?: any) => loadContainers(false, column);
const searchWithStatus = (item: string) => {
activeTag.value = item;
paginationConfig.state = activeTag.value;
@@ -800,21 +837,7 @@ const changePinned = (row: any, isPinned: boolean) => {
});
};
const refresh = async () => {
let filterItem = props.filters ? props.filters : '';
let params = {
name: searchName.value,
state: paginationConfig.state || 'all',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
filters: filterItem,
orderBy: paginationConfig.orderBy,
order: paginationConfig.order,
};
const [containerResult, statsResult] = await Promise.all([searchContainer(params), containerListStats()]);
syncContainerRows(containerResult.data.items || []);
applyStatsToRows(statsResult.data || []);
};
const refresh = () => loadContainers(true);
const loadSize = async (row: any) => {
containerItemStats(row.containerID).then((res) => {