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

F #5422: Add change owner and group forms (#1508)

This commit is contained in:
Sergio Betanzos 2021-10-07 13:15:40 +02:00 committed by GitHub
parent 0620c79d63
commit 33bf5e1f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 311 additions and 44 deletions

View File

@ -100,7 +100,7 @@ const DialogConfirmation = memo(
</Typography>
)}
{subheader && (
<Typography variant='subtitle1'>
<Typography variant='body1'>
{typeof subheader === 'string' ? Tr(subheader) : subheader}
</Typography>
)}
@ -144,7 +144,10 @@ export const DialogPropTypes = {
PropTypes.string,
PropTypes.node
]),
subheader: PropTypes.string,
subheader: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
contentProps: PropTypes.object,
handleAccept: PropTypes.func,
acceptButtonProps: PropTypes.object,

View File

@ -0,0 +1,100 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { Typography } from '@mui/material'
import { Controller } from 'react-hook-form'
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
const defaultGetRowId = item => typeof item === 'object' ? item?.id ?? item?.ID : item
const TableController = memo(
({ control, cy, name, Table, singleSelect = true, getRowId = defaultGetRowId, label, tooltip, error, formContext }) => {
const { clearErrors } = formContext
return (
<>
{error ? (
<ErrorHelper data-cy={`${cy}-error`} label={error?.message} mb={2} />
) : (
label && (
<Typography variant='body1' mb={2}>
{tooltip && <Tooltip title={tooltip} position='start' />}
{typeof label === 'string' ? Tr(label) : label}
</Typography>
)
)}
<Controller
render={({ onChange }) => (
<Table
pageSize={4}
singleSelect={singleSelect}
onlyGlobalSearch
onlyGlobalSelectedRows
getRowId={getRowId}
onSelectedRowsChange={rows => {
const rowValues = rows?.map(({ original }) => getRowId(original))
onChange(singleSelect ? rowValues?.[0] : rowValues)
clearErrors(name)
}}
/>
)}
name={name}
control={control}
/>
</>
)
},
(prevProps, nextProps) =>
prevProps.error === nextProps.error &&
prevProps.label === nextProps.label &&
prevProps.tooltip === nextProps.tooltip
)
TableController.propTypes = {
control: PropTypes.object,
cy: PropTypes.string,
type: PropTypes.string,
singleSelect: PropTypes.bool,
Table: PropTypes.any,
getRowId: PropTypes.func,
name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
tooltip: PropTypes.string,
error: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.objectOf(PropTypes.any)
]),
fieldProps: PropTypes.object,
formContext: PropTypes.shape({
setValue: PropTypes.func,
setError: PropTypes.func,
clearErrors: PropTypes.func,
watch: PropTypes.func,
register: PropTypes.func
})
}
TableController.displayName = 'TableController'
export default TableController

View File

@ -20,6 +20,7 @@ import PasswordController from 'client/components/FormControl/PasswordController
import SelectController from 'client/components/FormControl/SelectController'
import SliderController from 'client/components/FormControl/SliderController'
import SwitchController from 'client/components/FormControl/SwitchController'
import TableController from 'client/components/FormControl/TableController'
import TextController from 'client/components/FormControl/TextController'
import TimeController from 'client/components/FormControl/TimeController'
@ -36,6 +37,7 @@ export {
SelectController,
SliderController,
SwitchController,
TableController,
TextController,
TimeController,

View File

@ -33,7 +33,7 @@ const Legend = styled('legend')(({ theme }) => ({
borderBottom: `2px solid ${theme.palette.secondary.main}`
}))
const NOT_DEPEND_ATTRIBUTES = ['transform']
const NOT_DEPEND_ATTRIBUTES = ['transform', 'Table']
const INPUT_CONTROLLER = {
[INPUT_TYPES.TEXT]: FC.TextController,
@ -44,7 +44,8 @@ const INPUT_CONTROLLER = {
[INPUT_TYPES.CHECKBOX]: FC.CheckboxController,
[INPUT_TYPES.AUTOCOMPLETE]: FC.AutocompleteController,
[INPUT_TYPES.FILE]: FC.FileController,
[INPUT_TYPES.TIME]: FC.TimeController
[INPUT_TYPES.TIME]: FC.TimeController,
[INPUT_TYPES.TABLE]: FC.TableController
}
const FormWithSchema = ({ id, cy, fields, className, legend }) => {

View File

@ -0,0 +1,21 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 { createForm } from 'client/utils'
import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/ChangeGroupForm/schema'
const ChangeGroupForm = createForm(SCHEMA, FIELDS)
export default ChangeGroupForm

View File

@ -0,0 +1,37 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { string, object } from 'yup'
import { GroupsTable } from 'client/components/Tables'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
const GROUP = {
name: 'group',
label: 'Select the new group',
type: INPUT_TYPES.TABLE,
Table: GroupsTable,
validation: string()
.trim()
.required('You must select a group')
.default(() => undefined),
grid: { md: 12 }
}
export const FIELDS = [GROUP]
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,21 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 { createForm } from 'client/utils'
import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/ChangeUserForm/schema'
const ChangeUserForm = createForm(SCHEMA, FIELDS)
export default ChangeUserForm

View File

@ -0,0 +1,37 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { string, object } from 'yup'
import { UsersTable } from 'client/components/Tables'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
const USER = {
name: 'user',
label: 'Select the new owner',
type: INPUT_TYPES.TABLE,
Table: UsersTable,
validation: string()
.trim()
.required('You must select an user')
.default(() => undefined),
grid: { md: 12 }
}
export const FIELDS = [USER]
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -23,7 +23,7 @@ const RecoverForm = createForm(
FIELDS,
{
description: (
<Typography variant='subtitle1' paddingX='1rem'>
<Typography variant='subtitle1' padding='1rem'>
{`Recovers a stuck VM that is waiting for a driver operation.
The recovery may be done by failing, succeeding or retrying the
current operation. YOU NEED TO MANUALLY CHECK THE VM STATUS ON THE HOST,

View File

@ -14,6 +14,8 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import AttachNicForm from 'client/components/Forms/Vm/AttachNicForm'
import ChangeUserForm from 'client/components/Forms/Vm/ChangeUserForm'
import ChangeGroupForm from 'client/components/Forms/Vm/ChangeGroupForm'
import CreateDiskSnapshotForm from 'client/components/Forms/Vm/CreateDiskSnapshotForm'
import CreateSnapshotForm from 'client/components/Forms/Vm/CreateSnapshotForm'
import RecoverForm from 'client/components/Forms/Vm/RecoverForm'
@ -25,6 +27,8 @@ export * from 'client/components/Forms/Vm/CreateSchedActionForm'
export {
AttachNicForm,
ChangeUserForm,
ChangeGroupForm,
CreateDiskSnapshotForm,
CreateSnapshotForm,
RecoverForm,

View File

@ -88,7 +88,7 @@ const ActionItem = memo(({ item, selectedRows }) => {
buttonProps={buttonProps}
options={options?.map(option => {
const { accessor, form, onSubmit, dialogProps, disabled: optionDisabled } = option ?? {}
const { title, children } = dialogProps ?? {}
const { description, subheader, title, children } = dialogProps ?? {}
return {
...option,
@ -98,6 +98,8 @@ const ActionItem = memo(({ item, selectedRows }) => {
: optionDisabled,
dialogProps: {
...dialogProps,
description: typeof description === 'function' ? description(selectedRows) : description,
subheader: typeof subheader === 'function' ? subheader(selectedRows) : subheader,
title: typeof title === 'function' ? title(selectedRows) : title,
children: typeof children === 'function' ? children(selectedRows) : children
},

View File

@ -16,15 +16,22 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useGroup, useGroupApi } from 'client/features/One'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import GroupColumns from 'client/components/Tables/Groups/columns'
import GroupRow from 'client/components/Tables/Groups/row'
const GroupsTable = () => {
const columns = useMemo(() => GroupColumns, [])
const GroupsTable = props => {
const { view, getResourceView, filterPool } = useAuth()
const columns = useMemo(() => createColumns({
filters: getResourceView('GROUP')?.filters,
columns: GroupColumns
}), [view])
const groups = useGroup()
const { getGroups } = useGroupApi()
@ -32,7 +39,7 @@ const GroupsTable = () => {
const { status, fetchRequest, loading, reloading, STATUS } = useFetch(getGroups)
const { INIT, PENDING } = STATUS
useEffect(() => { fetchRequest() }, [])
useEffect(() => { fetchRequest() }, [filterPool])
if (groups?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
@ -45,6 +52,7 @@ const GroupsTable = () => {
isLoading={loading || reloading}
getRowId={row => String(row.ID)}
RowComponent={GroupRow}
{...props}
/>
)
}

View File

@ -16,15 +16,22 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, useEffect } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useUser, useUserApi } from 'client/features/One'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import UserColumns from 'client/components/Tables/Users/columns'
import UserRow from 'client/components/Tables/Users/row'
const UsersTable = () => {
const columns = useMemo(() => UserColumns, [])
const UsersTable = props => {
const { view, getResourceView, filterPool } = useAuth()
const columns = useMemo(() => createColumns({
filters: getResourceView('USER')?.filters,
columns: UserColumns
}), [view])
const users = useUser()
const { getUsers } = useUserApi()
@ -32,7 +39,7 @@ const UsersTable = () => {
const { status, fetchRequest, loading, reloading, STATUS } = useFetch(getUsers)
const { INIT, PENDING } = STATUS
useEffect(() => { fetchRequest() }, [])
useEffect(() => { fetchRequest() }, [filterPool])
if (users?.length === 0 && [INIT, PENDING].includes(status)) {
return <SkeletonTable />
@ -45,6 +52,7 @@ const UsersTable = () => {
isLoading={loading || reloading}
getRowId={row => String(row.ID)}
RowComponent={UserRow}
{...props}
/>
)
}

View File

@ -16,6 +16,7 @@
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { Typography } from '@mui/material'
import {
RefreshDouble,
AddSquare,
@ -33,7 +34,7 @@ import { useAuth } from 'client/features/Auth'
import { useVmApi } from 'client/features/One'
import { Translate } from 'client/components/HOC'
import { RecoverForm } from 'client/components/Forms/Vm'
import { RecoverForm, ChangeUserForm, ChangeGroupForm } from 'client/components/Forms/Vm'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS, VM_ACTIONS_BY_STATE } from 'client/constants'
@ -46,22 +47,31 @@ const isDisabled = action => rows => {
return states.some(state => !VM_ACTIONS_BY_STATE[action]?.includes(state))
}
const MessageToConfirmAction = rows => {
const names = rows?.map?.(({ original }) => original?.NAME)
const ListVmNames = ({ rows = [] }) => (
<Typography>
<Translate word={T.VMs} />
{`: ${rows?.map?.(({ original }) => original?.NAME).join(', ')}`}
</Typography>
)
return (
<>
<p>
<Translate word={T.VMs} />
{`: ${names.join(', ')}`}
</p>
<p>
<Translate word={T.DoYouWantProceed} />
</p>
</>
)
const SubHeader = rows => {
const isMultiple = rows?.length > 1
const firstRow = rows?.[0]?.original
return isMultiple
? <ListVmNames rows={rows} />
: <>{`#${firstRow?.ID} ${firstRow?.NAME}`}</>
}
const MessageToConfirmAction = rows => (
<>
<ListVmNames rows={rows} />
<Translate word={T.DoYouWantProceed} />
</>
)
ListVmNames.displayName = 'ListVmNames'
SubHeader.displayName = 'SubHeader'
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
const Actions = () => {
@ -86,6 +96,7 @@ const Actions = () => {
resched,
unresched,
recover,
changeOwnership,
lock,
unlock
} = useVmApi()
@ -277,7 +288,7 @@ const Actions = () => {
accessor: VM_ACTIONS.HOLD,
disabled: isDisabled(VM_ACTIONS.HOLD),
name: T.Hold,
form: () => undefined,
isConfirmDialog: true,
dialogProps: {
title: T.Hold,
children: MessageToConfirmAction
@ -291,7 +302,7 @@ const Actions = () => {
accessor: VM_ACTIONS.RELEASE,
disabled: isDisabled(VM_ACTIONS.RELEASE),
name: T.Release,
form: () => undefined,
isConfirmDialog: true,
dialogProps: {
title: T.Release,
children: MessageToConfirmAction
@ -305,7 +316,7 @@ const Actions = () => {
accessor: VM_ACTIONS.RESCHED,
disabled: isDisabled(VM_ACTIONS.RESCHED),
name: T.Reschedule,
form: () => undefined,
isConfirmDialog: true,
dialogProps: {
title: T.Reschedule,
children: MessageToConfirmAction
@ -319,7 +330,7 @@ const Actions = () => {
accessor: VM_ACTIONS.UNRESCHED,
disabled: isDisabled(VM_ACTIONS.UNRESCHED),
name: T.UnReschedule,
form: () => undefined,
isConfirmDialog: true,
dialogProps: {
title: T.UnReschedule,
children: MessageToConfirmAction
@ -334,15 +345,8 @@ const Actions = () => {
disabled: isDisabled(VM_ACTIONS.RECOVER),
name: T.Recover,
dialogProps: {
// eslint-disable-next-line react/display-name
title: rows => {
const isMultiple = rows?.length > 1
const { ID, NAME } = rows?.[0]?.original
return isMultiple
? <Translate word={T.RecoverSeveralVMs} />
: <Translate word={T.RecoverSomething} values={`#${ID} ${NAME}`} />
}
title: T.Recover,
subheader: SubHeader
},
form: RecoverForm,
onSubmit: async (_, rows) => {
@ -361,14 +365,30 @@ const Actions = () => {
accessor: VM_ACTIONS.CHANGE_OWNER,
disabled: isDisabled(VM_ACTIONS.CHANGE_OWNER),
name: T.ChangeOwner,
form: () => undefined,
onSubmit: () => undefined
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader
},
form: ChangeUserForm,
onSubmit: async (newOwnership, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => changeOwnership(id, newOwnership)))
await Promise.all(ids.map(id => getVm(id)))
}
}, {
accessor: VM_ACTIONS.CHANGE_GROUP,
disabled: isDisabled(VM_ACTIONS.CHANGE_GROUP),
name: T.ChangeGroup,
form: () => undefined,
onSubmit: () => undefined
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader
},
form: ChangeGroupForm,
onSubmit: async (newOwnership, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => changeOwnership(id, newOwnership)))
await Promise.all(ids.map(id => getVm(id)))
}
}]
},
{

View File

@ -68,7 +68,8 @@ export const INPUT_TYPES = {
PASSWORD: 'password',
SELECT: 'select',
SLIDER: 'slider',
TEXT: 'text'
TEXT: 'text',
TABLE: 'table'
}
export const DEBUG_LEVEL = {

View File

@ -37,7 +37,9 @@ module.exports = {
Cancel: 'Cancel',
Change: 'Change',
ChangeGroup: 'Change group',
CurrentGroup: 'Current group: %s',
ChangeOwner: 'Change owner',
CurrentOwner: 'Current owner: %s',
Clear: 'Clear',
Clone: 'Clone',
CloneSeveralTemplates: 'Clone several Templates',