From b933bee95e7ac12f63c53276c6725c44124399d4 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 22 Oct 2023 11:35:22 +0200 Subject: [PATCH] feat(docker/networks): migrate networks datatable to React [EE-4670] (#10351) Co-authored-by: LP B --- .../networkRowContent.html | 36 --- .../network-row-content/networkRowContent.js | 21 -- .../networks-datatable/networksDatatable.html | 238 ------------------ .../networks-datatable/networksDatatable.js | 15 -- .../networksDatatableController.js | 80 ------ app/docker/models/network.js | 33 --- app/docker/models/network.ts | 79 ++++++ app/docker/models/node.ts | 4 +- app/docker/react/components/index.ts | 9 + app/docker/views/networks/networks.html | 15 +- app/react/docker/networks/ListView/.keep | 0 .../networks/ListView/NestedNetwordsTable.tsx | 20 ++ .../networks/ListView/NetworksDatatable.tsx | 113 +++++++++ .../networks/ListView/columns/helper.ts | 5 + .../docker/networks/ListView/columns/index.ts | 63 +++++ .../docker/networks/ListView/columns/name.tsx | 31 +++ app/react/docker/networks/ListView/types.ts | 11 + 17 files changed, 333 insertions(+), 440 deletions(-) delete mode 100644 app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html delete mode 100644 app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js delete mode 100644 app/docker/components/datatables/networks-datatable/networksDatatable.html delete mode 100644 app/docker/components/datatables/networks-datatable/networksDatatable.js delete mode 100644 app/docker/components/datatables/networks-datatable/networksDatatableController.js delete mode 100644 app/docker/models/network.js create mode 100644 app/docker/models/network.ts delete mode 100644 app/react/docker/networks/ListView/.keep create mode 100644 app/react/docker/networks/ListView/NestedNetwordsTable.tsx create mode 100644 app/react/docker/networks/ListView/NetworksDatatable.tsx create mode 100644 app/react/docker/networks/ListView/columns/helper.ts create mode 100644 app/react/docker/networks/ListView/columns/index.ts create mode 100644 app/react/docker/networks/ListView/columns/name.tsx create mode 100644 app/react/docker/networks/ListView/types.ts diff --git a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html deleted file mode 100644 index d335615a7..000000000 --- a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - {{ item.Name | truncate: 40 }} - System - -{{ item.StackName ? item.StackName : '-' }} -{{ item.Driver }} -{{ item.Attachable }} -{{ item.IPAM.Driver }} -{{ item.IPAM.IPV4Configs[0].Subnet ? item.IPAM.IPV4Configs[0].Subnet : '-' }} -{{ item.IPAM.IPV4Configs[0].Gateway ? item.IPAM.IPV4Configs[0].Gateway : '-' }} -{{ item.IPAM.IPV6Configs[0].Subnet ? item.IPAM.IPV6Configs[0].Subnet : '-' }} -{{ item.IPAM.IPV6Configs[0].Gateway ? item.IPAM.IPV6Configs[0].Gateway : '-' }} -{{ item.NodeName ? item.NodeName : '-' }} - - - - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = RCO.ADMINISTRATORS }} - - diff --git a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js deleted file mode 100644 index 62380d8e4..000000000 --- a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js +++ /dev/null @@ -1,21 +0,0 @@ -import { ResourceControlOwnership as RCO } from '@/react/portainer/access-control/types'; - -angular.module('portainer.docker').directive('networkRowContent', [ - function networkRowContent() { - var directive = { - templateUrl: './networkRowContent.html', - restrict: 'A', - transclude: true, - scope: { - item: '<', - parentCtrl: '<', - allowCheckbox: '<', - allowExpand: '<', - }, - controller: ($scope) => { - $scope.RCO = RCO; - }, - }; - return directive; - }, -]); diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html deleted file mode 100644 index 785a63629..000000000 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.html +++ /dev/null @@ -1,238 +0,0 @@ -
- - -
-
-
- -
- {{ $ctrl.titleText }} -
- -
- - -
-
- - - - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Loading...
No network available.
-
- -
-
-
diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.js b/app/docker/components/datatables/networks-datatable/networksDatatable.js deleted file mode 100644 index d40efa330..000000000 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('portainer.docker').component('networksDatatable', { - templateUrl: './networksDatatable.html', - controller: 'NetworksDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - showHostColumn: '<', - removeAction: '<', - refreshCallback: '<', - }, -}); diff --git a/app/docker/components/datatables/networks-datatable/networksDatatableController.js b/app/docker/components/datatables/networks-datatable/networksDatatableController.js deleted file mode 100644 index f8e13c605..000000000 --- a/app/docker/components/datatables/networks-datatable/networksDatatableController.js +++ /dev/null @@ -1,80 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.docker').controller('NetworksDatatableController', [ - '$scope', - '$controller', - 'NetworkHelper', - 'DatatableService', - function ($scope, $controller, NetworkHelper, DatatableService) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - - this.disableRemove = function (item) { - return NetworkHelper.isSystemNetwork(item); - }; - - this.state = Object.assign(this.state, { - expandedItems: [], - }); - - /** - * Do not allow system networks to be selected - */ - this.allowSelection = function (item) { - return !this.disableRemove(item); - }; - - this.$onInit = function () { - this.setDefaults(); - this.prepareTableFromDataset(); - - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } - - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } - - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } - this.onSettingsRepeaterChange(); - }; - - this.expandItem = function (item, expanded) { - item.Expanded = expanded; - }; - - this.itemCanExpand = function (item) { - return item.Subs.length > 0; - }; - - this.hasExpandableItems = function () { - return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; - }; - - this.expandAll = function () { - this.state.expandAll = !this.state.expandAll; - _.forEach(this.state.filteredDataSet, (item) => { - if (this.itemCanExpand(item)) { - this.expandItem(item, this.state.expandAll); - } - }); - }; - }, -]); diff --git a/app/docker/models/network.js b/app/docker/models/network.js deleted file mode 100644 index d4ca83148..000000000 --- a/app/docker/models/network.js +++ /dev/null @@ -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; -} diff --git a/app/docker/models/network.ts b/app/docker/models/network.ts new file mode 100644 index 000000000..56b373c2f --- /dev/null +++ b/app/docker/models/network.ts @@ -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; + + Options?: Record; + + Ingress: boolean; + + Labels: Record; + + 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; + } +} diff --git a/app/docker/models/node.ts b/app/docker/models/node.ts index 8cf806881..d0abfbc79 100644 --- a/app/docker/models/node.ts +++ b/app/docker/models/node.ts @@ -10,9 +10,7 @@ import { ResourceObject, } from 'docker-types/generated/1.41'; -type WithRequiredProperty = Type & { - [Property in Key]-?: Type[Property]; -}; +import { WithRequiredProperty } from '@/types'; export class NodeViewModel { Model: Node; diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index cfcdbbcb3..a76d7e2a8 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -22,6 +22,7 @@ import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolum import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable'; 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( 'gpusList', r2a(withControlledInput(GpusList), ['value', 'onChange']) diff --git a/app/docker/views/networks/networks.html b/app/docker/views/networks/networks.html index 7b38e2e8b..67496334e 100644 --- a/app/docker/views/networks/networks.html +++ b/app/docker/views/networks/networks.html @@ -1,16 +1,3 @@ -
-
- -
-
+ diff --git a/app/react/docker/networks/ListView/.keep b/app/react/docker/networks/ListView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/docker/networks/ListView/NestedNetwordsTable.tsx b/app/react/docker/networks/ListView/NestedNetwordsTable.tsx new file mode 100644 index 000000000..116b23edf --- /dev/null +++ b/app/react/docker/networks/ListView/NestedNetwordsTable.tsx @@ -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; +}) { + const environmentId = useEnvironmentId(); + const isSwarm = useIsSwarm(environmentId); + + const columns = useColumns(isSwarm); + return ; +} diff --git a/app/react/docker/networks/ListView/NetworksDatatable.tsx b/app/react/docker/networks/ListView/NetworksDatatable.tsx new file mode 100644 index 000000000..26ba1fa2f --- /dev/null +++ b/app/react/docker/networks/ListView/NetworksDatatable.tsx @@ -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( + storageKey, + 'name', + (set) => ({ + ...refreshableSettings(set), + }) +); + +type DatasetType = Array; +interface Props { + dataset: DatasetType; + onRemove(selectedItems: DatasetType): void; + onRefresh(): Promise; +} + +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 ( + + 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 && ( + + + + + + )} + + )} + emptyContentLabel="No networks available." + renderTableActions={(selectedRows) => ( +
+ + + + + + +
+ )} + renderTableSettings={() => ( + + + + )} + getRowId={(row) => `${row.Name}-${row.Id}`} + /> + ); +} diff --git a/app/react/docker/networks/ListView/columns/helper.ts b/app/react/docker/networks/ListView/columns/helper.ts new file mode 100644 index 000000000..bdc6af12a --- /dev/null +++ b/app/react/docker/networks/ListView/columns/helper.ts @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { DecoratedNetwork } from '../types'; + +export const columnHelper = createColumnHelper(); diff --git a/app/react/docker/networks/ListView/columns/index.ts b/app/react/docker/networks/ListView/columns/index.ts new file mode 100644 index 000000000..b2be3b05c --- /dev/null +++ b/app/react/docker/networks/ListView/columns/index.ts @@ -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(), + 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(), + ]), + [isHostColumnVisible] + ); +} diff --git a/app/react/docker/networks/ListView/columns/name.tsx b/app/react/docker/networks/ListView/columns/name.tsx new file mode 100644 index 000000000..77321a4a9 --- /dev/null +++ b/app/react/docker/networks/ListView/columns/name.tsx @@ -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 ( + <> + + {truncate(item.Name, 40)} + + {item.ResourceControl?.System && ( + + System + + )} + + ); + }, +}); diff --git a/app/react/docker/networks/ListView/types.ts b/app/react/docker/networks/ListView/types.ts new file mode 100644 index 000000000..56cad2570 --- /dev/null +++ b/app/react/docker/networks/ListView/types.ts @@ -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; + IPV6Configs?: Array; + }; +};