From 128ed6284143ffb13a098bf70a68e084ab0ce028 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Mon, 13 Sep 2021 16:26:20 +0200 Subject: [PATCH] F #5422: Add extra configuration to instantiate form (#1450) --- install.sh | 5 +- .../etc/sunstone/admin/cluster-tab.yaml | 55 ++++ src/fireedge/etc/sunstone/admin/host-tab.yaml | 27 +- src/fireedge/etc/sunstone/sunstone-views.yaml | 2 +- src/fireedge/etc/sunstone/user/vm-tab.yaml | 17 -- src/fireedge/package-lock.json | 166 ++++++++--- src/fireedge/package.json | 13 +- .../src/client/apps/provision/_app.js | 4 +- src/fireedge/src/client/apps/sunstone/_app.js | 4 +- .../src/client/apps/sunstone/routes.js | 5 +- .../src/client/apps/sunstone/routesOne.js | 51 +++- .../components/Cards/SelectCard/SelectCard.js | 3 +- .../components/Cards/SelectCard/index.js | 4 +- .../FormControl/AutocompleteController.js | 4 +- .../components/FormControl/FileController.js | 1 - .../client/components/FormStepper/index.js | 80 ++--- .../components/Forms/ButtonToTriggerForm.js | 31 +- .../client/components/Forms/FormWithSchema.js | 6 +- .../Vm/AttachDiskForm/ImageSteps/index.js | 20 +- .../Vm/AttachDiskForm/VolatileSteps/index.js | 20 +- .../Steps/AdvancedOptions/index.js | 36 +-- .../Steps/AdvancedOptions/schema.js | 44 +-- .../Steps/NetworksTable/index.js | 74 +++-- .../Forms/Vm/AttachNicForm/Steps/index.js | 50 ++-- .../Forms/Vm/CreateDiskSnapshotForm/index.js | 10 +- .../PunctualForm/index.js | 30 +- .../PunctualForm/schema.js | 2 +- .../RelativeForm/index.js | 19 +- .../Forms/Vm/CreateSnapshotForm/index.js | 8 +- .../Forms/Vm/ResizeCapacityForm/index.js | 12 +- .../Forms/Vm/ResizeDiskForm/index.js | 8 +- .../Forms/Vm/SaveAsDiskForm/index.js | 8 +- .../Steps/BasicConfiguration/diskSchema.js | 25 +- .../Steps/BasicConfiguration/index.js | 18 ++ .../BasicConfiguration/ownershipSchema.js | 61 ++++ .../Steps/BasicConfiguration/schema.js | 15 +- .../Steps/BasicConfiguration/vcenterSchema.js | 41 +++ .../Steps/BasicConfiguration/vmGroupSchema.js | 80 +++++ .../Steps/ExtraConfiguration/booting.js | 196 ++++++++++++ .../Steps/ExtraConfiguration/index.js | 50 +++- .../Steps/ExtraConfiguration/networking.js | 130 ++++++++ .../Steps/ExtraConfiguration/placement.js | 77 +++++ .../ExtraConfiguration/scheduleAction.js | 130 ++++++++ .../Steps/ExtraConfiguration/schema.js | 85 +++++- .../Steps/VmTemplatesTable/index.js | 12 +- .../VmTemplate/InstantiateForm/Steps/index.js | 53 ++-- .../Forms/VmTemplate/InstantiateForm/index.js | 10 +- .../components/HOC/InternalLayout/index.js | 28 +- .../src/client/components/Header/index.js | 29 +- .../src/client/components/Header/styles.js | 8 +- .../components/Sidebar/SidebarCollapseItem.js | 34 ++- .../Status/LinearProgressWithLabel.js | 4 +- .../components/Tables/Clusters/index.js | 3 +- .../client/components/Tables/Hosts/index.js | 10 +- .../src/client/components/Tables/Hosts/row.js | 14 +- .../components/Tabs/Cluster/Info/index.js | 102 +++++++ .../Tabs/Cluster/Info/information.js | 58 ++++ .../client/components/Tabs/Cluster/index.js | 82 +++++ .../Tabs/Common/Attribute/Attribute.js | 17 +- .../components/Tabs/Common/Ownership.js | 3 + .../client/components/Tabs/Host/Info/index.js | 132 +++++++++ .../components/Tabs/Host/Info/information.js | 92 ++++++ .../Hosts/detail.js => Tabs/Host/index.js} | 96 +++--- .../client/components/Tabs/User/Info/index.js | 96 ++++++ .../components/Tabs/User/Info/information.js | 48 +++ .../src/client/components/Tabs/User/index.js | 82 +++++ .../Tabs/Vm/Capacity/information.js | 2 +- .../client/components/Tabs/Vm/Info/index.js | 16 +- .../components/Tabs/Vm/Info/information.js | 9 +- .../client/components/Tabs/Vm/Network/Item.js | 4 +- .../components/Tabs/Vm/Network/index.js | 14 +- .../Tabs/Vm/SchedActions/Actions.js | 49 +-- .../components/Tabs/Vm/Snapshot/index.js | 2 +- .../components/Tabs/Vm/Storage/Actions.js | 8 +- .../components/Tabs/Vm/Storage/SubItem.js | 6 +- .../components/Tabs/VmTemplate/Info/index.js | 103 +++++++ .../Tabs/VmTemplate/Info/information.js | 51 ++++ .../components/Tabs/VmTemplate/Template.js | 41 +++ .../components/Tabs/VmTemplate/index.js | 84 ++++++ .../src/client/components/Tabs/index.js | 4 +- src/fireedge/src/client/constants/cluster.js | 24 ++ src/fireedge/src/client/constants/index.js | 2 + .../src/client/constants/translates.js | 22 +- .../src/client/containers/Clusters/Detail.js | 42 +++ .../src/client/containers/Clusters/index.js | 22 +- .../src/client/containers/Groups/Detail.js | 43 +++ .../src/client/containers/Hosts/Detail.js | 42 +++ .../src/client/containers/Hosts/index.js | 22 +- .../src/client/containers/Users/Detail.js | 42 +++ .../containers/VirtualMachines/Detail.js | 42 +++ .../containers/VmTemplates/Instantiate.js | 36 +-- .../client/containers/VmTemplates/index.js | 21 +- .../src/client/features/Auth/hooks.js | 7 +- .../src/client/features/Auth/slice.js | 4 +- .../src/client/features/General/actions.js | 1 + .../src/client/features/General/hooks.js | 4 +- .../src/client/features/General/slice.js | 8 +- src/fireedge/src/client/features/One/hooks.js | 4 +- src/fireedge/src/client/features/One/slice.js | 6 +- .../client/features/One/vmGroup/actions.js | 29 ++ .../src/client/features/One/vmGroup/hooks.js | 40 +++ .../client/features/One/vmGroup/services.js | 64 ++++ src/fireedge/src/client/hooks/useListForm.js | 179 ++++++----- src/fireedge/src/client/models/Helper.js | 4 +- src/fireedge/src/client/models/Host.js | 20 +- .../src/client/models/VirtualMachine.js | 2 +- .../src/client/providers/notistackProvider.js | 2 +- src/fireedge/src/client/router/index.js | 4 +- src/fireedge/src/client/utils/helpers.js | 11 +- src/fireedge/src/client/utils/schema.js | 280 ++++++++++++++---- .../utils/constants/commands/vmgroup.js | 60 ++-- 111 files changed, 3448 insertions(+), 774 deletions(-) create mode 100644 src/fireedge/etc/sunstone/admin/cluster-tab.yaml create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/ownershipSchema.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vcenterSchema.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vmGroupSchema.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement.js create mode 100644 src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction.js create mode 100644 src/fireedge/src/client/components/Tabs/Cluster/Info/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Cluster/Info/information.js create mode 100644 src/fireedge/src/client/components/Tabs/Cluster/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Host/Info/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Host/Info/information.js rename src/fireedge/src/client/components/{Tables/Hosts/detail.js => Tabs/Host/index.js} (55%) create mode 100644 src/fireedge/src/client/components/Tabs/User/Info/index.js create mode 100644 src/fireedge/src/client/components/Tabs/User/Info/information.js create mode 100644 src/fireedge/src/client/components/Tabs/User/index.js create mode 100644 src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js create mode 100644 src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js create mode 100644 src/fireedge/src/client/components/Tabs/VmTemplate/Template.js create mode 100644 src/fireedge/src/client/components/Tabs/VmTemplate/index.js create mode 100644 src/fireedge/src/client/constants/cluster.js create mode 100644 src/fireedge/src/client/containers/Clusters/Detail.js create mode 100644 src/fireedge/src/client/containers/Groups/Detail.js create mode 100644 src/fireedge/src/client/containers/Hosts/Detail.js create mode 100644 src/fireedge/src/client/containers/Users/Detail.js create mode 100644 src/fireedge/src/client/containers/VirtualMachines/Detail.js create mode 100644 src/fireedge/src/client/features/One/vmGroup/actions.js create mode 100644 src/fireedge/src/client/features/One/vmGroup/hooks.js create mode 100644 src/fireedge/src/client/features/One/vmGroup/services.js diff --git a/install.sh b/install.sh index 3f5fb3f275..d834d05be4 100755 --- a/install.sh +++ b/install.sh @@ -2851,7 +2851,10 @@ FIREEDGE_ETC_FILES="src/fireedge/etc/fireedge-server.conf" FIREEDGE_SUNSTONE_ETC="src/fireedge/etc/sunstone/sunstone-server.conf \ src/fireedge/etc/sunstone/sunstone-views.yaml" -FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml" +FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/cluster-tab.yaml \ + src/fireedge/etc/sunstone/admin/host-tab.yaml \ + src/fireedge/etc/sunstone/admin/vm-tab.yaml \ + src/fireedge/etc/sunstone/admin/vm-template-tab.yaml" FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml" diff --git a/src/fireedge/etc/sunstone/admin/cluster-tab.yaml b/src/fireedge/etc/sunstone/admin/cluster-tab.yaml new file mode 100644 index 0000000000..59de6ed281 --- /dev/null +++ b/src/fireedge/etc/sunstone/admin/cluster-tab.yaml @@ -0,0 +1,55 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +--- +# This file describes the information and actions available in the HOST tab + +# Resource + +resource_name: "CLUSTER" + +# Actions - Which buttons are visible to operate over the resources + +actions: + rename: true + delete: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + attributes_panel: + enabled: true + actions: + add: true + edit: true + delete: true + + host: + enabled: true + vnet: + enabled: true + datastore: + enabled: true diff --git a/src/fireedge/etc/sunstone/admin/host-tab.yaml b/src/fireedge/etc/sunstone/admin/host-tab.yaml index 71279fb6c8..c69f46450b 100644 --- a/src/fireedge/etc/sunstone/admin/host-tab.yaml +++ b/src/fireedge/etc/sunstone/admin/host-tab.yaml @@ -74,34 +74,15 @@ info-tabs: edit: true delete: true - monitoring: - enabled: true - pool: - enabled: true vms: enabled: true + wild: + enabled: true + zombies: + enabled: true esx: enabled: true pci: enabled: true numa: enabled: true - nsx: - enabled: true - -# Dialogs - -dialogs: - create_dialog: - general: true - vcenter: - enabled: true - hypervisor: - - vcenter - storage: - enabled: true - numa: - enabled: true - hypervisors: - - vcenter - - kvm diff --git a/src/fireedge/etc/sunstone/sunstone-views.yaml b/src/fireedge/etc/sunstone/sunstone-views.yaml index 12a719960c..e0de67ec4f 100644 --- a/src/fireedge/etc/sunstone/sunstone-views.yaml +++ b/src/fireedge/etc/sunstone/sunstone-views.yaml @@ -14,7 +14,7 @@ # limitations under the License. # #--------------------------------------------------------------------------- # -# This file describes which Sunstone views are avaiable according to the +# This file describes which Sunstone views are available according to the # primary group a user belongs to groups: oneadmin: diff --git a/src/fireedge/etc/sunstone/user/vm-tab.yaml b/src/fireedge/etc/sunstone/user/vm-tab.yaml index 1c07d11f70..5d48fa2a46 100644 --- a/src/fireedge/etc/sunstone/user/vm-tab.yaml +++ b/src/fireedge/etc/sunstone/user/vm-tab.yaml @@ -159,20 +159,3 @@ info-tabs: enabled: true actions: update_configuration: true - -# Dialogs - -dialogs: - create_dialog: - general: true - vcenter: - enabled: true - hypervisor: - - vcenter - storage: - enabled: true - numa: - enabled: true - hypervisors: - - vcenter - - kvm diff --git a/src/fireedge/package-lock.json b/src/fireedge/package-lock.json index fa551a39c9..edd0717db3 100644 --- a/src/fireedge/package-lock.json +++ b/src/fireedge/package-lock.json @@ -66,6 +66,7 @@ "prop-types": "15.7.2", "qrcode": "1.4.4", "react": "17.0.2", + "react-beautiful-dnd": "13.1.0", "react-dom": "17.0.2", "react-flatpickr": "3.10.7", "react-flow-renderer": "9.6.0", @@ -543,9 +544,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.4.tgz", - "integrity": "sha512-xmzz+7fRpjrvDUj+GV7zfz/R3gSK2cOxGlazaXooxspCr539cbTXJKvBJzSVI2pPhcRGquoOtaIkKCsHQUiO3w==", + "version": "7.15.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.5.tgz", + "integrity": "sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2378,9 +2379,9 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "node_modules/@types/react": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz", - "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==", + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz", + "integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3387,13 +3388,13 @@ } }, "node_modules/browserslist": { - "version": "4.16.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", - "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz", + "integrity": "sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==", "dependencies": { - "caniuse-lite": "^1.0.30001251", + "caniuse-lite": "^1.0.30001254", "colorette": "^1.3.0", - "electron-to-chromium": "^1.3.811", + "electron-to-chromium": "^1.3.830", "escalade": "^3.1.1", "node-releases": "^1.1.75" }, @@ -3528,9 +3529,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001252", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz", - "integrity": "sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==", + "version": "1.0.30001255", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz", + "integrity": "sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/browserslist" @@ -4249,6 +4250,14 @@ "node": "*" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.2.0.tgz", @@ -4444,9 +4453,9 @@ } }, "node_modules/deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/define-properties": { "version": "1.1.3", @@ -4603,9 +4612,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "node_modules/electron-to-chromium": { - "version": "1.3.829", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.829.tgz", - "integrity": "sha512-5EXDbvsaLRxS1UOfRr8Hymp3dR42bvBNPgzVuPwUFj3v66bpvDUcNwwUywQUQYn/scz26/3Sgd3fNVGQOlVwvQ==" + "version": "1.3.830", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.830.tgz", + "integrity": "sha512-gBN7wNAxV5vl1430dG+XRcQhD4pIeYeak6p6rjdCtlz5wWNwDad8jwvphe5oi1chL5MV6RNRikfffBBiFuj+rQ==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -7907,6 +7916,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -9378,6 +9392,11 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9460,6 +9479,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0", + "react-dom": "^16.8.5 || ^17.0.0" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -9798,7 +9835,8 @@ "node_modules/redux-thunk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", - "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==", + "license": "MIT" }, "node_modules/regenerate": { "version": "1.4.2", @@ -11604,6 +11642,14 @@ "node": ">=0.10.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } + }, "node_modules/util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -12615,9 +12661,9 @@ } }, "@babel/parser": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.4.tgz", - "integrity": "sha512-xmzz+7fRpjrvDUj+GV7zfz/R3gSK2cOxGlazaXooxspCr539cbTXJKvBJzSVI2pPhcRGquoOtaIkKCsHQUiO3w==" + "version": "7.15.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.5.tgz", + "integrity": "sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.15.4", @@ -13904,9 +13950,9 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/react": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz", - "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==", + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz", + "integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -14729,13 +14775,13 @@ } }, "browserslist": { - "version": "4.16.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", - "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz", + "integrity": "sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==", "requires": { - "caniuse-lite": "^1.0.30001251", + "caniuse-lite": "^1.0.30001254", "colorette": "^1.3.0", - "electron-to-chromium": "^1.3.811", + "electron-to-chromium": "^1.3.830", "escalade": "^3.1.1", "node-releases": "^1.1.75" } @@ -14839,9 +14885,9 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { - "version": "1.0.30001252", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz", - "integrity": "sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==" + "version": "1.0.30001255", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz", + "integrity": "sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==" }, "chalk": { "version": "2.4.2", @@ -15419,6 +15465,14 @@ "randomfill": "^1.0.3" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.2.0.tgz", @@ -15571,9 +15625,9 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "define-properties": { "version": "1.1.3", @@ -15709,9 +15763,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.829", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.829.tgz", - "integrity": "sha512-5EXDbvsaLRxS1UOfRr8Hymp3dR42bvBNPgzVuPwUFj3v66bpvDUcNwwUywQUQYn/scz26/3Sgd3fNVGQOlVwvQ==" + "version": "1.3.830", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.830.tgz", + "integrity": "sha512-gBN7wNAxV5vl1430dG+XRcQhD4pIeYeak6p6rjdCtlz5wWNwDad8jwvphe5oi1chL5MV6RNRikfffBBiFuj+rQ==" }, "elliptic": { "version": "6.5.4", @@ -18242,6 +18296,11 @@ "fs-monkey": "1.0.3" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -19368,6 +19427,11 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -19436,6 +19500,20 @@ "object-assign": "^4.1.1" } }, + "react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -21139,6 +21217,12 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "optional": true }, + "use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", + "requires": {} + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/src/fireedge/package.json b/src/fireedge/package.json index e069cebed7..a981175301 100644 --- a/src/fireedge/package.json +++ b/src/fireedge/package.json @@ -89,8 +89,8 @@ "fs-extra": "9.0.1", "fuse.js": "6.4.1", "helmet": "4.1.1", - "http-proxy-middleware": "1.0.5", "http": "0.0.1-security", + "http-proxy-middleware": "1.0.5", "https": "1.0.0", "iconoir-react": "1.1.0", "immutable": "4.0.0-rc.12", @@ -108,6 +108,8 @@ "process": "0.11.10", "prop-types": "15.7.2", "qrcode": "1.4.4", + "react": "17.0.2", + "react-beautiful-dnd": "13.1.0", "react-dom": "17.0.2", "react-flatpickr": "3.10.7", "react-flow-renderer": "9.6.0", @@ -116,17 +118,16 @@ "react-minimal-pie-chart": "8.2.0", "react-opennebula-ace": "1.0.1", "react-redux": "7.2.4", - "react-router-dom": "5.2.0", "react-router": "5.2.0", + "react-router-dom": "5.2.0", "react-table": "7.7.0", "react-transition-group": "4.4.1", "react-virtual": "2.7.1", - "react": "17.0.2", - "redux-thunk": "2.3.0", "redux": "4.1.0", + "redux-thunk": "2.3.0", "rimraf": "3.0.2", - "socket.io-client": "4.1.2", "socket.io": "4.1.2", + "socket.io-client": "4.1.2", "speakeasy": "2.0.0", "sprintf-js": "1.1.2", "style-loader": "3.2.1", @@ -136,9 +137,9 @@ "upcast": "4.0.0", "url": "0.11.0", "uuid": "8.3.1", + "webpack": "5.41.0", "webpack-cli": "4.7.2", "webpack-node-externals": "2.5.2", - "webpack": "5.41.0", "window-or-global": "1.0.1", "winston": "3.3.3", "worker-loader": "3.0.8", diff --git a/src/fireedge/src/client/apps/provision/_app.js b/src/fireedge/src/client/apps/provision/_app.js index 356c9cb9fd..6c669a2a17 100644 --- a/src/fireedge/src/client/apps/provision/_app.js +++ b/src/fireedge/src/client/apps/provision/_app.js @@ -42,13 +42,13 @@ const ProvisionApp = () => { const provisionTemplate = useProvisionTemplate() const { getProvisionsTemplates } = useProvisionApi() - const { changeTitle } = useGeneralApi() + const { changeAppTitle } = useGeneralApi() useEffect(() => { (async () => { try { if (jwt) { - changeTitle(APP_NAME) + changeAppTitle(APP_NAME) getAuthUser() !provisionTemplate?.length && await getProvisionsTemplates() } diff --git a/src/fireedge/src/client/apps/sunstone/_app.js b/src/fireedge/src/client/apps/sunstone/_app.js index 669b1d1b03..f870046986 100644 --- a/src/fireedge/src/client/apps/sunstone/_app.js +++ b/src/fireedge/src/client/apps/sunstone/_app.js @@ -39,13 +39,13 @@ const APP_NAME = _APPS.sunstone.name const SunstoneApp = () => { const { isLogged, jwt, firstRender, view, views } = useAuth() const { getAuthUser, logout, getSunstoneViews, getSunstoneConfig } = useAuthApi() - const { changeTitle } = useGeneralApi() + const { changeAppTitle } = useGeneralApi() useEffect(() => { (async () => { try { if (jwt) { - changeTitle(APP_NAME) + changeAppTitle(APP_NAME) getAuthUser() await getSunstoneViews() await getSunstoneConfig() diff --git a/src/fireedge/src/client/apps/sunstone/routes.js b/src/fireedge/src/client/apps/sunstone/routes.js index 19324396e3..26fcbf9b3c 100644 --- a/src/fireedge/src/client/apps/sunstone/routes.js +++ b/src/fireedge/src/client/apps/sunstone/routes.js @@ -77,11 +77,10 @@ export const getEndpointsByView = (views, endpoints = []) => { const [resource, dialogName] = paths - return dialogName + return dialogName && !dialogName.includes(':') // filter params. eg: '/vm/:id' ? bulkActions[`${dialogName}_dialog`] : resource === name.toLowerCase() - } - ) && route + }) && route return endpoints .map(({ routes: subRoutes, ...restOfProps }) => { diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 29d31e1873..a768508220 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -45,6 +45,7 @@ import { import loadable from '@loadable/component' const VirtualMachines = loadable(() => import('client/containers/VirtualMachines'), { ssr: false }) +const VirtualMachineDetail = loadable(() => import('client/containers/VirtualMachines/Detail'), { ssr: false }) const VirtualRouters = loadable(() => import('client/containers/VirtualRouters'), { ssr: false }) const VmTemplates = loadable(() => import('client/containers/VmTemplates'), { ssr: false }) @@ -65,18 +66,23 @@ const VNetworkTemplates = loadable(() => import('client/containers/VNetworkTempl // const SecurityGroups = loadable(() => import('client/containers/SecurityGroups'), { ssr: false }) const Clusters = loadable(() => import('client/containers/Clusters'), { ssr: false }) +const ClusterDetail = loadable(() => import('client/containers/Clusters/Detail'), { ssr: false }) const Hosts = loadable(() => import('client/containers/Hosts'), { ssr: false }) +const HostDetail = loadable(() => import('client/containers/Hosts/Detail'), { ssr: false }) const Zones = loadable(() => import('client/containers/Zones'), { ssr: false }) const Users = loadable(() => import('client/containers/Users'), { ssr: false }) +const UserDetail = loadable(() => import('client/containers/Users/Detail'), { ssr: false }) const Groups = loadable(() => import('client/containers/Groups'), { ssr: false }) +const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), { ssr: false }) // const VDCs = loadable(() => import('client/containers/VDCs'), { ssr: false }) // const ACLs = loadable(() => import('client/containers/ACLs'), { ssr: false }) export const PATH = { INSTANCE: { VMS: { - LIST: '/vm' + LIST: '/vm', + DETAIL: '/vm/:id' }, VROUTERS: { LIST: '/virtual-router' @@ -118,10 +124,12 @@ export const PATH = { }, INFRASTRUCTURE: { CLUSTERS: { - LIST: '/cluster' + LIST: '/cluster', + DETAIL: '/cluster/:id' }, HOSTS: { - LIST: '/host' + LIST: '/host', + DETAIL: '/host/:id' }, ZONES: { LIST: '/zone' @@ -129,15 +137,17 @@ export const PATH = { }, SYSTEM: { USERS: { - LIST: '/user' + LIST: '/user', + DETAIL: '/user/:id' }, GROUPS: { - LIST: '/group' + LIST: '/group', + DETAIL: '/group/:id' } } } -export const ENDPOINTS = [ +const ENDPOINTS = [ { label: 'Instances', sidebar: true, @@ -150,6 +160,11 @@ export const ENDPOINTS = [ icon: VmsIcons, Component: VirtualMachines }, + { + label: params => `VM #${params.id}`, + path: PATH.INSTANCE.VMS.DETAIL, + Component: VirtualMachineDetail + }, { label: 'Virtual Routers', path: PATH.INSTANCE.VROUTERS.LIST, @@ -173,8 +188,6 @@ export const ENDPOINTS = [ }, { label: 'Instantiate VM Template', - sidebar: true, - icon: TemplateIcon, path: PATH.TEMPLATE.VMS.INSTANTIATE, Component: InstantiateVmTemplates } @@ -248,6 +261,11 @@ export const ENDPOINTS = [ icon: ClusterIcon, Component: Clusters }, + { + label: params => `Clusters #${params.id}`, + path: PATH.INFRASTRUCTURE.CLUSTERS.DETAIL, + Component: ClusterDetail + }, { label: 'Hosts', path: PATH.INFRASTRUCTURE.HOSTS.LIST, @@ -255,6 +273,11 @@ export const ENDPOINTS = [ icon: HostIcon, Component: Hosts }, + { + label: params => `Hosts #${params.id}`, + path: PATH.INFRASTRUCTURE.HOSTS.DETAIL, + Component: HostDetail + }, { label: 'Zones', path: PATH.INFRASTRUCTURE.ZONES.LIST, @@ -276,15 +299,27 @@ export const ENDPOINTS = [ icon: UserIcon, Component: Users }, + { + label: params => `User #${params.id}`, + path: PATH.SYSTEM.USERS.DETAIL, + Component: UserDetail + }, { label: 'Groups', path: PATH.SYSTEM.GROUPS.LIST, sidebar: true, icon: GroupIcon, Component: Groups + }, + { + label: params => `Group #${params.id}`, + path: PATH.SYSTEM.GROUPS.DETAIL, + Component: GroupDetail } ] } ] +export { ENDPOINTS } + export default { PATH, ENDPOINTS } diff --git a/src/fireedge/src/client/components/Cards/SelectCard/SelectCard.js b/src/fireedge/src/client/components/Cards/SelectCard/SelectCard.js index 5f5c3a4626..c2cdcea514 100644 --- a/src/fireedge/src/client/components/Cards/SelectCard/SelectCard.js +++ b/src/fireedge/src/client/components/Cards/SelectCard/SelectCard.js @@ -152,7 +152,7 @@ const SelectCard = memo(({ ) }) -SelectCard.propTypes = { +export const SelectCardProps = { stylesProps: PropTypes.object, action: PropTypes.node, actions: PropTypes.arrayOf( @@ -223,6 +223,7 @@ SelectCard.defaultProps = { skeletonHeight: 140 } +SelectCard.propTypes = SelectCardProps SelectCard.displayName = 'SelectCard' export default SelectCard diff --git a/src/fireedge/src/client/components/Cards/SelectCard/index.js b/src/fireedge/src/client/components/Cards/SelectCard/index.js index a25bcb1a04..92df76a285 100644 --- a/src/fireedge/src/client/components/Cards/SelectCard/index.js +++ b/src/fireedge/src/client/components/Cards/SelectCard/index.js @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import SelectCard from 'client/components/Cards/SelectCard/SelectCard' +import SelectCard, { SelectCardProps } from 'client/components/Cards/SelectCard/SelectCard' import Action from 'client/components/Cards/SelectCard/Action' -export { Action } +export { Action, SelectCardProps } export default SelectCard diff --git a/src/fireedge/src/client/components/FormControl/AutocompleteController.js b/src/fireedge/src/client/components/FormControl/AutocompleteController.js index 9383b73d2e..cd76af20e5 100644 --- a/src/fireedge/src/client/components/FormControl/AutocompleteController.js +++ b/src/fireedge/src/client/components/FormControl/AutocompleteController.js @@ -41,9 +41,9 @@ const AutocompleteController = memo( ? newValue?.map(value => typeof value === 'string' ? value : ({ text: value, value }) ) - : newValue.value + : newValue?.value - return onChange(newValueToChange) + return onChange(newValueToChange ?? '') }} options={values} value={selected} diff --git a/src/fireedge/src/client/components/FormControl/FileController.js b/src/fireedge/src/client/components/FormControl/FileController.js index 5a8093bad1..4a4979cf45 100644 --- a/src/fireedge/src/client/components/FormControl/FileController.js +++ b/src/fireedge/src/client/components/FormControl/FileController.js @@ -149,7 +149,6 @@ const FileController = memo( FileController.propTypes = { control: PropTypes.object, cy: PropTypes.string, - multiline: PropTypes.bool, name: PropTypes.string.isRequired, label: PropTypes.string, error: PropTypes.oneOfType([ diff --git a/src/fireedge/src/client/components/FormStepper/index.js b/src/fireedge/src/client/components/FormStepper/index.js index 0799a9d972..63d7c68b1c 100644 --- a/src/fireedge/src/client/components/FormStepper/index.js +++ b/src/fireedge/src/client/components/FormStepper/index.js @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ -import { useState, useMemo, useCallback, useEffect } from 'react' +import { useState, useMemo, useCallback, useEffect, JSXElementConstructor } from 'react' import PropTypes from 'prop-types' import { useFormContext } from 'react-hook-form' @@ -23,10 +22,20 @@ import { useMediaQuery } from '@material-ui/core' import { useGeneral } from 'client/features/General' import CustomMobileStepper from 'client/components/FormStepper/MobileStepper' import CustomStepper from 'client/components/FormStepper/Stepper' -import { groupBy } from 'client/utils' +import { groupBy, Step, ResolverCallback } from 'client/utils' const FIRST_STEP = 0 +/** + * Represents a form with one or more steps. + * Finally, it submit the result. + * + * @param {object} props - Props + * @param {Step[]} props.steps - Steps + * @param {ResolverCallback} props.schema - Function to get form schema + * @param {Function} props.onSubmit - Submit function + * @returns {JSXElementConstructor} Stepper form component + */ const FormStepper = ({ steps, schema, onSubmit }) => { const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')) const { watch, reset, errors, setError } = useFormContext() @@ -43,16 +52,16 @@ const FormStepper = ({ steps, schema, onSubmit }) => { reset({ ...formData }, { errors: false }) }, [formData]) - const validateSchema = step => { - const { id, resolver, optionsValidate } = steps[step] + const validateSchema = async stepIdx => { + const { id, resolver, optionsValidate, ...step } = steps[stepIdx] const stepData = watch(id) const allData = { ...formData, [id]: stepData } const stepSchema = typeof resolver === 'function' ? resolver(allData) : resolver - return stepSchema - .validate(stepData, optionsValidate) - .then(() => ({ id, data: stepData })) + await stepSchema.validate(stepData, optionsValidate) + + return { id, data: stepData, ...step } } const setErrors = ({ inner = [], ...rest }) => { @@ -76,33 +85,36 @@ const FormStepper = ({ steps, schema, onSubmit }) => { isBackAction && handleBack(stepToAdvance) - steps - .slice(FIRST_STEP, stepToAdvance) - .forEach((_, step, stepsToValidate) => { - validateSchema(step) - .then(({ id, data }) => { - activeStep === step && - setFormData(prev => ({ ...prev, [id]: data })) + const stepsForward = steps.slice(FIRST_STEP, stepToAdvance) - step === stepsToValidate.length - 1 && - Number.isInteger(stepToAdvance) && - setActiveStep(stepToAdvance) - }) - .catch(setErrors) - }) + stepsForward.forEach(async (_, stepIdx, stepsToValidate) => { + try { + const { id, data } = await validateSchema(stepIdx) + + activeStep === stepIdx && setFormData(prev => ({ ...prev, [id]: data })) + + stepIdx === stepsToValidate.length - 1 && setActiveStep(stepToAdvance) + } catch (validateError) { + setErrors(validateError) + } + }) } - const handleNext = () => { - validateSchema(activeStep) - .then(({ id, data }) => { - if (activeStep === lastStep) { - onSubmit(schema().cast({ ...formData, [id]: data })) - } else { - setFormData(prev => ({ ...prev, [id]: data })) - setActiveStep(prevActiveStep => prevActiveStep + 1) - } - }) - .catch(setErrors) + const handleNext = async () => { + try { + const { id, data } = await validateSchema(activeStep) + + if (activeStep === lastStep) { + const submitData = schema().cast({ ...formData, [id]: data }) + + onSubmit(submitData) + } else { + setFormData(prev => ({ ...prev, [id]: data })) + setActiveStep(prevActiveStep => prevActiveStep + 1) + } + } catch (validateError) { + setErrors(validateError) + } } const handleBack = useCallback(stepToBack => { @@ -112,9 +124,7 @@ const FormStepper = ({ steps, schema, onSubmit }) => { const stepData = watch(id) setFormData(prev => ({ ...prev, [id]: stepData })) - setActiveStep(prevActiveStep => - Number.isInteger(stepToBack) ? stepToBack : (prevActiveStep - 1) - ) + setActiveStep(prevStep => Number.isInteger(stepToBack) ? stepToBack : (prevStep - 1)) }, [activeStep]) const { id, content: Content } = useMemo(() => steps[activeStep], [ diff --git a/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js b/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js index c8b2673576..112897506a 100644 --- a/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js +++ b/src/fireedge/src/client/components/Forms/ButtonToTriggerForm.js @@ -14,7 +14,7 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useState } from 'react' +import { useState, useMemo } from 'react' import PropTypes from 'prop-types' import { @@ -47,26 +47,22 @@ const ButtonToTriggerForm = ({ const open = Boolean(anchorEl) const { display, show, hide, values: Form } = useDialog() - const { - steps, - defaultValues, - resolver, - fields, - onSubmit: handleSubmit - } = Form ?? {} + const { onSubmit: handleSubmit, form } = Form ?? {} + + const formConfig = useMemo(() => form?.() ?? {}, [form]) + const { steps, defaultValues, resolver, fields, transformBeforeSubmit } = formConfig const handleTriggerSubmit = async formData => { try { - await handleSubmit?.(formData) + const data = transformBeforeSubmit?.(formData) ?? formData + await handleSubmit?.(data) } finally { hide() } } - const openDialogForm = ({ form = {}, ...rest }) => { - const formParams = typeof form === 'function' ? form() : form - - show({ ...formParams, ...rest }) + const openDialogForm = formParams => { + show(formParams) handleClose() } @@ -154,13 +150,10 @@ ButtonToTriggerForm.propTypes = { PropTypes.shape({ cy: PropTypes.string, name: PropTypes.string, - form: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.func - ]) + form: PropTypes.func, + handleSubmit: PropTypes.func }) - ), - handleSubmit: PropTypes.func + ) } ButtonToTriggerForm.displayName = 'ButtonToTriggerForm' diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index 401a1cacd1..9b54464415 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -18,7 +18,7 @@ import { createElement, useMemo } from 'react' import PropTypes from 'prop-types' import { styled, Grid } from '@material-ui/core' -import { useFormContext, useWatch } from 'react-hook-form' +import { useFormContext } from 'react-hook-form' import * as FC from 'client/components/FormControl' import { INPUT_TYPES } from 'client/constants' @@ -45,7 +45,7 @@ const InputController = { } const FormWithSchema = ({ id, cy, fields, className, legend }) => { - const { control, errors, ...formContext } = useFormContext() + const { control, errors, watch, ...formContext } = useFormContext() const getFields = useMemo(() => typeof fields === 'function' ? fields() : fields, []) return ( @@ -60,7 +60,7 @@ const FormWithSchema = ({ id, cy, fields, className, legend }) => { ? Array.isArray(dependOf) ? dependOf.map(d => `${id}.${d}`) : `${id}.${dependOf}` : dependOf - valueOfDependField = useWatch({ control, name: nameOfDependField }) + valueOfDependField = watch(nameOfDependField) } const { name, type, htmlType, grid, ...fieldProps } = Object diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js index 0c5a992590..ad9a6ae2cd 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/ImageSteps/index.js @@ -13,26 +13,10 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ -import * as yup from 'yup' - import ImagesTable from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/ImagesTable' import AdvancedOptions from 'client/components/Forms/Vm/AttachDiskForm/ImageSteps/AdvancedOptions' +import { createSteps } from 'client/utils' -const Steps = stepProps => { - const image = ImagesTable(stepProps) - const advanced = AdvancedOptions(stepProps) - - const steps = [image, advanced] - - const resolver = () => yup.object({ - [image.id]: image.resolver(), - [advanced.id]: advanced.resolver() - }) - - const defaultValues = resolver().default() - - return { steps, defaultValues, resolver } -} +const Steps = createSteps([ImagesTable, AdvancedOptions]) export default Steps diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js index b97f726625..c8df7059ba 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachDiskForm/VolatileSteps/index.js @@ -13,26 +13,10 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ -import * as yup from 'yup' - import BasicConfiguration from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/BasicConfiguration' import AdvancedOptions from 'client/components/Forms/Vm/AttachDiskForm/VolatileSteps/AdvancedOptions' +import { createSteps } from 'client/utils' -const Steps = stepProps => { - const configuration = BasicConfiguration(stepProps) - const advancedOptions = AdvancedOptions(stepProps) - - const steps = [configuration, advancedOptions] - - const resolver = () => yup.object({ - [configuration.id]: configuration.resolver(), - [advancedOptions.id]: advancedOptions.resolver() - }) - - const defaultValues = resolver().default() - - return { steps, defaultValues, resolver } -} +const Steps = createSteps([BasicConfiguration, AdvancedOptions]) export default Steps diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/index.js index d7f0582ca8..5e9e05d339 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/index.js @@ -14,33 +14,35 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' import FormWithSchema from 'client/components/Forms/FormWithSchema' -import { - SCHEMA, - FIELDS -} from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema' +import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema' import { T } from 'client/constants' export const STEP_ID = 'advanced' -const AdvancedOptions = ({ nics = [] } = {}) => ({ +const Content = props => ( + +) + +const AdvancedOptions = props => ({ id: STEP_ID, label: T.AdvancedOptions, - resolver: () => SCHEMA(nics), + resolver: SCHEMA, optionsValidate: { abortEarly: false }, - content: useCallback( - () => ( - - ), - [nics?.length, nics?.[0]?.ID] - ) + content: () => Content(props) }) +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func, + nics: PropTypes.array +} + export default AdvancedOptions diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema.js index fcb04d2afc..c08dcf920c 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema.js @@ -19,7 +19,7 @@ import * as yup from 'yup' import { getValidationFromFields } from 'client/utils' import { INPUT_TYPES } from 'client/constants' -const RDP = { +const RDP_FIELD = { name: 'RDP', label: 'RDP connection', type: INPUT_TYPES.CHECKBOX, @@ -34,14 +34,26 @@ const RDP = { grid: { md: 12 } } -const ALIAS = nics => ({ +const ALIAS_FIELD = ({ nics = [] } = {}) => ({ name: 'PARENT', label: 'Attach as an alias', - type: INPUT_TYPES.SELECT, - values: [{ text: '', value: '' }] - .concat(nics?.map?.(({ NAME, IP = '', NETWORK = '', NIC_ID = '' } = {}) => - ({ text: `${NIC_ID} - ${NETWORK} ${IP}`, value: NAME }) - )), + dependOf: 'NAME', + type: name => { + const hasAlias = nics?.some(nic => nic.PARENT === name) + + return name && hasAlias ? INPUT_TYPES.HIDDEN : INPUT_TYPES.SELECT + }, + values: name => [ + { text: '', value: '' }, + ...nics + .filter(({ PARENT }) => !PARENT) // filter nic alias + .filter(({ NAME }) => NAME !== name || !name) // filter it self + .map(nic => { + const { NAME, IP = '', NETWORK = '', NIC_ID = '' } = nic + + return { text: `${NAME ?? NIC_ID} - ${NETWORK} ${IP}`, value: NAME } + }) + ], validation: yup .string() .trim() @@ -49,12 +61,12 @@ const ALIAS = nics => ({ .default(undefined) }) -const EXTERNAL = { +const EXTERNAL_FIELD = { name: 'EXTERNAL', label: 'External', type: INPUT_TYPES.CHECKBOX, tooltip: 'The NIC will be attached as an external alias of the VM', - dependOf: ALIAS.name, + dependOf: ALIAS_FIELD().name, htmlType: type => !type?.length ? INPUT_TYPES.HIDDEN : undefined, validation: yup .boolean() @@ -63,15 +75,13 @@ const EXTERNAL = { return String(value).toUpperCase() === 'YES' }) - .default(false), - grid: { md: 12 } + .default(false) } -export const FIELDS = nics => [ - RDP, - ALIAS(nics), - EXTERNAL +export const FIELDS = props => [ + RDP_FIELD, + ALIAS_FIELD(props), + EXTERNAL_FIELD ] -export const SCHEMA = nics => - yup.object(getValidationFromFields(FIELDS(nics))) +export const SCHEMA = yup.object(getValidationFromFields(FIELDS())) diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/index.js index c6dee13e09..364c0b02ce 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/index.js @@ -14,56 +14,52 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' import { useListForm } from 'client/hooks' import { VNetworksTable } from 'client/components/Tables' -import { - SCHEMA -} from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema' +import { SCHEMA } from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable/schema' import { T } from 'client/constants' export const STEP_ID = 'network' +const Content = ({ data, setFormData }) => { + const { NAME } = data?.[0] ?? {} + + const { + handleSelect, + handleClear + } = useListForm({ key: STEP_ID, setList: setFormData }) + + const handleSelectedRows = rows => { + const { original = {} } = rows?.[0] ?? {} + + original.ID !== undefined ? handleSelect(original) : handleClear() + } + + return ( + String(row.NAME)} + initialState={{ selectedRowIds: { [NAME]: true } }} + onSelectedRowsChange={handleSelectedRows} + /> + ) +} + const NetworkStep = () => ({ id: STEP_ID, label: T.Network, - resolver: () => SCHEMA, - content: useCallback( - ({ data, setFormData }) => { - const selectedNetwork = data?.[0] - - const { - handleSelect, - handleClear - } = useListForm({ key: STEP_ID, setList: setFormData }) - - const handleSelectedRows = rows => { - const { original } = rows?.[0] ?? {} - const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = original ?? {} - - const network = { - NETWORK_ID: ID, - NETWORK: NAME, - NETWORK_UID: UID, - NETWORK_UNAME: UNAME, - SECURITY_GROUPS - } - - ID !== undefined ? handleSelect(network) : handleClear() - } - - return ( - - ) - }, []) + resolver: SCHEMA, + content: Content }) +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + export default NetworkStep diff --git a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js index a1b000270b..3a3e6f915c 100644 --- a/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/AttachNicForm/Steps/index.js @@ -13,26 +13,40 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ -import * as yup from 'yup' +import NetworksTable, { STEP_ID as NETWORK_STEP } from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable' +import AdvancedOptions, { STEP_ID as ADVANCED_STEP } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions' +import { mapUserInputs, createSteps } from 'client/utils' -import NetworksTable from 'client/components/Forms/Vm/AttachNicForm/Steps/NetworksTable' -import AdvancedOptions from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions' +const Steps = createSteps( + [NetworksTable, AdvancedOptions], + { + transformBeforeSubmit: formData => { + const { [NETWORK_STEP]: network, [ADVANCED_STEP]: advanced } = formData + const { ID, NAME, UID, UNAME, SECURITY_GROUPS } = network?.[0] -const Steps = stepProps => { - const network = NetworksTable(stepProps) - const advanced = AdvancedOptions(stepProps) + return { + NETWORK_ID: ID, + NETWORK: NAME, + NETWORK_UID: UID, + NETWORK_UNAME: UNAME, + SECURITY_GROUPS, + ...mapUserInputs(advanced) + } + }, + transformInitialValue: initialValue => { + const { NETWORK_ID, NETWORK, NETWORK_UID, NETWORK_UNAME, ...rest } = initialValue ?? {} - const steps = [network, advanced] - - const resolver = () => yup.object({ - [network.id]: network.resolver(), - [advanced.id]: advanced.resolver() - }) - - const defaultValues = resolver().default() - - return { steps, defaultValues, resolver } -} + return { + [NETWORK_STEP]: [{ + ID: NETWORK_ID, + NAME: NETWORK, + UID: NETWORK_UID, + UNAME: NETWORK_UNAME + }], + [ADVANCED_STEP]: rest + } + } + } +) export default Steps diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateDiskSnapshotForm/index.js b/src/fireedge/src/client/components/Forms/Vm/CreateDiskSnapshotForm/index.js index ab42d446d4..594bc34476 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateDiskSnapshotForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateDiskSnapshotForm/index.js @@ -13,15 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/CreateDiskSnapshotForm/schema' -const CreateDiskSnapshotForm = ({ snapshot } = {}) => { - return { - resolver: () => SCHEMA, - defaultValues: SCHEMA.cast(snapshot, { stripUnknown: true }), - fields: FIELDS - } -} +const CreateDiskSnapshotForm = createForm(SCHEMA, FIELDS) export default CreateDiskSnapshotForm diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/index.js b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/index.js index e3b158a46a..613e654a38 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/index.js @@ -13,15 +13,31 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { isoDateToMilliseconds } from 'client/models/Helper' +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema' -const PunctualForm = ({ vm, schedule } = {}) => ({ - resolver: () => SCHEMA, - defaultValues: schedule - ? SCHEMA.cast(schedule, { stripUnknown: true }) - : SCHEMA.default(), - fields: () => FIELDS(vm) +const PunctualForm = createForm(SCHEMA, FIELDS, { + transformBeforeSubmit: formData => { + const { ARGS, TIME: time, END_VALUE, END_TYPE, PERIODIC: _, ...restOfData } = formData + const argValues = Object.values(ARGS) + + const newSchedAction = { + TIME: isoDateToMilliseconds(time), + END_TYPE, + ...restOfData + } + + argValues.length && (newSchedAction.ARGS = argValues.join(',')) + + if (END_VALUE) { + newSchedAction.END_VALUE = END_TYPE === '1' + ? END_VALUE + : isoDateToMilliseconds(END_VALUE) + } + + return newSchedAction + } }) export default PunctualForm diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema.js index 2d20d90b08..356a600151 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema.js @@ -89,7 +89,7 @@ const TIME_FIELD = { const REPEAT_FIELD = { name: 'REPEAT', - label: 'Periodicity', + label: 'Granularity of the action', type: INPUT_TYPES.SELECT, dependOf: PERIODIC_FIELD.name, htmlType: isPeriodic => !isPeriodic ? INPUT_TYPES.HIDDEN : undefined, diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/index.js b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/index.js index e88002d26e..f5e8e54359 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/index.js @@ -13,15 +13,20 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/schema' -const RelativeForm = ({ vm, schedule } = {}) => ({ - resolver: () => SCHEMA, - defaultValues: schedule - ? SCHEMA.cast(schedule, { stripUnknown: true }) - : SCHEMA.default(), - fields: () => FIELDS(vm) +const RelativeForm = createForm(SCHEMA, FIELDS, { + transformBeforeSubmit: formData => { + const { ARGS, TIME: time, PERIOD: _, ...restOfData } = formData + const argValues = Object.values(ARGS) + + const newSchedAction = { TIME: `+${time}`, ...restOfData } + + argValues.length && (newSchedAction.ARGS = argValues.join(',')) + + return newSchedAction + } }) export default RelativeForm diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/index.js b/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/index.js index 6e2d06dbaa..4ce5a1e811 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSnapshotForm/index.js @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/CreateSnapshotForm/schema' -const CreateSnapshotForm = () => ({ - resolver: () => SCHEMA, - defaultValues: SCHEMA.default(), - fields: FIELDS -}) +const CreateSnapshotForm = createForm(SCHEMA, FIELDS) export default CreateSnapshotForm diff --git a/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/index.js b/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/index.js index ec69d2bcfe..6ae3292fb3 100644 --- a/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/ResizeCapacityForm/index.js @@ -13,17 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/ResizeCapacityForm/schema' -const ResizeCapacityForm = ({ vm } = {}) => { - const { TEMPLATE = {} } = vm ?? {} - - return { - resolver: () => SCHEMA, - defaultValues: SCHEMA.cast(TEMPLATE, { stripUnknown: true }), - fields: FIELDS - } -} +const ResizeCapacityForm = createForm(SCHEMA, FIELDS) export default ResizeCapacityForm diff --git a/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/index.js b/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/index.js index 9d75e35c90..162862c810 100644 --- a/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/ResizeDiskForm/index.js @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/ResizeDiskForm/schema' -const ResizeDiskForm = ({ disk } = {}) => ({ - resolver: () => SCHEMA, - defaultValues: SCHEMA.cast(disk, { stripUnknown: true }), - fields: FIELDS -}) +const ResizeDiskForm = createForm(SCHEMA, FIELDS) export default ResizeDiskForm diff --git a/src/fireedge/src/client/components/Forms/Vm/SaveAsDiskForm/index.js b/src/fireedge/src/client/components/Forms/Vm/SaveAsDiskForm/index.js index a0541a1cad..fb2c374448 100644 --- a/src/fireedge/src/client/components/Forms/Vm/SaveAsDiskForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/SaveAsDiskForm/index.js @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ +import { createForm } from 'client/utils' import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/SaveAsDiskForm/schema' -const SaveAsDiskForm = () => ({ - resolver: () => SCHEMA, - defaultValues: SCHEMA.default(), - fields: FIELDS -}) +const SaveAsDiskForm = createForm(SCHEMA, FIELDS) export default SaveAsDiskForm diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/diskSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/diskSchema.js index 4b984bdc90..b347c42264 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/diskSchema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/diskSchema.js @@ -23,7 +23,12 @@ import { getState } from 'client/models/Image' import { stringToBoolean } from 'client/models/Helper' import { T, INPUT_TYPES } from 'client/constants' -const SIZE = ({ +export const PARENT = 'DISK' + +const addParentToField = ({ name, ...field }, idx) => + ({ ...field, name: [`${PARENT}[${idx}]`, name].join('.') }) + +const SIZE_FIELD = ({ DISK_ID, IMAGE, IMAGE_ID, @@ -36,7 +41,7 @@ const SIZE = ({ const state = !isVolatile && getState({ STATE: IMAGE_STATE }) return { - name: `DISK[${DISK_ID}].SIZE`, + name: 'SIZE', label: isVolatile ? ( <> {`DISK ${DISK_ID}: `} @@ -63,22 +68,24 @@ const SIZE = ({ .typeError('Disk must be a number') .required('Disk size field is required') .default(() => +SIZE), - grid: { md: 12 } + grid: { md: 12 }, + fieldProps: { disabled: isPersistent } } } export const FIELDS = vmTemplate => { - const { TEMPLATE: { DISK } = {} } = vmTemplate ?? {} - const disks = [DISK].flat().filter(Boolean) + const disks = [vmTemplate?.TEMPLATE?.DISK ?? []].flat() - return disks?.map(SIZE) + return disks?.map(SIZE_FIELD).map(addParentToField) } export const SCHEMA = yup .object({ - DISK: yup.array(yup.object({ SIZE: SIZE().validation })) + [PARENT]: yup.array(yup.object({ + [SIZE_FIELD().name]: SIZE_FIELD().validation + })) }) - .transform(({ DISK, ...rest }) => ({ + .transform(({ [PARENT]: disks, ...rest }) => ({ ...rest, - DISK: [DISK].flat().filter(Boolean) + [PARENT]: [disks].flat().filter(Boolean) })) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js index db198126c0..6f9648bab1 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/index.js @@ -53,6 +53,24 @@ const Content = () => { legend={Tr(T.Disks)} id={STEP_ID} /> + + + ) } diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/ownershipSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/ownershipSchema.js new file mode 100644 index 0000000000..79f049b08b --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/ownershipSchema.js @@ -0,0 +1,61 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { object, string } from 'yup' + +import { useGroup, useUser } from 'client/features/One' +import { getValidationFromFields } from 'client/utils' +import { INPUT_TYPES } from 'client/constants' + +export const UID_FIELD = { + name: 'AS_UID', + label: 'Instantiate as different User', + type: INPUT_TYPES.AUTOCOMPLETE, + values: () => { + const users = useUser() + + return users + .map(({ ID: value, NAME: text }) => ({ text, value })) + .sort((a, b) => a.value - b.value) + }, + validation: string() + .trim() + .notRequired() + .default(undefined), + grid: { md: 12 } +} + +export const GID_FIELD = { + name: 'AS_GID', + label: 'Instantiate as different Group', + type: INPUT_TYPES.AUTOCOMPLETE, + values: () => { + const groups = useGroup() + + return groups + .map(({ ID: value, NAME: text }) => ({ text, value })) + .sort((a, b) => a.value - b.value) + }, + validation: string() + .trim() + .notRequired() + .default(undefined), + grid: { md: 12 } +} + +export const FIELDS = [UID_FIELD, GID_FIELD] + +export const SCHEMA = object(getValidationFromFields(FIELDS)) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js index 7ee7a9ee50..ac5a26850f 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema.js @@ -14,19 +14,28 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import * as yup from 'yup' +import { object } from 'yup' import { FIELDS as INFORMATION_FIELDS, SCHEMA as INFORMATION_SCHEMA } from './informationSchema' import { FIELDS as CAPACITY_FIELDS, SCHEMA as CAPACITY_SCHEMA } from './capacitySchema' import { FIELDS as DISK_FIELDS, SCHEMA as DISK_SCHEMA } from './diskSchema' +import { FIELDS as VM_GROUP_FIELDS, SCHEMA as VM_GROUP_SCHEMA } from './vmGroupSchema' +import { FIELDS as OWNERSHIP_FIELDS, SCHEMA as OWNERSHIP_SCHEMA } from './ownershipSchema' +import { FIELDS as VCENTER_FIELDS, SCHEMA as VCENTER_SCHEMA } from './vcenterSchema' export const FIELDS = { INFORMATION: INFORMATION_FIELDS, CAPACITY: CAPACITY_FIELDS, - DISK: vmTemplate => DISK_FIELDS(vmTemplate) + DISK: vmTemplate => DISK_FIELDS(vmTemplate), + OWNERSHIP: OWNERSHIP_FIELDS, + VM_GROUP: VM_GROUP_FIELDS, + VCENTER: VCENTER_FIELDS } -export const SCHEMA = yup.object() +export const SCHEMA = object() .concat(INFORMATION_SCHEMA) .concat(CAPACITY_SCHEMA) .concat(DISK_SCHEMA) + .concat(OWNERSHIP_SCHEMA) + .concat(VM_GROUP_SCHEMA) + .concat(VCENTER_SCHEMA) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vcenterSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vcenterSchema.js new file mode 100644 index 0000000000..b2c6e47309 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vcenterSchema.js @@ -0,0 +1,41 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { object, string } from 'yup' + +import { getValidationFromFields } from 'client/utils' +import { INPUT_TYPES } from 'client/constants' + +const VCENTER_FOLDER_FIELD = { + name: 'VCENTER_VM_FOLDER', + label: 'vCenter VM Folder', + tooltip: ` + If specified, the the VMs and Template folder path where + the VM will be created inside the data center. + The path is delimited by slashes (e.g /Management/VMs). + If no path is set the VM will be placed in the same folder where the template is located. + `, + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .notRequired() + .default(undefined), + grid: { md: 12 } +} + +export const FIELDS = [VCENTER_FOLDER_FIELD] + +export const SCHEMA = object(getValidationFromFields(FIELDS)) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vmGroupSchema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vmGroupSchema.js new file mode 100644 index 0000000000..a47984cfee --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/vmGroupSchema.js @@ -0,0 +1,80 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { object, string } from 'yup' + +import { useVmGroup } from 'client/features/One' +import { INPUT_TYPES } from 'client/constants' + +const PARENT = 'VMGROUP' + +export const VM_GROUP_FIELD = { + name: `${PARENT}.VMGROUP_ID`, + label: 'Associate VM to a VM Group', + type: INPUT_TYPES.AUTOCOMPLETE, + values: () => { + const vmGroups = useVmGroup() + + return vmGroups + ?.map(({ ID, NAME }) => ({ text: `#${ID} ${NAME}`, value: ID })) + ?.sort((a, b) => { + const compareOptions = { numeric: true, ignorePunctuation: true } + + return a.value.localeCompare(b.value, undefined, compareOptions) + }) + }, + grid: { md: 12 } +} + +export const ROLE_FIELD = { + name: `${PARENT}.ROLE`, + label: 'Role', + type: INPUT_TYPES.AUTOCOMPLETE, + dependOf: VM_GROUP_FIELD.name, + htmlType: vmGroup => vmGroup && vmGroup !== '' ? undefined : INPUT_TYPES.HIDDEN, + values: vmGroupSelected => { + const vmGroups = useVmGroup() + + const roles = vmGroups + ?.filter(({ ID }) => ID === vmGroupSelected) + ?.map(({ ROLES }) => + [ROLES?.ROLE ?? []].flat().map(({ NAME: ROLE_NAME }) => ROLE_NAME) + ) + ?.flat() + + return roles.map(role => ({ text: role, value: role })) + }, + grid: { md: 12 } +} + +export const FIELDS = [VM_GROUP_FIELD, ROLE_FIELD] + +export const SCHEMA = object({ + [PARENT]: object({ + VMGROUP_ID: string() + .trim() + .notRequired() + .default(undefined), + ROLE: string() + .trim() + .default(undefined) + .when( + 'VMGROUP_ID', + (vmGroup, schema) => + vmGroup && vmGroup !== '' ? schema.required('Role field is required') : schema + ) + }) +}) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js new file mode 100644 index 0000000000..3d3ce5a423 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting.js @@ -0,0 +1,196 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { useMemo } from 'react' +import PropTypes from 'prop-types' + +import { + NetworkAlt as NetworkIcon, + BoxIso as ImageIcon, + Check as CheckIcon, + Square as BlankSquareIcon +} from 'iconoir-react' +import { Divider, makeStyles } from '@material-ui/core' +import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd' +import { useFormContext } from 'react-hook-form' + +import { Tr } from 'client/components/HOC' +import { Action } from 'client/components/Cards/SelectCard' +import { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable' +import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking' +import { set } from 'client/utils' +import { T } from 'client/constants' + +const useStyles = makeStyles(theme => ({ + container: { + margin: '1em' + }, + list: { + padding: '1em' + }, + item: { + border: `1px solid ${theme.palette.divider}`, + borderRadius: '0.5em', + padding: '1em', + marginBottom: '1em', + display: 'flex', + alignItems: 'center', + gap: '0.5em', + backgroundColor: theme.palette.background.default + } +})) + +const Booting = ({ data, setFormData }) => { + const classes = useStyles() + const { watch } = useFormContext() + const bootOrder = data?.OS?.BOOT?.split(',') + + const disks = useMemo(() => { + const templateSeleted = watch(`${TEMPLATE_ID}[0]`) + const listOfDisks = [templateSeleted?.TEMPLATE?.DISK ?? []].flat() + + return listOfDisks?.map(disk => { + const { DISK_ID, IMAGE, IMAGE_ID } = disk + const isVolatile = !IMAGE && !IMAGE_ID + + const name = isVolatile + ? `DISK ${DISK_ID}: ${Tr(T.VolatileDisk)}` + : `DISK ${DISK_ID}: ${IMAGE}` + + return { + ID: `disk${DISK_ID}`, + NAME: ( + <> + + {name} + + ) + } + }) + }, []) + + const nics = data?.[NIC_ID] + ?.map((nic, idx) => ({ + ID: `nic${idx}`, + NAME: ( + <> + + {`NIC ${idx}: ${nic.NETWORK}`} + + ) + })) + + const enabledItems = [...disks, ...nics] + .filter(item => bootOrder.includes(item.ID)) + .sort((a, b) => bootOrder.indexOf(a.ID) - bootOrder.indexOf(b.ID)) + + const restOfItems = [...disks, ...nics] + .filter(item => !bootOrder.includes(item.ID)) + + /** @param {string[]} newBootOrder - New boot order */ + const reorder = newBootOrder => { + setFormData(prev => { + const newData = set({ ...prev }, 'extra.OS.BOOT', newBootOrder.join(',')) + + return { ...prev, extra: { ...prev.extra, OS: newData } } + }) + } + + /** @param {DropResult} result - Drop result */ + const onDragEnd = result => { + const { destination, source, draggableId } = result + const newBootOrder = [...bootOrder] + + if ( + destination && + destination.index !== source.index && + newBootOrder.includes(draggableId) + ) { + newBootOrder.splice(source.index, 1) // remove current position + newBootOrder.splice(destination.index, 0, draggableId) // set in new position + + reorder(newBootOrder) + } + } + + const handleEnable = itemId => { + const newBootOrder = [...bootOrder] + const itemIndex = bootOrder.indexOf(itemId) + + itemIndex >= 0 + ? newBootOrder.splice(itemIndex, 1) + : newBootOrder.push(itemId) + + reorder(newBootOrder) + } + + return ( + +
+ + {({ droppableProps, innerRef, placeholder }) => ( +
+ {enabledItems.map(({ ID, NAME }, idx) => ( + + {({ draggableProps, dragHandleProps, innerRef }) => ( +
+ } + handleClick={() => handleEnable(ID)} + /> + {NAME} +
+ )} +
+ ))} + {placeholder} +
+ )} +
+ + {restOfItems.map(({ ID, NAME }) => ( +
+ } + handleClick={() => handleEnable(ID)} + /> + {NAME} +
+ ))} +
+
+ ) +} + +Booting.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + +Booting.displayName = 'Booting' + +export default Booting diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js index 3223c18872..dcd13b88cd 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/index.js @@ -14,16 +14,53 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useCallback } from 'react' +import PropTypes from 'prop-types' +import { useFormContext } from 'react-hook-form' +import { useTheme } from '@material-ui/core' +import { WarningCircledOutline as WarningIcon } from 'iconoir-react' + +import Tabs from 'client/components/Tabs' import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import Networking from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking' +import Placement from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement' +import ScheduleAction from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction' +import Booting from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/booting' import { T } from 'client/constants' export const STEP_ID = 'extra' -const Content = () => { +const Content = ({ data, setFormData }) => { + const theme = useTheme() + const { errors } = useFormContext() + + const tabs = [ + { + name: 'network', + renderContent: Networking({ data, setFormData }) + }, + { + name: 'placement', + renderContent: Placement({ data, setFormData }) + }, + { + name: 'schedule action', + renderContent: ScheduleAction({ data, setFormData }) + }, + { + name: 'os booting', + renderContent: Booting({ data, setFormData }) + } + ] + .map((tab, idx) => ({ + ...tab, + icon: errors[STEP_ID]?.[idx] && ( + + ) + })) + return ( -
TODO: Tabs with extra configuration
+ ) } @@ -32,7 +69,12 @@ const ExtraConfiguration = () => ({ label: T.AdvancedOptions, resolver: SCHEMA, optionsValidate: { abortEarly: false }, - content: useCallback(Content, []) + content: Content }) +Content.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + export default ExtraConfiguration diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js new file mode 100644 index 0000000000..fa115efd61 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking.js @@ -0,0 +1,130 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' +import { makeStyles } from '@material-ui/core' +import { Edit, Trash } from 'iconoir-react' + +import { useListForm } from 'client/hooks' +import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' +import SelectCard, { Action } from 'client/components/Cards/SelectCard' +import { AttachNicForm } from 'client/components/Forms/Vm' +import { Tr, Translate } from 'client/components/HOC' + +import { STEP_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { NIC_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { T } from 'client/constants' + +const useStyles = makeStyles({ + root: { + paddingBlock: '1em', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))', + gap: '1em' + } +}) + +export const TAB_ID = 'NIC' + +const Networking = ({ data, setFormData }) => { + const classes = useStyles() + const nics = data?.[TAB_ID] + ?.map((nic, idx) => ({ ...nic, NAME: `NIC${idx}` })) + + const { handleRemove, handleSave } = useListForm({ + parent: STEP_ID, + key: TAB_ID, + list: nics, + setList: setFormData, + getItemId: (item) => item.NAME, + addItemId: (item, id) => ({ ...item, NAME: id }) + }) + + return ( + <> + AttachNicForm({ nics }), + onSubmit: formData => + handleSave(NIC_SCHEMA.cast(formData)) + }]} + /> +
+ {nics?.map(item => { + const { NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item + const hasAlias = nics?.some(nic => nic.PARENT === NAME) + + return ( + + {Object + .entries({ RDP, SSH, ALIAS: PARENT, EXTERNAL }) + .map(([k, v]) => v ? `${k}` : '') + .filter(Boolean) + .join(' | ') + } + } + action={ + <> + {!hasAlias && + handleRemove(NAME)} + icon={} + /> + } + , + tooltip: + }} + dialogProps={{ + title: <>{`: ${NAME} - ${NETWORK}`} + }} + options={[{ + form: () => AttachNicForm({ nics }, item), + onSubmit: newValues => handleSave(newValues, NAME) + }]} + /> + + } + /> + ) + })} +
+ + ) +} + +Networking.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + +Networking.displayName = 'Networking' + +export default Networking diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement.js new file mode 100644 index 0000000000..91a61fd3a2 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/placement.js @@ -0,0 +1,77 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' +import { makeStyles } from '@material-ui/core' + +import FormWithSchema from 'client/components/Forms/FormWithSchema' +import { Tr } from 'client/components/HOC' + +import { STEP_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { + HOST_REQ_FIELD, + HOST_RANK_FIELD, + DS_REQ_FIELD, + DS_RANK_FIELD +} from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { T } from 'client/constants' + +const useStyles = makeStyles({ + root: { + paddingBlock: '1em', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))', + gap: '1em' + } +}) + +const Placement = () => { + const classes = useStyles() + + // TODO - Host requirements: add button to select HOST in list => ID="" + // TODO - Host policy options: Packing|Stripping|Load-aware + + // TODO - DS requirements: add button to select DATASTORE in list => ID="" + // TODO - DS policy options: Packing|Stripping + + return ( + <> + + + + ) +} + +Placement.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + +Placement.displayName = 'Placement' + +export default Placement diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction.js new file mode 100644 index 0000000000..83f15e6d05 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/scheduleAction.js @@ -0,0 +1,130 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' +import { makeStyles } from '@material-ui/core' +import { Edit, Trash } from 'iconoir-react' + +import { useListForm } from 'client/hooks' +import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' +import SelectCard, { Action } from 'client/components/Cards/SelectCard' +import { PunctualForm, RelativeForm } from 'client/components/Forms/Vm' +import { Tr, Translate } from 'client/components/HOC' + +import { STEP_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { SCHED_ACTION_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' +import { T } from 'client/constants' + +const useStyles = makeStyles({ + root: { + paddingBlock: '1em', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(300px, auto))', + gap: '1em' + } +}) + +export const TAB_ID = 'SCHED_ACTION' + +const ScheduleAction = ({ data, setFormData }) => { + const classes = useStyles() + const scheduleActions = data?.[TAB_ID] + ?.map((nic, idx) => ({ ...nic, NAME: `ACTION${idx}` })) + + const { handleRemove, handleSave } = useListForm({ + parent: STEP_ID, + key: TAB_ID, + list: scheduleActions, + setList: setFormData, + addItemId: (item, id) => ({ ...item, ID: id }) + }) + + return ( + <> + PunctualForm(), + onSubmit: formData => + handleSave(SCHED_ACTION_SCHEMA.cast(formData)) + }, + { + cy: 'add-sched-action-relative', + name: 'Relative action', + form: () => RelativeForm(), + onSubmit: formData => + handleSave(SCHED_ACTION_SCHEMA.cast(formData)) + }]} + /> +
+ {scheduleActions?.map(item => { + const { ID, NAME, ACTION, TIME } = item + const isRelative = String(TIME).includes('+') + + return ( + + handleRemove(ID)} + icon={} + /> + , + tooltip: + }} + dialogProps={{ + title: <>{`: ${NAME}`} + }} + options={[{ + form: () => isRelative + ? RelativeForm(undefined, item) + : PunctualForm(undefined, item), + onSubmit: newValues => handleSave(newValues, ID) + }]} + /> + + } + /> + ) + })} +
+ + ) +} + +ScheduleAction.propTypes = { + data: PropTypes.any, + setFormData: PropTypes.func +} + +ScheduleAction.displayName = 'ScheduleAction' + +export default ScheduleAction diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js index 7ebd5f14e3..b4bcf86604 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema.js @@ -14,10 +14,87 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import * as yup from 'yup' +import { array, object, string, lazy } from 'yup' +import { v4 as uuidv4 } from 'uuid' -// import { getValidationFromFields } from 'client/utils' +import { SCHEMA as NETWORK_SCHEMA } from 'client/components/Forms/Vm/AttachNicForm/Steps/AdvancedOptions/schema' +import { SCHEMA as PUNCTUAL_SCHEMA } from 'client/components/Forms/Vm/CreateSchedActionForm/PunctualForm/schema' +import { SCHEMA as RELATIVE_SCHEMA } from 'client/components/Forms/Vm/CreateSchedActionForm/RelativeForm/schema' +import { INPUT_TYPES } from 'client/constants' +import { getValidationFromFields } from 'client/utils' -export const FIELDS = {} +const ID_SCHEMA = string().uuid().required().default(uuidv4) -export const SCHEMA = yup.object() +export const HOST_REQ_FIELD = { + name: 'SCHED_REQUIREMENTS', + label: 'Host requirements expression', + tooltip: ` + Boolean expression that rules out provisioning hosts + from list of machines suitable to run this VM`, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired() +} + +export const HOST_RANK_FIELD = { + name: 'SCHED_RANK', + label: 'Host policy expression', + tooltip: ` + This field sets which attribute will be used + to sort the suitable hosts for this VM`, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired() +} + +export const DS_REQ_FIELD = { + name: 'DS_SCHED_REQUIREMENTS', + label: 'Datastore requirements expression', + tooltip: ` + Boolean expression that rules out entries from + the pool of datastores suitable to run this VM.`, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired() +} + +export const DS_RANK_FIELD = { + name: 'DS_SCHED_RANK', + label: 'Datastore policy expression', + tooltip: ` + This field sets which attribute will be used to + sort the suitable datastores for this VM`, + type: INPUT_TYPES.TEXT, + validation: string().trim().notRequired() +} + +export const NIC_SCHEMA = object({ + NAME: string().trim(), + NETWORK_ID: string().trim(), + NETWORK: string().trim(), + NETWORK_UNAME: string().trim(), + SECURITY_GROUPS: string().trim() +}).concat(NETWORK_SCHEMA) + +export const SCHED_ACTION_SCHEMA = lazy(({ TIME } = {}) => { + const isRelative = String(TIME).includes('+') + const schema = isRelative ? RELATIVE_SCHEMA : PUNCTUAL_SCHEMA + + return object({ ID: ID_SCHEMA }).concat(schema) +}) + +export const SCHEMA = object({ + NIC: array(NIC_SCHEMA), + SCHED_ACTION: array(SCHED_ACTION_SCHEMA), + OS: object({ + BOOT: string().trim().notRequired() + }), + ...getValidationFromFields([ + HOST_REQ_FIELD, + HOST_RANK_FIELD, + DS_REQ_FIELD, + DS_RANK_FIELD + ]) +}) + .transform(({ SCHED_ACTION, NIC, ...rest }) => ({ + ...rest, + SCHED_ACTION: [SCHED_ACTION ?? []].flat(), + NIC: [NIC ?? []].flat() + })) diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js index 910f717ef9..fab939c4dd 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/index.js @@ -24,6 +24,8 @@ import { VmTemplatesTable } from 'client/components/Tables' import { SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable/schema' import { STEP_ID as CONFIGURATION_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration' import { SCHEMA as CONFIGURATION_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration/schema' +import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema' import { T } from 'client/constants' export const STEP_ID = 'template' @@ -45,10 +47,14 @@ const Content = ({ data, setFormData }) => { const extendedTemplate = ID ? await getVmTemplate(ID, { extended: true }) : {} - const configuration = CONFIGURATION_SCHEMA - .cast(extendedTemplate?.TEMPLATE, { stripUnknown: true }) + setFormData(prev => ({ + ...prev, + [CONFIGURATION_ID]: CONFIGURATION_SCHEMA + .cast(extendedTemplate?.TEMPLATE, { stripUnknown: true }), + [EXTRA_ID]: EXTRA_SCHEMA + .cast(extendedTemplate?.TEMPLATE, { stripUnknown: true }) + })) - setFormData(prev => ({ ...prev, [CONFIGURATION_ID]: configuration })) handleSelect(extendedTemplate) } diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js index db77289a30..481932b7b6 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/Steps/index.js @@ -13,35 +13,40 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -/* eslint-disable jsdoc/require-jsdoc */ -import * as yup from 'yup' +import VmTemplatesTable, { STEP_ID as TEMPLATE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable' +import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration' +import ExtraConfiguration, { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +import { jsonToXml } from 'client/models/Helper' +import { createSteps } from 'client/utils' -import VmTemplatesTable, { STEP_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/VmTemplatesTable' -import BasicConfiguration from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/BasicConfiguration' -import ExtraConfiguration from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration' +const Steps = createSteps(() => { + // const { [STEP_ID]: initialTemplate } = initialValues ?? {} -const Steps = initialValues => { - const { [STEP_ID]: initialTemplate } = initialValues ?? {} - - const steps = [ - BasicConfiguration(), - ExtraConfiguration() + return [ + VmTemplatesTable, + BasicConfiguration, + ExtraConfiguration ] +}, { + transformBeforeSubmit: formData => { + const { + [TEMPLATE_ID]: [templateSelected] = [], + [BASIC_ID]: { name, instances, hold, persistent, ...restOfConfig } = {}, + [EXTRA_ID]: extraTemplate = {} + } = formData ?? {} - !initialTemplate?.ID && steps.unshift(VmTemplatesTable()) + const templates = [...new Array(instances)] + .map((_, idx) => { + const replacedName = name?.replace(/%idx/gi, idx) - const schema = {} - for (const { id, resolver } of steps) { - schema[id] = typeof resolver === 'function' ? resolver() : resolver + const template = jsonToXml({ TEMPLATE: { ...extraTemplate, ...restOfConfig } }) + const data = { name: replacedName, instances, hold, persistent, template } + + return data + }) + + return [templateSelected, templates] } - - const resolvers = () => yup.object(schema) - - const defaultValues = initialTemplate?.ID - ? resolvers().cast(initialValues, { stripUnknown: true }) - : resolvers().default() - - return { steps, defaultValues, resolvers } -} +}) export default Steps diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/index.js index ee280ae710..1f2e5f59d6 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/InstantiateForm/index.js @@ -23,17 +23,21 @@ import FormStepper from 'client/components/FormStepper' import Steps from 'client/components/Forms/VmTemplate/InstantiateForm/Steps' const InstantiateForm = ({ initialValues, onSubmit }) => { - const { steps, defaultValues, resolvers } = Steps(initialValues) + const { steps, defaultValues, resolver, transformBeforeSubmit } = Steps(initialValues) const methods = useForm({ mode: 'onSubmit', defaultValues, - resolver: yupResolver(resolvers()) + resolver: yupResolver(resolver()) }) return ( - + onSubmit(transformBeforeSubmit?.(data) ?? data)} + /> ) } diff --git a/src/fireedge/src/client/components/HOC/InternalLayout/index.js b/src/fireedge/src/client/components/HOC/InternalLayout/index.js index b70264ded9..b412d3364a 100644 --- a/src/fireedge/src/client/components/HOC/InternalLayout/index.js +++ b/src/fireedge/src/client/components/HOC/InternalLayout/index.js @@ -14,22 +14,28 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useRef } from 'react' +import { useRef, useEffect } from 'react' import PropTypes from 'prop-types' +import { useParams } from 'react-router-dom' import clsx from 'clsx' - import { Box, Container } from '@material-ui/core' import { CSSTransition } from 'react-transition-group' -import { useGeneral } from 'client/features/General' +import { useGeneral, useGeneralApi } from 'client/features/General' import Header from 'client/components/Header' import Footer from 'client/components/Footer' import internalStyles from 'client/components/HOC/InternalLayout/styles' -const InternalLayout = ({ children }) => { +const InternalLayout = ({ title, children }) => { const classes = internalStyles() const container = useRef() const { isFixMenu } = useGeneral() + const { changeTitle } = useGeneralApi() + const params = useParams() + + useEffect(() => { + changeTitle(typeof title === 'function' ? title(params) : title) + }, [title]) return ( @@ -61,15 +67,11 @@ const InternalLayout = ({ children }) => { } InternalLayout.propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - PropTypes.string - ]) -} - -InternalLayout.defaultProps = { - children: [] + title: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func + ]), + children: PropTypes.any } export default InternalLayout diff --git a/src/fireedge/src/client/components/Header/index.js b/src/fireedge/src/client/components/Header/index.js index b052c820ba..ec900d6004 100644 --- a/src/fireedge/src/client/components/Header/index.js +++ b/src/fireedge/src/client/components/Header/index.js @@ -38,7 +38,7 @@ import headerStyles from 'client/components/Header/styles' const Header = ({ scrollContainer }) => { const { isOneAdmin } = useAuth() - const { title } = useGeneral() + const { appTitle, title } = useGeneral() const { fixMenu } = useGeneralApi() const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg')) @@ -57,24 +57,33 @@ const Header = ({ scrollContainer }) => { } return ( - + {!isUpLg && ( - + )} - {!isMobile && ( + + {!isMobile && ( + + {'One'} + {appTitle} + + )} - {'One'} - {title} + {title} - )} - + + {!isOneAdmin && } diff --git a/src/fireedge/src/client/components/Header/styles.js b/src/fireedge/src/client/components/Header/styles.js index d2f9d0677d..77d30d70b8 100644 --- a/src/fireedge/src/client/components/Header/styles.js +++ b/src/fireedge/src/client/components/Header/styles.js @@ -25,14 +25,18 @@ const styles = makeStyles(theme => ({ }, title: { userSelect: 'none', - flexGrow: 1, display: 'inline-flex', '& span': { textTransform: 'capitalize' } }, app: { color: ({ isScroll }) => isScroll ? theme.palette.primary.main - : theme.palette.secondary.main + : theme.palette.secondary.main, + '&::after': { + content: '"|"', + margin: '0.5em', + color: theme.palette.primary.contrastText + } }, /* POPOVER */ backdrop: { diff --git a/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js b/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js index 417fa53f55..c3cfd20663 100644 --- a/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js +++ b/src/fireedge/src/client/components/Sidebar/SidebarCollapseItem.js @@ -73,19 +73,22 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => { {expanded ? : } - {routes?.map((subItem, index) => ( - - - - - - ))} + {routes + ?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string') + ?.map((subItem, index) => ( + + + + + + )) + } ) } @@ -98,7 +101,10 @@ SidebarCollapseItem.propTypes = { ]), routes: PropTypes.arrayOf( PropTypes.shape({ - label: PropTypes.string, + label: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func + ]), path: PropTypes.string }) ) diff --git a/src/fireedge/src/client/components/Status/LinearProgressWithLabel.js b/src/fireedge/src/client/components/Status/LinearProgressWithLabel.js index 7b1211ded1..f03a9eb822 100644 --- a/src/fireedge/src/client/components/Status/LinearProgressWithLabel.js +++ b/src/fireedge/src/client/components/Status/LinearProgressWithLabel.js @@ -15,9 +15,7 @@ * ------------------------------------------------------------------------- */ import { memo } from 'react' import PropTypes from 'prop-types' - -import { withStyles } from '@material-ui/core/styles' -import { Typography, LinearProgress } from '@material-ui/core' +import { withStyles, Typography, LinearProgress } from '@material-ui/core' const BorderLinearProgress = withStyles(({ palette }) => ({ root: { diff --git a/src/fireedge/src/client/components/Tables/Clusters/index.js b/src/fireedge/src/client/components/Tables/Clusters/index.js index 5d4c6d759e..45b04c02d1 100644 --- a/src/fireedge/src/client/components/Tables/Clusters/index.js +++ b/src/fireedge/src/client/components/Tables/Clusters/index.js @@ -24,7 +24,7 @@ import { SkeletonTable, EnhancedTable } from 'client/components/Tables' import ClusterColumns from 'client/components/Tables/Clusters/columns' import ClusterRow from 'client/components/Tables/Clusters/row' -const ClustersTable = () => { +const ClustersTable = props => { const columns = useMemo(() => ClusterColumns, []) const clusters = useCluster() @@ -47,6 +47,7 @@ const ClustersTable = () => { isLoading={loading || reloading} getRowId={row => String(row.ID)} RowComponent={ClusterRow} + {...props} /> ) } diff --git a/src/fireedge/src/client/components/Tables/Hosts/index.js b/src/fireedge/src/client/components/Tables/Hosts/index.js index f21cce9e4f..724abbf8fe 100644 --- a/src/fireedge/src/client/components/Tables/Hosts/index.js +++ b/src/fireedge/src/client/components/Tables/Hosts/index.js @@ -20,12 +20,11 @@ import { useAuth } from 'client/features/Auth' import { useFetch } from 'client/hooks' import { useHost, useHostApi } from 'client/features/One' -import { SkeletonTable, EnhancedTable } from 'client/components/Tables' +import { SkeletonTable, EnhancedTable, EnhancedTableProps } from 'client/components/Tables' import HostColumns from 'client/components/Tables/Hosts/columns' import HostRow from 'client/components/Tables/Hosts/row' -import HostDetail from 'client/components/Tables/Hosts/detail' -const HostsTable = () => { +const HostsTable = props => { const columns = useMemo(() => HostColumns, []) const hosts = useHost() @@ -47,10 +46,13 @@ const HostsTable = () => { data={hosts} isLoading={loading || reloading} getRowId={row => String(row.ID)} - renderDetail={row => } RowComponent={HostRow} + {...props} /> ) } +HostsTable.propTypes = EnhancedTableProps +HostsTable.displayName = 'HostsTable' + export default HostsTable diff --git a/src/fireedge/src/client/components/Tables/Hosts/row.js b/src/fireedge/src/client/components/Tables/Hosts/row.js index 00743f99f1..325a044079 100644 --- a/src/fireedge/src/client/components/Tables/Hosts/row.js +++ b/src/fireedge/src/client/components/Tables/Hosts/row.js @@ -21,8 +21,10 @@ import { Typography } from '@material-ui/core' import { StatusCircle, LinearProgressWithLabel, StatusChip } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' +import { Tr } from 'client/components/HOC' import * as HostModel from 'client/models/Host' +import { T } from 'client/constants' const Row = ({ original, value, ...props }) => { const classes = rowStyles() @@ -71,8 +73,16 @@ const Row = ({ original, value, ...props }) => {
- - + +
) diff --git a/src/fireedge/src/client/components/Tabs/Cluster/Info/index.js b/src/fireedge/src/client/components/Tabs/Cluster/Info/index.js new file mode 100644 index 0000000000..1ee92cbed6 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Cluster/Info/index.js @@ -0,0 +1,102 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { useContext } from 'react' +import PropTypes from 'prop-types' + +import { useClusterApi } from 'client/features/One' +import { TabContext } from 'client/components/Tabs/TabProvider' +import { AttributePanel } from 'client/components/Tabs/Common' +import Information from 'client/components/Tabs/Cluster/Info/information' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' +import * as Helper from 'client/models/Helper' +import { cloneObject, set } from 'client/utils' + +const HIDDEN_ATTRIBUTES_REG = /^(HOST|RESERVED_CPU|RESERVED_MEM)$/ + +const ClusterInfoTab = ({ tabProps = {} }) => { + const { + information_panel: informationPanel, + attributes_panel: attributesPanel + } = tabProps + + const { rename, update } = useClusterApi() + const { handleRefetch, data: cluster = {} } = useContext(TabContext) + const { ID, TEMPLATE } = cluster + + const handleRename = async newName => { + const response = await rename(ID, newName) + String(response) === String(ID) && await handleRefetch?.() + } + + const handleAttributeInXml = async (path, newValue) => { + const newTemplate = cloneObject(TEMPLATE) + + set(newTemplate, path, newValue) + + const xml = Helper.jsonToXml(newTemplate) + + // 0: Replace the whole template + const response = await update(ID, xml, 0) + + String(response) === String(ID) && await handleRefetch?.() + } + + const getActions = actions => Helper.getActionsAvailable(actions) + + const { attributes } = Helper.filterAttributes(TEMPLATE, { hidden: HIDDEN_ATTRIBUTES_REG }) + + const ATTRIBUTE_FUNCTION = { + handleAdd: handleAttributeInXml, + handleEdit: handleAttributeInXml, + handleDelete: handleAttributeInXml + } + + return ( +
+ {informationPanel?.enabled && ( + + )} + {attributesPanel?.enabled && ( + + )} +
+ ) +} + +ClusterInfoTab.propTypes = { + tabProps: PropTypes.object +} + +ClusterInfoTab.displayName = 'ClusterInfoTab' + +export default ClusterInfoTab diff --git a/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js b/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js new file mode 100644 index 0000000000..d785bdf0c1 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js @@ -0,0 +1,58 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' + +import { List } from 'client/components/Tabs/Common' + +import { T, CLUSTER_ACTIONS } from 'client/constants' + +const InformationPanel = ({ cluster = {}, handleRename, actions }) => { + const { ID, NAME, TEMPLATE } = cluster + const { RESERVED_MEM, RESERVED_CPU } = TEMPLATE + + const info = [ + { name: T.ID, value: ID }, + { + name: T.Name, + value: NAME, + canEdit: actions?.includes?.(CLUSTER_ACTIONS.RENAME), + handleEdit: handleRename + } + ] + + const overcommitment = [ + { name: T.ReservedMemory, value: RESERVED_MEM }, + { name: T.ReservedCpu, value: RESERVED_CPU } + ] + + return ( + <> + + + + ) +} + +InformationPanel.displayName = 'InformationPanel' + +InformationPanel.propTypes = { + actions: PropTypes.arrayOf(PropTypes.string), + handleRename: PropTypes.func, + cluster: PropTypes.object +} + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tabs/Cluster/index.js b/src/fireedge/src/client/components/Tabs/Cluster/index.js new file mode 100644 index 0000000000..c5e57b60e6 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Cluster/index.js @@ -0,0 +1,82 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { memo, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { LinearProgress } from '@material-ui/core' + +import { useFetch } from 'client/hooks' +import { useAuth } from 'client/features/Auth' +import { useClusterApi } from 'client/features/One' + +import Tabs from 'client/components/Tabs' +import { sentenceCase, camelCase } from 'client/utils' + +import TabProvider from 'client/components/Tabs/TabProvider' +import Info from 'client/components/Tabs/Cluster/Info' + +const getTabComponent = tabName => ({ + info: Info +}[tabName]) + +const ClusterTabs = memo(({ id }) => { + const { getCluster } = useClusterApi() + const { data, fetchRequest, loading, error } = useFetch(getCluster) + + const handleRefetch = () => fetchRequest(id, { reload: true }) + + const [tabsAvailable, setTabs] = useState(() => []) + const { view, getResourceView } = useAuth() + + useEffect(() => { + fetchRequest(id) + }, [id]) + + useEffect(() => { + const infoTabs = getResourceView('CLUSTER')?.['info-tabs'] ?? {} + + setTabs(() => Object.entries(infoTabs) + ?.filter(([_, { enabled } = {}]) => !!enabled) + ?.map(([tabName, tabProps]) => { + const camelName = camelCase(tabName) + const TabContent = getTabComponent(camelName) + + return TabContent && { + name: sentenceCase(camelName), + renderContent: props => TabContent({ ...props, tabProps }) + } + }) + ?.filter(Boolean)) + }, [view]) + + if ((!data && !error) || loading) { + return + } + + return ( + + + + ) +}) + +ClusterTabs.propTypes = { + id: PropTypes.string.isRequired +} + +ClusterTabs.displayName = 'ClusterTabs' + +export default ClusterTabs diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js index 9ccac4cbee..cdc3df2a9c 100644 --- a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js +++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js @@ -15,7 +15,8 @@ * ------------------------------------------------------------------------- */ import { memo, useMemo, useState, createRef } from 'react' import PropTypes from 'prop-types' -import { makeStyles, Typography } from '@material-ui/core' +import { Link as RouterLink } from 'react-router-dom' +import { makeStyles, Typography, Link } from '@material-ui/core' import { useDialog } from 'client/hooks' import { DialogConfirmation } from 'client/components/Dialogs' @@ -46,6 +47,7 @@ const Attribute = memo(({ handleEdit, handleDelete, handleGetOptionList, + link, name, path = name, value, @@ -60,7 +62,7 @@ const Attribute = memo(({ const inputRef = createRef() const handleEditAttribute = async () => { - await handleEdit?.(inputRef.current.value, path) + await handleEdit?.(path, inputRef.current.value) setIsEditing(false) } @@ -111,10 +113,18 @@ const Attribute = memo(({ <> - {value} + {link + ? ( + + {value} + + ) + : value + } {canEdit && ( @@ -147,6 +157,7 @@ export const AttributePropTypes = { handleEdit: PropTypes.func, handleDelete: PropTypes.func, handleGetOptionList: PropTypes.func, + link: PropTypes.string, name: PropTypes.string.isRequired, value: PropTypes.oneOfType([ PropTypes.string, diff --git a/src/fireedge/src/client/components/Tabs/Common/Ownership.js b/src/fireedge/src/client/components/Tabs/Common/Ownership.js index 248d02f121..f1952ebb06 100644 --- a/src/fireedge/src/client/components/Tabs/Common/Ownership.js +++ b/src/fireedge/src/client/components/Tabs/Common/Ownership.js @@ -19,6 +19,7 @@ import PropTypes from 'prop-types' import { useUserApi, useGroupApi, RESOURCES } from 'client/features/One' import { List } from 'client/components/Tabs/Common' import { T, SERVERADMIN_ID, ACTIONS } from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' const Ownership = memo(({ actions, @@ -55,6 +56,7 @@ const Ownership = memo(({ name: T.Owner, value: userName, valueInOptionList: userId, + link: PATH.SYSTEM.USERS.DETAIL.replace(':id', userId), canEdit: actions?.includes?.(ACTIONS.CHANGE_OWNER), handleGetOptionList: getUserOptions, handleEdit: user => handleEdit?.({ user }) @@ -63,6 +65,7 @@ const Ownership = memo(({ name: T.Group, value: groupName, valueInOptionList: groupId, + link: PATH.SYSTEM.GROUPS.DETAIL.replace(':id', groupId), canEdit: actions?.includes?.(ACTIONS.CHANGE_GROUP), handleGetOptionList: getGroupOptions, handleEdit: group => handleEdit?.({ group }) diff --git a/src/fireedge/src/client/components/Tabs/Host/Info/index.js b/src/fireedge/src/client/components/Tabs/Host/Info/index.js new file mode 100644 index 0000000000..7f655f78c1 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Host/Info/index.js @@ -0,0 +1,132 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { useContext } from 'react' +import PropTypes from 'prop-types' + +import { useHostApi } from 'client/features/One' +import { TabContext } from 'client/components/Tabs/TabProvider' +import { AttributePanel } from 'client/components/Tabs/Common' +import Information from 'client/components/Tabs/Host/Info/information' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' +import * as Helper from 'client/models/Helper' +import { cloneObject, set } from 'client/utils' + +const NSX_ATTRIBUTES_REG = /^NSX_/ +const VCENTER_ATTRIBUTES_REG = /^VCENTER_(?!(RESOURCE_POOL)$)/ +const HIDDEN_ATTRIBUTES_REG = /^(HOST|VM|WILDS|ZOMBIES|RESERVED_CPU|RESERVED_MEM|EC2_ACCESS|EC2_SECRET|CAPACITY|REGION_NAME)$/ + +const HostInfoTab = ({ tabProps = {} }) => { + const { + information_panel: informationPanel, + vcenter_panel: vcenterPanel, + nsx_panel: nsxPanel, + attributes_panel: attributesPanel + } = tabProps + + const { rename, updateUserTemplate } = useHostApi() + const { handleRefetch, data: host = {} } = useContext(TabContext) + const { ID, TEMPLATE } = host + + const handleRename = async newName => { + const response = await rename(ID, newName) + String(response) === String(ID) && await handleRefetch?.() + } + + const handleAttributeInXml = async (path, newValue) => { + const newTemplate = cloneObject(TEMPLATE) + + set(newTemplate, path, newValue) + + const xml = Helper.jsonToXml(newTemplate) + + // 0: Replace the whole template + const response = await updateUserTemplate(ID, xml, 0) + + String(response) === String(ID) && await handleRefetch?.() + } + + const getActions = actions => Helper.getActionsAvailable(actions) + + const { + attributes, + nsx: nsxAttributes, + vcenter: vcenterAttributes + } = Helper.filterAttributes(TEMPLATE, { + extra: { + vcenter: VCENTER_ATTRIBUTES_REG, + nsx: NSX_ATTRIBUTES_REG + }, + hidden: HIDDEN_ATTRIBUTES_REG + }) + + const ATTRIBUTE_FUNCTION = { + handleAdd: handleAttributeInXml, + handleEdit: handleAttributeInXml, + handleDelete: handleAttributeInXml + } + + return ( +
+ {informationPanel?.enabled && ( + + )} + {attributesPanel?.enabled && attributes && ( + + )} + {vcenterPanel?.enabled && vcenterAttributes && ( + + )} + {nsxPanel?.enabled && nsxAttributes && ( + + )} +
+ ) +} + +HostInfoTab.propTypes = { + tabProps: PropTypes.object +} + +HostInfoTab.displayName = 'HostInfoTab' + +export default HostInfoTab diff --git a/src/fireedge/src/client/components/Tabs/Host/Info/information.js b/src/fireedge/src/client/components/Tabs/Host/Info/information.js new file mode 100644 index 0000000000..d602f61895 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Host/Info/information.js @@ -0,0 +1,92 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' + +import { StatusChip, LinearProgressWithLabel } from 'client/components/Status' +import { List } from 'client/components/Tabs/Common' + +import * as Host from 'client/models/Host' +import * as Datastore from 'client/models/Datastore' +import { T, VM_ACTIONS } from 'client/constants' + +const InformationPanel = ({ host = {}, handleRename, actions }) => { + const { ID, NAME, IM_MAD, VM_MAD, CLUSTER_ID, CLUSTER } = host + const { name: stateName, color: stateColor } = Host.getState(host) + const datastores = Host.getDatastores(host) + const { + percentCpuUsed, + percentCpuLabel, + percentMemUsed, + percentMemLabel + } = Host.getAllocatedInfo(host) + + const info = [ + { name: T.ID, value: ID }, + { + name: T.Name, + value: NAME, + canEdit: actions?.includes?.(VM_ACTIONS.RENAME), + handleEdit: handleRename + }, + { + name: T.State, + value: + }, + { name: T.Cluster, value: `#${CLUSTER_ID} ${CLUSTER}` }, + { name: T.IM_MAD, value: IM_MAD }, + { name: T.VM_MAD, value: VM_MAD } + ] + + const capacity = [{ + name: T.AllocatedMemory, + value: + }, { + name: T.AllocatedCpu, + value: + }] + + const datastore = datastores.map(ds => { + const { percentOfUsed, percentLabel } = Datastore.getCapacityInfo(ds) + + return { + name: `#${ds?.ID}`, // TODO: add datastore name + value: + } + }) + + return ( + <> + + + + + ) +} + +InformationPanel.displayName = 'InformationPanel' + +InformationPanel.propTypes = { + actions: PropTypes.arrayOf(PropTypes.string), + handleRename: PropTypes.func, + host: PropTypes.object +} + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tables/Hosts/detail.js b/src/fireedge/src/client/components/Tabs/Host/index.js similarity index 55% rename from src/fireedge/src/client/components/Tables/Hosts/detail.js rename to src/fireedge/src/client/components/Tabs/Host/index.js index 1ea27a3a29..7b37381aa6 100644 --- a/src/fireedge/src/client/components/Tables/Hosts/detail.js +++ b/src/fireedge/src/client/components/Tabs/Host/index.js @@ -14,74 +14,76 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ -import { useEffect } from 'react' +import { memo, useEffect, useState } from 'react' import PropTypes from 'prop-types' import { LinearProgress } from '@material-ui/core' -import Tabs from 'client/components/Tabs' -import { StatusBadge } from 'client/components/Status' - import { useFetch, useSocket } from 'client/hooks' +import { useAuth } from 'client/features/Auth' import { useHostApi } from 'client/features/One' -import * as HostModel from 'client/models/Host' +import Tabs from 'client/components/Tabs' +import { sentenceCase, camelCase } from 'client/utils' -const HostDetail = ({ id }) => { +import TabProvider from 'client/components/Tabs/TabProvider' +import Info from 'client/components/Tabs/Host/Info' + +const getTabComponent = tabName => ({ + info: Info +}[tabName]) + +const HostTabs = memo(({ id }) => { + const { getHooksSocket } = useSocket() const { getHost } = useHostApi() - const { getHooksSocket } = useSocket() - const socket = getHooksSocket({ resource: 'host', id }) + const { + data, + fetchRequest, + loading, + error + } = useFetch(getHost, getHooksSocket({ resource: 'host', id })) - const { data, fetchRequest, loading, error } = useFetch(getHost, socket) - const isLoading = (!data && !error) || loading + const handleRefetch = () => fetchRequest(id, { reload: true }) + + const [tabsAvailable, setTabs] = useState(() => []) + const { view, getResourceView } = useAuth() useEffect(() => { fetchRequest(id) }, [id]) - if (isLoading) { + useEffect(() => { + const infoTabs = getResourceView('HOST')?.['info-tabs'] ?? {} + + setTabs(() => Object.entries(infoTabs) + ?.filter(([_, { enabled } = {}]) => !!enabled) + ?.map(([tabName, tabProps]) => { + const camelName = camelCase(tabName) + const TabContent = getTabComponent(camelName) + + return TabContent && { + name: sentenceCase(camelName), + renderContent: props => TabContent({ ...props, tabProps }) + } + }) + ?.filter(Boolean)) + }, [view]) + + if ((!data && !error) || loading) { return } - if (error) { - return
{error}
- } - - const { ID, NAME, IM_MAD, VM_MAD /* VMS, CLUSTER */ } = data - - const { name: stateName, color: stateColor } = HostModel.getState(data) - - const tabs = [ - { - name: 'info', - renderContent: ( -
-
- - - {`#${ID} - ${NAME}`} - -
-
-

IM_MAD: {IM_MAD}

-

VM_MAD: {VM_MAD}

-
-
- ) - } - ] - return ( - + + + ) -} +}) -HostDetail.propTypes = { +HostTabs.propTypes = { id: PropTypes.string.isRequired } -export default HostDetail +HostTabs.displayName = 'HostTabs' + +export default HostTabs diff --git a/src/fireedge/src/client/components/Tabs/User/Info/index.js b/src/fireedge/src/client/components/Tabs/User/Info/index.js new file mode 100644 index 0000000000..7180479986 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/User/Info/index.js @@ -0,0 +1,96 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { useContext } from 'react' +import PropTypes from 'prop-types' + +import { useUserApi } from 'client/features/One' +import { TabContext } from 'client/components/Tabs/TabProvider' +import { AttributePanel } from 'client/components/Tabs/Common' +import Information from 'client/components/Tabs/User/Info/information' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' +import * as Helper from 'client/models/Helper' +import { cloneObject, set } from 'client/utils' + +const HIDDEN_ATTRIBUTES_REG = /^(SSH_PUBLIC_KEY|SSH_PRIVATE_KEY|SSH_PASSPHRASE|SUNSTONE|FIREEDGE)$/ + +const UserInfoTab = ({ tabProps = {} }) => { + const { + information_panel: informationPanel, + attributes_panel: attributesPanel + } = tabProps + + const { updateUser } = useUserApi() + const { handleRefetch, data: user = {} } = useContext(TabContext) + const { ID, TEMPLATE } = user + + const handleAttributeInXml = async (path, newValue) => { + const newTemplate = cloneObject(TEMPLATE) + + set(newTemplate, path, newValue) + + const xml = Helper.jsonToXml(newTemplate) + + // 0: Replace the whole template + const response = await updateUser(ID, xml, 0) + + String(response) === String(ID) && await handleRefetch?.() + } + + const getActions = actions => Helper.getActionsAvailable(actions) + + const { attributes } = Helper.filterAttributes(TEMPLATE, { hidden: HIDDEN_ATTRIBUTES_REG }) + + const ATTRIBUTE_FUNCTION = { + handleAdd: handleAttributeInXml, + handleEdit: handleAttributeInXml, + handleDelete: handleAttributeInXml + } + + return ( +
+ {informationPanel?.enabled && ( + + )} + {attributesPanel?.enabled && attributes && ( + + )} +
+ ) +} + +UserInfoTab.propTypes = { + tabProps: PropTypes.object +} + +UserInfoTab.displayName = 'UserInfoTab' + +export default UserInfoTab diff --git a/src/fireedge/src/client/components/Tabs/User/Info/information.js b/src/fireedge/src/client/components/Tabs/User/Info/information.js new file mode 100644 index 0000000000..b20f8de36a --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/User/Info/information.js @@ -0,0 +1,48 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' + +import { List } from 'client/components/Tabs/Common' + +import * as Helper from 'client/models/Helper' +import { T } from 'client/constants' + +const InformationPanel = ({ user = {} }) => { + const { ID, NAME, ENABLED } = user + const isEnabled = Helper.stringToBoolean(ENABLED) + + const info = [ + { name: T.ID, value: ID }, + { name: T.Name, value: NAME }, + { + name: T.State, + value: Helper.booleanToString(isEnabled) + } + ] + + return ( + + ) +} + +InformationPanel.displayName = 'InformationPanel' + +InformationPanel.propTypes = { + user: PropTypes.object +} + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tabs/User/index.js b/src/fireedge/src/client/components/Tabs/User/index.js new file mode 100644 index 0000000000..05bd4477b9 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/User/index.js @@ -0,0 +1,82 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { memo, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { LinearProgress } from '@material-ui/core' + +import { useFetch } from 'client/hooks' +import { useAuth } from 'client/features/Auth' +import { useUserApi } from 'client/features/One' + +import Tabs from 'client/components/Tabs' +import { sentenceCase, camelCase } from 'client/utils' + +import TabProvider from 'client/components/Tabs/TabProvider' +import Info from 'client/components/Tabs/User/Info' + +const getTabComponent = tabName => ({ + info: Info +}[tabName]) + +const UserTabs = memo(({ id }) => { + const { getUser } = useUserApi() + const { data, fetchRequest, loading, error } = useFetch(getUser) + + const handleRefetch = () => fetchRequest(id, { reload: true }) + + const [tabsAvailable, setTabs] = useState(() => []) + const { view, getResourceView } = useAuth() + + useEffect(() => { + fetchRequest(id) + }, [id]) + + useEffect(() => { + const infoTabs = getResourceView('USER')?.['info-tabs'] ?? {} + + setTabs(() => Object.entries(infoTabs) + ?.filter(([_, { enabled } = {}]) => !!enabled) + ?.map(([tabName, tabProps]) => { + const camelName = camelCase(tabName) + const TabContent = getTabComponent(camelName) + + return TabContent && { + name: sentenceCase(camelName), + renderContent: props => TabContent({ ...props, tabProps }) + } + }) + ?.filter(Boolean)) + }, [view]) + + if ((!data && !error) || loading) { + return + } + + return ( + + + + ) +}) + +UserTabs.propTypes = { + id: PropTypes.string.isRequired +} + +UserTabs.displayName = 'UserTabs' + +export default UserTabs diff --git a/src/fireedge/src/client/components/Tabs/Vm/Capacity/information.js b/src/fireedge/src/client/components/Tabs/Vm/Capacity/information.js index 971b6fc32b..481d126f97 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Capacity/information.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Capacity/information.js @@ -78,7 +78,7 @@ const InformationPanel = ({ actions, vm = {}, handleResizeCapacity }) => { title: T.ResizeCapacity }} options={[{ - form: () => ResizeCapacityForm({ vm }), + form: () => ResizeCapacityForm(undefined, vm.TEMPLATE), onSubmit: handleResizeCapacity }]} /> diff --git a/src/fireedge/src/client/components/Tabs/Vm/Info/index.js b/src/fireedge/src/client/components/Tabs/Vm/Info/index.js index 72df0613ad..dc90e00626 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Info/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Info/index.js @@ -63,7 +63,7 @@ const VmInfoTab = ({ tabProps = {} }) => { String(response) === String(ID) && await handleRefetch?.() } - const handleAttributeInXml = async (newValue, path) => { + const handleAttributeInXml = async (path, newValue) => { const newTemplate = cloneObject(USER_TEMPLATE) set(newTemplate, path, newValue) @@ -98,7 +98,7 @@ const VmInfoTab = ({ tabProps = {} }) => { const ATTRIBUTE_FUNCTION = { handleAdd: handleAttributeInXml, handleEdit: handleAttributeInXml, - handleDelete: path => handleAttributeInXml(undefined, path) + handleDelete: handleAttributeInXml } return ( @@ -108,14 +108,14 @@ const VmInfoTab = ({ tabProps = {} }) => { gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))', padding: '0.8em' }}> - {informationPanel?.enabled && + {informationPanel?.enabled && ( - } - {permissionsPanel?.enabled && + )} + {permissionsPanel?.enabled && ( { otherAdmin={PERMISSIONS.OTHER_A} handleEdit={handleChangePermission} /> - } - {ownershipPanel?.enabled && + )} + {ownershipPanel?.enabled && ( { groupName={GNAME} handleEdit={handleChangeOwnership} /> - } + )} {attributesPanel?.enabled && attributes && ( { const { ID, NAME, RESCHED, STIME, ETIME, LOCK, DEPLOY_ID } = vm @@ -31,6 +32,8 @@ const InformationPanel = ({ vm = {}, handleRename, actions }) => { const { HID: hostId, HOSTNAME: hostname = '--', CID: clusterId } = VirtualMachine.getLastHistory(vm) const clusterName = clusterId === '-1' ? 'default' : '--' // TODO: get from cluster list + const pathToHostDetail = PATH.INFRASTRUCTURE.HOSTS.DETAIL.replace(':id', hostId) + const pathToClusterDetail = PATH.INFRASTRUCTURE.CLUSTERS.DETAIL.replace(':id', clusterId) const ips = VirtualMachine.getIps(vm) @@ -68,11 +71,13 @@ const InformationPanel = ({ vm = {}, handleRename, actions }) => { }, { name: T.Host, - value: hostId ? `#${hostId} ${hostname}` : '' + value: hostId ? `#${hostId} ${hostname}` : '', + link: !Number.isNaN(+hostId) && pathToHostDetail }, { name: T.Cluster, - value: clusterId ? `#${clusterId} ${clusterName}` : '' + value: clusterId ? `#${clusterId} ${clusterName}` : '', + link: !Number.isNaN(+clusterId) && pathToClusterDetail }, { name: T.DeployID, diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js index 9cee06f79b..6e0014c450 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js @@ -36,7 +36,7 @@ import { Action } from 'client/components/Cards/SelectCard' import { DialogConfirmation } from 'client/components/Dialogs' import Multiple from 'client/components/Tables/Vms/multiple' -import { Tr, Translate } from 'client/components/HOC' +import { Translate } from 'client/components/HOC' import { T, VM_ACTIONS } from 'client/constants' const AccordionSummary = withStyles({ @@ -141,7 +141,7 @@ const NetworkItem = ({ nic = {}, actions }) => { {ALIAS?.map(({ NIC_ID, NETWORK = '-', BRIDGE, IP, MAC }) => (
- {`${Tr(T.Alias)} ${NIC_ID} | ${NETWORK}`} + {`${NIC_ID} | ${NETWORK}`} { @@ -43,16 +42,13 @@ const VmNetworkTab = ({ tabProps: { actions } = {} }) => { const hypervisor = VirtualMachine.getHypervisor(vm) const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor) - const handleAttachNic = async ({ network, advanced }) => { - const networkSelected = network?.[0] - const isAlias = !!advanced?.PARENT?.length - const newNic = { ...networkSelected, ...mapUserInputs(advanced) } - - const template = Helper.jsonToXml({ - [isAlias ? 'NIC_ALIAS' : 'NIC']: newNic - }) + const handleAttachNic = async formData => { + const isAlias = !!formData?.PARENT?.length + const data = { [isAlias ? 'NIC_ALIAS' : 'NIC']: formData } + const template = Helper.jsonToXml(data) const response = await attachNic(vm.ID, template) + String(response) === String(vm.ID) && await handleRefetch?.(vm.ID) } diff --git a/src/fireedge/src/client/components/Tabs/Vm/SchedActions/Actions.js b/src/fireedge/src/client/components/Tabs/Vm/SchedActions/Actions.js index 3834d06dc9..47d0029062 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/SchedActions/Actions.js +++ b/src/fireedge/src/client/components/Tabs/Vm/SchedActions/Actions.js @@ -29,34 +29,12 @@ import * as Helper from 'client/models/Helper' import { Tr, Translate } from 'client/components/HOC' import { T, VM_ACTIONS } from 'client/constants' -const mapToSchedAction = formData => { - const { ARGS, TIME: time, PERIOD, END_VALUE, END_TYPE, PERIODIC: _, ...restOfData } = formData - - const newSchedAction = { - TIME: PERIOD ? `+${time}` : Helper.isoDateToMilliseconds(time), - END_TYPE, - ...restOfData - } - - ARGS && (newSchedAction.ARGS = Object.values(ARGS).join(',')) - - if (END_VALUE) { - newSchedAction.END_VALUE = END_TYPE === '1' - ? END_VALUE - : Helper.isoDateToMilliseconds(END_VALUE) - } - - return Helper.jsonToXml({ SCHED_ACTION: newSchedAction }) -} - const CreateSchedAction = memo(() => { const { addScheduledAction } = useVmApi() const { handleRefetch, data: vm } = useContext(TabContext) const handleCreateSchedAction = async formData => { - const template = mapToSchedAction(formData) - - const data = { template } + const data = { template: Helper.jsonToXml({ SCHED_ACTION: formData }) } const response = await addScheduledAction(vm.ID, data) String(response) === String(vm.ID) && await handleRefetch?.(vm.ID) @@ -75,13 +53,13 @@ const CreateSchedAction = memo(() => { options={[{ cy: 'create-sched-action-punctual', name: 'Punctual action', - form: () => PunctualForm({ vm }), + form: () => PunctualForm(vm), onSubmit: handleCreateSchedAction }, { cy: 'create-sched-action-relative', name: 'Relative action', - form: () => RelativeForm({ vm }), + form: () => RelativeForm(vm), onSubmit: handleCreateSchedAction }]} /> @@ -95,9 +73,11 @@ const UpdateSchedAction = memo(({ schedule, name }) => { const { handleRefetch, data: vm } = useContext(TabContext) const handleUpdate = async formData => { - const template = mapToSchedAction(formData) + const data = { + id_sched: ID, + template: Helper.jsonToXml({ SCHED_ACTION: formData }) + } - const data = { id_sched: ID, template } const response = await updateScheduledAction(vm.ID, data) String(response) === String(vm.ID) && await handleRefetch?.(vm.ID) @@ -111,12 +91,12 @@ const UpdateSchedAction = memo(({ schedule, name }) => { tooltip: }} dialogProps={{ - title: `${Tr([T.ActionOverSomething, [T.Update, T.ScheduledAction]])}: ${name}` + title: `${Tr(T.Update)} ${T.ScheduledAction}: ${name}` }} options={[{ form: () => isRelative - ? RelativeForm({ schedule, vm }) - : PunctualForm({ schedule, vm }), + ? RelativeForm(vm, schedule) + : PunctualForm(vm, schedule), onSubmit: handleUpdate }]} /> @@ -162,15 +142,14 @@ const CharterAction = memo(() => { const handleCreateCharter = async () => { const schedActions = leases .map(([action, { time, warning: { time: warningTime } = {} } = {}]) => ({ - PERIOD: true, - TIME: +time, + TIME: `+${+time}`, ACTION: action, - ...(warningTime && { WARNING: warningTime }) + ...(warningTime && { WARNING: `-${+warningTime}` }) })) const response = await Promise.all( schedActions.map(schedAction => { - const data = { template: mapToSchedAction(schedAction) } + const data = { template: Helper.jsonToXml({ SCHED_ACTION: schedAction }) } return addScheduledAction(vm.ID, data) }) ) @@ -225,7 +204,7 @@ const ActionPropTypes = { name: PropTypes.string } -CreateSchedAction.propTypes = {} +CreateSchedAction.propTypes = ActionPropTypes CreateSchedAction.displayName = 'CreateSchedActionButton' UpdateSchedAction.propTypes = ActionPropTypes UpdateSchedAction.displayName = 'UpdateSchedActionButton' diff --git a/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js b/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js index 22ca38037f..fd186910f8 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Snapshot/index.js @@ -56,7 +56,7 @@ const VmSnapshotTab = ({ tabProps: { actions } = {} }) => { title: Tr(T.TakeSnapshot) }} options={[{ - form: CreateSnapshotForm, + form: () => CreateSnapshotForm(), onSubmit: handleSnapshotCreate }]} /> diff --git a/src/fireedge/src/client/components/Tabs/Vm/Storage/Actions.js b/src/fireedge/src/client/components/Tabs/Vm/Storage/Actions.js index eeaa5802c5..2a69a5cd4a 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Storage/Actions.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Storage/Actions.js @@ -78,7 +78,7 @@ const SaveAsAction = memo(({ disk, snapshot, name: imageName }) => { : `${Tr(T.SaveAs)} ${Tr(T.Image)}: #${diskId} - ${imageName}` }} options={[{ - form: SaveAsDiskForm, + form: () => SaveAsDiskForm(), onSubmit: handleSaveAs }]} /> @@ -106,7 +106,7 @@ const ResizeAction = memo(({ disk, name: imageName }) => { title: `${Tr(T.Resize)}: #${DISK_ID} - ${imageName}` }} options={[{ - form: () => ResizeDiskForm({ disk }), + form: () => ResizeDiskForm(undefined, disk), onSubmit: handleResize }]} /> @@ -134,7 +134,7 @@ const SnapshotCreateAction = memo(({ disk, name: imageName }) => { title: `${Tr(T.TakeSnapshot)}: #${DISK_ID} - ${imageName}` }} options={[{ - form: CreateDiskSnapshotForm, + form: () => CreateDiskSnapshotForm(), onSubmit: handleSnapshotCreate }]} /> @@ -165,7 +165,7 @@ const SnapshotRenameAction = memo(({ disk, snapshot }) => { title: `${Tr(T.Rename)}: #${ID} - ${NAME}` }} options={[{ - form: () => CreateDiskSnapshotForm({ snapshot }), + form: () => CreateDiskSnapshotForm(undefined, snapshot), onSubmit: handleRename }]} /> diff --git a/src/fireedge/src/client/components/Tabs/Vm/Storage/SubItem.js b/src/fireedge/src/client/components/Tabs/Vm/Storage/SubItem.js index 80fd8639f5..85c4997360 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Storage/SubItem.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Storage/SubItem.js @@ -21,7 +21,7 @@ import { Typography, Paper } from '@material-ui/core' import * as Actions from 'client/components/Tabs/Vm/Storage/Actions' import { StatusChip } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' -import { Tr } from 'client/components/HOC' +import { Translate } from 'client/components/HOC' import * as Helper from 'client/models/Helper' import { prettyBytes } from 'client/utils' @@ -56,8 +56,8 @@ const StorageSubItem = ({ disk, snapshot = {}, actions = [] }) => {
{NAME} - {isActive && } - + {isActive && } />} + } />
diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js new file mode 100644 index 0000000000..ed1abace16 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js @@ -0,0 +1,103 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { useContext } from 'react' +import PropTypes from 'prop-types' + +import { useVmTemplateApi } from 'client/features/One' +import { TabContext } from 'client/components/Tabs/TabProvider' +import { Permissions, Ownership } from 'client/components/Tabs/Common' +import Information from 'client/components/Tabs/VmTemplate/Info/information' +import * as Helper from 'client/models/Helper' + +const VmTemplateInfoTab = ({ tabProps = {} }) => { + const { + information_panel: informationPanel, + permissions_panel: permissionsPanel, + ownership_panel: ownershipPanel + } = tabProps + + const { rename, changeOwnership, changePermissions } = useVmTemplateApi() + const { handleRefetch, data: template = {} } = useContext(TabContext) + const { ID, UNAME, UID, GNAME, GID, PERMISSIONS } = template + + const handleChangeOwnership = async newOwnership => { + const response = await changeOwnership(ID, newOwnership) + String(response) === String(ID) && await handleRefetch?.() + } + + const handleChangePermission = async newPermission => { + const response = await changePermissions(ID, newPermission) + String(response) === String(ID) && await handleRefetch?.() + } + + const handleRename = async newName => { + const response = await rename(ID, newName) + String(response) === String(ID) && await handleRefetch?.() + } + + const getActions = actions => Helper.getActionsAvailable(actions) + + return ( +
+ {informationPanel?.enabled && ( + + )} + {permissionsPanel?.enabled && ( + + )} + {ownershipPanel?.enabled && ( + + )} +
+ ) +} + +VmTemplateInfoTab.propTypes = { + tabProps: PropTypes.object +} + +VmTemplateInfoTab.displayName = 'VmTemplateInfoTab' + +export default VmTemplateInfoTab diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js new file mode 100644 index 0000000000..223ce65e5e --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js @@ -0,0 +1,51 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import PropTypes from 'prop-types' + +import { List } from 'client/components/Tabs/Common' + +import * as Helper from 'client/models/Helper' +import { T } from 'client/constants' + +const InformationPanel = ({ template = {} }) => { + const { ID, NAME, REGTIME, LOCK } = template + + const info = [ + { name: T.ID, value: ID }, + { name: T.Name, value: NAME }, + { + name: T.StartTime, + value: Helper.timeToString(REGTIME) + }, + { + name: T.Locked, + value: Helper.levelLockToString(LOCK?.LOCKED) + } + ] + + return ( + + ) +} + +InformationPanel.displayName = 'InformationPanel' + +InformationPanel.propTypes = { + template: PropTypes.object +} + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Template.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Template.js new file mode 100644 index 0000000000..9255df6105 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Template.js @@ -0,0 +1,41 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { useContext } from 'react' +import { Accordion, AccordionDetails } from '@material-ui/core' + +import { TabContext } from 'client/components/Tabs/TabProvider' + +const TemplateTab = () => { + const { data: template = {} } = useContext(TabContext) + const { TEMPLATE } = template + + return ( + + +
+          
+            {JSON.stringify(TEMPLATE, null, 2)}
+          
+        
+
+
+ ) +} + +TemplateTab.displayName = 'TemplateTab' + +export default TemplateTab diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/index.js b/src/fireedge/src/client/components/Tabs/VmTemplate/index.js new file mode 100644 index 0000000000..17a1734a62 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/VmTemplate/index.js @@ -0,0 +1,84 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); you may * + * not use this file except in compliance with the License. You may obtain * + * a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + * ------------------------------------------------------------------------- */ +/* eslint-disable jsdoc/require-jsdoc */ +import { memo, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { LinearProgress } from '@material-ui/core' + +import { useFetch } from 'client/hooks' +import { useAuth } from 'client/features/Auth' +import { useVmTemplateApi } from 'client/features/One' + +import Tabs from 'client/components/Tabs' +import { sentenceCase, camelCase } from 'client/utils' + +import TabProvider from 'client/components/Tabs/TabProvider' +import Info from 'client/components/Tabs/VmTemplate/Info' +import Template from 'client/components/Tabs/VmTemplate/Template' + +const getTabComponent = tabName => ({ + info: Info, + template: Template +}[tabName]) + +const VmTemplateTabs = memo(({ id }) => { + const { getVmTemplate } = useVmTemplateApi() + const { data, fetchRequest, loading, error } = useFetch(getVmTemplate) + + const handleRefetch = () => fetchRequest(id, { reload: true }) + + const [tabsAvailable, setTabs] = useState(() => []) + const { view, getResourceView } = useAuth() + + useEffect(() => { + fetchRequest(id) + }, [id]) + + useEffect(() => { + const infoTabs = getResourceView('VM-TEMPLATE')?.['info-tabs'] ?? {} + + setTabs(() => Object.entries(infoTabs) + ?.filter(([_, { enabled } = {}]) => !!enabled) + ?.map(([tabName, tabProps]) => { + const camelName = camelCase(tabName) + const TabContent = getTabComponent(camelName) + + return TabContent && { + name: sentenceCase(camelName), + renderContent: props => TabContent({ ...props, tabProps }) + } + }) + ?.filter(Boolean)) + }, [view]) + + if ((!data && !error) || loading) { + return + } + + return ( + + + + ) +}) + +VmTemplateTabs.propTypes = { + id: PropTypes.string.isRequired +} + +VmTemplateTabs.displayName = 'VmTemplateTabs' + +export default VmTemplateTabs diff --git a/src/fireedge/src/client/components/Tabs/index.js b/src/fireedge/src/client/components/Tabs/index.js index 1b22e95138..473952ca65 100644 --- a/src/fireedge/src/client/components/Tabs/index.js +++ b/src/fireedge/src/client/components/Tabs/index.js @@ -22,7 +22,7 @@ import { Tabs as MTabs, Tab as MTab } from '@material-ui/core' const Content = ({ name, renderContent: Content, hidden }) => (