mirror of https://github.com/portainer/portainer
fix(docker/networks): load containers from target node [EE-5446] (#8927)
parent
a35e18a904
commit
8e785e8bb4
|
@ -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()) {
|
||||
|
|
|
@ -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 (
|
||||
<RowProvider context={{ environment }}>
|
||||
|
|
|
@ -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<DockerContainerResponse[]>(
|
||||
urlBuilder(environmentId, undefined, 'json'),
|
||||
{
|
||||
params: { all, filters: filters && JSON.stringify(filters) },
|
||||
headers: nodeName
|
||||
? {
|
||||
[agentTargetHeader]: nodeName,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
return data.map((c) => parseViewModel(c));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<TableContainer>
|
||||
<TableTitle label="Containers in network" icon={Server} />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
data-cy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="dangerlight"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
data-cy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="dangerlight"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<TableContainer>
|
||||
<TableTitle label="Network details" icon={Share2} />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
data-cy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<Icon
|
||||
icon={Trash2}
|
||||
className="space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
data-cy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<Icon
|
||||
icon={Trash2}
|
||||
className="space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<TableContainer>
|
||||
<TableTitle label="Network options" icon={Share2} />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<DockerNetwork>(
|
||||
buildUrl(environmentId, networkId)
|
||||
buildUrl(environmentId, networkId),
|
||||
nodeName
|
||||
? {
|
||||
headers: {
|
||||
[agentTargetHeader]: nodeName,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
return network;
|
||||
} catch (e) {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 (
|
||||
<RowProvider context={{ environment }}>
|
||||
|
|
Loading…
Reference in New Issue