mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 09:04:48 +00:00
refactor(networks): migrate networks list view to react [BE-6563] (#3071)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
<page-header title="'Network list'" breadcrumbs="['Networks']" reload="true"> </page-header>
|
||||
|
||||
<networks-datatable ng-if="networks" dataset="networks" on-refresh="(getNetworks)" on-remove="(removeAction)"></networks-datatable>
|
||||
@@ -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();
|
||||
},
|
||||
]);
|
||||
@@ -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<T = Array<Node>>(
|
||||
export function useAgentNodes<T = Array<AgentNode>>(
|
||||
environmentId: EnvironmentId,
|
||||
apiVersion: number,
|
||||
{
|
||||
select,
|
||||
enabled,
|
||||
}: {
|
||||
select?: (data: Array<Node>) => T;
|
||||
select?: (data: Array<AgentNode>) => T;
|
||||
enabled?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
@@ -34,7 +34,7 @@ export function useAgentNodes<T = Array<Node>>(
|
||||
|
||||
async function getNodes(environmentId: EnvironmentId, apiVersion: number) {
|
||||
try {
|
||||
const response = await axios.get<Array<Node>>(
|
||||
const response = await axios.get<Array<AgentNode>>(
|
||||
buildAgentUrl(environmentId, apiVersion, 'agents')
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -91,7 +91,7 @@ function getNetwork(networkName: string): DockerNetwork {
|
||||
},
|
||||
],
|
||||
Driver: 'default',
|
||||
Options: null,
|
||||
Options: undefined,
|
||||
},
|
||||
Id: '4c52a72e3772fdfb5823cf519b759e3f716e6d98cfb3bfef056e32c9c878329f',
|
||||
Internal: false,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useDeleteNetworkListMutation } from '../queries/useDeleteNetworkListMutation';
|
||||
|
||||
import { NetworksDatatable } from './NetworksDatatable';
|
||||
|
||||
export function ListView() {
|
||||
const removeMutation = useDeleteNetworkListMutation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Network List" breadcrumbs={['Networks']} reload />
|
||||
|
||||
<NetworksDatatable
|
||||
onRemove={(networks) => removeMutation.mutate({ networks })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<TableSettings>(
|
||||
})
|
||||
);
|
||||
|
||||
type DatasetType = Array<DecoratedNetwork>;
|
||||
interface Props {
|
||||
dataset: DatasetType;
|
||||
onRemove(selectedItems: DatasetType): void;
|
||||
onRefresh(): Promise<void>;
|
||||
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 (
|
||||
<ExpandableDatatable<DecoratedNetwork>
|
||||
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 }))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Authorized>
|
||||
<Authorized authorizations="DockerNetworkCreate">
|
||||
|
||||
@@ -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<IPAMConfig>;
|
||||
IPV6Configs?: Array<IPAMConfig>;
|
||||
|
||||
@@ -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> = {}
|
||||
): 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> = {}): AgentNode {
|
||||
return {
|
||||
IPAddress: '10.0.0.1',
|
||||
NodeName: 'node1',
|
||||
NodeRole: 'worker',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -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<IPAMConfig>;
|
||||
IPV6Configs?: Array<IPAMConfig>;
|
||||
} = 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<DecoratedNetwork>,
|
||||
agents: Array<AgentNode> = []
|
||||
): Array<DecoratedNetwork> {
|
||||
const nonSwarmNetworks = networks.filter((item) => item.Scope !== 'swarm');
|
||||
const swarmNetworks = networks.filter((item) => item.Scope === 'swarm');
|
||||
|
||||
const swarmNetworksById = new Map<string, Array<DecoratedNetwork>>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<T = Array<DockerNetwork>>(
|
||||
environmentId: EnvironmentId,
|
||||
query: NetworksQuery,
|
||||
@@ -17,16 +26,24 @@ export function useNetworks<T = Array<DockerNetwork>>(
|
||||
enabled = true,
|
||||
onSuccess,
|
||||
select,
|
||||
autoRefreshRate,
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
onSuccess?(networks: T): void;
|
||||
select?(networks: Array<DockerNetwork>): 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<Array<DockerNetwork>>(
|
||||
const { data } = await axios.get<Array<NetworkListResponseItem>>(
|
||||
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 ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export type NetworkId = string;
|
||||
|
||||
export type NetworkOptions = Record<string, string>;
|
||||
|
||||
type IpamOptions = Record<string, string> | null;
|
||||
type IpamOptions = Record<string, string> | undefined;
|
||||
|
||||
export type NetworkResponseContainer = {
|
||||
EndpointID: string;
|
||||
|
||||
Reference in New Issue
Block a user