diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 13a5b9019d..42b98908af 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -43,6 +43,7 @@ import {
} from 'iconoir-react'
import loadable from '@loadable/component'
+import { RESOURCE_NAMES } from 'client/constants'
const VirtualMachines = loadable(() => import('client/containers/VirtualMachines'), { ssr: false })
const VirtualMachineDetail = loadable(() => import('client/containers/VirtualMachines/Detail'), { ssr: false })
@@ -57,9 +58,9 @@ const CreateVmTemplate = loadable(() => import('client/containers/VmTemplates/Cr
const Datastores = loadable(() => import('client/containers/Datastores'), { ssr: false })
const Images = loadable(() => import('client/containers/Images'), { ssr: false })
-// const Files = loadable(() => import('client/containers/Files'), { ssr: false })
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), { ssr: false })
const MarketplaceApps = loadable(() => import('client/containers/MarketplaceApps'), { ssr: false })
+const CreateMarketplaceApp = loadable(() => import('client/containers/MarketplaceApps/Create'), { ssr: false })
const VirtualNetworks = loadable(() => import('client/containers/VirtualNetworks'), { ssr: false })
const VNetworkTemplates = loadable(() => import('client/containers/VNetworkTemplates'), { ssr: false })
@@ -82,79 +83,76 @@ const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), {
export const PATH = {
INSTANCE: {
VMS: {
- LIST: '/vm',
- DETAIL: '/vm/:id'
+ LIST: `/${RESOURCE_NAMES.VM}`,
+ DETAIL: `/${RESOURCE_NAMES.VM}/:id`
},
VROUTERS: {
- LIST: '/virtual-router'
+ LIST: `/${RESOURCE_NAMES.V_ROUTER}`
}
},
TEMPLATE: {
VMS: {
- LIST: '/vm-template',
- DETAIL: '/vm-template/:id',
- INSTANTIATE: '/vm-template/instantiate',
- CREATE: '/vm-template/create'
+ LIST: `/${RESOURCE_NAMES.VM_TEMPLATE}`,
+ DETAIL: `/${RESOURCE_NAMES.VM_TEMPLATE}/:id`,
+ INSTANTIATE: `/${RESOURCE_NAMES.VM_TEMPLATE}/instantiate`,
+ CREATE: `/${RESOURCE_NAMES.VM_TEMPLATE}/create`
}
},
STORAGE: {
DATASTORES: {
- LIST: '/datastore',
- DETAIL: '/datastore/:id'
+ LIST: `/${RESOURCE_NAMES.DATASTORE}`,
+ DETAIL: `/${RESOURCE_NAMES.DATASTORE}/:id`
},
IMAGES: {
- LIST: '/image',
- DETAIL: '/image/:id'
- },
- FILES: {
- LIST: '/file',
- DETAIL: '/file/:id'
+ LIST: `/${RESOURCE_NAMES.IMAGE}`,
+ DETAIL: `/${RESOURCE_NAMES.IMAGE}/:id`
},
MARKETPLACES: {
- LIST: '/marketplace',
- DETAIL: '/marketplace/:id'
+ LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
+ DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`
},
MARKETPLACE_APPS: {
- LIST: '/marketplace-app',
- DETAIL: '/marketplace-app/:id'
+ LIST: `/${RESOURCE_NAMES.APP}`,
+ DETAIL: `/${RESOURCE_NAMES.APP}/:id`,
+ CREATE: `/${RESOURCE_NAMES.APP}/create`
}
},
NETWORK: {
VNETS: {
- LIST: '/virtual-network',
- DETAIL: '/virtual-network/:id'
+ LIST: `/${RESOURCE_NAMES.VNET}`,
+ DETAIL: `/${RESOURCE_NAMES.VNET}/:id`
},
VN_TEMPLATES: {
- LIST: '/network-template',
- DETAIL: '/network-template/:id'
+ LIST: `/${RESOURCE_NAMES.VN_TEMPLATE}`,
+ DETAIL: `/${RESOURCE_NAMES.VN_TEMPLATE}/:id`
},
SEC_GROUPS: {
- LIST: '/security-group',
- DETAIL: '/security-group/:id'
+ LIST: `/${RESOURCE_NAMES.SEC_GROUP}`,
+ DETAIL: `/${RESOURCE_NAMES.SEC_GROUP}/:id`
}
},
INFRASTRUCTURE: {
CLUSTERS: {
- LIST: '/cluster',
- DETAIL: '/cluster/:id'
+ LIST: `/${RESOURCE_NAMES.CLUSTER}`,
+ DETAIL: `/${RESOURCE_NAMES.CLUSTER}/:id`
},
HOSTS: {
- LIST: '/host',
- DETAIL: '/host/:id'
+ LIST: `/${RESOURCE_NAMES.HOST}`,
+ DETAIL: `/${RESOURCE_NAMES.HOST}/:id`
},
ZONES: {
- LIST: '/zone',
- DETAIL: '/zone/:id'
+ LIST: `/${RESOURCE_NAMES.ZONE}`,
+ DETAIL: `/${RESOURCE_NAMES.ZONE}/:id`
}
},
SYSTEM: {
USERS: {
- LIST: '/user',
- DETAIL: '/user/:id'
+ LIST: `/${RESOURCE_NAMES.USER}`,
+ DETAIL: `/${RESOURCE_NAMES.USER}/:id`
},
GROUPS: {
- LIST: '/group',
- DETAIL: '/group/:id'
+ LIST: `/${RESOURCE_NAMES.GROUP}`,
+ DETAIL: `/${RESOURCE_NAMES.GROUP}/:id`
}
}
}
@@ -242,6 +240,11 @@ const ENDPOINTS = [
sidebar: true,
icon: MarketplaceAppIcon,
Component: MarketplaceApps
+ },
+ {
+ label: 'Create Marketplace App',
+ path: PATH.STORAGE.MARKETPLACE_APPS.CREATE,
+ Component: CreateMarketplaceApp
}
]
},
diff --git a/src/fireedge/src/client/components/FormControl/TableController.js b/src/fireedge/src/client/components/FormControl/TableController.js
index aac311bcfb..b46f2b1298 100644
--- a/src/fireedge/src/client/components/FormControl/TableController.js
+++ b/src/fireedge/src/client/components/FormControl/TableController.js
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
-import { memo } from 'react'
+import { memo, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useController } from 'react-hook-form'
@@ -23,6 +23,10 @@ import { generateKey } from 'client/utils'
const defaultGetRowId = item => typeof item === 'object' ? item?.id ?? item?.ID : item
+const getSelectedRowIds = value => [value ?? []]
+ .flat()
+ .reduce((initialSelected, rowId) => ({ ...initialSelected, [rowId]: true }), {})
+
const TableController = memo(
({
control,
@@ -33,15 +37,23 @@ const TableController = memo(
Table,
singleSelect = true,
getRowId = defaultGetRowId,
- formContext = {}
+ formContext = {},
+ fieldProps: { initialState, ...fieldProps } = {}
}) => {
const { clearErrors } = formContext
const {
- field: { onChange },
+ field: { value, onChange },
fieldState: { error }
} = useController({ name, control })
+ const [initialRows, setInitialRows] = useState(() => getSelectedRowIds(value))
+
+ useEffect(() => {
+ onChange(singleSelect ? undefined : [])
+ setInitialRows({})
+ }, [Table])
+
return (
<>
@@ -58,12 +70,14 @@ const TableController = memo(
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={getRowId}
+ initialState={{ ...initialState, selectedRowIds: initialRows }}
onSelectedRowsChange={rows => {
const rowValues = rows?.map(({ original }) => getRowId(original))
onChange(singleSelect ? rowValues?.[0] : rowValues)
clearErrors(name)
}}
+ {...fieldProps}
/>
>
)
@@ -71,6 +85,7 @@ const TableController = memo(
(prevProps, nextProps) =>
prevProps.error === nextProps.error &&
prevProps.label === nextProps.label &&
+ prevProps.Table === nextProps.Table &&
prevProps.tooltip === nextProps.tooltip
)
diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js
index 2dba641926..2b0810451f 100644
--- a/src/fireedge/src/client/components/Forms/FormWithSchema.js
+++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js
@@ -14,7 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
-import { createElement, useMemo } from 'react'
+import { createElement, useMemo, isValidElement } from 'react'
import PropTypes from 'prop-types'
import { FormControl, Grid } from '@mui/material'
@@ -27,7 +27,6 @@ import { INPUT_TYPES } from 'client/constants'
const NOT_DEPEND_ATTRIBUTES = [
'watcher',
'transform',
- 'Table',
'getRowId',
'renderValue'
]
@@ -90,9 +89,11 @@ const FormWithSchema = ({ id, cy, fields, rootProps, className, legend, legendTo
const [key, value] = attribute
const isNotDependAttribute = NOT_DEPEND_ATTRIBUTES.includes(key)
- const finalValue = typeof value === 'function' && !isNotDependAttribute
- ? value(valueOfDependField, formContext)
- : value
+ const finalValue = (
+ typeof value === 'function' &&
+ !isNotDependAttribute &&
+ !isValidElement(value())
+ ) ? value(valueOfDependField, formContext) : value
return { ...field, [key]: finalValue }
}, {})
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/index.js
new file mode 100644
index 0000000000..54b9723105
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/index.js
@@ -0,0 +1,53 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import PropTypes from 'prop-types'
+
+import FormWithSchema from 'client/components/Forms/FormWithSchema'
+import { FIELDS, SCHEMA } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema'
+import { Step } from 'client/utils'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'configuration'
+
+const Content = () => {
+ return (
+
+ )
+}
+
+/**
+ * Step to configure the marketplace app.
+ *
+ * @returns {Step} Configuration step
+ */
+const ConfigurationStep = () => ({
+ id: STEP_ID,
+ label: T.Configuration,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content
+})
+
+Content.propTypes = {
+ data: PropTypes.any,
+ setFormData: PropTypes.func
+}
+
+export default ConfigurationStep
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js
new file mode 100644
index 0000000000..dde21c354f
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration/schema.js
@@ -0,0 +1,113 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import { string, boolean, object, ObjectSchema } from 'yup'
+import { makeStyles } from '@mui/styles'
+
+import { useSystem, useDatastore } from 'client/features/One'
+import { ImagesTable, VmsTable, VmTemplatesTable } from 'client/components/Tables'
+import { Field, arrayToOptions, getValidationFromFields, sentenceCase } from 'client/utils'
+import { isMarketExportSupport } from 'client/models/Datastore'
+import { T, INPUT_TYPES, STATES, RESOURCE_NAMES } from 'client/constants'
+
+const TYPES = {
+ IMAGE: RESOURCE_NAMES.IMAGE.toUpperCase(),
+ VM: RESOURCE_NAMES.VM.toUpperCase(),
+ VM_TEMPLATE: RESOURCE_NAMES.VM_TEMPLATE.toUpperCase()
+}
+
+const useTableStyles = makeStyles({
+ body: { gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' }
+})
+
+/** @type {Field} Type field */
+const TYPE = {
+ name: 'type',
+ type: INPUT_TYPES.TOGGLE,
+ values: arrayToOptions(Object.values(TYPES), {
+ addEmpty: false,
+ getText: type => sentenceCase(type).toUpperCase()
+ }),
+ validation: string()
+ .trim()
+ .required()
+ .uppercase()
+ .default(() => TYPES.IMAGE),
+ grid: { md: 12 }
+}
+
+/** @type {Field} App name field */
+const NAME = {
+ name: 'name',
+ label: T.Name,
+ type: INPUT_TYPES.TEXT,
+ validation: string()
+ .trim()
+ .required()
+ .default(() => undefined),
+ grid: { md: 12, lg: 6 }
+}
+
+/** @type {Field} Import image/templates field */
+const IMPORT = {
+ name: 'image',
+ label: T.DontAssociateApp,
+ type: INPUT_TYPES.SWITCH,
+ validation: boolean().default(() => false),
+ grid: { md: 12, lg: 6 }
+}
+
+/** @type {Field} Resource table field */
+const RES_TABLE = {
+ name: 'id',
+ type: INPUT_TYPES.TABLE,
+ dependOf: 'type',
+ label: type => `Select the ${
+ sentenceCase(type) ?? 'resource'} to create the App`,
+ Table: type => ({
+ [TYPES.IMAGE]: ImagesTable,
+ [TYPES.VM]: VmsTable,
+ [TYPES.VM_TEMPLATE]: VmTemplatesTable
+ })[type],
+ validation: string()
+ .trim()
+ .required()
+ .default(() => undefined),
+ grid: { md: 12 },
+ fieldProps: type => {
+ const { config: oneConfig } = useSystem()
+ const datastores = useDatastore()
+ const classes = useTableStyles()
+
+ return {
+ [TYPES.IMAGE]: {
+ filter: image => {
+ const datastore = datastores?.find(ds => ds?.ID === image?.DATASTORE_ID)
+ return isMarketExportSupport(datastore, oneConfig)
+ }
+ },
+ [TYPES.VM]: {
+ initialState: { filters: [{ id: 'STATE', value: STATES.POWEROFF }] }
+ },
+ [TYPES.VM_TEMPLATE]: { classes }
+ }[type]
+ }
+}
+
+/** @type {Field[]} - List of fields */
+export const FIELDS = [TYPE, NAME, IMPORT, RES_TABLE]
+
+/** @type {ObjectSchema} - Schema form */
+export const SCHEMA = object(getValidationFromFields(FIELDS))
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/index.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/index.js
new file mode 100644
index 0000000000..71179e9e0f
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/index.js
@@ -0,0 +1,74 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import PropTypes from 'prop-types'
+import { useFormContext } from 'react-hook-form'
+
+import { useSystem } from 'client/features/One'
+import { MarketplacesTable } from 'client/components/Tables'
+import { SCHEMA } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/schema'
+import { Step } from 'client/utils'
+import { T } from 'client/constants'
+
+export const STEP_ID = 'marketplace'
+
+const Content = ({ data }) => {
+ const { NAME } = data?.[0] ?? {}
+ const { setValue } = useFormContext()
+ const { config: oneConfig } = useSystem()
+
+ const handleSelectedRows = rows => {
+ const { original = {} } = rows?.[0] ?? {}
+
+ setValue(STEP_ID, original.ID !== undefined ? [original] : [])
+ }
+
+ return (
+ String(market.NAME)}
+ filter={market =>
+ oneConfig?.FEDERATION?.ZONE_ID === market.ZONE_ID &&
+ oneConfig?.MARKET_MAD_CONF?.some(marketMad => (
+ marketMad?.APP_ACTIONS?.includes('create') &&
+ `${marketMad?.NAME}`.toUpperCase() === `${market?.MARKET_MAD}`.toUpperCase()
+ ))
+ }
+ initialState={{ selectedRowIds: { [NAME]: true } }}
+ onSelectedRowsChange={handleSelectedRows}
+ />
+ )
+}
+
+/**
+ * Step to select the Marketplace.
+ *
+ * @type {Step} Marketplace step
+ */
+const MarketplaceStep = () => ({
+ id: STEP_ID,
+ label: T.SelectMarketplace,
+ resolver: SCHEMA,
+ content: Content
+})
+
+Content.propTypes = {
+ data: PropTypes.any,
+ setFormData: PropTypes.func
+}
+
+export default MarketplaceStep
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/schema.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/schema.js
new file mode 100644
index 0000000000..68ec6d8cb9
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable/schema.js
@@ -0,0 +1,24 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import { array, object, ArraySchema } from 'yup'
+
+/** @type {ArraySchema} Marketplace table schema */
+export const SCHEMA = array(object())
+ .min(1)
+ .max(1)
+ .required()
+ .ensure()
+ .default(() => [])
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..433c6643b1
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/Steps/index.js
@@ -0,0 +1,34 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/BasicConfiguration'
+import MarketplacesTable, { STEP_ID as MARKET_ID } from 'client/components/Forms/MarketplaceApp/CreateForm/Steps/MarketplacesTable'
+import { createSteps } from 'client/utils'
+
+const Steps = createSteps(
+ [BasicConfiguration, MarketplacesTable],
+ {
+ transformInitialValue: (initialValues, schema) => {
+ return schema.cast({ [BASIC_ID]: initialValues }, { stripUnknown: true })
+ },
+ transformBeforeSubmit: formData => {
+ const { [BASIC_ID]: configuration, [MARKET_ID]: [market] = [] } = formData
+
+ return { market: market?.ID, ...configuration }
+ }
+ }
+)
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/index.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/index.js
new file mode 100644
index 0000000000..2567454c35
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/CreateForm/index.js
@@ -0,0 +1,65 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import { useEffect, useMemo, JSXElementConstructor } from 'react'
+import PropTypes from 'prop-types'
+
+import { useForm, FormProvider } from 'react-hook-form'
+import { yupResolver } from '@hookform/resolvers/yup'
+
+import { useDatastoreApi } from 'client/features/One'
+import FormStepper from 'client/components/FormStepper'
+import Steps from 'client/components/Forms/MarketplaceApp/CreateForm/Steps'
+
+/**
+ * Form to create a Marketplace App.
+ *
+ * @param {object} props - Props
+ * @param {string} props.initialValues - Initial values
+ * @param {function():object} props.onSubmit - Handle submit function
+ * @returns {JSXElementConstructor} Form component
+ */
+const CreateForm = ({ initialValues, onSubmit }) => {
+ const { getDatastores } = useDatastoreApi()
+ const stepsForm = useMemo(() => Steps(initialValues, initialValues), [])
+ const { steps, defaultValues, resolver, transformBeforeSubmit } = stepsForm
+
+ const methods = useForm({
+ mode: 'onSubmit',
+ defaultValues,
+ resolver: yupResolver(resolver?.())
+ })
+
+ useEffect(() => {
+ getDatastores()
+ }, [])
+
+ return (
+
+ onSubmit(transformBeforeSubmit?.(data) ?? data)}
+ />
+
+ )
+}
+
+CreateForm.propTypes = {
+ initialValues: PropTypes.object,
+ onSubmit: PropTypes.func
+}
+
+export default CreateForm
diff --git a/src/fireedge/src/client/components/Forms/MarketplaceApp/index.js b/src/fireedge/src/client/components/Forms/MarketplaceApp/index.js
index f61882b993..60e6654b4f 100644
--- a/src/fireedge/src/client/components/Forms/MarketplaceApp/index.js
+++ b/src/fireedge/src/client/components/Forms/MarketplaceApp/index.js
@@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
+import CreateForm from 'client/components/Forms/MarketplaceApp/CreateForm'
import ExportForm from 'client/components/Forms/MarketplaceApp/ExportForm'
export {
+ CreateForm,
ExportForm
}
diff --git a/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js b/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js
index 06cb4b71cc..ef08e28f50 100644
--- a/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js
+++ b/src/fireedge/src/client/components/Tables/MarketplaceApps/actions.js
@@ -15,9 +15,10 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
-// import { useHistory } from 'react-router-dom'
+import { useHistory } from 'react-router-dom'
import {
RefreshDouble,
+ AddSquare,
CloudDownload
} from 'iconoir-react'
@@ -26,12 +27,10 @@ import { useGeneralApi } from 'client/features/General'
import { useMarketplaceAppApi } from 'client/features/One'
import { Translate } from 'client/components/HOC'
-import {
- ExportForm
-} from 'client/components/Forms/MarketplaceApp'
+import { ExportForm } from 'client/components/Forms/MarketplaceApp'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
-// import { PATH } from 'client/apps/sunstone/routesOne'
+import { PATH } from 'client/apps/sunstone/routesOne'
import { T, MARKETPLACE_APP_ACTIONS } from 'client/constants'
const MessageToConfirmAction = rows => {
@@ -53,6 +52,7 @@ const MessageToConfirmAction = rows => {
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
const Actions = () => {
+ const history = useHistory()
const { view, getResourceView } = useAuth()
const { enqueueSuccess } = useGeneralApi()
const { getMarketplaceApps, exportApp } = useMarketplaceAppApi()
@@ -68,6 +68,14 @@ const Actions = () => {
await getMarketplaceApps()
}
},
+ {
+ accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
+ tooltip: T.CreateMarketApp,
+ icon: AddSquare,
+ action: () => {
+ history.push(PATH.STORAGE.MARKETPLACE_APPS.CREATE)
+ }
+ },
{
accessor: MARKETPLACE_APP_ACTIONS.EXPORT,
tooltip: T.ImportIntoDatastore,
diff --git a/src/fireedge/src/client/components/Tables/Marketplaces/columns.js b/src/fireedge/src/client/components/Tables/Marketplaces/columns.js
index eac336d0c7..7c4ba29c1b 100644
--- a/src/fireedge/src/client/components/Tables/Marketplaces/columns.js
+++ b/src/fireedge/src/client/components/Tables/Marketplaces/columns.js
@@ -36,7 +36,17 @@ export default [
}),
filter: 'includesValue'
},
- { Header: 'Market', accessor: 'MARKET_MAD' },
+ {
+ Header: 'Market',
+ accessor: 'MARKET_MAD',
+ disableFilters: false,
+ Filter: ({ column }) => CategoryFilter({
+ column,
+ multiple: true,
+ title: 'Market mad'
+ }),
+ filter: 'includesValue'
+ },
{ Header: 'Total Capacity', accessor: 'TOTAL_MB' },
{ Header: 'Free Capacity', accessor: 'USED_MB' },
{ Header: 'Zone ID', accessor: 'ZONE_ID' },
diff --git a/src/fireedge/src/client/components/Tables/Marketplaces/index.js b/src/fireedge/src/client/components/Tables/Marketplaces/index.js
index 1a3bfcd95e..9660e446ab 100644
--- a/src/fireedge/src/client/components/Tables/Marketplaces/index.js
+++ b/src/fireedge/src/client/components/Tables/Marketplaces/index.js
@@ -15,21 +15,27 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
+import PropTypes from 'prop-types'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useMarketplace, useMarketplaceApi } from 'client/features/One'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
+import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import MarketplaceColumns from 'client/components/Tables/Marketplaces/columns'
import MarketplaceRow from 'client/components/Tables/Marketplaces/row'
-const MarketplacesTable = () => {
- const columns = useMemo(() => MarketplaceColumns, [])
+const MarketplacesTable = ({ filter, ...props }) => {
+ const { view, getResourceView, filterPool } = useAuth()
+
+ const columns = useMemo(() => createColumns({
+ filters: getResourceView('MARKETPLACE')?.filters,
+ columns: MarketplaceColumns
+ }), [view])
const marketplaces = useMarketplace()
const { getMarketplaces } = useMarketplaceApi()
- const { filterPool } = useAuth()
const { status, fetchRequest, loading, reloading, STATUS } = useFetch(getMarketplaces)
const { INIT, PENDING } = STATUS
@@ -43,12 +49,23 @@ const MarketplacesTable = () => {
return (
String(row.ID)}
RowComponent={MarketplaceRow}
+ {...props}
/>
)
}
+MarketplacesTable.propTypes = {
+ filter: PropTypes.func,
+ ...EnhancedTable.propTypes
+}
+
+MarketplacesTable.displayName = 'MarketplacesTable'
+
export default MarketplacesTable
diff --git a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js
index cd0ce60022..244b62e479 100644
--- a/src/fireedge/src/client/components/Tables/VmTemplates/actions.js
+++ b/src/fireedge/src/client/components/Tables/VmTemplates/actions.js
@@ -34,7 +34,13 @@ import { Tr, Translate } from 'client/components/HOC'
import { CloneForm } from 'client/components/Forms/VmTemplate'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
-import { T, VM_TEMPLATE_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
+
+import {
+ T,
+ VM_TEMPLATE_ACTIONS,
+ MARKETPLACE_APP_ACTIONS,
+ RESOURCE_NAMES
+} from 'client/constants'
const MessageToConfirmAction = rows => {
const names = rows?.map?.(({ original }) => original?.NAME)
@@ -109,6 +115,18 @@ const Actions = () => {
history.push(path, template)
}
},
+ {
+ accessor: VM_TEMPLATE_ACTIONS.CREATE_APP_DIALOG,
+ tooltip: T.CreateMarketApp,
+ selected: { max: 1 },
+ icon: Cart,
+ action: rows => {
+ const template = rows?.[0]?.original ?? {}
+ const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
+
+ history.push(path, [RESOURCE_NAMES.VM_TEMPLATE, template])
+ }
+ },
{
accessor: VM_TEMPLATE_ACTIONS.UPDATE_DIALOG,
label: T.Update,
diff --git a/src/fireedge/src/client/components/Tables/Vms/actions.js b/src/fireedge/src/client/components/Tables/Vms/actions.js
index eea644da43..ba61171cc5 100644
--- a/src/fireedge/src/client/components/Tables/Vms/actions.js
+++ b/src/fireedge/src/client/components/Tables/Vms/actions.js
@@ -46,7 +46,7 @@ import {
import { createActions } from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { getLastHistory, isAvailableAction } from 'client/models/VirtualMachine'
-import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
+import { T, VM_ACTIONS, RESOURCE_NAMES } from 'client/constants'
const isDisabled = action => rows =>
isAvailableAction(action)(rows, ({ values }) => values?.STATE)
@@ -153,6 +153,19 @@ const Actions = () => {
ids?.length > 1 && (await Promise.all(ids.map(id => getVm(id))))
}
},
+ {
+ accessor: VM_ACTIONS.CREATE_APP_DIALOG,
+ disabled: isDisabled(VM_ACTIONS.CREATE_APP_DIALOG),
+ tooltip: T.CreateMarketApp,
+ selected: { max: 1 },
+ icon: Cart,
+ action: rows => {
+ const vm = rows?.[0]?.original ?? {}
+ const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
+
+ history.push(path, [RESOURCE_NAMES.VM, vm])
+ }
+ },
{
accessor: VM_ACTIONS.SAVE_AS_TEMPLATE,
disabled: isDisabled(VM_ACTIONS.SAVE_AS_TEMPLATE),
@@ -518,23 +531,7 @@ const Actions = () => {
]
}), [view])
- const marketplaceAppActions = useMemo(() => createActions({
- filters: getResourceView('MARKETPLACE-APP')?.actions,
- actions: [
- {
- accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
- tooltip: T.CreateMarketApp,
- icon: Cart,
- selected: { max: 1 },
- disabled: true,
- action: rows => {
- // TODO: go to Marketplace App CREATE form
- }
- }
- ]
- }), [view])
-
- return [...vmActions, ...marketplaceAppActions]
+ return vmActions
}
export default Actions
diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js
index 2b174232b7..164be59955 100644
--- a/src/fireedge/src/client/constants/index.js
+++ b/src/fireedge/src/client/constants/index.js
@@ -87,6 +87,25 @@ export const SOCKETS = {
PROVISION: 'provision'
}
+/** @enum {string} Names of resource */
+export const RESOURCE_NAMES = {
+ APP: 'marketplace-app',
+ CLUSTER: 'cluster',
+ DATASTORE: 'datastore',
+ GROUP: 'group',
+ HOST: 'host',
+ IMAGE: 'image',
+ MARKETPLACE: 'marketplace',
+ SEC_GROUP: 'security-group',
+ USER: 'user',
+ V_ROUTER: 'virtual-router',
+ VM_TEMPLATE: 'vm-template',
+ VM: 'vm',
+ VN_TEMPLATE: 'network-template',
+ VNET: 'virtual-network',
+ ZONE: 'zone'
+}
+
export * as T from 'client/constants/translates'
export * as ACTIONS from 'client/constants/actions'
export * as STATES from 'client/constants/states'
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index d338f12906..4701766314 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -106,6 +106,7 @@ module.exports = {
SelectRequest: 'Select request',
SelectVmTemplate: 'Select a VM Template',
SelectDatastore: 'Select a Datastore to store the resource',
+ SelectMarketplace: 'Select Marketplace',
Share: 'Share',
Show: 'Show',
ShowAll: 'Show all',
diff --git a/src/fireedge/src/client/constants/vm.js b/src/fireedge/src/client/constants/vm.js
index b2226a0e62..37d062dbf7 100644
--- a/src/fireedge/src/client/constants/vm.js
+++ b/src/fireedge/src/client/constants/vm.js
@@ -434,6 +434,7 @@ export const VM_LCM_STATES = [
export const VM_ACTIONS = {
REFRESH: 'refresh',
CREATE_DIALOG: 'create_dialog',
+ CREATE_APP_DIALOG: 'create_app_dialog',
DEPLOY: 'deploy',
HOLD: 'hold',
LOCK: 'lock',
@@ -515,6 +516,7 @@ export const VM_ACTIONS_BY_STATE = {
STATES.UNDEPLOYED,
STATES.UNKNOWN
],
+ [VM_ACTIONS.CREATE_APP_DIALOG]: [STATES.POWEROFF],
[VM_ACTIONS.HOLD]: [STATES.PENDING],
[VM_ACTIONS.LOCK]: [],
[VM_ACTIONS.MIGRATE_LIVE]: [STATES.RUNNING, STATES.UNKNOWN],
diff --git a/src/fireedge/src/client/constants/vmTemplate.js b/src/fireedge/src/client/constants/vmTemplate.js
index f7995e4636..ed666feb04 100644
--- a/src/fireedge/src/client/constants/vmTemplate.js
+++ b/src/fireedge/src/client/constants/vmTemplate.js
@@ -21,6 +21,7 @@ export const VM_TEMPLATE_ACTIONS = {
IMPORT_DIALOG: 'import_dialog',
UPDATE_DIALOG: 'update_dialog',
INSTANTIATE_DIALOG: 'instantiate_dialog',
+ CREATE_APP_DIALOG: 'create_app_dialog',
CLONE: 'clone',
DELETE: 'delete',
LOCK: 'lock',
diff --git a/src/fireedge/src/client/containers/MarketplaceApps/Create.js b/src/fireedge/src/client/containers/MarketplaceApps/Create.js
new file mode 100644
index 0000000000..edef09269c
--- /dev/null
+++ b/src/fireedge/src/client/containers/MarketplaceApps/Create.js
@@ -0,0 +1,55 @@
+/* ------------------------------------------------------------------------- *
+ * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may *
+ * not use this file except in compliance with the License. You may obtain *
+ * a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an "AS IS" BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * ------------------------------------------------------------------------- */
+import { useMemo, JSXElementConstructor } from 'react'
+import { useHistory, useLocation } from 'react-router'
+import { Container } from '@mui/material'
+
+import { useGeneralApi } from 'client/features/General'
+// import { useMarketplaceAppApi } from 'client/features/One'
+import { CreateForm } from 'client/components/Forms/MarketplaceApp'
+import { isDevelopment } from 'client/utils'
+
+/**
+ * Displays the creation or modification form to a Marketplace App.
+ *
+ * @returns {JSXElementConstructor} Marketplace App form
+ */
+function CreateMarketplaceApp () {
+ const history = useHistory()
+ const { state: [resourceName, { ID } = {}] = [] } = useLocation()
+ const initialValues = useMemo(() => ({ type: resourceName, id: ID }), [])
+
+ const { enqueueSuccess } = useGeneralApi()
+ // const { } = useMarketplaceAppApi()
+
+ const onSubmit = async template => {
+ try {
+ isDevelopment() && console.log({ template })
+ history.goBack()
+ enqueueSuccess('TODO: Marketplace app request')
+ } catch (err) {
+ isDevelopment() && console.error(err)
+ }
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default CreateMarketplaceApp
diff --git a/src/fireedge/src/client/features/Auth/hooks.js b/src/fireedge/src/client/features/Auth/hooks.js
index 5e82635488..f678d7c98e 100644
--- a/src/fireedge/src/client/features/Auth/hooks.js
+++ b/src/fireedge/src/client/features/Auth/hooks.js
@@ -23,6 +23,7 @@ import * as provisionActions from 'client/features/Auth/provision'
import * as sunstoneActions from 'client/features/Auth/sunstone'
import { name as authSlice } from 'client/features/Auth/slice'
import { name as oneSlice, RESOURCES } from 'client/features/One/slice'
+import { RESOURCE_NAMES } from 'client/constants'
export const useAuth = () => {
const auth = useSelector(state => state[authSlice], shallowEqual)
@@ -40,7 +41,7 @@ export const useAuth = () => {
/**
* Looking for resource view of user authenticated.
*
- * @param {string} resourceName - Name of resource: VM, HOST, IMAGE, etc
+ * @param {RESOURCE_NAMES} resourceName - Name of resource
* @returns {{
* resource_name: string,
* actions: object[],
diff --git a/src/fireedge/src/client/models/Datastore.js b/src/fireedge/src/client/models/Datastore.js
index e996edcf52..7f59fb859c 100644
--- a/src/fireedge/src/client/models/Datastore.js
+++ b/src/fireedge/src/client/models/Datastore.js
@@ -52,9 +52,9 @@ export const getDeployMode = (datastore = {}) => {
/**
* Returns information about datastore capacity.
*
- * @param {object} props - Props object
- * @param {number} props.TOTAL_MB - Datastore total space in MB
- * @param {number} props.USED_MB - Datastore used space in MB
+ * @param {object} datastore - Datastore
+ * @param {number} datastore.TOTAL_MB - Total capacity in MB
+ * @param {number} datastore.USED_MB - Used capacity in MB
* @returns {{
* percentOfUsed: number,
* percentLabel: string
@@ -68,3 +68,19 @@ export const getCapacityInfo = ({ TOTAL_MB, USED_MB } = {}) => {
return { percentOfUsed, percentLabel }
}
+
+/**
+ * Returns `true` if Datastore allows to export to Marketplace.
+ *
+ * @param {object} props - Datastore ob
+ * @param {object} props.NAME - Name
+ * @param {object} oneConfig - One config from redux
+ * @returns {boolean} - Datastore supports to export
+ */
+export const isMarketExportSupport = ({ NAME } = {}, oneConfig) => {
+ // When in doubt, allow the action and let oned return failure
+ return !NAME || oneConfig?.DS_MAD_CONF?.some(dsMad => (
+ dsMad?.NAME === NAME &&
+ dsMad?.MARKETPLACE_ACTIONS?.includes?.('export')
+ ))
+}
diff --git a/src/fireedge/src/client/utils/string.js b/src/fireedge/src/client/utils/string.js
index dc6fea0470..784a4c3a1a 100644
--- a/src/fireedge/src/client/utils/string.js
+++ b/src/fireedge/src/client/utils/string.js
@@ -20,7 +20,7 @@
* @param {string} input - Input string
* @returns {string} Input string modified
*/
-export const upperCaseFirst = input => input.charAt(0).toUpperCase() + input.substr(1)
+export const upperCaseFirst = input => input?.charAt(0)?.toUpperCase() + input?.substr(1)
/**
* Transform into a lower case with spaces between words, then capitalize the string.
@@ -34,10 +34,10 @@ export const upperCaseFirst = input => input.charAt(0).toUpperCase() + input.sub
*/
export const sentenceCase = input => {
const sentence = input
- .replace(/[-_]([A-Za-z])/g, ' $1')
- .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .toLowerCase()
+ ?.replace(/[-_]([A-Za-z])/g, ' $1')
+ ?.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
+ ?.replace(/([a-z])([A-Z])/g, '$1 $2')
+ ?.toLowerCase()
return upperCaseFirst(sentence)
}