mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: group project access inconsistencies (#1178)
* fix: group project access inconsistencies * fix relative path * wip * refactor: make project tabs work as routes * refactor: finish refactoring project assign forms * fix: update snaps * fix: update snaps * add some basic cypress e2e tests to groups * add remaining cypress e2e tests for group CRUD * add groups e2e to gh workflows * refactor: simplify useMemo usage * add GO_BACK navigate const * fix: remove trailing slash on user creation request Co-authored-by: olav <mail@olav.io> Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
		
							parent
							
								
									59c8822cf2
								
							
						
					
					
						commit
						672a3f0b92
					
				
							
								
								
									
										25
									
								
								frontend/.github/workflows/e2e.groups.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/.github/workflows/e2e.groups.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | name: e2e:groups | ||||||
|  | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows | ||||||
|  | on: [deployment_status] | ||||||
|  | jobs: | ||||||
|  |   e2e: | ||||||
|  |     # only runs this job on successful deploy | ||||||
|  |     if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Dump GitHub context | ||||||
|  |         env: | ||||||
|  |           GITHUB_CONTEXT: ${{ toJson(github) }} | ||||||
|  |         run: | | ||||||
|  |           echo "$GITHUB_CONTEXT" | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |       - name: Run Cypress | ||||||
|  |         uses: cypress-io/github-action@v2 | ||||||
|  |         with: | ||||||
|  |           env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all | ||||||
|  |           config: baseUrl=${{ github.event.deployment_status.target_url }} | ||||||
|  |           record: true | ||||||
|  |           spec: cypress/integration/groups/groups.spec.ts | ||||||
|  |         env: | ||||||
|  |           CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} | ||||||
							
								
								
									
										160
									
								
								frontend/cypress/integration/groups/groups.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								frontend/cypress/integration/groups/groups.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,160 @@ | |||||||
|  | /// <reference types="cypress" />
 | ||||||
|  | 
 | ||||||
|  | export {}; | ||||||
|  | const baseUrl = Cypress.config().baseUrl; | ||||||
|  | const randomId = String(Math.random()).split('.')[1]; | ||||||
|  | const groupName = `unleash-e2e-${randomId}`; | ||||||
|  | const userIds: any[] = []; | ||||||
|  | 
 | ||||||
|  | // Disable all active splash pages by visiting them.
 | ||||||
|  | const disableActiveSplashScreens = () => { | ||||||
|  |     cy.visit(`/splash/operators`); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | describe('groups', () => { | ||||||
|  |     before(() => { | ||||||
|  |         disableActiveSplashScreens(); | ||||||
|  |         cy.login(); | ||||||
|  |         for (let i = 1; i <= 2; i++) { | ||||||
|  |             cy.request('POST', `${baseUrl}/api/admin/user-admin`, { | ||||||
|  |                 name: `unleash-e2e-user${i}-${randomId}`, | ||||||
|  |                 email: `unleash-e2e-user${i}-${randomId}@test.com`, | ||||||
|  |                 sendEmail: false, | ||||||
|  |                 rootRole: 3, | ||||||
|  |             }).then(response => userIds.push(response.body.id)); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     after(() => { | ||||||
|  |         userIds.forEach(id => | ||||||
|  |             cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`) | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |         cy.login(); | ||||||
|  |         cy.visit('/admin/groups'); | ||||||
|  |         if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { | ||||||
|  |             cy.get("[data-testid='CLOSE_SPLASH']").click(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     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(); | ||||||
|  | 
 | ||||||
|  |         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.wait('@createGroup'); | ||||||
|  |         cy.contains(groupName); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('gives an error if a group exists with the same name', () => { | ||||||
|  |         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_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' | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('can edit a group', () => { | ||||||
|  |         cy.contains(groupName).click(); | ||||||
|  | 
 | ||||||
|  |         cy.get("[data-testid='UG_EDIT_BTN_ID']").click(); | ||||||
|  | 
 | ||||||
|  |         cy.get("[data-testid='UG_DESC_ID']").type('-my edited description'); | ||||||
|  | 
 | ||||||
|  |         cy.get("[data-testid='UG_SAVE_BTN_ID']").click(); | ||||||
|  | 
 | ||||||
|  |         cy.contains('hello-world-my edited description'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     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_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', () => { | ||||||
|  |         cy.contains(groupName).click(); | ||||||
|  | 
 | ||||||
|  |         cy.get(`[data-testid='UG_REMOVE_USER_BTN_ID-${userIds[1]}']`).click(); | ||||||
|  | 
 | ||||||
|  |         cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click(); | ||||||
|  | 
 | ||||||
|  |         cy.contains(`unleash-e2e-user1-${randomId}`); | ||||||
|  |         cy.contains(`unleash-e2e-user2-${randomId}`).should('not.exist'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('can delete a group', () => { | ||||||
|  |         cy.contains(groupName).click(); | ||||||
|  | 
 | ||||||
|  |         cy.get("[data-testid='UG_DELETE_BTN_ID']").click(); | ||||||
|  |         cy.get("[data-testid='DIALOGUE_CONFIRM_ID'").click(); | ||||||
|  | 
 | ||||||
|  |         cy.contains(groupName).should('not.exist'); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @ -33,6 +33,7 @@ import { | |||||||
|     StyledButtonSection, |     StyledButtonSection, | ||||||
| } from './AddonForm.styles'; | } from './AddonForm.styles'; | ||||||
| import { useTheme } from '@mui/system'; | import { useTheme } from '@mui/system'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| interface IAddonFormProps { | interface IAddonFormProps { | ||||||
|     provider?: IAddonProvider; |     provider?: IAddonProvider; | ||||||
| @ -168,7 +169,7 @@ export const AddonForm: VFC<IAddonFormProps> = ({ | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const onCancel = () => { |     const onCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const onSubmit: FormEventHandler<HTMLFormElement> = async event => { |     const onSubmit: FormEventHandler<HTMLFormElement> = async event => { | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import { useState } from 'react'; | |||||||
| import { scrollToTop } from 'component/common/util'; | import { scrollToTop } from 'component/common/util'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { usePageTitle } from 'hooks/usePageTitle'; | import { usePageTitle } from 'hooks/usePageTitle'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const pageTitle = 'Create API token'; | const pageTitle = 'Create API token'; | ||||||
| 
 | 
 | ||||||
| @ -75,7 +76,7 @@ export const CreateApiToken = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import { formatUnknownError } from 'utils/formatUnknownError'; | |||||||
| import { UG_CREATE_BTN_ID } from 'utils/testIds'; | import { UG_CREATE_BTN_ID } from 'utils/testIds'; | ||||||
| import { Button } from '@mui/material'; | import { Button } from '@mui/material'; | ||||||
| import { CREATE } from 'constants/misc'; | import { CREATE } from 'constants/misc'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| export const CreateGroup = () => { | export const CreateGroup = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -58,7 +59,7 @@ export const CreateGroup = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -10,6 +10,8 @@ import { Button } from '@mui/material'; | |||||||
| import { EDIT } from 'constants/misc'; | import { EDIT } from 'constants/misc'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| import { useGroup } from 'hooks/api/getters/useGroup/useGroup'; | import { useGroup } from 'hooks/api/getters/useGroup/useGroup'; | ||||||
|  | import { UG_SAVE_BTN_ID } from 'utils/testIds'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| export const EditGroup = () => { | export const EditGroup = () => { | ||||||
|     const groupId = Number(useRequiredPathParam('groupId')); |     const groupId = Number(useRequiredPathParam('groupId')); | ||||||
| @ -40,7 +42,7 @@ export const EditGroup = () => { | |||||||
|         try { |         try { | ||||||
|             await updateGroup(groupId, payload); |             await updateGroup(groupId, payload); | ||||||
|             refetchGroup(); |             refetchGroup(); | ||||||
|             navigate(-1); |             navigate(GO_BACK); | ||||||
|             setToastData({ |             setToastData({ | ||||||
|                 title: 'Group updated successfully', |                 title: 'Group updated successfully', | ||||||
|                 type: 'success', |                 type: 'success', | ||||||
| @ -60,7 +62,7 @@ export const EditGroup = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
| @ -85,7 +87,12 @@ export const EditGroup = () => { | |||||||
|                 mode={EDIT} |                 mode={EDIT} | ||||||
|                 clearErrors={clearErrors} |                 clearErrors={clearErrors} | ||||||
|             > |             > | ||||||
|                 <Button type="submit" variant="contained" color="primary"> |                 <Button | ||||||
|  |                     type="submit" | ||||||
|  |                     variant="contained" | ||||||
|  |                     color="primary" | ||||||
|  |                     data-testid={UG_SAVE_BTN_ID} | ||||||
|  |                 > | ||||||
|                     Save |                     Save | ||||||
|                 </Button> |                 </Button> | ||||||
|             </GroupForm> |             </GroupForm> | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ import { FC, FormEvent, useEffect, useMemo, useState } from 'react'; | |||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect'; | import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect'; | ||||||
| import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable'; | import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable'; | ||||||
|  | import { UG_SAVE_BTN_ID } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| const StyledForm = styled('form')(() => ({ | const StyledForm = styled('form')(() => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -142,6 +143,7 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({ | |||||||
|                             type="submit" |                             type="submit" | ||||||
|                             variant="contained" |                             variant="contained" | ||||||
|                             color="primary" |                             color="primary" | ||||||
|  |                             data-testid={UG_SAVE_BTN_ID} | ||||||
|                         > |                         > | ||||||
|                             Save |                             Save | ||||||
|                         </Button> |                         </Button> | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import useToast from 'hooks/useToast'; | |||||||
| import { IGroup, IGroupUser, Role } from 'interfaces/group'; | import { IGroup, IGroupUser, Role } from 'interfaces/group'; | ||||||
| import { FC, FormEvent, useEffect, useMemo, useState } from 'react'; | import { FC, FormEvent, useEffect, useMemo, useState } from 'react'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { UG_SAVE_BTN_ID, UG_USERS_ROLE_ID } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| const StyledForm = styled('form')(() => ({ | const StyledForm = styled('form')(() => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -143,6 +144,7 @@ export const EditGroupUser: FC<IEditGroupUserProps> = ({ | |||||||
|                             Assign the role the user should have in this group |                             Assign the role the user should have in this group | ||||||
|                         </StyledInputDescription> |                         </StyledInputDescription> | ||||||
|                         <StyledSelect |                         <StyledSelect | ||||||
|  |                             data-testid={UG_USERS_ROLE_ID} | ||||||
|                             size="small" |                             size="small" | ||||||
|                             value={role} |                             value={role} | ||||||
|                             onChange={event => |                             onChange={event => | ||||||
| @ -159,6 +161,7 @@ export const EditGroupUser: FC<IEditGroupUserProps> = ({ | |||||||
| 
 | 
 | ||||||
|                     <StyledButtonContainer> |                     <StyledButtonContainer> | ||||||
|                         <Button |                         <Button | ||||||
|  |                             data-testid={UG_SAVE_BTN_ID} | ||||||
|                             type="submit" |                             type="submit" | ||||||
|                             variant="contained" |                             variant="contained" | ||||||
|                             color="primary" |                             color="primary" | ||||||
|  | |||||||
| @ -37,6 +37,13 @@ import { AddGroupUser } from './AddGroupUser/AddGroupUser'; | |||||||
| import { EditGroupUser } from './EditGroupUser/EditGroupUser'; | import { EditGroupUser } from './EditGroupUser/EditGroupUser'; | ||||||
| import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser'; | import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser'; | ||||||
| import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; | import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; | ||||||
|  | import { | ||||||
|  |     UG_EDIT_BTN_ID, | ||||||
|  |     UG_DELETE_BTN_ID, | ||||||
|  |     UG_ADD_USER_BTN_ID, | ||||||
|  |     UG_EDIT_USER_BTN_ID, | ||||||
|  |     UG_REMOVE_USER_BTN_ID, | ||||||
|  | } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| const StyledEdit = styled(Edit)(({ theme }) => ({ | const StyledEdit = styled(Edit)(({ theme }) => ({ | ||||||
|     fontSize: theme.fontSizes.mainHeader, |     fontSize: theme.fontSizes.mainHeader, | ||||||
| @ -134,6 +141,7 @@ export const Group: VFC = () => { | |||||||
|                     <ActionCell> |                     <ActionCell> | ||||||
|                         <Tooltip title="Edit user" arrow describeChild> |                         <Tooltip title="Edit user" arrow describeChild> | ||||||
|                             <IconButton |                             <IconButton | ||||||
|  |                                 data-testid={`${UG_EDIT_USER_BTN_ID}-${rowUser.id}`} | ||||||
|                                 onClick={() => { |                                 onClick={() => { | ||||||
|                                     setSelectedUser(rowUser); |                                     setSelectedUser(rowUser); | ||||||
|                                     setEditUserOpen(true); |                                     setEditUserOpen(true); | ||||||
| @ -148,6 +156,7 @@ export const Group: VFC = () => { | |||||||
|                             describeChild |                             describeChild | ||||||
|                         > |                         > | ||||||
|                             <IconButton |                             <IconButton | ||||||
|  |                                 data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`} | ||||||
|                                 onClick={() => { |                                 onClick={() => { | ||||||
|                                     setSelectedUser(rowUser); |                                     setSelectedUser(rowUser); | ||||||
|                                     setRemoveUserOpen(true); |                                     setRemoveUserOpen(true); | ||||||
| @ -240,6 +249,7 @@ export const Group: VFC = () => { | |||||||
|                         actions={ |                         actions={ | ||||||
|                             <> |                             <> | ||||||
|                                 <PermissionIconButton |                                 <PermissionIconButton | ||||||
|  |                                     data-testid={UG_EDIT_BTN_ID} | ||||||
|                                     to={`/admin/groups/${groupId}/edit`} |                                     to={`/admin/groups/${groupId}/edit`} | ||||||
|                                     component={Link} |                                     component={Link} | ||||||
|                                     data-loading |                                     data-loading | ||||||
| @ -251,6 +261,7 @@ export const Group: VFC = () => { | |||||||
|                                     <StyledEdit /> |                                     <StyledEdit /> | ||||||
|                                 </PermissionIconButton> |                                 </PermissionIconButton> | ||||||
|                                 <PermissionIconButton |                                 <PermissionIconButton | ||||||
|  |                                     data-testid={UG_DELETE_BTN_ID} | ||||||
|                                     data-loading |                                     data-loading | ||||||
|                                     onClick={() => setRemoveOpen(true)} |                                     onClick={() => setRemoveOpen(true)} | ||||||
|                                     permission={ADMIN} |                                     permission={ADMIN} | ||||||
| @ -296,6 +307,7 @@ export const Group: VFC = () => { | |||||||
|                                             } |                                             } | ||||||
|                                         /> |                                         /> | ||||||
|                                         <Button |                                         <Button | ||||||
|  |                                             data-testid={UG_ADD_USER_BTN_ID} | ||||||
|                                             variant="contained" |                                             variant="contained" | ||||||
|                                             color="primary" |                                             color="primary" | ||||||
|                                             onClick={() => { |                                             onClick={() => { | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import { IUser } from 'interfaces/user'; | |||||||
| import { useMemo, useState, VFC } from 'react'; | import { useMemo, useState, VFC } from 'react'; | ||||||
| import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; | import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; | ||||||
| import { IGroupUser, Role } from 'interfaces/group'; | import { IGroupUser, Role } from 'interfaces/group'; | ||||||
|  | import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| const StyledOption = styled('div')(({ theme }) => ({ | const StyledOption = styled('div')(({ theme }) => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -83,6 +84,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({ | |||||||
|     return ( |     return ( | ||||||
|         <StyledGroupFormUsersSelect> |         <StyledGroupFormUsersSelect> | ||||||
|             <Autocomplete |             <Autocomplete | ||||||
|  |                 data-testid={UG_USERS_ID} | ||||||
|                 size="small" |                 size="small" | ||||||
|                 multiple |                 multiple | ||||||
|                 limitTags={10} |                 limitTags={10} | ||||||
| @ -113,7 +115,11 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({ | |||||||
|                     <TextField {...params} label="Select users" /> |                     <TextField {...params} label="Select users" /> | ||||||
|                 )} |                 )} | ||||||
|             /> |             /> | ||||||
|             <Button variant="outlined" onClick={onAdd}> |             <Button | ||||||
|  |                 variant="outlined" | ||||||
|  |                 onClick={onAdd} | ||||||
|  |                 data-testid={UG_USERS_ADD_ID} | ||||||
|  |             > | ||||||
|                 Add |                 Add | ||||||
|             </Button> |             </Button> | ||||||
|         </StyledGroupFormUsersSelect> |         </StyledGroupFormUsersSelect> | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | |||||||
| import { Role } from 'interfaces/group'; | import { Role } from 'interfaces/group'; | ||||||
| import { Badge } from 'component/common/Badge/Badge'; | import { Badge } from 'component/common/Badge/Badge'; | ||||||
| import { StarRounded } from '@mui/icons-material'; | import { StarRounded } from '@mui/icons-material'; | ||||||
|  | import { UG_USERS_TABLE_ROLE_ID } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| const StyledPopupStar = styled(StarRounded)(({ theme }) => ({ | const StyledPopupStar = styled(StarRounded)(({ theme }) => ({ | ||||||
|     color: theme.palette.warning.main, |     color: theme.palette.warning.main, | ||||||
| @ -36,6 +37,7 @@ export const GroupUserRoleCell = ({ | |||||||
|                 condition={Boolean(onChange)} |                 condition={Boolean(onChange)} | ||||||
|                 show={ |                 show={ | ||||||
|                     <Select |                     <Select | ||||||
|  |                         data-testid={UG_USERS_TABLE_ROLE_ID} | ||||||
|                         size="small" |                         size="small" | ||||||
|                         value={value} |                         value={value} | ||||||
|                         onChange={event => |                         onChange={event => | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC | |||||||
| import { TablePlaceholder } from 'component/common/Table'; | import { TablePlaceholder } from 'component/common/Table'; | ||||||
| import { GroupCard } from './GroupCard/GroupCard'; | import { GroupCard } from './GroupCard/GroupCard'; | ||||||
| import { GroupEmpty } from './GroupEmpty/GroupEmpty'; | import { GroupEmpty } from './GroupEmpty/GroupEmpty'; | ||||||
|  | import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| type PageQueryType = Partial<Record<'search', string>>; | type PageQueryType = Partial<Record<'search', string>>; | ||||||
| 
 | 
 | ||||||
| @ -85,6 +86,7 @@ export const GroupsList: VFC = () => { | |||||||
|                                 component={Link} |                                 component={Link} | ||||||
|                                 variant="contained" |                                 variant="contained" | ||||||
|                                 color="primary" |                                 color="primary" | ||||||
|  |                                 data-testid={NAVIGATE_TO_CREATE_GROUP} | ||||||
|                             > |                             > | ||||||
|                                 New group |                                 New group | ||||||
|                             </Button> |                             </Button> | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import useToast from 'hooks/useToast'; | |||||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const CreateProjectRole = () => { | const CreateProjectRole = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -66,7 +67,7 @@ const CreateProjectRole = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import useProjectRoleForm from '../hooks/useProjectRoleForm'; | |||||||
| import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm'; | import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const EditProjectRole = () => { | const EditProjectRole = () => { | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
| @ -94,7 +95,7 @@ const EditProjectRole = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import { scrollToTop } from 'component/common/util'; | |||||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const CreateUser = () => { | const CreateUser = () => { | ||||||
|     const { setToastApiError } = useToast(); |     const { setToastApiError } = useToast(); | ||||||
| @ -72,7 +73,7 @@ const CreateUser = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo'; | |||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const EditUser = () => { | const EditUser = () => { | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
| @ -69,7 +70,7 @@ const EditUser = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import { useNavigate } from 'react-router'; | |||||||
| import { ReactComponent as LogoIcon } from 'assets/icons/logoBg.svg'; | import { ReactComponent as LogoIcon } from 'assets/icons/logoBg.svg'; | ||||||
| 
 | 
 | ||||||
| import { useStyles } from './NotFound.styles'; | import { useStyles } from './NotFound.styles'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const NotFound = () => { | const NotFound = () => { | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
| @ -14,7 +15,7 @@ const NotFound = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const onClickBack = () => { |     const onClickBack = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import UIContext from 'contexts/UIContext'; | |||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import Close from '@mui/icons-material/Close'; | import Close from '@mui/icons-material/Close'; | ||||||
| import { IToast } from 'interfaces/toast'; | import { IToast } from 'interfaces/toast'; | ||||||
|  | import { TOAST_TEXT } from 'utils/testIds'; | ||||||
| 
 | 
 | ||||||
| const Toast = ({ title, text, type, confetti }: IToast) => { | const Toast = ({ title, text, type, confetti }: IToast) => { | ||||||
|     const { setToast } = useContext(UIContext); |     const { setToast } = useContext(UIContext); | ||||||
| @ -72,7 +73,9 @@ const Toast = ({ title, text, type, confetti }: IToast) => { | |||||||
| 
 | 
 | ||||||
|                                 <ConditionallyRender |                                 <ConditionallyRender | ||||||
|                                     condition={Boolean(text)} |                                     condition={Boolean(text)} | ||||||
|                                     show={<p>{text}</p>} |                                     show={ | ||||||
|  |                                         <p data-testid={TOAST_TEXT}>{text}</p> | ||||||
|  |                                     } | ||||||
|                                 /> |                                 /> | ||||||
|                             </div> |                             </div> | ||||||
|                         </div> |                         </div> | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| import { CreateUnleashContext } from 'component/context/CreateUnleashContext/CreateUnleashContext'; | import { CreateUnleashContext } from 'component/context/CreateUnleashContext/CreateUnleashContext'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| export const CreateUnleashContextPage = () => { | export const CreateUnleashContextPage = () => { | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
| @ -7,7 +8,7 @@ export const CreateUnleashContextPage = () => { | |||||||
|     return ( |     return ( | ||||||
|         <CreateUnleashContext |         <CreateUnleashContext | ||||||
|             onSubmit={() => navigate('/context')} |             onSubmit={() => navigate('/context')} | ||||||
|             onCancel={() => navigate(-1)} |             onCancel={() => navigate(GO_BACK)} | ||||||
|         /> |         /> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import { formatUnknownError } from 'utils/formatUnknownError'; | |||||||
| import { ContextForm } from '../ContextForm/ContextForm'; | import { ContextForm } from '../ContextForm/ContextForm'; | ||||||
| import { useContextForm } from '../hooks/useContextForm'; | import { useContextForm } from '../hooks/useContextForm'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| export const EditContext = () => { | export const EditContext = () => { | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
| @ -71,7 +72,7 @@ export const EditContext = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const onCancel = () => { |     const onCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ import { PageContent } from 'component/common/PageContent/PageContent'; | |||||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const CreateEnvironment = () => { | const CreateEnvironment = () => { | ||||||
|     const { setToastApiError, setToastData } = useToast(); |     const { setToastApiError, setToastData } = useToast(); | ||||||
| @ -66,7 +67,7 @@ const CreateEnvironment = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import EnvironmentForm from '../EnvironmentForm/EnvironmentForm'; | |||||||
| import useEnvironmentForm from '../hooks/useEnvironmentForm'; | import useEnvironmentForm from '../hooks/useEnvironmentForm'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const EditEnvironment = () => { | const EditEnvironment = () => { | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
| @ -56,7 +57,7 @@ const EditEnvironment = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton'; | |||||||
| import UIContext from 'contexts/UIContext'; | import UIContext from 'contexts/UIContext'; | ||||||
| import { CF_CREATE_BTN_ID } from 'utils/testIds'; | import { CF_CREATE_BTN_ID } from 'utils/testIds'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const CreateFeature = () => { | const CreateFeature = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -70,7 +71,7 @@ const CreateFeature = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | |||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const EditFeature = () => { | const EditFeature = () => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
| @ -74,7 +75,7 @@ const EditFeature = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -80,16 +80,7 @@ exports[`returns all baseRoutes 1`] = ` | |||||||
|     "flag": "P", |     "flag": "P", | ||||||
|     "menu": {}, |     "menu": {}, | ||||||
|     "parent": "/projects", |     "parent": "/projects", | ||||||
|     "path": "/projects/:projectId/:activeTab", |     "path": "/projects/:projectId/*", | ||||||
|     "title": ":projectId", |  | ||||||
|     "type": "protected", |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "component": [Function], |  | ||||||
|     "flag": "P", |  | ||||||
|     "menu": {}, |  | ||||||
|     "parent": "/projects", |  | ||||||
|     "path": "/projects/:projectId", |  | ||||||
|     "title": ":projectId", |     "title": ":projectId", | ||||||
|     "type": "protected", |     "type": "protected", | ||||||
|   }, |   }, | ||||||
|  | |||||||
| @ -136,16 +136,7 @@ export const routes: IRoute[] = [ | |||||||
|         menu: {}, |         menu: {}, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         path: '/projects/:projectId/:activeTab', |         path: '/projects/:projectId/*', | ||||||
|         parent: '/projects', |  | ||||||
|         title: ':projectId', |  | ||||||
|         component: Project, |  | ||||||
|         flag: P, |  | ||||||
|         type: 'protected', |  | ||||||
|         menu: {}, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         path: '/projects/:projectId', |  | ||||||
|         parent: '/projects', |         parent: '/projects', | ||||||
|         title: ':projectId', |         title: ':projectId', | ||||||
|         component: Project, |         component: Project, | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; | |||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const CreateProject = () => { | const CreateProject = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -65,7 +66,7 @@ const CreateProject = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | |||||||
| import { useContext } from 'react'; | import { useContext } from 'react'; | ||||||
| import AccessContext from 'contexts/AccessContext'; | import AccessContext from 'contexts/AccessContext'; | ||||||
| import { Alert } from '@mui/material'; | import { Alert } from '@mui/material'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const EditProject = () => { | const EditProject = () => { | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
| @ -70,7 +71,7 @@ const EditProject = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && ( |     const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && ( | ||||||
|  | |||||||
| @ -16,80 +16,54 @@ import ProjectOverview from './ProjectOverview'; | |||||||
| import ProjectHealth from './ProjectHealth/ProjectHealth'; | import ProjectHealth from './ProjectHealth/ProjectHealth'; | ||||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||||
| import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||||
| import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; |  | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; |  | ||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
|  | import { Routes, Route, useLocation } from 'react-router-dom'; | ||||||
| 
 | 
 | ||||||
| const Project = () => { | const Project = () => { | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     const activeTab = useOptionalPathParam('activeTab'); |  | ||||||
|     const params = useQueryParams(); |     const params = useQueryParams(); | ||||||
|     const { project, error, loading, refetch } = useProject(projectId); |     const { project, error, loading, refetch } = useProject(projectId); | ||||||
|     const ref = useLoading(loading); |     const ref = useLoading(loading); | ||||||
|     const { setToastData } = useToast(); |     const { setToastData } = useToast(); | ||||||
|     const { classes: styles } = useStyles(); |     const { classes: styles } = useStyles(); | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|  |     const { pathname } = useLocation(); | ||||||
|     const { isOss } = useUiConfig(); |     const { isOss } = useUiConfig(); | ||||||
| 
 |  | ||||||
|     const basePath = `/projects/${projectId}`; |     const basePath = `/projects/${projectId}`; | ||||||
|     const projectName = project?.name || projectId; |     const projectName = project?.name || projectId; | ||||||
|     const tabData = [ | 
 | ||||||
|  |     const tabs = [ | ||||||
|         { |         { | ||||||
|             title: 'Overview', |             title: 'Overview', | ||||||
|             component: ( |  | ||||||
|                 <ProjectOverview |  | ||||||
|                     projectId={projectId} |  | ||||||
|                     projectName={projectName} |  | ||||||
|                 /> |  | ||||||
|             ), |  | ||||||
|             path: basePath, |             path: basePath, | ||||||
|             name: 'overview', |             name: 'overview', | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             title: 'Health', |             title: 'Health', | ||||||
|             component: ( |  | ||||||
|                 <ProjectHealth |  | ||||||
|                     projectId={projectId} |  | ||||||
|                     projectName={projectName} |  | ||||||
|                 /> |  | ||||||
|             ), |  | ||||||
|             path: `${basePath}/health`, |             path: `${basePath}/health`, | ||||||
|             name: 'health', |             name: 'health', | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             title: 'Access', |             title: 'Access', | ||||||
|             component: <ProjectAccess projectName={projectName} />, |  | ||||||
|             path: `${basePath}/access`, |             path: `${basePath}/access`, | ||||||
|             name: 'access', |             name: 'access', | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             title: 'Environments', |             title: 'Environments', | ||||||
|             component: ( |  | ||||||
|                 <ProjectEnvironment |  | ||||||
|                     projectId={projectId} |  | ||||||
|                     projectName={projectName} |  | ||||||
|                 /> |  | ||||||
|             ), |  | ||||||
|             path: `${basePath}/environments`, |             path: `${basePath}/environments`, | ||||||
|             name: 'environments', |             name: 'environments', | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             title: 'Archive', |             title: 'Archive', | ||||||
|             component: ( |  | ||||||
|                 <ProjectFeaturesArchive |  | ||||||
|                     projectId={projectId} |  | ||||||
|                     projectName={projectName} |  | ||||||
|                 /> |  | ||||||
|             ), |  | ||||||
|             path: `${basePath}/archive`, |             path: `${basePath}/archive`, | ||||||
|             name: 'archive', |             name: 'archive', | ||||||
|         }, |         }, | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     const activeTabIdx = activeTab |     const activeTab = [...tabs] | ||||||
|         ? tabData.findIndex(tab => tab.name === activeTab) |         .reverse() | ||||||
|         : 0; |         .find(tab => pathname.startsWith(tab.path)); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const created = params.get('created'); |         const created = params.get('created'); | ||||||
| @ -107,13 +81,13 @@ const Project = () => { | |||||||
|     }, []); |     }, []); | ||||||
| 
 | 
 | ||||||
|     const renderTabs = () => { |     const renderTabs = () => { | ||||||
|         return tabData.map((tab, index) => { |         return tabs.map(tab => { | ||||||
|             return ( |             return ( | ||||||
|                 <Tab |                 <Tab | ||||||
|  |                     data-loading | ||||||
|                     key={tab.title} |                     key={tab.title} | ||||||
|                     id={`tab-${index}`} |  | ||||||
|                     aria-controls={`tabpanel-${index}`} |  | ||||||
|                     label={tab.title} |                     label={tab.title} | ||||||
|  |                     value={tab.path} | ||||||
|                     onClick={() => navigate(tab.path)} |                     onClick={() => navigate(tab.path)} | ||||||
|                     className={styles.tabButton} |                     className={styles.tabButton} | ||||||
|                 /> |                 /> | ||||||
| @ -121,16 +95,6 @@ const Project = () => { | |||||||
|         }); |         }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const renderTabContent = () => { |  | ||||||
|         return tabData.map((tab, index) => { |  | ||||||
|             return ( |  | ||||||
|                 <TabPanel value={activeTabIdx} index={index} key={tab.path}> |  | ||||||
|                     {tab.component} |  | ||||||
|                 </TabPanel> |  | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|         <div ref={ref}> |         <div ref={ref}> | ||||||
|             <div className={styles.header}> |             <div className={styles.header}> | ||||||
| @ -167,7 +131,7 @@ const Project = () => { | |||||||
|                 <div className={styles.separator} /> |                 <div className={styles.separator} /> | ||||||
|                 <div className={styles.tabContainer}> |                 <div className={styles.tabContainer}> | ||||||
|                     <Tabs |                     <Tabs | ||||||
|                         value={activeTabIdx} |                         value={activeTab?.path} | ||||||
|                         indicatorColor="primary" |                         indicatorColor="primary" | ||||||
|                         textColor="primary" |                         textColor="primary" | ||||||
|                     > |                     > | ||||||
| @ -175,7 +139,13 @@ const Project = () => { | |||||||
|                     </Tabs> |                     </Tabs> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             {renderTabContent()} |             <Routes> | ||||||
|  |                 <Route path="health" element={<ProjectHealth />} /> | ||||||
|  |                 <Route path="access/*" element={<ProjectAccess />} /> | ||||||
|  |                 <Route path="environments" element={<ProjectEnvironment />} /> | ||||||
|  |                 <Route path="archive" element={<ProjectFeaturesArchive />} /> | ||||||
|  |                 <Route path="*" element={<ProjectOverview />} /> | ||||||
|  |             </Routes> | ||||||
|         </div> |         </div> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,15 +1,11 @@ | |||||||
| import { ProjectFeaturesArchiveTable } from 'component/archive/ProjectFeaturesArchiveTable'; | import { ProjectFeaturesArchiveTable } from 'component/archive/ProjectFeaturesArchiveTable'; | ||||||
| import { usePageTitle } from 'hooks/usePageTitle'; | import { usePageTitle } from 'hooks/usePageTitle'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||||
| 
 | 
 | ||||||
| interface IProjectFeaturesArchiveProps { | export const ProjectFeaturesArchive = () => { | ||||||
|     projectId: string; |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     projectName: string; |     const projectName = useProjectNameOrId(projectId); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const ProjectFeaturesArchive = ({ |  | ||||||
|     projectId, |  | ||||||
|     projectName, |  | ||||||
| }: IProjectFeaturesArchiveProps) => { |  | ||||||
|     usePageTitle(`Project archive – ${projectName}`); |     usePageTitle(`Project archive – ${projectName}`); | ||||||
| 
 | 
 | ||||||
|     return <ProjectFeaturesArchiveTable projectId={projectId} />; |     return <ProjectFeaturesArchiveTable projectId={projectId} />; | ||||||
|  | |||||||
| @ -4,13 +4,12 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit | |||||||
| import { usePageTitle } from 'hooks/usePageTitle'; | import { usePageTitle } from 'hooks/usePageTitle'; | ||||||
| import { ReportCard } from './ReportTable/ReportCard/ReportCard'; | import { ReportCard } from './ReportTable/ReportCard/ReportCard'; | ||||||
| import { ReportTable } from './ReportTable/ReportTable'; | import { ReportTable } from './ReportTable/ReportTable'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||||
| 
 | 
 | ||||||
| interface IProjectHealthProps { | const ProjectHealth = () => { | ||||||
|     projectId: string; |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     projectName: string; |     const projectName = useProjectNameOrId(projectId); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const ProjectHealth = ({ projectId, projectName }: IProjectHealthProps) => { |  | ||||||
|     usePageTitle(`Project health – ${projectName}`); |     usePageTitle(`Project health – ${projectName}`); | ||||||
| 
 | 
 | ||||||
|     const { healthReport, refetchHealthReport, error } = useHealthReport( |     const { healthReport, refetchHealthReport, error } = useHealthReport( | ||||||
|  | |||||||
| @ -1,18 +1,18 @@ | |||||||
| import useProject from 'hooks/api/getters/useProject/useProject'; | import useProject, { | ||||||
|  |     useProjectNameOrId, | ||||||
|  | } from 'hooks/api/getters/useProject/useProject'; | ||||||
| import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles'; | import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles'; | ||||||
| import ProjectInfo from './ProjectInfo/ProjectInfo'; | import ProjectInfo from './ProjectInfo/ProjectInfo'; | ||||||
| import { useStyles } from './Project.styles'; | import { useStyles } from './Project.styles'; | ||||||
| import { usePageTitle } from 'hooks/usePageTitle'; | import { usePageTitle } from 'hooks/usePageTitle'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| 
 | 
 | ||||||
| interface IProjectOverviewProps { | const refreshInterval = 15 * 1000; | ||||||
|     projectName: string; |  | ||||||
|     projectId: string; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| const ProjectOverview = ({ projectId, projectName }: IProjectOverviewProps) => { | const ProjectOverview = () => { | ||||||
|     const { project, loading } = useProject(projectId, { |     const projectId = useRequiredPathParam('projectId'); | ||||||
|         refreshInterval: 15 * 1000, // ms
 |     const projectName = useProjectNameOrId(projectId); | ||||||
|     }); |     const { project, loading } = useProject(projectId, { refreshInterval }); | ||||||
|     const { members, features, health, description, environments } = project; |     const { members, features, health, description, environments } = project; | ||||||
|     const { classes: styles } = useStyles(); |     const { classes: styles } = useStyles(); | ||||||
|     usePageTitle(`Project overview – ${projectName}`); |     usePageTitle(`Project overview – ${projectName}`); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { useContext, VFC } from 'react'; | import { useContext } from 'react'; | ||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
| import { Alert } from '@mui/material'; | import { Alert } from '@mui/material'; | ||||||
| @ -8,14 +8,11 @@ import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | |||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| import { usePageTitle } from 'hooks/usePageTitle'; | import { usePageTitle } from 'hooks/usePageTitle'; | ||||||
| import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable'; | import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable'; | ||||||
|  | import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||||
| 
 | 
 | ||||||
| interface IProjectAccess { | export const ProjectAccess = () => { | ||||||
|     projectName: string; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const ProjectAccess: VFC<IProjectAccess> = ({ projectName }) => { |  | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
| 
 |     const projectName = useProjectNameOrId(projectId); | ||||||
|     const { hasAccess } = useContext(AccessContext); |     const { hasAccess } = useContext(AccessContext); | ||||||
|     const { isOss } = useUiConfig(); |     const { isOss } = useUiConfig(); | ||||||
|     usePageTitle(`Project access – ${projectName}`); |     usePageTitle(`Project access – ${projectName}`); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import React, { FormEvent, useEffect, useMemo, useState } from 'react'; | import React, { FormEvent, useState } from 'react'; | ||||||
| import { | import { | ||||||
|     Autocomplete, |     Autocomplete, | ||||||
|     Button, |     Button, | ||||||
| @ -25,7 +25,8 @@ import { IUser } from 'interfaces/user'; | |||||||
| import { IGroup } from 'interfaces/group'; | import { IGroup } from 'interfaces/group'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { ProjectRoleDescription } from './ProjectRoleDescription/ProjectRoleDescription'; | import { ProjectRoleDescription } from './ProjectRoleDescription/ProjectRoleDescription'; | ||||||
| import { useAccess } from '../../../../hooks/api/getters/useAccess/useAccess'; | import { useNavigate } from 'react-router-dom'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const StyledForm = styled('form')(() => ({ | const StyledForm = styled('form')(() => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -88,96 +89,83 @@ interface IAccessOption { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface IProjectAccessAssignProps { | interface IProjectAccessAssignProps { | ||||||
|     open: boolean; |  | ||||||
|     setOpen: React.Dispatch<React.SetStateAction<boolean>>; |  | ||||||
|     selected?: IProjectAccess; |     selected?: IProjectAccess; | ||||||
|     accesses: IProjectAccess[]; |     accesses: IProjectAccess[]; | ||||||
|  |     users: IUser[]; | ||||||
|  |     groups: IGroup[]; | ||||||
|     roles: IProjectRole[]; |     roles: IProjectRole[]; | ||||||
|     entityType: string; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ProjectAccessAssign = ({ | export const ProjectAccessAssign = ({ | ||||||
|     open, |  | ||||||
|     setOpen, |  | ||||||
|     selected, |     selected, | ||||||
|     accesses, |     accesses, | ||||||
|  |     users, | ||||||
|  |     groups, | ||||||
|     roles, |     roles, | ||||||
|     entityType, |  | ||||||
| }: IProjectAccessAssignProps) => { | }: IProjectAccessAssignProps) => { | ||||||
|  |     const { uiConfig } = useUiConfig(); | ||||||
|  |     const { flags } = uiConfig; | ||||||
|  |     const entityType = flags.UG ? 'user / group' : 'user'; | ||||||
|  | 
 | ||||||
|     const projectId = useRequiredPathParam('projectId'); |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     const { refetchProjectAccess } = useProjectAccess(projectId); |     const { refetchProjectAccess } = useProjectAccess(projectId); | ||||||
|     const { addAccessToProject, changeUserRole, changeGroupRole, loading } = |     const { addAccessToProject, changeUserRole, changeGroupRole, loading } = | ||||||
|         useProjectApi(); |         useProjectApi(); | ||||||
|     const { users, groups } = useAccess(); |  | ||||||
|     const edit = Boolean(selected); |     const edit = Boolean(selected); | ||||||
| 
 | 
 | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
|     const { uiConfig } = useUiConfig(); |     const navigate = useNavigate(); | ||||||
| 
 | 
 | ||||||
|     const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>([]); |     const options = [ | ||||||
|  |         ...groups | ||||||
|  |             .filter( | ||||||
|  |                 (group: IGroup) => | ||||||
|  |                     edit || | ||||||
|  |                     !accesses.some( | ||||||
|  |                         ({ entity: { id }, type }) => | ||||||
|  |                             group.id === id && type === ENTITY_TYPE.GROUP | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |             .map((group: IGroup) => ({ | ||||||
|  |                 id: group.id, | ||||||
|  |                 entity: group, | ||||||
|  |                 type: ENTITY_TYPE.GROUP, | ||||||
|  |             })), | ||||||
|  |         ...users | ||||||
|  |             .filter( | ||||||
|  |                 (user: IUser) => | ||||||
|  |                     edit || | ||||||
|  |                     !accesses.some( | ||||||
|  |                         ({ entity: { id }, type }) => | ||||||
|  |                             user.id === id && type === ENTITY_TYPE.USER | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |             .map((user: IUser) => ({ | ||||||
|  |                 id: user.id, | ||||||
|  |                 entity: user, | ||||||
|  |                 type: ENTITY_TYPE.USER, | ||||||
|  |             })), | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>( | ||||||
|  |         () => | ||||||
|  |             options.filter( | ||||||
|  |                 ({ id, type }) => | ||||||
|  |                     id === selected?.entity.id && type === selected?.type | ||||||
|  |             ) | ||||||
|  |     ); | ||||||
|     const [role, setRole] = useState<IProjectRole | null>( |     const [role, setRole] = useState<IProjectRole | null>( | ||||||
|         roles.find(({ id }) => id === selected?.entity.roleId) ?? null |         roles.find(({ id }) => id === selected?.entity.roleId) ?? null | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     const payload = { | ||||||
|         setRole(roles.find(({ id }) => id === selected?.entity.roleId) ?? null); |         users: selectedOptions | ||||||
|     }, [roles, selected]); |             ?.filter(({ type }) => type === ENTITY_TYPE.USER) | ||||||
| 
 |             .map(({ id }) => ({ id })), | ||||||
|     const payload = useMemo( |         groups: selectedOptions | ||||||
|         () => ({ |             ?.filter(({ type }) => type === ENTITY_TYPE.GROUP) | ||||||
|             users: selectedOptions |             .map(({ id }) => ({ id })), | ||||||
|                 ?.filter(({ type }) => type === ENTITY_TYPE.USER) |     }; | ||||||
|                 .map(({ id }) => ({ id })), |  | ||||||
|             groups: selectedOptions |  | ||||||
|                 ?.filter(({ type }) => type === ENTITY_TYPE.GROUP) |  | ||||||
|                 .map(({ id }) => ({ id })), |  | ||||||
|         }), |  | ||||||
|         [selectedOptions] |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const options = useMemo( |  | ||||||
|         () => [ |  | ||||||
|             ...groups |  | ||||||
|                 .filter( |  | ||||||
|                     (group: IGroup) => |  | ||||||
|                         edit || |  | ||||||
|                         !accesses.some( |  | ||||||
|                             ({ entity: { id }, type }) => |  | ||||||
|                                 group.id === id && type === ENTITY_TYPE.GROUP |  | ||||||
|                         ) |  | ||||||
|                 ) |  | ||||||
|                 .map((group: IGroup) => ({ |  | ||||||
|                     id: group.id, |  | ||||||
|                     entity: group, |  | ||||||
|                     type: ENTITY_TYPE.GROUP, |  | ||||||
|                 })), |  | ||||||
|             ...users |  | ||||||
|                 .filter( |  | ||||||
|                     (user: IUser) => |  | ||||||
|                         edit || |  | ||||||
|                         !accesses.some( |  | ||||||
|                             ({ entity: { id }, type }) => |  | ||||||
|                                 user.id === id && type === ENTITY_TYPE.USER |  | ||||||
|                         ) |  | ||||||
|                 ) |  | ||||||
|                 .map((user: IUser) => ({ |  | ||||||
|                     id: user.id, |  | ||||||
|                     entity: user, |  | ||||||
|                     type: ENTITY_TYPE.USER, |  | ||||||
|                 })), |  | ||||||
|         ], |  | ||||||
|         [users, accesses, edit, groups] |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     useEffect(() => { |  | ||||||
|         const selectedOption = |  | ||||||
|             options.filter( |  | ||||||
|                 ({ id, type }) => |  | ||||||
|                     id === selected?.entity.id && type === selected?.type |  | ||||||
|             ) || []; |  | ||||||
|         setSelectedOptions(selectedOption); |  | ||||||
|         setRole(roles.find(({ id }) => id === selected?.entity.roleId) || null); |  | ||||||
|     }, [open, selected, options, roles]); |  | ||||||
| 
 | 
 | ||||||
|     const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { |     const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
| @ -193,7 +181,7 @@ export const ProjectAccessAssign = ({ | |||||||
|                 await changeGroupRole(projectId, role.id, selected.entity.id); |                 await changeGroupRole(projectId, role.id, selected.entity.id); | ||||||
|             } |             } | ||||||
|             refetchProjectAccess(); |             refetchProjectAccess(); | ||||||
|             setOpen(false); |             navigate(GO_BACK); | ||||||
|             setToastData({ |             setToastData({ | ||||||
|                 title: `${selectedOptions.length} ${ |                 title: `${selectedOptions.length} ${ | ||||||
|                     selectedOptions.length === 1 ? 'access' : 'accesses' |                     selectedOptions.length === 1 ? 'access' : 'accesses' | ||||||
| @ -277,10 +265,8 @@ export const ProjectAccessAssign = ({ | |||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         <SidebarModal |         <SidebarModal | ||||||
|             open={open} |             open | ||||||
|             onClose={() => { |             onClose={() => navigate(GO_BACK)} | ||||||
|                 setOpen(false); |  | ||||||
|             }} |  | ||||||
|             label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`} |             label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`} | ||||||
|         > |         > | ||||||
|             <FormTemplate |             <FormTemplate | ||||||
| @ -373,11 +359,7 @@ export const ProjectAccessAssign = ({ | |||||||
|                         > |                         > | ||||||
|                             Assign {entityType} |                             Assign {entityType} | ||||||
|                         </Button> |                         </Button> | ||||||
|                         <StyledCancelButton |                         <StyledCancelButton onClick={() => navigate(GO_BACK)}> | ||||||
|                             onClick={() => { |  | ||||||
|                                 setOpen(false); |  | ||||||
|                             }} |  | ||||||
|                         > |  | ||||||
|                             Cancel |                             Cancel | ||||||
|                         </StyledCancelButton> |                         </StyledCancelButton> | ||||||
|                     </StyledButtonContainer> |                     </StyledButtonContainer> | ||||||
|  | |||||||
| @ -0,0 +1,24 @@ | |||||||
|  | import { ProjectAccessAssign } from '../ProjectAccessAssign/ProjectAccessAssign'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import useProjectAccess from 'hooks/api/getters/useProjectAccess/useProjectAccess'; | ||||||
|  | import { useAccess } from 'hooks/api/getters/useAccess/useAccess'; | ||||||
|  | 
 | ||||||
|  | export const ProjectAccessCreate = () => { | ||||||
|  |     const projectId = useRequiredPathParam('projectId'); | ||||||
|  | 
 | ||||||
|  |     const { access } = useProjectAccess(projectId); | ||||||
|  |     const { users, groups } = useAccess(); | ||||||
|  | 
 | ||||||
|  |     if (!access || !users || !groups) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <ProjectAccessAssign | ||||||
|  |             accesses={access.rows} | ||||||
|  |             users={users} | ||||||
|  |             groups={groups} | ||||||
|  |             roles={access.roles} | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | import { ProjectAccessAssign } from '../ProjectAccessAssign/ProjectAccessAssign'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import useProjectAccess, { | ||||||
|  |     ENTITY_TYPE, | ||||||
|  | } from 'hooks/api/getters/useProjectAccess/useProjectAccess'; | ||||||
|  | import { useAccess } from 'hooks/api/getters/useAccess/useAccess'; | ||||||
|  | 
 | ||||||
|  | export const ProjectAccessEditGroup = () => { | ||||||
|  |     const projectId = useRequiredPathParam('projectId'); | ||||||
|  |     const groupId = useRequiredPathParam('groupId'); | ||||||
|  | 
 | ||||||
|  |     const { access } = useProjectAccess(projectId); | ||||||
|  |     const { users, groups } = useAccess(); | ||||||
|  | 
 | ||||||
|  |     if (!access || !users || !groups) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const group = access.rows.find( | ||||||
|  |         row => | ||||||
|  |             row.entity.id === Number(groupId) && row.type === ENTITY_TYPE.GROUP | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <ProjectAccessAssign | ||||||
|  |             accesses={access.rows} | ||||||
|  |             selected={group} | ||||||
|  |             users={users} | ||||||
|  |             groups={groups} | ||||||
|  |             roles={access.roles} | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,32 @@ | |||||||
|  | import { ProjectAccessAssign } from '../ProjectAccessAssign/ProjectAccessAssign'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import useProjectAccess, { | ||||||
|  |     ENTITY_TYPE, | ||||||
|  | } from 'hooks/api/getters/useProjectAccess/useProjectAccess'; | ||||||
|  | import { useAccess } from 'hooks/api/getters/useAccess/useAccess'; | ||||||
|  | 
 | ||||||
|  | export const ProjectAccessEditUser = () => { | ||||||
|  |     const projectId = useRequiredPathParam('projectId'); | ||||||
|  |     const userId = useRequiredPathParam('userId'); | ||||||
|  | 
 | ||||||
|  |     const { access } = useProjectAccess(projectId); | ||||||
|  |     const { users, groups } = useAccess(); | ||||||
|  | 
 | ||||||
|  |     if (!access || !users || !groups) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const user = access.rows.find( | ||||||
|  |         row => row.entity.id === Number(userId) && row.type === ENTITY_TYPE.USER | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <ProjectAccessAssign | ||||||
|  |             accesses={access.rows} | ||||||
|  |             selected={user} | ||||||
|  |             users={users} | ||||||
|  |             groups={groups} | ||||||
|  |             roles={access.roles} | ||||||
|  |         /> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -15,14 +15,19 @@ import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; | |||||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { useSearch } from 'hooks/useSearch'; | import { useSearch } from 'hooks/useSearch'; | ||||||
| import { useSearchParams } from 'react-router-dom'; | import { | ||||||
|  |     Link, | ||||||
|  |     Route, | ||||||
|  |     Routes, | ||||||
|  |     useNavigate, | ||||||
|  |     useSearchParams, | ||||||
|  | } from 'react-router-dom'; | ||||||
| import { createLocalStorage } from 'utils/createLocalStorage'; | import { createLocalStorage } from 'utils/createLocalStorage'; | ||||||
| import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | ||||||
| import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; | import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; | ||||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | import { PageContent } from 'component/common/PageContent/PageContent'; | ||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| import { Search } from 'component/common/Search/Search'; | import { Search } from 'component/common/Search/Search'; | ||||||
| import { ProjectAccessAssign } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign'; |  | ||||||
| import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | ||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||||
| @ -33,6 +38,9 @@ import { IUser } from 'interfaces/user'; | |||||||
| import { IGroup } from 'interfaces/group'; | import { IGroup } from 'interfaces/group'; | ||||||
| import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | ||||||
| import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; | import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; | ||||||
|  | import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate'; | ||||||
|  | import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser'; | ||||||
|  | import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup'; | ||||||
| 
 | 
 | ||||||
| export type PageQueryType = Partial< | export type PageQueryType = Partial< | ||||||
|     Record<'sort' | 'order' | 'search', string> |     Record<'sort' | 'order' | 'search', string> | ||||||
| @ -52,44 +60,17 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|     const { flags } = uiConfig; |     const { flags } = uiConfig; | ||||||
|     const entityType = flags.UG ? 'user / group' : 'user'; |     const entityType = flags.UG ? 'user / group' : 'user'; | ||||||
| 
 | 
 | ||||||
|  |     const navigate = useNavigate(); | ||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); |     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||||
|     const { setToastData } = useToast(); |     const { setToastData } = useToast(); | ||||||
| 
 | 
 | ||||||
|     const { access, refetchProjectAccess } = useProjectAccess(projectId); |     const { access, refetchProjectAccess } = useProjectAccess(projectId); | ||||||
|     const { removeUserFromRole, removeGroupFromRole } = useProjectApi(); |     const { removeUserFromRole, removeGroupFromRole } = useProjectApi(); | ||||||
|     const [assignOpen, setAssignOpen] = useState(false); |  | ||||||
|     const [removeOpen, setRemoveOpen] = useState(false); |     const [removeOpen, setRemoveOpen] = useState(false); | ||||||
|     const [groupOpen, setGroupOpen] = useState(false); |     const [groupOpen, setGroupOpen] = useState(false); | ||||||
|     const [selectedRow, setSelectedRow] = useState<IProjectAccess>(); |     const [selectedRow, setSelectedRow] = useState<IProjectAccess>(); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |  | ||||||
|         if (!assignOpen && !groupOpen) { |  | ||||||
|             setSelectedRow(undefined); |  | ||||||
|         } |  | ||||||
|     }, [assignOpen, groupOpen]); |  | ||||||
| 
 |  | ||||||
|     const roles = useMemo( |  | ||||||
|         () => access.roles || [], |  | ||||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps
 |  | ||||||
|         [JSON.stringify(access.roles)] |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const mappedData: IProjectAccess[] = useMemo(() => { |  | ||||||
|         const users = access.users || []; |  | ||||||
|         const groups = access.groups || []; |  | ||||||
|         return [ |  | ||||||
|             ...users.map(user => ({ |  | ||||||
|                 entity: user, |  | ||||||
|                 type: ENTITY_TYPE.USER, |  | ||||||
|             })), |  | ||||||
|             ...groups.map(group => ({ |  | ||||||
|                 entity: group, |  | ||||||
|                 type: ENTITY_TYPE.GROUP, |  | ||||||
|             })), |  | ||||||
|         ]; |  | ||||||
|     }, [access]); |  | ||||||
| 
 |  | ||||||
|     const columns = useMemo( |     const columns = useMemo( | ||||||
|         () => [ |         () => [ | ||||||
|             { |             { | ||||||
| @ -145,7 +126,8 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|             { |             { | ||||||
|                 Header: 'Role', |                 Header: 'Role', | ||||||
|                 accessor: (row: IProjectAccess) => |                 accessor: (row: IProjectAccess) => | ||||||
|                     roles.find(({ id }) => id === row.entity.roleId)?.name, |                     access?.roles.find(({ id }) => id === row.entity.roleId) | ||||||
|  |                         ?.name, | ||||||
|                 minWidth: 120, |                 minWidth: 120, | ||||||
|                 filterName: 'role', |                 filterName: 'role', | ||||||
|             }, |             }, | ||||||
| @ -187,19 +169,23 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|                 disableSortBy: true, |                 disableSortBy: true, | ||||||
|                 align: 'center', |                 align: 'center', | ||||||
|                 maxWidth: 200, |                 maxWidth: 200, | ||||||
|                 Cell: ({ row: { original: row } }: any) => ( |                 Cell: ({ | ||||||
|  |                     row: { original: row }, | ||||||
|  |                 }: { | ||||||
|  |                     row: { original: IProjectAccess }; | ||||||
|  |                 }) => ( | ||||||
|                     <ActionCell> |                     <ActionCell> | ||||||
|                         <PermissionIconButton |                         <PermissionIconButton | ||||||
|  |                             component={Link} | ||||||
|                             permission={UPDATE_PROJECT} |                             permission={UPDATE_PROJECT} | ||||||
|                             projectId={projectId} |                             projectId={projectId} | ||||||
|                             onClick={() => { |                             to={`edit/${ | ||||||
|                                 setSelectedRow(row); |                                 row.type === ENTITY_TYPE.USER ? 'user' : 'group' | ||||||
|                                 setAssignOpen(true); |                             }/${row.entity.id}`}
 | ||||||
|                             }} |                             disabled={access?.rows.length === 1} | ||||||
|                             disabled={mappedData.length === 1} |  | ||||||
|                             tooltipProps={{ |                             tooltipProps={{ | ||||||
|                                 title: |                                 title: | ||||||
|                                     mappedData.length === 1 |                                     access?.rows.length === 1 | ||||||
|                                         ? 'Cannot edit access. A project must have at least one owner' |                                         ? 'Cannot edit access. A project must have at least one owner' | ||||||
|                                         : 'Edit access', |                                         : 'Edit access', | ||||||
|                             }} |                             }} | ||||||
| @ -213,10 +199,10 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|                                 setSelectedRow(row); |                                 setSelectedRow(row); | ||||||
|                                 setRemoveOpen(true); |                                 setRemoveOpen(true); | ||||||
|                             }} |                             }} | ||||||
|                             disabled={mappedData.length === 1} |                             disabled={access?.rows.length === 1} | ||||||
|                             tooltipProps={{ |                             tooltipProps={{ | ||||||
|                                 title: |                                 title: | ||||||
|                                     mappedData.length === 1 |                                     access?.rows.length === 1 | ||||||
|                                         ? 'Cannot remove access. A project must have at least one owner' |                                         ? 'Cannot remove access. A project must have at least one owner' | ||||||
|                                         : 'Remove access', |                                         : 'Remove access', | ||||||
|                             }} |                             }} | ||||||
| @ -227,7 +213,7 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|                 ), |                 ), | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|         [roles, mappedData.length, projectId] |         [access, projectId] | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const [searchParams, setSearchParams] = useSearchParams(); |     const [searchParams, setSearchParams] = useSearchParams(); | ||||||
| @ -247,7 +233,7 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|     const { data, getSearchText, getSearchContext } = useSearch( |     const { data, getSearchText, getSearchContext } = useSearch( | ||||||
|         columns, |         columns, | ||||||
|         searchValue, |         searchValue, | ||||||
|         mappedData ?? [] |         access?.rows ?? [] | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const { |     const { | ||||||
| @ -319,7 +305,6 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|         setRemoveOpen(false); |         setRemoveOpen(false); | ||||||
|         setSelectedRow(undefined); |  | ||||||
|     }; |     }; | ||||||
|     return ( |     return ( | ||||||
|         <PageContent |         <PageContent | ||||||
| @ -348,9 +333,10 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|                                 } |                                 } | ||||||
|                             /> |                             /> | ||||||
|                             <Button |                             <Button | ||||||
|  |                                 component={Link} | ||||||
|  |                                 to={`create`} | ||||||
|                                 variant="contained" |                                 variant="contained" | ||||||
|                                 color="primary" |                                 color="primary" | ||||||
|                                 onClick={() => setAssignOpen(true)} |  | ||||||
|                             > |                             > | ||||||
|                                 Assign {entityType} |                                 Assign {entityType} | ||||||
|                             </Button> |                             </Button> | ||||||
| @ -399,19 +385,21 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|                     /> |                     /> | ||||||
|                 } |                 } | ||||||
|             /> |             /> | ||||||
|             <ProjectAccessAssign |             <Routes> | ||||||
|                 open={assignOpen} |                 <Route path="create" element={<ProjectAccessCreate />} /> | ||||||
|                 setOpen={setAssignOpen} |                 <Route | ||||||
|                 selected={selectedRow} |                     path="edit/group/:groupId" | ||||||
|                 accesses={mappedData} |                     element={<ProjectAccessEditGroup />} | ||||||
|                 roles={roles} |                 /> | ||||||
|                 entityType={entityType} |                 <Route | ||||||
|             /> |                     path="edit/user/:userId" | ||||||
|  |                     element={<ProjectAccessEditUser />} | ||||||
|  |                 /> | ||||||
|  |             </Routes> | ||||||
|             <Dialogue |             <Dialogue | ||||||
|                 open={removeOpen} |                 open={removeOpen} | ||||||
|                 onClick={() => removeAccess(selectedRow)} |                 onClick={() => removeAccess(selectedRow)} | ||||||
|                 onClose={() => { |                 onClose={() => { | ||||||
|                     setSelectedRow(undefined); |  | ||||||
|                     setRemoveOpen(false); |                     setRemoveOpen(false); | ||||||
|                 }} |                 }} | ||||||
|                 title={`Really remove ${entityType} from this project?`} |                 title={`Really remove ${entityType} from this project?`} | ||||||
| @ -422,12 +410,12 @@ export const ProjectAccessTable: VFC = () => { | |||||||
|                 group={selectedRow?.entity as IGroup} |                 group={selectedRow?.entity as IGroup} | ||||||
|                 projectId={projectId} |                 projectId={projectId} | ||||||
|                 subtitle={`Role: ${ |                 subtitle={`Role: ${ | ||||||
|                     roles.find(({ id }) => id === selectedRow?.entity.roleId) |                     access?.roles.find( | ||||||
|                         ?.name |                         ({ id }) => id === selectedRow?.entity.roleId | ||||||
|  |                     )?.name | ||||||
|                 }`}
 |                 }`}
 | ||||||
|                 onEdit={() => { |                 onEdit={() => { | ||||||
|                     setAssignOpen(true); |                     navigate(`edit/group/${selectedRow?.entity.id}`); | ||||||
|                     console.log('Assign Open true'); |  | ||||||
|                 }} |                 }} | ||||||
|                 onRemove={() => { |                 onRemove={() => { | ||||||
|                     setGroupOpen(false); |                     setGroupOpen(false); | ||||||
|  | |||||||
| @ -8,7 +8,9 @@ import ApiError from 'component/common/ApiError/ApiError'; | |||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||||
| import useProject from 'hooks/api/getters/useProject/useProject'; | import useProject, { | ||||||
|  |     useProjectNameOrId, | ||||||
|  | } from 'hooks/api/getters/useProject/useProject'; | ||||||
| import { FormControlLabel, FormGroup, Alert } from '@mui/material'; | import { FormControlLabel, FormGroup, Alert } from '@mui/material'; | ||||||
| import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | ||||||
| import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDisableConfirm'; | import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDisableConfirm'; | ||||||
| @ -19,17 +21,13 @@ import { getEnabledEnvs } from './helpers'; | |||||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||||
| import { useThemeStyles } from 'themes/themeStyles'; | import { useThemeStyles } from 'themes/themeStyles'; | ||||||
| import { usePageTitle } from 'hooks/usePageTitle'; | import { usePageTitle } from 'hooks/usePageTitle'; | ||||||
|  | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| 
 | 
 | ||||||
| interface IProjectEnvironmentListProps { | const ProjectEnvironmentList = () => { | ||||||
|     projectId: string; |     const projectId = useRequiredPathParam('projectId'); | ||||||
|     projectName: string; |     const projectName = useProjectNameOrId(projectId); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const ProjectEnvironmentList = ({ |  | ||||||
|     projectId, |  | ||||||
|     projectName, |  | ||||||
| }: IProjectEnvironmentListProps) => { |  | ||||||
|     usePageTitle(`Project environments – ${projectName}`); |     usePageTitle(`Project environments – ${projectName}`); | ||||||
|  | 
 | ||||||
|     // api state
 |     // api state
 | ||||||
|     const [envs, setEnvs] = useState<IProjectEnvironment[]>([]); |     const [envs, setEnvs] = useState<IProjectEnvironment[]>([]); | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesAp | |||||||
| import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; | import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| export const CreateStrategy = () => { | export const CreateStrategy = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -64,7 +65,7 @@ export const CreateStrategy = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import { formatUnknownError } from 'utils/formatUnknownError'; | |||||||
| import useStrategy from 'hooks/api/getters/useStrategy/useStrategy'; | import useStrategy from 'hooks/api/getters/useStrategy/useStrategy'; | ||||||
| import { UpdateButton } from 'component/common/UpdateButton/UpdateButton'; | import { UpdateButton } from 'component/common/UpdateButton/UpdateButton'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| export const EditStrategy = () => { | export const EditStrategy = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -68,7 +69,7 @@ export const EditStrategy = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import useTagTypesApi from 'hooks/api/actions/useTagTypesApi/useTagTypesApi'; | |||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const CreateTagType = () => { | const CreateTagType = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -55,7 +56,7 @@ const CreateTagType = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ import useToast from 'hooks/useToast'; | |||||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
|  | import { GO_BACK } from 'constants/navigate'; | ||||||
| 
 | 
 | ||||||
| const EditTagType = () => { | const EditTagType = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -54,7 +55,7 @@ const EditTagType = () => { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const handleCancel = () => { |     const handleCancel = () => { | ||||||
|         navigate(-1); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								frontend/src/constants/navigate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/constants/navigate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export const GO_BACK = -1; | ||||||
| @ -1,24 +1,30 @@ | |||||||
| import useSWR from 'swr'; | import useSWR from 'swr'; | ||||||
| import { useMemo } from 'react'; |  | ||||||
| import { formatApiPath } from 'utils/formatPath'; | import { formatApiPath } from 'utils/formatPath'; | ||||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | import handleErrorResponses from '../httpErrorResponseHandler'; | ||||||
|  | import { IGroup } from 'interfaces/group'; | ||||||
|  | import { IUser } from 'interfaces/user'; | ||||||
| 
 | 
 | ||||||
| export const useAccess = () => { | export interface IUseAccessOutput { | ||||||
|  |     users?: IUser[]; | ||||||
|  |     groups?: IGroup[]; | ||||||
|  |     loading: boolean; | ||||||
|  |     refetch: () => void; | ||||||
|  |     error?: Error; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const useAccess = (): IUseAccessOutput => { | ||||||
|     const { data, error, mutate } = useSWR( |     const { data, error, mutate } = useSWR( | ||||||
|         formatApiPath(`api/admin/user-admin/access`), |         formatApiPath(`api/admin/user-admin/access`), | ||||||
|         fetcher |         fetcher | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     return useMemo( |     return { | ||||||
|         () => ({ |         users: data?.users, | ||||||
|             users: data?.users ?? [], |         groups: data?.groups, | ||||||
|             groups: data?.groups ?? [], |         loading: !error && !data, | ||||||
|             loading: !error && !data, |         refetch: () => mutate(), | ||||||
|             refetch: () => mutate(), |         error, | ||||||
|             error, |     }; | ||||||
|         }), |  | ||||||
|         [data, error, mutate] |  | ||||||
|     ); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const fetcher = (path: string) => { | const fetcher = (path: string) => { | ||||||
|  | |||||||
| @ -29,4 +29,8 @@ const useProject = (id: string, options: SWRConfiguration = {}) => { | |||||||
|     }; |     }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const useProjectNameOrId = (id: string): string => { | ||||||
|  |     return useProject(id).project.name || id; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export default useProject; | export default useProject; | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import useSWR, { mutate, SWRConfiguration } from 'swr'; | import useSWR, { mutate, SWRConfiguration } from 'swr'; | ||||||
| import { useState, useEffect } from 'react'; | import { useState, useEffect, useMemo } from 'react'; | ||||||
| import { formatApiPath } from 'utils/formatPath'; | import { formatApiPath } from 'utils/formatPath'; | ||||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | import handleErrorResponses from '../httpErrorResponseHandler'; | ||||||
| import { IProjectRole } from 'interfaces/role'; | import { IProjectRole } from 'interfaces/role'; | ||||||
| @ -29,6 +29,7 @@ export interface IProjectAccessOutput { | |||||||
|     users: IProjectAccessUser[]; |     users: IProjectAccessUser[]; | ||||||
|     groups: IProjectAccessGroup[]; |     groups: IProjectAccessGroup[]; | ||||||
|     roles: IProjectRole[]; |     roles: IProjectRole[]; | ||||||
|  |     rows: IProjectAccess[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const useProjectAccess = ( | const useProjectAccess = ( | ||||||
| @ -58,23 +59,44 @@ const useProjectAccess = ( | |||||||
|         setLoading(!error && !data); |         setLoading(!error && !data); | ||||||
|     }, [data, error]); |     }, [data, error]); | ||||||
| 
 | 
 | ||||||
|     let access: IProjectAccessOutput = data |     const access: IProjectAccessOutput | undefined = useMemo(() => { | ||||||
|         ? { |         if (data) { | ||||||
|               roles: data.roles, |             return formatAccessData({ | ||||||
|               users: data.users, |                 roles: data.roles, | ||||||
|               groups: |                 users: data.users, | ||||||
|                   data?.groups.map((group: any) => ({ |                 groups: | ||||||
|                       ...group, |                     data?.groups.map((group: any) => ({ | ||||||
|                       users: mapGroupUsers(group.users ?? []), |                         ...group, | ||||||
|                   })) ?? [], |                         users: mapGroupUsers(group.users ?? []), | ||||||
|           } |                     })) ?? [], | ||||||
|         : { roles: [], users: [], groups: [] }; |             }); | ||||||
|  |         } | ||||||
|  |     }, [data]); | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|         access: access, |         access, | ||||||
|         error, |         error, | ||||||
|         loading, |         loading, | ||||||
|         refetchProjectAccess, |         refetchProjectAccess, | ||||||
|     }; |     }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const formatAccessData = (access: any): IProjectAccessOutput => { | ||||||
|  |     const users = access.users || []; | ||||||
|  |     const groups = access.groups || []; | ||||||
|  |     return { | ||||||
|  |         ...access, | ||||||
|  |         rows: [ | ||||||
|  |             ...users.map((user: any) => ({ | ||||||
|  |                 entity: user, | ||||||
|  |                 type: ENTITY_TYPE.USER, | ||||||
|  |             })), | ||||||
|  |             ...groups.map((group: any) => ({ | ||||||
|  |                 entity: group, | ||||||
|  |                 type: ENTITY_TYPE.GROUP, | ||||||
|  |             })), | ||||||
|  |         ], | ||||||
|  |     }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export default useProjectAccess; | export default useProjectAccess; | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| /* NAVIGATION */ | /* NAVIGATION */ | ||||||
| export const NAVIGATE_TO_CREATE_FEATURE = 'NAVIGATE_TO_CREATE_FEATURE'; | export const NAVIGATE_TO_CREATE_FEATURE = 'NAVIGATE_TO_CREATE_FEATURE'; | ||||||
|  | export const NAVIGATE_TO_CREATE_GROUP = 'NAVIGATE_TO_CREATE_GROUP'; | ||||||
| export const NAVIGATE_TO_CREATE_SEGMENT = 'NAVIGATE_TO_CREATE_SEGMENT'; | export const NAVIGATE_TO_CREATE_SEGMENT = 'NAVIGATE_TO_CREATE_SEGMENT'; | ||||||
| export const CREATE_API_TOKEN_BUTTON = 'CREATE_API_TOKEN_BUTTON'; | export const CREATE_API_TOKEN_BUTTON = 'CREATE_API_TOKEN_BUTTON'; | ||||||
| 
 | 
 | ||||||
| @ -12,7 +13,17 @@ export const CF_CREATE_BTN_ID = 'CF_CREATE_BTN_ID'; | |||||||
| /* CREATE GROUP */ | /* CREATE GROUP */ | ||||||
| export const UG_NAME_ID = 'UG_NAME_ID'; | export const UG_NAME_ID = 'UG_NAME_ID'; | ||||||
| export const UG_DESC_ID = 'UG_DESC_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_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_REMOVE_USER_BTN_ID = 'UG_REMOVE_USER_BTN_ID'; | ||||||
|  | export const UG_USERS_ROLE_ID = 'UG_USERS_ROLE_ID'; | ||||||
| 
 | 
 | ||||||
| /* SEGMENT */ | /* SEGMENT */ | ||||||
| export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID'; | export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID'; | ||||||
| @ -55,3 +66,4 @@ export const SIDEBAR_MODAL_ID = 'SIDEBAR_MODAL_ID'; | |||||||
| export const AUTH_PAGE_ID = 'AUTH_PAGE_ID'; | export const AUTH_PAGE_ID = 'AUTH_PAGE_ID'; | ||||||
| export const ANNOUNCER_ELEMENT_TEST_ID = 'ANNOUNCER_ELEMENT_TEST_ID'; | export const ANNOUNCER_ELEMENT_TEST_ID = 'ANNOUNCER_ELEMENT_TEST_ID'; | ||||||
| export const INSTANCE_STATUS_BAR_ID = 'INSTANCE_STATUS_BAR_ID'; | export const INSTANCE_STATUS_BAR_ID = 'INSTANCE_STATUS_BAR_ID'; | ||||||
|  | export const TOAST_TEXT = 'TOAST_TEXT'; | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user