mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-20 10:50:08 +03:00
parent
81e1104118
commit
e012409799
@ -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 => ({
|
||||
|
@ -129,6 +129,9 @@ module.exports = {
|
||||
startOneRequest: () => ({
|
||||
type: START_ONE_REQUEST
|
||||
}),
|
||||
successOneRequest: () => ({
|
||||
type: SUCCESS_ONE_REQUEST
|
||||
}),
|
||||
failureOneRequest: error => ({
|
||||
type: FAILURE_ONE_REQUEST,
|
||||
payload: { error }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -36,7 +36,7 @@ const SelectCard = memo(({
|
||||
return (
|
||||
<ConditionalWrap
|
||||
condition={!observerOff}
|
||||
wrap={children => <div ref={fromRef}>{children}</div>}>
|
||||
wrap={children => <span ref={fromRef}>{children}</span>}>
|
||||
{observerOff || isNearScreen ? (
|
||||
<Card
|
||||
className={clsx(classes.root, cardProps?.className, {
|
||||
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
|
||||
import { useGeneral } from 'client/hooks'
|
||||
import CustomMobileStepper from 'client/components/FormStepper/MobileStepper'
|
||||
import CustomStepper from 'client/components/FormStepper/Stepper'
|
||||
import { groupBy } from 'client/utils'
|
||||
@ -13,6 +14,7 @@ const FIRST_STEP = 0
|
||||
const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
|
||||
const { watch, reset, errors, setError } = useFormContext()
|
||||
const { isLoading } = useGeneral()
|
||||
|
||||
const [formData, setFormData] = useState(() => watch())
|
||||
const [activeStep, setActiveStep] = useState(FIRST_STEP)
|
||||
@ -54,7 +56,7 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
const handleStep = stepToAdvance => {
|
||||
const isBackAction = activeStep > stepToAdvance
|
||||
|
||||
isBackAction && handleBack(isBackAction)
|
||||
isBackAction && handleBack(stepToAdvance)
|
||||
|
||||
steps
|
||||
.slice(FIRST_STEP, stepToAdvance)
|
||||
@ -112,6 +114,7 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
activeStep={activeStep}
|
||||
lastStep={lastStep}
|
||||
disabledBack={disabledBack}
|
||||
isSubmitting={isLoading}
|
||||
handleNext={handleNext}
|
||||
handleBack={handleBack}
|
||||
errors={errors}
|
||||
@ -122,12 +125,13 @@ const FormStepper = ({ steps, schema, onSubmit }) => {
|
||||
activeStep={activeStep}
|
||||
lastStep={lastStep}
|
||||
disabledBack={disabledBack}
|
||||
isSubmitting={isLoading}
|
||||
handleStep={handleStep}
|
||||
handleNext={handleNext}
|
||||
handleBack={handleBack}
|
||||
errors={errors}
|
||||
/>
|
||||
), [isMobile, activeStep, errors[id]])}
|
||||
), [isLoading, isMobile, activeStep, errors[id]])}
|
||||
{/* FORM CONTENT */}
|
||||
{Content && <Content data={formData[id]} setFormData={setFormData} />}
|
||||
</>
|
||||
|
@ -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]
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Divider, Select, Breadcrumbs } from '@material-ui/core'
|
||||
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
|
||||
|
||||
import { useProvision, useListForm, useGeneral } from 'client/hooks'
|
||||
import { ListCards } from 'client/components/List'
|
||||
@ -8,7 +10,6 @@ import { T } from 'client/constants'
|
||||
import { STEP_ID as CONNECTION_ID } from 'client/containers/Providers/Form/Create/Steps/Connection'
|
||||
import { STEP_ID as INPUTS_ID } from 'client/containers/Providers/Form/Create/Steps/Inputs'
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Providers/Form/Create/Steps/Template/schema'
|
||||
import { Divider, Select } from '@material-ui/core'
|
||||
|
||||
export const STEP_ID = 'template'
|
||||
|
||||
@ -44,15 +45,18 @@ const Template = () => ({
|
||||
templateSelected && handleClear()
|
||||
}
|
||||
|
||||
const handleClick = ({ name, provider, provision }, isSelected) => {
|
||||
if (name === undefined || provider === undefined || provision === undefined) {
|
||||
const handleClick = ({ name, provider, plain = {} }, isSelected) => {
|
||||
const { provision_type: provisionType } = plain
|
||||
|
||||
if ([name, provisionType, provider].includes(undefined)) {
|
||||
showError({ message: 'This template has bad format. Ask your cloud administrator' })
|
||||
} else {
|
||||
// reset rest of form when change template
|
||||
setFormData({ [INPUTS_ID]: undefined, [CONNECTION_ID]: undefined })
|
||||
|
||||
isSelected
|
||||
? handleUnselect(name, item => item.name === name)
|
||||
: handleSelect({ name, provider, provision })
|
||||
: handleSelect({ name, provider, provision: provisionType })
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +66,7 @@ const Template = () => ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
|
||||
<Select
|
||||
color='secondary'
|
||||
data-cy='select-provision-type'
|
||||
@ -87,7 +91,7 @@ const Template = () => ({
|
||||
<option value="">{T.None}</option>
|
||||
<RenderOptions options={providersTypes} />
|
||||
</Select>}
|
||||
</div>
|
||||
</Breadcrumbs>
|
||||
<Divider style={{ margin: '1rem 0' }} />
|
||||
<ListCards
|
||||
keyProp='name'
|
||||
|
@ -10,7 +10,7 @@ import Steps from 'client/containers/Providers/Form/Create/Steps'
|
||||
|
||||
import { useFetch, useProvision, useGeneral } from 'client/hooks'
|
||||
import { PATH } from 'client/router/provision'
|
||||
import { mapUserInputs } from 'client/utils'
|
||||
import { get, mapUserInputs } from 'client/utils'
|
||||
|
||||
function ProviderCreateForm () {
|
||||
const history = useHistory()
|
||||
@ -34,44 +34,49 @@ function ProviderCreateForm () {
|
||||
resolver: yupResolver(resolvers())
|
||||
})
|
||||
|
||||
const getTemplate = ({ provision, provider, name } = {}) => {
|
||||
const template = provisionsTemplates
|
||||
?.[provision]
|
||||
?.providers?.[provider]
|
||||
?.find(providerSelected => providerSelected.name === name)
|
||||
const redirectWithError = (name = '') => {
|
||||
showError({
|
||||
message: `
|
||||
Cannot found provider template (${name}),
|
||||
ask your cloud administrator`
|
||||
})
|
||||
|
||||
if (!template) {
|
||||
showError({
|
||||
message: `
|
||||
Cannot found provider template (${provider}),
|
||||
ask your cloud administrator`
|
||||
})
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
} else return template
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
}
|
||||
|
||||
const getProviderTemplateByDir = ({ provision, provider, name }) =>
|
||||
provisionsTemplates
|
||||
?.[provision]
|
||||
?.providers
|
||||
?.[provider]
|
||||
?.find(providerSelected => providerSelected.name === name)
|
||||
|
||||
const onSubmit = formData => {
|
||||
const { template, inputs, connection, registration_time: time } = formData
|
||||
|
||||
const templateSelected = template?.[0]
|
||||
const providerTemplate = getTemplate(templateSelected)
|
||||
const providerTemplate = getProviderTemplateByDir(templateSelected)
|
||||
|
||||
if (!providerTemplate) return redirectWithError(templateSelected?.name)
|
||||
|
||||
const parseInputs = mapUserInputs(inputs)
|
||||
|
||||
const {
|
||||
plain,
|
||||
name,
|
||||
provider,
|
||||
location_key: locationKey,
|
||||
connection: { [locationKey]: connectionFixed }
|
||||
} = providerTemplate
|
||||
|
||||
const formatData = {
|
||||
...(!isUpdate && templateSelected),
|
||||
...(plain && { plain }),
|
||||
connection: {
|
||||
...connection,
|
||||
[locationKey]: connectionFixed
|
||||
},
|
||||
inputs: providerTemplate?.inputs
|
||||
?.map(input => ({ ...input, value: `${parseInputs[input?.name]}` })),
|
||||
...(!isUpdate && { plain, name, provider }),
|
||||
connection: { ...connection, [locationKey]: connectionFixed },
|
||||
inputs: providerTemplate?.inputs?.map(input => ({
|
||||
...input,
|
||||
value: `${parseInputs[input?.name]}`,
|
||||
default: `${parseInputs[input?.name]}`
|
||||
})),
|
||||
registration_time: time
|
||||
}
|
||||
|
||||
@ -95,14 +100,17 @@ function ProviderCreateForm () {
|
||||
inputs,
|
||||
name,
|
||||
provider,
|
||||
provision,
|
||||
registration_time: time
|
||||
} = data?.TEMPLATE?.PROVISION_BODY ?? {}
|
||||
|
||||
const templateSelected = { name, provision, provider }
|
||||
const providerTemplate = getTemplate(templateSelected)
|
||||
const { provision_type: provisionType } = data?.TEMPLATE?.PLAIN ?? {}
|
||||
|
||||
const { location_key: locationKey } = providerTemplate
|
||||
const templateSelected = { name, provision: provisionType, provider }
|
||||
const template = getProviderTemplateByDir(templateSelected)
|
||||
|
||||
if (!template) return redirectWithError(name)
|
||||
|
||||
const { location_key: locationKey } = template
|
||||
const { [locationKey]: _, ...connectionEditable } = connection
|
||||
|
||||
const inputsNameValue = inputs?.reduce((res, input) => (
|
||||
|
@ -1,18 +1,20 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { LinearProgress } from '@material-ui/core'
|
||||
|
||||
import { useProvision } from 'client/hooks'
|
||||
import { useProvision, useFetch, useGeneral } from 'client/hooks'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import { EmptyCard } from 'client/components/Cards'
|
||||
import { T } from 'client/constants'
|
||||
import { deepmerge } from 'client/utils/merge'
|
||||
|
||||
import {
|
||||
STEP_ID as PROVISION_TEMPLATE_ID
|
||||
} from 'client/containers/Provisions/Form/Create/Steps/Provision'
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Provisions/Form/Create/Steps/Template'
|
||||
import {
|
||||
FORM_FIELDS, STEP_FORM_SCHEMA
|
||||
} from 'client/containers/Provisions/Form/Create/Steps/Inputs/schema'
|
||||
import { console } from 'window-or-global'
|
||||
|
||||
export const STEP_ID = 'inputs'
|
||||
|
||||
@ -24,28 +26,53 @@ const Inputs = () => ({
|
||||
resolver: () => STEP_FORM_SCHEMA(inputs),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: useCallback(() => {
|
||||
const [fields, setFields] = useState([])
|
||||
const { provisionsTemplates } = useProvision()
|
||||
const [fields, setFields] = useState(undefined)
|
||||
const { changeLoading } = useGeneral()
|
||||
const { provisionsTemplates, getProvider } = useProvision()
|
||||
const { data: fetchData, fetchRequest, loading } = useFetch(getProvider)
|
||||
const { watch, reset } = useFormContext()
|
||||
|
||||
const getProvisionTemplateByDir = ({ provision, provider, name }) =>
|
||||
provisionsTemplates
|
||||
?.[provision]
|
||||
?.provisions
|
||||
?.[provider]
|
||||
?.find(provisionTemplate => provisionTemplate.name === name)
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
[PROVISION_TEMPLATE_ID]: provision,
|
||||
[STEP_ID]: currentInputs
|
||||
} = watch()
|
||||
const provisionTemplate = provisionsTemplates
|
||||
.find(({ name }) => name === provision?.[0])
|
||||
const { [PROVIDER_ID]: providerSelected, [STEP_ID]: currentInputs } = watch()
|
||||
|
||||
inputs = provisionTemplate?.inputs ?? []
|
||||
setFields(FORM_FIELDS(inputs))
|
||||
|
||||
// set defaults inputs values when first render
|
||||
!currentInputs && reset({
|
||||
...watch(),
|
||||
[STEP_ID]: STEP_FORM_SCHEMA(inputs).default()
|
||||
})
|
||||
if (!currentInputs) {
|
||||
changeLoading(true) // disable finish button until provider is fetched
|
||||
fetchRequest({ id: providerSelected[0] })
|
||||
} else {
|
||||
setFields(FORM_FIELDS(inputs))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchData) {
|
||||
const { [TEMPLATE_ID]: provisionTemplateSelected = [] } = watch()
|
||||
const { TEMPLATE: { PROVISION_BODY } = {} } = fetchData
|
||||
|
||||
const provisionTemplate = getProvisionTemplateByDir(provisionTemplateSelected?.[0])
|
||||
|
||||
// MERGE INPUTS provision template + PROVISION_BODY.inputs (provider fetch)
|
||||
inputs = provisionTemplate.inputs.map(templateInput =>
|
||||
PROVISION_BODY.inputs.find(
|
||||
providerInput => providerInput.name === templateInput.name
|
||||
) || templateInput
|
||||
) ?? []
|
||||
|
||||
setFields(FORM_FIELDS(inputs))
|
||||
reset({ ...watch(), [STEP_ID]: STEP_FORM_SCHEMA(inputs).default() })
|
||||
}
|
||||
}, [fetchData])
|
||||
|
||||
if (!fields && loading) {
|
||||
return <LinearProgress color='secondary' />
|
||||
}
|
||||
|
||||
return (fields?.length === 0) ? (
|
||||
<EmptyCard title={'✔️ There is not inputs to fill'} />
|
||||
) : (
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import { useFetch, useProvision, useListForm } from 'client/hooks'
|
||||
import { useProvision, useListForm } from 'client/hooks'
|
||||
import { ListCards } from 'client/components/List'
|
||||
import { EmptyCard, ProvisionCard } from 'client/components/Cards'
|
||||
import { PATH } from 'client/router/provision'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_FORM_SCHEMA } from './schema'
|
||||
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/Create/Steps/Inputs'
|
||||
import { STEP_ID as TEMPLATE_ID } from 'client/containers/Provisions/Form/Create/Steps/Template'
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/Create/Steps/Provider/schema'
|
||||
|
||||
export const STEP_ID = 'provider'
|
||||
|
||||
@ -16,44 +17,46 @@ const Provider = () => ({
|
||||
label: T.Provider,
|
||||
resolver: () => STEP_FORM_SCHEMA,
|
||||
content: useCallback(({ data, setFormData }) => {
|
||||
const { getProviders } = useProvision()
|
||||
const { data: providers, fetchRequest, loading, error } = useFetch(
|
||||
getProviders
|
||||
)
|
||||
const { providers } = useProvision()
|
||||
const template = useWatch({ name: TEMPLATE_ID })
|
||||
const templateSelected = template?.[0] ?? {}
|
||||
|
||||
const { handleSelect, handleUnselect } = useListForm({
|
||||
key: STEP_ID,
|
||||
setList: setFormData
|
||||
})
|
||||
const providersByTypeAndService = React.useMemo(() =>
|
||||
providers.filter(({ TEMPLATE: { PLAIN = {} } = {} }) =>
|
||||
PLAIN.provider === templateSelected.provider &&
|
||||
PLAIN.provision_type === templateSelected.provision
|
||||
)
|
||||
, [providers])
|
||||
|
||||
useEffect(() => { fetchRequest() }, [])
|
||||
const {
|
||||
handleSelect,
|
||||
handleUnselect
|
||||
} = useListForm({ key: STEP_ID, setList: setFormData })
|
||||
|
||||
useEffect(() => {
|
||||
if (providers) {
|
||||
// delete provider selected in template if not exists
|
||||
const provider = providers?.some(({ NAME }) => NAME === data?.[0])
|
||||
!provider && handleUnselect(data?.[0])
|
||||
}
|
||||
}, [providers])
|
||||
// delete provider selected at template if not exists
|
||||
const existsProvider = providers?.some(({ ID }) => ID === data?.[0])
|
||||
!existsProvider && handleUnselect(data?.[0])
|
||||
}, [])
|
||||
|
||||
if (error) {
|
||||
return <Redirect to={PATH.DASHBOARD} />
|
||||
const handleClick = (id, isSelected) => {
|
||||
// reset inputs when change provider
|
||||
setFormData(prev => ({ ...prev, [INPUTS_ID]: undefined }))
|
||||
isSelected ? handleUnselect(id) : handleSelect(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListCards
|
||||
list={providers}
|
||||
isLoading={!providers && loading}
|
||||
list={providersByTypeAndService}
|
||||
EmptyComponent={<EmptyCard title={'Your providers list is empty'} />}
|
||||
CardComponent={ProvisionCard}
|
||||
cardsProps={({ value: { NAME } }) => {
|
||||
const isSelected = data?.some(selected => selected === NAME)
|
||||
cardsProps={({ value: { ID } }) => {
|
||||
const isSelected = data?.some(selected => selected === ID)
|
||||
|
||||
return {
|
||||
isProvider: true,
|
||||
isSelected,
|
||||
handleClick: () =>
|
||||
isSelected ? handleUnselect(NAME) : handleSelect(NAME)
|
||||
handleClick: () => handleClick(ID, isSelected)
|
||||
}
|
||||
}}
|
||||
breakpoints={{ xs: 12, sm: 6, md: 4 }}
|
||||
|
@ -1,67 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
|
||||
import { useFetch, useProvision, useListForm } from 'client/hooks'
|
||||
import { ListCards } from 'client/components/List'
|
||||
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
|
||||
import { PATH } from 'client/router/provision'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/Create/Steps/Inputs'
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/Create/Steps/Provision/schema'
|
||||
|
||||
export const STEP_ID = 'provision'
|
||||
|
||||
const Provision = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.ProvisionTemplate,
|
||||
resolver: () => STEP_FORM_SCHEMA,
|
||||
content: useCallback(({ data, setFormData }) => {
|
||||
const { getTemplates } = useProvision()
|
||||
const { data: templates, fetchRequest, loading, error } = useFetch(
|
||||
getTemplates
|
||||
)
|
||||
|
||||
const { handleSelect, handleUnselect } = useListForm({
|
||||
key: STEP_ID,
|
||||
setList: setFormData
|
||||
})
|
||||
|
||||
useEffect(() => { fetchRequest() }, [])
|
||||
|
||||
const handleClick = (nameTemplate, nameProvider, isSelected) => {
|
||||
setFormData(({ [INPUTS_ID]: undefined, [PROVIDER_ID]: [nameProvider] }))
|
||||
isSelected ? handleUnselect(nameTemplate) : handleSelect(nameTemplate)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Redirect to={PATH.DASHBOARD} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ListCards
|
||||
list={templates}
|
||||
keyProp='name'
|
||||
isLoading={!templates && loading}
|
||||
EmptyComponent={
|
||||
<EmptyCard title={'Your provisions templates list is empty'} />
|
||||
}
|
||||
CardComponent={ProvisionTemplateCard}
|
||||
cardsProps={({ value: { name, defaults = {} } }) => {
|
||||
const isSelected = data?.some(selected => selected === name)
|
||||
const { provision: { provider } = {} } = defaults
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
title: name,
|
||||
handleClick: () => handleClick(name, provider, isSelected)
|
||||
}
|
||||
}}
|
||||
breakpoints={{ xs: 12, sm: 6, md: 4 }}
|
||||
/>
|
||||
)
|
||||
}, [])
|
||||
})
|
||||
|
||||
export default Provision
|
@ -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([])
|
@ -0,0 +1,121 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Divider, Select, Breadcrumbs } from '@material-ui/core'
|
||||
import ArrowIcon from '@material-ui/icons/ArrowForwardIosRounded'
|
||||
|
||||
import { useProvision, useListForm, useGeneral } from 'client/hooks'
|
||||
import { ListCards } from 'client/components/List'
|
||||
import { EmptyCard, ProvisionTemplateCard } from 'client/components/Cards'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { STEP_ID as PROVIDER_ID } from 'client/containers/Provisions/Form/Create/Steps/Provider'
|
||||
import { STEP_ID as INPUTS_ID } from 'client/containers/Provisions/Form/Create/Steps/Inputs'
|
||||
import { STEP_FORM_SCHEMA } from 'client/containers/Provisions/Form/Create/Steps/Template/schema'
|
||||
|
||||
export const STEP_ID = 'template'
|
||||
|
||||
const Template = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.ProvisionTemplate,
|
||||
resolver: () => STEP_FORM_SCHEMA,
|
||||
content: useCallback(({ data, setFormData }) => {
|
||||
const templateSelected = data?.[0]
|
||||
|
||||
const [provisionSelected, setProvision] = React.useState(templateSelected?.provision)
|
||||
const [providerSelected, setProvider] = React.useState(templateSelected?.provider)
|
||||
|
||||
const { showError } = useGeneral()
|
||||
const { provisionsTemplates, providers } = useProvision()
|
||||
const providersTypes = provisionsTemplates?.[provisionSelected]?.provisions ?? []
|
||||
const templatesAvailable = providersTypes?.[providerSelected] ?? []
|
||||
|
||||
const {
|
||||
handleSelect,
|
||||
handleUnselect,
|
||||
handleClear
|
||||
} = useListForm({ key: STEP_ID, setList: setFormData })
|
||||
|
||||
const handleChangeProvision = evt => {
|
||||
setProvision(evt.target.value)
|
||||
setProvider(undefined)
|
||||
templateSelected && handleClear()
|
||||
}
|
||||
|
||||
const handleChangeProvider = evt => {
|
||||
setProvider(evt.target.value)
|
||||
templateSelected && handleClear()
|
||||
}
|
||||
|
||||
const handleClick = (template, isSelected) => {
|
||||
const { name, provision_type: provisionType, provider, defaults, hosts } = template
|
||||
|
||||
if ([name, provisionType, provider].includes(undefined)) {
|
||||
showError({ message: 'This template has bad format. Ask your cloud administrator' })
|
||||
} else {
|
||||
// reset rest of form when change template
|
||||
const providerName = defaults?.provision?.provider_name ?? hosts?.[0]?.provision.provider_name
|
||||
const { ID } = providers?.find(({ NAME }) => NAME === providerName) ?? {}
|
||||
setFormData({ [INPUTS_ID]: undefined, [PROVIDER_ID]: [ID] })
|
||||
|
||||
isSelected
|
||||
? handleUnselect(name, item => item.name === name)
|
||||
: handleSelect({ name, provider, provision: provisionType })
|
||||
}
|
||||
}
|
||||
|
||||
const RenderOptions = ({ options = {} }) => Object.keys(options)?.map(option => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs separator={<ArrowIcon color="secondary" />}>
|
||||
<Select
|
||||
color='secondary'
|
||||
data-cy='select-provision-type'
|
||||
native
|
||||
style={{ minWidth: '8em' }}
|
||||
onChange={handleChangeProvision}
|
||||
value={provisionSelected}
|
||||
variant='outlined'
|
||||
>
|
||||
<option value="">{T.None}</option>
|
||||
<RenderOptions options={provisionsTemplates} />
|
||||
</Select>
|
||||
{provisionSelected && <Select
|
||||
color='secondary'
|
||||
data-cy='select-provider-type'
|
||||
native
|
||||
style={{ minWidth: '8em' }}
|
||||
onChange={handleChangeProvider}
|
||||
value={providerSelected}
|
||||
variant='outlined'
|
||||
>
|
||||
<option value="">{T.None}</option>
|
||||
<RenderOptions options={providersTypes} />
|
||||
</Select>}
|
||||
</Breadcrumbs>
|
||||
<Divider style={{ margin: '1rem 0' }} />
|
||||
<ListCards
|
||||
keyProp='name'
|
||||
list={templatesAvailable}
|
||||
EmptyComponent={
|
||||
<EmptyCard title={'Your providers templates list is empty'} />
|
||||
}
|
||||
CardComponent={ProvisionTemplateCard}
|
||||
cardsProps={({ value = {} }) => {
|
||||
const isSelected = data?.some(selected =>
|
||||
selected.name === value.name
|
||||
)
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
handleClick: () => handleClick(value, isSelected)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}, [])
|
||||
})
|
||||
|
||||
export default Template
|
@ -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([])
|
@ -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()
|
||||
})
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Redirect, useHistory } from 'react-router'
|
||||
|
||||
import { Container } from '@material-ui/core'
|
||||
import { Container, LinearProgress } from '@material-ui/core'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers'
|
||||
|
||||
import { useGeneral, useProvision, useSocket } from 'client/hooks'
|
||||
|
||||
import FormStepper from 'client/components/FormStepper'
|
||||
import Steps from 'client/containers/Provisions/Form/Create/Steps'
|
||||
import DebugLog from 'client/components/DebugLog'
|
||||
|
||||
import { useGeneral, useProvision, useSocket, useFetch } from 'client/hooks'
|
||||
import { PATH } from 'client/router/provision'
|
||||
import { set, mapUserInputs } from 'client/utils'
|
||||
|
||||
@ -18,7 +18,8 @@ function ProvisionCreateForm () {
|
||||
const history = useHistory()
|
||||
const { showError } = useGeneral()
|
||||
const { getProvision } = useSocket()
|
||||
const { createProvision, provisionsTemplates } = useProvision()
|
||||
const { getProviders, createProvision, provisionsTemplates, providers } = useProvision()
|
||||
const { data, fetchRequest, loading, error } = useFetch(getProviders)
|
||||
|
||||
const { steps, defaultValues, resolvers } = Steps()
|
||||
|
||||
@ -28,25 +29,42 @@ function ProvisionCreateForm () {
|
||||
resolver: yupResolver(resolvers())
|
||||
})
|
||||
|
||||
const redirectWithError = (name = '') => {
|
||||
showError({
|
||||
message: `
|
||||
Cannot found provision template (${name}),
|
||||
ask your cloud administrator`
|
||||
})
|
||||
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
}
|
||||
|
||||
const getProvisionTemplateByDir = ({ provision, provider, name }) =>
|
||||
provisionsTemplates
|
||||
?.[provision]
|
||||
?.provisions
|
||||
?.[provider]
|
||||
?.find(provisionTemplate => provisionTemplate.name === name)
|
||||
|
||||
const onSubmit = formData => {
|
||||
const { provision, provider, inputs } = formData
|
||||
const provisionSelected = provision[0]
|
||||
const providerSelected = provider[0]
|
||||
const { template, provider, inputs } = formData
|
||||
const provisionTemplateSelected = template?.[0] ?? {}
|
||||
const providerIdSelected = provider?.[0]
|
||||
const providerName = providers?.find(({ ID }) => ID === providerIdSelected)?.NAME
|
||||
|
||||
const provisionTemplate = provisionsTemplates
|
||||
.find(({ name }) => name === provisionSelected)
|
||||
const provisionTemplate = getProvisionTemplateByDir(provisionTemplateSelected)
|
||||
|
||||
if (!provisionTemplate) {
|
||||
showError({
|
||||
message: `
|
||||
Cannot found provider template (${provisionSelected}),
|
||||
ask your cloud administrator`
|
||||
if (!provisionTemplate) return redirectWithError(provisionTemplateSelected?.name)
|
||||
|
||||
// update provider name if changed during form
|
||||
if (provisionTemplate.defaults?.provision?.provider_name) {
|
||||
set(provisionTemplate, 'defaults.provision.provider_name', providerName)
|
||||
} else if (provisionTemplate.hosts?.length > 0) {
|
||||
provisionTemplate.hosts.forEach(host => {
|
||||
set(host, 'provision.provider_name', providerName)
|
||||
})
|
||||
history.push(PATH.PROVISIONS.LIST)
|
||||
}
|
||||
|
||||
set(provisionTemplate, 'defaults.provision.provider', providerSelected)
|
||||
|
||||
const parseInputs = mapUserInputs(inputs)
|
||||
const formatData = {
|
||||
...provisionTemplate,
|
||||
@ -57,11 +75,19 @@ function ProvisionCreateForm () {
|
||||
createProvision({ data: formatData }).then(res => res && setUuid(res))
|
||||
}
|
||||
|
||||
useEffect(() => { fetchRequest() }, [])
|
||||
|
||||
if (uuid) {
|
||||
return <DebugLog uuid={uuid} socket={getProvision} />
|
||||
}
|
||||
|
||||
return (
|
||||
if (error) {
|
||||
return <Redirect to={PATH.PROVIDERS.LIST} />
|
||||
}
|
||||
|
||||
return (!data) || loading ? (
|
||||
<LinearProgress color='secondary' />
|
||||
) : (
|
||||
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
|
||||
<FormProvider {...methods}>
|
||||
<FormStepper steps={steps} schema={resolvers} onSubmit={onSubmit} />
|
||||
|
@ -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 = {}) => {
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user