1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-22 18:50:08 +03:00

F #5638: Add form to create marketplace app (#1618)

This commit is contained in:
Sergio Betanzos 2021-11-26 13:16:30 +01:00 committed by GitHub
parent 3250528bef
commit ce2d741c15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 611 additions and 82 deletions

View File

@ -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
}
]
},

View File

@ -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 (
<>
<Legend title={label} tooltip={tooltip} />
@ -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
)

View File

@ -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 }
}, {})

View File

@ -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 (
<FormWithSchema
cy={'create-marketplace-app.configuration'}
fields={FIELDS}
id={STEP_ID}
/>
)
}
/**
* 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

View File

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

View File

@ -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 (
<MarketplacesTable
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={market => 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

View File

@ -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(() => [])

View File

@ -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

View File

@ -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 (
<FormProvider {...methods}>
<FormStepper
steps={steps}
schema={resolver}
onSubmit={data => onSubmit(transformBeforeSubmit?.(data) ?? data)}
/>
</FormProvider>
)
}
CreateForm.propTypes = {
initialValues: PropTypes.object,
onSubmit: PropTypes.func
}
export default CreateForm

View File

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

View File

@ -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,

View File

@ -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' },

View File

@ -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 (
<EnhancedTable
columns={columns}
data={marketplaces}
data={typeof filter === 'function'
? marketplaces?.filter(filter)
: marketplaces
}
isLoading={loading || reloading}
getRowId={row => String(row.ID)}
RowComponent={MarketplaceRow}
{...props}
/>
)
}
MarketplacesTable.propTypes = {
filter: PropTypes.func,
...EnhancedTable.propTypes
}
MarketplacesTable.displayName = 'MarketplacesTable'
export default MarketplacesTable

View File

@ -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,

View File

@ -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

View File

@ -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'

View File

@ -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',

View File

@ -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],

View File

@ -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',

View File

@ -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 (
<Container style={{ display: 'flex', flexFlow: 'column' }} disableGutters>
<CreateForm initialValues={initialValues} onSubmit={onSubmit} />
</Container>
)
}
export default CreateMarketplaceApp

View File

@ -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[],

View File

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

View File

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