From 679455158aebe03564a3e6c0adab5313af6dfd6e Mon Sep 17 00:00:00 2001 From: David Date: Mon, 20 Nov 2023 18:00:18 +0100 Subject: [PATCH] F OpenNebula/one#6122: Groups tab (#2825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: vichansson Co-authored-by: Tino Vázquez --- install.sh | 23 +- src/fireedge/etc/sunstone/cloud/vm-tab.yaml | 161 ++++++++ .../etc/sunstone/cloud/vm-template-tab.yaml | 111 +++++ .../etc/sunstone/groupadmin/backup-tab.yaml | 50 +++ .../etc/sunstone/groupadmin/file-tab.yaml | 50 +++ .../etc/sunstone/groupadmin/group-tab.yaml | 55 +++ .../etc/sunstone/groupadmin/image-tab.yaml | 64 +++ .../groupadmin/marketplace-app-tab.yaml | 60 +++ .../sunstone/groupadmin/sec-group-tab.yaml | 53 +++ .../etc/sunstone/groupadmin/user-tab.yaml | 70 ++++ .../etc/sunstone/groupadmin/vm-tab.yaml | 161 ++++++++ .../sunstone/groupadmin/vm-template-tab.yaml | 111 +++++ .../etc/sunstone/groupadmin/vnet-tab.yaml | 84 ++++ .../src/client/apps/sunstone/routesOne.js | 15 +- .../src/client/components/Cards/GroupCard.js | 164 ++++++++ .../src/client/components/Cards/UserCard.js | 11 + .../src/client/components/Cards/index.js | 2 + .../client/components/Date/DateRangeFilter.js | 5 + .../FormControl/SwitchController.js | 26 +- .../Group/CreateForm/Steps/General/index.js | 75 ++++ .../Group/CreateForm/Steps/General/schema.js | 122 ++++++ .../CreateForm/Steps/Permissions/index.js | 197 +++++++++ .../CreateForm/Steps/Permissions/schema.js | 387 ++++++++++++++++++ .../Group/CreateForm/Steps/System/index.js | 67 +++ .../Group/CreateForm/Steps/System/schema.js | 47 +++ .../Group/CreateForm/Steps/Views/index.js | 131 ++++++ .../Group/CreateForm/Steps/Views/schema.js | 95 +++++ .../Forms/Group/CreateForm/Steps/index.js | 110 +++++ .../Group/CreateForm}/index.js | 2 +- .../Forms/Group/EditAdminsForm/index.js | 38 ++ .../Forms/Group/EditAdminsForm/schema.js | 52 +++ .../Forms/Group/UpdateForm/Steps/index.js | 92 +++++ .../Group/UpdateForm}/index.js | 3 +- .../client/components/Forms/Group/index.js | 41 ++ .../components/Tables/Groups/actions.js | 121 ++++++ .../components/Tables/Groups/columns.js | 4 + .../client/components/Tables/Groups/index.js | 92 +---- .../client/components/Tables/Groups/row.js | 30 +- .../client/components/Tables/Users/columns.js | 2 +- .../client/components/Tables/Users/index.js | 8 +- .../Accounting/components/CustomizedChart.js | 0 .../Accounting/components/MetricSelector.js | 0 .../Tabs/Accounting/components/index.js | 17 + .../Accounting/helpers/dateUtils.js | 2 +- .../{User => }/Accounting/helpers/index.js | 6 +- .../{User => }/Accounting/helpers/metrics.js | 0 .../Accounting/helpers/useAccountingData.js | 15 +- .../components/Tabs/Accounting/index.js | 361 ++++++++++++++++ .../components/Tabs/Group/Users/Actions.js | 74 ++++ .../components/Tabs/Group/Users/index.js | 127 ++++++ .../src/client/components/Tabs/Group/index.js | 8 + .../Quota/Components/QuotaControls.js | 23 +- .../Components/helpers/scripts/common.js | 223 ++++++++++ .../Quota/Components/helpers/scripts/index.js | 36 ++ .../helpers/scripts/reducer/actions.js | 99 +++++ .../helpers/scripts/reducer/definitions.js | 61 +++ .../scripts/reducer/useQuotaControlReducer.js | 33 ++ .../Components/helpers/scripts/validation.js | 35 ++ .../helpers/subcomponents/HybridInput.js | 216 ++++++++++ .../subcomponents/ResourceIDAutocomplete.js | 206 ++++++++++ .../Components/helpers/subcomponents/index.js | 19 + .../components/Tabs/Quota/Components/index.js | 16 + .../src/client/components/Tabs/Quota/index.js | 317 ++++++++++++++ .../client/components/Tabs/Showback/index.js | 278 +++++++++++++ .../components/Tabs/User/Accounting/index.js | 310 -------------- .../src/client/components/Tabs/User/Group.js | 84 +++- .../components/Tabs/User/Quota/index.js | 301 -------------- .../client/components/Tabs/User/Showback.js | 277 ------------- .../src/client/components/Tabs/User/index.js | 12 +- src/fireedge/src/client/constants/acl.js | 64 +++ src/fireedge/src/client/constants/index.js | 2 + src/fireedge/src/client/constants/system.js | 17 + .../src/client/constants/translates.js | 66 +++ .../src/client/containers/Groups/Create.js | 211 ++++++++++ .../src/client/containers/Groups/index.js | 5 + .../Settings/Authentication/index.js | 2 +- .../containers/Settings/Showback/index.js | 98 +++++ .../src/client/containers/Settings/index.js | 48 ++- .../src/client/features/Auth/hooks.js | 8 +- .../src/client/features/OneApi/acl.js | 94 +++++ .../src/client/features/OneApi/group.js | 138 ++++++- .../src/client/features/OneApi/system.js | 25 +- src/fireedge/src/client/features/OneApi/vm.js | 49 +++ src/fireedge/src/client/models/Group.js | 88 ++++ src/fireedge/src/client/models/acl.js | 131 ++++++ src/fireedge/src/client/utils/helpers.js | 31 +- src/fireedge/src/client/utils/parser/index.js | 2 + .../src/client/utils/parser/parseACL.js | 145 +++++++ src/fireedge/src/server/routes/api/index.js | 1 + .../server/routes/api/sunstone/functions.js | 211 ++++++++-- .../src/server/routes/api/sunstone/index.js | 12 +- .../src/server/routes/api/sunstone/routes.js | 7 + .../src/server/routes/api/vmpool/functions.js | 208 ++++++++++ .../src/server/routes/api/vmpool/index.js | 31 ++ .../src/server/routes/api/vmpool/routes.js | 84 ++++ .../server/utils/constants/commands/group.js | 2 +- 96 files changed, 6822 insertions(+), 1106 deletions(-) create mode 100644 src/fireedge/etc/sunstone/cloud/vm-tab.yaml create mode 100644 src/fireedge/etc/sunstone/cloud/vm-template-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/backup-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/file-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/group-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/image-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/marketplace-app-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/sec-group-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/user-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/vm-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/vm-template-tab.yaml create mode 100644 src/fireedge/etc/sunstone/groupadmin/vnet-tab.yaml create mode 100644 src/fireedge/src/client/components/Cards/GroupCard.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/General/index.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/General/schema.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/index.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/schema.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/index.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/schema.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/index.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/schema.js create mode 100644 src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/index.js rename src/fireedge/src/client/components/{Tabs/User/Quota/Components => Forms/Group/CreateForm}/index.js (92%) create mode 100644 src/fireedge/src/client/components/Forms/Group/EditAdminsForm/index.js create mode 100644 src/fireedge/src/client/components/Forms/Group/EditAdminsForm/schema.js create mode 100644 src/fireedge/src/client/components/Forms/Group/UpdateForm/Steps/index.js rename src/fireedge/src/client/components/{Tabs/User/Accounting/components => Forms/Group/UpdateForm}/index.js (85%) create mode 100644 src/fireedge/src/client/components/Forms/Group/index.js create mode 100644 src/fireedge/src/client/components/Tables/Groups/actions.js rename src/fireedge/src/client/components/Tabs/{User => }/Accounting/components/CustomizedChart.js (100%) rename src/fireedge/src/client/components/Tabs/{User => }/Accounting/components/MetricSelector.js (100%) create mode 100644 src/fireedge/src/client/components/Tabs/Accounting/components/index.js rename src/fireedge/src/client/components/Tabs/{User => }/Accounting/helpers/dateUtils.js (96%) rename src/fireedge/src/client/components/Tabs/{User => }/Accounting/helpers/index.js (82%) rename src/fireedge/src/client/components/Tabs/{User => }/Accounting/helpers/metrics.js (100%) rename src/fireedge/src/client/components/Tabs/{User => }/Accounting/helpers/useAccountingData.js (88%) create mode 100644 src/fireedge/src/client/components/Tabs/Accounting/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Group/Users/Actions.js create mode 100644 src/fireedge/src/client/components/Tabs/Group/Users/index.js rename src/fireedge/src/client/components/Tabs/{User => }/Quota/Components/QuotaControls.js (95%) create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/common.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/actions.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/definitions.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/useQuotaControlReducer.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/validation.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/HybridInput.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/ResourceIDAutocomplete.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/Components/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Quota/index.js create mode 100644 src/fireedge/src/client/components/Tabs/Showback/index.js delete mode 100644 src/fireedge/src/client/components/Tabs/User/Accounting/index.js delete mode 100644 src/fireedge/src/client/components/Tabs/User/Quota/index.js delete mode 100644 src/fireedge/src/client/components/Tabs/User/Showback.js create mode 100644 src/fireedge/src/client/constants/acl.js create mode 100644 src/fireedge/src/client/constants/system.js create mode 100644 src/fireedge/src/client/containers/Groups/Create.js create mode 100644 src/fireedge/src/client/containers/Settings/Showback/index.js create mode 100644 src/fireedge/src/client/features/OneApi/acl.js create mode 100644 src/fireedge/src/client/models/Group.js create mode 100644 src/fireedge/src/client/models/acl.js create mode 100644 src/fireedge/src/client/utils/parser/parseACL.js create mode 100644 src/fireedge/src/server/routes/api/vmpool/functions.js create mode 100644 src/fireedge/src/server/routes/api/vmpool/index.js create mode 100644 src/fireedge/src/server/routes/api/vmpool/routes.js diff --git a/install.sh b/install.sh index d8d604e2622..372c545354f 100755 --- a/install.sh +++ b/install.sh @@ -296,7 +296,9 @@ ETC_DIRS="$ETC_LOCATION/vmm_exec \ $ETC_LOCATION/fireedge/provision/providers.d-extra \ $ETC_LOCATION/fireedge/sunstone \ $ETC_LOCATION/fireedge/sunstone/admin \ - $ETC_LOCATION/fireedge/sunstone/user" + $ETC_LOCATION/fireedge/sunstone/user \ + $ETC_LOCATION/fireedge/sunstone/groupadmin \ + $ETC_LOCATION/fireedge/sunstone/cloud" LIB_DIRS="$LIB_LOCATION/ruby \ $LIB_LOCATION/ruby/opennebula \ @@ -925,6 +927,8 @@ INSTALL_FIREEDGE_ETC_FILES=( FIREEDGE_SUNSTONE_ETC:$ETC_LOCATION/fireedge/sunstone FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN:$ETC_LOCATION/fireedge/sunstone/admin FIREEDGE_SUNSTONE_ETC_VIEW_USER:$ETC_LOCATION/fireedge/sunstone/user + FIREEDGE_SUNSTONE_ETC_VIEW_CLOUD:$ETC_LOCATION/fireedge/sunstone/cloud + FIREEDGE_SUNSTONE_ETC_VIEW_GROUPADMIN:$ETC_LOCATION/fireedge/sunstone/groupadmin ) INSTALL_FIREEDGE_DEV_DIRS=( @@ -2984,7 +2988,8 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \ src/fireedge/etc/sunstone/admin/vdc-tab.yaml \ src/fireedge/etc/sunstone/admin/user-tab.yaml \ src/fireedge/etc/sunstone/admin/backupjobs-tab.yaml \ - src/fireedge/etc/sunstone/admin/host-tab.yaml" + src/fireedge/etc/sunstone/admin/host-tab.yaml \ + src/fireedge/etc/sunstone/admin/group-tab.yaml" FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \ src/fireedge/etc/sunstone/user/vm-template-tab.yaml \ @@ -2995,6 +3000,20 @@ FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \ src/fireedge/etc/sunstone/user/sec-group-tab.yaml \ src/fireedge/etc/sunstone/user/vnet-tab.yaml" +FIREEDGE_SUNSTONE_ETC_VIEW_GROUPADMIN="src/fireedge/etc/sunstone/groupadmin/vm-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/vm-template-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/marketplace-app-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/image-tab.yaml\ + src/fireedge/etc/sunstone/groupadmin/file-tab.yaml\ + src/fireedge/etc/sunstone/groupadmin/backup-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/sec-group-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/vnet-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/user-tab.yaml \ + src/fireedge/etc/sunstone/groupadmin/group-tab.yaml" + +FIREEDGE_SUNSTONE_ETC_VIEW_CLOUD="src/fireedge/etc/sunstone/cloud/vm-tab.yaml \ + src/fireedge/etc/sunstone/cloud/vm-template-tab.yaml" + #----------------------------------------------------------------------------- # OneGate files #----------------------------------------------------------------------------- diff --git a/src/fireedge/etc/sunstone/cloud/vm-tab.yaml b/src/fireedge/etc/sunstone/cloud/vm-tab.yaml new file mode 100644 index 00000000000..9ac909db41d --- /dev/null +++ b/src/fireedge/etc/sunstone/cloud/vm-tab.yaml @@ -0,0 +1,161 @@ +--- +# This file describes the information and actions available in the VM tab + +# Resource + +resource_name: "VM" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + create_app_dialog: true # reference to create_dialog in marketplace-app-tab.yaml + deploy: false + migrate: true + migrate_live: true + hold: true + release: true + suspend: true + resume: true + stop: true + recover: true + reboot: true + reboot-hard: true + poweroff: true + poweroff-hard: true + undeploy: true + undeploy-hard: true + terminate: true + terminate-hard: true + resched: true + unresched: true + save_as_template: false + chown: false + chgrp: false + lock: true + unlock: true + vmrc: true + vnc: true + ssh: true + rdp: true + edit_labels: false + backup: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + owner: true + group: true + type: true + locked: true + ips: true + hostname: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: false + ownership_panel: + enabled: true + actions: + chown: false + chgrp: false + capacity_panel: + enabled: true + actions: + resize_capacity: true + vcenter_panel: + enabled: true + actions: + copy: true + add: false + edit: false + delete: false + lxc_panel: + enabled: true + actions: + copy: true + add: false + edit: false + delete: false + monitoring_panel: + enabled: true + actions: + copy: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: false + delete: false + + storage: + enabled: true + actions: + attach_disk: + enabled: true + not_on: + - firecracker + detach_disk: true + snapshot_disk_create: true + snapshot_disk_rename: true + snapshot_disk_revert: true + snapshot_disk_delete: true + resize_disk: true + disk_saveas: + enabled: true + not_on: + - vcenter + - firecracker + + network: + enabled: true + actions: + attach_nic: true + detach_nic: true + update_nic: true + attach_secgroup: true + detach_secgroup: true + + snapshot: + enabled: true + actions: + snapshot-create: true + snapshot-revert: true + snapshot-delete: true + + backup: + enabled: true + + history: + enabled: true + + sched_actions: + enabled: true + actions: + sched_action_create: true + sched_action_update: true + sched_action_delete: true + charter_create: true + + configuration: + enabled: true + actions: + update_configuration: true + + template: + enabled: true diff --git a/src/fireedge/etc/sunstone/cloud/vm-template-tab.yaml b/src/fireedge/etc/sunstone/cloud/vm-template-tab.yaml new file mode 100644 index 00000000000..58aed30ef86 --- /dev/null +++ b/src/fireedge/etc/sunstone/cloud/vm-template-tab.yaml @@ -0,0 +1,111 @@ +--- +# This file describes the information and actions available in the VM Template tab + +# Resource + +resource_name: "VM-TEMPLATE" + +# Features + +features: + # True to hide the CPU setting in the dialogs + hide_cpu: true + + # False to not scale the CPU. + # An integer value would be used as a multiplier as follows: + # CPU = cpu_factor * VCPU + # Set it to 1 to tie CPU and vCPU. + cpu_factor: 1 + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: false + import_dialog: false + update_dialog: false + instantiate_dialog: true + create_app_dialog: false # reference to create_dialog in marketplace-app-tab.yaml + clone: false + delete: false + chown: false + chgrp: false + lock: false + unlock: false + share: false + unshare: false + edit_labels: false + +# Filters - List of criteria to filter the resources + +filters: + label: true + owner: true + group: true + locked: true + vrouter: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: false + permissions_panel: + enabled: true + actions: + chmod: false + ownership_panel: + enabled: true + actions: + chown: false + chgrp: false + + template: + enabled: true + +# Dialogs + +dialogs: + instantiate_dialog: + information: true + ownership: true + capacity: true + vm_group: true + vcenter: + enabled: true + not_on: + - kvm + - lxc + - firecracker + network: true + storage: true + placement: true + sched_action: true + booting: true + create_dialog: + ownership: true + capacity: true + showback: true + vm_group: true + vcenter: + enabled: true + not_on: + - kvm + - lxc + - firecracker + network: true + storage: true + placement: true + input_output: true + sched_action: true + context: true + booting: true + numa: + enabled: true + not_on: + - lxc + backup: true diff --git a/src/fireedge/etc/sunstone/groupadmin/backup-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/backup-tab.yaml new file mode 100644 index 00000000000..d36f697b9a8 --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/backup-tab.yaml @@ -0,0 +1,50 @@ +--- +# This file describes the information and actions available in the IMAGE tab + +# Resource + +resource_name: "BACKUP" + +# Actions - Which buttons are visible to operate over the resources + +actions: + restore: true + chown: true + chgrp: true + delete: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: true + chgrp: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + vms: + enabled: true + increments: + enabled: true diff --git a/src/fireedge/etc/sunstone/groupadmin/file-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/file-tab.yaml new file mode 100644 index 00000000000..9e0f1449c7b --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/file-tab.yaml @@ -0,0 +1,50 @@ +--- +# This file describes the information and actions available in the FILE tab + +# Resource + +resource_name: "FILE" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + import_dialog: true + disable: true + enable: true + chown: true + chgrp: true + delete: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: true + chgrp: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true \ No newline at end of file diff --git a/src/fireedge/etc/sunstone/groupadmin/group-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/group-tab.yaml new file mode 100644 index 00000000000..d236d07dd39 --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/group-tab.yaml @@ -0,0 +1,55 @@ +--- +# This file describes the information and actions available in the GROUP tab + +# Resource + +resource_name: "GROUP" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + update_dialog: 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 + actions: + rename: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + + user: + enabled: true + actions: + edit_admins: true + + quota: + enabled: true + actions: + quotas_dialog: false + + accounting: + enabled: true + actions: + get_accounting: true + + showback: + enabled: true + actions: + get_showback: true \ No newline at end of file diff --git a/src/fireedge/etc/sunstone/groupadmin/image-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/image-tab.yaml new file mode 100644 index 00000000000..ffc11362b3b --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/image-tab.yaml @@ -0,0 +1,64 @@ +--- +# This file describes the information and actions available in the IMAGE tab + +# Resource + +resource_name: "IMAGE" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + import_dialog: true + dockerfile_dialog: false + clone: true + lock: true + unlock: true + disable: true + enable: true + persistent: true + nonpersistent: true + chown: true + chgrp: true + delete: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: true + chgrp: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + vms: + enabled: true + snapshot: + enabled: true + actions: + snapshot_flatten: true + snapshot_revert: true + snapshot_delete: true \ No newline at end of file diff --git a/src/fireedge/etc/sunstone/groupadmin/marketplace-app-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/marketplace-app-tab.yaml new file mode 100644 index 00000000000..cb989da1f91 --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/marketplace-app-tab.yaml @@ -0,0 +1,60 @@ +--- +# This file describes the information and actions available in the App tab + +# Resource + +resource_name: "MARKETPLACE-APP" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + export: true + download: true + chown: true + chgrp: true + enable: true + disable: true + delete: true + edit_labels: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + owner: true + group: true + state: true + type: true + marketplace: true + zone: true + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: true + chgrp: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + + template: + enabled: true diff --git a/src/fireedge/etc/sunstone/groupadmin/sec-group-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/sec-group-tab.yaml new file mode 100644 index 00000000000..28d57f944fa --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/sec-group-tab.yaml @@ -0,0 +1,53 @@ +--- +# This file describes the information and actions available in the SECURITY GROUP tab + +# Resource + +resource_name: "SECURITY-GROUP" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + update_dialog: true + clone: true + commit: true + chown: true + chgrp: 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 + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: true + chgrp: true + rules_panel: + enabled: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + + vms: + enabled: true diff --git a/src/fireedge/etc/sunstone/groupadmin/user-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/user-tab.yaml new file mode 100644 index 00000000000..7246b24f306 --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/user-tab.yaml @@ -0,0 +1,70 @@ +--- +# This file describes the information and actions available in the USER tab + +# Resource + +resource_name: "USER" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + chgrp: true + delete: true + enable: true + disable: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + + group: + enabled: true + actions: + chgrp: true + + quota: + enabled: true + actions: + quotas_dialog: true + + accounting: + enabled: true + actions: + get_accounting: true + + showback: + enabled: true + actions: + get_showback: true + + authentication: + enabled: true + actions: + change_authentication: true + update_password: true + login_token: true + two_factor_auth: true + public_ssh_key: true + private_ssh_key: true + private_ssh_key_passphrase: true \ No newline at end of file diff --git a/src/fireedge/etc/sunstone/groupadmin/vm-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/vm-tab.yaml new file mode 100644 index 00000000000..9ac909db41d --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/vm-tab.yaml @@ -0,0 +1,161 @@ +--- +# This file describes the information and actions available in the VM tab + +# Resource + +resource_name: "VM" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + create_app_dialog: true # reference to create_dialog in marketplace-app-tab.yaml + deploy: false + migrate: true + migrate_live: true + hold: true + release: true + suspend: true + resume: true + stop: true + recover: true + reboot: true + reboot-hard: true + poweroff: true + poweroff-hard: true + undeploy: true + undeploy-hard: true + terminate: true + terminate-hard: true + resched: true + unresched: true + save_as_template: false + chown: false + chgrp: false + lock: true + unlock: true + vmrc: true + vnc: true + ssh: true + rdp: true + edit_labels: false + backup: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + owner: true + group: true + type: true + locked: true + ips: true + hostname: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: false + ownership_panel: + enabled: true + actions: + chown: false + chgrp: false + capacity_panel: + enabled: true + actions: + resize_capacity: true + vcenter_panel: + enabled: true + actions: + copy: true + add: false + edit: false + delete: false + lxc_panel: + enabled: true + actions: + copy: true + add: false + edit: false + delete: false + monitoring_panel: + enabled: true + actions: + copy: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: false + delete: false + + storage: + enabled: true + actions: + attach_disk: + enabled: true + not_on: + - firecracker + detach_disk: true + snapshot_disk_create: true + snapshot_disk_rename: true + snapshot_disk_revert: true + snapshot_disk_delete: true + resize_disk: true + disk_saveas: + enabled: true + not_on: + - vcenter + - firecracker + + network: + enabled: true + actions: + attach_nic: true + detach_nic: true + update_nic: true + attach_secgroup: true + detach_secgroup: true + + snapshot: + enabled: true + actions: + snapshot-create: true + snapshot-revert: true + snapshot-delete: true + + backup: + enabled: true + + history: + enabled: true + + sched_actions: + enabled: true + actions: + sched_action_create: true + sched_action_update: true + sched_action_delete: true + charter_create: true + + configuration: + enabled: true + actions: + update_configuration: true + + template: + enabled: true diff --git a/src/fireedge/etc/sunstone/groupadmin/vm-template-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/vm-template-tab.yaml new file mode 100644 index 00000000000..322f76a4929 --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/vm-template-tab.yaml @@ -0,0 +1,111 @@ +--- +# This file describes the information and actions available in the VM Template tab + +# Resource + +resource_name: "VM-TEMPLATE" + +# Features + +features: + # True to hide the CPU setting in the dialogs + hide_cpu: true + + # False to not scale the CPU. + # An integer value would be used as a multiplier as follows: + # CPU = cpu_factor * VCPU + # Set it to 1 to tie CPU and vCPU. + cpu_factor: 1 + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: true + import_dialog: false + update_dialog: true + instantiate_dialog: true + create_app_dialog: false # reference to create_dialog in marketplace-app-tab.yaml + clone: true + delete: true + chown: false + chgrp: false + lock: true + unlock: true + share: true + unshare: true + edit_labels: false + +# Filters - List of criteria to filter the resources + +filters: + label: true + owner: true + group: true + locked: true + vrouter: true + + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: true + chgrp: true + + template: + enabled: true + +# Dialogs + +dialogs: + instantiate_dialog: + information: true + ownership: true + capacity: true + vm_group: true + vcenter: + enabled: true + not_on: + - kvm + - lxc + - firecracker + network: true + storage: true + placement: true + sched_action: true + booting: true + create_dialog: + ownership: true + capacity: true + showback: true + vm_group: true + vcenter: + enabled: true + not_on: + - kvm + - lxc + - firecracker + network: true + storage: true + placement: true + input_output: true + sched_action: true + context: true + booting: true + numa: + enabled: true + not_on: + - lxc + backup: true diff --git a/src/fireedge/etc/sunstone/groupadmin/vnet-tab.yaml b/src/fireedge/etc/sunstone/groupadmin/vnet-tab.yaml new file mode 100644 index 00000000000..2b12a2327b8 --- /dev/null +++ b/src/fireedge/etc/sunstone/groupadmin/vnet-tab.yaml @@ -0,0 +1,84 @@ +--- +# This file describes the information and actions available in the VIRTUAL NETWORK tab + +# Resource + +resource_name: "VIRTUAL-NETWORK" + +# Actions - Which buttons are visible to operate over the resources + +actions: + create_dialog: false + import_dialog: false + instantiate_dialog: true + reserve_dialog: true + update_dialog: true + change_cluster: false + chown: false + chgrp: false + lock: true + unlock: true + delete: true + +# Filters - List of criteria to filter the resources + +filters: + label: true + state: true + owner: true + group: true + locked: true + vn_mad: true + +# Info Tabs - Which info tabs are used to show extended information + +info-tabs: + info: + enabled: true + information_panel: + enabled: true + actions: + rename: true + permissions_panel: + enabled: true + actions: + chmod: true + ownership_panel: + enabled: true + actions: + chown: false + chgrp: false + qos_panel: + enabled: true + attributes_panel: + enabled: true + actions: + copy: true + add: true + edit: true + delete: true + + address: + enabled: true + actions: + add_ar: false + update_ar: true + delete_ar: true + + lease: + enabled: true + actions: + hold_lease: true + release_lease: true + + security: + enabled: true + actions: + add_secgroup: true + delete_secgroup: true + + virtual_router: + enabled: true + + cluster: + enabled: true diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 7306c25985e..03373fa420b 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -223,6 +223,9 @@ const Groups = loadable(() => import('client/containers/Groups'), { const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), { ssr: false, }) +const CreateGroup = loadable(() => import('client/containers/Groups/Create'), { + ssr: false, +}) const VDCs = loadable(() => import('client/containers/VDCs'), { ssr: false }) @@ -364,6 +367,7 @@ export const PATH = { GROUPS: { LIST: `/${RESOURCE_NAMES.GROUP}`, DETAIL: `/${RESOURCE_NAMES.GROUP}/:id`, + CREATE: `/${RESOURCE_NAMES.GROUP}/create`, }, VDCS: { LIST: `/${RESOURCE_NAMES.VDC}`, @@ -716,6 +720,11 @@ const ENDPOINTS = [ path: PATH.SYSTEM.USERS.DETAIL, Component: UserDetail, }, + { + title: T.Groups, + path: PATH.SYSTEM.GROUPS.CREATE, + Component: CreateGroup, + }, { title: T.Groups, path: PATH.SYSTEM.GROUPS.LIST, @@ -748,12 +757,6 @@ const ENDPOINTS = [ path: PATH.SYSTEM.VDCS.DETAIL, Component: VDCDetail, }, - // { - // title: T.Group, - // description: (params) => `#${params?.id}`, - // path: PATH.SYSTEM.GROUPS.DETAIL, - // Component: GroupDetail, - // }, ], }, ] diff --git a/src/fireedge/src/client/components/Cards/GroupCard.js b/src/fireedge/src/client/components/Cards/GroupCard.js new file mode 100644 index 00000000000..5587f7caa7e --- /dev/null +++ b/src/fireedge/src/client/components/Cards/GroupCard.js @@ -0,0 +1,164 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { Component, useMemo } from 'react' +import { Group, ModernTv, HardDrive, Network, BoxIso } from 'iconoir-react' +import { Typography, Grid, Box, Tooltip } from '@mui/material' + +import { LinearProgressWithTooltip } from 'client/components/Status' +import { getQuotaUsage } from 'client/models/Group' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' + +/** + * UserCard component to display user details and quota usage. + * + * @param {object} props - Component props + * @param {object} props.group - Group details + * @param {object} props.rootProps - Additional props for the root element + * @returns {Component} UserCard component + */ +const GroupCard = ({ group, rootProps }) => { + const { + ID, + NAME, + TOTAL_USERS, + VM_QUOTA, + DATASTORE_QUOTA, + NETWORK_QUOTA, + IMAGE_QUOTA, + } = group + + const vmQuotaUsage = useMemo(() => getQuotaUsage('VM', VM_QUOTA), [VM_QUOTA]) + const datastoreQuotaUsage = useMemo( + () => getQuotaUsage('DATASTORE', DATASTORE_QUOTA), + [DATASTORE_QUOTA] + ) + const networkQuotaUsage = useMemo( + () => getQuotaUsage('NETWORK', NETWORK_QUOTA), + [NETWORK_QUOTA] + ) + const imageQuotaUsage = useMemo( + () => getQuotaUsage('IMAGE', IMAGE_QUOTA), + [IMAGE_QUOTA] + ) + + return ( + + + + {NAME} + + + + {`#${ID}`} + + + + + + {TOTAL_USERS} + + + + + + + + + + + } + /> + } + /> + + + } + /> + } + /> + + + + + ) +} + +GroupCard.propTypes = { + group: PropTypes.shape({ + ID: PropTypes.string.isRequired, + NAME: PropTypes.string.isRequired, + TOTAL_USERS: PropTypes.number.isRequired, + VM_QUOTA: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.arrayOf(PropTypes.any), + ]), + DATASTORE_QUOTA: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.arrayOf(PropTypes.any), + ]), + NETWORK_QUOTA: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.arrayOf(PropTypes.any), + ]), + IMAGE_QUOTA: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.arrayOf(PropTypes.any), + ]), + }).isRequired, + rootProps: PropTypes.shape({ + className: PropTypes.string, + }), +} + +GroupCard.displayName = 'GroupCard' + +export default GroupCard diff --git a/src/fireedge/src/client/components/Cards/UserCard.js b/src/fireedge/src/client/components/Cards/UserCard.js index 3ed332a3d13..470436ff3a4 100644 --- a/src/fireedge/src/client/components/Cards/UserCard.js +++ b/src/fireedge/src/client/components/Cards/UserCard.js @@ -23,6 +23,7 @@ import { HardDrive, Network, BoxIso, + Wrench, } from 'iconoir-react' import { Typography, Grid, Box, Tooltip } from '@mui/material' @@ -32,6 +33,9 @@ import { } from 'client/components/Status' import { getState, getQuotaUsage } from 'client/models/User' +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' + /** * UserCard component to display user details and quota usage. * @@ -45,6 +49,7 @@ const UserCard = ({ user, rootProps }) => { ID, NAME, GNAME, + IS_ADMIN_GROUP, ENABLED, AUTH_DRIVER, VM_QUOTA, @@ -89,6 +94,11 @@ const UserCard = ({ user, rootProps }) => { {NAME} + {IS_ADMIN_GROUP && ( + + + + )} {!+ENABLED && ( @@ -167,6 +177,7 @@ UserCard.propTypes = { ID: PropTypes.string.isRequired, NAME: PropTypes.string.isRequired, GNAME: PropTypes.string.isRequired, + IS_ADMIN_GROUP: PropTypes.bool, ENABLED: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) .isRequired, AUTH_DRIVER: PropTypes.string.isRequired, diff --git a/src/fireedge/src/client/components/Cards/index.js b/src/fireedge/src/client/components/Cards/index.js index 7e8ad70a233..07af98ab44c 100644 --- a/src/fireedge/src/client/components/Cards/index.js +++ b/src/fireedge/src/client/components/Cards/index.js @@ -41,6 +41,7 @@ import ServiceTemplateCard from 'client/components/Cards/ServiceTemplateCard' import SnapshotCard from 'client/components/Cards/SnapshotCard' import TierCard from 'client/components/Cards/TierCard' import UserCard from 'client/components/Cards/UserCard' +import GroupCard from 'client/components/Cards/GroupCard' import VirtualDataCenterCard from 'client/components/Cards/VirtualDataCenterCard' import VmGroupCard from 'client/components/Cards/VmGroupCard' import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard' @@ -76,6 +77,7 @@ export { SnapshotCard, TierCard, UserCard, + GroupCard, VirtualDataCenterCard, VmGroupCard, VirtualMachineCard, diff --git a/src/fireedge/src/client/components/Date/DateRangeFilter.js b/src/fireedge/src/client/components/Date/DateRangeFilter.js index df6ecbb841a..8930bf95e5e 100644 --- a/src/fireedge/src/client/components/Date/DateRangeFilter.js +++ b/src/fireedge/src/client/components/Date/DateRangeFilter.js @@ -26,12 +26,14 @@ import { DateTime } from 'luxon' * @param {string} props.initialStartDate - The initial start date value. * @param {string} props.initialEndDate - The initial end date value. * @param {Function} props.onDateChange - Callback function when date changes. + * @param {object} props.views - Views to format in component * @returns {Component} DateRangeFilter component. */ export const DateRangeFilter = ({ initialStartDate, initialEndDate, onDateChange, + views, }) => { const [dateRange, setDateRange] = useState({ startDate: initialStartDate, @@ -62,6 +64,7 @@ export const DateRangeFilter = ({ renderInput={(params) => ( )} + views={views} /> ( )} + views={views} /> @@ -82,4 +86,5 @@ DateRangeFilter.propTypes = { initialStartDate: PropTypes.instanceOf(DateTime).isRequired, initialEndDate: PropTypes.instanceOf(DateTime).isRequired, onDateChange: PropTypes.func.isRequired, + views: PropTypes.array, } diff --git a/src/fireedge/src/client/components/FormControl/SwitchController.js b/src/fireedge/src/client/components/FormControl/SwitchController.js index 013a46fef3f..4949a875929 100644 --- a/src/fireedge/src/client/components/FormControl/SwitchController.js +++ b/src/fireedge/src/client/components/FormControl/SwitchController.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { memo, useCallback } from 'react' +import { memo, useCallback, useEffect } from 'react' import PropTypes from 'prop-types' import { @@ -23,7 +23,7 @@ import { FormHelperText, Switch, } from '@mui/material' -import { useController } from 'react-hook-form' +import { useController, useWatch } from 'react-hook-form' import { ErrorHelper, Tooltip } from 'client/components/FormControl' import { Tr, labelCanBeTranslated } from 'client/components/HOC' @@ -45,6 +45,8 @@ const SwitchController = memo( fieldProps = {}, readOnly = false, onConditionChange, + watcher, + dependencies, }) => { const { field: { value = false, onChange }, @@ -62,6 +64,21 @@ const SwitchController = memo( [onChange, onConditionChange] ) + // Add watcher to know if the dependencies fields have changes + const watch = useWatch({ + name: dependencies, + disabled: dependencies == null, + defaultValue: Array.isArray(dependencies) ? [] : undefined, + }) + + // Execute watcher function define on the field when dependenices fields have changes + useEffect(() => { + if (!watcher || !dependencies || !watch) return + + const watcherValue = watcher(watch, name) + watcherValue !== undefined && onChange(watcherValue) + }, [watch, watcher, dependencies]) + return ( { + // Style for info message + const useStyles = makeStyles(({ palette }) => ({ + groupInfo: { + '&': { + gridColumn: 'span 2', + marginTop: '1em', + backgroundColor: palette.background.paper, + }, + }, + })) + + const classes = useStyles() + + return ( + + + {Tr(T['groups.general.info'])} + + + + ) +} + +/** + * General Group configuration. + * + * @returns {object} General configuration step + */ +const General = () => ({ + id: STEP_ID, + label: T.General, + resolver: SCHEMA, + optionsValidate: { abortEarly: false }, + content: Content, +}) + +General.propTypes = { + data: PropTypes.object, + setFormData: PropTypes.func, +} + +export default General diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/General/schema.js new file mode 100644 index 00000000000..b9a8f4f023c --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/General/schema.js @@ -0,0 +1,122 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants' +import { Field, getObjectSchemaFromFields } from 'client/utils' +import { string, boolean } from 'yup' +import { + USERNAME_FIELD, + AUTH_TYPE_FIELD, + PASSWORD_FIELD, + CONFIRM_PASSWORD_FIELD, +} from 'client/components/Forms/User/CreateForm/Steps/General/schema' + +/** @type {Field} Name field */ +const NAME = { + name: 'name', + label: T['groups.name'], + type: INPUT_TYPES.TEXT, + validation: string() + .trim() + .required() + .default(() => undefined), + grid: { md: 12 }, +} + +/** @type {Field} Name field */ +const ADMIN_USER = { + name: 'adminUser', + label: T['groups.adminUser.title'], + type: INPUT_TYPES.SWITCH, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const ADMIN_USERNAME_FIELD = { + ...USERNAME_FIELD, + name: `username`, + dependOf: ADMIN_USER.name, + type: INPUT_TYPES.TEXT, + htmlType: (type) => !type && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(ADMIN_USER.name, { + is: (adminUser) => adminUser, + then: (schema) => schema.required(), + otherwise: (schema) => schema.strip(), + }), +} + +const ADMIN_AUTH_TYPE_FIELD = { + ...AUTH_TYPE_FIELD, + name: `authType`, + dependOf: ADMIN_USER.name, + htmlType: (type) => !type && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(ADMIN_USER.name, { + is: (adminUser) => adminUser, + then: (schema) => schema.required(), + otherwise: (schema) => schema.strip(), + }), +} + +const ADMIN_PASSWORD_FIELD = { + ...PASSWORD_FIELD, + name: `password`, + dependOf: ADMIN_USER.name, + htmlType: (type) => !type && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(ADMIN_USER.name, { + is: (adminUser) => adminUser, + then: (schema) => schema.required(), + otherwise: (schema) => schema.strip(), + }), +} + +const ADMIN_CONFIRM_PASSWORD_FIELD = { + ...CONFIRM_PASSWORD_FIELD, + name: `confirmPassword`, + dependOf: ADMIN_USER.name, + htmlType: (type) => !type && INPUT_TYPES.HIDDEN, + validation: string() + .trim() + .default(() => undefined) + .when(ADMIN_USER.name, { + is: (adminUser) => adminUser, + then: (schema) => schema.required(), + otherwise: (schema) => schema.strip(), + }) + .test('passwords-match', T.PasswordsMustMatch, function (value) { + return this.parent.password === value + }), +} + +const FIELDS = [ + NAME, + ADMIN_USER, + ADMIN_USERNAME_FIELD, + ADMIN_AUTH_TYPE_FIELD, + ADMIN_PASSWORD_FIELD, + ADMIN_CONFIRM_PASSWORD_FIELD, +] + +const SCHEMA = getObjectSchemaFromFields(FIELDS) + +export { SCHEMA, FIELDS } diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/index.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/index.js new file mode 100644 index 00000000000..41f1d1ba5fa --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/index.js @@ -0,0 +1,197 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import FormWithSchema from 'client/components/Forms/FormWithSchema' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' + +import { generateDocLink } from 'client/utils' + +import { + SCHEMA, + PERMISSIONS_VIEW_FIELDS, + PERMISSIONS_CREATE_FIELDS, + PERMISSIONS_CREATE_FIELDS_ADVANCED, + PERMISSIONS_VIEW_FIELDS_ADVANCED, +} from './schema' +import { + Stack, + Card, + CardContent, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, +} from '@mui/material' + +export const STEP_ID = 'permissions' + +const Content = (version) => ( + +
+ + + + + {Tr(T['groups.permissions.resources'])} + + + + + +
+
+ + + + {Tr(T['groups.permissions.resources'])} + + + + + +
+ + + + {' '} + {Tr(T['groups.permissions.help.title'])}{' '} + + + {' '} + {Tr(T['groups.permissions.help.paragraph.1'])}{' '} + + + {' '} + {Tr(T['groups.permissions.help.paragraph.2'])}{' '} + + + {' '} + {Tr(T['groups.permissions.help.paragraph.3'])}{' '} + + + {' '} + {Tr(T['groups.permissions.help.paragraph.4'])}{' '} + + + {' '} + + {Tr(T['groups.permissions.help.paragraph.link'])} + + + + +
+) + +/** + * Permissions Group configuration. + * + * @param {object} props - Step properties + * @param {string} props.version - Version of ONE + * @returns {object} Permissions configuration step + */ +const Permissions = ({ version }) => ({ + id: STEP_ID, + label: T.Permissions, + resolver: SCHEMA, + optionsValidate: { abortEarly: false }, + content: () => Content(version), +}) + +Permissions.propTypes = { + data: PropTypes.object, + setFormData: PropTypes.func, +} + +export default Permissions diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/schema.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/schema.js new file mode 100644 index 00000000000..4be482f3748 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Permissions/schema.js @@ -0,0 +1,387 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants' +import { getObjectSchemaFromFields } from 'client/utils' +import { boolean, object } from 'yup' + +const VIEW_VM = { + name: 'view.VM', + label: T.VMs, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_HOST = { + name: 'view.HOST', + label: T.Host, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_NET = { + name: 'view.NET', + label: T.VirtualNetworks, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_IMAGE = { + name: 'view.IMAGE', + label: T.Image, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_USER = { + name: 'view.USER', + label: T.User, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_TEMPLATE = { + name: 'view.TEMPLATE', + label: T.Template, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_GROUP = { + name: 'view.GROUP', + label: T.Group, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_DATASTORE = { + name: 'view.DATASTORE', + label: T.Datastore, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_CLUSTER = { + name: 'view.CLUSTER', + label: T.Cluster, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_DOCUMENT = { + name: 'view.DOCUMENT', + label: T.Services, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_ZONE = { + name: 'view.ZONE', + label: T.Zone, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_SECURITY_GROUP = { + name: 'view.SECGROUP', + label: T.SecurityGroup, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_VDC = { + name: 'view.VDC', + label: T.VDC, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_VROUTER = { + name: 'view.VROUTER', + label: T.VirtualRouter, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_MARKETPLACE = { + name: 'view.MARKETPLACE', + label: T.Marketplace, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_MARKETPLACEAPP = { + name: 'view.MARKETPLACEAPP', + label: T.Apps, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_VMGROUP = { + name: 'view.VMGROUP', + label: T.VMGroup, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const VIEW_VNTEMPLATE = { + name: 'view.VNTEMPLATE', + label: T.NetworkTemplate, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const PERMISSIONS_VIEW_FIELDS = [VIEW_VM, VIEW_DOCUMENT] +const PERMISSIONS_VIEW_SCHEMA = getObjectSchemaFromFields( + PERMISSIONS_VIEW_FIELDS +) + +const PERMISSIONS_VIEW_FIELDS_ADVANCED = [ + VIEW_HOST, + VIEW_NET, + VIEW_IMAGE, + VIEW_USER, + VIEW_TEMPLATE, + VIEW_GROUP, + VIEW_DATASTORE, + VIEW_CLUSTER, + VIEW_ZONE, + VIEW_SECURITY_GROUP, + VIEW_VDC, + VIEW_VROUTER, + VIEW_MARKETPLACE, + VIEW_MARKETPLACEAPP, + VIEW_VMGROUP, + VIEW_VNTEMPLATE, +] +const PERMISSIONS_VIEW_SCHEMA_ADVANCED = getObjectSchemaFromFields( + PERMISSIONS_VIEW_FIELDS_ADVANCED +) + +export { + PERMISSIONS_VIEW_FIELDS, + PERMISSIONS_VIEW_SCHEMA, + PERMISSIONS_VIEW_FIELDS_ADVANCED, + PERMISSIONS_VIEW_SCHEMA_ADVANCED, +} + +const CREATE_VM = { + name: 'create.VM', + label: T.VMs, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => true), + grid: { md: 12 }, +} + +const CREATE_HOST = { + name: 'create.HOST', + label: T.Host, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_NET = { + name: 'create.NET', + label: T.VirtualNetworks, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_IMAGE = { + name: 'create.IMAGE', + label: T.Image, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => true), + grid: { md: 12 }, +} + +const CREATE_USER = { + name: 'create.USER', + label: T.User, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_TEMPLATE = { + name: 'create.TEMPLATE', + label: T.Template, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => true), + grid: { md: 12 }, +} + +const CREATE_GROUP = { + name: 'create.GROUP', + label: T.Group, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_DATASTORE = { + name: 'create.DATASTORE', + label: T.Datastore, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_CLUSTER = { + name: 'create.CLUSTER', + label: T.Cluster, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_DOCUMENT = { + name: 'create.DOCUMENT', + label: T.Services, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => true), + grid: { md: 12 }, +} + +const CREATE_ZONE = { + name: 'create.ZONE', + label: T.Zone, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_SECURITY_GROUP = { + name: 'create.SECGROUP', + label: T.SecurityGroup, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => true), + grid: { md: 12 }, +} + +const CREATE_VDC = { + name: 'create.VDC', + label: T.VDC, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_VROUTER = { + name: 'create.VROUTER', + label: T.VirtualRouter, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => true), + grid: { md: 12 }, +} + +const CREATE_MARKETPLACE = { + name: 'create.MARKETPLACE', + label: T.Marketplace, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_MARKETPLACEAPP = { + name: 'create.MARKETPLACEAPP', + label: T.Apps, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_VMGROUP = { + name: 'create.VMGROUP', + label: T.VMGroup, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const CREATE_VNTEMPLATE = { + name: 'create.VNTEMPLATE', + label: T.NetworkTemplate, + type: INPUT_TYPES.CHECKBOX, + validation: boolean().default(() => false), + grid: { md: 12 }, +} + +const PERMISSIONS_CREATE_FIELDS = [ + CREATE_VM, + CREATE_NET, + CREATE_SECURITY_GROUP, + CREATE_VROUTER, + CREATE_IMAGE, + CREATE_TEMPLATE, + CREATE_DOCUMENT, +] +const PERMISSIONS_CREATE_SCHEMA = getObjectSchemaFromFields( + PERMISSIONS_CREATE_FIELDS +) + +const PERMISSIONS_CREATE_FIELDS_ADVANCED = [ + CREATE_HOST, + CREATE_USER, + CREATE_GROUP, + CREATE_DATASTORE, + CREATE_CLUSTER, + CREATE_ZONE, + CREATE_VDC, + CREATE_MARKETPLACE, + CREATE_MARKETPLACEAPP, + CREATE_VMGROUP, + CREATE_VNTEMPLATE, +] +const PERMISSIONS_CREATE_SCHEMA_ADVANCED = getObjectSchemaFromFields( + PERMISSIONS_CREATE_FIELDS_ADVANCED +) + +export { + PERMISSIONS_CREATE_FIELDS, + PERMISSIONS_CREATE_SCHEMA, + PERMISSIONS_CREATE_FIELDS_ADVANCED, + PERMISSIONS_CREATE_SCHEMA_ADVANCED, +} + +/** + * @returns {object} I/O schema + */ +export const SCHEMA = () => + object() + .concat(PERMISSIONS_VIEW_SCHEMA) + .concat(PERMISSIONS_VIEW_SCHEMA_ADVANCED) + .concat(PERMISSIONS_CREATE_SCHEMA) + .concat(PERMISSIONS_CREATE_SCHEMA_ADVANCED) diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/index.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/index.js new file mode 100644 index 00000000000..8df452e39ad --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/index.js @@ -0,0 +1,67 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import FormWithSchema from 'client/components/Forms/FormWithSchema' + +import { T } from 'client/constants' + +import { SCHEMA, SYSTEM_FIELDS } from './schema' +import { Stack } from '@mui/material' + +export const STEP_ID = 'system' + +/** + * Return content of the step. + * + * @returns {object} - Content of the step + */ +const Content = () => ( + + + +) + +/** + * Advanced Options group configuration that includes views and system. + * + * @returns {object} AdvancedOptions configuration step + */ +const System = () => ({ + id: STEP_ID, + label: T.System, + resolver: SCHEMA, + optionsValidate: { abortEarly: false }, + content: Content, +}) + +System.propTypes = { + data: PropTypes.object, + setFormData: PropTypes.func, +} + +export default System diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/schema.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/schema.js new file mode 100644 index 00000000000..a7b70d4677e --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/System/schema.js @@ -0,0 +1,47 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants' +import { Field, getObjectSchemaFromFields } from 'client/utils' +import { boolean } from 'yup' + +/** @type {Field} Make new images persistent by default field */ +const DEFAULT_IMAGE_PERSISTENT_NEW = { + name: 'OPENNEBULA.DEFAULT_IMAGE_PERSISTENT_NEW', + label: T['groups.system.defaultImagePersistentNew.title'], + tooltip: T['groups.system.defaultImagePersistentNew.tooltip'], + type: INPUT_TYPES.SWITCH, + validation: boolean() + .yesOrNo() + .default(() => false), + grid: { md: 12 }, +} + +/** @type {Field} Make save-as and clone images persistent by default field */ +const DEFAULT_IMAGE_PERSISTENT = { + name: 'OPENNEBULA.DEFAULT_IMAGE_PERSISTENT', + label: T['groups.system.defaultImagePersistent.title'], + tooltip: T['groups.system.defaultImagePersistent.tooltip'], + type: INPUT_TYPES.SWITCH, + validation: boolean() + .yesOrNo() + .default(() => false), + grid: { md: 12 }, +} + +const SYSTEM_FIELDS = [DEFAULT_IMAGE_PERSISTENT_NEW, DEFAULT_IMAGE_PERSISTENT] +const SCHEMA = getObjectSchemaFromFields(SYSTEM_FIELDS) + +export { SCHEMA, SYSTEM_FIELDS } diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/index.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/index.js new file mode 100644 index 00000000000..615813ee307 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/index.js @@ -0,0 +1,131 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import FormWithSchema from 'client/components/Forms/FormWithSchema' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' + +import { generateDocLink } from 'client/utils' + +import { VIEWS_SCHEMA, VIEWS_FIELDS } from './schema' +import { Stack, Card, CardContent, Typography } from '@mui/material' + +export const STEP_ID = 'views' + +const Content = (views, version) => { + // Get view fields + const fieldsUser = VIEWS_FIELDS(views, false) + const fieldsAdmin = VIEWS_FIELDS(views, true) + + return ( + + + + + + + {' '} + {Tr(T['groups.views.help.title1'])}{' '} + + + {' '} + {Tr(T['groups.views.help.paragraph.1'])}{' '} + + + {' '} + {Tr(T['groups.views.help.paragraph.2'])}{' '} + + + {' '} + {Tr(T['groups.views.help.paragraph.3'])}{' '} + + + + {Tr(T['groups.views.help.paragraph.link'])} + + + + + + ) +} + +/** + * View Options group configuration that includes views and system. + * + * @param {object} props - Object with properties + * @param {Array} props.views - List of views + * @param {string} props.version - Version of ONE + * @returns {object} ViewOptions configuration step + */ +const ViewOptions = ({ views, version }) => ({ + id: STEP_ID, + label: T['groups.views.title'], + resolver: VIEWS_SCHEMA(views), + optionsValidate: { abortEarly: false }, + content: () => Content(views, version), +}) + +ViewOptions.propTypes = { + data: PropTypes.object, + setFormData: PropTypes.func, +} + +export default ViewOptions diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/schema.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/schema.js new file mode 100644 index 00000000000..a54ddf8fb31 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/Views/schema.js @@ -0,0 +1,95 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants' +import { Field, getObjectSchemaFromFields, arrayToOptions } from 'client/utils' +import { boolean, string } from 'yup' + +/** + * Creates a function to define the fieldProps and can disable a switch using his own name. + * + * @param {string} name - Name of the field + * @returns {Function} - The function to check if the field is disabled + */ +const checkDisabled = (name) => (type) => ({ disabled: type === name }) + +/** @type {Field} View field */ +const VIEW = (name, admin) => ({ + name: admin ? `GROUP_ADMIN_VIEWS.${name}` : `VIEWS.${name}`, + label: name, + type: INPUT_TYPES.SWITCH, + dependOf: admin ? `GROUP_ADMIN_DEFAULT_VIEW` : `DEFAULT_VIEW`, + watcher: (value, nameField) => { + // Check the switch if it is the default view + const view = + nameField.split('.').length === 3 ? nameField.split('.')[2] : undefined + if (value === view) return true + }, + validation: boolean(), + grid: { md: 12 }, +}) + +/** @type {Field} Default view field */ +const DEFAULT_VIEW = (views, admin) => ({ + name: admin ? `GROUP_ADMIN_DEFAULT_VIEW` : `DEFAULT_VIEW`, + label: T['groups.views.default'], + type: INPUT_TYPES.SELECT, + values: arrayToOptions(views, { + addEmpty: false, + }), + validation: string().default(() => (admin ? 'groupadmin' : 'cloud')), + grid: { md: 9 }, +}) + +/** + * Generates view fields. + * + * @param {Array} views - Array with view names + * @param {boolean} admin - Create field for the admin group user + * @returns {Array} - Array of view fields + */ +const VIEWS_FIELDS = (views, admin) => { + // Add first the field of default view + const fields = [DEFAULT_VIEW(views, admin)] + + // Iterate over each view to generate a field + views.forEach((view) => { + // Create a view field + const newView = VIEW(view, admin) + + // Add function to disable or not the field (the field will be disabled if the default value is equals to the name of the field) + newView.fieldProps = checkDisabled(view) + + // Push field + fields.push(newView) + }) + + // Return fields + return fields +} + +/** + * Generates view schema. + * + * @param {Array} views - Array with view names + * @returns {object} - Schema of view fields + */ +const VIEWS_SCHEMA = (views) => + getObjectSchemaFromFields(VIEWS_FIELDS(views, false)).concat( + getObjectSchemaFromFields(VIEWS_FIELDS(views, true)) + ) + +/** Export fields and schema */ +export { VIEWS_SCHEMA, VIEWS_FIELDS } diff --git a/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/index.js new file mode 100644 index 00000000000..a74cf9fe335 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/Steps/index.js @@ -0,0 +1,110 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 General, { + STEP_ID as GENERAL_ID, +} from 'client/components/Forms/Group/CreateForm/Steps/General' + +import Permissions, { + STEP_ID as PERMISSIONS_ID, +} from 'client/components/Forms/Group/CreateForm/Steps/Permissions' + +import Views, { + STEP_ID as VIEWS_ID, +} from 'client/components/Forms/Group/CreateForm/Steps/Views' + +import System, { + STEP_ID as SYSTEM_ID, +} from 'client/components/Forms/Group/CreateForm/Steps/System' + +import { createSteps } from 'client/utils' + +import { ACL_RESOURCES } from 'client/constants' + +/** + * Create steps for Groups Create Form: + * 1. General: Name of the group and create or not admin user + * 2. Permissions: Set permissions about some resources for the group + * 3. Advanced options: Options that will be set on group template + * 3.1. Views: Views of the group + * 3.2. Default options + */ +const Steps = createSteps([General, Permissions, Views, System], { + transformBeforeSubmit: (formData) => { + // Get data from steps + const { [GENERAL_ID]: generalData } = formData + const { [PERMISSIONS_ID]: permissionsData } = formData + const { [VIEWS_ID]: views } = formData + const { [SYSTEM_ID]: system } = formData + + const response = {} + + // General info + response.group = { + name: generalData?.name, + } + + // Admin user + if (generalData?.adminUser) { + response.groupAdmin = { + adminUser: generalData?.adminUser, + username: generalData?.username, + password: generalData?.password, + authType: generalData?.authType, + } + } + + // Permissions + const formCreateResources = permissionsData.create + const formViewResources = permissionsData.view + + const createResources = Object.entries(formCreateResources) + .filter((resource) => resource[1]) + .map((resource) => ACL_RESOURCES[resource[0]]) + const viewResources = Object.entries(formViewResources) + .filter((resource) => resource[1]) + .map((resource) => ACL_RESOURCES[resource[0]]) + + response.permissions = { + view: viewResources, + create: createResources, + } + + // Views + response.views = { + FIREEDGE: { + VIEWS: Object.entries(views?.VIEWS) + .filter((resource) => resource[1]) + .map((resource) => resource[0]) + .join(','), + DEFAULT_VIEW: views?.DEFAULT_VIEW, + GROUP_ADMIN_VIEWS: Object.entries(views?.GROUP_ADMIN_VIEWS) + .filter((resource) => resource[1]) + .map((resource) => resource[0]) + .join(','), + GROUP_ADMIN_DEFAULT_VIEW: views?.GROUP_ADMIN_DEFAULT_VIEW, + }, + } + + // System + response.system = { + OPENNEBULA: system?.OPENNEBULA, + } + + return response + }, +}) + +export default Steps diff --git a/src/fireedge/src/client/components/Tabs/User/Quota/Components/index.js b/src/fireedge/src/client/components/Forms/Group/CreateForm/index.js similarity index 92% rename from src/fireedge/src/client/components/Tabs/User/Quota/Components/index.js rename to src/fireedge/src/client/components/Forms/Group/CreateForm/index.js index 3de31d834e6..e2d90b2d674 100644 --- a/src/fireedge/src/client/components/Tabs/User/Quota/Components/index.js +++ b/src/fireedge/src/client/components/Forms/Group/CreateForm/index.js @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -export { QuotaControls } from 'client/components/Tabs/User/Quota/Components/QuotaControls' +export { default } from 'client/components/Forms/Group/CreateForm/Steps' diff --git a/src/fireedge/src/client/components/Forms/Group/EditAdminsForm/index.js b/src/fireedge/src/client/components/Forms/Group/EditAdminsForm/index.js new file mode 100644 index 00000000000..d77f15e08c2 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/EditAdminsForm/index.js @@ -0,0 +1,38 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { createForm } from 'client/utils' +import { + SCHEMA, + FIELDS, +} from 'client/components/Forms/Group/EditAdminsForm/schema' + +const EditAdminsForm = createForm(SCHEMA, FIELDS, { + transformInitialValue: (admins, schema) => ({ + ...schema.cast({ admins }, { stripUnknown: false }), + }), + transformBeforeSubmit: (formData, initialValues) => { + const adminsToAdd = formData?.admins.filter( + (id) => !initialValues.includes(id) + ) + const adminsToRemove = initialValues.filter( + (id) => id && !formData?.admins.includes(id) + ) + + return { adminsToAdd, adminsToRemove } + }, +}) + +export default EditAdminsForm diff --git a/src/fireedge/src/client/components/Forms/Group/EditAdminsForm/schema.js b/src/fireedge/src/client/components/Forms/Group/EditAdminsForm/schema.js new file mode 100644 index 00000000000..39db6b92242 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/EditAdminsForm/schema.js @@ -0,0 +1,52 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { array, string, object } from 'yup' + +import { UsersTable } from 'client/components/Tables' +import { getValidationFromFields } from 'client/utils' +import { T, INPUT_TYPES } from 'client/constants' + +const ADMINS = (props) => ({ + name: 'admins', + label: T['groups.actions.edit.admins.form'], + type: INPUT_TYPES.TABLE, + Table: () => UsersTable, + fieldProps: { + filterData: props.filterData, + preserveState: true, + }, + singleSelect: false, + validation: array(string()) + .required() + .default(() => undefined), + grid: { md: 12 }, +}) + +/** + * Fields of the form. + * + * @param {object} props - Object to get filterData function + * @returns {object} Fields + */ +export const FIELDS = (props) => [ADMINS(props)] + +/** + * Schema of the form. + * + * @param {object} props - Object to get filterData function + * @returns {object} Schema + */ +export const SCHEMA = (props) => object(getValidationFromFields(FIELDS(props))) diff --git a/src/fireedge/src/client/components/Forms/Group/UpdateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Group/UpdateForm/Steps/index.js new file mode 100644 index 00000000000..b7fa8ad48f6 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/UpdateForm/Steps/index.js @@ -0,0 +1,92 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 Views, { + STEP_ID as VIEWS_ID, +} from 'client/components/Forms/Group/CreateForm/Steps/Views' + +import System, { + STEP_ID as SYSTEM_ID, +} from 'client/components/Forms/Group/CreateForm/Steps/System' + +import { createSteps } from 'client/utils' + +/** + * Create steps for Groups Update Form: + * 1. Advanced options: Options that will be set on group template + * 1.1. Views: Views of the group + * 1.2. Default options + */ +const Steps = createSteps([Views, System], { + transformInitialValue: (group, schema) => { + const objectSchema = { + [VIEWS_ID]: { + VIEWS: group?.TEMPLATE?.FIREEDGE?.VIEWS?.split(',').reduce( + (acc, view) => ({ ...acc, [view]: true }), + {} + ), + DEFAULT_VIEW: group?.TEMPLATE?.FIREEDGE?.DEFAULT_VIEW, + GROUP_ADMIN_VIEWS: group?.TEMPLATE?.FIREEDGE?.GROUP_ADMIN_VIEWS?.split( + ',' + ).reduce((acc, view) => ({ ...acc, [view]: true }), {}), + GROUP_ADMIN_DEFAULT_VIEW: + group?.TEMPLATE?.FIREEDGE?.GROUP_ADMIN_DEFAULT_VIEW, + }, + [SYSTEM_ID]: { + OPENNEBULA: group?.TEMPLATE?.OPENNEBULA, + }, + } + + const knownGroup = schema.cast(objectSchema, { + stripUnknown: true, + }) + + return knownGroup + }, + + transformBeforeSubmit: (formData) => { + // Get data from steps + const { [VIEWS_ID]: views } = formData + const { [SYSTEM_ID]: system } = formData + + const response = {} + + // Views + response.views = { + FIREEDGE: { + VIEWS: Object.entries(views?.VIEWS) + .filter((resource) => resource[1]) + .map((resource) => resource[0]) + .join(','), + DEFAULT_VIEW: views?.DEFAULT_VIEW, + GROUP_ADMIN_VIEWS: Object.entries(views?.GROUP_ADMIN_VIEWS) + .filter((resource) => resource[1]) + .map((resource) => resource[0]) + .join(','), + GROUP_ADMIN_DEFAULT_VIEW: views?.GROUP_ADMIN_DEFAULT_VIEW, + }, + } + + // System + response.system = { + OPENNEBULA: system?.OPENNEBULA, + } + + return response + }, +}) + +export default Steps diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/components/index.js b/src/fireedge/src/client/components/Forms/Group/UpdateForm/index.js similarity index 85% rename from src/fireedge/src/client/components/Tabs/User/Accounting/components/index.js rename to src/fireedge/src/client/components/Forms/Group/UpdateForm/index.js index f19c908f724..edf75f66b12 100644 --- a/src/fireedge/src/client/components/Tabs/User/Accounting/components/index.js +++ b/src/fireedge/src/client/components/Forms/Group/UpdateForm/index.js @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -export { MetricSelector } from 'client/components/Tabs/User/Accounting/components/MetricSelector' -export { CustomizedChart } from 'client/components/Tabs/User/Accounting/components/CustomizedChart' +export { default } from 'client/components/Forms/Group/UpdateForm/Steps' diff --git a/src/fireedge/src/client/components/Forms/Group/index.js b/src/fireedge/src/client/components/Forms/Group/index.js new file mode 100644 index 00000000000..5003be99217 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Group/index.js @@ -0,0 +1,41 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { ReactElement } from 'react' +import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC' +import { CreateStepsCallback } from 'client/utils/schema' + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const CreateForm = (configProps) => + AsyncLoadForm({ formPath: 'Group/CreateForm' }, configProps) + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const UpdateForm = (configProps) => + AsyncLoadForm({ formPath: 'Group/UpdateForm' }, configProps) + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const EditAdminsForm = (configProps) => + AsyncLoadForm({ formPath: 'Group/EditAdminsForm' }, configProps) + +export { CreateForm, UpdateForm, EditAdminsForm } diff --git a/src/fireedge/src/client/components/Tables/Groups/actions.js b/src/fireedge/src/client/components/Tables/Groups/actions.js new file mode 100644 index 00000000000..d8ce0478d62 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Groups/actions.js @@ -0,0 +1,121 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { Typography } from '@mui/material' +import { AddCircledOutline, Trash } from 'iconoir-react' +import { useMemo } from 'react' +import { useHistory } from 'react-router-dom' + +import { useViews } from 'client/features/Auth' +import { useRemoveGroupMutation } from 'client/features/OneApi/group' + +import { + createActions, + GlobalAction, +} from 'client/components/Tables/Enhanced/Utils' + +import { PATH } from 'client/apps/sunstone/routesOne' +import { Translate } from 'client/components/HOC' +import { RESOURCE_NAMES, T, GROUP_ACTIONS } from 'client/constants' + +const ListGroupNames = ({ rows = [] }) => + rows?.map?.(({ id, original }) => { + const { ID, NAME } = original + + return ( + + {`#${ID} ${NAME}`} + + ) + }) + +const MessageToConfirmAction = (rows, description) => ( + <> + + {description && } + + +) + +MessageToConfirmAction.displayName = 'MessageToConfirmAction' + +/** + * Generates the actions to operate resources on Groups table. + * + * @returns {GlobalAction} - Actions + */ +const Actions = () => { + const history = useHistory() + const { view, getResourceView } = useViews() + const [remove] = useRemoveGroupMutation() + + return useMemo( + () => + createActions({ + filters: getResourceView(RESOURCE_NAMES.GROUP)?.actions, + actions: [ + { + accessor: GROUP_ACTIONS.CREATE_DIALOG, + tooltip: T.Create, + icon: AddCircledOutline, + action: () => history.push(PATH.SYSTEM.GROUPS.CREATE), + }, + { + accessor: GROUP_ACTIONS.UPDATE_DIALOG, + label: T.Update, + tooltip: T.Update, + selected: { max: 1 }, + color: 'secondary', + action: (rows) => { + const group = rows?.[0]?.original ?? {} + const path = PATH.SYSTEM.GROUPS.CREATE + + history.push(path, group) + }, + }, + { + accessor: GROUP_ACTIONS.DELETE, + tooltip: T.Delete, + icon: Trash, + color: 'error', + selected: { min: 1 }, + dataCy: `group_${GROUP_ACTIONS.DELETE}`, + options: [ + { + isConfirmDialog: true, + dialogProps: { + title: T.Delete, + dataCy: `modal-${GROUP_ACTIONS.DELETE}`, + children: MessageToConfirmAction, + }, + onSubmit: (rows) => async () => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all(ids.map((id) => remove({ id }))) + }, + }, + ], + }, + ], + }), + [view] + ) +} + +export default Actions diff --git a/src/fireedge/src/client/components/Tables/Groups/columns.js b/src/fireedge/src/client/components/Tables/Groups/columns.js index 375600cca39..4dc500465bc 100644 --- a/src/fireedge/src/client/components/Tables/Groups/columns.js +++ b/src/fireedge/src/client/components/Tables/Groups/columns.js @@ -26,4 +26,8 @@ export default [ accessor: (row) => getTotalOfResources(row?.USERS), sortType: 'number', }, + { Header: 'VM quota', accessor: 'VM_QUOTA' }, + { Header: 'Datastore quota', accessor: 'DATASTORE_QUOTA' }, + { Header: 'Network quota', accessor: 'NETWORK_QUOTA' }, + { Header: 'Image quota', accessor: 'IMAGE_QUOTA' }, ] diff --git a/src/fireedge/src/client/components/Tables/Groups/index.js b/src/fireedge/src/client/components/Tables/Groups/index.js index ca07e9cdd72..f5390bfd27a 100644 --- a/src/fireedge/src/client/components/Tables/Groups/index.js +++ b/src/fireedge/src/client/components/Tables/Groups/index.js @@ -14,13 +14,12 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import { useMemo, Component } from 'react' -import { Chip, Box, Grid, Typography } from '@mui/material' import { useViews } from 'client/features/Auth' import { useGetGroupsQuery } from 'client/features/OneApi/group' import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced' import GroupColumns from 'client/components/Tables/Groups/columns' import GroupRow from 'client/components/Tables/Groups/row' -import { RESOURCE_NAMES, T } from 'client/constants' +import { RESOURCE_NAMES } from 'client/constants' const DEFAULT_DATA_CY = 'groups' @@ -31,7 +30,6 @@ const DEFAULT_DATA_CY = 'groups' * @param {object} [props.rootProps={}] - Root properties for the table. * @param {object} [props.searchProps={}] - Search properties for the table. * @param {Array} props.vdcGroups - Array of VDC groups. - * @param {string|number} props.primaryGroup - ID of the primary group. * @param {Array} [props.secondaryGroups=[]] - Array of IDs of the secondary groups. * @param {object} props.rest - Rest of the properties. * @returns {Component} Rendered component. @@ -41,8 +39,6 @@ const GroupsTable = (props) => { rootProps = {}, searchProps = {}, vdcGroups, - primaryGroup, - secondaryGroups = [], singleSelect = false, ...rest } = props ?? {} @@ -53,23 +49,6 @@ const GroupsTable = (props) => { const { view, getResourceView } = useViews() const { data = [], isFetching, refetch } = useGetGroupsQuery() - const primaryGroupName = useMemo(() => { - const primary = data.find( - (group) => - group.ID === primaryGroup || String(group.ID) === String(primaryGroup) - ) - - return primary?.NAME - }, [data, primaryGroup]) - - const secondaryGroupNames = useMemo(() => { - const foundGroups = data.filter((group) => - secondaryGroups.includes(String(group.ID)) - ) - - return foundGroups.map((group) => group.NAME) - }, [data, secondaryGroups]) - const columns = useMemo( () => createColumns({ @@ -80,63 +59,18 @@ const GroupsTable = (props) => { ) return ( -
- - - {T.Primary} - - - {primaryGroupName && ( - - - {primaryGroupName} - - } - color="primary" - /> - - )} - - {secondaryGroupNames.length > 0 && ( - - {T.Secondary} - - )} - - {secondaryGroupNames.length > 0 && - secondaryGroupNames.map((name, index) => ( - - - {name} - - } - color="secondary" - /> - - ))} - - - - String(row.ID)} - RowComponent={GroupRow} - singleSelect={singleSelect} - {...rest} - /> - -
+ String(row.ID)} + RowComponent={GroupRow} + singleSelect={singleSelect} + {...rest} + /> ) } diff --git a/src/fireedge/src/client/components/Tables/Groups/row.js b/src/fireedge/src/client/components/Tables/Groups/row.js index b58ce65db6c..07df4d56505 100644 --- a/src/fireedge/src/client/components/Tables/Groups/row.js +++ b/src/fireedge/src/client/components/Tables/Groups/row.js @@ -15,33 +15,11 @@ * ------------------------------------------------------------------------- */ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' -import { Group } from 'iconoir-react' -import { Typography } from '@mui/material' -import { rowStyles } from 'client/components/Tables/styles' +import { GroupCard } from 'client/components/Cards' -const Row = ({ original, value, ...props }) => { - const classes = rowStyles() - const { ID, NAME, TOTAL_USERS } = value - - return ( -
-
-
- - {NAME} - -
-
- {`#${ID}`} - - - {` ${TOTAL_USERS}`} - -
-
-
- ) -} +const Row = ({ original, value, ...props }) => ( + +) Row.propTypes = { original: PropTypes.object, diff --git a/src/fireedge/src/client/components/Tables/Users/columns.js b/src/fireedge/src/client/components/Tables/Users/columns.js index 63b09b8a233..ac052570fd9 100644 --- a/src/fireedge/src/client/components/Tables/Users/columns.js +++ b/src/fireedge/src/client/components/Tables/Users/columns.js @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ - export default [ { Header: 'ID', accessor: 'ID', sortType: 'number' }, { Header: 'Name', accessor: 'NAME' }, { Header: 'Group', accessor: 'GNAME' }, + { Header: 'GroupAdmin', accessor: 'IS_ADMIN_GROUP' }, { Header: 'Enabled', accessor: 'ENABLED' }, { Header: 'Auth driver', accessor: 'AUTH_DRIVER' }, { Header: 'VM quota', accessor: 'VM_QUOTA' }, diff --git a/src/fireedge/src/client/components/Tables/Users/index.js b/src/fireedge/src/client/components/Tables/Users/index.js index 9813d90e925..53f27041625 100644 --- a/src/fireedge/src/client/components/Tables/Users/index.js +++ b/src/fireedge/src/client/components/Tables/Users/index.js @@ -35,7 +35,13 @@ const UsersTable = (props) => { searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` const { view, getResourceView } = useViews() - const { data = [], isFetching, refetch } = useGetUsersQuery() + const { data: users = [], isFetching, refetch } = useGetUsersQuery() + + // Filter data if there is filter function + const data = + props?.filterData && typeof props?.filterData === 'function' + ? props?.filterData(users) + : users const columns = useMemo( () => diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/components/CustomizedChart.js b/src/fireedge/src/client/components/Tabs/Accounting/components/CustomizedChart.js similarity index 100% rename from src/fireedge/src/client/components/Tabs/User/Accounting/components/CustomizedChart.js rename to src/fireedge/src/client/components/Tabs/Accounting/components/CustomizedChart.js diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/components/MetricSelector.js b/src/fireedge/src/client/components/Tabs/Accounting/components/MetricSelector.js similarity index 100% rename from src/fireedge/src/client/components/Tabs/User/Accounting/components/MetricSelector.js rename to src/fireedge/src/client/components/Tabs/Accounting/components/MetricSelector.js diff --git a/src/fireedge/src/client/components/Tabs/Accounting/components/index.js b/src/fireedge/src/client/components/Tabs/Accounting/components/index.js new file mode 100644 index 00000000000..685e2797397 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Accounting/components/index.js @@ -0,0 +1,17 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ +export { MetricSelector } from 'client/components/Tabs/Accounting/components/MetricSelector' +export { CustomizedChart } from 'client/components/Tabs/Accounting/components/CustomizedChart' diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/dateUtils.js b/src/fireedge/src/client/components/Tabs/Accounting/helpers/dateUtils.js similarity index 96% rename from src/fireedge/src/client/components/Tabs/User/Accounting/helpers/dateUtils.js rename to src/fireedge/src/client/components/Tabs/Accounting/helpers/dateUtils.js index ba2b8bbba17..b43edeb29fd 100644 --- a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/dateUtils.js +++ b/src/fireedge/src/client/components/Tabs/Accounting/helpers/dateUtils.js @@ -22,7 +22,7 @@ import { DateTime } from 'luxon' */ export const getDefaultDateRange = () => { const today = DateTime.now() - const sevenDaysAgo = DateTime.now().minus({ days: 7 }) + const sevenDaysAgo = DateTime.now().minus({ days: 1 }) return { startDate: sevenDaysAgo, diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/index.js b/src/fireedge/src/client/components/Tabs/Accounting/helpers/index.js similarity index 82% rename from src/fireedge/src/client/components/Tabs/User/Accounting/helpers/index.js rename to src/fireedge/src/client/components/Tabs/Accounting/helpers/index.js index a2d7d31dd0c..0767aa7cf75 100644 --- a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/index.js +++ b/src/fireedge/src/client/components/Tabs/Accounting/helpers/index.js @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -export { getDefaultDateRange } from 'client/components/Tabs/User/Accounting/helpers/dateUtils' +export { getDefaultDateRange } from 'client/components/Tabs/Accounting/helpers/dateUtils' export { transformWithComputedMetrics, calculateDisplayMetrics, -} from 'client/components/Tabs/User/Accounting/helpers/metrics' -export { useAccountingData } from 'client/components/Tabs/User/Accounting/helpers/useAccountingData' +} from 'client/components/Tabs/Accounting/helpers/metrics' +export { useAccountingData } from 'client/components/Tabs/Accounting/helpers/useAccountingData' diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/metrics.js b/src/fireedge/src/client/components/Tabs/Accounting/helpers/metrics.js similarity index 100% rename from src/fireedge/src/client/components/Tabs/User/Accounting/helpers/metrics.js rename to src/fireedge/src/client/components/Tabs/Accounting/helpers/metrics.js diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/useAccountingData.js b/src/fireedge/src/client/components/Tabs/Accounting/helpers/useAccountingData.js similarity index 88% rename from src/fireedge/src/client/components/Tabs/User/Accounting/helpers/useAccountingData.js rename to src/fireedge/src/client/components/Tabs/Accounting/helpers/useAccountingData.js index 788b997d4b9..59020527dd0 100644 --- a/src/fireedge/src/client/components/Tabs/User/Accounting/helpers/useAccountingData.js +++ b/src/fireedge/src/client/components/Tabs/Accounting/helpers/useAccountingData.js @@ -14,8 +14,8 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import { useState, useEffect } from 'react' -import { useGetAccountingPoolQuery } from 'client/features/OneApi/vm' -import { transformWithComputedMetrics } from 'client/components/Tabs/User/Accounting/helpers' +import { useLazyGetAccountingPoolFilteredQuery } from 'client/features/OneApi/vm' +import { transformWithComputedMetrics } from 'client/components/Tabs/Accounting/helpers' const keyMap = { 'VM.ID': 'ID', @@ -39,10 +39,11 @@ const TIMEOUT = 8000 // 8 seconds * @param {number|string} id - The ID for which accounting data is to be fetched. * @returns {object} - Returns an object containing the processed data, loading state, and any error. */ -export const useAccountingData = ({ id }) => { - const { data: fetchedData } = useGetAccountingPoolQuery({ - filter: id, - }) +export const useAccountingData = ({ user, group, id, start, end }) => { + // Create the hook to fetch data + const [refetch, { data: fetchedData }] = + useLazyGetAccountingPoolFilteredQuery() + const [data, setData] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) @@ -82,5 +83,5 @@ export const useAccountingData = ({ id }) => { } }, [fetchedData]) - return { data, isLoading, error } + return { data, isLoading, setIsLoading, error, refetch } } diff --git a/src/fireedge/src/client/components/Tabs/Accounting/index.js b/src/fireedge/src/client/components/Tabs/Accounting/index.js new file mode 100644 index 00000000000..201d9396bd4 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Accounting/index.js @@ -0,0 +1,361 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { useState, useEffect, useCallback } from 'react' +import { Plus } from 'iconoir-react' +import { + Select, + MenuItem, + InputLabel, + FormControl, + Box, + Button, + Chip, + Tooltip, +} from '@mui/material' +import { + MetricSelector, + CustomizedChart, +} from 'client/components/Tabs/Accounting/components' +import { DateRangeFilter } from 'client/components/Date' +import AdapterLuxon from '@mui/lab/AdapterLuxon' +import { DateTime } from 'luxon' +import { LocalizationProvider } from '@mui/lab' +import { + getDefaultDateRange, + useAccountingData, + calculateDisplayMetrics, +} from 'client/components/Tabs/Accounting/helpers' +import { filterDataset } from 'client/components/Charts/MultiChart/helpers/scripts' + +const ACTION_ADD = 'add' +const ACTION_REMOVE = 'remove' +const DATASETS_LIMIT = 4 + +/** + * Generates a QuotaInfoTab for an user or a group. + * AccountingInfoTab component displays accounting information for a given ID. + * It provides options to filter the data by date range, chart type, and grouping. + * + * @param {object} props - Input properties + * @param {boolean} props.groups - If it's a group or not + * @returns {object} - The AccountingInfoTab component + */ +const generateAccountingInfoTab = ({ groups }) => { + const AccountingInfoTab = ({ id }) => { + const [dateRange, setDateRange] = useState(getDefaultDateRange()) // LAST 7 DAYS + const { data, isLoading, setIsLoading, error, refetch } = useAccountingData( + { + groups, + id, + start: dateRange.startDate.toSeconds(), + end: dateRange.endDate.toSeconds(), + } + ) + const [datasets, setDatasets] = useState([]) + const [visibleDatasets, setVisibleDatasets] = useState([]) + const [chartType, setChartType] = useState('line') + const [groupBy, setGroupBy] = useState('NAME') + const [showTooltip, setShowTooltip] = useState(false) + const [selectedMetrics, setSelectedMetrics] = useState({ + cpuHours: true, + memoryGBHours: true, + diskMBHours: true, + }) + + // Hook for the first time that the component is rendered + useEffect(() => { + // Make request to API + const params = groups + ? { + group: id, + start: dateRange.startDate.toSeconds(), + end: dateRange.endDate.toSeconds(), + } + : { + user: id, + start: dateRange.startDate.toSeconds(), + end: dateRange.endDate.toSeconds(), + } + refetch(params) + }, []) + + // Hook to create data set each time a request is made + useEffect(() => { + if (!isLoading && data) { + const newDataset = createDataset(data, dateRange, chartType) + setDatasets((prevDatasets) => [...prevDatasets, newDataset]) + setVisibleDatasets((prevVisible) => [...prevVisible, newDataset.id]) + } + }, [data, isLoading]) + + const isWithinDateRange = (record, startDate, endDate) => { + const recordDate = DateTime.fromSeconds(parseInt(record.STIME, 10)) + + return recordDate >= startDate && recordDate <= endDate + } + + // eslint-disable-next-line no-shadow + const createDataset = (data, dateRange) => { + const result = filterDataset( + data, + (record) => + isWithinDateRange(record, dateRange.startDate, dateRange.endDate), + (record) => + `${dateRange.startDate.toFormat( + 'MMM dd, yyyy' + )} - ${dateRange.endDate.toFormat('MMM dd, yyyy')}` + ) + + const filteredDataset = result.dataset + let filteredData = + filteredDataset && filteredDataset.data ? filteredDataset.data : [] + + filteredData.sort((a, b) => { + if (a.ETIME === '0') return 1 + if (b.ETIME === '0') return -1 + + return b.ETIME - a.ETIME + }) + + const seenIds = new Set() + filteredData = filteredData.filter((record) => { + if (seenIds.has(record.ID)) { + return false + } + seenIds.add(record.ID) + + return true + }) + + const metrics = calculateDisplayMetrics(filteredData) + const label = `${dateRange.startDate.toFormat( + 'MMM dd, yyyy' + )} - ${dateRange.endDate.toFormat('MMM dd, yyyy')}` + + return { + id: Date.now(), + data: filteredData, + metrics: metrics, + label: label, + isEmpty: result.isEmpty, + } + } + + // Event handlers + + const toggleDatasetVisibility = (datasetId) => { + setVisibleDatasets((prevVisible) => { + if (prevVisible.includes(datasetId)) { + // eslint-disable-next-line no-shadow + return prevVisible.filter((id) => id !== datasetId) + } else { + return [...prevVisible, datasetId] + } + }) + } + + /** + * Add or remove datasets. + * + * @param {string} action - Add or remove action + * @param {object} datasetToRemove - Dataset to remove + * @returns {void} - Nothing + */ + const handleDatasetChange = (action, datasetToRemove = null) => { + // Add case - Check if there are less datasets that DATASETS_LIMIT and, if not, make a request to the API to get data + if (action === ACTION_ADD) { + // Check number of datasets + if (datasets.length >= DATASETS_LIMIT) { + setShowTooltip(true) + setTimeout(() => setShowTooltip(false), 3000) + + return + } + + // Active loading + setIsLoading(true) + + // Make request to API + const params = groups + ? { + group: id, + start: dateRange.startDate.toSeconds(), + end: dateRange.endDate.toSeconds(), + } + : { + user: id, + start: dateRange.startDate.toSeconds(), + end: dateRange.endDate.toSeconds(), + } + refetch(params) + } + // Remove case + else if (action === ACTION_REMOVE && datasetToRemove) { + setDatasets((prevDatasets) => + prevDatasets.filter((dataset) => dataset.id !== datasetToRemove.id) + ) + + setVisibleDatasets((prevVisible) => + // eslint-disable-next-line no-shadow + prevVisible.filter((id) => id !== datasetToRemove.id) + ) + } + } + + const handleMetricChange = useCallback((event) => { + const { name, checked } = event.target + setSelectedMetrics((prevMetrics) => ({ + ...prevMetrics, + [name]: checked, + })) + }, []) + + const handleChartTypeChange = useCallback( + (event) => { + const newChartType = event.target.value + setChartType(newChartType) + }, + [data, dateRange] + ) + + const handleGroupByChange = (event) => { + setGroupBy(event.target.value) + } + + return ( + + + + + setDateRange(updatedRange)} + /> + + + + + + + Group By + + + + Chart Type + + + + + {chartType !== 'table' && ( + + )} + + {datasets.map((dataset) => ( + toggleDatasetVisibility(dataset.id)} + onDelete={(e) => { + e.stopPropagation() + handleDatasetChange('remove', dataset) + }} + style={{ + opacity: visibleDatasets.includes(dataset.id) ? 1 : 0.5, + }} + /> + ))} + + + + + ) + } + + AccountingInfoTab.propTypes = { + id: PropTypes.string, + } + + AccountingInfoTab.displayName = 'AccountingInfoTab' + + return AccountingInfoTab +} + +export default generateAccountingInfoTab diff --git a/src/fireedge/src/client/components/Tabs/Group/Users/Actions.js b/src/fireedge/src/client/components/Tabs/Group/Users/Actions.js new file mode 100644 index 00000000000..0f6ef272235 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Group/Users/Actions.js @@ -0,0 +1,74 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { memo } from 'react' +import PropTypes from 'prop-types' + +import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm' + +import { EditAdminsForm } from 'client/components/Forms/Group' + +import { T } from 'client/constants' + +/** + * Action to edit administrators of a group + */ +const EditAdminsActions = memo(({ admins, filterData, submit }) => { + // Handle submit form + const handleEditAdmins = (formData) => { + submit(formData.adminsToAdd, formData.adminsToRemove) + } + + return ( + + EditAdminsForm({ + initialValues: admins, + stepProps: { + filterData, + }, + }), + onSubmit: handleEditAdmins, + }, + ]} + /> + ) +}) + +EditAdminsActions.propTypes = { + admins: PropTypes.array, + filterData: PropTypes.func, + submit: PropTypes.func, +} +EditAdminsActions.displayName = 'EditAdminsActions' + +export { EditAdminsActions } diff --git a/src/fireedge/src/client/components/Tabs/Group/Users/index.js b/src/fireedge/src/client/components/Tabs/Group/Users/index.js new file mode 100644 index 00000000000..2d51df1230d --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Group/Users/index.js @@ -0,0 +1,127 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { ReactElement } from 'react' +import PropTypes from 'prop-types' +import { Stack } from '@mui/material' +import { UsersTable } from 'client/components/Tables' + +import { + useGetGroupQuery, + useAddAdminToGroupMutation, + useRemoveAdminFromGroupMutation, +} from 'client/features/OneApi/group' +import { getActionsAvailable } from 'client/models/Helper' +import { GROUP_ACTIONS, T } from 'client/constants' + +import { EditAdminsActions } from './Actions' + +import { useGeneralApi } from 'client/features/General' + +const _ = require('lodash') + +/** + * Renders users tab showing the users of the group. + * + * @param {object} props - Props + * @param {string} props.id - Group id + * @param {object} props.tabProps - Tab props + * @param {object} props.tabProps.actions - Actions to this tab + * @returns {ReactElement} Information tab + */ +const GroupUsersTab = ({ tabProps: { actions } = {}, id }) => { + const { enqueueSuccess } = useGeneralApi() + const [addAdmins] = useAddAdminToGroupMutation() + const [removeAdmins] = useRemoveAdminFromGroupMutation() + + const { data: group, refetch } = useGetGroupQuery({ id }) + const adminsGroup = Array.isArray(group.ADMINS?.ID) + ? group.ADMINS?.ID + : [group.ADMINS?.ID] + + const actionsAvailable = getActionsAvailable(actions) + + // Filter function to get only group users and add if the user is admin group + const filterData = (data) => { + const filterUsers = data.filter((user) => { + // filter users by group id + const groupsUser = Array.isArray(user.GROUPS.ID) + ? user.GROUPS.ID + : [user.GROUPS.ID] + + return groupsUser.some((groupUser) => groupUser === id) + }) + + const admins = Array.isArray(group.ADMINS?.ID) + ? group.ADMINS?.ID + : [group.ADMINS?.ID] + + return filterUsers.map((user) => { + const userCopy = _.cloneDeep(_.cloneDeep(user)) + userCopy.IS_ADMIN_GROUP = admins.some((admin) => admin === user.ID) + + return userCopy + }) + } + + // Add and remove administrators + const submitAdmins = async (adminsToAdd, adminsToRemove) => { + // Add admins + await Promise.all(adminsToAdd.map((user) => addAdmins({ id, user }))) + + // Remove admins + await Promise.all(adminsToRemove.map((user) => removeAdmins({ id, user }))) + + // Refresh info + refetch({ id }) + + // Success message + enqueueSuccess(T['groups.actions.edit.admins.success']) + } + + return ( +
+ {actionsAvailable?.includes?.(GROUP_ACTIONS.EDIT_ADMINS) && ( + + )} + + + +
+ ) +} + +GroupUsersTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +GroupUsersTab.displayName = 'GroupUsersTab' + +export default GroupUsersTab diff --git a/src/fireedge/src/client/components/Tabs/Group/index.js b/src/fireedge/src/client/components/Tabs/Group/index.js index 0c56b89df98..6549ca7fa32 100644 --- a/src/fireedge/src/client/components/Tabs/Group/index.js +++ b/src/fireedge/src/client/components/Tabs/Group/index.js @@ -24,10 +24,18 @@ import { getAvailableInfoTabs } from 'client/models/Helper' import Tabs from 'client/components/Tabs' import Info from 'client/components/Tabs/Group/Info' +import Users from 'client/components/Tabs/Group/Users' +import generateQuotasInfoTab from 'client/components/Tabs//Quota' +import generateAccountingInfoTab from 'client/components/Tabs/Accounting' +import generateShowbackInfoTab from 'client/components/Tabs/Showback' const getTabComponent = (tabName) => ({ info: Info, + user: Users, + quota: generateQuotasInfoTab({ groups: true }), + accounting: generateAccountingInfoTab({ groups: true }), + showback: generateShowbackInfoTab({ groups: true }), }[tabName]) const GroupTabs = memo(({ id }) => { diff --git a/src/fireedge/src/client/components/Tabs/User/Quota/Components/QuotaControls.js b/src/fireedge/src/client/components/Tabs/Quota/Components/QuotaControls.js similarity index 95% rename from src/fireedge/src/client/components/Tabs/User/Quota/Components/QuotaControls.js rename to src/fireedge/src/client/components/Tabs/Quota/Components/QuotaControls.js index 05adecd1152..f5d2f0b157f 100644 --- a/src/fireedge/src/client/components/Tabs/User/Quota/Components/QuotaControls.js +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/QuotaControls.js @@ -32,6 +32,11 @@ import { useUpdateUserQuotaMutation, } from 'client/features/OneApi/user' +import { + useGetGroupQuery, + useUpdateGroupQuotaMutation, +} from 'client/features/OneApi/group' + import { useGeneralApi } from 'client/features/General' import { T } from 'client/constants' import { @@ -42,12 +47,12 @@ import { getExistingValue, quotaIdentifiers, handleApplyGlobalQuotas, -} from 'client/components/Tabs/User/Quota/Components/helpers/scripts' +} from 'client/components/Tabs/Quota/Components/helpers/scripts' import { HybridInputField, ResourceIDAutocomplete, -} from 'client/components/Tabs/User/Quota/Components/helpers/subcomponents' +} from 'client/components/Tabs/Quota/Components/helpers/subcomponents' /** * QuotaControls Component @@ -70,13 +75,18 @@ export const QuotaControls = memo( existingData, clickedElement, nameMaps, + groups, }) => { const [state, actions] = useQuotaControlReducer() const [popoverAnchorEl, setPopoverAnchorEl] = useState(null) const [touchedFields, setTouchedFields] = useState({}) const { enqueueError, enqueueSuccess } = useGeneralApi() - const [updateUserQuota] = useUpdateUserQuotaMutation() + + const [updateQuota] = groups + ? useUpdateGroupQuotaMutation() + : useUpdateUserQuotaMutation() + const { palette } = useTheme() useEffect(() => { @@ -183,7 +193,9 @@ export const QuotaControls = memo( } }, [state.globalIds, state.values]) - const existingTemplate = useGetUserQuery({ id: userId }) + const existingTemplate = groups + ? useGetGroupQuery({ id: userId }) + : useGetUserQuery({ id: userId }) const filteredResourceIDs = useMemo( () => @@ -303,7 +315,7 @@ export const QuotaControls = memo( selectedType, actions, userId, - updateUserQuota, + updateQuota, enqueueError, enqueueSuccess, nameMaps @@ -373,6 +385,7 @@ QuotaControls.propTypes = { existingData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), clickedElement: PropTypes.object, nameMaps: PropTypes.object, + groups: PropTypes.bool, } QuotaControls.defaultProps = { diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/common.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/common.js new file mode 100644 index 00000000000..191a11f3042 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/common.js @@ -0,0 +1,223 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +/** + * @param {Array} values - Array of values + * @param {Array} globalIds - Global ids array + * @param {Array} markedForDeletion - Array of ids to delete + * @returns {string} - Display value string + */ +export const getConcatenatedValues = (values, globalIds, markedForDeletion) => + globalIds + .map((id) => (markedForDeletion.includes(id) ? 'Delete' : values[id]) || '') + .filter((value) => value !== '') + .join(', ') + +/** + * @param {number} resourceId - Resource id + * @param {string} identifier - Quota identifier + * @param {string} selectedType - Selected quota type + * @param {Array} existingData - Existing resource data + * @returns {object} Resource data + */ +export const getExistingValue = ( + resourceId, + identifier, + selectedType, + existingData = [] +) => { + if (selectedType === 'VM') { + const vmQuotaObject = existingData[0] + + return vmQuotaObject ? vmQuotaObject[identifier] : '' + } else { + const resourceData = existingData.find((data) => data.ID === resourceId) + + return resourceData ? resourceData[identifier] : '' + } +} + +const findIdByName = (nameMaps, selectedType, resourceNameOrId) => { + if (!isNaN(parseInt(resourceNameOrId, 10))) { + return resourceNameOrId + } + + const typeMap = nameMaps[selectedType] || {} + const foundEntry = Object.entries(typeMap).find( + ([id, name]) => name === resourceNameOrId + ) + + return foundEntry ? foundEntry[0] : resourceNameOrId +} + +/** + * @param {object} state - The current state object containing the quota information. + * @param {object} existingTemplate - The existing quota template data to compare against. + * @param {string} selectedType - The type of quota selected (e.g., 'VM', 'DATASTORE'). + * @param {object} actions - An object containing reducer actions for updating state. + * @param {number|string} userId - The ID of the user whose quota is being updated. + * @param {Function} updateQuota - The mutation function to call for updating the quota. + * @param {Function} enqueueError - Function to enqueue an error notification. + * @param {Function} enqueueSuccess - Function to enqueue a success notification. + * @param {object} nameMaps - Object containing the mappings of resource IDs to their names. + * @returns {Promise} A promise that resolves when the operation is complete. + */ +export const handleApplyGlobalQuotas = async ( + state, + existingTemplate, + selectedType, + actions, + userId, + updateQuota, + enqueueError, + enqueueSuccess, + nameMaps +) => { + if (!state.isValid) return + + const getExistingQuota = (quotaType, resourceId, existingTemplateData) => { + const quotaKey = `${quotaType}_QUOTA` + const quotaData = existingTemplateData[quotaKey] + + if (!Array.isArray(quotaData) && quotaType === 'VM') { + return quotaData || {} + } else if (Array.isArray(quotaData)) { + return ( + quotaData?.find((q) => q?.ID?.toString() === resourceId.toString()) || + {} + ) + } + + return ( + [quotaData]?.find((q) => q?.ID?.toString() === resourceId.toString()) || + {} + ) + } + + const applyQuotaChange = async (resourceIdOrName, value) => { + try { + const actualId = + findIdByName(nameMaps, selectedType, resourceIdOrName) || + resourceIdOrName + + const quota = { [state.selectedIdentifier]: value } + const existingQuota = getExistingQuota( + selectedType, + actualId, + existingTemplate?.data + ) + + const xmlData = quotasToXml(selectedType, actualId, { + ...existingQuota, + ...quota, + }) + const result = await updateQuota({ id: userId, template: xmlData }) + + if (result.error) { + throw new Error(result.error.message) + } + enqueueSuccess(`Quota updated successfully for ID ${actualId}`) + } catch (error) { + enqueueError( + `Error updating quota for ID ${resourceIdOrName}: ${error.message}` + ) + } + } + + if (selectedType === 'VM') { + const vmValue = state.globalValue || '' + await applyQuotaChange(0, vmValue) + } else { + for (const resourceId of state.globalIds) { + const isMarkedForDeletion = state.markedForDeletion.includes(resourceId) + const value = isMarkedForDeletion ? null : state.values[resourceId] + if (value !== undefined && value !== '') { + await applyQuotaChange(resourceId, value) + } else { + enqueueError(`No value specified for Resource ID ${resourceId}`) + } + } + } + + // Clear state after all updates are attempted + actions.setGlobalIds([]) + actions.setGlobalValue('') + actions.setValues({}) + state?.markedForDeletion?.forEach((id) => actions.setUnmarkForDeletion(id)) +} + +/** + * Converts an array of resources or a single resource object + * into an object mapping IDs to names. + * + * @param {object | Array} dataPool - The resource data pool from the API response. + * @returns {object} - An object mapping resource IDs to their names. + */ +export const nameMapper = (dataPool) => { + if (dataPool?.isSuccess) { + const resources = Array.isArray(dataPool.data) + ? dataPool.data + : [dataPool.data] + + return resources.reduce((map, resource) => { + if (resource.ID && resource.NAME) { + map[resource.ID] = resource.NAME + } + + return map + }, {}) + } + + return {} +} + +/** + * Convert quota data to XML format. + * + * @param {string} type - Quota type. + * @param {string} resourceId - Resource ID + * @param {object} quota - Quota data. + * @returns {string} XML representation of the quota. + */ +const quotasToXml = (type, resourceId, quota) => { + let innerXml = '' + + for (const [key, value] of Object.entries(quota)) { + innerXml += `<${key.toUpperCase()}>${value}` + } + + const finalXml = `` + + return finalXml +} + +export const quotaIdentifiers = { + VM: [ + { id: 'VMS', displayName: 'Virtual Machines' }, + { id: 'RUNNING_VMS', displayName: 'Running VMs' }, + { id: 'MEMORY', displayName: 'Memory' }, + { id: 'RUNNING_MEMORY', displayName: 'Running Memory' }, + { id: 'CPU', displayName: 'CPU' }, + { id: 'RUNNING_CPU', displayName: 'Running CPU' }, + { id: 'SYSTEM_DISK_SIZE', displayName: 'System Disk Size' }, + ], + DATASTORE: [ + { id: 'SIZE', displayName: 'Size' }, + { id: 'IMAGES', displayName: 'Images' }, + ], + NETWORK: [{ id: 'LEASES', displayName: 'Leases' }], + IMAGE: [{ id: 'RVMS', displayName: 'Running VMs' }], +} diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/index.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/index.js new file mode 100644 index 00000000000..7d57bf79373 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/index.js @@ -0,0 +1,36 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { validateResourceId, validateValue } from './validation' +import { useQuotaControlReducer } from './reducer/useQuotaControlReducer' +import { + getConcatenatedValues, + getExistingValue, + quotaIdentifiers, + handleApplyGlobalQuotas, + nameMapper, +} from './common' + +export { + validateResourceId, + validateValue, + useQuotaControlReducer, + getConcatenatedValues, + getExistingValue, + quotaIdentifiers, + handleApplyGlobalQuotas, + nameMapper, +} diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/actions.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/actions.js new file mode 100644 index 00000000000..6945a72cf05 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/actions.js @@ -0,0 +1,99 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ +/** + * Action creator for setting global IDs in the state. The action contains the new globalIds to be set. + * + * @param {number[]|string[]} globalIds - The new array of global IDs to be updated in the state. + * @returns {{ type: string, payload: number[]|string[] }} The action object to dispatch. + */ +export const setGlobalIds = (globalIds) => ({ + type: 'SET_GLOBAL_IDS', + payload: globalIds, +}) + +/** + * Action creator for setting the selected identifier in the state. The action contains the selectedIdentifier to be set. + * + * @param {number|string} selectedIdentifier - The identifier that has been selected. + * @returns {{ type: string, payload: number|string }} The action object to dispatch. + */ +export const setSelectedIdentifier = (selectedIdentifier) => ({ + type: 'SET_SELECTED_IDENTIFIER', + payload: selectedIdentifier, +}) + +/** + * Action creator for setting a global value in the state. The action contains the globalValue to be set. + * + * @param {any} globalValue - The new value to update in the state. + * @returns {{ type: string, payload: any }} The action object to dispatch. + */ +export const setGlobalValue = (globalValue) => ({ + type: 'SET_GLOBAL_VALUE', + payload: globalValue, +}) + +/** + * Action creator for setting multiple values in the state at once. The action contains the values object to be set. + * + * @param {object} values - An object containing multiple values to update in the state. + * @returns {{type: string, payload: object}} The action object to dispatch. + */ +export const setValues = (values) => ({ type: 'SET_VALUES', payload: values }) + +/** + * Action creator to mark a resource ID for deletion in the state. + * + * @param {number|string} resourceId - The ID of the resource to be marked for deletion. + * @returns {{ type: string, payload: number|string }} The action object to dispatch. + */ +export const setMarkForDeletion = (resourceId) => ({ + type: 'MARK_FOR_DELETION', + payload: resourceId, +}) + +/** + * Action creator to unmark a resource ID from being marked for deletion in the state. + * + * @param {number|string} resourceId - The ID of the resource to unmark for deletion. + * @returns {{ type: string, payload: number|string }} The action object to dispatch. + */ +export const setUnmarkForDeletion = (resourceId) => ({ + type: 'UNMARK_FOR_DELETION', + payload: resourceId, +}) + +/** + * Action creator for setting the 'isValid' flag in the state, indicating whether the current state is valid or not. + * + * @param {boolean} isValid - Boolean flag indicating the validity of the state. + * @returns {{ type: string, payload: boolean }} The action object to dispatch. + */ +export const setIsValid = (isValid) => ({ + type: 'SET_IS_VALID', + payload: isValid, +}) + +/** + * Action creator for setting the 'isApplyDisabled' flag in the state, which controls whether the apply action is disabled. + * + * @param {boolean} isDisabled - Boolean flag indicating if the apply action should be disabled. + * @returns {{ type: string, payload: boolean }} The action object to dispatch. + */ +export const setIsApplyDisabled = (isDisabled) => ({ + type: 'SET_IS_APPLY_DISABLED', + payload: isDisabled, +}) diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/definitions.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/definitions.js new file mode 100644 index 00000000000..77f34a392bf --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/definitions.js @@ -0,0 +1,61 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ +export const initialState = { + globalIds: [], + markedForDeletion: [], + selectedIdentifier: '', + globalValue: '', + values: {}, + isValid: true, + isApplyDisabled: true, +} + +/** + * @param {object} state - State variable. + * @param {object} action - Action object. + * @returns {object} - New state + */ +export const reducer = (state, action) => { + switch (action.type) { + case 'SET_GLOBAL_IDS': + return { ...state, globalIds: action.payload } + case 'SET_SELECTED_IDENTIFIER': + return { ...state, selectedIdentifier: action.payload } + case 'SET_GLOBAL_VALUE': + return { ...state, globalValue: action.payload } + case 'SET_IS_VALID': + return { ...state, isValid: action.payload } + case 'SET_IS_APPLY_DISABLED': + return { ...state, isApplyDisabled: action.payload } + case 'SET_VALUES': + return { ...state, values: action.payload } + case 'MARK_FOR_DELETION': + return { + ...state, + markedForDeletion: [...state.markedForDeletion, action.payload], + } + case 'UNMARK_FOR_DELETION': + return { + ...state, + markedForDeletion: state.markedForDeletion.filter( + (id) => id !== action.payload + ), + } + + default: + return state + } +} diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/useQuotaControlReducer.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/useQuotaControlReducer.js new file mode 100644 index 00000000000..93faa3e1793 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/reducer/useQuotaControlReducer.js @@ -0,0 +1,33 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { useReducer } from 'react' +import * as actionCreators from './actions' +import { reducer, initialState } from './definitions' + +/** + *@returns {Array} - Array containing state and an actions object. + */ +export const useQuotaControlReducer = () => { + const [state, dispatch] = useReducer(reducer, initialState) + + const boundActionCreators = Object.keys(actionCreators).reduce((acc, key) => { + acc[key] = (...args) => dispatch(actionCreators[key](...args)) + + return acc + }, {}) + + return [state, boundActionCreators] +} diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/validation.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/validation.js new file mode 100644 index 00000000000..c520ab14830 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/scripts/validation.js @@ -0,0 +1,35 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +/** + * @param {number} value - Value to validate + * @param {Array} globalIds - Global ids array + * @param {Function} callback - State update + * @returns {boolean} - Is valid? + */ +export const validateResourceId = (value, globalIds, callback) => { + const regex = /^\d+$/ + const isValid = regex.test(value) && !globalIds.includes(value) + callback(isValid) + + return isValid +} + +/** + * @param {number} value - Value to validate + * @returns {boolean} - Is valid? + */ +export const validateValue = (value) => value === 'Delete' || !isNaN(value) diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/HybridInput.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/HybridInput.js new file mode 100644 index 00000000000..fbed0ca9895 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/HybridInput.js @@ -0,0 +1,216 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { Component } from 'react' +import { + TextField, + InputAdornment, + IconButton, + Popover, + Paper, + Grid, +} from '@mui/material' +import { Cancel } from 'iconoir-react' + +/** + * @param {object} props - The props for the component. + * @param {string} props.selectedType - The currently selected quota type. + * @param {object} props.state - The state object containing various state indicators. + * @param {object} props.actions - An object containing reducer actions to mutate the state. + * @param {Function} props.validateValue - A function to validate the input value. + * @param {Function} props.getConcatenatedValues - A function to concatenate multiple values. + * @param {Function} props.setPopoverAnchorEl - A function to set the anchor for the popover. + * @param {HTMLElement} props.popoverAnchorEl - The anchor element for the popover. + * @param {object} props.palette - The MUI theme palette. + * @param {object} props.touchedFields - An object representing the touched state of fields. + * @param {Function} props.setTouchedFields - A function to set fields as touched upon interaction. + * @returns {Component} - Input component + */ +export const HybridInputField = ({ + selectedType, + state, + actions, + validateValue, + getConcatenatedValues, + setPopoverAnchorEl, + popoverAnchorEl, + palette, + touchedFields, + setTouchedFields, +}) => { + const isDisabled = () => + state.selectedIdentifier === '' || + (selectedType !== 'VM' && state.globalIds?.length === 0) || + (state.globalIds?.length === 1 && + state.markedForDeletion.includes(state.globalIds[0])) + + const getValue = () => { + if (selectedType === 'VM') { + return state.globalValue + } else if (state.globalIds.length > 1) { + return getConcatenatedValues( + state.values, + state.globalIds, + state.markedForDeletion + ) + } else { + return state.markedForDeletion.includes(state.globalIds[0]) + ? 'Delete' + : state.globalValue + } + } + + return ( + <> + { + const value = e.target.value + if (validateValue(value)) { + if (state.globalIds.length === 1 || selectedType === 'VM') { + actions.setGlobalValue(value) + if (selectedType !== 'VM') { + actions.setValues({ + ...state.values, + [state?.globalIds[0]]: value, + }) + } + } else { + const updatedValues = { ...state.values } + state.globalIds.forEach((id) => { + if (!state.markedForDeletion.includes(id)) { + updatedValues[id] = value + } + }) + actions.setValues(updatedValues) + } + } + }} + onClick={(event) => { + if (state.globalIds.length > 1) { + setPopoverAnchorEl(event.currentTarget) + } + }} + variant="outlined" + fullWidth + InputProps={{ + inputProps: { + style: { padding: '16px' }, + 'data-cy': 'qc-value-input', + }, + endAdornment: state.globalValue && + state.globalIds.length <= 1 && + !state.markedForDeletion.includes(state.globalIds[0]) && ( + + actions.setGlobalValue('')} + sx={{ + marginRight: '-4px', + }} + > + + + + ), + }} + /> + setPopoverAnchorEl(null)} + anchorOrigin={{ vertical: 'center', horizontal: 'right' }} + transformOrigin={{ vertical: 'center', horizontal: 'left' }} + sx={{ + '& .MuiPaper-root': { + padding: 2, + maxWidth: '500px', + borderRadius: 2, + boxShadow: '0px 4px 20px rgba(0,0,0,0.1)', + }, + }} + > + + + {state.globalIds.map((id, index) => ( + + { + const value = e.target.value + if (validateValue(value)) { + actions.setValues({ ...state.values, [id]: value }) + } + }} + variant="outlined" + fullWidth + size="small" + error={touchedFields[id] && !validateValue(state.values[id])} + helperText={ + touchedFields[id] && + !validateValue(state.values[id]) && + 'Invalid value' + } + onBlur={() => + setTouchedFields((prev) => ({ ...prev, [id]: true })) + } + /> + + ))} + + + + + ) +} + +HybridInputField.propTypes = { + selectedType: PropTypes.string, + state: PropTypes.shape({ + selectedIdentifier: PropTypes.string, + globalIds: PropTypes.arrayOf(PropTypes.string), + globalValue: PropTypes.string, + markedForDeletion: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + values: PropTypes.objectOf(PropTypes.string), + }), + actions: PropTypes.shape({ + setGlobalValue: PropTypes.func, + setValues: PropTypes.func, + setMarkForDeletion: PropTypes.func, + setUnmarkForDeletion: PropTypes.func, + }), + validateValue: PropTypes.func, + getConcatenatedValues: PropTypes.func, + setPopoverAnchorEl: PropTypes.func, + popoverAnchorEl: PropTypes.instanceOf(Element), + palette: PropTypes.object, + touchedFields: PropTypes.objectOf(PropTypes.bool), + setTouchedFields: PropTypes.func, +} diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/ResourceIDAutocomplete.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/ResourceIDAutocomplete.js new file mode 100644 index 00000000000..0b4aa373ec2 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/ResourceIDAutocomplete.js @@ -0,0 +1,206 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { Component, useState } from 'react' +import { Autocomplete, Box, Chip, IconButton, TextField } from '@mui/material' +import { Trash } from 'iconoir-react' + +/** + * @param {object} root0 - Component + * @param {string} root0.selectedType - Selected Quota type. + * @param {object} root0.state - Variable states. + * @param {object} root0.actions - Reducer actions object. + * @param {Function} root0.validateResourceId - Validates resource IDs. + * @param {Array} root0.filteredResourceIDs - Filtered resource IDs. + * @param {object} root0.palette - MUI theme. + * @param {object} root0.nameMaps - Object containing name mappings for resources. + * @returns {Component} - Autocomplete input. + */ +export const ResourceIDAutocomplete = ({ + selectedType, + state, + actions, + validateResourceId, + filteredResourceIDs, + palette, + nameMaps, +}) => { + const [inputValue, setInputValue] = useState('') + + return ( + { + setInputValue(newInputValue) + }} + options={filteredResourceIDs} + getOptionLabel={(option) => + nameMaps[selectedType]?.[option.toString()] ?? option.toString() + } + freeSolo + value={state.globalIds ?? []} + disabled={selectedType === 'VM'} + onKeyDown={(event) => { + if (event.key === 'Enter' && event.target.value) { + event.preventDefault() + const newId = event.target.value + if (validateResourceId(newId, state?.globalIds, actions.setIsValid)) { + const newValue = [...state.globalIds, newId] + actions.setGlobalIds(newValue) + } + } + }} + renderOption={(option, { selected }) => ( + { + if (!state.globalIds.includes(option?.key)) { + const newValue = [...state.globalIds, option?.key] + actions.setGlobalIds(newValue) + } + }} + > + {option?.key} + { + event.stopPropagation() + if (!state.globalIds.includes(option?.key)) { + const newValue = [...state.globalIds, option?.key] + actions.setGlobalIds(newValue) + } + actions.setMarkForDeletion(option?.key) + }} + > + + + + )} + renderInput={(params) => ( + { + const value = event.target.value.trim() + if ( + value && + validateResourceId(value, state?.globalIds, actions.setIsValid) + ) { + if (!state.globalIds.includes(value)) { + actions.setGlobalIds([...state.globalIds, value]) + } + } + setInputValue('') + }} + variant="outlined" + label="Resource IDs" + placeholder={ + inputValue || state?.globalIds?.length > 1 + ? '' + : 'Select or type a Resource ID' + } + fullWidth + error={!state.isValid} + helperText={ + !state.isValid && + 'Invalid format or duplicate ID. Please enter a positive number.' + } + sx={{ + '.MuiOutlinedInput-root': { + minHeight: '56px', + }, + '.MuiOutlinedInput-input': { + padding: '16px', + }, + }} + /> + )} + onChange={(_event, value) => { + state.globalIds.forEach((id) => { + if (!value.includes(id)) { + actions.setUnmarkForDeletion(id) + } + }) + if (value.length === 0) { + actions.setValues({}) + actions.setGlobalIds([]) + actions.setGlobalValue('') + } + }} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + { + actions.setUnmarkForDeletion(option) + const newValue = state.globalIds.filter((id) => id !== option) + actions.setGlobalIds(newValue) + }} + /> + )) + } + /> + ) +} + +ResourceIDAutocomplete.propTypes = { + selectedType: PropTypes.string, + state: PropTypes.shape({ + globalIds: PropTypes.array, + isValid: PropTypes.bool, + markedForDeletion: PropTypes.array, + }), + actions: PropTypes.objectOf(PropTypes.func), + validateResourceId: PropTypes.func, + filteredResourceIDs: PropTypes.arrayOf(PropTypes.any), + palette: PropTypes.shape({ + action: PropTypes.shape({ + hover: PropTypes.string, + }), + error: PropTypes.shape({ + main: PropTypes.string, + }), + background: PropTypes.shape({ + default: PropTypes.string, + }), + }), + nameMaps: PropTypes.object, +} + +ResourceIDAutocomplete.defaultProps = { + palette: {}, +} diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/index.js b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/index.js new file mode 100644 index 00000000000..d89619e6262 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/helpers/subcomponents/index.js @@ -0,0 +1,19 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { ResourceIDAutocomplete } from './ResourceIDAutocomplete' +import { HybridInputField } from './HybridInput' + +export { ResourceIDAutocomplete, HybridInputField } diff --git a/src/fireedge/src/client/components/Tabs/Quota/Components/index.js b/src/fireedge/src/client/components/Tabs/Quota/Components/index.js new file mode 100644 index 00000000000..1b32f515454 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/Components/index.js @@ -0,0 +1,16 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ +export { QuotaControls } from 'client/components/Tabs/Quota/Components/QuotaControls' diff --git a/src/fireedge/src/client/components/Tabs/Quota/index.js b/src/fireedge/src/client/components/Tabs/Quota/index.js new file mode 100644 index 00000000000..0284500ae39 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Quota/index.js @@ -0,0 +1,317 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { useState, useMemo } from 'react' +import { Box, Card, CardContent, Grid, Typography } from '@mui/material' +import { MultiChart } from 'client/components/Charts' +import { transformApiResponseToDataset } from 'client/components/Charts/MultiChart/helpers/scripts' +import { QuotaControls } from 'client/components/Tabs/Quota/Components' +import { useGetUserQuery } from 'client/features/OneApi/user' +import { useGetGroupQuery } from 'client/features/OneApi/group' +import { nameMapper } from 'client/components/Tabs/Quota/Components/helpers/scripts' +import { useGetDatastoresQuery } from 'client/features/OneApi/datastore' +import { useGetVNetworksQuery } from 'client/features/OneApi/network' +import { useGetImagesQuery } from 'client/features/OneApi/image' + +/** + * Generates a QuotaInfoTab for an user or a group. + * + * @param {object} props - Input properties + * @param {boolean} props.groups - If it's a group or not + * @returns {object} - The QuotasInfoTab component + */ +const generateQuotasInfoTab = ({ groups }) => { + const QuotasInfoTab = ({ id }) => { + const datastoresResponse = useGetDatastoresQuery() + const networksResponse = useGetVNetworksQuery() + const imagesResponse = useGetImagesQuery() + const [dsNameMap, setDsNameMap] = useState({}) + const [imgNameMap, setImgNameMap] = useState({}) + const [netNameMap, setNetNameMap] = useState({}) + const [selectedType, setSelectedType] = useState('VM') + const [clickedElement, setClickedElement] = useState(null) + const queryInfo = groups + ? useGetGroupQuery({ id }) + : useGetUserQuery({ id }) + + const apiData = queryInfo?.data || {} + + useMemo(() => { + if (datastoresResponse.isSuccess && datastoresResponse.data) { + setDsNameMap(nameMapper(datastoresResponse)) + } + if (networksResponse.isSuccess && networksResponse.data) { + setNetNameMap(nameMapper(networksResponse)) + } + if (imagesResponse.isSuccess && imagesResponse.data) { + setImgNameMap(nameMapper(imagesResponse)) + } + }, [datastoresResponse, networksResponse, imagesResponse]) + + const nameMaps = { + DATASTORE: dsNameMap, + NETWORK: netNameMap, + IMAGE: imgNameMap, + } + + const handleChartElementClick = (data) => { + setClickedElement(data) + } + + const generateKeyMap = (data) => { + const keyMap = {} + if (Array.isArray(data)) { + Object.keys(data[0] || {}).forEach((key) => { + keyMap[key] = key + }) + } else { + Object.keys(data || {}).forEach((key) => { + keyMap[key] = key + }) + } + + return keyMap + } + + const generateMetricKeys = (quotaTypes) => { + const metricKeys = {} + quotaTypes.forEach((config) => { + metricKeys[config.type] = Object.values(config.keyMap).filter( + (key) => key !== 'ID' + ) + }) + + return metricKeys + } + + const generateMetricNames = (quotaTypes) => { + const metricNames = {} + + quotaTypes.forEach((config) => { + Object.keys(config.keyMap).forEach((key) => { + const transformedKey = key + .replace(/_/g, ' ') + .split(' ') + .map( + (word) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join(' ') + + metricNames[key] = transformedKey + }) + }) + + return metricNames + } + + const processDataset = (dataset) => { + if (!dataset || !dataset.data) return { ...dataset } + + const newData = dataset.data.map((item) => { + const newItem = { ...item } + if (newItem.ID && nameMaps?.[selectedType]?.[newItem.ID]) { + newItem.ID = nameMaps?.[selectedType]?.[newItem.ID] + } + Object.keys(newItem).forEach((key) => { + const value = parseFloat(newItem[key]) + if (value < 0) { + newItem[key] = '0' + } + }) + + return newItem + }) + + return { + ...dataset, + data: newData, + } + } + + const quotaTypesConfig = [ + { + title: 'VM Quota', + quota: Array.isArray(apiData.VM_QUOTA) + ? apiData.VM_QUOTA + : [apiData.VM_QUOTA], + type: 'VM', + keyMap: generateKeyMap(apiData.VM_QUOTA), + }, + { + title: 'Datastore Quota', + quota: Array.isArray(apiData.DATASTORE_QUOTA) + ? apiData.DATASTORE_QUOTA + : [apiData.DATASTORE_QUOTA], + type: 'DATASTORE', + keyMap: generateKeyMap(apiData.DATASTORE_QUOTA), + }, + { + title: 'Network Quota', + quota: Array.isArray(apiData.NETWORK_QUOTA) + ? apiData.NETWORK_QUOTA + : [apiData.NETWORK_QUOTA], + type: 'NETWORK', + keyMap: generateKeyMap(apiData.NETWORK_QUOTA), + }, + { + title: 'Image Quota', + quota: Array.isArray(apiData.IMAGE_QUOTA) + ? apiData.IMAGE_QUOTA + : [apiData.IMAGE_QUOTA], + type: 'IMAGE', + keyMap: generateKeyMap(apiData.IMAGE_QUOTA), + }, + ] + const dynamicMetricKeys = generateMetricKeys(quotaTypesConfig) + const dynamicMetricNames = generateMetricNames(quotaTypesConfig) + + const allDatasets = quotaTypesConfig.map((quotaType, index) => { + const nestedQuotaData = { nestedData: quotaType.quota } + + const { dataset, error, isEmpty } = transformApiResponseToDataset( + nestedQuotaData, + quotaType.keyMap, + dynamicMetricKeys[quotaTypesConfig[index].type], + () => quotaType.type + ) + + return { + dataset: { ...dataset, error, isEmpty }, + } + }) + + const selectedDataset = allDatasets.find( + (_datasetObj, index) => quotaTypesConfig[index].type === selectedType + ) + + const processedDataset = processDataset(selectedDataset?.dataset) + + return ( + + + + + + + Quota Controls + + + ID + )} + clickedElement={clickedElement} + nameMaps={nameMaps} + groups={groups} + /> + + + + + + + + + + + + + ) + } + + QuotasInfoTab.propTypes = { + id: PropTypes.string.isRequired, + } + + QuotasInfoTab.displayName = 'QuotasInfoTab' + + return QuotasInfoTab +} + +export default generateQuotasInfoTab diff --git a/src/fireedge/src/client/components/Tabs/Showback/index.js b/src/fireedge/src/client/components/Tabs/Showback/index.js new file mode 100644 index 00000000000..6d9d6c40467 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Showback/index.js @@ -0,0 +1,278 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { LoadingDisplay } from 'client/components/LoadingState' +import { MultiChart } from 'client/components/Charts' +import { transformApiResponseToDataset } from 'client/components/Charts/MultiChart/helpers/scripts' +import { DateRangeFilter } from 'client/components/Date' +import { useLazyGetShowbackPoolFilteredQuery } from 'client/features/OneApi/vm' +import { Box, Button } from '@mui/material' +import { Component, useState, useEffect } from 'react' +import { DateTime } from 'luxon' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' + +const keyMap = { + VMID: 'OID', + VMNAME: 'NAME', + UNAME: 'UNAME', + GNAME: 'GNAME', + YEAR: 'YEAR', + MONTH: 'MONTH', + CPU_COST: 'cpuCost', + MEMORY_COST: 'memoryCost', + DISK_COST: 'diskCost', + TOTAL_COST: 'totalCost', + HOURS: 'hours', + RHOURS: 'rHours', +} + +const DataGridColumns = [ + { field: 'OID', headerName: 'ID', flex: 1 }, + { field: 'NAME', headerName: 'Name', flex: 1 }, + { field: 'UNAME', headerName: 'Owner', flex: 1 }, + { field: 'totalCost', headerName: 'Cost', flex: 1, type: 'number' }, + { field: 'hours', headerName: 'Hours', flex: 1, type: 'number' }, +] + +const smallTableColumns = [ + { field: 'MONTH', headerName: 'Month', flex: 1 }, + { field: 'totalCost', headerName: 'Total Cost', flex: 1, type: 'number' }, +] + +const metricKeys = ['cpuCost', 'memoryCost', 'diskCost', 'totalCost'] + +const metricNames = { + cpuCost: 'CPU', + memoryCost: 'Memory', + diskCost: 'Disk', +} + +const topMetricNames = { + MONTH: 'Month', + totalCost: 'Total Cost', +} + +const commonStyles = { + minHeight: '250px', + width: '100%', + position: 'relative', + marginTop: 2, +} + +const labelingFunc = (record) => `${record.YEAR}-${record.MONTH}` + +/** + * Generates a ShowbackInfoTab for an user or a group. + * + * @param {object} props - Input properties + * @param {boolean} props.groups - If it's a group or not + * @returns {object} - The ShowbackInfoTab component + */ +const generateShowbackInfoTab = ({ groups }) => { + /** + * ShowbackInfoTab component displays showback information for an user or a group. + * + * @param {string} id - User or group ID. + * @returns {Component} Rendered component. + */ + const ShowbackInfoTab = ({ id }) => { + // Create hooks for chart data + const [topChartsData, setTopChartsData] = useState([]) + const [transformedResult, setTransformedResult] = useState() + + // Create hooks for date range + const [dateRange, setDateRange] = useState({ + startDate: DateTime.now().minus({ months: 1 }), + endDate: DateTime.now(), + }) + + const handleDateChange = (newDateRange) => { + setDateRange(newDateRange) + } + + // Hook for fetch data + const [getShowback, queryData] = useLazyGetShowbackPoolFilteredQuery() + + // Call the API to refetch data + const refetchData = () => { + const paramName = groups ? 'group' : 'user' + getShowback({ + [paramName]: id, + startMonth: dateRange.startDate.month, + startYear: dateRange.startDate.year, + endMonth: dateRange.endDate.month, + endYear: dateRange.endDate.year, + }) + } + + // Refetch data when click on Get showback button + const handleGetShowbackClick = () => refetchData() + + // First render of the component + useEffect(() => refetchData(), []) + + // Hook after fetch data + useEffect(() => { + if (!queryData.isLoading && queryData.data) { + // Transform data + const transformedResultData = transformApiResponseToDataset( + queryData, + keyMap, + metricKeys, + labelingFunc + ) + + // Update data + setTransformedResult(transformedResultData) + setTopChartsData([transformedResultData].map(aggregateTotalCostByMonth)) + } + }, [queryData]) + + // Create dataset function + const aggregateTotalCostByMonth = (datasetWrapper) => { + const dataset = datasetWrapper.dataset + + if (!dataset.data || dataset.data.length === 0) { + return { + ...dataset, + isEmpty: datasetWrapper.isEmpty, + } + } + + const aggregated = dataset.data.reduce((acc, record) => { + if ( + record.MONTH && + record.totalCost !== null && + record.totalCost !== undefined + ) { + if (!acc[record.MONTH]) { + acc[record.MONTH] = { ...record, totalCost: 0 } + } + acc[record.MONTH].totalCost += parseFloat(record.totalCost) + } + + return acc + }, {}) + + // Return dataset + return { + id: dataset.id, + data: Object.values(aggregated), + metrics: dataset.metrics, + label: dataset.label, + isEmpty: datasetWrapper.isEmpty, + } + } + + // Show loading component + if ( + queryData.isLoading || + queryData.isFetching || + queryData.isError || + !queryData.data || + !topChartsData || + topChartsData.length === 0 + ) { + return ( + + ) + } + + return ( + + + + + + + + + + + + + + + + + + + + ) + } + + ShowbackInfoTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, + } + + ShowbackInfoTab.displayName = 'ShowbackInfoTab' + + return ShowbackInfoTab +} + +export default generateShowbackInfoTab diff --git a/src/fireedge/src/client/components/Tabs/User/Accounting/index.js b/src/fireedge/src/client/components/Tabs/User/Accounting/index.js deleted file mode 100644 index 01638a4a983..00000000000 --- a/src/fireedge/src/client/components/Tabs/User/Accounting/index.js +++ /dev/null @@ -1,310 +0,0 @@ -/* ------------------------------------------------------------------------- * - * Copyright 2002-2023, 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 PropTypes from 'prop-types' -import { Component, useState, useEffect, useCallback } from 'react' -import { Plus } from 'iconoir-react' -import { - Select, - MenuItem, - InputLabel, - FormControl, - Box, - Button, - Chip, - Tooltip, -} from '@mui/material' -import { - MetricSelector, - CustomizedChart, -} from 'client/components/Tabs/User/Accounting/components' -import { DateRangeFilter } from 'client/components/Date' -import AdapterLuxon from '@mui/lab/AdapterLuxon' -import { DateTime } from 'luxon' -import { LocalizationProvider } from '@mui/lab' -import { - getDefaultDateRange, - useAccountingData, - calculateDisplayMetrics, -} from 'client/components/Tabs/User/Accounting/helpers' -import { filterDataset } from 'client/components/Charts/MultiChart/helpers/scripts' - -const ACTION_ADD = 'add' -const ACTION_REMOVE = 'remove' -const DATASETS_LIMIT = 4 - -/** - * AccountingInfoTab component displays accounting information for a given ID. - * It provides options to filter the data by date range, chart type, and grouping. - * - * @param {object} props - Component properties. - * @param {string} props.id - The ID for which accounting information is to be displayed. - * @returns {Component} Rendered AccountingInfoTab component. - */ -const AccountingInfoTab = ({ id }) => { - const [dateRange, setDateRange] = useState(getDefaultDateRange()) // LAST 7 DAYS - const { data, isLoading, error } = useAccountingData({ id }) - const [datasets, setDatasets] = useState([]) - const [visibleDatasets, setVisibleDatasets] = useState([]) - const [chartType, setChartType] = useState('line') - const [groupBy, setGroupBy] = useState('NAME') - const [showTooltip, setShowTooltip] = useState(false) - const [selectedMetrics, setSelectedMetrics] = useState({ - cpuHours: true, - memoryGBHours: true, - diskMBHours: true, - }) - - useEffect(() => { - if (!isLoading && data) { - const defaultDataset = createDataset(data, dateRange, chartType) - setDatasets([defaultDataset]) - setVisibleDatasets([defaultDataset.id]) - } - }, [isLoading]) - - const isWithinDateRange = (record, startDate, endDate) => { - const recordDate = DateTime.fromSeconds(parseInt(record.STIME, 10)) - - return recordDate >= startDate && recordDate <= endDate - } - - // eslint-disable-next-line no-shadow - const createDataset = (data, dateRange) => { - const result = filterDataset( - data, - (record) => - isWithinDateRange(record, dateRange.startDate, dateRange.endDate), - (record) => - `${dateRange.startDate.toFormat( - 'MMM dd, yyyy' - )} - ${dateRange.endDate.toFormat('MMM dd, yyyy')}` - ) - - const filteredDataset = result.dataset - let filteredData = - filteredDataset && filteredDataset.data ? filteredDataset.data : [] - - filteredData.sort((a, b) => { - if (a.ETIME === '0') return 1 - if (b.ETIME === '0') return -1 - - return b.ETIME - a.ETIME - }) - - const seenIds = new Set() - filteredData = filteredData.filter((record) => { - if (seenIds.has(record.ID)) { - return false - } - seenIds.add(record.ID) - - return true - }) - - const metrics = calculateDisplayMetrics(filteredData) - const label = `${dateRange.startDate.toFormat( - 'MMM dd, yyyy' - )} - ${dateRange.endDate.toFormat('MMM dd, yyyy')}` - - return { - id: Date.now(), - data: filteredData, - metrics: metrics, - label: label, - isEmpty: result.isEmpty, - } - } - - // Event handlers - - const toggleDatasetVisibility = (datasetId) => { - setVisibleDatasets((prevVisible) => { - if (prevVisible.includes(datasetId)) { - // eslint-disable-next-line no-shadow - return prevVisible.filter((id) => id !== datasetId) - } else { - return [...prevVisible, datasetId] - } - }) - } - - const handleDatasetChange = useCallback( - (action, datasetToRemove = null) => { - if (action === ACTION_ADD) { - if (datasets.length >= DATASETS_LIMIT) { - setShowTooltip(true) - setTimeout(() => setShowTooltip(false), 3000) - - return - } - const newDataset = createDataset(data, dateRange) - setDatasets((prevDatasets) => [...prevDatasets, newDataset]) - - setVisibleDatasets((prevVisible) => [...prevVisible, newDataset.id]) - } else if (action === ACTION_REMOVE && datasetToRemove) { - setDatasets((prevDatasets) => - prevDatasets.filter((dataset) => dataset.id !== datasetToRemove.id) - ) - - setVisibleDatasets((prevVisible) => - // eslint-disable-next-line no-shadow - prevVisible.filter((id) => id !== datasetToRemove.id) - ) - } - }, - [datasets, data, dateRange] - ) - - const handleMetricChange = useCallback((event) => { - const { name, checked } = event.target - setSelectedMetrics((prevMetrics) => ({ - ...prevMetrics, - [name]: checked, - })) - }, []) - - const handleChartTypeChange = useCallback( - (event) => { - const newChartType = event.target.value - setChartType(newChartType) - }, - [data, dateRange] - ) - - const handleGroupByChange = (event) => { - setGroupBy(event.target.value) - } - - return ( - - - - - setDateRange(updatedRange)} - /> - - - - - - - Group By - - - - Chart Type - - - - - {chartType !== 'table' && ( - - )} - - {datasets.map((dataset) => ( - toggleDatasetVisibility(dataset.id)} - onDelete={(e) => { - e.stopPropagation() - handleDatasetChange('remove', dataset) - }} - style={{ - opacity: visibleDatasets.includes(dataset.id) ? 1 : 0.5, - }} - /> - ))} - - - - - ) -} - -AccountingInfoTab.propTypes = { - id: PropTypes.string, -} - -AccountingInfoTab.displayName = 'AccountingInfoTab' - -export default AccountingInfoTab diff --git a/src/fireedge/src/client/components/Tabs/User/Group.js b/src/fireedge/src/client/components/Tabs/User/Group.js index 89559bb0f72..247bf2e0850 100644 --- a/src/fireedge/src/client/components/Tabs/User/Group.js +++ b/src/fireedge/src/client/components/Tabs/User/Group.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { ReactElement } from 'react' +import { ReactElement, useMemo } from 'react' import PropTypes from 'prop-types' import { useHistory, generatePath } from 'react-router-dom' @@ -21,6 +21,11 @@ import { PATH } from 'client/apps/sunstone/routesOne' import { GroupsTable } from 'client/components/Tables' import { useGetUserQuery } from 'client/features/OneApi/user' +import { useGetGroupsQuery } from 'client/features/OneApi/group' + +import { Chip, Box, Grid, Typography } from '@mui/material' + +import { T } from 'client/constants' /** * Renders mainly information tab. @@ -32,6 +37,7 @@ import { useGetUserQuery } from 'client/features/OneApi/user' const GroupsInfoTab = ({ id }) => { const path = PATH.SYSTEM.GROUPS.DETAIL const history = useHistory() + const { data = [] } = useGetGroupsQuery() const { data: user } = useGetUserQuery({ id }) const { GROUPS } = user @@ -42,14 +48,76 @@ const GroupsInfoTab = ({ id }) => { const primaryGroup = GROUPS.ID[0] const secondaryGroups = GROUPS.ID.slice(1) + const primaryGroupName = useMemo(() => { + const primary = data.find( + (group) => + group.ID === primaryGroup || String(group.ID) === String(primaryGroup) + ) + + return primary?.NAME + }, [data, primaryGroup]) + + const secondaryGroupNames = useMemo(() => { + const foundGroups = data.filter((group) => + secondaryGroups.includes(String(group.ID)) + ) + + return foundGroups.map((group) => group.NAME) + }, [data, secondaryGroups]) + return ( - handleRowClick(row.ID)} - /> +
+ + {primaryGroupName && ( + + {T.Primary} + + )} + + {primaryGroupName && ( + + + {primaryGroupName} + + } + color="primary" + /> + + )} + + {secondaryGroupNames.length > 0 && ( + + {T.Secondary} + + )} + + {secondaryGroupNames.length > 0 && + secondaryGroupNames.map((name, index) => ( + + + {name} + + } + color="secondary" + /> + + ))} + + + + handleRowClick(row.ID)} + /> + +
) } diff --git a/src/fireedge/src/client/components/Tabs/User/Quota/index.js b/src/fireedge/src/client/components/Tabs/User/Quota/index.js deleted file mode 100644 index 6e35297d2f8..00000000000 --- a/src/fireedge/src/client/components/Tabs/User/Quota/index.js +++ /dev/null @@ -1,301 +0,0 @@ -/* ------------------------------------------------------------------------- * - * Copyright 2002-2023, 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 PropTypes from 'prop-types' -import { Component, useState, useMemo } from 'react' -import { Box, Card, CardContent, Grid, Typography } from '@mui/material' -import { MultiChart } from 'client/components/Charts' -import { transformApiResponseToDataset } from 'client/components/Charts/MultiChart/helpers/scripts' -import { QuotaControls } from 'client/components/Tabs/User/Quota/Components' -import { nameMapper } from 'client/components/Tabs/User/Quota/Components/helpers/scripts' -import { useGetUserQuery } from 'client/features/OneApi/user' -import { useGetDatastoresQuery } from 'client/features/OneApi/datastore' -import { useGetVNetworksQuery } from 'client/features/OneApi/network' -import { useGetImagesQuery } from 'client/features/OneApi/image' -/** - * QuotasInfoTab component. - * - * @param {object} props - Component properties. - * @param {string} props.id - User ID. - * @returns {Component} Rendered component. - */ -const QuotasInfoTab = ({ id }) => { - const datastoresResponse = useGetDatastoresQuery() - const networksResponse = useGetVNetworksQuery() - const imagesResponse = useGetImagesQuery() - const [dsNameMap, setDsNameMap] = useState({}) - const [imgNameMap, setImgNameMap] = useState({}) - const [netNameMap, setNetNameMap] = useState({}) - const [selectedType, setSelectedType] = useState('VM') - const [clickedElement, setClickedElement] = useState(null) - const queryInfo = useGetUserQuery({ id }) - const apiData = queryInfo?.data || {} - - useMemo(() => { - if (datastoresResponse.isSuccess && datastoresResponse.data) { - setDsNameMap(nameMapper(datastoresResponse)) - } - if (networksResponse.isSuccess && networksResponse.data) { - setNetNameMap(nameMapper(networksResponse)) - } - if (imagesResponse.isSuccess && imagesResponse.data) { - setImgNameMap(nameMapper(imagesResponse)) - } - }, [datastoresResponse, networksResponse, imagesResponse]) - - const nameMaps = { - DATASTORE: dsNameMap, - NETWORK: netNameMap, - IMAGE: imgNameMap, - } - - const handleChartElementClick = (data) => { - setClickedElement(data) - } - - const generateKeyMap = (data) => { - const keyMap = {} - if (Array.isArray(data)) { - Object.keys(data[0] || {}).forEach((key) => { - keyMap[key] = key - }) - } else { - Object.keys(data || {}).forEach((key) => { - keyMap[key] = key - }) - } - - return keyMap - } - - const generateMetricKeys = (quotaTypes) => { - const metricKeys = {} - quotaTypes.forEach((config) => { - metricKeys[config.type] = Object.values(config.keyMap).filter( - (key) => key !== 'ID' - ) - }) - - return metricKeys - } - - const generateMetricNames = (quotaTypes) => { - const metricNames = {} - - quotaTypes.forEach((config) => { - Object.keys(config.keyMap).forEach((key) => { - const transformedKey = key - .replace(/_/g, ' ') - .split(' ') - .map( - (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() - ) - .join(' ') - - metricNames[key] = transformedKey - }) - }) - - return metricNames - } - - const processDataset = (dataset) => { - if (!dataset || !dataset.data) return { ...dataset } - - const newData = dataset.data.map((item) => { - const newItem = { ...item } - if (newItem.ID && nameMaps?.[selectedType]?.[newItem.ID]) { - newItem.ID = nameMaps?.[selectedType]?.[newItem.ID] - } - Object.keys(newItem).forEach((key) => { - const value = parseFloat(newItem[key]) - if (value < 0) { - newItem[key] = '0' - } - }) - - return newItem - }) - - return { - ...dataset, - data: newData, - } - } - - const quotaTypesConfig = [ - { - title: 'VM Quota', - quota: Array.isArray(apiData.VM_QUOTA) - ? apiData.VM_QUOTA - : [apiData.VM_QUOTA], - type: 'VM', - keyMap: generateKeyMap(apiData.VM_QUOTA), - }, - { - title: 'Datastore Quota', - quota: Array.isArray(apiData.DATASTORE_QUOTA) - ? apiData.DATASTORE_QUOTA - : [apiData.DATASTORE_QUOTA], - type: 'DATASTORE', - keyMap: generateKeyMap(apiData.DATASTORE_QUOTA), - }, - { - title: 'Network Quota', - quota: Array.isArray(apiData.NETWORK_QUOTA) - ? apiData.NETWORK_QUOTA - : [apiData.NETWORK_QUOTA], - type: 'NETWORK', - keyMap: generateKeyMap(apiData.NETWORK_QUOTA), - }, - { - title: 'Image Quota', - quota: Array.isArray(apiData.IMAGE_QUOTA) - ? apiData.IMAGE_QUOTA - : [apiData.IMAGE_QUOTA], - type: 'IMAGE', - keyMap: generateKeyMap(apiData.IMAGE_QUOTA), - }, - ] - const dynamicMetricKeys = generateMetricKeys(quotaTypesConfig) - const dynamicMetricNames = generateMetricNames(quotaTypesConfig) - - const allDatasets = quotaTypesConfig.map((quotaType, index) => { - const nestedQuotaData = { nestedData: quotaType.quota } - - const { dataset, error, isEmpty } = transformApiResponseToDataset( - nestedQuotaData, - quotaType.keyMap, - dynamicMetricKeys[quotaTypesConfig[index].type], - () => quotaType.type - ) - - return { - dataset: { ...dataset, error, isEmpty }, - } - }) - - const selectedDataset = allDatasets.find( - (_datasetObj, index) => quotaTypesConfig[index].type === selectedType - ) - - const processedDataset = processDataset(selectedDataset?.dataset) - - return ( - - - - - - - Quota Controls - - - ID - )} - clickedElement={clickedElement} - nameMaps={nameMaps} - /> - - - - - - - - - - - - - ) -} - -QuotasInfoTab.propTypes = { - id: PropTypes.string.isRequired, -} - -export default QuotasInfoTab diff --git a/src/fireedge/src/client/components/Tabs/User/Showback.js b/src/fireedge/src/client/components/Tabs/User/Showback.js deleted file mode 100644 index 76176d146fe..00000000000 --- a/src/fireedge/src/client/components/Tabs/User/Showback.js +++ /dev/null @@ -1,277 +0,0 @@ -/* ------------------------------------------------------------------------- * - * Copyright 2002-2023, 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 PropTypes from 'prop-types' -import { LoadingDisplay } from 'client/components/LoadingState' -import { MultiChart } from 'client/components/Charts' -import { - transformApiResponseToDataset, - filterDataset, -} from 'client/components/Charts/MultiChart/helpers/scripts' -import { DateRangeFilter } from 'client/components/Date' -import { - useGetShowbackPoolQuery, - useLazyCalculateShowbackQuery, -} from 'client/features/OneApi/vm' -import { Box, Button } from '@mui/material' -import { Component, useState } from 'react' -import { DateTime } from 'luxon' -import { useGeneralApi } from 'client/features/General' - -const keyMap = { - VMID: 'OID', - VMNAME: 'NAME', - UNAME: 'UNAME', - GNAME: 'GNAME', - YEAR: 'YEAR', - MONTH: 'MONTH', - CPU_COST: 'cpuCost', - MEMORY_COST: 'memoryCost', - DISK_COST: 'diskCost', - TOTAL_COST: 'totalCost', - HOURS: 'hours', - RHOURS: 'rHours', -} - -const DataGridColumns = [ - { field: 'OID', headerName: 'ID', flex: 1 }, - { field: 'NAME', headerName: 'Name', flex: 1 }, - { field: 'UNAME', headerName: 'Owner', flex: 1 }, - { field: 'totalCost', headerName: 'Cost', flex: 1, type: 'number' }, - { field: 'hours', headerName: 'Hours', flex: 1, type: 'number' }, -] - -const smallTableColumns = [ - { field: 'MONTH', headerName: 'Month', flex: 1 }, - { field: 'totalCost', headerName: 'Total Cost', flex: 1, type: 'number' }, -] - -const metricKeys = ['cpuCost', 'memoryCost', 'diskCost', 'totalCost'] - -const metricNames = { - cpuCost: 'CPU', - memoryCost: 'Memory', - diskCost: 'Disk', -} - -const topMetricNames = { - MONTH: 'Month', - totalCost: 'Total Cost', -} - -const commonStyles = { - minHeight: '350px', - width: '100%', - position: 'relative', - marginTop: 2, -} - -const labelingFunc = (record) => `${record.YEAR}-${record.MONTH}` - -/** - * ShowbackInfoTab component displays showback information for a user. - * - * @param {object} props - Component properties. - * @param {string} props.id - User ID. - * @returns {Component} Rendered component. - */ -const ShowbackInfoTab = ({ id }) => { - const [calculateShowback] = useLazyCalculateShowbackQuery() - const { enqueueError, enqueueSuccess } = useGeneralApi() - - const filter = Number(id) - const startMonth = -1 - const startYear = -1 - const endMonth = -1 - const endYear = -1 - - const queryData = useGetShowbackPoolQuery({ - filter, - startMonth, - startYear, - endMonth, - endYear, - }) - - const [dateRange, setDateRange] = useState({ - startDate: DateTime.now().minus({ months: 1 }), - endDate: DateTime.now(), - }) - - const dateFilterFn = (record) => { - const recordDate = DateTime.fromObject({ - year: parseInt(record.YEAR, 10), - month: parseInt(record.MONTH, 10), - }) - - return recordDate >= dateRange.startDate && recordDate <= dateRange.endDate - } - - const handleDateChange = (newDateRange) => { - setDateRange(newDateRange) - } - - const handleCalculateClick = async () => { - const params = { - startMonth, - startYear, - endMonth, - endYear, - } - - try { - await calculateShowback(params) - enqueueSuccess('Showback calculated') - } catch (error) { - enqueueError(`Error calculating showback: ${error.message}`) - } - } - - const isLoading = queryData.isLoading - let error - - const aggregateTotalCostByMonth = (datasetWrapper) => { - const dataset = datasetWrapper.dataset - - if (!dataset.data || dataset.data.length === 0) { - return { - ...dataset, - isEmpty: datasetWrapper.isEmpty, - } - } - - const aggregated = dataset.data.reduce((acc, record) => { - if ( - record.MONTH && - record.totalCost !== null && - record.totalCost !== undefined - ) { - if (!acc[record.MONTH]) { - acc[record.MONTH] = { ...record, totalCost: 0 } - } - acc[record.MONTH].totalCost += parseFloat(record.totalCost) - } - - return acc - }, {}) - - return { - id: dataset.id, - data: Object.values(aggregated), - metrics: dataset.metrics, - label: dataset.label, - isEmpty: datasetWrapper.isEmpty, - } - } - - let filteredResult - let topChartsData - - if (!isLoading && queryData.isSuccess) { - const transformedResult = transformApiResponseToDataset( - queryData, - keyMap, - metricKeys, - labelingFunc - ) - error = transformedResult.error - - filteredResult = filterDataset( - transformedResult.dataset, - dateFilterFn, - labelingFunc - ) - - topChartsData = [filteredResult].map(aggregateTotalCostByMonth) - } - - if (isLoading || error) { - return - } - - return ( - - - - - - - - - - - - - - - - - - - - ) -} - -ShowbackInfoTab.propTypes = { - tabProps: PropTypes.object, - id: PropTypes.string, -} - -ShowbackInfoTab.displayName = 'ShowbackInfoTab' - -export default ShowbackInfoTab diff --git a/src/fireedge/src/client/components/Tabs/User/index.js b/src/fireedge/src/client/components/Tabs/User/index.js index 2b7a7810089..62da55545b1 100644 --- a/src/fireedge/src/client/components/Tabs/User/index.js +++ b/src/fireedge/src/client/components/Tabs/User/index.js @@ -25,18 +25,18 @@ import { getAvailableInfoTabs } from 'client/models/Helper' import Tabs from 'client/components/Tabs' import Info from 'client/components/Tabs/User/Info' import Group from 'client/components/Tabs/User//Group' -import Quota from 'client/components/Tabs/User//Quota' -import Accounting from 'client/components/Tabs/User//Accounting' -import Showback from 'client/components/Tabs/User//Showback' +import generateQuotasInfoTab from 'client/components/Tabs//Quota' +import generateAccountingInfoTab from 'client/components/Tabs/Accounting' +import generateShowbackInfoTab from 'client/components/Tabs/Showback' import Authentication from 'client/components/Tabs/User//Authentication' const getTabComponent = (tabName) => ({ info: Info, group: Group, - quota: Quota, - accounting: Accounting, - showback: Showback, + quota: generateQuotasInfoTab({ groups: false }), + accounting: generateAccountingInfoTab({ groups: false }), + showback: generateShowbackInfoTab({ groups: false }), authentication: Authentication, }[tabName]) diff --git a/src/fireedge/src/client/constants/acl.js b/src/fireedge/src/client/constants/acl.js new file mode 100644 index 00000000000..0eefafe9cff --- /dev/null +++ b/src/fireedge/src/client/constants/acl.js @@ -0,0 +1,64 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +// File with constants about ACLs + +// Types of id definition +export const ACL_TYPE_ID = { + INDIVIDUAL: '#', + GROUP: '@', + ALL: '*', + CLUSTER: '%', +} + +// Hex values for different user types +export const ACL_ID = { + '#': 0x100000000, + '@': 0x200000000, + '*': 0x400000000, + '%': 0x800000000, +} + +// Hex values for different resource types +export const ACL_RESOURCES = { + VM: { name: 'VM', value: 0x1000000000n }, + HOST: { name: 'HOST', value: 0x2000000000n }, + NET: { name: 'NET', value: 0x4000000000n }, + IMAGE: { name: 'IMAGE', value: 0x8000000000n }, + USER: { name: 'USER', value: 0x10000000000n }, + TEMPLATE: { name: 'TEMPLATE', value: 0x20000000000n }, + GROUP: { name: 'GROUP', value: 0x40000000000n }, + DATASTORE: { name: 'DATASTORE', value: 0x100000000000n }, + CLUSTER: { name: 'CLUSTER', value: 0x200000000000n }, + DOCUMENT: { name: 'DOCUMENT', value: 0x400000000000n }, + ZONE: { name: 'ZONE', value: 0x800000000000n }, + SECGROUP: { name: 'SECGROUP', value: 0x1000000000000n }, + VDC: { name: 'VDC', value: 0x2000000000000n }, + VROUTER: { name: 'VROUTER', value: 0x4000000000000n }, + MARKETPLACE: { name: 'MARKETPLACE', value: 0x8000000000000n }, + MARKETPLACEAPP: { name: 'MARKETPLACEAPP', value: 0x10000000000000n }, + VMGROUP: { name: 'VMGROUP', value: 0x20000000000000n }, + VNTEMPLATE: { name: 'VNTEMPLATE', value: 0x40000000000000n }, + BACKUPJOB: { name: 'BACKUPJOB', value: 0x100000000000000n }, +} + +// Hex values for different right types +export const ACL_RIGHTS = { + USE: { name: 'USE', value: 0x1 }, + MANAGE: { name: 'MANAGE', value: 0x2 }, + ADMIN: { name: 'ADMIN', value: 0x4 }, + CREATE: { name: 'CREATE', value: 0x8 }, +} diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 1d6cb79d5ec..970d6221af0 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -188,6 +188,7 @@ export * as T from 'client/constants/translates' export * as ACTIONS from 'client/constants/actions' export * as STATES from 'client/constants/states' +export * from 'client/constants/acl' export * from 'client/constants/backupjob' export * from 'client/constants/cluster' @@ -206,6 +207,7 @@ export * from 'client/constants/provision' export * from 'client/constants/quota' export * from 'client/constants/scheduler' export * from 'client/constants/securityGroup' +export * from 'client/constants/system' export * from 'client/constants/user' export * from 'client/constants/userInput' export * from 'client/constants/vdc' diff --git a/src/fireedge/src/client/constants/system.js b/src/fireedge/src/client/constants/system.js new file mode 100644 index 00000000000..f0175c063f2 --- /dev/null +++ b/src/fireedge/src/client/constants/system.js @@ -0,0 +1,17 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ +/** @enum {string} Base path for Open Nebula documentation */ +export const DOCS_BASE_PATH = 'https://docs.opennebula.io' diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 4225c078adf..81cb7b54316 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -1323,6 +1323,7 @@ module.exports = { /* Marketplace App schema */ /* Marketplace App - general */ + MarketplaceApp: 'Marketplace app', RegisteredAt: 'Registered %s', LastBackupTime: 'Last Backup Time: %s', LastBackupTimeInfo: 'Last Backup Time', @@ -1486,4 +1487,69 @@ module.exports = { 'validation.array.min': 'Must have at least %s item(s) to act as a default', 'validation.array.max': 'Must have less than or equal to %s item(s)', 'validation.array.length': 'Must have %s item(s)', + + /* system - groups */ + 'groups.users.total': 'Total users: %1$s', + 'groups.name': 'Group name', + 'groups.adminUser.title': 'Create an administrator user', + 'groups.views.title': 'Views', + 'groups.general.info': + 'New groups are automatically added to the default VDC', + 'groups.system.defaultImagePersistentNew.title': + 'Make new images persistent by default', + 'groups.system.defaultImagePersistentNew.tooltip': + 'Control the default value for the PERSISTENT attribute on image creation (oneimage create).', + 'groups.system.defaultImagePersistent.title': + 'Make save-as and clone images persistent by default', + 'groups.system.defaultImagePersistent.tooltip': + 'Control the default value for the PERSISTENT attribute on image creation (oneimage clone, onevm disk-saveas). If blank images will inherit the persistent attribute from the base image.', + 'groups.permissions.resources': 'More resources', + 'groups.permissions.view.section': 'Permissions - View', + 'groups.permissions.view.section.concept': + "This will create new ACL Rules to define which virtual resources this group's users will be able to view.", + 'groups.permissions.view.check': + 'Allow users to view the VMs and Services of other users in the same group', + 'groups.permissions.view.check.concept': + 'An ACL Rule will be created to give users in this group access to all the resources in the same group.', + 'groups.permissions.create.section': 'Permissions - Create', + 'groups.permissions.create.section.concept': + "This will create new ACL Rules to define which virtual resources this group's users will be able to create.", + 'groups.permissions.create.documents': 'Documents', + 'groups.permissions.create.documents.concept': + 'Documents are a special tool used for general purposes, mainly by OneFlow. If you want to enable users of this group to use service composition via OneFlow, let it checked.', + 'groups.permissions.help.title': 'Permissions of a group', + 'groups.permissions.help.paragraph.1': + 'Select the permissions that the users who belong to the group will have.', + 'groups.permissions.help.paragraph.2': + 'On "Permissions - Create" select if the users could or not create the resources that are select.', + 'groups.permissions.help.paragraph.3': + 'On "Permissions - View" select if the users could or not view resources that other users of the group have created.', + 'groups.permissions.help.paragraph.link': + 'See Open Nebula documentation to get more details about groups and permissions.', + 'groups.views.group.section': 'Views - Groups', + 'groups.views.group.tooltip': + 'Select the default view and the views that any user on the group could use', + 'groups.views.admin.section': 'Views - Admin', + 'groups.views.admin.tooltip': + 'Select the default view and the views that only the admin users of the group could use', + 'groups.views.default': 'Default view', + 'groups.views.help.title': 'Views of a group', + 'groups.views.help.paragraph.1': + 'Select the views that the users who belong to the group will have.', + 'groups.views.help.paragraph.2': + 'On "Views - Groups" select the views and the default view for a regular user of the group.', + 'groups.views.help.paragraph.3': + 'On "Views - Admin" select the views and the default view for an admin user of the group.', + 'groups.views.help.paragraph.link': + 'See Open Nebula documentation to get more details about views on Fireedge Sunstone.', + 'groups.actions.edit.admins': 'Edit administrators', + 'groups.actions.edit.admins.form': 'Select the administrators', + 'groups.actions.edit.admins.success': 'Administrators updated', + + /* Showback */ + 'showback.title': 'Showback', + 'showback.button.getShowback': 'Get showback', + 'showback.button.calculateShowback': 'Calculate showback', + 'showback.button.help.paragraph.1': + 'Generate showback data to the interval selected in start and end date. After generate the showback data, you can access to the reports on the user or group Showback details. ', } diff --git a/src/fireedge/src/client/containers/Groups/Create.js b/src/fireedge/src/client/containers/Groups/Create.js new file mode 100644 index 00000000000..69a28ffce1b --- /dev/null +++ b/src/fireedge/src/client/containers/Groups/Create.js @@ -0,0 +1,211 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 PropTypes from 'prop-types' +import { ReactElement } from 'react' +import { useHistory, useLocation } from 'react-router' + +import { useGeneralApi } from 'client/features/General' +import { + useAllocateGroupMutation, + useAddAdminToGroupMutation, + useUpdateGroupMutation, + useGetGroupQuery, +} from 'client/features/OneApi/group' +import { useAllocateUserMutation } from 'client/features/OneApi/user' +import { useAllocateAclMutation } from 'client/features/OneApi/acl' + +import { jsonToXml } from 'client/models/Helper' + +import { + DefaultFormStepper, + SkeletonStepsForm, +} from 'client/components/FormStepper' +import { CreateForm, UpdateForm } from 'client/components/Forms/Group' +import { PATH } from 'client/apps/sunstone/routesOne' + +import { createStringACL, createAclObjectFromString } from 'client/models/acl' +import { ACL_RIGHTS, ACL_TYPE_ID } from 'client/constants' + +import systemApi from 'client/features/OneApi/system' + +/** + * Displays the creation form for a group. + * + * @returns {ReactElement} - The group form component + */ +function CreateGroup() { + const history = useHistory() + const { state: { ID: groupId } = {} } = useLocation() + + const { enqueueSuccess, enqueueError } = useGeneralApi() + const [createGroup] = useAllocateGroupMutation() + const [updateGroup] = useUpdateGroupMutation() + const [addAdminToGroup] = useAddAdminToGroupMutation() + const [createUser] = useAllocateUserMutation() + const [createAcl] = useAllocateAclMutation() + + const { data: views } = systemApi.useGetSunstoneAvalaibleViewsQuery() + const { data: version } = systemApi.useGetOneVersionQuery() + + const { data: group } = useGetGroupQuery({ id: groupId }) + + const updateTemplate = async (props, id) => { + // Create group template with advanced options + if (props?.views || props?.system) { + // Create XML template + const template = jsonToXml({ ...props?.views, ...props?.system }) + + // Merge with existing template + const params = { + id: id, + template: template, + replace: 1, + } + + // Update group with template + await updateGroup(params) + } + } + + const onSubmit = async (props) => { + try { + // Request to create a group but not to update + if (!groupId) { + // Create group + const newGroupId = await createGroup(props.group).unwrap() + + // Create admin user + if (props?.groupAdmin?.adminUser) { + // Get data to create admin user + const user = { + ...props.groupAdmin, + group: [newGroupId], + } + + // Create new admin group user + const newUserId = await createUser(user).unwrap() + + // Crete admin object to add user to the group + const addAdmin = { + id: newGroupId, + user: newUserId, + } + + // Add user to group as admin + await addAdminToGroup(addAdmin).unwrap() + } + + // Create acl create permissions for the group + if ( + props?.permissions?.create && + props?.permissions?.create?.length > 0 + ) { + const createAclString = createStringACL( + ACL_TYPE_ID.GROUP, + newGroupId, + props.permissions.create, + ACL_TYPE_ID.ALL, + undefined, + [ACL_RIGHTS.CREATE.name], + undefined, + undefined + ) + await createAcl(createAclObjectFromString(createAclString)) + } + + // Create acl view permissions + if (props?.permissions?.view && props?.permissions?.view?.length > 0) { + const viewAclString = createStringACL( + ACL_TYPE_ID.GROUP, + newGroupId, + props.permissions.view, + ACL_TYPE_ID.GROUP, + newGroupId, + [ACL_RIGHTS.USE.name], + undefined, + undefined + ) + await createAcl(createAclObjectFromString(viewAclString)) + } + + // Update group template with advanced options + updateTemplate(props, newGroupId) + + // Only show group message + enqueueSuccess(`Group created - #${newGroupId}`) + } else { + // Update case. Only update template + + // Update group template with advanced options + updateTemplate(props, groupId) + + // Only show group message + enqueueSuccess(`Group updated - #${groupId}`) + } + + // Go to groups list + history.push(PATH.SYSTEM.GROUPS.LIST) + } catch (error) { + enqueueError('Error creating group: ' + error.message) + } + } + + return views && version ? ( + !groupId ? ( + } + > + {(config) => } + + ) : group ? ( + } + > + {(config) => } + + ) : ( + + ) + ) : ( + + ) +} + +CreateGroup.propTypes = { + group: PropTypes.object, + groupAdmin: PropTypes.shape({ + adminUser: PropTypes.bool, + }), + permissions: PropTypes.shape({ + create: PropTypes.array, + view: PropTypes.array, + }), + views: PropTypes.object, + system: PropTypes.object, +} + +export default CreateGroup diff --git a/src/fireedge/src/client/containers/Groups/index.js b/src/fireedge/src/client/containers/Groups/index.js index 9aa421bc1b4..c6e46cf5be6 100644 --- a/src/fireedge/src/client/containers/Groups/index.js +++ b/src/fireedge/src/client/containers/Groups/index.js @@ -33,6 +33,8 @@ import { SubmitButton } from 'client/components/FormControl' import { Tr } from 'client/components/HOC' import { T, Group } from 'client/constants' +import GroupActions from 'client/components/Tables/Groups/actions' + /** * Displays a list of Groups with a split pane between the list and selected row(s). * @@ -44,6 +46,8 @@ function Groups() { const hasSelectedRows = selectedRows?.length > 0 const moreThanOneSelected = selectedRows?.length > 1 + const actions = GroupActions() + return ( {({ getGridProps, GutterComponent }) => ( @@ -51,6 +55,7 @@ function Groups() { {hasSelectedRows && ( diff --git a/src/fireedge/src/client/containers/Settings/Authentication/index.js b/src/fireedge/src/client/containers/Settings/Authentication/index.js index dcfe6b595e4..66fb4088d5b 100644 --- a/src/fireedge/src/client/containers/Settings/Authentication/index.js +++ b/src/fireedge/src/client/containers/Settings/Authentication/index.js @@ -198,7 +198,7 @@ const Settings = () => { return ( diff --git a/src/fireedge/src/client/containers/Settings/Showback/index.js b/src/fireedge/src/client/containers/Settings/Showback/index.js new file mode 100644 index 00000000000..f5196d7ac19 --- /dev/null +++ b/src/fireedge/src/client/containers/Settings/Showback/index.js @@ -0,0 +1,98 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { ReactElement, useState } from 'react' +import { Paper, Box, Typography, Stack, Button } from '@mui/material' + +import { T } from 'client/constants' +import { Translate, Tr } from 'client/components/HOC' +import { DateTime } from 'luxon' +import { DateRangeFilter } from 'client/components/Date' + +import { useLazyCalculateShowbackQuery } from 'client/features/OneApi/vm' +import { useGeneralApi } from 'client/features/General' + +/** + * Section to calculate showback data. + * + * @returns {ReactElement} Settings showback + */ +const Settings = () => { + // Get functions to success and error + const { enqueueError, enqueueSuccess } = useGeneralApi() + + // Hook to calculate showback + const [calculateShowback] = useLazyCalculateShowbackQuery() + + // Create hooks for date range + const [dateRange, setDateRange] = useState({ + startDate: DateTime.now().minus({ months: 1 }), + endDate: DateTime.now(), + }) + + const handleDateChange = (newDateRange) => { + setDateRange(newDateRange) + } + + // Refetch data when click on Get showback button + const handleCalculateClick = async () => { + const params = { + startMonth: dateRange.startDate.month, + startYear: dateRange.startDate.year, + endMonth: dateRange.endDate.month, + endYear: dateRange.endDate.year, + } + + try { + await calculateShowback(params) + enqueueSuccess('Showback calculated') + } catch (error) { + enqueueError(`Error calculating showback: ${error.message}`) + } + } + + return ( + + + + + + + + + + + + {Tr(T['showback.button.help.paragraph.1'])} + + + + ) +} + +export default Settings diff --git a/src/fireedge/src/client/containers/Settings/index.js b/src/fireedge/src/client/containers/Settings/index.js index 6b14a7d5a3f..34ac0a5904c 100644 --- a/src/fireedge/src/client/containers/Settings/index.js +++ b/src/fireedge/src/client/containers/Settings/index.js @@ -22,27 +22,35 @@ import { T } from 'client/constants' import ConfigurationUISection from 'client/containers/Settings/ConfigurationUI' import AuthenticationSection from 'client/containers/Settings/Authentication' import LabelsSection from 'client/containers/Settings/LabelsSection' +import ShowbackSection from 'client/containers/Settings/Showback' + +import { useSystemData } from 'client/features/Auth' /** @returns {ReactElement} Settings container */ -const Settings = () => ( - <> - - - - - - - - - - - - -) +const Settings = () => { + const { adminGroup } = useSystemData() + + return ( + <> + + + + + + + + + + + {adminGroup ? : null} + + + ) +} export default Settings diff --git a/src/fireedge/src/client/features/Auth/hooks.js b/src/fireedge/src/client/features/Auth/hooks.js index c732cf552ce..d9205bc69d2 100644 --- a/src/fireedge/src/client/features/Auth/hooks.js +++ b/src/fireedge/src/client/features/Auth/hooks.js @@ -135,10 +135,10 @@ export const useAuthApi = () => { export const useViews = () => { const { jwt, view } = useSelector((state) => state[authSlice], shallowEqual) - const { data: views } = systemApi.endpoints.getSunstoneViews.useQueryState( - undefined, - { skip: !jwt } - ) + const { data: { views = {} } = {} } = + systemApi.endpoints.getSunstoneViews.useQueryState(undefined, { + skip: !jwt, + }) /** * Looking for resource view of user authenticated. diff --git a/src/fireedge/src/client/features/OneApi/acl.js b/src/fireedge/src/client/features/OneApi/acl.js new file mode 100644 index 00000000000..fd780ab59b3 --- /dev/null +++ b/src/fireedge/src/client/features/OneApi/acl.js @@ -0,0 +1,94 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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 { Actions, Commands } from 'server/utils/constants/commands/acl' +import { oneApi, ONE_RESOURCES_POOL } from 'client/features/OneApi' +import { Acl } from 'client/constants' + +const { ACL_POOL } = ONE_RESOURCES_POOL + +const aclApi = oneApi.injectEndpoints({ + endpoints: (builder) => ({ + getAcls: builder.query({ + /** + * Retrieves information for all the acls in the pool. + * + * @returns {Acl[]} Get list of acls + * @throws Fails when response isn't code 200 + */ + query: () => { + const name = Actions.ACL_INFO + const command = { name, ...Commands[name] } + + return { command } + }, + transformResponse: (data) => [data?.ACL_POOL?.ACL ?? []].flat(), + providesTags: (groups) => + groups + ? [ + ...groups.map(({ ID }) => ({ type: ACL_POOL, id: `${ID}` })), + ACL_POOL, + ] + : [ACL_POOL], + }), + allocateAcl: builder.mutation({ + /** + * Allocates a new acl in OpenNebula. + * + * @param {object} params - Request parameters + * @param {string} params.user - User for the new acl + * @param {string} params.resource - Resources for the new acl + * @param {string} params.right - Rights for the new acl + * @returns {number} The allocated Acl id + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = Actions.ACL_ADDRULE + const command = { name, ...Commands[name] } + + return { params, command } + }, + invalidatesTags: [ACL_POOL], + }), + removeAcl: builder.mutation({ + /** + * Deletes the given acl from the pool. + * + * @param {object} params - Request parameters + * @param {string|number} params.id - Acl id + * @returns {number} Acl id + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = Actions.ACL_DELRULE + const command = { name, ...Commands[name] } + + return { params, command } + }, + invalidatesTags: [ACL_POOL], + }), + }), +}) + +export const { + // Queries + useGetAclQuery, + + // Mutations + useAllocateAclMutation, + useRemoveAclMutation, +} = aclApi + +export default aclApi diff --git a/src/fireedge/src/client/features/OneApi/group.js b/src/fireedge/src/client/features/OneApi/group.js index e786ea95642..62f03d46a80 100644 --- a/src/fireedge/src/client/features/OneApi/group.js +++ b/src/fireedge/src/client/features/OneApi/group.js @@ -21,6 +21,13 @@ import { } from 'client/features/OneApi' import { Group } from 'client/constants' +import { + removeResourceOnPool, + updateNameOnResource, + updateResourceOnPool, + updateTemplateOnResource, +} from 'client/features/OneApi/common' + const { GROUP } = ONE_RESOURCES const { GROUP_POOL } = ONE_RESOURCES_POOL @@ -39,7 +46,44 @@ const groupApi = oneApi.injectEndpoints({ return { command } }, - transformResponse: (data) => [data?.GROUP_POOL?.GROUP ?? []].flat(), + transformResponse: (data) => { + const groupsArray = Array.isArray(data?.GROUP_POOL?.GROUP) + ? data?.GROUP_POOL?.GROUP + : [data?.GROUP_POOL?.GROUP].filter(Boolean) + + const quotasArray = Array.isArray(data?.GROUP_POOL?.QUOTAS) + ? data?.GROUP_POOL?.QUOTAS + : [data?.GROUP_POOL?.QUOTAS].filter(Boolean) + + const quotasLookup = new Map( + quotasArray.map((quota) => [quota.ID, quota]) + ) + + const defaultQuotas = data?.GROUP_POOL?.DEFAULT_GROUP_QUOTAS || {} + + const getStrippedQuotaValue = (quota) => { + if (typeof quota === 'object' && quota !== null) { + return Object.values(quota)[0] || '' + } + + return quota || '' + } + + return groupsArray.map((group) => { + const groupQuotas = quotasLookup.get(group.ID) || {} + + return { + ...group, + ...Object.fromEntries( + Object.entries(groupQuotas).map(([key, value]) => [ + key, + getStrippedQuotaValue(value) || + getStrippedQuotaValue(defaultQuotas[key]), + ]) + ), + } + }) + }, providesTags: (groups) => groups ? [ @@ -63,8 +107,48 @@ const groupApi = oneApi.injectEndpoints({ return { params: { id }, command } }, - transformResponse: (data) => data?.GROUP ?? {}, - invalidatesTags: (_, __, { id }) => [{ type: GROUP, id }], + transformResponse: (data) => { + const group = data?.GROUP ?? {} + + const getStrippedQuotaValue = (quota) => { + if (typeof quota === 'object' && quota !== null) { + return Object.values(quota)[0] || '' + } + + return quota || '' + } + + Object.entries(group).forEach(([key, value]) => { + if (key.endsWith('_QUOTA')) { + group[key] = getStrippedQuotaValue(value) + } + }) + + return group + }, + providesTags: (_, __, { id }) => [{ type: GROUP, id }], + async onQueryStarted({ id }, { dispatch, queryFulfilled }) { + try { + const { data: resourceFromQuery } = await queryFulfilled + + dispatch( + groupApi.util.updateQueryData( + 'getGroups', + undefined, + updateResourceOnPool({ id, resourceFromQuery }) + ) + ) + } catch { + // if the query fails, we want to remove the resource from the pool + dispatch( + groupApi.util.updateQueryData( + 'getGroups', + undefined, + removeResourceOnPool({ id }) + ) + ) + } + }, }), allocateGroup: builder.mutation({ /** @@ -104,6 +188,30 @@ const groupApi = oneApi.injectEndpoints({ return { params, command } }, invalidatesTags: (_, __, { id }) => [{ type: GROUP, id }], + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const patchGroup = dispatch( + groupApi.util.updateQueryData( + 'getGroup', + { id: params.id }, + updateTemplateOnResource(params) + ) + ) + + const patchGroups = dispatch( + groupApi.util.updateQueryData( + 'getGroups', + undefined, + updateTemplateOnResource(params) + ) + ) + + queryFulfilled.catch(() => { + patchGroup.undo() + patchGroups.undo() + }) + } catch {} + }, }), removeGroup: builder.mutation({ /** @@ -121,6 +229,30 @@ const groupApi = oneApi.injectEndpoints({ return { params, command } }, invalidatesTags: [GROUP_POOL], + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const patchGroup = dispatch( + groupApi.util.updateQueryData( + 'getGroup', + { id: params.id }, + updateNameOnResource(params) + ) + ) + + const patchGroups = dispatch( + groupApi.util.updateQueryData( + 'getGroups', + undefined, + updateNameOnResource(params) + ) + ) + + queryFulfilled.catch(() => { + patchGroup.undo() + patchGroups.undo() + }) + } catch {} + }, }), addAdminToGroup: builder.mutation({ /** diff --git a/src/fireedge/src/client/features/OneApi/system.js b/src/fireedge/src/client/features/OneApi/system.js index 9c8925ad68b..7bd30ae805c 100644 --- a/src/fireedge/src/client/features/OneApi/system.js +++ b/src/fireedge/src/client/features/OneApi/system.js @@ -72,10 +72,14 @@ const systemApi = oneApi.injectEndpoints({ }, async onQueryStarted(_, { dispatch, getState, queryFulfilled }) { try { - const { data: views = {} } = await queryFulfilled + const { data: { defaultView, views = {} } = {} } = + await queryFulfilled const currentView = getState().auth?.view - !currentView && dispatch(actions.changeView(Object.keys(views)[0])) + + // Set to default view if exists + !currentView && + dispatch(actions.changeView(defaultView || Object.keys(views)[0])) } catch {} }, providesTags: [{ type: SYSTEM, id: 'sunstone-views' }], @@ -97,6 +101,22 @@ const systemApi = oneApi.injectEndpoints({ providesTags: [{ type: SYSTEM, id: 'sunstone-config' }], keepUnusedDataFor: 600, }), + getSunstoneAvalaibleViews: builder.query({ + /** + * Returns the Sunstone avalaible views. + * + * @returns {object} The avalaible views + * @throws Fails when response isn't code 200 + */ + query: () => { + const name = SunstoneActions.SUNSTONE_AVAILABLE_VIEWS + const command = { name, ...SunstoneCommands[name] } + + return { command } + }, + providesTags: [{ type: SYSTEM, id: 'sunstone-avalaibles-views' }], + keepUnusedDataFor: 600, + }), }), }) @@ -110,6 +130,7 @@ export const { useLazyGetSunstoneConfigQuery, useGetSunstoneViewsQuery, useLazyGetSunstoneViewsQuery, + useGetSunstoneAvalaibleViewsQuery, } = systemApi export default systemApi diff --git a/src/fireedge/src/client/features/OneApi/vm.js b/src/fireedge/src/client/features/OneApi/vm.js index 02eb22df7a3..a9a5b1f9b19 100644 --- a/src/fireedge/src/client/features/OneApi/vm.js +++ b/src/fireedge/src/client/features/OneApi/vm.js @@ -19,6 +19,11 @@ import { Commands as ExtraCommands, } from 'server/routes/api/vm/routes' +import { + Actions as ExtraActionsPool, + Commands as ExtraCommandsPool, +} from 'server/routes/api/vmpool/routes' + import { oneApi, ONE_RESOURCES, @@ -214,6 +219,25 @@ const vmApi = oneApi.injectEndpoints({ return { params, command } }, }), + getAccountingPoolFiltered: builder.query({ + /** + * Returns the virtual machine history records filtered by user or group. + * + * @param {object} params - Request parameters + * @param {number} [params.user] - User id + * @param {number} [params.group] - Group id + * @param {number} [params.start] - Range start date + * @param {number} [params.end] - Range end date + * @returns {string} The information string + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = ExtraActionsPool.VM_POOL_ACCOUNTING_FILTER + const command = { name, ...ExtraCommandsPool[name] } + + return { params, command } + }, + }), getShowbackPool: builder.query({ /** * Returns the virtual machine showback records. @@ -234,6 +258,27 @@ const vmApi = oneApi.injectEndpoints({ return { params, command } }, }), + getShowbackPoolFiltered: builder.query({ + /** + * Returns the virtual machine showback records filtered by user or group. + * + * @param {object} params - Request parameters + * @param {number} [params.user] - User id + * @param {number} [params.group] - Group id + * @param {number} [params.startMonth] - First month for the time interval + * @param {number} [params.startYear] - First year for the time interval + * @param {number} [params.endMonth] - Last month for the time interval + * @param {number} [params.endYear] - Last year for the time interval + * @returns {string} The information string + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = ExtraActionsPool.VM_POOL_SHOWBACK_FILTER + const command = { name, ...ExtraCommandsPool[name] } + + return { params, command } + }, + }), calculateShowback: builder.query({ /** * Processes all the history records, and stores the monthly cost for each VM. @@ -1070,8 +1115,12 @@ export const { useLazyGetMonitoringPoolQuery, useGetAccountingPoolQuery, useLazyGetAccountingPoolQuery, + useGetAccountingPoolFilteredQuery, + useLazyGetAccountingPoolFilteredQuery, useGetShowbackPoolQuery, useLazyGetShowbackPoolQuery, + useGetShowbackPoolFilteredQuery, + useLazyGetShowbackPoolFilteredQuery, useCalculateShowbackQuery, useLazyCalculateShowbackQuery, diff --git a/src/fireedge/src/client/models/Group.js b/src/fireedge/src/client/models/Group.js new file mode 100644 index 00000000000..d3c9755c23c --- /dev/null +++ b/src/fireedge/src/client/models/Group.js @@ -0,0 +1,88 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +/** + * Computes quota usage details. + * + * @param {string} type - Quota type. + * @param {object} quota - User quota details. + * @returns {{ + * percentOfUsed: number, + * percentLabel: string + * }} - Quota used percentage and label. + */ +export const getQuotaUsage = (type, quota) => { + let quotas = {} + + switch (type) { + case 'DATASTORE': + quotas = { + images: computeQuotaUsageDetails(quota.IMAGES_USED, quota.IMAGES), + size: computeQuotaUsageDetails(quota.SIZE_USED, quota.SIZE), + } + break + case 'VM': + quotas = { + vms: computeQuotaUsageDetails(quota.VMS_USED, quota.VMS), + runningVms: computeQuotaUsageDetails( + quota.RUNNING_VMS_USED, + quota.RUNNING_VMS + ), + memory: computeQuotaUsageDetails(quota.MEMORY_USED, quota.MEMORY), + runningMemory: computeQuotaUsageDetails( + quota.RUNNING_MEMORY_USED, + quota.RUNNING_MEMORY + ), + cpu: computeQuotaUsageDetails(quota.CPU_USED, quota.CPU), + runningCpu: computeQuotaUsageDetails( + quota.RUNNING_CPU_USED, + quota.RUNNING_CPU + ), + systemDiskSize: computeQuotaUsageDetails( + quota.SYSTEM_DISK_SIZE_USED, + quota.SYSTEM_DISK_SIZE + ), + } + break + case 'NETWORK': + quotas = { + leases: computeQuotaUsageDetails(quota.LEASES_USED, quota.LEASES), + } + break + case 'IMAGE': + quotas = { + rvms: computeQuotaUsageDetails(quota.RVMS_USED, quota.RVMS), + } + break + default: + break + } + + return quotas +} +const computeQuotaUsageDetails = (usedValue = '0', maxValue = '-1') => { + if (maxValue === '-2') { + return { + percentOfUsed: 100, + percentLabel: '∞/∞', + } + } + + const percentOfUsed = +maxValue > 0 ? (+usedValue * 100) / +maxValue : 0 + const percentLabel = `${usedValue}/${maxValue}` + + return { percentOfUsed, percentLabel } +} diff --git a/src/fireedge/src/client/models/acl.js b/src/fireedge/src/client/models/acl.js new file mode 100644 index 00000000000..0f95080a082 --- /dev/null +++ b/src/fireedge/src/client/models/acl.js @@ -0,0 +1,131 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +// File to functions about ACL + +import { ACL_TYPE_ID, ACL_RIGHTS } from 'client/constants' +import { parseAcl } from 'client/utils' + +/** + * Create an ACL object to send to the API. + * + * @param {string} user - The user value in hex value + * @param {string} resource - The resource value in hex value + * @param {string} rights - The rights value in hex value + * @param {string} zone - The zone value in hex value + * @returns {object} - The object to send to the API + */ +export const createAclObject = (user, resource, rights, zone) => { + // Create response + const response = { + user: user, + resource: resource, + right: rights, + } + + // Add zone if exists + if (zone) { + response.zone = zone + } + + // Return response + return response +} + +/** + * Create an ACL object to sent to the API from a string rule like #5 HOST+VM/@12 INFO+CREATE+DELETE *. + * + * @param {string} rule - String rule + * @returns {object} - The object to send to the API + */ +export const createAclObjectFromString = (rule) => { + // Parse the rule to get values + const ret = parseAcl(rule) + + // Create response + const response = { + user: ret[0], + resource: ret[1], + right: ret[2], + } + + // Add zone if exists + if (ret.length === 4) { + response.zone = ret[3] + } + + // Return response + return response +} + +/** + * Create a string rule using the values from a form. + * + * @param {string} userType - Type of user, e.g. "#" + * @param {number} userId - The id of the user, e.g. 4 + * @param {Array} resources - List of resources, e.g. ["VM,"TEMPLATE",IMAGE"] + * @param {string} resourcesIdType - The type of the resources identifier, e.g. "#" + * @param {number} resourcesId - The id user of the resources, e.g. 4 + * @param {Array} rights - List of rights, e.g. ["CREATE","USE"] + * @param {string} zoneType - Type of the zone, e.g. "#" + * @param {number} zoneId - The id of the user zone, e.g. 3 + * @returns {string} - ACL string rule + */ +export const createStringACL = ( + userType, + userId, + resources, + resourcesIdType, + resourcesId, + rights, + zoneType, + zoneId +) => { + // Define the string as empty string + let acl = '' + + // User: Type of user identifier plus the user identifier, e.g. @105 + acl += userType === ACL_TYPE_ID.ALL ? userType + ' ' : userType + userId + ' ' + + // Resources: List of resources separated by '+' plus the resources ID definition, e.g. VM+NET+IMAGE+TEMPLATE/#104 + resources.forEach((resource, index) => { + if (index < resources.length - 1) acl += resource.name + '+' + else acl += resource.name + '/' + }) + + acl += + resourcesIdType === ACL_TYPE_ID.ALL + ? resourcesIdType + ' ' + : resourcesIdType + resourcesId + ' ' + + // Rights: List of rights separated by '+', e.g. CREATE+USE + rights.forEach((right, index) => { + if (index < rights.length - 1) acl += ACL_RIGHTS[right].name + '+' + else + acl += + zoneType && zoneId + ? ACL_RIGHTS[right].name + ' ' + : ACL_RIGHTS[right].name + }) + + // Zone: Type of zone identifier plus the zone identifier, e.g. #44 + if (zoneType && zoneId) { + acl += zoneType + zoneId + } + + // Return the ACL string + return acl +} diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js index 64ca7b52af6..175bf09f7bf 100644 --- a/src/fireedge/src/client/utils/helpers.js +++ b/src/fireedge/src/client/utils/helpers.js @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { HYPERVISORS, UNITS, VN_DRIVERS } from 'client/constants' +import { + HYPERVISORS, + UNITS, + VN_DRIVERS, + DOCS_BASE_PATH, +} from 'client/constants' import { isMergeableObject } from 'client/utils/merge' import { Field } from 'client/utils/schema' import DOMPurify from 'dompurify' @@ -519,3 +524,27 @@ export const extractIDValues = (arr = []) => { return idValues.join(',') } + +/** + * Generate a link to the Open Nebula documentation using the first two digits of the version (e.g., 6.99.0 => 6.99). + * + * @param {string} version - Version of ONE + * @param {string} path - Path to documentation + * @returns {string} - Link to doc + */ +export const generateDocLink = (version, path) => { + // Split version + const splitVersion = version?.split('.') + + // Version has to be something + if (!splitVersion || splitVersion.length === 0) return + + // Create version with two first digits + const versionDoc = + splitVersion.length === 1 + ? splitVersion[0] + : splitVersion[0] + '.' + splitVersion[1] + + // Return link + return DOCS_BASE_PATH + '/' + versionDoc + '/' + path +} diff --git a/src/fireedge/src/client/utils/parser/index.js b/src/fireedge/src/client/utils/parser/index.js index 65f74dd2916..902679f22b8 100644 --- a/src/fireedge/src/client/utils/parser/index.js +++ b/src/fireedge/src/client/utils/parser/index.js @@ -17,10 +17,12 @@ import templateToObject from 'client/utils/parser/templateToObject' import parseApplicationToForm from 'client/utils/parser/parseApplicationToForm' import parseFormToApplication from 'client/utils/parser/parseFormToApplication' import parseFormToDeployApplication from 'client/utils/parser/parseFormToDeployApplication' +import parseAcl from 'client/utils/parser/parseACL' export { templateToObject, parseApplicationToForm, parseFormToApplication, parseFormToDeployApplication, + parseAcl, } diff --git a/src/fireedge/src/client/utils/parser/parseACL.js b/src/fireedge/src/client/utils/parser/parseACL.js new file mode 100644 index 00000000000..88b7c249d0b --- /dev/null +++ b/src/fireedge/src/client/utils/parser/parseACL.js @@ -0,0 +1,145 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +// File to define parser functions to create ACLs rules +// Documentation about manage ACLs -> https://docs.opennebula.io/6.7/management_and_operations/users_groups_management/chmod.html#manage-acl + +import { ACL_ID, ACL_RESOURCES, ACL_RIGHTS } from 'client/constants' + +/** + * Parses a rule string, e.g. "#5 HOST+VM/@12 INFO+CREATE+DELETE *". + * + * @param {string} rule - The ACL rule + * @returns {number} - The hex value for the four components of a rule (user, resources, rights and zone) + */ +const parseAcl = (rule) => { + // Get each component + const ruleComponents = rule.split(' ') + + /** + * Array to store values. Position: + * 0: User + * 1: Resources + * 2: Rights + * 3: Zone + */ + const ret = [] + + // Get value for user + ret[0] = parseUser(ruleComponents[0]).toString(16) + + // Get value for resources + ret[1] = parseResources(ruleComponents[1]) + + // Get value for rights + ret[2] = parseRights(ruleComponents[2]) + + // Get value for zone (optional) + if (ruleComponents.length > 3) { + ret[3] = parseZone(ruleComponents[3]) + } + + // Return value + return ret +} + +/** + * Calculate the hex value for a user. + * + * @param {string} userString - The user string is composed only by an ID definition. + * @returns {number} - The hex value of the user + */ +const parseUser = (userString) => calculateIds(userString).toString(16) + +/** + * Calculate the hex value for resources. + * + * @param {string} resourceString - The resources string is composed by a list of ‘+’ separated resource types, ‘/’ and an ID definition, e.g. "HOST+VM/@12" + * @returns {number} - The hex value of the resources + */ +const parseResources = (resourceString) => { + // Get the resources lst and the id definition + const components = resourceString.split('/') + const resources = components[0] + const user = components[1] + + // Init value with 0 + let resourcesValue = 0n + + // Add the hex value of each resource + resources + .split('+') + .forEach((resource) => (resourcesValue += ACL_RESOURCES[resource].value)) + + // Add the value for the id definition + resourcesValue += BigInt(calculateIds(user)) + + // Return the hex decimal value + return resourcesValue.toString(16) +} + +/** + * Calculate the hex value for rights. + * + * @param {string} rightsString - The rights string is a list of operations separated by the ‘+’ character., e.g. "INFO+CREATE+DELETE" + * @returns {number} - The hex value of the rights + */ +const parseRights = (rightsString) => { + // Get each right + const rights = rightsString.split('+') + + // Init value with 0 + let rightsValue = 0 + + // Add the value of each right + rights.forEach((right) => (rightsValue += ACL_RIGHTS[right].value)) + + // Return the hex value of the rights + return rightsValue.toString(16) +} + +/** + * Calculate the hex value for zone. + * + * @param {string} zoneString - The zone string is an ID definition of the zones where the rule applies, e.g. "@12" + * @returns {number} - The hex value of the zone + */ +const parseZone = (zoneString) => + // Return the hex value for zone + calculateIds(zoneString).toString(16) + +/** + * Calculate integer value for a id definition. + * + * @param {string} id - Id definition. Position 0 it's the type and from 1 to final position it's the identifier, e.g. "#5" + * @returns {number} - The value for the id definition + */ +const calculateIds = (id) => { + // Get the hex value for the id definition type + let idValue = ACL_ID[id[0]] + + // Check the identifer + if (id.length > 1) { + // Get integer value of the identifier and add to the users value + idValue += parseInt(id.substring(1)) + } + + // Return the integer id value + return idValue +} + +// Export parseRule function +export default parseAcl diff --git a/src/fireedge/src/server/routes/api/index.js b/src/fireedge/src/server/routes/api/index.js index 135469e2472..16e70bfcbf3 100644 --- a/src/fireedge/src/server/routes/api/index.js +++ b/src/fireedge/src/server/routes/api/index.js @@ -44,6 +44,7 @@ const routes = [ 'oneflow', 'vcenter', 'vm', + 'vmpool', 'zendesk', 'oneprovision', 'sunstone', diff --git a/src/fireedge/src/server/routes/api/sunstone/functions.js b/src/fireedge/src/server/routes/api/sunstone/functions.js index 1ef4d9c56a9..f991a043010 100644 --- a/src/fireedge/src/server/routes/api/sunstone/functions.js +++ b/src/fireedge/src/server/routes/api/sunstone/functions.js @@ -17,7 +17,12 @@ const { parse } = require('yaml') const { getSunstoneConfig } = require('server/utils/yml') const { defaults, httpCodes } = require('server/utils/constants') -const { existsFile, httpResponse, getFiles } = require('server/utils/server') +const { + existsFile, + httpResponse, + getFiles, + getDirectories, +} = require('server/utils/server') const { Actions: ActionsUser } = require('server/utils/constants/commands/user') const { Actions: ActionsGroup, @@ -61,6 +66,36 @@ const responseHttp = (res = {}, next = defaultEmptyFunction, httpCode) => { } } +/** + * Create a response with the info of the view files that are in global.paths.SUNSTONE_PATH/{view name}. + * + * @param {Array} views - The name of the views + * @param {object} rtn - Response + */ +const fillViewsInfo = (views, rtn) => { + // Iterate over each name view + views.forEach((view) => { + // Get all the files in global.paths.SUNSTONE_PATH/{view name} folder + getFiles(`${global.paths.SUNSTONE_PATH}${view}`).forEach((viewPath) => { + // Get the content of each file + existsFile(viewPath, (viewData = '') => { + // Create array in the response object for a view + if (!rtn[view]) { + rtn[view] = [] + } + + // Get the data of the file + const jsonViewData = parse(viewData) || {} + + // Add data to the view in the response + if (jsonViewData && jsonViewData.resource_name) { + rtn[view].push(jsonViewData) + } + }) + }) + }) +} + /** * Get sunstone-server views. * @@ -86,52 +121,138 @@ const getViews = ( global.paths.SUNSTONE_VIEWS && global.paths.SUNSTONE_PATH ) { + // Get connection to ONE const oneConnect = oneConnection(user, password) + + // Connect to ONE to get the info of an user oneConnect({ action: ActionsUser.USER_INFO, parameters: [-1, false], callback: (err = {}, dataUser = {}) => { + // Check that the user has info and a group if (dataUser && dataUser.USER && dataUser.USER.GID) { + // Get info about the user group getInfoGroup( oneConnect, dataUser.USER.GID, (err = {}, vmgroupData = {}) => { + // Check that the group has info if (vmgroupData && vmgroupData.GROUP && vmgroupData.GROUP.NAME) { - existsFile( - global.paths.SUNSTONE_VIEWS, - (filedata) => { - const jsonFileData = parse(filedata) || {} - if ( - jsonFileData && - jsonFileData.groups && - jsonFileData.default - ) { - const views = - jsonFileData.groups[vmgroupData.GROUP.NAME] || + // Check if the user is admin of the group + const admins = Array.isArray(vmgroupData.GROUP.ADMINS) + ? vmgroupData.GROUP.ADMINS + : [vmgroupData.GROUP.ADMINS] + const isAdminGroup = admins.some( + (admin) => admin.ID === dataUser.USER.ID + ) + + // Get the views on the group template + const groupViews = + vmgroupData?.GROUP?.TEMPLATE?.FIREEDGE?.VIEWS?.split(',') + + // Get the admin views on the group template + const groupAdminViews = + vmgroupData?.GROUP?.TEMPLATE?.FIREEDGE?.GROUP_ADMIN_VIEWS?.split( + ',' + ) + + /** + * Three cases: + * 1 -> Group template has TEMPLATE.FIREEDGE.GROUP_ADMIN_VIEWS and the user is admin of the group + * 2 -> Group template has TEMPLATE.FIREEDGE.VIEWS + * 3 -> Group template has not TEMPLATE.FIREEDGE.VIEWS and TEMPLATE.FIREEDGE.GROUP_ADMIN_VIEWS + */ + + if ( + isAdminGroup && + groupAdminViews && + groupAdminViews.length > 0 + ) { + // First case: Group template has TEMPLATE.FIREEDGE.GROUP_ADMIN_VIEWS and the user is admin of the group + + // Create info views + const views = {} + + // Fill info of each view reading the files on global.paths.SUNSTONE_PATH/{view name} + fillViewsInfo(groupAdminViews, views) + + // Get default view of the group + const defaultView = + vmgroupData?.GROUP?.TEMPLATE?.FIREEDGE + ?.GROUP_ADMIN_DEFAULT_VIEW + + // Create response + const rtn = { + views: views, + defaultView: defaultView || views[0], + } + + // Return response + responseHttp(res, next, httpResponse(ok, rtn)) + } + // Check the views associated to the group + else if (groupViews && groupViews.length > 0) { + // Second case: Group template has TEMPLATE.FIREEDGE.VIEWS + + // Create info views + const views = {} + + // Fill info of each view reading the files on global.paths.SUNSTONE_PATH/{view name} + fillViewsInfo(groupViews, views) + + // Get default view of the group + const defaultView = + vmgroupData?.GROUP?.TEMPLATE?.FIREEDGE?.DEFAULT_VIEW + + // Create response + const rtn = { + views: views, + defaultView: defaultView || views[0], + } + + // Return response + responseHttp(res, next, httpResponse(ok, rtn)) + } else { + // Third case: Group template has not TEMPLATE.FIREEDGE.VIEWS and TEMPLATE.FIREEDGE.GROUP_ADMIN_VIEWS + // If group template has not views, use configuration on global.paths.SUNSTONE_VIEWS file + existsFile( + global.paths.SUNSTONE_VIEWS, + (filedata) => { + // Get the content of global.paths.SUNSTONE_VIEWS file + const jsonFileData = parse(filedata) || {} + + // Check that the file has content and the attributes groups and default + if ( + jsonFileData && + jsonFileData.groups && jsonFileData.default - const rtn = {} - views.forEach((view) => { - getFiles( - `${global.paths.SUNSTONE_PATH}${view}` - ).forEach((viewPath) => { - existsFile(viewPath, (viewData = '') => { - if (!rtn[view]) { - rtn[view] = [] - } - const jsonViewData = parse(viewData) || {} - if (jsonViewData && jsonViewData.resource_name) { - rtn[view].push(jsonViewData) - } - }) - }) - }) - responseHttp(res, next, httpResponse(ok, rtn)) + ) { + // Get the views of the group and, if there is no group, the default view + const groupViewsFile = + jsonFileData.groups[vmgroupData.GROUP.NAME] || + jsonFileData.default + + // Create info views + const views = {} + + // Fill info of each view reading the files on global.paths.SUNSTONE_PATH/{view name} + fillViewsInfo(groupViewsFile, views) + + // Create response + const rtn = { + views: views, + defaultView: undefined, + } + + // Return response + responseHttp(res, next, httpResponse(ok, rtn)) + } + }, + () => { + responseHttp(res, next, notFound) } - }, - () => { - responseHttp(res, next, notFound) - } - ) + ) + } } else { responseHttp(res, next, httpInternalError) } @@ -176,8 +297,30 @@ const getConfig = ( ) } +/** + * Get available views in the one installation. + * + * @param {object} res - http response + * @param {Function} next - express stepper + */ +const getAvailableViews = (res = {}, next = defaultEmptyFunction) => { + let error = false + + const views = getDirectories( + `${global.paths.SUNSTONE_PATH}`, + () => (error = true) + ).map((dir) => dir.filename) + + responseHttp( + res, + next, + error ? httpResponse(notFound, error) : httpResponse(ok, views) + ) +} + const sunstoneApi = { getViews, getConfig, + getAvailableViews, } module.exports = sunstoneApi diff --git a/src/fireedge/src/server/routes/api/sunstone/index.js b/src/fireedge/src/server/routes/api/sunstone/index.js index a5ece758e56..d841073a675 100644 --- a/src/fireedge/src/server/routes/api/sunstone/index.js +++ b/src/fireedge/src/server/routes/api/sunstone/index.js @@ -15,9 +15,13 @@ * ------------------------------------------------------------------------- */ const { Actions, Commands } = require('server/routes/api/sunstone/routes') -const { getConfig, getViews } = require('server/routes/api/sunstone/functions') +const { + getConfig, + getViews, + getAvailableViews, +} = require('server/routes/api/sunstone/functions') -const { SUNSTONE_VIEWS, SUNSTONE_CONFIG } = Actions +const { SUNSTONE_VIEWS, SUNSTONE_CONFIG, SUNSTONE_AVAILABLE_VIEWS } = Actions module.exports = [ { @@ -28,4 +32,8 @@ module.exports = [ ...Commands[SUNSTONE_CONFIG], action: getConfig, }, + { + ...Commands[SUNSTONE_AVAILABLE_VIEWS], + action: getAvailableViews, + }, ] diff --git a/src/fireedge/src/server/routes/api/sunstone/routes.js b/src/fireedge/src/server/routes/api/sunstone/routes.js index b86b166e570..e1bb92eb177 100644 --- a/src/fireedge/src/server/routes/api/sunstone/routes.js +++ b/src/fireedge/src/server/routes/api/sunstone/routes.js @@ -21,10 +21,12 @@ const basepath = '/sunstone' const SUNSTONE_VIEWS = 'sunstone.views' const SUNSTONE_CONFIG = 'sunstone.config' +const SUNSTONE_AVAILABLE_VIEWS = 'sunstone.availableViews' const Actions = { SUNSTONE_VIEWS, SUNSTONE_CONFIG, + SUNSTONE_AVAILABLE_VIEWS, } module.exports = { @@ -40,5 +42,10 @@ module.exports = { httpMethod: GET, auth: true, }, + [SUNSTONE_AVAILABLE_VIEWS]: { + path: `${basepath}/views/available`, + httpMethod: GET, + auth: true, + }, }, } diff --git a/src/fireedge/src/server/routes/api/vmpool/functions.js b/src/fireedge/src/server/routes/api/vmpool/functions.js new file mode 100644 index 00000000000..b59e517a56d --- /dev/null +++ b/src/fireedge/src/server/routes/api/vmpool/functions.js @@ -0,0 +1,208 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ +const { defaults, httpCodes } = require('server/utils/constants') +const { httpResponse } = require('server/utils/server') +const { Actions: vmActions } = require('server/utils/constants/commands/vm') + +const { VM_POOL_ACCOUNTING, VM_POOL_SHOWBACK } = vmActions + +const { ok, internalServerError } = httpCodes +const { defaultEmptyFunction } = defaults + +/** + * Get the accouting info for an user or a group. User uses the XML API parameter to filter by users. Group gets all the for an interval of time and filter by the group id before return it. + * + * @param {object} res - http response + * @param {Function} next - express stepper + * @param {object} params - Parameters of the request + * @param {object} userData - Data about the user + * @param {Function} oneConnection - Function to connect to the XML API + */ +const accounting = ( + res = {}, + next = defaultEmptyFunction, + params = {}, + userData = {}, + oneConnection = defaultEmptyFunction +) => { + // Get the params + const { + user: userId, + group: groupId, + start: startTime, + end: endTime, + } = params + + // Get user data + const { user, password } = userData + + // Get connection to ONE + const oneConnect = oneConnection(user, password) + + // If there is an userId, sent to the XML API the id of the user. If not, send -2 to get all the data + const filterParameter = userId ?? '-2' + + /** + * Response http request. + * + * @param {object} httpRes - http response + * @param {Function} next - express stepper + * @param {object} httpCode - object http code + */ + const responseHttp = ( + httpRes = {}, + next = defaultEmptyFunction, + httpCode + ) => { + if (httpRes && httpRes.locals && httpRes.locals.httpCode && httpCode) { + httpRes.locals.httpCode = httpCode + next() + } + } + + // Connect to XML API + oneConnect({ + action: VM_POOL_ACCOUNTING, + parameters: [ + parseInt(filterParameter, 10), + parseInt(startTime, 10), + parseInt(endTime, 10), + ], + callback: (error, value) => { + // Get the response + const responseData = value + + // Filter if there is not userId and there is a groupId + if (!userId && groupId && responseData && responseData.HISTORY_RECORDS) { + // Filter data by group id + const history = Array.isArray(responseData.HISTORY_RECORDS.HISTORY) + ? responseData.HISTORY_RECORDS.HISTORY + : [responseData.HISTORY_RECORDS.HISTORY] + responseData.HISTORY_RECORDS.HISTORY = history.filter( + (item) => item.VM.GID === groupId + ) + } + + // Return response + responseHttp( + res, + next, + error + ? httpResponse(internalServerError, error) + : httpResponse(ok, responseData) + ) + }, + }) +} + +/** + * Get the showback info for an user or a group. User uses the XML API parameter to filter by users. Group gets all the for an interval of time and filter by the group id before return it. + * + * @param {object} res - http response + * @param {Function} next - express stepper + * @param {object} params - Parameters of the request + * @param {object} userData - Data about the user + * @param {Function} oneConnection - Function to connect to the XML API + */ +const showback = ( + res = {}, + next = defaultEmptyFunction, + params = {}, + userData = {}, + oneConnection = defaultEmptyFunction +) => { + // Get the params + const { + user: userId, + group: groupId, + startMonth, + startYear, + endMonth, + endYear, + } = params + + // Get user data + const { user, password } = userData + + // Get connection to ONE + const oneConnect = oneConnection(user, password) + + // If there is an userId, sent to the XML API the id of the user. If not, send -2 to get all the data + const filterParameter = userId ?? '-2' + + /** + * Response http request. + * + * @param {object} httpRes - http response + * @param {Function} next - express stepper + * @param {object} httpCode - object http code + */ + const responseHttp = ( + httpRes = {}, + next = defaultEmptyFunction, + httpCode + ) => { + if (httpRes && httpRes.locals && httpRes.locals.httpCode && httpCode) { + httpRes.locals.httpCode = httpCode + next() + } + } + + // Connect to XML API + oneConnect({ + action: VM_POOL_SHOWBACK, + parameters: [ + parseInt(filterParameter, 10), + startMonth ? parseInt(startMonth, 10) : -1, + startYear ? parseInt(startYear, 10) : -1, + endMonth ? parseInt(endMonth, 10) : -1, + endYear ? parseInt(endYear, 10) : -1, + ], + callback: (error, value) => { + // Get the response + const responseData = value + + // Filter if there is not userId and there is a groupId + if (!userId && groupId && responseData && responseData.SHOWBACK_RECORDS) { + // Filter data by group id + const showbackHistory = Array.isArray( + responseData.SHOWBACK_RECORDS.SHOWBACK + ) + ? responseData.SHOWBACK_RECORDS.SHOWBACK + : [responseData.SHOWBACK_RECORDS.SHOWBACK] + responseData.SHOWBACK_RECORDS.SHOWBACK = showbackHistory.filter( + (item) => item.GID === groupId + ) + } + + // Return response + responseHttp( + res, + next, + error + ? httpResponse(internalServerError, error) + : httpResponse(ok, responseData) + ) + }, + }) +} + +const functionRoutes = { + accounting, + showback, +} + +module.exports = functionRoutes diff --git a/src/fireedge/src/server/routes/api/vmpool/index.js b/src/fireedge/src/server/routes/api/vmpool/index.js new file mode 100644 index 00000000000..ac4554aee58 --- /dev/null +++ b/src/fireedge/src/server/routes/api/vmpool/index.js @@ -0,0 +1,31 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +const { Actions, Commands } = require('server/routes/api/vmpool/routes') +const { accounting, showback } = require('server/routes/api/vmpool/functions') + +const { VM_POOL_ACCOUNTING_FILTER, VM_POOL_SHOWBACK_FILTER } = Actions + +module.exports = [ + { + ...Commands[VM_POOL_ACCOUNTING_FILTER], + action: accounting, + }, + { + ...Commands[VM_POOL_SHOWBACK_FILTER], + action: showback, + }, +] diff --git a/src/fireedge/src/server/routes/api/vmpool/routes.js b/src/fireedge/src/server/routes/api/vmpool/routes.js new file mode 100644 index 00000000000..6e3b13ebeca --- /dev/null +++ b/src/fireedge/src/server/routes/api/vmpool/routes.js @@ -0,0 +1,84 @@ +/* ------------------------------------------------------------------------- * + * Copyright 2002-2023, 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. * + * ------------------------------------------------------------------------- */ + +const { + httpMethod, + from: fromData, +} = require('../../../utils/constants/defaults') + +const basepath = '/vmpool' +const { GET } = httpMethod +const { query } = fromData + +const VM_POOL_ACCOUNTING_FILTER = 'vmpool.accounting.filter' +const VM_POOL_SHOWBACK_FILTER = 'vmpool.showback.filter' + +const Actions = { + VM_POOL_ACCOUNTING_FILTER, + VM_POOL_SHOWBACK_FILTER, +} + +module.exports = { + Actions, + Commands: { + [VM_POOL_ACCOUNTING_FILTER]: { + path: `${basepath}/accounting/filtered`, + httpMethod: GET, + auth: true, + params: { + user: { + from: query, + }, + group: { + from: query, + }, + start: { + from: query, + default: -1, + }, + end: { + from: query, + default: -1, + }, + }, + }, + [VM_POOL_SHOWBACK_FILTER]: { + path: `${basepath}/showback/filtered`, + httpMethod: GET, + auth: true, + params: { + user: { + from: query, + }, + group: { + from: query, + }, + startMonth: { + from: query, + }, + startYear: { + from: query, + }, + endMonth: { + from: query, + }, + endYear: { + from: query, + }, + }, + }, + }, +} diff --git a/src/fireedge/src/server/utils/constants/commands/group.js b/src/fireedge/src/server/utils/constants/commands/group.js index 18e207dab1d..3f0f0585829 100644 --- a/src/fireedge/src/server/utils/constants/commands/group.js +++ b/src/fireedge/src/server/utils/constants/commands/group.js @@ -135,7 +135,7 @@ module.exports = { default: 0, }, template: { - from: resource, + from: postBody, default: '', }, },