1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-20 10:50:08 +03:00

F #3951: Fix provision form (#628)

This commit is contained in:
Sergio Betanzos 2021-01-12 18:45:33 +01:00 committed by GitHub
parent 81e1104118
commit e012409799
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 415 additions and 246 deletions

View File

@ -1,5 +1,5 @@
const CHANGE_ZONE = 'CHANGE_ZONE'
const DISPLAY_LOADING = 'DISPLAY_LOADING'
const CHANGE_LOADING = 'CHANGE_LOADING'
const TOGGLE_MENU = 'TOGGLE_MENU'
const FIX_MENU = 'FIX_MENU'
@ -9,7 +9,7 @@ const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
const Actions = {
CHANGE_ZONE,
DISPLAY_LOADING,
CHANGE_LOADING,
TOGGLE_MENU,
FIX_MENU,
ENQUEUE_SNACKBAR,
@ -24,7 +24,7 @@ module.exports = {
payload: { zone }
}),
changeLoading: isLoading => ({
type: DISPLAY_LOADING,
type: CHANGE_LOADING,
payload: { isLoading }
}),
openMenu: isOpen => ({

View File

@ -129,6 +129,9 @@ module.exports = {
startOneRequest: () => ({
type: START_ONE_REQUEST
}),
successOneRequest: () => ({
type: SUCCESS_ONE_REQUEST
}),
failureOneRequest: error => ({
type: FAILURE_ONE_REQUEST,
payload: { error }

View File

@ -20,17 +20,12 @@ const ProvisionCard = memo(
const [{ image, ...body }, setBody] = useState({})
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const { NAME, TEMPLATE: { PLAIN = '{}', BODY = {} } } = value
const { NAME, TEMPLATE: { PLAIN = {}, BODY = {} } } = value
const stateInfo = PROVISIONS_STATES[body?.state]
useEffect(() => {
try {
const json = isProvider ? JSON.parse(PLAIN) : BODY
setBody({ ...json, image: json.image ?? DEFAULT_IMAGE })
} catch {
setBody({ image: DEFAULT_IMAGE })
console.warn('Image in plain property is not valid')
}
const json = isProvider ? PLAIN : BODY
setBody({ ...json, image: json.image ?? DEFAULT_IMAGE })
}, [])
const onError = evt => { evt.target.src = DEFAULT_IMAGE }
@ -70,7 +65,7 @@ ProvisionCard.propTypes = {
ID: PropTypes.string.isRequired,
NAME: PropTypes.string.isRequired,
TEMPLATE: PropTypes.shape({
PLAIN: PropTypes.string,
PLAIN: PropTypes.object,
BODY: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object

View File

@ -9,7 +9,8 @@ import { PROVIDER_IMAGES_URL, PROVISION_IMAGES_URL } from 'client/constants'
const ProvisionTemplateCard = React.memo(
({ value, isProvider, isSelected, handleClick }) => {
const { description, name, plain: { image } = {} } = value
const { description, name, plain = {} } = value
const { image } = isProvider ? plain : value
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const imgSource = React.useMemo(() =>
@ -37,7 +38,7 @@ const ProvisionTemplateCard = React.memo(
ProvisionTemplateCard.propTypes = {
value: PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
description: PropTypes.string,
plain: PropTypes.shape({
image: PropTypes.string
})

View File

@ -36,7 +36,7 @@ const SelectCard = memo(({
return (
<ConditionalWrap
condition={!observerOff}
wrap={children => <div ref={fromRef}>{children}</div>}>
wrap={children => <span ref={fromRef}>{children}</span>}>
{observerOff || isNearScreen ? (
<Card
className={clsx(classes.root, cardProps?.className, {

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { useMediaQuery } from '@material-ui/core'
import { useGeneral } from 'client/hooks'
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
import CustomStepper from 'client/components/FormStepper/Stepper'
import { groupBy } from 'client/utils'
@ -13,6 +14,7 @@ const FIRST_STEP = 0
const FormStepper = ({ steps, schema, onSubmit }) => {
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const { watch, reset, errors, setError } = useFormContext()
const { isLoading } = useGeneral()
const [formData, setFormData] = useState(() => watch())
const [activeStep, setActiveStep] = useState(FIRST_STEP)
@ -54,7 +56,7 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
const handleStep = stepToAdvance => {
const isBackAction = activeStep > stepToAdvance
isBackAction && handleBack(isBackAction)
isBackAction && handleBack(stepToAdvance)
steps
.slice(FIRST_STEP, stepToAdvance)
@ -112,6 +114,7 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
isSubmitting={isLoading}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}
@ -122,12 +125,13 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
activeStep={activeStep}
lastStep={lastStep}
disabledBack={disabledBack}
isSubmitting={isLoading}
handleStep={handleStep}
handleNext={handleNext}
handleBack={handleBack}
errors={errors}
/>
), [isMobile, activeStep, errors[id]])}
), [isLoading, isMobile, activeStep, errors[id]])}
{/* FORM CONTENT */}
{Content && <Content data={formData[id]} setFormData={setFormData} />}
</>

View File

@ -30,7 +30,7 @@ const Connection = () => ({
[STEP_ID]: currentConnections
} = watch()
const { name, provision, provider } = templateSelected?.[0]
const { name, provision, provider } = templateSelected?.[0] ?? {}
const providerTemplate = provisionsTemplates
?.[provision]
?.providers?.[provider]

View File

@ -1,4 +1,6 @@
import React, { useCallback } from 'react'
import { Divider, Select, Breadcrumbs } from '@material-ui/core'
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
import { useProvision, useListForm, useGeneral } from 'client/hooks'
import { ListCards } from 'client/components/List'
@ -8,7 +10,6 @@ import { T } from 'client/constants'
import { STEP_ID as CONNECTION_ID } from 'client/containers/Providers/Form/Create/Steps/Connection'
import { STEP_ID as INPUTS_ID } from 'client/containers/Providers/Form/Create/Steps/Inputs'
import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Template/schema'
import { Divider, Select } from '@material-ui/core'
export const STEP_ID = 'template'
@ -44,15 +45,18 @@ const Template = () => ({
templateSelected && handleClear()
}
const handleClick = ({ name, provider, provision }, isSelected) => {
if (name === undefined || provider === undefined || provision === undefined) {
const handleClick = ({ name, provider, plain = {} }, isSelected) => {
const { provision_type: provisionType } = plain
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({ [INPUTS_ID]: undefined, [CONNECTION_ID]: undefined })
isSelected
? handleUnselect(name, item => item.name === name)
: handleSelect({ name, provider, provision })
: handleSelect({ name, provider, provision: provisionType })
}
}
@ -62,7 +66,7 @@ const Template = () => ({
return (
<>
<div>
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
<Select
color='secondary'
data-cy='select-provision-type'
@ -87,7 +91,7 @@ const Template = () => ({
<option value="">{T.None}</option>
<RenderOptions options={providersTypes} />
</Select>}
</div>
</Breadcrumbs>
<Divider style={{ margin: '1rem 0' }} />
<ListCards
keyProp='name'

View File

@ -10,7 +10,7 @@ import Steps from 'client/containers/Providers/Form/Create/Steps'
import { useFetch, useProvision, useGeneral } from 'client/hooks'
import { PATH } from 'client/router/provision'
import { mapUserInputs } from 'client/utils'
import { get, mapUserInputs } from 'client/utils'
function ProviderCreateForm () {
const history = useHistory()
@ -34,44 +34,49 @@ function ProviderCreateForm () {
resolver: yupResolver(resolvers())
})
const getTemplate = ({ provision, provider, name } = {}) => {
const template = provisionsTemplates
?.[provision]
?.providers?.[provider]
?.find(providerSelected => providerSelected.name === name)
const redirectWithError = (name = '') => {
showError({
message: `
Cannot found provider template (${name}),
ask your cloud administrator`
})
if (!template) {
showError({
message: `
Cannot found provider template (${provider}),
ask your cloud administrator`
})
history.push(PATH.PROVIDERS.LIST)
} else return template
history.push(PATH.PROVIDERS.LIST)
}
const getProviderTemplateByDir = ({ provision, provider, name }) =>
provisionsTemplates
?.[provision]
?.providers
?.[provider]
?.find(providerSelected => providerSelected.name === name)
const onSubmit = formData => {
const { template, inputs, connection, registration_time: time } = formData
const templateSelected = template?.[0]
const providerTemplate = getTemplate(templateSelected)
const providerTemplate = getProviderTemplateByDir(templateSelected)
if (!providerTemplate) return redirectWithError(templateSelected?.name)
const parseInputs = mapUserInputs(inputs)
const {
plain,
name,
provider,
location_key: locationKey,
connection: { [locationKey]: connectionFixed }
} = providerTemplate
const formatData = {
...(!isUpdate && templateSelected),
...(plain && { plain }),
connection: {
...connection,
[locationKey]: connectionFixed
},
inputs: providerTemplate?.inputs
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` })),
...(!isUpdate && { plain, name, provider }),
connection: { ...connection, [locationKey]: connectionFixed },
inputs: providerTemplate?.inputs?.map(input => ({
...input,
value: `${parseInputs[input?.name]}`,
default: `${parseInputs[input?.name]}`
})),
registration_time: time
}
@ -95,14 +100,17 @@ function ProviderCreateForm () {
inputs,
name,
provider,
provision,
registration_time: time
} = data?.TEMPLATE?.PROVISION_BODY ?? {}
const templateSelected = { name, provision, provider }
const providerTemplate = getTemplate(templateSelected)
const { provision_type: provisionType } = data?.TEMPLATE?.PLAIN ?? {}
const { location_key: locationKey } = providerTemplate
const templateSelected = { name, provision: provisionType, provider }
const template = getProviderTemplateByDir(templateSelected)
if (!template) return redirectWithError(name)
const { location_key: locationKey } = template
const { [locationKey]: _, ...connectionEditable } = connection
const inputsNameValue = inputs?.reduce((res, input) => (

View File

@ -1,18 +1,20 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { LinearProgress } from '@material-ui/core'
import { useProvision } from 'client/hooks'
import { useProvision, useFetch, useGeneral } from 'client/hooks'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { EmptyCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { deepmerge } from 'client/utils/merge'
import {
STEP_ID as PROVISION_TEMPLATE_ID
} from 'client/containers/Provisions/Form/Create/Steps/Provision'
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Provisions/Form/Create/Steps/Template'
import {
FORM_FIELDS, STEP_FORM_SCHEMA
} from 'client/containers/Provisions/Form/Create/Steps/Inputs/schema'
import { console } from 'window-or-global'
export const STEP_ID = 'inputs'
@ -24,28 +26,53 @@ const Inputs = () => ({
resolver: () => STEP_FORM_SCHEMA(inputs),
optionsValidate: { abortEarly: false },
content: useCallback(() => {
const [fields, setFields] = useState([])
const { provisionsTemplates } = useProvision()
const [fields, setFields] = useState(undefined)
const { changeLoading } = useGeneral()
const { provisionsTemplates, 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 {
[PROVISION_TEMPLATE_ID]: provision,
[STEP_ID]: currentInputs
} = watch()
const provisionTemplate = provisionsTemplates
.find(({ name }) => name === provision?.[0])
const { [PROVIDER_ID]: providerSelected, [STEP_ID]: currentInputs } = watch()
inputs = provisionTemplate?.inputs ?? []
setFields(FORM_FIELDS(inputs))
// set defaults inputs values when first render
!currentInputs && reset({
...watch(),
[STEP_ID]: STEP_FORM_SCHEMA(inputs).default()
})
if (!currentInputs) {
changeLoading(true) // disable finish button until provider is fetched
fetchRequest({ id: providerSelected[0] })
} else {
setFields(FORM_FIELDS(inputs))
}
}, [])
useEffect(() => {
if (fetchData) {
const { [TEMPLATE_ID]: provisionTemplateSelected = [] } = watch()
const { TEMPLATE: { PROVISION_BODY } = {} } = fetchData
const provisionTemplate = getProvisionTemplateByDir(provisionTemplateSelected?.[0])
// MERGE INPUTS provision template + PROVISION_BODY.inputs (provider fetch)
inputs = provisionTemplate.inputs.map(templateInput =>
PROVISION_BODY.inputs.find(
providerInput => providerInput.name === templateInput.name
) || templateInput
) ?? []
setFields(FORM_FIELDS(inputs))
reset({ ...watch(), [STEP_ID]: STEP_FORM_SCHEMA(inputs).default() })
}
}, [fetchData])
if (!fields && loading) {
return <LinearProgress color='secondary' />
}
return (fields?.length === 0) ? (
<EmptyCard title={'✔️ There is not inputs to fill'} />
) : (

View File

@ -1,13 +1,14 @@
import React, { useCallback, useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import { useWatch } from 'react-hook-form'
import { useFetch, useProvision, useListForm } from 'client/hooks'
import { useProvision, useListForm } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionCard } from 'client/components/Cards'
import { PATH } from 'client/router/provision'
import { T } from 'client/constants'
import { STEP_FORM_SCHEMA } from './schema'
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/Create/Steps/Inputs'
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Provisions/Form/Create/Steps/Template'
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/Create/Steps/Provider/schema'
export const STEP_ID = 'provider'
@ -16,44 +17,46 @@ const Provider = () => ({
label: T.Provider,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { getProviders } = useProvision()
const { data: providers, fetchRequest, loading, error } = useFetch(
getProviders
)
const { providers } = useProvision()
const template = useWatch({ name: TEMPLATE_ID })
const templateSelected = template?.[0] ?? {}
const { handleSelect, handleUnselect } = useListForm({
key: STEP_ID,
setList: setFormData
})
const providersByTypeAndService = React.useMemo(() =>
providers.filter(({ TEMPLATE: { PLAIN = {} } = {} }) =>
PLAIN.provider === templateSelected.provider &&
PLAIN.provision_type === templateSelected.provision
)
, [providers])
useEffect(() => { fetchRequest() }, [])
const {
handleSelect,
handleUnselect
} = useListForm({ key: STEP_ID, setList: setFormData })
useEffect(() => {
if (providers) {
// delete provider selected in template if not exists
const provider = providers?.some(({ NAME }) => NAME === data?.[0])
!provider && handleUnselect(data?.[0])
}
}, [providers])
// delete provider selected at template if not exists
const existsProvider = providers?.some(({ ID }) => ID === data?.[0])
!existsProvider && handleUnselect(data?.[0])
}, [])
if (error) {
return <Redirect to={PATH.DASHBOARD} />
const handleClick = (id, isSelected) => {
// reset inputs when change provider
setFormData(prev => ({ ...prev, [INPUTS_ID]: undefined }))
isSelected ? handleUnselect(id) : handleSelect(id)
}
return (
<ListCards
list={providers}
isLoading={!providers && loading}
list={providersByTypeAndService}
EmptyComponent={<EmptyCard title={'Your providers list is empty'} />}
CardComponent={ProvisionCard}
cardsProps={({ value: { NAME } }) => {
const isSelected = data?.some(selected => selected === NAME)
cardsProps={({ value: { ID } }) => {
const isSelected = data?.some(selected => selected === ID)
return {
isProvider: true,
isSelected,
handleClick: () =>
isSelected ? handleUnselect(NAME) : handleSelect(NAME)
handleClick: () => handleClick(ID, isSelected)
}
}}
breakpoints={{ xs: 12, sm: 6, md: 4 }}

View File

@ -1,67 +0,0 @@
import React, { useCallback, useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import { useFetch, useProvision, useListForm } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
import { PATH } from 'client/router/provision'
import { T } from 'client/constants'
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/Create/Steps/Inputs'
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/Create/Steps/Provision/schema'
export const STEP_ID = 'provision'
const Provision = () => ({
id: STEP_ID,
label: T.ProvisionTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const { getTemplates } = useProvision()
const { data: templates, fetchRequest, loading, error } = useFetch(
getTemplates
)
const { handleSelect, handleUnselect } = useListForm({
key: STEP_ID,
setList: setFormData
})
useEffect(() => { fetchRequest() }, [])
const handleClick = (nameTemplate, nameProvider, isSelected) => {
setFormData(({ [INPUTS_ID]: undefined, [PROVIDER_ID]: [nameProvider] }))
isSelected ? handleUnselect(nameTemplate) : handleSelect(nameTemplate)
}
if (error) {
return <Redirect to={PATH.DASHBOARD} />
}
return (
<ListCards
list={templates}
keyProp='name'
isLoading={!templates && loading}
EmptyComponent={
<EmptyCard title={'Your provisions templates list is empty'} />
}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value: { name, defaults = {} } }) => {
const isSelected = data?.some(selected => selected === name)
const { provision: { provider } = {} } = defaults
return {
isSelected,
title: name,
handleClick: () => handleClick(name, provider, isSelected)
}
}}
breakpoints={{ xs: 12, sm: 6, md: 4 }}
/>
)
}, [])
})
export default Provision

View File

@ -1,8 +0,0 @@
import * as yup from 'yup'
export const STEP_FORM_SCHEMA = yup
.array(yup.string().trim())
.min(1, 'Select provision template')
.max(1, 'Max. one template selected')
.required('Provision template field is required')
.default([])

View File

@ -0,0 +1,121 @@
import React, { useCallback } from 'react'
import { Divider, Select, Breadcrumbs } from '@material-ui/core'
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
import { useProvision, useListForm, useGeneral } from 'client/hooks'
import { ListCards } from 'client/components/List'
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/Create/Steps/Inputs'
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/Create/Steps/Template/schema'
export const STEP_ID = 'template'
const Template = () => ({
id: STEP_ID,
label: T.ProvisionTemplate,
resolver: () => STEP_FORM_SCHEMA,
content: useCallback(({ data, setFormData }) => {
const templateSelected = data?.[0]
const [provisionSelected, setProvision] = React.useState(templateSelected?.provision)
const [providerSelected, setProvider] = React.useState(templateSelected?.provider)
const { showError } = useGeneral()
const { provisionsTemplates, providers } = useProvision()
const providersTypes = provisionsTemplates?.[provisionSelected]?.provisions ?? []
const templatesAvailable = providersTypes?.[providerSelected] ?? []
const {
handleSelect,
handleUnselect,
handleClear
} = useListForm({ key: STEP_ID, setList: setFormData })
const handleChangeProvision = evt => {
setProvision(evt.target.value)
setProvider(undefined)
templateSelected && handleClear()
}
const handleChangeProvider = evt => {
setProvider(evt.target.value)
templateSelected && handleClear()
}
const handleClick = (template, isSelected) => {
const { name, provision_type: provisionType, provider, 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({ [INPUTS_ID]: undefined, [PROVIDER_ID]: [ID] })
isSelected
? handleUnselect(name, item => item.name === name)
: handleSelect({ name, provider, provision: provisionType })
}
}
const RenderOptions = ({ options = {} }) => Object.keys(options)?.map(option => (
<option key={option} value={option}>{option}</option>
))
return (
<>
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
<Select
color='secondary'
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'
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>
<Divider style={{ margin: '1rem 0' }} />
<ListCards
keyProp='name'
list={templatesAvailable}
EmptyComponent={
<EmptyCard title={'Your providers templates list is empty'} />
}
CardComponent={ProvisionTemplateCard}
cardsProps={({ value = {} }) => {
const isSelected = data?.some(selected =>
selected.name === value.name
)
return {
isSelected,
handleClick: () => handleClick(value, isSelected)
}
}}
/>
</>
)
}, [])
})
export default Template

View File

@ -0,0 +1,40 @@
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)
.min(1, 'Select provision template')
.max(1, 'Max. one template selected')
.required('Provision template field is required')
.default([])

View File

@ -1,19 +1,19 @@
import * as yup from 'yup'
import Provision from './Provision'
import Template from './Template'
import Provider from './Provider'
import Inputs from './Inputs'
const Steps = () => {
const provision = Provision()
const template = Template()
const provider = Provider()
const inputs = Inputs()
const steps = [provision, provider, inputs]
const steps = [template, provider, inputs]
const resolvers = () => yup
.object({
[provision.id]: provision.resolver(),
[template.id]: template.resolver(),
[provider.id]: provider.resolver(),
[inputs.id]: inputs.resolver()
})

View File

@ -1,15 +1,15 @@
import React, { useState } from 'react'
import { useHistory } from 'react-router'
import React, { useState, useEffect } from 'react'
import { Redirect, useHistory } from 'react-router'
import { Container } from '@material-ui/core'
import { Container, LinearProgress } from '@material-ui/core'
import { useForm, FormProvider } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers'
import { useGeneral, useProvision, useSocket } from 'client/hooks'
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 { PATH } from 'client/router/provision'
import { set, mapUserInputs } from 'client/utils'
@ -18,7 +18,8 @@ function ProvisionCreateForm () {
const history = useHistory()
const { showError } = useGeneral()
const { getProvision } = useSocket()
const { createProvision, provisionsTemplates } = useProvision()
const { getProviders, createProvision, provisionsTemplates, providers } = useProvision()
const { data, fetchRequest, loading, error } = useFetch(getProviders)
const { steps, defaultValues, resolvers } = Steps()
@ -28,25 +29,42 @@ 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 { provision, provider, inputs } = formData
const provisionSelected = provision[0]
const providerSelected = provider[0]
const { template, provider, inputs } = formData
const provisionTemplateSelected = template?.[0] ?? {}
const providerIdSelected = provider?.[0]
const providerName = providers?.find(({ ID }) => ID === providerIdSelected)?.NAME
const provisionTemplate = provisionsTemplates
.find(({ name }) => name === provisionSelected)
const provisionTemplate = getProvisionTemplateByDir(provisionTemplateSelected)
if (!provisionTemplate) {
showError({
message: `
Cannot found provider template (${provisionSelected}),
ask your cloud administrator`
if (!provisionTemplate) return redirectWithError(provisionTemplateSelected?.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 => {
set(host, 'provision.provider_name', providerName)
})
history.push(PATH.PROVISIONS.LIST)
}
set(provisionTemplate, 'defaults.provision.provider', providerSelected)
const parseInputs = mapUserInputs(inputs)
const formatData = {
...provisionTemplate,
@ -57,11 +75,19 @@ function ProvisionCreateForm () {
createProvision({ data: formatData }).then(res => res && setUuid(res))
}
useEffect(() => { fetchRequest() }, [])
if (uuid) {
return <DebugLog uuid={uuid} socket={getProvision} />
}
return (
if (error) {
return <Redirect to={PATH.PROVIDERS.LIST} />
}
return (!data) || loading ? (
<LinearProgress color='secondary' />
) : (
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
<FormProvider {...methods}>
<FormStepper steps={steps} schema={resolvers} onSubmit={onSubmit} />

View File

@ -13,17 +13,27 @@ const useRequest = request => {
const doFetch = useCallback(
debounce(payload =>
request({ ...payload }).then(response => {
if (isMounted.current) {
if (response !== undefined) {
setData(response)
setError(false)
} else setError(true)
setLoading(false)
setReloading(false)
}
})
request({ ...payload })
.then(response => {
if (isMounted.current) {
if (response !== undefined) {
setData(response)
setError(false)
} else setError(true)
}
})
.catch(() => {
if (isMounted.current) {
setData(undefined)
setError(true)
}
})
.finally(() => {
if (isMounted.current) {
setLoading(false)
setReloading(false)
}
})
), [isMounted])
const fetchRequest = useCallback((payload, options = {}) => {

View File

@ -25,12 +25,12 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
)
const handleUnselect = useCallback(
(id, filter = item => item === id) =>
(id, filter = item => item !== id) =>
setList(prevList => ({
...prevList,
[key]: prevList[key]?.filter(filter)
})),
[key, list]
[key, setList]
)
const handleClear = useCallback(

View File

@ -4,7 +4,8 @@ import { useSelector, useDispatch, shallowEqual } from 'react-redux'
import {
setProviders,
setProvisions,
setProvisionsTemplates
setProvisionsTemplates,
successOneRequest
} from 'client/actions/pool'
import { enqueueError, enqueueSuccess } from 'client/actions/general'
@ -16,15 +17,8 @@ export default function useProvision () {
const {
providers,
provisionsTemplates,
provisions,
filterPool: filter
} = useSelector(
state => ({
...state?.Opennebula,
filterPool: state?.Authenticated?.filterPool
}),
shallowEqual
)
provisions
} = useSelector(({ Opennebula }) => Opennebula, shallowEqual)
// --------------------------------------------
// ALL PROVISION TEMPLATES REQUESTS
@ -33,7 +27,7 @@ export default function useProvision () {
const getProvisionsTemplates = useCallback(
() =>
serviceProvision
.getProvisionsTemplates({ filter })
.getProvisionsTemplates({})
.then(doc => {
dispatch(setProvisionsTemplates(doc))
return doc
@ -50,18 +44,24 @@ export default function useProvision () {
// --------------------------------------------
const getProvider = useCallback(
({ id }) =>
serviceProvision.getProvider({ id }).catch(err => {
dispatch(enqueueError(err ?? `Error GET (${id}) provider`))
throw err
}),
({ id } = {}) =>
serviceProvision
.getProvider({ id })
.then(doc => {
dispatch(successOneRequest())
return doc
})
.catch(err => {
dispatch(enqueueError(err ?? `Error GET (${id}) provider`))
throw err
}),
[dispatch]
)
const getProviders = useCallback(
({ end, start } = { end: -1, start: -1 }) =>
serviceProvision
.getProviders({ filter, end, start })
.getProviders({ end, start })
.then(doc => {
dispatch(setProviders(doc))
return doc
@ -70,7 +70,7 @@ export default function useProvision () {
dispatch(enqueueError(err ?? 'Error GET providers'))
return err
}),
[dispatch, filter]
[dispatch]
)
const createProvider = useCallback(
@ -79,7 +79,7 @@ export default function useProvision () {
.createProvider({ data })
.then(id => dispatch(enqueueSuccess(`Provider created - ID: ${id}`)))
.catch(err => dispatch(enqueueError(err ?? 'Error CREATE provider')))
, [dispatch, providers]
, [dispatch]
)
const updateProvider = useCallback(
@ -88,20 +88,17 @@ export default function useProvision () {
.updateProvider({ id, data })
.then(() => dispatch(enqueueSuccess(`Provider updated - ID: ${id}`)))
.catch(err => dispatch(enqueueError(err ?? 'Error UPDATE provider')))
, [dispatch, providers]
, [dispatch]
)
const deleteProvider = useCallback(
({ id }) =>
serviceProvision
.deleteProvider({ id })
.then(() => {
const newList = providers.filter(({ ID }) => ID !== id)
dispatch(enqueueSuccess(`Provider deleted - ID: ${id}`))
dispatch(setProviders(newList))
})
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE provider')))
, [dispatch, providers]
.then(() => dispatch(enqueueSuccess(`Provider deleted - ID: ${id}`)))
.then(() => getProviders())
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE provider'))),
[dispatch]
)
// --------------------------------------------
@ -119,7 +116,7 @@ export default function useProvision () {
const getProvisions = useCallback(
({ end, start } = { end: -1, start: -1 }) =>
serviceProvision
.getProvisions({ filter, end, start })
.getProvisions({ end, start })
.then(doc => {
dispatch(setProvisions(doc))
return doc
@ -128,7 +125,7 @@ export default function useProvision () {
dispatch(enqueueError(err?.message ?? 'Error GET provisions'))
return err
}),
[dispatch, filter]
[dispatch]
)
const createProvision = useCallback(
@ -142,7 +139,7 @@ export default function useProvision () {
.catch(err => {
dispatch(enqueueError(err?.message ?? 'Error CREATE Provision'))
}),
[dispatch, provisions]
[dispatch]
)
const deleteProvision = useCallback(
@ -151,7 +148,7 @@ export default function useProvision () {
.deleteProvision({ id })
.then(() => dispatch(enqueueSuccess(`Provision deleted - ID: ${id}`)))
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE provision')))
, [dispatch, provisions]
, [dispatch]
)
const getProvisionLog = useCallback(
@ -175,7 +172,7 @@ export default function useProvision () {
return doc
})
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE datastore')))
, [dispatch, provisions]
, [dispatch]
)
const deleteVNetwork = useCallback(
@ -187,7 +184,7 @@ export default function useProvision () {
return doc
})
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE network')))
, [dispatch, provisions]
, [dispatch]
)
const deleteHost = useCallback(
@ -199,7 +196,7 @@ export default function useProvision () {
return doc
})
.catch(err => dispatch(enqueueError(err ?? 'Error DELETE host')))
, [dispatch, provisions]
, [dispatch]
)
const configureHost = useCallback(
@ -211,7 +208,7 @@ export default function useProvision () {
return doc
})
.catch(err => dispatch(enqueueError(err ?? 'Error CONFIGURE host')))
, [dispatch, provisions]
, [dispatch]
)
return {

View File

@ -57,6 +57,8 @@ const General = (state = initial, action) => {
notification => notification.key !== action.key
)
}
case GeneralActions.CHANGE_LOADING:
return { ...state, ...action.payload }
case GeneralActions.CHANGE_ZONE:
return { ...state, ...action.payload }
case GeneralActions.TOGGLE_MENU:

View File

@ -100,7 +100,10 @@ export const requestData = (url = '', data = {}) => {
type: err.message,
message: 'Error request: %s'
}
messageTerminal(configErrorParser)
process?.env?.NODE_ENV === 'development' &&
messageTerminal(configErrorParser)
return params.error(err)
})
}