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, | ||||
| } from './AddonForm.styles'; | ||||
| import { useTheme } from '@mui/system'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| interface IAddonFormProps { | ||||
|     provider?: IAddonProvider; | ||||
| @ -168,7 +169,7 @@ export const AddonForm: VFC<IAddonFormProps> = ({ | ||||
|     }; | ||||
| 
 | ||||
|     const onCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     const onSubmit: FormEventHandler<HTMLFormElement> = async event => { | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { useState } from 'react'; | ||||
| import { scrollToTop } from 'component/common/util'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const pageTitle = 'Create API token'; | ||||
| 
 | ||||
| @ -75,7 +76,7 @@ export const CreateApiToken = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { UG_CREATE_BTN_ID } from 'utils/testIds'; | ||||
| import { Button } from '@mui/material'; | ||||
| import { CREATE } from 'constants/misc'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| export const CreateGroup = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -58,7 +59,7 @@ export const CreateGroup = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -10,6 +10,8 @@ import { Button } from '@mui/material'; | ||||
| import { EDIT } from 'constants/misc'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| 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 = () => { | ||||
|     const groupId = Number(useRequiredPathParam('groupId')); | ||||
| @ -40,7 +42,7 @@ export const EditGroup = () => { | ||||
|         try { | ||||
|             await updateGroup(groupId, payload); | ||||
|             refetchGroup(); | ||||
|             navigate(-1); | ||||
|             navigate(GO_BACK); | ||||
|             setToastData({ | ||||
|                 title: 'Group updated successfully', | ||||
|                 type: 'success', | ||||
| @ -60,7 +62,7 @@ export const EditGroup = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
| @ -85,7 +87,12 @@ export const EditGroup = () => { | ||||
|                 mode={EDIT} | ||||
|                 clearErrors={clearErrors} | ||||
|             > | ||||
|                 <Button type="submit" variant="contained" color="primary"> | ||||
|                 <Button | ||||
|                     type="submit" | ||||
|                     variant="contained" | ||||
|                     color="primary" | ||||
|                     data-testid={UG_SAVE_BTN_ID} | ||||
|                 > | ||||
|                     Save | ||||
|                 </Button> | ||||
|             </GroupForm> | ||||
|  | ||||
| @ -10,6 +10,7 @@ import { FC, FormEvent, useEffect, useMemo, useState } from 'react'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect'; | ||||
| import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable'; | ||||
| import { UG_SAVE_BTN_ID } from 'utils/testIds'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
| @ -142,6 +143,7 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({ | ||||
|                             type="submit" | ||||
|                             variant="contained" | ||||
|                             color="primary" | ||||
|                             data-testid={UG_SAVE_BTN_ID} | ||||
|                         > | ||||
|                             Save | ||||
|                         </Button> | ||||
|  | ||||
| @ -8,6 +8,7 @@ import useToast from 'hooks/useToast'; | ||||
| import { IGroup, IGroupUser, Role } from 'interfaces/group'; | ||||
| import { FC, FormEvent, useEffect, useMemo, useState } from 'react'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { UG_SAVE_BTN_ID, UG_USERS_ROLE_ID } from 'utils/testIds'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
| @ -143,6 +144,7 @@ export const EditGroupUser: FC<IEditGroupUserProps> = ({ | ||||
|                             Assign the role the user should have in this group | ||||
|                         </StyledInputDescription> | ||||
|                         <StyledSelect | ||||
|                             data-testid={UG_USERS_ROLE_ID} | ||||
|                             size="small" | ||||
|                             value={role} | ||||
|                             onChange={event => | ||||
| @ -159,6 +161,7 @@ export const EditGroupUser: FC<IEditGroupUserProps> = ({ | ||||
| 
 | ||||
|                     <StyledButtonContainer> | ||||
|                         <Button | ||||
|                             data-testid={UG_SAVE_BTN_ID} | ||||
|                             type="submit" | ||||
|                             variant="contained" | ||||
|                             color="primary" | ||||
|  | ||||
| @ -37,6 +37,13 @@ import { AddGroupUser } from './AddGroupUser/AddGroupUser'; | ||||
| import { EditGroupUser } from './EditGroupUser/EditGroupUser'; | ||||
| import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser'; | ||||
| 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 }) => ({ | ||||
|     fontSize: theme.fontSizes.mainHeader, | ||||
| @ -134,6 +141,7 @@ export const Group: VFC = () => { | ||||
|                     <ActionCell> | ||||
|                         <Tooltip title="Edit user" arrow describeChild> | ||||
|                             <IconButton | ||||
|                                 data-testid={`${UG_EDIT_USER_BTN_ID}-${rowUser.id}`} | ||||
|                                 onClick={() => { | ||||
|                                     setSelectedUser(rowUser); | ||||
|                                     setEditUserOpen(true); | ||||
| @ -148,6 +156,7 @@ export const Group: VFC = () => { | ||||
|                             describeChild | ||||
|                         > | ||||
|                             <IconButton | ||||
|                                 data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`} | ||||
|                                 onClick={() => { | ||||
|                                     setSelectedUser(rowUser); | ||||
|                                     setRemoveUserOpen(true); | ||||
| @ -240,6 +249,7 @@ export const Group: VFC = () => { | ||||
|                         actions={ | ||||
|                             <> | ||||
|                                 <PermissionIconButton | ||||
|                                     data-testid={UG_EDIT_BTN_ID} | ||||
|                                     to={`/admin/groups/${groupId}/edit`} | ||||
|                                     component={Link} | ||||
|                                     data-loading | ||||
| @ -251,6 +261,7 @@ export const Group: VFC = () => { | ||||
|                                     <StyledEdit /> | ||||
|                                 </PermissionIconButton> | ||||
|                                 <PermissionIconButton | ||||
|                                     data-testid={UG_DELETE_BTN_ID} | ||||
|                                     data-loading | ||||
|                                     onClick={() => setRemoveOpen(true)} | ||||
|                                     permission={ADMIN} | ||||
| @ -296,6 +307,7 @@ export const Group: VFC = () => { | ||||
|                                             } | ||||
|                                         /> | ||||
|                                         <Button | ||||
|                                             data-testid={UG_ADD_USER_BTN_ID} | ||||
|                                             variant="contained" | ||||
|                                             color="primary" | ||||
|                                             onClick={() => { | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { IUser } from 'interfaces/user'; | ||||
| import { useMemo, useState, VFC } from 'react'; | ||||
| import { useUsers } from 'hooks/api/getters/useUsers/useUsers'; | ||||
| import { IGroupUser, Role } from 'interfaces/group'; | ||||
| import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds'; | ||||
| 
 | ||||
| const StyledOption = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
| @ -83,6 +84,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({ | ||||
|     return ( | ||||
|         <StyledGroupFormUsersSelect> | ||||
|             <Autocomplete | ||||
|                 data-testid={UG_USERS_ID} | ||||
|                 size="small" | ||||
|                 multiple | ||||
|                 limitTags={10} | ||||
| @ -113,7 +115,11 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({ | ||||
|                     <TextField {...params} label="Select users" /> | ||||
|                 )} | ||||
|             /> | ||||
|             <Button variant="outlined" onClick={onAdd}> | ||||
|             <Button | ||||
|                 variant="outlined" | ||||
|                 onClick={onAdd} | ||||
|                 data-testid={UG_USERS_ADD_ID} | ||||
|             > | ||||
|                 Add | ||||
|             </Button> | ||||
|         </StyledGroupFormUsersSelect> | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import { Role } from 'interfaces/group'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { StarRounded } from '@mui/icons-material'; | ||||
| import { UG_USERS_TABLE_ROLE_ID } from 'utils/testIds'; | ||||
| 
 | ||||
| const StyledPopupStar = styled(StarRounded)(({ theme }) => ({ | ||||
|     color: theme.palette.warning.main, | ||||
| @ -36,6 +37,7 @@ export const GroupUserRoleCell = ({ | ||||
|                 condition={Boolean(onChange)} | ||||
|                 show={ | ||||
|                     <Select | ||||
|                         data-testid={UG_USERS_TABLE_ROLE_ID} | ||||
|                         size="small" | ||||
|                         value={value} | ||||
|                         onChange={event => | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC | ||||
| import { TablePlaceholder } from 'component/common/Table'; | ||||
| import { GroupCard } from './GroupCard/GroupCard'; | ||||
| import { GroupEmpty } from './GroupEmpty/GroupEmpty'; | ||||
| import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds'; | ||||
| 
 | ||||
| type PageQueryType = Partial<Record<'search', string>>; | ||||
| 
 | ||||
| @ -85,6 +86,7 @@ export const GroupsList: VFC = () => { | ||||
|                                 component={Link} | ||||
|                                 variant="contained" | ||||
|                                 color="primary" | ||||
|                                 data-testid={NAVIGATE_TO_CREATE_GROUP} | ||||
|                             > | ||||
|                                 New group | ||||
|                             </Button> | ||||
|  | ||||
| @ -8,6 +8,7 @@ import useToast from 'hooks/useToast'; | ||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const CreateProjectRole = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -66,7 +67,7 @@ const CreateProjectRole = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -12,6 +12,7 @@ import useProjectRoleForm from '../hooks/useProjectRoleForm'; | ||||
| import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const EditProjectRole = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
| @ -94,7 +95,7 @@ const EditProjectRole = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { scrollToTop } from 'component/common/util'; | ||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const CreateUser = () => { | ||||
|     const { setToastApiError } = useToast(); | ||||
| @ -72,7 +73,7 @@ const CreateUser = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -13,6 +13,7 @@ import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const EditUser = () => { | ||||
|     useEffect(() => { | ||||
| @ -69,7 +70,7 @@ const EditUser = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { useNavigate } from 'react-router'; | ||||
| import { ReactComponent as LogoIcon } from 'assets/icons/logoBg.svg'; | ||||
| 
 | ||||
| import { useStyles } from './NotFound.styles'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const NotFound = () => { | ||||
|     const navigate = useNavigate(); | ||||
| @ -14,7 +15,7 @@ const NotFound = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const onClickBack = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -7,6 +7,7 @@ import UIContext from 'contexts/UIContext'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import Close from '@mui/icons-material/Close'; | ||||
| import { IToast } from 'interfaces/toast'; | ||||
| import { TOAST_TEXT } from 'utils/testIds'; | ||||
| 
 | ||||
| const Toast = ({ title, text, type, confetti }: IToast) => { | ||||
|     const { setToast } = useContext(UIContext); | ||||
| @ -72,7 +73,9 @@ const Toast = ({ title, text, type, confetti }: IToast) => { | ||||
| 
 | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={Boolean(text)} | ||||
|                                     show={<p>{text}</p>} | ||||
|                                     show={ | ||||
|                                         <p data-testid={TOAST_TEXT}>{text}</p> | ||||
|                                     } | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { CreateUnleashContext } from 'component/context/CreateUnleashContext/CreateUnleashContext'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| export const CreateUnleashContextPage = () => { | ||||
|     const navigate = useNavigate(); | ||||
| @ -7,7 +8,7 @@ export const CreateUnleashContextPage = () => { | ||||
|     return ( | ||||
|         <CreateUnleashContext | ||||
|             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 { useContextForm } from '../hooks/useContextForm'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| export const EditContext = () => { | ||||
|     useEffect(() => { | ||||
| @ -71,7 +72,7 @@ export const EditContext = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const onCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -15,6 +15,7 @@ import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const CreateEnvironment = () => { | ||||
|     const { setToastApiError, setToastData } = useToast(); | ||||
| @ -66,7 +67,7 @@ const CreateEnvironment = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -11,6 +11,7 @@ import EnvironmentForm from '../EnvironmentForm/EnvironmentForm'; | ||||
| import useEnvironmentForm from '../hooks/useEnvironmentForm'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const EditEnvironment = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
| @ -56,7 +57,7 @@ const EditEnvironment = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||
| import UIContext from 'contexts/UIContext'; | ||||
| import { CF_CREATE_BTN_ID } from 'utils/testIds'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const CreateFeature = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -70,7 +71,7 @@ const CreateFeature = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -11,6 +11,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const EditFeature = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
| @ -74,7 +75,7 @@ const EditFeature = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -80,16 +80,7 @@ exports[`returns all baseRoutes 1`] = ` | ||||
|     "flag": "P", | ||||
|     "menu": {}, | ||||
|     "parent": "/projects", | ||||
|     "path": "/projects/:projectId/:activeTab", | ||||
|     "title": ":projectId", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "flag": "P", | ||||
|     "menu": {}, | ||||
|     "parent": "/projects", | ||||
|     "path": "/projects/:projectId", | ||||
|     "path": "/projects/:projectId/*", | ||||
|     "title": ":projectId", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|  | ||||
| @ -136,16 +136,7 @@ export const routes: IRoute[] = [ | ||||
|         menu: {}, | ||||
|     }, | ||||
|     { | ||||
|         path: '/projects/:projectId/:activeTab', | ||||
|         parent: '/projects', | ||||
|         title: ':projectId', | ||||
|         component: Project, | ||||
|         flag: P, | ||||
|         type: 'protected', | ||||
|         menu: {}, | ||||
|     }, | ||||
|     { | ||||
|         path: '/projects/:projectId', | ||||
|         path: '/projects/:projectId/*', | ||||
|         parent: '/projects', | ||||
|         title: ':projectId', | ||||
|         component: Project, | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const CreateProject = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -65,7 +66,7 @@ const CreateProject = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -13,6 +13,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useContext } from 'react'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { Alert } from '@mui/material'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const EditProject = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
| @ -70,7 +71,7 @@ const EditProject = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && ( | ||||
|  | ||||
| @ -16,80 +16,54 @@ import ProjectOverview from './ProjectOverview'; | ||||
| import ProjectHealth from './ProjectHealth/ProjectHealth'; | ||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||
| import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||
| import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { Routes, Route, useLocation } from 'react-router-dom'; | ||||
| 
 | ||||
| const Project = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const activeTab = useOptionalPathParam('activeTab'); | ||||
|     const params = useQueryParams(); | ||||
|     const { project, error, loading, refetch } = useProject(projectId); | ||||
|     const ref = useLoading(loading); | ||||
|     const { setToastData } = useToast(); | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const navigate = useNavigate(); | ||||
|     const { pathname } = useLocation(); | ||||
|     const { isOss } = useUiConfig(); | ||||
| 
 | ||||
|     const basePath = `/projects/${projectId}`; | ||||
|     const projectName = project?.name || projectId; | ||||
|     const tabData = [ | ||||
| 
 | ||||
|     const tabs = [ | ||||
|         { | ||||
|             title: 'Overview', | ||||
|             component: ( | ||||
|                 <ProjectOverview | ||||
|                     projectId={projectId} | ||||
|                     projectName={projectName} | ||||
|                 /> | ||||
|             ), | ||||
|             path: basePath, | ||||
|             name: 'overview', | ||||
|         }, | ||||
|         { | ||||
|             title: 'Health', | ||||
|             component: ( | ||||
|                 <ProjectHealth | ||||
|                     projectId={projectId} | ||||
|                     projectName={projectName} | ||||
|                 /> | ||||
|             ), | ||||
|             path: `${basePath}/health`, | ||||
|             name: 'health', | ||||
|         }, | ||||
|         { | ||||
|             title: 'Access', | ||||
|             component: <ProjectAccess projectName={projectName} />, | ||||
|             path: `${basePath}/access`, | ||||
|             name: 'access', | ||||
|         }, | ||||
|         { | ||||
|             title: 'Environments', | ||||
|             component: ( | ||||
|                 <ProjectEnvironment | ||||
|                     projectId={projectId} | ||||
|                     projectName={projectName} | ||||
|                 /> | ||||
|             ), | ||||
|             path: `${basePath}/environments`, | ||||
|             name: 'environments', | ||||
|         }, | ||||
|         { | ||||
|             title: 'Archive', | ||||
|             component: ( | ||||
|                 <ProjectFeaturesArchive | ||||
|                     projectId={projectId} | ||||
|                     projectName={projectName} | ||||
|                 /> | ||||
|             ), | ||||
|             path: `${basePath}/archive`, | ||||
|             name: 'archive', | ||||
|         }, | ||||
|     ]; | ||||
| 
 | ||||
|     const activeTabIdx = activeTab | ||||
|         ? tabData.findIndex(tab => tab.name === activeTab) | ||||
|         : 0; | ||||
|     const activeTab = [...tabs] | ||||
|         .reverse() | ||||
|         .find(tab => pathname.startsWith(tab.path)); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         const created = params.get('created'); | ||||
| @ -107,13 +81,13 @@ const Project = () => { | ||||
|     }, []); | ||||
| 
 | ||||
|     const renderTabs = () => { | ||||
|         return tabData.map((tab, index) => { | ||||
|         return tabs.map(tab => { | ||||
|             return ( | ||||
|                 <Tab | ||||
|                     data-loading | ||||
|                     key={tab.title} | ||||
|                     id={`tab-${index}`} | ||||
|                     aria-controls={`tabpanel-${index}`} | ||||
|                     label={tab.title} | ||||
|                     value={tab.path} | ||||
|                     onClick={() => navigate(tab.path)} | ||||
|                     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 ( | ||||
|         <div ref={ref}> | ||||
|             <div className={styles.header}> | ||||
| @ -167,7 +131,7 @@ const Project = () => { | ||||
|                 <div className={styles.separator} /> | ||||
|                 <div className={styles.tabContainer}> | ||||
|                     <Tabs | ||||
|                         value={activeTabIdx} | ||||
|                         value={activeTab?.path} | ||||
|                         indicatorColor="primary" | ||||
|                         textColor="primary" | ||||
|                     > | ||||
| @ -175,7 +139,13 @@ const Project = () => { | ||||
|                     </Tabs> | ||||
|                 </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> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -1,15 +1,11 @@ | ||||
| import { ProjectFeaturesArchiveTable } from 'component/archive/ProjectFeaturesArchiveTable'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||
| 
 | ||||
| interface IProjectFeaturesArchiveProps { | ||||
|     projectId: string; | ||||
|     projectName: string; | ||||
| } | ||||
| 
 | ||||
| export const ProjectFeaturesArchive = ({ | ||||
|     projectId, | ||||
|     projectName, | ||||
| }: IProjectFeaturesArchiveProps) => { | ||||
| export const ProjectFeaturesArchive = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|     usePageTitle(`Project archive – ${projectName}`); | ||||
| 
 | ||||
|     return <ProjectFeaturesArchiveTable projectId={projectId} />; | ||||
|  | ||||
| @ -4,13 +4,12 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { ReportCard } from './ReportTable/ReportCard/ReportCard'; | ||||
| import { ReportTable } from './ReportTable/ReportTable'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||
| 
 | ||||
| interface IProjectHealthProps { | ||||
|     projectId: string; | ||||
|     projectName: string; | ||||
| } | ||||
| 
 | ||||
| const ProjectHealth = ({ projectId, projectName }: IProjectHealthProps) => { | ||||
| const ProjectHealth = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|     usePageTitle(`Project health – ${projectName}`); | ||||
| 
 | ||||
|     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 ProjectInfo from './ProjectInfo/ProjectInfo'; | ||||
| import { useStyles } from './Project.styles'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| 
 | ||||
| interface IProjectOverviewProps { | ||||
|     projectName: string; | ||||
|     projectId: string; | ||||
| } | ||||
| const refreshInterval = 15 * 1000; | ||||
| 
 | ||||
| const ProjectOverview = ({ projectId, projectName }: IProjectOverviewProps) => { | ||||
|     const { project, loading } = useProject(projectId, { | ||||
|         refreshInterval: 15 * 1000, // ms
 | ||||
|     }); | ||||
| const ProjectOverview = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|     const { project, loading } = useProject(projectId, { refreshInterval }); | ||||
|     const { members, features, health, description, environments } = project; | ||||
|     const { classes: styles } = useStyles(); | ||||
|     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 useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { Alert } from '@mui/material'; | ||||
| @ -8,14 +8,11 @@ import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable'; | ||||
| import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; | ||||
| 
 | ||||
| interface IProjectAccess { | ||||
|     projectName: string; | ||||
| } | ||||
| 
 | ||||
| export const ProjectAccess: VFC<IProjectAccess> = ({ projectName }) => { | ||||
| export const ProjectAccess = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
| 
 | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const { isOss } = useUiConfig(); | ||||
|     usePageTitle(`Project access – ${projectName}`); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import React, { FormEvent, useEffect, useMemo, useState } from 'react'; | ||||
| import React, { FormEvent, useState } from 'react'; | ||||
| import { | ||||
|     Autocomplete, | ||||
|     Button, | ||||
| @ -25,7 +25,8 @@ import { IUser } from 'interfaces/user'; | ||||
| import { IGroup } from 'interfaces/group'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 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')(() => ({ | ||||
|     display: 'flex', | ||||
| @ -88,55 +89,34 @@ interface IAccessOption { | ||||
| } | ||||
| 
 | ||||
| interface IProjectAccessAssignProps { | ||||
|     open: boolean; | ||||
|     setOpen: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     selected?: IProjectAccess; | ||||
|     accesses: IProjectAccess[]; | ||||
|     users: IUser[]; | ||||
|     groups: IGroup[]; | ||||
|     roles: IProjectRole[]; | ||||
|     entityType: string; | ||||
| } | ||||
| 
 | ||||
| export const ProjectAccessAssign = ({ | ||||
|     open, | ||||
|     setOpen, | ||||
|     selected, | ||||
|     accesses, | ||||
|     users, | ||||
|     groups, | ||||
|     roles, | ||||
|     entityType, | ||||
| }: IProjectAccessAssignProps) => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { flags } = uiConfig; | ||||
|     const entityType = flags.UG ? 'user / group' : 'user'; | ||||
| 
 | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { refetchProjectAccess } = useProjectAccess(projectId); | ||||
|     const { addAccessToProject, changeUserRole, changeGroupRole, loading } = | ||||
|         useProjectApi(); | ||||
|     const { users, groups } = useAccess(); | ||||
|     const edit = Boolean(selected); | ||||
| 
 | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const navigate = useNavigate(); | ||||
| 
 | ||||
|     const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>([]); | ||||
|     const [role, setRole] = useState<IProjectRole | null>( | ||||
|         roles.find(({ id }) => id === selected?.entity.roleId) ?? null | ||||
|     ); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setRole(roles.find(({ id }) => id === selected?.entity.roleId) ?? null); | ||||
|     }, [roles, selected]); | ||||
| 
 | ||||
|     const payload = useMemo( | ||||
|         () => ({ | ||||
|             users: selectedOptions | ||||
|                 ?.filter(({ type }) => type === ENTITY_TYPE.USER) | ||||
|                 .map(({ id }) => ({ id })), | ||||
|             groups: selectedOptions | ||||
|                 ?.filter(({ type }) => type === ENTITY_TYPE.GROUP) | ||||
|                 .map(({ id }) => ({ id })), | ||||
|         }), | ||||
|         [selectedOptions] | ||||
|     ); | ||||
| 
 | ||||
|     const options = useMemo( | ||||
|         () => [ | ||||
|     const options = [ | ||||
|         ...groups | ||||
|             .filter( | ||||
|                 (group: IGroup) => | ||||
| @ -165,19 +145,27 @@ export const ProjectAccessAssign = ({ | ||||
|                 entity: user, | ||||
|                 type: ENTITY_TYPE.USER, | ||||
|             })), | ||||
|         ], | ||||
|         [users, accesses, edit, groups] | ||||
|     ); | ||||
|     ]; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         const selectedOption = | ||||
|     const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>( | ||||
|         () => | ||||
|             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 [role, setRole] = useState<IProjectRole | null>( | ||||
|         roles.find(({ id }) => id === selected?.entity.roleId) ?? null | ||||
|     ); | ||||
| 
 | ||||
|     const payload = { | ||||
|         users: selectedOptions | ||||
|             ?.filter(({ type }) => type === ENTITY_TYPE.USER) | ||||
|             .map(({ id }) => ({ id })), | ||||
|         groups: selectedOptions | ||||
|             ?.filter(({ type }) => type === ENTITY_TYPE.GROUP) | ||||
|             .map(({ id }) => ({ id })), | ||||
|     }; | ||||
| 
 | ||||
|     const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { | ||||
|         e.preventDefault(); | ||||
| @ -193,7 +181,7 @@ export const ProjectAccessAssign = ({ | ||||
|                 await changeGroupRole(projectId, role.id, selected.entity.id); | ||||
|             } | ||||
|             refetchProjectAccess(); | ||||
|             setOpen(false); | ||||
|             navigate(GO_BACK); | ||||
|             setToastData({ | ||||
|                 title: `${selectedOptions.length} ${ | ||||
|                     selectedOptions.length === 1 ? 'access' : 'accesses' | ||||
| @ -277,10 +265,8 @@ export const ProjectAccessAssign = ({ | ||||
| 
 | ||||
|     return ( | ||||
|         <SidebarModal | ||||
|             open={open} | ||||
|             onClose={() => { | ||||
|                 setOpen(false); | ||||
|             }} | ||||
|             open | ||||
|             onClose={() => navigate(GO_BACK)} | ||||
|             label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`} | ||||
|         > | ||||
|             <FormTemplate | ||||
| @ -373,11 +359,7 @@ export const ProjectAccessAssign = ({ | ||||
|                         > | ||||
|                             Assign {entityType} | ||||
|                         </Button> | ||||
|                         <StyledCancelButton | ||||
|                             onClick={() => { | ||||
|                                 setOpen(false); | ||||
|                             }} | ||||
|                         > | ||||
|                         <StyledCancelButton onClick={() => navigate(GO_BACK)}> | ||||
|                             Cancel | ||||
|                         </StyledCancelButton> | ||||
|                     </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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 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 { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | ||||
| import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { Search } from 'component/common/Search/Search'; | ||||
| import { ProjectAccessAssign } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign'; | ||||
| import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| @ -33,6 +38,9 @@ import { IUser } from 'interfaces/user'; | ||||
| import { IGroup } from 'interfaces/group'; | ||||
| import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | ||||
| 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< | ||||
|     Record<'sort' | 'order' | 'search', string> | ||||
| @ -52,44 +60,17 @@ export const ProjectAccessTable: VFC = () => { | ||||
|     const { flags } = uiConfig; | ||||
|     const entityType = flags.UG ? 'user / group' : 'user'; | ||||
| 
 | ||||
|     const navigate = useNavigate(); | ||||
|     const theme = useTheme(); | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||
|     const { setToastData } = useToast(); | ||||
| 
 | ||||
|     const { access, refetchProjectAccess } = useProjectAccess(projectId); | ||||
|     const { removeUserFromRole, removeGroupFromRole } = useProjectApi(); | ||||
|     const [assignOpen, setAssignOpen] = useState(false); | ||||
|     const [removeOpen, setRemoveOpen] = useState(false); | ||||
|     const [groupOpen, setGroupOpen] = useState(false); | ||||
|     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( | ||||
|         () => [ | ||||
|             { | ||||
| @ -145,7 +126,8 @@ export const ProjectAccessTable: VFC = () => { | ||||
|             { | ||||
|                 Header: 'Role', | ||||
|                 accessor: (row: IProjectAccess) => | ||||
|                     roles.find(({ id }) => id === row.entity.roleId)?.name, | ||||
|                     access?.roles.find(({ id }) => id === row.entity.roleId) | ||||
|                         ?.name, | ||||
|                 minWidth: 120, | ||||
|                 filterName: 'role', | ||||
|             }, | ||||
| @ -187,19 +169,23 @@ export const ProjectAccessTable: VFC = () => { | ||||
|                 disableSortBy: true, | ||||
|                 align: 'center', | ||||
|                 maxWidth: 200, | ||||
|                 Cell: ({ row: { original: row } }: any) => ( | ||||
|                 Cell: ({ | ||||
|                     row: { original: row }, | ||||
|                 }: { | ||||
|                     row: { original: IProjectAccess }; | ||||
|                 }) => ( | ||||
|                     <ActionCell> | ||||
|                         <PermissionIconButton | ||||
|                             component={Link} | ||||
|                             permission={UPDATE_PROJECT} | ||||
|                             projectId={projectId} | ||||
|                             onClick={() => { | ||||
|                                 setSelectedRow(row); | ||||
|                                 setAssignOpen(true); | ||||
|                             }} | ||||
|                             disabled={mappedData.length === 1} | ||||
|                             to={`edit/${ | ||||
|                                 row.type === ENTITY_TYPE.USER ? 'user' : 'group' | ||||
|                             }/${row.entity.id}`}
 | ||||
|                             disabled={access?.rows.length === 1} | ||||
|                             tooltipProps={{ | ||||
|                                 title: | ||||
|                                     mappedData.length === 1 | ||||
|                                     access?.rows.length === 1 | ||||
|                                         ? 'Cannot edit access. A project must have at least one owner' | ||||
|                                         : 'Edit access', | ||||
|                             }} | ||||
| @ -213,10 +199,10 @@ export const ProjectAccessTable: VFC = () => { | ||||
|                                 setSelectedRow(row); | ||||
|                                 setRemoveOpen(true); | ||||
|                             }} | ||||
|                             disabled={mappedData.length === 1} | ||||
|                             disabled={access?.rows.length === 1} | ||||
|                             tooltipProps={{ | ||||
|                                 title: | ||||
|                                     mappedData.length === 1 | ||||
|                                     access?.rows.length === 1 | ||||
|                                         ? 'Cannot remove access. A project must have at least one owner' | ||||
|                                         : 'Remove access', | ||||
|                             }} | ||||
| @ -227,7 +213,7 @@ export const ProjectAccessTable: VFC = () => { | ||||
|                 ), | ||||
|             }, | ||||
|         ], | ||||
|         [roles, mappedData.length, projectId] | ||||
|         [access, projectId] | ||||
|     ); | ||||
| 
 | ||||
|     const [searchParams, setSearchParams] = useSearchParams(); | ||||
| @ -247,7 +233,7 @@ export const ProjectAccessTable: VFC = () => { | ||||
|     const { data, getSearchText, getSearchContext } = useSearch( | ||||
|         columns, | ||||
|         searchValue, | ||||
|         mappedData ?? [] | ||||
|         access?.rows ?? [] | ||||
|     ); | ||||
| 
 | ||||
|     const { | ||||
| @ -319,7 +305,6 @@ export const ProjectAccessTable: VFC = () => { | ||||
|             }); | ||||
|         } | ||||
|         setRemoveOpen(false); | ||||
|         setSelectedRow(undefined); | ||||
|     }; | ||||
|     return ( | ||||
|         <PageContent | ||||
| @ -348,9 +333,10 @@ export const ProjectAccessTable: VFC = () => { | ||||
|                                 } | ||||
|                             /> | ||||
|                             <Button | ||||
|                                 component={Link} | ||||
|                                 to={`create`} | ||||
|                                 variant="contained" | ||||
|                                 color="primary" | ||||
|                                 onClick={() => setAssignOpen(true)} | ||||
|                             > | ||||
|                                 Assign {entityType} | ||||
|                             </Button> | ||||
| @ -399,19 +385,21 @@ export const ProjectAccessTable: VFC = () => { | ||||
|                     /> | ||||
|                 } | ||||
|             /> | ||||
|             <ProjectAccessAssign | ||||
|                 open={assignOpen} | ||||
|                 setOpen={setAssignOpen} | ||||
|                 selected={selectedRow} | ||||
|                 accesses={mappedData} | ||||
|                 roles={roles} | ||||
|                 entityType={entityType} | ||||
|             <Routes> | ||||
|                 <Route path="create" element={<ProjectAccessCreate />} /> | ||||
|                 <Route | ||||
|                     path="edit/group/:groupId" | ||||
|                     element={<ProjectAccessEditGroup />} | ||||
|                 /> | ||||
|                 <Route | ||||
|                     path="edit/user/:userId" | ||||
|                     element={<ProjectAccessEditUser />} | ||||
|                 /> | ||||
|             </Routes> | ||||
|             <Dialogue | ||||
|                 open={removeOpen} | ||||
|                 onClick={() => removeAccess(selectedRow)} | ||||
|                 onClose={() => { | ||||
|                     setSelectedRow(undefined); | ||||
|                     setRemoveOpen(false); | ||||
|                 }} | ||||
|                 title={`Really remove ${entityType} from this project?`} | ||||
| @ -422,12 +410,12 @@ export const ProjectAccessTable: VFC = () => { | ||||
|                 group={selectedRow?.entity as IGroup} | ||||
|                 projectId={projectId} | ||||
|                 subtitle={`Role: ${ | ||||
|                     roles.find(({ id }) => id === selectedRow?.entity.roleId) | ||||
|                         ?.name | ||||
|                     access?.roles.find( | ||||
|                         ({ id }) => id === selectedRow?.entity.roleId | ||||
|                     )?.name | ||||
|                 }`}
 | ||||
|                 onEdit={() => { | ||||
|                     setAssignOpen(true); | ||||
|                     console.log('Assign Open true'); | ||||
|                     navigate(`edit/group/${selectedRow?.entity.id}`); | ||||
|                 }} | ||||
|                 onRemove={() => { | ||||
|                     setGroupOpen(false); | ||||
|  | ||||
| @ -8,7 +8,9 @@ import ApiError from 'component/common/ApiError/ApiError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| 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 useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; | ||||
| import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDisableConfirm'; | ||||
| @ -19,17 +21,13 @@ import { getEnabledEnvs } from './helpers'; | ||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| import { useThemeStyles } from 'themes/themeStyles'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| 
 | ||||
| interface IProjectEnvironmentListProps { | ||||
|     projectId: string; | ||||
|     projectName: string; | ||||
| } | ||||
| 
 | ||||
| const ProjectEnvironmentList = ({ | ||||
|     projectId, | ||||
|     projectName, | ||||
| }: IProjectEnvironmentListProps) => { | ||||
| const ProjectEnvironmentList = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const projectName = useProjectNameOrId(projectId); | ||||
|     usePageTitle(`Project environments – ${projectName}`); | ||||
| 
 | ||||
|     // api state
 | ||||
|     const [envs, setEnvs] = useState<IProjectEnvironment[]>([]); | ||||
|     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 { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { CreateButton } from 'component/common/CreateButton/CreateButton'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| export const CreateStrategy = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -64,7 +65,7 @@ export const CreateStrategy = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useStrategy from 'hooks/api/getters/useStrategy/useStrategy'; | ||||
| import { UpdateButton } from 'component/common/UpdateButton/UpdateButton'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| export const EditStrategy = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -68,7 +69,7 @@ export const EditStrategy = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -8,6 +8,7 @@ import useTagTypesApi from 'hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const CreateTagType = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -55,7 +56,7 @@ const CreateTagType = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
| @ -10,6 +10,7 @@ import useToast from 'hooks/useToast'; | ||||
| import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { GO_BACK } from 'constants/navigate'; | ||||
| 
 | ||||
| const EditTagType = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| @ -54,7 +55,7 @@ const EditTagType = () => { | ||||
|     }; | ||||
| 
 | ||||
|     const handleCancel = () => { | ||||
|         navigate(-1); | ||||
|         navigate(GO_BACK); | ||||
|     }; | ||||
| 
 | ||||
|     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 { useMemo } from 'react'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| 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( | ||||
|         formatApiPath(`api/admin/user-admin/access`), | ||||
|         fetcher | ||||
|     ); | ||||
| 
 | ||||
|     return useMemo( | ||||
|         () => ({ | ||||
|             users: data?.users ?? [], | ||||
|             groups: data?.groups ?? [], | ||||
|     return { | ||||
|         users: data?.users, | ||||
|         groups: data?.groups, | ||||
|         loading: !error && !data, | ||||
|         refetch: () => mutate(), | ||||
|         error, | ||||
|         }), | ||||
|         [data, error, mutate] | ||||
|     ); | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| 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; | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import useSWR, { mutate, SWRConfiguration } from 'swr'; | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useState, useEffect, useMemo } from 'react'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | ||||
| import { IProjectRole } from 'interfaces/role'; | ||||
| @ -29,6 +29,7 @@ export interface IProjectAccessOutput { | ||||
|     users: IProjectAccessUser[]; | ||||
|     groups: IProjectAccessGroup[]; | ||||
|     roles: IProjectRole[]; | ||||
|     rows: IProjectAccess[]; | ||||
| } | ||||
| 
 | ||||
| const useProjectAccess = ( | ||||
| @ -58,8 +59,9 @@ const useProjectAccess = ( | ||||
|         setLoading(!error && !data); | ||||
|     }, [data, error]); | ||||
| 
 | ||||
|     let access: IProjectAccessOutput = data | ||||
|         ? { | ||||
|     const access: IProjectAccessOutput | undefined = useMemo(() => { | ||||
|         if (data) { | ||||
|             return formatAccessData({ | ||||
|                 roles: data.roles, | ||||
|                 users: data.users, | ||||
|                 groups: | ||||
| @ -67,14 +69,34 @@ const useProjectAccess = ( | ||||
|                         ...group, | ||||
|                         users: mapGroupUsers(group.users ?? []), | ||||
|                     })) ?? [], | ||||
|             }); | ||||
|         } | ||||
|         : { roles: [], users: [], groups: [] }; | ||||
|     }, [data]); | ||||
| 
 | ||||
|     return { | ||||
|         access: access, | ||||
|         access, | ||||
|         error, | ||||
|         loading, | ||||
|         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; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| /* NAVIGATION */ | ||||
| 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 CREATE_API_TOKEN_BUTTON = 'CREATE_API_TOKEN_BUTTON'; | ||||
| 
 | ||||
| @ -12,7 +13,17 @@ export const CF_CREATE_BTN_ID = 'CF_CREATE_BTN_ID'; | ||||
| /* CREATE GROUP */ | ||||
| export const UG_NAME_ID = 'UG_NAME_ID'; | ||||
| export const UG_DESC_ID = 'UG_DESC_ID'; | ||||
| export const UG_USERS_ID = 'UG_USERS_ID'; | ||||
| export const UG_USERS_ADD_ID = 'UG_USERS_ADD_ID'; | ||||
| export const UG_USERS_TABLE_ROLE_ID = 'UG_USERS_TABLE_ROLE_ID'; | ||||
| export const UG_CREATE_BTN_ID = 'UG_CREATE_BTN_ID'; | ||||
| export const UG_SAVE_BTN_ID = 'UG_SAVE_BTN_ID'; | ||||
| export const UG_EDIT_BTN_ID = 'UG_EDIT_BTN_ID'; | ||||
| export const UG_DELETE_BTN_ID = 'UG_DELETE_BTN_ID'; | ||||
| export const UG_ADD_USER_BTN_ID = 'UG_ADD_USER_BTN_ID'; | ||||
| export const UG_EDIT_USER_BTN_ID = 'UG_EDIT_USER_BTN_ID'; | ||||
| export const UG_REMOVE_USER_BTN_ID = 'UG_REMOVE_USER_BTN_ID'; | ||||
| export const UG_USERS_ROLE_ID = 'UG_USERS_ROLE_ID'; | ||||
| 
 | ||||
| /* SEGMENT */ | ||||
| export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID'; | ||||
| @ -55,3 +66,4 @@ export const SIDEBAR_MODAL_ID = 'SIDEBAR_MODAL_ID'; | ||||
| export const AUTH_PAGE_ID = 'AUTH_PAGE_ID'; | ||||
| export const ANNOUNCER_ELEMENT_TEST_ID = 'ANNOUNCER_ELEMENT_TEST_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