mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-28 00:17:12 +01:00
refactor: project permissions list (#9082)
Re-organized project permissions.
This commit is contained in:
parent
900df537e3
commit
b5f0d3e86a
@ -16,9 +16,11 @@ import {
|
|||||||
toggleAllPermissions,
|
toggleAllPermissions,
|
||||||
togglePermission,
|
togglePermission,
|
||||||
} from 'utils/permissions';
|
} from 'utils/permissions';
|
||||||
import { RolePermissionCategory } from './RolePermissionCategory';
|
import { RolePermissionEnvironment } from './RolePermissionEnvironment';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { RolePermissionProject } from './RolePermissionProject';
|
||||||
|
import { RolePermissionCategoryAccordion } from './RolePermissionCategoryAccordion';
|
||||||
|
|
||||||
interface IPermissionCategoriesProps {
|
interface IPermissionCategoriesProps {
|
||||||
type: PredefinedRoleType;
|
type: PredefinedRoleType;
|
||||||
@ -45,6 +47,7 @@ export const RolePermissionCategories = ({
|
|||||||
const granularAdminPermissionsEnabled = useUiFlag(
|
const granularAdminPermissionsEnabled = useUiFlag(
|
||||||
'granularAdminPermissions',
|
'granularAdminPermissions',
|
||||||
);
|
);
|
||||||
|
const sortProjectRoles = useUiFlag('sortProjectRoles');
|
||||||
|
|
||||||
const isProjectRole = PROJECT_ROLE_TYPES.includes(type);
|
const isProjectRole = PROJECT_ROLE_TYPES.includes(type);
|
||||||
|
|
||||||
@ -95,7 +98,7 @@ export const RolePermissionCategories = ({
|
|||||||
label !== 'Authentication'),
|
label !== 'Authentication'),
|
||||||
)
|
)
|
||||||
.map(({ label, type, permissions }) => (
|
.map(({ label, type, permissions }) => (
|
||||||
<RolePermissionCategory
|
<RolePermissionCategoryAccordion
|
||||||
key={label}
|
key={label}
|
||||||
title={label}
|
title={label}
|
||||||
context={label.toLowerCase()}
|
context={label.toLowerCase()}
|
||||||
@ -116,11 +119,22 @@ export const RolePermissionCategories = ({
|
|||||||
}
|
}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
checkedPermissions={checkedPermissions}
|
checkedPermissions={checkedPermissions}
|
||||||
onPermissionChange={(permission: IPermission) =>
|
|
||||||
onPermissionChange(permission)
|
|
||||||
}
|
|
||||||
onCheckAll={() => onCheckAll(permissions)}
|
onCheckAll={() => onCheckAll(permissions)}
|
||||||
|
>
|
||||||
|
{type === 'project' && sortProjectRoles ? (
|
||||||
|
<RolePermissionProject
|
||||||
|
permissions={permissions}
|
||||||
|
checkedPermissions={checkedPermissions}
|
||||||
|
onPermissionChange={onPermissionChange}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<RolePermissionEnvironment
|
||||||
|
permissions={permissions}
|
||||||
|
checkedPermissions={checkedPermissions}
|
||||||
|
onPermissionChange={onPermissionChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RolePermissionCategoryAccordion>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
@ -5,9 +5,7 @@ import {
|
|||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
Divider,
|
Divider,
|
||||||
FormControlLabel,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
styled,
|
styled,
|
||||||
Typography,
|
Typography,
|
||||||
@ -24,8 +22,8 @@ interface IEnvironmentPermissionAccordionProps {
|
|||||||
Icon: ReactNode;
|
Icon: ReactNode;
|
||||||
isInitiallyExpanded?: boolean;
|
isInitiallyExpanded?: boolean;
|
||||||
context: string;
|
context: string;
|
||||||
onPermissionChange: (permission: IPermission) => void;
|
|
||||||
onCheckAll: () => void;
|
onCheckAll: () => void;
|
||||||
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccordionHeader = styled(Box)(({ theme }) => ({
|
const AccordionHeader = styled(Box)(({ theme }) => ({
|
||||||
@ -42,14 +40,14 @@ const StyledTitle = styled(StringTruncator)(({ theme }) => ({
|
|||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const RolePermissionCategory = ({
|
export const RolePermissionCategoryAccordion = ({
|
||||||
title,
|
title,
|
||||||
permissions,
|
permissions,
|
||||||
checkedPermissions,
|
checkedPermissions,
|
||||||
Icon,
|
Icon,
|
||||||
isInitiallyExpanded = false,
|
isInitiallyExpanded = false,
|
||||||
context,
|
context,
|
||||||
onPermissionChange,
|
children,
|
||||||
onCheckAll,
|
onCheckAll,
|
||||||
}: IEnvironmentPermissionAccordionProps) => {
|
}: IEnvironmentPermissionAccordionProps) => {
|
||||||
const [expanded, setExpanded] = useState(isInitiallyExpanded);
|
const [expanded, setExpanded] = useState(isInitiallyExpanded);
|
||||||
@ -139,34 +137,8 @@ export const RolePermissionCategory = ({
|
|||||||
{isAllChecked ? 'Unselect ' : 'Select '}
|
{isAllChecked ? 'Unselect ' : 'Select '}
|
||||||
all {context} permissions
|
all {context} permissions
|
||||||
</Button>
|
</Button>
|
||||||
<Box
|
|
||||||
display='grid'
|
{children}
|
||||||
gridTemplateColumns={{
|
|
||||||
sm: '1fr 1fr',
|
|
||||||
xs: '1fr',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{permissions?.map((permission: IPermission) => (
|
|
||||||
<FormControlLabel
|
|
||||||
data-testid={getRoleKey(permission)}
|
|
||||||
key={getRoleKey(permission)}
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={Boolean(
|
|
||||||
checkedPermissions[
|
|
||||||
getRoleKey(permission)
|
|
||||||
],
|
|
||||||
)}
|
|
||||||
onChange={() =>
|
|
||||||
onPermissionChange(permission)
|
|
||||||
}
|
|
||||||
color='primary'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={permission.displayName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Box>
|
</Box>
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Box, Checkbox, FormControlLabel } from '@mui/material';
|
||||||
|
import type { ICheckedPermissions, IPermission } from 'interfaces/permissions';
|
||||||
|
import { getRoleKey } from 'utils/permissions';
|
||||||
|
|
||||||
|
interface IEnvironmentPermissionAccordionProps {
|
||||||
|
permissions: IPermission[];
|
||||||
|
checkedPermissions: ICheckedPermissions;
|
||||||
|
onPermissionChange: (permission: IPermission) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RolePermissionEnvironment = ({
|
||||||
|
permissions,
|
||||||
|
checkedPermissions,
|
||||||
|
onPermissionChange,
|
||||||
|
}: IEnvironmentPermissionAccordionProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display='grid'
|
||||||
|
gridTemplateColumns={{
|
||||||
|
sm: '1fr 1fr',
|
||||||
|
xs: '1fr',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{permissions?.map((permission: IPermission) => (
|
||||||
|
<FormControlLabel
|
||||||
|
data-testid={getRoleKey(permission)}
|
||||||
|
key={getRoleKey(permission)}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={Boolean(
|
||||||
|
checkedPermissions[getRoleKey(permission)],
|
||||||
|
)}
|
||||||
|
onChange={() => onPermissionChange(permission)}
|
||||||
|
color='primary'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={permission.displayName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { styled, Typography } from '@mui/material';
|
||||||
|
import type { ICheckedPermissions, IPermission } from 'interfaces/permissions';
|
||||||
|
import { createProjectPermissionsStructure } from './createProjectPermissionsStructure';
|
||||||
|
import { RolePermissionProjectItem } from './RolePermissionProjectItem';
|
||||||
|
|
||||||
|
interface IEnvironmentPermissionAccordionProps {
|
||||||
|
permissions: IPermission[];
|
||||||
|
checkedPermissions: ICheckedPermissions;
|
||||||
|
onPermissionChange: (permission: IPermission) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledGrid = styled('div')(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
gridTemplateColumns: '1fr',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledSectionTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const RolePermissionProject = ({
|
||||||
|
permissions,
|
||||||
|
checkedPermissions,
|
||||||
|
onPermissionChange,
|
||||||
|
}: IEnvironmentPermissionAccordionProps) => {
|
||||||
|
const permissionsStructure = useMemo(
|
||||||
|
() => createProjectPermissionsStructure(permissions),
|
||||||
|
[permissions],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledGrid>
|
||||||
|
{permissionsStructure.map((category) => (
|
||||||
|
<div>
|
||||||
|
<StyledSectionTitle>{category.label}</StyledSectionTitle>
|
||||||
|
<div>
|
||||||
|
{category.permissions.map(
|
||||||
|
([permission, parentPermission]) => (
|
||||||
|
<RolePermissionProjectItem
|
||||||
|
permission={permission}
|
||||||
|
onChange={() =>
|
||||||
|
onPermissionChange(permission)
|
||||||
|
}
|
||||||
|
isChecked={Boolean(
|
||||||
|
checkedPermissions[permission.name],
|
||||||
|
)}
|
||||||
|
hasParentPermission={Boolean(
|
||||||
|
parentPermission,
|
||||||
|
)}
|
||||||
|
isParentPermissionChecked={Boolean(
|
||||||
|
parentPermission &&
|
||||||
|
checkedPermissions[
|
||||||
|
parentPermission
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</StyledGrid>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import type { IPermission } from 'interfaces/permissions';
|
||||||
|
|
||||||
|
type RolePermissionProjectItemProps = {
|
||||||
|
permission: IPermission;
|
||||||
|
onChange: () => void;
|
||||||
|
isChecked: boolean;
|
||||||
|
hasParentPermission?: boolean;
|
||||||
|
isParentPermissionChecked?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledLabel = styled(Typography)(({ theme }) => ({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const RolePermissionProjectItem = ({
|
||||||
|
permission,
|
||||||
|
onChange,
|
||||||
|
isChecked,
|
||||||
|
hasParentPermission,
|
||||||
|
isParentPermissionChecked,
|
||||||
|
}: RolePermissionProjectItemProps) => (
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginLeft: hasParentPermission ? theme.spacing(1.5) : 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
data-testid={permission}
|
||||||
|
key={permission.name}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={Boolean(isChecked || isParentPermissionChecked)}
|
||||||
|
onChange={onChange}
|
||||||
|
color='primary'
|
||||||
|
disabled={isParentPermissionChecked}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<StyledLabel>{permission.displayName}</StyledLabel>}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
@ -0,0 +1,93 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { createProjectPermissionsStructure } from './createProjectPermissionsStructure';
|
||||||
|
|
||||||
|
describe('createProjectPermissionsStructure', () => {
|
||||||
|
it('returns an empty array when no permissions are given', () => {
|
||||||
|
const result = createProjectPermissionsStructure([]);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('groups known permissions into existing categories', () => {
|
||||||
|
const result = createProjectPermissionsStructure([
|
||||||
|
{
|
||||||
|
name: 'CREATE_FEATURE',
|
||||||
|
displayName: 'Create Feature',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UPDATE_FEATURE',
|
||||||
|
displayName: 'Update Feature',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PROJECT_USER_ACCESS_READ',
|
||||||
|
displayName: 'Read Project Access',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SOME_UNKNOWN_PERMISSION',
|
||||||
|
displayName: 'Unknown Permission',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const featuresCategory = result.find(
|
||||||
|
(cat) => cat.label === 'Features and strategies',
|
||||||
|
);
|
||||||
|
const projectSettingsCategory = result.find(
|
||||||
|
(cat) => cat.label === 'Project settings',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(featuresCategory?.permissions).toHaveLength(2);
|
||||||
|
expect(projectSettingsCategory?.permissions).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('places unknown permissions into the "Other" category', () => {
|
||||||
|
const result = createProjectPermissionsStructure([
|
||||||
|
{
|
||||||
|
name: 'SOME_UNKNOWN_PERMISSION',
|
||||||
|
displayName: 'Unknown Permission',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const otherCategory = result.find((cat) => cat.label === 'Other');
|
||||||
|
expect(otherCategory).toBeDefined();
|
||||||
|
expect(otherCategory?.permissions).toHaveLength(1);
|
||||||
|
const [[permission]] = otherCategory!.permissions;
|
||||||
|
expect(permission.name).toBe('SOME_UNKNOWN_PERMISSION');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits categories when they have no assigned permissions', () => {
|
||||||
|
const result = createProjectPermissionsStructure([
|
||||||
|
{
|
||||||
|
name: 'CREATE_FEATURE',
|
||||||
|
displayName: 'Create Feature',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UPDATE_FEATURE',
|
||||||
|
displayName: 'Update Feature',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
const projectSettingsCategory = result.find(
|
||||||
|
(cat) => cat.label === 'Project settings',
|
||||||
|
);
|
||||||
|
expect(projectSettingsCategory).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes parent permission names in the structure', () => {
|
||||||
|
const result = createProjectPermissionsStructure([
|
||||||
|
{
|
||||||
|
name: 'PROJECT_USER_ACCESS_READ',
|
||||||
|
displayName: 'Read Project Access',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const permissions = result[0].permissions;
|
||||||
|
expect(permissions[0]).toEqual([
|
||||||
|
expect.objectContaining({ name: 'PROJECT_USER_ACCESS_READ' }),
|
||||||
|
'UPDATE_PROJECT',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
type ProjectPermissionCategory,
|
||||||
|
PROJECT_PERMISSIONS_STRUCTURE,
|
||||||
|
} from '@server/types/permissions';
|
||||||
|
import type { IPermission } from 'interfaces/permissions';
|
||||||
|
import { getRoleKey } from 'utils/permissions';
|
||||||
|
|
||||||
|
export const createProjectPermissionsStructure = (
|
||||||
|
permissions: IPermission[],
|
||||||
|
) => {
|
||||||
|
const allPermissions =
|
||||||
|
permissions?.map((permission) => getRoleKey(permission)).sort() || [];
|
||||||
|
|
||||||
|
const allStructuredPermissions = PROJECT_PERMISSIONS_STRUCTURE.flatMap(
|
||||||
|
(category) => category.permissions.map(([permission]) => permission),
|
||||||
|
).sort() as string[];
|
||||||
|
|
||||||
|
const otherPermissions = allPermissions.filter(
|
||||||
|
(permission) => !allStructuredPermissions.includes(permission),
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissionsStructure = [
|
||||||
|
...PROJECT_PERMISSIONS_STRUCTURE,
|
||||||
|
{
|
||||||
|
label: 'Other',
|
||||||
|
permissions: otherPermissions.map((p) => [p]),
|
||||||
|
} as ProjectPermissionCategory,
|
||||||
|
]
|
||||||
|
.map((category) => ({
|
||||||
|
label: category.label,
|
||||||
|
permissions: category.permissions
|
||||||
|
.filter(([permission]) => allPermissions.includes(permission))
|
||||||
|
.map(([permission, parentPermission]) => [
|
||||||
|
permissions.find((p) => getRoleKey(p) === permission),
|
||||||
|
parentPermission,
|
||||||
|
]) as [IPermission, string?][],
|
||||||
|
}))
|
||||||
|
.filter((category) => category.permissions.length > 0);
|
||||||
|
|
||||||
|
return permissionsStructure;
|
||||||
|
};
|
@ -90,6 +90,7 @@ export type UiFlags = {
|
|||||||
showUserDeviceCount?: boolean;
|
showUserDeviceCount?: boolean;
|
||||||
flagOverviewRedesign?: boolean;
|
flagOverviewRedesign?: boolean;
|
||||||
granularAdminPermissions?: boolean;
|
granularAdminPermissions?: boolean;
|
||||||
|
sortProjectRoles?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -60,7 +60,8 @@ export type IFlagKey =
|
|||||||
| 'streaming'
|
| 'streaming'
|
||||||
| 'etagVariant'
|
| 'etagVariant'
|
||||||
| 'deltaApi'
|
| 'deltaApi'
|
||||||
| 'uniqueSdkTracking';
|
| 'uniqueSdkTracking'
|
||||||
|
| 'sortProjectRoles';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -285,6 +286,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_UNIQUE_SDK_TRACKING,
|
process.env.UNLEASH_EXPERIMENTAL_UNIQUE_SDK_TRACKING,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
sortProjectRoles: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_SORT_PROJECT_ROLES,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -171,3 +171,56 @@ export const MAINTENANCE_MODE_PERMISSIONS = [
|
|||||||
UPDATE_MAINTENANCE_MODE,
|
UPDATE_MAINTENANCE_MODE,
|
||||||
READ_LOGS,
|
READ_LOGS,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export type ProjectPermissionCategory = {
|
||||||
|
label: string;
|
||||||
|
permissions: Array<[string, string?]>; // [permission, is subset of]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECT_PERMISSIONS_STRUCTURE: ProjectPermissionCategory[] = [
|
||||||
|
{
|
||||||
|
label: 'Features and strategies',
|
||||||
|
permissions: [
|
||||||
|
[CREATE_FEATURE],
|
||||||
|
[UPDATE_FEATURE],
|
||||||
|
[UPDATE_FEATURE_DEPENDENCY],
|
||||||
|
[DELETE_FEATURE],
|
||||||
|
[UPDATE_FEATURE_VARIANTS],
|
||||||
|
[MOVE_FEATURE_TOGGLE],
|
||||||
|
[CREATE_FEATURE_STRATEGY],
|
||||||
|
[UPDATE_FEATURE_STRATEGY],
|
||||||
|
[DELETE_FEATURE_STRATEGY],
|
||||||
|
[UPDATE_FEATURE_ENVIRONMENT],
|
||||||
|
[UPDATE_FEATURE_ENVIRONMENT_VARIANTS],
|
||||||
|
[UPDATE_PROJECT_SEGMENT],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Project settings',
|
||||||
|
permissions: [
|
||||||
|
[UPDATE_PROJECT],
|
||||||
|
[PROJECT_USER_ACCESS_READ, UPDATE_PROJECT],
|
||||||
|
[PROJECT_USER_ACCESS_WRITE, UPDATE_PROJECT],
|
||||||
|
[PROJECT_DEFAULT_STRATEGY_READ, UPDATE_PROJECT],
|
||||||
|
[PROJECT_DEFAULT_STRATEGY_WRITE, UPDATE_PROJECT],
|
||||||
|
[PROJECT_SETTINGS_READ, UPDATE_PROJECT],
|
||||||
|
[PROJECT_SETTINGS_WRITE, UPDATE_PROJECT],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API tokens',
|
||||||
|
permissions: [
|
||||||
|
[READ_PROJECT_API_TOKEN],
|
||||||
|
[CREATE_PROJECT_API_TOKEN],
|
||||||
|
[DELETE_PROJECT_API_TOKEN],
|
||||||
|
[DELETE_PROJECT],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Change requests',
|
||||||
|
permissions: [
|
||||||
|
[PROJECT_CHANGE_REQUEST_WRITE],
|
||||||
|
[PROJECT_CHANGE_REQUEST_READ],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -54,6 +54,7 @@ process.nextTick(async () => {
|
|||||||
showUserDeviceCount: true,
|
showUserDeviceCount: true,
|
||||||
flagOverviewRedesign: false,
|
flagOverviewRedesign: false,
|
||||||
granularAdminPermissions: true,
|
granularAdminPermissions: true,
|
||||||
|
sortProjectRoles: true,
|
||||||
deltaApi: true,
|
deltaApi: true,
|
||||||
uniqueSdkTracking: true,
|
uniqueSdkTracking: true,
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user