1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-26 10:03:37 +03:00

F #3951: Adjust provision templates & GUI forms (#822)

This commit is contained in:
Sergio Betanzos 2021-02-17 12:24:15 +01:00 committed by GitHub
parent 72ae53d73f
commit 50622a8bf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 732 additions and 516 deletions

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in Frankfurt'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in London'
provider: 'aws'
plain:
provision_type: 'hybrid+_qemu'
image: 'AWS.webp'
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in North Virginia'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in North California'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on Packet in Amsterdam'
provider: 'packet'
plain:
image: 'EQUINIX'
location_key: 'facility'
provision_type: 'hybrid+'
image: 'EQUINIX.webp'
location_key: 'facility'
connection:
token: 'Packet token'
project: 'Packet project'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on Packet in Parsippany, USA, NJ'
provider: 'packet'
plain:
image: 'EQUINIX'
location_key: 'facility'
provision_type: 'hybrid+'
image: 'EQUINIX.webp'
location_key: 'facility'
connection:
token: 'Packet token'
project: 'Packet project'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on Packet in Tokyo, Japan'
provider: 'packet'
plain:
image: 'EQUINIX'
location_key: 'facility'
provision_type: 'hybrid+'
image: 'EQUINIX.webp'
location_key: 'facility'
connection:
token: 'Packet token'
project: 'Packet project'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on Packet in Sunnyvale, USA, CA'
provider: 'packet'
plain:
image: 'EQUINIX'
location_key: 'facility'
provision_type: 'hybrid+'
image: 'EQUINIX.webp'
location_key: 'facility'
connection:
token: 'Packet token'
project: 'Packet project'

View File

@ -0,0 +1,20 @@
---
# ---------------------------------------------------------------------------- #
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# ---------------------------------------------------------------------------- #
image: 'OPENNEBULA-AWS'
provider: 'aws'
provision_type: 'hybrid+'

View File

@ -21,10 +21,6 @@
# ------------------------------------------------------------------------------
name: 'aws-cluster'
provision_type: 'hybrid+'
provider: 'aws'
image: 'OPENNEBULA-AWS.png'
extends:
- common.d/defaults.yml
@ -32,6 +28,7 @@ extends:
- common.d/kvm_hosts.yml
- aws.d/datastores.yml
- aws.d/defaults.yml
- aws.d/fireedge.yml
- aws.d/inputs.yml
- aws.d/networks.yml

View File

@ -0,0 +1,20 @@
---
# ---------------------------------------------------------------------------- #
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# ---------------------------------------------------------------------------- #
image: 'OPENNEBULA-EQUINIX'
provider: 'packet'
provision_type: 'hybrid+'

View File

@ -21,17 +21,14 @@
# ------------------------------------------------------------------------------
name: 'packet-cluster'
provision_type: 'hybrid+'
provider: 'packet'
image: 'OPENNEBULA-EQUINIX.png'
extends:
- common.d/defaults.yml
- common.d/resources.yml
- common.d/kvm_hosts.yml
- packet.d/defaults.yml
- packet.d/datastores.yml
- packet.d/defaults.yml
- packet.d/fireedge.yml
- packet.d/inputs.yml
- packet.d/networks.yml

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in Frankfurt'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_firecracker'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in London'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_firecracker'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in North Virginia'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_firecracker'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in North California'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_firecracker'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -0,0 +1,20 @@
---
# ---------------------------------------------------------------------------- #
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# ---------------------------------------------------------------------------- #
image: 'OPENNEBULA-AWS'
provider: 'aws'
provision_type: 'hybrid+_firecracker'

View File

@ -21,10 +21,6 @@
# ------------------------------------------------------------------------------
name: 'aws-cluster'
provision_type: 'hybrid+_firecracker'
provider: 'aws'
image: 'OPENNEBULA-AWS.png'
extends:
- common.d/defaults.yml
@ -32,6 +28,7 @@ extends:
- common.d/firecracker_hosts.yml
- aws.d/datastores.yml
- aws.d/defaults.yml
- aws.d/fireedge.yml
- aws.d/inputs.yml
- aws.d/networks.yml

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in Frankfurt'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_qemu'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in London'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_qemu'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in North Virginia'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_qemu'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -4,10 +4,10 @@ description: 'Elastic cluster on AWS in North California'
provider: 'aws'
plain:
image: 'AWS'
location_key: 'region'
provision_type: 'hybrid+_qemu'
image: 'AWS.webp'
location_key: 'region'
connection:
access_key: 'AWS access key'
secret_key: 'AWS secret key'

View File

@ -0,0 +1,20 @@
---
# ---------------------------------------------------------------------------- #
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# ---------------------------------------------------------------------------- #
image: 'OPENNEBULA-AWS'
provider: 'aws'
provision_type: 'hybrid+_qemu'

View File

@ -21,10 +21,6 @@
# ------------------------------------------------------------------------------
name: 'aws-cluster'
provision_type: 'hybrid+_qemu'
provider: 'aws'
image: 'OPENNEBULA-AWS.png'
extends:
- common.d/defaults.yml
@ -32,6 +28,7 @@ extends:
- common.d/qemu_hosts.yml
- aws.d/datastores.yml
- aws.d/defaults.yml
- aws.d/fireedge.yml
- aws.d/inputs.yml
- aws.d/networks.yml

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -8,11 +8,13 @@ import SelectCard from 'client/components/Cards/SelectCard'
import Action from 'client/components/Cards/SelectCard/Action'
import { StatusBadge } from 'client/components/Status'
import { isExternalURL } from 'client/utils'
import * as Types from 'client/types/provision'
import {
PROVISIONS_STATES,
PROVIDER_IMAGES_URL,
PROVISION_IMAGES_URL,
DEFAULT_IMAGE
DEFAULT_IMAGE,
IMAGE_FORMATS
} from 'client/constants'
const ProvisionCard = memo(
@ -28,23 +30,42 @@ const ProvisionCard = memo(
setBody({ ...json, image: json.image ?? DEFAULT_IMAGE })
}, [])
const onError = evt => {
evt.target.src = evt.target.src === DEFAULT_IMAGE ? DEFAULT_IMAGE : ''
}
const isExternalImage = isExternalURL(image)
const mediaProps = useMemo(() => {
const src = isExternalImage ? image : `${IMAGES_URL}/${image}`
const onError = evt => { evt.target.src = DEFAULT_IMAGE }
return {
component: 'picture',
children: (
<>
{(image && !isExternalImage) && IMAGE_FORMATS.map(format => (
<source
key={format}
srcSet={`${src}.${format}`}
type={`image/${format}`}
/>
))}
<img
decoding='async'
draggable={false}
loading='lazy'
src={src}
onError={onError}
/>
</>
)
}
}, [image, isExternalImage])
const imgSource = useMemo(() => (
isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
), [image])
const dataCy = isProvider ? 'provider' : 'provision'
return (
<SelectCard
title={NAME}
subheader={`#${ID}`}
isSelected={isSelected}
handleClick={handleClick}
action={actions?.map(action =>
<Action key={action?.cy} {...action} />
)}
dataCy={isProvider ? 'provider' : 'provision'}
handleClick={handleClick}
icon={
isProvider ? (
<ProviderIcon />
@ -54,14 +75,11 @@ const ProvisionCard = memo(
</StatusBadge>
)
}
cardProps={{ 'data-cy': `${dataCy}-card` }}
cardHeaderProps={{ titleTypographyProps: { 'data-cy': `${dataCy}-card-title` } }}
mediaProps={{
component: 'img',
image: imgSource,
draggable: false,
onError
}}
isSelected={isSelected}
mediaProps={mediaProps}
subheader={`#${ID}`}
title={NAME}
disableFilterImage={isExternalImage}
/>
)
}, (prev, next) => (
@ -73,17 +91,10 @@ const ProvisionCard = memo(
)
ProvisionCard.propTypes = {
value: PropTypes.shape({
ID: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TEMPLATE: PropTypes.shape({
PLAIN: PropTypes.object,
BODY: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
])
})
}),
value: PropTypes.oneOfType([
Types.Provider,
Types.Provision
]),
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
isProvider: PropTypes.bool,
@ -97,11 +108,11 @@ ProvisionCard.propTypes = {
}
ProvisionCard.defaultProps = {
value: {},
isSelected: undefined,
actions: undefined,
handleClick: undefined,
isProvider: false,
actions: undefined
isSelected: undefined,
value: {}
}
ProvisionCard.displayName = 'ProvisionCard'

View File

@ -1,6 +1,8 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import * as Types from 'client/types/provision'
import ProvidersIcon from '@material-ui/icons/Public'
import SelectCard from 'client/components/Cards/SelectCard'
@ -8,38 +10,56 @@ import { isExternalURL } from 'client/utils'
import {
PROVIDER_IMAGES_URL,
PROVISION_IMAGES_URL,
DEFAULT_IMAGE
DEFAULT_IMAGE,
IMAGE_FORMATS
} from 'client/constants'
const ProvisionTemplateCard = React.memo(
({ value, isProvider, isSelected, handleClick }) => {
({ value, isProvider, isSelected, isValid, handleClick }) => {
const { description, name, plain = {} } = value
const { image } = isProvider ? plain : value
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const { image = '' } = isProvider ? plain : value
const onError = evt => {
evt.target.src = evt.target.src === DEFAULT_IMAGE ? DEFAULT_IMAGE : ''
}
const isExternalImage = isExternalURL(image)
const mediaProps = React.useMemo(() => {
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const src = isExternalImage ? image : `${IMAGES_URL}/${image}`
const onError = evt => { evt.target.src = DEFAULT_IMAGE }
return {
component: 'picture',
children: (
<>
{(image && !isExternalImage) && IMAGE_FORMATS.map(format => (
<source
key={format}
srcSet={`${IMAGES_URL}/${image}.${format}`}
type={`image/${format}`}
/>
))}
<img
decoding='async'
draggable={false}
loading='lazy'
src={src}
onError={onError}
/>
</>
)
}
}, [image, isProvider])
const imgSource = React.useMemo(() =>
isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
, [image])
const dataCy = isProvider ? 'provider' : 'provision'
return (
<SelectCard
title={name}
subheader={description}
icon={<ProvidersIcon />}
isSelected={isSelected}
dataCy={isProvider ? 'provider' : 'provision'}
disableFilterImage={isExternalImage}
handleClick={handleClick}
mediaProps={image && {
component: 'img',
image: imgSource,
draggable: false,
onError
}}
cardProps={{ 'data-cy': `${dataCy}-template-card` }}
cardHeaderProps={{ titleTypographyProps: { 'data-cy': `${dataCy}-template-card-title` } }}
icon={<ProvidersIcon />}
cardActionAreaProps={{ disabled: !isValid }}
isSelected={isSelected}
mediaProps={mediaProps}
subheader={description}
title={name}
/>
)
},
@ -47,23 +67,22 @@ const ProvisionTemplateCard = React.memo(
)
ProvisionTemplateCard.propTypes = {
value: PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string,
plain: PropTypes.shape({
image: PropTypes.string
})
}),
handleClick: PropTypes.func,
isProvider: PropTypes.bool,
isSelected: PropTypes.bool,
handleClick: PropTypes.func
isValid: PropTypes.bool,
value: PropTypes.oneOfType([
Types.ProviderTemplate,
Types.ProvisionTemplate
])
}
ProvisionTemplateCard.defaultProps = {
value: { name: '', description: '' },
handleClick: undefined,
isProvider: undefined,
isSelected: false,
handleClick: undefined
isValid: true,
value: { name: '', description: '' }
}
ProvisionTemplateCard.displayName = 'ProvisionTemplateCard'

View File

@ -13,22 +13,25 @@ import Action from 'client/components/Cards/SelectCard/Action'
import selectCardStyles from 'client/components/Cards/SelectCard/styles'
const SelectCard = memo(({
stylesProps,
action,
actions,
cardActionsProps,
icon,
title,
subheader,
cardHeaderProps,
mediaProps,
isSelected,
handleClick,
cardProps,
cardActionAreaProps,
children,
dataCy,
disableFilterImage,
handleClick,
icon,
isSelected,
mediaProps,
observerOff,
children
stylesProps,
subheader,
title
}) => {
const classes = selectCardStyles({ ...stylesProps, isSelected })
const classes = selectCardStyles({ ...stylesProps, isSelected, disableFilterImage })
const { isNearScreen, fromRef } = useNearScreen({
distance: '100px'
})
@ -39,17 +42,22 @@ const SelectCard = memo(({
wrap={children => <span ref={fromRef}>{children}</span>}>
{observerOff || isNearScreen ? (
<Card
{...cardProps}
className={clsx(classes.root, cardProps?.className, {
[classes.actionArea]: !handleClick
})}
{...cardProps}
data-cy={dataCy ? `${dataCy}-card` : undefined}
>
{/* CARD ACTION AREA */}
<ConditionalWrap
condition={handleClick && !action}
wrap={children =>
<CardActionArea className={classes.actionArea} onClick={handleClick}>
<CardActionArea
{...cardActionAreaProps}
className={clsx(classes.actionArea, cardActionAreaProps?.className)}
onClick={handleClick}
>
{children}
</CardActionArea>
}
@ -57,6 +65,7 @@ const SelectCard = memo(({
{/* CARD HEADER */}
{(title || subheader || icon || action) && (
<CardHeader
{...cardHeaderProps}
action={action}
avatar={icon}
classes={{
@ -69,14 +78,16 @@ const SelectCard = memo(({
variant: 'body1',
noWrap: true,
className: classes.header,
title: typeof title === 'string' ? title : undefined
title: typeof title === 'string' ? title : undefined,
...(dataCy) && { 'data-cy': `${dataCy}-card-title` }
}}
subheader={subheader}
subheaderTypographyProps={{
variant: 'body2',
noWrap: true,
className: classes.subheader,
title: typeof subheader === 'string' ? subheader : undefined
title: typeof subheader === 'string' ? subheader : undefined,
...(dataCy) && { 'data-cy': `${dataCy}-card-subheader` }
}}
{...cardHeaderProps}
/>
@ -161,29 +172,35 @@ SelectCard.propTypes = {
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
cardProps: PropTypes.object,
cardActionAreaProps: PropTypes.object,
observerOff: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.string
])
]),
dataCy: PropTypes.string,
disableFilterImage: PropTypes.bool
}
SelectCard.defaultProps = {
stylesProps: undefined,
action: undefined,
actions: undefined,
cardActionsProps: undefined,
icon: undefined,
title: undefined,
subheader: undefined,
cardHeaderProps: undefined,
mediaProps: undefined,
isSelected: false,
handleClick: undefined,
cardProps: {},
cardActionAreaProps: {},
children: undefined,
dataCy: undefined,
disableFilterImage: false,
handleClick: undefined,
icon: undefined,
isSelected: false,
mediaProps: undefined,
observerOff: false,
children: undefined
stylesProps: undefined,
subheader: undefined,
title: undefined
}
SelectCard.displayName = 'SelectCard'

View File

@ -1,7 +1,7 @@
import { makeStyles } from '@material-ui/core'
export default makeStyles(theme => ({
root: ({ isSelected }) => ({
root: ({ isSelected, disableFilterImage }) => ({
height: '100%',
transition: theme.transitions.create(
['background-color', 'box-shadow'], { duration: '0.2s' }
@ -21,7 +21,12 @@ export default makeStyles(theme => ({
}),
actionArea: {
height: '100%',
minHeight: ({ minHeight = 140 }) => minHeight
display: 'flex',
flexDirection: 'column',
minHeight: ({ minHeight = 140 }) => minHeight,
'&:disabled': {
filter: 'brightness(0.5)'
}
},
mediaActionArea: {
display: 'flex',
@ -32,12 +37,25 @@ export default makeStyles(theme => ({
}
},
media: {
'& img': {
width: '100%',
objectFit: 'cover',
maxHeight: ({ mediaHeight = 140 }) => mediaHeight
},
flexGrow: 1,
transition: theme.transitions.create('filter', { duration: '0.2s' }),
filter: ({ isSelected }) => (theme.palette.type === 'dark' || isSelected)
? 'contrast(0) brightness(2)'
: 'contrast(0) brightness(0.8)'
filter: ({ isSelected, disableFilterImage }) => {
return disableFilterImage
? 'none'
: (theme.palette.type === 'dark' || isSelected)
? 'contrast(0) brightness(2)'
: 'contrast(0) brightness(0.8)'
}
},
headerRoot: {
alignItems: 'end',
alignSelf: 'stretch'
},
headerRoot: { alignItems: 'end' },
headerContent: { overflow: 'auto' },
headerAvatar: {
display: 'flex',

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { CSSTransition, TransitionGroup } from 'react-transition-group'

View File

@ -34,6 +34,7 @@ export const IMAGES_URL = `${STATIC_FILES_URL}/images`
export const PROVIDER_IMAGES_URL = `${IMAGES_URL}/providers`
export const PROVISION_IMAGES_URL = `${IMAGES_URL}/provisions`
export const DEFAULT_IMAGE = `${IMAGES_URL}/default.webp`
export const IMAGE_FORMATS = ['webp', 'png', 'jpg']
export const FONTS_URL = `${STATIC_FILES_URL}/fonts`
export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`

View File

@ -1,14 +1,12 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useProvision } from 'client/hooks'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Providers/Form/Create/Steps/Template'
import { FORM_FIELDS, STEP_FORM_SCHEMA } from './schema'
import {
FORM_FIELDS, STEP_FORM_SCHEMA
} from 'client/containers/Providers/Form/Create/Steps/Connection/schema'
export const STEP_ID = 'connection'
@ -19,37 +17,13 @@ const Connection = () => ({
label: T.ConfigureConnection,
resolver: () => STEP_FORM_SCHEMA(connection),
optionsValidate: { abortEarly: false },
content: useCallback(() => {
content: useCallback(({ data }) => {
const [fields, setFields] = useState([])
const { provisionsTemplates } = useProvision()
const { watch, reset } = useFormContext()
useEffect(() => {
const {
[TEMPLATE_ID]: templateSelected,
[STEP_ID]: currentConnections
} = watch()
const { name, provision, provider } = templateSelected?.[0] ?? {}
const providerTemplate = provisionsTemplates
?.[provision]
?.providers?.[provider]
?.find(providerSelected => providerSelected.name === name) ?? {}
const {
location_key: locationKey = '',
connection: { [locationKey]: _, ...connectionEditable } = {}
} = providerTemplate
connection = connectionEditable
connection = data
setFields(FORM_FIELDS(connection))
// set defaults connection values when first render
!currentConnections && reset({
...watch(),
[STEP_ID]: STEP_FORM_SCHEMA(connection).default()
})
}, [])
}, [data])
return (fields?.length === 0) ? (
<EmptyCard title={'✔️ There is not connections to fill'} />

View File

@ -1,17 +1,17 @@
import * as yup from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
import { capitalize, getValidationFromFields } from 'client/utils'
export const FORM_FIELDS = connection =>
Object.entries(connection)?.map(([name, label]) => ({
Object.entries(connection)?.map(([name, value]) => ({
name,
label,
label: capitalize(name),
type: INPUT_TYPES.TEXT,
validation: yup
.string()
.trim()
.required(`${label} field is required`)
.default('')
.required(`${name} field is required`)
.default(value)
}))
export const STEP_FORM_SCHEMA = connection => yup.object(

View File

@ -2,10 +2,11 @@ import React, { useCallback } from 'react'
import { Divider, Select, Breadcrumbs, Link } from '@material-ui/core'
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
import { useProvision, useListForm, useGeneral } from 'client/hooks'
import { useProvision, useListForm } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
import { isExternalURL, sanitize } from 'client/utils'
import * as ProviderTemplateModel from 'client/models/ProviderTemplate'
import { T } from 'client/constants'
import { STEP_ID as CONFIGURATION_ID } from 'client/containers/Providers/Form/Create/Steps/BasicConfiguration'
@ -18,129 +19,130 @@ const Template = () => ({
id: STEP_ID,
label: T.ProviderTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const templateSelected = data?.[0]
content: useCallback(
({ data, setFormData }) => {
const templateSelected = data?.[0]
const [provisionSelected, setProvision] = React.useState(templateSelected?.provision)
const [providerSelected, setProvider] = React.useState(templateSelected?.provider)
const [provisionSelected, setProvision] = React.useState(() => templateSelected?.plain?.provision_type)
const [providerSelected, setProvider] = React.useState(() => templateSelected?.provider)
const { showError } = useGeneral()
const { provisionsTemplates } = useProvision()
const provisionSelectedDescription = provisionsTemplates?.[provisionSelected]?.description
const providersTypes = provisionsTemplates?.[provisionSelected]?.providers ?? []
const templatesAvailable = providersTypes?.[providerSelected]
const { provisionsTemplates } = useProvision()
const provisionSelectedDescription = provisionsTemplates?.[provisionSelected]?.description
const providersTypes = provisionsTemplates?.[provisionSelected]?.providers ?? []
const templatesAvailable = providersTypes?.[providerSelected]
const {
handleSelect,
handleUnselect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const {
handleSelect,
handleUnselect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const handleChangeProvision = evt => {
setProvision(evt.target.value)
setProvider(undefined)
templateSelected && handleClear()
}
const handleChangeProvision = evt => {
setProvision(evt.target.value)
setProvider(undefined)
templateSelected && handleClear()
}
const handleChangeProvider = evt => {
setProvider(evt.target.value)
templateSelected && handleClear()
}
const handleChangeProvider = evt => {
setProvider(evt.target.value)
templateSelected && handleClear()
}
const handleClick = ({ name, description, provider, plain = {} }, isSelected) => {
const { provision_type: provisionType } = plain
const handleClick = (template, isSelected) => {
const { name, description, plain = {}, connection } = template
const { location_key: locationKey = '' } = plain
const { [locationKey]: _, ...connectionEditable } = connection
if ([name, provisionType, provider].includes(undefined)) {
showError({ message: 'This template has bad format. Ask your cloud administrator' })
} else {
// reset rest of form when change template
setFormData({ [CONFIGURATION_ID]: { name, description }, [CONNECTION_ID]: undefined })
setFormData({ [CONFIGURATION_ID]: { name, description }, [CONNECTION_ID]: connectionEditable })
isSelected
? handleUnselect(name, item => item.name === name)
: handleSelect({ name, provider, provision: provisionType })
: handleSelect(template)
}
}
const RenderOptions = ({ options = {} }) => Object.keys(options)?.map(option => (
<option key={option} value={option}>{option}</option>
))
const RenderOptions = ({ options = {} }) => Object.keys(options)?.map(option => (
<option key={option} value={option}>{option}</option>
))
const RenderDescription = ({ description = '' }) => (
<p>{(sanitize`${description}`)?.split(' ').map((string, idx) =>
isExternalURL(string)
? <Link key={`link-${idx}`} color='textPrimary' target='_blank' href={string}>{string}</Link>
: ` ${string}`
)}</p>
)
const RenderDescription = ({ description = '' }) => (
<p>{(sanitize`${description}`)?.split(' ').map((string, idx) =>
isExternalURL(string)
? <Link key={`link-${idx}`} color='textPrimary' target='_blank' href={string}>{string}</Link>
: ` ${string}`
)}</p>
)
return (
<>
{/* -- SELECTORS -- */}
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
<Select
color='secondary'
inputProps = {{ 'data-cy': 'select-provision-type' }}
native
style={{ minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant='outlined'
>
<option value="">{T.None}</option>
<RenderOptions options={provisionsTemplates} />
</Select>
{provisionSelected && <Select
color='secondary'
inputProps = {{ 'data-cy': 'select-provider-type' }}
native
style={{ minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant='outlined'
>
<option value="">{T.None}</option>
<RenderOptions options={providersTypes} />
</Select>}
</Breadcrumbs>
return (
<>
{/* -- SELECTORS -- */}
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
<Select
color='secondary'
inputProps = {{ 'data-cy': 'select-provision-type' }}
native
style={{ minWidth: '8em' }}
onChange={handleChangeProvision}
value={provisionSelected}
variant='outlined'
>
<option value="">{T.None}</option>
<RenderOptions options={provisionsTemplates} />
</Select>
{provisionSelected && <Select
color='secondary'
inputProps = {{ 'data-cy': 'select-provider-type' }}
native
style={{ minWidth: '8em' }}
onChange={handleChangeProvider}
value={providerSelected}
variant='outlined'
>
<option value="">{T.None}</option>
<RenderOptions options={providersTypes} />
</Select>}
</Breadcrumbs>
{/* -- DESCRIPTION -- */}
{React.useMemo(() => provisionSelectedDescription && (
<RenderDescription description={provisionSelectedDescription} />
), [provisionSelectedDescription])}
{/* -- DESCRIPTION -- */}
{React.useMemo(() => provisionSelectedDescription && (
<RenderDescription description={provisionSelectedDescription} />
), [provisionSelectedDescription])}
<Divider style={{ margin: '1rem 0' }} />
<Divider style={{ margin: '1rem 0' }} />
{/* -- LIST -- */}
<ListCards
keyProp='name'
list={templatesAvailable}
EmptyComponent={
<EmptyCard title={
!provisionSelected
? 'Please choose your provision type'
: !providerSelected
? 'Please choose your provider type'
: 'Your providers templates list is empty'
} />
}
gridProps={{ 'data-cy': 'providers-templates' }}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected =>
selected.name === value.name
)
return {
isProvider: true,
isSelected,
handleClick: () => handleClick(value, isSelected)
{/* -- LIST -- */}
<ListCards
keyProp='name'
list={templatesAvailable}
EmptyComponent={
<EmptyCard title={
!provisionSelected
? 'Please choose your provision type'
: !providerSelected
? 'Please choose your provider type'
: 'Your providers templates list is empty'
} />
}
}}
/>
</>
)
}, [])
gridProps={{ 'data-cy': 'providers-templates' }}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected =>
selected.name === value.name
)
const isValid = ProviderTemplateModel.isValidProviderTemplate(value)
return {
isProvider: true,
isSelected,
isValid,
handleClick: () => handleClick(value, isSelected)
}
}}
/>
</>
)
}, [])
})
export default Template

View File

@ -1,39 +1,7 @@
import * as yup from 'yup'
import { getValidationFromFields } from 'client/utils'
const NAME = {
name: 'name',
validation: yup
.string()
.trim()
.required('Template field is required')
.default(undefined)
}
const PROVISION = {
name: 'provision',
validation: yup
.string()
.trim()
.required('Provision type field is required')
.default(undefined)
}
const PROVIDER = {
name: 'provider',
validation: yup
.string()
.trim()
.required('Provider type field is required')
.default(undefined)
}
export const PROVIDER_TEMPLATE_SCHEMA = yup.object(
getValidationFromFields([NAME, PROVISION, PROVIDER])
)
export const STEP_FORM_SCHEMA = yup
.array(PROVIDER_TEMPLATE_SCHEMA)
.array(yup.object())
.min(1, 'Select provider template')
.max(1, 'Max. one template selected')
.required('Provider template field is required')

View File

@ -9,6 +9,7 @@ import FormStepper from 'client/components/FormStepper'
import Steps from 'client/containers/Providers/Form/Create/Steps'
import { useFetch, useProvision, useGeneral } from 'client/hooks'
import * as ProviderTemplateModel from 'client/models/ProviderTemplate'
import { PATH } from 'client/router/provision'
function ProviderCreateForm () {
@ -19,8 +20,7 @@ function ProviderCreateForm () {
const {
getProvider,
createProvider,
updateProvider,
provisionsTemplates
updateProvider
} = useProvision()
const { data, fetchRequest, loading, error } = useFetch(getProvider)
@ -33,61 +33,66 @@ function ProviderCreateForm () {
resolver: yupResolver(resolvers())
})
const redirectWithError = (name = '') => {
showError({
message: `
Cannot found provider template (${name}),
ask your cloud administrator`
})
const redirectWithError = (message = 'Error') => {
showError({ message })
history.push(PATH.PROVIDERS.LIST)
}
const getProviderTemplateByDir = ({ provision, provider, name }) =>
provisionsTemplates
?.[provision]
?.providers
?.[provider]
?.find(providerSelected => providerSelected.name === name)
const onSubmit = formData => {
const { template, configuration, connection, registration_time: time } = formData
const { name, description } = configuration
const callCreateProvider = formData => {
const { template, configuration, connection } = formData
const templateSelected = template?.[0]
const providerTemplate = getProviderTemplateByDir(templateSelected)
const { name, description } = configuration
if (!providerTemplate) return redirectWithError(templateSelected?.name)
const isValid = ProviderTemplateModel.isValidProviderTemplate(templateSelected)
const {
inputs,
name: templateName,
plain,
provider,
location_key: locationKey,
connection: { [locationKey]: connectionFixed }
} = providerTemplate
!isValid && redirectWithError(`
The template selected has a bad format.
Ask your cloud administrator`
)
const { inputs, plain, provider } = templateSelected
const { location_key: locationKey } = plain
const { [locationKey]: connectionFixed } = templateSelected.connection
const formatData = {
...(!isUpdate && {
name,
plain,
provider,
inputs,
template: templateName
}),
description,
connection: { ...connection, [locationKey]: connectionFixed },
registration_time: time
description,
inputs,
name,
plain,
provider
}
if (isUpdate) {
updateProvider({ id, data: formatData })
.then(() => history.push(PATH.PROVIDERS.LIST))
} else {
createProvider({ data: formatData })
.then(() => history.push(PATH.PROVIDERS.LIST))
createProvider({ data: formatData })
.then(() => history.push(PATH.PROVIDERS.LIST))
}
const callUpdateProvider = formData => {
const { configuration, connection: connectionEditable } = formData
const { description } = configuration
const {
PLAIN: { location_key: locationKey } = {},
PROVISION_BODY: {
connection: { [locationKey]: connectionFixed },
registration_time: registrationTime
}
} = data?.TEMPLATE
const formatData = {
description,
connection: { ...connectionEditable, [locationKey]: connectionFixed },
registration_time: registrationTime
}
updateProvider({ id, data: formatData })
.then(() => history.push(PATH.PROVIDERS.LIST))
}
const onSubmit = formData => {
isUpdate ? callUpdateProvider(formData) : callCreateProvider(formData)
}
useEffect(() => {
@ -97,29 +102,17 @@ function ProviderCreateForm () {
useEffect(() => {
if (data) {
const {
connection,
description,
name,
provider,
template: templateName,
registration_time: time
} = data?.TEMPLATE?.PROVISION_BODY ?? {}
const { provision_type: provisionType } = data?.TEMPLATE?.PLAIN ?? {}
const templateSelected = { name: templateName, provision: provisionType, provider }
const template = getProviderTemplateByDir(templateSelected)
if (!template) return redirectWithError(templateName)
const { location_key: locationKey } = template
const { [locationKey]: _, ...connectionEditable } = connection
PLAIN: { location_key: locationKey } = {},
PROVISION_BODY: {
connection: { [locationKey]: connectionEditable },
description,
name
}
} = data?.TEMPLATE
methods.reset({
connection: connectionEditable,
configuration: { name, description },
template: [templateSelected],
registration_time: time
configuration: { name, description }
}, { errors: false })
}
}, [data])

View File

@ -6,6 +6,7 @@ import DeleteIcon from '@material-ui/icons/Delete'
import { useProvision, useOpennebula, useFetchAll } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { DatastoreCard } from 'client/components/Cards'
import * as Types from 'client/types/provision'
const Datastores = memo(({ hidden, data, fetchRequest }) => {
const {
@ -51,7 +52,7 @@ const Datastores = memo(({ hidden, data, fetchRequest }) => {
prev.hidden === next.hidden && prev.data === next.data)
Datastores.propTypes = {
data: PropTypes.object.isRequired,
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
}

View File

@ -7,6 +7,7 @@ import ConfigureIcon from '@material-ui/icons/Settings'
import { useProvision, useOpennebula, useFetchAll } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { HostCard } from 'client/components/Cards'
import * as Types from 'client/types/provision'
const Hosts = memo(({ hidden, data, fetchRequest }) => {
const {
@ -59,7 +60,7 @@ const Hosts = memo(({ hidden, data, fetchRequest }) => {
prev.hidden === next.hidden && prev.data === next.data)
Hosts.propTypes = {
data: PropTypes.object.isRequired,
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
}

View File

@ -14,6 +14,8 @@ import NetworksTab from 'client/containers/Provisions/DialogInfo/networks'
import HostsTab from 'client/containers/Provisions/DialogInfo/hosts'
import LogTab from 'client/containers/Provisions/DialogInfo/log'
import * as Types from 'client/types/provision'
const TABS = [
{ name: 'info', icon: InfoIcon, content: InfoTab },
{ name: 'datastores', icon: DatastoreIcon, content: DatastoresTab },
@ -69,7 +71,7 @@ const DialogInfo = ({ data, ...methods }) => {
}
DialogInfo.propTypes = {
data: PropTypes.object.isRequired,
data: Types.Provision.isRequired,
fetchRequest: PropTypes.func
}

View File

@ -1,5 +1,4 @@
import React, { memo } from 'react'
import PropTypes from 'prop-types'
import { List, ListItem, Typography, Grid, Paper, Divider } from '@material-ui/core'
import { CheckBox, CheckBoxOutlineBlank } from '@material-ui/icons'
@ -8,6 +7,7 @@ import clsx from 'clsx'
import useStyles from 'client/containers/Provisions/DialogInfo/styles'
import { StatusChip } from 'client/components/Status'
import { Tr } from 'client/components/HOC'
import * as Types from 'client/types/provision'
import { T, PROVISIONS_STATES } from 'client/constants'
const Info = memo(({ data }) => {
@ -124,7 +124,7 @@ const Info = memo(({ data }) => {
})
Info.propTypes = {
data: PropTypes.object.isRequired
data: Types.Provision.isRequired
}
Info.defaultProps = {

View File

@ -5,6 +5,7 @@ import { LinearProgress } from '@material-ui/core'
import { useProvision, useFetch, useSocket } from 'client/hooks'
import DebugLog from 'client/components/DebugLog'
import * as Types from 'client/types/provision'
const Log = React.memo(({ hidden, data: { ID } }) => {
const { getProvision } = useSocket()
@ -46,7 +47,7 @@ const Log = React.memo(({ hidden, data: { ID } }) => {
prev.hidden === next.hidden && prev.data === next.data)
Log.propTypes = {
data: PropTypes.object.isRequired,
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
}

View File

@ -6,6 +6,7 @@ import DeleteIcon from '@material-ui/icons/Delete'
import { useProvision, useOpennebula, useFetchAll } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { NetworkCard } from 'client/components/Cards'
import * as Types from 'client/types/provision'
const Networks = memo(({ hidden, data, fetchRequest }) => {
const {
@ -51,7 +52,7 @@ const Networks = memo(({ hidden, data, fetchRequest }) => {
prev.hidden === next.hidden && prev.data === next.data)
Networks.propTypes = {
data: PropTypes.object.isRequired,
data: Types.Provision.isRequired,
hidden: PropTypes.bool,
fetchRequest: PropTypes.func
}

View File

@ -26,23 +26,16 @@ const Inputs = () => ({
content: useCallback(() => {
const [fields, setFields] = useState(undefined)
const { changeLoading } = useGeneral()
const { provisionsTemplates, getProvider } = useProvision()
const { getProvider } = useProvision()
const { data: fetchData, fetchRequest, loading } = useFetch(getProvider)
const { watch, reset } = useFormContext()
const getProvisionTemplateByDir = ({ provision, provider, name }) =>
provisionsTemplates
?.[provision]
?.provisions
?.[provider]
?.find(provisionTemplate => provisionTemplate.name === name)
useEffect(() => {
const { [PROVIDER_ID]: providerSelected, [STEP_ID]: currentInputs } = watch()
const { [PROVIDER_ID]: providerSelected = [], [STEP_ID]: currentInputs } = watch()
if (!currentInputs) {
changeLoading(true) // disable finish button until provider is fetched
fetchRequest({ id: providerSelected[0] })
fetchRequest({ id: providerSelected[0]?.ID })
} else {
setFields(FORM_FIELDS(inputs))
}
@ -53,7 +46,7 @@ const Inputs = () => ({
const { [TEMPLATE_ID]: provisionTemplateSelected = [] } = watch()
const { TEMPLATE: { PROVISION_BODY } = {} } = fetchData
const provisionTemplate = getProvisionTemplateByDir(provisionTemplateSelected?.[0])
const provisionTemplate = provisionTemplateSelected?.[0]
// MERGE INPUTS provision template + PROVISION_BODY.inputs (provider fetch)
inputs = provisionTemplate.inputs.map(templateInput =>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react'
import React, { useCallback } from 'react'
import { useWatch } from 'react-hook-form'
import { useProvision, useListForm } from 'client/hooks'
@ -18,31 +18,30 @@ const Provider = () => ({
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { providers } = useProvision()
const template = useWatch({ name: TEMPLATE_ID })
const templateSelected = template?.[0] ?? {}
const provisionTemplate = useWatch({ name: TEMPLATE_ID })
const provisionTemplateSelected = provisionTemplate?.[0] ?? {}
const providersByTypeAndService = React.useMemo(() =>
providers.filter(({ TEMPLATE: { PLAIN = {} } = {} }) =>
PLAIN.provider === templateSelected.provider &&
PLAIN.provision_type === templateSelected.provision
PLAIN.provider === provisionTemplateSelected.provider &&
PLAIN.provision_type === provisionTemplateSelected.provision_type
)
, [providers])
, [])
const {
handleSelect,
handleUnselect
} = useListForm({ key: STEP_ID, setList: setFormData })
useEffect(() => {
// delete provider selected at template if not exists
const existsProvider = providers?.some(({ ID }) => ID === data?.[0])
!existsProvider && handleUnselect(data?.[0])
}, [])
const handleClick = (provider, isSelected) => {
const { ID } = provider
const handleClick = (id, isSelected) => {
// reset inputs when change provider
// reset inputs when selected provider changes
setFormData(prev => ({ ...prev, [INPUTS_ID]: undefined }))
isSelected ? handleUnselect(id) : handleSelect(id)
isSelected
? handleUnselect(ID, item => item.ID === ID)
: handleSelect(provider)
}
return (
@ -51,13 +50,13 @@ const Provider = () => ({
EmptyComponent={<EmptyCard title={'Your providers list is empty'} />}
CardComponent={ProvisionCard}
gridProps={{ 'data-cy': 'providers' }}
cardsProps={({ value: { ID } }) => {
const isSelected = data?.some(selected => selected === ID)
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected => selected.ID === value.ID)
return {
isProvider: true,
isSelected,
handleClick: () => handleClick(ID, isSelected)
handleClick: () => handleClick(value, isSelected)
}
}}
breakpoints={{ xs: 12, sm: 6, md: 4 }}

View File

@ -1,7 +1,7 @@
import * as yup from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.string().trim())
.array(yup.object())
.min(1, 'Select provider')
.max(1, 'Max. one provider selected')
.required('Provider field is required')

View File

@ -2,10 +2,11 @@ import React, { useCallback } from 'react'
import { Divider, Select, Breadcrumbs, Link } from '@material-ui/core'
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
import { useProvision, useListForm, useGeneral } from 'client/hooks'
import { useProvision, useListForm } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
import { isExternalURL, sanitize } from 'client/utils'
import * as ProvisionTemplateModel from 'client/models/ProvisionTemplate'
import { T } from 'client/constants'
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
@ -22,10 +23,9 @@ const Template = () => ({
content: useCallback(({ data, setFormData }) => {
const templateSelected = data?.[0]
const [provisionSelected, setProvision] = React.useState(templateSelected?.provision)
const [provisionSelected, setProvision] = React.useState(templateSelected?.provision_type)
const [providerSelected, setProvider] = React.useState(templateSelected?.provider)
const { showError } = useGeneral()
const { provisionsTemplates, providers } = useProvision()
const provisionSelectedDescription = provisionsTemplates?.[provisionSelected]?.description
const providersTypes = provisionsTemplates?.[provisionSelected]?.provisions ?? []
@ -49,24 +49,21 @@ const Template = () => ({
}
const handleClick = (template, isSelected) => {
const { name, description, provision_type: provisionType, provider, defaults, hosts } = template
const { name, description, defaults, hosts } = template
if ([name, provisionType, provider].includes(undefined)) {
showError({ message: 'This template has bad format. Ask your cloud administrator' })
} else {
// reset rest of form when change template
const providerName = defaults?.provision?.provider_name ?? hosts?.[0]?.provision.provider_name
const { ID } = providers?.find(({ NAME }) => NAME === providerName) ?? {}
setFormData({
[PROVIDER_ID]: [ID],
[CONFIGURATION_ID]: { name, description },
[INPUTS_ID]: undefined
})
// reset rest of form when change template
const providerName = defaults?.provision?.provider_name ?? hosts?.[0]?.provision.provider_name
const providerFromProvisionTemplate = providers?.find(({ NAME }) => NAME === providerName) ?? {}
isSelected
? handleUnselect(name, item => item.name === name)
: handleSelect({ name, provider, provision: provisionType })
}
setFormData({
[PROVIDER_ID]: [providerFromProvisionTemplate],
[CONFIGURATION_ID]: { name, description },
[INPUTS_ID]: undefined
})
isSelected
? handleUnselect(name, item => item.name === name)
: handleSelect(template)
}
const RenderOptions = ({ options = {} }) => Object.keys(options)?.map(option => (
@ -134,12 +131,13 @@ const Template = () => ({
gridProps={{ 'data-cy': 'provisions-templates' }}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected =>
selected.name === value.name
)
const isSelected = data?.some(selected => selected.name === value.name)
const isValid = ProvisionTemplateModel.isValidProvisionTemplate(value)
return {
isSelected,
isValid,
handleClick: () => handleClick(value, isSelected)
}
}}

View File

@ -1,39 +1,7 @@
import * as yup from 'yup'
import { getValidationFromFields } from 'client/utils'
const NAME = {
name: 'name',
validation: yup
.string()
.trim()
.required('Template field is required')
.default(undefined)
}
const PROVISION = {
name: 'provision',
validation: yup
.string()
.trim()
.required('Provision type field is required')
.default(undefined)
}
const PROVIDER = {
name: 'provider',
validation: yup
.string()
.trim()
.required('Provider type field is required')
.default(undefined)
}
export const PROVIDER_TEMPLATE_SCHEMA = yup.object(
getValidationFromFields([NAME, PROVISION, PROVIDER])
)
export const STEP_FORM_SCHEMA = yup
.array(PROVIDER_TEMPLATE_SCHEMA)
.array(yup.object())
.min(1, 'Select provision template')
.max(1, 'Max. one template selected')
.required('Provision template field is required')

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { Redirect, useHistory } from 'react-router'
import { Redirect } from 'react-router'
import { Container, LinearProgress } from '@material-ui/core'
import { useForm, FormProvider } from 'react-hook-form'
@ -9,16 +9,16 @@ import FormStepper from 'client/components/FormStepper'
import Steps from 'client/containers/Provisions/Form/Create/Steps'
import DebugLog from 'client/components/DebugLog'
import { useGeneral, useProvision, useSocket, useFetch } from 'client/hooks'
import { useProvision, useSocket, useFetch } from 'client/hooks'
import { PATH } from 'client/router/provision'
import { set, mapUserInputs } from 'client/utils'
function ProvisionCreateForm () {
const [uuid, setUuid] = useState(undefined)
const history = useHistory()
const { showError } = useGeneral()
const { getProvision } = useSocket()
const { getProviders, createProvision, provisionsTemplates, providers } = useProvision()
const { getProviders, createProvision, providers } = useProvision()
const { data, fetchRequest, loading, error } = useFetch(getProviders)
const { steps, defaultValues, resolvers } = Steps()
@ -29,39 +29,17 @@ function ProvisionCreateForm () {
resolver: yupResolver(resolvers())
})
const redirectWithError = (name = '') => {
showError({
message: `
Cannot found provision template (${name}),
ask your cloud administrator`
})
history.push(PATH.PROVIDERS.LIST)
}
const getProvisionTemplateByDir = ({ provision, provider, name }) =>
provisionsTemplates
?.[provision]
?.provisions
?.[provider]
?.find(provisionTemplate => provisionTemplate.name === name)
const onSubmit = formData => {
const { template, provider, configuration, inputs } = formData
const { name, description } = configuration
const provisionTemplateSelected = template?.[0] ?? {}
const providerIdSelected = provider?.[0]
const providerName = providers?.find(({ ID }) => ID === providerIdSelected)?.NAME
const provisionTemplate = getProvisionTemplateByDir(provisionTemplateSelected)
if (!provisionTemplate) return redirectWithError(provisionTemplateSelected?.name)
const providerName = provider?.[0]?.NAME
// update provider name if changed during form
if (provisionTemplate.defaults?.provision?.provider_name) {
set(provisionTemplate, 'defaults.provision.provider_name', providerName)
} else if (provisionTemplate.hosts?.length > 0) {
provisionTemplate.hosts.forEach(host => {
if (provisionTemplateSelected.defaults?.provision?.provider_name) {
set(provisionTemplateSelected, 'defaults.provision.provider_name', providerName)
} else if (provisionTemplateSelected.hosts?.length > 0) {
provisionTemplateSelected.hosts.forEach(host => {
set(host, 'provision.provider_name', providerName)
})
}
@ -69,10 +47,10 @@ function ProvisionCreateForm () {
const parseInputs = mapUserInputs(inputs)
const formatData = {
...provisionTemplate,
...provisionTemplateSelected,
name,
description,
inputs: provisionTemplate?.inputs
inputs: provisionTemplateSelected?.inputs
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` }))
}

View File

@ -35,8 +35,28 @@ export default function useProvision () {
.catch(err => {
dispatch(enqueueError(err ?? 'Error GET templates'))
throw err
}),
[dispatch]
})
, [dispatch]
)
const getProviderTemplateByDir = useCallback(
({ provision, provider, name } = {}) =>
provisionsTemplates
?.[provision]
?.providers
?.[provider]
?.find(provider => provider.name === name)
, [provisionsTemplates]
)
const getProvisionTemplateByDir = useCallback(
({ provision, provider, name } = {}) =>
provisionsTemplates
?.[provision]
?.provisions
?.[provider]
?.find(provisionTemplate => provisionTemplate.name === name)
, [provisionsTemplates]
)
// --------------------------------------------
@ -54,8 +74,8 @@ export default function useProvision () {
.catch(err => {
dispatch(enqueueError(err ?? `Error GET (${id}) provider`))
throw err
}),
[dispatch]
})
, [dispatch]
)
const getProviders = useCallback(
@ -69,8 +89,8 @@ export default function useProvision () {
.catch(err => {
dispatch(enqueueError(err ?? 'Error GET providers'))
return err
}),
[dispatch]
})
, [dispatch]
)
const createProvider = useCallback(
@ -226,6 +246,8 @@ export default function useProvision () {
return {
getProvisionsTemplates,
getProviderTemplateByDir,
getProvisionTemplateByDir,
provisionsTemplates,
providers,

View File

@ -0,0 +1,10 @@
export const isValidProviderTemplate = ({ name, provider, plain = {}, connection }) => {
const { provision_type: provisionType, location_key: locationKey } = plain
const locationKeyConnectionNotExists = connection[locationKey] === undefined
return (
!(locationKey && locationKeyConnectionNotExists) ||
[name, provisionType, provider].includes(undefined)
)
}

View File

@ -0,0 +1,14 @@
export const isValidProvisionTemplate = ({
defaults,
hosts,
name,
provider,
provision_type: provisionType
}) => {
const providerName = defaults?.provision?.provider_name ?? hosts?.[0]?.provision.provider_name
return !(
providerName === undefined ||
[name, provisionType, provider].includes(undefined)
)
}

View File

@ -0,0 +1,157 @@
import PropTypes from 'prop-types'
export const UserInput = PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string,
type: PropTypes.oneOf([
'text',
'text64',
'password',
'number',
'number-float',
'range',
'range-float',
'boolean',
'list',
'array',
'list-multiple'
]).isRequired,
options: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
),
min_value: PropTypes.number,
max_value: PropTypes.number,
default: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
})
export const ProviderType = PropTypes.oneOf(['aws', 'packet'])
export const ProvisionType = PropTypes.oneOf([
'hybrid+',
'hybrid+_qemu',
'hybrid+_firecracker'
])
export const ProvisionHost = PropTypes.shape({
im_mad: PropTypes.string.isRequired,
vm_mad: PropTypes.string.isRequired,
provision: PropTypes.shape({
count: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
hostname: PropTypes.string
})
})
export const ProviderPlainInfo = PropTypes.shape({
image: PropTypes.string,
location_key: PropTypes.string,
provision_type: ProvisionType.isRequired
})
export const ProviderTemplate = PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string,
provider: ProviderType.isRequired,
plain: ProviderPlainInfo.isRequired,
connection: PropTypes.objectOf(PropTypes.string),
inputs: PropTypes.arrayOf(UserInput)
})
export const ProvisionTemplate = PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string,
provider: ProviderType.isRequired,
provision_type: ProvisionType.isRequired,
defaults: PropTypes.shape({
provision: PropTypes.shape({
provider_name: PropTypes.string
})
}).isRequired,
hosts: PropTypes.arrayOf(ProvisionHost),
inputs: PropTypes.arrayOf(UserInput)
})
export const Provider = PropTypes.shape({
ID: PropTypes.string.isRequired,
UID: PropTypes.string.isRequired,
GID: PropTypes.string.isRequired,
UNAME: PropTypes.string.isRequired,
GNAME: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TYPE: PropTypes.string.isRequired,
PERMISSIONS: PropTypes.shape({
OWNER_U: PropTypes.oneOf(['0', '1']).isRequired,
OWNER_M: PropTypes.oneOf(['0', '1']).isRequired,
OWNER_A: PropTypes.oneOf(['0', '1']).isRequired,
GROUP_U: PropTypes.oneOf(['0', '1']).isRequired,
GROUP_M: PropTypes.oneOf(['0', '1']).isRequired,
GROUP_A: PropTypes.oneOf(['0', '1']).isRequired,
OTHER_U: PropTypes.oneOf(['0', '1']).isRequired,
OTHER_M: PropTypes.oneOf(['0', '1']).isRequired,
OTHER_A: PropTypes.oneOf(['0', '1']).isRequired
}).isRequired,
TEMPLATE: PropTypes.shape({
PLAIN: ProviderPlainInfo,
PROVISION_BODY: PropTypes.oneOfType([
// encrypted
PropTypes.string,
PropTypes.shape({
provider: ProviderType,
connection: PropTypes.objectOf(PropTypes.string),
registration_time: PropTypes.number,
description: PropTypes.string,
inputs: PropTypes.arrayOf(UserInput)
}).isRequired
])
})
})
const ProvisionInfrastructure = PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
})
export const Provision = PropTypes.shape({
ID: PropTypes.string.isRequired,
UID: PropTypes.string.isRequired,
GID: PropTypes.string.isRequired,
UNAME: PropTypes.string.isRequired,
GNAME: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TYPE: PropTypes.string.isRequired,
PERMISSIONS: PropTypes.shape({
OWNER_U: PropTypes.oneOf(['0', '1']).isRequired,
OWNER_M: PropTypes.oneOf(['0', '1']).isRequired,
OWNER_A: PropTypes.oneOf(['0', '1']).isRequired,
GROUP_U: PropTypes.oneOf(['0', '1']).isRequired,
GROUP_M: PropTypes.oneOf(['0', '1']).isRequired,
GROUP_A: PropTypes.oneOf(['0', '1']).isRequired,
OTHER_U: PropTypes.oneOf(['0', '1']).isRequired,
OTHER_M: PropTypes.oneOf(['0', '1']).isRequired,
OTHER_A: PropTypes.oneOf(['0', '1']).isRequired
}).isRequired,
TEMPLATE: PropTypes.shape({
BODY: PropTypes.shape({
name: PropTypes.string,
description: PropTypes.string,
start_time: PropTypes.number,
state: PropTypes.number,
provider: PropTypes.string,
provision: PropTypes.shape({
infrastructure: PropTypes.shape({
clusters: PropTypes.arrayOf(ProvisionInfrastructure),
datastores: PropTypes.arrayOf(ProvisionInfrastructure),
networks: PropTypes.arrayOf(ProvisionInfrastructure)
}),
resource: PropTypes.object
}),
image: PropTypes.string,
provision_type: ProvisionType
}).isRequired
})
})