diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js
new file mode 100644
index 0000000000..ed1abace16
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/index.js
@@ -0,0 +1,103 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useContext } from 'react'
+import PropTypes from 'prop-types'
+
+import { useVmTemplateApi } from 'client/features/One'
+import { TabContext } from 'client/components/Tabs/TabProvider'
+import { Permissions, Ownership } from 'client/components/Tabs/Common'
+import Information from 'client/components/Tabs/VmTemplate/Info/information'
+import * as Helper from 'client/models/Helper'
+
+const VmTemplateInfoTab = ({ tabProps = {} }) => {
+ const {
+ information_panel: informationPanel,
+ permissions_panel: permissionsPanel,
+ ownership_panel: ownershipPanel
+ } = tabProps
+
+ const { rename, changeOwnership, changePermissions } = useVmTemplateApi()
+ const { handleRefetch, data: template = {} } = useContext(TabContext)
+ const { ID, UNAME, UID, GNAME, GID, PERMISSIONS } = template
+
+ const handleChangeOwnership = async newOwnership => {
+ const response = await changeOwnership(ID, newOwnership)
+ String(response) === String(ID) && await handleRefetch?.()
+ }
+
+ const handleChangePermission = async newPermission => {
+ const response = await changePermissions(ID, newPermission)
+ String(response) === String(ID) && await handleRefetch?.()
+ }
+
+ const handleRename = async newName => {
+ const response = await rename(ID, newName)
+ String(response) === String(ID) && await handleRefetch?.()
+ }
+
+ const getActions = actions => Helper.getActionsAvailable(actions)
+
+ return (
+
+ {informationPanel?.enabled && (
+
+ )}
+ {permissionsPanel?.enabled && (
+
+ )}
+ {ownershipPanel?.enabled && (
+
+ )}
+
+ )
+}
+
+VmTemplateInfoTab.propTypes = {
+ tabProps: PropTypes.object
+}
+
+VmTemplateInfoTab.displayName = 'VmTemplateInfoTab'
+
+export default VmTemplateInfoTab
diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js
new file mode 100644
index 0000000000..223ce65e5e
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Info/information.js
@@ -0,0 +1,51 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import PropTypes from 'prop-types'
+
+import { List } from 'client/components/Tabs/Common'
+
+import * as Helper from 'client/models/Helper'
+import { T } from 'client/constants'
+
+const InformationPanel = ({ template = {} }) => {
+ const { ID, NAME, REGTIME, LOCK } = template
+
+ const info = [
+ { name: T.ID, value: ID },
+ { name: T.Name, value: NAME },
+ {
+ name: T.StartTime,
+ value: Helper.timeToString(REGTIME)
+ },
+ {
+ name: T.Locked,
+ value: Helper.levelLockToString(LOCK?.LOCKED)
+ }
+ ]
+
+ return (
+
+ )
+}
+
+InformationPanel.displayName = 'InformationPanel'
+
+InformationPanel.propTypes = {
+ template: PropTypes.object
+}
+
+export default InformationPanel
diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/Template.js b/src/fireedge/src/client/components/Tabs/VmTemplate/Template.js
new file mode 100644
index 0000000000..9255df6105
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VmTemplate/Template.js
@@ -0,0 +1,41 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useContext } from 'react'
+import { Accordion, AccordionDetails } from '@material-ui/core'
+
+import { TabContext } from 'client/components/Tabs/TabProvider'
+
+const TemplateTab = () => {
+ const { data: template = {} } = useContext(TabContext)
+ const { TEMPLATE } = template
+
+ return (
+
+
+
+
+ {JSON.stringify(TEMPLATE, null, 2)}
+
+
+
+
+ )
+}
+
+TemplateTab.displayName = 'TemplateTab'
+
+export default TemplateTab
diff --git a/src/fireedge/src/client/components/Tabs/VmTemplate/index.js b/src/fireedge/src/client/components/Tabs/VmTemplate/index.js
new file mode 100644
index 0000000000..17a1734a62
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/VmTemplate/index.js
@@ -0,0 +1,84 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { memo, useEffect, useState } from 'react'
+import PropTypes from 'prop-types'
+import { LinearProgress } from '@material-ui/core'
+
+import { useFetch } from 'client/hooks'
+import { useAuth } from 'client/features/Auth'
+import { useVmTemplateApi } from 'client/features/One'
+
+import Tabs from 'client/components/Tabs'
+import { sentenceCase, camelCase } from 'client/utils'
+
+import TabProvider from 'client/components/Tabs/TabProvider'
+import Info from 'client/components/Tabs/VmTemplate/Info'
+import Template from 'client/components/Tabs/VmTemplate/Template'
+
+const getTabComponent = tabName => ({
+ info: Info,
+ template: Template
+}[tabName])
+
+const VmTemplateTabs = memo(({ id }) => {
+ const { getVmTemplate } = useVmTemplateApi()
+ const { data, fetchRequest, loading, error } = useFetch(getVmTemplate)
+
+ const handleRefetch = () => fetchRequest(id, { reload: true })
+
+ const [tabsAvailable, setTabs] = useState(() => [])
+ const { view, getResourceView } = useAuth()
+
+ useEffect(() => {
+ fetchRequest(id)
+ }, [id])
+
+ useEffect(() => {
+ const infoTabs = getResourceView('VM-TEMPLATE')?.['info-tabs'] ?? {}
+
+ setTabs(() => Object.entries(infoTabs)
+ ?.filter(([_, { enabled } = {}]) => !!enabled)
+ ?.map(([tabName, tabProps]) => {
+ const camelName = camelCase(tabName)
+ const TabContent = getTabComponent(camelName)
+
+ return TabContent && {
+ name: sentenceCase(camelName),
+ renderContent: props => TabContent({ ...props, tabProps })
+ }
+ })
+ ?.filter(Boolean))
+ }, [view])
+
+ if ((!data && !error) || loading) {
+ return
+ }
+
+ return (
+
+
+
+ )
+})
+
+VmTemplateTabs.propTypes = {
+ id: PropTypes.string.isRequired
+}
+
+VmTemplateTabs.displayName = 'VmTemplateTabs'
+
+export default VmTemplateTabs
diff --git a/src/fireedge/src/client/components/Tabs/index.js b/src/fireedge/src/client/components/Tabs/index.js
index 1b22e95138..473952ca65 100644
--- a/src/fireedge/src/client/components/Tabs/index.js
+++ b/src/fireedge/src/client/components/Tabs/index.js
@@ -22,7 +22,7 @@ import { Tabs as MTabs, Tab as MTab } from '@material-ui/core'
const Content = ({ name, renderContent: Content, hidden }) => (
(
)
const Tabs = ({ tabs = [], renderHiddenTabs = false }) => {
- const [tabSelected, setTab] = useState(() => 6)
+ const [tabSelected, setTab] = useState(() => 0)
const renderTabs = useMemo(() => (
+ }
+
+ return (
+
+ {}
+
+ )
+}
+
+export default ClusterDetail
diff --git a/src/fireedge/src/client/containers/Clusters/index.js b/src/fireedge/src/client/containers/Clusters/index.js
index 14ef763e78..42e82c8fd4 100644
--- a/src/fireedge/src/client/containers/Clusters/index.js
+++ b/src/fireedge/src/client/containers/Clusters/index.js
@@ -14,12 +14,19 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
-
+import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { ClustersTable } from 'client/components/Tables'
+import ClusterTabs from 'client/components/Tabs/Cluster'
+import SplitPane from 'client/components/SplitPane'
function Clusters () {
+ const [selectedRows, onSelectedRowsChange] = useState([])
+
+ const getRowIds = () =>
+ JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
+
return (
-
+
+
+
+ {selectedRows?.length > 0 && (
+
+ {selectedRows?.length === 1
+ ?
+ :
{getRowIds()}
+ }
+
+ )}
+
)
}
diff --git a/src/fireedge/src/client/containers/Groups/Detail.js b/src/fireedge/src/client/containers/Groups/Detail.js
new file mode 100644
index 0000000000..c8d58161e1
--- /dev/null
+++ b/src/fireedge/src/client/containers/Groups/Detail.js
@@ -0,0 +1,43 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useParams, Redirect } from 'react-router-dom'
+import { Container, Box } from '@material-ui/core'
+
+// import GroupTabs from 'client/components/Tabs/Group'
+
+function GroupDetail () {
+ const { id } = useParams()
+
+ if (Number.isNaN(+id)) {
+ return
+ }
+
+ return (
+
+ {/* */}
+ {id}
+
+ )
+}
+
+export default GroupDetail
diff --git a/src/fireedge/src/client/containers/Hosts/Detail.js b/src/fireedge/src/client/containers/Hosts/Detail.js
new file mode 100644
index 0000000000..36927ccfdb
--- /dev/null
+++ b/src/fireedge/src/client/containers/Hosts/Detail.js
@@ -0,0 +1,42 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useParams, Redirect } from 'react-router-dom'
+import { Container, Box } from '@material-ui/core'
+
+import HostTabs from 'client/components/Tabs/Host'
+
+function HostDetail () {
+ const { id } = useParams()
+
+ if (Number.isNaN(+id)) {
+ return
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default HostDetail
diff --git a/src/fireedge/src/client/containers/Hosts/index.js b/src/fireedge/src/client/containers/Hosts/index.js
index 169a448cbc..c98e22f979 100644
--- a/src/fireedge/src/client/containers/Hosts/index.js
+++ b/src/fireedge/src/client/containers/Hosts/index.js
@@ -14,12 +14,19 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
-
+import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { HostsTable } from 'client/components/Tables'
+import HostTabs from 'client/components/Tabs/Host'
+import SplitPane from 'client/components/SplitPane'
function Hosts () {
+ const [selectedRows, onSelectedRowsChange] = useState([])
+
+ const getRowIds = () =>
+ JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
+
return (
-
+
+
+
+ {selectedRows?.length > 0 && (
+
+ {selectedRows?.length === 1
+ ?
+ :
{getRowIds()}
+ }
+
+ )}
+
)
}
diff --git a/src/fireedge/src/client/containers/Users/Detail.js b/src/fireedge/src/client/containers/Users/Detail.js
new file mode 100644
index 0000000000..02d6ba6d43
--- /dev/null
+++ b/src/fireedge/src/client/containers/Users/Detail.js
@@ -0,0 +1,42 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useParams, Redirect } from 'react-router-dom'
+import { Container, Box } from '@material-ui/core'
+
+import UserTabs from 'client/components/Tabs/User'
+
+function UserDetail () {
+ const { id } = useParams()
+
+ if (Number.isNaN(+id)) {
+ return
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default UserDetail
diff --git a/src/fireedge/src/client/containers/VirtualMachines/Detail.js b/src/fireedge/src/client/containers/VirtualMachines/Detail.js
new file mode 100644
index 0000000000..a7d887b830
--- /dev/null
+++ b/src/fireedge/src/client/containers/VirtualMachines/Detail.js
@@ -0,0 +1,42 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useParams, Redirect } from 'react-router-dom'
+import { Container, Box } from '@material-ui/core'
+
+import VmTabs from 'client/components/Tabs/Vm'
+
+function VirtualMachineDetail () {
+ const { id } = useParams()
+
+ if (Number.isNaN(+id)) {
+ return
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default VirtualMachineDetail
diff --git a/src/fireedge/src/client/containers/VmTemplates/Instantiate.js b/src/fireedge/src/client/containers/VmTemplates/Instantiate.js
index 2dd05e9ca3..29820c6aad 100644
--- a/src/fireedge/src/client/containers/VmTemplates/Instantiate.js
+++ b/src/fireedge/src/client/containers/VmTemplates/Instantiate.js
@@ -14,14 +14,15 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
+import { useEffect } from 'react'
import { useHistory, useParams } from 'react-router'
-
import { Container } from '@material-ui/core'
import { useGeneralApi } from 'client/features/General'
-import { useVmTemplateApi } from 'client/features/One'
+import { useVmTemplateApi, useUserApi, useVmGroupApi } from 'client/features/One'
import { InstantiateForm } from 'client/components/Forms/VmTemplate'
import { PATH } from 'client/apps/sunstone/routesOne'
+import { isDevelopment } from 'client/utils'
function InstantiateVmTemplate () {
const history = useHistory()
@@ -29,27 +30,28 @@ function InstantiateVmTemplate () {
const initialValues = { template: { ID: templateId } }
const { enqueueInfo } = useGeneralApi()
+ const { getUsers } = useUserApi()
+ const { getVmGroups } = useVmGroupApi()
const { instantiate } = useVmTemplateApi()
- const onSubmit = async formData => {
- const {
- template: [{ ID, NAME }] = [],
- configuration: { name, instances, ...configuration } = {}
- } = formData
+ const onSubmit = async ([templateSelected, templates]) => {
+ try {
+ const { ID, NAME } = templateSelected
- await Promise.all([...new Array(instances)]
- .map((_, idx) => {
- const replacedName = name?.replace(/%idx/gi, idx)
- const data = { ...configuration, name: replacedName }
+ await Promise.all(templates.map(template => instantiate(ID, template)))
- return instantiate(ID, data)
- })
- )
-
- history.push(templateId ? PATH.TEMPLATE.VMS.LIST : PATH.INSTANCE.VMS.LIST)
- enqueueInfo(`VM Template instantiated x${instances} - ${NAME}`)
+ history.push(templateId ? PATH.TEMPLATE.VMS.LIST : PATH.INSTANCE.VMS.LIST)
+ enqueueInfo(`VM Template instantiated x${templates.length} - #${ID} ${NAME}`)
+ } catch (err) {
+ isDevelopment() && console.error(err)
+ }
}
+ useEffect(() => {
+ getUsers()
+ getVmGroups()
+ }, [])
+
return (
diff --git a/src/fireedge/src/client/containers/VmTemplates/index.js b/src/fireedge/src/client/containers/VmTemplates/index.js
index 0f53934a24..5428267d00 100644
--- a/src/fireedge/src/client/containers/VmTemplates/index.js
+++ b/src/fireedge/src/client/containers/VmTemplates/index.js
@@ -14,12 +14,20 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
+import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { VmTemplatesTable } from 'client/components/Tables'
+import VmTemplateTabs from 'client/components/Tabs/VmTemplate'
+import SplitPane from 'client/components/SplitPane'
function VmTemplates () {
+ const [selectedRows, onSelectedRowsChange] = useState([])
+
+ const getRowIds = () =>
+ JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
+
return (
-
+
+
+
+ {selectedRows?.length > 0 && (
+
+ {selectedRows?.length === 1
+ ?
+ :
{getRowIds()}
+ }
+
+ )}
+
)
}
diff --git a/src/fireedge/src/client/features/Auth/hooks.js b/src/fireedge/src/client/features/Auth/hooks.js
index c1399e6868..35037ddea2 100644
--- a/src/fireedge/src/client/features/Auth/hooks.js
+++ b/src/fireedge/src/client/features/Auth/hooks.js
@@ -18,13 +18,14 @@ import { useCallback } from 'react'
import { useDispatch, useSelector, shallowEqual } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
-import { RESOURCES } from 'client/features/One'
import * as actions from 'client/features/Auth/actions'
import * as actionsView from 'client/features/Auth/actionsView'
+import { name as authSlice } from 'client/features/Auth/slice'
+import { name as oneSlice, RESOURCES } from 'client/features/One/slice'
export const useAuth = () => {
- const auth = useSelector(state => state.auth, shallowEqual)
- const groups = useSelector(state => state.one[RESOURCES.group], shallowEqual)
+ const auth = useSelector(state => state[authSlice], shallowEqual)
+ const groups = useSelector(state => state[oneSlice][RESOURCES.group], shallowEqual)
const { user, jwt, view, views, isLoginInProgress } = auth
diff --git a/src/fireedge/src/client/features/Auth/slice.js b/src/fireedge/src/client/features/Auth/slice.js
index 7044adb985..01f95a19f2 100644
--- a/src/fireedge/src/client/features/Auth/slice.js
+++ b/src/fireedge/src/client/features/Auth/slice.js
@@ -38,7 +38,7 @@ const initial = () => ({
isLoading: false
})
-const { actions, reducer } = createSlice({
+const { name, actions, reducer } = createSlice({
name: 'auth',
initialState: ({ ...initial(), firstRender: true }),
extraReducers: builder => {
@@ -84,4 +84,4 @@ const { actions, reducer } = createSlice({
}
})
-export { actions, reducer }
+export { name, actions, reducer }
diff --git a/src/fireedge/src/client/features/General/actions.js b/src/fireedge/src/client/features/General/actions.js
index 45fadfb5f6..121d98e188 100644
--- a/src/fireedge/src/client/features/General/actions.js
+++ b/src/fireedge/src/client/features/General/actions.js
@@ -19,6 +19,7 @@ export const fixMenu = createAction('Fix menu')
export const changeZone = createAction('Change zone')
export const changeLoading = createAction('Change loading')
export const changeTitle = createAction('Change title')
+export const changeAppTitle = createAction('Change App title')
export const dismissSnackbar = createAction('Dismiss snackbar')
export const deleteSnackbar = createAction('Delete snackbar')
diff --git a/src/fireedge/src/client/features/General/hooks.js b/src/fireedge/src/client/features/General/hooks.js
index 4ddad398df..c1cf0ad9ef 100644
--- a/src/fireedge/src/client/features/General/hooks.js
+++ b/src/fireedge/src/client/features/General/hooks.js
@@ -17,10 +17,11 @@
import { useDispatch, useSelector } from 'react-redux'
import * as actions from 'client/features/General/actions'
+import { name } from 'client/features/General/slice'
import { generateKey } from 'client/utils'
export const useGeneral = () => (
- useSelector(state => state.general)
+ useSelector(state => state[name])
)
export const useGeneralApi = () => {
@@ -30,6 +31,7 @@ export const useGeneralApi = () => {
fixMenu: isFixed => dispatch(actions.fixMenu(isFixed)),
changeLoading: isLoading => dispatch(actions.changeLoading(isLoading)),
changeTitle: title => dispatch(actions.changeTitle(title)),
+ changeAppTitle: appTitle => dispatch(actions.changeAppTitle(appTitle)),
changeZone: zone => dispatch(actions.changeZone(zone)),
// dismiss all if no key has been defined
diff --git a/src/fireedge/src/client/features/General/slice.js b/src/fireedge/src/client/features/General/slice.js
index d5987181f7..8b6a260629 100644
--- a/src/fireedge/src/client/features/General/slice.js
+++ b/src/fireedge/src/client/features/General/slice.js
@@ -22,13 +22,14 @@ import { generateKey } from 'client/utils'
const initial = {
zone: 0,
title: null,
+ appTitle: null,
isLoading: false,
isFixMenu: false,
notifications: []
}
-const { reducer } = createSlice({
+const { name, reducer } = createSlice({
name: 'general',
initialState: initial,
extraReducers: builder => {
@@ -43,6 +44,9 @@ const { reducer } = createSlice({
.addCase(actions.changeTitle, (state, { payload }) => {
return { ...state, title: payload }
})
+ .addCase(actions.changeAppTitle, (state, { payload }) => {
+ return { ...state, appTitle: payload }
+ })
.addCase(actions.changeZone, (state, { payload }) => {
return { ...state, zone: payload }
})
@@ -111,4 +115,4 @@ const { reducer } = createSlice({
}
})
-export { reducer }
+export { name, reducer }
diff --git a/src/fireedge/src/client/features/One/hooks.js b/src/fireedge/src/client/features/One/hooks.js
index 01b3cd88bd..2f3910fecc 100644
--- a/src/fireedge/src/client/features/One/hooks.js
+++ b/src/fireedge/src/client/features/One/hooks.js
@@ -15,9 +15,10 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useSelector, shallowEqual } from 'react-redux'
+import { name } from 'client/features/One/slice'
export const useOne = () => (
- useSelector(state => state.one, shallowEqual)
+ useSelector(state => state[name], shallowEqual)
)
export * from 'client/features/One/application/hooks'
@@ -33,6 +34,7 @@ export * from 'client/features/One/provider/hooks'
export * from 'client/features/One/provision/hooks'
export * from 'client/features/One/user/hooks'
export * from 'client/features/One/vm/hooks'
+export * from 'client/features/One/vmGroup/hooks'
export * from 'client/features/One/vmTemplate/hooks'
export * from 'client/features/One/vnetwork/hooks'
export * from 'client/features/One/vnetworkTemplate/hooks'
diff --git a/src/fireedge/src/client/features/One/slice.js b/src/fireedge/src/client/features/One/slice.js
index 41a959657e..4890ab730c 100644
--- a/src/fireedge/src/client/features/One/slice.js
+++ b/src/fireedge/src/client/features/One/slice.js
@@ -81,8 +81,8 @@ const initial = {
[RESOURCES.document.defaults]: []
}
-const { actions, reducer } = createSlice({
- name: 'pool',
+const { name, actions, reducer } = createSlice({
+ name: 'one',
initialState: initial,
extraReducers: builder => {
builder
@@ -127,4 +127,4 @@ const { actions, reducer } = createSlice({
}
})
-export { actions, reducer, RESOURCES }
+export { name, actions, reducer, RESOURCES }
diff --git a/src/fireedge/src/client/features/One/vmGroup/actions.js b/src/fireedge/src/client/features/One/vmGroup/actions.js
new file mode 100644
index 0000000000..043e7d9814
--- /dev/null
+++ b/src/fireedge/src/client/features/One/vmGroup/actions.js
@@ -0,0 +1,29 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import { createAction } from 'client/features/One/utils'
+import { vmGroupService } from 'client/features/One/vmGroup/services'
+import { RESOURCES } from 'client/features/One/slice'
+
+export const getVmGroup = createAction(
+ 'vmgroup/detail',
+ vmGroupService.getVmGroup
+)
+
+export const getVmGroups = createAction(
+ 'vmgroup/pool',
+ vmGroupService.getVmGroups,
+ response => ({ [RESOURCES.vmgroup]: response })
+)
diff --git a/src/fireedge/src/client/features/One/vmGroup/hooks.js b/src/fireedge/src/client/features/One/vmGroup/hooks.js
new file mode 100644
index 0000000000..1f109d8ef6
--- /dev/null
+++ b/src/fireedge/src/client/features/One/vmGroup/hooks.js
@@ -0,0 +1,40 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+/* eslint-disable jsdoc/require-jsdoc */
+import { useCallback } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { unwrapResult } from '@reduxjs/toolkit'
+
+import * as actions from 'client/features/One/vmGroup/actions'
+import { RESOURCES } from 'client/features/One/slice'
+
+export const useVmGroup = () => (
+ useSelector(state => state.one[RESOURCES.vmgroup])
+)
+
+export const useVmGroupApi = () => {
+ const dispatch = useDispatch()
+
+ const unwrapDispatch = useCallback(
+ action => dispatch(action).then(unwrapResult)
+ , [dispatch]
+ )
+
+ return {
+ getVmGroup: id => unwrapDispatch(actions.getVmGroup({ id })),
+ getVmGroups: () => unwrapDispatch(actions.getVmGroups())
+ }
+}
diff --git a/src/fireedge/src/client/features/One/vmGroup/services.js b/src/fireedge/src/client/features/One/vmGroup/services.js
new file mode 100644
index 0000000000..43c9ae87cc
--- /dev/null
+++ b/src/fireedge/src/client/features/One/vmGroup/services.js
@@ -0,0 +1,64 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import { Actions, Commands } from 'server/utils/constants/commands/vmgroup'
+import { httpCodes } from 'server/utils/constants'
+import { requestConfig, RestClient } from 'client/utils'
+
+export const vmGroupService = ({
+ /**
+ * Retrieves information for the VM group.
+ *
+ * @param {object} params - Request parameters
+ * @param {string} params.id - VM group id
+ * @param {boolean} params.decrypt - `true` to decrypt contained secrets
+ * @returns {object} Get VM group identified by id
+ * @throws Fails when response isn't code 200
+ */
+ getVmGroup: async params => {
+ const name = Actions.VM_GROUP_INFO
+ const command = { name, ...Commands[name] }
+ const config = requestConfig(params, command)
+
+ const res = await RestClient.request(config)
+
+ if (!res?.id || res?.id !== httpCodes.ok.id) throw res
+
+ return res?.data?.VM_GROUP ?? {}
+ },
+
+ /**
+ * Retrieves information for all or part of the
+ * VM groups in the pool.
+ *
+ * @param {object} data - Request params
+ * @param {string} data.filter - Filter flag
+ * @param {number} data.start - Range start ID
+ * @param {number} data.end - Range end ID
+ * @returns {Array} List of VM groups
+ * @throws Fails when response isn't code 200
+ */
+ getVmGroups: async ({ filter, start, end }) => {
+ const name = Actions.VM_GROUP_POOL_INFO
+ const command = { name, ...Commands[name] }
+ const config = requestConfig({ filter, start, end }, command)
+
+ const res = await RestClient.request(config)
+
+ if (!res?.id || res?.id !== httpCodes.ok.id) throw res
+
+ return [res?.data?.VM_GROUP_POOL?.VM_GROUP ?? []].flat()
+ }
+})
diff --git a/src/fireedge/src/client/hooks/useListForm.js b/src/fireedge/src/client/hooks/useListForm.js
index e64ecda2e1..98e99fcbbb 100644
--- a/src/fireedge/src/client/hooks/useListForm.js
+++ b/src/fireedge/src/client/hooks/useListForm.js
@@ -14,23 +14,84 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useCallback, useState, SetStateAction } from 'react'
+import { v4 as uuidv4 } from 'uuid'
import { set } from 'client/utils'
+// ----------------------------------------------------------
+// Types
+// ----------------------------------------------------------
+
+/** @callback NoParamsCallback */
+
+/**
+ * @callback SimpleCallback
+ * @param {string|number} id - Item id
+ */
+
+/**
+ * @callback NewListCallback
+ * @param {object[]} newList - New list
+ */
+
+/**
+ * @callback FilterCallback
+ * @param {object} item - Item from list
+ * @returns {boolean} Filter condition
+ */
+
+/**
+ * @callback DeleteCallback ------------------
+ * @param {string|number} id - Item id
+ * @param {FilterCallback} [filter] - Filter function to remove the item
+ */
+
+/**
+ * @callback SaveCallback ------------------
+ * @param {object} newValues - New item values
+ * @param {string|number} [id] - Default uuid4
+ */
+
+/**
+ * @typedef {object} HookListForm
+ * @property {object} editingData - Current editing data
+ * @property {NoParamsCallback} handleClear - Clear the data form list
+ * @property {NewListCallback} handleSetList - Resets the list with a new value
+ * @property {DeleteCallback} handleUnselect - Removes an item from data form list
+ * @property {DeleteCallback} handleRemove - Removes an item from data form list
+ * @property {SimpleCallback} handleSelect - Add an item to data form list
+ * @property {SimpleCallback} handleClone - Clones an item and change two attributes
+ * @property {SimpleCallback} handleEdit - Find the element by id and set value to editing state
+ * @property {function(newValues, id)} handleSave - Saves the data from editing state
+ */
+
+// ----------------------------------------------------------
+// Constants
+// ----------------------------------------------------------
+
+const ZERO_DELETE_COUNT = 0
const NEXT_INDEX = index => index + 1
const EXISTS_INDEX = index => index !== -1
-const getIndexById = (list, id) =>
- list.findIndex(({ id: itemId }) => itemId === id)
+// parent ? [parent.id, index].join('.')
+const defaultGetItemId = item => typeof item === 'object' ? item?.id ?? item?.ID : item
+const defaultAddItemId = (item, id) => ({ ...item, id })
+
+// ----------------------------------------------------------
+// Hook function
+// ----------------------------------------------------------
/**
* Hook to manage a list with selectable elements in a form data.
*
* @param {object} props - Props
* @param {boolean} props.multiple - If `true`, can be more than one select elements
+ * @param {string} props.parent - Key of parent in the form
* @param {string} props.key - Key of list in the form
* @param {object[]} props.list - Form data
* @param {SetStateAction} props.setList - State action from the form
- * @param {{ id: string|number }} props.defaultValue - Default value of element
+ * @param {object} props.defaultValue - Default value of element
+ * @param {function(object, number):string|number} props.getItemId - Function to change how detects unique item
+ * @param {function(object, string|number, number):object} props.addItemId - Function to add ID
* @example Example usage.
* // const INITIAL_STATE = { listKey: [{ id: 'item1' }, { id: 'item2' }] }
* // const [formData, setFormData] = useState(INITIAL_STATE)
@@ -41,39 +102,39 @@ const getIndexById = (list, id) =>
* // setList: setFormData
* // defaultValue: { id: 'default' }
* // })
- * @returns {{
- * editingData: Function,
- * handleSelect: Function,
- * handleUnselect: Function,
- * handleClear: Function,
- * handleClone: Function,
- * handleRemove: Function,
- * handleSetList: Function,
- * handleSave: Function,
- * handleEdit: Function
- * }} - Functions to manage the list
+ * @returns {HookListForm} - Functions to manage the list
*/
-const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
- const [editingData, setEditingData] = useState({})
+const useListForm = ({
+ multiple,
+ key,
+ parent,
+ list,
+ setList,
+ defaultValue,
+ getItemId = defaultGetItemId,
+ addItemId = defaultAddItemId
+}) => {
+ const [editingData, setEditingData] = useState(() => undefined)
+
+ const getIndexById = useCallback(
+ (listToFind, searchId = -1) =>
+ listToFind.findIndex((item, idx) => getItemId(item, idx) === searchId),
+ []
+ )
const handleSetList = useCallback(
- /**
- * Resets the list with a new value.
- *
- * @param {Array} newList - New list
- */
- newList => {
- setList(data => ({ ...set(data, key, newList) }))
- },
- [key, setList, list]
+ newList => setList(
+ data => {
+ const path = parent ? [parent, key].join('.') : key
+ const newData = set({ ...data }, path, newList)
+
+ return parent ? { ...data, [parent]: newData } : { ...data, ...newData }
+ }
+ ),
+ [key, parent, setList]
)
const handleSelect = useCallback(
- /**
- * Add an item to data form list.
- *
- * @param {string|number} id - Element id
- */
id => {
setList(prevList => ({
...prevList,
@@ -84,84 +145,54 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
)
const handleUnselect = useCallback(
- /**
- * Removes an item from data form list.
- *
- * @param {string|number} id - Element id
- * @param {Function} [filter] - Filter function to remove the item.
- */
- (id, filter) => {
- setList(prevList => ({
- ...prevList,
- [key]: prevList[key]?.filter(filter ?? (item => item !== id))
- }))
+ id => {
+ const newList = [...list]
+ ?.filter((item, idx) => getItemId(item, idx) !== id)
+
+ handleSetList(newList)
},
- [key, setList]
+ []
)
const handleClear = useCallback(
- /** Clear the data form list. */
() => {
setList(prevList => ({ ...prevList, [key]: [] }))
},
[key]
)
- /**
- * Clones an item and change two attributes:
- * - id: id from default value
- * - name: same name of element cloned, with the suffix '_clone'
- *
- * @param {string|number} id - Element id
- */
const handleClone = useCallback(
id => {
const itemIndex = getIndexById(list, id)
- const { id: itemId, name = itemId, ...item } = list[itemIndex]
+ const cloneItem = addItemId(list[itemIndex], undefined, itemIndex)
const cloneList = [...list]
- const cloneItem = {
- ...item,
- id: defaultValue.id,
- name: `${name}_clone`
- }
- const ZERO_DELETE_COUNT = 0
cloneList.splice(NEXT_INDEX(itemIndex), ZERO_DELETE_COUNT, cloneItem)
handleSetList(cloneList)
},
- [list]
+ [list, defaultValue]
)
- /** Removes an item from data form list. */
const handleRemove = useCallback(handleUnselect, [key, list])
const handleSave = useCallback(
- /**
- * Saves the data from editing state.
- *
- * @param {object} values - New element data
- * @param {string|number} [id] - Element id
- */
- (values, id = editingData?.id) => {
+ (values, id = getItemId(values) ?? uuidv4()) => {
const itemIndex = getIndexById(list, id)
const index = EXISTS_INDEX(itemIndex) ? itemIndex : list.length
- const newList = set(list, index, { ...editingData, ...values })
+ const newList = Object.assign([], [...list],
+ { [index]: getItemId(values) ? values : addItemId(values, id, itemIndex) }
+ )
handleSetList(newList)
},
- [key, list, editingData]
+ [list]
)
const handleEdit = useCallback(
- /**
- * Find the element by id and set value to editing state.
- *
- * @param {string|number} id - Element id
- */
id => {
- const index = list.findIndex(item => item.id === id)
+ const index = getIndexById(list, id)
const openData = list[index] ?? defaultValue
setEditingData(openData)
diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js
index 0736279e8b..c5e73a116e 100644
--- a/src/fireedge/src/client/models/Helper.js
+++ b/src/fireedge/src/client/models/Helper.js
@@ -33,7 +33,7 @@ export const jsonToXml = (json, addRoot = true) => {
* @param {boolean} bool - Boolean value.
* @returns {'Yes'|'No'} - If true return 'Yes', in other cases, return 'No'.
*/
-export const booleanToString = bool => bool ? 'Yes' : 'No'
+export const booleanToString = bool => bool ? T.Yes : T.No
/**
* Converts the string value into a boolean.
@@ -194,7 +194,7 @@ export const getActionsAvailable = (actions = {}, hypervisor = '') =>
* @param {RegExp} [options.hidden] - RegExp of hidden attributes
* @returns {{attributes: object}} List of filtered attributes
*/
-export const filterAttributes = (list, options = {}) => {
+export const filterAttributes = (list = {}, options = {}) => {
const { extra = {}, hidden = /^$/ } = options
const response = {}
diff --git a/src/fireedge/src/client/models/Host.js b/src/fireedge/src/client/models/Host.js
index 392eb4659c..97b6d625af 100644
--- a/src/fireedge/src/client/models/Host.js
+++ b/src/fireedge/src/client/models/Host.js
@@ -20,16 +20,21 @@ import { HOST_STATES, StateInfo } from 'client/constants'
* Returns information about the host state.
*
* @param {object} host - Host
- * @param {number} host.STATE - Host state
* @returns {StateInfo} Host state object
*/
-export const getState = ({ STATE = 0 } = {}) => HOST_STATES[+STATE]
+export const getState = host => HOST_STATES[+host?.STATE ?? 0]
+
+/**
+ * @param {object} host - Host
+ * @returns {Array} List of datastores from resource
+ */
+export const getDatastores = host =>
+ [host?.HOST_SHARE?.DATASTORES?.DS ?? []].flat()
/**
* Returns the allocate information.
*
* @param {object} host - Host
- * @param {object} host.HOST_SHARE - Host share object
* @returns {{
* percentCpuUsed: number,
* percentCpuLabel: string,
@@ -37,17 +42,18 @@ export const getState = ({ STATE = 0 } = {}) => HOST_STATES[+STATE]
* percentMemLabel: string
* }} Allocated information object
*/
-export const getAllocatedInfo = ({ HOST_SHARE = {} } = {}) => {
- const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM } = HOST_SHARE
+export const getAllocatedInfo = host => {
+ const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM } = host?.HOST_SHARE ?? {}
const percentCpuUsed = +CPU_USAGE * 100 / +TOTAL_CPU || 0
const percentCpuLabel = `${CPU_USAGE} / ${TOTAL_CPU}
(${Math.round(isFinite(percentCpuUsed) ? percentCpuUsed : '--')}%)`
+ const isMemUsageNegative = +MEM_USAGE < 0
const percentMemUsed = +MEM_USAGE * 100 / +TOTAL_MEM || 0
- const usedMemBytes = prettyBytes(+MEM_USAGE)
+ const usedMemBytes = prettyBytes(Math.abs(+MEM_USAGE))
const totalMemBytes = prettyBytes(+TOTAL_MEM)
- const percentMemLabel = `${usedMemBytes} / ${totalMemBytes}
+ const percentMemLabel = `${isMemUsageNegative ? '-' : ''}${usedMemBytes} / ${totalMemBytes}
(${Math.round(isFinite(percentMemUsed) ? percentMemUsed : '--')}%)`
return {
diff --git a/src/fireedge/src/client/models/VirtualMachine.js b/src/fireedge/src/client/models/VirtualMachine.js
index 513a23c551..372ec413c4 100644
--- a/src/fireedge/src/client/models/VirtualMachine.js
+++ b/src/fireedge/src/client/models/VirtualMachine.js
@@ -49,7 +49,7 @@ export const getHistoryAction = action => HISTORY_ACTIONS[+action]
* @returns {object} History records from resource
*/
export const getHistoryRecords = vm =>
- [vm?.HISTORY_RECORDS?.HISTORY ?? {}].flat()
+ [vm?.HISTORY_RECORDS?.HISTORY ?? []].flat()
/**
* @param {object} vm - Virtual machine
diff --git a/src/fireedge/src/client/providers/notistackProvider.js b/src/fireedge/src/client/providers/notistackProvider.js
index 119292c435..1457e5a435 100644
--- a/src/fireedge/src/client/providers/notistackProvider.js
+++ b/src/fireedge/src/client/providers/notistackProvider.js
@@ -22,7 +22,7 @@ import { makeStyles } from '@material-ui/core/styles'
const useStyles = makeStyles(({ palette }) => ({
containerRoot: {
marginLeft: 20,
- wordBreak: 'break-all'
+ wordBreak: 'break-word'
},
variantSuccess: {
backgroundColor: palette.success.main,
diff --git a/src/fireedge/src/client/router/index.js b/src/fireedge/src/client/router/index.js
index 1fc6c89b23..ee9f5c94a5 100644
--- a/src/fireedge/src/client/router/index.js
+++ b/src/fireedge/src/client/router/index.js
@@ -28,9 +28,9 @@ import {
import { ProtectedRoute, NoAuthRoute } from 'client/components/Route'
import { InternalLayout } from 'client/components/HOC'
-const renderRoute = ({ Component, ...rest }, index) => (
+const renderRoute = ({ Component, label, ...rest }, index) => (
-
+
} />
diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js
index 2c014a28d5..c63dfe25c7 100644
--- a/src/fireedge/src/client/utils/helpers.js
+++ b/src/fireedge/src/client/utils/helpers.js
@@ -96,13 +96,10 @@ export const addOpacityToColor = (color, opacity) => {
* @returns {object} List of validations
*/
export const getValidationFromFields = fields =>
- fields.reduce(
- (schema, field) => ({
- ...schema,
- [field?.name]: field?.validation
- }),
- {}
- )
+ fields.reduce((schema, field) => ({
+ ...schema,
+ [field?.name]: field?.validation
+ }), {})
/**
* Filter an object list by property.
diff --git a/src/fireedge/src/client/utils/schema.js b/src/fireedge/src/client/utils/schema.js
index a81897253b..e1b16e4d6f 100644
--- a/src/fireedge/src/client/utils/schema.js
+++ b/src/fireedge/src/client/utils/schema.js
@@ -13,9 +13,144 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
-import * as yup from 'yup'
+/* eslint-disable jsdoc/valid-types */
+
+// eslint-disable-next-line no-unused-vars
+import { JSXElementConstructor, SetStateAction } from 'react'
+// eslint-disable-next-line no-unused-vars
+import { GridProps, TextFieldProps, CheckboxProps, InputBaseComponentProps } from '@material-ui/core'
+import { string, number, boolean, array, object, BaseSchema } from 'yup'
+
import { INPUT_TYPES } from 'client/constants'
+// ----------------------------------------------------------
+// Types
+// ----------------------------------------------------------
+
+/**
+ * @typedef {object} ValidateOptions
+ * @property {boolean} [strict]
+ * - Only validate the input, and skip and coercion or transformation.
+ * Default - `false`
+ * @property {boolean} [abortEarly]
+ * - Return from validation methods on the first error rather than after all validations run.
+ * Default - `true`
+ * @property {boolean} [stripUnknown]
+ * - Remove unspecified keys from objects.
+ * Default - `false`
+ * @property {boolean} [recursive]
+ * - When `false` validations will not descend into nested schema (relevant for objects or arrays).
+ * Default - `true`
+ * @property {object} [context]
+ * - Any context needed for validating schema conditions
+ */
+
+/**
+ * Callback of field parameter when depend of another field.
+ *
+ * @callback DependOfCallback
+ * @param {string|string[]} value - Value
+ * @returns {any|any[]}
+ */
+
+/**
+ * @typedef {object} Field
+ * @property {string|DependOfCallback} name
+ * - Path name in the form
+ * - IMPORTANT: Name is required and unique
+ * (can not start with a number or use number as key name)
+ * Name also supports dot and bracket syntax.
+ * - For example: 'firstName', 'person.firstName' or 'person[0].firstName'
+ * @property {string|string[]} dependOf
+ * - Path names of other fields on the form.
+ * Can be used by the rest of the properties, receiving it by function parameters
+ * @property {string|DependOfCallback} label
+ * - Label of field
+ * @property {string|DependOfCallback} [tooltip]
+ * - Text description
+ * @property {INPUT_TYPES|DependOfCallback} type
+ * - Field type to draw
+ * @property {string|DependOfCallback} [htmlType]
+ * - Type of the input element. It should be a valid HTML5 input type
+ * @property {{text: string|JSXElementConstructor, value: any}[]|DependOfCallback} [values]
+ * - Type of the input element. It should be a valid HTML5 input type
+ * @property {boolean|DependOfCallback} [multiline]
+ * - If `true`, a textarea element will be rendered instead of an input.
+ * @property {GridProps|DependOfCallback} [grid]
+ * - Grid properties to override in the wrapper element
+ * - Default: { xs: 12, md: 6 }
+ * @property {BaseSchema|DependOfCallback} [validation]
+ * - Schema to validate the field value
+ * @property {TextFieldProps|CheckboxProps|InputBaseComponentProps} [fieldProps]
+ * - Extra properties to material field
+ * @property {{message: string, test: Function}[]|DependOfCallback} [validationBeforeTransform]
+ * - Tests to validate the field value.
+ * - **Only for file inputs.**
+ * @property {Function|DependOfCallback} [transform]
+ * - Transform the file value.
+ * - For example: to save file as string value in base64 format.
+ * - **Only for file inputs.**
+ */
+
+/**
+ * @typedef {object} Form
+ * @property {BaseSchema|function(object):BaseSchema} resolver - Schema
+ * @property {Field[]|function(object):Field[]} fields - Fields to draw at form
+ * @property {object} defaultValues - Default values
+ */
+
+/**
+ * @typedef {object} Step
+ * @property {string} id - Id
+ * @property {string} label - Label
+ * @property {BaseSchema|function(object):BaseSchema} resolver - Schema
+ * @property {function(object, SetStateAction):JSXElementConstructor} content - Content
+ * @property {ValidateOptions|undefined} optionsValidate - Validate options
+ */
+
+/**
+ * @typedef {object} StepsForm
+ * @property {Step[]} steps - Steps
+ * @property {BaseSchema} resolver - Schema
+ * @property {object} defaultValues - Default values
+ */
+
+/**
+ * @typedef {object} ExtraParams
+ * @property {function(object):object} [transformBeforeSubmit] - Transform validated form data after submit
+ * @property {function(object):object} [transformInitialValue] - Transform initial value after load form
+ */
+
+/**
+ * @callback StepComponent
+ * @param {object} stepProps - Properties passes to all Step functions
+ * @returns {function(object):Step}
+ */
+
+/**
+ * @callback CreateStepsCallback
+ * @param {object} stepProps - Properties passes to all Step functions
+ * @param {object} initialValues - Initial values to form
+ * @returns {StepsForm & ExtraParams}
+ */
+
+/**
+ * @callback CreateFormCallback
+ * @param {object} props - Properties passes to schema and field
+ * @param {object} initialValues - Initial values to form
+ * @returns {Form & ExtraParams}
+ */
+
+/**
+ * @typedef {('text'|'text64'|'password'|'number'|'number-float'|'range'|
+ * 'range-float'|'boolean'|'list'|'array'|'list-multiple')} UserInputType
+ * - OpenNebula types for user inputs
+ */
+
+// ----------------------------------------------------------
+// Constants
+// ----------------------------------------------------------
+
const requiredSchema = (mandatory, name, schema) =>
mandatory
? schema.required(`${name} field is required`)
@@ -32,21 +167,19 @@ const getOptionsFromList = options => options
)
?.filter(({ text, value } = {}) => text && value)
-/**
- * @typedef {(
- * 'text'|
- * 'text64'|
- * 'password'|
- * 'number'|
- * 'number-float'|
- * 'range'|
- * 'range-float'|
- * 'boolean'|
- * 'list'|
- * 'array'|
- * 'list-multiple'
- * )} TypeInput - OpenNebula types for user inputs
- */
+const parseUserInputValue = value => {
+ if (value === true) {
+ return 'YES'
+ } else if (value === false) {
+ return 'NO'
+ } else if (Array.isArray(value)) {
+ return value.join(',')
+ } else return value
+}
+
+// ----------------------------------------------------------
+// Function (to export)
+// ----------------------------------------------------------
/**
* Get input schema for the user input defined in OpenNebula resource.
@@ -54,16 +187,10 @@ const getOptionsFromList = options => options
* @param {object} userInput - User input from OpenNebula document
* @param {boolean} userInput.mandatory - If `true`, the input will be required
* @param {string} userInput.name - Name of input
- * @param {userInput} userInput.type - Input type
+ * @param {UserInputType} userInput.type - Input type
* @param {string} [userInput.options] - Options available for the input
* @param {number|string|string[]} [userInput.defaultValue] - Default value for the input
- * @returns {{
- * type: INPUT_TYPES,
- * htmlType: string,
- * multiple: boolean,
- * validation: yup.AnyObjectSchema,
- * fieldProps: object
- * }} Schema properties
+ * @returns {Field} Field properties
*/
export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }) => {
switch (type) {
@@ -72,18 +199,18 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
case 'password': return {
type: INPUT_TYPES.TEXT,
htmlType: type === 'password' ? 'password' : 'text',
- validation: yup.string()
+ validation: string()
.trim()
- .concat(requiredSchema(mandatory, name, yup.string()))
+ .concat(requiredSchema(mandatory, name, string()))
.default(defaultValue || undefined)
}
case 'number':
case 'number-float': return {
type: INPUT_TYPES.TEXT,
htmlType: 'number',
- validation: yup.number()
+ validation: number()
.typeError(`${name} must be a number`)
- .concat(requiredSchema(mandatory, name, yup.number()))
+ .concat(requiredSchema(mandatory, name, number()))
.transform(value => !isNaN(value) ? value : null)
.default(() => parseFloat(defaultValue) ?? undefined)
}
@@ -93,9 +220,9 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
return {
type: INPUT_TYPES.SLIDER,
- validation: yup.number()
+ validation: number()
.typeError(`${name} must be a number`)
- .concat(requiredSchema(mandatory, name, yup.number()))
+ .concat(requiredSchema(mandatory, name, number()))
.min(min, `${name} must be greater than or equal to ${min}`)
.max(max, `${name} must be less than or equal to ${max}`)
.transform(value => !isNaN(value) ? value : undefined)
@@ -105,8 +232,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
}
case 'boolean': return {
type: INPUT_TYPES.CHECKBOX,
- validation: yup.boolean()
- .concat(requiredSchema(mandatory, name, yup.boolean()))
+ validation: boolean()
+ .concat(requiredSchema(mandatory, name, boolean()))
.default(defaultValue === 'YES' ?? false)
}
case 'list': {
@@ -116,9 +243,9 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
return {
values,
type: INPUT_TYPES.SELECT,
- validation: yup.string()
+ validation: string()
.trim()
- .concat(requiredSchema(mandatory, name, yup.string()))
+ .concat(requiredSchema(mandatory, name, string()))
.oneOf(values.map(({ value }) => value))
.default(defaultValue ?? firstOption)
}
@@ -129,8 +256,8 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
return {
type: INPUT_TYPES.AUTOCOMPLETE,
multiple: true,
- validation: yup.array(yup.string().trim())
- .concat(requiredSchema(mandatory, name, yup.array()))
+ validation: array(string().trim())
+ .concat(requiredSchema(mandatory, name, array()))
.default(defaultValues),
fieldProps: { freeSolo: true }
}
@@ -143,33 +270,21 @@ export const schemaUserInput = ({ mandatory, name, type, options, defaultValue }
values,
type: INPUT_TYPES.SELECT,
multiple: true,
- validation: yup.array(yup.string().trim())
- .concat(requiredSchema(mandatory, name, yup.array()))
+ validation: array(string().trim())
+ .concat(requiredSchema(mandatory, name, array()))
.default(defaultValues)
}
}
default: return {
type: INPUT_TYPES.TEXT,
- validation: yup.string()
+ validation: string()
.trim()
- .concat(requiredSchema(mandatory, name, yup.string()))
+ .concat(requiredSchema(mandatory, name, string()))
.default(defaultValue || undefined)
}
}
}
-// Parser USER INPUTS
-
-const parseUserInputValue = value => {
- if (value === true) {
- return 'YES'
- } if (value === false) {
- return 'NO'
- } else if (Array.isArray(value)) {
- return value.join(',')
- } else return value
-}
-
/**
* Parse JS values to OpenNebula values.
*
@@ -177,10 +292,67 @@ const parseUserInputValue = value => {
* @example Example of parsed
* // { user_ssh: true } => { user_ssh: 'YES' }
* // { groups: [1, 2, 3] } => { groups: '1,2,3' }
- * @returns {object}
- * - Returns same object with values can be operated by OpenNebula
+ * @returns {object} - Returns same object with values can be operated by OpenNebula
*/
export const mapUserInputs = (userInputs = {}) =>
Object.entries(userInputs)?.reduce((res, [key, value]) => ({
...res, [key]: parseUserInputValue(value)
}), {})
+
+/**
+ * Returns parameters needed to create stepper form.
+ *
+ * @param {StepComponent[]|function(object):StepComponent[]} steps - Step functions list or function to get it
+ * @param {ExtraParams} [extraParams] - Extra parameters
+ * @returns {CreateStepsCallback} Function to get steps
+ */
+export const createSteps = (steps, extraParams = {}) =>
+ (stepProps = {}, initialValues) => {
+ const { transformInitialValue = () => initialValues } = extraParams
+
+ const stepCallbacks = typeof steps === 'function' ? steps(stepProps) : steps
+ const performedSteps = stepCallbacks.map(step => step(stepProps))
+
+ const schemas = object()
+ for (const { id, resolver } of performedSteps) {
+ const schema = typeof resolver === 'function' ? resolver() : resolver
+
+ schemas.concat(object({ [id]: schema }))
+ }
+
+ const defaultValues = initialValues
+ ? transformInitialValue(initialValues, schemas)
+ : schemas.default()
+
+ return {
+ steps: performedSteps,
+ defaultValues,
+ resolver: () => schemas,
+ ...extraParams
+ }
+ }
+
+/**
+ * Returns parameters needed to create a form.
+ *
+ * @param {BaseSchema|function(object):BaseSchema} schema - Schema to validate the form
+ * @param {Field[]|function(object):Field[]} fields - Fields to draw in the form
+ * @param {ExtraParams} [extraParams] - Extra parameters
+ * @returns {CreateFormCallback} Function to get form parameters
+ */
+export const createForm = (schema, fields, extraParams = {}) =>
+ (props = {}, initialValues) => {
+ const schemaCallback = typeof schema === 'function' ? schema(props) : schema
+ const fieldsCallback = typeof fields === 'function' ? fields(props) : fields
+
+ const defaultValues = initialValues
+ ? schemaCallback.cast(initialValues, { stripUnknown: true })
+ : schemaCallback.default()
+
+ return {
+ resolver: () => schemaCallback,
+ fields: () => fieldsCallback,
+ defaultValues,
+ ...extraParams
+ }
+ }
diff --git a/src/fireedge/src/server/utils/constants/commands/vmgroup.js b/src/fireedge/src/server/utils/constants/commands/vmgroup.js
index 8e54be4a52..58b58e9093 100644
--- a/src/fireedge/src/server/utils/constants/commands/vmgroup.js
+++ b/src/fireedge/src/server/utils/constants/commands/vmgroup.js
@@ -19,34 +19,34 @@ const {
httpMethod: { GET, POST, PUT, DELETE }
} = require('../defaults')
-const VMGROUP_ALLOCATE = 'vmgroup.allocate'
-const VMGROUP_DELETE = 'vmgroup.delete'
-const VMGROUP_UPDATE = 'vmgroup.update'
-const VMGROUP_CHMOD = 'vmgroup.chmod'
-const VMGROUP_CHOWN = 'vmgroup.chown'
-const VMGROUP_RENAME = 'vmgroup.rename'
-const VMGROUP_INFO = 'vmgroup.info'
-const VMGROUP_LOCK = 'vmgroup.lock'
-const VMGROUP_UNLOCK = 'vmgroup.unlock'
-const VMGROUP_POOL_INFO = 'vmgrouppool.info'
+const VM_GROUP_ALLOCATE = 'vmgroup.allocate'
+const VM_GROUP_DELETE = 'vmgroup.delete'
+const VM_GROUP_UPDATE = 'vmgroup.update'
+const VM_GROUP_CHMOD = 'vmgroup.chmod'
+const VM_GROUP_CHOWN = 'vmgroup.chown'
+const VM_GROUP_RENAME = 'vmgroup.rename'
+const VM_GROUP_INFO = 'vmgroup.info'
+const VM_GROUP_LOCK = 'vmgroup.lock'
+const VM_GROUP_UNLOCK = 'vmgroup.unlock'
+const VM_GROUP_POOL_INFO = 'vmgrouppool.info'
const Actions = {
- VMGROUP_ALLOCATE,
- VMGROUP_DELETE,
- VMGROUP_UPDATE,
- VMGROUP_CHMOD,
- VMGROUP_CHOWN,
- VMGROUP_RENAME,
- VMGROUP_INFO,
- VMGROUP_LOCK,
- VMGROUP_UNLOCK,
- VMGROUP_POOL_INFO
+ VM_GROUP_ALLOCATE,
+ VM_GROUP_DELETE,
+ VM_GROUP_UPDATE,
+ VM_GROUP_CHMOD,
+ VM_GROUP_CHOWN,
+ VM_GROUP_RENAME,
+ VM_GROUP_INFO,
+ VM_GROUP_LOCK,
+ VM_GROUP_UNLOCK,
+ VM_GROUP_POOL_INFO
}
module.exports = {
Actions,
Commands: {
- [VMGROUP_ALLOCATE]: {
+ [VM_GROUP_ALLOCATE]: {
// inspected
httpMethod: POST,
params: {
@@ -56,7 +56,7 @@ module.exports = {
}
}
},
- [VMGROUP_DELETE]: {
+ [VM_GROUP_DELETE]: {
// inspected
httpMethod: DELETE,
params: {
@@ -66,7 +66,7 @@ module.exports = {
}
}
},
- [VMGROUP_UPDATE]: {
+ [VM_GROUP_UPDATE]: {
// inspected
httpMethod: PUT,
params: {
@@ -84,7 +84,7 @@ module.exports = {
}
}
},
- [VMGROUP_CHMOD]: {
+ [VM_GROUP_CHMOD]: {
// inspected
httpMethod: PUT,
params: {
@@ -130,7 +130,7 @@ module.exports = {
}
}
},
- [VMGROUP_CHOWN]: {
+ [VM_GROUP_CHOWN]: {
// inspected
httpMethod: PUT,
params: {
@@ -148,7 +148,7 @@ module.exports = {
}
}
},
- [VMGROUP_RENAME]: {
+ [VM_GROUP_RENAME]: {
// inspected
httpMethod: PUT,
params: {
@@ -162,7 +162,7 @@ module.exports = {
}
}
},
- [VMGROUP_INFO]: {
+ [VM_GROUP_INFO]: {
// inspected
httpMethod: GET,
params: {
@@ -176,7 +176,7 @@ module.exports = {
}
}
},
- [VMGROUP_LOCK]: {
+ [VM_GROUP_LOCK]: {
// inspected
httpMethod: PUT,
params: {
@@ -190,7 +190,7 @@ module.exports = {
}
}
},
- [VMGROUP_UNLOCK]: {
+ [VM_GROUP_UNLOCK]: {
// inspected
httpMethod: PUT,
params: {
@@ -200,7 +200,7 @@ module.exports = {
}
}
},
- [VMGROUP_POOL_INFO]: {
+ [VM_GROUP_POOL_INFO]: {
// inspected
httpMethod: GET,
params: {