From e01240979991d30fa0598c38f8b875e8f0501e10 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Tue, 12 Jan 2021 18:45:33 +0100 Subject: [PATCH] F #3951: Fix provision form (#628) --- src/fireedge/src/client/actions/general.js | 6 +- src/fireedge/src/client/actions/pool.js | 3 + .../client/components/Cards/ProvisionCard.js | 13 +- .../components/Cards/ProvisionTemplateCard.js | 5 +- .../components/Cards/SelectCard/index.js | 2 +- .../client/components/FormStepper/index.js | 8 +- .../Form/Create/Steps/Connection/index.js | 2 +- .../Form/Create/Steps/Template/index.js | 16 ++- .../containers/Providers/Form/Create/index.js | 62 +++++---- .../Form/Create/Steps/Inputs/index.js | 67 +++++++--- .../Form/Create/Steps/Provider/index.js | 57 +++++---- .../Form/Create/Steps/Provision/index.js | 67 ---------- .../Form/Create/Steps/Provision/schema.js | 8 -- .../Form/Create/Steps/Template/index.js | 121 ++++++++++++++++++ .../Form/Create/Steps/Template/schema.js | 40 ++++++ .../Provisions/Form/Create/Steps/index.js | 8 +- .../Provisions/Form/Create/index.js | 66 +++++++--- src/fireedge/src/client/hooks/useFetch.js | 32 +++-- src/fireedge/src/client/hooks/useListForm.js | 4 +- src/fireedge/src/client/hooks/useProvision.js | 67 +++++----- src/fireedge/src/client/reducers/general.js | 2 + src/fireedge/src/client/utils/utils.js | 5 +- 22 files changed, 415 insertions(+), 246 deletions(-) delete mode 100644 src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/index.js delete mode 100644 src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/schema.js create mode 100644 src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/index.js create mode 100644 src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/schema.js diff --git a/src/fireedge/src/client/actions/general.js b/src/fireedge/src/client/actions/general.js index 33418310fe..cd23921ff0 100644 --- a/src/fireedge/src/client/actions/general.js +++ b/src/fireedge/src/client/actions/general.js @@ -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 => ({ diff --git a/src/fireedge/src/client/actions/pool.js b/src/fireedge/src/client/actions/pool.js index b9ac2bcfe1..e194cac17b 100644 --- a/src/fireedge/src/client/actions/pool.js +++ b/src/fireedge/src/client/actions/pool.js @@ -129,6 +129,9 @@ module.exports = { startOneRequest: () => ({ type: START_ONE_REQUEST }), + successOneRequest: () => ({ + type: SUCCESS_ONE_REQUEST + }), failureOneRequest: error => ({ type: FAILURE_ONE_REQUEST, payload: { error } diff --git a/src/fireedge/src/client/components/Cards/ProvisionCard.js b/src/fireedge/src/client/components/Cards/ProvisionCard.js index 1a208a153a..8a69c9b28c 100644 --- a/src/fireedge/src/client/components/Cards/ProvisionCard.js +++ b/src/fireedge/src/client/components/Cards/ProvisionCard.js @@ -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 diff --git a/src/fireedge/src/client/components/Cards/ProvisionTemplateCard.js b/src/fireedge/src/client/components/Cards/ProvisionTemplateCard.js index d10cf885a9..1ccd8f9b72 100644 --- a/src/fireedge/src/client/components/Cards/ProvisionTemplateCard.js +++ b/src/fireedge/src/client/components/Cards/ProvisionTemplateCard.js @@ -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 }) diff --git a/src/fireedge/src/client/components/Cards/SelectCard/index.js b/src/fireedge/src/client/components/Cards/SelectCard/index.js index 6fa6794d7a..5aa883d8ae 100644 --- a/src/fireedge/src/client/components/Cards/SelectCard/index.js +++ b/src/fireedge/src/client/components/Cards/SelectCard/index.js @@ -36,7 +36,7 @@ const SelectCard = memo(({ return (
{children}
}> + wrap={children => {children}}> {observerOff || isNearScreen ? ( { 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 && } diff --git a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/index.js b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/index.js index 5abaf1874b..a5acb4ee79 100644 --- a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/index.js +++ b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/index.js @@ -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] diff --git a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Template/index.js b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Template/index.js index 7647d7ea82..621adc97b5 100644 --- a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Template/index.js +++ b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Template/index.js @@ -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 ( <> -
+ }> } -
+ { - 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) => ( diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Inputs/index.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Inputs/index.js index f761f16073..fe39197fb2 100644 --- a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Inputs/index.js +++ b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Inputs/index.js @@ -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 + } + return (fields?.length === 0) ? ( ) : ( diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provider/index.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provider/index.js index a377ab5c34..d5e06f0bfc 100644 --- a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provider/index.js +++ b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provider/index.js @@ -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 + const handleClick = (id, isSelected) => { + // reset inputs when change provider + setFormData(prev => ({ ...prev, [INPUTS_ID]: undefined })) + isSelected ? handleUnselect(id) : handleSelect(id) } return ( } 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 }} diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/index.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/index.js deleted file mode 100644 index e5760615de..0000000000 --- a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/index.js +++ /dev/null @@ -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 - } - - return ( - - } - 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 diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/schema.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/schema.js deleted file mode 100644 index 4ecc7fa90f..0000000000 --- a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Provision/schema.js +++ /dev/null @@ -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([]) diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/index.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/index.js new file mode 100644 index 0000000000..369b91d6f2 --- /dev/null +++ b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/index.js @@ -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 => ( + + )) + + return ( + <> + }> + + {provisionSelected && } + + + + } + CardComponent={ProvisionTemplateCard} + cardsProps={({ value = {} }) => { + const isSelected = data?.some(selected => + selected.name === value.name + ) + + return { + isSelected, + handleClick: () => handleClick(value, isSelected) + } + }} + /> + + ) + }, []) +}) + +export default Template diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/schema.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/schema.js new file mode 100644 index 0000000000..0c458aa127 --- /dev/null +++ b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/Template/schema.js @@ -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([]) diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/index.js b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/index.js index 8e52b2172a..16e3eb024e 100644 --- a/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/index.js +++ b/src/fireedge/src/client/containers/Provisions/Form/Create/Steps/index.js @@ -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() }) diff --git a/src/fireedge/src/client/containers/Provisions/Form/Create/index.js b/src/fireedge/src/client/containers/Provisions/Form/Create/index.js index 5bb457996a..a70eb6e97a 100644 --- a/src/fireedge/src/client/containers/Provisions/Form/Create/index.js +++ b/src/fireedge/src/client/containers/Provisions/Form/Create/index.js @@ -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 } - return ( + if (error) { + return + } + + return (!data) || loading ? ( + + ) : ( diff --git a/src/fireedge/src/client/hooks/useFetch.js b/src/fireedge/src/client/hooks/useFetch.js index 17a91dccb7..cbb2c9b5ad 100644 --- a/src/fireedge/src/client/hooks/useFetch.js +++ b/src/fireedge/src/client/hooks/useFetch.js @@ -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 = {}) => { diff --git a/src/fireedge/src/client/hooks/useListForm.js b/src/fireedge/src/client/hooks/useListForm.js index 888109e093..43ae7d1193 100644 --- a/src/fireedge/src/client/hooks/useListForm.js +++ b/src/fireedge/src/client/hooks/useListForm.js @@ -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( diff --git a/src/fireedge/src/client/hooks/useProvision.js b/src/fireedge/src/client/hooks/useProvision.js index 1e2296f10c..ce27dae4e4 100644 --- a/src/fireedge/src/client/hooks/useProvision.js +++ b/src/fireedge/src/client/hooks/useProvision.js @@ -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 { diff --git a/src/fireedge/src/client/reducers/general.js b/src/fireedge/src/client/reducers/general.js index 4d4a93b377..86708f6f29 100644 --- a/src/fireedge/src/client/reducers/general.js +++ b/src/fireedge/src/client/reducers/general.js @@ -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: diff --git a/src/fireedge/src/client/utils/utils.js b/src/fireedge/src/client/utils/utils.js index 1438df95ca..36b45633cd 100644 --- a/src/fireedge/src/client/utils/utils.js +++ b/src/fireedge/src/client/utils/utils.js @@ -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) }) }