1
0
mirror of https://github.com/OpenNebula/one.git synced 2024-12-22 13:33:52 +03:00

F OpenNebula/one#6117: Marketplace tab (#2975)

Signed-off-by: David Carracedo <dcarracedo@opennebula.io>
This commit is contained in:
David 2024-03-08 12:36:08 +01:00 committed by GitHub
parent 868597a4b3
commit 45bcac8e1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1814 additions and 21 deletions

View File

@ -2996,7 +2996,8 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
src/fireedge/etc/sunstone/admin/acl-tab.yaml \
src/fireedge/etc/sunstone/admin/cluster-tab.yaml \
src/fireedge/etc/sunstone/admin/support-tab.yaml \
src/fireedge/etc/sunstone/admin/zone-tab.yaml"
src/fireedge/etc/sunstone/admin/zone-tab.yaml \
src/fireedge/etc/sunstone/admin/marketplace-tab.yaml"
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \
src/fireedge/etc/sunstone/user/vm-template-tab.yaml \

View File

@ -13,8 +13,8 @@ actions:
chown: true
chgrp: true
delete: true
lock: true
unlock: true
disable: true
enable: true
# Filters - List of criteria to filter the resources
@ -47,5 +47,5 @@ info-tabs:
edit: true
delete: true
app:
apps:
enabled: true

View File

@ -174,9 +174,17 @@ const CreateDockerfile = loadable(
ssr: false,
}
)
// Marketplace
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), {
ssr: false,
})
const CreateMarketplace = loadable(
() => import('client/containers/Marketplaces/Create'),
{ ssr: false }
)
// Marketplace app
const MarketplaceApps = loadable(
() => import('client/containers/MarketplaceApps'),
{ ssr: false }
@ -185,6 +193,12 @@ const CreateMarketplaceApp = loadable(
() => import('client/containers/MarketplaceApps/Create'),
{ ssr: false }
)
const MarketplaceAppDetail = loadable(
() => import('client/containers/MarketplaceApps/Detail'),
{
ssr: false,
}
)
const VirtualNetworks = loadable(
() => import('client/containers/VirtualNetworks'),
@ -356,6 +370,7 @@ export const PATH = {
MARKETPLACES: {
LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`,
CREATE: `/${RESOURCE_NAMES.MARKETPLACE}/create`,
},
MARKETPLACE_APPS: {
LIST: `/${RESOURCE_NAMES.APP}`,
@ -625,6 +640,14 @@ const ENDPOINTS = [
icon: MarketplaceIcon,
Component: Marketplaces,
},
{
title: (_, state) =>
state?.ID !== undefined ? T.UpdateMarketplace : T.CreateMarketplace,
description: (_, state) =>
state?.ID !== undefined && `#${state.ID} ${state.NAME}`,
path: PATH.STORAGE.MARKETPLACES.CREATE,
Component: CreateMarketplace,
},
{
title: T.Apps,
path: PATH.STORAGE.MARKETPLACE_APPS.LIST,
@ -637,6 +660,12 @@ const ENDPOINTS = [
path: PATH.STORAGE.MARKETPLACE_APPS.CREATE,
Component: CreateMarketplaceApp,
},
{
title: T.App,
description: (params) => `#${params?.id}`,
path: PATH.STORAGE.MARKETPLACE_APPS.DETAIL,
Component: MarketplaceAppDetail,
},
{
title: T.CreateBackupJob,
path: PATH.STORAGE.BACKUPJOBS.CREATE,

View File

@ -21,6 +21,7 @@ import {
Group,
Server,
WarningCircledOutline as WarningIcon,
MinusPinAlt as ZoneIcon,
} from 'iconoir-react'
import { Box, Typography, Tooltip } from '@mui/material'
@ -47,7 +48,8 @@ const MarketplaceCard = memo(
({ market, rootProps, actions }) => {
const classes = rowStyles()
const { ID, NAME, UNAME, GNAME, MARKET_MAD, MARKETPLACEAPPS } = market
const { ID, NAME, UNAME, GNAME, MARKET_MAD, MARKETPLACEAPPS, ZONE_ID } =
market
const { color: stateColor, name: stateName } = getState(market)
const error = useMemo(() => getErrorMessage(market), [market])
@ -96,6 +98,10 @@ const MarketplaceCard = memo(
<Server />
<span>{` ${apps}`}</span>
</span>
<span title={`${Tr(T.Zone)}: ${ZONE_ID}`}>
<ZoneIcon />
<span>{` ${ZONE_ID}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}>

View File

@ -111,10 +111,7 @@ const Steps = createSteps([General, Hosts, Vnets, Datastores], {
)
// Check if the name has been changed
const changeName =
initialValues?.NAME === formData?.general?.NAME
? undefined
: formData?.general?.NAME
const changeName = initialValues?.NAME === formData?.general?.NAME
return {
...formData,

View File

@ -0,0 +1,91 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T, MARKET_TYPES } from 'client/constants'
import { string } from 'yup'
import { Field } from 'client/utils'
/** @type {Field} BASE_URL field */
const BASE_URL = {
name: 'BASE_URL',
label: (type) => {
if (type === MARKET_TYPES.HTTP.value)
return T['marketplace.form.configuration.http.url']
else if (type === MARKET_TYPES.DOCKER_REGISTRY.value)
return T['marketplace.form.configuration.dockerRegistry.url']
},
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) =>
type !== MARKET_TYPES.HTTP.value &&
type !== MARKET_TYPES.DOCKER_REGISTRY.value &&
INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when('$general.MARKET_MAD', (type, schema) => {
if (type)
return type !== MARKET_TYPES.HTTP.value &&
type !== MARKET_TYPES.DOCKER_REGISTRY.value
? schema.strip()
: schema.required()
})
.default(() => undefined),
grid: { md: 12 },
fieldProps: { placeholder: 'http://frontend.opennebula.org/' },
}
/** @type {Field} ENDPOINT field */
const ENDPOINT = {
name: 'ENDPOINT',
label: (type) => {
if (type === MARKET_TYPES.S3.value)
return T['marketplace.form.configuration.s3.endpoint']
else if (type === MARKET_TYPES.OPENNEBULA.value)
return T['marketplace.form.configuration.one.url']
},
tooltip: (type) => {
if (type === MARKET_TYPES.S3.value)
return T['marketplace.form.configuration.s3.endpoint.tooltip']
},
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) =>
type !== MARKET_TYPES.S3.value &&
type !== MARKET_TYPES.OPENNEBULA.value &&
INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when(
['$general.MARKET_MAD', '$configuration.AWS'],
(type, aws, schema) => {
if (type) {
if (
type === MARKET_TYPES.OPENNEBULA.value ||
(type === MARKET_TYPES.S3.value && !aws)
)
return schema.required()
else if (type === MARKET_TYPES.S3.value && aws)
return schema.notRequired()
else return schema.strip()
}
}
)
.default(() => undefined),
grid: { md: 12 },
}
const FIELDS = [BASE_URL, ENDPOINT]
export { FIELDS }

View File

@ -0,0 +1,43 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T, MARKET_TYPES } from 'client/constants'
import { boolean } from 'yup'
import { Field } from 'client/utils'
/** @type {Field} SSL field */
const SSL = {
name: 'SSL',
label: T['marketplace.form.configuration.dockerRegistry.ssl'],
tooltip: T['marketplace.form.configuration.dockerRegistry.ssl.tooltip'],
type: INPUT_TYPES.SWITCH,
dependOf: '$general.MARKET_MAD',
htmlType: (type) =>
type !== MARKET_TYPES.DOCKER_REGISTRY.value && INPUT_TYPES.HIDDEN,
validation: boolean()
.yesOrNo()
.afterSubmit((value, { context }) => {
if (context?.general?.MARKET_MAD === MARKET_TYPES.DOCKER_REGISTRY.value) {
return value ? 'YES' : 'NO'
} else {
return undefined
}
}),
grid: { xs: 12, md: 6 },
}
const FIELDS = [SSL]
export { FIELDS }

View File

@ -0,0 +1,64 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T, MARKET_TYPES } from 'client/constants'
import { string, array } from 'yup'
import { Field } from 'client/utils'
/** @type {Field} PUBLIC_DIR field */
const PUBLIC_DIR = {
name: 'PUBLIC_DIR',
label: T['marketplace.form.configuration.http.path'],
tooltip: T['marketplace.form.configuration.http.path.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.HTTP.value && INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when('$general.MARKET_MAD', (type, schema) => {
if (type)
return type !== MARKET_TYPES.HTTP.value
? schema.strip()
: schema.required()
})
.default(() => undefined),
grid: { md: 12 },
fieldProps: { placeholder: '/var/local/market-http' },
}
/** @type {Field} BRIDGE_LIST field */
const BRIDGE_LIST = {
name: 'BRIDGE_LIST',
label: T['marketplace.form.configuration.http.bridge'],
tooltip: [T.PressKeysToAddAValue, ['ENTER']],
type: INPUT_TYPES.AUTOCOMPLETE,
multiple: true,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.HTTP.value && INPUT_TYPES.HIDDEN,
validation: array(string().trim())
.notRequired()
.default(() => undefined)
.afterSubmit((value, { context }) =>
context?.general?.MARKET_MAD === MARKET_TYPES.HTTP.value && value
? value.join(' ')
: undefined
),
grid: { md: 12 },
fieldProps: { freeSolo: true },
}
const FIELDS = [PUBLIC_DIR, BRIDGE_LIST]
export { FIELDS }

View File

@ -0,0 +1,207 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T, MARKET_TYPES } from 'client/constants'
import { string, boolean, number } from 'yup'
import { Field } from 'client/utils'
/** @type {Field} AWS field */
const AWS = {
name: 'AWS',
label: T['marketplace.form.configuration.s3.aws'],
tooltip: T['marketplace.form.configuration.s3.aws.tooltip'],
type: INPUT_TYPES.SWITCH,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.S3.value && INPUT_TYPES.HIDDEN,
validation: boolean()
.yesOrNo()
.afterSubmit((value, { context }) => {
if (context?.general?.MARKET_MAD === MARKET_TYPES.S3.value) {
return value ? 'YES' : 'NO'
} else {
return undefined
}
})
.default(() => true),
grid: { xs: 12, md: 6 },
}
/** @type {Field} ACCESS_KEY_ID field */
const ACCESS_KEY_ID = {
name: 'ACCESS_KEY_ID',
label: T['marketplace.form.configuration.s3.accessKey'],
tooltip: T['marketplace.form.configuration.s3.accessKey.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.S3.value && INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when(
'$general.MARKET_MAD',
(type, schema) =>
type &&
(type !== MARKET_TYPES.S3.value ? schema.strip() : schema.required())
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} SECRET_ACCESS_KEY field */
const SECRET_ACCESS_KEY = {
name: 'SECRET_ACCESS_KEY',
label: T['marketplace.form.configuration.s3.secretAccessKey'],
tooltip: T['marketplace.form.configuration.s3.secretAccessKey.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.S3.value && INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when(
'$general.MARKET_MAD',
(type, schema) =>
type &&
(type !== MARKET_TYPES.S3.value ? schema.strip() : schema.required())
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} BUCKET field */
const BUCKET = {
name: 'BUCKET',
label: T['marketplace.form.configuration.s3.bucket'],
tooltip: T['marketplace.form.configuration.s3.bucket.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.S3.value && INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when(
'$general.MARKET_MAD',
(type, schema) =>
type &&
(type !== MARKET_TYPES.S3.value ? schema.strip() : schema.required())
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} REGION field */
const REGION = {
name: 'REGION',
label: T['marketplace.form.configuration.s3.region'],
tooltip: T['marketplace.form.configuration.s3.region.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) => type !== MARKET_TYPES.S3.value && INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.when(
'$general.MARKET_MAD',
(type, schema) =>
type &&
(type !== MARKET_TYPES.S3.value ? schema.strip() : schema.required())
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} SIGNATURE_VERSION field */
const SIGNATURE_VERSION = {
name: 'SIGNATURE_VERSION',
type: INPUT_TYPES.TEXT,
htmlType: INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.afterSubmit((value, { context }) =>
context?.general?.MARKET_MAD === MARKET_TYPES.S3.value &&
!context?.configuration?.AWS
? 's3'
: undefined
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} FORCE_PATH_STYLE field */
const FORCE_PATH_STYLE = {
name: 'FORCE_PATH_STYLE',
type: INPUT_TYPES.TEXT,
htmlType: INPUT_TYPES.HIDDEN,
validation: string()
.trim()
.afterSubmit((value, { context }) =>
context?.general?.MARKET_MAD === MARKET_TYPES.S3.value &&
!context?.configuration?.AWS
? 'YES'
: undefined
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} TOTAL_MB field */
const TOTAL_MB = {
name: 'TOTAL_MB',
label: T['marketplace.form.configuration.s3.totalMB'],
tooltip: T['marketplace.form.configuration.s3.totalMB.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) =>
type !== MARKET_TYPES.S3.value ? INPUT_TYPES.HIDDEN : 'number',
validation: number()
.when(
'$general.MARKET_MAD',
(type, schema) =>
type &&
(type !== MARKET_TYPES.S3.value ? schema.strip() : schema.notRequired())
)
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} TOTAL_MB field */
const READ_LENGTH = {
name: 'READ_LENGTH',
label: T['marketplace.form.configuration.s3.readLength'],
tooltip: T['marketplace.form.configuration.s3.readLength.tooltip'],
type: INPUT_TYPES.TEXT,
dependOf: '$general.MARKET_MAD',
htmlType: (type) =>
type !== MARKET_TYPES.S3.value ? INPUT_TYPES.HIDDEN : 'number',
validation: number()
.when(
'$general.MARKET_MAD',
(type, schema) =>
type &&
(type !== MARKET_TYPES.S3.value ? schema.strip() : schema.notRequired())
)
.default(() => undefined),
grid: { md: 12 },
}
const FIELDS = [
AWS,
ACCESS_KEY_ID,
SECRET_ACCESS_KEY,
BUCKET,
REGION,
SIGNATURE_VERSION,
FORCE_PATH_STYLE,
TOTAL_MB,
READ_LENGTH,
]
export { FIELDS }

View File

@ -0,0 +1,301 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { T, MARKET_TYPES } from 'client/constants'
import { SCHEMA, FIELDS } from './schema'
import { Grid, Card, CardContent, Typography, Alert } from '@mui/material'
import { Tr } from 'client/components/HOC'
import { generateDocLink } from 'client/utils'
import { useFormContext } from 'react-hook-form'
import makeStyles from '@mui/styles/makeStyles'
import { find } from 'lodash'
export const STEP_ID = 'configuration'
/**
* Return content of the step.
*
* @param {string} version - OpenNebula version
* @returns {object} - Content of the step
*/
const Content = (version) => {
// Style for info message
const useStyles = makeStyles(({ palette }) => ({
groupInfo: {
'&': {
gridColumn: 'span 2',
marginTop: '1em',
backgroundColor: palette.background.paper,
},
},
}))
const classes = useStyles()
const { getValues } = useFormContext()
const isDockerhub =
getValues('general.MARKET_MAD') === MARKET_TYPES.DOCKERHUB.value
const type = find(MARKET_TYPES, { value: getValues('general.MARKET_MAD') })
return (
<Grid mt={2} container>
<Grid item xs={8}>
{isDockerhub && (
<Alert
severity="info"
variant="outlined"
className={classes.groupInfo}
>
{Tr(T['marketplace.form.configuration.dockerhub.info'])}
</Alert>
)}
<FormWithSchema id={STEP_ID} cy={`${STEP_ID}`} fields={FIELDS} />
</Grid>
<Grid item xs={4}>
<Card
elevation={2}
sx={{
height: '100%',
minHeight: '630px',
maxHeight: '630px',
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
marginLeft: '1em',
marginTop: '1rem',
}}
>
<CardContent
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
gap: '1em',
}}
>
<Typography variant="h6" component="div" gutterBottom>
{Tr(T['marketplace.general.help.title'])}
</Typography>
{type?.value === 'one' && (
<>
<Typography variant="body2" gutterBottom>
<b>{Tr(T['marketplace.types.one'])}</b>
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.form.configuration.one.help.paragraph.1'])}
<a
target="_blank"
href="http://marketplace.opennebula.io/appliance"
rel="noreferrer"
>
{Tr(
T[
'marketplace.form.configuration.one.help.paragraph.1.link'
]
)}
</a>
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.form.configuration.one.help.paragraph.2'])}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
<a
target="_blank"
href={generateDocLink(
version,
'marketplace/public_marketplaces/opennebula.html'
)}
rel="noreferrer"
>
{Tr(T['marketplace.form.configuration.one.help.link'])}
</a>
</Typography>
</>
)}
{type?.value === 'http' && (
<>
<Typography variant="body2" gutterBottom>
<b>{Tr(T['marketplace.types.http'])}</b>
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(
T['marketplace.form.configuration.http.help.paragraph.1']
)}
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(
T['marketplace.form.configuration.http.help.paragraph.2']
)}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
<a
target="_blank"
href={generateDocLink(
version,
'marketplace/private_marketplaces/market_http.html'
)}
rel="noreferrer"
>
{Tr(T['marketplace.form.configuration.http.help.link'])}
</a>
</Typography>
</>
)}
{type?.value === 's3' && (
<>
<Typography variant="body2" gutterBottom>
<b>{Tr(T['marketplace.types.s3'])}</b>
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.form.configuration.s3.help.paragraph.1'])}
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.form.configuration.s3.help.paragraph.2'])}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
<a
target="_blank"
href={generateDocLink(
version,
'marketplace/private_marketplaces/market_s3.html'
)}
rel="noreferrer"
>
{Tr(T['marketplace.form.configuration.s3.help.link'])}
</a>
</Typography>
</>
)}
{type?.value === 'dockerhub' && (
<>
<Typography variant="body2" gutterBottom>
<b>{Tr(T['marketplace.types.dockerhub'])}</b>
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(
T[
'marketplace.form.configuration.dockerhub.help.paragraph.1'
]
)}
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(
T[
'marketplace.form.configuration.dockerhub.help.paragraph.2'
]
)}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
<a
target="_blank"
href={generateDocLink(
version,
'marketplace/public_marketplaces/dockerhub.html'
)}
rel="noreferrer"
>
{Tr(
T['marketplace.form.configuration.dockerhub.help.link']
)}
</a>
</Typography>
</>
)}
{type?.value === 'docker_registry' && (
<>
<Typography variant="body2" gutterBottom>
<b>{Tr(T['marketplace.types.dockerRegistry'])}</b>
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(
T[
'marketplace.form.configuration.dockerRegistry.help.paragraph.1'
]
)}
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(
T[
'marketplace.form.configuration.dockerRegistry.help.paragraph.2'
]
)}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
<a
target="_blank"
href={generateDocLink(
version,
'marketplace/private_marketplaces/docker_registry.html'
)}
rel="noreferrer"
>
{Tr(
T[
'marketplace.form.configuration.dockerRegistry.help.link'
]
)}
</a>
</Typography>
</>
)}
</CardContent>
</Card>
</Grid>
</Grid>
)
}
/**
* Configuration attributes.
*
* @param {object} props - Step props
* @param {string} props.version - OpenNebula version
* @returns {object} AdvancedOptions configuration step
*/
const Configuration = ({ version }) => ({
id: STEP_ID,
label: T['marketplace.configuration.title'],
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: () => Content(version),
})
Configuration.propTypes = {
data: PropTypes.object,
setFormData: PropTypes.func,
}
export default Configuration

View File

@ -0,0 +1,31 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { getObjectSchemaFromFields } from 'client/utils'
import { FIELDS as COMMON_FIELDS } from './fieldsCommon'
import { FIELDS as HTTP_FIELDS } from './fieldsHttp'
import { FIELDS as S3_FIELDS } from './fieldsS3'
import { FIELDS as DOCKER_REGISTRY_FIELDS } from './fieldsDockerRegistry'
const FIELDS = [
...HTTP_FIELDS,
...S3_FIELDS,
...DOCKER_REGISTRY_FIELDS,
...COMMON_FIELDS,
]
const SCHEMA = getObjectSchemaFromFields(FIELDS)
export { SCHEMA, FIELDS }

View File

@ -0,0 +1,115 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { T } from 'client/constants'
import { SCHEMA, FIELDS } from './schema'
import { Grid, Card, CardContent, Typography } from '@mui/material'
import { Tr } from 'client/components/HOC'
import { generateDocLink } from 'client/utils'
export const STEP_ID = 'general'
const Content = (version) => (
<Grid mt={2} container>
<Grid item xs={8}>
<FormWithSchema id={STEP_ID} cy={`${STEP_ID}`} fields={FIELDS} />
</Grid>
<Grid item xs={4}>
<Card
elevation={2}
sx={{
height: '100%',
minHeight: '630px',
maxHeight: '630px',
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
marginLeft: '1em',
marginTop: '1rem',
}}
>
<CardContent
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
gap: '1em',
}}
>
<Typography variant="h6" component="div" gutterBottom>
{' '}
{Tr(T['marketplace.general.help.title'])}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.general.help.paragraph.1'])}{' '}
</Typography>
<ul>
<li>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.general.help.paragraph.2.1'])}{' '}
</Typography>
</li>
<li>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.general.help.paragraph.2.2'])}{' '}
</Typography>
</li>
</ul>
<Typography variant="body2" gutterBottom>
{Tr(T['marketplace.general.help.paragraph.3'])}{' '}
</Typography>
<Typography variant="body2" gutterBottom>
{' '}
<a
target="_blank"
href={generateDocLink(version, 'marketplace/index.html')}
rel="noreferrer"
>
{Tr(T['marketplace.form.create.help.link'])}
</a>
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
)
/**
* General Group configuration.
*
* @param {object} props - Step props
* @param {string} props.version - OpenNebula version
* @returns {object} General configuration step
*/
const General = ({ version }) => ({
id: STEP_ID,
label: T.General,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: () => Content(version),
})
General.propTypes = {
data: PropTypes.object,
setFormData: PropTypes.func,
}
export default General

View File

@ -0,0 +1,65 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T, MARKET_TYPES } from 'client/constants'
import { Field, getObjectSchemaFromFields, arrayToOptions } from 'client/utils'
import { string } from 'yup'
/** @type {Field} Name field */
const NAME = {
name: 'NAME',
label: T['marketplace.form.create.general.name'],
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.required()
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} Name field */
const DESCRIPTION = {
name: 'DESCRIPTION',
label: T['marketplace.form.create.general.description'],
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.notRequired()
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} Name field */
const TYPE = {
name: 'MARKET_MAD',
label: T['marketplace.form.create.general.type'],
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.keys(MARKET_TYPES), {
addEmpty: true,
getText: (key) => T[MARKET_TYPES[key].text],
getValue: (key) => MARKET_TYPES[key].value,
}),
validation: string()
.trim()
.required()
.default(() => undefined),
grid: { md: 12 },
}
const FIELDS = [NAME, DESCRIPTION, TYPE]
const SCHEMA = getObjectSchemaFromFields(FIELDS)
export { SCHEMA, FIELDS }

View File

@ -0,0 +1,72 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/Marketplace/CreateForm/Steps/General'
import Configuration, {
STEP_ID as CONFIGURATION_ID,
} from 'client/components/Forms/Marketplace/CreateForm/Steps/Configuration'
import { createSteps } from 'client/utils'
/**
* Create steps for Marketplace Create Form:
* 1. General: General attributes for marketplace
* 2. Configuration: Configuration attributes for marketplace depending its type
*/
const Steps = createSteps([General, Configuration], {
transformInitialValue: (marketplace, schema) => {
const knownTemplate = schema.cast(
{
[GENERAL_ID]: {
NAME: marketplace.NAME,
DESCRIPTION: marketplace.TEMPLATE.DESCRIPTION,
MARKET_MAD: marketplace.MARKET_MAD,
},
[CONFIGURATION_ID]: {
...marketplace.TEMPLATE,
BRIDGE_LIST: marketplace?.TEMPLATE.BRIDGE_LIST?.split(' '),
},
},
{
stripUnknown: true,
}
)
return knownTemplate
},
transformBeforeSubmit: (formData, initialValues) => {
// Get data from steps
const { [GENERAL_ID]: generalData } = formData
const { [CONFIGURATION_ID]: configurationData } = formData
// Check if the name has been changed
const changeName =
initialValues && initialValues?.NAME !== generalData?.NAME
? generalData?.NAME
: undefined
return {
...generalData,
...configurationData,
changeName,
}
},
})
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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. *
* ------------------------------------------------------------------------- */
export { default } from 'client/components/Forms/Marketplace/CreateForm/Steps'

View File

@ -0,0 +1,27 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement } from 'react'
import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
import { CreateStepsCallback } from 'client/utils/schema'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'Marketplace/CreateForm' }, configProps)
export { CreateForm }

View File

@ -35,7 +35,17 @@ const MarketplaceAppsTable = (props) => {
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetMarketplaceAppsQuery()
const {
data: marketplaceApps = [],
isFetching,
refetch,
} = useGetMarketplaceAppsQuery()
// Filter data if there is filter function
const data =
props?.filterData && typeof props?.filterData === 'function'
? props?.filterData(marketplaceApps)
: marketplaceApps
const columns = useMemo(
() =>

View File

@ -0,0 +1,210 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Typography } from '@mui/material'
import { AddCircledOutline, Trash, MoreVert, Group } from 'iconoir-react'
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { useViews } from 'client/features/Auth'
import {
useRemoveMarketplaceMutation,
useEnableMarketplaceMutation,
useDisableMarketplaceMutation,
useChangeMarketplaceOwnershipMutation,
} from 'client/features/OneApi/marketplace'
import {
createActions,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { Translate } from 'client/components/HOC'
import { RESOURCE_NAMES, T, MARKETPLACE_ACTIONS } from 'client/constants'
import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm'
const ListMarketplaceNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`group-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListMarketplaceNames rows={rows} />
const MessageToConfirmAction = (rows, description) => (
<>
<ListMarketplaceNames rows={rows} />
{description && <Translate word={description} />}
<Translate word={T.DoYouWantProceed} />
</>
)
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
/**
* Generates the actions to operate resources on Groups table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
const [remove] = useRemoveMarketplaceMutation()
const [enable] = useEnableMarketplaceMutation()
const [disable] = useDisableMarketplaceMutation()
const [changeOwnership] = useChangeMarketplaceOwnershipMutation()
return useMemo(
() =>
createActions({
filters: getResourceView(RESOURCE_NAMES.MARKETPLACE)?.actions,
actions: [
{
accessor: MARKETPLACE_ACTIONS.CREATE_DIALOG,
tooltip: T.Create,
icon: AddCircledOutline,
action: () => history.push(PATH.STORAGE.MARKETPLACES.CREATE),
},
{
accessor: MARKETPLACE_ACTIONS.UPDATE_DIALOG,
label: T.Update,
tooltip: T.Update,
selected: { max: 1 },
color: 'secondary',
action: (rows) => {
const group = rows?.[0]?.original ?? {}
const path = PATH.STORAGE.MARKETPLACES.CREATE
history.push(path, group)
},
},
{
tooltip: T.Enable,
icon: MoreVert,
selected: true,
color: 'secondary',
dataCy: 'marketplace-enable',
options: [
{
accessor: MARKETPLACE_ACTIONS.ENABLE,
name: T.Enable,
isConfirmDialog: true,
dialogProps: {
title: T.Enable,
children: MessageToConfirmAction,
dataCy: `modal-${MARKETPLACE_ACTIONS.ENABLE}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => enable({ id })))
},
},
{
accessor: MARKETPLACE_ACTIONS.DISABLE,
name: T.Disable,
isConfirmDialog: true,
dialogProps: {
title: T.Disable,
children: MessageToConfirmAction,
dataCy: `modal-${MARKETPLACE_ACTIONS.DISABLE}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => disable({ id })))
},
},
],
},
{
tooltip: T.Ownership,
icon: Group,
selected: true,
color: 'secondary',
dataCy: 'marketplace-ownership',
options: [
{
accessor: MARKETPLACE_ACTIONS.CHANGE_OWNER,
name: T.ChangeOwner,
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader,
dataCy: `modal-${MARKETPLACE_ACTIONS.CHANGE_OWNER}`,
},
form: ChangeUserForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
{
accessor: MARKETPLACE_ACTIONS.CHANGE_GROUP,
name: T.ChangeGroup,
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader,
dataCy: `modal-${MARKETPLACE_ACTIONS.CHANGE_GROUP}`,
},
form: ChangeGroupForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
],
},
{
accessor: MARKETPLACE_ACTIONS.DELETE,
tooltip: T.Delete,
icon: Trash,
color: 'error',
selected: { min: 1 },
dataCy: `marketplace_${MARKETPLACE_ACTIONS.DELETE}`,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
dataCy: `modal-${MARKETPLACE_ACTIONS.DELETE}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => remove({ id })))
},
},
],
},
],
}),
[view]
)
}
export default Actions

View File

@ -0,0 +1,77 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { MarketplaceAppsTable } from 'client/components/Tables'
import { useGetMarketplaceQuery } from 'client/features/OneApi/marketplace'
import { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
const _ = require('lodash')
/**
* Renders marketplace apps tab showing the apps of the cluster.
*
* @param {object} props - Props
* @param {string} props.id - Marketplace id
* @returns {ReactElement} Marketplace apps tab
*/
const MarketplaceApps = ({ id }) => {
// Get info about the marketplaceApps
const { data: marketplace } = useGetMarketplaceQuery({ id })
// Define function to get details of a marketplace app
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(
generatePath(PATH.STORAGE.MARKETPLACE_APPS.DETAIL, { id: String(rowId) })
)
}
// Get apps of the marketplace
const apps = _.isEmpty(marketplace?.MARKETPLACEAPPS)
? []
: Array.isArray(marketplace?.MARKETPLACEAPPS?.ID)
? marketplace?.MARKETPLACEAPPS?.ID
: [marketplace?.MARKETPLACEAPPS?.ID]
return (
<div>
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
<MarketplaceAppsTable
disableRowSelect
filterData={(dataToFilter) =>
dataToFilter.filter((app) => _.includes(apps, app.ID))
}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</Stack>
</div>
)
}
MarketplaceApps.propTypes = {
id: PropTypes.string,
}
MarketplaceApps.displayName = 'MarketplaceApps'
export default MarketplaceApps

View File

@ -24,10 +24,12 @@ import { getAvailableInfoTabs } from 'client/models/Helper'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/Marketplace/Info'
import MarketplaceApps from 'client/components/Tabs/Marketplace/MarketplaceApps'
const getTabComponent = (tabName) =>
({
info: Info,
apps: MarketplaceApps,
}[tabName])
const MarketplaceTabs = memo(({ id }) => {

View File

@ -104,12 +104,13 @@ export const MARKETPLACE_APP_STATES = [
/** @enum {string} Datastore actions */
export const MARKETPLACE_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
UPDATE_DIALOG: 'update_dialog',
DELETE: 'delete',
// INFORMATION
RENAME: ACTIONS.RENAME,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
ENABLE: 'enable',
DISABLE: 'disable',
}
/**
@ -119,3 +120,26 @@ export const MARKETPLACE_ACTIONS = {
export const MARKET_THRESHOLD = {
CAPACITY: { high: 66, low: 33 },
}
export const MARKET_TYPES = {
OPENNEBULA: {
text: 'marketplace.types.one',
value: 'one',
},
HTTP: {
text: 'marketplace.types.http',
value: 'http',
},
S3: {
text: 'marketplace.types.s3',
value: 's3',
},
DOCKERHUB: {
text: 'marketplace.types.dockerhub',
value: 'dockerhub',
},
DOCKER_REGISTRY: {
text: 'marketplace.types.dockerRegistry',
value: 'docker_registry',
},
}

View File

@ -77,6 +77,7 @@ module.exports = {
CreateFile: 'Create File',
CreateHost: 'Create Host',
CreateImage: 'Create Image',
CreateMarketplace: 'Create Marketplace',
CreateMarketApp: 'Create Marketplace App',
CreateProvider: 'Create Provider',
CreateProvision: 'Create Provision',
@ -221,6 +222,7 @@ module.exports = {
UpdateVirtualNetworkTemplate: 'Update Virtual Network Template',
UpdateVmConfiguration: 'Update VM Configuration',
UpdateVmTemplate: 'Update VM Template',
UpdateMarketplace: 'Update Marketplace',
/* questions */
Yes: 'Yes',
@ -1482,6 +1484,107 @@ module.exports = {
ReservedCpu: 'Allocated CPU',
/* Marketplace App schema */
'marketplace.configuration.title': 'Configuration attributes',
'marketplace.form.create.general.name': 'Name',
'marketplace.form.create.general.description': 'Description',
'marketplace.form.create.general.type': 'Storage backend',
'marketplace.form.create.help.link':
'See Open Nebula documentation to get more details about marketplaces.',
'marketplace.general.help.title': 'Marketplace',
'marketplace.general.help.paragraph.1':
'OpenNebula Marketplaces provide a simple way to integrate your cloud with popular application/image providers. Think of them as external datastores. A Marketplace can be:',
'marketplace.general.help.paragraph.2.1':
'Public: accessible universally by all OpenNebula installations.',
'marketplace.general.help.paragraph.2.2':
'Private: local within an organization and specific for a single OpenNebula (a single zone) or shared by a federation (a collection of zones).',
'marketplace.general.help.paragraph.3':
'Please, select Name, Description and Storage backend of the Marketplace.',
'marketplace.form.configuration.one.url': 'Endpoint URL for marketplace',
'marketplace.form.configuration.one.help.paragraph.1':
'The OpenNebula Marketplace is a catalog of virtual appliances ready to run in OpenNebula environments available at ',
'marketplace.form.configuration.one.help.paragraph.1.link':
'http://marketplace.opennebula.io/appliance',
'marketplace.form.configuration.one.help.paragraph.2':
'Please, fill the configuration attributes for Markeplace OpenNebula Systems.',
'marketplace.form.configuration.one.help.link':
'See Open Nebula documentation to get more details about OpenNebula Systems marketplaces.',
'marketplace.form.configuration.http.url':
'Base URL of the Marketplace HTTP endpoint',
'marketplace.form.configuration.http.path': 'Marketapp directory path',
'marketplace.form.configuration.http.path.tooltip':
'Absolute directory path to place images (the HTTP server document root) in the Front-end or in the Hosts pointed at by the Storage bridge list',
'marketplace.form.configuration.http.bridge': 'Storage bridge list',
'marketplace.form.configuration.http.bridge.tooltip':
'Space separated list of servers to access the public directory. If not defined, the public directory will be local to the Front-end',
'marketplace.form.configuration.http.help.paragraph.1':
'This Marketplace uses a conventional HTTP server to expose the images (Marketplace Appliances) uploaded to the Marketplace. The image will be placed in a specific directory (available on or at least accessible from the Front-end), that must be also served by a dedicated HTTP service.',
'marketplace.form.configuration.http.help.paragraph.2':
'Please, fill the configuration attributes for HTTP Marketplace.',
'marketplace.form.configuration.http.help.link':
'See Open Nebula documentation to get more details about HTTP marketplaces.',
'marketplace.form.configuration.s3.accessKey': 'Access Key Id',
'marketplace.form.configuration.s3.accessKey.tooltip':
'The access key of the S3 user',
'marketplace.form.configuration.s3.secretAccessKey': 'Secret Access Key',
'marketplace.form.configuration.s3.secretAccessKey.tooltip':
'The secret key of the S3 user',
'marketplace.form.configuration.s3.bucket': 'S3 bucket to store marketapps',
'marketplace.form.configuration.s3.bucket.tooltip':
'The bucket where the files will be stored',
'marketplace.form.configuration.s3.region': 'Region',
'marketplace.form.configuration.s3.region.tooltip':
'The region to connect to. If you are using Ceph-S3 any value here will work',
'marketplace.form.configuration.s3.aws': 'Use Amazon AWS S3 Service',
'marketplace.form.configuration.s3.aws.toolkit':
'Check in case that Amazon AWS S3 Service will be used instead Ceph S3',
'marketplace.form.configuration.s3.endpoint': 'Endpoint URL for marketplace',
'marketplace.form.configuration.s3.endpoint.tooltip':
'This is only required if you are connecting to a service other than Amazon AWS S3. Preferably dont use an endpoint that includes the bucket as the leading part of the hosts URL',
'marketplace.form.configuration.s3.totalMB': 'Total Marketplace size in MB',
'marketplace.form.configuration.s3.totalMB.tooltip':
'This parameter defines the total size of the Marketplace in MB. It defaults to 1048576 (MB).',
'marketplace.form.configuration.s3.readLength': 'Read block length in MB',
'marketplace.form.configuration.s3.readLength.tooltip':
'Split the file into chunks of this size in MB, never user a value larger than 100. Defaults to 32 (MB).',
'marketplace.form.configuration.s3.help.paragraph.1':
'This Marketplace uses an S3 API-capable service as the Back-end. This means Marketplace Appliances will be stored in the official AWS S3 service , or in services that implement that API, like Ceph Object Gateway S3.',
'marketplace.form.configuration.s3.help.paragraph.2':
'Please, fill the configuration attributes for S3 Marketplace.',
'marketplace.form.configuration.s3.help.link':
'See Open Nebula documentation to get more details about S3 marketplaces.',
'marketplace.form.configuration.dockerhub.info':
'No configuration attributes are needed for Dockerhub.',
'marketplace.form.configuration.dockerhub.help.paragraph.1':
'The DockerHub Marketplace provide access to DockerHub Official Images. The OpenNebula context packages are installed during the import process so once an image is imported its fully prepared to be used.',
'marketplace.form.configuration.dockerhub.help.paragraph.2':
'Please, fill the configuration attributes for DockerHub Marketplace.',
'marketplace.form.configuration.dockerhub.help.link':
'See Open Nebula documentation to get more details about DockerHub marketplaces.',
'marketplace.form.configuration.dockerRegistry.url':
'Marketplace Docker registry url',
'marketplace.form.configuration.dockerRegistry.url.tooltip':
'Base URL of the Marketplace Docker registry endpoint',
'marketplace.form.configuration.dockerRegistry.ssl': 'SSL connection',
'marketplace.form.configuration.dockerRegistry.ssl.tooltip':
'Check if the registry is behind SSL proxy',
'marketplace.form.configuration.dockerRegistry.help.paragraph.1':
'This Marketplace uses a private Docker registry server to expose the images in it as Marketplace Appliances.',
'marketplace.form.configuration.dockerRegistry.help.paragraph.2':
'Please, fill the configuration attributes for Docker Registry Marketplace.',
'marketplace.form.configuration.dockerRegistry.help.link':
'See Open Nebula documentation to get more details about Docker Registry marketplaces.',
'marketplace.types.one': 'OpenNebula Systems',
'marketplace.types.http': 'HTTP',
'marketplace.types.s3': 'Amazon S3',
'marketplace.types.dockerhub': 'DockerHub',
'marketplace.types.dockerRegistry': 'Docker Registry',
/* Marketplace App - general */
MarketplaceApp: 'Marketplace app',
RegisteredAt: 'Registered %s',

View File

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement } from 'react'
import { useParams, Redirect } from 'react-router-dom'
import MarketplaceAppTabs from 'client/components/Tabs/MarketplaceApp'
/**
* Displays the detail information about a Marketplace app.
*
* @returns {ReactElement} Host detail component.
*/
function MarketplaceAppDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <MarketplaceAppTabs id={id} />
}
export default MarketplaceAppDetail

View File

@ -0,0 +1,125 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement } from 'react'
import { useHistory, useLocation } from 'react-router'
import { useGeneralApi } from 'client/features/General'
import {
useAllocateMarketplaceMutation,
useGetMarketplaceQuery,
useUpdateMarketplaceMutation,
useRenameMarketplaceMutation,
} from 'client/features/OneApi/marketplace'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/Marketplace'
import { PATH } from 'client/apps/sunstone/routesOne'
import { jsonToXml } from 'client/models/Helper'
import systemApi from 'client/features/OneApi/system'
/**
* Displays the creation form for a marketplace.
*
* @returns {ReactElement} - The marketplace form component
*/
function CreateMarketplace() {
const history = useHistory()
const { state: { ID: marketplaceId } = {} } = useLocation()
const { enqueueSuccess, enqueueError } = useGeneralApi()
const [createMarketplace] = useAllocateMarketplaceMutation()
const [updateMarketplace] = useUpdateMarketplaceMutation()
const [renameMarketplace] = useRenameMarketplaceMutation()
const { data: views } = systemApi.useGetSunstoneAvalaibleViewsQuery()
const { data: version } = systemApi.useGetOneVersionQuery()
const { data: marketplace } = marketplaceId
? useGetMarketplaceQuery({ id: marketplaceId })
: { data: undefined }
const onSubmit = async (template) => {
try {
// Request to create a marketplace but not to update
if (!marketplaceId) {
// Create marketplace
const newMarketplaceId = await createMarketplace({
template: jsonToXml(template),
}).unwrap()
// Only show marketplace message
enqueueSuccess(`Marketplace created - #${newMarketplaceId}`)
} else {
// Rename if the name has been changed
if (template?.changeName) {
await renameMarketplace({
id: marketplaceId,
name: template?.NAME,
}).unwrap()
delete template?.changeName
}
// Name in not on the template
delete template?.NAME
// Update marketplace
await updateMarketplace({
id: marketplaceId,
template: jsonToXml(template),
}).unwrap()
// Only show marketplace message
enqueueSuccess(`Marketplace updated - #${marketplaceId}`)
}
// Go to marketplaces list
history.push(PATH.STORAGE.MARKETPLACES.LIST)
} catch (error) {
enqueueError('Error creating marketplace')
}
}
return views &&
version &&
(!marketplaceId || (marketplaceId && marketplace)) ? (
<CreateForm
onSubmit={onSubmit}
initialValues={marketplace}
stepProps={{
views,
version,
}}
fallback={<SkeletonStepsForm />}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
) : (
<SkeletonStepsForm />
)
}
CreateMarketplace.propTypes = {
marketplace: PropTypes.object,
views: PropTypes.object,
system: PropTypes.object,
}
export default CreateMarketplace

View File

@ -33,6 +33,8 @@ import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { T, Marketplace } from 'client/constants'
import MarketplaceActions from 'client/components/Tables/Marketplaces/actions'
/**
* Displays a list of Marketplaces with a split pane between the list and selected row(s).
*
@ -44,6 +46,8 @@ function Marketplaces() {
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
const actions = MarketplaceActions()
return (
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
@ -51,6 +55,7 @@ function Marketplaces() {
<MarketplacesTable
onSelectedRowsChange={onSelectedRowsChange}
useUpdateMutation={useUpdateMarketplaceMutation}
globalActions={actions}
/>
{hasSelectedRows && (

View File

@ -88,7 +88,7 @@ const clusterApi = oneApi.injectEndpoints({
dispatch(
clusterApi.util.updateQueryData(
'getGClusters',
'getClusters',
undefined,
updateResourceOnPool({ id, resourceFromQuery })
)

View File

@ -27,7 +27,7 @@ import { xmlToJson } from 'client/models/Helper'
* @param {string} resourceId - The resource ID
* @returns {boolean} - True if the parameters are valid, false otherwise
*/
const isUpdateOnPool = (draft, resourceId) =>
export const isUpdateOnPool = (draft, resourceId) =>
Array.isArray(draft) && resourceId !== undefined
/**

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Draft, ThunkAction } from '@reduxjs/toolkit'
import { Actions, Commands } from 'server/utils/constants/commands/market'
import {
oneApi,
@ -21,9 +22,54 @@ import {
} from 'client/features/OneApi'
import { Permission, Marketplace } from 'client/constants'
import {
removeResourceOnPool,
updateNameOnResource,
updateResourceOnPool,
isUpdateOnPool,
} from 'client/features/OneApi/common'
import { xmlToJson } from 'client/models/Helper'
const { MARKETPLACE } = ONE_RESOURCES
const { MARKETPLACE_POOL } = ONE_RESOURCES_POOL
/**
* Update the resource markteplace in the store.
*
* @param {object} params - Request params
* @param {number|string} params.id - The id of the resource
* @param {string} params.template - The new user template contents on XML format
* @param {0|1} params.replace
* - Update type:
* ``0``: Replace the whole template.
* ``1``: Merge new template with the existing one.
* @param {string} [templateAttribute] - The attribute name of the resource template. By default is `TEMPLATE`.
* @returns {function(Draft):ThunkAction} - Dispatches the action
*/
export const updateMarketplaceOnResource =
(
{ id: resourceId, template: xml, replace = 0 },
templateAttribute = 'TEMPLATE'
) =>
(draft) => {
const updatePool = isUpdateOnPool(draft, resourceId)
const newTemplateJson = xmlToJson(xml)
const resource = updatePool
? draft.find(({ ID }) => +ID === +resourceId)
: draft
if (updatePool && !resource) return
resource[templateAttribute] =
+replace === 0
? newTemplateJson
: { ...resource[templateAttribute], ...newTemplateJson }
resource.MARKET_MAD = newTemplateJson?.MARKET_MAD
}
const marketplaceApi = oneApi.injectEndpoints({
endpoints: (builder) => ({
getMarketplaces: builder.query({
@ -70,6 +116,28 @@ const marketplaceApi = oneApi.injectEndpoints({
},
transformResponse: (data) => data?.MARKETPLACE ?? {},
providesTags: (_, __, { id }) => [{ type: MARKETPLACE, id }],
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: resourceFromQuery } = await queryFulfilled
dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplace',
undefined,
updateResourceOnPool({ id, resourceFromQuery })
)
)
} catch {
// if the query fails, we want to remove the resource from the pool
dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplaces',
undefined,
removeResourceOnPool({ id })
)
)
}
},
}),
allocateMarketplace: builder.mutation({
/**
@ -104,6 +172,30 @@ const marketplaceApi = oneApi.injectEndpoints({
return { params, command }
},
invalidatesTags: [MARKETPLACE_POOL],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchMarketplace = dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplace',
{ id: params.id },
updateNameOnResource(params)
)
)
const patchMarketplaces = dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplaces',
undefined,
updateNameOnResource(params)
)
)
queryFulfilled.catch(() => {
patchMarketplace.undo()
patchMarketplaces.undo()
})
} catch {}
},
}),
updateMarketplace: builder.mutation({
/**
@ -126,6 +218,30 @@ const marketplaceApi = oneApi.injectEndpoints({
return { params, command }
},
providesTags: (_, __, { id }) => [{ type: MARKETPLACE, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchMarketplace = dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplace',
{ id: params.id },
updateMarketplaceOnResource(params)
)
)
const patchMarketplaces = dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplaces',
undefined,
updateMarketplaceOnResource(params)
)
)
queryFulfilled.catch(() => {
patchMarketplace.undo()
patchMarketplaces.undo()
})
} catch {}
},
}),
changeMarketplacePermissions: builder.mutation({
/**
@ -193,10 +309,30 @@ const marketplaceApi = oneApi.injectEndpoints({
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: MARKETPLACE, id },
MARKETPLACE_POOL,
],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchMarketplace = dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplace',
{ id: params.id },
updateNameOnResource(params)
)
)
const patchMarketplaces = dispatch(
marketplaceApi.util.updateQueryData(
'getMarketplaces',
undefined,
updateNameOnResource(params)
)
)
queryFulfilled.catch(() => {
patchMarketplace.undo()
patchMarketplaces.undo()
})
} catch {}
},
}),
enableMarketplace: builder.mutation({
/**

View File

@ -136,11 +136,11 @@ module.exports = {
from: resource,
default: 0,
},
userId: {
user: {
from: postBody,
default: -1,
},
groupId: {
group: {
from: postBody,
default: -1,
},
@ -166,7 +166,7 @@ module.exports = {
params: {
id: {
from: resource,
default: 0,
default: -1,
},
enable: {
from: postBody,