1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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:
Nuno Góis 2022-08-04 12:57:25 +01:00 committed by GitHub
parent 59c8822cf2
commit 672a3f0b92
48 changed files with 584 additions and 296 deletions

View 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 }}

View 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');
});
});

View File

@ -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 => {

View File

@ -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 (

View File

@ -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 (

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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={() => {

View File

@ -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>

View File

@ -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 =>

View File

@ -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>

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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>

View File

@ -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)}
/> />
); );
}; };

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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",
}, },

View File

@ -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,

View File

@ -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 (

View File

@ -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) && (

View File

@ -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>
); );
}; };

View File

@ -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} />;

View File

@ -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(

View File

@ -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}`);

View File

@ -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}`);

View File

@ -1,4 +1,4 @@
import React, { FormEvent, useEffect, useMemo, useState } from 'react'; import React, { FormEvent, useState } from 'react';
import { import {
Autocomplete, Autocomplete,
Button, Button,
@ -25,7 +25,8 @@ import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group'; import { IGroup } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ProjectRoleDescription } from './ProjectRoleDescription/ProjectRoleDescription'; import { ProjectRoleDescription } from './ProjectRoleDescription/ProjectRoleDescription';
import { useAccess } from '../../../../hooks/api/getters/useAccess/useAccess'; import { useNavigate } from 'react-router-dom';
import { GO_BACK } from 'constants/navigate';
const StyledForm = styled('form')(() => ({ const StyledForm = styled('form')(() => ({
display: 'flex', display: 'flex',
@ -88,96 +89,83 @@ interface IAccessOption {
} }
interface IProjectAccessAssignProps { interface IProjectAccessAssignProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
selected?: IProjectAccess; selected?: IProjectAccess;
accesses: IProjectAccess[]; accesses: IProjectAccess[];
users: IUser[];
groups: IGroup[];
roles: IProjectRole[]; roles: IProjectRole[];
entityType: string;
} }
export const ProjectAccessAssign = ({ export const ProjectAccessAssign = ({
open,
setOpen,
selected, selected,
accesses, accesses,
users,
groups,
roles, roles,
entityType,
}: IProjectAccessAssignProps) => { }: IProjectAccessAssignProps) => {
const { uiConfig } = useUiConfig();
const { flags } = uiConfig;
const entityType = flags.UG ? 'user / group' : 'user';
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { refetchProjectAccess } = useProjectAccess(projectId); const { refetchProjectAccess } = useProjectAccess(projectId);
const { addAccessToProject, changeUserRole, changeGroupRole, loading } = const { addAccessToProject, changeUserRole, changeGroupRole, loading } =
useProjectApi(); useProjectApi();
const { users, groups } = useAccess();
const edit = Boolean(selected); const edit = Boolean(selected);
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const navigate = useNavigate();
const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>([]); const options = [
...groups
.filter(
(group: IGroup) =>
edit ||
!accesses.some(
({ entity: { id }, type }) =>
group.id === id && type === ENTITY_TYPE.GROUP
)
)
.map((group: IGroup) => ({
id: group.id,
entity: group,
type: ENTITY_TYPE.GROUP,
})),
...users
.filter(
(user: IUser) =>
edit ||
!accesses.some(
({ entity: { id }, type }) =>
user.id === id && type === ENTITY_TYPE.USER
)
)
.map((user: IUser) => ({
id: user.id,
entity: user,
type: ENTITY_TYPE.USER,
})),
];
const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>(
() =>
options.filter(
({ id, type }) =>
id === selected?.entity.id && type === selected?.type
)
);
const [role, setRole] = useState<IProjectRole | null>( const [role, setRole] = useState<IProjectRole | null>(
roles.find(({ id }) => id === selected?.entity.roleId) ?? null roles.find(({ id }) => id === selected?.entity.roleId) ?? null
); );
useEffect(() => { const payload = {
setRole(roles.find(({ id }) => id === selected?.entity.roleId) ?? null); users: selectedOptions
}, [roles, selected]); ?.filter(({ type }) => type === ENTITY_TYPE.USER)
.map(({ id }) => ({ id })),
const payload = useMemo( groups: selectedOptions
() => ({ ?.filter(({ type }) => type === ENTITY_TYPE.GROUP)
users: selectedOptions .map(({ id }) => ({ id })),
?.filter(({ type }) => type === ENTITY_TYPE.USER) };
.map(({ id }) => ({ id })),
groups: selectedOptions
?.filter(({ type }) => type === ENTITY_TYPE.GROUP)
.map(({ id }) => ({ id })),
}),
[selectedOptions]
);
const options = useMemo(
() => [
...groups
.filter(
(group: IGroup) =>
edit ||
!accesses.some(
({ entity: { id }, type }) =>
group.id === id && type === ENTITY_TYPE.GROUP
)
)
.map((group: IGroup) => ({
id: group.id,
entity: group,
type: ENTITY_TYPE.GROUP,
})),
...users
.filter(
(user: IUser) =>
edit ||
!accesses.some(
({ entity: { id }, type }) =>
user.id === id && type === ENTITY_TYPE.USER
)
)
.map((user: IUser) => ({
id: user.id,
entity: user,
type: ENTITY_TYPE.USER,
})),
],
[users, accesses, edit, groups]
);
useEffect(() => {
const selectedOption =
options.filter(
({ id, type }) =>
id === selected?.entity.id && type === selected?.type
) || [];
setSelectedOptions(selectedOption);
setRole(roles.find(({ id }) => id === selected?.entity.roleId) || null);
}, [open, selected, options, roles]);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@ -193,7 +181,7 @@ export const ProjectAccessAssign = ({
await changeGroupRole(projectId, role.id, selected.entity.id); await changeGroupRole(projectId, role.id, selected.entity.id);
} }
refetchProjectAccess(); refetchProjectAccess();
setOpen(false); navigate(GO_BACK);
setToastData({ setToastData({
title: `${selectedOptions.length} ${ title: `${selectedOptions.length} ${
selectedOptions.length === 1 ? 'access' : 'accesses' selectedOptions.length === 1 ? 'access' : 'accesses'
@ -277,10 +265,8 @@ export const ProjectAccessAssign = ({
return ( return (
<SidebarModal <SidebarModal
open={open} open
onClose={() => { onClose={() => navigate(GO_BACK)}
setOpen(false);
}}
label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`} label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`}
> >
<FormTemplate <FormTemplate
@ -373,11 +359,7 @@ export const ProjectAccessAssign = ({
> >
Assign {entityType} Assign {entityType}
</Button> </Button>
<StyledCancelButton <StyledCancelButton onClick={() => navigate(GO_BACK)}>
onClick={() => {
setOpen(false);
}}
>
Cancel Cancel
</StyledCancelButton> </StyledCancelButton>
</StyledButtonContainer> </StyledButtonContainer>

View File

@ -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}
/>
);
};

View File

@ -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}
/>
);
};

View File

@ -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}
/>
);
};

View File

@ -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);

View File

@ -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();

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -0,0 +1 @@
export const GO_BACK = -1;

View File

@ -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) => {

View File

@ -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;

View File

@ -1,5 +1,5 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { IProjectRole } from 'interfaces/role'; import { IProjectRole } from 'interfaces/role';
@ -29,6 +29,7 @@ export interface IProjectAccessOutput {
users: IProjectAccessUser[]; users: IProjectAccessUser[];
groups: IProjectAccessGroup[]; groups: IProjectAccessGroup[];
roles: IProjectRole[]; roles: IProjectRole[];
rows: IProjectAccess[];
} }
const useProjectAccess = ( const useProjectAccess = (
@ -58,23 +59,44 @@ const useProjectAccess = (
setLoading(!error && !data); setLoading(!error && !data);
}, [data, error]); }, [data, error]);
let access: IProjectAccessOutput = data const access: IProjectAccessOutput | undefined = useMemo(() => {
? { if (data) {
roles: data.roles, return formatAccessData({
users: data.users, roles: data.roles,
groups: users: data.users,
data?.groups.map((group: any) => ({ groups:
...group, data?.groups.map((group: any) => ({
users: mapGroupUsers(group.users ?? []), ...group,
})) ?? [], users: mapGroupUsers(group.users ?? []),
} })) ?? [],
: { roles: [], users: [], groups: [] }; });
}
}, [data]);
return { return {
access: access, access,
error, error,
loading, loading,
refetchProjectAccess, refetchProjectAccess,
}; };
}; };
const formatAccessData = (access: any): IProjectAccessOutput => {
const users = access.users || [];
const groups = access.groups || [];
return {
...access,
rows: [
...users.map((user: any) => ({
entity: user,
type: ENTITY_TYPE.USER,
})),
...groups.map((group: any) => ({
entity: group,
type: ENTITY_TYPE.GROUP,
})),
],
};
};
export default useProjectAccess; export default useProjectAccess;

View File

@ -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';