mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-26 00:01:11 +00:00
fix: container list (#14156)
* fix: container list * fix: container list
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user