mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
refactor(docker): move components to react [EE-3348] (#7084)
This commit is contained in:
parent
7238372d8d
commit
46e1a01625
41 changed files with 61 additions and 76 deletions
138
app/react/docker/networks/ItemView/ItemView.tsx
Normal file
138
app/react/docker/networks/ItemView/ItemView.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useCurrentStateAndParams } from '@uirouter/react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { AccessControlPanel } from '@/portainer/access-control/AccessControlPanel/AccessControlPanel';
|
||||
import { ResourceControlType } from '@/portainer/access-control/types';
|
||||
import { DockerContainer } from '@/react/docker/containers/types';
|
||||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useNetwork, useDeleteNetwork } from '../queries';
|
||||
import { isSystemNetwork } from '../network.helper';
|
||||
import { DockerNetwork, NetworkContainer } from '../types';
|
||||
import { useContainers } from '../../containers/queries';
|
||||
|
||||
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
||||
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
||||
import { NetworkContainersTable } from './NetworkContainersTable';
|
||||
|
||||
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 deleteNetworkMutation = useDeleteNetwork();
|
||||
const filters = {
|
||||
network: [networkId],
|
||||
};
|
||||
const containersQuery = useContainers(environmentId, filters);
|
||||
|
||||
useEffect(() => {
|
||||
if (networkQuery.data && containersQuery.data) {
|
||||
setNetworkContainers(
|
||||
filterContainersInNetwork(networkQuery.data, containersQuery.data)
|
||||
);
|
||||
}
|
||||
}, [networkQuery.data, containersQuery.data]);
|
||||
|
||||
if (!networkQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const network = networkQuery.data;
|
||||
|
||||
const resourceControl = network.Portainer?.ResourceControl
|
||||
? new ResourceControlViewModel(network.Portainer.ResourceControl)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Network details"
|
||||
breadcrumbs={[
|
||||
{ link: 'docker.networks', label: 'Networks' },
|
||||
{
|
||||
link: 'docker.networks.network',
|
||||
label: networkQuery.data.Name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<NetworkDetailsTable
|
||||
network={networkQuery.data}
|
||||
onRemoveNetworkClicked={onRemoveNetworkClicked}
|
||||
/>
|
||||
|
||||
<AccessControlPanel
|
||||
onUpdateSuccess={() =>
|
||||
queryClient.invalidateQueries([
|
||||
'environments',
|
||||
environmentId,
|
||||
'docker',
|
||||
'networks',
|
||||
networkId,
|
||||
])
|
||||
}
|
||||
resourceControl={resourceControl}
|
||||
resourceType={ResourceControlType.Network}
|
||||
disableOwnershipChange={isSystemNetwork(networkQuery.data.Name)}
|
||||
resourceId={networkId}
|
||||
/>
|
||||
<NetworkOptionsTable options={networkQuery.data.Options} />
|
||||
<NetworkContainersTable
|
||||
networkContainers={networkContainers}
|
||||
nodeName={nodeName}
|
||||
environmentId={environmentId}
|
||||
networkId={networkId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
async function onRemoveNetworkClicked() {
|
||||
const message = 'Do you want to delete the network?';
|
||||
const confirmed = await confirmDeletionAsync(message);
|
||||
|
||||
if (confirmed) {
|
||||
deleteNetworkMutation.mutate(
|
||||
{ environmentId, networkId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.stateService.go('docker.networks');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { NetworkContainer } from '../types';
|
||||
|
||||
import { NetworkContainersTable } from './NetworkContainersTable';
|
||||
|
||||
const networkContainers: NetworkContainer[] = [
|
||||
{
|
||||
EndpointID:
|
||||
'069d703f3ff4939956233137c4c6270d7d46c04fb10c44d3ec31fde1b46d6610',
|
||||
IPv4Address: '10.0.1.3/24',
|
||||
IPv6Address: '',
|
||||
MacAddress: '02:42:0a:00:01:03',
|
||||
Name: 'portainer-agent_agent.8hjjodl4hoyhuq1kscmzccyqn.wnv2pp17f8ayeopke2z56yw5x',
|
||||
Id: 'd54c74b7e1c5649d2a880d3fc02c6201d1d2f85a4fee718f978ec8b147239295',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@uirouter/react', () => ({
|
||||
...jest.requireActual('@uirouter/react'),
|
||||
useCurrentStateAndParams: jest.fn(() => ({
|
||||
params: { endpointId: 1 },
|
||||
})),
|
||||
}));
|
||||
|
||||
test('Network container values should be visible and the link should be valid', async () => {
|
||||
const user = new UserViewModel({ Username: 'test', Role: 1 });
|
||||
const { findByText } = renderWithQueryClient(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<NetworkContainersTable
|
||||
networkContainers={networkContainers}
|
||||
nodeName=""
|
||||
environmentId={1}
|
||||
networkId="pc8xc9s6ot043vl1q5iz4zhfs"
|
||||
/>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await expect(findByText('Containers in network')).resolves.toBeVisible();
|
||||
await expect(findByText(networkContainers[0].Name)).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(networkContainers[0].IPv4Address)
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(networkContainers[0].MacAddress)
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText('Leave network', { exact: false })
|
||||
).resolves.toBeVisible();
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
import { Authorized } from '@/portainer/hooks/useUser';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { DetailsTable } from '@@/DetailsTable';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { NetworkContainer, NetworkId } from '../types';
|
||||
import { useDisconnectContainer } from '../queries';
|
||||
|
||||
type Props = {
|
||||
networkContainers: NetworkContainer[];
|
||||
nodeName: string;
|
||||
environmentId: EnvironmentId;
|
||||
networkId: NetworkId;
|
||||
};
|
||||
|
||||
const tableHeaders = [
|
||||
'Container Name',
|
||||
'IPv4 Address',
|
||||
'IPv6 Address',
|
||||
'MacAddress',
|
||||
'Actions',
|
||||
];
|
||||
|
||||
export function NetworkContainersTable({
|
||||
networkContainers,
|
||||
nodeName,
|
||||
environmentId,
|
||||
networkId,
|
||||
}: Props) {
|
||||
const disconnectContainer = useDisconnectContainer();
|
||||
|
||||
if (networkContainers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle title="Containers in network" icon="fa-server" />
|
||||
<WidgetBody 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,
|
||||
}}
|
||||
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="danger"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="fa fa-trash-alt space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
122
app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx
Normal file
122
app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { render } from '@/react-tools/test-utils';
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { DockerNetwork } from '../types';
|
||||
|
||||
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
||||
|
||||
jest.mock('@uirouter/react', () => ({
|
||||
...jest.requireActual('@uirouter/react'),
|
||||
useCurrentStateAndParams: jest.fn(() => ({
|
||||
params: { endpointId: 1 },
|
||||
})),
|
||||
}));
|
||||
|
||||
test('Network details values should be visible', async () => {
|
||||
const network = getNetwork('test');
|
||||
|
||||
const { findByText } = await renderComponent(true, network);
|
||||
|
||||
await expect(findByText(network.Name)).resolves.toBeVisible();
|
||||
await expect(findByText(network.Id)).resolves.toBeVisible();
|
||||
await expect(findByText(network.Driver)).resolves.toBeVisible();
|
||||
await expect(findByText(network.Scope)).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(network.IPAM?.Config[0].Gateway || 'not found', { exact: false })
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(network.IPAM?.Config[0].Subnet || 'not found', { exact: false })
|
||||
).resolves.toBeVisible();
|
||||
});
|
||||
|
||||
test(`System networks shouldn't show a delete button`, async () => {
|
||||
const systemNetwork = getNetwork('bridge');
|
||||
const { queryByText } = await renderComponent(true, systemNetwork);
|
||||
|
||||
const deleteButton = queryByText('Delete this network');
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
|
||||
test('Non system networks should have a delete button', async () => {
|
||||
const nonSystemNetwork = getNetwork('non system network');
|
||||
|
||||
const { queryByText } = await renderComponent(true, nonSystemNetwork);
|
||||
|
||||
const button = queryByText('Delete this network');
|
||||
expect(button).toBeVisible();
|
||||
});
|
||||
|
||||
async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
|
||||
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
|
||||
|
||||
const queries = render(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<NetworkDetailsTable
|
||||
network={network}
|
||||
onRemoveNetworkClicked={() => {}}
|
||||
/>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await expect(queries.findByText('Network details')).resolves.toBeVisible();
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
function getNetwork(networkName: string): DockerNetwork {
|
||||
return {
|
||||
Attachable: false,
|
||||
Containers: {
|
||||
a761fcafdae3bdae42cf3702c8554b3e1b0334f85dd6b65b3584aff7246279e4: {
|
||||
EndpointID:
|
||||
'404afa6e25cede7c0fd70180777b662249cd83e40fa9a41aa593d2bac0fc5e18',
|
||||
IPv4Address: '172.17.0.2/16',
|
||||
IPv6Address: '',
|
||||
MacAddress: '02:42:ac:11:00:02',
|
||||
Name: 'portainer',
|
||||
},
|
||||
},
|
||||
Driver: 'bridge',
|
||||
IPAM: {
|
||||
Config: [
|
||||
{
|
||||
Gateway: '172.17.0.1',
|
||||
Subnet: '172.17.0.0/16',
|
||||
},
|
||||
],
|
||||
Driver: 'default',
|
||||
Options: null,
|
||||
},
|
||||
Id: '4c52a72e3772fdfb5823cf519b759e3f716e6d98cfb3bfef056e32c9c878329f',
|
||||
Internal: false,
|
||||
Name: networkName,
|
||||
Options: {
|
||||
'com.docker.network.bridge.default_bridge': 'true',
|
||||
'com.docker.network.bridge.enable_icc': 'true',
|
||||
'com.docker.network.bridge.enable_ip_masquerade': 'true',
|
||||
'com.docker.network.bridge.host_binding_ipv4': '0.0.0.0',
|
||||
'com.docker.network.bridge.name': 'docker0',
|
||||
'com.docker.network.driver.mtu': '1500',
|
||||
},
|
||||
Portainer: {
|
||||
ResourceControl: {
|
||||
Id: 41,
|
||||
ResourceId:
|
||||
'85d807847e4a4adb374a2a105124eda607ef584bef2eb6acf8091f3afd8446db',
|
||||
Type: 4,
|
||||
UserAccesses: [
|
||||
{
|
||||
UserId: 2,
|
||||
AccessLevel: 1,
|
||||
},
|
||||
],
|
||||
TeamAccesses: [],
|
||||
Public: true,
|
||||
System: false,
|
||||
AdministratorsOnly: true,
|
||||
},
|
||||
},
|
||||
Scope: 'local',
|
||||
};
|
||||
}
|
120
app/react/docker/networks/ItemView/NetworkDetailsTable.tsx
Normal file
120
app/react/docker/networks/ItemView/NetworkDetailsTable.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { Fragment } from 'react';
|
||||
import DockerNetworkHelper from 'Docker/helpers/networkHelper';
|
||||
|
||||
import { Authorized } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { DetailsTable } from '@@/DetailsTable';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { isSystemNetwork } from '../network.helper';
|
||||
import { DockerNetwork, IPConfig } from '../types';
|
||||
|
||||
interface Props {
|
||||
network: DockerNetwork;
|
||||
onRemoveNetworkClicked: () => void;
|
||||
}
|
||||
|
||||
export function NetworkDetailsTable({
|
||||
network,
|
||||
onRemoveNetworkClicked,
|
||||
}: Props) {
|
||||
const allowRemoveNetwork = !isSystemNetwork(network.Name);
|
||||
const ipv4Configs: IPConfig[] = DockerNetworkHelper.getIPV4Configs(
|
||||
network.IPAM?.Config
|
||||
);
|
||||
const ipv6Configs: IPConfig[] = DockerNetworkHelper.getIPV6Configs(
|
||||
network.IPAM?.Config
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle title="Network details" icon="fa-sitemap" />
|
||||
<WidgetBody 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()}
|
||||
>
|
||||
<i
|
||||
className="fa fa-trash-alt 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>
|
||||
))}
|
||||
|
||||
{/* 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>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function getConfigDetails(configValue?: string) {
|
||||
return configValue ? ` - ${configValue}` : '';
|
||||
}
|
||||
|
||||
function getAuxiliaryAddresses(auxiliaryAddresses?: object) {
|
||||
return auxiliaryAddresses
|
||||
? ` - ${Object.values(auxiliaryAddresses).join(' - ')}`
|
||||
: '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { render } from '@/react-tools/test-utils';
|
||||
|
||||
import { NetworkOptions } from '../types';
|
||||
|
||||
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
||||
|
||||
const options: NetworkOptions = {
|
||||
'com.docker.network.bridge.default_bridge': 'true',
|
||||
'com.docker.network.bridge.enable_icc': 'true',
|
||||
'com.docker.network.bridge.enable_ip_masquerade': 'true',
|
||||
'com.docker.network.bridge.host_binding_ipv4': '0.0.0.0',
|
||||
'com.docker.network.bridge.name': 'docker0',
|
||||
'com.docker.network.driver.mtu': '1500',
|
||||
};
|
||||
|
||||
test('Network options values should be visible', async () => {
|
||||
const { findByText, findAllByText } = render(
|
||||
<NetworkOptionsTable options={options} />
|
||||
);
|
||||
|
||||
await expect(findByText('Network options')).resolves.toBeVisible();
|
||||
// expect to find three 'true' values for the first 3 options
|
||||
const cells = await findAllByText('true');
|
||||
expect(cells).toHaveLength(3);
|
||||
await expect(
|
||||
findByText(options['com.docker.network.bridge.host_binding_ipv4'])
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(options['com.docker.network.bridge.name'])
|
||||
).resolves.toBeVisible();
|
||||
await expect(
|
||||
findByText(options['com.docker.network.driver.mtu'])
|
||||
).resolves.toBeVisible();
|
||||
});
|
35
app/react/docker/networks/ItemView/NetworkOptionsTable.tsx
Normal file
35
app/react/docker/networks/ItemView/NetworkOptionsTable.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { DetailsTable } from '@@/DetailsTable';
|
||||
|
||||
import { NetworkOptions } from '../types';
|
||||
|
||||
type Props = {
|
||||
options: NetworkOptions;
|
||||
};
|
||||
|
||||
export function NetworkOptionsTable({ options }: Props) {
|
||||
const networkEntries = Object.entries(options);
|
||||
|
||||
if (networkEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<Widget>
|
||||
<WidgetTitle title="Network options" icon="fa-cogs" />
|
||||
<WidgetBody className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
1
app/react/docker/networks/ItemView/index.ts
Normal file
1
app/react/docker/networks/ItemView/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { ItemView } from './ItemView';
|
5
app/react/docker/networks/network.helper.ts
Normal file
5
app/react/docker/networks/network.helper.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const systemNetworks = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||
|
||||
export function isSystemNetwork(networkName: string) {
|
||||
return systemNetworks.includes(networkName);
|
||||
}
|
70
app/react/docker/networks/network.service.ts
Normal file
70
app/react/docker/networks/network.service.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { ContainerId } from '@/react/docker/containers/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
|
||||
import { NetworkId, DockerNetwork } from './types';
|
||||
|
||||
type NetworkAction = 'connect' | 'disconnect' | 'create';
|
||||
|
||||
export async function getNetwork(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId
|
||||
) {
|
||||
try {
|
||||
const { data: network } = await axios.get<DockerNetwork>(
|
||||
buildUrl(environmentId, networkId)
|
||||
);
|
||||
return network;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve network details');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteNetwork(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId
|
||||
) {
|
||||
try {
|
||||
await axios.delete(buildUrl(environmentId, networkId));
|
||||
return networkId;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to remove network');
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectContainer(
|
||||
environmentId: EnvironmentId,
|
||||
networkId: NetworkId,
|
||||
containerId: ContainerId
|
||||
) {
|
||||
try {
|
||||
await axios.post(buildUrl(environmentId, networkId, 'disconnect'), {
|
||||
Container: containerId,
|
||||
Force: false,
|
||||
});
|
||||
return { networkId, environmentId };
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to disconnect container from network'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(
|
||||
environmentId: EnvironmentId,
|
||||
networkId?: NetworkId,
|
||||
action?: NetworkAction
|
||||
) {
|
||||
let url = `endpoints/${environmentId}/docker/networks`;
|
||||
|
||||
if (networkId) {
|
||||
url += `/${networkId}`;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
82
app/react/docker/networks/queries.ts
Normal file
82
app/react/docker/networks/queries.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import {
|
||||
error as notifyError,
|
||||
success as notifySuccess,
|
||||
} from '@/portainer/services/notifications';
|
||||
import { ContainerId } from '@/react/docker/containers/types';
|
||||
|
||||
import {
|
||||
getNetwork,
|
||||
deleteNetwork,
|
||||
disconnectContainer,
|
||||
} from './network.service';
|
||||
import { NetworkId } from './types';
|
||||
|
||||
export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) {
|
||||
return useQuery(
|
||||
['environments', environmentId, 'docker', 'networks', networkId],
|
||||
() => getNetwork(environmentId, networkId),
|
||||
{
|
||||
onError: (err) => {
|
||||
notifyError('Failure', err as Error, 'Unable to get network');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDeleteNetwork() {
|
||||
return useMutation(
|
||||
({
|
||||
environmentId,
|
||||
networkId,
|
||||
}: {
|
||||
environmentId: EnvironmentId;
|
||||
networkId: NetworkId;
|
||||
}) => deleteNetwork(environmentId, networkId),
|
||||
{
|
||||
onSuccess: (networkId) => {
|
||||
notifySuccess('Network successfully removed', networkId);
|
||||
},
|
||||
onError: (err) => {
|
||||
notifyError('Failure', err as Error, 'Unable to remove network');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDisconnectContainer() {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
({
|
||||
containerId,
|
||||
environmentId,
|
||||
networkId,
|
||||
}: {
|
||||
containerId: ContainerId;
|
||||
environmentId: EnvironmentId;
|
||||
networkId: NetworkId;
|
||||
}) => disconnectContainer(environmentId, networkId, containerId),
|
||||
{
|
||||
onSuccess: ({ networkId, environmentId }) => {
|
||||
notifySuccess('Container successfully disconnected', networkId);
|
||||
return client.invalidateQueries([
|
||||
'environments',
|
||||
environmentId,
|
||||
'docker',
|
||||
'networks',
|
||||
networkId,
|
||||
]);
|
||||
},
|
||||
onError: (err) => {
|
||||
notifyError(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to disconnect container from network'
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
50
app/react/docker/networks/types.ts
Normal file
50
app/react/docker/networks/types.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { PortainerMetadata } from '@/react/docker/types';
|
||||
|
||||
import { ContainerId } from '../containers/types';
|
||||
|
||||
export type IPConfig = {
|
||||
Subnet: string;
|
||||
Gateway: string;
|
||||
IPRange?: string;
|
||||
AuxiliaryAddresses?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type NetworkId = string;
|
||||
|
||||
export type NetworkOptions = Record<string, string>;
|
||||
|
||||
type IpamOptions = Record<string, string> | null;
|
||||
|
||||
export type NetworkResponseContainer = {
|
||||
EndpointID: string;
|
||||
IPv4Address: string;
|
||||
IPv6Address: string;
|
||||
MacAddress: string;
|
||||
Name: string;
|
||||
};
|
||||
|
||||
export interface NetworkContainer extends NetworkResponseContainer {
|
||||
Id: ContainerId;
|
||||
}
|
||||
|
||||
export type NetworkResponseContainers = Record<
|
||||
ContainerId,
|
||||
NetworkResponseContainer
|
||||
>;
|
||||
|
||||
export interface DockerNetwork {
|
||||
Name: string;
|
||||
Id: NetworkId;
|
||||
Driver: string;
|
||||
Scope: string;
|
||||
Attachable: boolean;
|
||||
Internal: boolean;
|
||||
IPAM: {
|
||||
Config: IPConfig[];
|
||||
Driver: string;
|
||||
Options: IpamOptions;
|
||||
};
|
||||
Portainer?: PortainerMetadata;
|
||||
Options: NetworkOptions;
|
||||
Containers: NetworkResponseContainers;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue