From fc34a53e92ba6c1870ab9539bfa21b6af08964cc Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Fri, 26 Apr 2024 09:33:14 +0100 Subject: [PATCH] feat(permissions): manage permissions from a single location (#3730) Co-authored-by: Zach Aysan --- .../common/dispatcher/action-constants.js | 3 - frontend/common/dispatcher/app-actions.js | 20 - .../common/providers/OrganisationProvider.js | 79 --- .../common/providers/OrganisationProvider.tsx | 95 ++++ frontend/common/providers/Permission.tsx | 9 +- .../common/providers/UserGroupsProvider.js | 42 -- frontend/common/services/useGroup.ts | 113 ++++- frontend/common/stores/organisation-store.js | 4 + frontend/common/stores/project-store.js | 10 + frontend/common/stores/user-group-store.js | 137 ----- frontend/common/types/requests.ts | 26 +- frontend/common/types/responses.ts | 24 + frontend/web/components/ActionButton.tsx | 26 + frontend/web/components/EditPermissions.tsx | 169 ++++--- frontend/web/components/FeatureAction.tsx | 41 +- frontend/web/components/GroupSelect.tsx | 25 +- frontend/web/components/PermissionsTabs.tsx | 151 ++++++ .../web/components/ProjectManageWidget.tsx | 2 +- .../web/components/RolePermissionsList.tsx | 105 +--- frontend/web/components/RolesTable.tsx | 39 +- frontend/web/components/UserAction.tsx | 136 +++++ frontend/web/components/UserGroupList.tsx | 58 ++- frontend/web/components/UserSelect.js | 37 +- frontend/web/components/modals/CreateFlag.js | 1 - frontend/web/components/modals/CreateGroup.js | 454 ----------------- .../web/components/modals/CreateGroup.tsx | 478 ++++++++++++++++++ frontend/web/components/modals/CreateRole.tsx | 99 +--- .../pages/OrganisationGroupsPage.js | 126 ----- .../pages/OrganisationSettingsPage.js | 361 +++++-------- frontend/web/project/project-components.js | 3 +- frontend/web/project/toast.js | 4 + frontend/web/routes.js | 6 - .../web/styles/3rdParty/_react-select.scss | 2 +- 33 files changed, 1445 insertions(+), 1440 deletions(-) delete mode 100644 frontend/common/providers/OrganisationProvider.js create mode 100644 frontend/common/providers/OrganisationProvider.tsx delete mode 100644 frontend/common/providers/UserGroupsProvider.js delete mode 100644 frontend/common/stores/user-group-store.js create mode 100644 frontend/web/components/ActionButton.tsx create mode 100644 frontend/web/components/PermissionsTabs.tsx create mode 100644 frontend/web/components/UserAction.tsx delete mode 100644 frontend/web/components/modals/CreateGroup.js create mode 100644 frontend/web/components/modals/CreateGroup.tsx delete mode 100644 frontend/web/components/pages/OrganisationGroupsPage.js diff --git a/frontend/common/dispatcher/action-constants.js b/frontend/common/dispatcher/action-constants.js index f6dd057227f9..87ef249fad24 100644 --- a/frontend/common/dispatcher/action-constants.js +++ b/frontend/common/dispatcher/action-constants.js @@ -5,7 +5,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), { 'CONFIRM_TWO_FACTOR': 'CONFIRM_TWO_FACTOR', 'CREATE_ENV': 'CREATE_ENV', 'CREATE_FLAG': 'CREATE_FLAG', - 'CREATE_GROUP': 'CREATE_GROUP', 'CREATE_ORGANISATION': 'CREATE_ORGANISATION', 'CREATE_PROJECT': 'CREATE_PROJECT', 'DELETE_CHANGE_REQUEST': 'DELETE_CHANGE_REQUEST', @@ -32,7 +31,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), { 'GET_ENVIRONMENT': 'GET_ENVIRONMENT', 'GET_FEATURE_USAGE': 'GET_FEATURE_USAGE', 'GET_FLAGS': 'GET_FLAGS', - 'GET_GROUPS': 'GET_GROUPS', 'GET_IDENTITY': 'GET_IDENTITY', 'GET_IDENTITY_SEGMENTS': 'GET_IDENTITY_SEGMENTS', 'GET_ORGANISATION': 'GET_ORGANISATION', @@ -51,7 +49,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), { 'TOGGLE_USER_FLAG': 'TOGGLE_USER_FLAG', 'TWO_FACTOR_LOGIN': 'TWO_FACTOR_LOGIN', 'UPDATE_CHANGE_REQUEST': 'UPDATE_CHANGE_REQUEST', - 'UPDATE_GROUP': 'UPDATE_GROUP', 'UPDATE_SUBSCRIPTION': 'UPDATE_SUBSCRIPTION', 'UPDATE_USER_ROLE': 'UPDATE_USER_ROLE', }) diff --git a/frontend/common/dispatcher/app-actions.js b/frontend/common/dispatcher/app-actions.js index 51b43e00f3a1..7b98bf388829 100644 --- a/frontend/common/dispatcher/app-actions.js +++ b/frontend/common/dispatcher/app-actions.js @@ -47,13 +47,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { segmentOverrides, }) }, - createGroup(orgId, data) { - Dispatcher.handleViewAction({ - actionType: Actions.CREATE_GROUP, - data, - orgId, - }) - }, createOrganisation(name) { Dispatcher.handleViewAction({ actionType: Actions.CREATE_ORGANISATION, @@ -263,12 +256,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { sort, }) }, - getGroups(orgId) { - Dispatcher.handleViewAction({ - actionType: Actions.GET_GROUPS, - orgId, - }) - }, getIdentity(envId, id) { Dispatcher.handleViewAction({ actionType: Actions.GET_IDENTITY, @@ -422,13 +409,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { changeRequest, }) }, - updateGroup(orgId, data) { - Dispatcher.handleViewAction({ - actionType: Actions.UPDATE_GROUP, - data, - orgId, - }) - }, updateSubscription(hostedPageId) { Dispatcher.handleViewAction({ actionType: Actions.UPDATE_SUBSCRIPTION, diff --git a/frontend/common/providers/OrganisationProvider.js b/frontend/common/providers/OrganisationProvider.js deleted file mode 100644 index 9374ad366c56..000000000000 --- a/frontend/common/providers/OrganisationProvider.js +++ /dev/null @@ -1,79 +0,0 @@ -import { Component } from 'react' -import OrganisationStore from 'common/stores/organisation-store' -import AccountStore from 'common/stores/account-store' -import UserGroupStore from 'common/stores/user-group-store' - -const OrganisationProvider = class extends Component { - static displayName = 'OrganisationProvider' - - constructor(props, context) { - super(props, context) - this.state = { - groups: UserGroupStore.getGroups(), - invites: OrganisationStore.getInvites(), - isLoading: OrganisationStore.isLoading, - name: - AccountStore.getOrganisation() && AccountStore.getOrganisation().name, - project: OrganisationStore.getProject(), - projects: OrganisationStore.getProjects(), - subscriptionMeta: OrganisationStore.getSubscriptionMeta(), - users: OrganisationStore.getUsers(), - } - ES6Component(this) - if (props.onRemoveProject) { - this.listenTo(OrganisationStore, 'removed', props.onRemoveProject) - } - - this.listenTo(OrganisationStore, 'change', () => { - this.setState({ - groups: UserGroupStore.getGroups(), - inviteLinks: OrganisationStore.getInviteLinks(), - invites: OrganisationStore.getInvites(), - isLoading: OrganisationStore.isLoading, - isSaving: OrganisationStore.isSaving, - project: OrganisationStore.getProject(), - projects: OrganisationStore.getProjects(this.props.id), - subscriptionMeta: OrganisationStore.getSubscriptionMeta(), - users: OrganisationStore.getUsers(), - }) - }) - this.listenTo(OrganisationStore, 'saved', () => { - this.props.onSave && this.props.onSave(OrganisationStore.savedId) - }) - } - - createProject = (name) => { - AppActions.createProject(name) - } - - selectProject = (id) => { - AppActions.getProject(id) - } - - render() { - return this.props.children({ - ...{ - groups: UserGroupStore.getGroups(), - inviteLinks: OrganisationStore.getInviteLinks(), - invites: OrganisationStore.getInvites(), - isLoading: OrganisationStore.isLoading, - isSaving: OrganisationStore.isSaving, - project: OrganisationStore.getProject(), - projects: OrganisationStore.getProjects(this.props.id), - subscriptionMeta: OrganisationStore.getSubscriptionMeta(), - users: OrganisationStore.getUsers(), - }, - createProject: this.createProject, - invalidateInviteLink: AppActions.invalidateInviteLink, - selectProject: this.selectProject, - }) - } -} - -OrganisationProvider.propTypes = { - children: OptionalFunc, - id: OptionalString, - onSave: OptionalFunc, -} - -module.exports = OrganisationProvider diff --git a/frontend/common/providers/OrganisationProvider.tsx b/frontend/common/providers/OrganisationProvider.tsx new file mode 100644 index 000000000000..97c548e7720f --- /dev/null +++ b/frontend/common/providers/OrganisationProvider.tsx @@ -0,0 +1,95 @@ +import { FC, ReactNode, useEffect, useState } from 'react' +import OrganisationStore from 'common/stores/organisation-store' +import AccountStore from 'common/stores/account-store' +import AppActions from 'common/dispatcher/app-actions' +import { + Invite, + InviteLink, + Project, + SubscriptionMeta, + User, + UserGroupSummary, +} from 'common/types/responses' +import { useGetGroupsQuery } from 'common/services/useGroup' + +type OrganisationProviderType = { + onRemoveProject?: () => void + onSave?: (data: { environmentId: number; projectId: number }) => void + id?: number + children: (props: { + createProject: typeof AppActions.createProject + invalidateInviteLink: typeof AppActions.invalidateInviteLink + inviteLinks: InviteLink[] | null + invites: Invite[] | null + isLoading: boolean + isSaving: boolean + name: string + project: Project | null + groups: UserGroupSummary[] | null + projects: Project[] | null + subscriptionMeta: SubscriptionMeta | null + users: User[] | null + }) => ReactNode +} + +const OrganisationProvider: FC = ({ + children, + id, + onRemoveProject, + onSave, +}) => { + const [_, setUpdate] = useState(Date.now()) + const { data: groups } = useGetGroupsQuery( + { orgId: id!, page: 1 }, + { skip: !id }, + ) + useEffect(() => { + const _onRemoveProject = () => onRemoveProject?.() + OrganisationStore.on('removed', _onRemoveProject) + return () => { + OrganisationStore.off('removed', _onRemoveProject) + } + //eslint-disable-next-line + }, []) + + useEffect(() => { + const _onSave = () => onSave?.(OrganisationStore.savedId) + OrganisationStore.on('saved', _onSave) + return () => { + OrganisationStore.off('saved', _onSave) + } + //eslint-disable-next-line + }, []) + + useEffect(() => { + const onChange = () => { + setUpdate(Date.now()) + } + OrganisationStore.on('change', onChange) + return () => { + OrganisationStore.off('change', onChange) + } + //eslint-disable-next-line + }, []) + + return ( + <> + {children({ + createProject: AppActions.createProject, + groups: groups?.results || [], + invalidateInviteLink: AppActions.invalidateInviteLink, + inviteLinks: OrganisationStore.getInviteLinks(), + invites: OrganisationStore.getInvites(), + isLoading: OrganisationStore.isLoading, + isSaving: OrganisationStore.isSaving, + name: AccountStore.getOrganisation()?.name || '', + project: OrganisationStore.getProject(id), + projects: OrganisationStore.getProjects(), + subscriptionMeta: OrganisationStore.getSubscriptionMeta(), + users: OrganisationStore.getUsers(), + })} + + ) +} + +export default OrganisationProvider diff --git a/frontend/common/providers/Permission.tsx b/frontend/common/providers/Permission.tsx index 92b6227b6e82..1852b7dc940b 100644 --- a/frontend/common/providers/Permission.tsx +++ b/frontend/common/providers/Permission.tsx @@ -4,7 +4,7 @@ import { PermissionLevel } from 'common/types/requests' import AccountStore from 'common/stores/account-store' // we need this to make JSX compile type PermissionType = { - id: string + id: any permission: string level: PermissionLevel children: (data: { permission: boolean; isLoading: boolean }) => ReactNode @@ -15,9 +15,12 @@ export const useHasPermission = ({ level, permission, }: Omit) => { - const { data, isLoading } = useGetPermissionQuery({ id, level }) + const { data, isLoading } = useGetPermissionQuery( + { id: `${id}`, level }, + { skip: !id || !level }, + ) const hasPermission = !!data?.[permission] || !!data?.ADMIN - return { isLoading, permission: hasPermission || AccountStore.isAdmin() } + return { isLoading, permission: !!hasPermission || !!AccountStore.isAdmin() } } const Permission: FC = ({ diff --git a/frontend/common/providers/UserGroupsProvider.js b/frontend/common/providers/UserGroupsProvider.js deleted file mode 100644 index b8aab1e801a4..000000000000 --- a/frontend/common/providers/UserGroupsProvider.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import UserGroupsStore from 'common/stores/user-group-store' - -const UserGroupProvider = class extends React.Component { - static displayName = 'UserGroupProvider' - - constructor(props, context) { - super(props, context) - this.state = { - isLoading: !UserGroupsStore.model, - userGroups: UserGroupsStore.model, - userGroupsPaging: UserGroupsStore.paging, - } - ES6Component(this) - } - - componentDidMount() { - this.listenTo(UserGroupsStore, 'change', () => { - this.setState({ - isLoading: UserGroupsStore.isLoading, - isSaving: UserGroupsStore.isSaving, - userGroups: UserGroupsStore.model, - userGroupsPaging: UserGroupsStore.paging, - }) - }) - - this.listenTo(UserGroupsStore, 'saved', () => { - this.props.onSave && this.props.onSave() - }) - } - - render() { - return this.props.children({ ...this.state }) - } -} - -UserGroupProvider.propTypes = { - children: OptionalFunc, - onSave: OptionalFunc, -} - -module.exports = UserGroupProvider diff --git a/frontend/common/services/useGroup.ts b/frontend/common/services/useGroup.ts index 525f7fd24169..f969fe07be00 100644 --- a/frontend/common/services/useGroup.ts +++ b/frontend/common/services/useGroup.ts @@ -1,6 +1,8 @@ import { Res } from 'common/types/responses' import { Req } from 'common/types/requests' import { service } from 'common/service' +import { getStore } from 'common/store' +import Utils from 'common/utils/utils' export const groupService = service .enhanceEndpoints({ @@ -8,6 +10,39 @@ export const groupService = service }) .injectEndpoints({ endpoints: (builder) => ({ + createGroup: builder.mutation({ + invalidatesTags: [{ id: 'LIST', type: 'Group' }], + queryFn: async (query, { dispatch }, _, baseQuery) => { + //Create the group + const { data, error } = await baseQuery({ + body: query.data, + method: 'POST', + url: `organisations/${query.orgId}/groups/`, + }) + if (error) { + return { error } + } + //Add the members + if (query.users?.length) { + const { error } = await baseQuery({ + body: { user_ids: query.users.map((u) => u.id) }, + method: 'POST', + url: `organisations/${query.orgId}/groups/${data.id}/`, + }) + } + // Make the admins + await Promise.all( + (query.usersToAddAdmin || []).map((v) => + createGroupAdmin(getStore(), { + group: data.id, + orgId: query.orgId, + user: v, + }), + ), + ) + return { data } + }, + }), createGroupAdmin: builder.mutation< Res['groupAdmin'], Req['createGroupAdmin'] @@ -50,10 +85,63 @@ export const groupService = service }), getGroups: builder.query({ providesTags: [{ id: 'LIST', type: 'Group' }], - query: (query) => ({ - url: `organisations/${query.orgId}/groups/?page=${query.page}`, + query: ({ orgId, ...rest }) => ({ + url: `organisations/${orgId}/groups/?${Utils.toParam({ ...rest })}`, }), }), + updateGroup: builder.mutation({ + invalidatesTags: (res) => [ + { id: 'LIST', type: 'Group' }, + { id: res?.id, type: 'Group' }, + ], + queryFn: async (query, { dispatch }, _, baseQuery) => { + //Create the group + const { data, error } = await baseQuery({ + body: query.data, + method: 'PUT', + url: `organisations/${query.orgId}/groups/${query.data.id}`, + }) + if (error) { + return { error } + } + //Add the members + if (query.users?.length) { + await baseQuery({ + body: { user_ids: query.data.users.map((u) => u.id) }, + method: 'POST', + url: `organisations/${query.orgId}/groups/${data.id}/add-users/`, + }) + } + if (query.usersToRemove?.length) { + await baseQuery({ + body: { user_ids: query.usersToRemove }, + method: 'POST', + url: `organisations/${query.orgId}/groups/${data.id}/remove-users/`, + }) + } + // Make the admins + await Promise.all( + (query.usersToAddAdmin || []).map((v) => + createGroupAdmin(getStore(), { + group: data.id, + orgId: query.orgId, + user: v, + }), + ), + ) + + await Promise.all( + (query.usersToRemoveAdmin || []).map((v) => + deleteGroupAdmin(getStore(), { + group: data.id, + orgId: query.orgId, + user: v, + }), + ), + ) + return { data } + }, + }), // END OF ENDPOINTS }), }) @@ -106,15 +194,34 @@ export async function getGroup( ) { return store.dispatch(groupService.endpoints.getGroup.initiate(data, options)) } +export async function createGroup( + store: any, + data: Req['createGroup'], + options?: Parameters[1], +) { + return store.dispatch( + groupService.endpoints.createGroup.initiate(data, options), + ) +} +export async function updateGroup( + store: any, + data: Req['updateGroup'], + options?: Parameters[1], +) { + return store.dispatch( + groupService.endpoints.updateGroup.initiate(data, options), + ) +} // END OF FUNCTION_EXPORTS export const { useCreateGroupAdminMutation, + useCreateGroupMutation, useDeleteGroupAdminMutation, useDeleteGroupMutation, - useGetGroupQuery, useGetGroupsQuery, + useUpdateGroupMutation, // END OF EXPORTS } = groupService diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index 297961c252e9..c1e1cdf85660 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -1,4 +1,6 @@ import Constants from 'common/constants' +import { projectService } from "common/services/useProject"; +import { getStore } from "common/store"; import sortBy from 'lodash/sortBy' const Dispatcher = require('../dispatcher/dispatcher') @@ -45,6 +47,7 @@ const controller = { ).then((res) => { project.environments = res store.model.projects = store.model.projects.concat(project) + getStore().dispatch(projectService.util.invalidateTags(['Project'])) store.savedId = { environmentId: res[0].api_key, projectId: project.id, @@ -85,6 +88,7 @@ const controller = { AsyncStorage.removeItem('lastEnv') store.trigger('removed') store.saved() + getStore().dispatch(projectService.util.invalidateTags(['Project'])) }) }, deleteUser: (id) => { diff --git a/frontend/common/stores/project-store.js b/frontend/common/stores/project-store.js index ce3fb9f18580..7c3a2f9bd4c3 100644 --- a/frontend/common/stores/project-store.js +++ b/frontend/common/stores/project-store.js @@ -3,6 +3,9 @@ import OrganisationStore from './organisation-store' import Constants from 'common/constants' import Utils from 'common/utils/utils' +import { getStore } from 'common/store' +import { projectService } from 'common/services/useProject' +import { environmentService } from 'common/services/useEnvironment' const Dispatcher = require('../dispatcher/dispatcher') const BaseStore = require('./base/_store') @@ -49,6 +52,9 @@ const controller = { ]) } store.saved() + getStore().dispatch( + environmentService.util.invalidateTags(['Environment']), + ) AppActions.refreshOrganisation() }), ), @@ -74,6 +80,9 @@ const controller = { const index = _.findIndex(store.model.environments, { id: env.id }) store.model.environments[index] = res store.saved() + getStore().dispatch( + environmentService.util.invalidateTags(['Environment']), + ) AppActions.refreshOrganisation() }) }, @@ -81,6 +90,7 @@ const controller = { store.saving() data.put(`${Project.api}projects/${project.id}/`, project).then((res) => { store.model = Object.assign(store.model, res) + getStore().dispatch(projectService.util.invalidateTags(['Project'])) store.saved() }) }, diff --git a/frontend/common/stores/user-group-store.js b/frontend/common/stores/user-group-store.js deleted file mode 100644 index 4948dfaff44c..000000000000 --- a/frontend/common/stores/user-group-store.js +++ /dev/null @@ -1,137 +0,0 @@ -const Dispatcher = require('../dispatcher/dispatcher') -const BaseStore = require('./base/_store') -const data = require('../data/base/_data') -const { - createGroupAdmin, - deleteGroupAdmin, - getGroups, -} = require('../services/useGroup') -const { getStore } = require('../store') - -const PAGE_SIZE = 999 - -const controller = { - createGroup: (orgId, group) => { - store.saving() - data - .post(`${Project.api}organisations/${orgId}/groups/`, group) - .then((res) => { - let prom = Promise.resolve() - if (group.users) { - prom = data.post( - `${Project.api}organisations/${orgId}/groups/${res.id}/add-users/`, - { user_ids: group.users.map((u) => u.id) }, - ) - } - prom.then((res) => { - Promise.all( - (group.usersToAddAdmin || []).map((v) => - createGroupAdmin(getStore(), { - group: res.id, - orgId, - user: v.id, - }), - ), - ).then(() => { - controller.getGroups(orgId) - }) - }) - }) - .catch((e) => API.ajaxHandler(store, e)) - }, - getGroups: (orgId) => { - store.loading() - getGroups( - getStore(), - { orgId: `${orgId}`, page: 1 }, - { forceRefetch: true }, - ).then((response) => { - store.groups = response.data.results - - store.loaded() - store.saved() - }) - }, - updateGroup: (orgId, group) => { - store.saving() - data - .put(`${Project.api}organisations/${orgId}/groups/${group.id}/`, group) - .then((currentGroup) => { - const toRemove = group.usersToRemove.filter( - (toRemove) => - !!currentGroup.users.find((user) => user.id === toRemove.id), - ) - const toAdd = group.users.filter( - (toRemove) => - !currentGroup.users.find((user) => user.id === toRemove.id), - ) - - Promise.all([ - data.post( - `${Project.api}organisations/${orgId}/groups/${group.id}/add-users/`, - { user_ids: toAdd.map((u) => u.id) }, - ), - data.post( - `${Project.api}organisations/${orgId}/groups/${group.id}/remove-users/`, - { user_ids: toRemove.map((u) => u.id) }, - ), - ]).then(() => { - Promise.all( - (group.usersToAddAdmin || []) - .map((v) => - createGroupAdmin(getStore(), { - group: group.id, - orgId, - user: v.id, - }), - ) - .concat( - (group.usersToRemoveAdmin || []).map((v) => - deleteGroupAdmin(getStore(), { - group: group.id, - orgId, - user: v.id, - }), - ), - ), - ).then(() => { - controller.getGroups(orgId) - }) - }) - }) - - .catch((e) => API.ajaxHandler(store, e)) - }, -} - -const store = Object.assign({}, BaseStore, { - getGroups() { - return store.groups - }, - getPaging() { - return store.paging - }, - id: 'identitylist', - paging: { - pageSize: PAGE_SIZE, - }, -}) - -store.dispatcherIndex = Dispatcher.register(store, (payload) => { - const action = payload.action // this is our action from handleViewAction - - switch (action.actionType) { - case Actions.UPDATE_GROUP: - controller.updateGroup(action.orgId, action.data) - break - case Actions.CREATE_GROUP: - controller.createGroup(action.orgId, action.data) - break - case Actions.GET_GROUPS: - controller.getGroups(action.orgId) - break - default: - } -}) -controller.store = store -module.exports = controller.store diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 0b9d1e11bd4c..d628a37d53f2 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -1,11 +1,12 @@ import { Account, - Segment, - Tag, - FeatureStateValue, - FeatureState, ExternalResource, + FeatureState, + FeatureStateValue, ImportStrategy, + Segment, + Tag, + UserGroup, } from './responses' export type PagedRequest = T & { @@ -101,7 +102,7 @@ export type Req = { user: number | string } getGroups: PagedRequest<{ - orgId: string + orgId: number }> deleteGroup: { id: number | string; orgId: number | string } getGroup: { id: string; orgId: string } @@ -372,6 +373,21 @@ export type Req = { id: string } getProject: { id: string } + createGroup: { + orgId: string + data: Omit + users: UserGroup['users'] + usersToAddAdmin: number[] | null + } getUserGroupPermission: { project_id: string } + updateGroup: Req['createGroup'] & { + orgId: string + data: UserGroup + users: UserGroup['users'] + + usersToAddAdmin: number[] | null + usersToRemoveAdmin: number[] | null + usersToRemove: number[] | null + } // END OF TYPES } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index aa977394fcb8..48f7711ed8ab 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -541,6 +541,30 @@ export type AuthType = 'EMAIL' | 'GITHUB' | 'GOOGLE' export type SignupType = 'NO_INVITE' | 'INVITE_EMAIL' | 'INVITE_LINK' +export type Invite = { + id: number + email: string + date_created: string + invited_by: User + permission_groups: number[] +} + +export type InviteLink = { + id: number + hash: string + date_created: string + role: string + expires_at: string | null +} + +export type SubscriptionMeta = { + max_seats: number | null + max_api_calls: number | null + max_projects: number | null + payment_source: string | null + chargebee_email: string | null +} + export type Account = { first_name: string last_name: string diff --git a/frontend/web/components/ActionButton.tsx b/frontend/web/components/ActionButton.tsx new file mode 100644 index 000000000000..02bf62e91323 --- /dev/null +++ b/frontend/web/components/ActionButton.tsx @@ -0,0 +1,26 @@ +import { FC } from 'react' +import classNames from 'classnames' +import Icon from './Icon' +import Button from './base/forms/Button' + +type ActionButtonType = { + onClick: () => void + 'data-test'?: string +} + +const ActionButton: FC = ({ onClick, ...rest }) => { + return ( + + ) +} + +export default ActionButton diff --git a/frontend/web/components/EditPermissions.tsx b/frontend/web/components/EditPermissions.tsx index af4de1a330fc..7c1aafe060c2 100644 --- a/frontend/web/components/EditPermissions.tsx +++ b/frontend/web/components/EditPermissions.tsx @@ -1,4 +1,4 @@ -import React, { FC, forwardRef, useEffect, useState } from 'react' +import React, { FC, forwardRef, useCallback, useEffect, useState } from 'react' import { find } from 'lodash' import { close as closeIcon } from 'ionicons/icons' import { IonIcon } from '@ionic/react' @@ -9,6 +9,7 @@ import { Role, User, UserGroup, + UserGroupSummary, UserPermission, } from 'common/types/responses' import Utils from 'common/utils/utils' @@ -55,34 +56,33 @@ import { } from 'common/services/useGroupWithRole' import MyRoleSelect from './MyRoleSelect' -import { setInterceptClose } from './modals/base/ModalDefault' import Panel from './base/grid/Panel' import InputGroup from './base/forms/InputGroup' import classNames from 'classnames' - -const OrganisationProvider = require('common/providers/OrganisationProvider') +import OrganisationProvider from 'common/providers/OrganisationProvider' +import { useGetPermissionQuery } from 'common/services/usePermission' +import { useHasPermission } from 'common/providers/Permission' const Project = require('common/project') type EditPermissionModalType = { - group?: UserGroup + group?: UserGroupSummary id: number className?: string isGroup?: boolean level: PermissionLevel name: string - onSave: () => void - envId?: number + onSave?: () => void + envId?: number | string | undefined parentId?: string parentLevel?: string parentSettingsLink?: string - roleTabTitle: string + roleTabTitle?: string permissions?: UserPermission[] push: (route: string) => void user?: User role?: Role roles?: Role[] permissionChanged: () => void - editPermissionsFromSettings?: boolean isEditUserPermission?: boolean isEditGroupPermission?: boolean } @@ -100,9 +100,37 @@ type EntityPermissions = Omit< id?: number user?: number } +const withAdminPermissions = (InnerComponent: any) => { + const WrappedComponent: FC = (props) => { + const { id, level } = props + const notReady = !id || !level + const { isLoading: permissionsLoading, permission } = useHasPermission({ + id: id, + level, + permission: 'ADMIN', + }) -const _EditPermissionsModal: FC = forwardRef( - (props) => { + if (permissionsLoading || notReady) { + return ( +
+ +
+ ) + } + if (!permission) { + return ( +
+ To manage permissions you need to be admin of this {level}. +
+ ) + } + + return + } + return WrappedComponent +} +const _EditPermissionsModal: FC = withAdminPermissions( + forwardRef((props) => { const [entityPermissions, setEntityPermissions] = useState({ admin: false, permissions: [] }) const [parentError, setParentError] = useState(false) @@ -120,30 +148,8 @@ const _EditPermissionsModal: FC = forwardRef( }[] >([]) - useEffect(() => { - setInterceptClose(() => { - if (valueChanged) { - return new Promise((resolve) => { - openConfirm({ - body: 'Closing this will discard your unsaved changes.', - noText: 'Cancel', - onNo: () => resolve(false), - onYes: () => resolve(true), - title: 'Discard changes', - yesText: 'Ok', - }) - }) - } else { - return Promise.resolve(true) - } - }) - return () => { - setInterceptClose(null) - } - }, [valueChanged]) const { className, - editPermissionsFromSettings, envId, group, id, @@ -192,6 +198,7 @@ const _EditPermissionsModal: FC = forwardRef( })) setRolesSelected(resultArray) } + //eslint-disable-next-line }, [userWithRolesDataSuccesfull]) useEffect(() => { @@ -202,6 +209,7 @@ const _EditPermissionsModal: FC = forwardRef( })) setRolesSelected(resultArray) } + //eslint-disable-next-line }, [groupWithRolesDataSuccesfull]) const processResults = (results: (UserPermission | GroupPermission)[]) => { @@ -278,16 +286,14 @@ const _EditPermissionsModal: FC = forwardRef( `${level.charAt(0).toUpperCase() + level.slice(1)} permissions Saved`, ) permissionChanged?.() - setInterceptClose(null) onSave?.() setSaving(false) - if (editPermissionsFromSettings) { - close() - } } if (errorUpdating || errorCreating) { setSaving(false) } + + //eslint-disable-next-line }, [ errorUpdating, errorCreating, @@ -334,7 +340,7 @@ const _EditPermissionsModal: FC = forwardRef( { skip: !role || - !id || + (!envId && !id) || !Utils.getFlagsmithHasFeature('show_role_management') || level !== 'environment', }, @@ -351,6 +357,7 @@ const _EditPermissionsModal: FC = forwardRef( ) setEntityPermissions(entityPermissions) } + //eslint-disable-next-line }, [organisationPermissions, organisationIsLoading]) useEffect(() => { @@ -358,6 +365,7 @@ const _EditPermissionsModal: FC = forwardRef( const entityPermissions = processResults(projectPermissions?.results) setEntityPermissions(entityPermissions) } + //eslint-disable-next-line }, [projectPermissions, projectIsLoading]) useEffect(() => { @@ -365,6 +373,7 @@ const _EditPermissionsModal: FC = forwardRef( const entityPermissions = processResults(envPermissions?.results) setEntityPermissions(entityPermissions) } + //eslint-disable-next-line }, [envPermissions, envIsLoading]) useEffect(() => { @@ -418,13 +427,10 @@ const _EditPermissionsModal: FC = forwardRef( return entityPermissions.permissions.includes(key) } - const close = () => { - closeModal() - } - - const save = () => { + const save = useCallback(() => { const entityId = typeof entityPermissions.id === 'undefined' ? '' : entityPermissions.id + setValueChanged(false) if (!role) { const url = isGroup ? `${level}s/${id}/user-group-permissions/${entityId}` @@ -435,17 +441,19 @@ const _EditPermissionsModal: FC = forwardRef( `${Project.api}${url}${entityId && '/'}`, entityPermissions, ) - .then(() => { - setInterceptClose(null) + .then((res: EntityPermissions) => { + setEntityPermissions(res) toast( `${ level.charAt(0).toUpperCase() + level.slice(1) } Permissions Saved`, ) onSave && onSave() - close() }) .catch(() => { + toast(`Error Saving Permissions`, 'danger') + }) + .finally(() => { setSaving(false) }) } else { @@ -483,8 +491,25 @@ const _EditPermissionsModal: FC = forwardRef( }).then(onRoleSaved as any) } } - } + }, [ + createRolePermissions, + entityPermissions, + envId, + id, + isGroup, + level, + onSave, + permissionWasCreated, + role, + updateRolePermissions, + ]) + useEffect(() => { + if (valueChanged) { + save() + } + //eslint-disable-next-line + }, [valueChanged]) const togglePermission = (key: string) => { if (role) { permissionChanged?.() @@ -584,7 +609,7 @@ const _EditPermissionsModal: FC = forwardRef( org_id: id, role_id: roleId, }).then(onRoleRemoved as any) - } else { + } else if (roleSelected) { deleteRolePermissionGroup({ group_id: roleSelected.group_role_id, organisation_id: id, @@ -616,6 +641,7 @@ const _EditPermissionsModal: FC = forwardRef( } toast('Role assigned') } + //eslint-disable-next-line }, [userAdded, usersData, groupsData, groupAdded]) const getRoles = ( @@ -686,7 +712,7 @@ const _EditPermissionsModal: FC = forwardRef( { toggleAdmin() setValueChanged(true) @@ -726,7 +752,9 @@ const _EditPermissionsModal: FC = forwardRef( setValueChanged(true) togglePermission(p.key) }} - disabled={disabled || admin() || !hasRbacPermission} + disabled={ + disabled || admin() || !hasRbacPermission || saving + } checked={!disabled && hasPermission(p.key)} /> @@ -738,11 +766,17 @@ const _EditPermissionsModal: FC = forwardRef(

This will edit the permissions for{' '} - {isGroup - ? `the ${name} group` - : role - ? ` ${role.name}` - : ` ${name}`} + {isGroup ? ( + `the ${group?.name || ''} group` + ) : user ? ( + <> + {user.first_name || ''} {user.last_name || ''} + + ) : role ? ( + ` ${role.name}` + ) : ( + ` ${name}` + )} .

@@ -831,27 +865,14 @@ const _EditPermissionsModal: FC = forwardRef( /> )} -
- {!role && ( - - )} - -
) - }, + }), ) -export const EditPermissionsModal = ConfigProvider(_EditPermissionsModal) +export const EditPermissionsModal = ConfigProvider( + _EditPermissionsModal, +) as FC const rolesWidths = [250, 600, 100] const EditPermissions: FC = (props) => { @@ -913,7 +934,6 @@ const EditPermissions: FC = (props) => { = (props) => { ) } const hasRbacPermission = Utils.getPlansPermission('RBAC') - return (
@@ -943,7 +962,7 @@ const EditPermissions: FC = (props) => { - {({ isLoading, users }: { isLoading: boolean; users?: User[] }) => ( + {({ isLoading, users }) => (
{isLoading && !users?.length && (
diff --git a/frontend/web/components/FeatureAction.tsx b/frontend/web/components/FeatureAction.tsx index 07966317d22f..57c69b2091ef 100644 --- a/frontend/web/components/FeatureAction.tsx +++ b/frontend/web/components/FeatureAction.tsx @@ -5,11 +5,11 @@ import useOutsideClick from 'common/useOutsideClick' import Utils from 'common/utils/utils' import Constants from 'common/constants' import Permission from 'common/providers/Permission' -import Button from './base/forms/Button' import Icon from './Icon' import { Tag } from 'common/types/responses' import color from 'color' import { getTagColor } from './tags/Tag' +import ActionButton from './ActionButton' interface FeatureActionProps { projectId: string @@ -97,30 +97,20 @@ export const FeatureAction: FC = ({ return (
- + data-test={`feature-action-${featureIndex}`} + />
{isOpen && (
handleActionClick('copy')} + onClick={(e) => { + e.stopPropagation() + handleActionClick('copy') + }} > Copy Feature Name @@ -129,7 +119,10 @@ export const FeatureAction: FC = ({
handleActionClick('audit')} + onClick={(e) => { + e.stopPropagation() + handleActionClick('audit') + }} > Show Audit Logs @@ -140,7 +133,10 @@ export const FeatureAction: FC = ({
handleActionClick('history')} + onClick={(e) => { + e.stopPropagation() + handleActionClick('history') + }} > Show History @@ -165,7 +161,10 @@ export const FeatureAction: FC = ({ !removeFeaturePermission || readOnly || isProtected, })} data-test={`remove-feature-btn-${featureIndex}`} - onClick={() => handleActionClick('remove')} + onClick={(e) => { + e.stopPropagation() + handleActionClick('remove') + }} > Remove feature diff --git a/frontend/web/components/GroupSelect.tsx b/frontend/web/components/GroupSelect.tsx index 50c07b00719c..ec04ef7e1446 100644 --- a/frontend/web/components/GroupSelect.tsx +++ b/frontend/web/components/GroupSelect.tsx @@ -52,18 +52,19 @@ const GroupSelect: FC = ({
{grouplist && grouplist.map((v) => ( -
- { - const isRemove = value?.includes(v.id) - if (isRemove && onRemove) { - onRemove(v.id, false) - } else if (!isRemove && onAdd) { - onAdd(v.id, false) - } - }} - space - > +
{ + const isRemove = value?.includes(v.id) + if (isRemove && onRemove) { + onRemove(v.id, false) + } else if (!isRemove && onAdd) { + onAdd(v.id, false) + } + }} + className='assignees-list-item clickable' + key={v.id} + > + diff --git a/frontend/web/components/PermissionsTabs.tsx b/frontend/web/components/PermissionsTabs.tsx new file mode 100644 index 000000000000..9f9fbff82af2 --- /dev/null +++ b/frontend/web/components/PermissionsTabs.tsx @@ -0,0 +1,151 @@ +import React, { FC, Ref, useEffect, useState } from 'react' +import { EditPermissionsModal } from './EditPermissions' +import { + Environment, + Project, + UserGroup, + Role, + User, UserGroupSummary +} from "common/types/responses"; +import Tabs from './base/forms/Tabs' +import TabItem from './base/forms/TabItem' +import Input from './base/forms/Input' +import Utils from 'common/utils/utils' +import RolePermissionsList from './RolePermissionsList' +import ProjectFilter from './ProjectFilter' +import OrganisationStore from 'common/stores/organisation-store' + +type PermissionsTabsType = { + orgId?: number + group?: UserGroupSummary + user?: User + role?: Role | undefined + value?: number + onChange?: (v: number) => void + uncontrolled?: boolean + tabRef?: Ref +} + +const PermissionsTabs: FC = ({ + group, + onChange, + orgId, + role, + tabRef, + uncontrolled, + user, + value, +}) => { + const [searchProject, setSearchProject] = useState('') + const [searchEnv, setSearchEnv] = useState('') + const projectData: Project[] = OrganisationStore.getProjects() + const [project, setProject] = useState('') + const [environments, setEnvironments] = useState([]) + + useEffect(() => { + if (project && projectData) { + const environments = projectData.find( + (p) => p.id === parseInt(project), + )?.environments + setEnvironments(environments || []) + } + }, [project, projectData]) + + if (!orgId) { + return + } + + return ( + + Organisation} + > + + + Project}> + +
Permissions
+ + setSearchProject(Utils.safeParseEventValue(e)) + } + size='small' + placeholder='Search' + search + /> +
+ +
+ Environment} + > + +
Permissions
+ + setSearchEnv(Utils.safeParseEventValue(e)) + } + size='small' + placeholder='Search' + search + /> +
+
+ +
+ {environments.length > 0 && ( + { + return { + id: role ? v.id : v.api_key, + name: v.name, + } + })} + role={role} + level={'environment'} + ref={tabRef} + /> + )} +
+ + ) +} + +export default PermissionsTabs diff --git a/frontend/web/components/ProjectManageWidget.tsx b/frontend/web/components/ProjectManageWidget.tsx index a849f231b579..dffc4290c768 100644 --- a/frontend/web/components/ProjectManageWidget.tsx +++ b/frontend/web/components/ProjectManageWidget.tsx @@ -34,7 +34,7 @@ const ProjectManageWidget: FC = ({ ) const { permission: canCreateProject } = useHasPermission({ - id: organisationId as string, + id: organisationId, level: 'organisation', permission: Utils.getCreateProjectPermission(organisation), }) diff --git a/frontend/web/components/RolePermissionsList.tsx b/frontend/web/components/RolePermissionsList.tsx index 92beb2663dfd..8b55ae7c74f3 100644 --- a/frontend/web/components/RolePermissionsList.tsx +++ b/frontend/web/components/RolePermissionsList.tsx @@ -1,19 +1,18 @@ import React, { - useState, + FC, forwardRef, - useImperativeHandle, Ref, - FC, + useImperativeHandle, + useState, } from 'react' import Icon from './Icon' import { EditPermissionsModal } from './EditPermissions' import { - useGetRoleProjectPermissionsQuery, useGetRoleEnvironmentPermissionsQuery, + useGetRoleProjectPermissionsQuery, } from 'common/services/useRolePermission' -import Format from 'common/utils/format' -import { PermissionLevel, Req } from 'common/types/requests' -import { Role } from 'common/types/responses' +import { PermissionLevel } from 'common/types/requests' +import { Role, User, UserGroup, UserGroupSummary } from "common/types/responses"; import PanelSearch from './PanelSearch' import PermissionsSummaryList from './PermissionsSummaryList' @@ -25,63 +24,65 @@ type NameAndId = { type RolePermissionsListProps = { mainItems: NameAndId[] - role: Role + role?: Role | undefined ref?: Ref level: PermissionLevel filter: string + orgId?: string + user?: User + group?: UserGroupSummary } export type PermissionsSummaryType = { level: PermissionLevel levelId: number - role: Role + organisationId: number + role?: Role } const PermissionsSummary: FC = ({ level, levelId, + organisationId, role, }) => { const { data: projectPermissions, isLoading: projectIsLoading } = useGetRoleProjectPermissionsQuery( { - organisation_id: role.organisation, + organisation_id: organisationId, project_id: levelId, - role_id: role.id, + role_id: parseInt(`${role?.id}`), }, - { skip: !levelId || level !== 'project' }, + { skip: !levelId || level !== 'project' || !role }, ) const { data: envPermissions, isLoading: envIsLoading } = useGetRoleEnvironmentPermissionsQuery( { env_id: levelId, - organisation_id: role?.organisation, - role_id: role?.id, + organisation_id: organisationId, + role_id: parseInt(`${role?.id}`), }, - { skip: !levelId || level !== 'environment' }, + { skip: !levelId || level !== 'environment' || !role }, ) const permissions = projectPermissions || envPermissions const roleResult = permissions?.results.filter( (item) => item.role === role?.id, ) - const roleRermissions = + const rolePermissions = roleResult && roleResult.length > 0 ? roleResult[0].permissions : [] const isAdmin = roleResult && roleResult.length > 0 ? roleResult[0].admin : false return projectIsLoading || envIsLoading ? null : ( - + ) } const RolePermissionsList: React.FC = forwardRef( - ({ filter, level, mainItems, role }, ref) => { + ({ filter, group, level, mainItems, orgId, role, user }, ref) => { const [expandedItems, setExpandedItems] = useState<(string | number)[]>([]) - const [unsavedProjects, setUnsavedProjects] = useState<(string | number)[]>( - [], - ) const mainItemsFiltered = mainItems && @@ -92,13 +93,6 @@ const RolePermissionsList: React.FC = forwardRef( }) const toggleExpand = async (id: string | number) => { - if (unsavedProjects.includes(id)) { - await checkClose().then((res) => { - if (res) { - removeUnsavedProject(id) - } - }) - } setExpandedItems((prevExpanded) => prevExpanded.includes(id) ? prevExpanded.filter((item) => item !== id) @@ -106,43 +100,6 @@ const RolePermissionsList: React.FC = forwardRef( ) } - const removeUnsavedProject = (projectId: string | number) => { - setUnsavedProjects((prevUnsavedProjects) => - prevUnsavedProjects.filter((id) => id !== projectId), - ) - } - - const checkClose = () => { - if (unsavedProjects.length > 0) { - return new Promise((resolve) => { - openConfirm({ - body: 'Closing this will discard your unsaved changes.', - noText: 'Cancel', - onNo: () => resolve(false), - onYes: () => resolve(true), - title: 'Discard changes', - yesText: 'Ok', - }) - }) - } else { - return Promise.resolve(true) - } - } - useImperativeHandle( - ref, - () => { - return { - onClosing() { - return checkClose() - }, - tabChanged() { - return unsavedProjects.length > 0 - }, - } - }, - [unsavedProjects], - ) - return ( = forwardRef( renderRow={(mainItem: NameAndId, index: number) => (
= forwardRef(
{mainItem.name}{' '} - {unsavedProjects.includes(mainItem.id) && ( -
Unsaved
- )}
@@ -173,6 +127,7 @@ const RolePermissionsList: React.FC = forwardRef(
@@ -195,15 +150,9 @@ const RolePermissionsList: React.FC = forwardRef( level={level} role={role} className='mt-2 px-3' - permissionChanged={() => { - if (!unsavedProjects.includes(mainItem.id)) { - setUnsavedProjects((prevUnsavedProjects) => [ - ...prevUnsavedProjects, - mainItem.id, - ]) - } - }} - onSave={() => removeUnsavedProject(mainItem.id)} + isGroup={!!group} + group={group} + user={user} /> )}
diff --git a/frontend/web/components/RolesTable.tsx b/frontend/web/components/RolesTable.tsx index 1ffbeeb5da93..7e8043ba735e 100644 --- a/frontend/web/components/RolesTable.tsx +++ b/frontend/web/components/RolesTable.tsx @@ -3,11 +3,14 @@ import CreateRole from './modals/CreateRole' import { useGetRolesQuery } from 'common/services/useRole' import { User, Role } from 'common/types/responses' import PanelSearch from './PanelSearch' -import UserGroupStore from 'common/stores/user-group-store' import Button from './base/forms/Button' import ConfirmDeleteRole from './modals/ConfirmDeleteRole' import Icon from './Icon' import Panel from './base/grid/Panel' +import { useGetGroupsQuery } from 'common/services/useGroup' +import Utils from 'common/utils/utils' +import Constants from 'common/constants' +import { useHasPermission } from 'common/providers/Permission' const rolesWidths = [250, 100] type RolesTableType = { @@ -16,12 +19,19 @@ type RolesTableType = { } const RolesTable: FC = ({ organisationId, users }) => { - const groups = UserGroupStore.getGroups() // todo: this will become a hook + const { data: groups } = useGetGroupsQuery( + { orgId: organisationId, page: 1 }, + { skip: !organisationId }, + ) const { data: roles } = useGetRolesQuery( { organisation_id: organisationId }, { skip: !organisationId }, ) - + const { permission: isAdmin } = useHasPermission({ + id: organisationId, + level: 'organisation', + permission: 'ADMIN', + }) const createRole = () => { openModal( 'Create Role', @@ -59,7 +69,7 @@ const RolesTable: FC = ({ organisationId, users }) => { toast('Role Updated') }} users={users} - groups={groups} + groups={groups?.results || []} />, 'side-modal', ) @@ -68,14 +78,19 @@ const RolesTable: FC = ({ organisationId, users }) => { <>
Roles
- + {Utils.renderWithPermission( + isAdmin, + Constants.organisationPermissions('Admin'), + , + )}

Create custom roles, assign permissions and keys to the role, and then diff --git a/frontend/web/components/UserAction.tsx b/frontend/web/components/UserAction.tsx new file mode 100644 index 000000000000..ef60325568b5 --- /dev/null +++ b/frontend/web/components/UserAction.tsx @@ -0,0 +1,136 @@ +import { FC, useCallback, useLayoutEffect, useRef, useState } from 'react' +import classNames from 'classnames' + +import useOutsideClick from 'common/useOutsideClick' +import Utils from 'common/utils/utils' +import Constants from 'common/constants' +import Permission from 'common/providers/Permission' +import Button from './base/forms/Button' +import Icon from './Icon' +import ActionButton from './ActionButton' + +interface FeatureActionProps { + canRemove: boolean + onRemove: () => void + onEdit: () => void + canEdit: () => void +} + +type ActionType = 'edit' | 'remove' + +function calculateListPosition( + btnEl: HTMLElement, + listEl: HTMLElement, +): { top: number; left: number } { + const listPosition = listEl.getBoundingClientRect() + const btnPosition = btnEl.getBoundingClientRect() + const pageTop = window.visualViewport?.pageTop ?? 0 + return { + left: btnPosition.right - listPosition.width, + top: pageTop + btnPosition.bottom, + } +} + +export const FeatureAction: FC = ({ + canEdit, + canRemove, + onEdit, + onRemove, +}) => { + const [isOpen, setIsOpen] = useState(false) + + const btnRef = useRef(null) + const listRef = useRef(null) + + const close = useCallback(() => setIsOpen(false), []) + + const handleOutsideClick = useCallback( + () => isOpen && close(), + [close, isOpen], + ) + + const handleActionClick = useCallback( + (action: ActionType) => { + if (action === 'edit') { + onEdit() + } else if (action === 'remove') { + onRemove() + } + close() + }, + [close, onRemove, onEdit], + ) + + useOutsideClick(listRef, handleOutsideClick) + + useLayoutEffect(() => { + if (!isOpen || !listRef.current || !btnRef.current) return + const listPosition = calculateListPosition(btnRef.current, listRef.current) + listRef.current.style.top = `${listPosition.top}px` + listRef.current.style.left = `${listPosition.left}px` + }, [isOpen]) + + if (!canEdit && !!canRemove) { + return ( + + ) + } + + if (!!canEdit && !canRemove) { + return ( + + ) + } + + if (!canEdit && !canRemove) { + return null + } + + return ( +

+
+ setIsOpen(true)} /> +
+ + {isOpen && ( +
+ {!!canEdit && ( +
{ + e.stopPropagation() + handleActionClick('edit') + }} + > + + Edit Permissions +
+ )} + + {!!canRemove && ( +
{ + e.stopPropagation() + handleActionClick('remove') + }} + > + + Remove +
+ )} +
+ )} +
+ ) +} + +export default FeatureAction diff --git a/frontend/web/components/UserGroupList.tsx b/frontend/web/components/UserGroupList.tsx index 8ccf8445f35e..f51afd1187a9 100644 --- a/frontend/web/components/UserGroupList.tsx +++ b/frontend/web/components/UserGroupList.tsx @@ -10,12 +10,13 @@ import { import { useGetUserGroupPermissionQuery } from 'common/services/useUserGroupPermission' import PanelSearch from './PanelSearch' import { sortBy } from 'lodash' -import InfoMessage from './InfoMessage' // we need this to make JSX compile +import InfoMessage from './InfoMessage' import Icon from './Icon' -import Panel from './base/grid/Panel' import PermissionsSummaryList from './PermissionsSummaryList' +import Panel from './base/grid/Panel' +import { useGetGroupSummariesQuery } from 'common/services/useGroupSummary' -type UserGroupsListType = { +type UserGroupListType = { noTitle?: boolean orgId: string projectId: string | boolean @@ -78,11 +79,12 @@ const UserGroupsRow: FC = ({ return ( - +
{name}
{permissionSummary && {permissionSummary}} @@ -128,7 +130,7 @@ const UserGroupsRow: FC = ({ ) } -const UserGroupsList: FC = ({ +const UserGroupList: FC = ({ noTitle, onClick, onEditPermissions, @@ -137,10 +139,14 @@ const UserGroupsList: FC = ({ showRemove, }) => { const [page, setPage] = useState(1) - const { data: userGroups, isLoading } = useGetGroupsQuery({ - orgId: `${orgId}`, - page, - }) + const { data: userGroups, isLoading } = useGetGroupSummariesQuery( + { + orgId: `${orgId}`, + }, + { + skip: !orgId, + }, + ) const { data: userGroupsPermission, isLoading: userGroupPermissionLoading } = useGetUserGroupPermissionQuery( { @@ -155,22 +161,20 @@ const UserGroupsList: FC = ({ ? [...userGroupsPermission] : [] - if (userGroupsPermission?.length > 0) { - userGroups?.results.forEach((group) => { - const existingPermissionIndex = - mergeduserGroupsPermissionWithUserGroups.findIndex( - (userGroupPermission) => userGroupPermission.group.id === group.id, - ) - if (existingPermissionIndex === -1) { - mergeduserGroupsPermissionWithUserGroups.push({ - admin: false, - group: group, - id: group.id, - permissions: [], - }) - } - }) - } + userGroups?.forEach?.((group) => { + const existingPermissionIndex = + mergeduserGroupsPermissionWithUserGroups.findIndex( + (userGroupPermission) => userGroupPermission.group.id === group.id, + ) + if (existingPermissionIndex === -1) { + mergeduserGroupsPermissionWithUserGroups.push({ + admin: false, + group: group, + id: group.id, + permissions: [], + }) + } + }) return ( @@ -189,7 +193,7 @@ const UserGroupsList: FC = ({ items={ userGroupsPermission ? sortBy(mergeduserGroupsPermissionWithUserGroups, 'group.name') - : sortBy(userGroups?.results, 'name') + : sortBy(userGroups, 'name') } paging={mergeduserGroupsPermissionWithUserGroups || userGroups} nextPage={() => setPage(page + 1)} @@ -268,4 +272,4 @@ const UserGroupsList: FC = ({ ) } -export default UserGroupsList +export default UserGroupList diff --git a/frontend/web/components/UserSelect.js b/frontend/web/components/UserSelect.js index a3ff828ffaad..f69530be3522 100644 --- a/frontend/web/components/UserSelect.js +++ b/frontend/web/components/UserSelect.js @@ -38,24 +38,25 @@ class TheComponent extends Component {
{users && users.map((v) => ( -
- { - const isRemove = value.includes(v.id) - if (isRemove && this.props.onRemove) { - this.props.onRemove(v.id) - } else if (!isRemove && this.props.onAdd) { - this.props.onAdd(v.id) - } - this.props.onChange && - this.props.onChange( - isRemove - ? value.filter((f) => f !== v.id) - : value.concat([v.id]), - ) - }} - space - > +
{ + const isRemove = value.includes(v.id) + if (isRemove && this.props.onRemove) { + this.props.onRemove(v.id) + } else if (!isRemove && this.props.onAdd) { + this.props.onAdd(v.id) + } + this.props.onChange && + this.props.onChange( + isRemove + ? value.filter((f) => f !== v.id) + : value.concat([v.id]), + ) + }} + className='assignees-list-item clickable' + key={v.id} + > + diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 75c10da3e794..89a42adb3278 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -106,7 +106,6 @@ const CreateFlag = class extends Component { tab: tab || 0, tags: tags || [], } - AppActions.getGroups(AccountStore.getOrganisation().id) } getState = () => {} diff --git a/frontend/web/components/modals/CreateGroup.js b/frontend/web/components/modals/CreateGroup.js deleted file mode 100644 index cf7f2d1a7338..000000000000 --- a/frontend/web/components/modals/CreateGroup.js +++ /dev/null @@ -1,454 +0,0 @@ -import React, { Component } from 'react' -import UserGroupsProvider from 'common/providers/UserGroupsProvider' -import ConfigProvider from 'common/providers/ConfigProvider' -import Switch from 'components/Switch' -import { getGroup } from 'common/services/useGroup' -import { getStore } from 'common/store' -import { components } from 'react-select' -import { setInterceptClose } from './base/ModalDefault' -import Icon from 'components/Icon' -import Tooltip from 'components/Tooltip' -import { IonIcon } from '@ionic/react' -import { informationCircle } from 'ionicons/icons' - -const widths = [80, 80] -const CreateGroup = class extends Component { - static displayName = 'CreateGroup' - - static contextTypes = { - router: propTypes.object.isRequired, - } - - constructor(props, context) { - super(props, context) - this.state = { - externalIdEdited: false, - groupNameEdited: false, - isLoading: !!this.props.group, - toggleChange: false, - userAddedOrUpdated: false, - userRemoved: false, - } - if (this.props.group) { - this.loadGroup() - } - } - - loadGroup = () => { - getGroup( - getStore(), - { - id: this.props.group.id, - orgId: this.props.orgId, - }, - { forceRefetch: true }, - ).then((res) => { - const group = res.data - this.setState({ - existingUsers: group - ? group.users.map((v) => ({ - group_admin: v.group_admin, - id: v.id, - })) - : [], - external_id: group ? group.external_id : undefined, - isLoading: false, - is_default: group ? group.is_default : false, - name: group ? group.name : '', - users: group - ? group.users.map((v) => ({ - edited: false, - group_admin: v.group_admin, - id: v.id, - })) - : [], - }) - }) - } - close() { - closeModal() - } - - onClosing = () => { - if ( - this.state.groupNameEdited || - this.state.externalIdEdited || - this.state.toggleChange || - this.state.userAddedOrUpdated || - this.state.userRemoved - ) { - return new Promise((resolve) => { - openConfirm({ - body: 'Closing this will discard your unsaved changes.', - noText: 'Cancel', - onNo: () => resolve(false), - onYes: () => resolve(true), - title: 'Discard changes', - yesText: 'Ok', - }) - }) - } else { - return Promise.resolve(true) - } - } - - componentDidMount = () => { - if (this.props.isEdit) { - setInterceptClose(this.onClosing) - } - if (!this.props.isEdit && !E2E) { - this.focusTimeout = setTimeout(() => { - this.input.focus() - this.focusTimeout = null - }, 500) - } - } - - componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout) - } - } - - getUsersToRemove = (users) => - _.filter(users, ({ id }) => !_.find(this.state.users, { id })) - - getUsersAdminChanged = (existingUsers, value) => { - return _.filter(this.state.users, (user) => { - if (!!user.group_admin !== value) { - //Ignore user - return false - } - const existingUser = _.find( - existingUsers, - (existingUser) => existingUser.id === user.id, - ) - const isAlreadyAdmin = !!existingUser?.group_admin - - return isAlreadyAdmin !== value - }) - } - - save = () => { - const { external_id, name, users } = this.state - - this.setState({ - externalIdEdited: false, - groupNameEdited: false, - toggleChange: false, - userAddedOrUpdated: false, - userRemoved: false, - }) - const data = { - external_id, - is_default: !!this.state.is_default, - name, - users, - usersToAddAdmin: this.getUsersAdminChanged( - this.state.existingUsers, - true, - ), - } - if (this.props.group) { - AppActions.updateGroup(this.props.orgId, { - ...data, - id: this.props.group.id, - usersToRemove: this.getUsersToRemove(this.state.existingUsers), - usersToRemoveAdmin: this.getUsersAdminChanged( - this.state.existingUsers, - false, - ), - }) - } else { - AppActions.createGroup(this.props.orgId, data) - } - } - - toggleUser = (id, group_admin, update) => { - const isMember = _.find(this.state.users, { id }) - const users = _.filter(this.state.users, (u) => u.id !== id) - this.setState({ - userAddedOrUpdated: true, - users: - isMember && !update - ? users - : users.concat([{ edited: true, group_admin, id }]), - }) - } - - render() { - const { external_id, isLoading, name } = this.state - const isEdit = !!this.props.group - const isAdmin = AccountStore.isAdmin() - const yourEmail = AccountStore.model.email - return ( - - {({ users }) => { - const activeUsers = _.intersectionBy(users, this.state.users, 'id') - const inactiveUsers = _.differenceBy(users, this.state.users, 'id') - return isLoading ? ( -
- -
- ) : ( - - {({ isSaving }) => ( -
{ - Utils.preventDefault(e) - this.save() - }} - > - - (this.input = e)} - data-test='groupName' - inputProps={{ - className: 'full-width', - name: 'groupName', - }} - value={name} - onChange={(e) => - this.setState({ - groupNameEdited: true, - name: Utils.safeParseEventValue(e), - }) - } - isValid={name && name.length} - type='text' - name='Name*' - unsaved={this.props.isEdit && this.state.groupNameEdited} - placeholder='E.g. Developers' - className='mb-5' - /> - - this.setState({ - externalIdEdited: true, - external_id: Utils.safeParseEventValue(e), - }) - } - isValid={name && name.length} - type='text' - name='Name*' - unsaved={this.props.isEdit && this.state.externalIdEdited} - placeholder='Add an optional external reference ID' - className='mb-5' - /> - - - - - this.setState({ - is_default: Utils.safeParseEventValue(e), - toggleChange: true, - }) - } - checked={!!this.state.is_default} - /> - - - - } - > - New users that sign up to your organisation will be - automatically added to this group with USER permissions - - - -
- -
- { + const { email, first_name, id, last_name } = + props.data.user || {} + return ( + + {`${first_name} ${last_name}`}{' '} + {id == AccountStore.getUserId() && '(You)'} +
+ {email} +
+
+ ) + }, + }} + value={{ label: 'Add a user' }} + onChange={(v: { value: number }) => { + toggleUser(v.value, false, false) + setEdited(true) + }} + options={inactiveUsers.map((user) => ({ + label: `${user.first_name || ''} ${ + user.last_name || '' + } ${user.email} ${user.id}`, + user, + value: user.id, + }))} + /> +
+ + + search ? ( + + No results found for {search} + + ) : ( + + This group has no members + + ) + } + id='org-members-list' + title='Members' + className='mt-4 no-pad overflow-visible' + renderSearchWithNoResults + items={sortBy(activeUsers, 'first_name')} + filterRow={(item: GroupUser, search: string) => { + const strToSearch = `${item.first_name} ${item.last_name} ${item.email} ${item.id}` + return ( + strToSearch + .toLowerCase() + .indexOf(search.toLowerCase()) !== -1 + ) + }} + header={ + <> + + +
User
+
+
+ + Admin + + } + > + Allows inviting additional team members to the + group + +
+
+ Remove +
+
+ + } + renderRow={({ + email, + first_name, + id, + last_name, + }: GroupUser) => { + const matchingUser = users.find((v) => v.id === id) + const isGroupAdmin = matchingUser?.group_admin + const userEdited = matchingUser?.edited + return ( + + +
+ {`${first_name} ${last_name}`}{' '} + {id == AccountStore.getUserId() && '(You)'}{' '} + {isEdit && userEdited && ( +
Unsaved
+ )} +
+
+ {email} +
+
+
+ { + toggleUser(id, e, true) + setEdited(true) + }} + checked={isGroupAdmin} + /> +
+
+ +
+
+ ) + }} + /> +
+
+ {group ? ( + <> + + + ) : ( + + )} +
+
+
+ + ) + }} +
+ ) + const editPermissionsEl = ( +
+ {!!groupData && ( + + )} +
+ ) + if (error) { + return + } + return isEdit ? ( + + + General + {!!edited && *} +
+ } + > + {editGroupEl} + + Permissions
}>{editPermissionsEl} + + ) : ( + editGroup + ) +} + +export default ConfigProvider(CreateGroup) diff --git a/frontend/web/components/modals/CreateRole.tsx b/frontend/web/components/modals/CreateRole.tsx index a05ff46b68eb..a310afee599e 100644 --- a/frontend/web/components/modals/CreateRole.tsx +++ b/frontend/web/components/modals/CreateRole.tsx @@ -46,6 +46,8 @@ import Utils from 'common/utils/utils' import Button from 'components/base/forms/Button' import Input from 'components/base/forms/Input' import SettingsButton from 'components/SettingsButton' +import PermissionsTabs from 'components/PermissionsTabs' +import AccountStore from 'common/stores/account-store' type TabRef = { onClosing: () => Promise @@ -70,8 +72,6 @@ const CreateRole: FC = ({ const buttonText = isEdit ? 'Update Role' : 'Create Role' const [tab, setTab] = useState(0) const [userGroupTab, setUserGroupTab] = useState(0) - const [project, setProject] = useState('') - const [environments, setEnvironments] = useState([]) const [showUserSelect, setShowUserSelect] = useState(false) const [showGroupSelect, setShowGroupSelect] = useState(false) const [userSelected, setUserSelected] = useState< @@ -87,8 +87,6 @@ const CreateRole: FC = ({ }[] >([]) - const projectData: Project[] = OrganisationStore.getProjects() - const [createRolePermissionUser, { data: usersData, isSuccess: userAdded }] = useCreateRolesPermissionUsersMutation() @@ -235,15 +233,6 @@ const CreateRole: FC = ({ }) }, [groups, groupSelected]) - useEffect(() => { - if (project && projectData) { - const environments = projectData.find( - (p) => p.id === parseInt(project), - )?.environments - setEnvironments(environments || []) - } - }, [project, projectData]) - useEffect(() => { if (userAdded && usersData) { setUserSelected( @@ -403,8 +392,6 @@ const CreateRole: FC = ({ }) const TabValue = () => { - const [searchProject, setSearchProject] = useState('') - const [searchEnv, setSearchEnv] = useState('') const ref = useRef(null) const ref2 = useRef(null) useEffect(() => { @@ -528,85 +515,13 @@ const CreateRole: FC = ({ tabLabel={Permissions} >
- - Organisation - } - > - - - Project} - > - -
Permissions
- - setSearchProject(Utils.safeParseEventValue(e)) - } - size='small' - placeholder='Search' - search - /> -
- -
- Environment - } - > - -
Permissions
- - setSearchEnv(Utils.safeParseEventValue(e)) - } - size='small' - placeholder='Search' - search - /> -
-
- -
- {environments.length > 0 && ( - - )} -
-
+ role={role} + orgId={AccountStore.getOrganisation()?.id} + />
diff --git a/frontend/web/components/pages/OrganisationGroupsPage.js b/frontend/web/components/pages/OrganisationGroupsPage.js deleted file mode 100644 index 38b61af8220b..000000000000 --- a/frontend/web/components/pages/OrganisationGroupsPage.js +++ /dev/null @@ -1,126 +0,0 @@ -import React, { Component } from 'react' -import UserGroupList from 'components/UserGroupList' -import CreateGroupModal from 'components/modals/CreateGroup' -import withAuditWebhooks from 'common/providers/withAuditWebhooks' -import Button from 'components/base/forms/Button' -import { EditPermissionsModal } from 'components/EditPermissions' -import ConfigProvider from 'common/providers/ConfigProvider' -import Constants from 'common/constants' -import Permission from 'common/providers/Permission' -import PageTitle from 'components/PageTitle' -import OrganisationManageWidget from 'components/OrganisationManageWidget' - -const OrganisationGroupsPage = class extends Component { - static contextTypes = { - router: propTypes.object.isRequired, - } - - static displayName = 'OrganisationGroupsPage' - - constructor(props, context) { - super(props, context) - this.state = { - manageSubscriptionLoaded: true, - role: 'ADMIN', - } - if (!AccountStore.getOrganisation()) { - return - } - AppActions.getOrganisation(AccountStore.getOrganisation().id) - } - - componentDidMount = () => { - API.trackPage(Constants.pages.ORGANISATION_SETTINGS) - $('body').trigger('click') - } - - editGroupPermissions = (group) => { - openModal( - 'Edit Organisation Permissions', - { - AppActions.getOrganisation(AccountStore.getOrganisation().id) - }} - level='organisation' - group={group} - push={this.context.router.history.push} - />, - 'p-0 side-modal', - ) - } - - render() { - return ( -
-
- -
- - - {({ organisation }, {}) => - !!organisation && ( -
-
- - - {({ permission }) => ( - <> - {Utils.renderWithPermission( - permission, - Constants.organisationPermissions( - 'Manage Groups', - ), - , - )} - - )} - - - } - title={'User Groups'} - > - Groups allow you to manage permissions for viewing and - editing projects, features and environments. - - -
-
- ) - } -
-
- ) - } -} - -OrganisationGroupsPage.propTypes = {} - -module.exports = ConfigProvider(withAuditWebhooks(OrganisationGroupsPage)) diff --git a/frontend/web/components/pages/OrganisationSettingsPage.js b/frontend/web/components/pages/OrganisationSettingsPage.js index fb2952a77d58..9d44c5c71a4d 100644 --- a/frontend/web/components/pages/OrganisationSettingsPage.js +++ b/frontend/web/components/pages/OrganisationSettingsPage.js @@ -7,9 +7,7 @@ import CreateGroupModal from 'components/modals/CreateGroup' import withAuditWebhooks from 'common/providers/withAuditWebhooks' import CreateAuditWebhookModal from 'components/modals/CreateAuditWebhook' import ConfirmRemoveAuditWebhook from 'components/modals/ConfirmRemoveAuditWebhook' -import ConfirmDeleteRole from 'components/modals/ConfirmDeleteRole' import Button from 'components/base/forms/Button' -import { EditPermissionsModal } from 'components/EditPermissions' import AdminAPIKeys from 'components/AdminAPIKeys' import Tabs from 'components/base/forms/Tabs' import TabItem from 'components/base/forms/TabItem' @@ -27,13 +25,17 @@ import { getStore } from 'common/store' import { getRoles } from 'common/services/useRole' import _data from 'common/data/base/_data' import RolesTable from 'components/RolesTable' +import CreateGroup from 'components/modals/CreateGroup' +import PermissionsTabs from 'components/PermissionsTabs' +import UserAction from 'components/UserAction' +import classNames from 'classnames' +import AccountStore from 'common/stores/account-store' -const widths = [450, 255, 250, 235, 150, 100] +const widths = [300, 200, 80] const SettingsTab = { 'Billing': 'billing', 'General': 'general', - 'Groups': 'groups', 'Keys': 'keys', 'Members': 'members', 'Projects': 'projects', @@ -66,6 +68,9 @@ const OrganisationSettingsPage = class extends Component { } componentDidMount = () => { + if (!AccountStore.getOrganisation()) { + return + } getRoles( getStore(), { organisation_id: AccountStore.getOrganisation().id }, @@ -73,7 +78,6 @@ const OrganisationSettingsPage = class extends Component { ).then((roles) => { this.setState({ roles: roles.data.results }) }) - AppActions.getGroups(AccountStore.getOrganisation().id) API.trackPage(Constants.pages.ORGANISATION_SETTINGS) $('body').trigger('click') if ( @@ -247,6 +251,19 @@ const OrganisationSettingsPage = class extends Component { ) } + editGroup = (group) => { + openModal( + 'Edit Group', + , + 'side-modal', + ) + } + deleteWebhook = (webhook) => { openModal( 'Remove Webhook', @@ -258,44 +275,15 @@ const OrganisationSettingsPage = class extends Component { ) } - editUserPermissions = (user, roles) => { + editUserPermissions = (user, organisationId) => { openModal( 'Edit Organisation Permissions', - { - AppActions.getOrganisation(AccountStore.getOrganisation().id) - }} - isEditUserPermission - level='organisation' - roles={roles} - user={user} - />, +
+ +
, 'p-0 side-modal', ) } - - editGroupPermissions = (group, roles) => { - openModal( - 'Edit Organisation Permissions', - { - AppActions.getOrganisation(AccountStore.getOrganisation().id) - }} - isEditGroupPermission - level='organisation' - group={group} - roles={roles} - push={this.context.router.history.push} - />, - 'p-0 side-modal', - ) - } - formatLastLoggedIn = (last_login) => { if (!last_login) return 'Never' @@ -343,10 +331,9 @@ const OrganisationSettingsPage = class extends Component { > {({ isSaving, organisation }, { deleteOrganisation }) => !!organisation && ( - + {({ error, - groups, invalidateInviteLink, inviteLinks, invites, @@ -355,7 +342,6 @@ const OrganisationSettingsPage = class extends Component { subscriptionMeta, users, }) => { - const canManageGroups = !!groups?.length const { max_seats } = subscriptionMeta || organisation.subscription || { max_seats: 1 } const isAWS = @@ -373,9 +359,9 @@ const OrganisationSettingsPage = class extends Component { const displayedTabs = [SettingsTab.Projects] if (this.state.permissionsError) { - if (canManageGroups) { - displayedTabs.push(SettingsTab.Groups) - } + displayedTabs.push( + ...[SettingsTab.Members].filter((v) => !!v), + ) } else { displayedTabs.push( ...[ @@ -419,43 +405,6 @@ const OrganisationSettingsPage = class extends Component { )} - {displayedTabs.includes(SettingsTab.Groups) && ( - - -
-
User Groups
- -

- Groups allow you to manage permissions for - viewing and editing projects, features and - environments. -

-
- - -
- - -
- )} - {displayedTabs.includes(SettingsTab.General) && ( @@ -747,23 +696,30 @@ const OrganisationSettingsPage = class extends Component {
Team Members
- + {Utils.renderWithPermission( + !this.state.permissionsError, + Constants.organisationPermissions( + 'Admin', + ), + , + )} {paymentsEnabled && @@ -1028,25 +984,17 @@ const OrganisationSettingsPage = class extends Component { User - Role - -
- Action
@@ -1054,11 +1002,11 @@ const OrganisationSettingsPage = class extends Component {
- Remove + Actions
} @@ -1073,11 +1021,23 @@ const OrganisationSettingsPage = class extends Component { last_name, role, } = user + + const onRemoveClick = () => { + this.deleteUser( + id, + Format.userDisplayName({ + email, + firstName: first_name, + lastName: last_name, + }), + email, + ) + } const onEditClick = () => { if (role !== 'ADMIN') { this.editUserPermissions( user, - this.state.roles, + organisation.id, ) } } @@ -1085,13 +1045,17 @@ const OrganisationSettingsPage = class extends Component { - + {`${first_name} ${last_name}`}{' '} {id === AccountStore.getUserId() && @@ -1101,12 +1065,13 @@ const OrganisationSettingsPage = class extends Component {
- -
+
+
{organisation.role === 'ADMIN' && id !== @@ -1115,14 +1080,6 @@ const OrganisationSettingsPage = class extends Component {