diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 463f08ac40..2a14e380ad 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -207,6 +207,12 @@ const ClusterDetail = loadable(
() => import('client/containers/Clusters/Detail'),
{ ssr: false }
)
+const CreateCluster = loadable(
+ () => import('client/containers/Clusters/Create'),
+ {
+ ssr: false,
+ }
+)
const Hosts = loadable(() => import('client/containers/Hosts'), { ssr: false })
const HostDetail = loadable(() => import('client/containers/Hosts/Detail'), {
ssr: false,
@@ -358,6 +364,7 @@ export const PATH = {
CLUSTERS: {
LIST: `/${RESOURCE_NAMES.CLUSTER}`,
DETAIL: `/${RESOURCE_NAMES.CLUSTER}/:id`,
+ CREATE: `/${RESOURCE_NAMES.CLUSTER}/create`,
},
HOSTS: {
LIST: `/${RESOURCE_NAMES.HOST}`,
@@ -687,6 +694,11 @@ const ENDPOINTS = [
icon: ClusterIcon,
Component: Clusters,
},
+ {
+ title: T.CreateCluster,
+ path: PATH.INFRASTRUCTURE.CLUSTERS.CREATE,
+ Component: CreateCluster,
+ },
{
title: T.Cluster,
description: (params) => `#${params?.id}`,
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Datastores/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Datastores/index.js
new file mode 100644
index 0000000000..757972c09e
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Datastores/index.js
@@ -0,0 +1,104 @@
+/* ------------------------------------------------------------------------- *
+ * 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, FIELDS } from './schema'
+import { Grid, Card, CardContent, Typography } from '@mui/material'
+import { Tr } from 'client/components/HOC'
+import { generateDocLink } from 'client/utils'
+
+export const STEP_ID = 'datastores'
+
+const Content = (version) => (
+
+
+
+
+
+
+
+
+ {Tr(T['cluster.form.create.datastores.help.title'])}
+
+
+ {Tr(T['cluster.form.create.datastores.help.paragraph.1'])}
+
+
+ {Tr(T['cluster.form.create.datastores.help.paragraph.2'])}
+
+
+ {Tr(T['cluster.form.create.datastores.help.paragraph.3'])}
+
+
+ {' '}
+
+ {Tr(T['cluster.form.create.help.link'])}
+
+
+
+
+
+
+)
+
+/**
+ * Datastores Cluster configuration.
+ *
+ * @param {object} props - Step properties
+ * @param {string} props.version - OpeNebula version
+ * @returns {object} Datastores configuration step
+ */
+const Datastores = ({ version }) => ({
+ id: STEP_ID,
+ label: T.SelectDatastores,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: () => Content(version),
+})
+
+Datastores.propTypes = {
+ data: PropTypes.object,
+ setFormData: PropTypes.func,
+}
+
+export default Datastores
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Datastores/schema.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Datastores/schema.js
new file mode 100644
index 0000000000..a1d6581c81
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Datastores/schema.js
@@ -0,0 +1,39 @@
+/* ------------------------------------------------------------------------- *
+ * 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, array } from 'yup'
+import { DatastoresTable } from 'client/components/Tables'
+
+/** @type {Field} Datastores field */
+const DATASTORES = {
+ name: 'ID',
+ label: T.SelectDatastores,
+ type: INPUT_TYPES.TABLE,
+ Table: () => DatastoresTable,
+ singleSelect: false,
+ validation: array(string().trim()).default(() => undefined),
+ grid: { md: 12 },
+ fieldProps: {
+ preserveState: true,
+ },
+}
+
+const FIELDS = [DATASTORES]
+
+const SCHEMA = getObjectSchemaFromFields(FIELDS)
+
+export { SCHEMA, FIELDS }
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/General/index.js
new file mode 100644
index 0000000000..65e5ced213
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/General/index.js
@@ -0,0 +1,118 @@
+/* ------------------------------------------------------------------------- *
+ * 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, FIELDS } from './schema'
+import { Grid, Card, CardContent, Typography } from '@mui/material'
+import { Tr } from 'client/components/HOC'
+import { generateDocLink } from 'client/utils'
+
+export const STEP_ID = 'general'
+
+const Content = (version) => (
+
+
+
+
+
+
+
+
+ {' '}
+ {Tr(T['cluster.form.create.general.help.title'])}{' '}
+
+
+
+ {Tr(T['cluster.form.create.general.help.paragraph.1.1'])}{' '}
+
+
+
+ -
+
+ {Tr(T['cluster.form.create.general.help.paragraph.1.2'])}{' '}
+
+
+ -
+
+ {Tr(T['cluster.form.create.general.help.paragraph.1.3'])}{' '}
+
+
+
+
+
+ {Tr(T['cluster.form.create.general.help.paragraph.2'])}{' '}
+
+
+
+ {' '}
+
+ {Tr(T['cluster.form.create.help.link'])}
+
+
+
+
+
+
+)
+
+/**
+ * General Cluster configuration.
+ *
+ * @param {object} props - Step properties
+ * @param {string} props.version - OpeNebula version
+ * @returns {object} General configuration step
+ */
+const General = ({ version }) => ({
+ id: STEP_ID,
+ label: T.General,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: () => Content(version),
+})
+
+General.propTypes = {
+ data: PropTypes.object,
+ setFormData: PropTypes.func,
+}
+
+export default General
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..52095814de
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/General/schema.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 { INPUT_TYPES, T } from 'client/constants'
+import { Field, getObjectSchemaFromFields } from 'client/utils'
+import { string } from 'yup'
+
+/** @type {Field} Name field */
+const NAME = {
+ name: 'NAME',
+ label: T['cluster.create.name'],
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .required()
+ .default(() => undefined),
+ grid: { md: 12 },
+}
+
+const FIELDS = [NAME]
+
+const SCHEMA = getObjectSchemaFromFields(FIELDS)
+
+export { SCHEMA, FIELDS }
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Hosts/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Hosts/index.js
new file mode 100644
index 0000000000..b33d47d9ea
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Hosts/index.js
@@ -0,0 +1,104 @@
+/* ------------------------------------------------------------------------- *
+ * 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, FIELDS } from './schema'
+import { Grid, Card, CardContent, Typography } from '@mui/material'
+import { Tr } from 'client/components/HOC'
+import { generateDocLink } from 'client/utils'
+
+export const STEP_ID = 'hosts'
+
+const Content = (version) => (
+
+
+
+
+
+
+
+
+ {' '}
+ {Tr(T['cluster.form.create.hosts.help.title'])}{' '}
+
+
+ {' '}
+ {Tr(T['cluster.form.create.hosts.help.paragraph.1'])}{' '}
+
+
+ {' '}
+ {Tr(T['cluster.form.create.hosts.help.paragraph.2'])}{' '}
+
+
+ {' '}
+
+ {Tr(T['cluster.form.create.help.link'])}
+
+
+
+
+
+
+)
+
+/**
+ * Hosts Cluster configuration.
+ *
+ * @param {object} props - Step properties
+ * @param {string} props.version - OpeNebula version
+ * @returns {object} Hosts configuration step
+ */
+const Hosts = ({ version }) => ({
+ id: STEP_ID,
+ label: T.SelectHosts,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: () => Content(version),
+})
+
+Hosts.propTypes = {
+ data: PropTypes.object,
+ setFormData: PropTypes.func,
+}
+
+export default Hosts
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Hosts/schema.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Hosts/schema.js
new file mode 100644
index 0000000000..7a91448137
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Hosts/schema.js
@@ -0,0 +1,39 @@
+/* ------------------------------------------------------------------------- *
+ * 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, array } from 'yup'
+import { HostsTable } from 'client/components/Tables'
+
+/** @type {Field} HostsTable field */
+const HOSTS = {
+ name: 'ID',
+ label: T.SelectHosts,
+ type: INPUT_TYPES.TABLE,
+ Table: () => HostsTable,
+ singleSelect: false,
+ validation: array(string().trim()).default(() => undefined),
+ grid: { md: 12 },
+ fieldProps: {
+ preserveState: true,
+ },
+}
+
+const FIELDS = [HOSTS]
+
+const SCHEMA = getObjectSchemaFromFields(FIELDS)
+
+export { SCHEMA, FIELDS }
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Vnets/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Vnets/index.js
new file mode 100644
index 0000000000..833f4c808b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Vnets/index.js
@@ -0,0 +1,101 @@
+/* ------------------------------------------------------------------------- *
+ * 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, FIELDS } from './schema'
+import { Grid, Card, CardContent, Typography } from '@mui/material'
+import { Tr } from 'client/components/HOC'
+import { generateDocLink } from 'client/utils'
+
+export const STEP_ID = 'vnets'
+
+const Content = (version) => (
+
+
+
+
+
+
+
+
+ {Tr(T['cluster.form.create.vnets.help.title'])}
+
+
+ {Tr(T['cluster.form.create.vnets.help.paragraph.1'])}
+
+
+ {Tr(T['cluster.form.create.vnets.help.paragraph.2'])}
+
+
+ {' '}
+
+ {Tr(T['cluster.form.create.help.link'])}
+
+
+
+
+
+
+)
+
+/**
+ * Vnets Cluster configuration.
+ *
+ * @param {object} props - Step properties
+ * @param {string} props.version - OpeNebula version
+ * @returns {object} Vnets configuration step
+ */
+const Vnets = ({ version }) => ({
+ id: STEP_ID,
+ label: T.SelectVirtualNetworks,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: () => Content(version),
+})
+
+Vnets.propTypes = {
+ data: PropTypes.object,
+ setFormData: PropTypes.func,
+}
+
+export default Vnets
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Vnets/schema.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Vnets/schema.js
new file mode 100644
index 0000000000..4f5625e993
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/Vnets/schema.js
@@ -0,0 +1,39 @@
+/* ------------------------------------------------------------------------- *
+ * 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, array } from 'yup'
+import { VNetworksTable } from 'client/components/Tables'
+
+/** @type {Field} Vnets field */
+const VNETS = {
+ name: 'ID',
+ label: T.SelectVirtualNetworks,
+ type: INPUT_TYPES.TABLE,
+ Table: () => VNetworksTable,
+ singleSelect: false,
+ validation: array(string().trim()).default(() => undefined),
+ grid: { md: 12 },
+ fieldProps: {
+ preserveState: true,
+ },
+}
+
+const FIELDS = [VNETS]
+
+const SCHEMA = getObjectSchemaFromFields(FIELDS)
+
+export { SCHEMA, FIELDS }
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..de0ae21fb0
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js
@@ -0,0 +1,135 @@
+/* ------------------------------------------------------------------------- *
+ * 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/Cluster/CreateForm/Steps/General'
+
+import Hosts, {
+ STEP_ID as HOSTS_ID,
+} from 'client/components/Forms/Cluster/CreateForm/Steps/Hosts'
+
+import Vnets, {
+ STEP_ID as VNETS_ID,
+} from 'client/components/Forms/Cluster/CreateForm/Steps/Vnets'
+
+import Datastores, {
+ STEP_ID as DATASTORES_ID,
+} from 'client/components/Forms/Cluster/CreateForm/Steps/Datastores'
+
+import { createSteps } from 'client/utils'
+const _ = require('lodash')
+
+/**
+ * Create steps for Cluster Create Form:
+ * 1. General: Name of the cluster
+ * 2. Hosts: Select hosts
+ * 3. Vnets: Select virtual networks
+ * 4. Datastores: Select datastores
+ */
+const Steps = createSteps([General, Hosts, Vnets, Datastores], {
+ transformInitialValue: (cluster, schema) => {
+ const knownTemplate = schema.cast(
+ {
+ [GENERAL_ID]: { NAME: cluster.NAME },
+ [HOSTS_ID]: !_.isEmpty(cluster?.HOSTS)
+ ? {
+ ID: Array.isArray(cluster?.HOSTS?.ID)
+ ? cluster?.HOSTS?.ID
+ : [cluster?.HOSTS?.ID],
+ }
+ : undefined,
+ [VNETS_ID]: !_.isEmpty(cluster?.VNETS)
+ ? {
+ ID: Array.isArray(cluster?.VNETS?.ID)
+ ? cluster?.VNETS?.ID
+ : [cluster?.VNETS?.ID],
+ }
+ : undefined,
+ [DATASTORES_ID]: !_.isEmpty(cluster?.DATASTORES)
+ ? {
+ ID: Array.isArray(cluster?.DATASTORES?.ID)
+ ? cluster?.DATASTORES?.ID
+ : [cluster?.DATASTORES?.ID],
+ }
+ : undefined,
+ },
+ {
+ stripUnknown: true,
+ }
+ )
+
+ return knownTemplate
+ },
+ transformBeforeSubmit: (formData, initialValues) => {
+ const update = !!initialValues
+
+ if (update) {
+ // Get hosts to add and hosts to delete
+ const initialHosts = !_.isEmpty(initialValues?.HOSTS?.ID)
+ ? Array.isArray(initialValues?.HOSTS?.ID)
+ ? initialValues?.HOSTS?.ID
+ : [initialValues?.HOSTS?.ID]
+ : undefined
+ const addHosts = _.difference(formData?.hosts?.ID, initialHosts)
+ const removeHosts = _.difference(initialHosts, formData?.hosts?.ID)
+
+ // Get vnets to add and vnets to delete
+ const initialVnets = !_.isEmpty(initialValues?.VNETS?.ID)
+ ? Array.isArray(initialValues?.VNETS?.ID)
+ ? initialValues?.VNETS?.ID
+ : [initialValues?.VNETS?.ID]
+ : undefined
+ const addVnets = _.difference(formData?.vnets?.ID, initialVnets)
+ const removeVnets = _.difference(initialVnets, formData?.vnets?.ID)
+
+ // Get datastores to add and datastores to delete
+ const initialDatastores = !_.isEmpty(initialValues?.DATASTORES?.ID)
+ ? Array.isArray(initialValues?.DATASTORES?.ID)
+ ? initialValues?.DATASTORES?.ID
+ : [initialValues?.DATASTORES?.ID]
+ : undefined
+ const addDatastores = _.difference(
+ formData?.datastores?.ID,
+ initialDatastores
+ )
+ const removeDatastores = _.difference(
+ initialDatastores,
+ formData?.datastores?.ID
+ )
+
+ // Check if the name has been changed
+ const changeName =
+ initialValues?.NAME === formData?.general?.NAME
+ ? undefined
+ : formData?.general?.NAME
+
+ return {
+ ...formData,
+ addHosts,
+ removeHosts,
+ addVnets,
+ removeVnets,
+ addDatastores,
+ removeDatastores,
+ changeName,
+ }
+ }
+
+ return formData
+ },
+})
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/index.js
new file mode 100644
index 0000000000..4fde39ca39
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/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 { default } from 'client/components/Forms/Cluster/CreateForm/Steps'
diff --git a/src/fireedge/src/client/components/Forms/Cluster/index.js b/src/fireedge/src/client/components/Forms/Cluster/index.js
index 9324be0326..0ac79305c8 100644
--- a/src/fireedge/src/client/components/Forms/Cluster/index.js
+++ b/src/fireedge/src/client/components/Forms/Cluster/index.js
@@ -24,4 +24,11 @@ import { CreateFormCallback } from 'client/utils/schema'
const ChangeClusterForm = (configProps) =>
AsyncLoadForm({ formPath: 'Cluster/ChangeClusterForm' }, configProps)
-export { ChangeClusterForm }
+/**
+ * @param {ConfigurationProps} configProps - Configuration
+ * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
+ */
+const CreateForm = (configProps) =>
+ AsyncLoadForm({ formPath: 'Cluster/CreateForm' }, configProps)
+
+export { ChangeClusterForm, CreateForm }
diff --git a/src/fireedge/src/client/components/Tables/Clusters/actions.js b/src/fireedge/src/client/components/Tables/Clusters/actions.js
new file mode 100644
index 0000000000..b89b023c6f
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/Clusters/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 { useRemoveClusterMutation } from 'client/features/OneApi/cluster'
+
+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, CLUSTER_ACTIONS } from 'client/constants'
+
+const ListClusterNames = ({ 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 Clusters table.
+ *
+ * @returns {GlobalAction} - Actions
+ */
+const Actions = () => {
+ const history = useHistory()
+ const { view, getResourceView } = useViews()
+ const [remove] = useRemoveClusterMutation()
+
+ return useMemo(
+ () =>
+ createActions({
+ filters: getResourceView(RESOURCE_NAMES.CLUSTER)?.actions,
+ actions: [
+ {
+ accessor: CLUSTER_ACTIONS.CREATE_DIALOG,
+ tooltip: T.Create,
+ icon: AddCircledOutline,
+ action: () => history.push(PATH.INFRASTRUCTURE.CLUSTERS.CREATE),
+ },
+ {
+ accessor: CLUSTER_ACTIONS.UPDATE_DIALOG,
+ label: T.Update,
+ tooltip: T.Update,
+ selected: { max: 1 },
+ color: 'secondary',
+ action: (rows) => {
+ const cluster = rows?.[0]?.original ?? {}
+ const path = PATH.INFRASTRUCTURE.CLUSTERS.CREATE
+
+ history.push(path, cluster)
+ },
+ },
+ {
+ accessor: CLUSTER_ACTIONS.DELETE,
+ tooltip: T.Delete,
+ icon: Trash,
+ color: 'error',
+ selected: { min: 1 },
+ dataCy: `cluster_${CLUSTER_ACTIONS.DELETE}`,
+ options: [
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: T.Delete,
+ dataCy: `modal-${CLUSTER_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/Clusters/row.js b/src/fireedge/src/client/components/Tables/Clusters/row.js
index 752c3bad0e..c3a4684c2a 100644
--- a/src/fireedge/src/client/components/Tables/Clusters/row.js
+++ b/src/fireedge/src/client/components/Tables/Clusters/row.js
@@ -29,23 +29,29 @@ const Row = ({ original, value, ...props }) => {
-
+
{NAME}
-
{`#${ID}`}
-
+ {`#${ID}`}
+
- {` ${HOSTS}`}
+ {`${HOSTS}`}
-
-
- {` ${DATASTORES}`}
-
-
+
- {` ${VNETS}`}
+ {`${VNETS}`}
+
+
+
+ {`${DATASTORES}`}
{PROVIDER_NAME && (
diff --git a/src/fireedge/src/client/components/Tables/Datastores/index.js b/src/fireedge/src/client/components/Tables/Datastores/index.js
index 5c88d540c9..6781c56ed7 100644
--- a/src/fireedge/src/client/components/Tables/Datastores/index.js
+++ b/src/fireedge/src/client/components/Tables/Datastores/index.js
@@ -54,7 +54,7 @@ const DatastoresTable = (props) => {
let values
- if (typeof filter === 'function') {
+ if (typeof filter === 'function' && dependOf) {
const { watch } = useFormContext()
const getDataForDepend = useCallback(
diff --git a/src/fireedge/src/client/components/Tables/Hosts/index.js b/src/fireedge/src/client/components/Tables/Hosts/index.js
index dfba785de5..39b2815130 100644
--- a/src/fireedge/src/client/components/Tables/Hosts/index.js
+++ b/src/fireedge/src/client/components/Tables/Hosts/index.js
@@ -54,7 +54,7 @@ const HostsTable = (props) => {
let values
- if (typeof filter === 'function') {
+ if (typeof filter === 'function' && dependOf) {
const { watch } = useFormContext()
const getDataForDepend = useCallback(
diff --git a/src/fireedge/src/client/components/Tables/VNetworks/index.js b/src/fireedge/src/client/components/Tables/VNetworks/index.js
index 382a731571..b992fd61df 100644
--- a/src/fireedge/src/client/components/Tables/VNetworks/index.js
+++ b/src/fireedge/src/client/components/Tables/VNetworks/index.js
@@ -54,7 +54,7 @@ const VNetworksTable = (props) => {
let values
- if (typeof filter === 'function') {
+ if (typeof filter === 'function' && dependOf) {
const { watch } = useFormContext()
const getDataForDepend = useCallback(
diff --git a/src/fireedge/src/client/components/Tabs/Cluster/Datastores/index.js b/src/fireedge/src/client/components/Tabs/Cluster/Datastores/index.js
new file mode 100644
index 0000000000..8fa792c0f3
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Cluster/Datastores/index.js
@@ -0,0 +1,77 @@
+/* ------------------------------------------------------------------------- *
+ * 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 { DatastoresTable } from 'client/components/Tables'
+import { useGetClusterQuery } from 'client/features/OneApi/cluster'
+import { useHistory, generatePath } from 'react-router-dom'
+import { PATH } from 'client/apps/sunstone/routesOne'
+const _ = require('lodash')
+
+/**
+ * Renders datastores tab showing the datastores of the cluster.
+ *
+ * @param {object} props - Props
+ * @param {string} props.id - Cluster id
+ * @returns {ReactElement} Datastores tab
+ */
+const Datastores = ({ id }) => {
+ // Get info about the cluster
+ const { data: cluster } = useGetClusterQuery({ id })
+
+ // Define function to get details of a datastore
+ const history = useHistory()
+ const handleRowClick = (rowId) => {
+ history.push(
+ generatePath(PATH.STORAGE.DATASTORES.DETAIL, { id: String(rowId) })
+ )
+ }
+
+ // Get datastores of the cluster
+ const datastores = _.isEmpty(cluster?.DATASTORES)
+ ? []
+ : Array.isArray(cluster?.DATASTORES?.ID)
+ ? cluster?.DATASTORES?.ID
+ : [cluster?.DATASTORES?.ID]
+
+ return (
+
+
+
+ dataToFilter.filter((ds) => _.includes(datastores, ds.ID))
+ }
+ onRowClick={(row) => handleRowClick(row.ID)}
+ />
+
+
+ )
+}
+
+Datastores.propTypes = {
+ id: PropTypes.string,
+}
+
+Datastores.displayName = 'Datastores'
+
+export default Datastores
diff --git a/src/fireedge/src/client/components/Tabs/Cluster/Hosts/index.js b/src/fireedge/src/client/components/Tabs/Cluster/Hosts/index.js
new file mode 100644
index 0000000000..77ea6e2f58
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Cluster/Hosts/index.js
@@ -0,0 +1,77 @@
+/* ------------------------------------------------------------------------- *
+ * 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 { HostsTable } from 'client/components/Tables'
+import { useGetClusterQuery } from 'client/features/OneApi/cluster'
+import { useHistory, generatePath } from 'react-router-dom'
+import { PATH } from 'client/apps/sunstone/routesOne'
+const _ = require('lodash')
+
+/**
+ * Renders hosts tab showing the hosts of the cluster.
+ *
+ * @param {object} props - Props
+ * @param {string} props.id - Cluster id
+ * @returns {ReactElement} Hosts tab
+ */
+const Hosts = ({ id }) => {
+ // Get info about the cluster
+ const { data: cluster } = useGetClusterQuery({ id })
+
+ // Define function to get details of a host
+ const history = useHistory()
+ const handleRowClick = (rowId) => {
+ history.push(
+ generatePath(PATH.INFRASTRUCTURE.HOSTS.DETAIL, { id: String(rowId) })
+ )
+ }
+
+ // Get hosts of the cluster
+ const hosts = _.isEmpty(cluster?.HOSTS)
+ ? []
+ : Array.isArray(cluster?.HOSTS?.ID)
+ ? cluster?.HOSTS?.ID
+ : [cluster?.HOSTS?.ID]
+
+ return (
+
+
+
+ dataToFilter.filter((host) => _.includes(hosts, host.ID))
+ }
+ onRowClick={(row) => handleRowClick(row.ID)}
+ />
+
+
+ )
+}
+
+Hosts.propTypes = {
+ id: PropTypes.string,
+}
+
+Hosts.displayName = 'Hosts'
+
+export default Hosts
diff --git a/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js b/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js
index b8a9bd69b9..f05ccc002c 100644
--- a/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js
+++ b/src/fireedge/src/client/components/Tabs/Cluster/Info/information.js
@@ -15,10 +15,13 @@
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
-
-import { useRenameClusterMutation } from 'client/features/OneApi/cluster'
+import {
+ useRenameClusterMutation,
+ useUpdateClusterMutation,
+} from 'client/features/OneApi/cluster'
import { List } from 'client/components/Tabs/Common'
import { T, Cluster, CLUSTER_ACTIONS } from 'client/constants'
+import { jsonToXml } from 'client/models/Helper'
/**
* Renders mainly information tab.
@@ -30,6 +33,7 @@ import { T, Cluster, CLUSTER_ACTIONS } from 'client/constants'
*/
const InformationPanel = ({ cluster = {}, actions }) => {
const [renameCluster] = useRenameClusterMutation()
+ const [updateCluster] = useUpdateClusterMutation()
const { ID, NAME, TEMPLATE } = cluster
const { RESERVED_MEM, RESERVED_CPU } = TEMPLATE
@@ -37,6 +41,7 @@ const InformationPanel = ({ cluster = {}, actions }) => {
await renameCluster({ id: ID, name: newName })
}
+ // Info section
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
@@ -48,9 +53,64 @@ const InformationPanel = ({ cluster = {}, actions }) => {
},
]
+ /**
+ * Update reserved CPU on the template cluster.
+ *
+ * @param {string} name - Name of the attribute
+ * @param {number} value - Value of the attribute
+ */
+ const handleOvercommitmentCPU = async (name, value) => {
+ const newTemplate = {
+ RESERVED_CPU: value + '%',
+ }
+
+ await updateCluster({
+ id: ID,
+ template: jsonToXml(newTemplate),
+ replace: 1,
+ })
+ }
+
+ /**
+ * Update reserved memory on the template cluster.
+ *
+ * @param {string} name - Name of the attribute
+ * @param {number} value - Value of the attribute
+ */
+ const handleOvercommitmentMemory = async (name, value) => {
+ const newTemplate = {
+ RESERVED_MEM: value + '%',
+ }
+
+ await updateCluster({
+ id: ID,
+ template: jsonToXml(newTemplate),
+ replace: 1,
+ })
+ }
+
+ // Overcommitment section
const overcommitment = [
- { name: T.ReservedMemory, value: RESERVED_MEM },
- { name: T.ReservedCpu, value: RESERVED_CPU },
+ {
+ name: T.ReservedCpu,
+ handleEdit: handleOvercommitmentCPU,
+ canEdit: true,
+ value: {RESERVED_CPU},
+ min: '-100',
+ max: '100',
+ currentValue: RESERVED_CPU?.replace(/%/g, ''),
+ dataCy: 'allocated-cpu',
+ },
+ {
+ name: T.ReservedMemory,
+ handleEdit: handleOvercommitmentMemory,
+ canEdit: true,
+ value: {RESERVED_MEM},
+ min: '-100',
+ max: '100',
+ currentValue: RESERVED_MEM?.replace(/%/g, ''),
+ dataCy: 'allocated-memory',
+ },
]
return (
diff --git a/src/fireedge/src/client/components/Tabs/Cluster/Vnets/index.js b/src/fireedge/src/client/components/Tabs/Cluster/Vnets/index.js
new file mode 100644
index 0000000000..fa25c558cd
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Cluster/Vnets/index.js
@@ -0,0 +1,75 @@
+/* ------------------------------------------------------------------------- *
+ * 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 { VNetworksTable } from 'client/components/Tables'
+import { useGetClusterQuery } from 'client/features/OneApi/cluster'
+import { useHistory, generatePath } from 'react-router-dom'
+import { PATH } from 'client/apps/sunstone/routesOne'
+const _ = require('lodash')
+
+/**
+ * Renders vnets tab showing the vnets of the cluster.
+ *
+ * @param {object} props - Props
+ * @param {string} props.id - Cluster id
+ * @returns {ReactElement} Vnets tab
+ */
+const Vnets = ({ id }) => {
+ // Get info about the cluster
+ const { data: cluster } = useGetClusterQuery({ id })
+
+ // Define function to get details of a vnet
+ const history = useHistory()
+ const handleRowClick = (rowId) => {
+ history.push(generatePath(PATH.NETWORK.VNETS.DETAIL, { id: String(rowId) }))
+ }
+
+ // Get vnets of the cluster
+ const vnets = _.isEmpty(cluster?.VNETS)
+ ? []
+ : Array.isArray(cluster?.VNETS?.ID)
+ ? cluster?.VNETS?.ID
+ : [cluster?.VNETS?.ID]
+
+ return (
+
+
+
+ dataToFilter.filter((vnet) => _.includes(vnets, vnet.ID))
+ }
+ onRowClick={(row) => handleRowClick(row.ID)}
+ />
+
+
+ )
+}
+
+Vnets.propTypes = {
+ id: PropTypes.string,
+}
+
+Vnets.displayName = 'Vnets'
+
+export default Vnets
diff --git a/src/fireedge/src/client/components/Tabs/Cluster/index.js b/src/fireedge/src/client/components/Tabs/Cluster/index.js
index c8af8ae8c7..225800dc80 100644
--- a/src/fireedge/src/client/components/Tabs/Cluster/index.js
+++ b/src/fireedge/src/client/components/Tabs/Cluster/index.js
@@ -24,10 +24,16 @@ import { getAvailableInfoTabs } from 'client/models/Helper'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/Cluster/Info'
+import Hosts from 'client/components/Tabs/Cluster/Hosts'
+import Vnets from 'client/components/Tabs/Cluster/Vnets'
+import Datastores from 'client/components/Tabs/Cluster/Datastores'
const getTabComponent = (tabName) =>
({
info: Info,
+ host: Hosts,
+ vnet: Vnets,
+ datastore: Datastores,
}[tabName])
const ClusterTabs = memo(({ id }) => {
diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js
index d6a5bb52d3..719dbc02c0 100644
--- a/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js
+++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js
@@ -150,8 +150,8 @@ const SliderInput = forwardRef(
value={newValue}
marks={[
{
- value: 0,
- label: unitParser ? prettyBytes(0) : '0',
+ value: min ?? 0,
+ label: unitParser ? prettyBytes(0) : min ?? '0',
},
{
value: max,
diff --git a/src/fireedge/src/client/components/Tabs/Host/Info/information.js b/src/fireedge/src/client/components/Tabs/Host/Info/information.js
index 670b363440..2a626546f8 100644
--- a/src/fireedge/src/client/components/Tabs/Host/Info/information.js
+++ b/src/fireedge/src/client/components/Tabs/Host/Info/information.js
@@ -73,22 +73,30 @@ const InformationPanel = ({ host = {}, actions }) => {
await renameHost({ id: ID, name: newName })
}
- const handleOvercommitment = async (name, value) => {
+ const handleOvercommitmentCPU = async (name, value) => {
let valueNumber = +value
- let newTemplate
- if (/cpu/i.test(name)) {
- valueNumber === 0 && (valueNumber = usageCpu)
- newTemplate = {
- RESERVED_CPU:
- value !== totalCpu ? totalCpu - valueNumber : reservedCpu ? 0 : '',
- }
+
+ valueNumber === 0 && (valueNumber = usageCpu)
+ const newTemplate = {
+ RESERVED_CPU:
+ value !== totalCpu ? totalCpu - valueNumber : reservedCpu ? 0 : '',
}
- if (/memory/i.test(name)) {
- valueNumber === 0 && (valueNumber = usageMem)
- newTemplate = {
- RESERVED_MEM:
- value !== totalMem ? totalMem - valueNumber : reservedMem ? 0 : '',
- }
+
+ newTemplate &&
+ (await updateHost({
+ id: ID,
+ template: jsonToXml(newTemplate),
+ replace: 1,
+ }))
+ }
+
+ const handleOvercommitmentMemory = async (name, value) => {
+ let valueNumber = +value
+
+ valueNumber === 0 && (valueNumber = usageMem)
+ const newTemplate = {
+ RESERVED_MEM:
+ value !== totalMem ? totalMem - valueNumber : reservedMem ? 0 : '',
}
newTemplate &&
@@ -129,7 +137,7 @@ const InformationPanel = ({ host = {}, actions }) => {
const capacity = [
{
name: T.AllocatedCpu,
- handleEdit: handleOvercommitment,
+ handleEdit: handleOvercommitmentCPU,
canEdit: true,
value: (
{
},
{
name: T.AllocatedMemory,
- handleEdit: handleOvercommitment,
+ handleEdit: handleOvercommitmentMemory,
canEdit: true,
value: (
{
+ try {
+ // Request to create a cluster but not to update
+ if (!clusterId) {
+ // Create cluster
+ const newClusterId = await createCluster({
+ name: general?.NAME,
+ }).unwrap()
+
+ // Add hosts
+ if (newClusterId && hosts?.ID) {
+ const hostIds = hosts?.ID?.map?.((host) => host)
+ await Promise.all(
+ hostIds.map((hostId) => addHost({ id: newClusterId, host: hostId }))
+ )
+ }
+
+ // Add vnets
+ if (newClusterId && vnets?.ID) {
+ const vnetIds = vnets?.ID?.map?.((vnet) => vnet)
+ await Promise.all(
+ vnetIds.map((vnetId) => addVnet({ id: newClusterId, vnet: vnetId }))
+ )
+ }
+
+ // Add datastores
+ if (newClusterId && datastores?.ID) {
+ const datastoreIds = datastores?.ID?.map?.((ds) => ds)
+ await Promise.all(
+ datastoreIds.map((dsId) =>
+ addDatastore({ id: newClusterId, datastore: dsId })
+ )
+ )
+ }
+
+ // Only show cluster message
+ enqueueSuccess(`Cluster created - #${newClusterId}`)
+
+ // Go to clusters list
+ history.push(PATH.INFRASTRUCTURE.CLUSTERS.LIST)
+ } else {
+ // Add hosts
+ if (addHosts?.length > 0) {
+ const hostIds = addHosts?.map?.((host) => host)
+ await Promise.all(
+ hostIds.map((hostId) => addHost({ id: clusterId, host: hostId }))
+ )
+ }
+
+ // Remove hosts
+ if (removeHosts?.length > 0) {
+ const hostIds = removeHosts?.map?.((host) => host)
+ await Promise.all(
+ hostIds.map((hostId) => removeHost({ id: clusterId, host: hostId }))
+ )
+ }
+
+ // Add vnets
+ if (addVnets?.length > 0) {
+ const vnetIds = addVnets?.map?.((vnet) => vnet)
+ await Promise.all(
+ vnetIds.map((vnetId) => addVnet({ id: clusterId, vnet: vnetId }))
+ )
+ }
+
+ // Remove vnets
+ if (removeVnets?.length > 0) {
+ const vnetIds = removeVnets?.map?.((vnet) => vnet)
+ await Promise.all(
+ vnetIds.map((vnetId) => removeVnet({ id: clusterId, vnet: vnetId }))
+ )
+ }
+
+ // Add datastores
+ if (addDatastores?.length > 0) {
+ const datastoreIds = addDatastores?.map?.((ds) => ds)
+ await Promise.all(
+ datastoreIds.map((dsId) =>
+ addDatastore({ id: clusterId, datastore: dsId })
+ )
+ )
+ }
+
+ // Remove datastores
+ if (removeDatastores?.length > 0) {
+ const datastoreIds = removeDatastores?.map?.((ds) => ds)
+ await Promise.all(
+ datastoreIds.map((dsId) =>
+ removeDatastore({ id: clusterId, datastore: dsId })
+ )
+ )
+ }
+
+ // Rename if the name has been changed
+ if (changeName) {
+ await rename({ id: clusterId, name: general?.NAME }).unwrap()
+ }
+
+ // Only show cluster message
+ enqueueSuccess(`Cluster updated - #${clusterId}`)
+
+ // Go to clusters list
+ history.push(PATH.INFRASTRUCTURE.CLUSTERS.LIST)
+ }
+ } catch (error) {
+ enqueueError('Error performing operation on cluster')
+ }
+ }
+
+ return views && version && (!clusterId || (clusterId && cluster)) ? (
+ }
+ >
+ {(config) => }
+
+ ) : (
+
+ )
+}
+
+export default CreateCluster
diff --git a/src/fireedge/src/client/containers/Clusters/index.js b/src/fireedge/src/client/containers/Clusters/index.js
index 165d909ac2..7718fcb112 100644
--- a/src/fireedge/src/client/containers/Clusters/index.js
+++ b/src/fireedge/src/client/containers/Clusters/index.js
@@ -32,6 +32,7 @@ import MultipleTags from 'client/components/MultipleTags'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { T, Cluster } from 'client/constants'
+import ClusterActions from 'client/components/Tables/Clusters/actions'
/**
* Displays a list of Clusters with a split pane between the list and selected row(s).
@@ -44,6 +45,8 @@ function Clusters() {
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
+ const actions = ClusterActions()
+
return (
{({ getGridProps, GutterComponent }) => (
@@ -51,6 +54,7 @@ function Clusters() {
{hasSelectedRows && (
diff --git a/src/fireedge/src/client/features/OneApi/cluster.js b/src/fireedge/src/client/features/OneApi/cluster.js
index 87085007aa..3d045d0d4a 100644
--- a/src/fireedge/src/client/features/OneApi/cluster.js
+++ b/src/fireedge/src/client/features/OneApi/cluster.js
@@ -25,6 +25,12 @@ import {
ONE_RESOURCES_POOL,
} from 'client/features/OneApi'
import { Cluster } from 'client/constants'
+import {
+ removeResourceOnPool,
+ updateNameOnResource,
+ updateResourceOnPool,
+ updateTemplateOnResource,
+} from 'client/features/OneApi/common'
const { CLUSTER, HOST, DATASTORE } = ONE_RESOURCES
const { CLUSTER_POOL, HOST_POOL, DATASTORE_POOL } = ONE_RESOURCES_POOL
@@ -78,19 +84,25 @@ const clusterApi = oneApi.injectEndpoints({
providesTags: (_, __, { id }) => [{ type: CLUSTER, id }],
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
- const { data: queryVm } = await queryFulfilled
+ const { data: resourceFromQuery } = await queryFulfilled
+ dispatch(
+ clusterApi.util.updateQueryData(
+ 'getGClusters',
+ undefined,
+ updateResourceOnPool({ id, resourceFromQuery })
+ )
+ )
+ } catch {
+ // if the query fails, we want to remove the resource from the pool
dispatch(
clusterApi.util.updateQueryData(
'getClusters',
undefined,
- (draft) => {
- const index = draft.findIndex(({ ID }) => +ID === +id)
- index !== -1 && (draft[index] = queryVm)
- }
+ removeResourceOnPool({ id })
)
)
- } catch {}
+ }
},
}),
getClusterAdmin: builder.query({
@@ -132,17 +144,41 @@ const clusterApi = oneApi.injectEndpoints({
/**
* Deletes the given cluster from the pool.
*
- * @param {number|string} id - Cluster id
+ * @param {number|string} params - Cluster id
* @returns {number} Cluster id
* @throws Fails when response isn't code 200
*/
- query: (id) => {
+ query: (params) => {
const name = Actions.CLUSTER_DELETE
const command = { name, ...Commands[name] }
- return { params: { id }, command }
+ return { params, command }
},
invalidatesTags: [CLUSTER_POOL],
+ async onQueryStarted(params, { dispatch, queryFulfilled }) {
+ try {
+ const patchCluster = dispatch(
+ clusterApi.util.updateQueryData(
+ 'getCluster',
+ { id: params.id },
+ updateNameOnResource(params)
+ )
+ )
+
+ const patchClusters = dispatch(
+ clusterApi.util.updateQueryData(
+ 'getClusters',
+ undefined,
+ updateNameOnResource(params)
+ )
+ )
+
+ queryFulfilled.catch(() => {
+ patchCluster.undo()
+ patchClusters.undo()
+ })
+ } catch {}
+ },
}),
updateCluster: builder.mutation({
/**
@@ -165,6 +201,30 @@ const clusterApi = oneApi.injectEndpoints({
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: CLUSTER, id }],
+ async onQueryStarted(params, { dispatch, queryFulfilled }) {
+ try {
+ const patchCluster = dispatch(
+ clusterApi.util.updateQueryData(
+ 'getCluster',
+ { id: params.id },
+ updateTemplateOnResource(params)
+ )
+ )
+
+ const patchClusters = dispatch(
+ clusterApi.util.updateQueryData(
+ 'getClusters',
+ undefined,
+ updateTemplateOnResource(params)
+ )
+ )
+
+ queryFulfilled.catch(() => {
+ patchCluster.undo()
+ patchClusters.undo()
+ })
+ } catch {}
+ },
}),
addHostToCluster: builder.mutation({
/**
diff --git a/src/fireedge/src/server/utils/constants/commands/cluster.js b/src/fireedge/src/server/utils/constants/commands/cluster.js
index d1af9ab357..696e385931 100644
--- a/src/fireedge/src/server/utils/constants/commands/cluster.js
+++ b/src/fireedge/src/server/utils/constants/commands/cluster.js
@@ -110,7 +110,7 @@ module.exports = {
default: 0,
},
host: {
- from: query,
+ from: postBody,
default: 0,
},
},