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

F #5902: Sunstone File Tab (#2252)

(cherry picked from commit b1b37fb77cd5f827983d9a26545fc72a2b323dd0)
This commit is contained in:
Jorge Miguel Lobo Escalona 2022-07-28 11:26:58 +02:00 committed by Tino Vázquez
parent 665e2bb802
commit 54bc59dbdd
No known key found for this signature in database
GPG Key ID: 14201E424D02047E
22 changed files with 1505 additions and 10 deletions

View File

@ -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,

View File

@ -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 (
<DatastoresTable
singleSelect
disableGlobalSort
displaySelectedRows
pageSize={5}
getRowId={(row) => 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

View File

@ -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 = () => (
<FormWithSchema id={STEP_ID} fields={FIELDS} cy={`${STEP_ID}`} />
)
/**
* General configuration about VM Template.
*
* @returns {object} General configuration step
*/
const General = () => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default General

View File

@ -0,0 +1,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))

View File

@ -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

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
export { default } from 'client/components/Forms/File/CreateForm/Steps'

View File

@ -0,0 +1,27 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
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 }

View File

@ -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) {

View File

@ -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 (
<Typography
key={`file-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListFilesNames rows={rows} />
const MessageToConfirmAction = (rows) => (
<>
<ListFilesNames rows={rows} />
<Translate word={T.DoYouWantProceed} />
</>
)
/**
* Generates the actions to operate resources on Image table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
const [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

View File

@ -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',
},
]

View File

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { 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 (
<EnhancedTable
columns={columns}
data={useMemo(() => 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

View File

@ -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 (
<div {...props} data-cy={`image-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<StatusCircle color={stateColor} tooltip={stateName} />
<Typography noWrap component="span" data-cy="name">
{NAME}
</Typography>
{locked && <Lock />}
<span className={classes.labels}>
{labels.map((label) => (
<StatusChip key={label} text={label} />
))}
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
<span title={`Owner: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
<span title={`Datastore: ${DATASTORE}`}>
<Folder />
<span>{` ${DATASTORE}`}</span>
</span>
<span title={`Running / Used VMs: ${RUNNING_VMS} / ${TOTAL_VMS}`}>
<ModernTv />
<span>{` ${RUNNING_VMS} / ${TOTAL_VMS}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}></div>
</div>
)
}
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
}
export default Row

View File

@ -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,

View File

@ -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 (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
image={image}
actions={getActions(informationPanel?.actions)}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
handleEdit={handleChangePermission}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
handleEdit={handleChangeOwnership}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
/>
)}
{attributesPanel?.enabled && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={TEMPLATE}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>
)}
</Stack>
)
}
ImageInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ImageInfoTab.displayName = 'ImageInfoTab'
export default ImageInfoTab

View File

@ -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: <StatusChip text={stateName} stateColor={stateColor} />,
dataCy: 'state',
},
{
name: T.RunningVMs,
value: `${[VMS?.ID ?? []].flat().length || 0}`,
},
]
return (
<>
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
</>
)
}
InformationPanel.propTypes = {
image: PropTypes.object,
actions: PropTypes.arrayOf(PropTypes.string),
}
InformationPanel.displayName = 'InformationPanel'
export default InformationPanel

View File

@ -0,0 +1,62 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { 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 (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
return isLoading ? (
<LinearProgress color="secondary" sx={{ width: '100%' }} />
) : (
<Tabs addBorder tabs={tabsAvailable ?? []} />
)
})
FileTabs.propTypes = { id: PropTypes.string.isRequired }
FileTabs.displayName = 'FileTabs'
export default FileTabs

View File

@ -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',

View File

@ -155,6 +155,7 @@ export const RESOURCE_NAMES = {
GROUP: 'group',
HOST: 'host',
IMAGE: 'image',
FILE: 'file',
MARKETPLACE: 'marketplace',
SEC_GROUP: 'security-group',
USER: 'user',

View File

@ -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',

View File

@ -0,0 +1,81 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { useHistory } from 'react-router'
import { jsonToXml } from 'client/models/Helper'
import { useGeneralApi } from 'client/features/General'
import {
useAllocateImageMutation,
useUploadImageMutation,
} from 'client/features/OneApi/image'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/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 (
<CreateForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
)
}
export default CreateFile

View File

@ -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 (
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<FilesTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
/>
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
file={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* 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 (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mb={1}>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => getImage({ id })}
/>
{typeof gotoPage === 'function' && (
<SubmitButton
data-cy="locate-on-table"
icon={<GotoIcon />}
tooltip={Tr(T.LocateOnTable)}
onClick={() => gotoPage()}
/>
)}
{typeof unselect === 'function' && (
<SubmitButton
data-cy="unselect"
icon={<Cancel />}
tooltip={Tr(T.Close)}
onClick={() => unselect()}
/>
)}
<Typography color="text.primary" noWrap>
{`#${id} | ${name}`}
</Typography>
</Stack>
<FileTabs id={file.ID} />
</Stack>
)
})
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 = [] }) => (
<Stack direction="row" flexWrap="wrap" gap={1} alignContent="flex-start">
<MultipleTags
limitTags={10}
tags={tags?.map(({ original, id, toggleRowSelected, gotoPage }) => (
<Chip
key={id}
label={original?.NAME ?? id}
onClick={gotoPage}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
))
GroupedTags.propTypes = { tags: PropTypes.array }
GroupedTags.displayName = 'GroupedTags'
export default Files

View File

@ -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,