Compare commits
4 Commits
2.27.0-rc1
...
feat/EE-43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d06439d51 | ||
|
|
8143fab676 | ||
|
|
24341cd1ac | ||
|
|
75fff1b88a |
@@ -1,4 +1,4 @@
|
||||
angular.module('portainer.docker').component('networksDatatable', {
|
||||
angular.module('portainer.docker').component('ngNetworksDatatable', {
|
||||
templateUrl: './networksDatatable.html',
|
||||
controller: 'NetworksDatatableController',
|
||||
bindings: {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
|
||||
export function NetworkViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
this.Scope = data.Scope;
|
||||
this.Driver = data.Driver;
|
||||
this.Attachable = data.Attachable;
|
||||
this.Internal = data.Internal;
|
||||
this.IPAM = data.IPAM;
|
||||
this.Containers = data.Containers;
|
||||
this.Options = data.Options;
|
||||
this.Ingress = data.Ingress;
|
||||
|
||||
this.Labels = data.Labels;
|
||||
if (this.Labels && this.Labels['com.docker.compose.project']) {
|
||||
this.StackName = this.Labels['com.docker.compose.project'];
|
||||
} else if (this.Labels && this.Labels['com.docker.stack.namespace']) {
|
||||
this.StackName = this.Labels['com.docker.stack.namespace'];
|
||||
}
|
||||
|
||||
if (data.Portainer) {
|
||||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
}
|
||||
|
||||
this.ConfigFrom = data.ConfigFrom;
|
||||
this.ConfigOnly = data.ConfigOnly;
|
||||
}
|
||||
79
app/docker/models/network.ts
Normal file
79
app/docker/models/network.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { IPAM, Network, NetworkContainer } from 'docker-types/generated/1.41';
|
||||
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
|
||||
import { PortainerMetadata } from '@/react/docker/types';
|
||||
|
||||
export class NetworkViewModel implements IResource {
|
||||
Id: string;
|
||||
|
||||
Name: string;
|
||||
|
||||
Scope: string;
|
||||
|
||||
Driver: string;
|
||||
|
||||
Attachable: boolean;
|
||||
|
||||
Internal: boolean;
|
||||
|
||||
IPAM?: IPAM;
|
||||
|
||||
Containers?: Record<string, NetworkContainer>;
|
||||
|
||||
Options?: Record<string, string>;
|
||||
|
||||
Ingress: boolean;
|
||||
|
||||
Labels: Record<string, string>;
|
||||
|
||||
StackName?: string;
|
||||
|
||||
NodeName?: string;
|
||||
|
||||
ConfigFrom?: { Network: string };
|
||||
|
||||
ConfigOnly?: boolean;
|
||||
|
||||
ResourceControl?: ResourceControlViewModel;
|
||||
|
||||
constructor(
|
||||
data: Network & {
|
||||
Portainer?: PortainerMetadata;
|
||||
ConfigFrom?: { Network: string };
|
||||
ConfigOnly?: boolean;
|
||||
}
|
||||
) {
|
||||
this.Id = data.Id || '';
|
||||
this.Name = data.Name || '';
|
||||
this.Scope = data.Scope || '';
|
||||
this.Driver = data.Driver || '';
|
||||
this.Attachable = data.Attachable || false;
|
||||
this.Internal = data.Internal || false;
|
||||
this.IPAM = data.IPAM;
|
||||
this.Containers = data.Containers;
|
||||
this.Options = data.Options;
|
||||
this.Ingress = data.Ingress || false;
|
||||
|
||||
this.Labels = data.Labels || {};
|
||||
if (this.Labels && this.Labels['com.docker.compose.project']) {
|
||||
this.StackName = this.Labels['com.docker.compose.project'];
|
||||
} else if (this.Labels && this.Labels['com.docker.stack.namespace']) {
|
||||
this.StackName = this.Labels['com.docker.stack.namespace'];
|
||||
}
|
||||
|
||||
if (data.Portainer) {
|
||||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(
|
||||
data.Portainer.ResourceControl
|
||||
);
|
||||
}
|
||||
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
}
|
||||
|
||||
this.ConfigFrom = data.ConfigFrom;
|
||||
this.ConfigOnly = data.ConfigOnly;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
ResourceObject,
|
||||
} from 'docker-types/generated/1.41';
|
||||
|
||||
type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
|
||||
[Property in Key]-?: Type[Property];
|
||||
};
|
||||
import { WithRequiredProperty } from '@/types';
|
||||
|
||||
export class NodeViewModel {
|
||||
Model: Node;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ProcessesDatatable } from '@/react/docker/containers/StatsView/Processe
|
||||
import { ScaleServiceButton } from '@/react/docker/services/ListView/ServicesDatatable/columns/schedulingMode/ScaleServiceButton';
|
||||
import { SecretsDatatable } from '@/react/docker/secrets/ListView/SecretsDatatable';
|
||||
import { StacksDatatable } from '@/react/docker/stacks/ListView/StacksDatatable';
|
||||
import { NetworksDatatable } from '@/react/docker/networks/ListView/NetworksDatatable';
|
||||
|
||||
import { containersModule } from './containers';
|
||||
import { servicesModule } from './services';
|
||||
@@ -57,6 +58,14 @@ const ngModule = angular
|
||||
['environment', 'stackName']
|
||||
)
|
||||
)
|
||||
.component(
|
||||
'networksDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(NetworksDatatable)), [
|
||||
'dataset',
|
||||
'onRefresh',
|
||||
'onRemove',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'gpu',
|
||||
r2a(Gpu, [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<networks-datatable
|
||||
<ng-networks-datatable
|
||||
title-text="Networks"
|
||||
title-icon="share-2"
|
||||
dataset="networks"
|
||||
@@ -11,6 +11,8 @@
|
||||
remove-action="removeAction"
|
||||
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||
refresh-callback="getNetworks"
|
||||
></networks-datatable>
|
||||
></ng-networks-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<networks-datatable ng-if="networks" dataset="networks" on-refresh="" on-remove="(removeAction)"></networks-datatable>
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ExpandableDatatableTableRow<D extends DefaultType>({
|
||||
cells={cells}
|
||||
onClick={expandOnClick ? () => row.toggleExpanded() : undefined}
|
||||
/>
|
||||
{row.getIsExpanded() && renderSubRow(row)}
|
||||
{row.getIsExpanded() && row.getCanExpand() && renderSubRow(row)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,14 @@ async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
|
||||
|
||||
function getNetwork(networkName: string): DockerNetwork {
|
||||
return {
|
||||
ConfigFrom: {
|
||||
Network: '',
|
||||
},
|
||||
ConfigOnly: false,
|
||||
Created: '',
|
||||
EnableIPv6: false,
|
||||
Ingress: false,
|
||||
Labels: {},
|
||||
Attachable: false,
|
||||
Containers: {
|
||||
a761fcafdae3bdae42cf3702c8554b3e1b0334f85dd6b65b3584aff7246279e4: {
|
||||
@@ -87,6 +95,8 @@ function getNetwork(networkName: string): DockerNetwork {
|
||||
],
|
||||
Driver: 'default',
|
||||
Options: null,
|
||||
IPV4Configs: [],
|
||||
IPV6Configs: [],
|
||||
},
|
||||
Id: '4c52a72e3772fdfb5823cf519b759e3f716e6d98cfb3bfef056e32c9c878329f',
|
||||
Internal: false,
|
||||
|
||||
20
app/react/docker/networks/ListView/NestedNetwordsTable.tsx
Normal file
20
app/react/docker/networks/ListView/NestedNetwordsTable.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { NestedDatatable } from '@@/datatables/NestedDatatable';
|
||||
|
||||
import { useIsSwarm } from '../../proxy/queries/useInfo';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
import { DecoratedNetwork } from './types';
|
||||
|
||||
export function NestedNetworksDatatable({
|
||||
dataset,
|
||||
}: {
|
||||
dataset: Array<DecoratedNetwork>;
|
||||
}) {
|
||||
const environmentId = useEnvironmentId();
|
||||
const isSwarm = useIsSwarm(environmentId);
|
||||
|
||||
const columns = useColumns(isSwarm);
|
||||
return <NestedDatatable columns={columns} dataset={dataset} />;
|
||||
}
|
||||
113
app/react/docker/networks/ListView/NetworksDatatable.tsx
Normal file
113
app/react/docker/networks/ListView/NetworksDatatable.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Plus, Share2, Trash2 } from 'lucide-react';
|
||||
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import {
|
||||
BasicTableSettings,
|
||||
createPersistedStore,
|
||||
refreshableSettings,
|
||||
RefreshableTableSettings,
|
||||
} from '@@/datatables/types';
|
||||
import { Button } from '@@/buttons';
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { useIsSwarm } from '../../proxy/queries/useInfo';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
import { DecoratedNetwork } from './types';
|
||||
import { NestedNetworksDatatable } from './NestedNetwordsTable';
|
||||
|
||||
const storageKey = 'docker.networks';
|
||||
|
||||
interface TableSettings extends BasicTableSettings, RefreshableTableSettings {}
|
||||
|
||||
const settingsStore = createPersistedStore<TableSettings>(
|
||||
storageKey,
|
||||
'name',
|
||||
(set) => ({
|
||||
...refreshableSettings(set),
|
||||
})
|
||||
);
|
||||
|
||||
type DatasetType = Array<DecoratedNetwork>;
|
||||
interface Props {
|
||||
dataset: DatasetType;
|
||||
onRemove(selectedItems: DatasetType): void;
|
||||
onRefresh(): Promise<void>;
|
||||
}
|
||||
|
||||
export function NetworksDatatable({ dataset, onRemove, onRefresh }: Props) {
|
||||
const settings = useTableState(settingsStore, storageKey);
|
||||
|
||||
const environmentId = useEnvironmentId();
|
||||
const isSwarm = useIsSwarm(environmentId);
|
||||
|
||||
const columns = useColumns(isSwarm);
|
||||
|
||||
useRepeater(settings.autoRefreshRate, onRefresh);
|
||||
|
||||
return (
|
||||
<ExpandableDatatable<DecoratedNetwork>
|
||||
settingsManager={settings}
|
||||
title="Networks"
|
||||
titleIcon={Share2}
|
||||
dataset={dataset}
|
||||
columns={columns}
|
||||
getRowCanExpand={({ original: item }) =>
|
||||
!!(item.Subs && item.Subs?.length > 0)
|
||||
}
|
||||
isRowSelectable={({ original: item }) => !item.ResourceControl?.System}
|
||||
renderSubRow={(row) => (
|
||||
<>
|
||||
{row.original.Subs && (
|
||||
<tr>
|
||||
<td colSpan={Number.MAX_SAFE_INTEGER}>
|
||||
<NestedNetworksDatatable dataset={row.original.Subs} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
emptyContentLabel="No networks available."
|
||||
renderTableActions={(selectedRows) => (
|
||||
<div className="flex gap-3">
|
||||
<Authorized
|
||||
authorizations={['DockerNetworkDelete', 'DockerNetworkCreate']}
|
||||
>
|
||||
<Button
|
||||
disabled={selectedRows.length === 0}
|
||||
color="dangerlight"
|
||||
onClick={() => onRemove(selectedRows)}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Authorized>
|
||||
<Authorized
|
||||
authorizations="DockerNetworkCreate"
|
||||
data-cy="network-addNetworkButton"
|
||||
>
|
||||
<Button icon={Plus} as={Link} props={{ to: '.new' }}>
|
||||
Add network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</div>
|
||||
)}
|
||||
renderTableSettings={() => (
|
||||
<TableSettingsMenu>
|
||||
<TableSettingsMenuAutoRefresh
|
||||
onChange={settings.setAutoRefreshRate}
|
||||
value={settings.autoRefreshRate}
|
||||
/>
|
||||
</TableSettingsMenu>
|
||||
)}
|
||||
getRowId={(row) => `${row.Name}-${row.Id}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
app/react/docker/networks/ListView/columns/helper.ts
Normal file
5
app/react/docker/networks/ListView/columns/helper.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { DecoratedNetwork } from '../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<DecoratedNetwork>();
|
||||
63
app/react/docker/networks/ListView/columns/index.ts
Normal file
63
app/react/docker/networks/ListView/columns/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createOwnershipColumn } from '@/react/docker/components/datatable/createOwnershipColumn';
|
||||
|
||||
import { buildExpandColumn } from '@@/datatables/expand-column';
|
||||
|
||||
import { DecoratedNetwork } from '../types';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
import { name } from './name';
|
||||
|
||||
export function useColumns(isHostColumnVisible?: boolean) {
|
||||
return useMemo(
|
||||
() =>
|
||||
_.compact([
|
||||
buildExpandColumn<DecoratedNetwork>(),
|
||||
name,
|
||||
columnHelper.accessor((item) => item.StackName || '-', {
|
||||
header: 'Stack',
|
||||
}),
|
||||
columnHelper.accessor('Driver', {
|
||||
header: 'Driver',
|
||||
}),
|
||||
columnHelper.accessor('Attachable', {
|
||||
header: 'Attachable',
|
||||
}),
|
||||
columnHelper.accessor('IPAM.Driver', {
|
||||
header: 'IPAM Driver',
|
||||
}),
|
||||
columnHelper.accessor(
|
||||
(item) => item.IPAM?.IPV4Configs?.[0]?.Subnet ?? '-',
|
||||
{
|
||||
header: 'IPV4 IPAM Subnet',
|
||||
}
|
||||
),
|
||||
columnHelper.accessor(
|
||||
(item) => item.IPAM?.IPV4Configs?.[0]?.Gateway ?? '-',
|
||||
{
|
||||
header: 'IPV4 IPAM Gateway',
|
||||
}
|
||||
),
|
||||
columnHelper.accessor(
|
||||
(item) => item.IPAM?.IPV6Configs?.[0]?.Subnet ?? '-',
|
||||
{
|
||||
header: 'IPV6 IPAM Subnet',
|
||||
}
|
||||
),
|
||||
columnHelper.accessor(
|
||||
(item) => item.IPAM?.IPV6Configs?.[0]?.Gateway ?? '-',
|
||||
{
|
||||
header: 'IPV6 IPAM Gateway',
|
||||
}
|
||||
),
|
||||
isHostColumnVisible &&
|
||||
columnHelper.accessor('NodeName', {
|
||||
header: 'Node',
|
||||
}),
|
||||
createOwnershipColumn<DecoratedNetwork>(),
|
||||
]),
|
||||
[isHostColumnVisible]
|
||||
);
|
||||
}
|
||||
31
app/react/docker/networks/ListView/columns/name.tsx
Normal file
31
app/react/docker/networks/ListView/columns/name.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { truncate } from '@/portainer/filters/filters';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const name = columnHelper.accessor('Name', {
|
||||
header: 'Name',
|
||||
id: 'name',
|
||||
cell({ row: { original: item } }) {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
to=".network"
|
||||
params={{ id: item.Id, nodeName: item.NodeName }}
|
||||
title={item.Name}
|
||||
>
|
||||
{truncate(item.Name, 40)}
|
||||
</Link>
|
||||
{item.ResourceControl?.System && (
|
||||
<span
|
||||
style={{ marginLeft: '10px' }}
|
||||
className="label label-info image-tag space-left"
|
||||
>
|
||||
System
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
11
app/react/docker/networks/ListView/types.ts
Normal file
11
app/react/docker/networks/ListView/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IPAMConfig } from 'docker-types/generated/1.41';
|
||||
|
||||
import { NetworkViewModel } from '@/docker/models/network';
|
||||
|
||||
export type DecoratedNetwork = NetworkViewModel & {
|
||||
Subs?: DecoratedNetwork[];
|
||||
IPAM: NetworkViewModel['IPAM'] & {
|
||||
IPV4Configs?: Array<IPAMConfig>;
|
||||
IPV6Configs?: Array<IPAMConfig>;
|
||||
};
|
||||
};
|
||||
@@ -32,7 +32,7 @@ export type NetworkResponseContainers = Record<
|
||||
NetworkResponseContainer
|
||||
>;
|
||||
|
||||
export interface DockerNetwork {
|
||||
export type DockerNetwork = {
|
||||
Name: string;
|
||||
Id: NetworkId;
|
||||
Driver: string;
|
||||
@@ -43,8 +43,17 @@ export interface DockerNetwork {
|
||||
Config: IPConfig[];
|
||||
Driver: string;
|
||||
Options: IpamOptions;
|
||||
IPV4Configs: IPConfig[];
|
||||
IPV6Configs: IPConfig[];
|
||||
};
|
||||
Portainer?: PortainerMetadata;
|
||||
Options: NetworkOptions;
|
||||
Containers: NetworkResponseContainers;
|
||||
}
|
||||
|
||||
Ingress: boolean;
|
||||
Labels: Record<string, string | undefined>;
|
||||
ConfigFrom: { Network: string };
|
||||
ConfigOnly: boolean;
|
||||
Created: string;
|
||||
EnableIPv6: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user