1
0
Fork 0
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:
Chaim Lev-Ari 2022-06-26 17:16:50 +03:00 committed by GitHub
parent 7238372d8d
commit 46e1a01625
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 61 additions and 76 deletions

View file

@ -3,11 +3,8 @@ import angular from 'angular';
import { EnvironmentStatus } from '@/portainer/environments/types';
import { reactModule } from './react';
import containersModule from './containers';
import { componentsModule } from './components';
import { networksModule } from './networks';
angular.module('portainer.docker', ['portainer.app', containersModule, componentsModule, networksModule, reactModule]).config([
angular.module('portainer.docker', ['portainer.app', reactModule]).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';

View file

@ -1,3 +0,0 @@
.root {
display: inline-flex;
}

View file

@ -1,141 +0,0 @@
import clsx from 'clsx';
import { Authorized } from '@/portainer/hooks/useUser';
import { react2angular } from '@/react-tools/react2angular';
import { DockerContainerStatus } from '@/docker/containers/types';
import { Link } from '@@/Link';
import styles from './ContainerQuickActions.module.css';
interface QuickActionsState {
showQuickActionAttach: boolean;
showQuickActionExec: boolean;
showQuickActionInspect: boolean;
showQuickActionLogs: boolean;
showQuickActionStats: boolean;
}
interface Props {
taskId?: string;
containerId?: string;
nodeName: string;
state: QuickActionsState;
status: DockerContainerStatus;
}
export function ContainerQuickActions({
taskId,
containerId,
nodeName,
state,
status,
}: Props) {
if (taskId) {
return <TaskQuickActions taskId={taskId} state={state} />;
}
const isActive = ['starting', 'running', 'healthy', 'unhealthy'].includes(
status
);
return (
<div className={clsx('space-x-1', styles.root)}>
{state.showQuickActionLogs && (
<Authorized authorizations="DockerContainerLogs">
<Link
to="docker.containers.container.logs"
params={{ id: containerId, nodeName }}
title="Logs"
>
<i className="fa fa-file-alt space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
{state.showQuickActionInspect && (
<Authorized authorizations="DockerContainerInspect">
<Link
to="docker.containers.container.inspect"
params={{ id: containerId, nodeName }}
title="Inspect"
>
<i className="fa fa-info-circle space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
{state.showQuickActionStats && isActive && (
<Authorized authorizations="DockerContainerStats">
<Link
to="docker.containers.container.stats"
params={{ id: containerId, nodeName }}
title="Stats"
>
<i className="fa fa-chart-area space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
{state.showQuickActionExec && isActive && (
<Authorized authorizations="DockerExecStart">
<Link
to="docker.containers.container.exec"
params={{ id: containerId, nodeName }}
title="Exec Console"
>
<i className="fa fa-terminal space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
{state.showQuickActionAttach && isActive && (
<Authorized authorizations="DockerContainerAttach">
<Link
to="docker.containers.container.attach"
params={{ id: containerId, nodeName }}
title="Attach Console"
>
<i className="fa fa-plug space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
</div>
);
}
interface TaskProps {
taskId: string;
state: QuickActionsState;
}
function TaskQuickActions({ taskId, state }: TaskProps) {
return (
<div className={clsx('space-x-1', styles.root)}>
{state.showQuickActionLogs && (
<Authorized authorizations="DockerTaskLogs">
<Link
to="docker.tasks.task.logs"
params={{ id: taskId }}
title="Logs"
>
<i className="fa fa-file-alt space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
{state.showQuickActionInspect && (
<Authorized authorizations="DockerTaskInspect">
<Link to="docker.tasks.task" params={{ id: taskId }} title="Inspect">
<i className="fa fa-info-circle space-right" aria-hidden="true" />
</Link>
</Authorized>
)}
</div>
);
}
export const ContainerQuickActionsAngular = react2angular(
ContainerQuickActions,
['taskId', 'containerId', 'nodeName', 'state', 'status']
);

View file

@ -1,4 +0,0 @@
export {
ContainerQuickActions,
ContainerQuickActionsAngular,
} from './ContainerQuickActions';

View file

@ -1,7 +0,0 @@
import angular from 'angular';
import { ContainerQuickActionsAngular } from './container-quick-actions';
export const componentsModule = angular
.module('portainer.docker.components', [])
.component('containerQuickActions', ContainerQuickActionsAngular).name;

View file

@ -1,252 +0,0 @@
import { useEffect } from 'react';
import {
useTable,
useSortBy,
useFilters,
useGlobalFilter,
usePagination,
Row,
} from 'react-table';
import { useRowSelectColumn } from '@lineup-lite/hooks';
import { useDebounce } from '@/portainer/hooks/useDebounce';
import type {
ContainersTableSettings,
DockerContainer,
} from '@/docker/containers/types';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { PaginationControls } from '@@/PaginationControls';
import {
QuickActionsSettings,
buildAction,
} from '@@/datatables/QuickActionsSettings';
import {
Table,
TableActions,
TableContainer,
TableHeaderRow,
TableRow,
TableSettingsMenu,
TableTitle,
TableTitleActions,
} from '@@/datatables';
import { multiple } from '@@/datatables/filter-types';
import { useTableSettings } from '@@/datatables/useTableSettings';
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
import { useRepeater } from '@@/datatables/useRepeater';
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
import { useRowSelect } from '@@/datatables/useRowSelect';
import { Checkbox } from '@@/form-components/Checkbox';
import { TableFooter } from '@@/datatables/TableFooter';
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
import { ContainersDatatableActions } from './ContainersDatatableActions';
import { ContainersDatatableSettings } from './ContainersDatatableSettings';
import { useColumns } from './columns';
export interface ContainerTableProps {
isAddActionVisible: boolean;
dataset: DockerContainer[];
onRefresh?(): Promise<void>;
isHostColumnVisible: boolean;
tableKey?: string;
}
export function ContainersDatatable({
isAddActionVisible,
dataset,
onRefresh,
isHostColumnVisible,
}: ContainerTableProps) {
const { settings, setTableSettings } =
useTableSettings<ContainersTableSettings>();
const [searchBarValue, setSearchBarValue] = useSearchBarState('containers');
const columns = useColumns();
const endpoint = useEnvironment();
useRepeater(settings.autoRefreshRate, onRefresh);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
selectedFlatRows,
allColumns,
gotoPage,
setPageSize,
setHiddenColumns,
toggleHideColumn,
setGlobalFilter,
state: { pageIndex, pageSize },
} = useTable<DockerContainer>(
{
defaultCanFilter: false,
columns,
data: dataset,
filterTypes: { multiple },
initialState: {
pageSize: settings.pageSize || 10,
hiddenColumns: settings.hiddenColumns,
sortBy: [settings.sortBy],
globalFilter: searchBarValue,
},
isRowSelectable(row: Row<DockerContainer>) {
return !row.original.IsPortainer;
},
autoResetSelectedRows: false,
getRowId(originalRow: DockerContainer) {
return originalRow.Id;
},
selectCheckboxComponent: Checkbox,
},
useFilters,
useGlobalFilter,
useSortBy,
usePagination,
useRowSelect,
useRowSelectColumn
);
const debouncedSearchValue = useDebounce(searchBarValue);
useEffect(() => {
setGlobalFilter(debouncedSearchValue);
}, [debouncedSearchValue, setGlobalFilter]);
useEffect(() => {
toggleHideColumn('host', !isHostColumnVisible);
}, [toggleHideColumn, isHostColumnVisible]);
const columnsToHide = allColumns.filter((colInstance) => {
const columnDef = columns.find((c) => c.id === colInstance.id);
return columnDef?.canHide;
});
const actions = [
buildAction('logs', 'Logs'),
buildAction('inspect', 'Inspect'),
buildAction('stats', 'Stats'),
buildAction('exec', 'Console'),
buildAction('attach', 'Attach'),
];
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
return (
<TableContainer>
<TableTitle icon="fa-cubes" label="Containers">
<SearchBar value={searchBarValue} onChange={handleSearchBarChange} />
<TableActions>
<ContainersDatatableActions
selectedItems={selectedFlatRows.map((row) => row.original)}
isAddActionVisible={isAddActionVisible}
endpointId={endpoint.Id}
/>
</TableActions>
<TableTitleActions>
<ColumnVisibilityMenu<DockerContainer>
columns={columnsToHide}
onChange={handleChangeColumnsVisibility}
value={settings.hiddenColumns}
/>
<TableSettingsMenu
quickActions={<QuickActionsSettings actions={actions} />}
>
<ContainersDatatableSettings isRefreshVisible={!!onRefresh} />
</TableSettingsMenu>
</TableTitleActions>
</TableTitle>
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<TableHeaderRow<DockerContainer>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={handleSortChange}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
{page.length > 0 ? (
page.map((row) => {
prepareRow(row);
const { key, className, role, style } = row.getRowProps();
return (
<TableRow<DockerContainer>
cells={row.cells}
key={key}
className={className}
role={role}
style={style}
/>
);
})
) : (
<tr>
<td colSpan={columns.length} className="text-center text-muted">
No container available.
</td>
</tr>
)}
</tbody>
</Table>
<TableFooter>
<SelectedRowsCount value={selectedFlatRows.length} />
<PaginationControls
showAll
pageLimit={pageSize}
page={pageIndex + 1}
onPageChange={(p) => gotoPage(p - 1)}
totalCount={dataset.length}
onPageLimitChange={handlePageSizeChange}
/>
</TableFooter>
</TableContainer>
);
function handlePageSizeChange(pageSize: number) {
setPageSize(pageSize);
setTableSettings((settings) => ({ ...settings, pageSize }));
}
function handleChangeColumnsVisibility(hiddenColumns: string[]) {
setHiddenColumns(hiddenColumns);
setTableSettings((settings) => ({ ...settings, hiddenColumns }));
}
function handleSearchBarChange(value: string) {
setSearchBarValue(value);
}
function handleSortChange(id: string, desc: boolean) {
setTableSettings((settings) => ({
...settings,
sortBy: { id, desc },
}));
}
}

View file

@ -1,292 +0,0 @@
import { useRouter } from '@uirouter/react';
import * as notifications from '@/portainer/services/notifications';
import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser';
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
import { setPortainerAgentTargetHeader } from '@/portainer/services/http-request.helper';
import type { ContainerId, DockerContainer } from '@/docker/containers/types';
import {
killContainer,
pauseContainer,
removeContainer,
restartContainer,
resumeContainer,
startContainer,
stopContainer,
} from '@/docker/containers/containers.service';
import type { EnvironmentId } from '@/portainer/environments/types';
import { Link } from '@@/Link';
import { ButtonGroup, Button } from '@@/buttons';
type ContainerServiceAction = (
endpointId: EnvironmentId,
containerId: ContainerId
) => Promise<void>;
interface Props {
selectedItems: DockerContainer[];
isAddActionVisible: boolean;
endpointId: EnvironmentId;
}
export function ContainersDatatableActions({
selectedItems,
isAddActionVisible,
endpointId,
}: Props) {
const selectedItemCount = selectedItems.length;
const hasPausedItemsSelected = selectedItems.some(
(item) => item.Status === 'paused'
);
const hasStoppedItemsSelected = selectedItems.some((item) =>
['stopped', 'created'].includes(item.Status)
);
const hasRunningItemsSelected = selectedItems.some((item) =>
['running', 'healthy', 'unhealthy', 'starting'].includes(item.Status)
);
const isAuthorized = useAuthorizations([
'DockerContainerStart',
'DockerContainerStop',
'DockerContainerKill',
'DockerContainerRestart',
'DockerContainerPause',
'DockerContainerUnpause',
'DockerContainerDelete',
'DockerContainerCreate',
]);
const router = useRouter();
if (!isAuthorized) {
return null;
}
return (
<div className="actionBar">
<ButtonGroup>
<Authorized authorizations="DockerContainerStart">
<Button
color="light"
onClick={() => onStartClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasStoppedItemsSelected}
>
<i className="fa fa-play space-right" aria-hidden="true" />
Start
</Button>
</Authorized>
<Authorized authorizations="DockerContainerStop">
<Button
color="light"
onClick={() => onStopClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasRunningItemsSelected}
>
<i className="fa fa-stop space-right" aria-hidden="true" />
Stop
</Button>
</Authorized>
<Authorized authorizations="DockerContainerKill">
<Button
color="light"
onClick={() => onKillClick(selectedItems)}
disabled={selectedItemCount === 0}
>
<i className="fa fa-bomb space-right" aria-hidden="true" />
Kill
</Button>
</Authorized>
<Authorized authorizations="DockerContainerRestart">
<Button
color="light"
onClick={() => onRestartClick(selectedItems)}
disabled={selectedItemCount === 0}
>
<i className="fa fa-sync space-right" aria-hidden="true" />
Restart
</Button>
</Authorized>
<Authorized authorizations="DockerContainerPause">
<Button
color="light"
onClick={() => onPauseClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasRunningItemsSelected}
>
<i className="fa fa-pause space-right" aria-hidden="true" />
Pause
</Button>
</Authorized>
<Authorized authorizations="DockerContainerUnpause">
<Button
color="light"
onClick={() => onResumeClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasPausedItemsSelected}
>
<i className="fa fa-play space-right" aria-hidden="true" />
Resume
</Button>
</Authorized>
<Authorized authorizations="DockerContainerDelete">
<Button
color="dangerlight"
onClick={() => onRemoveClick(selectedItems)}
disabled={selectedItemCount === 0}
>
<i className="fa fa-trash-alt space-right" aria-hidden="true" />
Remove
</Button>
</Authorized>
</ButtonGroup>
{isAddActionVisible && (
<Authorized authorizations="DockerContainerCreate">
<Link to="docker.containers.new" className="space-left">
<Button>
<i className="fa fa-plus space-right" aria-hidden="true" />
Add container
</Button>
</Link>
</Authorized>
)}
</div>
);
function onStartClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully started';
const errorMessage = 'Unable to start container';
executeActionOnContainerList(
selectedItems,
startContainer,
successMessage,
errorMessage
);
}
function onStopClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully stopped';
const errorMessage = 'Unable to stop container';
executeActionOnContainerList(
selectedItems,
stopContainer,
successMessage,
errorMessage
);
}
function onRestartClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully restarted';
const errorMessage = 'Unable to restart container';
executeActionOnContainerList(
selectedItems,
restartContainer,
successMessage,
errorMessage
);
}
function onKillClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully killed';
const errorMessage = 'Unable to kill container';
executeActionOnContainerList(
selectedItems,
killContainer,
successMessage,
errorMessage
);
}
function onPauseClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully paused';
const errorMessage = 'Unable to pause container';
executeActionOnContainerList(
selectedItems,
pauseContainer,
successMessage,
errorMessage
);
}
function onResumeClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully resumed';
const errorMessage = 'Unable to resume container';
executeActionOnContainerList(
selectedItems,
resumeContainer,
successMessage,
errorMessage
);
}
function onRemoveClick(selectedItems: DockerContainer[]) {
const isOneContainerRunning = selectedItems.some(
(container) => container.Status === 'running'
);
const runningTitle = isOneContainerRunning ? 'running' : '';
const title = `You are about to remove one or more ${runningTitle} containers.`;
confirmContainerDeletion(title, (result: string[]) => {
if (!result) {
return;
}
const cleanVolumes = !!result[0];
removeSelectedContainers(selectedItems, cleanVolumes);
});
}
async function executeActionOnContainerList(
containers: DockerContainer[],
action: ContainerServiceAction,
successMessage: string,
errorMessage: string
) {
for (let i = 0; i < containers.length; i += 1) {
const container = containers[i];
try {
setPortainerAgentTargetHeader(container.NodeName);
await action(endpointId, container.Id);
notifications.success(successMessage, container.Names[0]);
} catch (err) {
notifications.error(
'Failure',
err as Error,
`${errorMessage}:${container.Names[0]}`
);
}
}
router.stateService.reload();
}
async function removeSelectedContainers(
containers: DockerContainer[],
cleanVolumes: boolean
) {
for (let i = 0; i < containers.length; i += 1) {
const container = containers[i];
try {
setPortainerAgentTargetHeader(container.NodeName);
await removeContainer(endpointId, container, cleanVolumes);
notifications.success(
'Container successfully removed',
container.Names[0]
);
} catch (err) {
notifications.error(
'Failure',
err as Error,
'Unable to remove container'
);
}
}
router.stateService.reload();
}
}

View file

@ -1,50 +0,0 @@
import { react2angular } from '@/react-tools/react2angular';
import { EnvironmentProvider } from '@/portainer/environments/useEnvironment';
import type { Environment } from '@/portainer/environments/types';
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import {
ContainersDatatable,
ContainerTableProps,
} from './ContainersDatatable';
interface Props extends ContainerTableProps {
endpoint: Environment;
}
export function ContainersDatatableContainer({
endpoint,
tableKey = 'containers',
...props
}: Props) {
const defaultSettings = {
autoRefreshRate: 0,
truncateContainerName: 32,
hiddenQuickActions: [],
hiddenColumns: [],
pageSize: 10,
sortBy: { id: 'state', desc: false },
};
return (
<EnvironmentProvider environment={endpoint}>
<TableSettingsProvider defaults={defaultSettings} storageKey={tableKey}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<ContainersDatatable {...props} />
</TableSettingsProvider>
</EnvironmentProvider>
);
}
export const ContainersDatatableAngular = react2angular(
ContainersDatatableContainer,
[
'endpoint',
'isAddActionVisible',
'dataset',
'onRefresh',
'isHostColumnVisible',
'tableKey',
]
);

View file

@ -1,41 +0,0 @@
import type { ContainersTableSettings } from '@/docker/containers/types';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { useTableSettings } from '@@/datatables/useTableSettings';
import { Checkbox } from '@@/form-components/Checkbox';
interface Props {
isRefreshVisible: boolean;
}
export function ContainersDatatableSettings({ isRefreshVisible }: Props) {
const { settings, setTableSettings } =
useTableSettings<ContainersTableSettings>();
return (
<>
<Checkbox
id="settings-container-truncate-nae"
label="Truncate container name"
checked={settings.truncateContainerName > 0}
onChange={() =>
setTableSettings((settings) => ({
...settings,
truncateContainerName: settings.truncateContainerName > 0 ? 0 : 32,
}))
}
/>
{isRefreshVisible && (
<TableSettingsMenuAutoRefresh
value={settings.autoRefreshRate}
onChange={handleRefreshRateChange}
/>
)}
</>
);
function handleRefreshRateChange(autoRefreshRate: number) {
setTableSettings({ autoRefreshRate });
}
}

View file

@ -1,14 +0,0 @@
import { Column } from 'react-table';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import type { DockerContainer } from '@/docker/containers/types';
export const created: Column<DockerContainer> = {
Header: 'Created',
accessor: 'Created',
id: 'created',
Cell: ({ value }) => isoDateFromTimestamp(value),
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -1,13 +0,0 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/docker/containers/types';
export const host: Column<DockerContainer> = {
Header: 'Host',
accessor: (row) => row.NodeName || '-',
id: 'host',
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};

View file

@ -1,51 +0,0 @@
import { Column } from 'react-table';
import { useSref } from '@uirouter/react';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { EnvironmentStatus } from '@/portainer/environments/types';
import type { DockerContainer } from '@/docker/containers/types';
export const image: Column<DockerContainer> = {
Header: 'Image',
accessor: 'Image',
id: 'image',
disableFilters: true,
Cell: ImageCell,
canHide: true,
sortType: 'string',
Filter: () => null,
};
interface Props {
value: string;
}
function ImageCell({ value: imageName }: Props) {
const endpoint = useEnvironment();
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
const shortImageName = trimSHASum(imageName);
const linkProps = useSref('docker.images.image', { id: imageName });
if (offlineMode) {
return shortImageName;
}
return (
<a href={linkProps.href} onClick={linkProps.onClick}>
{shortImageName}
</a>
);
function trimSHASum(imageName: string) {
if (!imageName) {
return '';
}
if (imageName.indexOf('sha256:') === 0) {
return imageName.substring(7, 19);
}
return imageName.split('@sha256')[0];
}
}

View file

@ -1,30 +0,0 @@
import { useMemo } from 'react';
import { created } from './created';
import { host } from './host';
import { image } from './image';
import { ip } from './ip';
import { name } from './name';
import { ownership } from './ownership';
import { ports } from './ports';
import { quickActions } from './quick-actions';
import { stack } from './stack';
import { state } from './state';
export function useColumns() {
return useMemo(
() => [
name,
state,
quickActions,
stack,
image,
created,
ip,
host,
ports,
ownership,
],
[]
);
}

View file

@ -1,12 +0,0 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/docker/containers/types';
export const ip: Column<DockerContainer> = {
Header: 'IP Address',
accessor: (row) => row.IP || '-',
id: 'ip',
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -1,55 +0,0 @@
import { CellProps, Column, TableInstance } from 'react-table';
import _ from 'lodash';
import { useSref } from '@uirouter/react';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import type {
ContainersTableSettings,
DockerContainer,
} from '@/docker/containers/types';
import { useTableSettings } from '@@/datatables/useTableSettings';
export const name: Column<DockerContainer> = {
Header: 'Name',
accessor: (row) => {
const name = row.Names[0];
return name.substring(1, name.length);
},
id: 'name',
Cell: NameCell,
disableFilters: true,
Filter: () => null,
canHide: true,
sortType: 'string',
};
export function NameCell({
value: name,
row: { original: container },
}: CellProps<TableInstance>) {
const { settings } = useTableSettings<ContainersTableSettings>();
const truncate = settings.truncateContainerName;
const endpoint = useEnvironment();
const offlineMode = endpoint.Status !== 1;
const linkProps = useSref('docker.containers.container', {
id: container.Id,
nodeName: container.NodeName,
});
let shortName = name;
if (truncate > 0) {
shortName = _.truncate(name, { length: truncate });
}
if (offlineMode) {
return <span>{shortName}</span>;
}
return (
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
{shortName}
</a>
);
}

View file

@ -1,34 +0,0 @@
import { Column } from 'react-table';
import clsx from 'clsx';
import { ownershipIcon } from '@/portainer/filters/filters';
import type { DockerContainer } from '@/docker/containers/types';
import { ResourceControlOwnership } from '@/portainer/access-control/types';
export const ownership: Column<DockerContainer> = {
Header: 'Ownership',
id: 'ownership',
accessor: (row) =>
row.ResourceControl?.Ownership || ResourceControlOwnership.ADMINISTRATORS,
Cell: OwnershipCell,
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
interface Props {
value: 'public' | 'private' | 'restricted' | 'administrators';
}
function OwnershipCell({ value }: Props) {
return (
<>
<i
className={clsx(ownershipIcon(value), 'space-right')}
aria-hidden="true"
/>
{value || ResourceControlOwnership.ADMINISTRATORS}
</>
);
}

View file

@ -1,41 +0,0 @@
import { Column } from 'react-table';
import _ from 'lodash';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import type { DockerContainer, Port } from '@/docker/containers/types';
export const ports: Column<DockerContainer> = {
Header: 'Published Ports',
accessor: 'Ports',
id: 'ports',
Cell: PortsCell,
disableSortBy: true,
disableFilters: true,
canHide: true,
Filter: () => null,
};
interface Props {
value: Port[];
}
function PortsCell({ value: ports }: Props) {
const { PublicURL: publicUrl } = useEnvironment();
if (ports.length === 0) {
return '-';
}
return _.uniqBy(ports, 'public').map((port) => (
<a
key={`${port.host}:${port.public}`}
className="image-tag"
href={`http://${publicUrl || port.host}:${port.public}`}
target="_blank"
rel="noreferrer"
>
<i className="fa fa-external-link-alt" aria-hidden="true" />
{port.public}:{port.private}
</a>
));
}

View file

@ -1,71 +0,0 @@
import { CellProps, Column } from 'react-table';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { useAuthorizations } from '@/portainer/hooks/useUser';
import { ContainerQuickActions } from '@/docker/components/container-quick-actions/ContainerQuickActions';
import type {
ContainersTableSettings,
DockerContainer,
} from '@/docker/containers/types';
import { EnvironmentStatus } from '@/portainer/environments/types';
import { useTableSettings } from '@@/datatables/useTableSettings';
export const quickActions: Column<DockerContainer> = {
Header: 'Quick Actions',
id: 'actions',
Cell: QuickActionsCell,
disableFilters: true,
disableSortBy: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
function QuickActionsCell({
row: { original: container },
}: CellProps<DockerContainer>) {
const endpoint = useEnvironment();
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
const { settings } = useTableSettings<ContainersTableSettings>();
const { hiddenQuickActions = [] } = settings;
const wrapperState = {
showQuickActionAttach: !hiddenQuickActions.includes('attach'),
showQuickActionExec: !hiddenQuickActions.includes('exec'),
showQuickActionInspect: !hiddenQuickActions.includes('inspect'),
showQuickActionLogs: !hiddenQuickActions.includes('logs'),
showQuickActionStats: !hiddenQuickActions.includes('stats'),
};
const someOn =
wrapperState.showQuickActionAttach ||
wrapperState.showQuickActionExec ||
wrapperState.showQuickActionInspect ||
wrapperState.showQuickActionLogs ||
wrapperState.showQuickActionStats;
const isAuthorized = useAuthorizations([
'DockerContainerStats',
'DockerContainerLogs',
'DockerExecStart',
'DockerContainerInspect',
'DockerTaskInspect',
'DockerTaskLogs',
]);
if (offlineMode || !someOn || !isAuthorized) {
return null;
}
return (
<ContainerQuickActions
containerId={container.Id}
nodeName={container.NodeName}
status={container.Status}
state={wrapperState}
/>
);
}

View file

@ -1,13 +0,0 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/docker/containers/types';
export const stack: Column<DockerContainer> = {
Header: 'Stack',
accessor: (row) => row.StackName || '-',
id: 'stack',
sortType: 'string',
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -1,61 +0,0 @@
import { Column } from 'react-table';
import clsx from 'clsx';
import _ from 'lodash';
import type {
DockerContainer,
DockerContainerStatus,
} from '@/docker/containers/types';
import { DefaultFilter } from '@@/datatables/Filter';
export const state: Column<DockerContainer> = {
Header: 'State',
accessor: 'Status',
id: 'state',
Cell: StatusCell,
sortType: 'string',
filter: 'multiple',
Filter: DefaultFilter,
canHide: true,
};
function StatusCell({ value: status }: { value: DockerContainerStatus }) {
const statusNormalized = _.toLower(status);
const hasHealthCheck = ['starting', 'healthy', 'unhealthy'].includes(
statusNormalized
);
const statusClassName = getClassName();
return (
<span
className={clsx('label', `label-${statusClassName}`, {
interactive: hasHealthCheck,
})}
title={hasHealthCheck ? 'This container has a health check' : ''}
>
{status}
</span>
);
function getClassName() {
if (includeString(['paused', 'starting', 'unhealthy'])) {
return 'warning';
}
if (includeString(['created'])) {
return 'info';
}
if (includeString(['stopped', 'dead', 'exited'])) {
return 'danger';
}
return 'success';
function includeString(values: DockerContainerStatus[]) {
return values.some((val) => statusNormalized.includes(val));
}
}
}

View file

@ -1,129 +0,0 @@
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
) {
await axios.post<void>(
urlBuilder(endpointId, id, 'start'),
{},
{ transformResponse: genericHandler }
);
}
export async function stopContainer(
endpointId: EnvironmentId,
id: ContainerId
) {
await axios.post<void>(urlBuilder(endpointId, id, 'stop'), {});
}
export async function restartContainer(
endpointId: EnvironmentId,
id: ContainerId
) {
await axios.post<void>(urlBuilder(endpointId, id, 'restart'), {});
}
export async function killContainer(
endpointId: EnvironmentId,
id: ContainerId
) {
await axios.post<void>(urlBuilder(endpointId, id, 'kill'), {});
}
export async function pauseContainer(
endpointId: EnvironmentId,
id: ContainerId
) {
await axios.post<void>(urlBuilder(endpointId, id, 'pause'), {});
}
export async function resumeContainer(
endpointId: EnvironmentId,
id: ContainerId
) {
await axios.post<void>(urlBuilder(endpointId, id, 'unpause'), {});
}
export async function renameContainer(
endpointId: EnvironmentId,
id: ContainerId,
name: string
) {
await axios.post<void>(
urlBuilder(endpointId, id, 'rename'),
{},
{ params: { name }, transformResponse: genericHandler }
);
}
export async function removeContainer(
endpointId: EnvironmentId,
container: DockerContainer,
removeVolumes: boolean
) {
try {
const { data } = await axios.delete<null | { message: string }>(
urlBuilder(endpointId, container.Id),
{
params: { v: removeVolumes ? 1 : 0, force: true },
transformResponse: genericHandler,
}
);
if (data && data.message) {
throw new PortainerError(data.message);
}
} catch (e) {
throw new PortainerError('Unable to remove container', e as Error);
}
}
export async function getContainers(
environmentId: EnvironmentId,
filters?: Filters
) {
try {
const { data } = await axios.get<DockerContainer[]>(
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,
action?: string
) {
let url = `/endpoints/${endpointId}/docker/containers`;
if (id) {
url += `/${id}`;
}
if (action) {
url += `/${action}`;
}
return url;
}

View file

@ -1,7 +0,0 @@
import angular from 'angular';
import { ContainersDatatableAngular } from './components/ContainersDatatable/ContainersDatatableContainer';
export default angular
.module('portainer.docker.containers', [])
.component('containersDatatable', ContainersDatatableAngular).name;

View file

@ -1,18 +0,0 @@
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',
},
}
);
}

View file

@ -1,53 +0,0 @@
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
import {
PaginationTableSettings,
RefreshableTableSettings,
SettableColumnsTableSettings,
SettableQuickActionsTableSettings,
SortableTableSettings,
} from '@@/datatables/types';
export type DockerContainerStatus =
| 'paused'
| 'stopped'
| 'created'
| 'healthy'
| 'unhealthy'
| 'starting'
| 'running'
| 'dead'
| 'exited';
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
export interface ContainersTableSettings
extends SortableTableSettings,
PaginationTableSettings,
SettableColumnsTableSettings,
SettableQuickActionsTableSettings<QuickAction>,
RefreshableTableSettings {
truncateContainerName: number;
}
export interface Port {
host: string;
public: string;
private: string;
}
export type ContainerId = string;
export type DockerContainer = {
IsPortainer: boolean;
Status: DockerContainerStatus;
NodeName: string;
Id: ContainerId;
IP: string;
Names: string[];
Created: string;
ResourceControl: ResourceControlViewModel;
Ports: Port[];
StackName?: string;
Image: string;
};

View file

@ -1,52 +0,0 @@
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();
});

View file

@ -1,98 +0,0 @@
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>
);
}

View file

@ -1,122 +0,0 @@
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',
};
}

View file

@ -1,120 +0,0 @@
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(' - ')}`
: '';
}
}

View file

@ -1,138 +0,0 @@
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 '@/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 { useContainers } from '../../containers/queries';
import { DockerNetwork, NetworkContainer } from '../types';
import { NetworkDetailsTable } from './NetworkDetailsTable';
import { NetworkOptionsTable } from './NetworkOptionsTable';
import { NetworkContainersTable } from './NetworkContainersTable';
export function NetworkDetailsView() {
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;
}
}

View file

@ -1,34 +0,0 @@
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();
});

View file

@ -1,35 +0,0 @@
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>
);
}

View file

@ -1,5 +0,0 @@
import { react2angular } from '@/react-tools/react2angular';
import { NetworkDetailsView } from './NetworkDetailsView';
export const NetworkDetailsViewAngular = react2angular(NetworkDetailsView, []);

View file

@ -1,7 +0,0 @@
import angular from 'angular';
import { NetworkDetailsViewAngular } from './edit';
export const networksModule = angular
.module('portainer.docker.networks', [])
.component('networkDetailsView', NetworkDetailsViewAngular).name;

View file

@ -1,5 +0,0 @@
const systemNetworks = ['host', 'bridge', 'ingress', 'nat', 'none'];
export function isSystemNetwork(networkName: string) {
return systemNetworks.includes(networkName);
}

View file

@ -1,71 +0,0 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/portainer/environments/types';
import { ContainerId } from '../containers/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;
}

View file

@ -1,83 +0,0 @@
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 '../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'
);
},
}
);
}

View file

@ -1,50 +0,0 @@
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;
}

View file

@ -1,6 +1,29 @@
import angular from 'angular';
export const componentsModule = angular.module(
'portainer.docker.react.components',
[]
).name;
import { r2a } from '@/react-tools/react2angular';
import { ContainersDatatableContainer } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableContainer';
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
export const componentsModule = angular
.module('portainer.docker.react.components', [])
.component(
'containersDatatable',
r2a(ContainersDatatableContainer, [
'endpoint',
'isAddActionVisible',
'dataset',
'onRefresh',
'isHostColumnVisible',
'tableKey',
])
)
.component(
'containerQuickActions',
r2a(ContainerQuickActions, [
'containerId',
'nodeName',
'state',
'status',
'taskId',
])
).name;

View file

@ -1,6 +1,8 @@
import angular from 'angular';
export const viewsModule = angular.module(
'portainer.docker.react.views',
[]
).name;
import { ItemView } from '@/react/docker/networks/ItemView';
import { r2a } from '@/react-tools/react2angular';
export const viewsModule = angular
.module('portainer.docker.react.views', [])
.component('networkDetailsView', r2a(ItemView, [])).name;

View file

@ -8,7 +8,7 @@ import {
resumeContainer,
startContainer,
stopContainer,
} from '@/docker/containers/containers.service';
} from '@/react/docker/containers/containers.service';
import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container';
angular.module('portainer.docker').factory('ContainerService', ContainerServiceFactory);