diff --git a/docs/docs/system-administration/rbac.md b/docs/docs/system-administration/rbac.md index b57fcc3d6e85..aa779af4980d 100644 --- a/docs/docs/system-administration/rbac.md +++ b/docs/docs/system-administration/rbac.md @@ -134,6 +134,11 @@ Assigning roles to groups has several benefits over assigning permissions direct Permissions can be assigned at four levels: user group, organisation, project, and environment. +## Tagged Permissions + +When creating a role, some permissions allow you to grant access when features have specific tags. For example, you can +configure a role to create change requests only for features tagged with "marketing". + ### User group | Permission | Ability | @@ -149,25 +154,27 @@ Permissions can be assigned at four levels: user group, organisation, project, a ### Project -| Permission | Ability | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | -| Administrator | Grants full read and write access to all environments, features and segments. | -| View Project | Allows viewing this project. The project is hidden from users without this permission. | -| Create Environment | Allows creating new environments in this project. Users are automatically granted Administrator permissions on any environments they create. | -| Create Feature | Allows creating new features in all environments. | -| Delete Feature | Allows deleting features from all environments. | -| Manage Segments | Grants write access to segments in this project. | -| View audit log | Allows viewing all audit log entries for this project. | +### Project + +| Permission | Ability | Supports Tags | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| Administrator | Grants full read and write access to all environments, features, and segments. | | +| View Project | Allows viewing this project. The project is hidden from users without this permission. | | +| Create Environment | Allows creating new environments in this project. Users are automatically granted Administrator permissions on any environments they create. | | +| Create Feature | Allows creating new features in all environments. | | +| Delete Feature | Allows deleting features from all environments. | Yes | +| Manage Segments | Grants write access to segments in this project. | | +| View audit log | Allows viewing all audit log entries for this project. | | ### Environment -| Permission | Ability | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -| Administrator | Grants full read and write access to all feature states, overrides, identities and change requests in this environment. | -| View Environment | Allows viewing this environment. The environment is hidden from users without this permission. | -| Update Feature State | Allows updating updating any feature state or values in this environment. | -| Manage Identities | Grants read and write access to identities in this environment. | -| Manage Segment Overrides | Grants write access to segment overrides in this environment. | -| Create Change Request | Allows creating change requests for features in this environment. | -| Approve Change Request | Allows approving or denying change requests in this environment. | -| View Identities | Grants read-only access to identities in this environment. | +| Permission | Ability | Supports Tags | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------- | +| Administrator | Grants full read and write access to all feature states, overrides, identities, and change requests in this environment. | | +| View Environment | Allows viewing this environment. The environment is hidden from users without this permission. | | +| Update Feature State | Allows updating any feature state or values in this environment. | Yes | +| Manage Identities | Grants read and write access to identities in this environment. | | +| Manage Segment Overrides | Grants write access to segment overrides in this environment. | | +| Create Change Request | Allows creating change requests for features in this environment. | Yes | +| Approve Change Request | Allows approving or denying change requests in this environment. | Yes | +| View Identities | Grants read-only access to identities in this environment. | | diff --git a/frontend/api/index.js b/frontend/api/index.js index 0f5aceb97470..863e937e9201 100755 --- a/frontend/api/index.js +++ b/frontend/api/index.js @@ -124,6 +124,10 @@ app.get('/config/project-overrides', (req, res) => { name: 'githubAppURL', value: process.env.GITHUB_APP_URL, }, + { + name: 'e2eToken', + value: process.env.E2E_TEST_TOKEN || '', + }, ] let output = values.map(getVariable).join('') let dynatrace = '' diff --git a/frontend/common/providers/Permission.tsx b/frontend/common/providers/Permission.tsx index 212f41bee1ee..cd04e8459902 100644 --- a/frontend/common/providers/Permission.tsx +++ b/frontend/common/providers/Permission.tsx @@ -1,11 +1,15 @@ -import React, { FC, ReactNode } from 'react' +import React, { FC, ReactNode, useMemo } from 'react' import { useGetPermissionQuery } from 'common/services/usePermission' import { PermissionLevel } from 'common/types/requests' -import AccountStore from 'common/stores/account-store' // we need this to make JSX compile +import AccountStore from 'common/stores/account-store' +import intersection from 'lodash/intersection' +import { add } from 'ionicons/icons'; +import { cloneDeep } from 'lodash'; // we need this to make JSX compile type PermissionType = { id: any permission: string + tags?: number[] level: PermissionLevel children: (data: { permission: boolean; isLoading: boolean }) => ReactNode } @@ -14,11 +18,26 @@ export const useHasPermission = ({ id, level, permission, + tags, }: Omit) => { - const { data, isLoading, isSuccess } = useGetPermissionQuery( - { id: `${id}`, level }, - { skip: !id || !level }, - ) + const { + data: permissionsData, + isLoading, + isSuccess, + } = useGetPermissionQuery({ id: `${id}`, level }, { skip: !id || !level }) + const data = useMemo(() => { + if (!tags?.length || !permissionsData?.tag_based_permissions) + return permissionsData + const addedPermissions = cloneDeep(permissionsData) + permissionsData.tag_based_permissions.forEach((tagBasedPermission) => { + if (intersection(tagBasedPermission.tags, tags).length) { + tagBasedPermission.permissions.forEach((key) => { + addedPermissions[key] = true + }) + } + }) + return addedPermissions + }, [permissionsData, tags]) const hasPermission = !!data?.[permission] || !!data?.ADMIN return { isLoading, @@ -32,11 +51,13 @@ const Permission: FC = ({ id, level, permission, + tags, }) => { const { isLoading, permission: hasPermission } = useHasPermission({ id, level, permission, + tags, }) return ( <> diff --git a/frontend/common/services/usePermission.ts b/frontend/common/services/usePermission.ts index cbaf91f05a52..b91586aa9ee7 100644 --- a/frontend/common/services/usePermission.ts +++ b/frontend/common/services/usePermission.ts @@ -13,16 +13,25 @@ export const permissionService = service query: ({ id, level }: Req['getPermission']) => ({ url: `${level}s/${id}/my-permissions/`, }), - transformResponse(baseQueryReturnValue: { - admin: boolean - permissions: string[] - }) { + transformResponse( + baseQueryReturnValue: { + admin: boolean + permissions: string[] + tag_based_permissions?: Res['permission']['tag_based_permissions'] + }, + _, + ) { const res: Res['permission'] = { ADMIN: baseQueryReturnValue.admin, } + if (baseQueryReturnValue.tag_based_permissions) { + res.tag_based_permissions = + baseQueryReturnValue.tag_based_permissions + } baseQueryReturnValue.permissions.forEach((v) => { res[v] = true }) + return res }, }), diff --git a/frontend/common/services/useProject.ts b/frontend/common/services/useProject.ts index b929c6c47eec..205cc545ef86 100644 --- a/frontend/common/services/useProject.ts +++ b/frontend/common/services/useProject.ts @@ -18,7 +18,7 @@ export const projectService = service query: (data) => ({ url: `projects/?organisation=${data.organisationId}`, }), - transformResponse: (res) => sortBy(res, 'name'), + transformResponse: (res) => sortBy(res, (v) => v.name.toLowerCase()), }), // END OF ENDPOINTS }), diff --git a/frontend/common/services/useRole.ts b/frontend/common/services/useRole.ts index 2bfdb8476e1f..8356d2ef215b 100644 --- a/frontend/common/services/useRole.ts +++ b/frontend/common/services/useRole.ts @@ -22,6 +22,7 @@ export const roleService = service }), }), getRole: builder.query({ + providesTags: (res) => [{ id: res?.id, type: 'Role' }], query: (query: Req['getRole']) => ({ url: `organisations/${query.organisation_id}/roles/${query.role_id}/`, }), @@ -33,7 +34,10 @@ export const roleService = service }), }), updateRole: builder.mutation({ - invalidatesTags: (res) => [{ id: 'LIST', type: 'Role' }], + invalidatesTags: (res, _, req) => [ + { id: 'LIST', type: 'Role' }, + { id: req.role_id, type: 'Role' }, + ], query: (query: Req['updateRole']) => ({ body: query.body, method: 'PUT', diff --git a/frontend/common/services/useRolePermission.ts b/frontend/common/services/useRolePermission.ts index 9bf4b26391dc..2dfbfe80461d 100644 --- a/frontend/common/services/useRolePermission.ts +++ b/frontend/common/services/useRolePermission.ts @@ -121,15 +121,12 @@ export async function getRoleProjectPermissions( typeof rolePermissionService.endpoints.getRoleProjectPermissions.initiate >[1], ) { - store.dispatch( + return store.dispatch( rolePermissionService.endpoints.getRoleProjectPermissions.initiate( data, options, ), ) - return Promise.all( - store.dispatch(rolePermissionService.util.getRunningQueriesThunk()), - ) } export async function getRoleEnvironmentPermissions( @@ -139,15 +136,12 @@ export async function getRoleEnvironmentPermissions( typeof rolePermissionService.endpoints.getRoleEnvironmentPermissions.initiate >[1], ) { - store.dispatch( + return store.dispatch( rolePermissionService.endpoints.getRoleEnvironmentPermissions.initiate( data, options, ), ) - return Promise.all( - store.dispatch(rolePermissionService.util.getRunningQueriesThunk()), - ) } export async function createRolePermissions( diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index 3174c2aff953..6c33760ce886 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -7,6 +7,8 @@ import Constants from 'common/constants' import dataRelay from 'data-relay' import { sortBy } from 'lodash' import Project from 'common/project' +import { getStore } from 'common/store' +import { service } from "common/service"; const controller = { acceptInvite: (id) => { @@ -341,6 +343,7 @@ const controller = { API.reset().finally(() => { store.model = user store.organisation = null + getStore().dispatch(service.util.resetApiState()) store.trigger('logout') }) }) diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index af4cd739af0c..5aad2f9d0707 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -39,7 +39,11 @@ const controller = { ) : ['Development', 'Production'] data - .post(`${Project.api}projects/`, { name, organisation: store.id }) + .post( + `${Project.api}projects/`, + { name, organisation: store.id }, + E2E ? { 'X-E2E-Test-Auth-Token': Project.e2eToken } : {}, + ) .then((project) => { Promise.all( defaultEnvironmentNames.map((envName) => { diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 55bca8eedc18..dfa7d68ac381 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -14,6 +14,8 @@ import { UserGroup, AttributeName, Identity, + Role, + RolePermission, } from './responses' export type PagedRequest = T & { @@ -158,7 +160,7 @@ export type Req = { updateRole: { organisation_id: number role_id: number - body: { description: string | null; name: string } + body: Role } deleteRole: { organisation_id: number; role_id: number } getRolePermissionEnvironment: { @@ -179,7 +181,7 @@ export type Req = { level: PermissionLevel body: { admin?: boolean - permissions: string[] + permissions: RolePermission['permissions'] project: number environment: number } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 9dd860a61d32..0679b3795ca4 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -261,8 +261,12 @@ export type UserPermission = { id: number role?: number } + +export type RolePermission = Omit & { + permissions: { permission_key: string; tags: number[] }[] +} export type GroupPermission = Omit & { - group: UserGroup + group: UserGroupSummary } export type AuditLogItem = { @@ -328,6 +332,7 @@ export type Identity = { export type AvailablePermission = { key: string description: string + supports_tag: boolean } export type APIKey = { @@ -657,7 +662,10 @@ export type Res = { } identity: { id: string } //todo: we don't consider this until we migrate identity-store identities: EdgePagedResponse - permission: Record + permission: Record & { + ADMIN: boolean + tag_based_permissions?: { permissions: string[]; tags: number[] }[] + } availablePermissions: AvailablePermission[] tag: Tag tags: Tag[] @@ -695,7 +703,7 @@ export type Res = { versionFeatureState: FeatureState[] role: Role roles: PagedResponse - rolePermission: PagedResponse + rolePermission: PagedResponse projectFlags: PagedResponse projectFlag: ProjectFlag identityFeatureStatesAll: IdentityFeatureState[] diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 4b98764d802c..83ad6329db5c 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -341,36 +341,6 @@ const Utils = Object.assign({}, require('./base/_utils'), { return `/organisation/${orgId}/projects` }, - getPermissionList( - isAdmin: boolean, - permissions: string[] | undefined | null, - numberToTruncate = 3, - ): { - items: string[] - truncatedItems: string[] - } { - if (isAdmin) { - return { - items: ['Administrator'], - truncatedItems: [], - } - } - if (!permissions) return { items: [], truncatedItems: [] } - - const items = - permissions && permissions.length - ? permissions - .slice(0, numberToTruncate) - .map((item) => `${Format.enumeration.get(item)}`) - : [] - - return { - items, - truncatedItems: (permissions || []) - .slice(numberToTruncate) - .map((item) => `${Format.enumeration.get(item)}`), - } - }, getPlanName: (plan: string) => { if (plan && plan.includes('free')) { return planNames.free diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 9d0d2b35c16c..2fc0afc1fbcf 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -439,6 +439,7 @@ const App = class extends Component { > } id='audit-log-link' to={`/project/${projectId}/audit-log`} + data-test='audit-log-link' > Audit Log @@ -661,6 +663,7 @@ const App = class extends Component { } id='org-settings-link' + data-test='org-settings-link' to={`/organisation/${ AccountStore.getOrganisation().id }/settings`} diff --git a/frontend/web/components/CompareEnvironments.js b/frontend/web/components/CompareEnvironments.js index 780418e16a9a..cf11c7ca3590 100644 --- a/frontend/web/components/CompareEnvironments.js +++ b/frontend/web/components/CompareEnvironments.js @@ -242,6 +242,7 @@ class CompareEnvironments extends Component { ) => { + return ( + +
+ {props.data.value === 'GRANTED' && ( + + )} + {props.data.value === 'GRANTED_FOR_TAGS' && ( + + )} + {props.children} +
+
+ ) +} + type EditPermissionModalType = { group?: UserGroupSummary id: number @@ -84,7 +104,7 @@ type EditPermissionModalType = { user?: User role?: Role roles?: Role[] - permissionChanged: () => void + permissionChanged?: () => void isEditUserPermission?: boolean isEditGroupPermission?: boolean } @@ -96,12 +116,19 @@ type EditPermissionsType = Omit & { tabClassName?: string } type EntityPermissions = Omit< - UserPermission, + RolePermission, 'user' | 'id' | 'group' | 'isGroup' > & { id?: number + group?: number user?: number + tags?: number[] } +const permissionOptions = [ + { label: 'Granted', value: 'GRANTED' }, + { label: 'Granted for tags', value: 'GRANTED_FOR_TAGS' }, + { label: 'None', value: 'NONE' }, +] const withAdminPermissions = (InnerComponent: any) => { const WrappedComponent: FC = (props) => { const { id, level } = props @@ -121,7 +148,10 @@ const withAdminPermissions = (InnerComponent: any) => { } if (!permission) { return ( -
+
To manage permissions you need to be admin of this {level}.
) @@ -132,24 +162,7 @@ const withAdminPermissions = (InnerComponent: any) => { return WrappedComponent } const _EditPermissionsModal: FC = withAdminPermissions( - forwardRef((props) => { - const [entityPermissions, setEntityPermissions] = - useState({ admin: false, permissions: [] }) - const [parentError, setParentError] = useState(false) - const [saving, setSaving] = useState(false) - const [showRoles, setShowRoles] = useState(false) - const [valueChanged, setValueChanged] = useState(false) - - const [permissionWasCreated, setPermissionWasCreated] = - useState(false) - const [rolesSelected, setRolesSelected] = useState< - { - role: number - user_role_id?: number - group_role_id?: number - }[] - >([]) - + forwardRef((props: EditPermissionModalType) => { const { className, envId, @@ -171,6 +184,38 @@ const _EditPermissionsModal: FC = withAdminPermissions( user, } = props + const [entityPermissions, setEntityPermissions] = + useState({ + admin: false, + permissions: [], + }) + const [parentError, setParentError] = useState(false) + const [saving, setSaving] = useState(false) + const [showRoles, setShowRoles] = useState(false) + const [valueChanged, setValueChanged] = useState(false) + + const projectId = + props.level === 'project' + ? props.id + : props.level === 'environment' + ? props.parentId + : undefined + + const { data: tags, isLoading: tagsLoading } = useGetTagsQuery( + { projectId: `${projectId}` }, + { skip: !projectId }, + ) + + const [permissionWasCreated, setPermissionWasCreated] = + useState(false) + const [rolesSelected, setRolesSelected] = useState< + { + role: number + user_role_id: number | undefined + group_role_id: number | undefined + }[] + >([]) + const { data: permissions } = useGetAvailablePermissionsQuery({ level }) const { data: userWithRolesData, isSuccess: userWithRolesDataSuccesfull } = useGetUserWithRolesQuery( @@ -195,6 +240,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( useEffect(() => { if (user && userWithRolesDataSuccesfull) { const resultArray = userWithRolesData?.results?.map((userRole) => ({ + group_role_id: undefined, role: userRole.id, user_role_id: user?.id, })) @@ -208,6 +254,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( const resultArray = groupWithRolesData?.results?.map((groupRole) => ({ group_role_id: group?.id, role: groupRole.id, + user_role_id: undefined, })) setRolesSelected(resultArray) } @@ -215,13 +262,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( }, [groupWithRolesDataSuccesfull]) const processResults = (results: (UserPermission | GroupPermission)[]) => { - let entityPermissions: - | (Omit & { - user?: any - group?: any - role?: any - }) - | undefined = isGroup + const foundPermission = isGroup ? find( results || [], (r) => (r as GroupPermission).group.id === group?.id, @@ -232,18 +273,22 @@ const _EditPermissionsModal: FC = withAdminPermissions( results || [], (r) => (r as UserPermission).user?.id === user?.id, ) - - if (!entityPermissions) { - entityPermissions = { admin: false, permissions: [] } - } - if (user) { - entityPermissions.user = user.id - } - if (group) { - entityPermissions.group = group.id - } - return entityPermissions + const permissions = + (role && (level === 'project' || level === 'environment') + ? foundPermission?.permissions + : (foundPermission?.permissions || []).map((v) => ({ + permission_key: v, + tags: [], + }))) || [] + return { + ...(foundPermission || {}), + group: group?.id, + //Since role permissions and other permissions are different in data structure, adjust permissions to match + permissions, + user: user?.id, + } as EntityPermissions } + const [ createRolePermissionUser, { data: usersData, isSuccess: userAdded }, @@ -278,6 +323,8 @@ const _EditPermissionsModal: FC = withAdminPermissions( }, ] = useCreateRolePermissionsMutation() + const tagBasedPermissions = + Utils.getFlagsmithHasFeature('tag_based_permissions') && !!role useEffect(() => { const isSaving = isRolePermCreating || isRolePermUpdating if (isSaving) { @@ -387,7 +434,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( if ( !entityPermissions.admin && !entityPermissions.permissions.find( - (v) => v === `VIEW_${parentLevel.toUpperCase()}`, + (v) => v.permission_key === `VIEW_${parentLevel.toUpperCase()}`, ) ) { // e.g. trying to set an environment permission but don't have view_project @@ -426,7 +473,24 @@ const _EditPermissionsModal: FC = withAdminPermissions( const hasPermission = (key: string) => { if (admin()) return true - return entityPermissions.permissions.includes(key) + return entityPermissions.permissions.find( + (permission) => permission.permission_key === key, + ) + } + + const getPermissionType = (key: string) => { + if (admin()) return 'GRANTED' + const permission = entityPermissions.permissions.find( + (v) => v.permission_key === key, + ) + + if (!permission) return 'NONE' + + if (permission.tags?.length || limitedPermissions.includes(key)) { + return 'GRANTED_FOR_TAGS' + } + + return 'GRANTED' } const save = useCallback(() => { @@ -439,19 +503,33 @@ const _EditPermissionsModal: FC = withAdminPermissions( : `${level}s/${id}/user-permissions/${entityId}` setSaving(true) const action = entityId ? 'put' : 'post' - _data[action]( - `${Project.api}${url}${entityId && '/'}`, - entityPermissions, - ) - .then((res: EntityPermissions) => { - setEntityPermissions(res) - toast( - `${ - level.charAt(0).toUpperCase() + level.slice(1) - } Permissions Saved`, - ) - onSave && onSave() - }) + _data[action](`${Project.api}${url}${entityId && '/'}`, { + ...entityPermissions, + permissions: entityPermissions.permissions.map( + (v) => v.permission_key, + ), + }) + .then( + ( + res: Omit & { + permissions: string[] + }, + ) => { + setEntityPermissions({ + ...res, + permissions: (res.permissions || []).map((v) => ({ + permission_key: v, + tags: [], + })), + }) + toast( + `${ + level.charAt(0).toUpperCase() + level.slice(1) + } Permissions Saved`, + ) + onSave && onSave() + }, + ) .catch(() => { toast(`Error Saving Permissions`, 'danger') }) @@ -460,7 +538,10 @@ const _EditPermissionsModal: FC = withAdminPermissions( }) } else { const body = { - permissions: entityPermissions.permissions, + permissions: + level === 'organisation' + ? entityPermissions.permissions.map((v) => v.permission_key) + : entityPermissions.permissions, } as Partial if (level === 'project') { body.admin = entityPermissions.admin @@ -468,7 +549,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( } if (level === 'environment') { body.admin = entityPermissions.admin - body.environment = envId || id + body.environment = (envId || id) as number } if (entityId || permissionWasCreated) { updateRolePermissions({ @@ -505,6 +586,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( role, updateRolePermissions, ]) + const [limitedPermissions, setLimitedPermissions] = useState([]) useEffect(() => { if (valueChanged) { @@ -512,13 +594,51 @@ const _EditPermissionsModal: FC = withAdminPermissions( } //eslint-disable-next-line }, [valueChanged]) + + const selectPermissions = ( + key: string, + value: 'GRANTED' | 'GRANTED_FOR_TAGS' | 'NONE', + tags: number[] = [], + ) => { + const updatedPermissions = [ + ...entityPermissions.permissions.filter( + (v) => v.permission_key !== key, + ), + ] + const updatedLimitedPermissions = limitedPermissions.filter( + (v) => v !== key, + ) + if (value === 'NONE') { + setEntityPermissions({ + ...entityPermissions, + permissions: updatedPermissions, + }) + } else { + setEntityPermissions({ + ...entityPermissions, + permissions: updatedPermissions.concat([ + { + permission_key: key, + tags, + }, + ]), + }) + } + if (value === 'GRANTED_FOR_TAGS') { + setLimitedPermissions(updatedLimitedPermissions.concat([key])) + } else { + setLimitedPermissions(updatedLimitedPermissions) + } + } const togglePermission = (key: string) => { if (role) { permissionChanged?.() const updatedPermissions = [...entityPermissions.permissions] - const index = updatedPermissions.indexOf(key) + const index = updatedPermissions.findIndex( + (v) => v.permission_key === key, + ) if (index === -1) { - updatedPermissions.push(key) + updatedPermissions.push({ permission_key: key, tags: [] }) } else { updatedPermissions.splice(index, 1) } @@ -530,10 +650,14 @@ const _EditPermissionsModal: FC = withAdminPermissions( } else { const newEntityPermissions = { ...entityPermissions } - const index = newEntityPermissions.permissions.indexOf(key) - + const index = newEntityPermissions.permissions.findIndex( + (v) => v.permission_key === key, + ) if (index === -1) { - newEntityPermissions.permissions.push(key) + newEntityPermissions.permissions.push({ + permission_key: key, + tags: [], + }) } else { newEntityPermissions.permissions.splice(index, 1) } @@ -600,7 +724,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( deleteRolePermissionUser({ organisation_id: id, role_id: roleId, - user_id: roleSelected?.user_role_id, + user_id: roleSelected?.user_role_id!, }).then(onRoleRemoved as any) } } @@ -613,7 +737,7 @@ const _EditPermissionsModal: FC = withAdminPermissions( }).then(onRoleRemoved as any) } else if (roleSelected) { deleteRolePermissionGroup({ - group_id: roleSelected.group_role_id, + group_id: roleSelected.group_role_id!, organisation_id: id, role_id: roleId, }).then(onRoleRemoved as any) @@ -628,7 +752,8 @@ const _EditPermissionsModal: FC = withAdminPermissions( if (user) { setRolesSelected( (rolesSelected || []).concat({ - role: usersData?.role, + group_role_id: undefined, + role: usersData?.role!, user_role_id: usersData?.id, }), ) @@ -637,7 +762,8 @@ const _EditPermissionsModal: FC = withAdminPermissions( setRolesSelected( (rolesSelected || []).concat({ group_role_id: groupsData?.id, - role: groupsData?.role, + role: groupsData?.role!, + user_role_id: undefined, }), ) } @@ -657,27 +783,30 @@ const _EditPermissionsModal: FC = withAdminPermissions( if (matchedRole) { if (user) { return { + group_role_id: undefined, ...role, user_role_id: matchedRole.user_role_id, } } if (group) { return { + user_role_id: undefined, ...role, group_role_id: matchedRole.group_role_id, } } } - return role + return { + group_role_id: undefined, + user_role_id: undefined, + ...role, + } }) } const rolesAdded = getRoles(roles, rolesSelected || []) - const isAdmin = admin() - const [search, setSearch] = useState() - return !permissions || !entityPermissions ? (
@@ -696,50 +825,90 @@ const _EditPermissionsModal: FC = withAdminPermissions( { - toggleAdmin() - setValueChanged(true) - }} - checked={isAdmin} - /> - -
- )} + data-test={`admin-switch-${level}`} + onChange={() => { + toggleAdmin() + setValueChanged(true) + }} + checked={isAdmin} + /> + +
+ )} { + filterRow={(item: AvailablePermission, search: string) => { const name = Format.enumeration.get(item.key).toLowerCase() return name.includes(search?.toLowerCase() || '') }} title='Permissions' - className='no-pad mb-2' + className='no-pad mb-2 overflow-visible' items={permissions} - renderRow={(p: AvailablePermission) => { - const levelUpperCase = level.toUpperCase() - const disabled = - level !== 'organisation' && - p.key !== `VIEW_${levelUpperCase}` && - !hasPermission(`VIEW_${levelUpperCase}`) - return ( - - - - {Format.enumeration.get(p.key)} + renderRow={(p: AvailablePermission, index: number) => { + const levelUpperCase = level.toUpperCase() + const disabled = + level !== 'organisation' && + p.key !== `VIEW_${levelUpperCase}` && + !hasPermission(`VIEW_${levelUpperCase}`) + const permission = entityPermissions.permissions.find( + (v) => v.permission_key === p.key, + ) + const permissionType = getPermissionType(p.key) + return ( + + + + {Format.enumeration.get(p.key)}
{p.description}
-
- { - setValueChanged(true) - togglePermission(p.key) - }} - disabled={disabled || admin() || saving} - checked={!disabled && hasPermission(p.key)} - /> + {permissionType === 'GRANTED_FOR_TAGS' && ( + { + setValueChanged(true) + selectPermissions(p.key, 'GRANTED_FOR_TAGS', v) + }} + /> + )} +
+ {tagBasedPermissions ? ( +
+ { const { email, first_name, id, last_name } = @@ -415,7 +416,11 @@ const CreateGroup: FC = ({ group, orgId, roles }) => {
{group ? ( <> - diff --git a/frontend/web/components/modals/CreateRole.tsx b/frontend/web/components/modals/CreateRole.tsx index a310afee599e..579437cfab30 100644 --- a/frontend/web/components/modals/CreateRole.tsx +++ b/frontend/web/components/modals/CreateRole.tsx @@ -349,6 +349,7 @@ const CreateRole: FC = ({ className: 'full-width', name: 'roleName', }} + data-test='role-name' value={roleName} unsaved={isEdit && roleNameChanged} onChange={(event: InputEvent) => { @@ -445,6 +446,7 @@ const CreateRole: FC = ({ Members} + data-test='members-tab' >
@@ -513,6 +515,7 @@ const CreateRole: FC = ({ Permissions} + data-test='permissions-tab' >
{({ permission: publishPermission }) => ( diff --git a/frontend/web/components/pages/EnvironmentSettingsPage.js b/frontend/web/components/pages/EnvironmentSettingsPage.js index 687969ddf64e..4ba9dfa4782a 100644 --- a/frontend/web/components/pages/EnvironmentSettingsPage.js +++ b/frontend/web/components/pages/EnvironmentSettingsPage.js @@ -17,7 +17,7 @@ import Icon from 'components/Icon' import PageTitle from 'components/PageTitle' import { getStore } from 'common/store' import { getRoles } from 'common/services/useRole' -import { getRolesEnvironmentPermissions } from 'common/services/useRolePermission' +import { getRoleEnvironmentPermissions } from 'common/services/useRolePermission' import AccountStore from 'common/stores/account-store' import { Link } from 'react-router-dom' import { enableFeatureVersioning } from 'common/services/useEnableFeatureVersioning' @@ -67,7 +67,7 @@ const EnvironmentSettingsPage = class extends Component { { organisation_id: AccountStore.getOrganisation().id }, { forceRefetch: true }, ).then((roles) => { - getRolesEnvironmentPermissions( + getRoleEnvironmentPermissions( getStore(), { env_id: env.id, @@ -76,6 +76,7 @@ const EnvironmentSettingsPage = class extends Component { }, { forceRefetch: true }, ).then((res) => { + debugger const matchingItems = roles.data.results.filter((item1) => res.data.results.some((item2) => item2.role === item1.id), ) @@ -165,8 +166,6 @@ const EnvironmentSettingsPage = class extends Component { allow_client_traits: !!this.state.allow_client_traits, banner_colour: this.state.banner_colour, banner_text: this.state.banner_text, - use_identity_overrides_in_local_eval: - this.state.use_identity_overrides_in_local_eval, description: description || env.description, hide_disabled_flags: this.state.hide_disabled_flags, hide_sensitive_data: !!this.state.hide_sensitive_data, @@ -176,6 +175,8 @@ const EnvironmentSettingsPage = class extends Component { name: name || env.name, use_identity_composite_key_for_hashing: !!this.state.use_identity_composite_key_for_hashing, + use_identity_overrides_in_local_eval: + this.state.use_identity_overrides_in_local_eval, use_mv_v2_evaluation: !!this.state.use_mv_v2_evaluation, }), ) @@ -256,10 +257,10 @@ const EnvironmentSettingsPage = class extends Component { props: { webhooks, webhooksLoading }, state: { allow_client_traits, - use_identity_overrides_in_local_eval, hide_sensitive_data, name, use_identity_composite_key_for_hashing, + use_identity_overrides_in_local_eval, use_v2_feature_versioning, }, } = this @@ -286,8 +287,6 @@ const EnvironmentSettingsPage = class extends Component { ) { setTimeout(() => { this.setState({ - use_identity_overrides_in_local_eval: - !!env.use_identity_overrides_in_local_eval, allow_client_traits: !!env.allow_client_traits, banner_colour: env.banner_colour || Constants.tagColors[0], banner_text: env.banner_text, @@ -301,6 +300,8 @@ const EnvironmentSettingsPage = class extends Component { name: env.name, use_identity_composite_key_for_hashing: !!env.use_identity_composite_key_for_hashing, + use_identity_overrides_in_local_eval: + !!env.use_identity_overrides_in_local_eval, use_v2_feature_versioning: !!env.use_v2_feature_versioning, }) }, 10) @@ -447,15 +448,18 @@ const EnvironmentSettingsPage = class extends Component { {Utils.getFlagsmithHasFeature('feature_versioning') && (
- { - this.setState({ - use_v2_feature_versioning: true, - }) - }} - /> + {use_v2_feature_versioning === false && ( + { + this.setState({ + use_v2_feature_versioning: true, + }) + }} + /> + )} + {' '} for your selected environment. - - {({ permission }) => ( - - -
- { + +
+ { + this.setState( + { + search: Utils.safeParseEventValue(e), + }, + this.filter, + ) + }} + value={this.state.search} + /> + + { + this.setState( + { + tag_strategy, + }, + this.filter, + ) + }} + value={this.state.tags} + onToggleArchived={(value) => { + if (value !== this.state.showArchived) { + FeatureListStore.isLoading = true this.setState( { - search: - Utils.safeParseEventValue(e), + showArchived: + !this.state.showArchived, }, this.filter, ) - }} - value={this.state.search} - /> - - { - this.setState( - { - tag_strategy, - }, - this.filter, - ) - }} - value={this.state.tags} - onToggleArchived={(value) => { - if ( - value !== this.state.showArchived - ) { - FeatureListStore.isLoading = true - this.setState( - { - showArchived: - !this.state.showArchived, - }, - this.filter, - ) - } - }} - showArchived={this.state.showArchived} - onClearAll={() => { - FeatureListStore.isLoading = true - this.setState( - { showArchived: false, tags: [] }, - this.filter, - ) - }} - onChange={(tags, isAutomated) => { - FeatureListStore.isLoading = true - if ( - tags.includes('') && - tags.length > 1 - ) { - if ( - !this.state.tags.includes('') - ) { - this.setState( - { tags: [''] }, - this.filter, - ) - } else { - this.setState( - { - tags: tags.filter( - (v) => !!v, - ), - }, - this.filter, - ) - } - } else { - this.setState( - { tags }, - this.filter, - ) - } - }} - /> - { + } + }} + showArchived={this.state.showArchived} + onClearAll={() => { + FeatureListStore.isLoading = true + this.setState( + { showArchived: false, tags: [] }, + this.filter, + ) + }} + onChange={(tags, isAutomated) => { + FeatureListStore.isLoading = true + if ( + tags.includes('') && + tags.length > 1 + ) { + if (!this.state.tags.includes('')) { this.setState( - { - is_enabled: enabled, - value_search: valueSearch, - }, + { tags: [''] }, this.filter, ) - }} - /> - { - FeatureListStore.isLoading = true + } else { this.setState( { - owners: owners, + tags: tags.filter((v) => !!v), }, this.filter, ) - }} - /> - { - FeatureListStore.isLoading = true - this.setState( - { - group_owners: group_owners, - }, - this.filter, - ) - }} - /> - - { - FeatureListStore.isLoading = true - this.setState({ sort }, this.filter) - }} - /> - -
- - } - nextPage={() => - this.filter(FeatureListStore.paging.next) - } - prevPage={() => - this.filter(FeatureListStore.paging.previous) - } - goToPage={(page) => this.filter(page)} - items={projectFlags?.filter((v) => !v.ignore)} - renderFooter={() => ( - <> - + { + this.setState( + { + is_enabled: enabled, + value_search: valueSearch, + }, + this.filter, + ) + }} + /> + { + FeatureListStore.isLoading = true + this.setState( + { + owners: owners, + }, + this.filter, + ) + }} + /> + { + FeatureListStore.isLoading = true + this.setState( + { + group_owners: group_owners, + }, + this.filter, + ) + }} + /> + - { + FeatureListStore.isLoading = true + this.setState({ sort }, this.filter) + }} /> - + +
+ + } + nextPage={() => + this.filter(FeatureListStore.paging.next) + } + prevPage={() => + this.filter(FeatureListStore.paging.previous) + } + goToPage={(page) => this.filter(page)} + items={projectFlags?.filter((v) => !v.ignore)} + renderFooter={() => ( + <> + + + + )} + renderRow={(projectFlag, i) => ( + ( + id={this.props.match.params.environmentId} + > + {({ permission }) => ( )} - /> -
- )} -
+ + )} + /> + { - getRolesProjectPermissions( + getRoleProjectPermissions( getStore(), { organisation_id: AccountStore.getOrganisation().id, diff --git a/frontend/web/components/pages/UserPage.tsx b/frontend/web/components/pages/UserPage.tsx index 43454dc20e09..f8b3967f9971 100644 --- a/frontend/web/components/pages/UserPage.tsx +++ b/frontend/web/components/pages/UserPage.tsx @@ -336,626 +336,720 @@ const UserPage: FC = (props) => { id={environmentId} > {({ permission: manageUserPermission }) => ( - - {({ permission }) => ( -
- - {( - { - environmentFlags, - identity, - identityFlags, - isLoading, - projectFlags, - traits, - }: any, - { toggleFlag }: any, - ) => - isLoading && - !tags.length && - !showArchived && - typeof search !== 'string' && - (!identityFlags || !actualFlags || !projectFlags) ? ( -
- -
- ) : ( - <> - -
- + + {( + { + environmentFlags, + identity, + identityFlags, + isLoading, + projectFlags, + traits, + }: any, + { toggleFlag }: any, + ) => + isLoading && + !tags.length && + !showArchived && + typeof search !== 'string' && + (!identityFlags || !actualFlags || !projectFlags) ? ( +
+ +
+ ) : ( + <> + +
+ + {showAliases && ( +
+ + Alias:{' '} + } + > + Aliases allow you to add searchable names to + an identity + + - {showAliases && ( -
- - Alias:{' '} - - } +
+ )} +
+ +
+ } + > + View and manage feature states and traits for this user. +
+
+
+
+ + + + Features +
+ - Aliases allow you to add searchable names - to an identity - - +
+
+ } + renderFooter={() => ( + <> + + + + + )} + header={ + +
+ { + FeatureListStore.isLoading = true + setSearch(Utils.safeParseEventValue(e)) + }} + value={search} /> - - )} -
- -
- } - > - View and manage feature states and traits for this - user. -
- -
-
- - - - Features -
- - Overriding features here will take - priority over any segment override. - Any features that are not overridden - for this user will fallback to any - segment overrides or the environment - defaults. - -
-
- } - renderFooter={() => ( - <> - + { + setTagStrategy(strategy) + }} + isLoading={FeatureListStore.isLoading} + onToggleArchived={(value) => { + if (value !== showArchived) { + FeatureListStore.isLoading = true + setShowArchived(!showArchived) + } + }} + showArchived={showArchived} + onChange={(newTags) => { + FeatureListStore.isLoading = true + setTags( + newTags.includes('') && + newTags.length > 1 + ? [''] + : newTags, + ) + }} /> - { + setIsEnabled(enabled) + setValueSearch(valueSearch) + }} + /> + { + FeatureListStore.isLoading = true + setOwners(newOwners) + }} /> - { + FeatureListStore.isLoading = true + setGroupOwners(newGroupOwners) + }} + /> + + { + FeatureListStore.isLoading = true + setSort(newSort) + }} /> - - )} - header={ - -
- { - FeatureListStore.isLoading = true - setSearch( - Utils.safeParseEventValue(e), - ) - }} - value={search} - /> - - { - setTagStrategy(strategy) - }} - isLoading={ - FeatureListStore.isLoading - } - onToggleArchived={(value) => { - if (value !== showArchived) { - FeatureListStore.isLoading = - true - setShowArchived(!showArchived) - } - }} - showArchived={showArchived} - onChange={(newTags) => { - FeatureListStore.isLoading = true - setTags( - newTags.includes('') && - newTags.length > 1 - ? [''] - : newTags, - ) - }} - /> - { - setIsEnabled(enabled) - setValueSearch(valueSearch) - }} - /> - { - FeatureListStore.isLoading = true - setOwners(newOwners) - }} - /> - { - FeatureListStore.isLoading = true - setGroupOwners(newGroupOwners) - }} - /> - - { - FeatureListStore.isLoading = true - setSort(newSort) - }} - /> - -
- } - isLoading={FeatureListStore.isLoading} - items={projectFlags} - renderRow={( - { description, id: featureId, name }: any, - i: number, - ) => { - const identityFlag = - identityFlags[featureId] || {} - const environmentFlag = - (environmentFlags && - environmentFlags[featureId]) || - {} - const hasUserOverride = - identityFlag.identity || - identityFlag.identity_uuid - const flagEnabled = hasUserOverride - ? identityFlag.enabled - : environmentFlag.enabled - const flagValue = hasUserOverride - ? identityFlag.feature_state_value - : environmentFlag.feature_state_value - const actualEnabled = - actualFlags && actualFlags[name]?.enabled - const actualValue = - actualFlags && - actualFlags[name]?.feature_state_value - const flagEnabledDifferent = hasUserOverride - ? false - : actualEnabled !== flagEnabled - const flagValueDifferent = hasUserOverride - ? false - : !valuesEqual(actualValue, flagValue) - const projectFlag = projectFlags?.find( - (p: any) => - p.id === environmentFlag.feature, - ) - const isMultiVariateOverride = - flagValueDifferent && - projectFlag?.multivariate_options?.find( - (v: any) => - Utils.featureStateToValue(v) === - actualValue, +
+ + } + isLoading={FeatureListStore.isLoading} + items={projectFlags} + renderRow={( + { description, id: featureId, name, tags }: any, + i: number, + ) => { + return ( + + {({ permission }) => { + const identityFlag = + identityFlags[featureId] || {} + const environmentFlag = + (environmentFlags && + environmentFlags[featureId]) || + {} + const hasUserOverride = + identityFlag.identity || + identityFlag.identity_uuid + const flagEnabled = hasUserOverride + ? identityFlag.enabled + : environmentFlag.enabled + const flagValue = hasUserOverride + ? identityFlag.feature_state_value + : environmentFlag.feature_state_value + const actualEnabled = + actualFlags && + actualFlags[name]?.enabled + const actualValue = + actualFlags && + actualFlags[name]?.feature_state_value + const flagEnabledDifferent = + hasUserOverride + ? false + : actualEnabled !== flagEnabled + const flagValueDifferent = hasUserOverride + ? false + : !valuesEqual(actualValue, flagValue) + const projectFlag = projectFlags?.find( + (p: any) => + p.id === environmentFlag.feature, ) - const flagDifferent = - flagEnabledDifferent || flagValueDifferent - - const onClick = () => { - if (permission) { - editFeature( - projectFlag, - environmentFlags[featureId], - identityFlags[featureId] || - actualFlags![name], - identityFlags[featureId] - ?.multivariate_feature_state_values, + const isMultiVariateOverride = + flagValueDifferent && + projectFlag?.multivariate_options?.find( + (v: any) => + Utils.featureStateToValue(v) === + actualValue, ) + const flagDifferent = + flagEnabledDifferent || + flagValueDifferent + + const onClick = () => { + if (permission) { + editFeature( + projectFlag, + environmentFlags[featureId], + identityFlags[featureId] || + actualFlags![name], + identityFlags[featureId] + ?.multivariate_feature_state_values, + ) + } } - } - const isCompact = - getViewMode() === 'compact' - if (name === preselect && actualFlags) { - setPreselect(null) - onClick() - } + const isCompact = + getViewMode() === 'compact' + if (name === preselect && actualFlags) { + setPreselect(null) + onClick() + } - return ( -
- - - - - - - {description ? ( - {name} - } - > - {description} - - ) : ( - name - )} - - - - - - {hasUserOverride ? ( -
- Overriding defaults -
- ) : flagEnabledDifferent ? ( -
+ + + + - - {isMultiVariateOverride ? ( - - This flag is being - overridden by a - variation defined on - your feature, the - control value is{' '} - - {flagEnabled - ? 'on' - : 'off'} - {' '} - for this user - + + {description ? ( + {name} + } + > + {description} + ) : ( - - This flag is being - overridden by segments - and would normally be{' '} - - {flagEnabled - ? 'on' - : 'off'} - {' '} - for this user - + name )} - - -
- ) : flagValueDifferent ? ( - isMultiVariateOverride ? ( -
- - This feature is being - overridden by a % - variation in the - environment, the control - value of this feature is{' '} - + + + + + {hasUserOverride ? ( +
+ Overriding defaults
- ) : ( + ) : flagEnabledDifferent ? (
- - This feature is being - overridden by segments and - would normally be{' '} - {' '} - for this user - + + + {isMultiVariateOverride ? ( + + This flag is being + overridden by a + variation defined on + your feature, the + control value is{' '} + + {flagEnabled + ? 'on' + : 'off'} + {' '} + for this user + + ) : ( + + This flag is being + overridden by + segments and would + normally be{' '} + + {flagEnabled + ? 'on' + : 'off'} + {' '} + for this user + + )} + +
- ) - ) : ( - getViewMode() === 'default' && ( -
- Using environment defaults -
- ) - )} - - - -
- -
-
e.stopPropagation()} - > - {Utils.renderWithPermission( - permission, - Constants.environmentPermissions( - Utils.getManageFeaturePermissionDescription( - false, - true, + ) : flagValueDifferent ? ( + isMultiVariateOverride ? ( +
+ + This feature is being + overridden by a % + variation in the + environment, the control + value of this feature is{' '} + + +
+ ) : ( +
+ + This feature is being + overridden by segments + and would normally be{' '} + {' '} + for this user + +
+ ) + ) : ( + getViewMode() === + 'default' && ( +
+ Using environment defaults +
+ ) + )} + + + +
+ +
+
e.stopPropagation()} + > + {Utils.renderWithPermission( + permission, + Constants.environmentPermissions( + Utils.getManageFeaturePermissionDescription( + false, + true, + ), ), - ), - - confirmToggle( - projectFlag, - actualFlags![name], - () => - toggleFlag({ - environmentFlag: - actualFlags![name], - environmentId, - identity: id, - identityFlag, - projectFlag: { - id: featureId, - }, - }), - ) - } - />, - )} -
-
e.stopPropagation()} - > - {hasUserOverride && ( - <> - {Utils.renderWithPermission( - permission, - Constants.environmentPermissions( - Utils.getManageFeaturePermissionDescription( - false, - true, + + confirmToggle( + projectFlag, + actualFlags![name], + () => + toggleFlag({ + environmentFlag: + actualFlags![name], + environmentId, + identity: id, + identityFlag, + projectFlag: { + id: featureId, + }, + }), + ) + } + />, + )} +
+
e.stopPropagation()} + > + {hasUserOverride && ( + <> + {Utils.renderWithPermission( + permission, + Constants.environmentPermissions( + Utils.getManageFeaturePermissionDescription( + false, + true, + ), ), - ), - , - )} - - )} + , + )} + + )} +
+ ) + }} + + ) + }} + renderSearchWithNoResults + paging={FeatureListStore.paging} + search={search} + nextPage={() => + AppActions.getFeatures( + projectId, + environmentId, + true, + search, + sort, + FeatureListStore.paging.next, + getFilter(), + ) + } + prevPage={() => + AppActions.getFeatures( + projectId, + environmentId, + true, + search, + sort, + FeatureListStore.paging.previous, + getFilter(), + ) + } + goToPage={(pageNumber: number) => + AppActions.getFeatures( + projectId, + environmentId, + true, + search, + sort, + pageNumber, + getFilter(), + ) + } + /> + + {!preventAddTrait && ( + + + {Utils.renderWithPermission( + manageUserPermission, + Constants.environmentPermissions( + Utils.getManageUserPermissionDescription(), + ), + , + )} +
+ } + header={ + + + Trait + + Value +
+ Remove +
+
+ } + renderRow={( + { id, trait_key, trait_value }: any, + i: number, + ) => ( + + editTrait({ + id, + trait_key, + trait_value, + }) + } + > + +
+ {trait_key}
- ) - }} - renderSearchWithNoResults - paging={FeatureListStore.paging} - search={search} - nextPage={() => - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - FeatureListStore.paging.next, - getFilter(), - ) - } - prevPage={() => - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - FeatureListStore.paging.previous, - getFilter(), - ) - } - goToPage={(pageNumber: number) => - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - pageNumber, - getFilter(), - ) - } - /> - - {!preventAddTrait && ( - - + + + +
e.stopPropagation()} + > + {Utils.renderWithPermission( + manageUserPermission, + Constants.environmentPermissions( + Utils.getManageUserPermissionDescription(), + ), + , + )} +
+
+ )} + renderNoResults={ + + className='no-pad' + action={ +
{Utils.renderWithPermission( manageUserPermission, Constants.environmentPermissions( @@ -963,7 +1057,9 @@ const UserPage: FC = (props) => { ),
} + > +
+ + This user has no traits. + +
+
+ } + filterRow={( + { trait_key }: any, + searchString: string, + ) => + trait_key + .toLowerCase() + .indexOf(searchString.toLowerCase()) > -1 + } + /> + + )} + + {({ segments }: any) => + !segments ? ( +
+ +
+ ) : ( + + - - Trait + + Name - Value + Description -
- Remove -
} + items={segments || []} renderRow={( - { id, trait_key, trait_value }: any, + { created_date, description, name }: any, i: number, ) => ( - editTrait({ - id, - trait_key, - trait_value, - }) - } + key={i} + onClick={() => editSegment(segments[i])} > - +
+ editSegment(segments[i]) + } > - {trait_key} + + {name} + +
+
+ Created{' '} + {moment(created_date).format( + 'DD/MMM/YYYY', + )}
- - - -
e.stopPropagation()} - > - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , + + {description && ( +
{description}
)} -
+
)} renderNoResults={ - {Utils.renderWithPermission( - manageUserPermission, - Constants.environmentPermissions( - Utils.getManageUserPermissionDescription(), - ), - , - )} -
- } >
- This user has no traits. + This user is not a member of any + segments.
} filterRow={( - { trait_key }: any, + { name }: any, searchString: string, ) => - trait_key + name .toLowerCase() .indexOf(searchString.toLowerCase()) > -1 } /> - )} - - {({ segments }: any) => - !segments ? ( -
- -
- ) : ( - - - - Name - - - Description - - - } - items={segments || []} - renderRow={( - { - created_date, - description, - name, - }: any, - i: number, - ) => ( - - editSegment(segments[i]) - } - > - -
- editSegment(segments[i]) - } - > - - {name} - -
-
- Created{' '} - {moment(created_date).format( - 'DD/MMM/YYYY', - )} -
-
- - {description && ( -
{description}
- )} -
-
- )} - renderNoResults={ - -
- - This user is not a member of any - segments. - -
-
- } - filterRow={( - { name }: any, - searchString: string, - ) => - name - .toLowerCase() - .indexOf( - searchString.toLowerCase(), - ) > -1 - } - /> -
- ) - } -
- -
-
- - - - - - -
-
- - ) - } - -
- )} - + ) + } + + +
+
+ + + + + + +
+
+ + ) + } + +
)}
diff --git a/frontend/web/components/pages/UsersAndPermissionsPage.tsx b/frontend/web/components/pages/UsersAndPermissionsPage.tsx index 473824f93b5c..dbe521f29b1c 100644 --- a/frontend/web/components/pages/UsersAndPermissionsPage.tsx +++ b/frontend/web/components/pages/UsersAndPermissionsPage.tsx @@ -38,6 +38,7 @@ import Icon from 'components/Icon' import RolesTable from 'components/RolesTable' import UsersGroups from 'components/UsersGroups' import PlanBasedBanner, { getPlanBasedOption } from 'components/PlanBasedAccess' +import { useHasPermission } from 'common/providers/Permission' type UsersAndPermissionsPageType = { router: RouterChildContext['router'] @@ -68,15 +69,22 @@ const UsersAndPermissionsInner: FC = ({ subscriptionMeta, users, }) => { - const orgId = AccountStore.getOrganisation().id const paymentsEnabled = Utils.getFlagsmithHasFeature('payments_enabled') const verifySeatsLimit = Utils.getFlagsmithHasFeature( 'verify_seats_limit_for_invite_links', ) - const permissionsError = !( - AccountStore.getUser() && AccountStore.getOrganisationRole() === 'ADMIN' - ) + const manageUsersPermission = useHasPermission({ + id: AccountStore.getOrganisation()?.id, + level: 'organisation', + permission: 'MANAGE_USERS', + }) + const manageGroupsPermission = useHasPermission({ + id: AccountStore.getOrganisation()?.id, + level: 'organisation', + permission: 'MANAGE_USER_GROUPS', + }) + const roleChanged = (id: number, { value: role }: { value: string }) => { AppActions.updateUserRole(id, role) } @@ -222,11 +230,12 @@ const UsersAndPermissionsInner: FC = ({
Team Members
{Utils.renderWithPermission( - !permissionsError, + !manageUsersPermission.permission, Constants.organisationPermissions('Admin'),
- + { !!this.state.tags.length) && !isLoading) ? (
- - {({ permission }) => ( -
- { - this.setState( - { search: Utils.safeParseEventValue(e) }, - () => { - AppActions.searchFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - this.getFilter(), - this.props.pageSize, - ) - }, - ) - }} - nextPage={() => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - ( - FeatureListStore.paging as PagedResponse - ).next || 1, - this.getFilter(), - this.props.pageSize, - ) - } - prevPage={() => - AppActions.getFeatures( +
+ { + this.setState( + { search: Utils.safeParseEventValue(e) }, + () => { + AppActions.searchFeatures( this.props.projectId, this.props.environmentId, true, this.state.search, this.state.sort, - ( - FeatureListStore.paging as PagedResponse - ).previous, this.getFilter(), this.props.pageSize, ) - } - goToPage={(page: number) => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - page, - this.getFilter(), - this.props.pageSize, - ) - } - onSortChange={(sort: string) => { - this.setState({ sort }, () => { - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - 0, - this.getFilter(), - this.props.pageSize, - ) - }) - }} - sorting={[ - { - default: true, - label: 'Name', - order: 'asc', - value: 'name', - }, - { - label: 'Created Date', - order: 'asc', - value: 'created_date', - }, - ]} - items={projectFlags} - header={ - this.props.hideTags ? null : ( - - + }, + ) + }} + nextPage={() => + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + ( + FeatureListStore.paging as PagedResponse + ).next || 1, + this.getFilter(), + this.props.pageSize, + ) + } + prevPage={() => + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + ( + FeatureListStore.paging as PagedResponse + ).previous, + this.getFilter(), + this.props.pageSize, + ) + } + goToPage={(page: number) => + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + page, + this.getFilter(), + this.props.pageSize, + ) + } + onSortChange={(sort: string) => { + this.setState({ sort }, () => { + AppActions.getFeatures( + this.props.projectId, + this.props.environmentId, + true, + this.state.search, + this.state.sort, + 0, + this.getFilter(), + this.props.pageSize, + ) + }) + }} + sorting={[ + { + default: true, + label: 'Name', + order: 'asc', + value: 'name', + }, + { + label: 'Created Date', + order: 'asc', + value: 'created_date', + }, + ]} + items={projectFlags} + header={ + this.props.hideTags ? null : ( + + + this.setState( + { showArchived: false, tags: [] }, + this.filter, + ) + } + projectId={projectId} + value={this.state.tags} + tagStrategy={this.state.tag_strategy} + onChangeStrategy={(tag_strategy) => { + this.setState( + { tag_strategy }, + this.filter, + ) + }} + onChange={(tags) => { + FeatureListStore.isLoading = true + if ( + tags.includes('') && + tags.length > 1 + ) { + if (!this.state.tags.includes('')) { this.setState( - { showArchived: false, tags: [] }, + { tags: [''] }, this.filter, ) - } - projectId={projectId} - value={this.state.tags} - tagStrategy={this.state.tag_strategy} - onChangeStrategy={(tag_strategy) => { + } else { this.setState( - { tag_strategy }, + { + tags: tags.filter((v) => !!v), + }, this.filter, ) - }} - onChange={(tags) => { + } + } else { + this.setState({ tags }, this.filter) + } + AsyncStorage.setItem( + `${projectId}tags`, + JSON.stringify(tags), + ) + }} + > +
+ { FeatureListStore.isLoading = true - if ( - tags.includes('') && - tags.length > 1 - ) { - if ( - !this.state.tags.includes('') - ) { - this.setState( - { tags: [''] }, - this.filter, - ) - } else { - this.setState( - { - tags: tags.filter( - (v) => !!v, - ), - }, - this.filter, - ) - } - } else { - this.setState( - { tags }, - this.filter, - ) - } - AsyncStorage.setItem( - `${projectId}tags`, - JSON.stringify(tags), + this.setState( + { + showArchived: + !this.state.showArchived, + }, + this.filter, ) }} - > -
- { - FeatureListStore.isLoading = - true - this.setState( - { - showArchived: - !this.state.showArchived, - }, - this.filter, - ) - }} - className='px-2 py-2 ml-2 mr-2' - tag={{ - color: '#0AADDF', - label: 'Archived', - }} - /> -
- - - ) - } - renderRow={( - projectFlag: ProjectFlag, - i: number, - ) => ( + className='px-2 py-2 ml-2 mr-2' + tag={{ + color: '#0AADDF', + label: 'Archived', + }} + /> +
+
+
+ ) + } + renderRow={( + projectFlag: ProjectFlag, + i: number, + ) => ( + + {({ permission }) => ( { projectFlag={projectFlag} /> )} - filterRow={() => true} - /> -
- )} - + + )} + filterRow={() => true} + /> +
) : null} diff --git a/frontend/web/components/tags/AddEditTags.tsx b/frontend/web/components/tags/AddEditTags.tsx index 0363268e061c..0eb8c0ad0cea 100644 --- a/frontend/web/components/tags/AddEditTags.tsx +++ b/frontend/web/components/tags/AddEditTags.tsx @@ -217,7 +217,11 @@ const AddEditTags: FC = ({ <>
editTag(tag)} - className='clickable' + className={ + !readOnly + ? 'clickable' + : 'opacity-0 pointer-events-none' + } >
diff --git a/frontend/web/components/tags/TagValues.tsx b/frontend/web/components/tags/TagValues.tsx index 719641b22f1b..f99ddda2f03c 100644 --- a/frontend/web/components/tags/TagValues.tsx +++ b/frontend/web/components/tags/TagValues.tsx @@ -2,6 +2,9 @@ import React, { FC, Fragment, ReactNode } from 'react' import Button from 'components/base/forms/Button' import Tag from './Tag' import { useGetTagsQuery } from 'common/services/useTag' +import Utils from 'common/utils/utils' +import Constants from 'common/constants' +import { useHasPermission } from 'common/providers/Permission' type TagValuesType = { onAdd?: () => void @@ -22,6 +25,16 @@ const TagValues: FC = ({ }) => { const { data: tags } = useGetTagsQuery({ projectId }) const Wrapper = inline ? Fragment : Row + const permissionType = Utils.getFlagsmithHasFeature('manage_tags_permission') + ? 'MANAGE_TAGS' + : 'ADMIN' + + const { permission: createEditTagPermission } = useHasPermission({ + id: projectId, + level: 'project', + permission: permissionType, + }) + return ( {children} @@ -36,11 +49,22 @@ const TagValues: FC = ({ /> ), )} - {!!onAdd && ( - - )} + {!!onAdd && + Utils.renderWithPermission( + createEditTagPermission, + Constants.projectPermissions( + permissionType === 'ADMIN' ? 'Admin' : 'Manage Tags', + ), + , + )} ) } diff --git a/frontend/web/project/project-components.js b/frontend/web/project/project-components.js index 5a81e3a065ac..345ff3968c82 100644 --- a/frontend/web/project/project-components.js +++ b/frontend/web/project/project-components.js @@ -149,7 +149,7 @@ global.Select = class extends PureComponent { className={`react-select ${props.size ? props.size : ''}`} classNamePrefix='react-select' {...props} - components={{ ...(props.components || {}), Option }} + components={{ Option, ...(props.components || {}) }} /> ) diff --git a/frontend/web/styles/components/_panel.scss b/frontend/web/styles/components/_panel.scss index 65695c058c7d..fa9f32df0837 100644 --- a/frontend/web/styles/components/_panel.scss +++ b/frontend/web/styles/components/_panel.scss @@ -77,7 +77,9 @@ } } } - +.overflow-visible>.panel-content { + overflow: visible; +} .dark { .panel { .icon { diff --git a/frontend/web/styles/project/_modals.scss b/frontend/web/styles/project/_modals.scss index d0ebfa3df366..9cca000670b1 100644 --- a/frontend/web/styles/project/_modals.scss +++ b/frontend/web/styles/project/_modals.scss @@ -160,7 +160,7 @@ $side-width: 750px; .assignees-list-item { color: $bg-dark100; font-weight: 500; - padding: 16px 0; + padding: 8px 0; border-bottom: 1px solid $basic-alpha-16; } &.right {