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

F #5422: Add bulk actions to vms (#1481)

This commit is contained in:
Sergio Betanzos 2021-09-27 16:42:44 +02:00 committed by GitHub
parent feb4a86066
commit 735a8d4feb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 760 additions and 311 deletions

View File

@ -24,6 +24,8 @@ resource_name: "VM"
# Actions - Which buttons are visible to operate over the resources
actions:
refresh: true
create_dialog: true
deploy: true
migrate: true
migrate_live: true
@ -46,11 +48,12 @@ actions:
resched: true
unresched: true
save_as_template: true
lockU: true
chown: true
chgrp: true
lock: true
unlock: true
startvnc: true
startvmrc: true
startspice: true
vmrc: true
spice: true
vnc: true
ssh: true
rdp: true

View File

@ -24,6 +24,7 @@ resource_name: "VM"
# Actions - Which buttons are visible to operate over the resources
actions:
refresh: true
create_dialog: true
deploy: true
migrate: true
@ -46,12 +47,13 @@ actions:
terminate_hard: true
resched: true
unresched: true
save_as_template: true
lockU: true
save_as_template: false
chown: false
chgrp: false
lock: true
unlock: true
startvnc: true
startvmrc: true
startspice: true
vmrc: true
spice: true
vnc: true
ssh: true
rdp: true
@ -82,8 +84,8 @@ info-tabs:
ownership_panel:
enabled: true
actions:
chown: true
chgrp: true
chown: false
chgrp: false
vcenter_panel:
enabled: true
actions:

View File

@ -91,35 +91,44 @@ export const PATH = {
TEMPLATE: {
VMS: {
LIST: '/vm-template',
DETAIL: '/vm-template/:id',
INSTANTIATE: '/vm-template/instantiate'
}
},
STORAGE: {
DATASTORES: {
LIST: '/datastore'
LIST: '/datastore',
DETAIL: '/datastore/:id'
},
IMAGES: {
LIST: '/image'
LIST: '/image',
DETAIL: '/image/:id'
},
FILES: {
LIST: '/file'
LIST: '/file',
DETAIL: '/file/:id'
},
MARKETPLACES: {
LIST: '/marketplace'
LIST: '/marketplace',
DETAIL: '/marketplace/:id'
},
MARKETPLACE_APPS: {
LIST: '/marketplaces-app'
LIST: '/marketplaces-app',
DETAIL: '/marketplaces-app/:id'
}
},
NETWORK: {
VNETS: {
LIST: '/virtual-network'
LIST: '/virtual-network',
DETAIL: '/virtual-network/:id'
},
VN_TEMPLATES: {
LIST: '/network-template'
LIST: '/network-template',
DETAIL: '/network-template/:id'
},
SEC_GROUPS: {
LIST: '/security-group'
LIST: '/security-group',
DETAIL: '/security-group/:id'
}
},
INFRASTRUCTURE: {
@ -132,7 +141,8 @@ export const PATH = {
DETAIL: '/host/:id'
},
ZONES: {
LIST: '/zone'
LIST: '/zone',
DETAIL: '/zone/:id'
}
},
SYSTEM: {

View File

@ -43,7 +43,7 @@ const useStyles = makeStyles(theme => ({
const ButtonComponent = forwardRef(
({ icon, endicon, children, size = 'small', ...props }, ref) =>
icon ? (
icon && !endicon ? (
<IconButton ref={ref} {...props}>{children}</IconButton>
) : (
<Button ref={ref}

View File

@ -37,7 +37,6 @@ import { Translate } from 'client/components/HOC'
const ButtonToTriggerForm = ({
buttonProps = {},
dialogProps = {},
options = []
}) => {
const buttonId = buttonProps['data-cy'] ?? 'main-button-form'
@ -47,7 +46,7 @@ const ButtonToTriggerForm = ({
const open = Boolean(anchorEl)
const { display, show, hide, values: Form } = useDialog()
const { onSubmit: handleSubmit, form, isConfirmDialog = false } = Form ?? {}
const { onSubmit: handleSubmit, form, isConfirmDialog = false, dialogProps = {} } = Form ?? {}
const formConfig = useMemo(() => form?.() ?? {}, [form])
const { steps, defaultValues, resolver, fields, transformBeforeSubmit } = formConfig
@ -96,9 +95,10 @@ const ButtonToTriggerForm = ({
<Paper variant='outlined'>
<ClickAwayListener onClickAway={handleClose}>
<MenuList variant='menu' disablePadding dense>
{options.map(({ cy, icon: Icon, name, ...option }) => (
{options.map(({ cy, disabled, icon: Icon, name, ...option }) => (
<MenuItem
key={name}
disabled={disabled}
data-cy={cy}
onClick={() => openDialogForm(option)}
>
@ -150,11 +150,11 @@ const ButtonToTriggerForm = ({
export const ButtonToTriggerFormPropTypes = {
buttonProps: PropTypes.shape(SubmitButtonPropTypes),
dialogProps: PropTypes.shape(DialogPropTypes),
options: PropTypes.arrayOf(
PropTypes.shape({
cy: PropTypes.string,
isConfirmDialog: PropTypes.bool,
dialogProps: PropTypes.shape(DialogPropTypes),
name: PropTypes.string,
icon: PropTypes.any,
form: PropTypes.func,

View File

@ -67,12 +67,10 @@ const Networking = ({ data, setFormData }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'add-nic',
label: 'Add nic'
}}
dialogProps={{
title: `Add new: ${Tr(T.NIC)}`
label: Tr(T.AttachNic)
}}
options={[{
dialogProps: { title: T.AttachNic },
form: () => AttachNicForm({ nics }),
onSubmit: handleSave
}]}
@ -118,10 +116,10 @@ const Networking = ({ data, setFormData }) => {
icon: <Edit size={18} />,
tooltip: <Translate word={T.Edit} />
}}
dialogProps={{
title: <><Translate word={T.Edit} />{`: ${NAME} - ${NETWORK}`}</>
}}
options={[{
dialogProps: {
title: <Translate word={T.EditSomething} values={[`${NAME} - ${NETWORK}`]} />
},
form: () => AttachNicForm({ nics }, item),
onSubmit: newValues => handleSave(newValues, NAME)
}]}

View File

@ -60,22 +60,19 @@ const ScheduleAction = ({ data, setFormData }) => {
'data-cy': 'add-sched-action',
label: Tr(T.AddAction)
}}
dialogProps={{
title: Tr(T.ScheduledAction)
}}
options={[{
cy: 'add-sched-action-punctual',
name: 'Punctual action',
dialogProps: { title: T.ScheduledAction },
form: () => PunctualForm(),
onSubmit: formData =>
handleSave(SCHED_ACTION_SCHEMA.cast(formData))
onSubmit: formData => handleSave(SCHED_ACTION_SCHEMA.cast(formData))
},
{
cy: 'add-sched-action-relative',
name: 'Relative action',
dialogProps: { title: T.ScheduledAction },
form: () => RelativeForm(),
onSubmit: formData =>
handleSave(SCHED_ACTION_SCHEMA.cast(formData))
onSubmit: formData => handleSave(SCHED_ACTION_SCHEMA.cast(formData))
}]}
/>
<div className={classes.root}>
@ -100,10 +97,10 @@ const ScheduleAction = ({ data, setFormData }) => {
icon: <Edit size={18} />,
tooltip: <Translate word={T.Edit} />
}}
dialogProps={{
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
}}
options={[{
dialogProps: {
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
},
form: () => isRelative
? RelativeForm(undefined, item)
: PunctualForm(undefined, item),

View File

@ -75,21 +75,20 @@ const Storage = ({ data, setFormData }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'add-disk',
label: 'Add disk'
}}
dialogProps={{
title: `Add new: ${Tr(T.Disk)}`
label: Tr(T.AttachDisk)
}}
options={[
{
cy: 'attach-image-disk',
name: T.Image,
dialogProps: { title: T.AttachImage },
form: () => ImageSteps({ hypervisor: HYPERVISOR }),
onSubmit: handleSave
},
{
cy: 'attach-volatile-disk',
name: T.Volatile,
dialogProps: { title: T.AttachVolatile },
form: () => VolatileSteps({ hypervisor: HYPERVISOR }),
onSubmit: handleSave
}
@ -164,10 +163,10 @@ const Storage = ({ data, setFormData }) => {
icon: <Edit size={18} />,
tooltip: <Translate word={T.Edit} />
}}
dialogProps={{
title: <><Translate word={T.Edit} />{`: ${NAME}`}</>
}}
options={[{
dialogProps: {
title: <Translate word={T.EditSomething} values={[NAME]} />
},
form: () => isVolatile
? VolatileSteps({ hypervisor: HYPERVISOR }, item)
: ImageSteps({ hypervisor: HYPERVISOR }, item),

View File

@ -21,6 +21,7 @@ import { sprintf } from 'sprintf-js'
import { useAuth } from 'client/features/Auth'
import { DEFAULT_LANGUAGE, LANGUAGES_URL } from 'client/constants'
import { isDevelopment } from 'client/utils'
const TranslateContext = createContext()
let languageScript = root.document?.createElement('script')
@ -39,7 +40,7 @@ const GenerateScript = (
root.document.body.appendChild(script)
languageScript = script
} catch (error) {
console.warn('Error while generating script language')
isDevelopment() && console.error('Error while generating script language', error)
}
}

View File

@ -85,12 +85,12 @@ const ActionItem = memo(({ item, selectedRows }) => {
) : (
<ButtonToTriggerForm
buttonProps={buttonProps}
dialogProps={{
...dialogProps,
title: typeof title === 'function' ? title(selectedRows) : title,
children: typeof children === 'function' ? children(selectedRows) : children
}}
options={options?.map(({ form, onSubmit, ...option }) => ({
dialogProps: {
...dialogProps,
title: typeof title === 'function' ? title(selectedRows) : title,
children: typeof children === 'function' ? children(selectedRows) : children
},
form: form ? () => form(selectedRows) : undefined,
onSubmit: data => onSubmit(data, selectedRows),
...option

View File

@ -59,9 +59,9 @@ const GlobalActions = ({ globalActions, selectedRows }) => {
<Action key={item.accessor} item={item} />
))}
{numberOfRowSelected > 0 && (
actionsSelected?.map(item => {
actionsSelected?.map((item, idx) => {
const { min = 1, max = Number.MAX_SAFE_INTEGER } = item?.selected ?? {}
const key = item.accessor ?? item.label
const key = item.accessor ?? item.label ?? item.tooltip ?? idx
if (min < numberOfRowSelected && numberOfRowSelected > max) {
return null

View File

@ -21,12 +21,9 @@ import {
AddSquare,
Import,
Trash,
PlayOutline,
Lock,
NoLock,
UserSquareAlt,
Group,
ShareAndroid,
Undo,
Cart
} from 'iconoir-react'
@ -84,6 +81,19 @@ const Actions = () => {
// TODO: go to IMPORT form
}
},
{
accessor: VM_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG,
label: 'Instantiate',
tooltip: 'Instantiate',
icon: PlayOutline,
selected: { max: 1 },
action: rows => {
const template = rows?.[0]?.original ?? {}
const path = PATH.TEMPLATE.VMS.INSTANTIATE
history.push(path, template)
}
},
{
accessor: VM_TEMPLATE_ACTIONS.UPDATE_DIALOG,
label: 'Update',
@ -97,35 +107,23 @@ const Actions = () => {
// history.push(path)
}
},
{
accessor: VM_TEMPLATE_ACTIONS.INSTANTIATE_DIALOG,
label: 'Instantiate',
tooltip: 'Instantiate',
selected: { max: 1 },
action: rows => {
const template = rows?.[0]?.original ?? {}
const path = PATH.TEMPLATE.VMS.INSTANTIATE
history.push(path, template)
}
},
{
accessor: VM_TEMPLATE_ACTIONS.CLONE,
label: 'Clone',
tooltip: 'Clone',
selected: true,
dialogProps: {
title: rows => {
const isMultiple = rows?.length > 1
const { ID, NAME } = rows?.[0]?.original
return [
isMultiple ? 'Clone several Templates' : 'Clone Template',
!isMultiple && `#${ID} ${NAME}`
].filter(Boolean).join(' - ')
}
},
options: [{
dialogProps: {
title: rows => {
const isMultiple = rows?.length > 1
const { ID, NAME } = rows?.[0]?.original
return [
isMultiple ? 'Clone several Templates' : 'Clone Template',
!isMultiple && `#${ID} ${NAME}`
].filter(Boolean).join(' - ')
}
},
form: rows => {
const vmTemplates = rows?.map(({ original }) => original)
const stepProps = { isMultiple: vmTemplates.length > 1 }
@ -153,76 +151,69 @@ const Actions = () => {
},
{
tooltip: 'Change ownership',
label: 'Ownership',
icon: Group,
selected: true,
disabled: true,
options: [{
cy: `action.${VM_TEMPLATE_ACTIONS.CHANGE_OWNER}`,
icon: UserSquareAlt,
name: 'Change owner',
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_TEMPLATE_ACTIONS.CHANGE_GROUP}`,
icon: Group,
name: 'Change group',
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_TEMPLATE_ACTIONS.SHARE}`,
icon: ShareAndroid,
disabled: true,
name: 'Share',
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_TEMPLATE_ACTIONS.UNSHARE}`,
icon: Undo,
disabled: true,
name: 'Unshare',
isConfirmDialog: true,
onSubmit: () => undefined
}]
},
{
accessor: VM_TEMPLATE_ACTIONS.LOCK,
tooltip: 'Lock',
label: 'Lock',
tooltip: 'Lock/Unlock',
icon: Lock,
selected: true,
dialogProps: {
title: 'Lock',
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'Lock: ' + templates.join(', ')
}
},
options: [{
cy: `action.${VM_TEMPLATE_ACTIONS.LOCK}`,
name: 'Lock',
isConfirmDialog: true,
dialogProps: {
title: 'Lock',
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'Templates: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const templateIds = rows?.map?.(({ original }) => original?.ID)
await Promise.all(templateIds.map(id => lock(id)))
await Promise.all(templateIds.map(id => getVmTemplate(id)))
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => lock(id)))
await Promise.all(ids.map(id => getVmTemplate(id)))
}
}]
},
{
accessor: VM_TEMPLATE_ACTIONS.UNLOCK,
tooltip: 'Unlock',
label: 'Unlock',
icon: NoLock,
selected: true,
dialogProps: {
title: 'Unlock',
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'Unlock: ' + templates.join(', ')
}
},
options: [{
}, {
cy: `action.${VM_TEMPLATE_ACTIONS.UNLOCK}`,
name: 'Unlock',
isConfirmDialog: true,
dialogProps: {
title: 'Unlock',
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'Templates: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const templateIds = rows?.map?.(({ original }) => original?.ID)
await Promise.all(templateIds.map(id => unlock(id)))
await Promise.all(templateIds.map(id => getVmTemplate(id)))
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => unlock(id)))
await Promise.all(ids.map(id => getVmTemplate(id)))
}
}]
},
@ -231,19 +222,19 @@ const Actions = () => {
tooltip: 'Delete',
icon: Trash,
selected: true,
dialogProps: {
title: 'Delete',
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'Delete: ' + templates.join(', ')
}
},
options: [{
isConfirmDialog: true,
dialogProps: {
title: 'Delete',
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'Templates: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const templateIds = rows?.map?.(({ original }) => original?.ID)
await Promise.all(templateIds.map(id => remove(id)))
await getVmTemplates()
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => remove(id)))
await Promise.all(ids.map(id => getVmTemplate(id)))
}
}]
}

View File

@ -14,13 +14,353 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import * as Icons from 'iconoir-react'
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import {
RefreshDouble,
AddSquare,
PlayOutline,
SaveFloppyDisk,
TransitionRight,
SystemShut,
Group,
Trash,
Lock,
Cart
} from 'iconoir-react'
export default [
{ title: 'Delete', icon: Icons.Trash, handleClick: () => undefined },
{ title: 'Resume', icon: Icons.PlayOutline, handleClick: () => undefined },
{ title: 'Power Off', icon: Icons.OffRounded, handleClick: () => undefined },
{ title: 'Reboot', icon: Icons.Refresh, handleClick: () => undefined },
{ title: 'Lock', icon: Icons.Lock, handleClick: () => undefined },
{ title: 'Unlock', icon: Icons.NoLock, handleClick: () => undefined }
]
import { useAuth } from 'client/features/Auth'
import { useVmApi } from 'client/features/One'
import { Tr } from 'client/components/HOC'
// import { } from 'client/components/Forms/Vm'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useAuth()
const {
getVm,
getVms,
terminate,
terminateHard,
undeploy,
undeployHard,
poweroff,
poweroffHard,
reboot,
rebootHard,
hold,
release,
stop,
suspend,
resume,
resched,
unresched,
lock,
unlock
} = useVmApi()
const vmActions = useMemo(() => createActions({
filters: getResourceView('VM')?.actions,
actions: [
{
accessor: VM_ACTIONS.REFRESH,
tooltip: T.Refresh,
icon: RefreshDouble,
action: async () => {
await getVms({ state: -1 })
}
},
{
accessor: VM_ACTIONS.CREATE_DIALOG,
tooltip: T.Create,
icon: AddSquare,
action: () => {
const path = PATH.TEMPLATE.VMS.INSTANTIATE
history.push(path)
}
},
{
accessor: VM_ACTIONS.RESUME,
tooltip: T.Resume,
selected: true,
icon: PlayOutline,
action: async rows => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => resume(id)))
await Promise.all(ids.map(id => getVm(id)))
}
},
{
accessor: VM_ACTIONS.SAVE_AS_TEMPLATE,
tooltip: T.SaveAsTemplate,
selected: { max: 1 },
disabled: true,
icon: SaveFloppyDisk,
action: () => {}
},
{
tooltip: T.Manage,
icon: SystemShut,
selected: true,
options: [{
cy: `action.${VM_ACTIONS.SUSPEND}`,
name: T.Suspend,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => suspend(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.STOP}`,
name: T.Stop,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => stop(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.POWEROFF}`,
name: T.Poweroff,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => poweroff(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.POWEROFF_HARD}`,
name: T.PoweroffHard,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => poweroffHard(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.REBOOT}`,
name: T.Reboot,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => reboot(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.REBOOT_HARD}`,
name: T.RebootHard,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => rebootHard(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.UNDEPLOY}`,
name: T.Undeploy,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => undeploy(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.UNDEPLOY_HARD}`,
name: T.UndeployHard,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => undeployHard(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}]
},
{
tooltip: 'Hosting',
icon: TransitionRight,
selected: true,
options: [{
cy: `action.${VM_ACTIONS.DEPLOY}`,
name: T.Deploy,
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_ACTIONS.MIGRATE}`,
name: T.Migrate,
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_ACTIONS.MIGRATE_LIVE}`,
name: T.MigrateLive,
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_ACTIONS.HOLD}`,
name: T.Hold,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => hold(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.RELEASE}`,
name: T.Release,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => release(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.RESCHED}`,
name: T.Reschedule,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => resched(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.UNRESCHED}`,
name: T.UnReschedule,
isConfirmDialog: true,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => unresched(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.RECOVER}`,
name: T.Recover,
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}]
},
{
tooltip: 'Change ownership',
icon: Group,
selected: true,
options: [{
cy: `action.${VM_ACTIONS.CHANGE_OWNER}`,
name: T.ChangeOwner,
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}, {
cy: `action.${VM_ACTIONS.CHANGE_GROUP}`,
name: T.ChangeGroup,
disabled: true,
isConfirmDialog: true,
onSubmit: () => undefined
}]
},
{
tooltip: `${Tr(T.Lock)}/${Tr(T.Unlock)}`,
icon: Lock,
selected: true,
options: [{
cy: `action.${VM_ACTIONS.LOCK}`,
name: T.Lock,
isConfirmDialog: true,
dialogProps: {
title: T.Lock,
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'VMs: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => lock(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.UNLOCK}`,
name: T.Unlock,
isConfirmDialog: true,
dialogProps: {
title: T.Unlock,
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'VMs: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => unlock(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}]
},
{
tooltip: T.Terminate,
icon: Trash,
selected: true,
options: [{
cy: `action.${VM_ACTIONS.TERMINATE}`,
name: T.Terminate,
isConfirmDialog: true,
dialogProps: {
title: T.Terminate,
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'VMs: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => terminate(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
cy: `action.${VM_ACTIONS.TERMINATE_HARD}`,
name: T.TerminateHard,
isConfirmDialog: true,
dialogProps: {
title: T.TerminateHard,
children: rows => {
const templates = rows?.map?.(({ original }) => original?.NAME)
return 'VMs: ' + templates.join(', ')
}
},
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => terminateHard(id)))
await Promise.all(ids.map(id => getVm(id)))
}
}]
}
]
}), [view])
const marketplaceAppActions = useMemo(() => createActions({
filters: getResourceView('MARKETPLACE-APP')?.actions,
actions: [
{
accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
tooltip: 'Create Marketplace App',
icon: Cart,
selected: { max: 1 },
disabled: true,
action: rows => {
// TODO: go to Marketplace App CREATE form
}
}
]
}), [view])
return [...vmActions, ...marketplaceAppActions]
}
export default Actions

View File

@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { generatePath } from 'react-router-dom'
import { useUserApi, useGroupApi, RESOURCES } from 'client/features/One'
import { List } from 'client/components/Tabs/Common'
@ -56,7 +57,7 @@ const Ownership = memo(({
name: T.Owner,
value: userName,
valueInOptionList: userId,
link: PATH.SYSTEM.USERS.DETAIL.replace(':id', userId),
link: generatePath(PATH.SYSTEM.USERS.DETAIL, { id: userId }),
canEdit: actions?.includes?.(ACTIONS.CHANGE_OWNER),
handleGetOptionList: getUserOptions,
handleEdit: (_, user) => handleEdit?.({ user })
@ -65,7 +66,7 @@ const Ownership = memo(({
name: T.Group,
value: groupName,
valueInOptionList: groupId,
link: PATH.SYSTEM.GROUPS.DETAIL.replace(':id', groupId),
link: generatePath(PATH.SYSTEM.GROUPS.DETAIL, { id: groupId }),
canEdit: actions?.includes?.(ACTIONS.CHANGE_GROUP),
handleGetOptionList: getGroupOptions,
handleEdit: (_, group) => handleEdit?.({ group })

View File

@ -15,24 +15,26 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { generatePath } from 'react-router'
import { StatusChip, LinearProgressWithLabel } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import * as Host from 'client/models/Host'
import * as Datastore from 'client/models/Datastore'
import { getState, getDatastores, getAllocatedInfo } from 'client/models/Host'
import { getCapacityInfo } from 'client/models/Datastore'
import { T, VM_ACTIONS } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
const InformationPanel = ({ host = {}, handleRename, actions }) => {
const { ID, NAME, IM_MAD, VM_MAD, CLUSTER_ID, CLUSTER } = host
const { name: stateName, color: stateColor } = Host.getState(host)
const datastores = Host.getDatastores(host)
const { name: stateName, color: stateColor } = getState(host)
const datastores = getDatastores(host)
const {
percentCpuUsed,
percentCpuLabel,
percentMemUsed,
percentMemLabel
} = Host.getAllocatedInfo(host)
} = getAllocatedInfo(host)
const info = [
{ name: T.ID, value: ID },
@ -46,7 +48,12 @@ const InformationPanel = ({ host = {}, handleRename, actions }) => {
name: T.State,
value: <StatusChip text={stateName} stateColor={stateColor} />
},
{ name: T.Cluster, value: `#${CLUSTER_ID} ${CLUSTER}` },
{
name: T.Cluster,
value: `#${CLUSTER_ID} ${CLUSTER}`,
link: !Number.isNaN(+CLUSTER_ID) &&
generatePath(PATH.INFRASTRUCTURE.CLUSTERS.DETAIL, { id: CLUSTER_ID })
},
{ name: T.IM_MAD, value: IM_MAD },
{ name: T.VM_MAD, value: VM_MAD }
]
@ -60,7 +67,7 @@ const InformationPanel = ({ host = {}, handleRename, actions }) => {
}]
const datastore = datastores.map(ds => {
const { percentOfUsed, percentLabel } = Datastore.getCapacityInfo(ds)
const { percentOfUsed, percentLabel } = getCapacityInfo(ds)
return {
name: `#${ds?.ID}`, // TODO: add datastore name

View File

@ -74,10 +74,8 @@ const InformationPanel = ({ actions, vm = {}, handleResizeCapacity }) => {
'data-cy': 'resize-capacity',
label: T.Resize
}}
dialogProps={{
title: T.ResizeCapacity
}}
options={[{
dialogProps: { title: T.ResizeCapacity },
form: () => ResizeCapacityForm(undefined, vm.TEMPLATE),
onSubmit: handleResizeCapacity
}]}

View File

@ -14,28 +14,41 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { generatePath } from 'react-router-dom'
import { useCluster, useClusterApi } from 'client/features/One'
import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import Multiple from 'client/components/Tables/Vms/multiple'
import * as VirtualMachine from 'client/models/VirtualMachine'
import { getState, getLastHistory, getIps } from 'client/models/VirtualMachine'
import * as Helper from 'client/models/Helper'
import { T, VM_ACTIONS } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
const InformationPanel = ({ vm = {}, handleRename, actions }) => {
const clusters = useCluster()
const { getCluster } = useClusterApi()
const { ID, NAME, RESCHED, STIME, ETIME, LOCK, DEPLOY_ID } = vm
const { name: stateName, color: stateColor } = getState(vm)
const { HID: hostId, HOSTNAME: hostname = '--', CID: clusterId } = getLastHistory(vm)
const ips = getIps(vm)
const { name: stateName, color: stateColor } = VirtualMachine.getState(vm)
const [clusterName, setClusterName] = useState(
() => clusterId === '-1' ? 'default' : clusters.find(c => c.ID === clusterId)?.NAME
)
const { HID: hostId, HOSTNAME: hostname = '--', CID: clusterId } = VirtualMachine.getLastHistory(vm)
const clusterName = clusterId === '-1' ? 'default' : '--' // TODO: get from cluster list
const pathToHostDetail = PATH.INFRASTRUCTURE.HOSTS.DETAIL.replace(':id', hostId)
const pathToClusterDetail = PATH.INFRASTRUCTURE.CLUSTERS.DETAIL.replace(':id', clusterId)
useEffect(() => {
const loadCluster = async () => {
const cluster = await getCluster(clusterId)
cluster?.NAME && setClusterName(cluster.NAME)
}
const ips = VirtualMachine.getIps(vm)
!clusterName && loadCluster()
}, [])
const info = [
{ name: T.ID, value: ID },
@ -69,21 +82,23 @@ const InformationPanel = ({ vm = {}, handleRename, actions }) => {
name: T.EndTime,
value: Helper.timeToString(ETIME)
},
{
hostId && {
name: T.Host,
value: hostId ? `#${hostId} ${hostname}` : '',
link: !Number.isNaN(+hostId) && pathToHostDetail
value: `#${hostId} ${hostname}`,
link: !Number.isNaN(+hostId) &&
generatePath(PATH.INFRASTRUCTURE.HOSTS.DETAIL, { id: hostId })
},
{
clusterId && {
name: T.Cluster,
value: clusterId ? `#${clusterId} ${clusterName}` : '',
link: !Number.isNaN(+clusterId) && pathToClusterDetail
value: clusterName ? `#${clusterId} ${clusterName}` : `#${clusterId} --`,
link: !Number.isNaN(+clusterId) &&
generatePath(PATH.INFRASTRUCTURE.CLUSTERS.DETAIL, { id: clusterId })
},
{
name: T.DeployID,
value: DEPLOY_ID
}
]
].filter(Boolean)
return (
<List

View File

@ -59,12 +59,10 @@ const VmNetworkTab = ({ tabProps: { actions } = {} }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'attach-nic',
label: `${Tr(T.Attach)} ${Tr(T.NIC)}`
}}
dialogProps={{
title: `${Tr(T.Attach)} ${Tr(T.NIC)}`
label: Tr(T.AttachNic)
}}
options={[{
dialogProps: { title: T.AttachNic },
form: () => AttachNicForm({ nics }),
onSubmit: handleAttachNic
}]}

View File

@ -47,18 +47,17 @@ const CreateSchedAction = memo(() => {
'data-cy': 'create-sched-action',
label: Tr(T.AddAction)
}}
dialogProps={{
title: Tr(T.ScheduledAction)
}}
options={[{
cy: 'create-sched-action-punctual',
name: 'Punctual action',
dialogProps: { title: T.ScheduledAction },
form: () => PunctualForm(vm),
onSubmit: handleCreateSchedAction
},
{
cy: 'create-sched-action-relative',
name: 'Relative action',
dialogProps: { title: T.ScheduledAction },
form: () => RelativeForm(vm),
onSubmit: handleCreateSchedAction
}]}
@ -90,10 +89,10 @@ const UpdateSchedAction = memo(({ schedule, name }) => {
icon: <Edit size={18} />,
tooltip: <Translate word={T.Edit} />
}}
dialogProps={{
title: `${Tr(T.Update)} ${T.ScheduledAction}: ${name}`
}}
options={[{
dialogProps: {
title: <Translate word={T.UpdateScheduledAction} values={[name]} />
},
form: () => isRelative
? RelativeForm(vm, schedule)
: PunctualForm(vm, schedule),
@ -122,12 +121,12 @@ const DeleteSchedAction = memo(({ schedule, name }) => {
icon: <Trash size={18} />,
tooltip: <Translate word={T.Delete} />
}}
dialogProps={{
title: `${Tr(T.Delete)} ${Tr(T.ScheduledAction)}: ${name}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.DeleteScheduledAction} values={[name]} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
onSubmit: handleDelete
}]}
/>
@ -166,37 +165,37 @@ const CharterAction = memo(() => {
icon: <ClockOutline />,
tooltip: Tr(T.Charter)
}}
dialogProps={{
title: Tr(T.ScheduledAction),
children: (
<>
{leases.map(([action, { time } = {}], idx) => {
const allPeriods = {
years: time / 365 / 24 / 3600,
months: time / 30 / 24 / 3600,
weeks: time / 7 / 24 / 3600,
days: time / 24 / 3600,
hours: time / 3600,
minutes: time / 60
}
const [period, parsedTime] = Object
.entries(allPeriods)
.find(([_, time]) => time >= 1)
return (
<p key={`${action}-${idx}`}>
{`${action} - ${parsedTime} ${period}`}
</p>
)
})}
<hr />
<p>{Tr(T.DoYouWantProceed)}</p>
</>
)
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: Tr(T.ScheduledAction),
children: (
<>
{leases.map(([action, { time } = {}], idx) => {
const allPeriods = {
years: time / 365 / 24 / 3600,
months: time / 30 / 24 / 3600,
weeks: time / 7 / 24 / 3600,
days: time / 24 / 3600,
hours: time / 3600,
minutes: time / 60
}
const [period, parsedTime] = Object
.entries(allPeriods)
.find(([_, time]) => time >= 1)
return (
<p key={`${action}-${idx}`}>
{`${action} - ${parsedTime} ${period}`}
</p>
)
})}
<hr />
<p>{Tr(T.DoYouWantProceed)}</p>
</>
)
},
onSubmit: handleCreateCharter
}]}
/>

View File

@ -23,7 +23,7 @@ import { useVmApi } from 'client/features/One'
import { TabContext } from 'client/components/Tabs/TabProvider'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { Tr } from 'client/components/HOC'
import { Tr, Translate } from 'client/components/HOC'
import { T, VM_ACTIONS } from 'client/constants'
const RevertAction = memo(({ snapshot }) => {
@ -39,12 +39,12 @@ const RevertAction = memo(({ snapshot }) => {
'data-cy': `${VM_ACTIONS.SNAPSHOT_REVERT}-${SNAPSHOT_ID}`,
icon: <UndoAction size={18} />
}}
dialogProps={{
title: `${Tr(T.Revert)}: #${SNAPSHOT_ID} - ${NAME}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.RevertSomething} values={`#${SNAPSHOT_ID} - ${NAME}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
onSubmit: handleRevert
}]}
/>
@ -64,12 +64,12 @@ const DeleteAction = memo(({ snapshot }) => {
'data-cy': `${VM_ACTIONS.SNAPSHOT_DELETE}-${SNAPSHOT_ID}`,
icon: <Trash size={18} />
}}
dialogProps={{
title: `${Tr(T.Delete)}: #${SNAPSHOT_ID} - ${NAME}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.DeleteSomething} values={`#${SNAPSHOT_ID} - ${NAME}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
onSubmit: handleDelete
}]}
/>

View File

@ -52,10 +52,8 @@ const VmSnapshotTab = ({ tabProps: { actions } = {} }) => {
'data-cy': 'snapshot-create',
label: Tr(T.TakeSnapshot)
}}
dialogProps={{
title: Tr(T.TakeSnapshot)
}}
options={[{
dialogProps: { title: T.TakeSnapshot },
form: () => CreateSnapshotForm(),
onSubmit: handleSnapshotCreate
}]}

View File

@ -24,7 +24,7 @@ import { TabContext } from 'client/components/Tabs/TabProvider'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { SaveAsDiskForm, CreateDiskSnapshotForm, ResizeDiskForm } from 'client/components/Forms/Vm'
import { Tr } from 'client/components/HOC'
import { Tr, Translate } from 'client/components/HOC'
import { T, VM_ACTIONS } from 'client/constants'
const DetachAction = memo(({ disk, name: imageName }) => {
@ -41,12 +41,12 @@ const DetachAction = memo(({ disk, name: imageName }) => {
icon: <Trash size={18} />,
tooltip: Tr(T.Detach)
}}
dialogProps={{
title: `${Tr(T.Detach)}: #${DISK_ID} - ${imageName}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.DetachSomething} values={`#${DISK_ID} - ${imageName}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
onSubmit: handleDetach
}]}
/>
@ -74,12 +74,13 @@ const SaveAsAction = memo(({ disk, snapshot, name: imageName }) => {
icon: <SaveActionFloppy size={18} />,
tooltip: Tr(T.SaveAs)
}}
dialogProps={{
title: snapshot
? `${Tr(T.SaveAs)} ${Tr(T.Image)}: #${snapshotId} - ${snapshotName}`
: `${Tr(T.SaveAs)} ${Tr(T.Image)}: #${diskId} - ${imageName}`
}}
options={[{
dialogProps: {
title: snapshot
? <Translate word={T.SaveAsImage} values={`#${snapshotId} - ${snapshotName}`} />
: <Translate word={T.SaveAsImage} values={`#${diskId} - ${imageName}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
form: () => SaveAsDiskForm(),
onSubmit: handleSaveAs
}]}
@ -104,10 +105,15 @@ const ResizeAction = memo(({ disk, name: imageName }) => {
icon: <Expand size={18} />,
tooltip: Tr(T.Resize)
}}
dialogProps={{
title: `${Tr(T.Resize)}: #${DISK_ID} - ${imageName}`
}}
options={[{
dialogProps: {
title: (
<Translate
word={T.ResizeSomething}
values={`#${DISK_ID} - ${imageName}`}
/>
)
},
form: () => ResizeDiskForm(undefined, disk),
onSubmit: handleResize
}]}
@ -132,10 +138,15 @@ const SnapshotCreateAction = memo(({ disk, name: imageName }) => {
icon: <Camera size={18} />,
tooltip: Tr(T.TakeSnapshot)
}}
dialogProps={{
title: `${Tr(T.TakeSnapshot)}: #${DISK_ID} - ${imageName}`
}}
options={[{
dialogProps: {
title: (
<Translate
word={T.TakeSnapshotSomething}
values={`#${DISK_ID} - ${imageName}`}
/>
)
},
form: () => CreateDiskSnapshotForm(),
onSubmit: handleSnapshotCreate
}]}
@ -163,10 +174,11 @@ const SnapshotRenameAction = memo(({ disk, snapshot }) => {
icon: <Edit size={18} />,
tooltip: Tr(T.Edit)
}}
dialogProps={{
title: `${Tr(T.Rename)}: #${ID} - ${NAME}`
}}
options={[{
dialogProps: {
title: <Translate word={T.RenameSomething} values={`#${ID} - ${NAME}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
form: () => CreateDiskSnapshotForm(undefined, snapshot),
onSubmit: handleRename
}]}
@ -192,12 +204,12 @@ const SnapshotRevertAction = memo(({ disk, snapshot }) => {
icon: <UndoAction size={18} />,
tooltip: Tr(T.Revert)
}}
dialogProps={{
title: `${Tr(T.Revert)}: #${ID} - ${NAME}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.RevertSomething} values={`#${ID} - ${NAME}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
onSubmit: handleRevert
}]}
/>
@ -222,12 +234,12 @@ const SnapshotDeleteAction = memo(({ disk, snapshot }) => {
icon: <Trash size={18} />,
tooltip: Tr(T.Delete)
}}
dialogProps={{
title: `${Tr(T.Delete)}: #${ID} - ${NAME}`,
children: <p>{Tr(T.DoYouWantProceed)}</p>
}}
options={[{
isConfirmDialog: true,
dialogProps: {
title: <Translate word={T.DeleteSomething} values={`#${ID} - ${NAME}`} />,
children: <p>{Tr(T.DoYouWantProceed)}</p>
},
onSubmit: handleDelete
}]}
/>

View File

@ -51,21 +51,20 @@ const VmStorageTab = ({ tabProps: { actions } = {} }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'attach-disk',
label: `${Tr(T.Attach)} ${Tr(T.Disk)}`
}}
dialogProps={{
title: `${Tr(T.Attach)} ${Tr(T.Disk)}`
label: Tr(T.AttachDisk)
}}
options={[
{
cy: 'attach-image-disk',
name: T.Image,
dialogProps: { title: T.AttachImage },
form: () => ImageSteps({ hypervisor }),
onSubmit: handleAttachDisk
},
{
cy: 'attach-volatile-disk',
name: T.Volatile,
dialogProps: { title: T.AttachVolatile },
form: () => VolatileSteps({ hypervisor }),
onSubmit: handleAttachDisk
}

View File

@ -29,41 +29,79 @@ module.exports = {
Active: 'Active',
AddAction: 'Add action',
Attach: 'Attach',
AttachDisk: 'Attach disk',
AttachImage: 'Attach image disk',
AttachVolatile: 'Attach volatile disk',
AttachNic: 'Attach NIC',
BackToList: 'Back to %s list',
Cancel: 'Cancel',
Change: 'Change',
ChangeGroup: 'Change group',
ChangeOwner: 'Change owner',
Clone: 'Clone',
Configuration: 'Configuration',
Create: 'Create',
Delete: 'Delete',
DeleteSomething: 'Delete %s',
DeleteScheduledAction: 'Delete scheduled action: %s',
DeleteSomething: 'Delete: %s',
Deploy: 'Deploy',
Detach: 'Detach',
DetachSomething: 'Detach %s',
DetachSomething: 'Detach: %s',
Done: 'Done',
Edit: 'Edit',
EditSomething: 'Edit: %s',
Finish: 'Finish',
Hold: 'Hold',
Info: 'Info',
Instantiate: 'Instantiate',
Lock: 'Lock',
Migrate: 'Migrate',
MigrateLive: 'Migrate live',
Poweroff: 'Poweroff',
PoweroffHard: 'Poweroff hard',
Reboot: 'Reboot',
RebootHard: 'Reboot hard',
Recover: 'Recover',
Refresh: 'Refresh',
Release: 'Release',
Remove: 'Remove',
Rename: 'Rename',
RenameSomething: 'Rename: %s',
Reschedule: 'Reschedule',
Resize: 'Resize',
ResizeSomething: 'Resize: %s',
ResizeCapacity: 'Resize capacity',
Resume: 'Resume',
Revert: 'Revert',
RevertSomething: 'Revert: %s',
Terminate: 'Terminate',
TerminateHard: 'Terminate hard',
Save: 'Save',
SaveAs: 'Save as',
SaveAsImage: 'Save as Image',
SaveAsTemplate: 'Save as Template',
Search: 'Search',
Select: 'Select',
SelectVmTemplate: 'Select a VM Template',
SelectGroup: 'Select a group',
SelectRequest: 'Select request',
SelectVmTemplate: 'Select a VM Template',
Show: 'Show',
ShowAll: 'Show all',
SignIn: 'Sign In',
SignOut: 'Sign Out',
Stop: 'Stop',
Submit: 'Submit',
Suspend: 'Suspend',
Take: 'Take',
TakeSnapshot: 'Take snapshot',
TakeSnapshotOf: 'Take snapshot of %s',
TakeSnapshotSomething: 'Take snapshot: %s',
TakeSnapshotOf: 'Take snapshot: %s',
Undeploy: 'Undeploy',
UndeployHard: 'Undeploy hard',
Unlock: 'Unlock',
UnReschedule: 'Un-Reschedule',
Update: 'Update',
UpdateScheduledAction: 'Update scheduled action: %s',
/* questions */
Yes: 'Yes',
@ -241,7 +279,6 @@ module.exports = {
/* instances schema */
IP: 'IP',
Reschedule: 'Reschedule',
DeployID: 'Deploy ID',
Deployment: 'Deployment',
Monitoring: 'Monitoring',

View File

@ -432,39 +432,41 @@ export const VM_LCM_STATES = [
/** @enum {string} Virtual machine actions */
export const VM_ACTIONS = {
REFRESH: 'refresh',
CREATE_DIALOG: 'create_dialog',
DEPLOY: 'deploy',
MIGRATE: 'migrate',
MIGRATE_LIVE: 'migrate_live',
MIGRATE_POFF: 'migrate_poff',
MIGRATE_POFF_HARD: 'migrate_poff_hard',
HOLD: 'hold',
RELEASE: 'release',
SUSPEND: 'suspend',
RESUME: 'resume',
STOP: 'stop',
RECOVER: 'recover',
REBOOT: 'reboot',
REBOOT_HARD: 'reboot_hard',
POWEROFF: 'poweroff',
LOCK: 'lock',
MIGRATE_LIVE: 'migrate_live',
MIGRATE_POFF_HARD: 'migrate_poff_hard',
MIGRATE_POFF: 'migrate_poff',
MIGRATE: 'migrate',
POWEROFF_HARD: 'poweroff_hard',
UNDEPLOY: 'undeploy',
UNDEPLOY_HARD: 'undeploy_hard',
TERMINATE: 'terminate',
TERMINATE_HARD: 'terminate_hard',
POWEROFF: 'poweroff',
REBOOT_HARD: 'reboot_hard',
REBOOT: 'reboot',
RECOVER: 'recover',
RELEASE: 'release',
RESCHED: 'resched',
UNRESCHED: 'unresched',
RESUME: 'resume',
SAVE_AS_TEMPLATE: 'save_as_template',
LOCK: 'lockU',
STOP: 'stop',
SUSPEND: 'suspend',
TERMINATE_HARD: 'terminate_hard',
TERMINATE: 'terminate',
UNDEPLOY_HARD: 'undeploy_hard',
UNDEPLOY: 'undeploy',
UNLOCK: 'unlock',
STAR_TVNC: 'startvnc',
STAR_TVMRC: 'startvmrc',
STAR_TSPICE: 'startspice',
UNRESCHED: 'unresched',
// REMOTE
VMRC: 'vmrc',
SPICE: 'spice',
VNC: 'vnc',
SSH: 'ssh',
RDP: 'rdp',
SAVE_RDP: 'save_rdp',
SAVE_VIRT_VIEWER: 'save_virt_viewer',
FILE_RDP: 'file_rdp',
FILE_VIRT_VIEWER: 'file_virt_viewer',
RENAME: ACTIONS.RENAME,
CHANGE_MODE: ACTIONS.CHANGE_MODE,

View File

@ -16,7 +16,7 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { useHistory, generatePath } from 'react-router-dom'
import { Container, Box } from '@material-ui/core'
import { PATH } from 'client/apps/sunstone/routesFlow'
@ -65,9 +65,8 @@ function ApplicationsTemplates () {
gridProps={{ 'data-cy': 'applications-templates' }}
CardComponent={ApplicationTemplateCard}
cardsProps={({ value }) => ({
handleEdit: () => history.push(
PATH.APPLICATIONS_TEMPLATES.EDIT.replace(':id', value?.ID)
),
handleEdit: () =>
history.push(generatePath(PATH.APPLICATIONS_TEMPLATES.EDIT, { id: value?.ID })),
handleDeploy: () => setShowDialog(value),
handleRemove: undefined // TODO
})}

View File

@ -16,7 +16,7 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { useHistory, generatePath } from 'react-router-dom'
import { useTheme, Container, Box } from '@material-ui/core'
import { Trash as DeleteIcon, Settings as EditIcon } from 'iconoir-react'
@ -88,7 +88,7 @@ function Providers () {
actions: [
{
handleClick: () =>
history.push(PATH.PROVIDERS.EDIT.replace(':id', ID)),
history.push(generatePath(PATH.PROVIDERS.EDIT, { id: ID })),
icon: <EditIcon />,
cy: 'provider-edit'
},

View File

@ -38,12 +38,14 @@ const ResponseForm = ({
const { control, handleSubmit, errors, formState } = useForm()
const onSubmit = async dataForm => {
const config = requestConfig(dataForm, { name, httpMethod, params })
try {
const config = requestConfig(dataForm, { name, httpMethod, params })
const { id, ...res } = await RestClient.request(config) ?? {}
id === 401 && console.log('ERROR')
id === 200 && handleChangeResponse(JSON.stringify(res, null, '\t'))
const { id, ...res } = await RestClient.request(config) ?? {}
handleChangeResponse(JSON.stringify(res, null, '\t'))
} catch (err) {
console.log('ERROR', err)
}
}
return (

View File

@ -19,11 +19,13 @@ import { useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { VmsTable } from 'client/components/Tables'
import VmActions from 'client/components/Tables/Vms/actions'
import VmTabs from 'client/components/Tabs/Vm'
import SplitPane from 'client/components/SplitPane'
function VirtualMachines () {
const [selectedRows, onSelectedRowsChange] = useState([])
const actions = VmActions()
const getRowIds = () =>
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
@ -38,7 +40,10 @@ function VirtualMachines () {
component={Container}
>
<SplitPane>
<VmsTable onSelectedRowsChange={onSelectedRowsChange} />
<VmsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
/>
{selectedRows?.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>

View File

@ -23,7 +23,7 @@ import { httpCodes } from 'server/utils/constants'
/**
* @param {string} type - Name of redux action
* @param {Promise} service - Request from service
* @param {Function} [wrapResult] - Function to wrapping the response
* @param {function(object, object)} [wrapResult] - Function to wrapping the response
* @returns {AsyncThunkAction} Asynchronous redux action
*/
export const createAction = (type, service, wrapResult) =>

View File

@ -26,23 +26,43 @@ export const getVm = createAction(`${VM}/detail`, vmService.getVm)
export const getVms = createAction(
`${VM}/pool`,
vmService.getVms,
(response, { vms: currentVms }) => {
(response, { [RESOURCES.vm]: currentVms }) => {
const vms = filterBy([...currentVms, ...response], 'ID')
return { [RESOURCES.vm]: vms }
}
)
export const terminateVm = createAction(
`${VM}/delete`,
payload => vmService.actionVm({
...payload,
action: {
params: { hard: false },
perform: 'terminate'
}
})
)
export const terminate = createAction(`${VM}/terminate`,
({ id }) => vmService.actionVm({ id, action: 'terminate' }))
export const terminateHard = createAction(`${VM}/terminate-hard`,
({ id }) => vmService.actionVm({ id, action: 'terminate-hard' }))
export const undeploy = createAction(`${VM}/undeploy`,
({ id }) => vmService.actionVm({ id, action: 'undeploy' }))
export const undeployHard = createAction(`${VM}/undeploy-hard`,
({ id }) => vmService.actionVm({ id, action: 'undeploy-hard' }))
export const poweroff = createAction(`${VM}/poweroff`,
({ id }) => vmService.actionVm({ id, action: 'poweroff' }))
export const poweroffHard = createAction(`${VM}/poweroff-hard`,
({ id }) => vmService.actionVm({ id, action: 'poweroff-hard' }))
export const reboot = createAction(`${VM}/reboot`,
({ id }) => vmService.actionVm({ id, action: 'reboot' }))
export const rebootHard = createAction(`${VM}/reboot-hard`,
({ id }) => vmService.actionVm({ id, action: 'reboot-hard' }))
export const hold = createAction(`${VM}/hold`,
({ id }) => vmService.actionVm({ id, action: 'hold' }))
export const release = createAction(`${VM}/release`,
({ id }) => vmService.actionVm({ id, action: 'release' }))
export const stop = createAction(`${VM}/stop`,
({ id }) => vmService.actionVm({ id, action: 'stop' }))
export const suspend = createAction(`${VM}/suspend`,
({ id }) => vmService.actionVm({ id, action: 'suspend' }))
export const resume = createAction(`${VM}/resume`,
({ id }) => vmService.actionVm({ id, action: 'resume' }))
export const resched = createAction(`${VM}/resched`,
({ id }) => vmService.actionVm({ id, action: 'resched' }))
export const unresched = createAction(`${VM}/unresched`,
({ id }) => vmService.actionVm({ id, action: 'unresched' }))
export const updateUserTemplate = createAction(`${VM}/update`, vmService.updateUserTemplate)
export const rename = createAction(`${VM}/rename`, vmService.rename)

View File

@ -36,7 +36,21 @@ export const useVmApi = () => {
return {
getVm: id => unwrapDispatch(actions.getVm({ id })),
getVms: options => unwrapDispatch(actions.getVms(options)),
terminateVm: id => unwrapDispatch(actions.terminateVm({ id })),
terminate: id => unwrapDispatch(actions.terminate({ id })),
terminateHard: id => unwrapDispatch(actions.terminateHard({ id })),
undeploy: id => unwrapDispatch(actions.undeploy({ id })),
undeployHard: id => unwrapDispatch(actions.undeployHard({ id })),
poweroff: id => unwrapDispatch(actions.poweroff({ id })),
poweroffHard: id => unwrapDispatch(actions.poweroffHard({ id })),
reboot: id => unwrapDispatch(actions.reboot({ id })),
rebootHard: id => unwrapDispatch(actions.rebootHard({ id })),
hold: id => unwrapDispatch(actions.hold({ id })),
release: id => unwrapDispatch(actions.release({ id })),
stop: id => unwrapDispatch(actions.stop({ id })),
suspend: id => unwrapDispatch(actions.suspend({ id })),
resume: id => unwrapDispatch(actions.resume({ id })),
resched: id => unwrapDispatch(actions.resched({ id })),
unresched: id => unwrapDispatch(actions.unresched({ id })),
updateUserTemplate: (id, template, replace) =>
unwrapDispatch(actions.updateUserTemplate({ id, template, replace })),
rename: (id, name) => unwrapDispatch(actions.rename({ id, name })),

View File

@ -14,7 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useReducer, useCallback, useEffect, useRef, ReducerState, ReducerAction } from 'react'
import { fakeDelay } from 'client/utils'
import { fakeDelay, isDevelopment } from 'client/utils'
const STATUS = {
INIT: 'INIT',
@ -144,7 +144,7 @@ const useFetch = (request, socket) => {
const { reload = false, delay = 0 } = options
if (!(Number.isInteger(delay) && delay >= 0)) {
console.error(`
isDevelopment() && console.error(`
Delay must be a number >= 0!
If you're using it as a function, it must also return a number >= 0.`)
}

View File

@ -14,7 +14,7 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useReducer, useCallback, useEffect, useRef, ReducerState, ReducerAction } from 'react'
import { fakeDelay } from 'client/utils'
import { fakeDelay, isDevelopment } from 'client/utils'
const STATUS = {
INIT: 'INIT',
@ -126,7 +126,7 @@ const useFetchAll = () => {
const { reload = false, delay = 0 } = options
if (!(Number.isInteger(delay) && delay >= 0)) {
console.error(`
isDevelopment() && console.error(`
Delay must be a number >= 0!
If you're using it as a function, it must also return a number >= 0.`)
}

View File

@ -238,11 +238,13 @@ const commandXMLRPC = (resource = '', method = '', defaultMethod = '') => {
const commandWithDefault = defaultMethod
? `${command}.${defaultMethod}`
: command
if (typeof method === 'string' && method !== 'action') {
if (method) {
command = allowedActions.includes(method)
? `${command}.${method}`
: commandWithDefault
}
return command
}