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) }