Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom root roles #3975

Merged
merged 11 commits into from
Jun 14, 2023
11 changes: 8 additions & 3 deletions frontend/src/component/admin/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import AdminMenu from './menu/AdminMenu';
import { Network } from './network/Network';
import CreateProjectRole from './projectRoles/CreateProjectRole/CreateProjectRole';
import EditProjectRole from './projectRoles/EditProjectRole/EditProjectRole';
import { Roles } from './roles/Roles';
import ProjectRoles from './projectRoles/ProjectRoles/ProjectRoles';
import { ServiceAccounts } from './serviceAccounts/ServiceAccounts';
import CreateUser from './users/CreateUser/CreateUser';
Expand All @@ -27,8 +28,11 @@ export const Admin = () => (
<AdminMenu />
<Routes>
<Route path="users" element={<UsersAdmin />} />
<Route path="create-project-role" element={<CreateProjectRole />} />
<Route path="roles/:id/edit" element={<EditProjectRole />} />
<Route path="project-roles/new" element={<CreateProjectRole />} />
<Route
path="project-roles/:id/edit"
element={<EditProjectRole />}
/>
Comment on lines +31 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should keep the same URL as before for project roles or just add a new one for root-roles? I see pros and cons with both approaches, the main concern is that we are changing the meaning of the existing resource (but this is not on the API level, so this should be fine)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the goal is to eventually merge both root roles and project roles on the same tab, I don't think we should prioritize this for now.

<Route path="api" element={<ApiTokenPage />} />
<Route path="api/create-token" element={<CreateApiToken />} />
<Route path="users/:id/edit" element={<EditUser />} />
Expand All @@ -42,7 +46,8 @@ export const Admin = () => (
element={<EditGroupContainer />}
/>
<Route path="groups/:groupId" element={<Group />} />
<Route path="roles" element={<ProjectRoles />} />
<Route path="roles" element={<Roles />} />
<Route path="project-roles" element={<ProjectRoles />} />
<Route path="instance" element={<InstanceAdmin />} />
<Route path="network/*" element={<Network />} />
<Route path="maintenance" element={<MaintenanceAdmin />} />
Expand Down
28 changes: 8 additions & 20 deletions frontend/src/component/admin/groups/GroupForm/GroupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { Autocomplete, Box, Button, styled, TextField } from '@mui/material';
import { Box, Button, styled } from '@mui/material';
import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds';
import Input from 'component/common/Input/Input';
import { IGroupUser } from 'interfaces/group';
Expand All @@ -10,9 +10,10 @@
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
import { Link } from 'react-router-dom';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { IProjectRole } from 'interfaces/role';
import IRole, { IProjectRole } from 'interfaces/role';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';

const StyledForm = styled('form')(() => ({
display: 'flex',
Expand Down Expand Up @@ -128,13 +129,11 @@

const groupRootRolesEnabled = Boolean(uiConfig.flags.groupRootRoles);

const roleIdToRole = (rootRoleId: number | null): IProjectRole | null => {
return (
roles.find((role: IProjectRole) => role.id === rootRoleId) || null
);
const roleIdToRole = (rootRoleId: number | null): IRole | null => {
return roles.find((role: IRole) => role.id === rootRoleId) || null;
};

const renderRoleOption = (

Check warning on line 136 in frontend/src/component/admin/groups/GroupForm/GroupForm.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

'renderRoleOption' is assigned a value but never used
props: React.HTMLAttributes<HTMLLIElement>,
option: IProjectRole
) => (
Expand Down Expand Up @@ -214,23 +213,12 @@
</Box>
</StyledInputDescription>
<StyledAutocompleteWrapper>
<Autocomplete
<RoleSelect
data-testid="GROUP_ROOT_ROLE"
size="small"
openOnFocus
value={roleIdToRole(rootRole)}
onChange={(_, newValue) =>
setRootRole(newValue?.id || null)
setValue={role =>
setRootRole(role?.id || null)
}
options={roles.filter(
(role: IProjectRole) =>
role.name !== 'Viewer'
)}
renderOption={renderRoleOption}
getOptionLabel={option => option.name}
renderInput={params => (
<TextField {...params} label="Role" />
)}
/>
</StyledAutocompleteWrapper>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
import { Badge } from 'component/common/Badge/Badge';
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import { IProjectRole } from 'interfaces/role';
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';

const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
Expand Down Expand Up @@ -86,14 +86,12 @@ const InfoBadgeDescription = styled('span')(({ theme }) => ({

interface IGroupCardProps {
group: IGroup;
rootRoles: IProjectRole[];
onEditUsers: (group: IGroup) => void;
onRemoveGroup: (group: IGroup) => void;
}

export const GroupCard = ({
group,
rootRoles,
onEditUsers,
onRemoveGroup,
}: IGroupCardProps) => {
Expand All @@ -117,17 +115,7 @@ export const GroupCard = ({
show={
<InfoBadgeDescription>
<p>Root role:</p>
<Badge
color="success"
icon={<TopicOutlinedIcon />}
>
{
rootRoles.find(
(role: IProjectRole) =>
role.id === group.rootRole
)?.name
}
</Badge>
<RoleBadge roleId={group.rootRole!} />
</InfoBadgeDescription>
}
/>
Expand Down
8 changes: 0 additions & 8 deletions frontend/src/component/admin/groups/GroupsList/GroupsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import { Add } from '@mui/icons-material';
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { IProjectRole } from 'interfaces/role';

type PageQueryType = Partial<Record<'search', string>>;

Expand Down Expand Up @@ -51,7 +49,6 @@ export const GroupsList: VFC = () => {
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || ''
);
const { roles } = useUsers();

const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));

Expand Down Expand Up @@ -85,10 +82,6 @@ export const GroupsList: VFC = () => {
setRemoveOpen(true);
};

const getBindableRootRoles = () => {
return roles.filter((role: IProjectRole) => role.type === 'root');
};

return (
<PageContent
isLoading={loading}
Expand Down Expand Up @@ -141,7 +134,6 @@ export const GroupsList: VFC = () => {
<Grid key={group.id} item xs={12} md={6}>
<GroupCard
group={group}
rootRoles={getBindableRootRoles()}
onEditUsers={onEditUsers}
onRemoveGroup={onRemoveGroup}
/>
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/component/admin/menu/AdminMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,21 @@ function AdminMenu() {
}
/>
)}
{flags.RE && (
{flags.customRootRoles && (
<Tab
value="roles"
label={
<CenteredNavLink to="/admin/roles">
<span>Roles</span>
</CenteredNavLink>
}
/>
)}
{flags.RE && (
<Tab
value="project-roles"
label={
<CenteredNavLink to="/admin/project-roles">
<span>Project roles</span>
</CenteredNavLink>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
import { useNavigate } from 'react-router-dom';
import ProjectRoleForm from '../ProjectRoleForm/ProjectRoleForm';
import useProjectRoleForm from '../hooks/useProjectRoleForm';
Expand Down Expand Up @@ -33,7 +33,7 @@ const CreateProjectRole = () => {
getRoleKey,
} = useProjectRoleForm();

const { createRole, loading } = useProjectRolesApi();
const { addRole, loading } = useRolesApi();

const onSubmit = async (e: Event) => {
e.preventDefault();
Expand All @@ -44,8 +44,8 @@ const CreateProjectRole = () => {
if (validName && validPermissions) {
const payload = getProjectRolePayload();
try {
await createRole(payload);
navigate('/admin/roles');
await addRole(payload);
navigate('/admin/project-roles');
setToastData({
title: 'Project role created',
text: 'Now you can start assigning your project roles to project members.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
import { useRole } from 'hooks/api/getters/useRole/useRole';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useNavigate } from 'react-router-dom';
Expand All @@ -15,8 +15,8 @@ import { GO_BACK } from 'constants/navigate';
const EditProjectRole = () => {
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const projectId = useRequiredPathParam('id');
const { role } = useProjectRole(projectId);
const roleId = useRequiredPathParam('id');
const { role, refetch } = useRole(roleId);

const navigate = useNavigate();
const {
Expand All @@ -35,19 +35,18 @@ const EditProjectRole = () => {
validateName,
clearErrors,
getRoleKey,
} = useProjectRoleForm(role.name, role.description, role?.permissions);
} = useProjectRoleForm(role?.name, role?.description, role?.permissions);

const formatApiCode = () => {
return `curl --location --request PUT '${
uiConfig.unleashUrl
}/api/admin/roles/${role.id}' \\
}/api/admin/roles/${role?.id}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getProjectRolePayload(), undefined, 2)}'`;
};

const { refetch } = useProjectRole(projectId);
const { editRole, loading } = useProjectRolesApi();
const { updateRole, loading } = useRolesApi();

const onSubmit = async (e: Event) => {
e.preventDefault();
Expand All @@ -58,9 +57,9 @@ const EditProjectRole = () => {

if (validName && validPermissions) {
try {
await editRole(projectId, payload);
await updateRole(+roleId, payload);
refetch();
navigate('/admin/roles');
navigate('/admin/project-roles');
setToastData({
type: 'success',
title: 'Project role updated',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
Typography,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { IPermission } from 'interfaces/project';
import { IPermission } from 'interfaces/permissions';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ICheckedPermission } from 'component/admin/projectRoles/hooks/useProjectRoleForm';

Expand All @@ -23,10 +23,10 @@ interface IEnvironmentPermissionAccordionProps {
title: string;
Icon: ReactNode;
isInitiallyExpanded?: boolean;
context: 'project' | 'environment';
context: string;
onPermissionChange: (permission: IPermission) => void;
onCheckAll: () => void;
getRoleKey: (permission: { id: number; environment?: string }) => string;
getRoleKey?: (permission: { id: number; environment?: string }) => string;
}

const AccordionHeader = styled(Box)(({ theme }) => ({
Expand All @@ -52,7 +52,7 @@ export const PermissionAccordion: VFC<IEnvironmentPermissionAccordionProps> = ({
context,
onPermissionChange,
onCheckAll,
getRoleKey,
getRoleKey = permission => permission.id.toString(),
}) => {
const [expanded, setExpanded] = useState(isInitiallyExpanded);
const permissionMap = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import {
IPermission,
IProjectEnvironmentPermissions,
IProjectRolePermissions,
} from 'interfaces/project';
IPermissions,
} from 'interfaces/permissions';
import { ICheckedPermission } from '../hooks/useProjectRoleForm';

interface IProjectRoleForm {
Expand All @@ -21,7 +21,7 @@ interface IProjectRoleForm {
errors: { [key: string]: string };
children: ReactNode;
permissions:
| IProjectRolePermissions
| IPermissions
| {
project: IPermission[];
environments: IProjectEnvironmentPermissions[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
} from 'component/common/Table';
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import useProjectRoles from 'hooks/api/getters/useProjectRoles/useProjectRoles';
import IRole, { IProjectRole } from 'interfaces/role';
import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
import { IProjectRole } from 'interfaces/role';
import { useRolesApi } from 'hooks/api/actions/useRolesApi/useRolesApi';
import useToast from 'hooks/useToast';
import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm';
import { formatUnknownError } from 'utils/formatUnknownError';
Expand All @@ -30,19 +30,15 @@ import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { Search } from 'component/common/Search/Search';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';

const ROOTROLE = 'root';
const BUILTIN_ROLE_TYPE = 'project';

const ProjectRoleList = () => {
const navigate = useNavigate();
const { roles, refetch, loading } = useProjectRoles();
const { projectRoles: data, refetch, loading } = useRoles();

const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));

const paginationFilter = (role: IRole) => role?.type !== ROOTROLE;
const data = roles.filter(paginationFilter);

const { deleteRole } = useProjectRolesApi();
const { removeRole } = useRolesApi();
const [currentRole, setCurrentRole] = useState<IProjectRole | null>(null);
const [delDialog, setDelDialog] = useState(false);
const [confirmName, setConfirmName] = useState('');
Expand All @@ -51,7 +47,7 @@ const ProjectRoleList = () => {
const deleteProjectRole = async () => {
if (!currentRole?.id) return;
try {
await deleteRole(currentRole?.id);
await removeRole(currentRole?.id);
refetch();
setToastData({
type: 'success',
Expand Down Expand Up @@ -99,7 +95,7 @@ const ProjectRoleList = () => {
data-loading
disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => {
navigate(`/admin/roles/${id}/edit`);
navigate(`/admin/project-roles/${id}/edit`);
}}
permission={ADMIN}
tooltipProps={{
Expand Down Expand Up @@ -208,7 +204,7 @@ const ProjectRoleList = () => {
variant="contained"
color="primary"
onClick={() =>
navigate('/admin/create-project-role')
navigate('/admin/project-roles/new')
}
>
New project role
Expand Down
Loading
Loading