mirror of
https://github.com/OpenNebula/one.git
synced 2025-01-26 10:03:37 +03:00
F OpenNebula/one#6119: Add Cluster tab (#2885)
This commit is contained in:
parent
592a2a64f5
commit
863ed452f7
@ -2986,11 +2986,12 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/backup-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/datastore-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vdc-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/user-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/user-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/backupjobs-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/host-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/group-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/acl-tab.yaml"
|
||||
src/fireedge/etc/sunstone/admin/acl-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/cluster-tab.yaml"
|
||||
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \
|
||||
src/fireedge/etc/sunstone/user/vm-template-tab.yaml \
|
||||
|
@ -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}`,
|
||||
|
@ -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) => (
|
||||
<Grid mt={2} container>
|
||||
<Grid item xs={8}>
|
||||
<FormWithSchema id={STEP_ID} cy={`${STEP_ID}`} fields={FIELDS} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
minHeight: '630px',
|
||||
maxHeight: '630px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
marginLeft: '1em',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" gutterBottom>
|
||||
{Tr(T['cluster.form.create.datastores.help.title'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.datastores.help.paragraph.1'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.datastores.help.paragraph.2'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.datastores.help.paragraph.3'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href={generateDocLink(
|
||||
version,
|
||||
'management_and_operations/host_cluster_management/cluster_guide.html'
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{Tr(T['cluster.form.create.help.link'])}
|
||||
</a>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
@ -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 }
|
@ -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) => (
|
||||
<Grid mt={2} container>
|
||||
<Grid item xs={8}>
|
||||
<FormWithSchema id={STEP_ID} cy={`${STEP_ID}`} fields={FIELDS} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
minHeight: '630px',
|
||||
maxHeight: '630px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
marginLeft: '1em',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" gutterBottom>
|
||||
{' '}
|
||||
{Tr(T['cluster.form.create.general.help.title'])}{' '}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.general.help.paragraph.1.1'])}{' '}
|
||||
</Typography>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.general.help.paragraph.1.2'])}{' '}
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.general.help.paragraph.1.3'])}{' '}
|
||||
</Typography>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.general.help.paragraph.2'])}{' '}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href={generateDocLink(
|
||||
version,
|
||||
'management_and_operations/host_cluster_management/cluster_guide.html'
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{Tr(T['cluster.form.create.help.link'])}
|
||||
</a>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
@ -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 }
|
@ -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) => (
|
||||
<Grid mt={2} container>
|
||||
<Grid item xs={8}>
|
||||
<FormWithSchema id={STEP_ID} cy={`${STEP_ID}`} fields={FIELDS} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
minHeight: '630px',
|
||||
maxHeight: '630px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
marginLeft: '1em',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" gutterBottom>
|
||||
{' '}
|
||||
{Tr(T['cluster.form.create.hosts.help.title'])}{' '}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{' '}
|
||||
{Tr(T['cluster.form.create.hosts.help.paragraph.1'])}{' '}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{' '}
|
||||
{Tr(T['cluster.form.create.hosts.help.paragraph.2'])}{' '}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href={generateDocLink(
|
||||
version,
|
||||
'management_and_operations/host_cluster_management/cluster_guide.html'
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{Tr(T['cluster.form.create.help.link'])}
|
||||
</a>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
@ -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 }
|
@ -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) => (
|
||||
<Grid mt={2} container>
|
||||
<Grid item xs={8}>
|
||||
<FormWithSchema id={STEP_ID} cy={`${STEP_ID}`} fields={FIELDS} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
elevation={2}
|
||||
sx={{
|
||||
height: '100%',
|
||||
minHeight: '630px',
|
||||
maxHeight: '630px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
marginLeft: '1em',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1em',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" gutterBottom>
|
||||
{Tr(T['cluster.form.create.vnets.help.title'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.vnets.help.paragraph.1'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{Tr(T['cluster.form.create.vnets.help.paragraph.2'])}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href={generateDocLink(
|
||||
version,
|
||||
'management_and_operations/host_cluster_management/cluster_guide.html'
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{Tr(T['cluster.form.create.help.link'])}
|
||||
</a>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
@ -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 }
|
@ -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
|
@ -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'
|
@ -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 }
|
||||
|
121
src/fireedge/src/client/components/Tables/Clusters/actions.js
Normal file
121
src/fireedge/src/client/components/Tables/Clusters/actions.js
Normal file
@ -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 (
|
||||
<Typography
|
||||
key={`cluster-${id}`}
|
||||
variant="inherit"
|
||||
component="span"
|
||||
display="block"
|
||||
>
|
||||
{`#${ID} ${NAME}`}
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
|
||||
const MessageToConfirmAction = (rows, description) => (
|
||||
<>
|
||||
<ListClusterNames rows={rows} />
|
||||
{description && <Translate word={description} />}
|
||||
<Translate word={T.DoYouWantProceed} />
|
||||
</>
|
||||
)
|
||||
|
||||
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
|
@ -29,23 +29,29 @@ const Row = ({ original, value, ...props }) => {
|
||||
<div data-cy={`cluster-${ID}`} {...props}>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography noWrap component="span">
|
||||
<Typography noWrap component="span" data-cy="cluster-card-name">
|
||||
{NAME}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span>{`#${ID}`}</span>
|
||||
<span title={`Total Hosts: ${HOSTS}`}>
|
||||
<span data-cy="cluster-card-id">{`#${ID}`}</span>
|
||||
<span data-cy="cluster-card-hosts" title={`Total Hosts: ${HOSTS}`}>
|
||||
<HardDrive />
|
||||
<span>{` ${HOSTS}`}</span>
|
||||
<span>{`${HOSTS}`}</span>
|
||||
</span>
|
||||
<span title={`Total Datastores: ${DATASTORES}`}>
|
||||
<Folder />
|
||||
<span>{` ${DATASTORES}`}</span>
|
||||
</span>
|
||||
<span title={`Total Virtual Networks: ${VNETS}`}>
|
||||
<span
|
||||
data-cy="cluster-card-vnets"
|
||||
title={`Total Virtual Networks: ${VNETS}`}
|
||||
>
|
||||
<NetworkAlt />
|
||||
<span>{` ${VNETS}`}</span>
|
||||
<span>{`${VNETS}`}</span>
|
||||
</span>
|
||||
<span
|
||||
data-cy="cluster-card-datastores"
|
||||
title={`Total Datastores: ${DATASTORES}`}
|
||||
>
|
||||
<Folder />
|
||||
<span>{`${DATASTORES}`}</span>
|
||||
</span>
|
||||
{PROVIDER_NAME && (
|
||||
<span title={`Provider: ${PROVIDER_NAME}`}>
|
||||
|
@ -54,7 +54,7 @@ const DatastoresTable = (props) => {
|
||||
|
||||
let values
|
||||
|
||||
if (typeof filter === 'function') {
|
||||
if (typeof filter === 'function' && dependOf) {
|
||||
const { watch } = useFormContext()
|
||||
|
||||
const getDataForDepend = useCallback(
|
||||
|
@ -54,7 +54,7 @@ const HostsTable = (props) => {
|
||||
|
||||
let values
|
||||
|
||||
if (typeof filter === 'function') {
|
||||
if (typeof filter === 'function' && dependOf) {
|
||||
const { watch } = useFormContext()
|
||||
|
||||
const getDataForDepend = useCallback(
|
||||
|
@ -54,7 +54,7 @@ const VNetworksTable = (props) => {
|
||||
|
||||
let values
|
||||
|
||||
if (typeof filter === 'function') {
|
||||
if (typeof filter === 'function' && dependOf) {
|
||||
const { watch } = useFormContext()
|
||||
|
||||
const getDataForDepend = useCallback(
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<DatastoresTable
|
||||
disableRowSelect
|
||||
filter={(dataToFilter) =>
|
||||
dataToFilter.filter((ds) => _.includes(datastores, ds.ID))
|
||||
}
|
||||
onRowClick={(row) => handleRowClick(row.ID)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Datastores.propTypes = {
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
Datastores.displayName = 'Datastores'
|
||||
|
||||
export default Datastores
|
@ -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 (
|
||||
<div>
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<HostsTable
|
||||
disableRowSelect
|
||||
filter={(dataToFilter) =>
|
||||
dataToFilter.filter((host) => _.includes(hosts, host.ID))
|
||||
}
|
||||
onRowClick={(row) => handleRowClick(row.ID)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Hosts.propTypes = {
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
Hosts.displayName = 'Hosts'
|
||||
|
||||
export default Hosts
|
@ -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: <span>{RESERVED_CPU}</span>,
|
||||
min: '-100',
|
||||
max: '100',
|
||||
currentValue: RESERVED_CPU?.replace(/%/g, ''),
|
||||
dataCy: 'allocated-cpu',
|
||||
},
|
||||
{
|
||||
name: T.ReservedMemory,
|
||||
handleEdit: handleOvercommitmentMemory,
|
||||
canEdit: true,
|
||||
value: <span>{RESERVED_MEM}</span>,
|
||||
min: '-100',
|
||||
max: '100',
|
||||
currentValue: RESERVED_MEM?.replace(/%/g, ''),
|
||||
dataCy: 'allocated-memory',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<Stack
|
||||
display="grid"
|
||||
gap="1em"
|
||||
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
|
||||
padding={{ sm: '0.8em' }}
|
||||
>
|
||||
<VNetworksTable
|
||||
disableRowSelect
|
||||
filter={(dataToFilter) =>
|
||||
dataToFilter.filter((vnet) => _.includes(vnets, vnet.ID))
|
||||
}
|
||||
onRowClick={(row) => handleRowClick(row.ID)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Vnets.propTypes = {
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
Vnets.displayName = 'Vnets'
|
||||
|
||||
export default Vnets
|
@ -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 }) => {
|
||||
|
@ -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,
|
||||
|
@ -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: (
|
||||
<LinearProgressWithLabel
|
||||
@ -147,7 +155,7 @@ const InformationPanel = ({ host = {}, actions }) => {
|
||||
},
|
||||
{
|
||||
name: T.AllocatedMemory,
|
||||
handleEdit: handleOvercommitment,
|
||||
handleEdit: handleOvercommitmentMemory,
|
||||
canEdit: true,
|
||||
value: (
|
||||
<LinearProgressWithLabel
|
||||
|
@ -30,6 +30,7 @@ import * as ACTIONS from 'client/constants/actions'
|
||||
/** @enum {string} Cluster actions */
|
||||
export const CLUSTER_ACTIONS = {
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
UPDATE_DIALOG: 'update_dialog',
|
||||
DELETE: 'delete',
|
||||
|
||||
RENAME: ACTIONS.RENAME,
|
||||
|
@ -167,7 +167,7 @@ module.exports = {
|
||||
SelectClusters: 'Select Clusters',
|
||||
SelectDatastore: 'Select a Datastore to store the resource',
|
||||
SelectDatastoreImage: 'Select a Datastore',
|
||||
SelectDatastores: 'Select Datastores',
|
||||
SelectDatastores: 'Select datastores',
|
||||
SelectDockerHubTag: 'Select DockerHub image tag (default latest)',
|
||||
SelectGroup: 'Select a group',
|
||||
SelectHost: 'Select a host',
|
||||
@ -392,6 +392,34 @@ module.exports = {
|
||||
Infrastructure: 'Infrastructure',
|
||||
Zone: 'Zone',
|
||||
Zones: 'Zones',
|
||||
'cluster.form.create.general.help.title': 'Cluster',
|
||||
'cluster.form.create.general.help.paragraph.1.1':
|
||||
'Clusters group together hosts, datastores and virtual networks that are configured to work together. A cluster is used to:',
|
||||
'cluster.form.create.general.help.paragraph.1.2':
|
||||
'Ensure that VMs use resources that are compatible.',
|
||||
'cluster.form.create.general.help.paragraph.1.3':
|
||||
'Assign resources to user groups by creating Virtual Private Clouds.',
|
||||
'cluster.form.create.general.help.paragraph.2':
|
||||
'Please, select a name for the cluster',
|
||||
'cluster.form.create.help.link':
|
||||
'See Open Nebula documentation to get more details about clusters.',
|
||||
'cluster.form.create.hosts.help.title': 'Hosts',
|
||||
'cluster.form.create.hosts.help.paragraph.1':
|
||||
'Please, select one or more hosts in the hosts table. Hosts are not mandatory, so you can skip this step.',
|
||||
'cluster.form.create.hosts.help.paragraph.2':
|
||||
'Remember that hosts can be in only one cluster at a time so if a host it is added to this cluster, it will be removed from any other cluster.',
|
||||
'cluster.form.create.vnets.help.title': 'Virtual Networks',
|
||||
'cluster.form.create.vnets.help.paragraph.1':
|
||||
'Please, select one or more virtual networks in the virtual networks table. Virtual networks are not mandatory, so you can skip this step.',
|
||||
'cluster.form.create.vnets.help.paragraph.2':
|
||||
'Virtual networks can be added to multiple clusters. This means that any host in those clusters is properly configured to use leases from those virtual networks.',
|
||||
'cluster.form.create.datastores.help.title': 'Datastores',
|
||||
'cluster.form.create.datastores.help.paragraph.1':
|
||||
'Please, select one or more datastores in the datastores table. Datastores are not mandatory, so you can skip this step.',
|
||||
'cluster.form.create.datastores.help.paragraph.2':
|
||||
'Datastores can be added to multiple clusters. This means that any host in those clusters is properly configured to run VMs using images from those datastores.',
|
||||
'cluster.form.create.datastores.help.paragraph.3':
|
||||
'Remember that in order to create a complete environment where the scheduler can deploy VMs, your clusters need to have at least one System Datastore.',
|
||||
|
||||
/* sections - network */
|
||||
Network: 'Network',
|
||||
@ -1327,6 +1355,10 @@ module.exports = {
|
||||
'Comma separated list of CPU IDs that will be isolated from the NUMA scheduler',
|
||||
|
||||
/* Cluster schema */
|
||||
CreateCluster: 'Create Cluster',
|
||||
'cluster.create.name': 'Name',
|
||||
'cluster.create.general.info': 'Cluster info',
|
||||
|
||||
/* Cluster schema - capacity */
|
||||
ReservedMemory: 'Allocated Memory',
|
||||
ReservedCpu: 'Allocated CPU',
|
||||
|
205
src/fireedge/src/client/containers/Clusters/Create.js
Normal file
205
src/fireedge/src/client/containers/Clusters/Create.js
Normal file
@ -0,0 +1,205 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useHistory, useLocation } from 'react-router'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import {
|
||||
useAllocateClusterMutation,
|
||||
useGetClusterQuery,
|
||||
useAddHostToClusterMutation,
|
||||
useAddDatastoreToClusterMutation,
|
||||
useAddNetworkToClusterMutation,
|
||||
useRemoveHostFromClusterMutation,
|
||||
useRemoveDatastoreFromClusterMutation,
|
||||
useRemoveNetworkFromClusterMutation,
|
||||
useRenameClusterMutation,
|
||||
} from 'client/features/OneApi/cluster'
|
||||
|
||||
import {
|
||||
DefaultFormStepper,
|
||||
SkeletonStepsForm,
|
||||
} from 'client/components/FormStepper'
|
||||
import { CreateForm } from 'client/components/Forms/Cluster'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
import systemApi from 'client/features/OneApi/system'
|
||||
|
||||
/**
|
||||
* Displays the creation form for a cluster.
|
||||
*
|
||||
* @returns {ReactElement} - The cluster form component
|
||||
*/
|
||||
function CreateCluster() {
|
||||
const history = useHistory()
|
||||
const { state: { ID: clusterId } = {} } = useLocation()
|
||||
|
||||
const { enqueueSuccess, enqueueError } = useGeneralApi()
|
||||
const [createCluster] = useAllocateClusterMutation()
|
||||
const [addHost] = useAddHostToClusterMutation()
|
||||
const [addDatastore] = useAddDatastoreToClusterMutation()
|
||||
const [addVnet] = useAddNetworkToClusterMutation()
|
||||
const [removeHost] = useRemoveHostFromClusterMutation()
|
||||
const [removeDatastore] = useRemoveDatastoreFromClusterMutation()
|
||||
const [removeVnet] = useRemoveNetworkFromClusterMutation()
|
||||
const [rename] = useRenameClusterMutation()
|
||||
|
||||
const { data: views } = systemApi.useGetSunstoneAvalaibleViewsQuery()
|
||||
const { data: version } = systemApi.useGetOneVersionQuery()
|
||||
|
||||
const { data: cluster } = clusterId
|
||||
? useGetClusterQuery({ id: clusterId })
|
||||
: { data: undefined }
|
||||
|
||||
const onSubmit = async ({
|
||||
general,
|
||||
hosts,
|
||||
addHosts,
|
||||
removeHosts,
|
||||
vnets,
|
||||
addVnets,
|
||||
removeVnets,
|
||||
datastores,
|
||||
addDatastores,
|
||||
removeDatastores,
|
||||
changeName,
|
||||
}) => {
|
||||
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)) ? (
|
||||
<CreateForm
|
||||
initialValues={cluster}
|
||||
onSubmit={onSubmit}
|
||||
stepProps={{
|
||||
views,
|
||||
version,
|
||||
}}
|
||||
fallback={<SkeletonStepsForm />}
|
||||
>
|
||||
{(config) => <DefaultFormStepper {...config} />}
|
||||
</CreateForm>
|
||||
) : (
|
||||
<SkeletonStepsForm />
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateCluster
|
@ -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 (
|
||||
<SplitPane gridTemplateRows="1fr auto 1fr">
|
||||
{({ getGridProps, GutterComponent }) => (
|
||||
@ -51,6 +54,7 @@ function Clusters() {
|
||||
<ClustersTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
useUpdateMutation={useUpdateClusterMutation}
|
||||
globalActions={actions}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
|
@ -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({
|
||||
/**
|
||||
|
@ -110,7 +110,7 @@ module.exports = {
|
||||
default: 0,
|
||||
},
|
||||
host: {
|
||||
from: query,
|
||||
from: postBody,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user