mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
(cherry picked from commit b1b37fb77cd5f827983d9a26545fc72a2b323dd0)
This commit is contained in:
parent
665e2bb802
commit
54bc59dbdd
@ -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,
|
||||
|
@ -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
|
@ -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
|
@ -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))
|
@ -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
|
@ -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'
|
27
src/fireedge/src/client/components/Forms/File/index.js
Normal file
27
src/fireedge/src/client/components/Forms/File/index.js
Normal 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 }
|
@ -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) {
|
||||
|
211
src/fireedge/src/client/components/Tables/Files/actions.js
Normal file
211
src/fireedge/src/client/components/Tables/Files/actions.js
Normal 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
|
66
src/fireedge/src/client/components/Tables/Files/columns.js
Normal file
66
src/fireedge/src/client/components/Tables/Files/columns.js
Normal 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',
|
||||
},
|
||||
]
|
67
src/fireedge/src/client/components/Tables/Files/index.js
Normal file
67
src/fireedge/src/client/components/Tables/Files/index.js
Normal 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
|
101
src/fireedge/src/client/components/Tables/Files/row.js
Normal file
101
src/fireedge/src/client/components/Tables/Files/row.js
Normal 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
|
@ -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,
|
||||
|
145
src/fireedge/src/client/components/Tabs/File/Info/index.js
Normal file
145
src/fireedge/src/client/components/Tabs/File/Info/index.js
Normal 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
|
166
src/fireedge/src/client/components/Tabs/File/Info/information.js
Normal file
166
src/fireedge/src/client/components/Tabs/File/Info/information.js
Normal 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
|
62
src/fireedge/src/client/components/Tabs/File/index.js
Normal file
62
src/fireedge/src/client/components/Tabs/File/index.js
Normal 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
|
@ -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',
|
||||
|
@ -155,6 +155,7 @@ export const RESOURCE_NAMES = {
|
||||
GROUP: 'group',
|
||||
HOST: 'host',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
MARKETPLACE: 'marketplace',
|
||||
SEC_GROUP: 'security-group',
|
||||
USER: 'user',
|
||||
|
@ -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',
|
||||
|
81
src/fireedge/src/client/containers/Files/Create.js
Normal file
81
src/fireedge/src/client/containers/Files/Create.js
Normal 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
|
156
src/fireedge/src/client/containers/Files/index.js
Normal file
156
src/fireedge/src/client/containers/Files/index.js
Normal 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
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user