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,55 +89,34 @@ 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 = [
 | 
				
			||||||
    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(
 | 
					 | 
				
			||||||
        () => [
 | 
					 | 
				
			||||||
        ...groups
 | 
					        ...groups
 | 
				
			||||||
            .filter(
 | 
					            .filter(
 | 
				
			||||||
                (group: IGroup) =>
 | 
					                (group: IGroup) =>
 | 
				
			||||||
@ -165,19 +145,27 @@ export const ProjectAccessAssign = ({
 | 
				
			|||||||
                entity: user,
 | 
					                entity: user,
 | 
				
			||||||
                type: ENTITY_TYPE.USER,
 | 
					                type: ENTITY_TYPE.USER,
 | 
				
			||||||
            })),
 | 
					            })),
 | 
				
			||||||
        ],
 | 
					    ];
 | 
				
			||||||
        [users, accesses, edit, groups]
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>(
 | 
				
			||||||
        const selectedOption =
 | 
					        () =>
 | 
				
			||||||
            options.filter(
 | 
					            options.filter(
 | 
				
			||||||
                ({ id, type }) =>
 | 
					                ({ id, type }) =>
 | 
				
			||||||
                    id === selected?.entity.id && type === selected?.type
 | 
					                    id === selected?.entity.id && type === selected?.type
 | 
				
			||||||
            ) || [];
 | 
					            )
 | 
				
			||||||
        setSelectedOptions(selectedOption);
 | 
					    );
 | 
				
			||||||
        setRole(roles.find(({ id }) => id === selected?.entity.roleId) || null);
 | 
					    const [role, setRole] = useState<IProjectRole | null>(
 | 
				
			||||||
    }, [open, selected, options, roles]);
 | 
					        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>) => {
 | 
					    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,8 +59,9 @@ const useProjectAccess = (
 | 
				
			|||||||
        setLoading(!error && !data);
 | 
					        setLoading(!error && !data);
 | 
				
			||||||
    }, [data, error]);
 | 
					    }, [data, error]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let access: IProjectAccessOutput = data
 | 
					    const access: IProjectAccessOutput | undefined = useMemo(() => {
 | 
				
			||||||
        ? {
 | 
					        if (data) {
 | 
				
			||||||
 | 
					            return formatAccessData({
 | 
				
			||||||
                roles: data.roles,
 | 
					                roles: data.roles,
 | 
				
			||||||
                users: data.users,
 | 
					                users: data.users,
 | 
				
			||||||
                groups:
 | 
					                groups:
 | 
				
			||||||
@ -67,14 +69,34 @@ const useProjectAccess = (
 | 
				
			|||||||
                        ...group,
 | 
					                        ...group,
 | 
				
			||||||
                        users: mapGroupUsers(group.users ?? []),
 | 
					                        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