fix(networks): fix auto-refresh interval and address review follow-ups [BE-6563] (#3165)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Chaim Lev-Ari
2026-07-19 08:59:55 +03:00
committed by GitHub
parent a27627d1fb
commit 7f6fe01a68
6 changed files with 50 additions and 19 deletions
@@ -6,9 +6,14 @@ import { isAxiosError } from '@/portainer/services/axios/utils/isAxiosError';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
export function useApiVersion(environmentId: EnvironmentId) {
return useQuery(['environment', environmentId, 'agent', 'ping'], () =>
getApiVersion(environmentId)
export function useApiVersion(
environmentId: EnvironmentId,
{ enabled = true }: { enabled?: boolean } = {}
) {
return useQuery(
['environment', environmentId, 'agent', 'api-version'],
() => getApiVersion(environmentId),
{ enabled }
);
}
@@ -36,7 +36,9 @@ const settingsStore = createPersistedStore<TableSettings>(
);
interface Props {
onRemove(selectedItems: Array<{ nodeName?: string; id: string }>): void;
onRemove(
selectedItems: Array<{ nodeName?: string; id: string; name: string }>
): void;
}
export function NetworksDatatable({ onRemove }: Props) {
@@ -45,7 +47,7 @@ export function NetworksDatatable({ onRemove }: Props) {
const environmentId = useEnvironmentId();
const isSwarm = useIsSwarm(environmentId);
const datasetQuery = useNetworksData(settings.autoRefreshRate);
const datasetQuery = useNetworksData(settings.autoRefreshRate * 1000);
const columns = useColumns(isSwarm);
const dataset = datasetQuery.data;
@@ -84,7 +86,11 @@ export function NetworksDatatable({ onRemove }: Props) {
confirmMessage="Do you want to remove the selected network(s)?"
onConfirmed={() =>
onRemove(
selectedRows.map((n) => ({ id: n.Id, nodeName: n.NodeName }))
selectedRows.map((n) => ({
id: n.Id,
name: n.Name,
nodeName: n.NodeName,
}))
)
}
/>
@@ -61,6 +61,20 @@ describe('groupSwarmNetworksManagerNodesFirst', () => {
expect(result.map((n) => n.Id)).toEqual(['net1', 'net2']);
});
it('should not mutate the input networks', () => {
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' }),
];
groupSwarmNetworksManagerNodesFirst([worker, manager], agents);
expect(manager.Subs).toEqual([]);
expect(worker.Subs).toEqual([]);
});
});
function createNetwork(
@@ -45,13 +45,15 @@ export function useNetworksData(autoRefreshRate?: number) {
}
);
const isSwarmAgent = useIsSwarmAgent();
const apiVersionQuery = useApiVersion(environmentId);
const apiVersionQuery = useApiVersion(environmentId, {
enabled: isSwarmAgent,
});
const agentsQuery = useAgentNodes(environmentId, apiVersionQuery.data || 1, {
enabled: isSwarmAgent,
});
if (!networksQuery.data) {
return { isLoading: true };
return { isLoading: networksQuery.isLoading };
}
const networks = groupSwarmNetworksManagerNodesFirst(
@@ -82,8 +84,7 @@ export function groupSwarmNetworksManagerNodesFirst(
const groupedSwarmNetworks = Array.from(swarmNetworksById.values()).map(
(group) => {
const [item, ...rest] = _.sortBy(group, getRole);
item.Subs = rest;
return item;
return { ...item, Subs: rest };
}
);
@@ -2,8 +2,8 @@ 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 { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { withInvalidate } from '@/react-tools/react-query';
import { queryKeys } from './queryKeys';
import { deleteNetwork } from './useDeleteNetworkMutation';
@@ -16,14 +16,16 @@ export function useDeleteNetworkListMutation() {
mutationFn: ({
networks,
}: {
networks: Array<{ nodeName?: string; id: string }>;
networks: Array<{ nodeName?: string; id: string; name: string }>;
}) =>
processItemsInBatches(networks, ({ id, nodeName }) =>
deleteNetwork(environmentId, id, { nodeName }).then(() =>
notifySuccess('Network successfully removed', id)
)
),
processItemsInBatches(networks, async ({ id, name, nodeName }) => {
try {
await deleteNetwork(environmentId, id, { nodeName });
notifySuccess('Network successfully removed', name);
} catch (err) {
notifyError(`Unable to remove network ${name}`, err);
}
}),
...withInvalidate(queryClient, [queryKeys.base(environmentId)]),
...withError('Failed to remove networks'),
});
}
@@ -3,6 +3,7 @@ import { IPAMConfig, Network } from 'docker-types';
import axios, { parseAxiosError } from '@/portainer/services/axios/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { withError } from '@/react-tools/react-query';
import { withFiltersQueryParam } from '../../proxy/queries/utils';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
@@ -43,6 +44,7 @@ export function useNetworks<T = Array<DockerNetwork>>(
select,
refetchInterval: autoRefreshRate ?? false,
...withError('Unable to retrieve networks'),
}
);
}
@@ -130,6 +132,7 @@ function toDockerNetwork(req: NetworkListResponseItem): DockerNetwork {
function toIpamConfig(req: IPAMConfig): IPConfig {
return {
...req,
Subnet: req.Subnet ?? '',
Gateway: req.Gateway ?? '',
};