diff --git a/app/docker/__module.js b/app/docker/__module.js index 448147b62..0a89b205c 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -431,8 +431,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([ url: '/:id?nodeName', views: { 'content@': { - templateUrl: '~@/docker/views/containers/edit/container.html', - controller: 'ContainerController', + component: 'containerItemView', }, }, }; diff --git a/app/docker/react/components/containers.ts b/app/docker/react/components/containers.ts index 6d3f377ad..31da49555 100644 --- a/app/docker/react/components/containers.ts +++ b/app/docker/react/components/containers.ts @@ -1,76 +1,6 @@ import angular from 'angular'; -import { withCurrentUser } from '@/react-tools/withCurrentUser'; -import { withUIRouter } from '@/react-tools/withUIRouter'; -import { r2a } from '@/react-tools/react2angular'; -import { ContainerNetworksDatatable } from '@/react/docker/containers/ItemView/ContainerNetworksDatatable'; -import { RestartPolicySection } from '@/react/docker/containers/ItemView/RestartPolicySection/RestartPolicySection'; -import { ContainerActionsSection } from '@/react/docker/containers/ItemView/ContainerActionsSection/ContainerActionsSection'; -import { ContainerStatusSection } from '@/react/docker/containers/ItemView/ContainerStatusSection/ContainerStatusSection'; -import { CreateImageSection } from '@/react/docker/containers/ItemView/CreateImageSection/CreateImageSection'; -import { ContainerDetailsSection } from '@/react/docker/containers/ItemView/ContainerDetailsSection/ContainerDetailsSection'; -import { VolumesSection } from '@/react/docker/containers/ItemView/VolumesSection/VolumesSection'; - -const ngModule = angular - .module('portainer.docker.react.components.containers', []) - .component( - 'dockerContainerNetworksDatatable', - r2a(withUIRouter(withCurrentUser(ContainerNetworksDatatable)), [ - 'container', - 'dataset', - 'nodeName', - ]) - ) - .component( - 'containerRestartPolicy', - r2a(withUIRouter(withCurrentUser(RestartPolicySection)), [ - 'environmentId', - 'containerId', - 'nodeName', - 'name', - 'maximumRetryCount', - 'onUpdateSuccess', - ]) - ) - .component( - 'containerActionsSection', - r2a(withUIRouter(withCurrentUser(ContainerActionsSection)), [ - 'onSuccess', - 'environmentId', - 'nodeName', - 'container', - ]) - ) - .component( - 'containerStatusSection', - r2a(withUIRouter(withCurrentUser(ContainerStatusSection)), [ - 'environmentId', - 'container', - 'nodeName', - 'onSuccessUpdate', - 'registryId', - ]) - ) - .component( - 'createImageSection', - r2a(withUIRouter(withCurrentUser(CreateImageSection)), [ - 'environmentId', - 'containerId', - 'onSuccess', - ]) - ) - .component( - 'containerDetailsSection', - r2a(withUIRouter(withCurrentUser(ContainerDetailsSection)), [ - 'environmentId', - 'container', - 'nodeName', - 'onUpdateSuccess', - ]) - ) - .component( - 'volumesSection', - r2a(withUIRouter(withCurrentUser(VolumesSection)), ['volumes', 'nodeName']) - ); - -export const containersModule = ngModule.name; +export const containersModule = angular.module( + 'portainer.docker.react.components.containers', + [] +).name; diff --git a/app/docker/react/views/containers.ts b/app/docker/react/views/containers.ts index 9780c0719..442a72691 100644 --- a/app/docker/react/views/containers.ts +++ b/app/docker/react/views/containers.ts @@ -9,6 +9,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter'; import { LogView } from '@/react/docker/containers/LogView'; import { CreateView } from '@/react/docker/containers/CreateView'; import { InspectView } from '@/react/docker/containers/InspectView/InspectView'; +import { ItemView } from '@/react/docker/containers/ItemView/ItemView'; export const containersModule = angular .module('portainer.docker.react.views.containers', []) @@ -20,6 +21,10 @@ export const containersModule = angular 'containersView', r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), ['endpoint']) ) + .component( + 'containerItemView', + r2a(withUIRouter(withCurrentUser(ItemView)), []) + ) // the view only contains the information panel when logging is disabled // this is a temporary solution to avoid creating a publicly exposed component // or an AngularJS component until the logs view is migrated to React @@ -53,8 +58,7 @@ function config($stateRegistryProvider: StateRegistry) { url: '/:id?nodeName', views: { 'content@': { - templateUrl: '~@/docker/views/containers/edit/container.html', - controller: 'ContainerController', + component: 'containerItemView', }, }, }); diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts index 6a99e6a76..5b49a9217 100644 --- a/app/react-tools/test-mocks.ts +++ b/app/react-tools/test-mocks.ts @@ -223,45 +223,47 @@ export function createMockStack(overrides?: Partial): Stack { export function createMockContainer( overrides?: Partial ): ContainerDetailsViewModel { - return { - Id: 'container-id-123', - Image: 'sha256:abcd1234', - State: { - Status: 'running', - Running: true, - Paused: false, - Restarting: false, - OOMKilled: false, - Dead: false, - Pid: 1234, - ExitCode: 0, - Error: '', - StartedAt: '2024-01-01T00:00:00Z', - FinishedAt: '0001-01-01T00:00:00Z', - Health: undefined, - }, - Created: '2024-01-01T00:00:00Z', - Name: '/test-container', - NetworkSettings: { - Ports: { - '80/tcp': [{ HostIp: '0.0.0.0', HostPort: '8080' }], + return _.merge( + { + Id: 'container-id-123', + Image: 'sha256:abcd1234', + State: { + Status: 'running', + Running: true, + Paused: false, + Restarting: false, + OOMKilled: false, + Dead: false, + Pid: 1234, + ExitCode: 0, + Error: '', + StartedAt: '2024-01-01T00:00:00Z', + FinishedAt: '0001-01-01T00:00:00Z', + Health: undefined, }, + Created: '2024-01-01T00:00:00Z', + Name: '/test-container', + NetworkSettings: { + Ports: { + '80/tcp': [{ HostIp: '0.0.0.0', HostPort: '8080' }], + }, + }, + Args: [], + Config: { + Image: 'nginx:latest', + Cmd: ['nginx', '-g', 'daemon off;'], + Entrypoint: [], + Env: ['PATH=/usr/local/bin', 'NODE_ENV=production'], + Labels: { 'com.example.label': 'value' }, + }, + HostConfig: { + RestartPolicy: { Name: 'always', MaximumRetryCount: 0 }, + Sysctls: { 'net.ipv4.ip_forward': '1' }, + DeviceRequests: [], + }, + Mounts: [], + Model: {} as ContainerDetailsViewModel['Model'], }, - Args: [], - Config: { - Image: 'nginx:latest', - Cmd: ['nginx', '-g', 'daemon off;'], - Entrypoint: [], - Env: ['PATH=/usr/local/bin', 'NODE_ENV=production'], - Labels: { 'com.example.label': 'value' }, - }, - HostConfig: { - RestartPolicy: { Name: 'always', MaximumRetryCount: 0 }, - Sysctls: { 'net.ipv4.ip_forward': '1' }, - DeviceRequests: [], - }, - Mounts: [], - Model: {} as ContainerDetailsViewModel['Model'], - ...overrides, - } as ContainerDetailsViewModel; + overrides + ) as ContainerDetailsViewModel; } diff --git a/app/react/docker/containers/CreateView/useInitialValues.ts b/app/react/docker/containers/CreateView/useInitialValues.ts index 7b143ae3b..2e3009fd6 100644 --- a/app/react/docker/containers/CreateView/useInitialValues.ts +++ b/app/react/docker/containers/CreateView/useInitialValues.ts @@ -39,6 +39,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useCurrentUser } from '@/react/hooks/useUser'; import { useWebhooks } from '@/react/portainer/webhooks/useWebhooks'; import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries'; +import { ContainerDetailsViewModel } from '@/docker/models/containerDetails'; import { getImageConfig } from '@@/ImageConfigFieldset/getImageConfig'; import { EnvVarValues } from '@@/form-components/EnvironmentVariablesFieldset'; @@ -69,9 +70,13 @@ export function useInitialValues(submitting: boolean, isWindows: boolean) { const networksQuery = useNetworksForSelector(); - const fromContainerQuery = useContainer(environmentId, from, nodeName, { - enabled: !submitting, - }); + const fromContainerQuery = useContainer( + { environmentId, containerId: from, nodeName }, + { + enabled: !submitting, + select: (c) => new ContainerDetailsViewModel(c), + } + ); const runningContainersQuery = useContainers(environmentId, { enabled: !!from, diff --git a/app/react/docker/containers/ItemView/ContainerActionsSection/queries/useRecreateContainer.ts b/app/react/docker/containers/ItemView/ContainerActionsSection/queries/useRecreateContainer.ts index 1107057c7..ee695a9d7 100644 --- a/app/react/docker/containers/ItemView/ContainerActionsSection/queries/useRecreateContainer.ts +++ b/app/react/docker/containers/ItemView/ContainerActionsSection/queries/useRecreateContainer.ts @@ -26,7 +26,7 @@ export function useRecreateContainer() { }: RecreateContainerParams) => recreateContainer(environmentId, containerId, pullImage, { nodeName }), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ + queryClient.removeQueries({ queryKey: containerQueryKeys.container( variables.environmentId, variables.containerId diff --git a/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ConnectNetworkForm.tsx b/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ConnectNetworkForm.tsx index 7560da2a4..92f6017c6 100644 --- a/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ConnectNetworkForm.tsx +++ b/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ConnectNetworkForm.tsx @@ -1,10 +1,10 @@ import { Form, Formik } from 'formik'; import { SchemaOf, object, string } from 'yup'; -import { useRouter } from '@uirouter/react'; import { useAuthorizations } from '@/react/hooks/useUser'; import { useConnectContainerMutation } from '@/react/docker/networks/queries/useConnectContainerMutation'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; +import { notifySuccess } from '@/portainer/services/notifications'; import { FormControl } from '@@/form-components/FormControl'; import { LoadingButton } from '@@/buttons'; @@ -27,7 +27,6 @@ export function ConnectNetworkForm({ const environmentId = useEnvironmentId(); const { authorized } = useAuthorizations('DockerNetworkConnect'); const connectMutation = useConnectContainerMutation(environmentId); - const router = useRouter(); if (!authorized) { return null; } @@ -72,7 +71,7 @@ export function ConnectNetworkForm({ { containerId, networkId, nodeName }, { onSuccess() { - router.stateService.reload(); + notifySuccess('Success', `Connected container to ${networkId}`); }, } ); diff --git a/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ContainerNetworksDatatable.tsx b/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ContainerNetworksDatatable.tsx index d73d0c44f..f81352e93 100644 --- a/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ContainerNetworksDatatable.tsx +++ b/app/react/docker/containers/ItemView/ContainerNetworksDatatable/ContainerNetworksDatatable.tsx @@ -19,11 +19,11 @@ const store = createPersistedStore(storageKey, 'name'); export function ContainerNetworksDatatable({ dataset, - container, + containerId, nodeName, }: { dataset: NetworkSettings['Networks']; - container: ContainerListViewModel; + containerId: ContainerListViewModel['Id']; nodeName?: string; }) { const tableState = useTableState(store, storageKey); @@ -56,7 +56,7 @@ export function ContainerNetworksDatatable({ )} description={ n.id)} /> @@ -64,7 +64,7 @@ export function ContainerNetworksDatatable({ extendTableOptions={mergeOptions( withMeta({ table: 'container-networks', - containerId: container.Id, + containerId, }) )} data-cy="container-networks-datatable" diff --git a/app/react/docker/containers/ItemView/ContainerNetworksDatatable/actions.tsx b/app/react/docker/containers/ItemView/ContainerNetworksDatatable/actions.tsx index 795988b5e..a42a8328a 100644 --- a/app/react/docker/containers/ItemView/ContainerNetworksDatatable/actions.tsx +++ b/app/react/docker/containers/ItemView/ContainerNetworksDatatable/actions.tsx @@ -1,5 +1,4 @@ import { CellContext } from '@tanstack/react-table'; -import { useRouter } from '@uirouter/react'; import { Authorized } from '@/react/hooks/useUser'; import { useDisconnectContainer } from '@/react/docker/networks/queries/useDisconnectContainerMutation'; @@ -25,7 +24,6 @@ export function buildActions({ nodeName }: { nodeName?: string } = {}) { options: { meta }, }, }: CellContext) { - const router = useRouter(); const environmentId = useEnvironmentId(); const disconnectMutation = useDisconnectContainer({ environmentId, @@ -60,7 +58,6 @@ export function buildActions({ nodeName }: { nodeName?: string } = {}) { { onSuccess() { notifySuccess('Container successfully disconnected', networkId); - router.stateService.reload(); }, } ); diff --git a/app/react/docker/containers/ItemView/HealthStatus.tsx b/app/react/docker/containers/ItemView/HealthStatus.tsx index 347e1826d..1bcd96906 100644 --- a/app/react/docker/containers/ItemView/HealthStatus.tsx +++ b/app/react/docker/containers/ItemView/HealthStatus.tsx @@ -1,14 +1,13 @@ import { ComponentProps } from 'react'; import { HeartPulse, Server } from 'lucide-react'; +import { Health } from 'docker-types/generated/1.44'; import { TableContainer, TableTitle } from '@@/datatables'; import { DetailsTable } from '@@/DetailsTable'; import { Icon } from '@@/Icon'; -import { Health } from '../types/response'; - const StatusMode: Record< - Health['Status'], + Exclude, ComponentProps['mode'] > = { healthy: 'success', @@ -27,23 +26,29 @@ export function HealthStatus({ health }: Props) { -
- - {health.Status} -
+ {health.Status && health.Status !== 'none' ? ( +
+ + {health.Status} +
+ ) : ( +
No health status
+ )}
{health.FailingStreak}
- - {health.Log[health.Log.length - 1].Output} - + {!!health.Log && ( + + {health.Log[health.Log.length - 1].Output} + + )}
); diff --git a/app/react/docker/containers/ItemView/ItemView.test.tsx b/app/react/docker/containers/ItemView/ItemView.test.tsx new file mode 100644 index 000000000..02674f43d --- /dev/null +++ b/app/react/docker/containers/ItemView/ItemView.test.tsx @@ -0,0 +1,86 @@ +import { render, screen } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; + +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { createMockContainer, createMockUser } from '@/react-tools/test-mocks'; +import { server } from '@/setup-tests/server'; +import { User } from '@/portainer/users/types'; +import { ContainerDetailsViewModel } from '@/docker/models/containerDetails'; + +import { ItemView } from './ItemView'; + +vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ + ...(await importOriginal()), + useCurrentStateAndParams: vi.fn(() => ({ + params: { id: 'container-id-123', endpointId: '1', nodeName: undefined }, + })), +})); + +describe('ItemView', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders page header with container details title', async () => { + renderComponent(); + + expect( + await screen.findByRole('heading', { + name: 'Container details', + level: 1, + }) + ).toBeVisible(); + }); + + it('displays container name in breadcrumbs with leading slash trimmed', async () => { + renderComponent(); + + expect(await screen.findByText('test-container')).toBeVisible(); + expect(screen.queryByText('/test-container')).not.toBeInTheDocument(); + }); + + it('renders health status section when container has health data', async () => { + renderComponent({ + container: { + State: { + Health: { + Status: 'healthy', + FailingStreak: 0, + Log: [ + { + Start: '2024-01-01T00:00:00Z', + End: '2024-01-01T00:00:01Z', + ExitCode: 0, + Output: 'Health check passed', + }, + ], + }, + }, + }, + }); + + expect(await screen.findByText('Container health')).toBeVisible(); + }); +}); + +function renderComponent({ + user = createMockUser({ Role: 1 }), + container, +}: { + user?: User; + container?: Partial; +} = {}) { + server.use( + http.get('/api/endpoints/:endpointId/docker/containers/:id/json', () => + HttpResponse.json(createMockContainer(container)) + ) + ); + + const Wrapped = withTestQueryProvider( + withTestRouter(withUserProvider(ItemView, user)) + ); + + return render(); +} diff --git a/app/react/docker/containers/ItemView/ItemView.tsx b/app/react/docker/containers/ItemView/ItemView.tsx new file mode 100644 index 000000000..aca33286a --- /dev/null +++ b/app/react/docker/containers/ItemView/ItemView.tsx @@ -0,0 +1,129 @@ +import { useCurrentStateAndParams } from '@uirouter/react'; + +import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel'; +import { ContainerDetailsViewModel } from '@/docker/models/containerDetails'; +import { ResourceControlType } from '@/react/portainer/access-control/types'; +import { trimContainerName } from '@/docker/filters/utils'; +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; +import { useRegistries } from '@/react/portainer/registries/queries/useRegistries'; +import { Registry } from '@/react/portainer/registries/types/registry'; + +import { PageHeader } from '@@/PageHeader'; +import { findBestMatchRegistry } from '@@/ImageConfigFieldset/findRegistryMatch'; + +import { useContainer } from '../queries/useContainer'; + +import { ContainerActionsSection } from './ContainerActionsSection/ContainerActionsSection'; +import { ContainerStatusSection } from './ContainerStatusSection/ContainerStatusSection'; +import { CreateImageSection } from './CreateImageSection/CreateImageSection'; +import { ContainerDetailsSection } from './ContainerDetailsSection/ContainerDetailsSection'; +import { VolumesSection } from './VolumesSection/VolumesSection'; +import { ContainerNetworksDatatable } from './ContainerNetworksDatatable'; +import { HealthStatus } from './HealthStatus'; + +export function ItemView() { + const environmentId = useEnvironmentId(); + const { + params: { id: containerId, nodeName }, + } = useCurrentStateAndParams(); + + const containerQuery = useContainer( + { environmentId, containerId, nodeName }, + { select: (c) => new ContainerDetailsViewModel(c) } + ); + + const registriesQuery = useRegistries(); + + if ( + containerQuery.isLoading || + !containerQuery.data || + !registriesQuery.data + ) { + return null; + } + + const container = containerQuery.data; + const registryId = getRegistryId(container, registriesQuery.data); + + async function handleSuccess() { + containerQuery.refetch(); + } + + const containerName = + trimContainerName(container.Name) || container.Id || 'unknown'; + + return ( + <> + + +
+ + + +
+ + + + {container.State?.Health && ( + + )} + +
+ + + + + +
+ + {container.NetworkSettings?.Networks && ( + + )} + + ); +} + +function getRegistryId( + container: ContainerDetailsViewModel, + registries?: Array +) { + const imageName = container.Config?.Image; + if (!imageName || !registries) { + return undefined; + } + + const registry = findBestMatchRegistry(imageName, registries); + return registry?.Id; +} diff --git a/app/react/docker/containers/LogView/LogView.tsx b/app/react/docker/containers/LogView/LogView.tsx index 8991fb748..2509cd859 100644 --- a/app/react/docker/containers/LogView/LogView.tsx +++ b/app/react/docker/containers/LogView/LogView.tsx @@ -1,6 +1,7 @@ import { useCurrentStateAndParams } from '@uirouter/react'; import { useContainer } from '@/react/docker/containers/queries/useContainer'; +import { ContainerDetailsViewModel } from '@/docker/models/containerDetails'; import { InformationPanel } from '@@/InformationPanel'; import { TextTip } from '@@/Tip/TextTip'; @@ -11,7 +12,12 @@ export function LogView() { params: { endpointId: environmentId, id: containerId, nodeName }, } = useCurrentStateAndParams(); - const containerQuery = useContainer(environmentId, containerId, nodeName); + const containerQuery = useContainer( + { environmentId, containerId, nodeName }, + { + select: (c) => new ContainerDetailsViewModel(c), + } + ); if (!containerQuery.data || containerQuery.isLoading) { return null; } diff --git a/app/react/docker/containers/queries/useContainer.ts b/app/react/docker/containers/queries/useContainer.ts index a28ef821d..a9dbb3e2b 100644 --- a/app/react/docker/containers/queries/useContainer.ts +++ b/app/react/docker/containers/queries/useContainer.ts @@ -12,7 +12,7 @@ import { PortainerResponse } from '@/react/docker/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { ContainerId } from '@/react/docker/containers/types'; import { EnvironmentId } from '@/react/portainer/environments/types'; -import { queryClient } from '@/react-tools/react-query'; +import { queryClient, withGlobalError } from '@/react-tools/react-query'; import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl'; import { withAgentTargetHeader } from '../../proxy/queries/utils'; @@ -76,26 +76,28 @@ export interface ContainerDetailsJSON { NetworkSettings?: NetworkSettings; } -export function useContainer( - environmentId: EnvironmentId, - containerId?: ContainerId, - nodeName?: string, - { enabled }: { enabled?: boolean } = {} +export function useContainer( + { + environmentId, + containerId, + nodeName, + }: { + environmentId: EnvironmentId; + containerId?: ContainerId; + nodeName?: string; + }, + { + enabled, + select, + }: { enabled?: boolean; select?(container: ContainerDetailsResponse): T } = {} ) { - return useQuery( - containerId ? queryKeys.container(environmentId, containerId) : [], - () => - containerId - ? getContainer(environmentId, containerId, { nodeName }) - : undefined, - { - meta: { - title: 'Failure', - message: 'Unable to retrieve container', - }, - enabled: enabled && !!containerId, - } - ); + return useQuery({ + queryKey: queryKeys.container(environmentId, containerId!), + queryFn: () => getContainer(environmentId, containerId!, { nodeName }), + enabled: enabled && !!containerId, + select, + ...withGlobalError('Unable to retrieve container'), + }); } export function invalidateContainer( diff --git a/app/react/docker/containers/types/response.ts b/app/react/docker/containers/types/response.ts index 673e31689..e492853a7 100644 --- a/app/react/docker/containers/types/response.ts +++ b/app/react/docker/containers/types/response.ts @@ -7,12 +7,6 @@ export type SummaryNetworkSettings = NonNullable< ContainerSummary['NetworkSettings'] >; -export interface Health { - Status: 'healthy' | 'unhealthy' | 'starting'; - FailingStreak: number; - Log: Array<{ Output: string }>; -} - /** * Raw container list response item */ diff --git a/app/react/docker/networks/queries/useConnectContainerMutation.ts b/app/react/docker/networks/queries/useConnectContainerMutation.ts index 8b387f080..bd2329cc1 100644 --- a/app/react/docker/networks/queries/useConnectContainerMutation.ts +++ b/app/react/docker/networks/queries/useConnectContainerMutation.ts @@ -5,11 +5,11 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { mutationOptions, - withError, + withGlobalError, withInvalidate, } from '@/react-tools/react-query'; -import { queryKeys as dockerQueryKeys } from '../../queries/utils'; +import { queryKeys as containerQueryKeys } from '../../containers/queries/query-keys'; import { withAgentTargetHeader } from '../../proxy/queries/utils'; import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl'; @@ -25,8 +25,8 @@ export function useConnectContainerMutation(environmentId: EnvironmentId) { (params: Omit) => connectContainer({ ...params, environmentId }), mutationOptions( - withError('Failed connecting container to network'), - withInvalidate(queryClient, [dockerQueryKeys.containers(environmentId)]) + withGlobalError('Failed connecting container to network'), + withInvalidate(queryClient, [containerQueryKeys.list(environmentId)]) ) ); } diff --git a/app/react/docker/networks/queries/useDisconnectContainerMutation.ts b/app/react/docker/networks/queries/useDisconnectContainerMutation.ts index cf74e2c5d..b4f51c81d 100644 --- a/app/react/docker/networks/queries/useDisconnectContainerMutation.ts +++ b/app/react/docker/networks/queries/useDisconnectContainerMutation.ts @@ -4,11 +4,12 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios'; import { mutationOptions, - withError, + withGlobalError, withInvalidate, } from '@/react-tools/react-query'; import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl'; +import { queryKeys as containerQueryKeys } from '../../containers/queries/query-keys'; import { withAgentTargetHeader } from '../../proxy/queries/utils'; import { ContainerId } from '../../containers/types'; import { NetworkId } from '../types'; @@ -33,8 +34,11 @@ export function useDisconnectContainer({ nodeName?: string; }) => disconnectContainer(environmentId, networkId, containerId, nodeName), mutationOptions( - withInvalidate(client, [queryKeys.item(environmentId, networkId)]), - withError('Unable to disconnect container from network') + withInvalidate(client, [ + queryKeys.item(environmentId, networkId), + containerQueryKeys.list(environmentId), + ]), + withGlobalError('Unable to disconnect container from network') ) ); }