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

F #5888: Add Storage->Image tab (#2241)

Co-authored-by: Sergio Betanzos <sbetanzos@opennebula.io>
(cherry picked from commit f5f8266da88ea68f3132c4b2e19bade343be5ffc)
This commit is contained in:
Jorge Miguel Lobo Escalona 2022-07-20 13:04:31 +02:00 committed by Tino Vázquez
parent aa54a8ca55
commit a29d070d36
No known key found for this signature in database
GPG Key ID: 14201E424D02047E
54 changed files with 2702 additions and 88 deletions

View File

@ -29,6 +29,7 @@ import { useGeneralApi } from 'client/features/General'
import systemApi from 'client/features/OneApi/system'
import Sidebar from 'client/components/Sidebar'
import Notifier from 'client/components/Notifier'
import NotifierUpload from 'client/components/Notifier/upload'
import { AuthLayout } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
import { _APPS } from 'client/constants'
@ -73,6 +74,7 @@ const SunstoneApp = () => {
<>
<Sidebar endpoints={endpoints} />
<Notifier />
<NotifierUpload />
</>
)}
<Router redirectWhenAuth={PATH.DASHBOARD} endpoints={endpoints} />

View File

@ -97,6 +97,15 @@ const Datastores = loadable(() => import('client/containers/Datastores'), {
const Images = loadable(() => import('client/containers/Images'), {
ssr: false,
})
const CreateImages = loadable(() => import('client/containers/Images/Create'), {
ssr: false,
})
const CreateDockerfile = loadable(
() => import('client/containers/Images/Dockerfile'),
{
ssr: false,
}
)
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), {
ssr: false,
})
@ -189,6 +198,8 @@ export const PATH = {
IMAGES: {
LIST: `/${RESOURCE_NAMES.IMAGE}`,
DETAIL: `/${RESOURCE_NAMES.IMAGE}/:id`,
CREATE: `/${RESOURCE_NAMES.IMAGE}/create`,
DOCKERFILE: `/${RESOURCE_NAMES.IMAGE}/dockerfile`,
},
MARKETPLACES: {
LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
@ -364,6 +375,16 @@ const ENDPOINTS = [
icon: ImageIcon,
Component: Images,
},
{
title: T.CreateImage,
path: PATH.STORAGE.IMAGES.CREATE,
Component: CreateImages,
},
{
title: T.CreateDockerfile,
path: PATH.STORAGE.IMAGES.DOCKERFILE,
Component: CreateDockerfile,
},
{
title: T.Marketplaces,
path: PATH.STORAGE.MARKETPLACES.LIST,

View File

@ -0,0 +1,98 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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, memo } from 'react'
import PropTypes from 'prop-types'
import { Typography, Paper, Grid, Box } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
const useStyles = makeStyles(
({ palette, typography, breakpoints, shadows }) => ({
root: {
padding: '0.8em',
color: palette.text.primary,
backgroundColor: palette.background.paper,
fontWeight: typography.fontWeightRegular,
fontSize: '1em',
borderRadius: 6,
display: 'flex',
gap: 8,
cursor: 'pointer',
[breakpoints.down('md')]: {
flexWrap: 'wrap',
},
},
figure: {
flexBasis: '10%',
aspectRatio: '16/9',
display: 'flex',
justifyContent: 'center',
},
main: {
flex: 'auto',
overflow: 'hidden',
alignSelf: 'center',
},
title: {
color: palette.text.primary,
display: 'flex',
gap: 6,
alignItems: 'center',
},
})
)
const ImageCreateCard = memo(
/**
* @param {object} props - Props
* @param {string} props.name - Card name
* @param {Function} props.onClick - Card name
* @param {ReactElement} props.Icon - Card Icon
* @returns {ReactElement} - Card
*/
({ name = '', onClick, Icon }) => {
const classes = useStyles()
return (
<Grid item xs={12} md={6} onClick={onClick}>
<Paper variant="outlined" className={classes.root}>
{Icon && (
<Box className={classes.figure}>
<Icon />
</Box>
)}
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{name}
</Typography>
</div>
</div>
</Paper>
</Grid>
)
}
)
ImageCreateCard.propTypes = {
name: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
Icon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
}
ImageCreateCard.displayName = 'ImageCreateCard'
export default ImageCreateCard

View File

@ -0,0 +1,99 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { ModernTv } from 'iconoir-react'
import { Typography, Paper } from '@mui/material'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { Tr, Translate } from 'client/components/HOC'
import { stringToBoolean, timeFromMilliseconds } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { T, DiskSnapshot } from 'client/constants'
const ImageSnapshotCard = memo(
/**
* @param {object} props - Props
* @param {DiskSnapshot} props.snapshot - Disk snapshot
* @param {function({ snapshot: DiskSnapshot }):ReactElement} [props.actions] - Actions
* @returns {ReactElement} - Card
*/
({ snapshot = {}, actions }) => {
const classes = rowStyles()
const {
ID,
NAME,
ACTIVE,
DATE,
SIZE: SNAPSHOT_SIZE,
MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
} = snapshot
const isActive = useMemo(() => stringToBoolean(ACTIVE), [ACTIVE])
const time = useMemo(() => timeFromMilliseconds(+DATE), [DATE])
const timeFormat = useMemo(() => time.toFormat('ff'), [DATE])
const timeAgo = useMemo(() => `created ${time.toRelative()}`, [DATE])
const sizeInfo = useMemo(() => {
const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
const monitorSize = +SNAPSHOT_MONITOR_SIZE
? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
: '-'
return `${monitorSize}/${size}`
}, [SNAPSHOT_SIZE, SNAPSHOT_MONITOR_SIZE])
return (
<Paper variant="outlined" className={classes.root}>
<div className={classes.main}>
<div className={classes.title}>
<Typography noWrap component="span">
{NAME}
</Typography>
<span className={classes.labels}>
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
<StatusChip text={<Translate word={T.Snapshot} />} />
</span>
</div>
<div className={classes.caption}>
<span title={timeFormat}>{`#${ID} ${timeAgo}`}</span>
<span
title={`${Tr(T.Monitoring)} / ${Tr(T.DiskSize)}: ${sizeInfo}`}
>
<ModernTv />
<span>{` ${sizeInfo}`}</span>
</span>
</div>
</div>
{typeof actions === 'function' && (
<div className={classes.actions}>{actions({ snapshot })}</div>
)}
</Paper>
)
}
)
ImageSnapshotCard.propTypes = {
snapshot: PropTypes.object.isRequired,
actions: PropTypes.func,
}
ImageSnapshotCard.displayName = 'ImageSnapshotCard'
export default ImageSnapshotCard

View File

@ -23,6 +23,8 @@ import DiskCard from 'client/components/Cards/DiskCard'
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
import EmptyCard from 'client/components/Cards/EmptyCard'
import HostCard from 'client/components/Cards/HostCard'
import ImageCreateCard from 'client/components/Cards/ImageCreateCard'
import ImageSnapshotCard from 'client/components/Cards/ImageSnapshotCard'
import MarketplaceAppCard from 'client/components/Cards/MarketplaceAppCard'
import MarketplaceCard from 'client/components/Cards/MarketplaceCard'
import NetworkCard from 'client/components/Cards/NetworkCard'
@ -52,6 +54,8 @@ export {
DiskSnapshotCard,
EmptyCard,
HostCard,
ImageCreateCard,
ImageSnapshotCard,
MarketplaceAppCard,
MarketplaceCard,
NetworkCard,

View File

@ -0,0 +1,69 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { memo, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { ErrorHelper } from 'client/components/FormControl'
import { generateKey } from 'client/utils'
import InputCode from 'client/components/FormControl/InputCode'
const DockerfileController = memo(
({ control, cy = `input-${generateKey()}`, name = '' }) => {
const {
getValues,
setValue,
formState: { errors },
} = useFormContext()
const [internalError, setInternalError] = useState()
const messageError = name
.split('.')
.reduce((errs, current) => errs?.[current], errors)?.message?.[0]
useEffect(() => {
setInternalError(messageError)
}, [messageError])
return (
<div data-cy={cy}>
<InputCode
mode="dockerfile"
height="600px"
value={getValues(name)}
onChange={(value) => {
setValue(name, value)
}}
onFocus={(e) => {
setInternalError()
}}
/>
{internalError && <ErrorHelper label={internalError} />}
</div>
)
},
(prevProps, nextProps) => prevProps.cy === nextProps.cy
)
DockerfileController.propTypes = {
control: PropTypes.object,
cy: PropTypes.string,
name: PropTypes.string.isRequired,
}
DockerfileController.displayName = 'DockerfileController'
export default DockerfileController

View File

@ -35,9 +35,9 @@ const WrapperToLoadMode = ({ children, mode }) => {
return () => {
// remove all styles when component will be unmounted
document
.querySelectorAll('[id^=ace]')
.forEach((child) => child.parentNode.removeChild(child))
document.querySelectorAll('[id^=ace-]').forEach((child) => {
child.parentNode.removeChild(child)
})
}
}, [])

View File

@ -29,6 +29,7 @@ import SubmitButton, {
SubmitButtonPropTypes,
} from 'client/components/FormControl/SubmitButton'
import InputCode from 'client/components/FormControl/InputCode'
import DockerfileController from 'client/components/FormControl/DockerfileController'
import ErrorHelper from 'client/components/FormControl/ErrorHelper'
import Tooltip from 'client/components/FormControl/Tooltip'
@ -47,6 +48,7 @@ export {
SubmitButton,
SubmitButtonPropTypes,
InputCode,
DockerfileController,
ErrorHelper,
Tooltip,
}

View File

@ -51,6 +51,7 @@ const INPUT_CONTROLLER = {
[INPUT_TYPES.TIME]: FC.TimeController,
[INPUT_TYPES.TABLE]: FC.TableController,
[INPUT_TYPES.TOGGLE]: FC.ToggleController,
[INPUT_TYPES.DOCKERFILE]: FC.DockerfileController,
}
/**

View File

@ -0,0 +1,59 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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/Image/CloneForm/Steps/BasicConfiguration/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'configuration'
const Content = (props) => (
<FormWithSchema
cy="clone-configuration"
id={STEP_ID}
fields={() => FIELDS(props)}
/>
)
/**
* Step to configure the marketplace app.
*
* @param {object} isMultiple - is multiple rows
* @returns {Step} Configuration step
*/
const ConfigurationStep = (isMultiple) => ({
id: STEP_ID,
label: T.Configuration,
resolver: () => SCHEMA(isMultiple),
optionsValidate: { abortEarly: false },
content: () => Content(isMultiple),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
nics: PropTypes.array,
isMultiple: PropTypes.bool,
}
export default ConfigurationStep

View File

@ -0,0 +1,55 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, object, ObjectSchema } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
const PREFIX = {
name: 'prefix',
label: T.Prefix,
tooltip: T.PrefixMultipleConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.required()
.default(() => T.CopyOf),
}
const NAME = {
name: 'name',
label: T.Name,
tooltip: T.NewTemplateNameConcept,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.required()
.default(() => undefined),
}
/**
* @param {object} [stepProps] - Step props
* @param {boolean} [stepProps.isMultiple]
* - If true, the prefix will be added to the name of the new template
* @returns {Field[]} Fields
*/
export const FIELDS = ({ isMultiple } = {}) => [isMultiple ? PREFIX : NAME]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (stepProps) =>
object(getValidationFromFields(FIELDS(stepProps)))

View File

@ -0,0 +1,79 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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/Image/CloneForm/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
disableGlobalSort
displaySelectedRows
pageSize={5}
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.SelectDatastoreImage,
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-2022, 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,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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/Image/CloneForm/Steps/BasicConfiguration'
import DatastoresTable, {
STEP_ID as DATASTORE_ID,
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
import { createSteps } from 'client/utils'
const Steps = createSteps(
(app) => [BasicConfiguration, DatastoresTable].filter(Boolean),
{
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,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema'
import { T } from 'client/constants'
export const STEP_ID = 'dockerfile'
const Content = () => <FormWithSchema id={STEP_ID} fields={FIELDS} />
/**
* Docker file.
*
* @returns {object} Dockerfile step
*/
const DockerFile = () => ({
id: STEP_ID,
label: T.Dockerfile,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default DockerFile

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, object, ObjectSchema } from 'yup'
import { Field, getValidationFromFields, encodeBase64 } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
/** @type {Field} Dockerfile field */
export const DOCKERFILE = {
name: 'PATH',
label: T.Dockerfile,
type: INPUT_TYPES.DOCKERFILE,
cy: 'dockerfile',
validation: string()
.trim()
.required()
.afterSubmit((value) => encodeBase64(value)),
grid: { md: 12 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [DOCKERFILE]
/**
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Image/CreateDockerfile/Steps/General/schema'
import { T } from 'client/constants'
export const STEP_ID = 'general'
const Content = () => <FormWithSchema id={STEP_ID} fields={FIELDS} />
/**
* General configuration about VM Template.
*
* @returns {object} General configuration step
*/
const General = () => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default General

View File

@ -0,0 +1,64 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, boolean, number, object, ObjectSchema } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
export const IMAGE_LOCATION_TYPES = {
PATH: 'path',
UPLOAD: 'upload',
EMPTY: 'empty',
}
/** @type {Field} Name field */
export const NAME = {
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string().trim().required(),
grid: { xs: 12, md: 6 },
}
/** @type {Field} Context field */
export const CONTEXT = {
name: 'CONTEXT',
label: T.Context,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { xs: 12, md: 6 },
}
/** @type {Field} Size field */
export const SIZE = {
name: 'SIZE',
htmlType: 'number',
label: T.Size,
type: INPUT_TYPES.TEXT,
tooltip: T.ImageSize,
validation: number().positive().required(),
grid: { md: 12 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [NAME, CONTEXT, SIZE]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,63 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 Datastore, {
STEP_ID as DATASTORE_ID,
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/Image/CreateForm/Steps/General'
import AdvancedOptions, {
STEP_ID as ADVANCE_ID,
} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions'
import CustomAttributes, {
STEP_ID as CUSTOM_ID,
} from 'client/components/Forms/Image/CreateForm/Steps/CustomAttributes'
import { jsonToXml } from 'client/models/Helper'
import { createSteps, cloneObject, set } from 'client/utils'
const Steps = createSteps(
[General, Datastore, AdvancedOptions, CustomAttributes],
{
transformBeforeSubmit: (formData) => {
const {
[GENERAL_ID]: general = {},
[DATASTORE_ID]: [datastore] = [],
[ADVANCE_ID]: advanced = {},
[CUSTOM_ID]: custom = {},
} = formData ?? {}
const generalData = cloneObject(general)
set(generalData, 'UPLOAD', undefined)
set(generalData, 'IMAGE_LOCATION', undefined)
return {
template: jsonToXml({
...custom,
...advanced,
...generalData,
}),
datastore: datastore?.ID,
file: general?.UPLOAD,
}
},
}
)
export default Steps

View File

@ -0,0 +1,55 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 Datastore, {
STEP_ID as DATASTORE_ID,
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/Image/CreateDockerfile/Steps/General'
import Dockerfile, {
STEP_ID as DOCKERFILE_ID,
} from 'client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile'
import { jsonToXml } from 'client/models/Helper'
import { createSteps, cloneObject, set } from 'client/utils'
const Steps = createSteps([General, Datastore, Dockerfile], {
transformBeforeSubmit: (formData) => {
const {
[GENERAL_ID]: general = {},
[DATASTORE_ID]: [datastore] = [],
[DOCKERFILE_ID]: dockerfile = {},
} = formData ?? {}
const generalData = cloneObject(general)
set(generalData, 'CONTEXT', undefined)
set(generalData, 'SIZE', undefined)
return {
template: jsonToXml({
...{
PATH: `dockerfile://?fileb64=${dockerfile.PATH}&amp;context=${general.CONTEXT}&amp;size=${general.SIZE}`,
},
...generalData,
}),
datastore: datastore?.ID,
}
},
})
export default Steps

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema'
import { T } from 'client/constants'
export const STEP_ID = 'advanced'
const Content = () => <FormWithSchema id={STEP_ID} fields={FIELDS} />
/**
* Advanced options create image.
*
* @returns {object} Advanced options configuration step
*/
const AdvancedOptions = () => ({
id: STEP_ID,
label: T.AdvancedOptions,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default AdvancedOptions

View File

@ -0,0 +1,180 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, object, ObjectSchema } from 'yup'
import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
import {
IMAGE_LOCATION_TYPES,
IMAGE_LOCATION_FIELD,
} from 'client/components/Forms/Image/CreateForm/Steps/General/schema'
import { useGetSunstoneConfigQuery } from 'client/features/OneApi/system'
export const BUS_TYPES = {
VD: 'path',
SD: 'upload',
HD: 'empty',
CUSTOM: 'custom',
}
const FORMAT_TYPES = {
RAW: 'raw',
QCOW2: 'qcow2',
CUSTOM: 'custom',
}
const BUS = {
[BUS_TYPES.VD]: T.Vd,
[BUS_TYPES.SD]: T.Sd,
[BUS_TYPES.HD]: T.Hd,
[BUS_TYPES.CUSTOM]: T.Custom,
}
const htmlType = (opt) => (value) => value !== opt && INPUT_TYPES.HIDDEN
/** @type {Field} Bus field */
export const DEV_PREFIX = {
name: 'DEV_PREFIX',
label: T.Bus,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.entries(BUS), {
addEmpty: true,
getText: ([_, name]) => name,
getValue: ([key]) => key,
}),
validation: string()
.trim()
.default(() => undefined)
.afterSubmit((value, { context }) => {
const notEmptyString = value === '' ? undefined : value
return BUS_TYPES.CUSTOM === value
? context?.advanced?.CUSTOM_DEV_PREFIX
: notEmptyString
}),
grid: { md: 6 },
}
/** @type {Field} Dev Prefix field */
export const CUSTOM_DEV_PREFIX = {
name: 'CUSTOM_DEV_PREFIX',
dependOf: DEV_PREFIX.name,
htmlType: htmlType(BUS_TYPES.CUSTOM),
label: T.CustomBus,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.when(DEV_PREFIX.name, {
is: (location) => location === BUS_TYPES.CUSTOM,
then: (schema) => schema.required(),
otherwise: (schema) => schema.strip(),
})
.default(() => undefined),
grid: { md: 12 },
}
/** @type {Field} Device field */
export const DEVICE = {
name: 'DEVICE',
label: T.TargetDevice,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.default(() => undefined),
grid: { md: 6 },
}
/** @type {Field} Format field */
export const FORMAT_FIELD = {
name: 'FORMAT',
dependOf: `$general.${IMAGE_LOCATION_FIELD.name}`,
htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY),
label: T.Format,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.values(FORMAT_TYPES), {
addEmpty: true,
getText: (type) => type.toUpperCase(),
getValue: (type) => type,
}),
validation: string()
.trim()
.afterSubmit((value, { context }) => {
const notEmptyString = value === '' ? undefined : value
return FORMAT_TYPES.CUSTOM === value
? context?.advanced?.CUSTOM_FORMAT
: notEmptyString
}),
grid: { md: 6 },
}
/** @type {Field} Custom format field */
export const CUSTOM_FORMAT = {
name: 'CUSTOM_FORMAT',
dependOf: [`$general.${IMAGE_LOCATION_FIELD.name}`, FORMAT_FIELD.name],
htmlType: ([imageLocation, formatField] = []) =>
(imageLocation !== IMAGE_LOCATION_TYPES.EMPTY ||
formatField !== FORMAT_TYPES.CUSTOM) &&
INPUT_TYPES.HIDDEN,
label: T.CustomFormat,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.when(FORMAT_FIELD.name, {
is: (format) => format === FORMAT_TYPES.CUSTOM,
then: (schema) => schema.required(),
otherwise: (schema) => schema.strip(),
}),
grid: { md: 12 },
}
/** @type {Field} FS field */
export const FS = {
name: 'FS',
dependOf: `$general.${IMAGE_LOCATION_FIELD.name}`,
htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY),
label: T.Fs,
type: INPUT_TYPES.SELECT,
values: () => {
const { data: sunstoneConfig = {} } = useGetSunstoneConfigQuery()
return arrayToOptions(sunstoneConfig?.supported_fs || [], {
addEmpty: true,
getText: (fs) => fs.toUpperCase(),
getValue: (fs) => fs,
})
},
validation: string()
.trim()
.afterSubmit((value) => (value === '' ? undefined : value)),
grid: { md: 6 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [
DEV_PREFIX,
DEVICE,
CUSTOM_DEV_PREFIX,
FORMAT_FIELD,
FS,
CUSTOM_FORMAT,
]
/**
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { useCallback } from 'react'
import { object } from 'yup'
import { useFormContext, useWatch } from 'react-hook-form'
import { Box } from '@mui/material'
import { AttributePanel } from 'client/components/Tabs/Common'
import { cleanEmpty, cloneObject, set } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'custom-attributes'
const Content = () => {
const { setValue } = useFormContext()
const customVars = useWatch({ name: STEP_ID, defaultValue: {} })
const handleChangeAttribute = useCallback(
(path, newValue) => {
const newCustomVars = cloneObject(customVars)
set(newCustomVars, path, newValue)
setValue(STEP_ID, cleanEmpty(newCustomVars))
},
[customVars]
)
return (
<Box display="grid" gap="1em">
<AttributePanel
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={customVars}
filtersSpecialAttributes={false}
/>
</Box>
)
}
/**
* Custom variables about VM Template.
*
* @returns {object} Custom configuration step
*/
const CustomAttributes = () => ({
id: STEP_ID,
label: T.CustomAttributes,
resolver: object(),
optionsValidate: { abortEarly: false },
content: Content,
})
export default CustomAttributes

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Image/CreateForm/Steps/General/schema'
import { T } from 'client/constants'
export const STEP_ID = 'general'
const Content = () => <FormWithSchema id={STEP_ID} fields={FIELDS} />
/**
* General configuration about VM Template.
*
* @returns {object} General configuration step
*/
const General = () => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default General

View File

@ -0,0 +1,185 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, boolean, number, object, ObjectSchema, mixed } from 'yup'
import {
Field,
arrayToOptions,
getValidationFromFields,
upperCaseFirst,
} from 'client/utils'
import { T, INPUT_TYPES, IMAGE_TYPES_STR } from 'client/constants'
export const IMAGE_LOCATION_TYPES = {
PATH: 'path',
UPLOAD: 'upload',
EMPTY: 'empty',
}
const IMAGE_LOCATION = {
[IMAGE_LOCATION_TYPES.PATH]: T.Path,
[IMAGE_LOCATION_TYPES.UPLOAD]: T.Upload,
[IMAGE_LOCATION_TYPES.EMPTY]: T.EmptyDisk,
}
const htmlType = (opt, inputNumber) => (location) => {
if (location === opt && inputNumber) {
return 'number'
}
return location !== opt && INPUT_TYPES.HIDDEN
}
/** @type {Field} name field */
export const NAME = {
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string().trim().required(),
grid: { xs: 12, md: 6 },
}
/** @type {Field} Description field */
export const DESCRIPTION = {
name: 'DESCRIPTION',
label: T.Description,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: string().trim(),
grid: { xs: 12, md: 6 },
}
/** @type {Field} Type field */
export const TYPE = {
name: 'TYPE',
label: T.Type,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.values(IMAGE_TYPES_STR), {
addEmpty: false,
getText: (type) => {
switch (type) {
case IMAGE_TYPES_STR.OS:
return T.Os
case IMAGE_TYPES_STR.CDROM:
return T.Cdrom
case IMAGE_TYPES_STR.DATABLOCK:
return T.Datablock
default:
return upperCaseFirst(type.toLowerCase())
}
},
getValue: (type) => type,
}),
validation: string()
.trim()
.default(() => IMAGE_TYPES_STR.OS),
grid: { xs: 12, md: 6 },
}
/** @type {Field} Persistent field */
export const PERSISTENT = {
name: 'PERSISTENT',
label: T.MakePersistent,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { xs: 12, md: 6 },
}
/** @type {Field} Toggle select type image */
export const IMAGE_LOCATION_FIELD = {
name: 'IMAGE_LOCATION',
type: INPUT_TYPES.TOGGLE,
values: arrayToOptions(Object.entries(IMAGE_LOCATION), {
addEmpty: false,
getText: ([_, name]) => name,
getValue: ([image]) => image,
}),
validation: string()
.trim()
.required()
.default(() => IMAGE_LOCATION_TYPES.PATH),
grid: { md: 12 },
notNull: true,
}
/** @type {Field} path field */
export const PATH_FIELD = {
name: 'PATH',
dependOf: IMAGE_LOCATION_FIELD.name,
htmlType: htmlType(IMAGE_LOCATION_TYPES.PATH),
label: T.ImagePath,
type: INPUT_TYPES.TEXT,
validation: string()
.trim()
.when(IMAGE_LOCATION_FIELD.name, {
is: (location) => location === IMAGE_LOCATION_TYPES.PATH,
then: (schema) => schema.required(),
otherwise: (schema) => schema.strip(),
}),
grid: { md: 12 },
}
/** @type {Field} upload field */
export const UPLOAD_FIELD = {
name: 'UPLOAD',
dependOf: IMAGE_LOCATION_FIELD.name,
htmlType: htmlType(IMAGE_LOCATION_TYPES.UPLOAD),
label: T.Upload,
type: INPUT_TYPES.FILE,
validation: mixed().when(IMAGE_LOCATION_FIELD.name, {
is: (location) => location === IMAGE_LOCATION_TYPES.UPLOAD,
then: (schema) => schema.required(),
otherwise: (schema) => schema.strip(),
}),
grid: { md: 12 },
}
/** @type {Field} size field */
export const SIZE = {
name: 'SIZE',
dependOf: IMAGE_LOCATION_FIELD.name,
htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY, true),
label: T.Size,
type: INPUT_TYPES.TEXT,
tooltip: T.ImageSize,
validation: number()
.positive()
.default(() => undefined)
.when(IMAGE_LOCATION_FIELD.name, {
is: (location) => location === IMAGE_LOCATION_TYPES.EMPTY,
then: (schema) => schema.required(),
otherwise: (schema) => schema.strip(),
}),
grid: { md: 12 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [
NAME,
DESCRIPTION,
TYPE,
PERSISTENT,
IMAGE_LOCATION_FIELD,
PATH_FIELD,
UPLOAD_FIELD,
SIZE,
]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,62 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 Datastore, {
STEP_ID as DATASTORE_ID,
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
import General, {
STEP_ID as GENERAL_ID,
} from 'client/components/Forms/Image/CreateForm/Steps/General'
import AdvancedOptions, {
STEP_ID as ADVANCE_ID,
} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions'
import CustomAttributes, {
STEP_ID as CUSTOM_ID,
} from 'client/components/Forms/Image/CreateForm/Steps/CustomAttributes'
import { createSteps, cloneObject, set } from 'client/utils'
const Steps = createSteps(
[General, Datastore, AdvancedOptions, CustomAttributes],
{
transformBeforeSubmit: (formData) => {
const {
[GENERAL_ID]: general = {},
[DATASTORE_ID]: [datastore] = [],
[ADVANCE_ID]: advanced = {},
[CUSTOM_ID]: custom = {},
} = formData ?? {}
const generalData = cloneObject(general)
set(generalData, 'UPLOAD', undefined)
set(generalData, 'IMAGE_LOCATION', undefined)
return {
template: {
...custom,
...advanced,
...generalData,
},
datastore: datastore?.ID,
file: general?.UPLOAD,
}
},
}
)
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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/Image/CreateForm/Steps'

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const CloneForm = (configProps) =>
AsyncLoadForm({ formPath: 'Image/CloneForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'Image/CreateForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const CreateDockerfileForm = (configProps) =>
AsyncLoadForm({ formPath: 'Image/CreateDockerfile' }, configProps)
export { CloneForm, CreateForm, CreateDockerfileForm }

View File

@ -0,0 +1,84 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 {
Snackbar,
Alert,
CircularProgress,
Box,
Typography,
} from '@mui/material'
import { useGeneral, useGeneralApi } from 'client/features/General'
/**
* @returns {ReactElement} App rendered.
*/
const NotifierUpload = () => {
const { upload } = useGeneral()
const { uploadSnackbar } = useGeneralApi()
const handleClose = () => uploadSnackbar(0)
return (
<Snackbar
open={upload > 0}
autoHideDuration={10000}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
>
<Alert
icon={false}
onClose={handleClose}
severity="info"
variant="filled"
sx={{ width: '100%' }}
>
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress
variant="determinate"
value={upload}
color="inherit"
/>
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography
variant="caption"
component="div"
color="text.secondary"
>
{`${upload}%`}
</Typography>
</Box>
</Box>
</Alert>
</Snackbar>
)
}
export default NotifierUpload

View File

@ -0,0 +1,388 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { useHistory } from 'react-router-dom'
import { Typography, Grid } from '@mui/material'
import {
MoreVert,
AddCircledOutline,
Lock,
Cart,
Group,
Trash,
Code,
PageEdit,
} from 'iconoir-react'
import { useViews } from 'client/features/Auth'
import {
useLockImageMutation,
useCloneImageMutation,
useUnlockImageMutation,
useEnableImageMutation,
useDisableImageMutation,
usePersistentImageMutation,
useChangeImageOwnershipMutation,
useRemoveImageMutation,
} from 'client/features/OneApi/image'
import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
import { CloneForm } from 'client/components/Forms/Image'
import {
createActions,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils'
import ImageCreateCard from 'client/components/Cards/ImageCreateCard'
import { Tr, Translate } from 'client/components/HOC'
import { PATH } from 'client/apps/sunstone/routesOne'
import { isAvailableAction } from 'client/models/VirtualMachine'
import { T, IMAGE_ACTIONS, VM_ACTIONS, RESOURCE_NAMES } from 'client/constants'
const isDisabled = (action) => (rows) =>
!isAvailableAction(
action,
rows.map(({ original }) => original)
)
const ListImagesNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`image-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListImagesNames rows={rows} />
const MessageToConfirmAction = (rows) => (
<>
<ListImagesNames rows={rows} />
<Translate word={T.DoYouWantProceed} />
</>
)
/**
* Generates the actions to operate resources on Image table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
const [clone] = useCloneImageMutation()
const [lock] = useLockImageMutation()
const [unlock] = useUnlockImageMutation()
const [enable] = useEnableImageMutation()
const [disable] = useDisableImageMutation()
const [persistent] = usePersistentImageMutation()
const [changeOwnership] = useChangeImageOwnershipMutation()
const [deleteImage] = useRemoveImageMutation()
const resourcesView = getResourceView(RESOURCE_NAMES.IMAGE)?.actions
const imageActions = useMemo(
() =>
createActions({
filters: resourcesView,
actions: [
{
accessor: IMAGE_ACTIONS.CREATE_DIALOG,
dataCy: `image_${IMAGE_ACTIONS.CREATE_DIALOG}`,
tooltip: T.Create,
icon: AddCircledOutline,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.CreateImage,
children: () => (
<Grid container spacing={3}>
<ImageCreateCard
name={Tr(T.CreateImage)}
Icon={PageEdit}
onClick={() => history.push(PATH.STORAGE.IMAGES.CREATE)}
/>
{resourcesView?.dockerfile_dialog && (
<ImageCreateCard
name={Tr(T.CreateDockerfile)}
Icon={Code}
onClick={() =>
history.push(PATH.STORAGE.IMAGES.DOCKERFILE)
}
/>
)}
</Grid>
),
fixedWidth: true,
fixedHeight: true,
handleAccept: undefined,
dataCy: `modal-${IMAGE_ACTIONS.CREATE_DIALOG}`,
},
},
],
},
{
accessor: VM_ACTIONS.CREATE_APP_DIALOG,
dataCy: `image_${VM_ACTIONS.CREATE_APP_DIALOG}`,
disabled: isDisabled(VM_ACTIONS.CREATE_APP_DIALOG),
tooltip: T.CreateMarketApp,
selected: { max: 1 },
icon: Cart,
action: (rows) => {
const vm = rows?.[0]?.original ?? {}
const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
history.push(path, [RESOURCE_NAMES.VM, vm])
},
},
{
accessor: IMAGE_ACTIONS.CLONE,
label: T.Clone,
tooltip: T.Clone,
selected: true,
color: 'secondary',
options: [
{
dialogProps: {
title: (rows) => {
const isMultiple = rows?.length > 1
const { ID, NAME } = rows?.[0]?.original ?? {}
return [
Tr(
isMultiple ? T.CloneSeveralTemplates : T.CloneTemplate
),
!isMultiple && `#${ID} ${NAME}`,
]
.filter(Boolean)
.join(' - ')
},
dataCy: 'modal-clone',
},
form: (rows) => {
const names = rows?.map(({ original }) => original?.NAME)
const stepProps = { isMultiple: names.length > 1 }
const initialValues = { name: `Copy of ${names?.[0]}` }
return CloneForm({ stepProps, initialValues })
},
onSubmit:
(rows) =>
async ({ prefix, name, datastore } = {}) => {
const images = rows?.map?.(
({ original: { ID, NAME } = {} }) =>
// overwrite all names with prefix+NAME
({
id: ID,
name: prefix ? `${prefix} ${NAME}` : name,
datastore,
})
)
await Promise.all(images.map(clone))
},
},
],
},
{
tooltip: T.Lock,
icon: Lock,
selected: true,
color: 'secondary',
dataCy: 'image-lock',
options: [
{
accessor: IMAGE_ACTIONS.LOCK,
name: T.Lock,
isConfirmDialog: true,
dialogProps: {
title: T.Lock,
dataCy: `modal-${IMAGE_ACTIONS.LOCK}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => lock({ id })))
},
},
{
accessor: IMAGE_ACTIONS.UNLOCK,
name: T.Unlock,
isConfirmDialog: true,
dialogProps: {
title: T.Unlock,
dataCy: `modal-${IMAGE_ACTIONS.UNLOCK}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => unlock({ id })))
},
},
],
},
{
tooltip: T.Enable,
icon: MoreVert,
selected: true,
color: 'secondary',
dataCy: 'image-enable',
options: [
{
accessor: IMAGE_ACTIONS.ENABLE,
name: T.Enable,
isConfirmDialog: true,
dialogProps: {
title: T.Enable,
children: MessageToConfirmAction,
dataCy: `modal-${IMAGE_ACTIONS.ENABLE}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => enable(id)))
},
},
{
accessor: IMAGE_ACTIONS.DISABLE,
name: T.Disable,
isConfirmDialog: true,
dialogProps: {
title: T.Disable,
children: MessageToConfirmAction,
dataCy: `modal-${IMAGE_ACTIONS.DISABLE}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => disable(id)))
},
},
{
accessor: IMAGE_ACTIONS.PERSISTENT,
name: T.Persistent,
isConfirmDialog: true,
dialogProps: {
title: T.Persistent,
children: MessageToConfirmAction,
dataCy: `modal-${IMAGE_ACTIONS.PERSISTENT}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => persistent({ id, persistent: true }))
)
},
},
{
accessor: IMAGE_ACTIONS.NON_PERSISTENT,
name: T.NonPersistyent,
isConfirmDialog: true,
dialogProps: {
title: T.NonPersistyent,
children: MessageToConfirmAction,
dataCy: `modal-${IMAGE_ACTIONS.NON_PERSISTENT}`,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => persistent({ id, persistent: false }))
)
},
},
],
},
{
tooltip: T.Ownership,
icon: Group,
selected: true,
color: 'secondary',
dataCy: 'image-ownership',
options: [
{
accessor: IMAGE_ACTIONS.CHANGE_OWNER,
disabled: isDisabled(IMAGE_ACTIONS.CHANGE_OWNER),
name: T.ChangeOwner,
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader,
dataCy: `modal-${IMAGE_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: IMAGE_ACTIONS.CHANGE_GROUP,
disabled: isDisabled(IMAGE_ACTIONS.CHANGE_GROUP),
name: T.ChangeGroup,
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader,
dataCy: `modal-${IMAGE_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: IMAGE_ACTIONS.DELETE,
tooltip: T.Delete,
icon: Trash,
color: 'error',
selected: { min: 1 },
dataCy: `image_${IMAGE_ACTIONS.DELETE}`,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
dataCy: `modal-${IMAGE_ACTIONS.DELETE}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => deleteImage({ id })))
},
},
],
},
],
}),
[view]
)
return imageActions
}
export default Actions

View File

@ -21,7 +21,7 @@ import { useGetVmsQuery } from 'client/features/OneApi/vm'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VmColumns from 'client/components/Tables/Vms/columns'
import VmRow from 'client/components/Tables/Vms/row'
import { RESOURCE_NAMES } from 'client/constants'
import { RESOURCE_NAMES, VM_STATES, STATES } from 'client/constants'
const DEFAULT_DATA_CY = 'vms'
@ -55,7 +55,7 @@ const VmsTable = (props) => {
?.filter((vm) =>
host?.ID ? [host?.VMS?.ID ?? []].flat().includes(vm.ID) : true
)
?.filter(({ STATE }) => STATE !== '6') ?? [],
?.filter(({ STATE }) => VM_STATES[STATE]?.name !== STATES.DONE) ?? [],
}),
})

View File

@ -1,18 +1,3 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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. *
* ------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { InfoEmpty } from 'iconoir-react'
@ -24,19 +25,23 @@ import { T } from 'client/constants'
/**
* Renders default empty tab.
*
* @param {object} props - Props
* @param {string} props.label - label string
* @returns {ReactElement} Empty tab
*/
const EmptyTab = () => {
const EmptyTab = ({ label = T.NoDataAvailable }) => {
const classes = useStyles()
return (
<span className={classes.noDataMessage}>
<InfoEmpty />
<Translate word={T.NoDataAvailable} />
<Translate word={label} />
</span>
)
}
EmptyTab.propTypes = {
label: PropTypes.string,
}
EmptyTab.displayName = 'EmptyTab'
export default EmptyTab

View File

@ -0,0 +1,142 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { memo } from 'react'
import PropTypes from 'prop-types'
import { Trash, UndoAction } from 'iconoir-react'
import {
useFlattenImageSnapshotMutation,
useRevertImageSnapshotMutation,
useDeleteImageSnapshotMutation,
} from 'client/features/OneApi/image'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { Tr, Translate } from 'client/components/HOC'
import { T, IMAGE_ACTIONS } from 'client/constants'
const SnapshotFlattenAction = memo(({ id, snapshot }) => {
const [flattenImageSnapshot] = useFlattenImageSnapshotMutation()
const { ID, NAME = T.Snapshot } = snapshot
const handleDelete = async () => {
await flattenImageSnapshot({ id, snapshot: ID })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': IMAGE_ACTIONS.SNAPSHOT_FLATTEN,
tooltip: Tr(T.Flatten),
label: Tr(T.Flatten),
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate word={T.FlattenSnapshot} values={`#${ID} - ${NAME}`} />
),
children: (
<>
<p>{Tr(T.DeleteOtherSnapshots)}</p>
<p>{Tr(T.DoYouWantProceed)}</p>
</>
),
},
onSubmit: handleDelete,
},
]}
/>
)
})
const SnapshotRevertAction = memo(({ id, snapshot }) => {
const [revertImageSnapshot] = useRevertImageSnapshotMutation()
const { ID, NAME = T.Snapshot } = snapshot
const handleRevert = async () => {
await revertImageSnapshot({ id, snapshot: ID })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': IMAGE_ACTIONS.SNAPSHOT_REVERT,
icon: <UndoAction />,
tooltip: Tr(T.Revert),
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate word={T.RevertSomething} values={`#${ID} - ${NAME}`} />
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleRevert,
},
]}
/>
)
})
const SnapshotDeleteAction = memo(({ id, snapshot }) => {
const [deleteImageSnapshot] = useDeleteImageSnapshotMutation()
const { ID, NAME = T.Snapshot } = snapshot
const handleDelete = async () => {
await deleteImageSnapshot({ id, snapshot: ID })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': IMAGE_ACTIONS.SNAPSHOT_DELETE,
icon: <Trash />,
tooltip: Tr(T.Delete),
}}
options={[
{
isConfirmDialog: true,
dialogProps: {
title: (
<Translate word={T.DeleteSomething} values={`#${ID} - ${NAME}`} />
),
children: <p>{Tr(T.DoYouWantProceed)}</p>,
},
onSubmit: handleDelete,
},
]}
/>
)
})
const ActionPropTypes = {
id: PropTypes.string,
snapshot: PropTypes.object,
}
SnapshotFlattenAction.propTypes = ActionPropTypes
SnapshotFlattenAction.displayName = 'SnapshotFlattenAction'
SnapshotRevertAction.propTypes = ActionPropTypes
SnapshotRevertAction.displayName = 'SnapshotRevertAction'
SnapshotDeleteAction.propTypes = ActionPropTypes
SnapshotDeleteAction.displayName = 'SnapshotDeleteAction'
export { SnapshotFlattenAction, SnapshotDeleteAction, SnapshotRevertAction }

View File

@ -0,0 +1,84 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { T } from 'client/constants'
import { useGetImageQuery } from 'client/features/OneApi/image'
import EmptyTab from 'client/components/Tabs/EmptyTab'
import ImageSnapshotCard from 'client/components/Cards/ImageSnapshotCard'
import {
SnapshotFlattenAction,
SnapshotDeleteAction,
SnapshotRevertAction,
} from 'client/components/Tabs/Image/Snapshots/Actions'
import { getSnapshots } from 'client/models/Image'
/**
* Renders the list of disks from a VM.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string[]} props.tabProps.actions - Actions tab
* @param {string} props.id - Image id
* @returns {ReactElement} Storage tab
*/
const ImageStorageTab = ({ tabProps: { actions } = {}, id }) => {
const { data: image = {} } = useGetImageQuery({ id })
const [snapshots] = useMemo(() => [getSnapshots(image)], [image])
return (
<div>
<Stack gap="1em" py="0.8em">
{snapshots.length ? (
snapshots?.map?.((snapshot) => (
<ImageSnapshotCard
key={snapshot.ID}
snapshot={snapshot}
actions={() => (
<>
{actions.snapshot_flatten && (
<SnapshotFlattenAction id={id} snapshot={snapshot} />
)}
{actions.snapshot_revert && (
<SnapshotRevertAction id={id} snapshot={snapshot} />
)}
{actions.snapshot_delete && (
<SnapshotDeleteAction id={id} snapshot={snapshot} />
)}
</>
)}
/>
))
) : (
<EmptyTab label={T.NotSnapshotCurrenty} />
)}
</Stack>
</div>
)
}
ImageStorageTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ImageStorageTab.displayName = 'ImageStorageTab'
export default ImageStorageTab

View File

@ -0,0 +1,59 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { T } from 'client/constants'
import EmptyTab from 'client/components/Tabs/EmptyTab'
import { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { useGetImageQuery } from 'client/features/OneApi/image'
import { VmsTable } from 'client/components/Tables'
/**
* Renders mainly Vms tab.
*
* @param {object} props - Props
* @param {string} props.id - Image id
* @returns {ReactElement} vms tab
*/
const VmsTab = ({ id }) => {
const { data: image = {} } = useGetImageQuery({ id })
const path = PATH.INSTANCE.VMS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
return (
<VmsTable
disableGlobalSort
displaySelectedRows
host={image}
onRowClick={(row) => handleRowClick(row.ID)}
noDataMessage={<EmptyTab label={T.NotVmsCurrenty} />}
/>
)
}
VmsTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VmsTab.displayName = 'VmsTab'
export default VmsTab

View File

@ -24,10 +24,14 @@ import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/Image/Info'
import Vms from 'client/components/Tabs/Image/Vms'
import Snapshots from 'client/components/Tabs/Image/Snapshots'
const getTabComponent = (tabName) =>
({
info: Info,
vms: Vms,
snapshot: Snapshots,
}[tabName])
const ImageTabs = memo(({ id }) => {

View File

@ -165,6 +165,13 @@ export const IMAGE_STATES = [
export const IMAGE_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
DELETE: 'delete',
LOCK: 'lock',
UNLOCK: 'unlock',
CLONE: 'clone',
ENABLE: 'enable',
DISABLE: 'disable',
PERSISTENT: 'persistent',
NON_PERSISTENT: 'nonpersistent',
// INFORMATION
RENAME: ACTIONS.RENAME,
@ -172,4 +179,7 @@ export const IMAGE_ACTIONS = {
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
CHANGE_TYPE: 'chtype',
CHANGE_PERS: 'persistent',
SNAPSHOT_FLATTEN: 'flatten',
SNAPSHOT_REVERT: 'revert',
SNAPSHOT_DELETE: 'delete',
}

View File

@ -128,6 +128,7 @@ export const INPUT_TYPES = {
TEXT: 'text',
TABLE: 'table',
TOGGLE: 'toggle',
DOCKERFILE: 'dockerfile',
}
export const DEBUG_LEVEL = {

View File

@ -71,6 +71,8 @@ module.exports = {
CreateServiceTemplate: 'Create Service Template',
CreateVirtualNetwork: 'Create Virtual Network',
CreateVmTemplate: 'Create VM Template',
CreateImage: 'Create Image',
CreateDockerfile: 'Create Dockerfile',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@ -83,6 +85,7 @@ module.exports = {
DeleteAddressRange: 'Delete Address Range',
DeleteTemplate: 'Delete Template',
DeleteVirtualNetwork: 'Delete Virtual Network',
DeleteOtherSnapshots: 'This will delete all the other image snapshots',
Deploy: 'Deploy',
DeployServiceTemplate: 'Deploy Service Template',
Detach: 'Detach',
@ -95,6 +98,8 @@ module.exports = {
Enable: 'Enable',
Failure: 'Failure',
Finish: 'Finish',
Flatten: 'Flatten',
FlattenSnapshot: 'Flatten %s',
Hold: 'Hold',
Import: 'Import',
Info: 'Info',
@ -138,6 +143,7 @@ module.exports = {
Select: 'Select',
SelectCluster: 'Select Cluster',
SelectDatastore: 'Select a Datastore to store the resource',
SelectDatastoreImage: 'Select a Datastore',
SelectDockerHubTag: 'Select DockerHub image tag (default latest)',
SelectGroup: 'Select a group',
SelectHost: 'Select a host',
@ -358,6 +364,21 @@ module.exports = {
Marketplaces: 'Marketplaces',
App: 'App',
Apps: 'Apps',
Os: 'Operating system image',
Cdrom: 'Readonly CD-ROM',
Datablock: 'Generic storage datablock',
Path: 'Path/URL',
ImagePath: 'Path in OpenNebula server or URL',
Upload: 'Upload',
EmptyDisk: 'Empty disk image',
ImageSize: 'Image size, in Megabytes',
Vd: 'Virtio',
Sd: 'SCSI/SATA',
Hd: 'Parallel ATA (IDE)',
CustomBus: 'Custom bus',
Fs: 'Fs',
CustomFormat: 'Custom Format',
Dockerfile: 'Dockerfile',
/* sections - templates & instances */
Instances: 'Instances',
@ -560,6 +581,7 @@ module.exports = {
CustomAttributes: 'Custom Attributes',
Hypervisor: 'Hypervisor',
Logo: 'Logo',
MakePersistent: 'Make Persistent',
MakeNewImagePersistent: 'Make the new images persistent',
TemplateName: 'Template name',
Virtualization: 'Virtualization',
@ -1039,6 +1061,7 @@ module.exports = {
BasePath: 'Base path',
FileSystemType: 'Filesystem type',
Persistent: 'Persistent',
NonPersistyent: 'Non Persistent',
RunningVMs: 'Running VMs',
/* Disk - general */
DiskType: 'Disk type',
@ -1074,6 +1097,7 @@ module.exports = {
Iothread id used by this disk. Default is round robin.
Can be used only if IOTHREADS > 0. If this input is disabled
please first configure IOTHREADS value on OS & CPU -> Features`,
ImageLocation: 'Image Location',
/* Provision schema */
/* Provision - general */
@ -1097,6 +1121,11 @@ module.exports = {
Mandatory: 'Mandatory',
PressKeysToAddAValue: 'Press any of the following keys to add a value: %s',
/** Image */
NotVmsCurrenty: 'There are currently no VMs associated with this image',
NotSnapshotCurrenty:
'There are currently no snapshots associated with this image',
/* Validation */
/* Validation - mixed */
'validation.mixed.default': 'Is invalid',

View File

@ -0,0 +1,81 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { useHistory } from 'react-router'
import { jsonToXml } from 'client/models/Helper'
import { useGeneralApi } from 'client/features/General'
import {
useAllocateImageMutation,
useUploadImageMutation,
} from 'client/features/OneApi/image'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/Image'
import { PATH } from 'client/apps/sunstone/routesOne'
/**
* Displays the creation or modification form to a VM Template.
*
* @returns {ReactElement} VM Template form
*/
function CreateImage() {
const history = useHistory()
const [allocate] = useAllocateImageMutation()
const [upload] = useUploadImageMutation()
const { enqueueSuccess, uploadSnackbar } = useGeneralApi()
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
const onSubmit = async ({ template, datastore, file }) => {
if (file) {
const uploadProcess = (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
)
uploadSnackbar(percentCompleted)
percentCompleted === 100 && uploadSnackbar(0)
}
try {
const fileUploaded = await upload({
file,
uploadProcess,
}).unwrap()
template.PATH = fileUploaded[0]
} catch {}
}
try {
const newTemplateId = await allocate({
template: jsonToXml(template),
datastore,
}).unwrap()
history.push(PATH.STORAGE.IMAGES.LIST)
enqueueSuccess(`Image created - #${newTemplateId}`)
} catch {}
}
return (
<CreateForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
)
}
export default CreateImage

View File

@ -0,0 +1,56 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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 { useHistory } from 'react-router'
import { useGeneralApi } from 'client/features/General'
import { useAllocateImageMutation } from 'client/features/OneApi/image'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateDockerfileForm } from 'client/components/Forms/Image'
import { PATH } from 'client/apps/sunstone/routesOne'
/**
* Displays the creation or modification form to a VM Template.
*
* @returns {ReactElement} VM Template form
*/
function CreateDockerfile() {
const history = useHistory()
const [allocate] = useAllocateImageMutation()
const { enqueueSuccess } = useGeneralApi()
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
const onSubmit = async (stepTemplate) => {
try {
const newTemplateId = await allocate(stepTemplate).unwrap()
history.push(PATH.STORAGE.IMAGES.LIST)
enqueueSuccess(`Image created - #${newTemplateId}`)
} catch {}
}
return (
<CreateDockerfileForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
{(config) => <DefaultFormStepper {...config} />}
</CreateDockerfileForm>
)
}
export default CreateDockerfile

View File

@ -21,11 +21,9 @@ import Cancel from 'iconoir-react/dist/Cancel'
import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
import {
useLazyGetImageQuery,
useUpdateImageMutation,
} from 'client/features/OneApi/image'
import { useLazyGetImageQuery } from 'client/features/OneApi/image'
import { ImagesTable } from 'client/components/Tables'
import ImageActions from 'client/components/Tables/Images/actions'
import ImageTabs from 'client/components/Tabs/Image'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
@ -40,6 +38,7 @@ import { T, Image } from 'client/constants'
*/
function Images() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = ImageActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
@ -50,7 +49,7 @@ function Images() {
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<ImagesTable
onSelectedRowsChange={onSelectedRowsChange}
useUpdateMutation={useUpdateImageMutation}
globalActions={actions}
/>
{hasSelectedRows && (
@ -82,24 +81,19 @@ function Images() {
* @returns {ReactElement} Image details
*/
const InfoTabs = memo(({ image, gotoPage, unselect }) => {
const [get, { data: lazyData, isFetching }] = useLazyGetImageQuery()
const [getImage, { data: lazyData, isFetching }] = useLazyGetImageQuery()
const id = lazyData?.ID ?? image.ID
const name = lazyData?.NAME ?? image.NAME
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mx={1} mb={1}>
<Typography color="text.primary" noWrap flexGrow={1}>
{`#${id} | ${name}`}
</Typography>
{/* -- ACTIONS -- */}
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => get({ id })}
onClick={() => getImage({ id })}
/>
{typeof gotoPage === 'function' && (
<SubmitButton
@ -117,9 +111,11 @@ const InfoTabs = memo(({ image, gotoPage, unselect }) => {
onClick={() => unselect()}
/>
)}
{/* -- END ACTIONS -- */}
<Typography color="text.primary" noWrap>
{`#${id} | ${name}`}
</Typography>
</Stack>
<ImageTabs id={id} />
<ImageTabs id={image.ID} />
</Stack>
)
})

View File

@ -22,6 +22,7 @@ export const changeAppTitle = createAction('Change App title')
export const dismissSnackbar = createAction('Dismiss snackbar')
export const deleteSnackbar = createAction('Delete snackbar')
export const setUploadSnackbar = createAction('Change upload snackbar')
export const enqueueSnackbar = createAction(
'Enqueue snackbar',

View File

@ -32,6 +32,8 @@ export const useGeneralApi = () => {
changeAppTitle: (appTitle) => dispatch(actions.changeAppTitle(appTitle)),
changeZone: (zone) => dispatch(actions.changeZone(zone)),
uploadSnackbar: (percent) => dispatch(actions.setUploadSnackbar(percent)),
// dismiss all if no key has been defined
dismissSnackbar: (key) =>
dispatch(actions.dismissSnackbar({ key, dismissAll: !key })),

View File

@ -27,6 +27,7 @@ const initial = {
withGroupSwitcher: false,
isLoading: false,
isFixMenu: false,
upload: 0,
notifications: [],
}
@ -64,7 +65,11 @@ const slice = createSlice({
...state,
zone: payload,
}))
/* UPLOAD NOTIFICATION */
.addCase(actions.setUploadSnackbar, (state, { payload }) => ({
...state,
upload: payload,
}))
/* NOTIFICATION ACTIONS */
.addCase(actions.enqueueSnackbar, (state, { payload }) => {
const { key, options, message } = payload

View File

@ -20,6 +20,7 @@ import {
ONE_RESOURCES_POOL,
} from 'client/features/OneApi'
import { UpdateFromSocket } from 'client/features/OneApi/socket'
import http from 'client/utils/rest'
import {
FilterFlag,
Image,
@ -101,6 +102,35 @@ const imageApi = oneApi.injectEndpoints({
},
invalidatesTags: [IMAGE_POOL],
}),
uploadImage: builder.mutation({
/**
* Upload image.
*
* @param {object} params - request params
* @param {object} params.file - image file
* @param {Function} params.uploadProcess - upload process function
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
queryFn: async ({ file, uploadProcess }) => {
try {
const data = new FormData()
data.append('files', file)
const response = await http.request({
url: '/api/image/upload',
method: 'POST',
data,
onUploadProgress: uploadProcess,
})
return { data: response.data }
} catch (axiosError) {
const { response } = axiosError
return { error: { status: response?.status, data: response?.data } }
}
},
}),
cloneImage: builder.mutation({
/**
* Clones an existing image.
@ -408,4 +438,5 @@ export const {
useFlattenImageSnapshotMutation,
useLockImageMutation,
useUnlockImageMutation,
useUploadImageMutation,
} = imageApi

View File

@ -81,6 +81,22 @@ const systemApi = oneApi.injectEndpoints({
providesTags: [{ type: SYSTEM, id: 'sunstone-views' }],
keepUnusedDataFor: 600,
}),
getSunstoneConfig: builder.query({
/**
* Returns the Sunstone configuration for resource tabs.
*
* @returns {object} The loaded sunstone view files
* @throws Fails when response isn't code 200
*/
query: () => {
const name = SunstoneActions.SUNSTONE_CONFIG
const command = { name, ...SunstoneCommands[name] }
return { command }
},
providesTags: [{ type: SYSTEM, id: 'sunstone-config' }],
keepUnusedDataFor: 600,
}),
}),
})
@ -90,6 +106,8 @@ export const {
useLazyGetOneVersionQuery,
useGetOneConfigQuery,
useLazyGetOneConfigQuery,
useGetSunstoneConfigQuery,
useLazyGetSunstoneConfigQuery,
useGetSunstoneViewsQuery,
useLazyGetSunstoneViewsQuery,
} = systemApi

View File

@ -19,6 +19,7 @@ import {
IMAGE_STATES,
STATES,
Image,
DiskSnapshot,
} from 'client/constants'
import { prettyBytes } from 'client/utils'
@ -60,3 +61,15 @@ export const getDiskName = ({ IMAGE, SIZE, TYPE, FORMAT } = {}) => {
return IMAGE ?? { fs: `${FORMAT} - ${size}`, swap: size }[type]
}
/**
* @param {Image} image - Image
* @returns {DiskSnapshot[]} List of snapshots from resource
*/
export const getSnapshots = (image) => {
const {
SNAPSHOTS: { SNAPSHOT },
} = image ?? {}
return [SNAPSHOT].flat().filter(Boolean)
}

View File

@ -157,59 +157,60 @@ const upload = (
const { app, files, public: publicFile } = params
const { id, user, password } = userData
if (
global.paths.CPI &&
app &&
checkValidApp(app) &&
files &&
id &&
user &&
password
) {
const oneConnect = oneConnection(user, password)
checkUserAdmin(
oneConnect,
id,
(admin = false) => {
const pathUserData =
publicFile && admin ? `${app}` : `${app}${sep}${id}`
const pathUser = `${global.paths.CPI}${sep}${pathUserData}`
if (!existsSync(pathUser)) {
mkdirsSync(pathUser)
}
let method = ok
let message = ''
const data = []
for (const file of files) {
if (file && file.originalname && file.path && file.filename) {
const extFile = extname(file.originalname)
try {
const filenameApi = `${pathUserData}${sep}${file.filename}${extFile}`
const filename = `${pathUser}${sep}${file.filename}${extFile}`
moveSync(file.path, filename)
data.push(filenameApi)
} catch (error) {
method = internalServerError
message = error && error.message
break
}
}
}
res.locals.httpCode = httpResponse(
method,
data.length ? data : '',
message
)
next()
},
() => {
res.locals.httpCode = internalServerError
next()
}
!(
global.paths.CPI &&
app &&
checkValidApp(app) &&
files &&
id &&
user &&
password
)
} else {
) {
res.locals.httpCode = httpBadRequest
next()
}
const oneConnect = oneConnection(user, password)
checkUserAdmin(
oneConnect,
id,
(admin = false) => {
const pathUserData = publicFile && admin ? `${app}` : `${app}${sep}${id}`
const pathUser = `${global.paths.CPI}${sep}${pathUserData}`
if (!existsSync(pathUser)) {
mkdirsSync(pathUser)
}
let method = ok
let message = ''
const data = []
for (const file of files) {
if (file && file.originalname && file.path && file.filename) {
const extFile = extname(file.originalname)
try {
const filenameApi = `${pathUserData}${sep}${file.filename}${extFile}`
const filename = `${pathUser}${sep}${file.filename}${extFile}`
moveSync(file.path, filename)
data.push(filenameApi)
} catch (error) {
method = internalServerError
message = error && error.message
break
}
}
}
res.locals.httpCode = httpResponse(
method,
data.length ? data : '',
message
)
next()
},
() => {
res.locals.httpCode = internalServerError
next()
}
)
}
/**

View File

@ -0,0 +1,55 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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. *
* ------------------------------------------------------------------------- */
const { defaults, httpCodes } = require('server/utils/constants')
const { httpResponse } = require('server/utils/server')
const { defaultEmptyFunction } = defaults
const { ok, badRequest } = httpCodes
const httpBadRequest = httpResponse(badRequest, '', '')
/**
* Upload File.
*
* @param {object} res - response http
* @param {Function} next - express stepper
* @param {string} params - data response http
* @param {object} userData - user of http request
*/
const upload = (
res = {},
next = defaultEmptyFunction,
params = {},
userData = {}
) => {
const { files } = params
const { user, password } = userData
if (!(files && user && password)) {
res.locals.httpCode = httpBadRequest
next()
}
const data = files.map((file) => file.path)
res.locals.httpCode = httpResponse(ok, data.length ? data : '')
next()
}
const functionRoutes = {
upload,
}
module.exports = functionRoutes

View File

@ -0,0 +1,27 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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. *
* ------------------------------------------------------------------------- */
const { Actions, Commands } = require('server/routes/api/image/routes')
const { upload } = require('server/routes/api/image/functions')
const { IMAGE_UPLOAD } = Actions
module.exports = [
{
...Commands[IMAGE_UPLOAD],
action: upload,
},
]

View File

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, 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. *
* ------------------------------------------------------------------------- */
const { httpMethod } = require('../../../utils/constants/defaults')
const { POST } = httpMethod
const basepath = '/image'
const IMAGE_UPLOAD = 'image.upload'
const Actions = {
IMAGE_UPLOAD,
}
module.exports = {
Actions,
Commands: {
[IMAGE_UPLOAD]: {
path: `${basepath}/upload`,
httpMethod: POST,
auth: true,
params: {
files: {
from: 'files',
},
},
},
},
}

View File

@ -36,6 +36,7 @@ const routes = [
'2fa',
'auth',
'files',
'image',
'marketapp',
'oneflow',
'vcenter',