From 60477ae287bdb6a81718b79251807f0e54ea0e44 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 11 Sep 2023 15:27:04 +0100 Subject: [PATCH] refactor(docker/networks): migrate macvlan nodes selector to react [EE-4669] (#10183) --- .../macvlanNodesDatatable.html | 129 ------------------ .../macvlanNodesDatatable.js | 15 -- .../networkMacvlanForm.html | 25 ++-- .../networkMacvlanFormController.js | 9 +- app/docker/filters/filters.js | 12 +- app/docker/filters/utils.ts | 10 +- app/docker/models/node.js | 47 ------- app/docker/models/node.ts | 120 ++++++++++++++++ app/docker/react/components/index.ts | 2 + app/docker/react/components/networks.ts | 18 +++ .../MacvlanNodesSelector.tsx | 55 ++++++++ .../columns/column-helper.ts | 5 + .../MacvlanNodesSelector/columns/index.ts | 20 +++ .../MacvlanNodesSelector/columns/name.tsx | 38 ++++++ .../MacvlanNodesSelector/columns/status.tsx | 16 +++ .../CreateView/MacvlanNodesSelector/index.ts | 0 .../CreateView/MacvlanNodesSelector/types.ts | 14 ++ 17 files changed, 315 insertions(+), 220 deletions(-) delete mode 100644 app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html delete mode 100644 app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js delete mode 100644 app/docker/models/node.js create mode 100644 app/docker/models/node.ts create mode 100644 app/docker/react/components/networks.ts create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/column-helper.ts create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/name.tsx create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/index.ts create mode 100644 app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts diff --git a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html deleted file mode 100644 index ebe8db2db..000000000 --- a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html +++ /dev/null @@ -1,129 +0,0 @@ -
- - -
-
-
- -
- {{ $ctrl.titleText }} -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - -
-
- - - - - - - -
- - - - - {{ item.Hostname }} - {{ item.Hostname }} - {{ item.Role }}{{ item.EngineVersion }}{{ item.Addr }} - {{ item.Status }} -
Loading...
No node available.
-
- -
-
-
diff --git a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js deleted file mode 100644 index d7ad30bbb..000000000 --- a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('portainer.docker').component('macvlanNodesDatatable', { - templateUrl: './macvlanNodesDatatable.html', - controller: 'GenericDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - showIpAddressColumn: '<', - accessToNodeDetails: '<', - state: '=', - }, -}); diff --git a/app/docker/components/network-macvlan-form/networkMacvlanForm.html b/app/docker/components/network-macvlan-form/networkMacvlanForm.html index d52bcc6ed..8c689d971 100644 --- a/app/docker/components/network-macvlan-form/networkMacvlanForm.html +++ b/app/docker/components/network-macvlan-form/networkMacvlanForm.html @@ -37,23 +37,14 @@
-
-
- -
-
+ +
diff --git a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js index c654f979d..3080095fa 100644 --- a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js +++ b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js @@ -12,11 +12,18 @@ angular.module('portainer.docker').controller('NetworkMacvlanFormController', [ this.options = []; + ctrl.onChangeSelectedNodes = onChangeSelectedNodes.bind(ctrl); + function onChangeSelectedNodes(nodes) { + return $scope.$evalAsync(() => { + ctrl.data.DatatableState.selectedItems = nodes; + }); + } + ctrl.requiredNodeSelection = function () { if (ctrl.data.Scope !== 'local' || ctrl.data.DatatableState === undefined) { return false; } - return ctrl.data.DatatableState.selectedItemCount === 0; + return ctrl.data.DatatableState.selectedItems.length; }; ctrl.requiredConfigSelection = function () { diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js index 97dfe59c4..4f5944e25 100644 --- a/app/docker/filters/filters.js +++ b/app/docker/filters/filters.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { joinCommand, taskStatusBadge, trimSHA } from './utils'; +import { joinCommand, taskStatusBadge, nodeStatusBadge, trimSHA } from './utils'; function includeString(text, values) { return values.some(function (val) { @@ -75,15 +75,7 @@ angular return 'success'; }; }) - .filter('nodestatusbadge', function () { - 'use strict'; - return function (text) { - if (text === 'down' || text === 'Unhealthy') { - return 'danger'; - } - return 'success'; - }; - }) + .filter('nodestatusbadge', () => nodeStatusBadge) .filter('dockerNodeAvailabilityBadge', function () { 'use strict'; return function (text) { diff --git a/app/docker/filters/utils.ts b/app/docker/filters/utils.ts index 5acd7f9be..144df280e 100644 --- a/app/docker/filters/utils.ts +++ b/app/docker/filters/utils.ts @@ -1,5 +1,5 @@ +import { NodeStatus, TaskState } from 'docker-types/generated/1.41'; import _ from 'lodash'; -import { TaskState } from 'docker-types/generated/1.41'; export function trimSHA(imageName: string) { if (!imageName) { @@ -53,3 +53,11 @@ export function taskStatusBadge(text?: TaskState) { } return 'default'; } + +export function nodeStatusBadge(text: NodeStatus['State']) { + if (text === 'down' || text === 'unknown' || text === 'disconnected') { + return 'danger'; + } + + return 'success'; +} diff --git a/app/docker/models/node.js b/app/docker/models/node.js deleted file mode 100644 index 9386bf984..000000000 --- a/app/docker/models/node.js +++ /dev/null @@ -1,47 +0,0 @@ -export function NodeViewModel(data) { - this.Model = data; - this.Id = data.ID; - this.Version = data.Version.Index; - this.Name = data.Spec.Name; - this.Role = data.Spec.Role; - this.CreatedAt = data.CreatedAt; - this.UpdatedAt = data.UpdatedAt; - this.Availability = data.Spec.Availability; - - var labels = data.Spec.Labels; - if (labels) { - this.Labels = Object.keys(labels).map(function (key) { - return { key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true }; - }); - } else { - this.Labels = []; - } - - var engineLabels = data.Description.Engine.Labels; - if (engineLabels) { - this.EngineLabels = Object.keys(engineLabels).map(function (key) { - return { key: key, value: engineLabels[key] }; - }); - } else { - this.EngineLabels = []; - } - - this.Hostname = data.Description.Hostname; - this.PlatformArchitecture = data.Description.Platform.Architecture; - this.PlatformOS = data.Description.Platform.OS; - this.CPUs = data.Description.Resources.NanoCPUs; - this.Memory = data.Description.Resources.MemoryBytes; - this.EngineVersion = data.Description.Engine.EngineVersion; - this.Plugins = data.Description.Engine.Plugins; - this.Status = data.Status.State; - - if (data.Status.Addr) { - this.Addr = data.Status.Addr; - } - - if (data.ManagerStatus) { - this.Leader = data.ManagerStatus.Leader; - this.Reachability = data.ManagerStatus.Reachability; - this.ManagerAddr = data.ManagerStatus.Addr; - } -} diff --git a/app/docker/models/node.ts b/app/docker/models/node.ts new file mode 100644 index 000000000..8cf806881 --- /dev/null +++ b/app/docker/models/node.ts @@ -0,0 +1,120 @@ +import { + Node, + EngineDescription, + ManagerStatus, + NodeDescription, + NodeSpec, + NodeStatus, + ObjectVersion, + Platform, + ResourceObject, +} from 'docker-types/generated/1.41'; + +type WithRequiredProperty = Type & { + [Property in Key]-?: Type[Property]; +}; + +export class NodeViewModel { + Model: Node; + + Id: Node['ID']; + + Version: ObjectVersion['Index']; + + Name: NodeSpec['Name']; + + Role: NodeSpec['Role']; + + CreatedAt: Node['CreatedAt']; + + UpdatedAt: Node['UpdatedAt']; + + Availability: NodeSpec['Availability']; + + Labels: Array<{ + key: string; + value: string; + originalKey: string; + originalValue: string; + added: boolean; + }>; + + EngineLabels: Array<{ key: string; value: string }>; + + Hostname: NodeDescription['Hostname']; + + PlatformArchitecture: Platform['Architecture']; + + PlatformOS: Platform['OS']; + + CPUs: ResourceObject['NanoCPUs']; + + Memory: ResourceObject['MemoryBytes']; + + EngineVersion: EngineDescription['EngineVersion']; + + Plugins: EngineDescription['Plugins']; + + Status: NodeStatus['State']; + + Addr: WithRequiredProperty['Addr'] = ''; + + Leader: ManagerStatus['Leader']; + + Reachability: ManagerStatus['Reachability']; + + ManagerAddr: ManagerStatus['Addr']; + + constructor(data: Node) { + this.Model = data; + this.Id = data.ID; + this.Version = data.Version?.Index; + this.Name = data.Spec?.Name; + this.Role = data.Spec?.Role; + this.CreatedAt = data.CreatedAt; + this.UpdatedAt = data.UpdatedAt; + this.Availability = data.Spec?.Availability; + + const labels = data.Spec?.Labels; + if (labels) { + this.Labels = Object.keys(labels).map((key) => ({ + key, + value: labels[key], + originalKey: key, + originalValue: labels[key], + added: true, + })); + } else { + this.Labels = []; + } + + const engineLabels = data.Description?.Engine?.Labels; + if (engineLabels) { + this.EngineLabels = Object.keys(engineLabels).map((key) => ({ + key, + value: engineLabels[key], + })); + } else { + this.EngineLabels = []; + } + + this.Hostname = data.Description?.Hostname; + this.PlatformArchitecture = data.Description?.Platform?.Architecture; + this.PlatformOS = data.Description?.Platform?.OS; + this.CPUs = data.Description?.Resources?.NanoCPUs; + this.Memory = data.Description?.Resources?.MemoryBytes; + this.EngineVersion = data.Description?.Engine?.EngineVersion; + this.Plugins = data.Description?.Engine?.Plugins; + this.Status = data.Status?.State; + + if (data.Status?.Addr) { + this.Addr = data.Status?.Addr; + } + + if (data.ManagerStatus) { + this.Leader = data.ManagerStatus.Leader; + this.Reachability = data.ManagerStatus.Reachability; + this.ManagerAddr = data.ManagerStatus.Addr; + } + } +} diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index 0f6bf847f..4e466be12 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -27,11 +27,13 @@ import { StacksDatatable } from '@/react/docker/stacks/ListView/StacksDatatable' import { containersModule } from './containers'; import { servicesModule } from './services'; +import { networksModule } from './networks'; const ngModule = angular .module('portainer.docker.react.components', [ containersModule, servicesModule, + networksModule, ]) .component('dockerfileDetails', r2a(DockerfileDetails, ['image'])) .component('dockerHealthStatus', r2a(HealthStatus, ['health'])) diff --git a/app/docker/react/components/networks.ts b/app/docker/react/components/networks.ts new file mode 100644 index 000000000..eb6b4e72c --- /dev/null +++ b/app/docker/react/components/networks.ts @@ -0,0 +1,18 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { MacvlanNodesSelector } from '@/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector'; +import { withUIRouter } from '@/react-tools/withUIRouter'; + +export const networksModule = angular + .module('portainer.docker.react.components.networks', []) + .component( + 'macvlanNodesSelector', + r2a(withUIRouter(MacvlanNodesSelector), [ + 'dataset', + 'isIpColumnVisible', + 'haveAccessToNode', + 'value', + 'onChange', + ]) + ).name; diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx b/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx new file mode 100644 index 000000000..fd071f209 --- /dev/null +++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/MacvlanNodesSelector.tsx @@ -0,0 +1,55 @@ +import { HardDrive } from 'lucide-react'; + +import { NodeViewModel } from '@/docker/models/node'; + +import { Datatable } from '@@/datatables'; +import { createPersistedStore } from '@@/datatables/types'; +import { useTableState } from '@@/datatables/useTableState'; +import { mergeOptions } from '@@/datatables/extend-options/mergeOptions'; +import { withMeta } from '@@/datatables/extend-options/withMeta'; +import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected'; + +import { useColumns } from './columns'; + +const tableKey = 'macvlan-nodes-selector'; +const store = createPersistedStore(tableKey); + +export function MacvlanNodesSelector({ + dataset, + isIpColumnVisible, + haveAccessToNode, + value, + onChange, +}: { + dataset?: Array; + isIpColumnVisible: boolean; + haveAccessToNode: boolean; + value: Array; + onChange(value: Array): void; +}) { + const columns = useColumns(isIpColumnVisible); + const tableState = useTableState(store, tableKey); + + return ( + + title="Select the nodes where you want to deploy the local configuration" + titleIcon={HardDrive} + columns={columns} + dataset={dataset || []} + isLoading={!dataset} + emptyContentLabel="No node available" + settingsManager={tableState} + extendTableOptions={mergeOptions( + withMeta({ + table: 'macvlan-nodes', + haveAccessToNode, + }), + withControlledSelected( + (ids) => + onChange(dataset?.filter((n) => n.Id && ids.includes(n.Id)) || []), + value.map((n) => n.Id || '') + ) + )} + /> + ); +} diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/column-helper.ts b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/column-helper.ts new file mode 100644 index 000000000..ea1f2d20a --- /dev/null +++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/column-helper.ts @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { NodeViewModel } from '@/docker/models/node'; + +export const columnHelper = createColumnHelper(); diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts new file mode 100644 index 000000000..cbbf34268 --- /dev/null +++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/index.ts @@ -0,0 +1,20 @@ +import _ from 'lodash'; + +import { columnHelper } from './column-helper'; +import { name } from './name'; +import { status } from './status'; + +export function useColumns(isIpColumnVisible: boolean) { + return _.compact([ + name, + columnHelper.accessor('Role', {}), + columnHelper.accessor('EngineVersion', { + header: 'Engine', + }), + isIpColumnVisible && + columnHelper.accessor('Addr', { + header: 'IP Address', + }), + status, + ]); +} diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/name.tsx b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/name.tsx new file mode 100644 index 000000000..ffb99a51a --- /dev/null +++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/name.tsx @@ -0,0 +1,38 @@ +import { CellContext } from '@tanstack/react-table'; + +import { NodeViewModel } from '@/docker/models/node'; + +import { Link } from '@@/Link'; + +import { isTableMeta } from '../types'; + +import { columnHelper } from './column-helper'; + +export const name = columnHelper.accessor('Hostname', { + header: 'Name', + cell: Cell, +}); + +function Cell({ + getValue, + row: { original: item }, + table: { + options: { meta }, + }, +}: CellContext) { + if (!isTableMeta(meta)) { + throw new Error('Invalid table meta'); + } + + const value = getValue(); + + if (!meta.haveAccessToNode) { + return <>{value}; + } + + return ( + + {value} + + ); +} diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx new file mode 100644 index 000000000..88642d7ac --- /dev/null +++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/columns/status.tsx @@ -0,0 +1,16 @@ +import clsx from 'clsx'; + +import { nodeStatusBadge } from '@/docker/filters/utils'; + +import { columnHelper } from './column-helper'; + +export const status = columnHelper.accessor('Status', { + cell: ({ getValue }) => { + const value = getValue(); + return ( + + {value} + + ); + }, +}); diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/index.ts b/app/react/docker/networks/CreateView/MacvlanNodesSelector/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts b/app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts new file mode 100644 index 000000000..290c64a5d --- /dev/null +++ b/app/react/docker/networks/CreateView/MacvlanNodesSelector/types.ts @@ -0,0 +1,14 @@ +import { TableMeta as BaseTableMeta } from '@tanstack/react-table'; + +import { NodeViewModel } from '@/docker/models/node'; + +export type TableMeta = BaseTableMeta & { + table: 'macvlan-nodes'; + haveAccessToNode: boolean; +}; + +export function isTableMeta( + meta?: BaseTableMeta +): meta is TableMeta { + return !!meta && meta.table === 'macvlan-nodes'; +}