1
0
mirror of https://github.com/OpenNebula/one.git synced 2024-12-25 23:21:29 +03:00

F #5422: Add form to export marketplace app (#1601)

This commit is contained in:
Sergio Betanzos 2021-11-24 14:05:42 +01:00 committed by GitHub
parent 64dc6deac6
commit 7bfcf01c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 397 additions and 47 deletions

View File

@ -39,7 +39,7 @@ export const getSeverityFromData = data =>
export const getMessageInfo = (data = '') => {
try {
const { message, timestamp, severity } = JSON.parse(data)
const decryptMessage = atob(message)
const decryptMessage = decodeURIComponent(escape(atob(message)))
return { timestamp, severity, message: decryptMessage }
} catch {

View File

@ -0,0 +1,54 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { SCHEMA, FIELDS } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/BasicConfiguration/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'configuration'
const Content = () => (
<FormWithSchema
cy='export-app-configuration'
id={STEP_ID}
fields={FIELDS}
/>
)
/**
* Step to configure the marketplace app.
*
* @returns {Step} Configuration step
*/
const ConfigurationStep = () => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
nics: PropTypes.array
}
export default ConfigurationStep

View File

@ -0,0 +1,66 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { boolean, string, lazy, object, ObjectSchema } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
/** @type {Field} Name field */
const NAME_FIELD = {
name: 'name',
label: T.Name,
tooltip: T.ExportAppNameConcept,
type: INPUT_TYPES.TEXT,
validation: lazy((_, { context }) => {
return string()
.trim()
.required()
.default(() => context.app.NAME)
})
}
/** @type {Field} Template name field */
const TEMPLATE_NAME_FIELD = {
name: 'vmname',
label: T.VMTemplate,
tooltip: T.ExportTemplateNameConcept,
type: INPUT_TYPES.TEXT,
validation: lazy((_, { context }) => {
return string()
.trim()
.required()
.default(() => context.app.NAME)
})
}
/** @type {Field} Associate field */
const ASSOCIATED_FIELD = {
name: 'associated',
label: T.DontAssociateApp,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { md: 12 }
}
/** @type {Field[]} List of fields */
export const FIELDS = [
NAME_FIELD,
TEMPLATE_NAME_FIELD,
ASSOCIATED_FIELD
]
/** @type {ObjectSchema} Advanced options schema */
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,76 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { DatastoresTable } from 'client/components/Tables'
import { SCHEMA } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/DatastoresTable/schema'
import { Step, decodeBase64 } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'datastore'
const Content = ({ data, app }) => {
const { NAME } = data?.[0] ?? {}
const { setValue } = useFormContext()
const isKernelType = useMemo(() => {
const appTemplate = String(decodeBase64(app?.TEMPLATE?.APPTEMPLATE64, ''))
return appTemplate.includes('TYPE="KERNEL"')
}, [])
const handleSelectedRows = rows => {
const { original = {} } = rows?.[0] ?? {}
setValue(STEP_ID, original.ID !== undefined ? [original] : [])
}
return (
<DatastoresTable
singleSelect
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={row => String(row.NAME)}
initialState={{
selectedRowIds: { [NAME]: true },
filters: [{ id: 'TYPE', value: isKernelType ? 'FILE' : 'IMAGE' }]
}}
onSelectedRowsChange={handleSelectedRows}
/>
)
}
/**
* Step to select the Datastore.
*
* @param {object} app - Marketplace App resource
* @returns {Step} Datastore step
*/
const DatastoreStep = app => ({
id: STEP_ID,
label: T.SelectDatastore,
resolver: SCHEMA,
content: props => Content({ ...props, app })
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
app: PropTypes.object
}
export default DatastoreStep

View File

@ -0,0 +1,24 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object, ArraySchema } from 'yup'
/** @type {ArraySchema} Datastore table schema */
export const SCHEMA = array(object())
.min(1)
.max(1)
.required()
.ensure()
.default(() => [])

View File

@ -0,0 +1,38 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import BasicConfiguration, { STEP_ID as BASIC_ID } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/BasicConfiguration'
import DatastoresTable, { STEP_ID as DATASTORE_ID } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps/DatastoresTable'
import { createSteps } from 'client/utils'
const Steps = createSteps(
[BasicConfiguration, DatastoresTable],
{
transformInitialValue: (app, schema) => schema.cast({}, { context: { app } }),
transformBeforeSubmit: formData => {
const {
[BASIC_ID]: configuration,
[DATASTORE_ID]: [datastore] = []
} = formData
return {
datastore: datastore?.ID,
...configuration
}
}
}
)
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
export { default } from 'client/components/Forms/MarketplaceApp/ExportForm/Steps'

View File

@ -0,0 +1,20 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import ExportForm from 'client/components/Forms/MarketplaceApp/ExportForm'
export {
ExportForm
}

View File

@ -16,7 +16,7 @@
import { string, boolean, ref, ObjectSchema } from 'yup'
import { T, INPUT_TYPES } from 'client/constants'
import { Field, getObjectSchemaFromFields } from 'client/utils'
import { Field, getObjectSchemaFromFields, decodeBase64 } from 'client/utils'
const switchField = {
type: INPUT_TYPES.SWITCH,
@ -86,7 +86,7 @@ export const START_SCRIPT = {
.when(
'$extra.CONTEXT.START_SCRIPT_BASE64',
(scriptEncoded, schema) => scriptEncoded
? schema.default(() => decodeURIComponent(escape(atob(scriptEncoded))))
? schema.default(() => decodeBase64(scriptEncoded))
: schema
),
grid: { md: 12 },

View File

@ -37,7 +37,13 @@ export default [
{
Header: 'Type',
id: 'TYPE',
accessor: row => DatastoreModel.getType(row)?.name
accessor: row => DatastoreModel.getType(row),
Filter: ({ column }) => CategoryFilter({
column,
multiple: true,
title: 'Type'
}),
filter: 'includesValue'
},
{
Header: 'Clusters IDs',

View File

@ -44,7 +44,7 @@ const Row = ({ original, value, ...props }) => {
</Typography>
<span className={classes.labels}>
{LOCK && <Lock />}
<StatusChip text={TYPE?.name} />
<StatusChip text={TYPE} />
</span>
</div>
<div className={classes.caption}>

View File

@ -16,12 +16,20 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
// import { useHistory } from 'react-router-dom'
import { RefreshDouble } from 'iconoir-react'
import {
RefreshDouble,
CloudDownload
} from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import { useGeneralApi } from 'client/features/General'
import { useMarketplaceAppApi } from 'client/features/One'
import { Translate } from 'client/components/HOC'
import {
ExportForm
} from 'client/components/Forms/MarketplaceApp'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
// import { PATH } from 'client/apps/sunstone/routesOne'
import { T, MARKETPLACE_APP_ACTIONS } from 'client/constants'
@ -46,7 +54,8 @@ MessageToConfirmAction.displayName = 'MessageToConfirmAction'
const Actions = () => {
const { view, getResourceView } = useAuth()
const { getMarketplaceApps } = useMarketplaceAppApi()
const { enqueueSuccess } = useGeneralApi()
const { getMarketplaceApps, exportApp } = useMarketplaceAppApi()
const marketplaceAppActions = useMemo(() => createActions({
filters: getResourceView('MARKETPLACE-APP')?.actions,
@ -58,17 +67,25 @@ const Actions = () => {
action: async () => {
await getMarketplaceApps()
}
},
{
accessor: MARKETPLACE_APP_ACTIONS.EXPORT,
tooltip: T.ImportIntoDatastore,
selected: { max: 1 },
icon: CloudDownload,
options: [{
dialogProps: { title: T.DownloadAppToOpenNebula },
form: rows => {
const app = rows?.map(({ original }) => original)[0]
return ExportForm(app, app)
},
onSubmit: async (formData, rows) => {
const appId = rows?.[0]?.original?.ID
const response = await exportApp(appId, formData)
enqueueSuccess(response)
}
/* {
accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
tooltip: T.CreateMarketApp,
icon: AddSquare,
action: () => {
const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
history.push(path)
}]
}
} */
]
}), [view])

View File

@ -19,21 +19,16 @@ import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material'
import { NavArrowDown as ExpandMoreIcon } from 'iconoir-react'
import { TabContext } from 'client/components/Tabs/TabProvider'
import { decodeBase64 } from 'client/utils'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
const parseTemplateInB64 = template => {
try {
return decodeURIComponent(escape(atob(template)))
} catch (e) { return {} }
}
const AppTemplateTab = () => {
const { data: marketplaceApp = {} } = useContext(TabContext)
const { TEMPLATE: { APPTEMPLATE64, VMTEMPLATE64 } } = marketplaceApp
const appTemplate = useMemo(() => parseTemplateInB64(APPTEMPLATE64), [APPTEMPLATE64])
const vmTemplate = useMemo(() => parseTemplateInB64(VMTEMPLATE64), [VMTEMPLATE64])
const appTemplate = useMemo(() => decodeBase64(APPTEMPLATE64), [APPTEMPLATE64])
const vmTemplate = useMemo(() => decodeBase64(VMTEMPLATE64), [VMTEMPLATE64])
return (
<>

View File

@ -16,21 +16,8 @@
import * as STATES from 'client/constants/states'
import COLOR from 'client/constants/color'
/** @type {{name: string, shortName: string}} Datastore type information */
export const DATASTORE_TYPES = [
{
name: 'IMAGE',
shortName: 'img'
},
{
name: 'SYSTEM',
shortName: 'sys'
},
{
name: 'FILE',
shortName: 'fil'
}
]
/** @type {string[]} Datastore type information */
export const DATASTORE_TYPES = ['IMAGE', 'SYSTEM', 'FILE']
/** @type {STATES.StateInfo[]} Datastore states */
export const DATASTORE_STATES = [

View File

@ -16,5 +16,6 @@
export const MARKETPLACE_APP_ACTIONS = {
REFRESH: 'refresh',
CREATE_DIALOG: 'create_dialog',
RENAME: 'rename'
RENAME: 'rename',
EXPORT: 'export'
}

View File

@ -105,6 +105,7 @@ module.exports = {
SelectNetwork: 'Select a network',
SelectRequest: 'Select request',
SelectVmTemplate: 'Select a VM Template',
SelectDatastore: 'Select a Datastore to store the resource',
Share: 'Share',
Show: 'Show',
ShowAll: 'Show all',
@ -587,6 +588,11 @@ module.exports = {
/* Marketplace App - general */
Version: 'Version',
AppTemplate: 'App Template',
ImportIntoDatastore: 'Import into Datastore',
DownloadAppToOpenNebula: 'Download App to OpenNebula',
ExportAppNameConcept: 'Name that the resource will get for description purposes',
ExportTemplateNameConcept: 'The following template will be created in OpenNebula and the previous images will be referenced in the disks',
DontAssociateApp: 'Do not import/export associated VM templates/images',
/* User inputs */
UserInputs: 'User Inputs',

View File

@ -30,3 +30,5 @@ export const getMarketplaceApps = createAction(
marketplaceAppService.getMarketplaceApps,
response => ({ [RESOURCES.app]: response })
)
export const exportApp = createAction(`${APP}/export`, marketplaceAppService.export)

View File

@ -35,6 +35,7 @@ export const useMarketplaceAppApi = () => {
return {
getMarketplaceApp: id => unwrapDispatch(actions.getMarketplaceApp({ id })),
getMarketplaceApps: () => unwrapDispatch(actions.getMarketplaceApps())
getMarketplaceApps: () => unwrapDispatch(actions.getMarketplaceApps()),
exportApp: (id, data) => unwrapDispatch(actions.exportApp({ id, ...data }))
}
}

View File

@ -59,5 +59,32 @@ export const marketplaceAppService = ({
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return [res?.data?.MARKETPLACEAPP_POOL?.MARKETPLACEAPP ?? []].flat()
},
/**
* Exports the marketplace app to the OpenNebula cloud.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - App id
* @param {string} params.name - Image name
* @param {string|number} params.datastore - Datastore id or name
* @param {string|number} params.file - File datastore id or name
* @param {string} params.tag - DockerHub image tag (default latest)
* @param {string|number} params.template - Associate with VM template
* @param {boolean} params.associated - If `true`, Do not import/export associated VM templates/images
* @param {string} params.vmname - The name for the new VM Template, if the App contains one
* @returns {number} Template id
* @throws Fails when response isn't code 200
*/
export: async ({ id, ...data }) => {
const res = await RestClient.request({
url: `/api/marketapp/export/${id}`,
method: 'POST',
data
})
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data
}
})

View File

@ -56,10 +56,23 @@ export function sanitize (text, ...values) {
return DOMPurify.sanitize(dirty)
}
/**
* Decodes an string in base 64.
*
* @param {string} string - Value to decode
* @param {any} defaultValue - Default value if it fails
* @returns {any} Decoded value from string in base 64
*/
export const decodeBase64 = (string, defaultValue = {}) => {
try {
return decodeURIComponent(escape(atob(string)))
} catch (e) { return defaultValue }
}
/**
* Converts a long string of units into a readable format e.g KB, MB, GB, TB, YB.
*
* @param {number} value - The quantity of units.
* @param {number|string} value - The quantity of units.
* @param {string} unit - The unit of value.
* @param {number} fractionDigits
* - Number of digits after the decimal point. Must be in the range 0 - 20, inclusive
@ -67,17 +80,18 @@ export function sanitize (text, ...values) {
*/
export const prettyBytes = (value, unit = 'KB', fractionDigits = 0) => {
const UNITS = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
let ensuredValue = +value
if (Math.abs(value) === 0) return `${value} ${UNITS[0]}`
if (Math.abs(ensuredValue) === 0) return `${value} ${UNITS[0]}`
let idxUnit = UNITS.indexOf(unit)
while (value > 1024) {
value /= 1024
while (ensuredValue > 1024) {
ensuredValue /= 1024
idxUnit += 1
}
return `${value.toFixed(fractionDigits)} ${UNITS[idxUnit]}`
return `${ensuredValue.toFixed(fractionDigits)} ${UNITS[idxUnit]}`
}
/**