1
0
mirror of https://github.com/OpenNebula/one.git synced 2024-12-22 13:33:52 +03:00

F #6323: BackupJobs tab in FireEdge (#2798)

This commit is contained in:
Jorge Miguel Lobo Escalona 2023-11-02 16:27:02 +01:00 committed by GitHub
parent 72a5dee464
commit 5f0d555d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 3511 additions and 57 deletions

View File

@ -2982,7 +2982,8 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
src/fireedge/etc/sunstone/admin/backup-tab.yaml \
src/fireedge/etc/sunstone/admin/datastore-tab.yaml \
src/fireedge/etc/sunstone/admin/vdc-tab.yaml \
src/fireedge/etc/sunstone/admin/user-tab.yaml \
src/fireedge/etc/sunstone/admin/user-tab.yaml \
src/fireedge/etc/sunstone/admin/backupjobs-tab.yaml \
src/fireedge/etc/sunstone/admin/host-tab.yaml"
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \

View File

@ -0,0 +1,69 @@
---
# This file describes the information and actions available in the Backup Jobs tab
# Resource
resource_name: "BACKUPJOBS"
# Actions - Which buttons are visible to operate over the resources
actions:
create_dialog: true
update_dialog: true
delete: true
chown: true
chgrp: true
lock: true
unlock: true
start: true
cancel: true
# Filters - List of criteria to filter the resources
filters:
label: true
# Info Tabs - Which info tabs are used to show extended information
info-tabs:
info:
enabled: true
information_panel:
enabled: true
actions:
rename: true
priority: true
permissions_panel:
enabled: true
actions:
chmod: true
ownership_panel:
enabled: true
actions:
chown: true
chgrp: true
attributes_panel:
enabled: true
actions:
copy: true
add: true
edit: true
delete: true
vms:
enabled: true
sched_actions:
enabled: true
actions:
sched_action_create: true
sched_action_update: true
sched_action_delete: true
# Dialogs
dialogs:
create_dialog:
general: true
vms: true
datastores: true
sched_actions: true

View File

@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
import {
RefreshDouble as BackupIcon,
ClockOutline as BackupJobsIcon,
Server as ClusterIcon,
Db as DatastoreIcon,
Archive as FileIcon,
@ -233,6 +234,24 @@ const VDCCreate = loadable(() => import('client/containers/VDCs/Create'), {
ssr: false,
})
const BackupJobs = loadable(() => import('client/containers/BackupJobs'), {
ssr: false,
})
const BackupJobsDetail = loadable(
() => import('client/containers/BackupJobs/Detail'),
{
ssr: false,
}
)
const BackupJobsCreate = loadable(
() => import('client/containers/BackupJobs/Create'),
{
ssr: false,
}
)
// const ACLs = loadable(() => import('client/containers/ACLs'), { ssr: false })
export const PATH = {
@ -299,6 +318,11 @@ export const PATH = {
DETAIL: `/${RESOURCE_NAMES.APP}/:id`,
CREATE: `/${RESOURCE_NAMES.APP}/create`,
},
BACKUPJOBS: {
LIST: `/${RESOURCE_NAMES.BACKUPJOBS}`,
DETAIL: `/${RESOURCE_NAMES.BACKUPJOBS}/:id`,
CREATE: `/${RESOURCE_NAMES.BACKUPJOBS}/create`,
},
},
NETWORK: {
VNETS: {
@ -558,6 +582,24 @@ const ENDPOINTS = [
path: PATH.STORAGE.MARKETPLACE_APPS.CREATE,
Component: CreateMarketplaceApp,
},
{
title: T.CreateBackupJob,
path: PATH.STORAGE.BACKUPJOBS.CREATE,
Component: BackupJobsCreate,
},
{
title: T.BackupJobs,
path: PATH.STORAGE.BACKUPJOBS.LIST,
sidebar: true,
icon: BackupJobsIcon,
Component: BackupJobs,
},
{
title: T.BackupJob,
description: (params) => `#${params?.id}`,
path: PATH.STORAGE.BACKUPJOBS.DETAIL,
Component: BackupJobsDetail,
},
],
},
{

View File

@ -19,10 +19,11 @@ import { ReactElement, memo, useMemo } from 'react'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import {
CreateBackupJobSchedActionForm,
CreateCharterForm,
CreateRelativeCharterForm,
CreateSchedActionForm,
CreateRelativeSchedActionForm,
CreateSchedActionForm,
} from 'client/components/Forms/Vm'
import { Tr, Translate } from 'client/components/HOC'
@ -33,7 +34,7 @@ import {
VM_ACTIONS,
VM_ACTIONS_IN_CHARTER,
} from 'client/constants'
import { sentenceCase, hasRestrictedAttributes } from 'client/utils'
import { hasRestrictedAttributes, sentenceCase } from 'client/utils'
/**
* Returns a button to trigger form to create a scheduled action.
@ -45,34 +46,38 @@ import { sentenceCase, hasRestrictedAttributes } from 'client/utils'
* @returns {ReactElement} Button
*/
const CreateSchedButton = memo(
({ vm, relative, onSubmit, oneConfig, adminGroup }) => (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': VM_ACTIONS.SCHED_ACTION_CREATE,
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
name: T.PunctualAction,
dialogProps: {
title: T.ScheduleAction,
dataCy: 'modal-sched-actions',
({ vm, relative, onSubmit, oneConfig, adminGroup, backupjobs }) => {
const formConfig = {
stepProps: { vm, oneConfig, adminGroup },
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': VM_ACTIONS.SCHED_ACTION_CREATE,
label: T.AddAction,
variant: 'outlined',
}}
options={[
{
name: T.PunctualAction,
dialogProps: {
title: T.ScheduleAction,
dataCy: 'modal-sched-actions',
},
form: () =>
relative
? CreateRelativeSchedActionForm(formConfig)
: backupjobs
? CreateBackupJobSchedActionForm(formConfig)
: CreateSchedActionForm(formConfig),
onSubmit,
},
form: () =>
relative
? CreateRelativeSchedActionForm({
stepProps: { vm, oneConfig, adminGroup },
})
: CreateSchedActionForm({
stepProps: { vm, oneConfig, adminGroup },
}),
onSubmit,
},
]}
/>
)
]}
/>
)
}
)
/**
@ -86,7 +91,7 @@ const CreateSchedButton = memo(
* @returns {ReactElement} Button
*/
const UpdateSchedButton = memo(
({ vm, schedule, relative, onSubmit, oneConfig, adminGroup }) => {
({ vm, schedule, relative, onSubmit, oneConfig, adminGroup, backupjobs }) => {
const { ID, ACTION } = schedule
const titleAction = `#${ID} ${sentenceCase(ACTION)}`
const formConfig = {
@ -115,6 +120,8 @@ const UpdateSchedButton = memo(
form: () =>
relative
? CreateRelativeSchedActionForm(formConfig)
: backupjobs
? CreateBackupJobSchedActionForm(formConfig)
: CreateSchedActionForm(formConfig),
onSubmit,
},
@ -226,6 +233,7 @@ const ButtonPropTypes = {
schedule: PropTypes.object,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
backupjobs: PropTypes.bool,
}
CreateSchedButton.propTypes = ButtonPropTypes

View File

@ -0,0 +1,156 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Typography } from '@mui/material'
import { Tr } from 'client/components/HOC'
import { StatusCircle } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import Timer from 'client/components/Timer'
import { T } from 'client/constants'
import { Group, HighPriority, Lock, User } from 'iconoir-react'
import COLOR from 'client/constants/color'
import { timeFromMilliseconds } from 'client/models/Helper'
import PropTypes from 'prop-types'
const haveValues = function (object) {
return Object.values(object).length > 0
}
const BackupJobCard = memo(
/**
* @param {object} props - Props
* @param {object} props.template - BackupJob template
* @param {object} props.rootProps - Props to root component
* @param {function(string):Promise} [props.onClickLabel] - Callback to click label
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
* @returns {ReactElement} - Card
*/
({ template, rootProps, onClickLabel, onDeleteLabel }) => {
const classes = rowStyles()
const {
ID,
NAME,
UNAME,
GNAME,
OUTDATED_VMS,
BACKING_UP_VMS,
ERROR_VMS,
PRIORITY,
LAST_BACKUP_TIME,
LOCK,
} = template
const time = useMemo(() => {
const LastBackupTime = +LAST_BACKUP_TIME
if (LastBackupTime > 0) {
const timer = timeFromMilliseconds(LastBackupTime)
return (
<span title={timer.toFormat('ff')}>
<Timer translateWord={T.LastBackupTime} initial={timer} />
</span>
)
} else {
return ''
}
}, [LAST_BACKUP_TIME])
const status = useMemo(() => {
const completed = {
color: COLOR.success.main,
tooltip: T.Completed,
}
const noStarted = {
color: COLOR.warning.main,
tooltip: T.NotStartedYet,
}
const error = {
color: COLOR.error.main,
tooltip: T.Error,
}
const onGoing = {
color: COLOR.info.main,
tooltip: T.OnGoing,
}
if (haveValues(ERROR_VMS)) {
return error
}
if (!haveValues(OUTDATED_VMS) && !haveValues(BACKING_UP_VMS)) {
return LAST_BACKUP_TIME === '0' ? noStarted : completed
}
if (haveValues(OUTDATED_VMS)) {
return completed
}
if (haveValues(BACKING_UP_VMS)) {
return onGoing
}
}, [OUTDATED_VMS, BACKING_UP_VMS, ERROR_VMS, LAST_BACKUP_TIME])
return (
<div {...rootProps} data-cy={`backupjob-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<StatusCircle color={status.color} tooltip={Tr(status.tooltip)} />
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
{LOCK && <Lock data-cy="lock" />}
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`${Tr(T.Priority)}: ${PRIORITY}`}>
<HighPriority />
<span>{` ${PRIORITY}`}</span>
</span>
<span title={`${Tr(T.Owner)}: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`${Tr(T.Group)}: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
{time}
</div>
</div>
</div>
)
}
)
BackupJobCard.propTypes = {
template: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
onClickLabel: PropTypes.func,
onDeleteLabel: PropTypes.func,
}
BackupJobCard.displayName = 'BackupJobCard'
export default BackupJobCard

View File

@ -19,9 +19,11 @@ import { ReactElement, memo, useMemo } from 'react'
import { Box, Stack, Tooltip, Typography } from '@mui/material'
import {
Cpu,
Group,
HardDrive,
Lock,
Network,
User,
WarningCircledOutline as WarningIcon,
} from 'iconoir-react'
@ -75,6 +77,8 @@ const VirtualMachineCard = memo(
ETIME,
LOCK,
USER_TEMPLATE: { LABELS } = {},
GNAME,
UNAME,
TEMPLATE: { VCPU = '-', MEMORY } = {},
} = vm
@ -181,6 +185,18 @@ const VirtualMachineCard = memo(
<HardDrive />
<span data-cy="hostname">{HOSTNAME}</span>
</span>
{!!UNAME && (
<span title={`${Tr(T.Owner)}: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
)}
{!!GNAME && (
<span title={`${Tr(T.Group)}: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
)}
{!!ips?.length && (
<span title={`${Tr(T.IP)}`}>
<Network />

View File

@ -17,6 +17,7 @@ import AddressRangeCard from 'client/components/Cards/AddressRangeCard'
import ApplicationCard from 'client/components/Cards/ApplicationCard'
import ApplicationNetworkCard from 'client/components/Cards/ApplicationNetworkCard'
import ApplicationTemplateCard from 'client/components/Cards/ApplicationTemplateCard'
import BackupJobCard from 'client/components/Cards/BackupJobCard'
import ClusterCard from 'client/components/Cards/ClusterCard'
import DatastoreCard from 'client/components/Cards/DatastoreCard'
import DiskCard from 'client/components/Cards/DiskCard'
@ -51,6 +52,7 @@ export {
ApplicationCard,
ApplicationNetworkCard,
ApplicationTemplateCard,
BackupJobCard,
ClusterCard,
DatastoreCard,
DiskCard,

View File

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 {
FIELDS,
SCHEMA,
} from 'client/components/Forms/BackupJob/AddVmsForm/schema'
import { createForm } from 'client/utils'
const AddVmsForm = createForm(SCHEMA, FIELDS, {
transformInitialValue: ({ BACKUP_VMS: backupVms }, schema) => {
const vms = (backupVms && backupVms?.split(',')) || []
return {
...schema.cast(
{ BACKUP_VMS: vms.join(','), VMS: vms },
{ stripUnknown: true }
),
}
},
})
export default AddVmsForm

View File

@ -0,0 +1,84 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ObjectSchema, array, object, string } from 'yup'
import { VmsTable } from 'client/components/Tables'
import { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
const VMS_NAME = 'VMS'
const BACKUP_VMS_NAME = 'BACKUP_VMS'
/** @type {Field} DataTable field */
const VMS = {
name: VMS_NAME,
type: INPUT_TYPES.TABLE,
Table: () => VmsTable,
singleSelect: false,
validation: array(string().trim())
.required()
.default(() => undefined),
grid: { md: 12 },
value: (values, form) => {
const { VMS: vms } = values || {}
if (vms && form?.setValue) {
form?.setValue(VMS_NAME, vms)
}
},
}
/** @type {Field} Order Backup Vms field */
export const BACKUP_VMS = {
name: BACKUP_VMS_NAME,
label: T.VMsBackupJobOrder,
type: INPUT_TYPES.TEXT,
dependOf: [VMS_NAME],
watcher:
([vms = []] = []) =>
(value = '') => {
const arrayValue = (value && value?.split(',')) || []
let rtn = []
rtn = arrayValue
vms.forEach((vm) => {
if (!rtn.includes(vm)) {
rtn.push(vm)
}
})
const positionDelete = []
rtn.forEach((vm, i) => {
if (!vms.includes(vm)) {
positionDelete.push(i)
}
})
positionDelete
.sort((a, b) => b - a)
.forEach((index) => {
rtn.splice(index, 1)
})
return rtn.join(',')
},
multiline: true,
validation: string().trim(),
grid: { md: 12 },
}
/** @type {Field[]} List of fields */
export const FIELDS = [VMS, BACKUP_VMS]
/** @type {ObjectSchema} Schema */
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,70 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { SCHEMA } from 'client/components/Forms/BackupJob/CreateForm/Steps/DatastoreTable/schema'
import { DatastoresTable } from 'client/components/Tables'
import { T } from 'client/constants'
import { Step } from 'client/utils'
export const STEP_ID = 'datastores'
const Content = ({ data }) => {
const { NAME } = data?.[0] ?? {}
const { setValue } = useFormContext()
const handleSelectedRows = (rows) => {
const dataRows = rows?.map?.(({ original }) => original)
setValue(STEP_ID, dataRows)
}
return (
<DatastoresTable
singleSelect
disableGlobalSort
displaySelectedRows
pageSize={5}
getRowId={(row) => String(row.NAME)}
initialState={{
selectedRowIds: { [NAME]: true },
}}
onSelectedRowsChange={handleSelectedRows}
/>
)
}
/**
* Step to select the Datastore.
*
* @param {object} app - Marketplace App resource
* @returns {Step} Datastore step
*/
const DatastoresStep = (app) => ({
id: STEP_ID,
label: T.SelectDatastores,
resolver: SCHEMA,
content: (props) => Content({ ...props, app }),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
app: PropTypes.object,
}
export default DatastoresStep

View File

@ -0,0 +1,21 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ArraySchema, array, object } from 'yup'
/** @type {ArraySchema} Datastore table schema */
export const SCHEMA = array(object())
.ensure()
.default(() => [])

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 './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.General,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default General

View File

@ -0,0 +1,106 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 {
BACKUP_MODE_OPTIONS,
FS_FREEZE_OPTIONS,
INPUT_TYPES,
T,
} from 'client/constants'
import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
import { ObjectSchema, boolean, number, object, string } from 'yup'
const NAME = {
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string().trim().required(),
grid: { xs: 12, md: 6 },
}
const PRIORITY = {
name: 'PRIORITY',
label: T.Priority,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: number()
.positive()
.required()
.default(() => 1),
}
const FS_FREEZE = {
name: 'FS_FREEZE',
label: T.FSFreeze,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.keys(FS_FREEZE_OPTIONS), {
getText: (type) => type,
getValue: (type) => FS_FREEZE_OPTIONS[type],
}),
validation: string().trim(),
grid: { xs: 12, md: 6 },
}
const MODE = {
name: 'MODE',
label: T.Mode,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.keys(BACKUP_MODE_OPTIONS), {
addEmpty: true,
getText: (type) => type,
getValue: (type) => BACKUP_MODE_OPTIONS[type],
}),
validation: string().trim(),
grid: { xs: 12, md: 6 },
}
const KEEP_LAST = {
name: 'KEEP_LAST',
label: T.KeepLast,
type: INPUT_TYPES.TEXT,
htmlType: 'number',
validation: number()
.positive()
.required()
.default(() => 1),
}
/** @type {Field} Persistent field */
export const BACKUP_VOLATILE = {
name: 'BACKUP_VOLATILE',
label: T.MakePersistent,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { xs: 12, md: 6 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [
NAME,
PRIORITY,
FS_FREEZE,
MODE,
KEEP_LAST,
BACKUP_VOLATILE,
]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,143 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Box, Stack, styled } from '@mui/material'
import {
CreateSchedButton,
DeleteSchedButton,
UpdateSchedButton,
} from 'client/components/Buttons/ScheduleAction'
import { ScheduleActionCard } from 'client/components/Cards'
import { T } from 'client/constants'
import { Step, cleanEmpty } from 'client/utils'
import { useCallback } from 'react'
import { useFormContext, useWatch } from 'react-hook-form'
import { array } from 'yup'
import PropTypes from 'prop-types'
export const STEP_ID = 'SCHED_ACTION'
const StyledContainer = styled(Box)(({ theme }) => ({
marginTop: `${theme.spacing(0.5)}`,
}))
const Content = ({ oneConfig, adminGroup }) => {
const { setValue } = useFormContext()
const scheduleActions = useWatch({
name: STEP_ID,
defaultValue: [],
})
const handleAction = useCallback(
(type, action, index) => {
const newScheduleActions = [...(scheduleActions ?? [])]
const updatedScheduleAction = {
...action,
ACTION: 'backup',
}
switch (type) {
case 'create':
newScheduleActions.push(updatedScheduleAction)
break
case 'update':
newScheduleActions[index] = updatedScheduleAction
break
default:
newScheduleActions.splice(index, 1)
break
}
setValue(STEP_ID, cleanEmpty(newScheduleActions))
},
[scheduleActions]
)
const actions = scheduleActions ?? []
return (
<StyledContainer>
<Stack flexDirection="row" gap="1em">
<CreateSchedButton
onSubmit={(newAction) => handleAction('create', newAction)}
oneConfig={oneConfig}
adminGroup={adminGroup}
backupjobs
/>
</Stack>
<Stack
pb="1em"
display="grid"
gridTemplateColumns="repeat(auto-fit, minmax(300px, 0.5fr))"
gap="1em"
mt="1em"
>
{actions?.map((schedule, index) => {
const { ID, NAME } = schedule
const fakeValues = { ...schedule, ID: index }
return (
<ScheduleActionCard
key={ID ?? NAME}
schedule={fakeValues}
actions={
<>
<UpdateSchedButton
relative
vm={{}}
schedule={fakeValues}
onSubmit={(newAction) =>
handleAction('update', newAction, index)
}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
<DeleteSchedButton
schedule={fakeValues}
onSubmit={() => handleAction('delete', index)}
oneConfig={oneConfig}
adminGroup={adminGroup}
/>
</>
}
/>
)
})}
</Stack>
</StyledContainer>
)
}
/**
* Step to select the Schedule Actions.
*
* @param {object} app - app resource
* @returns {Step} Schedule Action step
*/
const ScheduleActions = (app) => ({
id: STEP_ID,
label: T.ScheduleAction,
resolver: array().ensure(),
content: (props) => Content({ ...props, app }),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default ScheduleActions

View File

@ -0,0 +1,69 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { SCHEMA } from 'client/components/Forms/BackupJob/CreateForm/Steps/VmsTable/schema'
import { VmsTable } from 'client/components/Tables'
import { T } from 'client/constants'
import { Step } from 'client/utils'
export const STEP_ID = 'vms'
const Content = ({ data }) => {
const { NAME } = data?.[0] ?? {}
const { setValue } = useFormContext()
const handleSelectedRows = (rows) => {
const dataRows = rows?.map?.(({ original }) => original)
setValue(STEP_ID, dataRows)
}
return (
<VmsTable
disableGlobalSort
displaySelectedRows
pageSize={5}
getRowId={(row) => String(row.NAME)}
initialState={{
selectedRowIds: { [NAME]: true },
}}
onSelectedRowsChange={handleSelectedRows}
/>
)
}
/**
* Step to select the Vms.
*
* @param {object} app - BackupJob App resource
* @returns {Step} Vms step
*/
const VmsStep = (app) => ({
id: STEP_ID,
label: T.SelectVms,
resolver: SCHEMA,
content: (props) => Content({ ...props, app }),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
app: PropTypes.object,
}
export default VmsStep

View File

@ -0,0 +1,21 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ArraySchema, array, object } from 'yup'
/** @type {ArraySchema} Datastore table schema */
export const SCHEMA = array(object())
.ensure()
.default(() => [])

View File

@ -0,0 +1,46 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 './DatastoreTable'
import General, { STEP_ID as GENERAL_ID } from './General'
import ScheduleActions, { STEP_ID as SCHEDULE_ID } from './ScheduleActions'
import Vms, { STEP_ID as VMS_ID } from './VmsTable'
import { createSteps, extractIDValues } from 'client/utils'
const Steps = createSteps([General, Vms, Datastore, ScheduleActions], {
transformBeforeSubmit: (formData) => {
const {
[GENERAL_ID]: general = {},
[VMS_ID]: vms = [],
[DATASTORE_ID]: datastores = [],
[SCHEDULE_ID]: scheduleactions = [],
} = formData ?? {}
const jsonTemplate = {
...general,
BACKUP_VMS: extractIDValues(vms),
DATASTORE_ID: extractIDValues(datastores),
}
if (scheduleactions.length) {
jsonTemplate.SCHED_ACTION = scheduleactions
}
return jsonTemplate
},
})
export default Steps

View File

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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/BackupJob/CreateForm/Steps'

View File

@ -0,0 +1,34 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
import { CreateFormCallback, CreateStepsCallback } from 'client/utils/schema'
import { ReactElement } from 'react'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const AddVmsForm = (configProps) =>
AsyncLoadForm({ formPath: 'BackupJob/AddVmsForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'BackupJob/CreateForm' }, configProps)
export { AddVmsForm, CreateForm }

View File

@ -14,22 +14,22 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useState } from 'react'
import PropTypes from 'prop-types'
import { useState } from 'react'
import { Grow, Menu, MenuItem, Typography, ListItemIcon } from '@mui/material'
import { Grow, ListItemIcon, Menu, MenuItem, Typography } from '@mui/material'
import { NavArrowDown } from 'iconoir-react'
import { useDialog } from 'client/hooks'
import {
DialogConfirmation,
DialogForm,
DialogPropTypes,
} from 'client/components/Dialogs'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import SubmitButton from 'client/components/FormControl/SubmitButton'
import FormStepper from 'client/components/FormStepper'
import FormWithSchema from 'client/components/Forms/FormWithSchema'
import { Translate } from 'client/components/HOC'
import { useDialog } from 'client/hooks'
import { isDevelopment } from 'client/utils'
const ButtonToTriggerForm = ({

View File

@ -24,6 +24,8 @@ import { DateTime } from 'luxon'
import { createForm } from 'client/utils'
import {
BACKUPJOB_SCHED_FIELDS,
BACKUPJOB_SCHED_SCHEMA,
TEMPLATE_SCHED_FIELDS,
TEMPLATE_SCHED_SCHEMA,
VM_SCHED_FIELDS,
@ -127,6 +129,15 @@ const RelativeForm = createForm(TEMPLATE_SCHED_SCHEMA, TEMPLATE_SCHED_FIELDS, {
transformBeforeSubmit: commonTransformBeforeSubmit,
})
export { RelativeForm }
const BackupJobForm = createForm(
BACKUPJOB_SCHED_SCHEMA,
BACKUPJOB_SCHED_FIELDS,
{
transformInitialValue: commonTransformInitialValue,
transformBeforeSubmit: commonTransformBeforeSubmit,
}
)
export { BackupJobForm, RelativeForm }
export default CreateSchedActionForm

View File

@ -23,7 +23,7 @@ import {
} from 'client/components/Forms/Vm/CreateSchedActionForm/fields'
import { ARGS_TYPES } from 'client/constants'
import { getRequiredArgsByAction } from 'client/models/Scheduler'
import { Field, getObjectSchemaFromFields, disableFields } from 'client/utils'
import { Field, disableFields, getObjectSchemaFromFields } from 'client/utils'
const ARG_SCHEMA = string()
.trim()
@ -136,3 +136,29 @@ export const TEMPLATE_SCHED_SCHEMA = COMMON_SCHEMA.concat(
PUNCTUAL_FIELDS.END_VALUE_FIELD,
])
)
/**
* @param {object} props - Props
* @param {object} props.vm - Vm resource
* @returns {Field[]} Fields
*/
export const BACKUPJOB_SCHED_FIELDS = ({ vm }) => [
...COMMON_FIELDS(vm, true),
PUNCTUAL_FIELDS.TIME_FIELD,
PUNCTUAL_FIELDS.END_TYPE_FIELD,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
]
/** @type {ObjectSchema} Schema */
export const BACKUPJOB_SCHED_SCHEMA = getObjectSchemaFromFields([
PUNCTUAL_FIELDS.TIME_FIELD,
PUNCTUAL_FIELDS.PERIODIC_FIELD(true),
PUNCTUAL_FIELDS.REPEAT_FIELD,
PUNCTUAL_FIELDS.WEEKLY_FIELD,
PUNCTUAL_FIELDS.MONTHLY_FIELD,
PUNCTUAL_FIELDS.YEARLY_FIELD,
PUNCTUAL_FIELDS.HOURLY_FIELD,
PUNCTUAL_FIELDS.DAYS_FIELD,
PUNCTUAL_FIELDS.END_TYPE_FIELD,
PUNCTUAL_FIELDS.END_VALUE_FIELD,
])

View File

@ -13,9 +13,9 @@
* 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'
import { ReactElement } from 'react'
/**
* @param {ConfigurationProps} configProps - Configuration
@ -139,6 +139,16 @@ const CreateRelativeSchedActionForm = (configProps) =>
configProps
)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const CreateBackupJobSchedActionForm = (configProps) =>
AsyncLoadForm(
{ formPath: 'Vm/CreateSchedActionForm', componentToLoad: 'BackupJobForm' },
configProps
)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
@ -176,6 +186,7 @@ export {
BackupForm,
ChangeGroupForm,
ChangeUserForm,
CreateBackupJobSchedActionForm,
CreateCharterForm,
CreateDiskSnapshotForm,
CreateRelativeCharterForm,

View File

@ -0,0 +1,252 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Typography } from '@mui/material'
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm'
import { useViews } from 'client/features/Auth'
import {
useCancelBackupJobMutation,
useChangeBackupJobOwnershipMutation,
useLockBackupJobMutation,
useRemoveBackupJobMutation,
useStartBackupJobMutation,
useUnlockBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
import { AddCircledOutline, Group, Lock, Trash } from 'iconoir-react'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
createActions,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils'
import { Translate } from 'client/components/HOC'
import { BACKUPJOB_ACTIONS, RESOURCE_NAMES, T } from 'client/constants'
import { isAvailableAction } from 'client/models/VirtualMachine'
const isDisabled = (action) => (rows) =>
!isAvailableAction(
action,
rows.map(({ original }) => original)
)
const ListBackupJobsNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`backupjob-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListBackupJobsNames rows={rows} />
const MessageToConfirmAction = (rows, description) => (
<>
<ListBackupJobsNames rows={rows} />
{description && <Translate word={description} />}
<Translate word={T.DoYouWantProceed} />
</>
)
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
/**
* Generates the actions to operate resources on Backup Jobs Template table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
const [changeOwnership] = useChangeBackupJobOwnershipMutation()
const [deleteBackupJob] = useRemoveBackupJobMutation()
const [lock] = useLockBackupJobMutation()
const [unlock] = useUnlockBackupJobMutation()
const [start] = useStartBackupJobMutation()
const [cancel] = useCancelBackupJobMutation()
return useMemo(
() =>
createActions({
filters: getResourceView(RESOURCE_NAMES.BACKUPJOBS)?.actions,
actions: [
{
accessor: BACKUPJOB_ACTIONS.CREATE_DIALOG,
dataCy: `backupjob_${BACKUPJOB_ACTIONS.CREATE_DIALOG}`,
tooltip: T.Create,
icon: AddCircledOutline,
action: () => history.push(PATH.STORAGE.BACKUPJOBS.CREATE),
},
{
accessor: BACKUPJOB_ACTIONS.START,
label: T.Start,
tooltip: T.Start,
selected: true,
color: 'secondary',
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Lock,
dataCy: `modal-${BACKUPJOB_ACTIONS.START}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => start({ id })))
},
},
],
},
{
accessor: BACKUPJOB_ACTIONS.CANCEL,
label: T.Cancel,
selected: true,
color: 'secondary',
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Cancel,
dataCy: `modal-${BACKUPJOB_ACTIONS.CANCEL}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => cancel({ id })))
},
},
],
},
{
tooltip: T.Ownership,
icon: Group,
selected: true,
color: 'secondary',
dataCy: 'backupjob-ownership',
options: [
{
accessor: BACKUPJOB_ACTIONS.CHANGE_OWNER,
disabled: isDisabled(BACKUPJOB_ACTIONS.CHANGE_OWNER),
name: T.ChangeOwner,
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader,
dataCy: `modal-${BACKUPJOB_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: BACKUPJOB_ACTIONS.CHANGE_GROUP,
disabled: isDisabled(BACKUPJOB_ACTIONS.CHANGE_GROUP),
name: T.ChangeGroup,
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader,
dataCy: `modal-${BACKUPJOB_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 }))
)
},
},
],
},
{
tooltip: T.Lock,
icon: Lock,
selected: true,
color: 'secondary',
dataCy: 'backupjob-lock',
options: [
{
accessor: BACKUPJOB_ACTIONS.LOCK,
name: T.Lock,
isConfirmDialog: true,
dialogProps: {
title: T.Lock,
dataCy: `modal-${BACKUPJOB_ACTIONS.LOCK}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => lock({ id })))
},
},
{
accessor: BACKUPJOB_ACTIONS.UNLOCK,
name: T.Unlock,
isConfirmDialog: true,
dialogProps: {
title: T.Unlock,
dataCy: `modal-${BACKUPJOB_ACTIONS.UNLOCK}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => unlock({ id })))
},
},
],
},
{
accessor: BACKUPJOB_ACTIONS.DELETE,
tooltip: T.Delete,
icon: Trash,
color: 'error',
selected: { min: 1 },
dataCy: `backupjob_${BACKUPJOB_ACTIONS.DELETE}`,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
dataCy: `modal-${BACKUPJOB_ACTIONS.DELETE}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => deleteBackupJob({ id })))
},
},
],
},
],
}),
[view]
)
}
export default Actions

View File

@ -0,0 +1,28 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Column } from 'react-table'
import { T } from 'client/constants'
/** @type {Column[]} VM Template columns */
const COLUMNS = [
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
{ Header: T.Name, id: 'name', accessor: 'NAME' },
]
COLUMNS.noFilterIds = ['id', 'name']
export default COLUMNS

View File

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { useViews } from 'client/features/Auth'
import { useGetBackupJobsQuery } from 'client/features/OneApi/backupjobs'
import BackupJobsColumns from 'client/components/Tables/BackupJobs/columns'
import BackupJobsRow from 'client/components/Tables/BackupJobs/row'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'backupjobs'
/**
* @param {object} props - Props
* @returns {ReactElement} Backup Jobs table
*/
const BackupJobsTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetBackupJobsQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.BACKUPJOBS)?.filters,
columns: BackupJobsColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={BackupJobsRow}
{...rest}
/>
)
}
BackupJobsTable.propTypes = { ...EnhancedTable.propTypes }
BackupJobsTable.displayName = 'BackupJobsTable'
export default BackupJobsTable

View File

@ -0,0 +1,74 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { memo, useCallback, useMemo } from 'react'
import { BackupJobCard } from 'client/components/Cards'
import backupjobApi, {
useUpdateBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
import { jsonToXml } from 'client/models/Helper'
const Row = memo(
({ original, value, onClickLabel, ...props }) => {
const [update] = useUpdateBackupJobMutation()
const state = backupjobApi.endpoints.getBackupJobs.useQueryState(
undefined,
{
selectFromResult: ({ data = [] }) =>
data.find((backupjob) => +backupjob.ID === +original.ID),
}
)
const memoBackupJob = useMemo(() => state ?? original, [state, original])
const handleDeleteLabel = useCallback(
(label) => {
const currentLabels = memoBackupJob.TEMPLATE?.LABELS?.split(',')
const newLabels = currentLabels.filter((l) => l !== label).join(',')
const newUserTemplate = { ...memoBackupJob.TEMPLATE, LABELS: newLabels }
const templateXml = jsonToXml(newUserTemplate)
update({ id: original.ID, template: templateXml, replace: 0 })
},
[memoBackupJob.TEMPLATE?.LABELS, update]
)
return (
<BackupJobCard
template={memoBackupJob}
rootProps={props}
onClickLabel={onClickLabel}
onDeleteLabel={handleDeleteLabel}
/>
)
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
className: PropTypes.string,
onClick: PropTypes.func,
onClickLabel: PropTypes.func,
}
Row.displayName = 'VirtualDataCenterRow'
export default Row

View File

@ -35,6 +35,8 @@ const VmsTable = (props) => {
searchProps = {},
initialState = {},
host,
backupjobs,
backupjobsState,
...rest
} = props ?? {}
@ -53,6 +55,7 @@ const VmsTable = (props) => {
data:
result?.data
?.filter((vm) => {
// this filters data for host
if (host?.ID) {
if (
host?.ERROR_VMS?.ID ||
@ -71,6 +74,24 @@ const VmsTable = (props) => {
return [host?.VMS?.ID ?? []].flat().includes(vm.ID)
}
// this filters data for backupjobs
if (backupjobs?.ID) {
if (backupjobsState) {
return [backupjobs?.[backupjobsState]?.ID ?? []]
.flat()
.includes(vm.ID)
} else {
return [
(backupjobs?.TEMPLATE?.BACKUP_VMS &&
backupjobs?.TEMPLATE?.BACKUP_VMS.split(',')) ??
[],
]
.flat()
.includes(vm.ID)
}
}
// This is for return data without filters
return true
})
?.filter(({ STATE }) => VM_STATES[STATE]?.name !== STATES.DONE) ?? [],

View File

@ -14,47 +14,47 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import AllImagesTable from 'client/components/Tables/AllImages'
import BackupJobsTable from 'client/components/Tables/BackupJobs'
import BackupsTable from 'client/components/Tables/Backups'
import ClustersTable from 'client/components/Tables/Clusters'
import DatastoresTable from 'client/components/Tables/Datastores'
import DataGridTable from 'client/components/Tables/DataGrid'
import DatastoresTable from 'client/components/Tables/Datastores'
import DockerHubTagsTable from 'client/components/Tables/DockerHubTags'
import EnhancedTable from 'client/components/Tables/Enhanced'
import FilesTable from 'client/components/Tables/Files'
import GroupsTable from 'client/components/Tables/Groups'
import HostsTable from 'client/components/Tables/Hosts'
import ImagesTable from 'client/components/Tables/Images'
import IncrementsTable from 'client/components/Tables/Increments'
import FilesTable from 'client/components/Tables/Files'
import MarketplaceAppsTable from 'client/components/Tables/MarketplaceApps'
import MarketplacesTable from 'client/components/Tables/Marketplaces'
import SecurityGroupsTable from 'client/components/Tables/SecurityGroups'
import ServicesTable from 'client/components/Tables/Services'
import ServiceTemplatesTable from 'client/components/Tables/ServiceTemplates'
import ServicesTable from 'client/components/Tables/Services'
import SkeletonTable from 'client/components/Tables/Skeleton'
import UsersTable from 'client/components/Tables/Users'
import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates'
import VNetworksTable from 'client/components/Tables/VNetworks'
import VRoutersTable from 'client/components/Tables/VRouters'
import VDCsTable from 'client/components/Tables/VirtualDataCenters'
import VirtualizedTable from 'client/components/Tables/Virtualized'
import VmsTable from 'client/components/Tables/Vms'
import VmGroupsTable from 'client/components/Tables/VmGroups'
import VmTemplatesTable from 'client/components/Tables/VmTemplates'
import VNetworksTable from 'client/components/Tables/VNetworks'
import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates'
import VRoutersTable from 'client/components/Tables/VRouters'
import VmsTable from 'client/components/Tables/Vms'
import ZonesTable from 'client/components/Tables/Zones'
import VDCsTable from 'client/components/Tables/VirtualDataCenters'
export * from 'client/components/Tables/Enhanced/Utils'
export {
AllImagesTable,
SkeletonTable,
EnhancedTable,
BackupJobsTable,
BackupsTable,
FilesTable,
VirtualizedTable,
ClustersTable,
DataGridTable,
DatastoresTable,
DockerHubTagsTable,
DataGridTable,
EnhancedTable,
FilesTable,
GroupsTable,
HostsTable,
ImagesTable,
@ -62,15 +62,17 @@ export {
MarketplaceAppsTable,
MarketplacesTable,
SecurityGroupsTable,
ServicesTable,
ServiceTemplatesTable,
ServicesTable,
SkeletonTable,
UsersTable,
VDCsTable,
VmsTable,
VNetworkTemplatesTable,
VNetworksTable,
VRoutersTable,
VirtualizedTable,
VmGroupsTable,
VmTemplatesTable,
VNetworksTable,
VNetworkTemplatesTable,
VRoutersTable,
VmsTable,
ZonesTable,
}

View File

@ -0,0 +1,146 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Stack } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { Tr } from 'client/components/HOC'
import Information from 'client/components/Tabs/BackupJobs/Info/information'
import {
AttributePanel,
Ownership,
Permissions,
} from 'client/components/Tabs/Common'
import { T } from 'client/constants'
import {
useChangeBackupJobOwnershipMutation,
useChangeBackupJobPermissionsMutation,
useGetBackupJobQuery,
useUpdateBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
import {
filterAttributes,
getActionsAvailable,
jsonToXml,
} from 'client/models/Helper'
import { cloneObject, set } from 'client/utils'
const HIDDEN_BACKUPJOBS_REG = /^(SCHED_ACTION|ERROR)$/
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string} props.id - Template id
* @returns {ReactElement} Information tab
*/
const BackupJobInfoTab = ({ tabProps = {}, id }) => {
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
attributes_panel: attributesPanel,
} = tabProps
const { data: backupjob = {} } = useGetBackupJobQuery({ id })
const { UNAME, UID, GNAME, GID, PERMISSIONS, TEMPLATE } = backupjob
const [changeOwnership] = useChangeBackupJobOwnershipMutation()
const [changePermissions] = useChangeBackupJobPermissionsMutation()
const [update] = useUpdateBackupJobMutation()
const getActions = (actions) => getActionsAvailable(actions)
const { attributes } = filterAttributes(TEMPLATE, {
hidden: HIDDEN_BACKUPJOBS_REG,
})
const handleChangePermission = async (newPermission) => {
await changePermissions({ id, ...newPermission })
}
const handleChangeOwnership = async (newOwnership) => {
await changeOwnership({ id, ...newOwnership })
}
const handleAttributeInXml = async (path, newValue) => {
const newTemplate = cloneObject(TEMPLATE)
set(newTemplate, path, newValue)
const xml = jsonToXml(newTemplate)
await update({ id, template: xml, replace: 0 })
}
return (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
backupjob={backupjob}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
handleEdit={handleChangePermission}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
handleEdit={handleChangeOwnership}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
/>
)}
{attributesPanel?.enabled && (
<AttributePanel
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
handleAdd={handleAttributeInXml}
handleEdit={handleAttributeInXml}
handleDelete={handleAttributeInXml}
/>
)}
</Stack>
)
}
BackupJobInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
BackupJobInfoTab.displayName = 'BackupJobInfoTab'
export default BackupJobInfoTab

View File

@ -0,0 +1,106 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import PropTypes from 'prop-types'
import { ReactElement, useMemo } from 'react'
import { List } from 'client/components/Tabs/Common'
import {
useRenameBackupJobMutation,
useUpdatePriorityBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
import { BACKUPJOB_ACTIONS, T } from 'client/constants'
import { timeFromMilliseconds } from 'client/models/Helper'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.backupjob - Template
* @param {string[]} props.actions - Available actions to information tab
* @returns {ReactElement} Information tab
*/
const InformationPanel = ({ backupjob = {}, actions }) => {
const [rename] = useRenameBackupJobMutation()
const [setPriority] = useUpdatePriorityBackupJobMutation()
const { ID, NAME, PRIORITY, LAST_BACKUP_TIME, LAST_BACKUP_DURATION } =
backupjob
const time = useMemo(() => {
const LastBackupTime = +LAST_BACKUP_TIME
if (LastBackupTime > 0) {
const timer = timeFromMilliseconds(+LAST_BACKUP_TIME)
return timer.toFormat('ff')
} else {
return '-'
}
}, [LAST_BACKUP_TIME])
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
}
const handlePriority = async (_, priority) => {
await setPriority({ id: ID, priority })
}
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
name: T.Name,
value: NAME,
canEdit: actions?.includes?.(BACKUPJOB_ACTIONS.RENAME),
handleEdit: handleRename,
dataCy: 'name',
},
{
name: T.Priority,
value: PRIORITY,
canEdit: actions?.includes?.(BACKUPJOB_ACTIONS.PRIORITY),
handleEdit: handlePriority,
dataCy: 'priority',
},
{
name: T.LastBackupTimeInfo,
value: time,
dataCy: 'lastBackupTime',
},
{
name: T.LastBackupDuration,
value: LAST_BACKUP_DURATION,
dataCy: 'lastDurationTime',
},
].filter(Boolean)
return (
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
backupjob: PropTypes.object,
}
export default InformationPanel

View File

@ -0,0 +1,153 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Stack } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement, useMemo } from 'react'
import {
CreateSchedButton,
DeleteSchedButton,
UpdateSchedButton,
} from 'client/components/Buttons/ScheduleAction'
import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
import {
useAddScheduledActionBackupJobMutation,
useDeleteScheduledActionBackupJobMutation,
useGetBackupJobQuery,
useUpdateScheduledActionBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
import { VM_ACTIONS } from 'client/constants'
import { getActionsAvailable, jsonToXml } from 'client/models/Helper'
import { getScheduleActions } from 'client/models/VirtualMachine'
const { SCHED_ACTION_CREATE, SCHED_ACTION_UPDATE, SCHED_ACTION_DELETE } =
VM_ACTIONS
/**
* Renders the list of schedule actions from a VM.
*
* @param {object} props - Props
* @param {object|boolean} props.tabProps - Tab properties
* @param {object} [props.tabProps.actions] - Actions from user view yaml
* @param {string} props.id - Virtual Machine id
* @returns {ReactElement} Schedule actions tab
*/
const BackupJobSchedulingTab = ({ tabProps: { actions } = {}, id }) => {
const [addScheduledAction] = useAddScheduledActionBackupJobMutation()
const [updateScheduledAction] = useUpdateScheduledActionBackupJobMutation()
const [deleteScheduledAction] = useDeleteScheduledActionBackupJobMutation()
const { data: backupjob = {} } = useGetBackupJobQuery({ id })
const [scheduling, actionsAvailable] = useMemo(
() => [getScheduleActions(backupjob), getActionsAvailable(actions)],
[backupjob]
)
const isCreateEnabled = actionsAvailable?.includes?.(SCHED_ACTION_CREATE)
const isUpdateEnabled = actionsAvailable?.includes?.(SCHED_ACTION_UPDATE)
const isDeleteEnabled = actionsAvailable?.includes?.(SCHED_ACTION_DELETE)
/**
* Add new schedule action to Backup Job.
*
* @param {object} formData - New schedule action
* @returns {Promise} - Add schedule action and refetch Backup Job data
*/
const handleCreateSchedAction = async (formData) => {
const template = jsonToXml({
SCHED_ACTION: { ...formData, ACTION: 'backup' },
})
await addScheduledAction({ id, template })
}
/**
* Update schedule action to Backup Job.
*
* @param {object} formData - Updated schedule action
* @param {string|number} schedId - Schedule action id
* @returns {Promise} - Update schedule action and refetch BackupJob data
*/
const handleUpdate = async (formData, schedId) => {
const template = jsonToXml({
SCHED_ACTION: { ...formData, ACTION: 'backup' },
})
await updateScheduledAction({ id, schedId, template })
}
/**
* Delete schedule action to BackupJob.
*
* @param {string|number} schedId - Schedule action id
* @returns {Promise} - Delete schedule action and refetch BackupJob data
*/
const handleRemove = async (schedId) => {
await deleteScheduledAction({ id, schedId })
}
return (
<>
{isCreateEnabled && (
<Stack flexDirection="row" gap="1em">
<CreateSchedButton
vm={backupjob}
onSubmit={handleCreateSchedAction}
backupjobs
/>
</Stack>
)}
<Stack gap="1em" py="0.8em">
{scheduling.map((schedule) => {
const { ID, NAME } = schedule
return (
<ScheduleActionCard
key={ID ?? NAME}
schedule={schedule}
actions={({ noMore }) => (
<>
{isUpdateEnabled && (
<UpdateSchedButton
vm={backupjob}
schedule={schedule}
backupjobs
onSubmit={(newAction) => handleUpdate(newAction, ID)}
/>
)}
{isDeleteEnabled && (
<DeleteSchedButton
onSubmit={() => handleRemove(ID)}
schedule={schedule}
/>
)}
</>
)}
/>
)
})}
</Stack>
</>
)
}
BackupJobSchedulingTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
BackupJobSchedulingTab.displayName = 'BackupJobSchedulingTab'
export default BackupJobSchedulingTab

View File

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { AddVmsForm } from 'client/components/Forms/BackupJob'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import { useUpdateBackupJobMutation } from 'client/features/OneApi/backupjobs'
import { jsonToXml } from 'client/models/Helper'
import Edit from 'iconoir-react/dist/Edit'
import PropTypes from 'prop-types'
import { memo } from 'react'
const AttachVms = memo(({ id, template }) => {
const [update] = useUpdateBackupJobMutation()
const formConfig = {
initialValues: template,
}
const handleEditVms = async ({ BACKUP_VMS } = {}) => {
const xml = jsonToXml({ ...template, BACKUP_VMS })
await update({ id, template: xml, replace: 0 })
}
return (
<ButtonToTriggerForm
buttonProps={{
'data-cy': `edit-vms`,
icon: <Edit />,
tooltip: Tr(T.Edit),
variant: 'outlined',
}}
options={[
{
cy: 'edit-vms',
name: T.Image,
dialogProps: {
title: T.SelectVms,
dataCy: 'modal-edit-vms',
},
form: () => AddVmsForm(formConfig),
onSubmit: handleEditVms,
},
]}
/>
)
})
AttachVms.propTypes = {
id: PropTypes.string,
template: PropTypes.object,
}
AttachVms.displayName = 'AttachVms'
export default AttachVms

View File

@ -0,0 +1,280 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 {
Alert,
Fade,
FormControl,
FormControlLabel,
List,
ListItem,
Paper,
Radio,
RadioGroup,
Typography,
} from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
import { ReactElement, useState } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { SubmitButton } from 'client/components/FormControl'
import { Translate } from 'client/components/HOC'
import { VmsTable } from 'client/components/Tables'
import AttachVms from 'client/components/Tabs/BackupJobs/VMs/Actions'
import { T } from 'client/constants'
import {
useGetBackupJobQuery,
useLazyGetBackupJobQuery,
useRetryBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
import PropTypes from 'prop-types'
const useStyles = makeStyles(({ palette, typography }) => ({
graphStyle: {
'&': {
width: '100% !important',
},
},
title: {
fontWeight: typography.fontWeightBold,
borderBottom: `1px solid ${palette.divider}`,
justifyContent: 'space-between',
},
stretch: {
flexDirection: 'column',
alignItems: 'stretch',
},
center: {
flexDirection: 'column',
alignItems: 'center',
},
box: {
marginBottom: '15px',
},
alert: {
'&': {
margin: '15px 0',
backgroundColor: palette.background.paper,
},
},
submit: {
fontSize: '1em',
},
checked: {
'& svg': {
color: 'rgba(168, 168, 168, 0.8)',
},
},
}))
const statBackingUp = 'backinUp'
const stateError = 'error'
const stateOutdated = 'outdated'
const stateAll = 'all'
const states = {
[stateAll]: {
select: T.All,
title: T.VMsBackupJob,
value: '',
},
[statBackingUp]: {
select: T.VMsBackupJobBackingUpState,
title: T.VMsBackupJobBackingUp,
value: 'BACKING_UP_VMS',
},
[stateError]: {
select: T.Error,
title: T.VMsBackupJobError,
value: 'ERROR_VMS',
},
[stateOutdated]: {
select: T.VMsBackupJobOutdatedState,
title: T.VMsBackupJobOutdated,
value: 'OUTDATED_VMS',
},
}
const AlertVmsErrors = ({
vmsErrors,
message = '',
id,
vmsOutdated,
state,
}) => {
const [get, { isFetching }] = useLazyGetBackupJobQuery()
const [retry] = useRetryBackupJobMutation()
const classes = useStyles()
const handleRetry = () => retry({ id })
return (
<>
<Fade in={!!vmsErrors?.ID && state === stateError} unmountOnExit>
<Alert
variant="outlined"
severity="error"
sx={{ gridColumn: 'span 2' }}
className={classes.alert}
action={
<SubmitButton
className={classes.submit}
onClick={handleRetry}
icon={<Translate word={T.Retry} />}
tooltip={<Translate word={T.Retry} />}
/>
}
>
{message}
</Alert>
</Fade>
<Fade in={!!vmsOutdated?.ID && state === stateError} unmountOnExit>
<Alert
variant="outlined"
severity="warning"
sx={{ gridColumn: 'span 2' }}
className={classes.alert}
action={
<SubmitButton
className={classes.submit}
icon={<RefreshDouble />}
tooltip={<Translate word={T.Refresh} />}
isSubmitting={isFetching}
onClick={() => get({ id })}
/>
}
>
<Translate word={T.BackupJobRefresh} />
</Alert>
</Fade>
</>
)
}
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - backup job id
* @returns {ReactElement} Information tab
*/
const VmsInfoTab = ({ id }) => {
const [state, setState] = useState(stateAll)
const classes = useStyles()
const path = PATH.INSTANCE.VMS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const { data: backupJobData } = useGetBackupJobQuery({ id })
const { TEMPLATE, ERROR_VMS, OUTDATED_VMS, ID } = backupJobData
const handleChangeState = (evt) => setState(evt.target.value)
return (
<>
<FormControl>
<Paper
variant="outlined"
sx={{ height: 'fit-content' }}
className={classes.box}
>
<List>
<ListItem className={classes.title}>
<Typography noWrap>
<Translate word={T.FilterBy} />
</Typography>
</ListItem>
<ListItem className={classes.center}>
<RadioGroup
row
aria-labelledby="filter_vms"
value={state}
onChange={handleChangeState}
>
{Object.keys(states).map((type) => (
<FormControlLabel
className={state === type ? classes.checked : ''}
key={type}
value={type}
control={<Radio />}
label={states[type].select}
/>
))}
</RadioGroup>
</ListItem>
</List>
</Paper>
</FormControl>
<Paper
variant="outlined"
sx={{ height: 'fit-content' }}
className={classes.box}
>
<List>
<ListItem className={classes.title}>
<Typography noWrap>
<Translate word={states?.[state]?.title || ''} />
</Typography>
<AttachVms id={ID} template={TEMPLATE} />
</ListItem>
<ListItem className={classes.stretch}>
<AlertVmsErrors
vmsErrors={ERROR_VMS}
vmsOutdated={OUTDATED_VMS}
message={TEMPLATE?.ERROR}
id={ID}
state={state}
/>
<VmsTable
disableRowSelect
disableGlobalSort
backupjobs={backupJobData}
backupjobsState={states?.[state]?.value || ''}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</ListItem>
</List>
</Paper>
</>
)
}
AlertVmsErrors.propTypes = {
vmsErrors: PropTypes.object,
message: PropTypes.string,
id: PropTypes.string,
vmsOutdated: PropTypes.object,
state: PropTypes.string,
}
AlertVmsErrors.displayName = 'AlertVmsErrors'
VmsInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VmsInfoTab.displayName = 'VmsInfoTab'
export default VmsInfoTab

View File

@ -0,0 +1,65 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Alert, LinearProgress } from '@mui/material'
import PropTypes from 'prop-types'
import { memo, useMemo } from 'react'
import { RESOURCE_NAMES } from 'client/constants'
import { useViews } from 'client/features/Auth'
import { useGetBackupJobQuery } from 'client/features/OneApi/backupjobs'
import { getAvailableInfoTabs } from 'client/models/Helper'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/BackupJobs/Info'
import SchedActions from 'client/components/Tabs/BackupJobs/SchedActions'
import VMs from 'client/components/Tabs/BackupJobs/VMs'
const getTabComponent = (tabName) =>
({
info: Info,
vms: VMs,
sched_actions: SchedActions,
}[tabName])
const BackupJobTabs = memo(({ id }) => {
const { view, getResourceView } = useViews()
const { isError, error, status, data } = useGetBackupJobQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.BACKUPJOBS
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
}, [view, id])
if (isError) {
return (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
if (status === 'fulfilled' || id === data?.ID) {
return <Tabs addBorder tabs={tabsAvailable ?? []} />
}
return <LinearProgress color="secondary" sx={{ width: '100%' }} />
})
BackupJobTabs.propTypes = { id: PropTypes.string.isRequired }
BackupJobTabs.displayName = 'BackupJobTabs'
export default BackupJobTabs

View File

@ -0,0 +1,38 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 * as ACTIONS from 'client/constants/actions'
/**
* @typedef BackupJob
* @property {string|number} ID - Id
* @property {string} NAME - Name
* @property {object} TEMPLATE - Template information
* @property {string} [TEMPLATE.DESCRIPTION] - BackupJob Description
*/
export const BACKUPJOB_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
UPDATE_DIALOG: 'update_dialog',
DELETE: 'delete',
RENAME: ACTIONS.RENAME,
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
PRIORITY: 'priority',
LOCK: 'lock',
UNLOCK: 'unlock',
START: 'start',
CANCEL: 'cancel',
}

View File

@ -181,11 +181,14 @@ export const RESOURCE_NAMES = {
SERVICE: 'service',
SERVICE_TEMPLATE: 'service-template',
ZONE: 'zone',
BACKUPJOBS: 'backupjobs',
}
export * as T from 'client/constants/translates'
export * as ACTIONS from 'client/constants/actions'
export * as STATES from 'client/constants/states'
export * from 'client/constants/backupjob'
export * from 'client/constants/cluster'
export * from 'client/constants/common'
export * from 'client/constants/datastore'

View File

@ -87,6 +87,7 @@ module.exports = {
CreateVmTemplate: 'Create VM Template',
CreateVDC: 'Create VDC',
UpdateVDC: 'Update VDC',
CreateBackupJob: 'Create BackupJob',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@ -180,6 +181,7 @@ module.exports = {
SelectTheNewGroup: 'Select the new group',
SelectTheNewOwner: 'Select the new owner',
SelectTheNewSecurityGroup: 'Select the new security group',
SelectVms: 'Select VMs',
SelectVmTemplate: 'Select a VM Template',
SelectYourActiveGroup: 'Select your active group',
Share: 'Share',
@ -280,6 +282,7 @@ module.exports = {
PasswordsMustMatch: 'Passwords must match',
Token2FA: '2FA Token',
KeepLoggedIn: 'Keep me logged in',
KeepLast: 'Keep Last',
Credentials: 'Credentials',
SwitchView: 'Switch view',
SwitchGroup: 'Switch group',
@ -408,6 +411,8 @@ module.exports = {
Backups: 'Backups',
BackupDatastore: 'Backup Datastore',
BackupRestored: 'Backup restored',
BackupJobRefresh:
'There are machines in outdated, you can refresh to see if the backups are already done',
Datastore: 'Datastore',
Datastores: 'Datastores',
Image: 'Image',
@ -444,6 +449,8 @@ module.exports = {
ResetBackup: 'Reset',
IncrementId: 'Increment ID',
RestoreBackup: 'Restore backup',
BackupJobs: 'BackupJobs',
BackupJob: 'BackupJob',
/* storage backends */
StorageBackend: 'Storage backend',
@ -484,6 +491,7 @@ module.exports = {
ResticPassword: 'Restic password',
ResticSFTPUser: 'Restic SFTP user',
ResticSFTPServer: 'Restic SFTP server',
Priority: 'Priority',
BackupIOPriority: 'Backup I/O priority',
BackupIOPriorityConcept:
'Run restic operations under a given ionice priority using the best-effort I/O scheduler',
@ -569,6 +577,13 @@ module.exports = {
Instances: 'Instances',
VM: 'VM',
VMs: 'VMs',
VMsBackupJob: 'VMs in BackupJob',
VMsBackupJobError: 'VMs in error',
VMsBackupJobBackingUp: 'VMs Backing Up',
VMsBackupJobOutdated: 'VMs Outdated',
VMsBackupJobBackingUpState: 'Backing Up',
VMsBackupJobOutdatedState: 'Outdated',
VMsBackupJobOrder: 'VM List (ordered)',
VirtualRouter: 'Virtual Router',
VirtualRouters: 'Virtual Routers',
VMGroup: 'VM Group',
@ -611,6 +626,8 @@ module.exports = {
RegistrationTime: 'Registration time',
StartTime: 'Start time',
Started: 'Started',
NotStartedYet: 'Not started yet',
OnGoing: 'On Going',
StartedOnTime: 'Started on %s',
Total: 'Total',
Prolog: 'Prolog',
@ -1198,6 +1215,7 @@ module.exports = {
UsedLeases: 'Used leases',
TotalLeases: 'Total leases',
TotalClusters: 'Total clusters',
Completed: 'Completed',
RecoverNetworkDescription: `
Recovers a Virtual Network in ERROR state or waiting for a driver operation to complete.
The recovery may be done by failing, succeeding or retrying the current operation.
@ -1307,6 +1325,9 @@ module.exports = {
/* Marketplace App schema */
/* Marketplace App - general */
RegisteredAt: 'Registered %s',
LastBackupTime: 'Last Backup Time: %s',
LastBackupTimeInfo: 'Last Backup Time',
LastBackupDuration: 'Last Backup Duration',
Version: 'Version',
AppTemplate: 'App Template',
TemplatesForTheApp: 'Templates for the App',

View File

@ -0,0 +1,59 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { PATH } from 'client/apps/sunstone/routesOne'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/BackupJob'
import { useGeneralApi } from 'client/features/General'
import { useCreateBackupJobMutation } from 'client/features/OneApi/backupjobs'
import { jsonToXml } from 'client/models/Helper'
/**
* Displays the creation or modification form to a BackupJob.
*
* @returns {ReactElement} Backup Job form
*/
function CreateBackupJob() {
const history = useHistory()
const { enqueueSuccess } = useGeneralApi()
const [create] = useCreateBackupJobMutation()
const onSubmit = async (template) => {
try {
const newBackupJobId = await create({
template: jsonToXml(template),
}).unwrap()
if (newBackupJobId) {
history.push(PATH.STORAGE.BACKUPJOBS.LIST)
enqueueSuccess(`BackupJob created - #${newBackupJobId}`)
}
} catch {}
}
return (
<CreateForm onSubmit={onSubmit} fallback={<SkeletonStepsForm />}>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
)
}
export default CreateBackupJob

View File

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { useParams, Redirect } from 'react-router-dom'
import VDCTabs from 'client/components/Tabs/Vdc'
/**
* Displays the detail information about a VDC.
*
* @returns {ReactElement} VDC detail component.
*/
function VDCDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <VDCTabs id={id} />
}
export default VDCDetail

View File

@ -0,0 +1,162 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Box, Chip, Stack, Typography } from '@mui/material'
import { Cancel, Pin as GotoIcon, RefreshDouble } from 'iconoir-react'
import PropTypes from 'prop-types'
import { ReactElement, memo, useState } from 'react'
import { Row } from 'react-table'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import MultipleTags from 'client/components/MultipleTags'
import SplitPane from 'client/components/SplitPane'
import { BackupJobsTable } from 'client/components/Tables'
import BackupJobActions from 'client/components/Tables/BackupJobs/actions'
import BackupJobsTabs from 'client/components/Tabs/BackupJobs'
import { T } from 'client/constants'
import {
useLazyGetBackupJobQuery,
useUpdateBackupJobMutation,
} from 'client/features/OneApi/backupjobs'
/**
* Displays a list of Backup Jobs with a split pane between the list and selected row(s).
*
* @returns {ReactElement} Backup Jobs list and selected row(s)
*/
function BackupJobs() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = BackupJobActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
return (
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<BackupJobsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateBackupJobMutation}
/>
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
template={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* Displays details of a Backup Job Template.
*
* @param {object} template - Backup Job Template id to display
* @param {Function} [gotoPage] - Function to navigate to a page of a Backup Job Template
* @param {Function} [unselect] - Function to unselect a Backup Job Template
* @returns {ReactElement} Backup Job Template details
*/
const InfoTabs = memo(({ template, gotoPage, unselect }) => {
const [getBackupJob, { data, isFetching }] = useLazyGetBackupJobQuery()
const id = data?.ID ?? template.ID
const name = data?.NAME ?? template.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 -- */}
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => getBackupJob({ id })}
/>
{typeof gotoPage === 'function' && (
<SubmitButton
data-cy="locate-on-table"
icon={<GotoIcon />}
tooltip={Tr(T.LocateOnTable)}
onClick={() => gotoPage()}
/>
)}
{typeof unselect === 'function' && (
<SubmitButton
data-cy="unselect"
icon={<Cancel />}
tooltip={Tr(T.Close)}
onClick={() => unselect()}
/>
)}
{/* -- END ACTIONS -- */}
</Stack>
<BackupJobsTabs id={id} />
</Stack>
)
})
InfoTabs.propTypes = {
template: PropTypes.object,
gotoPage: PropTypes.func,
unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
/**
* Displays a list of tags that represent the selected rows.
*
* @param {Row[]} tags - Row(s) to display as tags
* @returns {ReactElement} List of tags
*/
const GroupedTags = memo(({ tags = [] }) => (
<Stack direction="row" flexWrap="wrap" gap={1} alignContent="flex-start">
<MultipleTags
limitTags={10}
tags={tags?.map(({ original, id, toggleRowSelected, gotoPage }) => (
<Chip
key={id}
label={original?.NAME ?? id}
onClick={gotoPage}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
))
GroupedTags.propTypes = { tags: PropTypes.array }
GroupedTags.displayName = 'GroupedTags'
export default BackupJobs

View File

@ -0,0 +1,493 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Actions, Commands } from 'server/utils/constants/commands/backupjobs'
import { FilterFlag, Permission } from 'client/constants'
import {
ONE_RESOURCES,
ONE_RESOURCES_POOL,
oneApi,
} from 'client/features/OneApi'
import {
removeResourceOnPool,
updateNameOnResource,
updateResourceOnPool,
updateTemplateOnResource,
} from 'client/features/OneApi/common'
const { BACKUPJOB } = ONE_RESOURCES
const { BACKUPJOB_POOL } = ONE_RESOURCES_POOL
const backupjobApi = oneApi.injectEndpoints({
endpoints: (builder) => ({
getBackupJobs: builder.query({
/**
* Retrieves information for all or part of the Resources in the pool.
*
* @param {object} params - Request params
* @param {FilterFlag} [params.filter] - Filter flag
* @param {number} [params.start] - Range start ID
* @param {number} [params.end] - Range end ID
* @returns {Array[Object]} List of Backup Jobs
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_POOL_INFO
const command = { name, ...Commands[name] }
return { params, command }
},
transformResponse: (data) =>
[data?.BACKUPJOB_POOL?.BACKUPJOB ?? []].flat(),
providesTags: (backupjobs) =>
backupjobs
? [
...backupjobs.map(({ ID }) => ({
type: BACKUPJOB_POOL,
id: `${ID}`,
})),
BACKUPJOB_POOL,
]
: [BACKUPJOB_POOL],
}),
getBackupJob: builder.query({
/**
* Retrieves information for the BackupJob.
*
* @param {object} params - Request parameters
* @param {string} params.id - BackupJob id
* @param {boolean} [params.decrypt] - True to decrypt contained secrets (only admin)
* @returns {object} Get VDC identified by id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_INFO
const command = { name, ...Commands[name] }
return { params, command }
},
transformResponse: (data) => data?.BACKUPJOB ?? {},
providesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: resourceFromQuery } = await queryFulfilled
dispatch(
backupjobApi.util.updateQueryData(
'getBackupJobs',
undefined,
updateResourceOnPool({ id, resourceFromQuery })
)
)
} catch {
// if the query fails, we want to remove the resource from the pool
dispatch(
backupjobApi.util.updateQueryData(
'getBackupJobs',
undefined,
removeResourceOnPool({ id })
)
)
}
},
}),
allocateBackupJob: builder.mutation({
/**
* Allocates a new backup job in OpenNebula.
*
* @param {object} params - Request params
* @param {string} params.template - Template for the new backupjob
* @returns {number} The allocated backup job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_ALLOCATE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [BACKUPJOB_POOL],
}),
removeBackupJob: builder.mutation({
/**
* Deletes the given Backup Job from the pool.
*
* @param {object} params - Request params
* @param {number|string} params.id - Backup Job id
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_DELETE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [BACKUPJOB_POOL],
}),
createBackupJob: builder.mutation({
/**
* Creates a new BackupJob in OpenNebula.
*
* @param {object} params - Request params
* @param {string} params.template - Backup Job Template
* @returns {number} BackupJob id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_ALLOCATE
const command = { name, ...Commands[name] }
return { params, command }
},
}),
updateBackupJob: builder.mutation({
/**
* Replaces the template contents.
*
* @param {object} params - Request params
* @param {number|string} params.id - Backup Job id
* @param {string} params.template - The new template contents
* @param {0|1} params.replace
* - Update type:
* ``0``: Replace the whole template.
* ``1``: Merge new template with the existing one.
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_UPDATE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchBackupJob = dispatch(
backupjobApi.util.updateQueryData(
'getBackupJob',
{ id: params.id },
updateTemplateOnResource(params)
)
)
const patchBackupJobs = dispatch(
backupjobApi.util.updateQueryData(
'getBackupJobs',
undefined,
updateTemplateOnResource(params)
)
)
queryFulfilled.catch(() => {
patchBackupJob.undo()
patchBackupJobs.undo()
})
} catch {}
},
}),
renameBackupJob: builder.mutation({
/**
* Renames a Backup Job.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup Job id
* @param {string} params.name - The new name
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_RENAME
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchBackupJob = dispatch(
backupjobApi.util.updateQueryData(
'getBackupJob',
{ id: params.id },
updateNameOnResource(params)
)
)
const patchBackupJobs = dispatch(
backupjobApi.util.updateQueryData(
'getBackupJobs',
undefined,
updateNameOnResource(params)
)
)
queryFulfilled.catch(() => {
patchBackupJob.undo()
patchBackupJobs.undo()
})
} catch {}
},
}),
changeBackupJobOwnership: builder.mutation({
/**
* Changes the ownership of a Backup Job.
* If set `user` or `group` to -1, it's not changed.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup Job id
* @param {string|number|'-1'} [params.userId] - User id
* @param {Permission|'-1'} [params.groupId] - Group id
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_CHOWN
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: BACKUPJOB, id },
BACKUPJOB_POOL,
],
}),
lockBackupJob: builder.mutation({
/**
* Locks an backup job. Lock certain actions depending on blocking level.
* - `USE` (1): locks Admin, Manage and Use actions.
* - `MANAGE` (2): locks Manage and Use actions.
* - `ADMIN` (3): locks only Admin actions.
* - `ALL` (4): locks all actions.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup job id
* @param {'1'|'2'|'3'|'4'} params.lock - Lock level
* @returns {number} Backup job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_LOCK
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: BACKUPJOB, id },
BACKUPJOB_POOL,
],
}),
unlockBackupJob: builder.mutation({
/**
* Unlocks an Backupjob.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup job id
* @returns {number} Backup job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_UNLOCK
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [
{ type: BACKUPJOB, id },
BACKUPJOB_POOL,
],
}),
startBackupJob: builder.mutation({
/**
* Start an Backupjob.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup job id
* @returns {number} Backup job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_BACKUP
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
cancelBackupJob: builder.mutation({
/**
* Cancel an Backupjob.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup job id
* @returns {number} Backup job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_CANCEL
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
retryBackupJob: builder.mutation({
/**
* Retry an Backupjob.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup job id
* @returns {number} Backup job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_RETRY
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
changeBackupJobPermissions: builder.mutation({
/**
* Changes the permission bits of a Backup Job.
* If set any permission to -1, it's not changed.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup Job id
* @param {Permission|'-1'} params.ownerUse - User use
* @param {Permission|'-1'} params.ownerManage - User manage
* @param {Permission|'-1'} params.ownerAdmin - User administrator
* @param {Permission|'-1'} params.groupUse - Group use
* @param {Permission|'-1'} params.groupManage - Group manage
* @param {Permission|'-1'} params.groupAdmin - Group administrator
* @param {Permission|'-1'} params.otherUse - Other use
* @param {Permission|'-1'} params.otherManage - Other manage
* @param {Permission|'-1'} params.otherAdmin - Other administrator
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_CHMOD
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
addScheduledActionBackupJob: builder.mutation({
/**
* Add scheduled action to Backup Job.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - BackupJob id
* @param {string} params.template - Template containing the new scheduled action
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_SCHED_ADD
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
updateScheduledActionBackupJob: builder.mutation({
/**
* Update scheduled action to Backup Job.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup Job id
* @param {string} params.schedId - The ID of the scheduled action
* @param {string} params.template - Template containing the updated scheduled action
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_SCHED_UPDATE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
deleteScheduledActionBackupJob: builder.mutation({
/**
* Delete scheduled action to Backup Job.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup Job id
* @param {string} params.schedId - The ID of the scheduled action
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_SCHED_DELETE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
updatePriorityBackupJob: builder.mutation({
/**
* Update Priority to Backup Job.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Backup Job id
* @param {number} params.priority - priority number
* @returns {number} Backup Job id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.BACKUPJOB_PRIORITY
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: BACKUPJOB, id }],
}),
}),
})
export const {
// Queries
useGetBackupJobsQuery,
useLazyGetBackupJobsQuery,
useGetBackupJobQuery,
useLazyGetBackupJobQuery,
// Mutations
useAllocateBackupJobMutation,
useUpdatePriorityBackupJobMutation,
useRemoveBackupJobMutation,
useCreateBackupJobMutation,
useUpdateBackupJobMutation,
useRenameBackupJobMutation,
useChangeBackupJobOwnershipMutation,
useLockBackupJobMutation,
useUnlockBackupJobMutation,
useStartBackupJobMutation,
useCancelBackupJobMutation,
useRetryBackupJobMutation,
useChangeBackupJobPermissionsMutation,
useAddScheduledActionBackupJobMutation,
useUpdateScheduledActionBackupJobMutation,
useDeleteScheduledActionBackupJobMutation,
} = backupjobApi
export default backupjobApi

View File

@ -23,6 +23,7 @@ import { httpCodes } from 'server/utils/constants'
const ONE_RESOURCES = {
ACL: 'ACL',
APP: 'APP',
BACKUPJOB: 'BACKUPJOB',
CLUSTER: 'CLUSTER',
DATASTORE: 'DATASTORE',
FILE: 'FILE',

View File

@ -494,3 +494,19 @@ export const getUnknownAttributes = (obj = {}, knownAttributes) => {
return unknown
}
/**
* Extract the Ids for values selected in datatables.
*
* @param {any} arr - Data for Datatables.
* @returns {string} Returns string with ids.
*/
export const extractIDValues = (arr = []) => {
const dataArray = Array.isArray(arr) ? arr : [arr]
const idValues = dataArray
// eslint-disable-next-line no-prototype-builtins
.filter((obj) => obj.hasOwnProperty('ID'))
.map((obj) => obj.ID)
return idValues.join(',')
}

View File

@ -0,0 +1,333 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 {
from: { resource, postBody, query },
httpMethod: { GET, POST, PUT, DELETE },
} = require('../defaults')
const baseCommand = 'backupjob'
const baseCommandPool = `${baseCommand}pool`
const BACKUPJOB_ALLOCATE = `${baseCommand}.allocate`
const BACKUPJOB_DELETE = `${baseCommand}.delete`
const BACKUPJOB_INFO = `${baseCommand}.info`
const BACKUPJOB_UPDATE = `${baseCommand}.update`
const BACKUPJOB_RENAME = `${baseCommand}.rename`
const BACKUPJOB_CHOWN = `${baseCommand}.chown`
const BACKUPJOB_CHMOD = `${baseCommand}.chmod`
const BACKUPJOB_LOCK = `${baseCommand}.lock`
const BACKUPJOB_UNLOCK = `${baseCommand}.unlock`
const BACKUPJOB_BACKUP = `${baseCommand}.backup`
const BACKUPJOB_CANCEL = `${baseCommand}.cancel`
const BACKUPJOB_RETRY = `${baseCommand}.retry`
const BACKUPJOB_PRIORITY = `${baseCommand}.priority`
const BACKUPJOB_SCHED_ADD = `${baseCommand}.schedadd`
const BACKUPJOB_SCHED_DELETE = `${baseCommand}.scheddelete`
const BACKUPJOB_SCHED_UPDATE = `${baseCommand}.schedupdate`
const BACKUPJOB_POOL_INFO = `${baseCommandPool}.info`
const Actions = {
BACKUPJOB_ALLOCATE,
BACKUPJOB_DELETE,
BACKUPJOB_INFO,
BACKUPJOB_UPDATE,
BACKUPJOB_RENAME,
BACKUPJOB_CHOWN,
BACKUPJOB_CHMOD,
BACKUPJOB_LOCK,
BACKUPJOB_UNLOCK,
BACKUPJOB_BACKUP,
BACKUPJOB_CANCEL,
BACKUPJOB_RETRY,
BACKUPJOB_PRIORITY,
BACKUPJOB_SCHED_ADD,
BACKUPJOB_SCHED_DELETE,
BACKUPJOB_SCHED_UPDATE,
BACKUPJOB_POOL_INFO,
}
module.exports = {
Actions,
Commands: {
[BACKUPJOB_ALLOCATE]: {
// inspected
httpMethod: POST,
params: {
template: {
from: postBody,
default: '',
},
},
},
[BACKUPJOB_DELETE]: {
// inspected
httpMethod: DELETE,
params: {
id: {
from: resource,
default: 0,
},
},
},
[BACKUPJOB_INFO]: {
// inspected
httpMethod: GET,
params: {
id: {
from: resource,
default: -1,
},
decrypt: {
from: query,
default: false,
},
},
},
[BACKUPJOB_UPDATE]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
template: {
from: postBody,
default: '',
},
replace: {
from: postBody,
default: 0,
},
},
},
[BACKUPJOB_RENAME]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
name: {
from: postBody,
default: '',
},
},
},
[BACKUPJOB_CHOWN]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
user: {
from: postBody,
default: -1,
},
group: {
from: postBody,
default: -1,
},
},
},
[BACKUPJOB_CHMOD]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
ownerUse: {
from: postBody,
default: -1,
},
ownerManage: {
from: postBody,
default: -1,
},
ownerAdmin: {
from: postBody,
default: -1,
},
groupUse: {
from: postBody,
default: -1,
},
groupManage: {
from: postBody,
default: -1,
},
groupAdmin: {
from: postBody,
default: -1,
},
otherUse: {
from: postBody,
default: -1,
},
otherManage: {
from: postBody,
default: -1,
},
otherAdmin: {
from: postBody,
default: -1,
},
},
},
[BACKUPJOB_LOCK]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
level: {
from: postBody,
default: 4,
},
test: {
from: postBody,
default: false,
},
},
},
[BACKUPJOB_UNLOCK]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
},
},
[BACKUPJOB_BACKUP]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
},
},
[BACKUPJOB_CANCEL]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
},
},
[BACKUPJOB_RETRY]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
},
},
[BACKUPJOB_PRIORITY]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
priority: {
from: postBody,
default: 0,
},
},
},
[BACKUPJOB_SCHED_ADD]: {
// inspected
httpMethod: POST,
params: {
id: {
from: resource,
default: 0,
},
template: {
from: postBody,
default: '',
},
},
},
[BACKUPJOB_SCHED_DELETE]: {
// inspected
httpMethod: DELETE,
params: {
id: {
from: resource,
default: 0,
},
schedId: {
from: postBody,
default: 0,
},
},
},
[BACKUPJOB_SCHED_UPDATE]: {
// inspected
httpMethod: PUT,
params: {
id: {
from: resource,
default: 0,
},
schedId: {
from: postBody,
default: 0,
},
template: {
from: postBody,
default: '',
},
},
},
[BACKUPJOB_POOL_INFO]: {
// inspected
httpMethod: GET,
params: {
filter: {
from: query,
default: -2,
},
start: {
from: query,
default: -1,
},
end: {
from: query,
default: -1,
},
},
},
},
}

View File

@ -15,6 +15,7 @@
* ------------------------------------------------------------------------- */
const { Commands: acl } = require('./acl')
const { Commands: backupjobs } = require('./backupjobs')
const { Commands: cluster } = require('./cluster')
const { Commands: datastore } = require('./datastore')
const { Commands: document } = require('./document')
@ -38,6 +39,7 @@ const { Commands: zone } = require('./zone')
module.exports = {
...acl,
...backupjobs,
...cluster,
...datastore,
...document,