diff --git a/install.sh b/install.sh index 76dc89ab52..e942510d8e 100755 --- a/install.sh +++ b/install.sh @@ -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 \ diff --git a/src/fireedge/etc/sunstone/admin/marketplace-tab.yaml b/src/fireedge/etc/sunstone/admin/marketplace-tab.yaml index 23f0ba1986..509a8dc654 100644 --- a/src/fireedge/etc/sunstone/admin/marketplace-tab.yaml +++ b/src/fireedge/etc/sunstone/admin/marketplace-tab.yaml @@ -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 diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index aa3eeecfb7..cd1810965f 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -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, diff --git a/src/fireedge/src/client/components/Cards/MarketplaceCard.js b/src/fireedge/src/client/components/Cards/MarketplaceCard.js index 2032c92c1d..3f7a902aec 100644 --- a/src/fireedge/src/client/components/Cards/MarketplaceCard.js +++ b/src/fireedge/src/client/components/Cards/MarketplaceCard.js @@ -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( {` ${apps}`} + + + {` ${ZONE_ID}`} +
diff --git a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js index de0ae21fb0..9000fb47a0 100644 --- a/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js +++ b/src/fireedge/src/client/components/Forms/Cluster/CreateForm/Steps/index.js @@ -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, diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsCommon.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsCommon.js new file mode 100644 index 0000000000..158b9b821e --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsCommon.js @@ -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 } diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsDockerRegistry.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsDockerRegistry.js new file mode 100644 index 0000000000..bc5c1262e5 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsDockerRegistry.js @@ -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 } diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsHttp.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsHttp.js new file mode 100644 index 0000000000..aea1331c6a --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsHttp.js @@ -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 } diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsS3.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsS3.js new file mode 100644 index 0000000000..584993ab35 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/fieldsS3.js @@ -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 } diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/index.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/index.js new file mode 100644 index 0000000000..6f7c7ae702 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/index.js @@ -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 ( + + + {isDockerhub && ( + + {Tr(T['marketplace.form.configuration.dockerhub.info'])} + + )} + + + + + + + {Tr(T['marketplace.general.help.title'])} + + + {type?.value === 'one' && ( + <> + + {Tr(T['marketplace.types.one'])} + + + + {Tr(T['marketplace.form.configuration.one.help.paragraph.1'])} + + {Tr( + T[ + 'marketplace.form.configuration.one.help.paragraph.1.link' + ] + )} + + + + + {Tr(T['marketplace.form.configuration.one.help.paragraph.2'])}{' '} + + + + + {Tr(T['marketplace.form.configuration.one.help.link'])} + + + + )} + + {type?.value === 'http' && ( + <> + + {Tr(T['marketplace.types.http'])} + + + + {Tr( + T['marketplace.form.configuration.http.help.paragraph.1'] + )} + + + + {Tr( + T['marketplace.form.configuration.http.help.paragraph.2'] + )}{' '} + + + + + {Tr(T['marketplace.form.configuration.http.help.link'])} + + + + )} + + {type?.value === 's3' && ( + <> + + {Tr(T['marketplace.types.s3'])} + + + + {Tr(T['marketplace.form.configuration.s3.help.paragraph.1'])} + + + + {Tr(T['marketplace.form.configuration.s3.help.paragraph.2'])}{' '} + + + + + {Tr(T['marketplace.form.configuration.s3.help.link'])} + + + + )} + + {type?.value === 'dockerhub' && ( + <> + + {Tr(T['marketplace.types.dockerhub'])} + + + + {Tr( + T[ + 'marketplace.form.configuration.dockerhub.help.paragraph.1' + ] + )} + + + + {Tr( + T[ + 'marketplace.form.configuration.dockerhub.help.paragraph.2' + ] + )}{' '} + + + + + {Tr( + T['marketplace.form.configuration.dockerhub.help.link'] + )} + + + + )} + + {type?.value === 'docker_registry' && ( + <> + + {Tr(T['marketplace.types.dockerRegistry'])} + + + + {Tr( + T[ + 'marketplace.form.configuration.dockerRegistry.help.paragraph.1' + ] + )} + + + + {Tr( + T[ + 'marketplace.form.configuration.dockerRegistry.help.paragraph.2' + ] + )}{' '} + + + + + {Tr( + T[ + 'marketplace.form.configuration.dockerRegistry.help.link' + ] + )} + + + + )} + + + + + ) +} + +/** + * 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 diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/schema.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/schema.js new file mode 100644 index 0000000000..110b74be6c --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/Configuration/schema.js @@ -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 } diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/General/index.js new file mode 100644 index 0000000000..68f131531a --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/General/index.js @@ -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) => ( + + + + + + + + + {' '} + {Tr(T['marketplace.general.help.title'])}{' '} + + + + {Tr(T['marketplace.general.help.paragraph.1'])}{' '} + + +
    +
  • + + {Tr(T['marketplace.general.help.paragraph.2.1'])}{' '} + +
  • +
  • + + {Tr(T['marketplace.general.help.paragraph.2.2'])}{' '} + +
  • +
+ + + {Tr(T['marketplace.general.help.paragraph.3'])}{' '} + + + + {' '} + + {Tr(T['marketplace.form.create.help.link'])} + + +
+
+
+
+) + +/** + * 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 diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/General/schema.js new file mode 100644 index 0000000000..e7a801c06f --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/General/schema.js @@ -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 } diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/index.js new file mode 100644 index 0000000000..175581c4ea --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/Steps/index.js @@ -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 diff --git a/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/index.js b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/index.js new file mode 100644 index 0000000000..a8e212e6aa --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/CreateForm/index.js @@ -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' diff --git a/src/fireedge/src/client/components/Forms/Marketplace/index.js b/src/fireedge/src/client/components/Forms/Marketplace/index.js new file mode 100644 index 0000000000..b806dbfebb --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Marketplace/index.js @@ -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 } diff --git a/src/fireedge/src/client/components/Tables/MarketplaceApps/index.js b/src/fireedge/src/client/components/Tables/MarketplaceApps/index.js index 04f1236c5b..049197649e 100644 --- a/src/fireedge/src/client/components/Tables/MarketplaceApps/index.js +++ b/src/fireedge/src/client/components/Tables/MarketplaceApps/index.js @@ -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( () => diff --git a/src/fireedge/src/client/components/Tables/Marketplaces/actions.js b/src/fireedge/src/client/components/Tables/Marketplaces/actions.js new file mode 100644 index 0000000000..eb706c3bea --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Marketplaces/actions.js @@ -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 ( + + {`#${ID} ${NAME}`} + + ) + }) + +const SubHeader = (rows) => + +const MessageToConfirmAction = (rows, description) => ( + <> + + {description && } + + +) + +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 diff --git a/src/fireedge/src/client/components/Tabs/Marketplace/MarketplaceApps/index.js b/src/fireedge/src/client/components/Tabs/Marketplace/MarketplaceApps/index.js new file mode 100644 index 0000000000..966a4dcc13 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Marketplace/MarketplaceApps/index.js @@ -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 ( +
+ + + dataToFilter.filter((app) => _.includes(apps, app.ID)) + } + onRowClick={(row) => handleRowClick(row.ID)} + /> + +
+ ) +} + +MarketplaceApps.propTypes = { + id: PropTypes.string, +} + +MarketplaceApps.displayName = 'MarketplaceApps' + +export default MarketplaceApps diff --git a/src/fireedge/src/client/components/Tabs/Marketplace/index.js b/src/fireedge/src/client/components/Tabs/Marketplace/index.js index ad1be84d43..0e48ffedba 100644 --- a/src/fireedge/src/client/components/Tabs/Marketplace/index.js +++ b/src/fireedge/src/client/components/Tabs/Marketplace/index.js @@ -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 }) => { diff --git a/src/fireedge/src/client/constants/marketplace.js b/src/fireedge/src/client/constants/marketplace.js index c7045cdd4c..cabb991dec 100644 --- a/src/fireedge/src/client/constants/marketplace.js +++ b/src/fireedge/src/client/constants/marketplace.js @@ -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', + }, +} diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index c39cc670ea..86aa651060 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -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 don’t use an endpoint that includes the bucket as the leading part of the host’s 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 it’s 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', diff --git a/src/fireedge/src/client/containers/MarketplaceApps/Detail.js b/src/fireedge/src/client/containers/MarketplaceApps/Detail.js new file mode 100644 index 0000000000..4dd47d04ed --- /dev/null +++ b/src/fireedge/src/client/containers/MarketplaceApps/Detail.js @@ -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 + } + + return +} + +export default MarketplaceAppDetail diff --git a/src/fireedge/src/client/containers/Marketplaces/Create.js b/src/fireedge/src/client/containers/Marketplaces/Create.js new file mode 100644 index 0000000000..f527a23b66 --- /dev/null +++ b/src/fireedge/src/client/containers/Marketplaces/Create.js @@ -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)) ? ( + } + > + {(config) => } + + ) : ( + + ) +} + +CreateMarketplace.propTypes = { + marketplace: PropTypes.object, + views: PropTypes.object, + system: PropTypes.object, +} + +export default CreateMarketplace diff --git a/src/fireedge/src/client/containers/Marketplaces/index.js b/src/fireedge/src/client/containers/Marketplaces/index.js index b7c5f599ed..aa050578f7 100644 --- a/src/fireedge/src/client/containers/Marketplaces/index.js +++ b/src/fireedge/src/client/containers/Marketplaces/index.js @@ -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 ( {({ getGridProps, GutterComponent }) => ( @@ -51,6 +55,7 @@ function Marketplaces() { {hasSelectedRows && ( diff --git a/src/fireedge/src/client/features/OneApi/cluster.js b/src/fireedge/src/client/features/OneApi/cluster.js index 3d045d0d4a..682ff5b866 100644 --- a/src/fireedge/src/client/features/OneApi/cluster.js +++ b/src/fireedge/src/client/features/OneApi/cluster.js @@ -88,7 +88,7 @@ const clusterApi = oneApi.injectEndpoints({ dispatch( clusterApi.util.updateQueryData( - 'getGClusters', + 'getClusters', undefined, updateResourceOnPool({ id, resourceFromQuery }) ) diff --git a/src/fireedge/src/client/features/OneApi/common.js b/src/fireedge/src/client/features/OneApi/common.js index b16ee72170..6ff0a02227 100644 --- a/src/fireedge/src/client/features/OneApi/common.js +++ b/src/fireedge/src/client/features/OneApi/common.js @@ -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 /** diff --git a/src/fireedge/src/client/features/OneApi/marketplace.js b/src/fireedge/src/client/features/OneApi/marketplace.js index 8fea1fa783..bb6414cd24 100644 --- a/src/fireedge/src/client/features/OneApi/marketplace.js +++ b/src/fireedge/src/client/features/OneApi/marketplace.js @@ -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({ /** diff --git a/src/fireedge/src/server/utils/constants/commands/market.js b/src/fireedge/src/server/utils/constants/commands/market.js index d3db4fbb6b..43aed0611b 100644 --- a/src/fireedge/src/server/utils/constants/commands/market.js +++ b/src/fireedge/src/server/utils/constants/commands/market.js @@ -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,