diff --git a/src/fireedge/src/client/apps/sunstone/routesOne.js b/src/fireedge/src/client/apps/sunstone/routesOne.js index 2193575ffb..28c6c5b1d9 100644 --- a/src/fireedge/src/client/apps/sunstone/routesOne.js +++ b/src/fireedge/src/client/apps/sunstone/routesOne.js @@ -24,6 +24,7 @@ import { Box as StorageIcon, Db as DatastoreIcon, BoxIso as ImageIcon, + Folder as FileIcon, SimpleCart as MarketplaceIcon, CloudDownload as MarketplaceAppIcon, ServerConnection as NetworksIcon, @@ -97,6 +98,12 @@ const Datastores = loadable(() => import('client/containers/Datastores'), { const Images = loadable(() => import('client/containers/Images'), { ssr: false, }) +const Files = loadable(() => import('client/containers/Files'), { + ssr: false, +}) +const CreateFiles = loadable(() => import('client/containers/Files/Create'), { + ssr: false, +}) const CreateImages = loadable(() => import('client/containers/Images/Create'), { ssr: false, }) @@ -201,6 +208,11 @@ export const PATH = { CREATE: `/${RESOURCE_NAMES.IMAGE}/create`, DOCKERFILE: `/${RESOURCE_NAMES.IMAGE}/dockerfile`, }, + FILES: { + LIST: `/${RESOURCE_NAMES.FILE}`, + DETAIL: `/${RESOURCE_NAMES.FILE}/:id`, + CREATE: `/${RESOURCE_NAMES.FILE}/create`, + }, MARKETPLACES: { LIST: `/${RESOURCE_NAMES.MARKETPLACE}`, DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`, @@ -380,6 +392,18 @@ const ENDPOINTS = [ path: PATH.STORAGE.IMAGES.CREATE, Component: CreateImages, }, + { + title: T.Files, + path: PATH.STORAGE.FILES.LIST, + sidebar: true, + icon: FileIcon, + Component: Files, + }, + { + title: T.CreateFile, + path: PATH.STORAGE.FILES.CREATE, + Component: CreateFiles, + }, { title: T.CreateDockerfile, path: PATH.STORAGE.IMAGES.DOCKERFILE, diff --git a/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/DatastoresTable/index.js b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/DatastoresTable/index.js new file mode 100644 index 0000000000..bcf0d06879 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/File/CreateForm/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/Image/CloneForm/Steps/DatastoresTable/schema' + +import { Step } from 'client/utils' +import { T } from 'client/constants' + +export const STEP_ID = 'datastore' + +const Content = ({ data }) => { + 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: 'FILE' }], + }} + 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/File/CreateForm/Steps/General/index.js b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/General/index.js new file mode 100644 index 0000000000..a2fbca7b1f --- /dev/null +++ b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/General/index.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 FormWithSchema from 'client/components/Forms/FormWithSchema' + +import { + SCHEMA, + FIELDS, +} from 'client/components/Forms/File/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/File/CreateForm/Steps/General/schema.js b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/General/schema.js new file mode 100644 index 0000000000..53258d93e3 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/General/schema.js @@ -0,0 +1,159 @@ +/* ------------------------------------------------------------------------- * + * 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, mixed } from 'yup' +import { + Field, + arrayToOptions, + getValidationFromFields, + upperCaseFirst, +} from 'client/utils' +import { + T, + INPUT_TYPES, + IMAGE_TYPES_STR, + IMAGE_TYPES_FOR_FILES, +} from 'client/constants' + +export const IMAGE_LOCATION_TYPES = { + PATH: 'path', + UPLOAD: 'upload', +} + +const IMAGE_LOCATION = { + [IMAGE_LOCATION_TYPES.PATH]: T.Path, + [IMAGE_LOCATION_TYPES.UPLOAD]: T.Upload, +} + +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_FOR_FILES), { + 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: { md: 12 }, +} + +/** @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 }, +} + +/** + * @returns {Field[]} Fields + */ +export const FIELDS = [ + NAME, + DESCRIPTION, + TYPE, + IMAGE_LOCATION_FIELD, + PATH_FIELD, + UPLOAD_FIELD, +] + +/** + * @param {object} [stepProps] - Step props + * @returns {ObjectSchema} Schema + */ +export const SCHEMA = object(getValidationFromFields(FIELDS)) diff --git a/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/index.js b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/index.js new file mode 100644 index 0000000000..8cd4cf33ac --- /dev/null +++ b/src/fireedge/src/client/components/Forms/File/CreateForm/Steps/index.js @@ -0,0 +1,45 @@ +/* ------------------------------------------------------------------------- * + * 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/File/CreateForm/Steps/DatastoresTable' + +import General, { + STEP_ID as GENERAL_ID, +} from 'client/components/Forms/File/CreateForm/Steps/General' + +import { createSteps, cloneObject, set } from 'client/utils' + +const Steps = createSteps([General, Datastore], { + transformBeforeSubmit: (formData) => { + const { [GENERAL_ID]: general = {}, [DATASTORE_ID]: [datastore] = [] } = + formData ?? {} + + const generalData = cloneObject(general) + set(generalData, 'UPLOAD', undefined) + set(generalData, 'IMAGE_LOCATION', undefined) + + return { + template: { + ...generalData, + }, + datastore: datastore?.ID, + file: general?.UPLOAD, + } + }, +}) + +export default Steps diff --git a/src/fireedge/src/client/components/Forms/File/CreateForm/index.js b/src/fireedge/src/client/components/Forms/File/CreateForm/index.js new file mode 100644 index 0000000000..017c2be5d1 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/File/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/File/CreateForm/Steps' diff --git a/src/fireedge/src/client/components/Forms/File/index.js b/src/fireedge/src/client/components/Forms/File/index.js new file mode 100644 index 0000000000..2ae481bfc2 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/File/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 { CreateStepsCallback } from 'client/utils/schema' + +/** + * @param {ConfigurationProps} configProps - Configuration + * @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form + */ +const CreateForm = (configProps) => + AsyncLoadForm({ formPath: 'File/CreateForm' }, configProps) + +export { CreateForm } 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 index f3bfa22bfa..87215a365b 100644 --- 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 @@ -20,7 +20,12 @@ import { getValidationFromFields, upperCaseFirst, } from 'client/utils' -import { T, INPUT_TYPES, IMAGE_TYPES_STR } from 'client/constants' +import { + T, + INPUT_TYPES, + IMAGE_TYPES_STR, + IMAGE_TYPES_FOR_IMAGES, +} from 'client/constants' export const IMAGE_LOCATION_TYPES = { PATH: 'path', @@ -66,7 +71,7 @@ export const TYPE = { name: 'TYPE', label: T.Type, type: INPUT_TYPES.SELECT, - values: arrayToOptions(Object.values(IMAGE_TYPES_STR), { + values: arrayToOptions(Object.values(IMAGE_TYPES_FOR_IMAGES), { addEmpty: false, getText: (type) => { switch (type) { diff --git a/src/fireedge/src/client/components/Tables/Files/actions.js b/src/fireedge/src/client/components/Tables/Files/actions.js new file mode 100644 index 0000000000..6f59c9d533 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Files/actions.js @@ -0,0 +1,211 @@ +/* ------------------------------------------------------------------------- * + * 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 } from '@mui/material' +import { MoreVert, AddCircledOutline, Group, Trash } from 'iconoir-react' + +import { useViews } from 'client/features/Auth' +import { + useEnableImageMutation, + useDisableImageMutation, + useChangeImageOwnershipMutation, + useRemoveImageMutation, +} from 'client/features/OneApi/image' + +import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm' +import { + createActions, + GlobalAction, +} from 'client/components/Tables/Enhanced/Utils' + +import { Translate } from 'client/components/HOC' +import { PATH } from 'client/apps/sunstone/routesOne' +import { isAvailableAction } from 'client/models/VirtualMachine' +import { T, IMAGE_ACTIONS, RESOURCE_NAMES } from 'client/constants' + +const isDisabled = (action) => (rows) => + !isAvailableAction( + action, + rows.map(({ original }) => original) + ) + +const ListFilesNames = ({ 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 [enable] = useEnableImageMutation() + const [disable] = useDisableImageMutation() + const [changeOwnership] = useChangeImageOwnershipMutation() + const [deleteImage] = useRemoveImageMutation() + + const resourcesView = getResourceView(RESOURCE_NAMES.FILE)?.actions + + const fileActions = useMemo( + () => + createActions({ + filters: resourcesView, + actions: [ + { + accessor: IMAGE_ACTIONS.CREATE_DIALOG, + dataCy: `file_${IMAGE_ACTIONS.CREATE_DIALOG}`, + disabled: isDisabled(IMAGE_ACTIONS.CREATE_DIALOG), + tooltip: T.Create, + icon: AddCircledOutline, + action: (rows) => { + history.push(PATH.STORAGE.FILES.CREATE) + }, + }, + { + 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))) + }, + }, + ], + }, + { + 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 fileActions +} + +export default Actions diff --git a/src/fireedge/src/client/components/Tables/Files/columns.js b/src/fireedge/src/client/components/Tables/Files/columns.js new file mode 100644 index 0000000000..6520cc6ea0 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Files/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/Files/index.js b/src/fireedge/src/client/components/Tables/Files/index.js new file mode 100644 index 0000000000..988d03b943 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Files/index.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 { useMemo, ReactElement } from 'react' + +import { useViews } from 'client/features/Auth' +import { useGetFilesQuery } from 'client/features/OneApi/image' + +import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced' +import ImageColumns from 'client/components/Tables/Images/columns' +import ImageRow from 'client/components/Tables/Images/row' +import { RESOURCE_NAMES } from 'client/constants' + +const DEFAULT_DATA_CY = 'images' + +/** + * @param {object} props - Props + * @returns {ReactElement} Images table + */ +const FilesTable = (props) => { + const { rootProps = {}, searchProps = {}, ...rest } = props ?? {} + rootProps['data-cy'] ??= DEFAULT_DATA_CY + searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` + + const { view, getResourceView } = useViews() + const { data = [], isFetching, refetch } = useGetFilesQuery() + + const columns = useMemo( + () => + createColumns({ + filters: getResourceView(RESOURCE_NAMES.IMAGE)?.filters, + columns: ImageColumns, + }), + [view] + ) + + return ( + data, [data])} + rootProps={rootProps} + searchProps={searchProps} + refetch={refetch} + isLoading={isFetching} + getRowId={(row) => String(row.ID)} + RowComponent={ImageRow} + {...rest} + /> + ) +} + +FilesTable.propTypes = { ...EnhancedTable.propTypes } +FilesTable.displayName = 'FilesTable' + +export default FilesTable diff --git a/src/fireedge/src/client/components/Tables/Files/row.js b/src/fireedge/src/client/components/Tables/Files/row.js new file mode 100644 index 0000000000..c760f48e98 --- /dev/null +++ b/src/fireedge/src/client/components/Tables/Files/row.js @@ -0,0 +1,101 @@ +/* ------------------------------------------------------------------------- * + * 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, Folder, ModernTv } from 'iconoir-react' +import { Typography } from '@mui/material' + +import { StatusCircle, StatusChip } from 'client/components/Status' +import { rowStyles } from 'client/components/Tables/styles' + +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([PERSISTENT && 'PERSISTENT', TYPE, DISK_TYPE]), + ].filter(Boolean) + + const { color: stateColor, name: stateName } = ImageModel.getState(original) + + const time = Helper.timeFromMilliseconds(+REGTIME) + const timeAgo = `registered ${time.toRelative()}` + + return ( +
+
+
+ + + {NAME} + + {locked && } + + {labels.map((label) => ( + + ))} + +
+
+ {`#${ID} ${timeAgo}`} + + + {` ${UNAME}`} + + + + {` ${GNAME}`} + + + + {` ${DATASTORE}`} + + + + {` ${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/index.js b/src/fireedge/src/client/components/Tables/index.js index 186d71c382..2c03a493c9 100644 --- a/src/fireedge/src/client/components/Tables/index.js +++ b/src/fireedge/src/client/components/Tables/index.js @@ -20,6 +20,7 @@ import EnhancedTable from 'client/components/Tables/Enhanced' import GroupsTable from 'client/components/Tables/Groups' import HostsTable from 'client/components/Tables/Hosts' import ImagesTable from 'client/components/Tables/Images' +import FilesTable from 'client/components/Tables/Files' import MarketplaceAppsTable from 'client/components/Tables/MarketplaceApps' import MarketplacesTable from 'client/components/Tables/Marketplaces' import SecurityGroupsTable from 'client/components/Tables/SecurityGroups' @@ -40,6 +41,7 @@ export * from 'client/components/Tables/Enhanced/Utils' export { SkeletonTable, EnhancedTable, + FilesTable, VirtualizedTable, ClustersTable, DatastoresTable, diff --git a/src/fireedge/src/client/components/Tabs/File/Info/index.js b/src/fireedge/src/client/components/Tabs/File/Info/index.js new file mode 100644 index 0000000000..2dd2213647 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/File/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/Image/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/File/Info/information.js b/src/fireedge/src/client/components/Tabs/File/Info/information.js new file mode 100644 index 0000000000..10369ad68b --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/File/Info/information.js @@ -0,0 +1,166 @@ +/* ------------------------------------------------------------------------- * + * 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 { getType, getState } from 'client/models/Image' +import { + timeToString, + booleanToString, + levelLockToString, +} 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, + LOCK, + REGTIME, + DATASTORE_ID, + DATASTORE = '--', + VMS, + } = image + + const { color: stateColor, name: stateName } = getState(image) + const imageTypeName = getType(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.Locked, + value: levelLockToString(LOCK?.LOCKED), + dataCy: 'locked', + }, + { + 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/File/index.js b/src/fireedge/src/client/components/Tabs/File/index.js new file mode 100644 index 0000000000..a6513a960c --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/File/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 { 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/Image/Info' + +const getTabComponent = (tabName) => + ({ + info: Info, + }[tabName]) + +const FileTabs = 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 ? ( + + ) : ( + + ) +}) + +FileTabs.propTypes = { id: PropTypes.string.isRequired } +FileTabs.displayName = 'FileTabs' + +export default FileTabs diff --git a/src/fireedge/src/client/constants/image.js b/src/fireedge/src/client/constants/image.js index a7c3c9ead0..1d7a47406a 100644 --- a/src/fireedge/src/client/constants/image.js +++ b/src/fireedge/src/client/constants/image.js @@ -75,6 +75,20 @@ export const IMAGE_TYPES = [ IMAGE_TYPES_STR.CONTEXT, ] +/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type for tab files */ +export const IMAGE_TYPES_FOR_FILES = [ + IMAGE_TYPES_STR.KERNEL, + IMAGE_TYPES_STR.RAMDISK, + IMAGE_TYPES_STR.CONTEXT, +] + +/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type for tab images */ +export const IMAGE_TYPES_FOR_IMAGES = [ + IMAGE_TYPES_STR.OS, + IMAGE_TYPES_STR.CDROM, + IMAGE_TYPES_STR.DATABLOCK, +] + /** @enum {string} Disk type */ export const DISK_TYPES_STR = { FILE: 'FILE', diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 1a8ca7a918..753751280c 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -155,6 +155,7 @@ export const RESOURCE_NAMES = { GROUP: 'group', HOST: 'host', IMAGE: 'image', + FILE: 'file', MARKETPLACE: 'marketplace', SEC_GROUP: 'security-group', USER: 'user', diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 60593a2bda..dc2879391e 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -72,6 +72,7 @@ module.exports = { CreateVirtualNetwork: 'Create Virtual Network', CreateVmTemplate: 'Create VM Template', CreateImage: 'Create Image', + CreateFile: 'Create File', CreateDockerfile: 'Create Dockerfile', CurrentGroup: 'Current group: %s', CurrentOwner: 'Current owner: %s', diff --git a/src/fireedge/src/client/containers/Files/Create.js b/src/fireedge/src/client/containers/Files/Create.js new file mode 100644 index 0000000000..44e3517dc4 --- /dev/null +++ b/src/fireedge/src/client/containers/Files/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/File' +import { PATH } from 'client/apps/sunstone/routesOne' + +/** + * Displays the creation or modification form to a VM Template. + * + * @returns {ReactElement} VM Template form + */ +function CreateFile() { + 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.FILES.LIST) + enqueueSuccess(`File created - #${newTemplateId}`) + } catch {} + } + + return ( + }> + {(config) => } + + ) +} + +export default CreateFile diff --git a/src/fireedge/src/client/containers/Files/index.js b/src/fireedge/src/client/containers/Files/index.js new file mode 100644 index 0000000000..43bea7fac2 --- /dev/null +++ b/src/fireedge/src/client/containers/Files/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 { FilesTable } from 'client/components/Tables' +import fileActions from 'client/components/Tables/Files/actions' +import FileTabs from 'client/components/Tabs/File' +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 Files with a split pane between the list and selected row(s). + * + * @returns {ReactElement} Files list and selected row(s) + */ +function Files() { + const [selectedRows, onSelectedRowsChange] = useState(() => []) + const actions = fileActions() + + const hasSelectedRows = selectedRows?.length > 0 + const moreThanOneSelected = selectedRows?.length > 1 + + return ( + + {({ getGridProps, GutterComponent }) => ( + + + + {hasSelectedRows && ( + <> + + {moreThanOneSelected ? ( + + ) : ( + selectedRows[0]?.toggleRowSelected(false)} + /> + )} + + )} + + )} + + ) +} + +/** + * Displays details of an File. + * + * @param {Image} file - File 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(({ file, gotoPage, unselect }) => { + const [getImage, { data: lazyData, isFetching }] = useLazyGetImageQuery() + const id = lazyData?.ID ?? file.ID + const name = lazyData?.NAME ?? file.NAME + + return ( + + + } + 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()} + /> + )} + + {`#${id} | ${name}`} + + + + + ) +}) + +InfoTabs.propTypes = { + file: 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 Files diff --git a/src/fireedge/src/client/features/OneApi/image.js b/src/fireedge/src/client/features/OneApi/image.js index 2fbcfc9efb..1eeeb96a6b 100644 --- a/src/fireedge/src/client/features/OneApi/image.js +++ b/src/fireedge/src/client/features/OneApi/image.js @@ -26,6 +26,8 @@ import { Image, Permission, IMAGE_TYPES_STR, + IMAGE_TYPES_FOR_FILES, + IMAGE_TYPES_FOR_IMAGES, } from 'client/constants' import { getType } from 'client/models/Image' @@ -52,15 +54,41 @@ const imageApi = oneApi.injectEndpoints({ return { params, command } }, transformResponse: (data) => { - const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => { - const type = getType(image) + const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => + IMAGE_TYPES_FOR_IMAGES.some(() => getType(image)) + ) - return ( - type === IMAGE_TYPES_STR.OS || - type === IMAGE_TYPES_STR.CDROM || - type === IMAGE_TYPES_STR.DATABLOCK - ) - }) + return [images ?? []].flat() + }, + providesTags: (images) => + images + ? [ + ...images.map(({ ID }) => ({ type: IMAGE_POOL, id: `${ID}` })), + IMAGE_POOL, + ] + : [IMAGE_POOL], + }), + getFiles: 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_FILES.some(() => getType(image)) + ) return [images ?? []].flat() }, @@ -433,6 +461,8 @@ export const { useLazyGetImageQuery, useGetImagesQuery, useLazyGetImagesQuery, + useGetFilesQuery, + useLazyGetFilesQuery, // Mutations useAllocateImageMutation,