From 8e785e8bb46390b7c68c64eca8709fc93493e180 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 18 May 2023 12:53:30 +0700 Subject: [PATCH] fix(docker/networks): load containers from target node [EE-5446] (#8927) --- app/portainer/services/axios.ts | 4 +- .../ContainersDatatable.tsx | 9 +- .../docker/containers/queries/containers.ts | 39 +++-- .../docker/containers/queries/query-keys.ts | 6 +- .../docker/networks/ItemView/ItemView.tsx | 68 ++++----- .../ItemView/NetworkContainersTable.tsx | 90 ++++++------ .../networks/ItemView/NetworkDetailsTable.tsx | 136 +++++++++--------- .../networks/ItemView/NetworkOptionsTable.tsx | 18 ++- app/react/docker/networks/network.service.ts | 17 ++- app/react/docker/networks/queries.ts | 17 ++- .../ItemView/StackContainersDatatable.tsx | 10 +- 11 files changed, 216 insertions(+), 198 deletions(-) diff --git a/app/portainer/services/axios.ts b/app/portainer/services/axios.ts index a26d1559d..980d6534a 100644 --- a/app/portainer/services/axios.ts +++ b/app/portainer/services/axios.ts @@ -27,6 +27,8 @@ axios.interceptors.request.use(async (config) => { return newConfig; }); +export const agentTargetHeader = 'X-PortainerAgent-Target'; + export function agentInterceptor(config: AxiosRequestConfig) { if (!config.url || !config.url.includes('/docker/')) { return config; @@ -35,7 +37,7 @@ export function agentInterceptor(config: AxiosRequestConfig) { const newConfig = { headers: config.headers || {}, ...config }; const target = portainerAgentTargetHeader(); if (target) { - newConfig.headers['X-PortainerAgent-Target'] = target; + newConfig.headers[agentTargetHeader] = target; } if (portainerAgentManagerOperation()) { diff --git a/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatable.tsx b/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatable.tsx index aeec6adaa..9a79b12fe 100644 --- a/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatable.tsx +++ b/app/react/docker/containers/ListView/ContainersDatatable/ContainersDatatable.tsx @@ -52,12 +52,9 @@ export function ContainersDatatable({ const [search, setSearch] = useSearchBarState(storageKey); - const containersQuery = useContainers( - environment.Id, - true, - undefined, - settings.autoRefreshRate * 1000 - ); + const containersQuery = useContainers(environment.Id, { + autoRefreshRate: settings.autoRefreshRate * 1000, + }); return ( diff --git a/app/react/docker/containers/queries/containers.ts b/app/react/docker/containers/queries/containers.ts index 5aa511879..8f1a301cd 100644 --- a/app/react/docker/containers/queries/containers.ts +++ b/app/react/docker/containers/queries/containers.ts @@ -1,7 +1,11 @@ import { useQuery } from 'react-query'; import { EnvironmentId } from '@/react/portainer/environments/types'; -import axios, { parseAxiosError } from '@/portainer/services/axios'; +import axios, { + agentTargetHeader, + parseAxiosError, +} from '@/portainer/services/axios'; +import { withGlobalError } from '@/react-tools/react-query'; import { urlBuilder } from '../containers.service'; import { DockerContainerResponse } from '../types/response'; @@ -10,20 +14,27 @@ import { parseViewModel } from '../utils'; import { Filters } from './types'; import { queryKeys } from './query-keys'; +interface UseContainers { + all?: boolean; + filters?: Filters; + nodeName?: string; +} + export function useContainers( environmentId: EnvironmentId, - all = true, - filters?: Filters, - autoRefreshRate?: number + { + autoRefreshRate, + + ...params + }: UseContainers & { + autoRefreshRate?: number; + } = {} ) { return useQuery( - queryKeys.filters(environmentId, all, filters), - () => getContainers(environmentId, all, filters), + queryKeys.filters(environmentId, params), + () => getContainers(environmentId, params), { - meta: { - title: 'Failure', - message: 'Unable to retrieve containers', - }, + ...withGlobalError('Unable to retrieve containers'), refetchInterval() { return autoRefreshRate ?? false; }, @@ -33,14 +44,18 @@ export function useContainers( async function getContainers( environmentId: EnvironmentId, - all = true, - filters?: Filters + { all = true, filters, nodeName }: UseContainers = {} ) { try { const { data } = await axios.get( urlBuilder(environmentId, undefined, 'json'), { params: { all, filters: filters && JSON.stringify(filters) }, + headers: nodeName + ? { + [agentTargetHeader]: nodeName, + } + : undefined, } ); return data.map((c) => parseViewModel(c)); diff --git a/app/react/docker/containers/queries/query-keys.ts b/app/react/docker/containers/queries/query-keys.ts index 38026e498..d7843e4f9 100644 --- a/app/react/docker/containers/queries/query-keys.ts +++ b/app/react/docker/containers/queries/query-keys.ts @@ -8,8 +8,10 @@ export const queryKeys = { list: (environmentId: EnvironmentId) => [dockerQueryKeys.root(environmentId), 'containers'] as const, - filters: (environmentId: EnvironmentId, all?: boolean, filters?: Filters) => - [...queryKeys.list(environmentId), { all, filters }] as const, + filters: ( + environmentId: EnvironmentId, + params: { all?: boolean; filters?: Filters; nodeName?: string } = {} + ) => [...queryKeys.list(environmentId), params] as const, container: (environmentId: EnvironmentId, id: string) => [...queryKeys.list(environmentId), id] as const, diff --git a/app/react/docker/networks/ItemView/ItemView.tsx b/app/react/docker/networks/ItemView/ItemView.tsx index d426c98bd..b6131e037 100644 --- a/app/react/docker/networks/ItemView/ItemView.tsx +++ b/app/react/docker/networks/ItemView/ItemView.tsx @@ -1,7 +1,5 @@ -import { useState, useEffect } from 'react'; import { useRouter, useCurrentStateAndParams } from '@uirouter/react'; import { useQueryClient } from 'react-query'; -import _ from 'lodash'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel/AccessControlPanel'; @@ -15,7 +13,7 @@ import { PageHeader } from '@@/PageHeader'; import { useNetwork, useDeleteNetwork } from '../queries'; import { isSystemNetwork } from '../network.helper'; -import { DockerNetwork, NetworkContainer } from '../types'; +import { NetworkResponseContainers } from '../types'; import { NetworkDetailsTable } from './NetworkDetailsTable'; import { NetworkOptionsTable } from './NetworkOptionsTable'; @@ -25,28 +23,18 @@ export function ItemView() { const router = useRouter(); const queryClient = useQueryClient(); - const [networkContainers, setNetworkContainers] = useState< - NetworkContainer[] - >([]); const { params: { id: networkId, nodeName }, } = useCurrentStateAndParams(); const environmentId = useEnvironmentId(); - - const networkQuery = useNetwork(environmentId, networkId); + const networkQuery = useNetwork(environmentId, networkId, { nodeName }); const deleteNetworkMutation = useDeleteNetwork(); - const filters = { - network: [networkId], - }; - const containersQuery = useContainers(environmentId, true, filters); - - useEffect(() => { - if (networkQuery.data && containersQuery.data) { - setNetworkContainers( - filterContainersInNetwork(networkQuery.data, containersQuery.data) - ); - } - }, [networkQuery.data, containersQuery.data]); + const containersQuery = useContainers(environmentId, { + filters: { + network: [networkId], + }, + nodeName, + }); if (!networkQuery.data) { return null; @@ -54,6 +42,10 @@ export function ItemView() { const network = networkQuery.data; + const networkContainers = filterContainersInNetwork( + network.Containers, + containersQuery.data + ); const resourceControl = network.Portainer?.ResourceControl ? new ResourceControlViewModel(network.Portainer.ResourceControl) : undefined; @@ -116,24 +108,20 @@ export function ItemView() { ); } } - - function filterContainersInNetwork( - network: DockerNetwork, - containers: DockerContainer[] - ) { - const containersInNetwork = _.compact( - containers.map((container) => { - const containerInNetworkResponse = network.Containers[container.Id]; - if (containerInNetworkResponse) { - const containerInNetwork: NetworkContainer = { - ...containerInNetworkResponse, - Id: container.Id, - }; - return containerInNetwork; - } - return null; - }) - ); - return containersInNetwork; - } +} + +function filterContainersInNetwork( + networkContainers?: NetworkResponseContainers, + containers: DockerContainer[] = [] +) { + if (!networkContainers) { + return []; + } + + return containers + .filter((container) => networkContainers[container.Id]) + .map((container) => ({ + ...networkContainers[container.Id], + Id: container.Id, + })); } diff --git a/app/react/docker/networks/ItemView/NetworkContainersTable.tsx b/app/react/docker/networks/ItemView/NetworkContainersTable.tsx index 652a33c1e..481acf43d 100644 --- a/app/react/docker/networks/ItemView/NetworkContainersTable.tsx +++ b/app/react/docker/networks/ItemView/NetworkContainersTable.tsx @@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { Icon } from '@/react/components/Icon'; -import { Table, TableContainer, TableTitle } from '@@/datatables'; +import { TableContainer, TableTitle } from '@@/datatables'; import { DetailsTable } from '@@/DetailsTable'; import { Button } from '@@/buttons'; import { Link } from '@@/Link'; @@ -42,53 +42,51 @@ export function NetworkContainersTable({ return ( - - - {networkContainers.map((container) => ( - - + + + + + - - - - - - ))} - -
- + {networkContainers.map((container) => ( +
+ + {container.Name} + + {container.IPv4Address || '-'}{container.IPv6Address || '-'}{container.MacAddress || '-'} + + {container.IPv4Address || '-'}{container.IPv6Address || '-'}{container.MacAddress || '-'} - - - -
+ + Leave Network + + + + + ))} +
); } diff --git a/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx b/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx index 3078d69e5..00503e28c 100644 --- a/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx +++ b/app/react/docker/networks/ItemView/NetworkDetailsTable.tsx @@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react'; import DockerNetworkHelper from '@/docker/helpers/networkHelper'; import { Authorized } from '@/react/hooks/useUser'; -import { Table, TableContainer, TableTitle } from '@@/datatables'; +import { TableContainer, TableTitle } from '@@/datatables'; import { DetailsTable } from '@@/DetailsTable'; import { Button } from '@@/buttons'; import { Icon } from '@@/Icon'; @@ -32,76 +32,74 @@ export function NetworkDetailsTable({ return ( - - - {/* networkRowContent */} - {network.Name} - - {network.Id} - {allowRemoveNetwork && ( - - - - )} - - {network.Driver} - {network.Scope} - - {String(network.Attachable)} - - - {String(network.Internal)} - + + {/* networkRowContent */} + {network.Name} + + {network.Id} + {allowRemoveNetwork && ( + + + + )} + + {network.Driver} + {network.Scope} + + {String(network.Attachable)} + + + {String(network.Internal)} + - {/* IPV4 ConfigRowContent */} - {ipv4Configs.map((config) => ( - - - {`IPV4 Gateway${getConfigDetails(config.Gateway)}`} - - - {`IPV4 Excluded IPs${getAuxiliaryAddresses( - config.AuxiliaryAddresses - )}`} - - - ))} + {/* IPV4 ConfigRowContent */} + {ipv4Configs.map((config) => ( + + + {`IPV4 Gateway${getConfigDetails(config.Gateway)}`} + + + {`IPV4 Excluded IPs${getAuxiliaryAddresses( + config.AuxiliaryAddresses + )}`} + + + ))} - {/* IPV6 ConfigRowContent */} - {ipv6Configs.map((config) => ( - - - {`IPV6 Gateway${getConfigDetails(config.Gateway)}`} - - - {`IPV6 Excluded IPs${getAuxiliaryAddresses( - config.AuxiliaryAddresses - )}`} - - - ))} - -
+ {/* IPV6 ConfigRowContent */} + {ipv6Configs.map((config) => ( + + + {`IPV6 Gateway${getConfigDetails(config.Gateway)}`} + + + {`IPV6 Excluded IPs${getAuxiliaryAddresses( + config.AuxiliaryAddresses + )}`} + + + ))} +
); diff --git a/app/react/docker/networks/ItemView/NetworkOptionsTable.tsx b/app/react/docker/networks/ItemView/NetworkOptionsTable.tsx index 0feafffa6..de4f70a86 100644 --- a/app/react/docker/networks/ItemView/NetworkOptionsTable.tsx +++ b/app/react/docker/networks/ItemView/NetworkOptionsTable.tsx @@ -1,6 +1,6 @@ import { Share2 } from 'lucide-react'; -import { Table, TableContainer, TableTitle } from '@@/datatables'; +import { TableContainer, TableTitle } from '@@/datatables'; import { DetailsTable } from '@@/DetailsTable'; import { NetworkOptions } from '../types'; @@ -19,15 +19,13 @@ export function NetworkOptionsTable({ options }: Props) { return ( - - - {networkEntries.map(([key, value]) => ( - - {value} - - ))} - -
+ + {networkEntries.map(([key, value]) => ( + + {value} + + ))} +
); } diff --git a/app/react/docker/networks/network.service.ts b/app/react/docker/networks/network.service.ts index a75cc68b2..2aa4ca9f1 100644 --- a/app/react/docker/networks/network.service.ts +++ b/app/react/docker/networks/network.service.ts @@ -1,5 +1,8 @@ import { ContainerId } from '@/react/docker/containers/types'; -import axios, { parseAxiosError } from '@/portainer/services/axios'; +import axios, { + agentTargetHeader, + parseAxiosError, +} from '@/portainer/services/axios'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { NetworkId, DockerNetwork } from './types'; @@ -8,11 +11,19 @@ type NetworkAction = 'connect' | 'disconnect' | 'create'; export async function getNetwork( environmentId: EnvironmentId, - networkId: NetworkId + networkId: NetworkId, + { nodeName }: { nodeName?: string } = {} ) { try { const { data: network } = await axios.get( - buildUrl(environmentId, networkId) + buildUrl(environmentId, networkId), + nodeName + ? { + headers: { + [agentTargetHeader]: nodeName, + }, + } + : undefined ); return network; } catch (e) { diff --git a/app/react/docker/networks/queries.ts b/app/react/docker/networks/queries.ts index dbfee1d2c..e6a25666b 100644 --- a/app/react/docker/networks/queries.ts +++ b/app/react/docker/networks/queries.ts @@ -14,10 +14,21 @@ import { } from './network.service'; import { NetworkId } from './types'; -export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) { +export function useNetwork( + environmentId: EnvironmentId, + networkId: NetworkId, + { nodeName }: { nodeName?: string } = {} +) { return useQuery( - ['environments', environmentId, 'docker', 'networks', networkId], - () => getNetwork(environmentId, networkId), + [ + 'environments', + environmentId, + 'docker', + 'networks', + networkId, + { nodeName }, + ], + () => getNetwork(environmentId, networkId, { nodeName }), { onError: (err) => { notifyError('Failure', err as Error, 'Unable to get network'); diff --git a/app/react/docker/stacks/ItemView/StackContainersDatatable.tsx b/app/react/docker/stacks/ItemView/StackContainersDatatable.tsx index 782ebf867..2e53e52a4 100644 --- a/app/react/docker/stacks/ItemView/StackContainersDatatable.tsx +++ b/app/react/docker/stacks/ItemView/StackContainersDatatable.tsx @@ -49,14 +49,12 @@ export function StackContainersDatatable({ environment, stackName }: Props) { columns.filter((col) => col.canHide).map((col) => col.id) ); - const containersQuery = useContainers( - environment.Id, - true, - { + const containersQuery = useContainers(environment.Id, { + filters: { label: [`com.docker.compose.project=${stackName}`], }, - settings.autoRefreshRate * 1000 - ); + autoRefreshRate: settings.autoRefreshRate * 1000, + }); return (