diff --git a/src/fireedge/etc/sunstone/admin/vm-tab.yaml b/src/fireedge/etc/sunstone/admin/vm-tab.yaml index 83f73bbbec..573371b6c7 100644 --- a/src/fireedge/etc/sunstone/admin/vm-tab.yaml +++ b/src/fireedge/etc/sunstone/admin/vm-tab.yaml @@ -55,6 +55,7 @@ actions: ssh: true rdp: true edit_labels: true + backup: true # Filters - List of criteria to filter the resources @@ -71,7 +72,6 @@ filters: # Info Tabs - Which info tabs are used to show extended information info-tabs: - info: enabled: true information_panel: diff --git a/src/fireedge/package-lock.json b/src/fireedge/package-lock.json index e608ea0331..cb886af95a 100644 --- a/src/fireedge/package-lock.json +++ b/src/fireedge/package-lock.json @@ -57,7 +57,7 @@ "http": "0.0.1-security", "http-proxy-middleware": "1.0.5", "https": "1.0.0", - "iconoir-react": "4.7.1", + "iconoir-react": "5.3.2", "immutable": "4.0.0-rc.12", "intersection-observer": "0.11.0", "jsdom": "19.0.0", @@ -7858,11 +7858,11 @@ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, "node_modules/iconoir-react": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-4.7.1.tgz", - "integrity": "sha512-4QJr7qL4zU5Z7/wBSS0hUy/ThMRVqIuxInz79lPc1yIIdwsegqAKvPVfCeg1bHoFhgi5LbWB7pyx8/t/O9KHRQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-5.3.2.tgz", + "integrity": "sha512-5p5FQAkX6ZAuk1UE/4RmjI9BgI5FSqTbgyUT/m74bfHxl57YAuU7sRdnvrLe97I9IbbiC2uKnGl90xVcBD07rQ==", "peerDependencies": { - "react": "^16.8.6 || ^17" + "react": "^16.8.6 || ^17 || ^18" } }, "node_modules/iconv-lite": { @@ -9132,6 +9132,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dependencies": { "@babel/runtime": "^7.12.1", "tiny-warning": "^1.0.3" @@ -12723,6 +12724,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", "dependencies": { "browser-process-hrtime": "^1.0.0" } @@ -19155,9 +19157,9 @@ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, "iconoir-react": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-4.7.1.tgz", - "integrity": "sha512-4QJr7qL4zU5Z7/wBSS0hUy/ThMRVqIuxInz79lPc1yIIdwsegqAKvPVfCeg1bHoFhgi5LbWB7pyx8/t/O9KHRQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-5.3.2.tgz", + "integrity": "sha512-5p5FQAkX6ZAuk1UE/4RmjI9BgI5FSqTbgyUT/m74bfHxl57YAuU7sRdnvrLe97I9IbbiC2uKnGl90xVcBD07rQ==", "requires": {} }, "iconv-lite": { diff --git a/src/fireedge/package.json b/src/fireedge/package.json index 61c024c7c0..818f9f72ef 100644 --- a/src/fireedge/package.json +++ b/src/fireedge/package.json @@ -91,7 +91,7 @@ "http": "0.0.1-security", "http-proxy-middleware": "1.0.5", "https": "1.0.0", - "iconoir-react": "4.7.1", + "iconoir-react": "5.3.2", "immutable": "4.0.0-rc.12", "intersection-observer": "0.11.0", "jsdom": "19.0.0", diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 59ec758ea4..bb496c9969 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -38,6 +38,7 @@ import { User as UserIcon, Group as GroupIcon, HistoricShield as SecurityGroupIcon, + RefreshDouble as BackupIcon, } from 'iconoir-react' import loadable from '@loadable/component' @@ -117,6 +118,9 @@ const CreateSecurityGroups = loadable( ssr: false, } ) +const Backups = loadable(() => import('client/containers/Backups'), { + ssr: false, +}) const CreateImages = loadable(() => import('client/containers/Images/Create'), { ssr: false, }) @@ -229,6 +233,10 @@ export const PATH = { DETAIL: `/${RESOURCE_NAMES.FILE}/:id`, CREATE: `/${RESOURCE_NAMES.FILE}/create`, }, + BACKUPS: { + LIST: `/${RESOURCE_NAMES.BACKUP}`, + DETAIL: `/${RESOURCE_NAMES.BACKUP}/:id`, + }, MARKETPLACES: { LIST: `/${RESOURCE_NAMES.MARKETPLACE}`, DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`, @@ -426,6 +434,13 @@ const ENDPOINTS = [ path: PATH.STORAGE.IMAGES.DOCKERFILE, Component: CreateDockerfile, }, + { + title: T.Backups, + path: PATH.STORAGE.BACKUPS.LIST, + sidebar: true, + icon: BackupIcon, + Component: Backups, + }, { title: T.Marketplaces, path: PATH.STORAGE.MARKETPLACES.LIST, diff --git a/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/BasicConfiguration/index.js b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/BasicConfiguration/index.js new file mode 100644 index 0000000000..0a25940697 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/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/Backup/RestoreForm/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/Backup/RestoreForm/Steps/BasicConfiguration/schema.js b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/BasicConfiguration/schema.js new file mode 100644 index 0000000000..1250cbbf48 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/BasicConfiguration/schema.js @@ -0,0 +1,46 @@ +/* ------------------------------------------------------------------------- * + * 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 { boolean, object, ObjectSchema } from 'yup' +import { Field, getValidationFromFields } from 'client/utils' +import { T, INPUT_TYPES } from 'client/constants' + +const NO_NIC = { + name: 'no_nic', + label: T.DoNotRestoreNICAttributes, + type: INPUT_TYPES.SWITCH, + validation: boolean().yesOrNo(), + grid: { xs: 12, md: 6 }, +} + +const NO_IP = { + name: 'no_ip', + label: T.DoNotRestoreIPAttributes, + type: INPUT_TYPES.SWITCH, + validation: boolean().yesOrNo(), + grid: { xs: 12, md: 6 }, +} + +/** + * @returns {Field[]} Fields + */ +export const FIELDS = () => [NO_NIC, NO_IP] + +/** + * @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/Backup/RestoreForm/Steps/DatastoresTable/index.js b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/DatastoresTable/index.js new file mode 100644 index 0000000000..f87512ea5b --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/DatastoresTable/index.js @@ -0,0 +1,72 @@ +/* ------------------------------------------------------------------------- * + * 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 PropTypes from 'prop-types' +import { useFormContext } from 'react-hook-form' + +import { DatastoresTable } from 'client/components/Tables' +import { SCHEMA } from 'client/components/Forms/Backup/RestoreForm/Steps/DatastoresTable/schema' + +import { Step } 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 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: '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/Backup/RestoreForm/Steps/DatastoresTable/schema.js b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/Steps/DatastoresTable/schema.js new file mode 100644 index 0000000000..2432cb8c5e --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/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/Backup/RestoreForm/index.js b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/index.js new file mode 100644 index 0000000000..da24923e09 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Backup/RestoreForm/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/Backup/RestoreForm/Steps/BasicConfiguration' +import DatastoresTable, { + STEP_ID as DATASTORE_ID, +} from 'client/components/Forms/Backup/RestoreForm/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/Backup/index.js b/src/fireedge/src/client/components/Forms/Backup/index.js new file mode 100644 index 0000000000..fc98bce546 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Backup/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. * + * ------------------------------------------------------------------------- */ +import { ReactElement } from 'react' +import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC' +import { CreateFormCallback } from 'client/utils/schema' + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form + */ +const RestoreForm = (configProps) => + AsyncLoadForm({ formPath: 'Backup/RestoreForm' }, configProps) + +export { RestoreForm } diff --git a/src/fireedge/src/client/components/Forms/Vm/BackupForm/index.js b/src/fireedge/src/client/components/Forms/Vm/BackupForm/index.js new file mode 100644 index 0000000000..366b6956ba --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/BackupForm/index.js @@ -0,0 +1,21 @@ +/* ------------------------------------------------------------------------- * + * 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 { SCHEMA, FIELDS } from 'client/components/Forms/Vm/BackupForm/schema' +import { createForm } from 'client/utils' + +const BackupForm = createForm(SCHEMA, FIELDS) + +export default BackupForm diff --git a/src/fireedge/src/client/components/Forms/Vm/BackupForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/BackupForm/schema.js new file mode 100644 index 0000000000..e5e8780bae --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/BackupForm/schema.js @@ -0,0 +1,44 @@ +/* ------------------------------------------------------------------------- * + * 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 { number, object } from 'yup' +import { getValidationFromFields, arrayToOptions } from 'client/utils' +import { T, INPUT_TYPES } from 'client/constants' +import { useGetDatastoresQuery } from 'client/features/OneApi/datastore' + +const DS_ID = { + name: 'dsId', + label: T.BackupDatastore, + type: INPUT_TYPES.SELECT, + values: () => { + const { data: datastores = [] } = useGetDatastoresQuery() + + return arrayToOptions( + datastores.filter(({ TEMPLATE }) => TEMPLATE.TYPE === 'BACKUP_DS'), + { + getText: ({ NAME, ID } = {}) => `${ID}: ${NAME}`, + getValue: ({ ID } = {}) => ID, + } + ) + }, + validation: number() + .positive() + .required() + .default(() => undefined), +} + +export const FIELDS = [DS_ID] + +export const SCHEMA = object(getValidationFromFields(FIELDS)) diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/fields.js b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/fields.js index 2fb8444d78..fc3a81db43 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/fields.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/fields.js @@ -38,6 +38,7 @@ import { dateToMilliseconds, } from 'client/models/Helper' import { getSnapshotList, getDisks } from 'client/models/VirtualMachine' +import { useGetDatastoresQuery } from 'client/features/OneApi/datastore' // -------------------------------------------------------- // Constants @@ -113,6 +114,26 @@ const ACTION_FIELD_FOR_CHARTERS = { }), } +/** + * @returns {Field} Datastore id field + */ +const ARGS_DS_ID_FIELD = { + ...createArgField(ARGS_TYPES.DS_ID), + label: T.BackupDatastore, + type: INPUT_TYPES.SELECT, + values: () => { + const { data: datastores = [] } = useGetDatastoresQuery() + + return arrayToOptions( + datastores.filter(({ TEMPLATE }) => TEMPLATE.TYPE === 'BACKUP_DS'), + { + getText: ({ NAME, ID } = {}) => `${ID}: ${NAME}`, + getValue: ({ ID } = {}) => ID, + } + ) + }, +} + /** * @param {object} vm - Vm resource * @returns {Field} Disk id field @@ -462,6 +483,7 @@ export const PUNCTUAL_FIELDS = { ARGS_NAME_FIELD, ARGS_DISK_ID_FIELD, ARGS_SNAPSHOT_ID_FIELD, + ARGS_DS_ID_FIELD, PERIODIC_FIELD, REPEAT_FIELD, WEEKLY_FIELD, diff --git a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/schema.js index c645e28b6a..6cf5dda5e2 100644 --- a/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/schema.js +++ b/src/fireedge/src/client/components/Forms/Vm/CreateSchedActionForm/schema.js @@ -34,6 +34,7 @@ const ARG_SCHEMAS = { [ARGS_TYPES.DISK_ID]: ARG_SCHEMA, [ARGS_TYPES.NAME]: ARG_SCHEMA, [ARGS_TYPES.SNAPSHOT_ID]: ARG_SCHEMA, + [ARGS_TYPES.DS_ID]: ARG_SCHEMA, } /** @@ -44,6 +45,7 @@ const COMMON_FIELDS = (vm) => [ PUNCTUAL_FIELDS.ARGS_NAME_FIELD, PUNCTUAL_FIELDS.ARGS_DISK_ID_FIELD(vm), PUNCTUAL_FIELDS.ARGS_SNAPSHOT_ID_FIELD(vm), + PUNCTUAL_FIELDS.ARGS_DS_ID_FIELD, PUNCTUAL_FIELDS.PERIODIC_FIELD, PUNCTUAL_FIELDS.REPEAT_FIELD, PUNCTUAL_FIELDS.WEEKLY_FIELD, diff --git a/src/fireedge/src/client/components/Forms/Vm/index.js b/src/fireedge/src/client/components/Forms/Vm/index.js index 8fc5091bf3..832313378d 100644 --- a/src/fireedge/src/client/components/Forms/Vm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/index.js @@ -156,9 +156,17 @@ const CreateRelativeCharterForm = (configProps) => const UpdateConfigurationForm = (configProps) => AsyncLoadForm({ formPath: 'Vm/UpdateConfigurationForm' }, configProps) +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateFormCallback} Asynchronous loaded form + */ +const BackupForm = (configProps) => + AsyncLoadForm({ formPath: 'Vm/BackupForm' }, configProps) + export { AttachNicForm, AttachSecGroupForm, + BackupForm, ChangeGroupForm, ChangeUserForm, CreateCharterForm, diff --git a/src/fireedge/src/client/components/Tables/Backups/actions.js b/src/fireedge/src/client/components/Tables/Backups/actions.js new file mode 100644 index 0000000000..26bbda63dc --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Backups/actions.js @@ -0,0 +1,189 @@ +/* ------------------------------------------------------------------------- * + * 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 { Typography } from '@mui/material' +import { Group, Trash } from 'iconoir-react' +import { useMemo } from 'react' + +import { useViews } from 'client/features/Auth' +import { + useChangeImageOwnershipMutation, + useRemoveImageMutation, + useRestoreBackupMutation, +} from 'client/features/OneApi/image' + +import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm' +import { RestoreForm } from 'client/components/Forms/Backup' +import { + createActions, + GlobalAction, +} from 'client/components/Tables/Enhanced/Utils' + +import { Translate } from 'client/components/HOC' +import { IMAGE_ACTIONS, RESOURCE_NAMES, T } from 'client/constants' +import { isAvailableAction } from 'client/models/VirtualMachine' + +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 { view, getResourceView } = useViews() + const [changeOwnership] = useChangeImageOwnershipMutation() + const [restoreBackup] = useRestoreBackupMutation() + const [deleteImage] = useRemoveImageMutation() + + const resourcesView = getResourceView(RESOURCE_NAMES.BACKUP)?.actions + + const imageActions = useMemo( + () => + createActions({ + filters: resourcesView, + actions: [ + { + accessor: IMAGE_ACTIONS.RESTORE, + color: 'secondary', + dataCy: `image-${IMAGE_ACTIONS.RESTORE}`, + label: T.Restore, + tooltip: T.Restore, + selected: { min: 1 }, + options: [ + { + dialogProps: { + title: T.SelectCluster, + dataCy: 'modal-select-cluster', + }, + form: (rows) => RestoreForm(), + onSubmit: (rows) => async (formData) => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all( + ids.map((id) => + restoreBackup({ + id: id, + datastore: formData.datastore, + options: `NO_IP="${formData.no_ip}"\nNO_NIC="${formData.no_nic}"`, + }) + ) + ) + }, + }, + ], + }, + { + 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/Backups/columns.js b/src/fireedge/src/client/components/Tables/Backups/columns.js new file mode 100644 index 0000000000..6520cc6ea0 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Backups/columns.js @@ -0,0 +1,66 @@ +/* ------------------------------------------------------------------------- * + * 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 { CategoryFilter } from 'client/components/Tables/Enhanced/Utils' +import * as ImageModel from 'client/models/Image' + +const getTotalOfResources = (resources) => + [resources?.ID ?? []].flat().length || 0 + +export default [ + { Header: 'ID', accessor: 'ID', sortType: 'number' }, + { Header: 'Name', accessor: 'NAME' }, + { Header: 'Owner', accessor: 'UNAME' }, + { Header: 'Group', accessor: 'GNAME' }, + { Header: 'Locked', id: 'locked', accessor: 'LOCK' }, + { + Header: 'State', + id: 'STATE', + accessor: (row) => ImageModel.getState(row)?.name, + disableFilters: false, + Filter: ({ column }) => + CategoryFilter({ + column, + multiple: true, + title: 'State', + }), + filter: 'includesValue', + }, + { + Header: 'Type', + id: 'TYPE', + accessor: (row) => ImageModel.getType(row), + }, + { + Header: 'Disk Type', + id: 'DISK_TYPE', + accessor: (row) => ImageModel.getDiskType(row), + }, + { Header: 'Registration Time', accessor: 'REGTIME' }, + { Header: 'Datastore', accessor: 'DATASTORE' }, + { Header: 'Persistent', accessor: 'PERSISTENT' }, + { + Header: 'Running VMs', + accessor: 'RUNNING_VMS', + sortType: 'number', + }, + { + Header: 'Total VMs', + id: 'TOTAL_VMS', + accessor: (row) => getTotalOfResources(row?.VMS), + sortType: 'number', + }, +] diff --git a/src/fireedge/src/client/components/Tables/Backups/index.js b/src/fireedge/src/client/components/Tables/Backups/index.js new file mode 100644 index 0000000000..8d121c084d --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Backups/index.js @@ -0,0 +1,83 @@ +/* ------------------------------------------------------------------------- * + * 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, ReactElement } from 'react' + +import { useViews } from 'client/features/Auth' +import { useGetBackupsQuery } from 'client/features/OneApi/image' + +import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced' +import backupColumns from 'client/components/Tables/Backups/columns' +import BackupRow from 'client/components/Tables/Backups/row' +import { RESOURCE_NAMES } from 'client/constants' + +const DEFAULT_DATA_CY = 'backups' + +/** + * @param {object} props - Props + * @returns {ReactElement} Backups table + */ +const BackupsTable = (props) => { + const { rootProps = {}, searchProps = {}, vm, ...rest } = props ?? {} + rootProps['data-cy'] ??= DEFAULT_DATA_CY + searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` + + const { view, getResourceView } = useViews() + const { + data = [], + isFetching, + refetch, + } = useGetBackupsQuery(undefined, { + selectFromResult: (result) => { + const backupsIds = vm?.BACKUPS?.BACKUP_IDS?.ID + ? Array.isArray(vm?.BACKUPS?.BACKUP_IDS?.ID) + ? vm?.BACKUPS?.BACKUP_IDS?.ID + : [vm?.BACKUPS?.BACKUP_IDS?.ID] + : [] + + return { + ...result, + data: result?.data?.filter((backup) => backupsIds?.includes(backup.ID)), + } + }, + }) + + const columns = useMemo( + () => + createColumns({ + filters: getResourceView(RESOURCE_NAMES.BACKUP)?.filters, + columns: backupColumns, + }), + [view] + ) + + return ( + data, [data])} + rootProps={rootProps} + searchProps={searchProps} + refetch={refetch} + isLoading={isFetching} + getRowId={(row) => String(row.ID)} + RowComponent={BackupRow} + {...rest} + /> + ) +} + +BackupsTable.displayName = 'BackupsTable' + +export default BackupsTable diff --git a/src/fireedge/src/client/components/Tables/Backups/row.js b/src/fireedge/src/client/components/Tables/Backups/row.js new file mode 100644 index 0000000000..6eaee676db --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Backups/row.js @@ -0,0 +1,131 @@ +/* ------------------------------------------------------------------------- * + * 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 { + Lock, + User, + Group, + Db as DatastoreIcon, + ModernTv, + Pin as PersistentIcon, + Archive as DiskTypeIcon, +} from 'iconoir-react' +import { Typography } from '@mui/material' + +import Timer from 'client/components/Timer' +import { StatusCircle, StatusChip } from 'client/components/Status' +import { rowStyles } from 'client/components/Tables/styles' +import { T } from 'client/constants' + +import * as ImageModel from 'client/models/Image' +import * as Helper from 'client/models/Helper' + +const Row = ({ original, value, ...props }) => { + const classes = rowStyles() + const { + ID, + NAME, + UNAME, + GNAME, + REGTIME, + TYPE, + DISK_TYPE, + PERSISTENT, + locked, + DATASTORE, + TOTAL_VMS, + RUNNING_VMS, + } = value + + const labels = [...new Set([TYPE])].filter(Boolean) + + const { color: stateColor, name: stateName } = ImageModel.getState(original) + + const time = Helper.timeFromMilliseconds(+REGTIME) + + return ( +
+
+
+ + + {NAME} + + {locked && } + + {labels.map((label) => ( + + ))} + +
+
+ {`${ID}`} + + + + + + {` ${UNAME}`} + + + + {` ${GNAME}`} + + + + {` ${DATASTORE}`} + + + + + {PERSISTENT + ? T.Persistent.toLowerCase() + : T.NonPersistent.toLowerCase()} + + + + + {` ${DISK_TYPE.toLowerCase()}`} + + + + {` ${RUNNING_VMS} / ${TOTAL_VMS}`} + +
+
+
+
+ ) +} + +Row.propTypes = { + original: PropTypes.object, + value: PropTypes.object, + isSelected: PropTypes.bool, + handleClick: PropTypes.func, +} + +export default Row diff --git a/src/fireedge/src/client/components/Tables/Files/row.js b/src/fireedge/src/client/components/Tables/Files/row.js index c760f48e98..6eaee676db 100644 --- a/src/fireedge/src/client/components/Tables/Files/row.js +++ b/src/fireedge/src/client/components/Tables/Files/row.js @@ -16,11 +16,21 @@ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' -import { Lock, User, Group, Folder, ModernTv } from 'iconoir-react' +import { + Lock, + User, + Group, + Db as DatastoreIcon, + ModernTv, + Pin as PersistentIcon, + Archive as DiskTypeIcon, +} from 'iconoir-react' import { Typography } from '@mui/material' +import Timer from 'client/components/Timer' import { StatusCircle, StatusChip } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' +import { T } from 'client/constants' import * as ImageModel from 'client/models/Image' import * as Helper from 'client/models/Helper' @@ -42,14 +52,11 @@ const Row = ({ original, value, ...props }) => { RUNNING_VMS, } = value - const labels = [ - ...new Set([PERSISTENT && 'PERSISTENT', TYPE, DISK_TYPE]), - ].filter(Boolean) + const labels = [...new Set([TYPE])].filter(Boolean) const { color: stateColor, name: stateName } = ImageModel.getState(original) const time = Helper.timeFromMilliseconds(+REGTIME) - const timeAgo = `registered ${time.toRelative()}` return (
@@ -67,20 +74,43 @@ const Row = ({ original, value, ...props }) => {
- {`#${ID} ${timeAgo}`} - + {`${ID}`} + + + + {` ${UNAME}`} - + {` ${GNAME}`} - - + + {` ${DATASTORE}`} - + + + + {PERSISTENT + ? T.Persistent.toLowerCase() + : T.NonPersistent.toLowerCase()} + + + + + {` ${DISK_TYPE.toLowerCase()}`} + + {` ${RUNNING_VMS} / ${TOTAL_VMS}`} diff --git a/src/fireedge/src/client/components/Tables/Images/row.js b/src/fireedge/src/client/components/Tables/Images/row.js index c760f48e98..6eaee676db 100644 --- a/src/fireedge/src/client/components/Tables/Images/row.js +++ b/src/fireedge/src/client/components/Tables/Images/row.js @@ -16,11 +16,21 @@ /* eslint-disable jsdoc/require-jsdoc */ import PropTypes from 'prop-types' -import { Lock, User, Group, Folder, ModernTv } from 'iconoir-react' +import { + Lock, + User, + Group, + Db as DatastoreIcon, + ModernTv, + Pin as PersistentIcon, + Archive as DiskTypeIcon, +} from 'iconoir-react' import { Typography } from '@mui/material' +import Timer from 'client/components/Timer' import { StatusCircle, StatusChip } from 'client/components/Status' import { rowStyles } from 'client/components/Tables/styles' +import { T } from 'client/constants' import * as ImageModel from 'client/models/Image' import * as Helper from 'client/models/Helper' @@ -42,14 +52,11 @@ const Row = ({ original, value, ...props }) => { RUNNING_VMS, } = value - const labels = [ - ...new Set([PERSISTENT && 'PERSISTENT', TYPE, DISK_TYPE]), - ].filter(Boolean) + const labels = [...new Set([TYPE])].filter(Boolean) const { color: stateColor, name: stateName } = ImageModel.getState(original) const time = Helper.timeFromMilliseconds(+REGTIME) - const timeAgo = `registered ${time.toRelative()}` return (
@@ -67,20 +74,43 @@ const Row = ({ original, value, ...props }) => {
- {`#${ID} ${timeAgo}`} - + {`${ID}`} + + + + {` ${UNAME}`} - + {` ${GNAME}`} - - + + {` ${DATASTORE}`} - + + + + {PERSISTENT + ? T.Persistent.toLowerCase() + : T.NonPersistent.toLowerCase()} + + + + + {` ${DISK_TYPE.toLowerCase()}`} + + {` ${RUNNING_VMS} / ${TOTAL_VMS}`} diff --git a/src/fireedge/src/client/components/Tables/Vms/actions.js b/src/fireedge/src/client/components/Tables/Vms/actions.js index b33284ed88..e3e3ac408b 100644 --- a/src/fireedge/src/client/components/Tables/Vms/actions.js +++ b/src/fireedge/src/client/components/Tables/Vms/actions.js @@ -41,9 +41,11 @@ import { useMigrateMutation, useChangeVmOwnershipMutation, useRecoverMutation, + useBackupMutation, } from 'client/features/OneApi/vm' import { + BackupForm, RecoverForm, ChangeUserForm, ChangeGroupForm, @@ -121,6 +123,7 @@ const Actions = () => { const [saveAsTemplate] = useSaveAsTemplateMutation() const [actionVm] = useActionVmMutation() const [recover] = useRecoverMutation() + const [backup] = useBackupMutation() const [changeOwnership] = useChangeVmOwnershipMutation() const [deploy] = useDeployMutation() const [migrate] = useMigrateMutation() @@ -502,6 +505,23 @@ const Actions = () => { ) }, }, + { + accessor: VM_ACTIONS.BACKUP, + disabled: isDisabled(VM_ACTIONS.BACKUP), + name: T.Backup, + dialogProps: { + title: T.Backup, + subheader: SubHeader, + dataCy: `modal-${VM_ACTIONS.BACKUP}`, + }, + form: BackupForm, + onSubmit: (rows) => async (formData) => { + const ids = rows?.map?.(({ original }) => original?.ID) + await Promise.all( + ids.map((id) => backup({ id, ...formData })) + ) + }, + }, ], }, { diff --git a/src/fireedge/src/client/components/Tables/index.js b/src/fireedge/src/client/components/Tables/index.js index 2c03a493c9..3b8704718e 100644 --- a/src/fireedge/src/client/components/Tables/index.js +++ b/src/fireedge/src/client/components/Tables/index.js @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ +import BackupsTable from 'client/components/Tables/Backups' import ClustersTable from 'client/components/Tables/Clusters' import DatastoresTable from 'client/components/Tables/Datastores' import DockerHubTagsTable from 'client/components/Tables/DockerHubTags' @@ -41,6 +42,7 @@ export * from 'client/components/Tables/Enhanced/Utils' export { SkeletonTable, EnhancedTable, + BackupsTable, FilesTable, VirtualizedTable, ClustersTable, diff --git a/src/fireedge/src/client/components/Tabs/Backup/Info/index.js b/src/fireedge/src/client/components/Tabs/Backup/Info/index.js new file mode 100644 index 0000000000..932752efee --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Backup/Info/index.js @@ -0,0 +1,145 @@ +/* ------------------------------------------------------------------------- * + * 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, useCallback } from 'react' +import PropTypes from 'prop-types' +import { Stack } from '@mui/material' + +import { + useGetImageQuery, + useChangeImageOwnershipMutation, + useChangeImagePermissionsMutation, + useUpdateImageMutation, +} from 'client/features/OneApi/image' +import { + Permissions, + Ownership, + AttributePanel, +} from 'client/components/Tabs/Common' +import Information from 'client/components/Tabs/Backup/Info/information' + +import { Tr } from 'client/components/HOC' +import { T } from 'client/constants' +import { getActionsAvailable, jsonToXml } from 'client/models/Helper' +import { cloneObject, set } from 'client/utils' + +/** + * Renders mainly information tab. + * + * @param {object} props - Props + * @param {object} props.tabProps - Tab information + * @param {string} props.id - Image id + * @returns {ReactElement} Information tab + */ +const ImageInfoTab = ({ tabProps = {}, id }) => { + const { + information_panel: informationPanel, + permissions_panel: permissionsPanel, + ownership_panel: ownershipPanel, + attributes_panel: attributesPanel, + } = tabProps + + const [changeOwnership] = useChangeImageOwnershipMutation() + const [changePermissions] = useChangeImagePermissionsMutation() + const [update] = useUpdateImageMutation() + const { data: image } = useGetImageQuery({ id }) + + const { UNAME, UID, GNAME, GID, PERMISSIONS, TEMPLATE } = image + + const handleChangeOwnership = async (newOwnership) => { + await changeOwnership({ id, ...newOwnership }) + } + + const handleChangePermission = async (newPermission) => { + await changePermissions({ id, ...newPermission }) + } + + const handleAttributeInXml = async (path, newValue) => { + const newTemplate = cloneObject(TEMPLATE) + set(newTemplate, path, newValue) + + const xml = jsonToXml(newTemplate) + await update({ id, template: xml, replace: 0 }) + } + + const getActions = useCallback( + (actions) => getActionsAvailable(actions), + [getActionsAvailable] + ) + + const ATTRIBUTE_FUNCTION = { + handleAdd: handleAttributeInXml, + handleEdit: handleAttributeInXml, + handleDelete: handleAttributeInXml, + } + + return ( + + {informationPanel?.enabled && ( + + )} + {permissionsPanel?.enabled && ( + + )} + {ownershipPanel?.enabled && ( + + )} + {attributesPanel?.enabled && ( + + )} + + ) +} + +ImageInfoTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +ImageInfoTab.displayName = 'ImageInfoTab' + +export default ImageInfoTab diff --git a/src/fireedge/src/client/components/Tabs/Backup/Info/information.js b/src/fireedge/src/client/components/Tabs/Backup/Info/information.js new file mode 100644 index 0000000000..86a05e8767 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Backup/Info/information.js @@ -0,0 +1,163 @@ +/* ------------------------------------------------------------------------- * + * 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 { generatePath } from 'react-router-dom' + +import { + useRenameImageMutation, + useChangeImageTypeMutation, + usePersistentImageMutation, +} from 'client/features/OneApi/image' +import { StatusChip } from 'client/components/Status' +import { List } from 'client/components/Tabs/Common' + +import { getDiskType, getType, getState } from 'client/models/Image' +import { timeToString, booleanToString } from 'client/models/Helper' +import { arrayToOptions, prettyBytes } from 'client/utils' +import { T, Image, IMAGE_ACTIONS, IMAGE_TYPES } from 'client/constants' +import { PATH } from 'client/apps/sunstone/routesOne' + +/** + * Renders mainly information tab. + * + * @param {object} props - Props + * @param {Image} props.image - Image resource + * @param {string[]} props.actions - Available actions to information tab + * @returns {ReactElement} Information tab + */ +const InformationPanel = ({ image = {}, actions }) => { + const [rename] = useRenameImageMutation() + const [changeType] = useChangeImageTypeMutation() + const [persistent] = usePersistentImageMutation() + + const { + ID, + NAME, + SIZE, + PERSISTENT, + REGTIME, + DATASTORE_ID, + DATASTORE = '--', + VMS, + } = image + + const { color: stateColor, name: stateName } = getState(image) + const imageTypeName = getType(image) + const imageDiskTypeName = getDiskType(image) + + const handleRename = async (_, newName) => { + await rename({ id: ID, name: newName }) + } + + const handleChangeType = async (_, newType) => { + await changeType({ id: ID, type: newType }) + } + + const handleChangePersistent = async (_, newPersistent) => { + await persistent({ id: ID, persistent: !!+newPersistent }) + } + + const getTypeOptions = () => arrayToOptions(IMAGE_TYPES, { addEmpty: false }) + + const getPersistentOptions = () => + arrayToOptions([0, 1], { + addEmpty: false, + getText: booleanToString, + getValue: String, + }) + + const info = [ + { name: T.ID, value: ID, dataCy: 'id' }, + { + name: T.Name, + value: NAME, + dataCy: 'name', + canEdit: actions?.includes?.(IMAGE_ACTIONS.RENAME), + handleEdit: handleRename, + }, + DATASTORE_ID && { + name: T.Datastore, + value: `#${DATASTORE_ID} ${DATASTORE}`, + link: + !Number.isNaN(+DATASTORE_ID) && + generatePath(PATH.STORAGE.DATASTORES.DETAIL, { id: DATASTORE_ID }), + dataCy: 'datastoreId', + }, + { + name: T.RegistrationTime, + value: timeToString(REGTIME), + dataCy: 'regtime', + }, + { + name: T.Type, + value: imageTypeName, + valueInOptionList: imageTypeName, + canEdit: actions?.includes?.(IMAGE_ACTIONS.CHANGE_TYPE), + handleGetOptionList: getTypeOptions, + handleEdit: handleChangeType, + dataCy: 'type', + }, + { + name: T.DiskType, + value: imageDiskTypeName, + valueInOptionList: imageDiskTypeName, + dataCy: 'diskType', + }, + { + name: T.Persistent, + value: booleanToString(+PERSISTENT), + valueInOptionList: PERSISTENT, + canEdit: actions?.includes?.(IMAGE_ACTIONS.CHANGE_PERS), + handleGetOptionList: getPersistentOptions, + handleEdit: handleChangePersistent, + dataCy: 'persistent', + }, + { + name: T.Size, + value: prettyBytes(SIZE, 'MB'), + dataCy: 'size', + }, + { + name: T.State, + value: , + dataCy: 'state', + }, + { + name: T.RunningVMs, + value: `${[VMS?.ID ?? []].flat().length || 0}`, + }, + ] + + return ( + <> + + + ) +} + +InformationPanel.propTypes = { + image: PropTypes.object, + actions: PropTypes.arrayOf(PropTypes.string), +} + +InformationPanel.displayName = 'InformationPanel' + +export default InformationPanel diff --git a/src/fireedge/src/client/components/Tabs/Backup/Vms/index.js b/src/fireedge/src/client/components/Tabs/Backup/Vms/index.js new file mode 100644 index 0000000000..fded895e66 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Backup/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/Backup/index.js b/src/fireedge/src/client/components/Tabs/Backup/index.js new file mode 100644 index 0000000000..7fdf681be0 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Backup/index.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 { memo, useMemo } from 'react' +import PropTypes from 'prop-types' +import { Alert, LinearProgress } from '@mui/material' + +import { useViews } from 'client/features/Auth' +import { useGetImageQuery } from 'client/features/OneApi/image' +import { getAvailableInfoTabs } from 'client/models/Helper' +import { RESOURCE_NAMES } from 'client/constants' + +import Tabs from 'client/components/Tabs' +import Info from 'client/components/Tabs/Backup/Info' +import Vms from 'client/components/Tabs/Backup/Vms' + +const getTabComponent = (tabName) => + ({ + info: Info, + vms: Vms, + }[tabName]) + +const BackupTabs = memo(({ id }) => { + const { view, getResourceView } = useViews() + const { isLoading, isError, error } = useGetImageQuery({ id }) + + const tabsAvailable = useMemo(() => { + const resource = RESOURCE_NAMES.IMAGE + const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {} + + return getAvailableInfoTabs(infoTabs, getTabComponent, id) + }, [view]) + + if (isError) { + return ( + + {error.data} + + ) + } + + return isLoading ? ( + + ) : ( + + ) +}) + +BackupTabs.propTypes = { id: PropTypes.string.isRequired } +BackupTabs.displayName = 'BackupTabs' + +export default BackupTabs diff --git a/src/fireedge/src/client/components/Tabs/File/Info/information.js b/src/fireedge/src/client/components/Tabs/File/Info/information.js index 10369ad68b..8e67bd11d8 100644 --- a/src/fireedge/src/client/components/Tabs/File/Info/information.js +++ b/src/fireedge/src/client/components/Tabs/File/Info/information.js @@ -25,7 +25,7 @@ import { import { StatusChip } from 'client/components/Status' import { List } from 'client/components/Tabs/Common' -import { getType, getState } from 'client/models/Image' +import { getDiskType, getType, getState } from 'client/models/Image' import { timeToString, booleanToString, @@ -62,6 +62,7 @@ const InformationPanel = ({ image = {}, actions }) => { const { color: stateColor, name: stateName } = getState(image) const imageTypeName = getType(image) + const imageDiskTypeName = getDiskType(image) const handleRename = async (_, newName) => { await rename({ id: ID, name: newName }) @@ -115,6 +116,12 @@ const InformationPanel = ({ image = {}, actions }) => { handleEdit: handleChangeType, dataCy: 'type', }, + { + name: T.DiskType, + value: imageDiskTypeName, + valueInOptionList: imageDiskTypeName, + dataCy: 'diskType', + }, { name: T.Locked, value: levelLockToString(LOCK?.LOCKED), diff --git a/src/fireedge/src/client/components/Tabs/Image/Info/information.js b/src/fireedge/src/client/components/Tabs/Image/Info/information.js index 10369ad68b..8e67bd11d8 100644 --- a/src/fireedge/src/client/components/Tabs/Image/Info/information.js +++ b/src/fireedge/src/client/components/Tabs/Image/Info/information.js @@ -25,7 +25,7 @@ import { import { StatusChip } from 'client/components/Status' import { List } from 'client/components/Tabs/Common' -import { getType, getState } from 'client/models/Image' +import { getDiskType, getType, getState } from 'client/models/Image' import { timeToString, booleanToString, @@ -62,6 +62,7 @@ const InformationPanel = ({ image = {}, actions }) => { const { color: stateColor, name: stateName } = getState(image) const imageTypeName = getType(image) + const imageDiskTypeName = getDiskType(image) const handleRename = async (_, newName) => { await rename({ id: ID, name: newName }) @@ -115,6 +116,12 @@ const InformationPanel = ({ image = {}, actions }) => { handleEdit: handleChangeType, dataCy: 'type', }, + { + name: T.DiskType, + value: imageDiskTypeName, + valueInOptionList: imageDiskTypeName, + dataCy: 'diskType', + }, { name: T.Locked, value: levelLockToString(LOCK?.LOCKED), diff --git a/src/fireedge/src/client/components/Tabs/Vm/Backup.js b/src/fireedge/src/client/components/Tabs/Vm/Backup.js new file mode 100644 index 0000000000..24c1c99f92 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Vm/Backup.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 PropTypes from 'prop-types' +import { useGetVmQuery } from 'client/features/OneApi/vm' +import { BackupsTable } from 'client/components/Tables' + +/** + * Renders the list of backups from a VM. + * + * @param {object} props - Props + * @param {string} props.id - Virtual Machine id + * @returns {ReactElement} Backups tab + */ +const VmBackupTab = ({ id }) => { + const { data: vm = {} } = useGetVmQuery({ id }) + + return +} + +VmBackupTab.propTypes = { + tabProps: PropTypes.object, + id: PropTypes.string, +} + +VmBackupTab.displayName = 'VmBackupTab' + +export default VmBackupTab diff --git a/src/fireedge/src/client/components/Tabs/Vm/index.js b/src/fireedge/src/client/components/Tabs/Vm/index.js index 7fcb476cd7..7f7c6d5fc9 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/index.js @@ -28,6 +28,7 @@ import Network from 'client/components/Tabs/Vm/Network' import History from 'client/components/Tabs/Vm/History' import SchedActions from 'client/components/Tabs/Vm/SchedActions' import Snapshot from 'client/components/Tabs/Vm/Snapshot' +import Backup from 'client/components/Tabs/Vm/Backup' import Storage from 'client/components/Tabs/Vm/Storage' import Configuration from 'client/components/Tabs/Vm/Configuration' import Template from 'client/components/Tabs/Vm/Template' @@ -39,6 +40,7 @@ const getTabComponent = (tabName) => history: History, sched_actions: SchedActions, snapshot: Snapshot, + backup: Backup, storage: Storage, configuration: Configuration, template: Template, diff --git a/src/fireedge/src/client/constants/datastore.js b/src/fireedge/src/client/constants/datastore.js index bb30399ac2..189ac4dfac 100644 --- a/src/fireedge/src/client/constants/datastore.js +++ b/src/fireedge/src/client/constants/datastore.js @@ -54,7 +54,7 @@ import { Permissions } from 'client/constants/common' */ /** @type {string[]} Datastore type information */ -export const DATASTORE_TYPES = ['IMAGE', 'SYSTEM', 'FILE'] +export const DATASTORE_TYPES = ['IMAGE', 'SYSTEM', 'FILE', 'BACKUP'] /** @type {STATES.StateInfo[]} Datastore states */ export const DATASTORE_STATES = [ diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js index 1d7a47406a..61a491cb02 100644 --- a/src/fireedge/src/client/constants/image.js +++ b/src/fireedge/src/client/constants/image.js @@ -63,6 +63,7 @@ export const IMAGE_TYPES_STR = { KERNEL: 'KERNEL', RAMDISK: 'RAMDISK', CONTEXT: 'CONTEXT', + BACKUP: 'BACKUP', } /** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type */ @@ -73,6 +74,7 @@ export const IMAGE_TYPES = [ IMAGE_TYPES_STR.KERNEL, IMAGE_TYPES_STR.RAMDISK, IMAGE_TYPES_STR.CONTEXT, + IMAGE_TYPES_STR.BACKUP, ] /** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type for tab files */ @@ -89,6 +91,9 @@ export const IMAGE_TYPES_FOR_IMAGES = [ IMAGE_TYPES_STR.DATABLOCK, ] +/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type for tab files */ +export const IMAGE_TYPES_FOR_BACKUPS = [IMAGE_TYPES_STR.BACKUP] + /** @enum {string} Disk type */ export const DISK_TYPES_STR = { FILE: 'FILE', @@ -186,6 +191,7 @@ export const IMAGE_ACTIONS = { DISABLE: 'disable', PERSISTENT: 'persistent', NON_PERSISTENT: 'nonpersistent', + RESTORE: 'restore', // INFORMATION RENAME: ACTIONS.RENAME, diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 753751280c..5aef267be4 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -150,6 +150,7 @@ export const SOCKETS = { /** @enum {string} Names of resource */ export const RESOURCE_NAMES = { APP: 'marketplace-app', + BACKUP: 'backup', CLUSTER: 'cluster', DATASTORE: 'datastore', GROUP: 'group', diff --git a/src/fireedge/src/client/constants/scheduler.js b/src/fireedge/src/client/constants/scheduler.js index 326cee3a25..b2b3eb3107 100644 --- a/src/fireedge/src/client/constants/scheduler.js +++ b/src/fireedge/src/client/constants/scheduler.js @@ -66,6 +66,7 @@ export const ARGS_TYPES = { DISK_ID: 'DISK_ID', NAME: 'NAME', SNAPSHOT_ID: 'SNAPSHOT_ID', + DS_ID: 'DS_ID', } /** @enum {string} Period type */ diff --git a/src/fireedge/src/client/constants/states.js b/src/fireedge/src/client/constants/states.js index cce43b3ee0..77763d583b 100644 --- a/src/fireedge/src/client/constants/states.js +++ b/src/fireedge/src/client/constants/states.js @@ -137,3 +137,5 @@ export const UNKNOWN = 'UNKNOWN' export const USED = 'USED' export const USED_PERS = 'USED_PERS' export const WARNING = 'WARNING' +export const BACKUP = 'BACKUP' +export const BACKUP_POWEROFF = 'BACKUP_POWEROFF' diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 42936ed3f7..67a18a7a12 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -50,6 +50,7 @@ module.exports = { AttachNic: 'Attach NIC', AttachVolatile: 'Attach volatile disk', BackToList: 'Back to %s list', + Backup: 'Backup', Cancel: 'Cancel', Change: 'Change', ChangeGroup: 'Change group', @@ -139,6 +140,7 @@ module.exports = { ResizeSomething: 'Resize: %s', Resume: 'Resume', Retry: 'Retry', + Restore: 'Restore', Revert: 'Revert', RevertSomething: 'Revert: %s', Save: 'Save', @@ -360,6 +362,9 @@ module.exports = { SecurityGroups: 'Security groups', /* sections - storage */ + Backups: 'Backups', + BackupDatastore: 'Backup Datastore', + BackupRestored: 'Backup restored', Datastore: 'Datastore', Datastores: 'Datastores', Image: 'Image', @@ -385,6 +390,9 @@ module.exports = { Fs: 'Fs', CustomFormat: 'Custom Format', Dockerfile: 'Dockerfile', + Running: 'Running', + DoNotRestoreNICAttributes: 'Do not restore NIC attributes', + DoNotRestoreIPAttributes: 'Do not restore IP attributes', /* sections - templates & instances */ Instances: 'Instances', @@ -1079,7 +1087,7 @@ module.exports = { BasePath: 'Base path', FileSystemType: 'Filesystem type', Persistent: 'Persistent', - NonPersistyent: 'Non Persistent', + NonPersistent: 'Non Persistent', RunningVMs: 'Running VMs', /* Disk - general */ DiskType: 'Disk type', diff --git a/src/fireedge/src/client/constants/vm.js b/src/fireedge/src/client/constants/vm.js index bf6704a854..0d1dfe4513 100644 --- a/src/fireedge/src/client/constants/vm.js +++ b/src/fireedge/src/client/constants/vm.js @@ -709,10 +709,23 @@ export const VM_LCM_STATES = [ color: COLOR.info.main, meaning: '', }, + { + // 69 + name: STATES.BACKUP, + color: COLOR.info.main, + meaning: '', + }, + { + // 70 + name: STATES.BACKUP_POWEROFF, + color: COLOR.info.main, + meaning: '', + }, ] /** @enum {string} Virtual machine actions */ export const VM_ACTIONS = { + BACKUP: 'backup', CREATE_DIALOG: 'create_dialog', CREATE_APP_DIALOG: 'create_app_dialog', DEPLOY: 'deploy', @@ -791,6 +804,7 @@ export const VM_ACTIONS = { /** @enum {string} Virtual machine actions by state */ export const VM_ACTIONS_BY_STATE = { + [VM_ACTIONS.BACKUP]: [STATES.POWEROFF, STATES.RUNNING], [VM_ACTIONS.DEPLOY]: [ STATES.PENDING, STATES.HOLD, @@ -980,6 +994,7 @@ export const HYPERVISORS = { /** @type {string[]} Actions that can be scheduled */ export const VM_ACTIONS_WITH_SCHEDULE = [ + VM_ACTIONS.BACKUP, VM_ACTIONS.TERMINATE, VM_ACTIONS.TERMINATE_HARD, VM_ACTIONS.UNDEPLOY, @@ -1069,7 +1084,8 @@ export const VM_ACTIONS_IN_CHARTER = [ * 'alias-attach' | * 'alias-detach' | * 'poweroff-migrate' | - * 'poweroff-hard-migrate' + * 'poweroff-hard-migrate' | + * 'backup' * )} History actions */ export const HISTORY_ACTIONS = [ @@ -1123,6 +1139,7 @@ export const HISTORY_ACTIONS = [ 'alias-detach', 'poweroff-migrate', 'poweroff-hard-migrate', + 'backup', ] /** diff --git a/src/fireedge/src/client/containers/Backups/index.js b/src/fireedge/src/client/containers/Backups/index.js new file mode 100644 index 0000000000..2a10fde3c1 --- /dev/null +++ b/src/fireedge/src/client/containers/Backups/index.js @@ -0,0 +1,156 @@ +/* ------------------------------------------------------------------------- * + * 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, useState, memo } from 'react' +import PropTypes from 'prop-types' +import GotoIcon from 'iconoir-react/dist/Pin' +import RefreshDouble from 'iconoir-react/dist/RefreshDouble' +import Cancel from 'iconoir-react/dist/Cancel' +import { Typography, Box, Stack, Chip } from '@mui/material' +import { Row } from 'react-table' + +import { useLazyGetImageQuery } from 'client/features/OneApi/image' +import { BackupsTable } from 'client/components/Tables' +import BackupActions from 'client/components/Tables/Backups/actions' +import BackupTabs from 'client/components/Tabs/Backup' +import SplitPane from 'client/components/SplitPane' +import MultipleTags from 'client/components/MultipleTags' +import { SubmitButton } from 'client/components/FormControl' +import { Tr } from 'client/components/HOC' +import { T, Image } from 'client/constants' + +/** + * Displays a list of Backups with a split pane between the list and selected row(s). + * + * @returns {ReactElement} Backups list and selected row(s) + */ +function Backups() { + const [selectedRows, onSelectedRowsChange] = useState(() => []) + const actions = BackupActions() + + const hasSelectedRows = selectedRows?.length > 0 + const moreThanOneSelected = selectedRows?.length > 1 + + return ( + + {({ getGridProps, GutterComponent }) => ( + + + + {hasSelectedRows && ( + <> + + {moreThanOneSelected ? ( + + ) : ( + selectedRows[0]?.toggleRowSelected(false)} + /> + )} + + )} + + )} + + ) +} + +/** + * Displays details of an Image. + * + * @param {Image} image - Image to display + * @param {Function} [gotoPage] - Function to navigate to a page of an Image + * @param {Function} [unselect] - Function to unselect a Image + * @returns {ReactElement} Image details + */ +const InfoTabs = memo(({ image, gotoPage, unselect }) => { + const [getImage, { data: lazyData, isFetching }] = useLazyGetImageQuery() + const id = lazyData?.ID ?? image.ID + const name = lazyData?.NAME ?? image.NAME + + return ( + + + + {`#${id} | ${name}`} + + } + tooltip={Tr(T.Refresh)} + isSubmitting={isFetching} + onClick={() => getImage({ id })} + /> + {typeof gotoPage === 'function' && ( + } + tooltip={Tr(T.LocateOnTable)} + onClick={() => gotoPage()} + /> + )} + {typeof unselect === 'function' && ( + } + tooltip={Tr(T.Close)} + onClick={() => unselect()} + /> + )} + + + + ) +}) + +InfoTabs.propTypes = { + image: PropTypes.object.isRequired, + gotoPage: PropTypes.func, + unselect: PropTypes.func, +} + +InfoTabs.displayName = 'InfoTabs' + +/** + * Displays a list of tags that represent the selected rows. + * + * @param {Row[]} tags - Row(s) to display as tags + * @returns {ReactElement} List of tags + */ +const GroupedTags = memo(({ tags = [] }) => ( + + ( + toggleRowSelected(false)} + /> + ))} + /> + +)) + +GroupedTags.propTypes = { tags: PropTypes.array } +GroupedTags.displayName = 'GroupedTags' + +export default Backups diff --git a/src/fireedge/src/client/containers/Dashboard/Sunstone/index.js b/src/fireedge/src/client/containers/Dashboard/Sunstone/index.js index e045c7f956..8897de947a 100644 --- a/src/fireedge/src/client/containers/Dashboard/Sunstone/index.js +++ b/src/fireedge/src/client/containers/Dashboard/Sunstone/index.js @@ -19,8 +19,8 @@ import { useHistory } from 'react-router-dom' import { Box, CircularProgress, Grid } from '@mui/material' import { ModernTv as VmsIcons, - List as TemplatesIcon, - Archive as ImageIcon, + EmptyPage as TemplatesIcon, + BoxIso as ImageIcon, NetworkAlt as NetworkIcon, } from 'iconoir-react' diff --git a/src/fireedge/src/client/features/OneApi/image.js b/src/fireedge/src/client/features/OneApi/image.js index 1eeeb96a6b..de20e0b8c0 100644 --- a/src/fireedge/src/client/features/OneApi/image.js +++ b/src/fireedge/src/client/features/OneApi/image.js @@ -28,6 +28,7 @@ import { IMAGE_TYPES_STR, IMAGE_TYPES_FOR_FILES, IMAGE_TYPES_FOR_IMAGES, + IMAGE_TYPES_FOR_BACKUPS, } from 'client/constants' import { getType } from 'client/models/Image' @@ -55,7 +56,9 @@ const imageApi = oneApi.injectEndpoints({ }, transformResponse: (data) => { const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => - IMAGE_TYPES_FOR_IMAGES.some(() => getType(image)) + IMAGE_TYPES_FOR_IMAGES.some( + (imageType) => imageType === getType(image) + ) ) return [images ?? []].flat() @@ -87,7 +90,43 @@ const imageApi = oneApi.injectEndpoints({ }, transformResponse: (data) => { const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => - IMAGE_TYPES_FOR_FILES.some(() => getType(image)) + IMAGE_TYPES_FOR_FILES.some( + (imageType) => imageType === getType(image) + ) + ) + + return [images ?? []].flat() + }, + providesTags: (images) => + images + ? [ + ...images.map(({ ID }) => ({ type: IMAGE_POOL, id: `${ID}` })), + IMAGE_POOL, + ] + : [IMAGE_POOL], + }), + getBackups: builder.query({ + /** + * Retrieves information for all or part of the images in the pool. + * + * @param {object} params - Request params + * @param {FilterFlag} [params.filter] - Filter flag + * @param {number} [params.start] - Range start ID + * @param {number} [params.end] - Range end ID + * @returns {Image[]} List of images + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = Actions.IMAGE_POOL_INFO + const command = { name, ...Commands[name] } + + return { params, command } + }, + transformResponse: (data) => { + const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => + IMAGE_TYPES_FOR_BACKUPS.some( + (imageType) => imageType === getType(image) + ) ) return [images ?? []].flat() @@ -452,6 +491,25 @@ const imageApi = oneApi.injectEndpoints({ }, invalidatesTags: (_, __, { id }) => [{ type: IMAGE, id }, IMAGE_POOL], }), + restoreBackup: builder.mutation({ + /** + * Restores an image. + * + * @param {number|string} params - Request params + * @param {string} params.id - Image id + * @param {number} params.datastore - New type for the Image + * @param {string} params.options - New type for the Image + * @returns {number} Image id + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = Actions.IMAGE_RESTORE + const command = { name, ...Commands[name] } + + return { params, command } + }, + invalidatesTags: (_, __, id) => [{ type: IMAGE, id }], + }), }), }) @@ -463,6 +521,8 @@ export const { useLazyGetImagesQuery, useGetFilesQuery, useLazyGetFilesQuery, + useGetBackupsQuery, + useLazyGetBackupsQuery, // Mutations useAllocateImageMutation, @@ -482,4 +542,5 @@ export const { useLockImageMutation, useUnlockImageMutation, useUploadImageMutation, + useRestoreBackupMutation, } = imageApi diff --git a/src/fireedge/src/client/features/OneApi/vm.js b/src/fireedge/src/client/features/OneApi/vm.js index 7545cf530e..2c9a094fe7 100644 --- a/src/fireedge/src/client/features/OneApi/vm.js +++ b/src/fireedge/src/client/features/OneApi/vm.js @@ -875,6 +875,24 @@ const vmApi = oneApi.injectEndpoints({ }, invalidatesTags: (_, __, { id }) => [{ type: VM, id }], }), + backup: builder.mutation({ + /** + * Backup the VM. + * + * @param {object} params - Request parameters + * @param {string} params.id - Virtual machine id + * @param {number} params.dsId - Backup Datastore id + * @returns {number} Virtual machine id + * @throws Fails when response isn't code 200 + */ + query: (params) => { + const name = Actions.VM_BACKUP + const command = { name, ...Commands[name] } + + return { params, command } + }, + invalidatesTags: (_, __, { id }) => [{ type: VM, id }], + }), lockVm: builder.mutation({ /** * Locks a Virtual Machine. Lock certain actions depending on blocking level. @@ -1064,6 +1082,7 @@ export const { useUpdateUserTemplateMutation, useUpdateConfigurationMutation, useRecoverMutation, + useBackupMutation, useLockVmMutation, useUnlockVmMutation, useAddScheduledActionMutation, diff --git a/src/fireedge/src/client/models/Scheduler.js b/src/fireedge/src/client/models/Scheduler.js index f0d00491eb..428553057b 100644 --- a/src/fireedge/src/client/models/Scheduler.js +++ b/src/fireedge/src/client/models/Scheduler.js @@ -26,6 +26,7 @@ import { } from 'client/constants' const { + BACKUP, SNAPSHOT_DISK_CREATE, SNAPSHOT_DISK_REVERT, SNAPSHOT_DISK_DELETE, @@ -136,10 +137,11 @@ export const getRepeatInformation = (action) => { * @returns {ARGS_TYPES[]} Arguments */ export const getRequiredArgsByAction = (action) => { - const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES + const { DISK_ID, NAME, SNAPSHOT_ID, DS_ID } = ARGS_TYPES return ( { + [BACKUP]: [DS_ID], [SNAPSHOT_DISK_CREATE]: [DISK_ID, NAME], [SNAPSHOT_DISK_REVERT]: [DISK_ID, SNAPSHOT_ID], [SNAPSHOT_DISK_DELETE]: [DISK_ID, SNAPSHOT_ID], @@ -159,12 +161,13 @@ export const getRequiredArgsByAction = (action) => { export const transformStringToArgsObject = ({ ACTION, ARGS = {} } = {}) => { if (typeof ARGS !== 'string') return ARGS - // IMPORTANT - String data from ARGS has strict order: DISK_ID, NAME, SNAPSHOT_ID + // IMPORTANT - String data from ARGS has strict order: DISK_ID, NAME, SNAPSHOT_ID, DS_ID const [arg1, arg2] = ARGS.split(',') - const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES + const { DISK_ID, NAME, SNAPSHOT_ID, DS_ID } = ARGS_TYPES return ( { + [BACKUP]: { [DS_ID]: arg1 }, [SNAPSHOT_DISK_CREATE]: { [DISK_ID]: arg1, [NAME]: arg2 }, [SNAPSHOT_DISK_REVERT]: { [DISK_ID]: arg1, [SNAPSHOT_ID]: arg2 }, [SNAPSHOT_DISK_DELETE]: { [DISK_ID]: arg1, [SNAPSHOT_ID]: arg2 }, diff --git a/src/fireedge/src/server/utils/constants/commands/image.js b/src/fireedge/src/server/utils/constants/commands/image.js index 43ae6e75bb..0b18e59678 100644 --- a/src/fireedge/src/server/utils/constants/commands/image.js +++ b/src/fireedge/src/server/utils/constants/commands/image.js @@ -35,6 +35,7 @@ const IMAGE_SNAPFLAT = 'image.snapshotflatten' const IMAGE_INFO = 'image.info' const IMAGE_LOCK = 'image.lock' const IMAGE_UNLOCK = 'image.unlock' +const IMAGE_RESTORE = 'image.restore' const IMAGE_POOL_INFO = 'imagepool.info' const Actions = { @@ -54,6 +55,7 @@ const Actions = { IMAGE_INFO, IMAGE_LOCK, IMAGE_UNLOCK, + IMAGE_RESTORE, IMAGE_POOL_INFO, } @@ -324,6 +326,24 @@ module.exports = { }, }, }, + [IMAGE_RESTORE]: { + // inspected + httpMethod: POST, + params: { + id: { + from: resource, + default: 0, + }, + datastore: { + from: postBody, + default: -1, + }, + options: { + from: postBody, + default: '', + }, + }, + }, [IMAGE_POOL_INFO]: { // inspected httpMethod: GET, diff --git a/src/fireedge/src/server/utils/constants/commands/vm.js b/src/fireedge/src/server/utils/constants/commands/vm.js index 5b28486886..d841e552d3 100644 --- a/src/fireedge/src/server/utils/constants/commands/vm.js +++ b/src/fireedge/src/server/utils/constants/commands/vm.js @@ -52,6 +52,7 @@ const VM_SEC_GROUP_DETACH = 'vm.detachsg' const VM_SCHED_ADD = 'vm.schedadd' const VM_SCHED_UPDATE = 'vm.schedupdate' const VM_SCHED_DELETE = 'vm.scheddelete' +const VM_BACKUP = 'vm.backup' const VM_POOL_INFO = 'vmpool.info' const VM_POOL_INFO_EXTENDED = 'vmpool.infoextended' const VM_POOL_MONITORING = 'vmpool.monitoring' @@ -93,6 +94,7 @@ const Actions = { VM_SCHED_ADD, VM_SCHED_UPDATE, VM_SCHED_DELETE, + VM_BACKUP, VM_POOL_INFO, VM_POOL_INFO_EXTENDED, VM_POOL_MONITORING, @@ -675,6 +677,19 @@ module.exports = { }, }, }, + [VM_BACKUP]: { + httpMethod: POST, + params: { + id: { + from: resource, + default: 0, + }, + dsId: { + from: postBody, + default: 0, + }, + }, + }, [VM_POOL_INFO]: { // inspected httpMethod: GET,