mirror of
https://github.com/OpenNebula/one.git
synced 2024-12-22 13:33:52 +03:00
M #~: Google and digitalocean support in Fireedge (#1177)
This commit is contained in:
parent
6deedca690
commit
0c2956e138
@ -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}"
|
||||
|
@ -5,7 +5,9 @@ provider: 'google'
|
||||
|
||||
plain:
|
||||
image: 'GOOGLE'
|
||||
location_key: 'zone'
|
||||
location_key:
|
||||
- 'region'
|
||||
- 'zone'
|
||||
provision_type: 'virtual'
|
||||
|
||||
connection:
|
||||
|
@ -5,7 +5,9 @@ provider: 'google'
|
||||
|
||||
plain:
|
||||
image: 'GOOGLE'
|
||||
location_key: 'zone'
|
||||
location_key:
|
||||
- 'region'
|
||||
- 'zone'
|
||||
provision_type: 'virtual'
|
||||
|
||||
connection:
|
||||
|
@ -5,7 +5,9 @@ provider: 'google'
|
||||
|
||||
plain:
|
||||
image: 'GOOGLE'
|
||||
location_key: 'zone'
|
||||
location_key:
|
||||
- 'region'
|
||||
- 'zone'
|
||||
provision_type: 'virtual'
|
||||
|
||||
connection:
|
||||
|
@ -5,7 +5,9 @@ provider: 'google'
|
||||
|
||||
plain:
|
||||
image: 'GOOGLE'
|
||||
location_key: 'zone'
|
||||
location_key:
|
||||
- 'region'
|
||||
- 'zone'
|
||||
provision_type: 'virtual'
|
||||
|
||||
connection:
|
||||
|
@ -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"],
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
166
src/fireedge/src/client/components/FormControl/FileController.js
Normal file
166
src/fireedge/src/client/components/FormControl/FileController.js
Normal file
@ -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 (
|
||||
<FormControl fullWidth>
|
||||
<Controller
|
||||
render={() => (
|
||||
<input
|
||||
{...register(name)}
|
||||
className={classes.hide}
|
||||
id={cy}
|
||||
type='file'
|
||||
onChange={handleChange}
|
||||
{...fieldProps}
|
||||
/>
|
||||
)}
|
||||
name={name}
|
||||
control={control}
|
||||
/>
|
||||
<label htmlFor={cy} className={classes.label}>
|
||||
<SubmitButton
|
||||
color='secondary'
|
||||
component='span'
|
||||
data-cy={`${cy}-button`}
|
||||
isSubmitting={isLoading}
|
||||
label={success ? <Check /> : <InsertDriveFile />}
|
||||
className={clsx({
|
||||
[classes.buttonSuccess]: success
|
||||
})}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
{Boolean(error) && (
|
||||
<FormHelperText data-cy={`${cy}-error`}>
|
||||
<ErrorHelper label={error?.message} />
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
(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
|
@ -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,
|
||||
|
@ -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)}</StepLabel>
|
||||
</StepButton>
|
||||
</Step>
|
||||
|
@ -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 })
|
||||
)
|
||||
}
|
||||
|
@ -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 ? <Box display="none">{children}</Box> : children
|
||||
|
||||
const FormWithSchema = ({ id, cy, fields }) => {
|
||||
const { control, errors } = useFormContext()
|
||||
const { control, errors, ...formContext } = useFormContext()
|
||||
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
@ -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
|
||||
})}
|
||||
</Grid>
|
||||
|
@ -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
|
||||
|
@ -61,7 +61,8 @@ export const INPUT_TYPES = {
|
||||
SELECT: 'select',
|
||||
CHECKBOX: 'checkbox',
|
||||
SLIDER: 'slider',
|
||||
AUTOCOMPLETE: 'autocomplete'
|
||||
AUTOCOMPLETE: 'autocomplete',
|
||||
FILE: 'file'
|
||||
}
|
||||
|
||||
export const DEBUG_LEVEL = {
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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) ? (
|
||||
<EmptyCard title={'✔️ There is not connections to fill'} />
|
||||
<EmptyCard title={"✔️ There aren't connections to fill"} />
|
||||
) : (
|
||||
<FormWithSchema cy="form-provider" fields={fields} id={STEP_ID} />
|
||||
)
|
||||
}, [])
|
||||
})
|
||||
|
||||
export * from 'client/containers/Providers/Form/Create/Steps/Connection/schema'
|
||||
export default Connection
|
||||
|
@ -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))
|
||||
)
|
||||
|
@ -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 -- */}
|
||||
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
|
||||
<Select
|
||||
color='secondary'
|
||||
inputProps = {{ 'data-cy': 'select-provision-type' }}
|
||||
native
|
||||
style={{ minWidth: '8em' }}
|
||||
onChange={handleChangeProvision}
|
||||
value={provisionSelected}
|
||||
variant='outlined'
|
||||
>
|
||||
<RenderOptions options={provisionsTemplates} />
|
||||
</Select>
|
||||
{provisionSelected && <Select
|
||||
color='secondary'
|
||||
inputProps = {{ 'data-cy': 'select-provider-type' }}
|
||||
native
|
||||
style={{ minWidth: '8em' }}
|
||||
onChange={handleChangeProvider}
|
||||
value={providerSelected}
|
||||
variant='outlined'
|
||||
>
|
||||
<RenderOptions options={providersTypes} />
|
||||
</Select>}
|
||||
<FormControl>
|
||||
<InputLabel color='secondary' shrink id='select-provision-type-label'>
|
||||
{'Provision type'}
|
||||
</InputLabel>
|
||||
<Select
|
||||
color='secondary'
|
||||
inputProps={{ 'data-cy': 'select-provision-type' }}
|
||||
labelId='select-provision-type-label'
|
||||
native
|
||||
style={{ marginTop: '1em', minWidth: '8em' }}
|
||||
onChange={handleChangeProvision}
|
||||
value={provisionSelected}
|
||||
variant='outlined'
|
||||
>
|
||||
<RenderOptions options={provisionsTemplates} />
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<InputLabel color='secondary' shrink id='select-provider-type-label'>
|
||||
{'Provider type'}
|
||||
</InputLabel>
|
||||
<Select
|
||||
color='secondary'
|
||||
inputProps={{ 'data-cy': 'select-provider-type' }}
|
||||
labelId='select-provider-type-label'
|
||||
native
|
||||
style={{ marginTop: '1em', minWidth: '8em' }}
|
||||
onChange={handleChangeProvider}
|
||||
value={providerSelected}
|
||||
variant='outlined'
|
||||
>
|
||||
<RenderOptions options={providersTypes} />
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Breadcrumbs>
|
||||
|
||||
{/* -- DESCRIPTION -- */}
|
||||
@ -129,22 +140,10 @@ const Template = () => ({
|
||||
<ListCards
|
||||
keyProp='name'
|
||||
list={templatesAvailable}
|
||||
EmptyComponent={
|
||||
<EmptyCard title={
|
||||
!provisionSelected
|
||||
? 'Please choose your provision type'
|
||||
: !providerSelected
|
||||
? 'Please choose your provider type'
|
||||
: 'Your providers templates list is empty'
|
||||
} />
|
||||
}
|
||||
gridProps={{ 'data-cy': 'providers-templates' }}
|
||||
CardComponent={ProvisionTemplateCard}
|
||||
cardsProps={({ value = {} }) => {
|
||||
const isSelected = data?.some(selected =>
|
||||
selected.name === value.name
|
||||
)
|
||||
|
||||
const isSelected = data?.some(selected => selected.name === value.name)
|
||||
const isValid = ProviderTemplateModel.isValidProviderTemplate(value)
|
||||
|
||||
return {
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
@ -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 }
|
||||
}), {})
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user