From 3200fee963a51e414a690158a12893124c80bc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 11 Aug 2022 15:34:17 +0100 Subject: [PATCH] fix: remove group owner concept (#1210) * fix: remove group owner concept * fix: adapt e2e tests accordingly * refactor users select to match improvement * refactor: add user -> edit users * feat: add edit users to group card actions * add a few more UI improvements * fix: edit group users icon * improve loading behaviour * fix group users refresh on card view * improvement: create group form validation * fix edit group, some refactoring * fix: e2e tests, minor bugs * fix: infinite re-renders due to useHiddenColumns useEffect array dependency * fix re-rendering on useHiddenColumns for some tables * refactor: validations into functions / variables --- .../cypress/integration/groups/groups.spec.ts | 53 +---- .../apiToken/ApiTokenTable/ApiTokenTable.tsx | 16 +- .../admin/groups/CreateGroup/CreateGroup.tsx | 22 ++- .../admin/groups/EditGroup/EditGroup.tsx | 23 ++- .../Group/EditGroupUser/EditGroupUser.tsx | 183 ------------------ .../EditGroupUsers.tsx} | 60 ++---- .../component/admin/groups/Group/Group.tsx | 57 ++---- .../Group/RemoveGroupUser/RemoveGroupUser.tsx | 3 +- .../admin/groups/GroupForm/GroupForm.tsx | 5 +- .../GroupFormUsersSelect.tsx | 64 +++--- .../GroupFormUsersTable.tsx | 35 +--- .../GroupUserRoleCell/GroupUserRoleCell.tsx | 58 ------ .../groups/GroupsList/GroupCard/GroupCard.tsx | 8 + .../GroupCardActions/GroupCardActions.tsx | 19 +- .../GroupCardAvatars/GroupCardAvatars.tsx | 8 +- .../GroupPopover/GroupPopover.tsx | 19 +- .../admin/groups/GroupsList/GroupsList.tsx | 2 +- .../admin/groups/hooks/useGroupForm.ts | 25 +-- .../common/UserAvatar/UserAvatar.tsx | 41 +--- .../ProjectAccessAssign.tsx | 7 +- .../ProjectRoleDescription.tsx | 165 +++++++++------- .../ProjectAccessRoleCell.tsx | 70 +++++++ .../ProjectAccessTable/ProjectAccessTable.tsx | 22 ++- .../ProjectGroupView/ProjectGroupView.tsx | 18 +- .../hooks/api/getters/useGroup/useGroup.ts | 1 - frontend/src/interfaces/group.ts | 7 - frontend/src/utils/testIds.ts | 5 +- 27 files changed, 345 insertions(+), 651 deletions(-) delete mode 100644 frontend/src/component/admin/groups/Group/EditGroupUser/EditGroupUser.tsx rename frontend/src/component/admin/groups/Group/{AddGroupUser/AddGroupUser.tsx => EditGroupUsers/EditGroupUsers.tsx} (73%) delete mode 100644 frontend/src/component/admin/groups/GroupUserRoleCell/GroupUserRoleCell.tsx create mode 100644 frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessRoleCell/ProjectAccessRoleCell.tsx diff --git a/frontend/cypress/integration/groups/groups.spec.ts b/frontend/cypress/integration/groups/groups.spec.ts index 6977641946..efc9931f1c 100644 --- a/frontend/cypress/integration/groups/groups.spec.ts +++ b/frontend/cypress/integration/groups/groups.spec.ts @@ -39,23 +39,6 @@ describe('groups', () => { } }); - it('gives an error if a group does not have an owner', () => { - cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click(); - - cy.intercept('POST', '/api/admin/groups').as('createGroup'); - - cy.get("[data-testid='UG_NAME_ID']").type(groupName); - cy.get("[data-testid='UG_DESC_ID']").type('hello-world'); - cy.get("[data-testid='UG_USERS_ID']").click(); - cy.contains(`unleash-e2e-user1-${randomId}`).click(); - cy.get("[data-testid='UG_USERS_ADD_ID']").click(); - - cy.get("[data-testid='UG_CREATE_BTN_ID']").click(); - cy.get("[data-testid='TOAST_TEXT']").contains( - 'Group needs to have at least one Owner' - ); - }); - it('can create a group', () => { cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click(); @@ -65,9 +48,6 @@ describe('groups', () => { cy.get("[data-testid='UG_DESC_ID']").type('hello-world'); cy.get("[data-testid='UG_USERS_ID']").click(); cy.contains(`unleash-e2e-user1-${randomId}`).click(); - cy.get("[data-testid='UG_USERS_ADD_ID']").click(); - cy.get("[data-testid='UG_USERS_TABLE_ROLE_ID']").click(); - cy.contains('Owner').click(); cy.get("[data-testid='UG_CREATE_BTN_ID']").click(); cy.wait('@createGroup'); @@ -80,16 +60,8 @@ describe('groups', () => { cy.intercept('POST', '/api/admin/groups').as('createGroup'); cy.get("[data-testid='UG_NAME_ID']").type(groupName); - cy.get("[data-testid='UG_DESC_ID']").type('hello-world'); - cy.get("[data-testid='UG_USERS_ID']").click(); - cy.contains(`unleash-e2e-user1-${randomId}`).click(); - cy.get("[data-testid='UG_USERS_ADD_ID']").click(); - cy.get("[data-testid='UG_USERS_TABLE_ROLE_ID']").click(); - cy.contains('Owner').click(); - - cy.get("[data-testid='UG_CREATE_BTN_ID']").click(); - cy.get("[data-testid='TOAST_TEXT']").contains( - 'Group name already exists' + cy.get("[data-testid='INPUT_ERROR_TEXT'").contains( + 'A group with that name already exists.' ); }); @@ -108,34 +80,15 @@ describe('groups', () => { it('can add user to a group', () => { cy.contains(groupName).click(); - cy.get("[data-testid='UG_ADD_USER_BTN_ID']").click(); + cy.get("[data-testid='UG_EDIT_USERS_BTN_ID']").click(); cy.get("[data-testid='UG_USERS_ID']").click(); cy.contains(`unleash-e2e-user2-${randomId}`).click(); - cy.get("[data-testid='UG_USERS_ADD_ID']").click(); cy.get("[data-testid='UG_SAVE_BTN_ID']").click(); cy.contains(`unleash-e2e-user1-${randomId}`); cy.contains(`unleash-e2e-user2-${randomId}`); - cy.get("td span:contains('Owner')").should('have.length', 1); - cy.get("td span:contains('Member')").should('have.length', 1); - }); - - it('can edit user role in a group', () => { - cy.contains(groupName).click(); - - cy.get(`[data-testid='UG_EDIT_USER_BTN_ID-${userIds[1]}']`).click(); - - cy.get("[data-testid='UG_USERS_ROLE_ID']").click(); - cy.get("li[data-value='Owner']").click(); - - cy.get("[data-testid='UG_SAVE_BTN_ID']").click(); - - cy.contains(`unleash-e2e-user1-${randomId}`); - cy.contains(`unleash-e2e-user2-${randomId}`); - cy.get("td span:contains('Owner')").should('have.length', 2); - cy.contains('Member').should('not.exist'); }); it('can remove user from a group', () => { diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx index 013ea29f3a..7c80940907 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx @@ -27,10 +27,14 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli import { Search } from 'component/common/Search/Search'; import useHiddenColumns from 'hooks/useHiddenColumns'; +const hiddenColumnsSmall = ['Icon', 'createdAt']; +const hiddenColumnsFlagE = ['projects', 'environment']; + export const ApiTokenTable = () => { const { tokens, loading } = useApiTokens(); const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []); const { uiConfig } = useUiConfig(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const { getTableProps, @@ -53,16 +57,8 @@ export const ApiTokenTable = () => { useSortBy ); - useHiddenColumns( - setHiddenColumns, - ['Icon', 'createdAt'], - useMediaQuery(theme.breakpoints.down('md')) - ); - useHiddenColumns( - setHiddenColumns, - ['projects', 'environment'], - !uiConfig.flags.E - ); + useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen); + useHiddenColumns(setHiddenColumns, hiddenColumnsFlagE, !uiConfig.flags.E); return ( { const { setToastData, setToastApiError } = useToast(); @@ -26,14 +27,18 @@ export const CreateGroup = () => { getGroupPayload, clearErrors, errors, + setErrors, } = useGroupForm(); + const { groups } = useGroups(); const { createGroup, loading } = useGroupApi(); const handleSubmit = async (e: Event) => { e.preventDefault(); clearErrors(); + if (!isValid) return; + const payload = getGroupPayload(); try { const group = await createGroup(payload); @@ -62,6 +67,19 @@ export const CreateGroup = () => { navigate(GO_BACK); }; + const isNameEmpty = (name: string) => name.length; + const isNameUnique = (name: string) => + !groups?.filter(group => group.name === name).length; + const isValid = isNameEmpty(name) && isNameUnique(name); + + const onSetName = (name: string) => { + clearErrors(); + if (!isNameUnique(name)) { + setErrors({ name: 'A group with that name already exists.' }); + } + setName(name); + }; + return ( { name={name} description={description} users={users} - setName={setName} + setName={onSetName} setDescription={setDescription} setUsers={setUsers} errors={errors} handleSubmit={handleSubmit} handleCancel={handleCancel} mode={CREATE} - clearErrors={clearErrors} > - { - setOpen(false); - }} - > - Cancel - - - - - - ); -}; diff --git a/frontend/src/component/admin/groups/Group/AddGroupUser/AddGroupUser.tsx b/frontend/src/component/admin/groups/Group/EditGroupUsers/EditGroupUsers.tsx similarity index 73% rename from frontend/src/component/admin/groups/Group/AddGroupUser/AddGroupUser.tsx rename to frontend/src/component/admin/groups/Group/EditGroupUsers/EditGroupUsers.tsx index a3b1577dcb..7fbdb57ac5 100644 --- a/frontend/src/component/admin/groups/Group/AddGroupUser/AddGroupUser.tsx +++ b/frontend/src/component/admin/groups/Group/EditGroupUsers/EditGroupUsers.tsx @@ -5,12 +5,14 @@ import { useGroupApi } from 'hooks/api/actions/useGroupApi/useGroupApi'; import { useGroup } from 'hooks/api/getters/useGroup/useGroup'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useToast from 'hooks/useToast'; -import { IGroup, IGroupUser } from 'interfaces/group'; -import { FC, FormEvent, useEffect, useMemo, useState } from 'react'; +import { IGroup } from 'interfaces/group'; +import { FC, FormEvent, useEffect } from 'react'; import { formatUnknownError } from 'utils/formatUnknownError'; import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect'; import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable'; import { UG_SAVE_BTN_ID } from 'utils/testIds'; +import { useGroupForm } from 'component/admin/groups/hooks/useGroupForm'; +import { useGroups } from 'hooks/api/getters/useGroups/useGroups'; const StyledForm = styled('form')(() => ({ display: 'flex', @@ -33,63 +35,43 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ marginLeft: theme.spacing(3), })); -interface IAddGroupUserProps { +interface IEditGroupUsersProps { open: boolean; setOpen: React.Dispatch>; group: IGroup; } -export const AddGroupUser: FC = ({ +export const EditGroupUsers: FC = ({ open, setOpen, group, }) => { const { refetchGroup } = useGroup(group.id); + const { refetchGroups } = useGroups(); const { updateGroup, loading } = useGroupApi(); const { setToastData, setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); - const [users, setUsers] = useState(group.users); + const { users, setUsers, getGroupPayload } = useGroupForm( + group.name, + group.description, + group.users + ); useEffect(() => { setUsers(group.users); - }, [group.users, open]); - - const newUsers = useMemo(() => { - return users.filter( - user => !group.users.some(({ id }) => id === user.id) - ); - }, [group.users, users]); - - const payload = useMemo(() => { - const addUsers = [...group.users, ...newUsers]; - return { - name: group.name, - description: group.description, - users: addUsers.map(({ id, role }) => ({ - user: { id }, - role, - })), - }; - }, [group, newUsers]); + }, [group.users, open, setUsers]); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); try { - const message = - newUsers.length === 1 - ? `${ - newUsers[0].name || - newUsers[0].username || - newUsers[0].email - } added to the group` - : `${newUsers.length} users added to the group`; - await updateGroup(group.id, payload); + await updateGroup(group.id, getGroupPayload()); refetchGroup(); + refetchGroups(); setOpen(false); setToastData({ - title: message, + title: 'Group users saved successfully', type: 'success', }); } catch (error: unknown) { @@ -103,7 +85,7 @@ export const AddGroupUser: FC = ({ }/api/admin/groups/${group.id}' \\ --header 'Authorization: INSERT_API_KEY' \\ --header 'Content-Type: application/json' \\ - --data-raw '${JSON.stringify(payload, undefined, 2)}'`; + --data-raw '${JSON.stringify(getGroupPayload(), undefined, 2)}'`; }; return ( @@ -112,12 +94,12 @@ export const AddGroupUser: FC = ({ onClose={() => { setOpen(false); }} - label="Add user" + label="Edit users" > = ({
- Add users to this group + Edit users in this group
diff --git a/frontend/src/component/admin/groups/Group/Group.tsx b/frontend/src/component/admin/groups/Group/Group.tsx index 8ded2867ec..231fca285c 100644 --- a/frontend/src/component/admin/groups/Group/Group.tsx +++ b/frontend/src/component/admin/groups/Group/Group.tsx @@ -17,13 +17,12 @@ import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { sortTypes } from 'utils/sortTypes'; import { createLocalStorage } from 'utils/createLocalStorage'; -import { IGroupUser, Role } from 'interfaces/group'; +import { IGroupUser } from 'interfaces/group'; import { useSearch } from 'hooks/useSearch'; import { Search } from 'component/common/Search/Search'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; -import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { Add, Delete, Edit } from '@mui/icons-material'; import { ADMIN } from 'component/providers/AccessProvider/permissions'; @@ -31,16 +30,14 @@ import { MainHeader } from 'component/common/MainHeader/MainHeader'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; -import { AddGroupUser } from './AddGroupUser/AddGroupUser'; -import { EditGroupUser } from './EditGroupUser/EditGroupUser'; +import { EditGroupUsers } from './EditGroupUsers/EditGroupUsers'; import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; import { UG_EDIT_BTN_ID, UG_DELETE_BTN_ID, - UG_ADD_USER_BTN_ID, - UG_EDIT_USER_BTN_ID, + UG_EDIT_USERS_BTN_ID, UG_REMOVE_USER_BTN_ID, } from 'utils/testIds'; @@ -55,14 +52,13 @@ const StyledDelete = styled(Delete)(({ theme }) => ({ export const groupUsersPlaceholder: IGroupUser[] = Array(15).fill({ name: 'Name of the user', username: 'Username of the user', - role: Role.Member, }); export type PageQueryType = Partial< Record<'sort' | 'order' | 'search', string> >; -const defaultSort: SortingRule = { id: 'role', desc: true }; +const defaultSort: SortingRule = { id: 'joinedAt' }; const { value: storedParams, setValue: setStoredParams } = createLocalStorage( 'Group:v1', @@ -75,8 +71,7 @@ export const Group: VFC = () => { const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const { group, loading } = useGroup(groupId); const [removeOpen, setRemoveOpen] = useState(false); - const [addUserOpen, setAddUserOpen] = useState(false); - const [editUserOpen, setEditUserOpen] = useState(false); + const [editUsersOpen, setEditUsersOpen] = useState(false); const [removeUserOpen, setRemoveUserOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(); @@ -109,13 +104,6 @@ export const Group: VFC = () => { minWidth: 100, searchable: true, }, - { - Header: 'User type', - accessor: 'role', - Cell: GroupUserRoleCell, - maxWidth: 150, - filterName: 'type', - }, { Header: 'Joined', accessor: 'joinedAt', @@ -138,20 +126,6 @@ export const Group: VFC = () => { align: 'center', Cell: ({ row: { original: rowUser } }: any) => ( - - - { - setSelectedUser(rowUser); - setEditUserOpen(true); - }} - > - - - - { { setSelectedUser(rowUser); setRemoveUserOpen(true); @@ -176,7 +149,7 @@ export const Group: VFC = () => { disableSortBy: true, }, ], - [setSelectedUser, setRemoveUserOpen, group?.users.length] + [setSelectedUser, setRemoveUserOpen] ); const [searchParams, setSearchParams] = useSearchParams(); @@ -312,15 +285,15 @@ export const Group: VFC = () => { } /> { - setAddUserOpen(true); + setEditUsersOpen(true); }} maxWidth="700px" Icon={Add} permission={ADMIN} > - Add user + Edit users } @@ -374,15 +347,9 @@ export const Group: VFC = () => { setOpen={setRemoveOpen} group={group!} /> - - = ({ ...group, users: group.users .filter(({ id }) => id !== user?.id) - .map(({ id, role }) => ({ + .map(({ id }) => ({ user: { id }, - role, })), }; await updateGroup(group.id, groupPayload); diff --git a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx index 6e6e2d9bb2..704583770b 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx @@ -42,14 +42,13 @@ interface IGroupForm { name: string; description: string; users: IGroupUser[]; - setName: React.Dispatch>; + setName: (name: string) => void; setDescription: React.Dispatch>; setUsers: React.Dispatch>; handleSubmit: (e: any) => void; handleCancel: () => void; errors: { [key: string]: string }; mode: 'Create' | 'Edit'; - clearErrors: () => void; } export const GroupForm: FC = ({ @@ -63,7 +62,6 @@ export const GroupForm: FC = ({ handleCancel, errors, mode, - clearErrors, children, }) => ( @@ -77,7 +75,6 @@ export const GroupForm: FC = ({ id="group-name" error={Boolean(errors.name)} errorText={errors.name} - onFocus={() => clearErrors()} value={name} onChange={e => setName(e.target.value)} data-testid={UG_NAME_ID} diff --git a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx index 584cfaa867..e17f558b4e 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx @@ -1,17 +1,11 @@ -import { - Autocomplete, - Button, - Checkbox, - styled, - TextField, -} from '@mui/material'; +import { Autocomplete, Checkbox, styled, TextField } from '@mui/material'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import { IUser } from 'interfaces/user'; -import { useMemo, useState, VFC } from 'react'; +import { VFC } from 'react'; import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; -import { IGroupUser, Role } from 'interfaces/group'; -import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds'; +import { IGroupUser } from 'interfaces/group'; +import { UG_USERS_ID } from 'utils/testIds'; const StyledOption = styled('div')(({ theme }) => ({ display: 'flex', @@ -21,6 +15,10 @@ const StyledOption = styled('div')(({ theme }) => ({ }, })); +const StyledTags = styled('div')(({ theme }) => ({ + paddingLeft: theme.spacing(1), +})); + const StyledGroupFormUsersSelect = styled('div')(({ theme }) => ({ display: 'flex', marginBottom: theme.spacing(3), @@ -50,6 +48,14 @@ const renderOption = ( ); +const renderTags = (value: IGroupUser[]) => ( + + {value.length > 1 + ? `${value.length} users selected` + : value[0].name || value[0].username || value[0].email} + +); + interface IGroupFormUsersSelectProps { users: IGroupUser[]; setUsers: React.Dispatch>; @@ -60,26 +66,6 @@ export const GroupFormUsersSelect: VFC = ({ setUsers, }) => { const { users: usersAll } = useUsers(); - const [selectedUsers, setSelectedUsers] = useState([]); - - const usersOptions = useMemo( - () => - usersAll.filter( - (user: IUser) => !users?.map(({ id }) => id).includes(user.id) - ), - [usersAll, users] - ); - - const onAdd = () => { - const usersToBeAdded = selectedUsers.map( - (user: IUser): IGroupUser => ({ - ...user, - role: Role.Member, - }) - ); - setUsers((users: IGroupUser[]) => [...users, ...usersToBeAdded]); - setSelectedUsers([]); - }; return ( @@ -87,9 +73,10 @@ export const GroupFormUsersSelect: VFC = ({ data-testid={UG_USERS_ID} size="small" multiple - limitTags={10} + limitTags={1} + openOnFocus disableCloseOnSelect - value={selectedUsers} + value={users} onChange={(event, newValue, reason) => { if ( event.type === 'keydown' && @@ -98,9 +85,9 @@ export const GroupFormUsersSelect: VFC = ({ ) { return; } - setSelectedUsers(newValue); + setUsers(newValue); }} - options={[...usersOptions].sort((a, b) => { + options={[...usersAll].sort((a, b) => { const aName = a.name || a.username || ''; const bName = b.name || b.username || ''; return aName.localeCompare(bName); @@ -108,20 +95,15 @@ export const GroupFormUsersSelect: VFC = ({ renderOption={(props, option, { selected }) => renderOption(props, option as IUser, selected) } + isOptionEqualToValue={(option, value) => option.id === value.id} getOptionLabel={(option: IUser) => option.email || option.name || option.username || '' } renderInput={params => ( )} + renderTags={value => renderTags(value)} /> - ); }; diff --git a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx index c9193a329d..64aff7d552 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx @@ -3,7 +3,6 @@ import { IconButton, Tooltip, useMediaQuery } from '@mui/material'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { IGroupUser } from 'interfaces/group'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; -import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { Delete } from '@mui/icons-material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; @@ -14,6 +13,8 @@ import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import theme from 'themes/theme'; import useHiddenColumns from 'hooks/useHiddenColumns'; +const hiddenColumnsSmall = ['imageUrl', 'name']; + interface IGroupFormUsersTableProps { users: IGroupUser[]; setUsers: React.Dispatch>; @@ -23,6 +24,8 @@ export const GroupFormUsersTable: VFC = ({ users, setUsers, }) => { + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); + const columns = useMemo( () => [ { @@ -52,30 +55,6 @@ export const GroupFormUsersTable: VFC = ({ minWidth: 100, searchable: true, }, - { - Header: 'Group role', - accessor: 'role', - Cell: ({ row: { original: rowUser } }: any) => ( - - setUsers((users: IGroupUser[]) => { - const newUsers = [...users]; - const index = newUsers.findIndex( - user => user.id === rowUser.id - ); - newUsers[index] = { - ...rowUser, - role, - }; - return newUsers; - }) - } - /> - ), - maxWidth: 150, - filterName: 'type', - }, { Header: 'Action', id: 'Action', @@ -121,11 +100,7 @@ export const GroupFormUsersTable: VFC = ({ useFlexLayout ); - useHiddenColumns( - setHiddenColumns, - ['imageUrl', 'name'], - useMediaQuery(theme.breakpoints.down('md')) - ); + useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen); return ( ({ - color: theme.palette.warning.main, -})); - -interface IGroupUserRoleCellProps { - value?: string; - onChange?: (role: Role) => void; -} - -export const GroupUserRoleCell = ({ - value = Role.Member, - onChange, -}: IGroupUserRoleCellProps) => { - const renderBadge = () => ( - {capitalize(value)}} - elseShow={ - }> - {capitalize(value)} - - } - /> - ); - - return ( - - - onChange!(event.target.value as Role) - } - > - {Object.values(Role).map(role => ( - - {role} - - ))} - - } - elseShow={() => renderBadge()} - /> - - ); -}; diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx index 6f9309b624..b9f0cc5d13 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx @@ -8,6 +8,7 @@ import { GroupCardActions } from './GroupCardActions/GroupCardActions'; import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup'; import { useState } from 'react'; import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined'; +import { EditGroupUsers } from 'component/admin/groups/Group/EditGroupUsers/EditGroupUsers'; const StyledLink = styled(Link)(({ theme }) => ({ textDecoration: 'none', @@ -82,6 +83,7 @@ interface IGroupCardProps { } export const GroupCard = ({ group }: IGroupCardProps) => { + const [editUsersOpen, setEditUsersOpen] = useState(false); const [removeOpen, setRemoveOpen] = useState(false); const navigate = useNavigate(); return ( @@ -93,6 +95,7 @@ export const GroupCard = ({ group }: IGroupCardProps) => { setEditUsersOpen(true)} onRemove={() => setRemoveOpen(true)} /> @@ -147,6 +150,11 @@ export const GroupCard = ({ group }: IGroupCardProps) => { + ({ @@ -25,11 +25,13 @@ const StyledPopover = styled(Popover)(({ theme }) => ({ interface IGroupCardActions { groupId: number; + onEditUsers: () => void; onRemove: () => void; } export const GroupCardActions: FC = ({ groupId, + onEditUsers, onRemove, }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -86,6 +88,21 @@ export const GroupCardActions: FC = ({ Edit group + { + onEditUsers(); + handleClose(); + }} + > + + + + + + Edit group users + + + { onRemove(); diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx index 01933b912f..3b13b4c190 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx @@ -1,6 +1,6 @@ import { styled } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { IGroupUser, Role } from 'interfaces/group'; +import { IGroupUser } from 'interfaces/group'; import React, { useMemo, useState } from 'react'; import { GroupPopover } from './GroupPopover/GroupPopover'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; @@ -26,7 +26,10 @@ interface IGroupCardAvatarsProps { export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => { const shownUsers = useMemo( - () => users.sort((a, b) => (a.role < b.role ? 1 : -1)).slice(0, 9), + () => + users + .sort((a, b) => b?.joinedAt!.getTime() - a?.joinedAt!.getTime()) + .slice(0, 9), [users] ); @@ -49,7 +52,6 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => { { onPopoverOpen(event); setPopupUser(user); diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupPopover/GroupPopover.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupPopover/GroupPopover.tsx index 665ee8bb74..b9a50d1442 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupPopover/GroupPopover.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupPopover/GroupPopover.tsx @@ -1,8 +1,5 @@ import { Popover, styled } from '@mui/material'; -import { IGroupUser, Role } from 'interfaces/group'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { Badge } from 'component/common/Badge/Badge'; -import { StarRounded } from '@mui/icons-material'; +import { IGroupUser } from 'interfaces/group'; const StyledPopover = styled(Popover)(({ theme }) => ({ pointerEvents: 'none', @@ -11,10 +8,6 @@ const StyledPopover = styled(Popover)(({ theme }) => ({ }, })); -const StyledPopupStar = styled(StarRounded)(({ theme }) => ({ - color: theme.palette.warning.main, -})); - const StyledName = styled('div')(({ theme }) => ({ color: theme.palette.text.secondary, fontSize: theme.fontSizes.smallBody, @@ -50,16 +43,6 @@ export const GroupPopover = ({ horizontal: 'left', }} > - {user?.role}} - elseShow={ - }> - {user?.role} - - } - /> - {user?.name || user?.username}
{user?.email}
diff --git a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx index 64c555d9f6..d47238784f 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx @@ -121,7 +121,7 @@ export const GroupsList: VFC = () => { 0} diff --git a/frontend/src/component/admin/groups/hooks/useGroupForm.ts b/frontend/src/component/admin/groups/hooks/useGroupForm.ts index 719578d3ae..da4b5c4adf 100644 --- a/frontend/src/component/admin/groups/hooks/useGroupForm.ts +++ b/frontend/src/component/admin/groups/hooks/useGroupForm.ts @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import useQueryParams from 'hooks/useQueryParams'; -import { IGroupUser, Role } from 'interfaces/group'; +import { IGroupUser } from 'interfaces/group'; export const useGroupForm = ( initialName = '', @@ -14,30 +14,12 @@ export const useGroupForm = ( const [users, setUsers] = useState(initialUsers); const [errors, setErrors] = useState({}); - useEffect(() => { - if (!name) { - setName(groupQueryName || initialName); - } - }, [name, initialName, groupQueryName]); - - useEffect(() => { - setDescription(initialDescription); - }, [initialDescription]); - - const initialUsersStringified = JSON.stringify(initialUsers); - - useEffect(() => { - setUsers(initialUsers); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialUsersStringified]); - const getGroupPayload = () => { return { name, description, - users: users.map(({ id, role }) => ({ + users: users.map(({ id }) => ({ user: { id }, - role: role || Role.Member, })), }; }; @@ -56,5 +38,6 @@ export const useGroupForm = ( getGroupPayload, clearErrors, errors, + setErrors, }; }; diff --git a/frontend/src/component/common/UserAvatar/UserAvatar.tsx b/frontend/src/component/common/UserAvatar/UserAvatar.tsx index c2ec2288ae..08150a331c 100644 --- a/frontend/src/component/common/UserAvatar/UserAvatar.tsx +++ b/frontend/src/component/common/UserAvatar/UserAvatar.tsx @@ -1,15 +1,7 @@ -import { - Avatar, - AvatarProps, - Badge, - styled, - SxProps, - Theme, -} from '@mui/material'; +import { Avatar, AvatarProps, styled, SxProps, Theme } from '@mui/material'; import { IUser } from 'interfaces/user'; import { FC } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { StarRounded } from '@mui/icons-material'; const StyledAvatar = styled(Avatar)(({ theme }) => ({ width: theme.spacing(3.5), @@ -21,17 +13,8 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({ fontWeight: theme.fontWeight.bold, })); -const StyledStar = styled(StarRounded)(({ theme }) => ({ - color: theme.palette.warning.main, - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadiusExtraLarge, - fontSize: theme.fontSizes.smallBody, - marginLeft: theme.spacing(-1), -})); - interface IUserAvatarProps extends AvatarProps { user?: IUser; - star?: boolean; src?: string; title?: string; onMouseEnter?: (event: any) => void; @@ -42,7 +25,6 @@ interface IUserAvatarProps extends AvatarProps { export const UserAvatar: FC = ({ user, - star, src, title, onMouseEnter, @@ -74,7 +56,7 @@ export const UserAvatar: FC = ({ } } - const avatar = ( + return ( = ({ /> ); - - return ( - } - > - {avatar} - - } - elseShow={avatar} - /> - ); }; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx index ddb8d64ce5..c5765402b4 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx @@ -170,7 +170,7 @@ export const ProjectAccessAssign = ({ const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - if (!role) return; + if (!isValid) return; try { if (!edit) { @@ -263,6 +263,8 @@ export const ProjectAccessAssign = ({ ); + const isValid = selectedOptions.length > 0 && role; + return ( setRole(newValue)} options={roles} @@ -360,6 +364,7 @@ export const ProjectAccessAssign = ({ type="submit" variant="contained" color="primary" + disabled={!isValid} > Assign {entityType} diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx index 75c9090270..d9d68fa447 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription.tsx @@ -1,13 +1,18 @@ -import { styled } from '@mui/material'; -import { useMemo, VFC } from 'react'; +import { styled, SxProps, Theme } from '@mui/material'; +import { ForwardedRef, forwardRef, useMemo, VFC } from 'react'; import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -const StyledDescription = styled('div')(({ theme }) => ({ +const StyledDescription = styled('div', { + shouldForwardProp: prop => + prop !== 'roleId' && prop !== 'popover' && prop !== 'sx', +})(({ theme, popover }) => ({ width: '100%', maxWidth: theme.spacing(50), padding: theme.spacing(3), - backgroundColor: theme.palette.neutral.light, + backgroundColor: popover + ? theme.palette.background.paper + : theme.palette.neutral.light, color: theme.palette.text.secondary, fontSize: theme.fontSizes.smallBody, borderRadius: theme.shape.borderRadiusMedium, @@ -32,74 +37,60 @@ const StyledDescriptionSubHeader = styled('p')(({ theme }) => ({ marginBottom: theme.spacing(1), })); -interface IProjectRoleDescriptionProps { +interface IProjectRoleDescriptionStyleProps { + popover?: boolean; + className?: string; + sx?: SxProps; +} + +interface IProjectRoleDescriptionProps + extends IProjectRoleDescriptionStyleProps { roleId: number; } -export const ProjectRoleDescription: VFC = ({ - roleId, -}) => { - const { role } = useProjectRole(roleId.toString()); +export const ProjectRoleDescription: VFC = + forwardRef( + ( + { roleId, className, sx, ...props }: IProjectRoleDescriptionProps, + ref: ForwardedRef + ) => { + const { role } = useProjectRole(roleId.toString()); - const environments = useMemo(() => { - const environments = new Set(); - role.permissions - ?.filter((permission: any) => permission.environment) - .forEach((permission: any) => { - environments.add(permission.environment); - }); - return [...environments].sort(); - }, [role]); + const environments = useMemo(() => { + const environments = new Set(); + role.permissions + ?.filter((permission: any) => permission.environment) + .forEach((permission: any) => { + environments.add(permission.environment); + }); + return [...environments].sort(); + }, [role]); - const projectPermissions = useMemo(() => { - return role.permissions?.filter( - (permission: any) => !permission.environment - ); - }, [role]); + const projectPermissions = useMemo(() => { + return role.permissions?.filter( + (permission: any) => !permission.environment + ); + }, [role]); - return ( - - - - Project permissions - - - {role.permissions - ?.filter( - (permission: any) => !permission.environment - ) - .map( - (permission: any) => permission.displayName - ) - .sort() - .map((permission: any) => ( -

{permission}

- ))} -
- - } - /> - - - Environment permissions - - {environments.map((environment: any) => ( -
- - {environment} - + return ( + + + + Project permissions + {role.permissions - .filter( + ?.filter( (permission: any) => - permission.environment === - environment + !permission.environment ) .map( (permission: any) => @@ -110,11 +101,45 @@ export const ProjectRoleDescription: VFC = ({

{permission}

))}
-
- ))} - - } - /> -
+ + } + /> + + + Environment permissions + + {environments.map((environment: any) => ( +
+ + {environment} + + + {role.permissions + .filter( + (permission: any) => + permission.environment === + environment + ) + .map( + (permission: any) => + permission.displayName + ) + .sort() + .map((permission: any) => ( +

+ {permission} +

+ ))} +
+
+ ))} + + } + /> + + ); + } ); -}; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessRoleCell/ProjectAccessRoleCell.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessRoleCell/ProjectAccessRoleCell.tsx new file mode 100644 index 0000000000..81cd03220f --- /dev/null +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessRoleCell/ProjectAccessRoleCell.tsx @@ -0,0 +1,70 @@ +import { Link, Popover, styled } from '@mui/material'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +import React from 'react'; +import { VFC } from 'react'; +import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription'; + +const StyledLink = styled(Link)(() => ({ + textDecoration: 'none', + '&:hover, &:focus': { + textDecoration: 'underline', + }, +})); + +const StyledPopover = styled(Popover)(() => ({ + pointerEvents: 'none', +})); + +interface IProjectAccessRoleCellProps { + roleId: number; + value?: string; + emptyText?: string; +} + +export const ProjectAccessRoleCell: VFC = ({ + roleId, + value, + emptyText, +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + + const onPopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const onPopoverClose = () => { + setAnchorEl(null); + }; + + if (!value) return {emptyText}; + + return ( + <> + + { + onPopoverOpen(event); + }} + onMouseLeave={onPopoverClose} + > + {value} + + + + + + + ); +}; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx index 29754c44d8..4bcb79d4f3 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx @@ -43,6 +43,7 @@ import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAcce import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser'; import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup'; import useHiddenColumns from 'hooks/useHiddenColumns'; +import { ProjectAccessRoleCell } from './ProjectAccessRoleCell/ProjectAccessRoleCell'; export type PageQueryType = Partial< Record<'sort' | 'order' | 'search', string> @@ -69,6 +70,14 @@ const StyledGroupAvatar = styled(UserAvatar)(({ theme }) => ({ outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, })); +const hiddenColumnsSmall = [ + 'imageUrl', + 'username', + 'role', + 'added', + 'lastLogin', +]; + export const ProjectAccessTable: VFC = () => { const projectId = useRequiredPathParam('projectId'); @@ -149,6 +158,12 @@ export const ProjectAccessTable: VFC = () => { accessor: (row: IProjectAccess) => access?.roles.find(({ id }) => id === row.entity.roleId) ?.name, + Cell: ({ value, row: { original: row } }: any) => ( + + ), minWidth: 120, filterName: 'role', }, @@ -281,11 +296,7 @@ export const ProjectAccessTable: VFC = () => { useFlexLayout ); - useHiddenColumns( - setHiddenColumns, - ['imageUrl', 'username', 'role', 'added', 'lastLogin'], - isSmallScreen - ); + useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen); useEffect(() => { const tableState: PageQueryType = {}; @@ -335,6 +346,7 @@ export const ProjectAccessTable: VFC = () => { } setRemoveOpen(false); }; + return ( ({ }, })); -const defaultSort: SortingRule = { id: 'role', desc: true }; +const defaultSort: SortingRule = { id: 'joinedAt' }; const columns = [ { @@ -77,13 +76,6 @@ const columns = [ minWidth: 100, searchable: true, }, - { - Header: 'User type', - accessor: 'role', - Cell: GroupUserRoleCell, - maxWidth: 150, - filterName: 'type', - }, { id: 'joined', Header: 'Joined', @@ -104,6 +96,8 @@ const columns = [ }, ]; +const hiddenColumnsSmall = ['imageUrl', 'name', 'joined', 'lastLogin']; + interface IProjectGroupViewProps { open: boolean; setOpen: React.Dispatch>; @@ -156,11 +150,7 @@ export const ProjectGroupView: VFC = ({ useFlexLayout ); - useHiddenColumns( - setHiddenColumns, - ['imageUrl', 'name', 'joined', 'lastLogin'], - useMediaQuery(theme.breakpoints.down('md')) - ); + useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen); return ( users.map(user => ({ ...user.user, joinedAt: new Date(user.joinedAt), - role: user.role, })); export const useGroup = (groupId: number): IUseGroupOutput => { diff --git a/frontend/src/interfaces/group.ts b/frontend/src/interfaces/group.ts index 4c57ab53b6..95b9d820a7 100644 --- a/frontend/src/interfaces/group.ts +++ b/frontend/src/interfaces/group.ts @@ -1,10 +1,5 @@ import { IUser } from './user'; -export enum Role { - Owner = 'Owner', - Member = 'Member', -} - export interface IGroup { id: number; name: string; @@ -17,7 +12,6 @@ export interface IGroup { } export interface IGroupUser extends IUser { - role: Role; joinedAt?: Date; } @@ -25,5 +19,4 @@ export interface IGroupUserModel { user: { id: number; }; - role: Role; } diff --git a/frontend/src/utils/testIds.ts b/frontend/src/utils/testIds.ts index fdf54f584c..e0f7bfb955 100644 --- a/frontend/src/utils/testIds.ts +++ b/frontend/src/utils/testIds.ts @@ -15,15 +15,12 @@ export const UG_NAME_ID = 'UG_NAME_ID'; export const UG_DESC_ID = 'UG_DESC_ID'; export const UG_USERS_ID = 'UG_USERS_ID'; export const UG_USERS_ADD_ID = 'UG_USERS_ADD_ID'; -export const UG_USERS_TABLE_ROLE_ID = 'UG_USERS_TABLE_ROLE_ID'; export const UG_CREATE_BTN_ID = 'UG_CREATE_BTN_ID'; export const UG_SAVE_BTN_ID = 'UG_SAVE_BTN_ID'; export const UG_EDIT_BTN_ID = 'UG_EDIT_BTN_ID'; export const UG_DELETE_BTN_ID = 'UG_DELETE_BTN_ID'; -export const UG_ADD_USER_BTN_ID = 'UG_ADD_USER_BTN_ID'; -export const UG_EDIT_USER_BTN_ID = 'UG_EDIT_USER_BTN_ID'; +export const UG_EDIT_USERS_BTN_ID = 'UG_EDIT_USERS_BTN_ID'; export const UG_REMOVE_USER_BTN_ID = 'UG_REMOVE_USER_BTN_ID'; -export const UG_USERS_ROLE_ID = 'UG_USERS_ROLE_ID'; /* SEGMENT */ export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID';