diff --git a/app/docker/__module.js b/app/docker/__module.js index e4cd47a4a..230465cdd 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -3,8 +3,9 @@ import angular from 'angular'; import { EnvironmentStatus } from '@/portainer/environments/types'; import containersModule from './containers'; import { componentsModule } from './components'; +import { networksModule } from './networks'; -angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule]).config([ +angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule, networksModule]).config([ '$stateRegistryProvider', function ($stateRegistryProvider) { 'use strict'; @@ -323,8 +324,7 @@ angular.module('portainer.docker', ['portainer.app', containersModule, component url: '/:id?nodeName', views: { 'content@': { - templateUrl: './views/networks/edit/network.html', - controller: 'NetworkController', + component: 'networkDetailsView', }, }, }; diff --git a/app/docker/containers/containers.service.ts b/app/docker/containers/containers.service.ts index bce1ee41d..8d84d047d 100644 --- a/app/docker/containers/containers.service.ts +++ b/app/docker/containers/containers.service.ts @@ -2,10 +2,16 @@ import { EnvironmentId } from '@/portainer/environments/types'; import PortainerError from '@/portainer/error'; import axios from '@/portainer/services/axios'; +import { NetworkId } from '../networks/types'; import { genericHandler } from '../rest/response/handlers'; import { ContainerId, DockerContainer } from './types'; +export interface Filters { + label?: string[]; + network?: NetworkId[]; +} + export async function startContainer( endpointId: EnvironmentId, id: ContainerId @@ -86,15 +92,37 @@ export async function removeContainer( } } +export async function getContainers( + environmentId: EnvironmentId, + filters?: Filters +) { + try { + const { data } = await axios.get( + urlBuilder(environmentId, '', 'json'), + { + params: { all: 0, filters }, + } + ); + + return data; + } catch (e) { + throw new PortainerError('Unable to retrieve containers', e as Error); + } +} + function urlBuilder( endpointId: EnvironmentId, - id: ContainerId, + id?: ContainerId, action?: string ) { - const url = `/endpoints/${endpointId}/docker/containers/${id}`; + let url = `/endpoints/${endpointId}/docker/containers`; + + if (id) { + url += `/${id}`; + } if (action) { - return `${url}/${action}`; + url += `/${action}`; } return url; diff --git a/app/docker/containers/queries.ts b/app/docker/containers/queries.ts new file mode 100644 index 000000000..96e2fe059 --- /dev/null +++ b/app/docker/containers/queries.ts @@ -0,0 +1,18 @@ +import { useQuery } from 'react-query'; + +import { EnvironmentId } from '@/portainer/environments/types'; + +import { getContainers, Filters } from './containers.service'; + +export function useContainers(environmentId: EnvironmentId, filters?: Filters) { + return useQuery( + ['environments', environmentId, 'docker', 'containers', { filters }], + () => getContainers(environmentId, filters), + { + meta: { + title: 'Failure', + message: 'Unable to get containers in network', + }, + } + ); +} diff --git a/app/docker/networks/edit/NetworkContainersTable.test.tsx b/app/docker/networks/edit/NetworkContainersTable.test.tsx new file mode 100644 index 000000000..17cb912a9 --- /dev/null +++ b/app/docker/networks/edit/NetworkContainersTable.test.tsx @@ -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( + + + + ); + + 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(); +}); diff --git a/app/docker/networks/edit/NetworkContainersTable.tsx b/app/docker/networks/edit/NetworkContainersTable.tsx new file mode 100644 index 000000000..316f1f8e4 --- /dev/null +++ b/app/docker/networks/edit/NetworkContainersTable.tsx @@ -0,0 +1,97 @@ +import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget'; +import { DetailsTable } from '@/portainer/components/DetailsTable'; +import { Button } from '@/portainer/components/Button'; +import { Authorized } from '@/portainer/hooks/useUser'; +import { EnvironmentId } from '@/portainer/environments/types'; +import { Link } from '@/portainer/components/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 ( +
+
+ + + + + {networkContainers.map((container) => ( + + + + {container.Name} + + + {container.IPv4Address || '-'} + {container.IPv6Address || '-'} + {container.MacAddress || '-'} + + +