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 && ( + + + + )} + +
+
+ + {name} + +
+
+
+
+ ) + } +) + +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',