diff --git a/src/fireedge/src/client/apps/sunstone/_app.js b/src/fireedge/src/client/apps/sunstone/_app.js
index 4a0aa7ef5e..2439c2baed 100644
--- a/src/fireedge/src/client/apps/sunstone/_app.js
+++ b/src/fireedge/src/client/apps/sunstone/_app.js
@@ -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 = () => {
<>
+
>
)}
diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js
index 65b05186f8..2193575ffb 100644
--- a/src/fireedge/src/client/apps/sunstone/routesOne.js
+++ b/src/fireedge/src/client/apps/sunstone/routesOne.js
@@ -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,
diff --git a/src/fireedge/src/client/components/Cards/ImageCreateCard.js b/src/fireedge/src/client/components/Cards/ImageCreateCard.js
new file mode 100644
index 0000000000..e6020862be
--- /dev/null
+++ b/src/fireedge/src/client/components/Cards/ImageCreateCard.js
@@ -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 (
+
+
+ {Icon && (
+
+
+
+ )}
+
+
+
+
+ )
+ }
+)
+
+ImageCreateCard.propTypes = {
+ name: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+ Icon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
+}
+
+ImageCreateCard.displayName = 'ImageCreateCard'
+
+export default ImageCreateCard
diff --git a/src/fireedge/src/client/components/Cards/ImageSnapshotCard.js b/src/fireedge/src/client/components/Cards/ImageSnapshotCard.js
new file mode 100644
index 0000000000..23081faaf7
--- /dev/null
+++ b/src/fireedge/src/client/components/Cards/ImageSnapshotCard.js
@@ -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 (
+
+
+
+
+ {NAME}
+
+
+ {isActive && } />}
+ } />
+
+
+
+ {`#${ID} ${timeAgo}`}
+
+
+ {` ${sizeInfo}`}
+
+
+
+ {typeof actions === 'function' && (
+ {actions({ snapshot })}
+ )}
+
+ )
+ }
+)
+
+ImageSnapshotCard.propTypes = {
+ snapshot: PropTypes.object.isRequired,
+ actions: PropTypes.func,
+}
+
+ImageSnapshotCard.displayName = 'ImageSnapshotCard'
+
+export default ImageSnapshotCard
diff --git a/src/fireedge/src/client/components/Cards/index.js b/src/fireedge/src/client/components/Cards/index.js
index 9f341cff7d..96143519fc 100644
--- a/src/fireedge/src/client/components/Cards/index.js
+++ b/src/fireedge/src/client/components/Cards/index.js
@@ -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,
diff --git a/src/fireedge/src/client/components/FormControl/DockerfileController.js b/src/fireedge/src/client/components/FormControl/DockerfileController.js
new file mode 100644
index 0000000000..34ae5acab6
--- /dev/null
+++ b/src/fireedge/src/client/components/FormControl/DockerfileController.js
@@ -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 (
+
+ {
+ setValue(name, value)
+ }}
+ onFocus={(e) => {
+ setInternalError()
+ }}
+ />
+ {internalError && }
+
+ )
+ },
+ (prevProps, nextProps) => prevProps.cy === nextProps.cy
+)
+
+DockerfileController.propTypes = {
+ control: PropTypes.object,
+ cy: PropTypes.string,
+ name: PropTypes.string.isRequired,
+}
+
+DockerfileController.displayName = 'DockerfileController'
+
+export default DockerfileController
diff --git a/src/fireedge/src/client/components/FormControl/InputCode.js b/src/fireedge/src/client/components/FormControl/InputCode.js
index 4901b742cf..16ec493568 100644
--- a/src/fireedge/src/client/components/FormControl/InputCode.js
+++ b/src/fireedge/src/client/components/FormControl/InputCode.js
@@ -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)
+ })
}
}, [])
diff --git a/src/fireedge/src/client/components/FormControl/index.js b/src/fireedge/src/client/components/FormControl/index.js
index 7c1586255d..f9545d28d0 100644
--- a/src/fireedge/src/client/components/FormControl/index.js
+++ b/src/fireedge/src/client/components/FormControl/index.js
@@ -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,
}
diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js
index ef1d5d8839..e657374ff3 100644
--- a/src/fireedge/src/client/components/Forms/FormWithSchema.js
+++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js
@@ -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,
}
/**
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/index.js
new file mode 100644
index 0000000000..a1e8cd716a
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/index.js
@@ -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) => (
+ 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
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema.js
new file mode 100644
index 0000000000..25397f69ce
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema.js
@@ -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)))
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/index.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/index.js
new file mode 100644
index 0000000000..ecb0a1149c
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/index.js
@@ -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 (
+ 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
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema.js
new file mode 100644
index 0000000000..2432cb8c5e
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema.js
@@ -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(() => [])
diff --git a/src/fireedge/src/client/components/Forms/Image/CloneForm/index.js b/src/fireedge/src/client/components/Forms/Image/CloneForm/index.js
new file mode 100644
index 0000000000..0c6567bf1b
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CloneForm/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/index.js
new file mode 100644
index 0000000000..40bc07c35f
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/index.js
@@ -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 = () =>
+
+/**
+ * Docker file.
+ *
+ * @returns {object} Dockerfile step
+ */
+const DockerFile = () => ({
+ id: STEP_ID,
+ label: T.Dockerfile,
+ resolver: SCHEMA,
+ optionsValidate: { abortEarly: false },
+ content: Content,
+})
+
+export default DockerFile
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema.js
new file mode 100644
index 0000000000..953d742caf
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/index.js
new file mode 100644
index 0000000000..5daba02baa
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/index.js
@@ -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 = () =>
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/schema.js
new file mode 100644
index 0000000000..882a729afa
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/General/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/index.js
new file mode 100644
index 0000000000..220565fa04
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/Steps/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/index.js b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/index.js
new file mode 100644
index 0000000000..e89c034c82
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateDockerfile/index.js
@@ -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}&context=${general.CONTEXT}&size=${general.SIZE}`,
+ },
+ ...generalData,
+ }),
+ datastore: datastore?.ID,
+ }
+ },
+})
+
+export default Steps
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/index.js
new file mode 100644
index 0000000000..9ee4cf03b1
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/index.js
@@ -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 = () =>
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema.js
new file mode 100644
index 0000000000..0008d37980
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/CustomAttributes.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/CustomAttributes.js
new file mode 100644
index 0000000000..bfb414ad93
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/CustomAttributes.js
@@ -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 (
+
+
+
+ )
+}
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/index.js
new file mode 100644
index 0000000000..c97b358039
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/index.js
@@ -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 = () =>
+
+/**
+ * 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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/schema.js
new file mode 100644
index 0000000000..f3bfa22bfa
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/General/schema.js
@@ -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))
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/index.js
new file mode 100644
index 0000000000..40e5be8d62
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/Steps/index.js
@@ -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
diff --git a/src/fireedge/src/client/components/Forms/Image/CreateForm/index.js b/src/fireedge/src/client/components/Forms/Image/CreateForm/index.js
new file mode 100644
index 0000000000..abc8b4f240
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/CreateForm/index.js
@@ -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'
diff --git a/src/fireedge/src/client/components/Forms/Image/index.js b/src/fireedge/src/client/components/Forms/Image/index.js
new file mode 100644
index 0000000000..94153475b7
--- /dev/null
+++ b/src/fireedge/src/client/components/Forms/Image/index.js
@@ -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 }
diff --git a/src/fireedge/src/client/components/Notifier/upload.js b/src/fireedge/src/client/components/Notifier/upload.js
new file mode 100644
index 0000000000..a8bbc59f43
--- /dev/null
+++ b/src/fireedge/src/client/components/Notifier/upload.js
@@ -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 (
+ 0}
+ autoHideDuration={10000}
+ onClose={handleClose}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'right',
+ }}
+ >
+
+
+
+
+
+ {`${upload}%`}
+
+
+
+
+
+ )
+}
+
+export default NotifierUpload
diff --git a/src/fireedge/src/client/components/Tables/Images/actions.js b/src/fireedge/src/client/components/Tables/Images/actions.js
new file mode 100644
index 0000000000..72794d067c
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/Images/actions.js
@@ -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 (
+
+ {`#${ID} ${NAME}`}
+
+ )
+ })
+
+const SubHeader = (rows) =>
+
+const MessageToConfirmAction = (rows) => (
+ <>
+
+
+ >
+)
+
+/**
+ * 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: () => (
+
+ history.push(PATH.STORAGE.IMAGES.CREATE)}
+ />
+ {resourcesView?.dockerfile_dialog && (
+
+ history.push(PATH.STORAGE.IMAGES.DOCKERFILE)
+ }
+ />
+ )}
+
+ ),
+ 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
diff --git a/src/fireedge/src/client/components/Tables/Vms/index.js b/src/fireedge/src/client/components/Tables/Vms/index.js
index 8c0abf60d9..a6ee0f797a 100644
--- a/src/fireedge/src/client/components/Tables/Vms/index.js
+++ b/src/fireedge/src/client/components/Tables/Vms/index.js
@@ -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) ?? [],
}),
})
diff --git a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
index 71fe2fb7b5..1fd41804ff 100644
--- a/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
+++ b/src/fireedge/src/client/components/Tabs/Common/AttributePanel.js
@@ -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 *
* *
diff --git a/src/fireedge/src/client/components/Tabs/EmptyTab/index.js b/src/fireedge/src/client/components/Tabs/EmptyTab/index.js
index bb5e8a7a70..fc575eb577 100644
--- a/src/fireedge/src/client/components/Tabs/EmptyTab/index.js
+++ b/src/fireedge/src/client/components/Tabs/EmptyTab/index.js
@@ -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 (
-
+
)
}
-
+EmptyTab.propTypes = {
+ label: PropTypes.string,
+}
EmptyTab.displayName = 'EmptyTab'
export default EmptyTab
diff --git a/src/fireedge/src/client/components/Tabs/Image/Snapshots/Actions.js b/src/fireedge/src/client/components/Tabs/Image/Snapshots/Actions.js
new file mode 100644
index 0000000000..cd6982ce99
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Image/Snapshots/Actions.js
@@ -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 (
+
+ ),
+ children: (
+ <>
+ {Tr(T.DeleteOtherSnapshots)}
+ {Tr(T.DoYouWantProceed)}
+ >
+ ),
+ },
+ 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 (
+ ,
+ tooltip: Tr(T.Revert),
+ }}
+ options={[
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: (
+
+ ),
+ children: {Tr(T.DoYouWantProceed)}
,
+ },
+ 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 (
+ ,
+ tooltip: Tr(T.Delete),
+ }}
+ options={[
+ {
+ isConfirmDialog: true,
+ dialogProps: {
+ title: (
+
+ ),
+ children: {Tr(T.DoYouWantProceed)}
,
+ },
+ 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 }
diff --git a/src/fireedge/src/client/components/Tabs/Image/Snapshots/index.js b/src/fireedge/src/client/components/Tabs/Image/Snapshots/index.js
new file mode 100644
index 0000000000..0da46613d1
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Image/Snapshots/index.js
@@ -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 (
+
+
+ {snapshots.length ? (
+ snapshots?.map?.((snapshot) => (
+ (
+ <>
+ {actions.snapshot_flatten && (
+
+ )}
+ {actions.snapshot_revert && (
+
+ )}
+ {actions.snapshot_delete && (
+
+ )}
+ >
+ )}
+ />
+ ))
+ ) : (
+
+ )}
+
+
+ )
+}
+
+ImageStorageTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+ImageStorageTab.displayName = 'ImageStorageTab'
+
+export default ImageStorageTab
diff --git a/src/fireedge/src/client/components/Tabs/Image/Vms/index.js b/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
new file mode 100644
index 0000000000..9d3c5ae441
--- /dev/null
+++ b/src/fireedge/src/client/components/Tabs/Image/Vms/index.js
@@ -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 (
+ handleRowClick(row.ID)}
+ noDataMessage={}
+ />
+ )
+}
+
+VmsTab.propTypes = {
+ tabProps: PropTypes.object,
+ id: PropTypes.string,
+}
+
+VmsTab.displayName = 'VmsTab'
+
+export default VmsTab
diff --git a/src/fireedge/src/client/components/Tabs/Image/index.js b/src/fireedge/src/client/components/Tabs/Image/index.js
index 7631e21fb2..0bed8794b3 100644
--- a/src/fireedge/src/client/components/Tabs/Image/index.js
+++ b/src/fireedge/src/client/components/Tabs/Image/index.js
@@ -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 }) => {
diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js
index e7f4edccdd..a7c3c9ead0 100644
--- a/src/fireedge/src/client/constants/image.js
+++ b/src/fireedge/src/client/constants/image.js
@@ -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',
}
diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js
index 368c435ff5..1a8ca7a918 100644
--- a/src/fireedge/src/client/constants/index.js
+++ b/src/fireedge/src/client/constants/index.js
@@ -128,6 +128,7 @@ export const INPUT_TYPES = {
TEXT: 'text',
TABLE: 'table',
TOGGLE: 'toggle',
+ DOCKERFILE: 'dockerfile',
}
export const DEBUG_LEVEL = {
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index ddf505bd14..60593a2bda 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -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',
diff --git a/src/fireedge/src/client/containers/Images/Create.js b/src/fireedge/src/client/containers/Images/Create.js
new file mode 100644
index 0000000000..cd6d89e31c
--- /dev/null
+++ b/src/fireedge/src/client/containers/Images/Create.js
@@ -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 (
+ }>
+ {(config) => }
+
+ )
+}
+
+export default CreateImage
diff --git a/src/fireedge/src/client/containers/Images/Dockerfile.js b/src/fireedge/src/client/containers/Images/Dockerfile.js
new file mode 100644
index 0000000000..cd598aa097
--- /dev/null
+++ b/src/fireedge/src/client/containers/Images/Dockerfile.js
@@ -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 (
+ }>
+ {(config) => }
+
+ )
+}
+
+export default CreateDockerfile
diff --git a/src/fireedge/src/client/containers/Images/index.js b/src/fireedge/src/client/containers/Images/index.js
index f1086c51d2..f66bdb6819 100644
--- a/src/fireedge/src/client/containers/Images/index.js
+++ b/src/fireedge/src/client/containers/Images/index.js
@@ -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() {
{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 (
-
-
- {`#${id} | ${name}`}
-
-
- {/* -- ACTIONS -- */}
+
}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
- onClick={() => get({ id })}
+ onClick={() => getImage({ id })}
/>
{typeof gotoPage === 'function' && (
{
onClick={() => unselect()}
/>
)}
- {/* -- END ACTIONS -- */}
+
+ {`#${id} | ${name}`}
+
-
+
)
})
diff --git a/src/fireedge/src/client/features/General/actions.js b/src/fireedge/src/client/features/General/actions.js
index 2125ca9877..49420a90e5 100644
--- a/src/fireedge/src/client/features/General/actions.js
+++ b/src/fireedge/src/client/features/General/actions.js
@@ -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',
diff --git a/src/fireedge/src/client/features/General/hooks.js b/src/fireedge/src/client/features/General/hooks.js
index 6948cab549..c3d3031928 100644
--- a/src/fireedge/src/client/features/General/hooks.js
+++ b/src/fireedge/src/client/features/General/hooks.js
@@ -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 })),
diff --git a/src/fireedge/src/client/features/General/slice.js b/src/fireedge/src/client/features/General/slice.js
index 6be92feac6..7422be930e 100644
--- a/src/fireedge/src/client/features/General/slice.js
+++ b/src/fireedge/src/client/features/General/slice.js
@@ -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
diff --git a/src/fireedge/src/client/features/OneApi/image.js b/src/fireedge/src/client/features/OneApi/image.js
index c626552eed..74801138da 100644
--- a/src/fireedge/src/client/features/OneApi/image.js
+++ b/src/fireedge/src/client/features/OneApi/image.js
@@ -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
diff --git a/src/fireedge/src/client/features/OneApi/system.js b/src/fireedge/src/client/features/OneApi/system.js
index 8cc6e5402b..6e797bc9a3 100644
--- a/src/fireedge/src/client/features/OneApi/system.js
+++ b/src/fireedge/src/client/features/OneApi/system.js
@@ -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
diff --git a/src/fireedge/src/client/models/Image.js b/src/fireedge/src/client/models/Image.js
index 09b58a91b1..95f9e5cddf 100644
--- a/src/fireedge/src/client/models/Image.js
+++ b/src/fireedge/src/client/models/Image.js
@@ -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)
+}
diff --git a/src/fireedge/src/server/routes/api/files/functions.js b/src/fireedge/src/server/routes/api/files/functions.js
index 92f3de800e..a18fc7464f 100644
--- a/src/fireedge/src/server/routes/api/files/functions.js
+++ b/src/fireedge/src/server/routes/api/files/functions.js
@@ -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()
+ }
+ )
}
/**
diff --git a/src/fireedge/src/server/routes/api/image/functions.js b/src/fireedge/src/server/routes/api/image/functions.js
new file mode 100644
index 0000000000..4cca70821c
--- /dev/null
+++ b/src/fireedge/src/server/routes/api/image/functions.js
@@ -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
diff --git a/src/fireedge/src/server/routes/api/image/index.js b/src/fireedge/src/server/routes/api/image/index.js
new file mode 100644
index 0000000000..50829ea03e
--- /dev/null
+++ b/src/fireedge/src/server/routes/api/image/index.js
@@ -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,
+ },
+]
diff --git a/src/fireedge/src/server/routes/api/image/routes.js b/src/fireedge/src/server/routes/api/image/routes.js
new file mode 100644
index 0000000000..7d0d03f742
--- /dev/null
+++ b/src/fireedge/src/server/routes/api/image/routes.js
@@ -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',
+ },
+ },
+ },
+ },
+}
diff --git a/src/fireedge/src/server/routes/api/index.js b/src/fireedge/src/server/routes/api/index.js
index 471f431515..1edd3a6717 100644
--- a/src/fireedge/src/server/routes/api/index.js
+++ b/src/fireedge/src/server/routes/api/index.js
@@ -36,6 +36,7 @@ const routes = [
'2fa',
'auth',
'files',
+ 'image',
'marketapp',
'oneflow',
'vcenter',