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,
} from './AddonForm.styles';
import { useTheme } from '@mui/system';
import { GO_BACK } from 'constants/navigate';
interface IAddonFormProps {
provider?: IAddonProvider;
@ -168,7 +169,7 @@ export const AddonForm: VFC<IAddonFormProps> = ({
};
const onCancel = () => {
navigate(-1);
navigate(GO_BACK);
};
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {

View File

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

View File

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

View File

@ -10,6 +10,8 @@ import { Button } from '@mui/material';
import { EDIT } from 'constants/misc';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
import { UG_SAVE_BTN_ID } from 'utils/testIds';
import { GO_BACK } from 'constants/navigate';
export const EditGroup = () => {
const groupId = Number(useRequiredPathParam('groupId'));
@ -40,7 +42,7 @@ export const EditGroup = () => {
try {
await updateGroup(groupId, payload);
refetchGroup();
navigate(-1);
navigate(GO_BACK);
setToastData({
title: 'Group updated successfully',
type: 'success',
@ -60,7 +62,7 @@ export const EditGroup = () => {
};
const handleCancel = () => {
navigate(-1);
navigate(GO_BACK);
};
return (
@ -85,7 +87,12 @@ export const EditGroup = () => {
mode={EDIT}
clearErrors={clearErrors}
>
<Button type="submit" variant="contained" color="primary">
<Button
type="submit"
variant="contained"
color="primary"
data-testid={UG_SAVE_BTN_ID}
>
Save
</Button>
</GroupForm>

View File

@ -10,6 +10,7 @@ import { FC, FormEvent, useEffect, useMemo, useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect';
import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable';
import { UG_SAVE_BTN_ID } from 'utils/testIds';
const StyledForm = styled('form')(() => ({
display: 'flex',
@ -142,6 +143,7 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
type="submit"
variant="contained"
color="primary"
data-testid={UG_SAVE_BTN_ID}
>
Save
</Button>

View File

@ -8,6 +8,7 @@ import useToast from 'hooks/useToast';
import { IGroup, IGroupUser, Role } from 'interfaces/group';
import { FC, FormEvent, useEffect, useMemo, useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import { UG_SAVE_BTN_ID, UG_USERS_ROLE_ID } from 'utils/testIds';
const StyledForm = styled('form')(() => ({
display: 'flex',
@ -143,6 +144,7 @@ export const EditGroupUser: FC<IEditGroupUserProps> = ({
Assign the role the user should have in this group
</StyledInputDescription>
<StyledSelect
data-testid={UG_USERS_ROLE_ID}
size="small"
value={role}
onChange={event =>
@ -159,6 +161,7 @@ export const EditGroupUser: FC<IEditGroupUserProps> = ({
<StyledButtonContainer>
<Button
data-testid={UG_SAVE_BTN_ID}
type="submit"
variant="contained"
color="primary"

View File

@ -37,6 +37,13 @@ import { AddGroupUser } from './AddGroupUser/AddGroupUser';
import { EditGroupUser } from './EditGroupUser/EditGroupUser';
import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import {
UG_EDIT_BTN_ID,
UG_DELETE_BTN_ID,
UG_ADD_USER_BTN_ID,
UG_EDIT_USER_BTN_ID,
UG_REMOVE_USER_BTN_ID,
} from 'utils/testIds';
const StyledEdit = styled(Edit)(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
@ -134,6 +141,7 @@ export const Group: VFC = () => {
<ActionCell>
<Tooltip title="Edit user" arrow describeChild>
<IconButton
data-testid={`${UG_EDIT_USER_BTN_ID}-${rowUser.id}`}
onClick={() => {
setSelectedUser(rowUser);
setEditUserOpen(true);
@ -148,6 +156,7 @@ export const Group: VFC = () => {
describeChild
>
<IconButton
data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`}
onClick={() => {
setSelectedUser(rowUser);
setRemoveUserOpen(true);
@ -240,6 +249,7 @@ export const Group: VFC = () => {
actions={
<>
<PermissionIconButton
data-testid={UG_EDIT_BTN_ID}
to={`/admin/groups/${groupId}/edit`}
component={Link}
data-loading
@ -251,6 +261,7 @@ export const Group: VFC = () => {
<StyledEdit />
</PermissionIconButton>
<PermissionIconButton
data-testid={UG_DELETE_BTN_ID}
data-loading
onClick={() => setRemoveOpen(true)}
permission={ADMIN}
@ -296,6 +307,7 @@ export const Group: VFC = () => {
}
/>
<Button
data-testid={UG_ADD_USER_BTN_ID}
variant="contained"
color="primary"
onClick={() => {

View File

@ -11,6 +11,7 @@ import { IUser } from 'interfaces/user';
import { useMemo, useState, VFC } from 'react';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { IGroupUser, Role } from 'interfaces/group';
import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds';
const StyledOption = styled('div')(({ theme }) => ({
display: 'flex',
@ -83,6 +84,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
return (
<StyledGroupFormUsersSelect>
<Autocomplete
data-testid={UG_USERS_ID}
size="small"
multiple
limitTags={10}
@ -113,7 +115,11 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
<TextField {...params} label="Select users" />
)}
/>
<Button variant="outlined" onClick={onAdd}>
<Button
variant="outlined"
onClick={onAdd}
data-testid={UG_USERS_ADD_ID}
>
Add
</Button>
</StyledGroupFormUsersSelect>

View File

@ -4,6 +4,7 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Role } from 'interfaces/group';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
import { UG_USERS_TABLE_ROLE_ID } from 'utils/testIds';
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
@ -36,6 +37,7 @@ export const GroupUserRoleCell = ({
condition={Boolean(onChange)}
show={
<Select
data-testid={UG_USERS_TABLE_ROLE_ID}
size="small"
value={value}
onChange={event =>

View File

@ -12,6 +12,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC
import { TablePlaceholder } from 'component/common/Table';
import { GroupCard } from './GroupCard/GroupCard';
import { GroupEmpty } from './GroupEmpty/GroupEmpty';
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
type PageQueryType = Partial<Record<'search', string>>;
@ -85,6 +86,7 @@ export const GroupsList: VFC = () => {
component={Link}
variant="contained"
color="primary"
data-testid={NAVIGATE_TO_CREATE_GROUP}
>
New group
</Button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import UIContext from 'contexts/UIContext';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Close from '@mui/icons-material/Close';
import { IToast } from 'interfaces/toast';
import { TOAST_TEXT } from 'utils/testIds';
const Toast = ({ title, text, type, confetti }: IToast) => {
const { setToast } = useContext(UIContext);
@ -72,7 +73,9 @@ const Toast = ({ title, text, type, confetti }: IToast) => {
<ConditionallyRender
condition={Boolean(text)}
show={<p>{text}</p>}
show={
<p data-testid={TOAST_TEXT}>{text}</p>
}
/>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { useNavigate } from 'react-router-dom';
import { CreateUnleashContext } from 'component/context/CreateUnleashContext/CreateUnleashContext';
import { GO_BACK } from 'constants/navigate';
export const CreateUnleashContextPage = () => {
const navigate = useNavigate();
@ -7,7 +8,7 @@ export const CreateUnleashContextPage = () => {
return (
<CreateUnleashContext
onSubmit={() => navigate('/context')}
onCancel={() => navigate(-1)}
onCancel={() => navigate(GO_BACK)}
/>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,80 +16,54 @@ import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Routes, Route, useLocation } from 'react-router-dom';
const Project = () => {
const projectId = useRequiredPathParam('projectId');
const activeTab = useOptionalPathParam('activeTab');
const params = useQueryParams();
const { project, error, loading, refetch } = useProject(projectId);
const ref = useLoading(loading);
const { setToastData } = useToast();
const { classes: styles } = useStyles();
const navigate = useNavigate();
const { pathname } = useLocation();
const { isOss } = useUiConfig();
const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId;
const tabData = [
const tabs = [
{
title: 'Overview',
component: (
<ProjectOverview
projectId={projectId}
projectName={projectName}
/>
),
path: basePath,
name: 'overview',
},
{
title: 'Health',
component: (
<ProjectHealth
projectId={projectId}
projectName={projectName}
/>
),
path: `${basePath}/health`,
name: 'health',
},
{
title: 'Access',
component: <ProjectAccess projectName={projectName} />,
path: `${basePath}/access`,
name: 'access',
},
{
title: 'Environments',
component: (
<ProjectEnvironment
projectId={projectId}
projectName={projectName}
/>
),
path: `${basePath}/environments`,
name: 'environments',
},
{
title: 'Archive',
component: (
<ProjectFeaturesArchive
projectId={projectId}
projectName={projectName}
/>
),
path: `${basePath}/archive`,
name: 'archive',
},
];
const activeTabIdx = activeTab
? tabData.findIndex(tab => tab.name === activeTab)
: 0;
const activeTab = [...tabs]
.reverse()
.find(tab => pathname.startsWith(tab.path));
useEffect(() => {
const created = params.get('created');
@ -107,13 +81,13 @@ const Project = () => {
}, []);
const renderTabs = () => {
return tabData.map((tab, index) => {
return tabs.map(tab => {
return (
<Tab
data-loading
key={tab.title}
id={`tab-${index}`}
aria-controls={`tabpanel-${index}`}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
className={styles.tabButton}
/>
@ -121,16 +95,6 @@ const Project = () => {
});
};
const renderTabContent = () => {
return tabData.map((tab, index) => {
return (
<TabPanel value={activeTabIdx} index={index} key={tab.path}>
{tab.component}
</TabPanel>
);
});
};
return (
<div ref={ref}>
<div className={styles.header}>
@ -167,7 +131,7 @@ const Project = () => {
<div className={styles.separator} />
<div className={styles.tabContainer}>
<Tabs
value={activeTabIdx}
value={activeTab?.path}
indicatorColor="primary"
textColor="primary"
>
@ -175,7 +139,13 @@ const Project = () => {
</Tabs>
</div>
</div>
{renderTabContent()}
<Routes>
<Route path="health" element={<ProjectHealth />} />
<Route path="access/*" element={<ProjectAccess />} />
<Route path="environments" element={<ProjectEnvironment />} />
<Route path="archive" element={<ProjectFeaturesArchive />} />
<Route path="*" element={<ProjectOverview />} />
</Routes>
</div>
);
};

View File

@ -1,15 +1,11 @@
import { ProjectFeaturesArchiveTable } from 'component/archive/ProjectFeaturesArchiveTable';
import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
interface IProjectFeaturesArchiveProps {
projectId: string;
projectName: string;
}
export const ProjectFeaturesArchive = ({
projectId,
projectName,
}: IProjectFeaturesArchiveProps) => {
export const ProjectFeaturesArchive = () => {
const projectId = useRequiredPathParam('projectId');
const projectName = useProjectNameOrId(projectId);
usePageTitle(`Project archive ${projectName}`);
return <ProjectFeaturesArchiveTable projectId={projectId} />;

View File

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

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

View File

@ -1,4 +1,4 @@
import { useContext, VFC } from 'react';
import { useContext } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Alert } from '@mui/material';
@ -8,14 +8,11 @@ import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
interface IProjectAccess {
projectName: string;
}
export const ProjectAccess: VFC<IProjectAccess> = ({ projectName }) => {
export const ProjectAccess = () => {
const projectId = useRequiredPathParam('projectId');
const projectName = useProjectNameOrId(projectId);
const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig();
usePageTitle(`Project access ${projectName}`);

View File

@ -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<React.SetStateAction<boolean>>;
selected?: IProjectAccess;
accesses: IProjectAccess[];
users: IUser[];
groups: IGroup[];
roles: IProjectRole[];
entityType: string;
}
export const ProjectAccessAssign = ({
open,
setOpen,
selected,
accesses,
users,
groups,
roles,
entityType,
}: IProjectAccessAssignProps) => {
const { uiConfig } = useUiConfig();
const { flags } = uiConfig;
const entityType = flags.UG ? 'user / group' : 'user';
const projectId = useRequiredPathParam('projectId');
const { refetchProjectAccess } = useProjectAccess(projectId);
const { addAccessToProject, changeUserRole, changeGroupRole, loading } =
useProjectApi();
const { users, groups } = useAccess();
const edit = Boolean(selected);
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
const [selectedOptions, setSelectedOptions] = useState<IAccessOption[]>([]);
const 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>(
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<HTMLFormElement>) => {
e.preventDefault();
@ -193,7 +181,7 @@ export const ProjectAccessAssign = ({
await changeGroupRole(projectId, role.id, selected.entity.id);
}
refetchProjectAccess();
setOpen(false);
navigate(GO_BACK);
setToastData({
title: `${selectedOptions.length} ${
selectedOptions.length === 1 ? 'access' : 'accesses'
@ -277,10 +265,8 @@ export const ProjectAccessAssign = ({
return (
<SidebarModal
open={open}
onClose={() => {
setOpen(false);
}}
open
onClose={() => navigate(GO_BACK)}
label={`${!edit ? 'Assign' : 'Edit'} ${entityType} access`}
>
<FormTemplate
@ -373,11 +359,7 @@ export const ProjectAccessAssign = ({
>
Assign {entityType}
</Button>
<StyledCancelButton
onClick={() => {
setOpen(false);
}}
>
<StyledCancelButton onClick={() => navigate(GO_BACK)}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>

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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useSearch } from 'hooks/useSearch';
import { useSearchParams } from 'react-router-dom';
import {
Link,
Route,
Routes,
useNavigate,
useSearchParams,
} from 'react-router-dom';
import { createLocalStorage } from 'utils/createLocalStorage';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Search } from 'component/common/Search/Search';
import { ProjectAccessAssign } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useToast from 'hooks/useToast';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
@ -33,6 +38,9 @@ import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate';
import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser';
import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
export type PageQueryType = Partial<
Record<'sort' | 'order' | 'search', string>
@ -52,44 +60,17 @@ export const ProjectAccessTable: VFC = () => {
const { flags } = uiConfig;
const entityType = flags.UG ? 'user / group' : 'user';
const navigate = useNavigate();
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const { setToastData } = useToast();
const { access, refetchProjectAccess } = useProjectAccess(projectId);
const { removeUserFromRole, removeGroupFromRole } = useProjectApi();
const [assignOpen, setAssignOpen] = useState(false);
const [removeOpen, setRemoveOpen] = useState(false);
const [groupOpen, setGroupOpen] = useState(false);
const [selectedRow, setSelectedRow] = useState<IProjectAccess>();
useEffect(() => {
if (!assignOpen && !groupOpen) {
setSelectedRow(undefined);
}
}, [assignOpen, groupOpen]);
const roles = useMemo(
() => access.roles || [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(access.roles)]
);
const mappedData: IProjectAccess[] = useMemo(() => {
const users = access.users || [];
const groups = access.groups || [];
return [
...users.map(user => ({
entity: user,
type: ENTITY_TYPE.USER,
})),
...groups.map(group => ({
entity: group,
type: ENTITY_TYPE.GROUP,
})),
];
}, [access]);
const columns = useMemo(
() => [
{
@ -145,7 +126,8 @@ export const ProjectAccessTable: VFC = () => {
{
Header: 'Role',
accessor: (row: IProjectAccess) =>
roles.find(({ id }) => id === row.entity.roleId)?.name,
access?.roles.find(({ id }) => id === row.entity.roleId)
?.name,
minWidth: 120,
filterName: 'role',
},
@ -187,19 +169,23 @@ export const ProjectAccessTable: VFC = () => {
disableSortBy: true,
align: 'center',
maxWidth: 200,
Cell: ({ row: { original: row } }: any) => (
Cell: ({
row: { original: row },
}: {
row: { original: IProjectAccess };
}) => (
<ActionCell>
<PermissionIconButton
component={Link}
permission={UPDATE_PROJECT}
projectId={projectId}
onClick={() => {
setSelectedRow(row);
setAssignOpen(true);
}}
disabled={mappedData.length === 1}
to={`edit/${
row.type === ENTITY_TYPE.USER ? 'user' : 'group'
}/${row.entity.id}`}
disabled={access?.rows.length === 1}
tooltipProps={{
title:
mappedData.length === 1
access?.rows.length === 1
? 'Cannot edit access. A project must have at least one owner'
: 'Edit access',
}}
@ -213,10 +199,10 @@ export const ProjectAccessTable: VFC = () => {
setSelectedRow(row);
setRemoveOpen(true);
}}
disabled={mappedData.length === 1}
disabled={access?.rows.length === 1}
tooltipProps={{
title:
mappedData.length === 1
access?.rows.length === 1
? 'Cannot remove access. A project must have at least one owner'
: 'Remove access',
}}
@ -227,7 +213,7 @@ export const ProjectAccessTable: VFC = () => {
),
},
],
[roles, mappedData.length, projectId]
[access, projectId]
);
const [searchParams, setSearchParams] = useSearchParams();
@ -247,7 +233,7 @@ export const ProjectAccessTable: VFC = () => {
const { data, getSearchText, getSearchContext } = useSearch(
columns,
searchValue,
mappedData ?? []
access?.rows ?? []
);
const {
@ -319,7 +305,6 @@ export const ProjectAccessTable: VFC = () => {
});
}
setRemoveOpen(false);
setSelectedRow(undefined);
};
return (
<PageContent
@ -348,9 +333,10 @@ export const ProjectAccessTable: VFC = () => {
}
/>
<Button
component={Link}
to={`create`}
variant="contained"
color="primary"
onClick={() => setAssignOpen(true)}
>
Assign {entityType}
</Button>
@ -399,19 +385,21 @@ export const ProjectAccessTable: VFC = () => {
/>
}
/>
<ProjectAccessAssign
open={assignOpen}
setOpen={setAssignOpen}
selected={selectedRow}
accesses={mappedData}
roles={roles}
entityType={entityType}
/>
<Routes>
<Route path="create" element={<ProjectAccessCreate />} />
<Route
path="edit/group/:groupId"
element={<ProjectAccessEditGroup />}
/>
<Route
path="edit/user/:userId"
element={<ProjectAccessEditUser />}
/>
</Routes>
<Dialogue
open={removeOpen}
onClick={() => removeAccess(selectedRow)}
onClose={() => {
setSelectedRow(undefined);
setRemoveOpen(false);
}}
title={`Really remove ${entityType} from this project?`}
@ -422,12 +410,12 @@ export const ProjectAccessTable: VFC = () => {
group={selectedRow?.entity as IGroup}
projectId={projectId}
subtitle={`Role: ${
roles.find(({ id }) => id === selectedRow?.entity.roleId)
?.name
access?.roles.find(
({ id }) => id === selectedRow?.entity.roleId
)?.name
}`}
onEdit={() => {
setAssignOpen(true);
console.log('Assign Open true');
navigate(`edit/group/${selectedRow?.entity.id}`);
}}
onRemove={() => {
setGroupOpen(false);

View File

@ -8,7 +8,9 @@ import ApiError from 'component/common/ApiError/ApiError';
import useToast from 'hooks/useToast';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useProject from 'hooks/api/getters/useProject/useProject';
import useProject, {
useProjectNameOrId,
} from 'hooks/api/getters/useProject/useProject';
import { FormControlLabel, FormGroup, Alert } from '@mui/material';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDisableConfirm';
@ -19,17 +21,13 @@ import { getEnabledEnvs } from './helpers';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useThemeStyles } from 'themes/themeStyles';
import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
interface IProjectEnvironmentListProps {
projectId: string;
projectName: string;
}
const ProjectEnvironmentList = ({
projectId,
projectName,
}: IProjectEnvironmentListProps) => {
const ProjectEnvironmentList = () => {
const projectId = useRequiredPathParam('projectId');
const projectName = useProjectNameOrId(projectId);
usePageTitle(`Project environments ${projectName}`);
// api state
const [envs, setEnvs] = useState<IProjectEnvironment[]>([]);
const { setToastData, setToastApiError } = useToast();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;

View File

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

View File

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