diff --git a/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js b/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js index a6accce339..1b84c77fbc 100644 --- a/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js +++ b/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js @@ -100,7 +100,7 @@ const DialogConfirmation = memo( )} {subheader && ( - + {typeof subheader === 'string' ? Tr(subheader) : subheader} )} @@ -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, diff --git a/src/fireedge/src/client/components/FormControl/TableController.js b/src/fireedge/src/client/components/FormControl/TableController.js new file mode 100644 index 0000000000..e1b6db0064 --- /dev/null +++ b/src/fireedge/src/client/components/FormControl/TableController.js @@ -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 ? ( + + ) : ( + label && ( + + {tooltip && } + {typeof label === 'string' ? Tr(label) : label} + + ) + )} + ( + { + 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 diff --git a/src/fireedge/src/client/components/FormControl/index.js b/src/fireedge/src/client/components/FormControl/index.js index 6cad2420cd..088a844180 100644 --- a/src/fireedge/src/client/components/FormControl/index.js +++ b/src/fireedge/src/client/components/FormControl/index.js @@ -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, diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index cd77e45297..2b67493042 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -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 }) => { diff --git a/src/fireedge/src/client/components/Forms/Vm/ChangeGroupForm/index.js b/src/fireedge/src/client/components/Forms/Vm/ChangeGroupForm/index.js new file mode 100644 index 0000000000..ce8ed47a60 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/ChangeGroupForm/index.js @@ -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 diff --git a/src/fireedge/src/client/components/Forms/Vm/ChangeGroupForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/ChangeGroupForm/schema.js new file mode 100644 index 0000000000..8adb9925c8 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/ChangeGroupForm/schema.js @@ -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)) diff --git a/src/fireedge/src/client/components/Forms/Vm/ChangeUserForm/index.js b/src/fireedge/src/client/components/Forms/Vm/ChangeUserForm/index.js new file mode 100644 index 0000000000..5e6efa5784 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/ChangeUserForm/index.js @@ -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 diff --git a/src/fireedge/src/client/components/Forms/Vm/ChangeUserForm/schema.js b/src/fireedge/src/client/components/Forms/Vm/ChangeUserForm/schema.js new file mode 100644 index 0000000000..40aad0e9b4 --- /dev/null +++ b/src/fireedge/src/client/components/Forms/Vm/ChangeUserForm/schema.js @@ -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)) diff --git a/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js b/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js index 0d50b4b0d5..737bfb35de 100644 --- a/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/RecoverForm/index.js @@ -23,7 +23,7 @@ const RecoverForm = createForm( FIELDS, { description: ( - + {`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, diff --git a/src/fireedge/src/client/components/Forms/Vm/index.js b/src/fireedge/src/client/components/Forms/Vm/index.js index b865462f3e..b4efacaa69 100644 --- a/src/fireedge/src/client/components/Forms/Vm/index.js +++ b/src/fireedge/src/client/components/Forms/Vm/index.js @@ -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, diff --git a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/Action.js b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/Action.js index 0a1b5eb292..5a1054e197 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/Action.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/Utils/GlobalActions/Action.js @@ -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 }, diff --git a/src/fireedge/src/client/components/Tables/Groups/index.js b/src/fireedge/src/client/components/Tables/Groups/index.js index 513816d198..db3a9a5dbd 100644 --- a/src/fireedge/src/client/components/Tables/Groups/index.js +++ b/src/fireedge/src/client/components/Tables/Groups/index.js @@ -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 @@ -45,6 +52,7 @@ const GroupsTable = () => { isLoading={loading || reloading} getRowId={row => String(row.ID)} RowComponent={GroupRow} + {...props} /> ) } diff --git a/src/fireedge/src/client/components/Tables/Users/index.js b/src/fireedge/src/client/components/Tables/Users/index.js index e7740a1cb2..edd37f8b04 100644 --- a/src/fireedge/src/client/components/Tables/Users/index.js +++ b/src/fireedge/src/client/components/Tables/Users/index.js @@ -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 @@ -45,6 +52,7 @@ const UsersTable = () => { isLoading={loading || reloading} getRowId={row => String(row.ID)} RowComponent={UserRow} + {...props} /> ) } diff --git a/src/fireedge/src/client/components/Tables/Vms/actions.js b/src/fireedge/src/client/components/Tables/Vms/actions.js index 17b7672489..b33c5f7834 100644 --- a/src/fireedge/src/client/components/Tables/Vms/actions.js +++ b/src/fireedge/src/client/components/Tables/Vms/actions.js @@ -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 = [] }) => ( + + + {`: ${rows?.map?.(({ original }) => original?.NAME).join(', ')}`} + +) - return ( - <> -

- - {`: ${names.join(', ')}`} -

-

- -

- - ) +const SubHeader = rows => { + const isMultiple = rows?.length > 1 + const firstRow = rows?.[0]?.original + + return isMultiple + ? + : <>{`#${firstRow?.ID} ${firstRow?.NAME}`} } +const MessageToConfirmAction = rows => ( + <> + + + +) + +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 - ? - : - } + 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))) + } }] }, { diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 8b608f44ae..1a04e20dec 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -68,7 +68,8 @@ export const INPUT_TYPES = { PASSWORD: 'password', SELECT: 'select', SLIDER: 'slider', - TEXT: 'text' + TEXT: 'text', + TABLE: 'table' } export const DEBUG_LEVEL = { diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 4a24885415..690b53597e 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -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',