1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F #5422: Add extra configuration to instantiate form (#1450)

This commit is contained in:
Sergio Betanzos 2021-09-13 16:26:20 +02:00 committed by GitHub
parent 7f000b89ed
commit 128ed62841
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 3448 additions and 774 deletions

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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()
}

View File

@ -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()

View File

@ -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 }) => {

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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([

View File

@ -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], [

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 => (
<FormWithSchema
cy='attach-nic-advanced'
id={STEP_ID}
fields={FIELDS(props)}
/>
)
const AdvancedOptions = props => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: () => SCHEMA(nics),
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: useCallback(
() => (
<FormWithSchema
cy='attach-nic-advanced'
id={STEP_ID}
fields={FIELDS(nics)}
/>
),
[nics?.length, nics?.[0]?.ID]
)
content: () => Content(props)
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
nics: PropTypes.array
}
export default AdvancedOptions

View File

@ -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()))

View File

@ -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 (
<VNetworksTable
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={row => 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 (
<VNetworksTable
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
initialState={{ selectedRowIds: { [selectedNetwork?.ID]: true } }}
onSelectedRowsChange={handleSelectedRows}
/>
)
}, [])
resolver: SCHEMA,
content: Content
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
}
export default NetworkStep

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}))

View File

@ -53,6 +53,24 @@ const Content = () => {
legend={Tr(T.Disks)}
id={STEP_ID}
/>
<FormWithSchema
cy='instantiate-vm-template-configuration.ownership'
fields={FIELDS.OWNERSHIP}
legend={Tr(T.Ownership)}
id={STEP_ID}
/>
<FormWithSchema
cy='instantiate-vm-template-configuration.vmgroup'
fields={FIELDS.VM_GROUP}
legend={Tr(T.VMGroup)}
id={STEP_ID}
/>
<FormWithSchema
cy='instantiate-vm-template-configuration.vcenter'
fields={FIELDS.VCENTER}
legend={`vCenter ${Tr(T.Deployment)}`}
id={STEP_ID}
/>
</div>
)
}

View File

@ -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))

View File

@ -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)

View File

@ -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))

View File

@ -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
)
})
})

View File

@ -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: (
<>
<ImageIcon size={16} />
{name}
</>
)
}
})
}, [])
const nics = data?.[NIC_ID]
?.map((nic, idx) => ({
ID: `nic${idx}`,
NAME: (
<>
<NetworkIcon size={16} />
{`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 (
<DragDropContext onDragEnd={onDragEnd}>
<div className={classes.container}>
<Droppable droppableId='booting'>
{({ droppableProps, innerRef, placeholder }) => (
<div
{...droppableProps}
ref={innerRef}
className={classes.list}
>
{enabledItems.map(({ ID, NAME }, idx) => (
<Draggable key={ID} draggableId={ID} index={idx}>
{({ draggableProps, dragHandleProps, innerRef }) => (
<div
{...draggableProps}
{...dragHandleProps}
ref={innerRef}
className={classes.item}
>
<Action
cy={ID}
icon={<CheckIcon size={15} />}
handleClick={() => handleEnable(ID)}
/>
{NAME}
</div>
)}
</Draggable>
))}
{placeholder}
</div>
)}
</Droppable>
<Divider />
{restOfItems.map(({ ID, NAME }) => (
<div key={ID} className={classes.item}>
<Action
cy={ID}
icon={<BlankSquareIcon size={15} />}
handleClick={() => handleEnable(ID)}
/>
{NAME}
</div>
))}
</div>
</DragDropContext>
)
}
Booting.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
}
Booting.displayName = 'Booting'
export default Booting

View File

@ -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] && (
<WarningIcon color={theme.palette.error.main} />
)
}))
return (
<div>TODO: Tabs with extra configuration</div>
<Tabs tabs={tabs} />
)
}
@ -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

View File

@ -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 (
<>
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-nic',
label: 'Add nic'
}}
dialogProps={{
title: `Add new: ${Tr(T.NIC)}`
}}
options={[{
form: () => AttachNicForm({ nics }),
onSubmit: formData =>
handleSave(NIC_SCHEMA.cast(formData))
}]}
/>
<div className={classes.root}>
{nics?.map(item => {
const { NAME, RDP, SSH, NETWORK, PARENT, EXTERNAL } = item
const hasAlias = nics?.some(nic => nic.PARENT === NAME)
return (
<SelectCard
key={NAME}
title={`${NAME} - ${NETWORK}`}
subheader={<>
{Object
.entries({ RDP, SSH, ALIAS: PARENT, EXTERNAL })
.map(([k, v]) => v ? `${k}` : '')
.filter(Boolean)
.join(' | ')
}
</>}
action={
<>
{!hasAlias &&
<Action
data-cy={`remove-${NAME}`}
handleClick={() => handleRemove(NAME)}
icon={<Trash size={18} />}
/>
}
<ButtonToTriggerForm
buttonProps={{
'data-cy': `edit-${NAME}`,
icon: <Edit size={18} />,
tooltip: <Translate word={T.Edit} />
}}
dialogProps={{
title: <><Translate word={T.Edit} />{`: ${NAME} - ${NETWORK}`}</>
}}
options={[{
form: () => AttachNicForm({ nics }, item),
onSubmit: newValues => handleSave(newValues, NAME)
}]}
/>
</>
}
/>
)
})}
</div>
</>
)
}
Networking.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
}
Networking.displayName = 'Networking'
export default Networking

View File

@ -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="<id>"
// TODO - Host policy options: Packing|Stripping|Load-aware
// TODO - DS requirements: add button to select DATASTORE in list => ID="<id>"
// TODO - DS policy options: Packing|Stripping
return (
<>
<FormWithSchema
className={classes.information}
cy='instantiate-vm-template-extra.host-placement'
fields={[HOST_REQ_FIELD, HOST_RANK_FIELD]}
legend={Tr(T.Host)}
id={STEP_ID}
/>
<FormWithSchema
className={classes.information}
cy='instantiate-vm-template-extra.ds-placement'
fields={[DS_REQ_FIELD, DS_RANK_FIELD]}
legend={Tr(T.Datastore)}
id={STEP_ID}
/>
</>
)
}
Placement.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
}
Placement.displayName = 'Placement'
export default Placement

View File

@ -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 (
<>
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-sched-action',
label: Tr(T.AddAction)
}}
dialogProps={{
title: Tr(T.ScheduledAction)
}}
options={[{
cy: 'add-sched-action-punctual',
name: 'Punctual action',
form: () => 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))
}]}
/>
<div className={classes.root}>
{scheduleActions?.map(item => {
const { ID, NAME, ACTION, TIME } = item
const isRelative = String(TIME).includes('+')
return (
<SelectCard
key={ID}
title={`${NAME} - ${ACTION}`}
action={
<>
<Action
data-cy={`remove-${NAME}`}
handleClick={() => handleRemove(ID)}
icon={<Trash size={18} />}
/>
<ButtonToTriggerForm
buttonProps={{
'data-cy': `edit-${NAME}`,
icon: <Edit size={18} />,
tooltip: <Translate word={T.Edit} />
}}
dialogProps={{
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
}}
options={[{
form: () => isRelative
? RelativeForm(undefined, item)
: PunctualForm(undefined, item),
onSubmit: newValues => handleSave(newValues, ID)
}]}
/>
</>
}
/>
)
})}
</div>
</>
)
}
ScheduleAction.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func
}
ScheduleAction.displayName = 'ScheduleAction'
export default ScheduleAction

View File

@ -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()
}))

View File

@ -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)
}

View File

@ -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

View File

@ -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 (
<FormProvider {...methods}>
<FormStepper steps={steps} schema={resolvers} onSubmit={onSubmit} />
<FormStepper
steps={steps}
schema={resolver}
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
/>
</FormProvider>
)
}

View File

@ -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 (
<Box className={clsx(classes.root, { [classes.isDrawerFixed]: isFixMenu })}>
@ -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

View File

@ -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 (
<AppBar className={classes.appbar} data-cy="header" elevation={1}>
<AppBar className={classes.appbar} data-cy='header' elevation={1}>
<Toolbar>
{!isUpLg && (
<IconButton onClick={handleFixMenu} edge="start" color="inherit">
<IconButton onClick={handleFixMenu} edge='start' color='inherit'>
<MenuIcon />
</IconButton>
)}
{!isMobile && (
<Box flexGrow={1}>
{!isMobile && (
<Typography
variant='h6'
className={classes.title}
data-cy='header-app-title'
>
{'One'}
<span className={classes.app}>{appTitle}</span>
</Typography>
)}
<Typography
variant="h6"
variant='h6'
className={classes.title}
data-cy="header-title"
data-cy='header-title'
>
{'One'}
<span className={classes.app}>{title}</span>
{title}
</Typography>
)}
<Box flexGrow={isMobile ? 1 : 0} textAlign="end">
</Box>
<Box flexGrow={isMobile ? 1 : 0} textAlign='end'>
<User />
<View />
{!isOneAdmin && <Group />}

View File

@ -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: {

View File

@ -73,19 +73,22 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
{expanded ? <CollapseIcon /> : <ExpandMoreIcon />}
</MIcon>
</ListItem>
{routes?.map((subItem, index) => (
<Collapse
key={`subitem-${index}`}
in={expanded}
timeout='auto'
unmountOnExit
className={clsx({ [classes.subItemWrapper]: isUpLg && !isFixMenu })}
>
<List component='div' disablePadding>
<SidebarLink {...subItem} isSubItem />
</List>
</Collapse>
))}
{routes
?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string')
?.map((subItem, index) => (
<Collapse
key={`subitem-${index}`}
in={expanded}
timeout='auto'
unmountOnExit
className={clsx({ [classes.subItemWrapper]: isUpLg && !isFixMenu })}
>
<List component='div' disablePadding>
<SidebarLink {...subItem} isSubItem />
</List>
</Collapse>
))
}
</>
)
}
@ -98,7 +101,10 @@ SidebarCollapseItem.propTypes = {
]),
routes: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
path: PropTypes.string
})
)

View File

@ -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: {

View File

@ -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}
/>
)
}

View File

@ -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 => <HostDetail id={row.ID} />}
RowComponent={HostRow}
{...props}
/>
)
}
HostsTable.propTypes = EnhancedTableProps
HostsTable.displayName = 'HostsTable'
export default HostsTable

View File

@ -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 }) => {
</div>
</div>
<div className={classes.secondary}>
<LinearProgressWithLabel value={percentCpuUsed} label={percentCpuLabel} />
<LinearProgressWithLabel value={percentMemUsed} label={percentMemLabel} />
<LinearProgressWithLabel
value={percentCpuUsed}
label={percentCpuLabel}
title={`${Tr(T.AllocatedCpu)}`}
/>
<LinearProgressWithLabel
value={percentMemUsed}
label={percentMemLabel}
title={`${Tr(T.AllocatedMemory)}`}
/>
</div>
</div>
)

View File

@ -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 (
<div style={{
display: 'grid',
gap: '1em',
gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))',
padding: '0.8em'
}}>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
handleRename={handleRename}
cluster={cluster}
/>
)}
{attributesPanel?.enabled && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>
)}
</div>
)
}
ClusterInfoTab.propTypes = {
tabProps: PropTypes.object
}
ClusterInfoTab.displayName = 'ClusterInfoTab'
export default ClusterInfoTab

View File

@ -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 (
<>
<List title={T.Information} list={info} />
<List title={T.Overcommitment} list={overcommitment} />
</>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
handleRename: PropTypes.func,
cluster: PropTypes.object
}
export default InformationPanel

View File

@ -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 <LinearProgress color='secondary' style={{ width: '100%' }} />
}
return (
<TabProvider initialState={{ data, handleRefetch }}>
<Tabs tabs={tabsAvailable} />
</TabProvider>
)
})
ClusterTabs.propTypes = {
id: PropTypes.string.isRequired
}
ClusterTabs.displayName = 'ClusterTabs'
export default ClusterTabs

View File

@ -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(({
<>
<Typography
noWrap
component='span'
variant='body2'
title={typeof value === 'string' ? value : undefined}
>
{value}
{link
? (
<Link color='secondary' component={RouterLink} to={link}>
{value}
</Link>
)
: value
}
</Typography>
{canEdit && (
<Actions.Edit name={name} handleClick={handleActiveEditForm} />
@ -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,

View File

@ -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 })

View File

@ -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 (
<div style={{
display: 'grid',
gap: '1em',
gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))',
padding: '0.8em'
}}>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
handleRename={handleRename}
host={host}
/>
)}
{attributesPanel?.enabled && attributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>
)}
{vcenterPanel?.enabled && vcenterAttributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
actions={getActions(vcenterPanel?.actions)}
attributes={vcenterAttributes}
title={`vCenter ${Tr(T.Information)}`}
/>
)}
{nsxPanel?.enabled && nsxAttributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
actions={getActions(nsxPanel?.actions)}
attributes={nsxAttributes}
title={`NSX ${Tr(T.Information)}`}
/>
)}
</div>
)
}
HostInfoTab.propTypes = {
tabProps: PropTypes.object
}
HostInfoTab.displayName = 'HostInfoTab'
export default HostInfoTab

View File

@ -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: <StatusChip text={stateName} stateColor={stateColor} />
},
{ 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: <LinearProgressWithLabel value={percentMemUsed} label={percentMemLabel} />
}, {
name: T.AllocatedCpu,
value: <LinearProgressWithLabel value={percentCpuUsed} label={percentCpuLabel} />
}]
const datastore = datastores.map(ds => {
const { percentOfUsed, percentLabel } = Datastore.getCapacityInfo(ds)
return {
name: `#${ds?.ID}`, // TODO: add datastore name
value: <LinearProgressWithLabel value={percentOfUsed} label={percentLabel} />
}
})
return (
<>
<List
title={T.Information}
list={info}
containerProps={{ style: { gridRow: 'span 2' } }}
/>
<List title={T.Capacity} list={capacity} />
<List title={T.Datastores} list={datastore} />
</>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
handleRename: PropTypes.func,
host: PropTypes.object
}
export default InformationPanel

View File

@ -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 <LinearProgress color='secondary' style={{ width: '100%' }} />
}
if (error) {
return <div>{error}</div>
}
const { ID, NAME, IM_MAD, VM_MAD /* VMS, CLUSTER */ } = data
const { name: stateName, color: stateColor } = HostModel.getState(data)
const tabs = [
{
name: 'info',
renderContent: (
<div>
<div>
<StatusBadge
title={stateName}
stateColor={stateColor}
customTransform='translate(150%, 50%)'
/>
<span style={{ marginLeft: 20 }}>
{`#${ID} - ${NAME}`}
</span>
</div>
<div>
<p>IM_MAD: {IM_MAD}</p>
<p>VM_MAD: {VM_MAD}</p>
</div>
</div>
)
}
]
return (
<Tabs tabs={tabs} />
<TabProvider initialState={{ data, handleRefetch }}>
<Tabs tabs={tabsAvailable} />
</TabProvider>
)
}
})
HostDetail.propTypes = {
HostTabs.propTypes = {
id: PropTypes.string.isRequired
}
export default HostDetail
HostTabs.displayName = 'HostTabs'
export default HostTabs

View File

@ -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 (
<div style={{
display: 'grid',
gap: '1em',
gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))',
padding: '0.8em'
}}>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
user={user}
/>
)}
{attributesPanel?.enabled && attributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>
)}
</div>
)
}
UserInfoTab.propTypes = {
tabProps: PropTypes.object
}
UserInfoTab.displayName = 'UserInfoTab'
export default UserInfoTab

View File

@ -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 (
<List title={T.Information} list={info} />
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
user: PropTypes.object
}
export default InformationPanel

View File

@ -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 <LinearProgress color='secondary' style={{ width: '100%' }} />
}
return (
<TabProvider initialState={{ data, handleRefetch }}>
<Tabs tabs={tabsAvailable} />
</TabProvider>
)
})
UserTabs.propTypes = {
id: PropTypes.string.isRequired
}
UserTabs.displayName = 'UserTabs'
export default UserTabs

View File

@ -78,7 +78,7 @@ const InformationPanel = ({ actions, vm = {}, handleResizeCapacity }) => {
title: T.ResizeCapacity
}}
options={[{
form: () => ResizeCapacityForm({ vm }),
form: () => ResizeCapacityForm(undefined, vm.TEMPLATE),
onSubmit: handleResizeCapacity
}]}
/>

View File

@ -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 && (
<Information
actions={getActions(informationPanel?.actions)}
handleRename={handleRename}
vm={vm}
/>
}
{permissionsPanel?.enabled &&
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
ownerUse={PERMISSIONS.OWNER_U}
@ -129,8 +129,8 @@ const VmInfoTab = ({ tabProps = {} }) => {
otherAdmin={PERMISSIONS.OTHER_A}
handleEdit={handleChangePermission}
/>
}
{ownershipPanel?.enabled &&
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
userId={UID}
@ -139,7 +139,7 @@ const VmInfoTab = ({ tabProps = {} }) => {
groupName={GNAME}
handleEdit={handleChangeOwnership}
/>
}
)}
{attributesPanel?.enabled && attributes && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}

View File

@ -23,6 +23,7 @@ import Multiple from 'client/components/Tables/Vms/multiple'
import * as VirtualMachine from 'client/models/VirtualMachine'
import * as Helper from 'client/models/Helper'
import { T, VM_ACTIONS } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
const InformationPanel = ({ vm = {}, handleRename, actions }) => {
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,

View File

@ -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 }) => (
<div key={NIC_ID} className={classes.row}>
<Typography noWrap variant='body2'>
{`${Tr(T.Alias)} ${NIC_ID} | ${NETWORK}`}
<Translate word={T.Alias} />{`${NIC_ID} | ${NETWORK}`}
</Typography>
<span className={classes.labels}>
<Multiple

View File

@ -27,7 +27,6 @@ import { Tr } from 'client/components/HOC'
import * as VirtualMachine from 'client/models/VirtualMachine'
import * as Helper from 'client/models/Helper'
import { mapUserInputs } from 'client/utils'
import { T, VM_ACTIONS } from 'client/constants'
const VmNetworkTab = ({ tabProps: { actions } = {} }) => {
@ -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)
}

View File

@ -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: <Translate word={T.Edit} />
}}
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'

View File

@ -56,7 +56,7 @@ const VmSnapshotTab = ({ tabProps: { actions } = {} }) => {
title: Tr(T.TakeSnapshot)
}}
options={[{
form: CreateSnapshotForm,
form: () => CreateSnapshotForm(),
onSubmit: handleSnapshotCreate
}]}
/>

View File

@ -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
}]}
/>

View File

@ -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 = [] }) => {
<div className={classes.title}>
<Typography component='span'>{NAME}</Typography>
<span className={classes.labels}>
{isActive && <StatusChip text={Tr(T.Active)} />}
<StatusChip text={Tr(T.Snapshot)} />
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
<StatusChip text={<Translate word={T.Snapshot} />} />
</span>
</div>
<div className={classes.caption}>

View File

@ -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 (
<div style={{
display: 'grid',
gap: '1em',
gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))',
padding: '0.8em'
}}>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
handleRename={handleRename}
template={template}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
handleEdit={handleChangePermission}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
handleEdit={handleChangeOwnership}
/>
)}
</div>
)
}
VmTemplateInfoTab.propTypes = {
tabProps: PropTypes.object
}
VmTemplateInfoTab.displayName = 'VmTemplateInfoTab'
export default VmTemplateInfoTab

View File

@ -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 (
<List title={T.Information} list={info} />
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
template: PropTypes.object
}
export default InformationPanel

View File

@ -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 (
<Accordion expanded TransitionProps={{ unmountOnExit: true }}>
<AccordionDetails>
<pre>
<code style={{ whiteSpace: 'break-spaces' }}>
{JSON.stringify(TEMPLATE, null, 2)}
</code>
</pre>
</AccordionDetails>
</Accordion>
)
}
TemplateTab.displayName = 'TemplateTab'
export default TemplateTab

View File

@ -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 <LinearProgress color='secondary' style={{ width: '100%' }} />
}
return (
<TabProvider initialState={{ data, handleRefetch }}>
<Tabs tabs={tabsAvailable} />
</TabProvider>
)
})
VmTemplateTabs.propTypes = {
id: PropTypes.string.isRequired
}
VmTemplateTabs.displayName = 'VmTemplateTabs'
export default VmTemplateTabs

View File

@ -22,7 +22,7 @@ import { Tabs as MTabs, Tab as MTab } from '@material-ui/core'
const Content = ({ name, renderContent: Content, hidden }) => (
<div key={`tab-${name}`}
style={{
padding: 2,
padding: '1em 0.5em',
height: '100%',
overflow: 'auto',
display: hidden ? 'none' : 'block'
@ -33,7 +33,7 @@ const Content = ({ name, renderContent: Content, hidden }) => (
)
const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
const [tabSelected, setTab] = useState(() => 6)
const [tabSelected, setTab] = useState(() => 0)
const renderTabs = useMemo(() => (
<MTabs

View File

@ -0,0 +1,24 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
import * as ACTIONS from 'client/constants/actions'
/** @enum {string} Cluster actions */
export const CLUSTER_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
DELETE: 'delete',
RENAME: ACTIONS.RENAME
}

View File

@ -55,6 +55,7 @@ export const FILTER_POOL = {
USER_GROUPS_RESOURCES: '-1'
}
/** @enum {string} Input types */
export const INPUT_TYPES = {
AUTOCOMPLETE: 'autocomplete',
CHECKBOX: 'checkbox',
@ -86,6 +87,7 @@ export * as ACTIONS from 'client/constants/actions'
export * as STATES from 'client/constants/states'
export * from 'client/constants/flow'
export * from 'client/constants/provision'
export * from 'client/constants/cluster'
export * from 'client/constants/vm'
export * from 'client/constants/host'
export * from 'client/constants/image'

View File

@ -212,6 +212,7 @@ module.exports = {
/* tabs */
Information: 'Information',
Placement: 'Placement',
/* general schema */
ID: 'ID',
@ -241,6 +242,7 @@ module.exports = {
IP: 'IP',
Reschedule: 'Reschedule',
DeployID: 'Deploy ID',
Deployment: 'Deployment',
Monitoring: 'Monitoring',
/* flow schema */
@ -277,5 +279,23 @@ module.exports = {
ICMPV6: 'ICMPv6',
IPSEC: 'IPsec',
Outbound: 'Outbound',
Inbound: 'Inbound'
Inbound: 'Inbound',
/* Host schema */
IM_MAD: 'IM MAD',
VM_MAD: 'VM MAD',
Wilds: 'Wilds',
Zombies: 'Zombies',
Numa: 'Numa',
/* Host schema - capacity */
AllocatedMemory: 'Allocated Memory',
AllocatedCpu: 'Allocated CPU',
RealMemory: 'Real Memory',
RealCpu: 'Real CPU',
Overcommitment: 'Overcommitment',
/* Cluster schema */
/* Cluster schema - capacity */
ReservedMemory: 'Allocated Memory',
ReservedCpu: 'Allocated CPU'
}

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* 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 { useParams, Redirect } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
import ClusterTabs from 'client/components/Tabs/Cluster'
function ClusterDetail () {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to='/' />
}
return (
<Box
py={2}
overflow='auto'
display='flex'
flexDirection='column'
component={Container}
>
{<ClusterTabs id={id} />}
</Box>
)
}
export default ClusterDetail

View File

@ -14,12 +14,19 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { ClustersTable } from 'client/components/Tables'
import ClusterTabs from 'client/components/Tabs/Cluster'
import SplitPane from 'client/components/SplitPane'
function Clusters () {
const [selectedRows, onSelectedRowsChange] = useState([])
const getRowIds = () =>
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
return (
<Box
height={1}
@ -29,7 +36,18 @@ function Clusters () {
flexDirection='column'
component={Container}
>
<ClustersTable />
<SplitPane>
<ClustersTable onSelectedRowsChange={onSelectedRowsChange} />
{selectedRows?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
{selectedRows?.length === 1
? <ClusterTabs id={selectedRows[0]?.values.ID} />
: <pre><code>{getRowIds()}</code></pre>
}
</div>
)}
</SplitPane>
</Box>
)
}

View File

@ -0,0 +1,43 @@
/* ------------------------------------------------------------------------- *
* 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 { useParams, Redirect } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
// import GroupTabs from 'client/components/Tabs/Group'
function GroupDetail () {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to='/' />
}
return (
<Box
py={2}
overflow='auto'
display='flex'
flexDirection='column'
component={Container}
>
{/* <GroupTabs id={id} /> */}
{id}
</Box>
)
}
export default GroupDetail

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* 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 { useParams, Redirect } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
import HostTabs from 'client/components/Tabs/Host'
function HostDetail () {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to='/' />
}
return (
<Box
py={2}
overflow='auto'
display='flex'
flexDirection='column'
component={Container}
>
<HostTabs id={id} />
</Box>
)
}
export default HostDetail

View File

@ -14,12 +14,19 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { HostsTable } from 'client/components/Tables'
import HostTabs from 'client/components/Tabs/Host'
import SplitPane from 'client/components/SplitPane'
function Hosts () {
const [selectedRows, onSelectedRowsChange] = useState([])
const getRowIds = () =>
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
return (
<Box
height={1}
@ -29,7 +36,18 @@ function Hosts () {
flexDirection='column'
component={Container}
>
<HostsTable />
<SplitPane>
<HostsTable onSelectedRowsChange={onSelectedRowsChange} />
{selectedRows?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
{selectedRows?.length === 1
? <HostTabs id={selectedRows[0]?.values.ID} />
: <pre><code>{getRowIds()}</code></pre>
}
</div>
)}
</SplitPane>
</Box>
)
}

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* 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 { useParams, Redirect } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
import UserTabs from 'client/components/Tabs/User'
function UserDetail () {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to='/' />
}
return (
<Box
py={2}
overflow='auto'
display='flex'
flexDirection='column'
component={Container}
>
<UserTabs id={id} />
</Box>
)
}
export default UserDetail

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* 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 { useParams, Redirect } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
import VmTabs from 'client/components/Tabs/Vm'
function VirtualMachineDetail () {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to='/' />
}
return (
<Box
py={2}
overflow='auto'
display='flex'
flexDirection='column'
component={Container}
>
<VmTabs id={id} />
</Box>
)
}
export default VirtualMachineDetail

View File

@ -14,14 +14,15 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect } from 'react'
import { useHistory, useParams } from 'react-router'
import { Container } from '@material-ui/core'
import { useGeneralApi } from 'client/features/General'
import { useVmTemplateApi } from 'client/features/One'
import { useVmTemplateApi, useUserApi, useVmGroupApi } from 'client/features/One'
import { InstantiateForm } from 'client/components/Forms/VmTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
import { isDevelopment } from 'client/utils'
function InstantiateVmTemplate () {
const history = useHistory()
@ -29,27 +30,28 @@ function InstantiateVmTemplate () {
const initialValues = { template: { ID: templateId } }
const { enqueueInfo } = useGeneralApi()
const { getUsers } = useUserApi()
const { getVmGroups } = useVmGroupApi()
const { instantiate } = useVmTemplateApi()
const onSubmit = async formData => {
const {
template: [{ ID, NAME }] = [],
configuration: { name, instances, ...configuration } = {}
} = formData
const onSubmit = async ([templateSelected, templates]) => {
try {
const { ID, NAME } = templateSelected
await Promise.all([...new Array(instances)]
.map((_, idx) => {
const replacedName = name?.replace(/%idx/gi, idx)
const data = { ...configuration, name: replacedName }
await Promise.all(templates.map(template => instantiate(ID, template)))
return instantiate(ID, data)
})
)
history.push(templateId ? PATH.TEMPLATE.VMS.LIST : PATH.INSTANCE.VMS.LIST)
enqueueInfo(`VM Template instantiated x${instances} - ${NAME}`)
history.push(templateId ? PATH.TEMPLATE.VMS.LIST : PATH.INSTANCE.VMS.LIST)
enqueueInfo(`VM Template instantiated x${templates.length} - #${ID} ${NAME}`)
} catch (err) {
isDevelopment() && console.error(err)
}
}
useEffect(() => {
getUsers()
getVmGroups()
}, [])
return (
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
<InstantiateForm initialValues={initialValues} onSubmit={onSubmit} />

View File

@ -14,12 +14,20 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { VmTemplatesTable } from 'client/components/Tables'
import VmTemplateTabs from 'client/components/Tabs/VmTemplate'
import SplitPane from 'client/components/SplitPane'
function VmTemplates () {
const [selectedRows, onSelectedRowsChange] = useState([])
const getRowIds = () =>
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
return (
<Box
height={1}
@ -29,7 +37,18 @@ function VmTemplates () {
flexDirection='column'
component={Container}
>
<VmTemplatesTable />
<SplitPane>
<VmTemplatesTable onSelectedRowsChange={onSelectedRowsChange} />
{selectedRows?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
{selectedRows?.length === 1
? <VmTemplateTabs id={selectedRows[0]?.values.ID} />
: <pre><code>{getRowIds()}</code></pre>
}
</div>
)}
</SplitPane>
</Box>
)
}

View File

@ -18,13 +18,14 @@ import { useCallback } from 'react'
import { useDispatch, useSelector, shallowEqual } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import { RESOURCES } from 'client/features/One'
import * as actions from 'client/features/Auth/actions'
import * as actionsView from 'client/features/Auth/actionsView'
import { name as authSlice } from 'client/features/Auth/slice'
import { name as oneSlice, RESOURCES } from 'client/features/One/slice'
export const useAuth = () => {
const auth = useSelector(state => state.auth, shallowEqual)
const groups = useSelector(state => state.one[RESOURCES.group], shallowEqual)
const auth = useSelector(state => state[authSlice], shallowEqual)
const groups = useSelector(state => state[oneSlice][RESOURCES.group], shallowEqual)
const { user, jwt, view, views, isLoginInProgress } = auth

View File

@ -38,7 +38,7 @@ const initial = () => ({
isLoading: false
})
const { actions, reducer } = createSlice({
const { name, actions, reducer } = createSlice({
name: 'auth',
initialState: ({ ...initial(), firstRender: true }),
extraReducers: builder => {
@ -84,4 +84,4 @@ const { actions, reducer } = createSlice({
}
})
export { actions, reducer }
export { name, actions, reducer }

View File

@ -19,6 +19,7 @@ export const fixMenu = createAction('Fix menu')
export const changeZone = createAction('Change zone')
export const changeLoading = createAction('Change loading')
export const changeTitle = createAction('Change title')
export const changeAppTitle = createAction('Change App title')
export const dismissSnackbar = createAction('Dismiss snackbar')
export const deleteSnackbar = createAction('Delete snackbar')

View File

@ -17,10 +17,11 @@
import { useDispatch, useSelector } from 'react-redux'
import * as actions from 'client/features/General/actions'
import { name } from 'client/features/General/slice'
import { generateKey } from 'client/utils'
export const useGeneral = () => (
useSelector(state => state.general)
useSelector(state => state[name])
)
export const useGeneralApi = () => {
@ -30,6 +31,7 @@ export const useGeneralApi = () => {
fixMenu: isFixed => dispatch(actions.fixMenu(isFixed)),
changeLoading: isLoading => dispatch(actions.changeLoading(isLoading)),
changeTitle: title => dispatch(actions.changeTitle(title)),
changeAppTitle: appTitle => dispatch(actions.changeAppTitle(appTitle)),
changeZone: zone => dispatch(actions.changeZone(zone)),
// dismiss all if no key has been defined

View File

@ -22,13 +22,14 @@ import { generateKey } from 'client/utils'
const initial = {
zone: 0,
title: null,
appTitle: null,
isLoading: false,
isFixMenu: false,
notifications: []
}
const { reducer } = createSlice({
const { name, reducer } = createSlice({
name: 'general',
initialState: initial,
extraReducers: builder => {
@ -43,6 +44,9 @@ const { reducer } = createSlice({
.addCase(actions.changeTitle, (state, { payload }) => {
return { ...state, title: payload }
})
.addCase(actions.changeAppTitle, (state, { payload }) => {
return { ...state, appTitle: payload }
})
.addCase(actions.changeZone, (state, { payload }) => {
return { ...state, zone: payload }
})
@ -111,4 +115,4 @@ const { reducer } = createSlice({
}
})
export { reducer }
export { name, reducer }

View File

@ -15,9 +15,10 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useSelector, shallowEqual } from 'react-redux'
import { name } from 'client/features/One/slice'
export const useOne = () => (
useSelector(state => state.one, shallowEqual)
useSelector(state => state[name], shallowEqual)
)
export * from 'client/features/One/application/hooks'
@ -33,6 +34,7 @@ export * from 'client/features/One/provider/hooks'
export * from 'client/features/One/provision/hooks'
export * from 'client/features/One/user/hooks'
export * from 'client/features/One/vm/hooks'
export * from 'client/features/One/vmGroup/hooks'
export * from 'client/features/One/vmTemplate/hooks'
export * from 'client/features/One/vnetwork/hooks'
export * from 'client/features/One/vnetworkTemplate/hooks'

View File

@ -81,8 +81,8 @@ const initial = {
[RESOURCES.document.defaults]: []
}
const { actions, reducer } = createSlice({
name: 'pool',
const { name, actions, reducer } = createSlice({
name: 'one',
initialState: initial,
extraReducers: builder => {
builder
@ -127,4 +127,4 @@ const { actions, reducer } = createSlice({
}
})
export { actions, reducer, RESOURCES }
export { name, actions, reducer, RESOURCES }

View File

@ -0,0 +1,29 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
import { createAction } from 'client/features/One/utils'
import { vmGroupService } from 'client/features/One/vmGroup/services'
import { RESOURCES } from 'client/features/One/slice'
export const getVmGroup = createAction(
'vmgroup/detail',
vmGroupService.getVmGroup
)
export const getVmGroups = createAction(
'vmgroup/pool',
vmGroupService.getVmGroups,
response => ({ [RESOURCES.vmgroup]: response })
)

Some files were not shown because too many files have changed in this diff Show More