mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-25 02:50:08 +03:00
Merge branch 'master' of https://github.com/OpenNebula/one-ee
This commit is contained in:
commit
7a658cdc36
@ -54,7 +54,10 @@ public:
|
||||
|
||||
~Driver()
|
||||
{
|
||||
stream_thr.join();
|
||||
if (stream_thr.joinable())
|
||||
{
|
||||
stream_thr.join();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2906,6 +2906,7 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vm-template-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/marketplace-app-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vnet-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/image-tab.yaml\
|
||||
src/fireedge/etc/sunstone/admin/host-tab.yaml"
|
||||
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \
|
||||
|
@ -28,6 +28,8 @@
|
||||
</xs:element>
|
||||
|
||||
<xs:element name="CLUSTER_ENCRYPTED_ATTR" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="CONTEXT_RESTRICTED_DIRS" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="CONTEXT_SAFE_DIRS" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="DATASTORE_CAPACITY_CHECK" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="DATASTORE_ENCRYPTED_ATTR" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="DATASTORE_LOCATION" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
|
@ -239,21 +239,21 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
number = options[:multiple] || 1
|
||||
params = {}
|
||||
rc = 0
|
||||
client = helper.client(options)
|
||||
|
||||
number.times do
|
||||
params['merge_template'] = nil
|
||||
params['merge_template'] = JSON.parse(File.read(args[1])) if args[1]
|
||||
|
||||
unless params['merge_template']
|
||||
secret = "#{options[:username]}:#{options[:password]}"
|
||||
one_client = OpenNebula::Client.new(secret)
|
||||
response = client.get("#{RESOURCE_PATH}/#{args[0]}")
|
||||
|
||||
service_template = OpenNebula::ServiceTemplate.new_with_id(
|
||||
args[0], one_client
|
||||
)
|
||||
service_template.info
|
||||
if CloudClient.is_error?(response)
|
||||
rc = [response.code.to_i, response.to_s]
|
||||
break
|
||||
end
|
||||
|
||||
body = JSON.parse(service_template['/DOCUMENT/TEMPLATE/BODY'])
|
||||
body = JSON.parse(response.body)['DOCUMENT']['TEMPLATE']['BODY']
|
||||
|
||||
params['merge_template'] = helper.custom_attrs(
|
||||
body['custom_attrs']
|
||||
@ -266,7 +266,6 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
end
|
||||
|
||||
json = Service.build_json_action('instantiate', params)
|
||||
client = helper.client(options)
|
||||
response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json)
|
||||
|
||||
if CloudClient.is_error?(response)
|
||||
|
@ -25,23 +25,25 @@ resource_name: "IMAGE"
|
||||
|
||||
actions:
|
||||
create_dialog: true
|
||||
clone_dialog: true
|
||||
import_dialog: true
|
||||
create_app_dialog: true # reference to create_dialog in marketplace-app-tab.yaml
|
||||
chown: true
|
||||
chgrp: true
|
||||
enable: true
|
||||
disable: true
|
||||
persistent: true
|
||||
nonpersistent: true
|
||||
delete: true
|
||||
dockerfile_dialog: false
|
||||
clone: true
|
||||
lock: true
|
||||
unlock: true
|
||||
disable: true
|
||||
enable: true
|
||||
persistent: true
|
||||
nonpersistent: true
|
||||
chown: true
|
||||
chgrp: true
|
||||
delete: true
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
filters:
|
||||
label: true
|
||||
state: true
|
||||
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
|
||||
@ -52,8 +54,6 @@ info-tabs:
|
||||
enabled: true
|
||||
actions:
|
||||
rename: true
|
||||
chtype: true
|
||||
persistent: true
|
||||
permissions_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
@ -70,13 +70,11 @@ info-tabs:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
|
||||
vm:
|
||||
vms:
|
||||
enabled: true
|
||||
|
||||
snapshot:
|
||||
enabled: true
|
||||
actions:
|
||||
snapshot_flatten: true
|
||||
snapshot_revert: true
|
||||
snapshot_delete: true
|
||||
snapshot_delete: true
|
@ -29,6 +29,7 @@ import { useGeneralApi } from 'client/features/General'
|
||||
import systemApi from 'client/features/OneApi/system'
|
||||
import Sidebar from 'client/components/Sidebar'
|
||||
import Notifier from 'client/components/Notifier'
|
||||
import NotifierUpload from 'client/components/Notifier/upload'
|
||||
import { AuthLayout } from 'client/components/HOC'
|
||||
import { isDevelopment } from 'client/utils'
|
||||
import { _APPS } from 'client/constants'
|
||||
@ -73,6 +74,7 @@ const SunstoneApp = () => {
|
||||
<>
|
||||
<Sidebar endpoints={endpoints} />
|
||||
<Notifier />
|
||||
<NotifierUpload />
|
||||
</>
|
||||
)}
|
||||
<Router redirectWhenAuth={PATH.DASHBOARD} endpoints={endpoints} />
|
||||
|
@ -97,6 +97,15 @@ const Datastores = loadable(() => import('client/containers/Datastores'), {
|
||||
const Images = loadable(() => import('client/containers/Images'), {
|
||||
ssr: false,
|
||||
})
|
||||
const CreateImages = loadable(() => import('client/containers/Images/Create'), {
|
||||
ssr: false,
|
||||
})
|
||||
const CreateDockerfile = loadable(
|
||||
() => import('client/containers/Images/Dockerfile'),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
)
|
||||
const Marketplaces = loadable(() => import('client/containers/Marketplaces'), {
|
||||
ssr: false,
|
||||
})
|
||||
@ -189,6 +198,8 @@ export const PATH = {
|
||||
IMAGES: {
|
||||
LIST: `/${RESOURCE_NAMES.IMAGE}`,
|
||||
DETAIL: `/${RESOURCE_NAMES.IMAGE}/:id`,
|
||||
CREATE: `/${RESOURCE_NAMES.IMAGE}/create`,
|
||||
DOCKERFILE: `/${RESOURCE_NAMES.IMAGE}/dockerfile`,
|
||||
},
|
||||
MARKETPLACES: {
|
||||
LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
|
||||
@ -364,6 +375,16 @@ const ENDPOINTS = [
|
||||
icon: ImageIcon,
|
||||
Component: Images,
|
||||
},
|
||||
{
|
||||
title: T.CreateImage,
|
||||
path: PATH.STORAGE.IMAGES.CREATE,
|
||||
Component: CreateImages,
|
||||
},
|
||||
{
|
||||
title: T.CreateDockerfile,
|
||||
path: PATH.STORAGE.IMAGES.DOCKERFILE,
|
||||
Component: CreateDockerfile,
|
||||
},
|
||||
{
|
||||
title: T.Marketplaces,
|
||||
path: PATH.STORAGE.MARKETPLACES.LIST,
|
||||
|
98
src/fireedge/src/client/components/Cards/ImageCreateCard.js
Normal file
98
src/fireedge/src/client/components/Cards/ImageCreateCard.js
Normal file
@ -0,0 +1,98 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Typography, Paper, Grid, Box } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
({ palette, typography, breakpoints, shadows }) => ({
|
||||
root: {
|
||||
padding: '0.8em',
|
||||
color: palette.text.primary,
|
||||
backgroundColor: palette.background.paper,
|
||||
fontWeight: typography.fontWeightRegular,
|
||||
fontSize: '1em',
|
||||
borderRadius: 6,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
cursor: 'pointer',
|
||||
[breakpoints.down('md')]: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
},
|
||||
figure: {
|
||||
flexBasis: '10%',
|
||||
aspectRatio: '16/9',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
main: {
|
||||
flex: 'auto',
|
||||
overflow: 'hidden',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
title: {
|
||||
color: palette.text.primary,
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const ImageCreateCard = memo(
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.name - Card name
|
||||
* @param {Function} props.onClick - Card name
|
||||
* @param {ReactElement} props.Icon - Card Icon
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ name = '', onClick, Icon }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Grid item xs={12} md={6} onClick={onClick} data-cy="create">
|
||||
<Paper variant="outlined" className={classes.root}>
|
||||
{Icon && (
|
||||
<Box className={classes.figure}>
|
||||
<Icon />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography noWrap component="span">
|
||||
{name}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ImageCreateCard.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
Icon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
|
||||
}
|
||||
|
||||
ImageCreateCard.displayName = 'ImageCreateCard'
|
||||
|
||||
export default ImageCreateCard
|
@ -0,0 +1,99 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ModernTv } from 'iconoir-react'
|
||||
import { Typography, Paper } from '@mui/material'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
|
||||
import { stringToBoolean, timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import { T, DiskSnapshot } from 'client/constants'
|
||||
|
||||
const ImageSnapshotCard = memo(
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {DiskSnapshot} props.snapshot - Disk snapshot
|
||||
* @param {function({ snapshot: DiskSnapshot }):ReactElement} [props.actions] - Actions
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ snapshot = {}, actions }) => {
|
||||
const classes = rowStyles()
|
||||
|
||||
const {
|
||||
ID,
|
||||
NAME,
|
||||
ACTIVE,
|
||||
DATE,
|
||||
SIZE: SNAPSHOT_SIZE,
|
||||
MONITOR_SIZE: SNAPSHOT_MONITOR_SIZE,
|
||||
} = snapshot
|
||||
|
||||
const isActive = useMemo(() => stringToBoolean(ACTIVE), [ACTIVE])
|
||||
const time = useMemo(() => timeFromMilliseconds(+DATE), [DATE])
|
||||
const timeFormat = useMemo(() => time.toFormat('ff'), [DATE])
|
||||
const timeAgo = useMemo(() => `created ${time.toRelative()}`, [DATE])
|
||||
|
||||
const sizeInfo = useMemo(() => {
|
||||
const size = +SNAPSHOT_SIZE ? prettyBytes(+SNAPSHOT_SIZE, 'MB') : '-'
|
||||
const monitorSize = +SNAPSHOT_MONITOR_SIZE
|
||||
? prettyBytes(+SNAPSHOT_MONITOR_SIZE, 'MB')
|
||||
: '-'
|
||||
|
||||
return `${monitorSize}/${size}`
|
||||
}, [SNAPSHOT_SIZE, SNAPSHOT_MONITOR_SIZE])
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.root}>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography noWrap component="span">
|
||||
{NAME}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
{isActive && <StatusChip text={<Translate word={T.Active} />} />}
|
||||
<StatusChip text={<Translate word={T.Snapshot} />} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={classes.caption}>
|
||||
<span title={timeFormat}>{`#${ID} ${timeAgo}`}</span>
|
||||
<span
|
||||
title={`${Tr(T.Monitoring)} / ${Tr(T.DiskSize)}: ${sizeInfo}`}
|
||||
>
|
||||
<ModernTv />
|
||||
<span>{` ${sizeInfo}`}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{typeof actions === 'function' && (
|
||||
<div className={classes.actions}>{actions({ snapshot })}</div>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ImageSnapshotCard.propTypes = {
|
||||
snapshot: PropTypes.object.isRequired,
|
||||
actions: PropTypes.func,
|
||||
}
|
||||
|
||||
ImageSnapshotCard.displayName = 'ImageSnapshotCard'
|
||||
|
||||
export default ImageSnapshotCard
|
@ -23,6 +23,8 @@ import DiskCard from 'client/components/Cards/DiskCard'
|
||||
import DiskSnapshotCard from 'client/components/Cards/DiskSnapshotCard'
|
||||
import EmptyCard from 'client/components/Cards/EmptyCard'
|
||||
import HostCard from 'client/components/Cards/HostCard'
|
||||
import ImageCreateCard from 'client/components/Cards/ImageCreateCard'
|
||||
import ImageSnapshotCard from 'client/components/Cards/ImageSnapshotCard'
|
||||
import MarketplaceAppCard from 'client/components/Cards/MarketplaceAppCard'
|
||||
import MarketplaceCard from 'client/components/Cards/MarketplaceCard'
|
||||
import NetworkCard from 'client/components/Cards/NetworkCard'
|
||||
@ -52,6 +54,8 @@ export {
|
||||
DiskSnapshotCard,
|
||||
EmptyCard,
|
||||
HostCard,
|
||||
ImageCreateCard,
|
||||
ImageSnapshotCard,
|
||||
MarketplaceAppCard,
|
||||
MarketplaceCard,
|
||||
NetworkCard,
|
||||
|
@ -0,0 +1,69 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
|
||||
import { generateKey } from 'client/utils'
|
||||
import InputCode from 'client/components/FormControl/InputCode'
|
||||
|
||||
const DockerfileController = memo(
|
||||
({ control, cy = `input-${generateKey()}`, name = '' }) => {
|
||||
const {
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useFormContext()
|
||||
|
||||
const [internalError, setInternalError] = useState()
|
||||
const messageError = name
|
||||
.split('.')
|
||||
.reduce((errs, current) => errs?.[current], errors)?.message?.[0]
|
||||
|
||||
useEffect(() => {
|
||||
setInternalError(messageError)
|
||||
}, [messageError])
|
||||
|
||||
return (
|
||||
<div data-cy={cy}>
|
||||
<InputCode
|
||||
mode="dockerfile"
|
||||
height="600px"
|
||||
value={getValues(name)}
|
||||
onChange={(value) => {
|
||||
setValue(name, value)
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
setInternalError()
|
||||
}}
|
||||
/>
|
||||
{internalError && <ErrorHelper label={internalError} />}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.cy === nextProps.cy
|
||||
)
|
||||
|
||||
DockerfileController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
DockerfileController.displayName = 'DockerfileController'
|
||||
|
||||
export default DockerfileController
|
@ -35,9 +35,9 @@ const WrapperToLoadMode = ({ children, mode }) => {
|
||||
|
||||
return () => {
|
||||
// remove all styles when component will be unmounted
|
||||
document
|
||||
.querySelectorAll('[id^=ace]')
|
||||
.forEach((child) => child.parentNode.removeChild(child))
|
||||
document.querySelectorAll('[id^=ace-]').forEach((child) => {
|
||||
child.parentNode.removeChild(child)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -29,6 +29,7 @@ import SubmitButton, {
|
||||
SubmitButtonPropTypes,
|
||||
} from 'client/components/FormControl/SubmitButton'
|
||||
import InputCode from 'client/components/FormControl/InputCode'
|
||||
import DockerfileController from 'client/components/FormControl/DockerfileController'
|
||||
import ErrorHelper from 'client/components/FormControl/ErrorHelper'
|
||||
import Tooltip from 'client/components/FormControl/Tooltip'
|
||||
|
||||
@ -47,6 +48,7 @@ export {
|
||||
SubmitButton,
|
||||
SubmitButtonPropTypes,
|
||||
InputCode,
|
||||
DockerfileController,
|
||||
ErrorHelper,
|
||||
Tooltip,
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.TIME]: FC.TimeController,
|
||||
[INPUT_TYPES.TABLE]: FC.TableController,
|
||||
[INPUT_TYPES.TOGGLE]: FC.ToggleController,
|
||||
[INPUT_TYPES.DOCKERFILE]: FC.DockerfileController,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,59 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/Image/CloneForm/Steps/BasicConfiguration/schema'
|
||||
import { Step } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'configuration'
|
||||
|
||||
const Content = (props) => (
|
||||
<FormWithSchema
|
||||
cy="clone-configuration"
|
||||
id={STEP_ID}
|
||||
fields={() => FIELDS(props)}
|
||||
/>
|
||||
)
|
||||
|
||||
/**
|
||||
* Step to configure the marketplace app.
|
||||
*
|
||||
* @param {object} isMultiple - is multiple rows
|
||||
* @returns {Step} Configuration step
|
||||
*/
|
||||
const ConfigurationStep = (isMultiple) => ({
|
||||
id: STEP_ID,
|
||||
label: T.Configuration,
|
||||
resolver: () => SCHEMA(isMultiple),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: () => Content(isMultiple),
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
nics: PropTypes.array,
|
||||
isMultiple: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default ConfigurationStep
|
@ -0,0 +1,55 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, object, ObjectSchema } from 'yup'
|
||||
import { Field, getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const PREFIX = {
|
||||
name: 'prefix',
|
||||
label: T.Prefix,
|
||||
tooltip: T.PrefixMultipleConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => T.CopyOf),
|
||||
}
|
||||
|
||||
const NAME = {
|
||||
name: 'name',
|
||||
label: T.Name,
|
||||
tooltip: T.NewTemplateNameConcept,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [stepProps] - Step props
|
||||
* @param {boolean} [stepProps.isMultiple]
|
||||
* - If true, the prefix will be added to the name of the new template
|
||||
* @returns {Field[]} Fields
|
||||
*/
|
||||
export const FIELDS = ({ isMultiple } = {}) => [isMultiple ? PREFIX : NAME]
|
||||
|
||||
/**
|
||||
* @param {object} [stepProps] - Step props
|
||||
* @returns {ObjectSchema} Schema
|
||||
*/
|
||||
export const SCHEMA = (stepProps) =>
|
||||
object(getValidationFromFields(FIELDS(stepProps)))
|
@ -0,0 +1,79 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { DatastoresTable } from 'client/components/Tables'
|
||||
import { SCHEMA } from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable/schema'
|
||||
|
||||
import { Step, decodeBase64 } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'datastore'
|
||||
|
||||
const Content = ({ data, app }) => {
|
||||
const { NAME } = data?.[0] ?? {}
|
||||
const { setValue } = useFormContext()
|
||||
|
||||
const isKernelType = useMemo(() => {
|
||||
const appTemplate = String(decodeBase64(app?.TEMPLATE?.APPTEMPLATE64, ''))
|
||||
|
||||
return appTemplate.includes('TYPE="KERNEL"')
|
||||
}, [])
|
||||
|
||||
const handleSelectedRows = (rows) => {
|
||||
const { original = {} } = rows?.[0] ?? {}
|
||||
|
||||
setValue(STEP_ID, original.ID !== undefined ? [original] : [])
|
||||
}
|
||||
|
||||
return (
|
||||
<DatastoresTable
|
||||
singleSelect
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
pageSize={5}
|
||||
getRowId={(row) => String(row.NAME)}
|
||||
initialState={{
|
||||
selectedRowIds: { [NAME]: true },
|
||||
filters: [{ id: 'TYPE', value: isKernelType ? 'FILE' : 'IMAGE' }],
|
||||
}}
|
||||
onSelectedRowsChange={handleSelectedRows}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Step to select the Datastore.
|
||||
*
|
||||
* @param {object} app - Marketplace App resource
|
||||
* @returns {Step} Datastore step
|
||||
*/
|
||||
const DatastoreStep = (app) => ({
|
||||
id: STEP_ID,
|
||||
label: T.SelectDatastoreImage,
|
||||
resolver: SCHEMA,
|
||||
content: (props) => Content({ ...props, app }),
|
||||
})
|
||||
|
||||
Content.propTypes = {
|
||||
data: PropTypes.any,
|
||||
setFormData: PropTypes.func,
|
||||
app: PropTypes.object,
|
||||
}
|
||||
|
||||
export default DatastoreStep
|
@ -0,0 +1,24 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { array, object, ArraySchema } from 'yup'
|
||||
|
||||
/** @type {ArraySchema} Datastore table schema */
|
||||
export const SCHEMA = array(object())
|
||||
.min(1)
|
||||
.max(1)
|
||||
.required()
|
||||
.ensure()
|
||||
.default(() => [])
|
@ -0,0 +1,41 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import BasicConfiguration, {
|
||||
STEP_ID as BASIC_ID,
|
||||
} from 'client/components/Forms/Image/CloneForm/Steps/BasicConfiguration'
|
||||
import DatastoresTable, {
|
||||
STEP_ID as DATASTORE_ID,
|
||||
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
|
||||
import { createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
(app) => [BasicConfiguration, DatastoresTable].filter(Boolean),
|
||||
{
|
||||
transformInitialValue: (app, schema) =>
|
||||
schema.cast({}, { context: { app } }),
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const { [BASIC_ID]: configuration, [DATASTORE_ID]: [datastore] = [] } =
|
||||
formData
|
||||
|
||||
return {
|
||||
datastore: datastore?.ID,
|
||||
...configuration,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
@ -0,0 +1,41 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'dockerfile'
|
||||
|
||||
const Content = () => <FormWithSchema id={STEP_ID} fields={FIELDS} />
|
||||
|
||||
/**
|
||||
* Docker file.
|
||||
*
|
||||
* @returns {object} Dockerfile step
|
||||
*/
|
||||
const DockerFile = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.Dockerfile,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
|
||||
export default DockerFile
|
@ -0,0 +1,41 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, object, ObjectSchema } from 'yup'
|
||||
import { Field, getValidationFromFields, encodeBase64 } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
/** @type {Field} Dockerfile field */
|
||||
export const DOCKERFILE = {
|
||||
name: 'PATH',
|
||||
label: T.Dockerfile,
|
||||
type: INPUT_TYPES.DOCKERFILE,
|
||||
cy: 'dockerfile',
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.afterSubmit((value) => encodeBase64(value)),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Field[]} Fields
|
||||
*/
|
||||
export const FIELDS = [DOCKERFILE]
|
||||
|
||||
/**
|
||||
* @returns {ObjectSchema} Schema
|
||||
*/
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -0,0 +1,42 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/Image/CreateDockerfile/Steps/General/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'general'
|
||||
|
||||
const Content = () => <FormWithSchema id={STEP_ID} fields={FIELDS} />
|
||||
|
||||
/**
|
||||
* 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,64 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, boolean, number, object, ObjectSchema } from 'yup'
|
||||
import { Field, getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
export const IMAGE_LOCATION_TYPES = {
|
||||
PATH: 'path',
|
||||
UPLOAD: 'upload',
|
||||
EMPTY: 'empty',
|
||||
}
|
||||
|
||||
/** @type {Field} Name field */
|
||||
export const NAME = {
|
||||
name: 'NAME',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().required(),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Context field */
|
||||
export const CONTEXT = {
|
||||
name: 'CONTEXT',
|
||||
label: T.Context,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Size field */
|
||||
export const SIZE = {
|
||||
name: 'SIZE',
|
||||
htmlType: 'number',
|
||||
label: T.Size,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
tooltip: T.ImageSize,
|
||||
validation: number().positive().required(),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Field[]} Fields
|
||||
*/
|
||||
export const FIELDS = [NAME, CONTEXT, SIZE]
|
||||
|
||||
/**
|
||||
* @param {object} [stepProps] - Step props
|
||||
* @returns {ObjectSchema} Schema
|
||||
*/
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -0,0 +1,63 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import Datastore, {
|
||||
STEP_ID as DATASTORE_ID,
|
||||
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
|
||||
|
||||
import General, {
|
||||
STEP_ID as GENERAL_ID,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/General'
|
||||
|
||||
import AdvancedOptions, {
|
||||
STEP_ID as ADVANCE_ID,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions'
|
||||
|
||||
import CustomAttributes, {
|
||||
STEP_ID as CUSTOM_ID,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/CustomAttributes'
|
||||
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { createSteps, cloneObject, set } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[General, Datastore, AdvancedOptions, CustomAttributes],
|
||||
{
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[DATASTORE_ID]: [datastore] = [],
|
||||
[ADVANCE_ID]: advanced = {},
|
||||
[CUSTOM_ID]: custom = {},
|
||||
} = formData ?? {}
|
||||
|
||||
const generalData = cloneObject(general)
|
||||
set(generalData, 'UPLOAD', undefined)
|
||||
set(generalData, 'IMAGE_LOCATION', undefined)
|
||||
|
||||
return {
|
||||
template: jsonToXml({
|
||||
...custom,
|
||||
...advanced,
|
||||
...generalData,
|
||||
}),
|
||||
datastore: datastore?.ID,
|
||||
file: general?.UPLOAD,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
@ -0,0 +1,55 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import Datastore, {
|
||||
STEP_ID as DATASTORE_ID,
|
||||
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
|
||||
|
||||
import General, {
|
||||
STEP_ID as GENERAL_ID,
|
||||
} from 'client/components/Forms/Image/CreateDockerfile/Steps/General'
|
||||
|
||||
import Dockerfile, {
|
||||
STEP_ID as DOCKERFILE_ID,
|
||||
} from 'client/components/Forms/Image/CreateDockerfile/Steps/Dockerfile'
|
||||
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { createSteps, cloneObject, set } from 'client/utils'
|
||||
|
||||
const Steps = createSteps([General, Datastore, Dockerfile], {
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[DATASTORE_ID]: [datastore] = [],
|
||||
[DOCKERFILE_ID]: dockerfile = {},
|
||||
} = formData ?? {}
|
||||
|
||||
const generalData = cloneObject(general)
|
||||
set(generalData, 'CONTEXT', undefined)
|
||||
set(generalData, 'SIZE', undefined)
|
||||
|
||||
return {
|
||||
template: jsonToXml({
|
||||
...{
|
||||
PATH: `dockerfile://?fileb64=${dockerfile.PATH}&context=${general.CONTEXT}&size=${general.SIZE}`,
|
||||
},
|
||||
...generalData,
|
||||
}),
|
||||
datastore: datastore?.ID,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default Steps
|
@ -0,0 +1,43 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions/schema'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'advanced'
|
||||
|
||||
const Content = () => (
|
||||
<FormWithSchema id={STEP_ID} fields={FIELDS} cy={`${STEP_ID}`} />
|
||||
)
|
||||
|
||||
/**
|
||||
* Advanced options create image.
|
||||
*
|
||||
* @returns {object} Advanced options configuration step
|
||||
*/
|
||||
const AdvancedOptions = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.AdvancedOptions,
|
||||
resolver: SCHEMA,
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
|
||||
export default AdvancedOptions
|
@ -0,0 +1,180 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, object, ObjectSchema } from 'yup'
|
||||
import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
import {
|
||||
IMAGE_LOCATION_TYPES,
|
||||
IMAGE_LOCATION_FIELD,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/General/schema'
|
||||
import { useGetSunstoneConfigQuery } from 'client/features/OneApi/system'
|
||||
|
||||
export const BUS_TYPES = {
|
||||
VD: 'path',
|
||||
SD: 'upload',
|
||||
HD: 'empty',
|
||||
CUSTOM: 'custom',
|
||||
}
|
||||
|
||||
const FORMAT_TYPES = {
|
||||
RAW: 'raw',
|
||||
QCOW2: 'qcow2',
|
||||
CUSTOM: 'custom',
|
||||
}
|
||||
|
||||
const BUS = {
|
||||
[BUS_TYPES.VD]: T.Vd,
|
||||
[BUS_TYPES.SD]: T.Sd,
|
||||
[BUS_TYPES.HD]: T.Hd,
|
||||
[BUS_TYPES.CUSTOM]: T.Custom,
|
||||
}
|
||||
|
||||
const htmlType = (opt) => (value) => value !== opt && INPUT_TYPES.HIDDEN
|
||||
|
||||
/** @type {Field} Bus field */
|
||||
export const DEV_PREFIX = {
|
||||
name: 'DEV_PREFIX',
|
||||
label: T.Bus,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.entries(BUS), {
|
||||
addEmpty: true,
|
||||
getText: ([_, name]) => name,
|
||||
getValue: ([key]) => key,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => undefined)
|
||||
.afterSubmit((value, { context }) => {
|
||||
const notEmptyString = value === '' ? undefined : value
|
||||
|
||||
return BUS_TYPES.CUSTOM === value
|
||||
? context?.advanced?.CUSTOM_DEV_PREFIX
|
||||
: notEmptyString
|
||||
}),
|
||||
grid: { md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Dev Prefix field */
|
||||
export const CUSTOM_DEV_PREFIX = {
|
||||
name: 'CUSTOM_DEV_PREFIX',
|
||||
dependOf: DEV_PREFIX.name,
|
||||
htmlType: htmlType(BUS_TYPES.CUSTOM),
|
||||
label: T.CustomBus,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.when(DEV_PREFIX.name, {
|
||||
is: (location) => location === BUS_TYPES.CUSTOM,
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: (schema) => schema.strip(),
|
||||
})
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} Device field */
|
||||
export const DEVICE = {
|
||||
name: 'DEVICE',
|
||||
label: T.TargetDevice,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => undefined),
|
||||
grid: { md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Format field */
|
||||
export const FORMAT_FIELD = {
|
||||
name: 'FORMAT',
|
||||
dependOf: `$general.${IMAGE_LOCATION_FIELD.name}`,
|
||||
htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY),
|
||||
label: T.Format,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(FORMAT_TYPES), {
|
||||
addEmpty: true,
|
||||
getText: (type) => type.toUpperCase(),
|
||||
getValue: (type) => type,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.afterSubmit((value, { context }) => {
|
||||
const notEmptyString = value === '' ? undefined : value
|
||||
|
||||
return FORMAT_TYPES.CUSTOM === value
|
||||
? context?.advanced?.CUSTOM_FORMAT
|
||||
: notEmptyString
|
||||
}),
|
||||
grid: { md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Custom format field */
|
||||
export const CUSTOM_FORMAT = {
|
||||
name: 'CUSTOM_FORMAT',
|
||||
dependOf: [`$general.${IMAGE_LOCATION_FIELD.name}`, FORMAT_FIELD.name],
|
||||
htmlType: ([imageLocation, formatField] = []) =>
|
||||
(imageLocation !== IMAGE_LOCATION_TYPES.EMPTY ||
|
||||
formatField !== FORMAT_TYPES.CUSTOM) &&
|
||||
INPUT_TYPES.HIDDEN,
|
||||
label: T.CustomFormat,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.when(FORMAT_FIELD.name, {
|
||||
is: (format) => format === FORMAT_TYPES.CUSTOM,
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: (schema) => schema.strip(),
|
||||
}),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} FS field */
|
||||
export const FS = {
|
||||
name: 'FS',
|
||||
dependOf: `$general.${IMAGE_LOCATION_FIELD.name}`,
|
||||
htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY),
|
||||
label: T.Fs,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () => {
|
||||
const { data: sunstoneConfig = {} } = useGetSunstoneConfigQuery()
|
||||
|
||||
return arrayToOptions(sunstoneConfig?.supported_fs || [], {
|
||||
addEmpty: true,
|
||||
getText: (fs) => fs.toUpperCase(),
|
||||
getValue: (fs) => fs,
|
||||
})
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.afterSubmit((value) => (value === '' ? undefined : value)),
|
||||
grid: { md: 6 },
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Field[]} Fields
|
||||
*/
|
||||
export const FIELDS = [
|
||||
DEV_PREFIX,
|
||||
DEVICE,
|
||||
CUSTOM_DEV_PREFIX,
|
||||
FORMAT_FIELD,
|
||||
FS,
|
||||
CUSTOM_FORMAT,
|
||||
]
|
||||
|
||||
/**
|
||||
* @returns {ObjectSchema} Schema
|
||||
*/
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -0,0 +1,67 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useCallback } from 'react'
|
||||
import { object } from 'yup'
|
||||
import { useFormContext, useWatch } from 'react-hook-form'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
import { AttributePanel } from 'client/components/Tabs/Common'
|
||||
import { cleanEmpty, cloneObject, set } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'custom-attributes'
|
||||
|
||||
const Content = () => {
|
||||
const { setValue } = useFormContext()
|
||||
const customVars = useWatch({ name: STEP_ID, defaultValue: {} })
|
||||
|
||||
const handleChangeAttribute = useCallback(
|
||||
(path, newValue) => {
|
||||
const newCustomVars = cloneObject(customVars)
|
||||
set(newCustomVars, path, newValue)
|
||||
setValue(STEP_ID, cleanEmpty(newCustomVars))
|
||||
},
|
||||
[customVars]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box display="grid" gap="1em">
|
||||
<AttributePanel
|
||||
allActionsEnabled
|
||||
handleAdd={handleChangeAttribute}
|
||||
handleEdit={handleChangeAttribute}
|
||||
handleDelete={handleChangeAttribute}
|
||||
attributes={customVars}
|
||||
filtersSpecialAttributes={false}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom variables about VM Template.
|
||||
*
|
||||
* @returns {object} Custom configuration step
|
||||
*/
|
||||
const CustomAttributes = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.CustomAttributes,
|
||||
resolver: object(),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
|
||||
export default CustomAttributes
|
@ -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/Image/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,185 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { string, boolean, number, object, ObjectSchema, mixed } from 'yup'
|
||||
import {
|
||||
Field,
|
||||
arrayToOptions,
|
||||
getValidationFromFields,
|
||||
upperCaseFirst,
|
||||
} from 'client/utils'
|
||||
import { T, INPUT_TYPES, IMAGE_TYPES_STR } from 'client/constants'
|
||||
|
||||
export const IMAGE_LOCATION_TYPES = {
|
||||
PATH: 'path',
|
||||
UPLOAD: 'upload',
|
||||
EMPTY: 'empty',
|
||||
}
|
||||
|
||||
const IMAGE_LOCATION = {
|
||||
[IMAGE_LOCATION_TYPES.PATH]: T.Path,
|
||||
[IMAGE_LOCATION_TYPES.UPLOAD]: T.Upload,
|
||||
[IMAGE_LOCATION_TYPES.EMPTY]: T.EmptyDisk,
|
||||
}
|
||||
|
||||
const htmlType = (opt, inputNumber) => (location) => {
|
||||
if (location === opt && inputNumber) {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return location !== opt && INPUT_TYPES.HIDDEN
|
||||
}
|
||||
|
||||
/** @type {Field} name field */
|
||||
export const NAME = {
|
||||
name: 'NAME',
|
||||
label: T.Name,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string().trim().required(),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Description field */
|
||||
export const DESCRIPTION = {
|
||||
name: 'DESCRIPTION',
|
||||
label: T.Description,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
multiline: true,
|
||||
validation: string().trim(),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Type field */
|
||||
export const TYPE = {
|
||||
name: 'TYPE',
|
||||
label: T.Type,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions(Object.values(IMAGE_TYPES_STR), {
|
||||
addEmpty: false,
|
||||
getText: (type) => {
|
||||
switch (type) {
|
||||
case IMAGE_TYPES_STR.OS:
|
||||
return T.Os
|
||||
case IMAGE_TYPES_STR.CDROM:
|
||||
return T.Cdrom
|
||||
case IMAGE_TYPES_STR.DATABLOCK:
|
||||
return T.Datablock
|
||||
default:
|
||||
return upperCaseFirst(type.toLowerCase())
|
||||
}
|
||||
},
|
||||
getValue: (type) => type,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.default(() => IMAGE_TYPES_STR.OS),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Persistent field */
|
||||
export const PERSISTENT = {
|
||||
name: 'PERSISTENT',
|
||||
label: T.MakePersistent,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Toggle select type image */
|
||||
export const IMAGE_LOCATION_FIELD = {
|
||||
name: 'IMAGE_LOCATION',
|
||||
type: INPUT_TYPES.TOGGLE,
|
||||
values: arrayToOptions(Object.entries(IMAGE_LOCATION), {
|
||||
addEmpty: false,
|
||||
getText: ([_, name]) => name,
|
||||
getValue: ([image]) => image,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => IMAGE_LOCATION_TYPES.PATH),
|
||||
grid: { md: 12 },
|
||||
notNull: true,
|
||||
}
|
||||
/** @type {Field} path field */
|
||||
export const PATH_FIELD = {
|
||||
name: 'PATH',
|
||||
dependOf: IMAGE_LOCATION_FIELD.name,
|
||||
htmlType: htmlType(IMAGE_LOCATION_TYPES.PATH),
|
||||
label: T.ImagePath,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.when(IMAGE_LOCATION_FIELD.name, {
|
||||
is: (location) => location === IMAGE_LOCATION_TYPES.PATH,
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: (schema) => schema.strip(),
|
||||
}),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} upload field */
|
||||
export const UPLOAD_FIELD = {
|
||||
name: 'UPLOAD',
|
||||
dependOf: IMAGE_LOCATION_FIELD.name,
|
||||
htmlType: htmlType(IMAGE_LOCATION_TYPES.UPLOAD),
|
||||
label: T.Upload,
|
||||
type: INPUT_TYPES.FILE,
|
||||
validation: mixed().when(IMAGE_LOCATION_FIELD.name, {
|
||||
is: (location) => location === IMAGE_LOCATION_TYPES.UPLOAD,
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: (schema) => schema.strip(),
|
||||
}),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/** @type {Field} size field */
|
||||
export const SIZE = {
|
||||
name: 'SIZE',
|
||||
dependOf: IMAGE_LOCATION_FIELD.name,
|
||||
htmlType: htmlType(IMAGE_LOCATION_TYPES.EMPTY, true),
|
||||
label: T.Size,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
tooltip: T.ImageSize,
|
||||
validation: number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
.when(IMAGE_LOCATION_FIELD.name, {
|
||||
is: (location) => location === IMAGE_LOCATION_TYPES.EMPTY,
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: (schema) => schema.strip(),
|
||||
}),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Field[]} Fields
|
||||
*/
|
||||
export const FIELDS = [
|
||||
NAME,
|
||||
DESCRIPTION,
|
||||
TYPE,
|
||||
PERSISTENT,
|
||||
IMAGE_LOCATION_FIELD,
|
||||
PATH_FIELD,
|
||||
UPLOAD_FIELD,
|
||||
SIZE,
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {object} [stepProps] - Step props
|
||||
* @returns {ObjectSchema} Schema
|
||||
*/
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -0,0 +1,62 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import Datastore, {
|
||||
STEP_ID as DATASTORE_ID,
|
||||
} from 'client/components/Forms/Image/CloneForm/Steps/DatastoresTable'
|
||||
|
||||
import General, {
|
||||
STEP_ID as GENERAL_ID,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/General'
|
||||
|
||||
import AdvancedOptions, {
|
||||
STEP_ID as ADVANCE_ID,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/AdvancedOptions'
|
||||
|
||||
import CustomAttributes, {
|
||||
STEP_ID as CUSTOM_ID,
|
||||
} from 'client/components/Forms/Image/CreateForm/Steps/CustomAttributes'
|
||||
|
||||
import { createSteps, cloneObject, set } from 'client/utils'
|
||||
|
||||
const Steps = createSteps(
|
||||
[General, Datastore, AdvancedOptions, CustomAttributes],
|
||||
{
|
||||
transformBeforeSubmit: (formData) => {
|
||||
const {
|
||||
[GENERAL_ID]: general = {},
|
||||
[DATASTORE_ID]: [datastore] = [],
|
||||
[ADVANCE_ID]: advanced = {},
|
||||
[CUSTOM_ID]: custom = {},
|
||||
} = formData ?? {}
|
||||
|
||||
const generalData = cloneObject(general)
|
||||
set(generalData, 'UPLOAD', undefined)
|
||||
set(generalData, 'IMAGE_LOCATION', undefined)
|
||||
|
||||
return {
|
||||
template: {
|
||||
...custom,
|
||||
...advanced,
|
||||
...generalData,
|
||||
},
|
||||
datastore: datastore?.ID,
|
||||
file: general?.UPLOAD,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export default Steps
|
@ -0,0 +1,16 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export { default } from 'client/components/Forms/Image/CreateForm/Steps'
|
41
src/fireedge/src/client/components/Forms/Image/index.js
Normal file
41
src/fireedge/src/client/components/Forms/Image/index.js
Normal file
@ -0,0 +1,41 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
|
||||
import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
|
||||
*/
|
||||
const CloneForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'Image/CloneForm' }, configProps)
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
|
||||
*/
|
||||
const CreateForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'Image/CreateForm' }, configProps)
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
|
||||
*/
|
||||
const CreateDockerfileForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'Image/CreateDockerfile' }, configProps)
|
||||
|
||||
export { CloneForm, CreateForm, CreateDockerfileForm }
|
@ -22,7 +22,7 @@ import AdvancedOptions, {
|
||||
import { createSteps } from 'client/utils'
|
||||
|
||||
const Steps = createSteps([NetworksTable, AdvancedOptions], {
|
||||
transformInitialValue: (nic) => {
|
||||
transformInitialValue: (nic, schema) => {
|
||||
const {
|
||||
NETWORK,
|
||||
NETWORK_ID: ID,
|
||||
@ -32,6 +32,11 @@ const Steps = createSteps([NetworksTable, AdvancedOptions], {
|
||||
...rest
|
||||
} = nic ?? {}
|
||||
|
||||
const castedValue = schema.cast(
|
||||
{ [ADVANCED_ID]: rest },
|
||||
{ stripUnknown: true }
|
||||
)
|
||||
|
||||
return {
|
||||
[NETWORK_ID]: [
|
||||
{
|
||||
@ -43,7 +48,7 @@ const Steps = createSteps([NetworksTable, AdvancedOptions], {
|
||||
SECURITY_GROUPS,
|
||||
},
|
||||
],
|
||||
[ADVANCED_ID]: rest,
|
||||
[ADVANCED_ID]: castedValue[ADVANCED_ID],
|
||||
}
|
||||
},
|
||||
transformBeforeSubmit: (formData) => {
|
||||
|
84
src/fireedge/src/client/components/Notifier/upload.js
Normal file
84
src/fireedge/src/client/components/Notifier/upload.js
Normal file
@ -0,0 +1,84 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import {
|
||||
Snackbar,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Box,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { useGeneral, useGeneralApi } from 'client/features/General'
|
||||
|
||||
/**
|
||||
* @returns {ReactElement} App rendered.
|
||||
*/
|
||||
const NotifierUpload = () => {
|
||||
const { upload } = useGeneral()
|
||||
const { uploadSnackbar } = useGeneralApi()
|
||||
|
||||
const handleClose = () => uploadSnackbar(0)
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={upload > 0}
|
||||
autoHideDuration={10000}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
icon={false}
|
||||
onClose={handleClose}
|
||||
severity="info"
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={upload}
|
||||
color="inherit"
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
>
|
||||
{`${upload}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotifierUpload
|
388
src/fireedge/src/client/components/Tables/Images/actions.js
Normal file
388
src/fireedge/src/client/components/Tables/Images/actions.js
Normal file
@ -0,0 +1,388 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Typography, Grid } from '@mui/material'
|
||||
import {
|
||||
MoreVert,
|
||||
AddCircledOutline,
|
||||
Lock,
|
||||
Cart,
|
||||
Group,
|
||||
Trash,
|
||||
Code,
|
||||
PageEdit,
|
||||
} from 'iconoir-react'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import {
|
||||
useLockImageMutation,
|
||||
useCloneImageMutation,
|
||||
useUnlockImageMutation,
|
||||
useEnableImageMutation,
|
||||
useDisableImageMutation,
|
||||
usePersistentImageMutation,
|
||||
useChangeImageOwnershipMutation,
|
||||
useRemoveImageMutation,
|
||||
} from 'client/features/OneApi/image'
|
||||
|
||||
import { ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
|
||||
import { CloneForm } from 'client/components/Forms/Image'
|
||||
import {
|
||||
createActions,
|
||||
GlobalAction,
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
import ImageCreateCard from 'client/components/Cards/ImageCreateCard'
|
||||
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { T, IMAGE_ACTIONS, VM_ACTIONS, RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
const isDisabled = (action) => (rows) =>
|
||||
!isAvailableAction(
|
||||
action,
|
||||
rows.map(({ original }) => original)
|
||||
)
|
||||
|
||||
const ListImagesNames = ({ rows = [] }) =>
|
||||
rows?.map?.(({ id, original }) => {
|
||||
const { ID, NAME } = original
|
||||
|
||||
return (
|
||||
<Typography
|
||||
key={`image-${id}`}
|
||||
variant="inherit"
|
||||
component="span"
|
||||
display="block"
|
||||
>
|
||||
{`#${ID} ${NAME}`}
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
|
||||
const SubHeader = (rows) => <ListImagesNames rows={rows} />
|
||||
|
||||
const MessageToConfirmAction = (rows) => (
|
||||
<>
|
||||
<ListImagesNames 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 [clone] = useCloneImageMutation()
|
||||
const [lock] = useLockImageMutation()
|
||||
const [unlock] = useUnlockImageMutation()
|
||||
const [enable] = useEnableImageMutation()
|
||||
const [disable] = useDisableImageMutation()
|
||||
const [persistent] = usePersistentImageMutation()
|
||||
const [changeOwnership] = useChangeImageOwnershipMutation()
|
||||
const [deleteImage] = useRemoveImageMutation()
|
||||
|
||||
const resourcesView = getResourceView(RESOURCE_NAMES.IMAGE)?.actions
|
||||
|
||||
const imageActions = useMemo(
|
||||
() =>
|
||||
createActions({
|
||||
filters: resourcesView,
|
||||
actions: [
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.CREATE_DIALOG,
|
||||
dataCy: `image_${IMAGE_ACTIONS.CREATE_DIALOG}`,
|
||||
tooltip: T.Create,
|
||||
icon: AddCircledOutline,
|
||||
options: [
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.CreateImage,
|
||||
children: () => (
|
||||
<Grid container spacing={3}>
|
||||
<ImageCreateCard
|
||||
name={Tr(T.CreateImage)}
|
||||
Icon={PageEdit}
|
||||
onClick={() => history.push(PATH.STORAGE.IMAGES.CREATE)}
|
||||
/>
|
||||
{resourcesView?.dockerfile_dialog && (
|
||||
<ImageCreateCard
|
||||
name={Tr(T.CreateDockerfile)}
|
||||
Icon={Code}
|
||||
onClick={() =>
|
||||
history.push(PATH.STORAGE.IMAGES.DOCKERFILE)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
),
|
||||
fixedWidth: true,
|
||||
fixedHeight: true,
|
||||
handleAccept: undefined,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.CREATE_DIALOG}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
accessor: VM_ACTIONS.CREATE_APP_DIALOG,
|
||||
dataCy: `image_${VM_ACTIONS.CREATE_APP_DIALOG}`,
|
||||
disabled: isDisabled(VM_ACTIONS.CREATE_APP_DIALOG),
|
||||
tooltip: T.CreateMarketApp,
|
||||
selected: { max: 1 },
|
||||
icon: Cart,
|
||||
action: (rows) => {
|
||||
const vm = rows?.[0]?.original ?? {}
|
||||
const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
|
||||
|
||||
history.push(path, [RESOURCE_NAMES.VM, vm])
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.CLONE,
|
||||
label: T.Clone,
|
||||
tooltip: T.Clone,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
options: [
|
||||
{
|
||||
dialogProps: {
|
||||
title: (rows) => {
|
||||
const isMultiple = rows?.length > 1
|
||||
const { ID, NAME } = rows?.[0]?.original ?? {}
|
||||
|
||||
return [
|
||||
Tr(
|
||||
isMultiple ? T.CloneSeveralTemplates : T.CloneTemplate
|
||||
),
|
||||
!isMultiple && `#${ID} ${NAME}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' - ')
|
||||
},
|
||||
dataCy: 'modal-clone',
|
||||
},
|
||||
form: (rows) => {
|
||||
const names = rows?.map(({ original }) => original?.NAME)
|
||||
const stepProps = { isMultiple: names.length > 1 }
|
||||
const initialValues = { name: `Copy of ${names?.[0]}` }
|
||||
|
||||
return CloneForm({ stepProps, initialValues })
|
||||
},
|
||||
onSubmit:
|
||||
(rows) =>
|
||||
async ({ prefix, name, datastore } = {}) => {
|
||||
const images = rows?.map?.(
|
||||
({ original: { ID, NAME } = {} }) =>
|
||||
// overwrite all names with prefix+NAME
|
||||
({
|
||||
id: ID,
|
||||
name: prefix ? `${prefix} ${NAME}` : name,
|
||||
datastore,
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(images.map(clone))
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tooltip: T.Lock,
|
||||
icon: Lock,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
dataCy: 'image-lock',
|
||||
options: [
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.LOCK,
|
||||
name: T.Lock,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Lock,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.LOCK}`,
|
||||
children: MessageToConfirmAction,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => lock({ id })))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.UNLOCK,
|
||||
name: T.Unlock,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Unlock,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.UNLOCK}`,
|
||||
children: MessageToConfirmAction,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => unlock({ id })))
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tooltip: T.Enable,
|
||||
icon: MoreVert,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
dataCy: 'image-enable',
|
||||
options: [
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.ENABLE,
|
||||
name: T.Enable,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Enable,
|
||||
children: MessageToConfirmAction,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.ENABLE}`,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => enable(id)))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.DISABLE,
|
||||
name: T.Disable,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Disable,
|
||||
children: MessageToConfirmAction,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.DISABLE}`,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => disable(id)))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.PERSISTENT,
|
||||
name: T.Persistent,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Persistent,
|
||||
children: MessageToConfirmAction,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.PERSISTENT}`,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => persistent({ id, persistent: true }))
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.NON_PERSISTENT,
|
||||
name: T.NonPersistyent,
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.NonPersistyent,
|
||||
children: MessageToConfirmAction,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.NON_PERSISTENT}`,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => persistent({ id, persistent: false }))
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tooltip: T.Ownership,
|
||||
icon: Group,
|
||||
selected: true,
|
||||
color: 'secondary',
|
||||
dataCy: 'image-ownership',
|
||||
options: [
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.CHANGE_OWNER,
|
||||
disabled: isDisabled(IMAGE_ACTIONS.CHANGE_OWNER),
|
||||
name: T.ChangeOwner,
|
||||
dialogProps: {
|
||||
title: T.ChangeOwner,
|
||||
subheader: SubHeader,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.CHANGE_OWNER}`,
|
||||
},
|
||||
form: ChangeUserForm,
|
||||
onSubmit: (rows) => async (newOwnership) => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => changeOwnership({ id, ...newOwnership }))
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.CHANGE_GROUP,
|
||||
disabled: isDisabled(IMAGE_ACTIONS.CHANGE_GROUP),
|
||||
name: T.ChangeGroup,
|
||||
dialogProps: {
|
||||
title: T.ChangeGroup,
|
||||
subheader: SubHeader,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.CHANGE_GROUP}`,
|
||||
},
|
||||
form: ChangeGroupForm,
|
||||
onSubmit: (rows) => async (newOwnership) => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(
|
||||
ids.map((id) => changeOwnership({ id, ...newOwnership }))
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
accessor: IMAGE_ACTIONS.DELETE,
|
||||
tooltip: T.Delete,
|
||||
icon: Trash,
|
||||
color: 'error',
|
||||
selected: { min: 1 },
|
||||
dataCy: `image_${IMAGE_ACTIONS.DELETE}`,
|
||||
options: [
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Delete,
|
||||
dataCy: `modal-${IMAGE_ACTIONS.DELETE}`,
|
||||
children: MessageToConfirmAction,
|
||||
},
|
||||
onSubmit: (rows) => async () => {
|
||||
const ids = rows?.map?.(({ original }) => original?.ID)
|
||||
await Promise.all(ids.map((id) => deleteImage({ id })))
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[view]
|
||||
)
|
||||
|
||||
return imageActions
|
||||
}
|
||||
|
||||
export default Actions
|
@ -25,6 +25,7 @@ export default [
|
||||
{ Header: 'Name', accessor: 'NAME' },
|
||||
{ Header: 'Owner', accessor: 'UNAME' },
|
||||
{ Header: 'Group', accessor: 'GNAME' },
|
||||
{ Header: 'Locked', id: 'locked', accessor: 'LOCK' },
|
||||
{
|
||||
Header: 'State',
|
||||
id: 'STATE',
|
||||
|
@ -36,7 +36,7 @@ const Row = ({ original, value, ...props }) => {
|
||||
TYPE,
|
||||
DISK_TYPE,
|
||||
PERSISTENT,
|
||||
LOCK,
|
||||
locked,
|
||||
DATASTORE,
|
||||
TOTAL_VMS,
|
||||
RUNNING_VMS,
|
||||
@ -59,7 +59,7 @@ const Row = ({ original, value, ...props }) => {
|
||||
<Typography noWrap component="span" data-cy="name">
|
||||
{NAME}
|
||||
</Typography>
|
||||
{LOCK && <Lock />}
|
||||
{locked && <Lock />}
|
||||
<span className={classes.labels}>
|
||||
{labels.map((label) => (
|
||||
<StatusChip key={label} text={label} />
|
||||
|
@ -21,7 +21,7 @@ import { useGetVmsQuery } from 'client/features/OneApi/vm'
|
||||
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
|
||||
import VmColumns from 'client/components/Tables/Vms/columns'
|
||||
import VmRow from 'client/components/Tables/Vms/row'
|
||||
import { RESOURCE_NAMES } from 'client/constants'
|
||||
import { RESOURCE_NAMES, VM_STATES, STATES } from 'client/constants'
|
||||
|
||||
const DEFAULT_DATA_CY = 'vms'
|
||||
|
||||
@ -55,7 +55,7 @@ const VmsTable = (props) => {
|
||||
?.filter((vm) =>
|
||||
host?.ID ? [host?.VMS?.ID ?? []].flat().includes(vm.ID) : true
|
||||
)
|
||||
?.filter(({ STATE }) => STATE !== '6') ?? [],
|
||||
?.filter(({ STATE }) => VM_STATES[STATE]?.name !== STATES.DONE) ?? [],
|
||||
}),
|
||||
})
|
||||
|
||||
|
@ -1,18 +1,3 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
|
@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement } from 'react'
|
||||
import { InfoEmpty } from 'iconoir-react'
|
||||
|
||||
@ -24,19 +25,23 @@ import { T } from 'client/constants'
|
||||
/**
|
||||
* Renders default empty tab.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.label - label string
|
||||
* @returns {ReactElement} Empty tab
|
||||
*/
|
||||
const EmptyTab = () => {
|
||||
const EmptyTab = ({ label = T.NoDataAvailable }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<span className={classes.noDataMessage}>
|
||||
<InfoEmpty />
|
||||
<Translate word={T.NoDataAvailable} />
|
||||
<Translate word={label} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
EmptyTab.propTypes = {
|
||||
label: PropTypes.string,
|
||||
}
|
||||
EmptyTab.displayName = 'EmptyTab'
|
||||
|
||||
export default EmptyTab
|
||||
|
@ -26,7 +26,11 @@ import { StatusChip } from 'client/components/Status'
|
||||
import { List } from 'client/components/Tabs/Common'
|
||||
|
||||
import { getType, getState } from 'client/models/Image'
|
||||
import { timeToString, booleanToString } from 'client/models/Helper'
|
||||
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'
|
||||
@ -49,6 +53,7 @@ const InformationPanel = ({ image = {}, actions }) => {
|
||||
NAME,
|
||||
SIZE,
|
||||
PERSISTENT,
|
||||
LOCK,
|
||||
REGTIME,
|
||||
DATASTORE_ID,
|
||||
DATASTORE = '--',
|
||||
@ -110,6 +115,11 @@ const InformationPanel = ({ image = {}, actions }) => {
|
||||
handleEdit: handleChangeType,
|
||||
dataCy: 'type',
|
||||
},
|
||||
{
|
||||
name: T.Locked,
|
||||
value: levelLockToString(LOCK?.LOCKED),
|
||||
dataCy: 'locked',
|
||||
},
|
||||
{
|
||||
name: T.Persistent,
|
||||
value: booleanToString(+PERSISTENT),
|
||||
@ -127,6 +137,7 @@ const InformationPanel = ({ image = {}, actions }) => {
|
||||
{
|
||||
name: T.State,
|
||||
value: <StatusChip text={stateName} stateColor={stateColor} />,
|
||||
dataCy: 'state',
|
||||
},
|
||||
{
|
||||
name: T.RunningVMs,
|
||||
|
@ -0,0 +1,142 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Trash, UndoAction } from 'iconoir-react'
|
||||
|
||||
import {
|
||||
useFlattenImageSnapshotMutation,
|
||||
useRevertImageSnapshotMutation,
|
||||
useDeleteImageSnapshotMutation,
|
||||
} from 'client/features/OneApi/image'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { T, IMAGE_ACTIONS } from 'client/constants'
|
||||
|
||||
const SnapshotFlattenAction = memo(({ id, snapshot }) => {
|
||||
const [flattenImageSnapshot] = useFlattenImageSnapshotMutation()
|
||||
const { ID, NAME = T.Snapshot } = snapshot
|
||||
|
||||
const handleDelete = async () => {
|
||||
await flattenImageSnapshot({ id, snapshot: ID })
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': IMAGE_ACTIONS.SNAPSHOT_FLATTEN,
|
||||
tooltip: Tr(T.Flatten),
|
||||
label: Tr(T.Flatten),
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: (
|
||||
<Translate word={T.FlattenSnapshot} values={`#${ID} - ${NAME}`} />
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
<p>{Tr(T.DeleteOtherSnapshots)}</p>
|
||||
<p>{Tr(T.DoYouWantProceed)}</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
onSubmit: handleDelete,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const SnapshotRevertAction = memo(({ id, snapshot }) => {
|
||||
const [revertImageSnapshot] = useRevertImageSnapshotMutation()
|
||||
const { ID, NAME = T.Snapshot } = snapshot
|
||||
const handleRevert = async () => {
|
||||
await revertImageSnapshot({ id, snapshot: ID })
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': IMAGE_ACTIONS.SNAPSHOT_REVERT,
|
||||
icon: <UndoAction />,
|
||||
tooltip: Tr(T.Revert),
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: (
|
||||
<Translate word={T.RevertSomething} values={`#${ID} - ${NAME}`} />
|
||||
),
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>,
|
||||
},
|
||||
onSubmit: handleRevert,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const SnapshotDeleteAction = memo(({ id, snapshot }) => {
|
||||
const [deleteImageSnapshot] = useDeleteImageSnapshotMutation()
|
||||
const { ID, NAME = T.Snapshot } = snapshot
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteImageSnapshot({ id, snapshot: ID })
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': IMAGE_ACTIONS.SNAPSHOT_DELETE,
|
||||
icon: <Trash />,
|
||||
tooltip: Tr(T.Delete),
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: (
|
||||
<Translate word={T.DeleteSomething} values={`#${ID} - ${NAME}`} />
|
||||
),
|
||||
children: <p>{Tr(T.DoYouWantProceed)}</p>,
|
||||
},
|
||||
onSubmit: handleDelete,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const ActionPropTypes = {
|
||||
id: PropTypes.string,
|
||||
snapshot: PropTypes.object,
|
||||
}
|
||||
|
||||
SnapshotFlattenAction.propTypes = ActionPropTypes
|
||||
SnapshotFlattenAction.displayName = 'SnapshotFlattenAction'
|
||||
|
||||
SnapshotRevertAction.propTypes = ActionPropTypes
|
||||
SnapshotRevertAction.displayName = 'SnapshotRevertAction'
|
||||
|
||||
SnapshotDeleteAction.propTypes = ActionPropTypes
|
||||
SnapshotDeleteAction.displayName = 'SnapshotDeleteAction'
|
||||
|
||||
export { SnapshotFlattenAction, SnapshotDeleteAction, SnapshotRevertAction }
|
@ -0,0 +1,84 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Stack } from '@mui/material'
|
||||
import { T } from 'client/constants'
|
||||
import { useGetImageQuery } from 'client/features/OneApi/image'
|
||||
import EmptyTab from 'client/components/Tabs/EmptyTab'
|
||||
import ImageSnapshotCard from 'client/components/Cards/ImageSnapshotCard'
|
||||
import {
|
||||
SnapshotFlattenAction,
|
||||
SnapshotDeleteAction,
|
||||
SnapshotRevertAction,
|
||||
} from 'client/components/Tabs/Image/Snapshots/Actions'
|
||||
|
||||
import { getSnapshots } from 'client/models/Image'
|
||||
|
||||
/**
|
||||
* Renders the list of disks from a VM.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.tabProps - Tab information
|
||||
* @param {string[]} props.tabProps.actions - Actions tab
|
||||
* @param {string} props.id - Image id
|
||||
* @returns {ReactElement} Storage tab
|
||||
*/
|
||||
const ImageStorageTab = ({ tabProps: { actions } = {}, id }) => {
|
||||
const { data: image = {} } = useGetImageQuery({ id })
|
||||
|
||||
const [snapshots] = useMemo(() => [getSnapshots(image)], [image])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stack gap="1em" py="0.8em">
|
||||
{snapshots.length ? (
|
||||
snapshots?.map?.((snapshot) => (
|
||||
<ImageSnapshotCard
|
||||
key={snapshot.ID}
|
||||
snapshot={snapshot}
|
||||
actions={() => (
|
||||
<>
|
||||
{actions.snapshot_flatten && (
|
||||
<SnapshotFlattenAction id={id} snapshot={snapshot} />
|
||||
)}
|
||||
{actions.snapshot_revert && (
|
||||
<SnapshotRevertAction id={id} snapshot={snapshot} />
|
||||
)}
|
||||
{actions.snapshot_delete && (
|
||||
<SnapshotDeleteAction id={id} snapshot={snapshot} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<EmptyTab label={T.NotSnapshotCurrenty} />
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ImageStorageTab.propTypes = {
|
||||
tabProps: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
ImageStorageTab.displayName = 'ImageStorageTab'
|
||||
|
||||
export default ImageStorageTab
|
59
src/fireedge/src/client/components/Tabs/Image/Vms/index.js
Normal file
59
src/fireedge/src/client/components/Tabs/Image/Vms/index.js
Normal file
@ -0,0 +1,59 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { T } from 'client/constants'
|
||||
import EmptyTab from 'client/components/Tabs/EmptyTab'
|
||||
import { useHistory, generatePath } from 'react-router-dom'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { useGetImageQuery } from 'client/features/OneApi/image'
|
||||
import { VmsTable } from 'client/components/Tables'
|
||||
|
||||
/**
|
||||
* Renders mainly Vms tab.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.id - Image id
|
||||
* @returns {ReactElement} vms tab
|
||||
*/
|
||||
const VmsTab = ({ id }) => {
|
||||
const { data: image = {} } = useGetImageQuery({ id })
|
||||
const path = PATH.INSTANCE.VMS.DETAIL
|
||||
const history = useHistory()
|
||||
|
||||
const handleRowClick = (rowId) => {
|
||||
history.push(generatePath(path, { id: String(rowId) }))
|
||||
}
|
||||
|
||||
return (
|
||||
<VmsTable
|
||||
disableGlobalSort
|
||||
displaySelectedRows
|
||||
host={image}
|
||||
onRowClick={(row) => handleRowClick(row.ID)}
|
||||
noDataMessage={<EmptyTab label={T.NotVmsCurrenty} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
VmsTab.propTypes = {
|
||||
tabProps: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
}
|
||||
|
||||
VmsTab.displayName = 'VmsTab'
|
||||
|
||||
export default VmsTab
|
@ -24,10 +24,14 @@ import { RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
import Tabs from 'client/components/Tabs'
|
||||
import Info from 'client/components/Tabs/Image/Info'
|
||||
import Vms from 'client/components/Tabs/Image/Vms'
|
||||
import Snapshots from 'client/components/Tabs/Image/Snapshots'
|
||||
|
||||
const getTabComponent = (tabName) =>
|
||||
({
|
||||
info: Info,
|
||||
vms: Vms,
|
||||
snapshot: Snapshots,
|
||||
}[tabName])
|
||||
|
||||
const ImageTabs = memo(({ id }) => {
|
||||
|
@ -165,6 +165,13 @@ export const IMAGE_STATES = [
|
||||
export const IMAGE_ACTIONS = {
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
DELETE: 'delete',
|
||||
LOCK: 'lock',
|
||||
UNLOCK: 'unlock',
|
||||
CLONE: 'clone',
|
||||
ENABLE: 'enable',
|
||||
DISABLE: 'disable',
|
||||
PERSISTENT: 'persistent',
|
||||
NON_PERSISTENT: 'nonpersistent',
|
||||
|
||||
// INFORMATION
|
||||
RENAME: ACTIONS.RENAME,
|
||||
@ -172,4 +179,7 @@ export const IMAGE_ACTIONS = {
|
||||
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
|
||||
CHANGE_TYPE: 'chtype',
|
||||
CHANGE_PERS: 'persistent',
|
||||
SNAPSHOT_FLATTEN: 'flatten',
|
||||
SNAPSHOT_REVERT: 'revert',
|
||||
SNAPSHOT_DELETE: 'delete',
|
||||
}
|
||||
|
@ -128,6 +128,7 @@ export const INPUT_TYPES = {
|
||||
TEXT: 'text',
|
||||
TABLE: 'table',
|
||||
TOGGLE: 'toggle',
|
||||
DOCKERFILE: 'dockerfile',
|
||||
}
|
||||
|
||||
export const DEBUG_LEVEL = {
|
||||
|
@ -71,6 +71,8 @@ module.exports = {
|
||||
CreateServiceTemplate: 'Create Service Template',
|
||||
CreateVirtualNetwork: 'Create Virtual Network',
|
||||
CreateVmTemplate: 'Create VM Template',
|
||||
CreateImage: 'Create Image',
|
||||
CreateDockerfile: 'Create Dockerfile',
|
||||
CurrentGroup: 'Current group: %s',
|
||||
CurrentOwner: 'Current owner: %s',
|
||||
Delete: 'Delete',
|
||||
@ -83,6 +85,7 @@ module.exports = {
|
||||
DeleteAddressRange: 'Delete Address Range',
|
||||
DeleteTemplate: 'Delete Template',
|
||||
DeleteVirtualNetwork: 'Delete Virtual Network',
|
||||
DeleteOtherSnapshots: 'This will delete all the other image snapshots',
|
||||
Deploy: 'Deploy',
|
||||
DeployServiceTemplate: 'Deploy Service Template',
|
||||
Detach: 'Detach',
|
||||
@ -95,6 +98,8 @@ module.exports = {
|
||||
Enable: 'Enable',
|
||||
Failure: 'Failure',
|
||||
Finish: 'Finish',
|
||||
Flatten: 'Flatten',
|
||||
FlattenSnapshot: 'Flatten %s',
|
||||
Hold: 'Hold',
|
||||
Import: 'Import',
|
||||
Info: 'Info',
|
||||
@ -138,6 +143,7 @@ module.exports = {
|
||||
Select: 'Select',
|
||||
SelectCluster: 'Select Cluster',
|
||||
SelectDatastore: 'Select a Datastore to store the resource',
|
||||
SelectDatastoreImage: 'Select a Datastore',
|
||||
SelectDockerHubTag: 'Select DockerHub image tag (default latest)',
|
||||
SelectGroup: 'Select a group',
|
||||
SelectHost: 'Select a host',
|
||||
@ -358,6 +364,21 @@ module.exports = {
|
||||
Marketplaces: 'Marketplaces',
|
||||
App: 'App',
|
||||
Apps: 'Apps',
|
||||
Os: 'Operating system image',
|
||||
Cdrom: 'Readonly CD-ROM',
|
||||
Datablock: 'Generic storage datablock',
|
||||
Path: 'Path/URL',
|
||||
ImagePath: 'Path in OpenNebula server or URL',
|
||||
Upload: 'Upload',
|
||||
EmptyDisk: 'Empty disk image',
|
||||
ImageSize: 'Image size, in Megabytes',
|
||||
Vd: 'Virtio',
|
||||
Sd: 'SCSI/SATA',
|
||||
Hd: 'Parallel ATA (IDE)',
|
||||
CustomBus: 'Custom bus',
|
||||
Fs: 'Fs',
|
||||
CustomFormat: 'Custom Format',
|
||||
Dockerfile: 'Dockerfile',
|
||||
|
||||
/* sections - templates & instances */
|
||||
Instances: 'Instances',
|
||||
@ -560,6 +581,7 @@ module.exports = {
|
||||
CustomAttributes: 'Custom Attributes',
|
||||
Hypervisor: 'Hypervisor',
|
||||
Logo: 'Logo',
|
||||
MakePersistent: 'Make Persistent',
|
||||
MakeNewImagePersistent: 'Make the new images persistent',
|
||||
TemplateName: 'Template name',
|
||||
Virtualization: 'Virtualization',
|
||||
@ -1039,6 +1061,7 @@ module.exports = {
|
||||
BasePath: 'Base path',
|
||||
FileSystemType: 'Filesystem type',
|
||||
Persistent: 'Persistent',
|
||||
NonPersistyent: 'Non Persistent',
|
||||
RunningVMs: 'Running VMs',
|
||||
/* Disk - general */
|
||||
DiskType: 'Disk type',
|
||||
@ -1074,6 +1097,7 @@ module.exports = {
|
||||
Iothread id used by this disk. Default is round robin.
|
||||
Can be used only if IOTHREADS > 0. If this input is disabled
|
||||
please first configure IOTHREADS value on OS & CPU -> Features`,
|
||||
ImageLocation: 'Image Location',
|
||||
|
||||
/* Provision schema */
|
||||
/* Provision - general */
|
||||
@ -1097,6 +1121,11 @@ module.exports = {
|
||||
Mandatory: 'Mandatory',
|
||||
PressKeysToAddAValue: 'Press any of the following keys to add a value: %s',
|
||||
|
||||
/** Image */
|
||||
NotVmsCurrenty: 'There are currently no VMs associated with this image',
|
||||
NotSnapshotCurrenty:
|
||||
'There are currently no snapshots associated with this image',
|
||||
|
||||
/* Validation */
|
||||
/* Validation - mixed */
|
||||
'validation.mixed.default': 'Is invalid',
|
||||
|
81
src/fireedge/src/client/containers/Images/Create.js
Normal file
81
src/fireedge/src/client/containers/Images/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/Image'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
/**
|
||||
* Displays the creation or modification form to a VM Template.
|
||||
*
|
||||
* @returns {ReactElement} VM Template form
|
||||
*/
|
||||
function CreateImage() {
|
||||
const history = useHistory()
|
||||
const [allocate] = useAllocateImageMutation()
|
||||
const [upload] = useUploadImageMutation()
|
||||
const { enqueueSuccess, uploadSnackbar } = useGeneralApi()
|
||||
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
|
||||
const onSubmit = async ({ template, datastore, file }) => {
|
||||
if (file) {
|
||||
const uploadProcess = (progressEvent) => {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded / progressEvent.total) * 100
|
||||
)
|
||||
uploadSnackbar(percentCompleted)
|
||||
percentCompleted === 100 && uploadSnackbar(0)
|
||||
}
|
||||
try {
|
||||
const fileUploaded = await upload({
|
||||
file,
|
||||
uploadProcess,
|
||||
}).unwrap()
|
||||
template.PATH = fileUploaded[0]
|
||||
} catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
const newTemplateId = await allocate({
|
||||
template: jsonToXml(template),
|
||||
datastore,
|
||||
}).unwrap()
|
||||
history.push(PATH.STORAGE.IMAGES.LIST)
|
||||
enqueueSuccess(`Image created - #${newTemplateId}`)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
|
||||
{(config) => <DefaultFormStepper {...config} />}
|
||||
</CreateForm>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateImage
|
56
src/fireedge/src/client/containers/Images/Dockerfile.js
Normal file
56
src/fireedge/src/client/containers/Images/Dockerfile.js
Normal file
@ -0,0 +1,56 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { useHistory } from 'react-router'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useAllocateImageMutation } from 'client/features/OneApi/image'
|
||||
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
|
||||
|
||||
import {
|
||||
DefaultFormStepper,
|
||||
SkeletonStepsForm,
|
||||
} from 'client/components/FormStepper'
|
||||
import { CreateDockerfileForm } from 'client/components/Forms/Image'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
/**
|
||||
* Displays the creation or modification form to a VM Template.
|
||||
*
|
||||
* @returns {ReactElement} VM Template form
|
||||
*/
|
||||
function CreateDockerfile() {
|
||||
const history = useHistory()
|
||||
const [allocate] = useAllocateImageMutation()
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
useGetDatastoresQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
|
||||
const onSubmit = async (stepTemplate) => {
|
||||
try {
|
||||
const newTemplateId = await allocate(stepTemplate).unwrap()
|
||||
history.push(PATH.STORAGE.IMAGES.LIST)
|
||||
enqueueSuccess(`Image created - #${newTemplateId}`)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateDockerfileForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
|
||||
{(config) => <DefaultFormStepper {...config} />}
|
||||
</CreateDockerfileForm>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateDockerfile
|
@ -21,11 +21,9 @@ import Cancel from 'iconoir-react/dist/Cancel'
|
||||
import { Typography, Box, Stack, Chip } from '@mui/material'
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import {
|
||||
useLazyGetImageQuery,
|
||||
useUpdateImageMutation,
|
||||
} from 'client/features/OneApi/image'
|
||||
import { useLazyGetImageQuery } from 'client/features/OneApi/image'
|
||||
import { ImagesTable } from 'client/components/Tables'
|
||||
import ImageActions from 'client/components/Tables/Images/actions'
|
||||
import ImageTabs from 'client/components/Tabs/Image'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
@ -40,6 +38,7 @@ import { T, Image } from 'client/constants'
|
||||
*/
|
||||
function Images() {
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
const actions = ImageActions()
|
||||
|
||||
const hasSelectedRows = selectedRows?.length > 0
|
||||
const moreThanOneSelected = selectedRows?.length > 1
|
||||
@ -50,7 +49,7 @@ function Images() {
|
||||
<Box height={1} {...(hasSelectedRows && getGridProps())}>
|
||||
<ImagesTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
useUpdateMutation={useUpdateImageMutation}
|
||||
globalActions={actions}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
@ -82,24 +81,19 @@ function Images() {
|
||||
* @returns {ReactElement} Image details
|
||||
*/
|
||||
const InfoTabs = memo(({ image, gotoPage, unselect }) => {
|
||||
const [get, { data: lazyData, isFetching }] = useLazyGetImageQuery()
|
||||
const [getImage, { data: lazyData, isFetching }] = useLazyGetImageQuery()
|
||||
const id = lazyData?.ID ?? image.ID
|
||||
const name = lazyData?.NAME ?? image.NAME
|
||||
|
||||
return (
|
||||
<Stack overflow="auto">
|
||||
<Stack direction="row" alignItems="center" gap={1} mx={1} mb={1}>
|
||||
<Typography color="text.primary" noWrap flexGrow={1}>
|
||||
{`#${id} | ${name}`}
|
||||
</Typography>
|
||||
|
||||
{/* -- ACTIONS -- */}
|
||||
<Stack direction="row" alignItems="center" gap={1} mb={1}>
|
||||
<SubmitButton
|
||||
data-cy="detail-refresh"
|
||||
icon={<RefreshDouble />}
|
||||
tooltip={Tr(T.Refresh)}
|
||||
isSubmitting={isFetching}
|
||||
onClick={() => get({ id })}
|
||||
onClick={() => getImage({ id })}
|
||||
/>
|
||||
{typeof gotoPage === 'function' && (
|
||||
<SubmitButton
|
||||
@ -117,9 +111,11 @@ const InfoTabs = memo(({ image, gotoPage, unselect }) => {
|
||||
onClick={() => unselect()}
|
||||
/>
|
||||
)}
|
||||
{/* -- END ACTIONS -- */}
|
||||
<Typography color="text.primary" noWrap>
|
||||
{`#${id} | ${name}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<ImageTabs id={id} />
|
||||
<ImageTabs id={image.ID} />
|
||||
</Stack>
|
||||
)
|
||||
})
|
||||
|
@ -22,6 +22,7 @@ export const changeAppTitle = createAction('Change App title')
|
||||
|
||||
export const dismissSnackbar = createAction('Dismiss snackbar')
|
||||
export const deleteSnackbar = createAction('Delete snackbar')
|
||||
export const setUploadSnackbar = createAction('Change upload snackbar')
|
||||
|
||||
export const enqueueSnackbar = createAction(
|
||||
'Enqueue snackbar',
|
||||
|
@ -32,6 +32,8 @@ export const useGeneralApi = () => {
|
||||
changeAppTitle: (appTitle) => dispatch(actions.changeAppTitle(appTitle)),
|
||||
changeZone: (zone) => dispatch(actions.changeZone(zone)),
|
||||
|
||||
uploadSnackbar: (percent) => dispatch(actions.setUploadSnackbar(percent)),
|
||||
|
||||
// dismiss all if no key has been defined
|
||||
dismissSnackbar: (key) =>
|
||||
dispatch(actions.dismissSnackbar({ key, dismissAll: !key })),
|
||||
|
@ -27,6 +27,7 @@ const initial = {
|
||||
withGroupSwitcher: false,
|
||||
isLoading: false,
|
||||
isFixMenu: false,
|
||||
upload: 0,
|
||||
notifications: [],
|
||||
}
|
||||
|
||||
@ -64,7 +65,11 @@ const slice = createSlice({
|
||||
...state,
|
||||
zone: payload,
|
||||
}))
|
||||
|
||||
/* UPLOAD NOTIFICATION */
|
||||
.addCase(actions.setUploadSnackbar, (state, { payload }) => ({
|
||||
...state,
|
||||
upload: payload,
|
||||
}))
|
||||
/* NOTIFICATION ACTIONS */
|
||||
.addCase(actions.enqueueSnackbar, (state, { payload }) => {
|
||||
const { key, options, message } = payload
|
||||
|
@ -20,12 +20,14 @@ import {
|
||||
ONE_RESOURCES_POOL,
|
||||
} from 'client/features/OneApi'
|
||||
import { UpdateFromSocket } from 'client/features/OneApi/socket'
|
||||
import http from 'client/utils/rest'
|
||||
import {
|
||||
FilterFlag,
|
||||
Image,
|
||||
Permission,
|
||||
IMAGE_TYPES_STR,
|
||||
} from 'client/constants'
|
||||
import { getType } from 'client/models/Image'
|
||||
|
||||
const { IMAGE } = ONE_RESOURCES
|
||||
const { IMAGE_POOL } = ONE_RESOURCES_POOL
|
||||
@ -49,7 +51,19 @@ const imageApi = oneApi.injectEndpoints({
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
transformResponse: (data) => [data?.IMAGE_POOL?.IMAGE ?? []].flat(),
|
||||
transformResponse: (data) => {
|
||||
const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) => {
|
||||
const type = getType(image)
|
||||
|
||||
return (
|
||||
type === IMAGE_TYPES_STR.OS ||
|
||||
type === IMAGE_TYPES_STR.CDROM ||
|
||||
type === IMAGE_TYPES_STR.DATABLOCK
|
||||
)
|
||||
})
|
||||
|
||||
return [images ?? []].flat()
|
||||
},
|
||||
providesTags: (images) =>
|
||||
images
|
||||
? [
|
||||
@ -101,6 +115,35 @@ const imageApi = oneApi.injectEndpoints({
|
||||
},
|
||||
invalidatesTags: [IMAGE_POOL],
|
||||
}),
|
||||
uploadImage: builder.mutation({
|
||||
/**
|
||||
* Upload image.
|
||||
*
|
||||
* @param {object} params - request params
|
||||
* @param {object} params.file - image file
|
||||
* @param {Function} params.uploadProcess - upload process function
|
||||
* @returns {number} Virtual machine id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
queryFn: async ({ file, uploadProcess }) => {
|
||||
try {
|
||||
const data = new FormData()
|
||||
data.append('files', file)
|
||||
const response = await http.request({
|
||||
url: '/api/image/upload',
|
||||
method: 'POST',
|
||||
data,
|
||||
onUploadProgress: uploadProcess,
|
||||
})
|
||||
|
||||
return { data: response.data }
|
||||
} catch (axiosError) {
|
||||
const { response } = axiosError
|
||||
|
||||
return { error: { status: response?.status, data: response?.data } }
|
||||
}
|
||||
},
|
||||
}),
|
||||
cloneImage: builder.mutation({
|
||||
/**
|
||||
* Clones an existing image.
|
||||
@ -408,4 +451,5 @@ export const {
|
||||
useFlattenImageSnapshotMutation,
|
||||
useLockImageMutation,
|
||||
useUnlockImageMutation,
|
||||
useUploadImageMutation,
|
||||
} = imageApi
|
||||
|
@ -81,6 +81,22 @@ const systemApi = oneApi.injectEndpoints({
|
||||
providesTags: [{ type: SYSTEM, id: 'sunstone-views' }],
|
||||
keepUnusedDataFor: 600,
|
||||
}),
|
||||
getSunstoneConfig: builder.query({
|
||||
/**
|
||||
* Returns the Sunstone configuration for resource tabs.
|
||||
*
|
||||
* @returns {object} The loaded sunstone view files
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: () => {
|
||||
const name = SunstoneActions.SUNSTONE_CONFIG
|
||||
const command = { name, ...SunstoneCommands[name] }
|
||||
|
||||
return { command }
|
||||
},
|
||||
providesTags: [{ type: SYSTEM, id: 'sunstone-config' }],
|
||||
keepUnusedDataFor: 600,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@ -90,6 +106,8 @@ export const {
|
||||
useLazyGetOneVersionQuery,
|
||||
useGetOneConfigQuery,
|
||||
useLazyGetOneConfigQuery,
|
||||
useGetSunstoneConfigQuery,
|
||||
useLazyGetSunstoneConfigQuery,
|
||||
useGetSunstoneViewsQuery,
|
||||
useLazyGetSunstoneViewsQuery,
|
||||
} = systemApi
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
IMAGE_STATES,
|
||||
STATES,
|
||||
Image,
|
||||
DiskSnapshot,
|
||||
} from 'client/constants'
|
||||
import { prettyBytes } from 'client/utils'
|
||||
|
||||
@ -60,3 +61,15 @@ export const getDiskName = ({ IMAGE, SIZE, TYPE, FORMAT } = {}) => {
|
||||
|
||||
return IMAGE ?? { fs: `${FORMAT} - ${size}`, swap: size }[type]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Image} image - Image
|
||||
* @returns {DiskSnapshot[]} List of snapshots from resource
|
||||
*/
|
||||
export const getSnapshots = (image) => {
|
||||
const {
|
||||
SNAPSHOTS: { SNAPSHOT },
|
||||
} = image ?? {}
|
||||
|
||||
return [SNAPSHOT].flat().filter(Boolean)
|
||||
}
|
||||
|
@ -157,59 +157,60 @@ const upload = (
|
||||
const { app, files, public: publicFile } = params
|
||||
const { id, user, password } = userData
|
||||
if (
|
||||
global.paths.CPI &&
|
||||
app &&
|
||||
checkValidApp(app) &&
|
||||
files &&
|
||||
id &&
|
||||
user &&
|
||||
password
|
||||
) {
|
||||
const oneConnect = oneConnection(user, password)
|
||||
checkUserAdmin(
|
||||
oneConnect,
|
||||
id,
|
||||
(admin = false) => {
|
||||
const pathUserData =
|
||||
publicFile && admin ? `${app}` : `${app}${sep}${id}`
|
||||
const pathUser = `${global.paths.CPI}${sep}${pathUserData}`
|
||||
if (!existsSync(pathUser)) {
|
||||
mkdirsSync(pathUser)
|
||||
}
|
||||
let method = ok
|
||||
let message = ''
|
||||
const data = []
|
||||
for (const file of files) {
|
||||
if (file && file.originalname && file.path && file.filename) {
|
||||
const extFile = extname(file.originalname)
|
||||
try {
|
||||
const filenameApi = `${pathUserData}${sep}${file.filename}${extFile}`
|
||||
const filename = `${pathUser}${sep}${file.filename}${extFile}`
|
||||
moveSync(file.path, filename)
|
||||
data.push(filenameApi)
|
||||
} catch (error) {
|
||||
method = internalServerError
|
||||
message = error && error.message
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
res.locals.httpCode = httpResponse(
|
||||
method,
|
||||
data.length ? data : '',
|
||||
message
|
||||
)
|
||||
next()
|
||||
},
|
||||
() => {
|
||||
res.locals.httpCode = internalServerError
|
||||
next()
|
||||
}
|
||||
!(
|
||||
global.paths.CPI &&
|
||||
app &&
|
||||
checkValidApp(app) &&
|
||||
files &&
|
||||
id &&
|
||||
user &&
|
||||
password
|
||||
)
|
||||
} else {
|
||||
) {
|
||||
res.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
|
||||
const oneConnect = oneConnection(user, password)
|
||||
checkUserAdmin(
|
||||
oneConnect,
|
||||
id,
|
||||
(admin = false) => {
|
||||
const pathUserData = publicFile && admin ? `${app}` : `${app}${sep}${id}`
|
||||
const pathUser = `${global.paths.CPI}${sep}${pathUserData}`
|
||||
if (!existsSync(pathUser)) {
|
||||
mkdirsSync(pathUser)
|
||||
}
|
||||
let method = ok
|
||||
let message = ''
|
||||
const data = []
|
||||
for (const file of files) {
|
||||
if (file && file.originalname && file.path && file.filename) {
|
||||
const extFile = extname(file.originalname)
|
||||
try {
|
||||
const filenameApi = `${pathUserData}${sep}${file.filename}${extFile}`
|
||||
const filename = `${pathUser}${sep}${file.filename}${extFile}`
|
||||
moveSync(file.path, filename)
|
||||
data.push(filenameApi)
|
||||
} catch (error) {
|
||||
method = internalServerError
|
||||
message = error && error.message
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
res.locals.httpCode = httpResponse(
|
||||
method,
|
||||
data.length ? data : '',
|
||||
message
|
||||
)
|
||||
next()
|
||||
},
|
||||
() => {
|
||||
res.locals.httpCode = internalServerError
|
||||
next()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
55
src/fireedge/src/server/routes/api/image/functions.js
Normal file
55
src/fireedge/src/server/routes/api/image/functions.js
Normal file
@ -0,0 +1,55 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
const { defaults, httpCodes } = require('server/utils/constants')
|
||||
|
||||
const { httpResponse } = require('server/utils/server')
|
||||
|
||||
const { defaultEmptyFunction } = defaults
|
||||
|
||||
const { ok, badRequest } = httpCodes
|
||||
|
||||
const httpBadRequest = httpResponse(badRequest, '', '')
|
||||
|
||||
/**
|
||||
* Upload File.
|
||||
*
|
||||
* @param {object} res - response http
|
||||
* @param {Function} next - express stepper
|
||||
* @param {string} params - data response http
|
||||
* @param {object} userData - user of http request
|
||||
*/
|
||||
const upload = (
|
||||
res = {},
|
||||
next = defaultEmptyFunction,
|
||||
params = {},
|
||||
userData = {}
|
||||
) => {
|
||||
const { files } = params
|
||||
const { user, password } = userData
|
||||
if (!(files && user && password)) {
|
||||
res.locals.httpCode = httpBadRequest
|
||||
next()
|
||||
}
|
||||
|
||||
const data = files.map((file) => file.path)
|
||||
res.locals.httpCode = httpResponse(ok, data.length ? data : '')
|
||||
next()
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
upload,
|
||||
}
|
||||
module.exports = functionRoutes
|
27
src/fireedge/src/server/routes/api/image/index.js
Normal file
27
src/fireedge/src/server/routes/api/image/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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { Actions, Commands } = require('server/routes/api/image/routes')
|
||||
const { upload } = require('server/routes/api/image/functions')
|
||||
|
||||
const { IMAGE_UPLOAD } = Actions
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
...Commands[IMAGE_UPLOAD],
|
||||
action: upload,
|
||||
},
|
||||
]
|
42
src/fireedge/src/server/routes/api/image/routes.js
Normal file
42
src/fireedge/src/server/routes/api/image/routes.js
Normal file
@ -0,0 +1,42 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { httpMethod } = require('../../../utils/constants/defaults')
|
||||
|
||||
const { POST } = httpMethod
|
||||
|
||||
const basepath = '/image'
|
||||
const IMAGE_UPLOAD = 'image.upload'
|
||||
|
||||
const Actions = {
|
||||
IMAGE_UPLOAD,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Actions,
|
||||
Commands: {
|
||||
[IMAGE_UPLOAD]: {
|
||||
path: `${basepath}/upload`,
|
||||
httpMethod: POST,
|
||||
auth: true,
|
||||
params: {
|
||||
files: {
|
||||
from: 'files',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
@ -36,6 +36,7 @@ const routes = [
|
||||
'2fa',
|
||||
'auth',
|
||||
'files',
|
||||
'image',
|
||||
'marketapp',
|
||||
'oneflow',
|
||||
'vcenter',
|
||||
|
@ -526,7 +526,11 @@ const getToken = (
|
||||
const serverAdmin = getSunstoneAuth() ?? {}
|
||||
const { token: authToken } = createTokenServerAdmin(serverAdmin) ?? {}
|
||||
|
||||
!authToken && responser()
|
||||
if (!authToken) {
|
||||
responser()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const { username } = serverAdmin
|
||||
const oneClient = xmlrpc(`${username}:${username}`, authToken)
|
||||
@ -537,6 +541,8 @@ const getToken = (
|
||||
callback: (vmInfoErr, { VM } = {}) => {
|
||||
if (vmInfoErr || !VM) {
|
||||
responser(vmInfoErr, unauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!VM?.MONITORING?.VCENTER_ESX_HOST) {
|
||||
@ -544,6 +550,8 @@ const getToken = (
|
||||
Could not determine the vCenter ESX host where
|
||||
the VM is running. Wait till the VCENTER_ESX_HOST attribute is
|
||||
retrieved once the host has been monitored`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const history = VM?.HISTORY_RECORDS?.HISTORY
|
||||
@ -558,80 +566,104 @@ const getToken = (
|
||||
|
||||
if (String(hostHypervisor).toLowerCase() !== 'vcenter') {
|
||||
responser('VMRC Connection is only for vCenter hypervisor')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!VM?.DEPLOY_ID || isNaN(hostId)) {
|
||||
responser('VM is not deployed')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const responseError = (error) =>
|
||||
responser(error && error.message, internalServerError)
|
||||
|
||||
const responseToken = (ticketData) => {
|
||||
const { ticket } = ticketData
|
||||
const { protocol, hostname, port, path } = parse(ticket)
|
||||
|
||||
const httpProtocol = protocol === 'wss:' ? 'https' : 'http'
|
||||
const esxUrl = `${httpProtocol}://${hostname}:${port}`
|
||||
const token = path.replace('/ticket/', '')
|
||||
global.vcenterToken = { [token]: esxUrl }
|
||||
|
||||
responser(token, ok)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the vcenter token of vm.
|
||||
*
|
||||
* @param {string} sessionId - session id
|
||||
* @param {string} vcenterHost - host ip
|
||||
*/
|
||||
const getVcenterToken = (sessionId, vcenterHost) => {
|
||||
const vmIdFromDeployId = VM.DEPLOY_ID.match(regexGetVcenterId).groups.id
|
||||
|
||||
executeRequest(
|
||||
{
|
||||
params: {
|
||||
url: `https://${vcenterHost}/api/vcenter/vm/vm-${vmIdFromDeployId}/console/tickets`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'vmware-api-session-id': sessionId,
|
||||
},
|
||||
data: JSON.stringify({ type: 'WEBMKS' }),
|
||||
},
|
||||
agent: 'https',
|
||||
},
|
||||
{
|
||||
success: responseToken,
|
||||
error: responseError,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vmware-api-session-id.
|
||||
*
|
||||
* @param {string} hostInfoError - error when get info host.
|
||||
* @param {object} hostData - host data
|
||||
* @param {object} hostData.HOST - data host
|
||||
*/
|
||||
const getSession = (hostInfoError, { HOST } = {}) => {
|
||||
const { VCENTER_HOST, VCENTER_USER, VCENTER_PASSWORD } =
|
||||
HOST?.TEMPLATE ?? {}
|
||||
|
||||
if (
|
||||
hostInfoError ||
|
||||
!VCENTER_HOST ||
|
||||
!VCENTER_USER ||
|
||||
!VCENTER_PASSWORD
|
||||
) {
|
||||
responser(hostInfoError, unauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
executeRequest(
|
||||
{
|
||||
params: {
|
||||
url: `https://${VCENTER_HOST}/api/session`,
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(
|
||||
`${VCENTER_USER}:${VCENTER_PASSWORD}`
|
||||
)}`,
|
||||
},
|
||||
},
|
||||
agent: 'https',
|
||||
},
|
||||
{
|
||||
success: (sessionId) => getVcenterToken(sessionId, VCENTER_HOST),
|
||||
error: responseError,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
oneClient({
|
||||
action: ActionHost.HOST_INFO,
|
||||
parameters: [parseInt(hostId, 10), true],
|
||||
callback: (hostInfoError, { HOST } = {}) => {
|
||||
const { VCENTER_HOST, VCENTER_USER, VCENTER_PASSWORD } =
|
||||
HOST?.TEMPLATE ?? {}
|
||||
|
||||
if (
|
||||
hostInfoError ||
|
||||
!VCENTER_HOST ||
|
||||
!VCENTER_USER ||
|
||||
!VCENTER_PASSWORD
|
||||
) {
|
||||
responser(hostInfoError, unauthorized)
|
||||
}
|
||||
|
||||
executeRequest(
|
||||
{
|
||||
params: {
|
||||
url: `https://${VCENTER_HOST}/api/session`,
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(
|
||||
`${VCENTER_USER}:${VCENTER_PASSWORD}`
|
||||
)}`,
|
||||
},
|
||||
},
|
||||
agent: 'https',
|
||||
},
|
||||
{
|
||||
success: (sessionId) => {
|
||||
const vmIdFromDeployId =
|
||||
VM.DEPLOY_ID.match(regexGetVcenterId).groups.id
|
||||
|
||||
executeRequest(
|
||||
{
|
||||
params: {
|
||||
url: `https://${VCENTER_HOST}/api/vcenter/vm/vm-${vmIdFromDeployId}/console/tickets`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'vmware-api-session-id': sessionId,
|
||||
},
|
||||
data: JSON.stringify({ type: 'WEBMKS' }),
|
||||
},
|
||||
agent: 'https',
|
||||
},
|
||||
{
|
||||
success: (ticketData) => {
|
||||
const { ticket } = ticketData
|
||||
const { protocol, hostname, port, path } = parse(ticket)
|
||||
|
||||
const httpProtocol =
|
||||
protocol === 'wss:' ? 'https' : 'http'
|
||||
const esxUrl = `${httpProtocol}://${hostname}:${port}`
|
||||
const token = path.replace('/ticket/', '')
|
||||
global.vcenterToken = { [token]: esxUrl }
|
||||
|
||||
responser(token, ok)
|
||||
},
|
||||
error: (error) =>
|
||||
responser(error && error.message, internalServerError),
|
||||
}
|
||||
)
|
||||
},
|
||||
error: (error) =>
|
||||
responser(error && error.message, internalServerError),
|
||||
}
|
||||
)
|
||||
},
|
||||
callback: getSession,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
@ -399,6 +399,8 @@ void OpenNebulaTemplate::set_conf_default()
|
||||
set_conf_single("HOST_ENCRYPTED_ATTR", "NSX_PASSWORD");
|
||||
set_conf_single("HOST_ENCRYPTED_ATTR", "ONE_PASSWORD");
|
||||
set_conf_single("SHOWBACK_ONLY_RUNNING", "NO");
|
||||
set_conf_single("CONTEXT_RESTRICTED_DIRS", "/etc");
|
||||
set_conf_single("CONTEXT_SAFE_DIRS", "");
|
||||
|
||||
//DB CONFIGURATION
|
||||
vvalue.insert(make_pair("BACKEND","sqlite"));
|
||||
|
@ -122,11 +122,23 @@ EOF
|
||||
)
|
||||
|
||||
LOCK="tm-fs_lvm-${DST_DS_SYS_ID}.lock"
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${SRC_HOST}" "${CREATE_CMD}" \
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${DST_HOST}" "${CREATE_CMD}" \
|
||||
"Error creating LV named ${LV_NAME}"
|
||||
|
||||
# activate src volume (on DST)
|
||||
CMD=$(cat <<EOF
|
||||
set -ex -o pipefail
|
||||
${SYNC}
|
||||
${SUDO} ${LVSCAN}
|
||||
${SUDO} ${LVCHANGE} -ay "${SRC_DEV}"
|
||||
EOF
|
||||
)
|
||||
LOCK="tm-fs_lvm-${SRC_DS_SYS_ID}.lock"
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${DST_HOST}" "${CMD}" \
|
||||
"Error deactivating disk ${SRC_PATH}"
|
||||
|
||||
# copy volume data
|
||||
ssh_exec_and_log "$SRC_HOST" \
|
||||
ssh_exec_and_log "$DST_HOST" \
|
||||
"${DD} if=${SRC_DEV} of=${DST_DEV} bs=${DD_BLOCK_SIZE:-64k}" \
|
||||
"Error copying ${SRC} to ${DST}"
|
||||
|
||||
@ -144,20 +156,21 @@ EOF
|
||||
LOCK="tm-fs_lvm-${SRC_DS_SYS_ID}.lock"
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${SRC_HOST}" "${DELETE_CMD}" \
|
||||
"Error deleting old LV ${SRC_DEV}"
|
||||
fi
|
||||
else
|
||||
# activate
|
||||
CMD=$(cat <<EOF
|
||||
set -ex -o pipefail
|
||||
${SYNC}
|
||||
${SUDO} ${LVSCAN}
|
||||
${SUDO} ${LVCHANGE} -ay "${DST_DEV}"
|
||||
|
||||
# activate
|
||||
CMD=$(cat <<EOF
|
||||
set -ex -o pipefail
|
||||
${SYNC}
|
||||
${SUDO} ${LVSCAN}
|
||||
${SUDO} ${LVCHANGE} -ay "${DST_DEV}"
|
||||
|
||||
hostname -f >"${DST_DIR}/.host" || :
|
||||
hostname -f >"${DST_DIR}/.host" || :
|
||||
EOF
|
||||
)
|
||||
ssh_exec_and_log "$DST_HOST" "$CMD" \
|
||||
"Error activating disk $DST_PATH"
|
||||
ssh_exec_and_log "$DST_HOST" "$CMD" \
|
||||
"Error activating disk $DST_PATH"
|
||||
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
@ -122,11 +122,23 @@ EOF
|
||||
)
|
||||
|
||||
LOCK="tm-fs_lvm-${DST_DS_SYS_ID}.lock"
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${SRC_HOST}" "${CREATE_CMD}" \
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${DST_HOST}" "${CREATE_CMD}" \
|
||||
"Error creating LV named ${LV_NAME}"
|
||||
|
||||
# activate src volume (on DST)
|
||||
CMD=$(cat <<EOF
|
||||
set -ex -o pipefail
|
||||
${SYNC}
|
||||
${SUDO} ${LVSCAN}
|
||||
${SUDO} ${LVCHANGE} -ay "${SRC_DEV}"
|
||||
EOF
|
||||
)
|
||||
LOCK="tm-fs_lvm-${SRC_DS_SYS_ID}.lock"
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${DST_HOST}" "${CMD}" \
|
||||
"Error deactivating disk ${SRC_PATH}"
|
||||
|
||||
# copy volume data
|
||||
ssh_exec_and_log "$SRC_HOST" \
|
||||
ssh_exec_and_log "$DST_HOST" \
|
||||
"${DD} if=${SRC_DEV} of=${DST_DEV} bs=${DD_BLOCK_SIZE:-64k}" \
|
||||
"Error copying ${SRC} to ${DST}"
|
||||
|
||||
@ -144,21 +156,20 @@ EOF
|
||||
LOCK="tm-fs_lvm-${SRC_DS_SYS_ID}.lock"
|
||||
exclusive "${LOCK}" 120 ssh_exec_and_log "${SRC_HOST}" "${DELETE_CMD}" \
|
||||
"Error deleting old LV ${SRC_DEV}"
|
||||
fi
|
||||
else
|
||||
# activate
|
||||
CMD=$(cat <<EOF
|
||||
set -ex -o pipefail
|
||||
${SYNC}
|
||||
${SUDO} ${LVSCAN}
|
||||
${SUDO} ${LVCHANGE} -ay "${DST_DEV}"
|
||||
|
||||
# activate
|
||||
CMD=$(cat <<EOF
|
||||
set -ex -o pipefail
|
||||
${SYNC}
|
||||
${SUDO} ${LVSCAN}
|
||||
${SUDO} ${LVCHANGE} -ay "${DST_DEV}"
|
||||
|
||||
hostname -f >"${DST_DIR}/.host" || :
|
||||
hostname -f >"${DST_DIR}/.host" || :
|
||||
EOF
|
||||
)
|
||||
ssh_exec_and_log "$DST_HOST" "$CMD" \
|
||||
"Error activating disk $DST_PATH"
|
||||
|
||||
ssh_exec_and_log "$DST_HOST" "$CMD" \
|
||||
"Error activating disk $DST_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# After managing LV de/activation on different hosts, transfer normal files
|
||||
|
@ -70,6 +70,39 @@ const std::vector<ContextVariable> NETWORK6_CONTEXT = {
|
||||
{"EXTERNAL", "EXTERNAL", "", false},
|
||||
};
|
||||
|
||||
bool is_restricted(const string& path,
|
||||
const set<string>& restricted,
|
||||
const set<string>& safe)
|
||||
{
|
||||
auto canonical_c = realpath(path.c_str(), nullptr);
|
||||
|
||||
if (canonical_c == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string canonical_str(canonical_c);
|
||||
free(canonical_c);
|
||||
|
||||
for (auto& s : safe)
|
||||
{
|
||||
if (canonical_str.find(s) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& r : restricted)
|
||||
{
|
||||
if (canonical_str.find(r) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* CONTEXT - Public Interface */
|
||||
@ -129,6 +162,33 @@ int VirtualMachine::generate_context(string &files, int &disk_id,
|
||||
}
|
||||
|
||||
files = context->vector_value("FILES");
|
||||
|
||||
auto& nd = Nebula::instance();
|
||||
string restricted_dirs, safe_dirs;
|
||||
nd.get_configuration_attribute("CONTEXT_RESTRICTED_DIRS", restricted_dirs);
|
||||
nd.get_configuration_attribute("CONTEXT_SAFE_DIRS", safe_dirs);
|
||||
|
||||
set<string> restricted, safe;
|
||||
|
||||
one_util::split_unique(restricted_dirs, ' ', restricted);
|
||||
one_util::split_unique(safe_dirs, ' ', safe);
|
||||
|
||||
set<string> files_set;
|
||||
one_util::split_unique(files, ' ', files_set);
|
||||
for (auto& f : files_set)
|
||||
{
|
||||
if (is_restricted(f, restricted, safe))
|
||||
{
|
||||
string error = "CONTEXT/FILES cannot use " + f
|
||||
+ ", it's in restricted directories";
|
||||
|
||||
log("VM", Log::ERROR, error);
|
||||
set_template_error_message(error);
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
files_ds = context->vector_value("FILES_DS");
|
||||
|
||||
if (!files_ds.empty())
|
||||
|
@ -333,6 +333,9 @@ static int do_context_command(VirtualMachine * vm, const string& password,
|
||||
|
||||
if ( rc == -1 )
|
||||
{
|
||||
auto vmpool = Nebula::instance().get_vmpool();
|
||||
vmpool->update(vm);
|
||||
|
||||
return -1;
|
||||
}
|
||||
else if ( rc == 1 )
|
||||
|
@ -180,6 +180,11 @@ else
|
||||
fi
|
||||
done
|
||||
|
||||
# copy vm.xml and ds.xml from the $VM_DIR
|
||||
if ls $VM_DIR/*.xml > /dev/null; then
|
||||
multiline_exec_and_log "tar -cSf - $VM_DIR/*.xml | $SSH $DEST_HOST 'tar xSf - -C / '" \
|
||||
"Failed to copy xml files to $DEST_HOST"
|
||||
fi
|
||||
|
||||
# freeze/suspend domain and rsync raw disks again
|
||||
if [ -n "$RAW_DISKS" ]; then
|
||||
@ -287,4 +292,3 @@ fi
|
||||
if [ "x$CLEANUP_MEMORY_ON_STOP" = "xyes" ]; then
|
||||
(sudo -l | grep -q sysctl) && sudo -n sysctl vm.drop_caches=3 vm.compact_memory=1 &>/dev/null &
|
||||
fi
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user