diff --git a/frontend/.github/workflows/e2e.groups.yml b/frontend/.github/workflows/e2e.groups.yml
new file mode 100644
index 0000000000..7eb5e056e6
--- /dev/null
+++ b/frontend/.github/workflows/e2e.groups.yml
@@ -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 }}
diff --git a/frontend/cypress/integration/groups/groups.spec.ts b/frontend/cypress/integration/groups/groups.spec.ts
new file mode 100644
index 0000000000..6977641946
--- /dev/null
+++ b/frontend/cypress/integration/groups/groups.spec.ts
@@ -0,0 +1,160 @@
+///
+
+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');
+ });
+});
diff --git a/frontend/src/component/addons/AddonForm/AddonForm.tsx b/frontend/src/component/addons/AddonForm/AddonForm.tsx
index f7bf2a49e2..3a19477149 100644
--- a/frontend/src/component/addons/AddonForm/AddonForm.tsx
+++ b/frontend/src/component/addons/AddonForm/AddonForm.tsx
@@ -33,6 +33,7 @@ import {
StyledButtonSection,
} from './AddonForm.styles';
import { useTheme } from '@mui/system';
+import { GO_BACK } from 'constants/navigate';
interface IAddonFormProps {
provider?: IAddonProvider;
@@ -168,7 +169,7 @@ export const AddonForm: VFC = ({
};
const onCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
const onSubmit: FormEventHandler = async event => {
diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
index 07b9c0eb09..203f1ac1fb 100644
--- a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
+++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
@@ -12,6 +12,7 @@ import { useState } from 'react';
import { scrollToTop } from 'component/common/util';
import { formatUnknownError } from 'utils/formatUnknownError';
import { usePageTitle } from 'hooks/usePageTitle';
+import { GO_BACK } from 'constants/navigate';
const pageTitle = 'Create API token';
@@ -75,7 +76,7 @@ export const CreateApiToken = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx b/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx
index 8b6bcec4d7..8cb9dec9cb 100644
--- a/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx
+++ b/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx
@@ -9,6 +9,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { UG_CREATE_BTN_ID } from 'utils/testIds';
import { Button } from '@mui/material';
import { CREATE } from 'constants/misc';
+import { GO_BACK } from 'constants/navigate';
export const CreateGroup = () => {
const { setToastData, setToastApiError } = useToast();
@@ -58,7 +59,7 @@ export const CreateGroup = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/admin/groups/EditGroup/EditGroup.tsx b/frontend/src/component/admin/groups/EditGroup/EditGroup.tsx
index ab0f3a1972..2efac8018e 100644
--- a/frontend/src/component/admin/groups/EditGroup/EditGroup.tsx
+++ b/frontend/src/component/admin/groups/EditGroup/EditGroup.tsx
@@ -10,6 +10,8 @@ import { Button } from '@mui/material';
import { EDIT } from 'constants/misc';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
+import { UG_SAVE_BTN_ID } from 'utils/testIds';
+import { GO_BACK } from 'constants/navigate';
export const EditGroup = () => {
const groupId = Number(useRequiredPathParam('groupId'));
@@ -40,7 +42,7 @@ export const EditGroup = () => {
try {
await updateGroup(groupId, payload);
refetchGroup();
- navigate(-1);
+ navigate(GO_BACK);
setToastData({
title: 'Group updated successfully',
type: 'success',
@@ -60,7 +62,7 @@ export const EditGroup = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
@@ -85,7 +87,12 @@ export const EditGroup = () => {
mode={EDIT}
clearErrors={clearErrors}
>
-
diff --git a/frontend/src/component/admin/groups/Group/EditGroupUser/EditGroupUser.tsx b/frontend/src/component/admin/groups/Group/EditGroupUser/EditGroupUser.tsx
index bab86186d3..cdf1214340 100644
--- a/frontend/src/component/admin/groups/Group/EditGroupUser/EditGroupUser.tsx
+++ b/frontend/src/component/admin/groups/Group/EditGroupUser/EditGroupUser.tsx
@@ -8,6 +8,7 @@ import useToast from 'hooks/useToast';
import { IGroup, IGroupUser, Role } from 'interfaces/group';
import { FC, FormEvent, useEffect, useMemo, useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { UG_SAVE_BTN_ID, UG_USERS_ROLE_ID } from 'utils/testIds';
const StyledForm = styled('form')(() => ({
display: 'flex',
@@ -143,6 +144,7 @@ export const EditGroupUser: FC = ({
Assign the role the user should have in this group
@@ -159,6 +161,7 @@ export const EditGroupUser: FC = ({
({
fontSize: theme.fontSizes.mainHeader,
@@ -134,6 +141,7 @@ export const Group: VFC = () => {
{
setSelectedUser(rowUser);
setEditUserOpen(true);
@@ -148,6 +156,7 @@ export const Group: VFC = () => {
describeChild
>
{
setSelectedUser(rowUser);
setRemoveUserOpen(true);
@@ -240,6 +249,7 @@ export const Group: VFC = () => {
actions={
<>
{
setRemoveOpen(true)}
permission={ADMIN}
@@ -296,6 +307,7 @@ export const Group: VFC = () => {
}
/>
{
diff --git a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx
index 60f5f70fa9..584cfaa867 100644
--- a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx
+++ b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect.tsx
@@ -11,6 +11,7 @@ import { IUser } from 'interfaces/user';
import { useMemo, useState, VFC } from 'react';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { IGroupUser, Role } from 'interfaces/group';
+import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds';
const StyledOption = styled('div')(({ theme }) => ({
display: 'flex',
@@ -83,6 +84,7 @@ export const GroupFormUsersSelect: VFC = ({
return (
= ({
)}
/>
-
+
Add
diff --git a/frontend/src/component/admin/groups/GroupUserRoleCell/GroupUserRoleCell.tsx b/frontend/src/component/admin/groups/GroupUserRoleCell/GroupUserRoleCell.tsx
index 6382e56dc7..64186f2cbe 100644
--- a/frontend/src/component/admin/groups/GroupUserRoleCell/GroupUserRoleCell.tsx
+++ b/frontend/src/component/admin/groups/GroupUserRoleCell/GroupUserRoleCell.tsx
@@ -4,6 +4,7 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Role } from 'interfaces/group';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
+import { UG_USERS_TABLE_ROLE_ID } from 'utils/testIds';
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
@@ -36,6 +37,7 @@ export const GroupUserRoleCell = ({
condition={Boolean(onChange)}
show={
diff --git a/frontend/src/component/admin/projectRoles/CreateProjectRole/CreateProjectRole.tsx b/frontend/src/component/admin/projectRoles/CreateProjectRole/CreateProjectRole.tsx
index 80a0a2e38c..cb0b547901 100644
--- a/frontend/src/component/admin/projectRoles/CreateProjectRole/CreateProjectRole.tsx
+++ b/frontend/src/component/admin/projectRoles/CreateProjectRole/CreateProjectRole.tsx
@@ -8,6 +8,7 @@ import useToast from 'hooks/useToast';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { GO_BACK } from 'constants/navigate';
const CreateProjectRole = () => {
const { setToastData, setToastApiError } = useToast();
@@ -66,7 +67,7 @@ const CreateProjectRole = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx b/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx
index 0ef6667a3c..f739274204 100644
--- a/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx
+++ b/frontend/src/component/admin/projectRoles/EditProjectRole/EditProjectRole.tsx
@@ -12,6 +12,7 @@ import useProjectRoleForm from '../hooks/useProjectRoleForm';
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
const EditProjectRole = () => {
const { uiConfig } = useUiConfig();
@@ -94,7 +95,7 @@ const EditProjectRole = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/admin/users/CreateUser/CreateUser.tsx b/frontend/src/component/admin/users/CreateUser/CreateUser.tsx
index 0756df9a68..41ce786603 100644
--- a/frontend/src/component/admin/users/CreateUser/CreateUser.tsx
+++ b/frontend/src/component/admin/users/CreateUser/CreateUser.tsx
@@ -11,6 +11,7 @@ import { scrollToTop } from 'component/common/util';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { GO_BACK } from 'constants/navigate';
const CreateUser = () => {
const { setToastApiError } = useToast();
@@ -72,7 +73,7 @@ const CreateUser = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/admin/users/EditUser/EditUser.tsx b/frontend/src/component/admin/users/EditUser/EditUser.tsx
index d307511d22..482f6bfe24 100644
--- a/frontend/src/component/admin/users/EditUser/EditUser.tsx
+++ b/frontend/src/component/admin/users/EditUser/EditUser.tsx
@@ -13,6 +13,7 @@ import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
const EditUser = () => {
useEffect(() => {
@@ -69,7 +70,7 @@ const EditUser = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/common/NotFound/NotFound.tsx b/frontend/src/component/common/NotFound/NotFound.tsx
index 3155dfb92f..e20fd10beb 100644
--- a/frontend/src/component/common/NotFound/NotFound.tsx
+++ b/frontend/src/component/common/NotFound/NotFound.tsx
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router';
import { ReactComponent as LogoIcon } from 'assets/icons/logoBg.svg';
import { useStyles } from './NotFound.styles';
+import { GO_BACK } from 'constants/navigate';
const NotFound = () => {
const navigate = useNavigate();
@@ -14,7 +15,7 @@ const NotFound = () => {
};
const onClickBack = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx b/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx
index 40f59958b5..9d5bc5bb30 100644
--- a/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx
+++ b/frontend/src/component/common/ToastRenderer/Toast/Toast.tsx
@@ -7,6 +7,7 @@ import UIContext from 'contexts/UIContext';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Close from '@mui/icons-material/Close';
import { IToast } from 'interfaces/toast';
+import { TOAST_TEXT } from 'utils/testIds';
const Toast = ({ title, text, type, confetti }: IToast) => {
const { setToast } = useContext(UIContext);
@@ -72,7 +73,9 @@ const Toast = ({ title, text, type, confetti }: IToast) => {
{text}
}
+ show={
+ {text}
+ }
/>
diff --git a/frontend/src/component/context/CreateUnleashContext/CreateUnleashContextPage.tsx b/frontend/src/component/context/CreateUnleashContext/CreateUnleashContextPage.tsx
index c05275dcf8..fddaedd78b 100644
--- a/frontend/src/component/context/CreateUnleashContext/CreateUnleashContextPage.tsx
+++ b/frontend/src/component/context/CreateUnleashContext/CreateUnleashContextPage.tsx
@@ -1,5 +1,6 @@
import { useNavigate } from 'react-router-dom';
import { CreateUnleashContext } from 'component/context/CreateUnleashContext/CreateUnleashContext';
+import { GO_BACK } from 'constants/navigate';
export const CreateUnleashContextPage = () => {
const navigate = useNavigate();
@@ -7,7 +8,7 @@ export const CreateUnleashContextPage = () => {
return (
navigate('/context')}
- onCancel={() => navigate(-1)}
+ onCancel={() => navigate(GO_BACK)}
/>
);
};
diff --git a/frontend/src/component/context/EditContext/EditContext.tsx b/frontend/src/component/context/EditContext/EditContext.tsx
index 67883a9603..fd0b65eb77 100644
--- a/frontend/src/component/context/EditContext/EditContext.tsx
+++ b/frontend/src/component/context/EditContext/EditContext.tsx
@@ -12,6 +12,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { ContextForm } from '../ContextForm/ContextForm';
import { useContextForm } from '../hooks/useContextForm';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
export const EditContext = () => {
useEffect(() => {
@@ -71,7 +72,7 @@ export const EditContext = () => {
};
const onCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx
index f7bd0e789b..31901bcc9b 100644
--- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx
+++ b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx
@@ -15,6 +15,7 @@ import { PageContent } from 'component/common/PageContent/PageContent';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { GO_BACK } from 'constants/navigate';
const CreateEnvironment = () => {
const { setToastApiError, setToastData } = useToast();
@@ -66,7 +67,7 @@ const CreateEnvironment = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx b/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx
index 54dee41050..10ad7f5afd 100644
--- a/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx
+++ b/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx
@@ -11,6 +11,7 @@ import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import useEnvironmentForm from '../hooks/useEnvironmentForm';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
const EditEnvironment = () => {
const { uiConfig } = useUiConfig();
@@ -56,7 +57,7 @@ const EditEnvironment = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/feature/CreateFeature/CreateFeature.tsx b/frontend/src/component/feature/CreateFeature/CreateFeature.tsx
index f80033c857..5f52865d21 100644
--- a/frontend/src/component/feature/CreateFeature/CreateFeature.tsx
+++ b/frontend/src/component/feature/CreateFeature/CreateFeature.tsx
@@ -11,6 +11,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton';
import UIContext from 'contexts/UIContext';
import { CF_CREATE_BTN_ID } from 'utils/testIds';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { GO_BACK } from 'constants/navigate';
const CreateFeature = () => {
const { setToastData, setToastApiError } = useToast();
@@ -70,7 +71,7 @@ const CreateFeature = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/feature/EditFeature/EditFeature.tsx b/frontend/src/component/feature/EditFeature/EditFeature.tsx
index a36b296709..b4c5804715 100644
--- a/frontend/src/component/feature/EditFeature/EditFeature.tsx
+++ b/frontend/src/component/feature/EditFeature/EditFeature.tsx
@@ -11,6 +11,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
const EditFeature = () => {
const projectId = useRequiredPathParam('projectId');
@@ -74,7 +75,7 @@ const EditFeature = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
index 9a49a194d8..f4c18236f7 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
@@ -80,16 +80,7 @@ exports[`returns all baseRoutes 1`] = `
"flag": "P",
"menu": {},
"parent": "/projects",
- "path": "/projects/:projectId/:activeTab",
- "title": ":projectId",
- "type": "protected",
- },
- {
- "component": [Function],
- "flag": "P",
- "menu": {},
- "parent": "/projects",
- "path": "/projects/:projectId",
+ "path": "/projects/:projectId/*",
"title": ":projectId",
"type": "protected",
},
diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts
index 06ba81a1c8..a737ba31c8 100644
--- a/frontend/src/component/menu/routes.ts
+++ b/frontend/src/component/menu/routes.ts
@@ -136,16 +136,7 @@ export const routes: IRoute[] = [
menu: {},
},
{
- path: '/projects/:projectId/:activeTab',
- parent: '/projects',
- title: ':projectId',
- component: Project,
- flag: P,
- type: 'protected',
- menu: {},
- },
- {
- path: '/projects/:projectId',
+ path: '/projects/:projectId/*',
parent: '/projects',
title: ':projectId',
component: Project,
diff --git a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx
index 157db22682..adb8d82c30 100644
--- a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx
+++ b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx
@@ -9,6 +9,7 @@ import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { GO_BACK } from 'constants/navigate';
const CreateProject = () => {
const { setToastData, setToastApiError } = useToast();
@@ -65,7 +66,7 @@ const CreateProject = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/project/Project/EditProject/EditProject.tsx b/frontend/src/component/project/Project/EditProject/EditProject.tsx
index 149c6b4369..aee8014753 100644
--- a/frontend/src/component/project/Project/EditProject/EditProject.tsx
+++ b/frontend/src/component/project/Project/EditProject/EditProject.tsx
@@ -13,6 +13,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useContext } from 'react';
import AccessContext from 'contexts/AccessContext';
import { Alert } from '@mui/material';
+import { GO_BACK } from 'constants/navigate';
const EditProject = () => {
const { uiConfig } = useUiConfig();
@@ -70,7 +71,7 @@ const EditProject = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && (
diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx
index a53574c448..abb65bb241 100644
--- a/frontend/src/component/project/Project/Project.tsx
+++ b/frontend/src/component/project/Project/Project.tsx
@@ -16,80 +16,54 @@ import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
-import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
-import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import { Routes, Route, useLocation } from 'react-router-dom';
const Project = () => {
const projectId = useRequiredPathParam('projectId');
- const activeTab = useOptionalPathParam('activeTab');
const params = useQueryParams();
const { project, error, loading, refetch } = useProject(projectId);
const ref = useLoading(loading);
const { setToastData } = useToast();
const { classes: styles } = useStyles();
const navigate = useNavigate();
+ const { pathname } = useLocation();
const { isOss } = useUiConfig();
-
const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId;
- const tabData = [
+
+ const tabs = [
{
title: 'Overview',
- component: (
-
- ),
path: basePath,
name: 'overview',
},
{
title: 'Health',
- component: (
-
- ),
path: `${basePath}/health`,
name: 'health',
},
{
title: 'Access',
- component: ,
path: `${basePath}/access`,
name: 'access',
},
{
title: 'Environments',
- component: (
-
- ),
path: `${basePath}/environments`,
name: 'environments',
},
{
title: 'Archive',
- component: (
-
- ),
path: `${basePath}/archive`,
name: 'archive',
},
];
- const activeTabIdx = activeTab
- ? tabData.findIndex(tab => tab.name === activeTab)
- : 0;
+ const activeTab = [...tabs]
+ .reverse()
+ .find(tab => pathname.startsWith(tab.path));
useEffect(() => {
const created = params.get('created');
@@ -107,13 +81,13 @@ const Project = () => {
}, []);
const renderTabs = () => {
- return tabData.map((tab, index) => {
+ return tabs.map(tab => {
return (
navigate(tab.path)}
className={styles.tabButton}
/>
@@ -121,16 +95,6 @@ const Project = () => {
});
};
- const renderTabContent = () => {
- return tabData.map((tab, index) => {
- return (
-
- {tab.component}
-
- );
- });
- };
-
return (
@@ -167,7 +131,7 @@ const Project = () => {
@@ -175,7 +139,13 @@ const Project = () => {
- {renderTabContent()}
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
);
};
diff --git a/frontend/src/component/project/Project/ProjectFeaturesArchive/ProjectFeaturesArchive.tsx b/frontend/src/component/project/Project/ProjectFeaturesArchive/ProjectFeaturesArchive.tsx
index f4dfd49423..7d675d0e18 100644
--- a/frontend/src/component/project/Project/ProjectFeaturesArchive/ProjectFeaturesArchive.tsx
+++ b/frontend/src/component/project/Project/ProjectFeaturesArchive/ProjectFeaturesArchive.tsx
@@ -1,15 +1,11 @@
import { ProjectFeaturesArchiveTable } from 'component/archive/ProjectFeaturesArchiveTable';
import { usePageTitle } from 'hooks/usePageTitle';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
-interface IProjectFeaturesArchiveProps {
- projectId: string;
- projectName: string;
-}
-
-export const ProjectFeaturesArchive = ({
- projectId,
- projectName,
-}: IProjectFeaturesArchiveProps) => {
+export const ProjectFeaturesArchive = () => {
+ const projectId = useRequiredPathParam('projectId');
+ const projectName = useProjectNameOrId(projectId);
usePageTitle(`Project archive – ${projectName}`);
return ;
diff --git a/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx b/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx
index 3eab35fb4e..66d9e9b97c 100644
--- a/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx
+++ b/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx
@@ -4,13 +4,12 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { usePageTitle } from 'hooks/usePageTitle';
import { ReportCard } from './ReportTable/ReportCard/ReportCard';
import { ReportTable } from './ReportTable/ReportTable';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
-interface IProjectHealthProps {
- projectId: string;
- projectName: string;
-}
-
-const ProjectHealth = ({ projectId, projectName }: IProjectHealthProps) => {
+const ProjectHealth = () => {
+ const projectId = useRequiredPathParam('projectId');
+ const projectName = useProjectNameOrId(projectId);
usePageTitle(`Project health – ${projectName}`);
const { healthReport, refetchHealthReport, error } = useHealthReport(
diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx
index df3fdb682f..f81b939449 100644
--- a/frontend/src/component/project/Project/ProjectOverview.tsx
+++ b/frontend/src/component/project/Project/ProjectOverview.tsx
@@ -1,18 +1,18 @@
-import useProject from 'hooks/api/getters/useProject/useProject';
+import useProject, {
+ useProjectNameOrId,
+} from 'hooks/api/getters/useProject/useProject';
import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles';
import ProjectInfo from './ProjectInfo/ProjectInfo';
import { useStyles } from './Project.styles';
import { usePageTitle } from 'hooks/usePageTitle';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
-interface IProjectOverviewProps {
- projectName: string;
- projectId: string;
-}
+const refreshInterval = 15 * 1000;
-const ProjectOverview = ({ projectId, projectName }: IProjectOverviewProps) => {
- const { project, loading } = useProject(projectId, {
- refreshInterval: 15 * 1000, // ms
- });
+const ProjectOverview = () => {
+ const projectId = useRequiredPathParam('projectId');
+ const projectName = useProjectNameOrId(projectId);
+ const { project, loading } = useProject(projectId, { refreshInterval });
const { members, features, health, description, environments } = project;
const { classes: styles } = useStyles();
usePageTitle(`Project overview – ${projectName}`);
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx
index 1736778e9f..b62682f2fe 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx
@@ -1,4 +1,4 @@
-import { useContext, VFC } from 'react';
+import { useContext } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Alert } from '@mui/material';
@@ -8,14 +8,11 @@ import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable';
+import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
-interface IProjectAccess {
- projectName: string;
-}
-
-export const ProjectAccess: VFC = ({ projectName }) => {
+export const ProjectAccess = () => {
const projectId = useRequiredPathParam('projectId');
-
+ const projectName = useProjectNameOrId(projectId);
const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig();
usePageTitle(`Project access – ${projectName}`);
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx
index a9ec9fb3c2..7eac489145 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign.tsx
@@ -1,4 +1,4 @@
-import React, { FormEvent, useEffect, useMemo, useState } from 'react';
+import React, { FormEvent, useState } from 'react';
import {
Autocomplete,
Button,
@@ -25,7 +25,8 @@ import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ProjectRoleDescription } from './ProjectRoleDescription/ProjectRoleDescription';
-import { useAccess } from '../../../../hooks/api/getters/useAccess/useAccess';
+import { useNavigate } from 'react-router-dom';
+import { GO_BACK } from 'constants/navigate';
const StyledForm = styled('form')(() => ({
display: 'flex',
@@ -88,96 +89,83 @@ interface IAccessOption {
}
interface IProjectAccessAssignProps {
- open: boolean;
- setOpen: React.Dispatch>;
selected?: IProjectAccess;
accesses: IProjectAccess[];
+ users: IUser[];
+ groups: IGroup[];
roles: IProjectRole[];
- entityType: string;
}
export const ProjectAccessAssign = ({
- open,
- setOpen,
selected,
accesses,
+ users,
+ groups,
roles,
- entityType,
}: IProjectAccessAssignProps) => {
+ const { uiConfig } = useUiConfig();
+ const { flags } = uiConfig;
+ const entityType = flags.UG ? 'user / group' : 'user';
+
const projectId = useRequiredPathParam('projectId');
const { refetchProjectAccess } = useProjectAccess(projectId);
const { addAccessToProject, changeUserRole, changeGroupRole, loading } =
useProjectApi();
- const { users, groups } = useAccess();
const edit = Boolean(selected);
const { setToastData, setToastApiError } = useToast();
- const { uiConfig } = useUiConfig();
+ const navigate = useNavigate();
- const [selectedOptions, setSelectedOptions] = useState([]);
+ const options = [
+ ...groups
+ .filter(
+ (group: IGroup) =>
+ edit ||
+ !accesses.some(
+ ({ entity: { id }, type }) =>
+ group.id === id && type === ENTITY_TYPE.GROUP
+ )
+ )
+ .map((group: IGroup) => ({
+ id: group.id,
+ entity: group,
+ type: ENTITY_TYPE.GROUP,
+ })),
+ ...users
+ .filter(
+ (user: IUser) =>
+ edit ||
+ !accesses.some(
+ ({ entity: { id }, type }) =>
+ user.id === id && type === ENTITY_TYPE.USER
+ )
+ )
+ .map((user: IUser) => ({
+ id: user.id,
+ entity: user,
+ type: ENTITY_TYPE.USER,
+ })),
+ ];
+
+ const [selectedOptions, setSelectedOptions] = useState(
+ () =>
+ options.filter(
+ ({ id, type }) =>
+ id === selected?.entity.id && type === selected?.type
+ )
+ );
const [role, setRole] = useState(
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
- .filter(
- (group: IGroup) =>
- edit ||
- !accesses.some(
- ({ entity: { id }, type }) =>
- group.id === id && type === ENTITY_TYPE.GROUP
- )
- )
- .map((group: IGroup) => ({
- id: group.id,
- entity: group,
- type: ENTITY_TYPE.GROUP,
- })),
- ...users
- .filter(
- (user: IUser) =>
- edit ||
- !accesses.some(
- ({ entity: { id }, type }) =>
- user.id === id && type === ENTITY_TYPE.USER
- )
- )
- .map((user: IUser) => ({
- id: user.id,
- entity: user,
- type: ENTITY_TYPE.USER,
- })),
- ],
- [users, accesses, edit, groups]
- );
-
- useEffect(() => {
- const selectedOption =
- options.filter(
- ({ id, type }) =>
- id === selected?.entity.id && type === selected?.type
- ) || [];
- setSelectedOptions(selectedOption);
- setRole(roles.find(({ id }) => id === selected?.entity.roleId) || null);
- }, [open, selected, options, roles]);
+ const 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) => {
e.preventDefault();
@@ -193,7 +181,7 @@ export const ProjectAccessAssign = ({
await changeGroupRole(projectId, role.id, selected.entity.id);
}
refetchProjectAccess();
- setOpen(false);
+ navigate(GO_BACK);
setToastData({
title: `${selectedOptions.length} ${
selectedOptions.length === 1 ? 'access' : 'accesses'
@@ -277,10 +265,8 @@ export const ProjectAccessAssign = ({
return (
{
- setOpen(false);
- }}
+ open
+ onClose={() => navigate(GO_BACK)}
label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`}
>
Assign {entityType}
- {
- setOpen(false);
- }}
- >
+ navigate(GO_BACK)}>
Cancel
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate.tsx
new file mode 100644
index 0000000000..72162d7a60
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate.tsx
@@ -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 (
+
+ );
+};
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup.tsx
new file mode 100644
index 0000000000..17508ebcb1
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup.tsx
@@ -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 (
+
+ );
+};
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser.tsx
new file mode 100644
index 0000000000..3c8897195b
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser.tsx
@@ -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 (
+
+ );
+};
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx
index f6c4b01535..01ddcb5828 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx
@@ -15,14 +15,19 @@ import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useSearch } from 'hooks/useSearch';
-import { useSearchParams } from 'react-router-dom';
+import {
+ Link,
+ Route,
+ Routes,
+ useNavigate,
+ useSearchParams,
+} from 'react-router-dom';
import { createLocalStorage } from 'utils/createLocalStorage';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Search } from 'component/common/Search/Search';
-import { ProjectAccessAssign } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useToast from 'hooks/useToast';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
@@ -33,6 +38,9 @@ import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
+import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate';
+import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser';
+import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
export type PageQueryType = Partial<
Record<'sort' | 'order' | 'search', string>
@@ -52,44 +60,17 @@ export const ProjectAccessTable: VFC = () => {
const { flags } = uiConfig;
const entityType = flags.UG ? 'user / group' : 'user';
+ const navigate = useNavigate();
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const { setToastData } = useToast();
const { access, refetchProjectAccess } = useProjectAccess(projectId);
const { removeUserFromRole, removeGroupFromRole } = useProjectApi();
- const [assignOpen, setAssignOpen] = useState(false);
const [removeOpen, setRemoveOpen] = useState(false);
const [groupOpen, setGroupOpen] = useState(false);
const [selectedRow, setSelectedRow] = useState();
- useEffect(() => {
- if (!assignOpen && !groupOpen) {
- setSelectedRow(undefined);
- }
- }, [assignOpen, groupOpen]);
-
- const roles = useMemo(
- () => access.roles || [],
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [JSON.stringify(access.roles)]
- );
-
- const mappedData: IProjectAccess[] = useMemo(() => {
- const users = access.users || [];
- const groups = access.groups || [];
- return [
- ...users.map(user => ({
- entity: user,
- type: ENTITY_TYPE.USER,
- })),
- ...groups.map(group => ({
- entity: group,
- type: ENTITY_TYPE.GROUP,
- })),
- ];
- }, [access]);
-
const columns = useMemo(
() => [
{
@@ -145,7 +126,8 @@ export const ProjectAccessTable: VFC = () => {
{
Header: 'Role',
accessor: (row: IProjectAccess) =>
- roles.find(({ id }) => id === row.entity.roleId)?.name,
+ access?.roles.find(({ id }) => id === row.entity.roleId)
+ ?.name,
minWidth: 120,
filterName: 'role',
},
@@ -187,19 +169,23 @@ export const ProjectAccessTable: VFC = () => {
disableSortBy: true,
align: 'center',
maxWidth: 200,
- Cell: ({ row: { original: row } }: any) => (
+ Cell: ({
+ row: { original: row },
+ }: {
+ row: { original: IProjectAccess };
+ }) => (
{
- setSelectedRow(row);
- setAssignOpen(true);
- }}
- disabled={mappedData.length === 1}
+ to={`edit/${
+ row.type === ENTITY_TYPE.USER ? 'user' : 'group'
+ }/${row.entity.id}`}
+ disabled={access?.rows.length === 1}
tooltipProps={{
title:
- mappedData.length === 1
+ access?.rows.length === 1
? 'Cannot edit access. A project must have at least one owner'
: 'Edit access',
}}
@@ -213,10 +199,10 @@ export const ProjectAccessTable: VFC = () => {
setSelectedRow(row);
setRemoveOpen(true);
}}
- disabled={mappedData.length === 1}
+ disabled={access?.rows.length === 1}
tooltipProps={{
title:
- mappedData.length === 1
+ access?.rows.length === 1
? 'Cannot remove access. A project must have at least one owner'
: 'Remove access',
}}
@@ -227,7 +213,7 @@ export const ProjectAccessTable: VFC = () => {
),
},
],
- [roles, mappedData.length, projectId]
+ [access, projectId]
);
const [searchParams, setSearchParams] = useSearchParams();
@@ -247,7 +233,7 @@ export const ProjectAccessTable: VFC = () => {
const { data, getSearchText, getSearchContext } = useSearch(
columns,
searchValue,
- mappedData ?? []
+ access?.rows ?? []
);
const {
@@ -319,7 +305,6 @@ export const ProjectAccessTable: VFC = () => {
});
}
setRemoveOpen(false);
- setSelectedRow(undefined);
};
return (
{
}
/>
setAssignOpen(true)}
>
Assign {entityType}
@@ -399,19 +385,21 @@ export const ProjectAccessTable: VFC = () => {
/>
}
/>
-
+
+ } />
+ }
+ />
+ }
+ />
+
removeAccess(selectedRow)}
onClose={() => {
- setSelectedRow(undefined);
setRemoveOpen(false);
}}
title={`Really remove ${entityType} from this project?`}
@@ -422,12 +410,12 @@ export const ProjectAccessTable: VFC = () => {
group={selectedRow?.entity as IGroup}
projectId={projectId}
subtitle={`Role: ${
- roles.find(({ id }) => id === selectedRow?.entity.roleId)
- ?.name
+ access?.roles.find(
+ ({ id }) => id === selectedRow?.entity.roleId
+ )?.name
}`}
onEdit={() => {
- setAssignOpen(true);
- console.log('Assign Open true');
+ navigate(`edit/group/${selectedRow?.entity.id}`);
}}
onRemove={() => {
setGroupOpen(false);
diff --git a/frontend/src/component/project/ProjectEnvironment/ProjectEnvironment.tsx b/frontend/src/component/project/ProjectEnvironment/ProjectEnvironment.tsx
index 18f12f4712..7686110379 100644
--- a/frontend/src/component/project/ProjectEnvironment/ProjectEnvironment.tsx
+++ b/frontend/src/component/project/ProjectEnvironment/ProjectEnvironment.tsx
@@ -8,7 +8,9 @@ import ApiError from 'component/common/ApiError/ApiError';
import useToast from 'hooks/useToast';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
-import useProject from 'hooks/api/getters/useProject/useProject';
+import useProject, {
+ useProjectNameOrId,
+} from 'hooks/api/getters/useProject/useProject';
import { FormControlLabel, FormGroup, Alert } from '@mui/material';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDisableConfirm';
@@ -19,17 +21,13 @@ import { getEnabledEnvs } from './helpers';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useThemeStyles } from 'themes/themeStyles';
import { usePageTitle } from 'hooks/usePageTitle';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
-interface IProjectEnvironmentListProps {
- projectId: string;
- projectName: string;
-}
-
-const ProjectEnvironmentList = ({
- projectId,
- projectName,
-}: IProjectEnvironmentListProps) => {
+const ProjectEnvironmentList = () => {
+ const projectId = useRequiredPathParam('projectId');
+ const projectName = useProjectNameOrId(projectId);
usePageTitle(`Project environments – ${projectName}`);
+
// api state
const [envs, setEnvs] = useState([]);
const { setToastData, setToastApiError } = useToast();
diff --git a/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx b/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx
index 69e44a8e32..935479a596 100644
--- a/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx
+++ b/frontend/src/component/strategies/CreateStrategy/CreateStrategy.tsx
@@ -9,6 +9,7 @@ import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesAp
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { formatUnknownError } from 'utils/formatUnknownError';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
+import { GO_BACK } from 'constants/navigate';
export const CreateStrategy = () => {
const { setToastData, setToastApiError } = useToast();
@@ -64,7 +65,7 @@ export const CreateStrategy = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx b/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
index bb21258856..e5c9e30bc5 100644
--- a/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
+++ b/frontend/src/component/strategies/EditStrategy/EditStrategy.tsx
@@ -11,6 +11,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import useStrategy from 'hooks/api/getters/useStrategy/useStrategy';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
export const EditStrategy = () => {
const { setToastData, setToastApiError } = useToast();
@@ -68,7 +69,7 @@ export const EditStrategy = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/tags/CreateTagType/CreateTagType.tsx b/frontend/src/component/tags/CreateTagType/CreateTagType.tsx
index 67f4a1b94c..9f5ae6f702 100644
--- a/frontend/src/component/tags/CreateTagType/CreateTagType.tsx
+++ b/frontend/src/component/tags/CreateTagType/CreateTagType.tsx
@@ -8,6 +8,7 @@ import useTagTypesApi from 'hooks/api/actions/useTagTypesApi/useTagTypesApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
+import { GO_BACK } from 'constants/navigate';
const CreateTagType = () => {
const { setToastData, setToastApiError } = useToast();
@@ -55,7 +56,7 @@ const CreateTagType = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/component/tags/EditTagType/EditTagType.tsx b/frontend/src/component/tags/EditTagType/EditTagType.tsx
index a39cd41f51..5e9a0e841e 100644
--- a/frontend/src/component/tags/EditTagType/EditTagType.tsx
+++ b/frontend/src/component/tags/EditTagType/EditTagType.tsx
@@ -10,6 +10,7 @@ import useToast from 'hooks/useToast';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { GO_BACK } from 'constants/navigate';
const EditTagType = () => {
const { setToastData, setToastApiError } = useToast();
@@ -54,7 +55,7 @@ const EditTagType = () => {
};
const handleCancel = () => {
- navigate(-1);
+ navigate(GO_BACK);
};
return (
diff --git a/frontend/src/constants/navigate.ts b/frontend/src/constants/navigate.ts
new file mode 100644
index 0000000000..d8148b22b2
--- /dev/null
+++ b/frontend/src/constants/navigate.ts
@@ -0,0 +1 @@
+export const GO_BACK = -1;
diff --git a/frontend/src/hooks/api/getters/useAccess/useAccess.ts b/frontend/src/hooks/api/getters/useAccess/useAccess.ts
index b7be982177..8fc0970f63 100644
--- a/frontend/src/hooks/api/getters/useAccess/useAccess.ts
+++ b/frontend/src/hooks/api/getters/useAccess/useAccess.ts
@@ -1,24 +1,30 @@
import useSWR from 'swr';
-import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
+import { IGroup } from 'interfaces/group';
+import { IUser } from 'interfaces/user';
-export const useAccess = () => {
+export interface IUseAccessOutput {
+ users?: IUser[];
+ groups?: IGroup[];
+ loading: boolean;
+ refetch: () => void;
+ error?: Error;
+}
+
+export const useAccess = (): IUseAccessOutput => {
const { data, error, mutate } = useSWR(
formatApiPath(`api/admin/user-admin/access`),
fetcher
);
- return useMemo(
- () => ({
- users: data?.users ?? [],
- groups: data?.groups ?? [],
- loading: !error && !data,
- refetch: () => mutate(),
- error,
- }),
- [data, error, mutate]
- );
+ return {
+ users: data?.users,
+ groups: data?.groups,
+ loading: !error && !data,
+ refetch: () => mutate(),
+ error,
+ };
};
const fetcher = (path: string) => {
diff --git a/frontend/src/hooks/api/getters/useProject/useProject.ts b/frontend/src/hooks/api/getters/useProject/useProject.ts
index 51f9fd1633..a7977e10fe 100644
--- a/frontend/src/hooks/api/getters/useProject/useProject.ts
+++ b/frontend/src/hooks/api/getters/useProject/useProject.ts
@@ -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;
diff --git a/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts b/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts
index 445d74201f..0cd09e96a8 100644
--- a/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts
+++ b/frontend/src/hooks/api/getters/useProjectAccess/useProjectAccess.ts
@@ -1,5 +1,5 @@
import useSWR, { mutate, SWRConfiguration } from 'swr';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IProjectRole } from 'interfaces/role';
@@ -29,6 +29,7 @@ export interface IProjectAccessOutput {
users: IProjectAccessUser[];
groups: IProjectAccessGroup[];
roles: IProjectRole[];
+ rows: IProjectAccess[];
}
const useProjectAccess = (
@@ -58,23 +59,44 @@ const useProjectAccess = (
setLoading(!error && !data);
}, [data, error]);
- let access: IProjectAccessOutput = data
- ? {
- roles: data.roles,
- users: data.users,
- groups:
- data?.groups.map((group: any) => ({
- ...group,
- users: mapGroupUsers(group.users ?? []),
- })) ?? [],
- }
- : { roles: [], users: [], groups: [] };
+ const access: IProjectAccessOutput | undefined = useMemo(() => {
+ if (data) {
+ return formatAccessData({
+ roles: data.roles,
+ users: data.users,
+ groups:
+ data?.groups.map((group: any) => ({
+ ...group,
+ users: mapGroupUsers(group.users ?? []),
+ })) ?? [],
+ });
+ }
+ }, [data]);
+
return {
- access: access,
+ access,
error,
loading,
refetchProjectAccess,
};
};
+const formatAccessData = (access: any): IProjectAccessOutput => {
+ const users = access.users || [];
+ const groups = access.groups || [];
+ return {
+ ...access,
+ rows: [
+ ...users.map((user: any) => ({
+ entity: user,
+ type: ENTITY_TYPE.USER,
+ })),
+ ...groups.map((group: any) => ({
+ entity: group,
+ type: ENTITY_TYPE.GROUP,
+ })),
+ ],
+ };
+};
+
export default useProjectAccess;
diff --git a/frontend/src/utils/testIds.ts b/frontend/src/utils/testIds.ts
index b0dfb8e10a..fdf54f584c 100644
--- a/frontend/src/utils/testIds.ts
+++ b/frontend/src/utils/testIds.ts
@@ -1,5 +1,6 @@
/* NAVIGATION */
export const NAVIGATE_TO_CREATE_FEATURE = 'NAVIGATE_TO_CREATE_FEATURE';
+export const NAVIGATE_TO_CREATE_GROUP = 'NAVIGATE_TO_CREATE_GROUP';
export const NAVIGATE_TO_CREATE_SEGMENT = 'NAVIGATE_TO_CREATE_SEGMENT';
export const CREATE_API_TOKEN_BUTTON = 'CREATE_API_TOKEN_BUTTON';
@@ -12,7 +13,17 @@ export const CF_CREATE_BTN_ID = 'CF_CREATE_BTN_ID';
/* CREATE GROUP */
export const UG_NAME_ID = 'UG_NAME_ID';
export const UG_DESC_ID = 'UG_DESC_ID';
+export const UG_USERS_ID = 'UG_USERS_ID';
+export const UG_USERS_ADD_ID = 'UG_USERS_ADD_ID';
+export const UG_USERS_TABLE_ROLE_ID = 'UG_USERS_TABLE_ROLE_ID';
export const UG_CREATE_BTN_ID = 'UG_CREATE_BTN_ID';
+export const UG_SAVE_BTN_ID = 'UG_SAVE_BTN_ID';
+export const UG_EDIT_BTN_ID = 'UG_EDIT_BTN_ID';
+export const UG_DELETE_BTN_ID = 'UG_DELETE_BTN_ID';
+export const UG_ADD_USER_BTN_ID = 'UG_ADD_USER_BTN_ID';
+export const UG_EDIT_USER_BTN_ID = 'UG_EDIT_USER_BTN_ID';
+export const UG_REMOVE_USER_BTN_ID = 'UG_REMOVE_USER_BTN_ID';
+export const UG_USERS_ROLE_ID = 'UG_USERS_ROLE_ID';
/* SEGMENT */
export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID';
@@ -55,3 +66,4 @@ export const SIDEBAR_MODAL_ID = 'SIDEBAR_MODAL_ID';
export const AUTH_PAGE_ID = 'AUTH_PAGE_ID';
export const ANNOUNCER_ELEMENT_TEST_ID = 'ANNOUNCER_ELEMENT_TEST_ID';
export const INSTANCE_STATUS_BAR_ID = 'INSTANCE_STATUS_BAR_ID';
+export const TOAST_TEXT = 'TOAST_TEXT';