Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Manage user's groups #4312

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions frontend/web/components/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { FC } from 'react'
import classNames from 'classnames'
import Icon from './Icon'
import Button from './base/forms/Button'
import Button, { ButtonType } from './base/forms/Button'

type ActionButtonType = {
onClick: () => void
'data-test'?: string
size?: ButtonType['size']
}

const ActionButton: FC<ActionButtonType> = ({ onClick, ...rest }) => {
const ActionButton: FC<ActionButtonType> = ({
onClick,
size = 'xSmall',
...rest
}) => {
return (
<Button
className={classNames('btn btn-with-icon btn-xs')}
size={size}
className={classNames('btn btn-with-icon')}
data-test={rest['data-test']}
onClick={(e) => {
e.stopPropagation()
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/UserAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const FeatureAction: FC<FeatureActionProps> = ({
return (
<div>
<div ref={btnRef}>
<ActionButton onClick={() => setIsOpen(true)} />
<ActionButton size='small' onClick={() => setIsOpen(true)} />
</div>

{isOpen && (
Expand Down
150 changes: 150 additions & 0 deletions frontend/web/components/UsersGroups.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { FC } from 'react'
import {
useGetGroupsQuery,
useUpdateGroupMutation,
} from 'common/services/useGroup'
import { sortBy } from 'lodash'
import { User, UserGroup, UserGroupSummary } from 'common/types/responses'
import Switch from './Switch'
import PanelSearch from './PanelSearch'
import ErrorMessage from './ErrorMessage'

type UsersGroupsType = {
user: User
orgId: number
}
const widths = [120]

const UsersGroups: FC<UsersGroupsType> = ({ orgId, user }) => {
const {
data,
error: groupsError,
isLoading,
} = useGetGroupsQuery({ orgId })
const [updateGroup, { error: saveError, isLoading: isSaving }] =
useUpdateGroupMutation({})
const error = groupsError || saveError
const onGroupsUpdated = (res: { error?: any }) => {
if (res.error) {
toast('Error updating group', 'danger')
} else {
toast('Updated user groups')
}
}
return isLoading ? (
<div className='text-center'>
<Loader />
</div>
) : (
<>
<ErrorMessage error={error} />
<PanelSearch
noResultsText={(search: string) =>
search ? (
<Flex className='text-center'>
No results found for <strong>{search}</strong>
</Flex>
) : (
<Flex className='text-center'>This group has no members</Flex>
)
}
id='org-members-list'
title='Groups'
className='no-pad overflow-visible'
renderSearchWithNoResults
items={sortBy(data?.results, 'name')}
filterRow={(item: UserGroupSummary, search: string) => {
const strToSearch = `${item.name} ${item.external_id}`
return strToSearch.toLowerCase().indexOf(search.toLowerCase()) !== -1
}}
header={
<>
<Row className='table-header'>
<Flex className='table-column px-3'>
<div>Group</div>
</Flex>
<div className='table-column ml-1' style={{ width: widths[0] }}>
Member
</div>
<div className='table-column ml-1' style={{ width: widths[1] }}>
Admin
</div>
</Row>
</>
}
renderRow={(group: UserGroup) => {
const { external_id, id, name, users } = group
const groupUser = users.find((v) => v.id === user.id)
const isInGroup = !!groupUser
const isAdmin = groupUser?.group_admin
return (
<Row className='list-item' key={id}>
<Flex className='table-column px-3'>
<div className='font-weight-medium'>{name}</div>
<div className='text-muted'>{external_id}</div>
</Flex>
<div className='table-column' style={{ width: widths[0] }}>
<Switch
disabled={isSaving}
checked={isInGroup}
onChange={(v: boolean) => {
if (v) {
updateGroup({
data: { ...group, users: group.users.concat([user]) },
orgId: `${orgId}`,
users: group.users.concat([user]),
usersToAddAdmin: null,
usersToRemove: null,
usersToRemoveAdmin: null,
}).then(onGroupsUpdated)
} else {
updateGroup({
data: group,
orgId: `${orgId}`,
users: group.users,
usersToAddAdmin: null,
usersToRemove: [user.id],
usersToRemoveAdmin: null,
}).then(onGroupsUpdated)
}
}}
/>
</div>
<div className='table-column' style={{ width: widths[1] }}>
<Switch
disabled={isSaving}
checked={isAdmin}
onChange={(v: boolean) => {
if (v) {
updateGroup({
data: { ...group, users: group.users.concat([user]) },
orgId: `${orgId}`,
users: isInGroup
? group.users
: group.users.concat([user]),
usersToAddAdmin: [user.id],
usersToRemove: null,
usersToRemoveAdmin: null,
}).then(onGroupsUpdated)
} else {
updateGroup({
data: group,
orgId: `${orgId}`,
users: group.users,
usersToAddAdmin: null,
usersToRemove: null,
usersToRemoveAdmin: [user.id],
}).then(onGroupsUpdated)
}
}}
/>
</div>
</Row>
)
}}
/>
</>
)
}

export default UsersGroups
24 changes: 20 additions & 4 deletions frontend/web/components/pages/UsersAndPermissionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import sortBy from 'lodash/sortBy'
import UserAction from 'components/UserAction'
import Icon from 'components/Icon'
import RolesTable from 'components/RolesTable'
import UsersGroups from 'components/UsersGroups'
import PlanBasedBanner, { getPlanBasedOption } from 'components/PlanBasedAccess'

type UsersAndPermissionsPageType = {
Expand Down Expand Up @@ -103,9 +104,24 @@ const UsersAndPermissionsInner: FC<UsersAndPermissionsInnerType> = ({

const editUserPermissions = (user: User, organisationId: number) => {
openModal(
'Edit Organisation Permissions',
<div className='p-4'>
<PermissionsTabs uncontrolled user={user} orgId={organisationId} />
`${user.first_name} ${user.last_name}`,
<div>
<Tabs uncontrolled>
<TabItem tabLabel='Permissions'>
<div className='pt-4'>
<PermissionsTabs
uncontrolled
user={user}
orgId={organisationId}
/>
</div>
</TabItem>
<TabItem tabLabel='Groups'>
<div className='pt-4'>
<UsersGroups user={user} orgId={organisationId} />
</div>
</TabItem>
</Tabs>
</div>,
'p-0 side-modal',
)
Expand Down Expand Up @@ -553,7 +569,7 @@ const UsersAndPermissionsInner: FC<UsersAndPermissionsInnerType> = ({
style={{
width: widths[2],
}}
className='table-column text-end'
className='table-column d-flex justify-content-end'
>
<UserAction
onRemove={onRemoveClick}
Expand Down
Loading