diff --git a/share/oneprovision/edge-clusters/onprem/provisions/onprem.d/networks.yml b/share/oneprovision/edge-clusters/onprem/provisions/onprem.d/networks.yml index 7fd4935e1c..2f940cfc28 100644 --- a/share/oneprovision/edge-clusters/onprem/provisions/onprem.d/networks.yml +++ b/share/oneprovision/edge-clusters/onprem/provisions/onprem.d/networks.yml @@ -19,9 +19,7 @@ networks: - name: "${provision}-public" vn_mad: 'bridge' phydev: "${input.public_phydev}" - bridge: '${input.public_network_bridge}' netrole: 'public' - dns: "${input.dns}" ar: - type: IP4 ip: "${input.first_public_ip}" diff --git a/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west1-b.yml b/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west1-b.yml index 62b0f17788..21c8fc1980 100644 --- a/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west1-b.yml +++ b/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west1-b.yml @@ -5,7 +5,9 @@ provider: 'google' plain: image: 'GOOGLE' - location_key: 'zone' + location_key: + - 'region' + - 'zone' provision_type: 'virtual' connection: diff --git a/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west2-b.yml b/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west2-b.yml index 0874cc8255..8e5b356662 100644 --- a/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west2-b.yml +++ b/share/oneprovision/edge-clusters/virtual/providers/google/google-europe-west2-b.yml @@ -5,7 +5,9 @@ provider: 'google' plain: image: 'GOOGLE' - location_key: 'zone' + location_key: + - 'region' + - 'zone' provision_type: 'virtual' connection: diff --git a/share/oneprovision/edge-clusters/virtual/providers/google/google-us-east1-b.yml b/share/oneprovision/edge-clusters/virtual/providers/google/google-us-east1-b.yml index c2883de452..c28ffb243b 100644 --- a/share/oneprovision/edge-clusters/virtual/providers/google/google-us-east1-b.yml +++ b/share/oneprovision/edge-clusters/virtual/providers/google/google-us-east1-b.yml @@ -5,7 +5,9 @@ provider: 'google' plain: image: 'GOOGLE' - location_key: 'zone' + location_key: + - 'region' + - 'zone' provision_type: 'virtual' connection: diff --git a/share/oneprovision/edge-clusters/virtual/providers/google/google-us-west1-b.yml b/share/oneprovision/edge-clusters/virtual/providers/google/google-us-west1-b.yml index 3f7df1ed5a..cb88573800 100644 --- a/share/oneprovision/edge-clusters/virtual/providers/google/google-us-west1-b.yml +++ b/share/oneprovision/edge-clusters/virtual/providers/google/google-us-west1-b.yml @@ -5,7 +5,9 @@ provider: 'google' plain: image: 'GOOGLE' - location_key: 'zone' + location_key: + - 'region' + - 'zone' provision_type: 'virtual' connection: diff --git a/src/fireedge/.babelrc b/src/fireedge/.babelrc index 9d340a9dfa..d2b739cbfb 100644 --- a/src/fireedge/.babelrc +++ b/src/fireedge/.babelrc @@ -1,5 +1,15 @@ { - "presets": [ "@babel/preset-env", "@babel/preset-react" ], + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "10" + } + } + ], + "@babel/preset-react" + ], "plugins": [ ["module-resolver", { "root": ["./src"], diff --git a/src/fireedge/package.json b/src/fireedge/package.json index 1944988577..12670e0651 100644 --- a/src/fireedge/package.json +++ b/src/fireedge/package.json @@ -68,7 +68,6 @@ "babel-loader": "8.1.0", "babel-plugin-module-resolver": "4.0.0", "babel-preset-react-hmre": "1.1.1", - "body-parser": "1.19.0", "btoa": "1.2.1", "clsx": "1.1.1", "colors": "1.4.0", @@ -105,7 +104,7 @@ "react-flow-renderer": "5.11.1", "react-hook-form": "6.8.6", "react-json-pretty": "2.2.0", - "react-minimal-pie-chart": "8.1.0", + "react-minimal-pie-chart": "8.2.0", "react-redux": "7.2.1", "react-router": "5.2.0", "react-router-dom": "5.2.0", @@ -127,7 +126,7 @@ "xml2js": "0.4.23", "xmlrpc": "1.3.2", "yaml": "1.10.0", - "yup": "0.29.3", + "yup": "0.32.9", "zeromq": "5.2.0" } } diff --git a/src/fireedge/src/client/components/FormControl/FileController.js b/src/fireedge/src/client/components/FormControl/FileController.js new file mode 100644 index 0000000000..f73f3d6318 --- /dev/null +++ b/src/fireedge/src/client/components/FormControl/FileController.js @@ -0,0 +1,166 @@ +import React, { memo, useState, useRef, useEffect } from 'react' +import PropTypes from 'prop-types' +import clsx from 'clsx' + +import { makeStyles, FormControl, FormHelperText } from '@material-ui/core' +import { Check, InsertDriveFile } from '@material-ui/icons' +import { Controller } from 'react-hook-form' + +import { ErrorHelper, SubmitButton } from 'client/components/FormControl' + +const useStyles = makeStyles(theme => ({ + hide: { + display: 'none' + }, + label: { + display: 'flex', + alignItems: 'center', + gap: '1em', + padding: '0.5em', + borderBottom: `1px solid ${theme.palette.text.secondary}` + }, + button: { + '&:hover': { + backgroundColor: theme.palette.secondary.dark + } + }, + buttonSuccess: { + backgroundColor: theme.palette.success.main, + '&:hover': { + backgroundColor: theme.palette.success.dark + } + } +})) + +const FileController = memo( + ({ control, cy, name, label, error, fieldProps, validationBeforeTransform, transform, formContext }) => { + const { setValue, setError, clearErrors, watch, register } = formContext + + const classes = useStyles() + const [isLoading, setLoading] = useState(() => false) + const [success, setSuccess] = useState(() => !error && !!watch(name)) + const timer = useRef() + + useEffect(() => { + return () => { + clearTimeout(timer.current) + } + }, []) + + const handleDelayState = message => { + // simulate is loading for one second + timer.current = window.setTimeout(() => { + setSuccess(!message) + setLoading(false) + + message && setError(name, { type: 'manual', message }) + }, 1000) + } + + const handleChange = async event => { + try { + const file = event.target.files?.[0] + + if (!file) return + + setSuccess(false) + setLoading(true) + clearErrors(name) + + const errorMessage = validationBeforeTransform + ?.map(({ message, test }) => test(file) && message) + ?.filter(Boolean) + + if (errorMessage?.length) throw errorMessage[0] + + const parsedValue = transform ? await transform(file) : file + setValue(name, parsedValue) + handleDelayState() + } catch (message) { + setValue(name, undefined) + handleDelayState(message) + } + } + + return ( + + ( + + )} + name={name} + control={control} + /> + + {Boolean(error) && ( + + + + )} + + ) + }, + (prevProps, nextProps) => + prevProps.error === nextProps.error && prevProps.type === nextProps.type +) + +FileController.propTypes = { + control: PropTypes.object, + cy: PropTypes.string, + multiline: PropTypes.bool, + name: PropTypes.string.isRequired, + label: PropTypes.string, + error: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.objectOf(PropTypes.any) + ]), + validationBeforeTransform: PropTypes.arrayOf( + PropTypes.shape({ + message: PropTypes.string, + test: PropTypes.func + }) + ), + transform: PropTypes.func, + fieldProps: PropTypes.object, + formContext: PropTypes.shape({ + setValue: PropTypes.func, + setError: PropTypes.func, + clearErrors: PropTypes.func, + watch: PropTypes.func, + register: PropTypes.func + }) +} + +FileController.defaultProps = { + control: {}, + cy: 'cy', + name: '', + label: '', + error: false, + validationBeforeTransform: undefined, + transform: undefined, + fieldProps: undefined +} + +FileController.displayName = 'FileController' + +export default FileController diff --git a/src/fireedge/src/client/components/FormControl/index.js b/src/fireedge/src/client/components/FormControl/index.js index 79056399e3..7342bbaaa8 100644 --- a/src/fireedge/src/client/components/FormControl/index.js +++ b/src/fireedge/src/client/components/FormControl/index.js @@ -4,6 +4,7 @@ import SelectController from 'client/components/FormControl/SelectController' import SliderController from 'client/components/FormControl/SliderController' import CheckboxController from 'client/components/FormControl/CheckboxController' import AutocompleteController from 'client/components/FormControl/AutocompleteController' +import FileController from 'client/components/FormControl/FileController' import SubmitButton from 'client/components/FormControl/SubmitButton' import InputCode from 'client/components/FormControl/InputCode' @@ -16,6 +17,7 @@ export { SliderController, CheckboxController, AutocompleteController, + FileController, SubmitButton, InputCode, diff --git a/src/fireedge/src/client/components/FormStepper/Stepper.js b/src/fireedge/src/client/components/FormStepper/Stepper.js index dbc0d6223c..7e3ae17d73 100644 --- a/src/fireedge/src/client/components/FormStepper/Stepper.js +++ b/src/fireedge/src/client/components/FormStepper/Stepper.js @@ -21,7 +21,7 @@ const useStyles = makeStyles(theme => ({ position: 'sticky', top: -15, minHeight: 100, - background: fade(theme.palette.background.paper, 0.65), + background: fade(theme.palette.background.paper, 0.95), zIndex: theme.zIndex.mobileStepper }, icon: { @@ -79,7 +79,7 @@ const CustomStepper = ({ error: classes.error } }} - {...(Boolean(errors[id]) && { error: true })} + {...(Boolean(errors[id]?.message) && { error: true })} >{Tr(label)} diff --git a/src/fireedge/src/client/components/FormStepper/index.js b/src/fireedge/src/client/components/FormStepper/index.js index 082f4c26b9..fe58836588 100644 --- a/src/fireedge/src/client/components/FormStepper/index.js +++ b/src/fireedge/src/client/components/FormStepper/index.js @@ -37,19 +37,19 @@ const FormStepper = ({ steps, schema, onSubmit }) => { .then(() => ({ id, data: stepData })) } - const setErrors = ({ inner: errors = [], ...rest }) => { + const setErrors = ({ inner = [], ...rest }) => { changeLoading(false) - const errorsByPath = groupBy(errors, 'path') ?? {} + const errorsByPath = groupBy(inner, 'path') ?? {} const totalErrors = Object.keys(errorsByPath).length totalErrors > 0 ? setError(id, { - type: 'manual', - message: `${totalErrors} error(s) occurred` - }) + type: 'manual', + message: `${totalErrors} error(s) occurred` + }) : setError(id, rest) - errors?.forEach(({ path, type, message }) => + inner?.forEach(({ path, type, message }) => setError(`${id}.${path}`, { type, message }) ) } diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index 23071de1d1..76ed815c2f 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -14,13 +14,14 @@ const InputController = { [INPUT_TYPES.SELECT]: FC.SelectController, [INPUT_TYPES.SLIDER]: FC.SliderController, [INPUT_TYPES.CHECKBOX]: FC.CheckboxController, - [INPUT_TYPES.AUTOCOMPLETE]: FC.AutocompleteController + [INPUT_TYPES.AUTOCOMPLETE]: FC.AutocompleteController, + [INPUT_TYPES.FILE]: FC.FileController } const HiddenInput = ({ isHidden, children }) => isHidden ? {children} : children const FormWithSchema = ({ id, cy, fields }) => { - const { control, errors } = useFormContext() + const { control, errors, ...formContext } = useFormContext() return ( @@ -48,12 +49,13 @@ const FormWithSchema = ({ id, cy, fields }) => { {React.createElement(InputController[type], { control, cy: dataCy, - type: htmlTypeValue, + error: inputError, + formContext, name: inputName, + type: htmlTypeValue, values: typeof values === 'function' ? values(dependValue) : values, - error: inputError, ...restOfProps })} diff --git a/src/fireedge/src/client/components/Widgets/TotalProviders/index.js b/src/fireedge/src/client/components/Widgets/TotalProviders/index.js index 86b9a0dffa..f46f03e42d 100644 --- a/src/fireedge/src/client/components/Widgets/TotalProviders/index.js +++ b/src/fireedge/src/client/components/Widgets/TotalProviders/index.js @@ -21,7 +21,7 @@ const TotalProviders = () => { const chartData = React.useMemo(() => { const groups = groupBy(providers, 'TEMPLATE.PLAIN.provider') - return PROVIDERS_TYPES?.map(({ id, name, color }) => ({ + return Object.values(PROVIDERS_TYPES).map(({ id, name, color }) => ({ color, title: name, value: groups[id]?.length ?? 0 diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index a02e8b14ce..eb86da0b77 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -61,7 +61,8 @@ export const INPUT_TYPES = { SELECT: 'select', CHECKBOX: 'checkbox', SLIDER: 'slider', - AUTOCOMPLETE: 'autocomplete' + AUTOCOMPLETE: 'autocomplete', + FILE: 'file' } export const DEBUG_LEVEL = { diff --git a/src/fireedge/src/client/constants/provision.js b/src/fireedge/src/client/constants/provision.js index 9f855cd1ed..c17a34d57d 100644 --- a/src/fireedge/src/client/constants/provision.js +++ b/src/fireedge/src/client/constants/provision.js @@ -34,30 +34,35 @@ export const PROVISIONS_STATES = [ } ] -export const PROVIDERS_TYPES = [ - { +export const PROVIDERS_TYPES = { + aws: { id: 'aws', name: 'AWS', color: '#ef931f' }, - { + packet: { id: 'packet', name: 'Packet', color: '#364562' }, - { + dummy: { id: 'dummy', name: 'Dummy', color: '#436637' }, - { + google: { id: 'google', name: 'Google Cloud', - color: 'linear-gradient(90deg, #fbbc05 0%, #ea4335 33%, #34a853 66%, #4285f4 100%)' + color: '#dc382b' }, - { + digitalocean: { id: 'digitalocean', name: 'Digital Ocean', color: '#2381f5' } -] +} + +export const CREDENTIALS_FILE = { + // Google Cloud provider needs an input file to credential connection + [PROVIDERS_TYPES.google.id]: 'credentials' +} 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 8102b2f570..479cfa784f 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 @@ -1,36 +1,58 @@ import React, { useCallback, useEffect, useState } from 'react' +import { useFormContext } from 'react-hook-form' import FormWithSchema from 'client/components/Forms/FormWithSchema' import { EmptyCard } from 'client/components/Cards' +import { capitalize } from 'client/utils' import { T } from 'client/constants' +import * as ProviderTemplateModel from 'client/models/ProviderTemplate' import { FORM_FIELDS, STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Connection/schema' +import { + STEP_ID as TEMPLATE_ID +} from 'client/containers/Providers/Form/Create/Steps/Template' + export const STEP_ID = 'connection' let connection = {} +let providerType -const Connection = () => ({ +const Connection = ({ isUpdate }) => ({ id: STEP_ID, label: T.ConfigureConnection, - resolver: () => STEP_FORM_SCHEMA(connection), + resolver: () => STEP_FORM_SCHEMA({ connection, providerType }), optionsValidate: { abortEarly: false }, content: useCallback(({ data }) => { const [fields, setFields] = useState([]) + const { watch } = useFormContext() useEffect(() => { - connection = data - setFields(FORM_FIELDS(connection)) + const { [TEMPLATE_ID]: templateSelected, [STEP_ID]: currentConnection } = watch() + + const { provider, ...template } = templateSelected?.[0] + + providerType = provider + + connection = isUpdate + // when is updating, connections have the name as input label + ? Object.keys(currentConnection) + .reduce((res, name) => ({ ...res, [name]: capitalize(name) }), {}) + // set connections from template, to take value as input labels + : ProviderTemplateModel.getConnectionEditable(template) + + setFields(FORM_FIELDS({ connection, providerType })) }, [data]) return (fields?.length === 0) ? ( - + ) : ( ) }, []) }) +export * from 'client/containers/Providers/Form/Create/Steps/Connection/schema' export default Connection diff --git a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/schema.js b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/schema.js index 263e70cefe..4a44a466e9 100644 --- a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/schema.js +++ b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/Connection/schema.js @@ -1,19 +1,46 @@ import * as yup from 'yup' -import { INPUT_TYPES } from 'client/constants' -import { capitalize, getValidationFromFields } from 'client/utils' +import { INPUT_TYPES, CREDENTIALS_FILE } from 'client/constants' +import { getValidationFromFields, isBase64, prettyBytes } from 'client/utils' -export const FORM_FIELDS = connection => - Object.entries(connection)?.map(([name, value]) => ({ - name, - label: capitalize(name), - type: INPUT_TYPES.PASSWORD, - validation: yup +const MAX_SIZE_JSON = 102_400 +const JSON_FORMAT = 'application/json' + +export const FORM_FIELDS = ({ connection, providerType }) => + Object.entries(connection)?.map(([name, label]) => { + const isInputFile = CREDENTIALS_FILE[providerType] === String(name).toLowerCase() + + let validation = yup .string() .trim() .required(`${name} field is required`) - .default(value) - })) + .default(undefined) -export const STEP_FORM_SCHEMA = connection => yup.object( - getValidationFromFields(FORM_FIELDS(connection)) + if (isInputFile) { + validation = validation.test('is-base64', 'File has invalid format', isBase64) + } + + return { + name, + label, + type: isInputFile ? INPUT_TYPES.FILE : INPUT_TYPES.PASSWORD, + validation, + ...(isInputFile && { + fieldProps: { accept: JSON_FORMAT }, + validationBeforeTransform: [{ + message: `Only the following formats are accepted: ${JSON_FORMAT}`, + test: value => value?.type !== JSON_FORMAT + }, { + message: `The file is too large. Max ${prettyBytes(MAX_SIZE_JSON, '')}`, + test: value => value?.size > MAX_SIZE_JSON + }], + transform: async file => { + const json = await new Response(file).json() + return btoa(JSON.stringify(json)) + } + }) + } + }) + +export const STEP_FORM_SCHEMA = props => yup.object( + getValidationFromFields(FORM_FIELDS(props)) ) 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 880c9bcdc0..0967040aee 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,18 +1,19 @@ import React, { useCallback, useEffect } from 'react' -import { Divider, Select, Breadcrumbs } from '@material-ui/core' +import { Divider, Select, Breadcrumbs, InputLabel, FormControl } from '@material-ui/core' import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded' import Marked from 'marked' import { useProvision, useListForm } from 'client/hooks' import { ListCards } from 'client/components/List' -import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards' +import { ProvisionTemplateCard } from 'client/components/Cards' import { sanitize } from 'client/utils' import * as ProviderTemplateModel from 'client/models/ProviderTemplate' import { T } from 'client/constants' +import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Template/schema' + import { STEP_ID as CONFIGURATION_ID } from 'client/containers/Providers/Form/Create/Steps/BasicConfiguration' import { STEP_ID as CONNECTION_ID } from 'client/containers/Providers/Form/Create/Steps/Connection' -import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Template/schema' export const STEP_ID = 'template' @@ -59,14 +60,12 @@ const Template = () => ({ } const handleClick = (template, isSelected) => { - const { name, description, plain = {}, connection } = template - const { location_key: locationKey = '' } = plain - const { [locationKey]: _, ...connectionEditable } = connection ?? {} + const { name, description } = template // reset rest of form when change template setFormData({ [CONFIGURATION_ID]: { name, description }, - [CONNECTION_ID]: connectionEditable + [CONNECTION_ID]: {} }) isSelected @@ -94,28 +93,40 @@ const Template = () => ({ <> {/* -- SELECTORS -- */} }> - - {provisionSelected && } + + + {'Provision type'} + + + + + + {'Provider type'} + + + {/* -- DESCRIPTION -- */} @@ -129,22 +140,10 @@ const Template = () => ({ - } gridProps={{ 'data-cy': 'providers-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 = ProviderTemplateModel.isValidProviderTemplate(value) return { diff --git a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/index.js b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/index.js index e42cf15747..333338aebc 100644 --- a/src/fireedge/src/client/containers/Providers/Form/Create/Steps/index.js +++ b/src/fireedge/src/client/containers/Providers/Form/Create/Steps/index.js @@ -7,7 +7,7 @@ import Connection from './Connection' const Steps = ({ isUpdate }) => { const template = Template() const configuration = BasicConfiguration({ isUpdate }) - const connection = Connection() + const connection = Connection({ isUpdate }) const steps = [configuration, connection] !isUpdate && steps.unshift(template) diff --git a/src/fireedge/src/client/containers/Providers/Form/Create/index.js b/src/fireedge/src/client/containers/Providers/Form/Create/index.js index a95abfa3d4..6bba6abd00 100644 --- a/src/fireedge/src/client/containers/Providers/Form/Create/index.js +++ b/src/fireedge/src/client/containers/Providers/Form/Create/index.js @@ -39,31 +39,26 @@ function ProviderCreateForm () { history.push(PATH.PROVIDERS.LIST) } - const callCreateProvider = formData => { + const callCreateProvider = async formData => { const { template, configuration, connection } = formData const templateSelected = template?.[0] - const { name, description } = configuration const isValid = ProviderTemplateModel.isValidProviderTemplate(templateSelected) !isValid && redirectWithError(` - The template selected has a bad format. - Ask your cloud administrator` + The template selected has a bad format. + Ask your cloud administrator` ) - const { inputs, plain, provider } = templateSelected - const { location_key: locationKey } = plain - - const connectionFixed = templateSelected.connection?.[locationKey] + const { name, description } = configuration + const connectionFixed = ProviderTemplateModel.getConnectionFixed(templateSelected) const formatData = { - connection: { ...connection, [locationKey]: connectionFixed }, + ...templateSelected, + connection: { ...connection, ...connectionFixed }, description, - inputs, - name, - plain, - provider + name } createProvider({ data: formatData }) @@ -75,17 +70,12 @@ function ProviderCreateForm () { const { description } = configuration const [provider = {}, connection = []] = data - const { - PLAIN: { location_key: locationKey } = {}, - PROVISION_BODY: currentBodyTemplate - } = provider?.TEMPLATE - - const { [locationKey]: connectionFixed } = connection + const { PROVISION_BODY: currentBodyTemplate } = provider?.TEMPLATE const formatData = { ...currentBodyTemplate, description, - connection: { ...connectionEditable, [locationKey]: connectionFixed } + connection: { ...connection, ...connectionEditable } } updateProvider({ id, data: formatData }) @@ -109,15 +99,17 @@ function ProviderCreateForm () { const [provider = {}, connection = []] = data const { - PLAIN: { location_key: locationKey } = {}, - PROVISION_BODY: { description, name } + PLAIN = {}, + PROVISION_BODY: { description, ...currentBodyTemplate } } = provider?.TEMPLATE - const { [locationKey]: _, ...connectionEditable } = connection + const connectionEditable = ProviderTemplateModel + .getConnectionEditable({ plain: PLAIN, connection }) methods.reset({ + template: [currentBodyTemplate], connection: connectionEditable, - configuration: { name, description } + configuration: { description } }, { errors: false }) } }, [data]) diff --git a/src/fireedge/src/client/models/ProviderTemplate.js b/src/fireedge/src/client/models/ProviderTemplate.js index 2255c39408..af09a729f7 100644 --- a/src/fireedge/src/client/models/ProviderTemplate.js +++ b/src/fireedge/src/client/models/ProviderTemplate.js @@ -1,11 +1,36 @@ export const isValidProviderTemplate = ({ name, provider, plain = {}, connection }) => { const { provision_type: provisionType, location_key: locationKey } = plain + const keys = typeof locationKey === 'string' ? locationKey.split(',') : locationKey + const hasConnection = connection !== undefined - const locationKeyConnectionNotExists = !hasConnection || connection?.[locationKey] === undefined + + const locationKeyConnectionNotExists = + !hasConnection || keys.some(key => connection?.[key] === undefined) return ( !(locationKey && locationKeyConnectionNotExists) || [name, provisionType, provider].includes(undefined) ) } + +export const getLocationKeys = ({ location_key: locationKey }) => + typeof locationKey === 'string' ? locationKey.split(',') : locationKey + +export const getConnectionFixed = ({ connection = {}, ...template }) => { + const keys = getLocationKeys(template?.plain) + + return Object.entries(connection).reduce((res, [name, value]) => ({ + ...res, + ...keys.includes(name) && { [name]: value } + }), {}) +} + +export const getConnectionEditable = ({ connection = {}, ...template }) => { + const keys = getLocationKeys(template?.plain) + + return Object.entries(connection).reduce((res, [name, value]) => ({ + ...res, + ...!keys.includes(name) && { [name]: value } + }), {}) +} diff --git a/src/fireedge/src/client/types/provision.js b/src/fireedge/src/client/types/provision.js index 2326ea8762..570bdcff65 100644 --- a/src/fireedge/src/client/types/provision.js +++ b/src/fireedge/src/client/types/provision.js @@ -55,7 +55,10 @@ export const ProvisionHost = PropTypes.shape({ export const ProviderPlainInfo = PropTypes.shape({ image: PropTypes.string, - location_key: PropTypes.string, + location_key: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string) + ]), provision_type: ProvisionType.isRequired }) diff --git a/src/fireedge/src/client/utils/helpers.js b/src/fireedge/src/client/utils/helpers.js index 1a41e841fa..2453fefa81 100644 --- a/src/fireedge/src/client/utils/helpers.js +++ b/src/fireedge/src/client/utils/helpers.js @@ -111,3 +111,23 @@ export const groupBy = (array, key) => }, {}) export const cloneObject = obj => JSON.parse(JSON.stringify(obj)) + +/** + * Check if value is in base64 + * + * @param {String} stringToValidate String to check + * @param {Boolean} options.exact Only match and exact string + * @returns {Boolean} + */ +export const isBase64 = (stringToValidate, options = {}) => { + if (stringToValidate === '') return false + + const { exact = true } = options + + const BASE64_REG = /(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)/g + const EXACT_BASE64_REG = /(?:^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$)/ + + const regex = exact ? EXACT_BASE64_REG : BASE64_REG + + return regex.test(stringToValidate) +} diff --git a/src/fireedge/src/server/index.js b/src/fireedge/src/server/index.js index f7c8f38f7b..87838bbfe7 100644 --- a/src/fireedge/src/server/index.js +++ b/src/fireedge/src/server/index.js @@ -5,7 +5,6 @@ import helmet from 'helmet' import morgan from 'morgan' import cors from 'cors' import compression from 'compression' -import bodyParser from 'body-parser' import { env } from 'process' import { accessSync, @@ -124,8 +123,8 @@ if (appConfig.cors) { app.use(cors()) } // post params parser body -app.use(bodyParser.urlencoded({ extended: false })) -app.use(bodyParser.json()) +app.use(express.urlencoded({ extended: false })) +app.use(express.json()) app.use(`${basename}/api`, entrypointApi) // opennebula Api routes const frontApps = Object.keys(defaultApps) diff --git a/src/fireedge/src/server/routes/api/provision/schemas.js b/src/fireedge/src/server/routes/api/provision/schemas.js index 02db058ef9..d196abdc63 100644 --- a/src/fireedge/src/server/routes/api/provision/schemas.js +++ b/src/fireedge/src/server/routes/api/provision/schemas.js @@ -12,7 +12,7 @@ /* See the License for the specific language governing permissions and */ /* limitations under the License. */ /* -------------------------------------------------------------------------- */ -const providers = ['aws', 'packet', 'dummy'] +const providers = ['aws', 'packet', 'dummy', 'google', 'digitalocean'] const provider = { id: '/Provider',