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

F #6980: Add existing users to existing groups (#3515)

Signed-off-by: Ángel Cívico Martos <acivico@opennnebula.io>
Co-authored-by: Tino Vázquez <cvazquez@opennebula.io>
This commit is contained in:
Angel
2025-03-27 11:30:08 +01:00
committed by GitHub
parent 1008a716e2
commit e54eed739f
20 changed files with 670 additions and 48 deletions

View File

@ -39,6 +39,8 @@ info-tabs:
enabled: true
actions:
edit_admins: true
add_users: true
remove_users: true
quota:
enabled: true

View File

@ -42,6 +42,9 @@ info-tabs:
enabled: true
actions:
chgrp: true
add_to_group: true
remove_from_group: true
change_primary_group: true
quota:
enabled: true

View File

@ -38,6 +38,7 @@ info-tabs:
enabled: true
actions:
edit_admins: true
add_users: true
quota:
enabled: true

View File

@ -15,8 +15,8 @@
* ------------------------------------------------------------------------- */
import { createForm } from '@UtilsModule'
import {
SCHEMA,
FIELDS,
SCHEMA,
} from '@modules/components/Forms/Group/EditAdminsForm/schema'
const EditAdminsForm = createForm(SCHEMA, FIELDS, {

View File

@ -0,0 +1,29 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2025, 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 '@UtilsModule'
import {
FIELDS,
SCHEMA,
} from '@modules/components/Forms/Group/EditUsersForm/schema'
const EditUsersForm = createForm(SCHEMA, FIELDS, {
transformInitialValue: (users, schema) => ({
...schema.cast({ users }, { stripUnknown: false }),
}),
transformBeforeSubmit: (formData, initialValues) => {},
})
export default EditUsersForm

View File

@ -0,0 +1,52 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2025, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object, string } from 'yup'
import { INPUT_TYPES, T } from '@ConstantsModule'
import { UsersTable } from '@modules/components/Tables'
import { getValidationFromFields } from '@UtilsModule'
const USERS = (props) => ({
name: 'users',
label: T['groups.actions.edit.users.form'],
type: INPUT_TYPES.TABLE,
Table: () => UsersTable.Table,
fieldProps: {
filterData: props.filterData,
preserveState: true,
},
singleSelect: false,
validation: array(string())
.required()
.default(() => undefined),
grid: { md: 12 },
})
/**
* Fields of the form.
*
* @param {object} props - Object to get filterData function
* @returns {object} Fields
*/
export const FIELDS = (props) => [USERS(props)]
/**
* Schema of the form.
*
* @param {object} props - Object to get filterData function
* @returns {object} Schema
*/
export const SCHEMA = (props) => object(getValidationFromFields(FIELDS(props)))

View File

@ -38,4 +38,11 @@ const UpdateForm = (configProps) =>
const EditAdminsForm = (configProps) =>
AsyncLoadForm({ formPath: 'Group/EditAdminsForm' }, configProps)
export { CreateForm, UpdateForm, EditAdminsForm }
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const EditUsersForm = (configProps) =>
AsyncLoadForm({ formPath: 'Group/EditUsersForm' }, configProps)
export { CreateForm, UpdateForm, EditAdminsForm, EditUsersForm }

View File

@ -0,0 +1,29 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2025, 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 '@UtilsModule'
import {
FIELDS,
SCHEMA,
} from '@modules/components/Forms/User/EditGroupForm/schema'
const EditGroupForm = createForm(SCHEMA, FIELDS, {
transformInitialValue: (groups, schema) => ({
...schema.cast({ groups }, { stripUnknown: false }),
}),
transformBeforeSubmit: (formData, initialValues) => {},
})
export default EditGroupForm

View File

@ -0,0 +1,52 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2025, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { array, object, string } from 'yup'
import { INPUT_TYPES, T } from '@ConstantsModule'
import { GroupsTable } from '@modules/components/Tables'
import { getValidationFromFields } from '@UtilsModule'
const GROUPS = (props) => ({
name: 'groups',
label: T['user.actions.edit.group.form'],
type: INPUT_TYPES.TABLE,
Table: () => GroupsTable.Table,
fieldProps: {
filterData: props.filterData,
preserveState: true,
},
singleSelect: false,
validation: array(string())
.required()
.default(() => undefined),
grid: { md: 12 },
})
/**
* Fields of the form.
*
* @param {object} props - Object to get filterData function
* @returns {object} Fields
*/
export const FIELDS = (props) => [GROUPS(props)]
/**
* Schema of the form.
*
* @param {object} props - Object to get filterData function
* @returns {object} Schema
*/
export const SCHEMA = (props) => object(getValidationFromFields(FIELDS(props)))

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 '@modules/components/HOC'
import { CreateStepsCallback } from '@UtilsModule'
import { ReactElement } from 'react'
/**
* @param {ConfigurationProps} configProps - Configuration
@ -24,4 +24,11 @@ import { CreateStepsCallback } from '@UtilsModule'
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'User/CreateForm' }, configProps)
export { CreateForm }
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const EditGroupForm = (configProps) =>
AsyncLoadForm({ formPath: 'User/EditGroupForm' }, configProps)
export { CreateForm, EditGroupForm }

View File

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { RESOURCE_NAMES, T } from '@ConstantsModule'
import { GroupAPI, useViews } from '@FeaturesModule'
import { getGroupQuotaUsage } from '@ModelsModule'
import { LinearProgressWithTooltip } from '@modules/components/Status'
import EnhancedTable, {
createColumns,
@ -20,9 +23,6 @@ import EnhancedTable, {
import WrapperRow from '@modules/components/Tables/Enhanced/WrapperRow'
import GroupColumns from '@modules/components/Tables/Groups/columns'
import GroupRow from '@modules/components/Tables/Groups/row'
import { RESOURCE_NAMES, T } from '@ConstantsModule'
import { useViews, GroupAPI } from '@FeaturesModule'
import { getGroupQuotaUsage } from '@ModelsModule'
import { Component, useMemo } from 'react'
const DEFAULT_DATA_CY = 'groups'
@ -51,7 +51,16 @@ const GroupsTable = (props) => {
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = GroupAPI.useGetGroupsQuery()
const {
data: groups = [],
isFetching,
refetch,
} = GroupAPI.useGetGroupsQuery()
const data =
props?.filterData && typeof props?.filterData === 'function'
? props?.filterData(groups)
: groups
const columns = useMemo(
() =>
@ -133,7 +142,7 @@ const GroupsTable = (props) => {
return (
<EnhancedTable
columns={columns}
data={data}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}

View File

@ -15,6 +15,9 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo } from 'react'
import { RESOURCE_NAMES, T } from '@ConstantsModule'
import { UserAPI, useViews } from '@FeaturesModule'
import { getUserQuotaUsage } from '@ModelsModule'
import { LinearProgressWithTooltip } from '@modules/components/Status'
import EnhancedTable, {
createColumns,
@ -22,9 +25,6 @@ import EnhancedTable, {
import WrapperRow from '@modules/components/Tables/Enhanced/WrapperRow'
import UserColumns from '@modules/components/Tables/Users/columns'
import UserRow from '@modules/components/Tables/Users/row'
import { RESOURCE_NAMES, T } from '@ConstantsModule'
import { useViews, UserAPI } from '@FeaturesModule'
import { getUserQuotaUsage } from '@ModelsModule'
const DEFAULT_DATA_CY = 'users'

View File

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import PropTypes from 'prop-types'
import { memo } from 'react'
import ButtonToTriggerForm from '@modules/components/Forms/ButtonToTriggerForm'
import { EditAdminsForm } from '@modules/components/Forms/Group'
import { EditAdminsForm, EditUsersForm } from '@modules/components/Forms/Group'
import { T } from '@ConstantsModule'
@ -64,6 +64,84 @@ const EditAdminsActions = memo(({ admins, filterData, submit }) => {
)
})
const AddUsersAction = memo(({ users, filterData, submit }) => {
// Handle submit form
const handleEditAdmins = (formData) => {
submit(formData.users)
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-user',
label: T['groups.actions.add.user'],
variant: 'outlined',
sx: {
m: '1em',
},
}}
options={[
{
cy: 'add-user',
name: T['groups.actions.add.user'],
dialogProps: {
title: T['groups.actions.add.user'],
dataCy: 'modal-add-user',
},
form: () =>
EditUsersForm({
initialValues: users,
stepProps: {
filterData,
},
}),
onSubmit: handleEditAdmins,
},
]}
/>
)
})
const RemoveUsersAction = memo(({ users, filterData, submit }) => {
// Handle submit form
const handleEditAdmins = (formData) => {
submit(formData.users)
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'remove-user',
label: T['groups.actions.remove.user'],
variant: 'outlined',
sx: {
m: '1em',
},
}}
options={[
{
cy: 'remove-user',
name: T['groups.actions.remove.user'],
dialogProps: {
title: T['groups.actions.remove.user'],
dataCy: 'modal-remove-user',
},
form: () =>
EditUsersForm({
initialValues: users,
stepProps: {
filterData,
},
}),
onSubmit: handleEditAdmins,
},
]}
/>
)
})
EditAdminsActions.propTypes = {
admins: PropTypes.array,
filterData: PropTypes.func,
@ -71,4 +149,18 @@ EditAdminsActions.propTypes = {
}
EditAdminsActions.displayName = 'EditAdminsActions'
export { EditAdminsActions }
AddUsersAction.propTypes = {
users: PropTypes.array,
filterData: PropTypes.func,
submit: PropTypes.func,
}
AddUsersAction.displayName = 'AddUsersAction'
RemoveUsersAction.propTypes = {
users: PropTypes.array,
filterData: PropTypes.func,
submit: PropTypes.func,
}
RemoveUsersAction.displayName = 'RemoveUsersAction'
export { AddUsersAction, EditAdminsActions, RemoveUsersAction }

View File

@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { UsersTable } from '@modules/components/Tables'
import { Stack } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { GroupAPI, useGeneralApi } from '@FeaturesModule'
import { getActionsAvailable } from '@ModelsModule'
import { GROUP_ACTIONS, T } from '@ConstantsModule'
import { GroupAPI, UserAPI, useGeneralApi } from '@FeaturesModule'
import { getActionsAvailable } from '@ModelsModule'
import { EditAdminsActions } from './Actions'
import { AddUsersAction, EditAdminsActions, RemoveUsersAction } from './Actions'
const _ = require('lodash')
@ -35,12 +35,15 @@ const _ = require('lodash')
* @param {object} props.tabProps.actions - Actions to this tab
* @returns {ReactElement} Information tab
*/
const GroupUsersTab = ({ tabProps: { actions } = {}, id }) => {
const GroupUsersTab = ({ tabProps: { actions } = {}, id: groupId }) => {
const { enqueueSuccess } = useGeneralApi()
const [addAdmins] = GroupAPI.useAddAdminToGroupMutation()
const [removeAdmins] = GroupAPI.useRemoveAdminFromGroupMutation()
const [addUser] = UserAPI.useAddGroupMutation()
const [removeUser] = UserAPI.useRemoveFromGroupMutation()
const { data: users } = UserAPI.useGetUsersQuery()
const { data: group, refetch } = GroupAPI.useGetGroupQuery({ id })
const { data: group, refetch } = GroupAPI.useGetGroupQuery({ id: groupId })
const adminsGroup = Array.isArray(group.ADMINS?.ID)
? group.ADMINS?.ID
: [group.ADMINS?.ID]
@ -48,14 +51,15 @@ const GroupUsersTab = ({ tabProps: { actions } = {}, id }) => {
const actionsAvailable = getActionsAvailable(actions)
// Filter function to get only group users and add if the user is admin group
const filterData = (data) => {
const filterDataByAdmin = (data) => {
// Returns all users of this group
const filterUsers = data.filter((user) => {
// filter users by group id
const groupsUser = Array.isArray(user.GROUPS.ID)
? user.GROUPS.ID
: [user.GROUPS.ID]
return groupsUser.some((groupUser) => groupUser === id)
return groupsUser.some((groupUser) => groupUser === groupId)
})
const admins = Array.isArray(group.ADMINS?.ID)
@ -70,30 +74,103 @@ const GroupUsersTab = ({ tabProps: { actions } = {}, id }) => {
})
}
// Filter users and show the ones that are not in a group
const filterDataNotInGroup = (data) =>
data.filter((user) => {
// filter users by group id
const groupsUser = Array.isArray(user.GROUPS.ID)
? user.GROUPS.ID
: [user.GROUPS.ID]
return !groupsUser.some((groupUser) => groupUser === groupId)
})
// Filter users and show the ones that are in the current group and are not his primary group
const filterDataInGroup = (data) =>
data.filter((user) => {
const USER_GROUPS = [].concat(user.GROUPS.ID ?? [])
const primaryGroupId = user?.GID ?? USER_GROUPS?.[0]
// filter users by group id
const groupsUser = Array.isArray(user.GROUPS.ID)
? user.GROUPS.ID
: [user.GROUPS.ID]
return (
groupsUser.some((groupUser) => groupUser === groupId) &&
primaryGroupId !== groupId
)
})
// Add and remove administrators
const submitAdmins = async (adminsToAdd, adminsToRemove) => {
// Add admins
await Promise.all(adminsToAdd.map((user) => addAdmins({ id, user })))
await Promise.all(
adminsToAdd.map((user) => addAdmins({ id: groupId, user }))
)
// Remove admins
await Promise.all(adminsToRemove.map((user) => removeAdmins({ id, user })))
await Promise.all(
adminsToRemove.map((user) => removeAdmins({ id: groupId, user }))
)
// Refresh info
refetch({ id })
refetch({ id: groupId })
// Success message
enqueueSuccess(T['groups.actions.edit.admins.success'])
}
const submitNewUsers = async (usersToAdd) => {
await Promise.all(
usersToAdd.map((user) => addUser({ id: user, group: groupId }))
)
// Refresh info
refetch({ id: groupId })
// Success message
enqueueSuccess(T['groups.actions.add.user.success'])
}
const submitDeleteUsers = async (usersToAdd) => {
await Promise.all(
usersToAdd.map((user) => removeUser({ id: user, group: groupId }))
)
// Refresh info
refetch({ id: groupId })
// Success message
enqueueSuccess(T['groups.actions.add.user.success'])
}
return (
<div>
{actionsAvailable?.includes?.(GROUP_ACTIONS.EDIT_ADMINS) && (
<EditAdminsActions
admins={adminsGroup}
filterData={filterData}
filterData={filterDataByAdmin}
submit={submitAdmins}
/>
)}
{actionsAvailable?.includes?.(GROUP_ACTIONS.ADD_USERS) && (
<AddUsersAction
users={users}
filterData={filterDataNotInGroup}
submit={submitNewUsers}
/>
)}
{actionsAvailable?.includes?.(GROUP_ACTIONS.REMOVE_USERS) && (
<RemoveUsersAction
users={users}
filterData={filterDataInGroup}
submit={submitDeleteUsers}
/>
)}
<Stack
display="grid"
gap="1em"
@ -103,8 +180,8 @@ const GroupUsersTab = ({ tabProps: { actions } = {}, id }) => {
<UsersTable.Table
disableRowSelect
disableGlobalSort
groupId={id}
filterData={filterData}
groupId={groupId}
filterData={filterDataByAdmin}
/>
</Stack>
</div>

View File

@ -23,7 +23,7 @@ import { getAvailableInfoTabs } from '@ModelsModule'
import { BaseTab as Tabs } from '@modules/components/Tabs'
import Info from '@modules/components/Tabs/Group/Info'
import Users from '@modules/components/Tabs/Group/Users'
import GroupUsersTab from '@modules/components/Tabs/Group/Users'
import generateQuotasInfoTab from '@modules/components/Tabs/Quota'
import generateAccountingInfoTab from '@modules/components/Tabs/Accounting'
import generateShowbackInfoTab from '@modules/components/Tabs/Showback'
@ -31,7 +31,7 @@ import generateShowbackInfoTab from '@modules/components/Tabs/Showback'
const getTabComponent = (tabName) =>
({
info: Info,
user: Users,
user: GroupUsersTab,
quota: generateQuotasInfoTab({ groups: true }),
accounting: generateAccountingInfoTab({ groups: true }),
showback: generateShowbackInfoTab({ groups: true }),

View File

@ -0,0 +1,159 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2025, 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 } from 'react'
import { T } from '@ConstantsModule'
import ButtonToTriggerForm from '@modules/components/Forms/ButtonToTriggerForm'
import { EditGroupForm } from '@modules/components/Forms/User'
const AddToGroup = memo(({ groups, filterData, submit }) => {
// Handle submit form
const habdleSubmit = (formData) => {
submit(formData.groups)
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'add-to-group',
label: T['users.actions.add.to.group'],
variant: 'outlined',
sx: {
m: '1em',
},
}}
options={[
{
cy: 'add-to-group',
name: T['users.actions.add.to.group'],
dialogProps: {
title: T['users.actions.add.to.group'],
dataCy: 'modal-add-to-group',
},
form: () =>
EditGroupForm({
initialValues: groups,
stepProps: {
filterData,
},
}),
onSubmit: habdleSubmit,
},
]}
/>
)
})
const RemoveFromGroup = memo(({ groups, filterData, submit }) => {
const handleSubmit = (formData) => {
submit(formData.groups)
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'remove-from-group',
label: T['users.actions.remove.from.group'],
variant: 'outlined',
sx: {
m: '1em',
},
}}
options={[
{
cy: 'remove-from-group',
name: T['users.actions.remove.from.group'],
dialogProps: {
title: T['users.actions.remove.from.group'],
dataCy: 'modal-remove-from-group',
},
form: () =>
EditGroupForm({
initialValues: groups,
stepProps: {
filterData,
},
}),
onSubmit: handleSubmit,
},
]}
/>
)
})
const ChangePrimaryGroup = memo(({ groups, filterData, submit }) => {
const handleSubmit = (formData) => {
submit(formData.groups)
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': 'change-primary-group',
label: T['users.actions.change.primary.group'],
variant: 'outlined',
sx: {
m: '1em',
},
}}
options={[
{
cy: 'change-primary-group',
name: T['users.actions.change.primary.group'],
dialogProps: {
title: T['users.actions.change.primary.group'],
dataCy: 'modal-change-primary-group',
},
form: () =>
EditGroupForm({
initialValues: groups,
stepProps: {
filterData,
},
}),
onSubmit: handleSubmit,
},
]}
/>
)
})
AddToGroup.propTypes = {
groups: PropTypes.array,
filterData: PropTypes.func,
submit: PropTypes.func,
}
AddToGroup.displayName = 'AddToGroupAction'
RemoveFromGroup.propTypes = {
groups: PropTypes.array,
filterData: PropTypes.func,
submit: PropTypes.func,
}
RemoveFromGroup.displayName = 'RemoveFromGroupAction'
ChangePrimaryGroup.propTypes = {
groups: PropTypes.array,
filterData: PropTypes.func,
submit: PropTypes.func,
}
ChangePrimaryGroup.displayName = 'ChangePrimaryGroupAction'
export { AddToGroup, ChangePrimaryGroup, RemoveFromGroup }

View File

@ -13,35 +13,46 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { useHistory, generatePath } from 'react-router-dom'
import { ReactElement, useMemo } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { PATH } from '@modules/components/path'
import { UserAPI, GroupAPI } from '@FeaturesModule'
import { GroupAPI, UserAPI, useGeneralApi } from '@FeaturesModule'
import { getActionsAvailable } from '@ModelsModule'
import { Box, Divider } from '@mui/material'
import { T } from '@ConstantsModule'
import { Tr } from '@modules/components/HOC'
import { AddToGroup, ChangePrimaryGroup, RemoveFromGroup } from './Action'
import { T, USER_ACTIONS } from '@ConstantsModule'
import { GroupCard } from '@modules/components/Cards'
import { Tr } from '@modules/components/HOC'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - Datastore id
* @param props.tabProps
* @param props.tabProps.actions
* @returns {ReactElement} Information tab
*/
const GroupsInfoTab = ({ id }) => {
const GroupsInfoTab = ({ tabProps: { actions } = {}, id: userId }) => {
const path = PATH.SYSTEM.GROUPS.DETAIL
const history = useHistory()
const { data = [] } = GroupAPI.useGetGroupsQuery()
const { data: user } = UserAPI.useGetUserQuery({ id })
const { enqueueSuccess } = useGeneralApi()
const { data: groups = [], refetch } = GroupAPI.useGetGroupsQuery()
const { data: user } = UserAPI.useGetUserQuery({ id: userId })
const [addUser] = UserAPI.useAddGroupMutation()
const [removeUser] = UserAPI.useRemoveFromGroupMutation()
const [changeGroup] = UserAPI.useChangeGroupMutation()
const USER_GROUPS = [].concat(user.GROUPS.ID ?? [])
const actionsAvailable = getActionsAvailable(actions)
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
@ -50,25 +61,98 @@ const GroupsInfoTab = ({ id }) => {
const primaryGroup = useMemo(
() =>
data.find(
groups.find(
(group) =>
group.ID === primaryGroupId ||
String(group.ID) === String(primaryGroupId)
),
[data]
[groups]
)
const secondaryGroups = useMemo(
() =>
data.filter(
groups.filter(
(group) =>
group?.ID !== primaryGroupId && USER_GROUPS?.includes(group?.ID)
),
[data]
[groups]
)
/* Filter groups showing only the ones the user is not linked into */
const filterGroupsNotLinked = (data) =>
data.filter(
(group) => !USER_GROUPS.some((userGroup) => userGroup === group.ID)
)
/* Filter groups showing only the ones the user is linked into and it's not its primary group */
const filterGroupsLinked = (data) =>
data.filter(
(group) =>
USER_GROUPS.some((userGroup) => userGroup === group.ID) &&
group.ID !== primaryGroupId
)
/* Filter groups showing only the ones the user has not as primary group */
const filterByNotPrimaryGroup = (data) =>
data.filter((group) => group.ID !== primaryGroupId)
const submitAddToGroup = async (groupsToAdd) => {
await Promise.all(
groupsToAdd.map((groupId) => addUser({ id: userId, group: groupId }))
)
refetch()
// Success message
enqueueSuccess(T['user.actions.edit.group.success'])
}
const submitRemoveFromGroup = async (groupsToAdd) => {
await Promise.all(
groupsToAdd.map((groupId) => removeUser({ id: userId, group: groupId }))
)
refetch()
// Success message
enqueueSuccess(T['user.actions.edit.group.success'])
}
const changePrimaryGroup = (group) => {
changeGroup({ id: userId, group: group })
refetch()
// Success message
enqueueSuccess(T['user.actions.edit.group.success'])
}
return (
<div>
{actionsAvailable?.includes?.(USER_ACTIONS.ADD_TO_GROUP) && (
<AddToGroup
groups={groups}
filterData={filterGroupsNotLinked}
submit={submitAddToGroup}
/>
)}
{actionsAvailable?.includes?.(USER_ACTIONS.REMOVE_FROM_GROUP) && (
<RemoveFromGroup
groups={groups}
filterData={filterGroupsLinked}
submit={submitRemoveFromGroup}
/>
)}
{actionsAvailable?.includes?.(USER_ACTIONS.CHANGE_PRIMARY_GROUP) && (
<ChangePrimaryGroup
groups={groups}
filterData={filterByNotPrimaryGroup}
submit={changePrimaryGroup}
/>
)}
<Box
sx={{
position: 'relative',

View File

@ -41,4 +41,6 @@ export const GROUP_ACTIONS = {
QUOTAS_DIALOG: 'quotas_dialog',
DELETE: 'delete',
EDIT_ADMINS: 'edit_admins',
ADD_USERS: 'add_users',
REMOVE_USERS: 'remove_users',
}

View File

@ -2203,8 +2203,11 @@ module.exports = {
'See OpenNebula documentation to get more details about views on Fireedge Sunstone.',
'groups.actions.edit.admins': 'Edit administrators',
'groups.actions.edit.admins.form': 'Select the administrators',
'groups.actions.edit.users.form': 'Select the users',
'groups.actions.edit.admins.success': 'Administrators updated',
'groups.actions.add.user.success': 'Users updated',
'groups.actions.add.user': 'Add users',
'groups.actions.remove.user': 'Remove users',
'groups.view.admin.name': 'Admin view',
'groups.view.admin.description': 'View used by admin users',
'groups.view.user.name': 'User view',
@ -2215,6 +2218,12 @@ module.exports = {
'groups.view.groupadmin.description':
'View used by the admin users of the group',
/* User */
'users.actions.add.to.group': 'Add to group',
'users.actions.remove.from.group': 'Remove from group',
'user.actions.edit.group.success': 'Group updated',
'users.actions.change.primary.group': 'Change primary group',
/* Showback */
'showback.title': 'Showback',
'showback.button.getShowback': 'Get showback',

View File

@ -14,10 +14,15 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
// eslint-disable-next-line prettier/prettier, no-unused-vars
import { VmQuota, NetworkQuota, DatastoreQuota, ImageQuota } from '@modules/constants/quota'
import * as ACTIONS from '@modules/constants/actions'
import * as STATES from '@modules/constants/states'
import { COLOR } from '@modules/constants/color'
import {
DatastoreQuota,
ImageQuota,
NetworkQuota,
VmQuota,
} from '@modules/constants/quota'
import * as STATES from '@modules/constants/states'
/**
* @typedef LoginToken
@ -77,6 +82,9 @@ export const USER_ACTIONS = {
CHANGE_AUTH: 'change_authentication',
ENABLE: 'enable',
DISABLE: 'disable',
ADD_TO_GROUP: 'add_to_group',
REMOVE_FROM_GROUP: 'remove_from_group',
CHANGE_PRIMARY_GROUP: 'change_primary_group',
}
export const AUTH_DRIVER = {