mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-16 22:50:10 +03:00
parent
7f000b89ed
commit
128ed62841
@ -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"
|
||||
|
||||
|
55
src/fireedge/etc/sunstone/admin/cluster-tab.yaml
Normal file
55
src/fireedge/etc/sunstone/admin/cluster-tab.yaml
Normal 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
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
166
src/fireedge/package-lock.json
generated
166
src/fireedge/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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 }) => {
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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([
|
||||
|
@ -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], [
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}))
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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))
|
@ -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)
|
||||
|
@ -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))
|
@ -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
|
||||
)
|
||||
})
|
||||
})
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
||||
}))
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 />}
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
})
|
||||
)
|
||||
|
@ -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: {
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
|
102
src/fireedge/src/client/components/Tabs/Cluster/Info/index.js
Normal file
102
src/fireedge/src/client/components/Tabs/Cluster/Info/index.js
Normal 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
|
@ -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
|
82
src/fireedge/src/client/components/Tabs/Cluster/index.js
Normal file
82
src/fireedge/src/client/components/Tabs/Cluster/index.js
Normal 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
|
@ -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,
|
||||
|
@ -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 })
|
||||
|
132
src/fireedge/src/client/components/Tabs/Host/Info/index.js
Normal file
132
src/fireedge/src/client/components/Tabs/Host/Info/index.js
Normal 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
|
@ -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
|
@ -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
|
96
src/fireedge/src/client/components/Tabs/User/Info/index.js
Normal file
96
src/fireedge/src/client/components/Tabs/User/Info/index.js
Normal 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
|
@ -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
|
82
src/fireedge/src/client/components/Tabs/User/index.js
Normal file
82
src/fireedge/src/client/components/Tabs/User/index.js
Normal 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
|
@ -78,7 +78,7 @@ const InformationPanel = ({ actions, vm = {}, handleResizeCapacity }) => {
|
||||
title: T.ResizeCapacity
|
||||
}}
|
||||
options={[{
|
||||
form: () => ResizeCapacityForm({ vm }),
|
||||
form: () => ResizeCapacityForm(undefined, vm.TEMPLATE),
|
||||
onSubmit: handleResizeCapacity
|
||||
}]}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
@ -56,7 +56,7 @@ const VmSnapshotTab = ({ tabProps: { actions } = {} }) => {
|
||||
title: Tr(T.TakeSnapshot)
|
||||
}}
|
||||
options={[{
|
||||
form: CreateSnapshotForm,
|
||||
form: () => CreateSnapshotForm(),
|
||||
onSubmit: handleSnapshotCreate
|
||||
}]}
|
||||
/>
|
||||
|
@ -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
|
||||
}]}
|
||||
/>
|
||||
|
@ -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}>
|
||||
|
103
src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js
Normal file
103
src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js
Normal 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
|
@ -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
|
@ -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
|
84
src/fireedge/src/client/components/Tabs/VmTemplate/index.js
Normal file
84
src/fireedge/src/client/components/Tabs/VmTemplate/index.js
Normal 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
|
@ -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
|
||||
|
24
src/fireedge/src/client/constants/cluster.js
Normal file
24
src/fireedge/src/client/constants/cluster.js
Normal 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
|
||||
}
|
@ -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'
|
||||
|
@ -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'
|
||||
}
|
||||
|
42
src/fireedge/src/client/containers/Clusters/Detail.js
Normal file
42
src/fireedge/src/client/containers/Clusters/Detail.js
Normal 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
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
43
src/fireedge/src/client/containers/Groups/Detail.js
Normal file
43
src/fireedge/src/client/containers/Groups/Detail.js
Normal 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
|
42
src/fireedge/src/client/containers/Hosts/Detail.js
Normal file
42
src/fireedge/src/client/containers/Hosts/Detail.js
Normal 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
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
42
src/fireedge/src/client/containers/Users/Detail.js
Normal file
42
src/fireedge/src/client/containers/Users/Detail.js
Normal 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
|
42
src/fireedge/src/client/containers/VirtualMachines/Detail.js
Normal file
42
src/fireedge/src/client/containers/VirtualMachines/Detail.js
Normal 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
|
@ -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} />
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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'
|
||||
|
@ -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 }
|
||||
|
29
src/fireedge/src/client/features/One/vmGroup/actions.js
Normal file
29
src/fireedge/src/client/features/One/vmGroup/actions.js
Normal 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
Loading…
x
Reference in New Issue
Block a user