fix: fix container list

This commit is contained in:
guoyuqi
2026-10-08 17:44:29 +08:00
parent 2701af9054
commit d41dd382ab
4 changed files with 181 additions and 96 deletions
+18 -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
@@ -964,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
containerService.StreamLogs(c, streamLog)
}
func (b *BaseApi) CleanNetworks(c *gin.Context) {
result, err := containerService.CleanNetworks()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
+81 -34
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,9 @@ 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) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
@@ -123,7 +125,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 +208,32 @@ 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})
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if !containersOnly {
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(c, client)
}
containers, err := client.ContainerList(c, container.ListOptions{All: true})
if err != nil {
return data, err
}
data.ContainerCount = len(containers)
for _, item := range containers {
switch item.State {
@@ -292,27 +299,67 @@ 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})
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
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
}
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
return loadCpuAndMem(ctx, client, id)
}), nil
}
// A fixed worker pool bounds Docker stats requests, including for legacy callers
// that request all containers. Stopped containers do not need a stats sample.
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
datas := make([]dto.ContainerListStats, len(list))
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 i, item := range list {
datas[i].ContainerID = item.ID
}
jobs := make(chan int)
var wg sync.WaitGroup
for worker := 0; worker < min(8, len(list)); worker++ {
wg.Add(1)
go func() {
defer wg.Done()
for index := range jobs {
if ctx.Err() != nil || list[index].State != "running" {
continue
}
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
datas[index] = load(sampleCtx, list[index].ID)
cancel()
}
}()
}
dispatch:
for index := range list {
select {
case <-ctx.Done():
break dispatch
case jobs <- index:
}
}
close(jobs)
wg.Wait()
return datas, nil
return datas
}
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
@@ -1715,11 +1762,11 @@ 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 {
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
}
@@ -1942,11 +1989,11 @@ func transPortToStr(ports []container.Port) []string {
return docker.SimplifyPorts(ports)
}
func loadComposeCount(client *client.Client) int {
func loadComposeCount(ctx context.Context, client *client.Client) int {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options)
list, err := client.ContainerList(ctx, options)
if err != nil {
return 0
}
+16 -19
View File
@@ -3,13 +3,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`, {});
@@ -44,12 +43,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`);
@@ -90,12 +88,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}`);
@@ -490,7 +490,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 { computed, reactive, ref } from 'vue';
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
import {
containerItemStats,
containerListStats,
@@ -703,47 +703,87 @@ const dialogContainerLogRef = ref();
const dialogRenameRef = ref();
const dialogPruneRef = ref();
const search = async (column?: any) => {
if (!isActive.value || !isExist.value) {
let requestController: AbortController | undefined;
let requestVersion = 0;
let refreshPending = false;
onBeforeUnmount(() => {
requestVersion++;
requestController?.abort();
});
const loadContainers = async (background = false, column?: any) => {
if (!isActive.value || !isExist.value || (background && refreshPending)) {
return;
}
requestController?.abort();
const controller = new AbortController();
requestController = controller;
const version = ++requestVersion;
const isCurrent = () => version === requestVersion && !controller.signal.aborted;
refreshPending = true;
loading.value = !background;
localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false');
let filterItem = (router.currentRoute.value.query?.filters as string) || '';
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.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 || [];
// Counts and resource samples must never hold up the table itself.
const statusRequest = loadContainerStatus(undefined, true, controller.signal)
.then((res) => {
if (isCurrent()) {
updateTags(res.data || {});
}
})
.catch(() => {});
try {
const res = await searchContainer(params, undefined, controller.signal);
if (!isCurrent()) {
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;
// Clear old usage when a previously running container has stopped.
applyStatsToRows(
containers
.filter((item) => item.state !== 'running')
.map((item) => ({
containerID: item.containerID,
...Object.fromEntries(statFields.map((field) => [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 (isCurrent()) {
applyStatsToRows(stats.data || []);
}
}
} catch {
// The HTTP layer reports failures; keep the last successful rows visible.
} finally {
if (isCurrent()) {
loading.value = false;
}
await statusRequest;
if (isCurrent()) {
refreshPending = false;
}
}
};
const search = (column?: any) => loadContainers(false, column);
const searchWithStatus = (item: string) => {
activeTag.value = item;
paginationConfig.state = activeTag.value;
@@ -772,21 +812,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) => {