diff --git a/app/agent/services/agentService.js b/app/agent/services/agentService.js
index 8dcb06ad82..03a6de4bad 100644
--- a/app/agent/services/agentService.js
+++ b/app/agent/services/agentService.js
@@ -20,6 +20,10 @@ function AgentServiceFactory(Agent, AgentVersion1, HttpRequestHelper, Host, Stat
return Host.info({ endpointId }).$promise;
}
+ /**
+ * @deprecated
+ * useAgentNodes instead
+ */
async function agents(endpointId) {
const agentVersion = getAgentApiVersion();
const service = agentVersion > 1 ? Agent : AgentVersion1;
diff --git a/app/docker/__module.js b/app/docker/__module.js
index 8f71001f2d..8b20235587 100644
--- a/app/docker/__module.js
+++ b/app/docker/__module.js
@@ -251,8 +251,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
url: '/networks',
views: {
'content@': {
- templateUrl: './views/networks/networks.html',
- controller: 'NetworksController',
+ component: 'networksListView',
},
},
data: {
diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts
index e180200b91..df6aa7cd93 100644
--- a/app/docker/react/components/index.ts
+++ b/app/docker/react/components/index.ts
@@ -16,7 +16,6 @@ import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolum
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
import { SecretsDatatable } from '@/react/docker/secrets/ListView/SecretsDatatable';
import { StacksDatatable } from '@/react/docker/stacks/ListView/StacksDatatable';
-import { NetworksDatatable } from '@/react/docker/networks/ListView/NetworksDatatable';
import { HostDetailsPanel } from '@/react/docker/host/HostDetailsPanel/HostDetailsPanel';
import { containersModule } from './containers';
@@ -37,14 +36,6 @@ const ngModule = angular
])
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
- .component(
- 'networksDatatable',
- r2a(withUIRouter(withCurrentUser(NetworksDatatable)), [
- 'dataset',
- 'onRefresh',
- 'onRemove',
- ])
- )
.component(
'gpusList',
r2a(withControlledInput(GpusList), ['value', 'onChange'])
diff --git a/app/docker/react/views/index.ts b/app/docker/react/views/index.ts
index 07da95a7fa..67d3ac6e24 100644
--- a/app/docker/react/views/index.ts
+++ b/app/docker/react/views/index.ts
@@ -11,6 +11,7 @@ import { containersModule } from './containers';
import { configsModule } from './configs';
import { imagesModule } from './images';
import { stacksModule } from './stacks';
+import { networksModule } from './networks';
export const viewsModule = angular
.module('portainer.docker.react.views', [
@@ -18,6 +19,7 @@ export const viewsModule = angular
configsModule,
imagesModule,
stacksModule,
+ networksModule,
])
.component(
'dockerDashboardView',
diff --git a/app/docker/react/views/networks.ts b/app/docker/react/views/networks.ts
new file mode 100644
index 0000000000..d7d6c1120a
--- /dev/null
+++ b/app/docker/react/views/networks.ts
@@ -0,0 +1,13 @@
+import angular from 'angular';
+
+import { r2a } from '@/react-tools/react2angular';
+import { withCurrentUser } from '@/react-tools/withCurrentUser';
+import { withUIRouter } from '@/react-tools/withUIRouter';
+import { ListView } from '@/react/docker/networks/ListView/ListView';
+
+export const networksModule = angular
+ .module('portainer.docker.react.views.networks', [])
+ .component(
+ 'networksListView',
+ r2a(withUIRouter(withCurrentUser(ListView)), [])
+ ).name;
diff --git a/app/docker/views/networks/networks.html b/app/docker/views/networks/networks.html
deleted file mode 100644
index 67496334e7..0000000000
--- a/app/docker/views/networks/networks.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js
deleted file mode 100644
index 5b191bfea6..0000000000
--- a/app/docker/views/networks/networksController.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import _ from 'lodash-es';
-import DockerNetworkHelper from '@/docker/helpers/networkHelper';
-import { processItemsInBatches } from '@/react/common/processItemsInBatches';
-
-angular.module('portainer.docker').controller('NetworksController', [
- '$q',
- '$scope',
- '$state',
- 'NetworkService',
- 'Notifications',
- 'HttpRequestHelper',
- 'endpoint',
- 'AgentService',
- function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, endpoint, AgentService) {
- $scope.removeAction = async function (selectedItems) {
- async function doRemove(network) {
- HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName);
- return NetworkService.remove(network.Id)
- .then(function success() {
- Notifications.success('Network successfully removed', network.Name);
- var index = $scope.networks.indexOf(network);
- $scope.networks.splice(index, 1);
- })
- .catch(function error(err) {
- Notifications.error('Failure', err, 'Unable to remove network');
- });
- }
-
- await processItemsInBatches(selectedItems, doRemove);
- $state.reload();
- };
-
- $scope.getNetworks = getNetworks;
-
- function groupSwarmNetworksManagerNodesFirst(networks, agents) {
- const getRole = (item) => _.find(agents, (agent) => agent.NodeName === item.NodeName).NodeRole;
-
- const nonSwarmNetworks = _.remove(networks, (item) => item.Scope !== 'swarm');
- const grouped = _.toArray(_.groupBy(networks, (item) => item.Id));
- const sorted = _.map(grouped, (arr) => _.sortBy(arr, (item) => getRole(item)));
- const arr = _.map(sorted, (a) => {
- const item = a[0];
- for (let i = 1; i < a.length; i++) {
- item.Subs.push(a[i]);
- }
- return item;
- });
- const res = _.concat(arr, ...nonSwarmNetworks);
- return res;
- }
-
- function getNetworks() {
- const req = {
- networks: NetworkService.networks(true, true, true),
- };
-
- if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
- req.agents = AgentService.agents(endpoint.Id);
- }
-
- $q.all(req)
- .then((data) => {
- const networks = _.forEach(data.networks, (item) => (item.Subs = []));
- if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
- $scope.networks = groupSwarmNetworksManagerNodesFirst(data.networks, data.agents);
- } else {
- $scope.networks = networks;
- }
-
- _.forEach($scope.networks, (network) => {
- network.IPAM.IPV4Configs = DockerNetworkHelper.getIPV4Configs(network.IPAM.Config);
- network.IPAM.IPV6Configs = DockerNetworkHelper.getIPV6Configs(network.IPAM.Config);
- });
- })
- .catch((err) => {
- $scope.networks = [];
- Notifications.error('Failure', err, 'Unable to retrieve networks');
- });
- }
-
- function initView() {
- getNetworks();
- }
-
- initView();
- },
-]);
diff --git a/app/react/docker/agent/queries/useAgentNodes.ts b/app/react/docker/agent/queries/useAgentNodes.ts
index 1264949ee3..b96c605af4 100644
--- a/app/react/docker/agent/queries/useAgentNodes.ts
+++ b/app/react/docker/agent/queries/useAgentNodes.ts
@@ -5,20 +5,20 @@ import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildAgentUrl } from './build-url';
-interface Node {
+export interface AgentNode {
IPAddress: string;
NodeName: string;
NodeRole: string;
}
-export function useAgentNodes>(
+export function useAgentNodes>(
environmentId: EnvironmentId,
apiVersion: number,
{
select,
enabled,
}: {
- select?: (data: Array) => T;
+ select?: (data: Array) => T;
enabled?: boolean;
} = {}
) {
@@ -34,7 +34,7 @@ export function useAgentNodes>(
async function getNodes(environmentId: EnvironmentId, apiVersion: number) {
try {
- const response = await axios.get>(
+ const response = await axios.get>(
buildAgentUrl(environmentId, apiVersion, 'agents')
);
return response.data;
diff --git a/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx b/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx
index 494fbb5036..8e1b9964d8 100644
--- a/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx
+++ b/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx
@@ -91,7 +91,7 @@ function getNetwork(networkName: string): DockerNetwork {
},
],
Driver: 'default',
- Options: null,
+ Options: undefined,
},
Id: '4c52a72e3772fdfb5823cf519b759e3f716e6d98cfb3bfef056e32c9c878329f',
Internal: false,
diff --git a/app/react/docker/networks/ListView/ListView.tsx b/app/react/docker/networks/ListView/ListView.tsx
new file mode 100644
index 0000000000..03dc565f62
--- /dev/null
+++ b/app/react/docker/networks/ListView/ListView.tsx
@@ -0,0 +1,19 @@
+import { PageHeader } from '@@/PageHeader';
+
+import { useDeleteNetworkListMutation } from '../queries/useDeleteNetworkListMutation';
+
+import { NetworksDatatable } from './NetworksDatatable';
+
+export function ListView() {
+ const removeMutation = useDeleteNetworkListMutation();
+
+ return (
+ <>
+
+
+ removeMutation.mutate({ networks })}
+ />
+ >
+ );
+}
diff --git a/app/react/docker/networks/ListView/NetworksDatatable.tsx b/app/react/docker/networks/ListView/NetworksDatatable.tsx
index 668ad7e59c..c67c1329f3 100644
--- a/app/react/docker/networks/ListView/NetworksDatatable.tsx
+++ b/app/react/docker/networks/ListView/NetworksDatatable.tsx
@@ -13,7 +13,6 @@ import {
import { AddButton } from '@@/buttons';
import { TableSettingsMenu } from '@@/datatables';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
-import { useRepeater } from '@@/datatables/useRepeater';
import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
@@ -22,6 +21,7 @@ import { useIsSwarm } from '../../proxy/queries/useInfo';
import { useColumns } from './columns';
import { DecoratedNetwork } from './types';
import { NestedNetworksDatatable } from './NestedNetworksTable';
+import { useNetworksData } from './useNetworksData';
const storageKey = 'docker.networks';
@@ -35,30 +35,29 @@ const settingsStore = createPersistedStore(
})
);
-type DatasetType = Array;
interface Props {
- dataset: DatasetType;
- onRemove(selectedItems: DatasetType): void;
- onRefresh(): Promise;
+ onRemove(selectedItems: Array<{ nodeName?: string; id: string }>): void;
}
-export function NetworksDatatable({ dataset, onRemove, onRefresh }: Props) {
+export function NetworksDatatable({ onRemove }: Props) {
const settings = useTableState(settingsStore, storageKey);
const environmentId = useEnvironmentId();
const isSwarm = useIsSwarm(environmentId);
+ const datasetQuery = useNetworksData(settings.autoRefreshRate);
const columns = useColumns(isSwarm);
- useRepeater(settings.autoRefreshRate, onRefresh);
+ const dataset = datasetQuery.data;
return (
settingsManager={settings}
title="Networks"
titleIcon={Network}
- dataset={dataset}
+ dataset={dataset || []}
columns={columns}
+ isLoading={datasetQuery.isLoading}
getRowCanExpand={({ original: item }) =>
!!(item.Subs && item.Subs?.length > 0)
}
@@ -83,7 +82,11 @@ export function NetworksDatatable({ dataset, onRemove, onRefresh }: Props) {
disabled={selectedRows.length === 0}
data-cy="network-removeNetworkButton"
confirmMessage="Do you want to remove the selected network(s)?"
- onConfirmed={() => onRemove(selectedRows)}
+ onConfirmed={() =>
+ onRemove(
+ selectedRows.map((n) => ({ id: n.Id, nodeName: n.NodeName }))
+ )
+ }
/>
diff --git a/app/react/docker/networks/ListView/types.ts b/app/react/docker/networks/ListView/types.ts
index b2b9013022..cc00b28f46 100644
--- a/app/react/docker/networks/ListView/types.ts
+++ b/app/react/docker/networks/ListView/types.ts
@@ -3,7 +3,7 @@ import { IPAMConfig } from 'docker-types';
import { NetworkViewModel } from '@/docker/models/network';
export type DecoratedNetwork = NetworkViewModel & {
- Subs?: DecoratedNetwork[];
+ Subs: DecoratedNetwork[];
IPAM: NetworkViewModel['IPAM'] & {
IPV4Configs?: Array;
IPV6Configs?: Array;
diff --git a/app/react/docker/networks/ListView/useNetworksData.test.ts b/app/react/docker/networks/ListView/useNetworksData.test.ts
new file mode 100644
index 0000000000..c0efc19bf2
--- /dev/null
+++ b/app/react/docker/networks/ListView/useNetworksData.test.ts
@@ -0,0 +1,91 @@
+import { describe, it, expect } from 'vitest';
+
+import { AgentNode } from '../../agent/queries/useAgentNodes';
+
+import { groupSwarmNetworksManagerNodesFirst } from './useNetworksData';
+import { DecoratedNetwork } from './types';
+
+describe('groupSwarmNetworksManagerNodesFirst', () => {
+ it('should leave non-swarm networks untouched and unsubbed', () => {
+ const local = createNetwork({ Id: 'net1', Scope: 'local' });
+
+ const result = groupSwarmNetworksManagerNodesFirst([local], []);
+
+ expect(result).toEqual([local]);
+ });
+
+ it('should group swarm networks sharing an Id, nesting the rest under Subs', () => {
+ const manager = createNetwork({
+ Id: 'net1',
+ NodeName: 'node-manager',
+ });
+ const worker = createNetwork({
+ Id: 'net1',
+ NodeName: 'node-worker',
+ });
+ const agents = [
+ createAgent({ NodeName: 'node-manager', NodeRole: 'manager' }),
+ createAgent({ NodeName: 'node-worker', NodeRole: 'worker' }),
+ ];
+
+ const [result] = groupSwarmNetworksManagerNodesFirst(
+ [worker, manager],
+ agents
+ );
+
+ expect(result.NodeName).toBe('node-manager');
+ expect(result.Subs).toEqual([worker]);
+ });
+
+ it('should place networks with no matching agent role after known roles', () => {
+ const manager = createNetwork({ Id: 'net1', NodeName: 'node-manager' });
+ const unknown = createNetwork({ Id: 'net1', NodeName: 'node-unknown' });
+ const agents = [
+ createAgent({ NodeName: 'node-manager', NodeRole: 'manager' }),
+ ];
+
+ const [result] = groupSwarmNetworksManagerNodesFirst(
+ [unknown, manager],
+ agents
+ );
+
+ expect(result.NodeName).toBe('node-manager');
+ expect(result.Subs).toEqual([unknown]);
+ });
+
+ it('should put grouped swarm networks before non-swarm networks', () => {
+ const swarm = createNetwork({ Id: 'net1', Scope: 'swarm' });
+ const local = createNetwork({ Id: 'net2', Scope: 'local' });
+
+ const result = groupSwarmNetworksManagerNodesFirst([local, swarm], []);
+
+ expect(result.map((n) => n.Id)).toEqual(['net1', 'net2']);
+ });
+});
+
+function createNetwork(
+ overrides: Partial = {}
+): DecoratedNetwork {
+ return {
+ Id: 'network1',
+ Name: 'test-network',
+ Scope: 'swarm',
+ Driver: 'overlay',
+ Attachable: false,
+ Internal: false,
+ Ingress: false,
+ Labels: {},
+ IPAM: {},
+ Subs: [],
+ ...overrides,
+ } as DecoratedNetwork;
+}
+
+function createAgent(overrides: Partial = {}): AgentNode {
+ return {
+ IPAddress: '10.0.0.1',
+ NodeName: 'node1',
+ NodeRole: 'worker',
+ ...overrides,
+ };
+}
diff --git a/app/react/docker/networks/ListView/useNetworksData.ts b/app/react/docker/networks/ListView/useNetworksData.ts
new file mode 100644
index 0000000000..44525c75d2
--- /dev/null
+++ b/app/react/docker/networks/ListView/useNetworksData.ts
@@ -0,0 +1,95 @@
+import _ from 'lodash';
+import { IPAMConfig } from 'docker-types';
+
+import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
+import { NetworkViewModel } from '@/docker/models/network';
+import DockerNetworkHelper from '@/docker/helpers/networkHelper';
+
+import { useAgentNodes, AgentNode } from '../../agent/queries/useAgentNodes';
+import { useIsSwarmAgent } from '../../proxy/queries/useIsSwarmAgent';
+import { useNetworks } from '../queries/useNetworks';
+import { useApiVersion } from '../../agent/queries/useApiVersion';
+
+import { DecoratedNetwork } from './types';
+
+export function useNetworksData(autoRefreshRate?: number) {
+ const environmentId = useEnvironmentId();
+
+ const networksQuery = useNetworks(
+ environmentId,
+ {
+ local: true,
+ swarm: true,
+ swarmAttachable: true,
+ },
+ {
+ select: (networks) =>
+ networks.map((n) => {
+ const network = new NetworkViewModel(n);
+ const ipam: NetworkViewModel['IPAM'] & {
+ IPV4Configs?: Array;
+ IPV6Configs?: Array;
+ } = network.IPAM ?? {};
+
+ ipam.IPV4Configs = DockerNetworkHelper.getIPV4Configs(ipam.Config);
+ ipam.IPV6Configs = DockerNetworkHelper.getIPV6Configs(ipam.Config);
+
+ network.IPAM = ipam;
+ return {
+ ...network,
+ IPAM: ipam,
+ Subs: [],
+ } satisfies DecoratedNetwork;
+ }),
+ autoRefreshRate,
+ }
+ );
+ const isSwarmAgent = useIsSwarmAgent();
+ const apiVersionQuery = useApiVersion(environmentId);
+ const agentsQuery = useAgentNodes(environmentId, apiVersionQuery.data || 1, {
+ enabled: isSwarmAgent,
+ });
+
+ if (!networksQuery.data) {
+ return { isLoading: true };
+ }
+
+ const networks = groupSwarmNetworksManagerNodesFirst(
+ networksQuery.data,
+ agentsQuery.data
+ );
+
+ return {
+ data: networks,
+ isLoading: networksQuery.isLoading,
+ };
+}
+
+export function groupSwarmNetworksManagerNodesFirst(
+ networks: Array,
+ agents: Array = []
+): Array {
+ const nonSwarmNetworks = networks.filter((item) => item.Scope !== 'swarm');
+ const swarmNetworks = networks.filter((item) => item.Scope === 'swarm');
+
+ const swarmNetworksById = new Map>();
+ swarmNetworks.forEach((item) => {
+ const group = swarmNetworksById.get(item.Id) ?? [];
+ group.push(item);
+ swarmNetworksById.set(item.Id, group);
+ });
+
+ const groupedSwarmNetworks = Array.from(swarmNetworksById.values()).map(
+ (group) => {
+ const [item, ...rest] = _.sortBy(group, getRole);
+ item.Subs = rest;
+ return item;
+ }
+ );
+
+ return [...groupedSwarmNetworks, ...nonSwarmNetworks];
+
+ function getRole(item: NetworkViewModel) {
+ return agents.find((agent) => agent.NodeName === item.NodeName)?.NodeRole;
+ }
+}
diff --git a/app/react/docker/networks/queries/useDeleteNetworkListMutation.tsx b/app/react/docker/networks/queries/useDeleteNetworkListMutation.tsx
new file mode 100644
index 0000000000..9ced2a9574
--- /dev/null
+++ b/app/react/docker/networks/queries/useDeleteNetworkListMutation.tsx
@@ -0,0 +1,29 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { processItemsInBatches } from '@/react/common/processItemsInBatches';
+import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
+import { notifySuccess } from '@/portainer/services/notifications';
+import { withError, withInvalidate } from '@/react-tools/react-query';
+
+import { queryKeys } from './queryKeys';
+import { deleteNetwork } from './useDeleteNetworkMutation';
+
+export function useDeleteNetworkListMutation() {
+ const environmentId = useEnvironmentId();
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ networks,
+ }: {
+ networks: Array<{ nodeName?: string; id: string }>;
+ }) =>
+ processItemsInBatches(networks, ({ id, nodeName }) =>
+ deleteNetwork(environmentId, id, { nodeName }).then(() =>
+ notifySuccess('Network successfully removed', id)
+ )
+ ),
+ ...withInvalidate(queryClient, [queryKeys.base(environmentId)]),
+ ...withError('Failed to remove networks'),
+ });
+}
diff --git a/app/react/docker/networks/queries/useDeleteNetworkMutation.ts b/app/react/docker/networks/queries/useDeleteNetworkMutation.ts
index 1f1c359a37..71ae203d82 100644
--- a/app/react/docker/networks/queries/useDeleteNetworkMutation.ts
+++ b/app/react/docker/networks/queries/useDeleteNetworkMutation.ts
@@ -1,10 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import {
- mutationOptions,
- withError,
- withInvalidate,
-} from '@/react-tools/react-query';
+import { withError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios/axios';
@@ -17,14 +13,20 @@ import { queryKeys } from './queryKeys';
export function useDeleteNetwork(environmentId: EnvironmentId) {
const queryClient = useQueryClient();
- return useMutation(
- ({ networkId, nodeName }: { networkId: NetworkId; nodeName?: string }) =>
- deleteNetwork(environmentId, networkId, { nodeName }),
- mutationOptions(
- withInvalidate(queryClient, [queryKeys.base(environmentId)]),
- withError('Unable to remove network')
- )
- );
+ return useMutation({
+ mutationFn: ({
+ networkId,
+ nodeName,
+ }: {
+ networkId: NetworkId;
+ nodeName?: string;
+ }) => deleteNetwork(environmentId, networkId, { nodeName }),
+ ...withError('Unable to remove network'),
+ onSuccess(_, { networkId }) {
+ queryClient.cancelQueries(queryKeys.item(environmentId, networkId));
+ return queryClient.invalidateQueries(queryKeys.base(environmentId));
+ },
+ });
}
/**
diff --git a/app/react/docker/networks/queries/useNetworks.ts b/app/react/docker/networks/queries/useNetworks.ts
index 9844c47e00..d4479f31c1 100644
--- a/app/react/docker/networks/queries/useNetworks.ts
+++ b/app/react/docker/networks/queries/useNetworks.ts
@@ -1,15 +1,24 @@
import { useQuery } from '@tanstack/react-query';
+import { IPAMConfig, Network } from 'docker-types';
import axios, { parseAxiosError } from '@/portainer/services/axios/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
-import { DockerNetwork } from '../types';
import { withFiltersQueryParam } from '../../proxy/queries/utils';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
+import { PortainerResponse } from '../../types';
+import { DockerNetwork, IPConfig, NetworkResponseContainers } from '../types';
import { queryKeys } from './queryKeys';
import { NetworksQuery } from './types';
+export type NetworkListResponseItem = PortainerResponse<
+ Network & {
+ ConfigFrom?: { Network: string };
+ ConfigOnly?: boolean;
+ }
+>;
+
export function useNetworks>(
environmentId: EnvironmentId,
query: NetworksQuery,
@@ -17,16 +26,24 @@ export function useNetworks>(
enabled = true,
onSuccess,
select,
+ autoRefreshRate,
}: {
enabled?: boolean;
onSuccess?(networks: T): void;
select?(networks: Array): T;
+ autoRefreshRate?: number;
} = {}
) {
return useQuery(
queryKeys.list(environmentId, query),
() => getNetworks(environmentId, query),
- { enabled, onSuccess, select }
+ {
+ enabled,
+ onSuccess,
+ select,
+
+ refetchInterval: autoRefreshRate ?? false,
+ }
);
}
@@ -38,16 +55,18 @@ export async function getNetworks(
{ local, swarm, swarmAttachable, filters }: NetworksQuery
) {
try {
- const { data } = await axios.get>(
+ const { data } = await axios.get>(
buildDockerProxyUrl(environmentId, 'networks'),
{
params: { ...withFiltersQueryParam(filters) },
}
);
+ const parsed = data.map(toDockerNetwork);
+
return !local && !swarm && !swarmAttachable
- ? data
- : data.filter(
+ ? parsed
+ : parsed.filter(
(network) =>
(local && network.Scope === 'local') ||
(swarm && network.Scope === 'swarm') ||
@@ -59,3 +78,60 @@ export async function getNetworks(
throw parseAxiosError(err, 'Unable to retrieve networks');
}
}
+
+function toDockerNetwork(req: NetworkListResponseItem): DockerNetwork {
+ return {
+ ...req,
+
+ Name: req.Name || '',
+ Id: req.Id || '',
+ Driver: req.Driver || '',
+ Scope: req.Scope || '',
+ Attachable: req.Attachable ?? false,
+ Internal: req.Internal ?? false,
+ IPAM: toIpam(req.IPAM),
+ Options: req.Options ?? {},
+ Containers: toContainers(req.Containers),
+ };
+
+ function toContainers(
+ req: NetworkListResponseItem['Containers']
+ ): NetworkResponseContainers {
+ if (!req) return {};
+ return Object.fromEntries(
+ Object.entries(req).map(([id, c]) => [
+ id,
+ {
+ EndpointID: c.EndpointID ?? '',
+ IPv4Address: c.IPv4Address ?? '',
+ IPv6Address: c.IPv6Address ?? '',
+ MacAddress: c.MacAddress ?? '',
+ Name: c.Name ?? '',
+ },
+ ])
+ );
+ }
+
+ function toIpam(req: NetworkListResponseItem['IPAM']): DockerNetwork['IPAM'] {
+ if (!req) {
+ return {
+ Config: [],
+ Driver: '',
+ Options: {},
+ };
+ }
+
+ return {
+ Config: req.Config?.map(toIpamConfig) || [],
+ Driver: req.Driver || '',
+ Options: req.Options,
+ };
+ }
+
+ function toIpamConfig(req: IPAMConfig): IPConfig {
+ return {
+ Subnet: req.Subnet ?? '',
+ Gateway: req.Gateway ?? '',
+ };
+ }
+}
diff --git a/app/react/docker/networks/types.ts b/app/react/docker/networks/types.ts
index 9f1cfc3b1d..e53069c8de 100644
--- a/app/react/docker/networks/types.ts
+++ b/app/react/docker/networks/types.ts
@@ -12,7 +12,7 @@ export type NetworkId = string;
export type NetworkOptions = Record;
-type IpamOptions = Record | null;
+type IpamOptions = Record | undefined;
export type NetworkResponseContainer = {
EndpointID: string;