diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index f6821b19774..d12a01b36f5 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -18,6 +18,9 @@ export interface Props { onRolesChange: (newRoles: Role[]) => void; onBasicRoleChange?: (newRole: OrgRole) => void; canUpdateRoles?: boolean; + /** + * Set {@link RolePickerMenu}'s button to display either `Apply` (apply=true) or `Update` (apply=false) + */ apply?: boolean; } diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index 3bcb1894859..de92e711c26 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -5,7 +5,6 @@ import { contextSrv } from 'app/core/core'; import { Role, AccessControlAction } from 'app/types'; import { RolePicker } from './RolePicker'; -// @ts-ignore import { fetchTeamRoles, updateTeamRoles } from './api'; export interface Props { @@ -13,26 +12,56 @@ export interface Props { orgId?: number; roleOptions: Role[]; disabled?: boolean; + onApplyRoles?: (newRoles: Role[]) => void; + pendingRoles?: Role[]; + /** + * Set whether the component should send a request with the new roles to the + * backend in TeamRolePicker.onRolesChange (apply=false), or call {@link onApplyRoles} + * with the updated list of roles (apply=true). + * + * Besides it sets the RolePickerMenu's Button title to + * * `Update` in case apply equals false + * * `Apply` in case apply equals true + * + * @default false + */ + apply?: boolean; } -export const TeamRolePicker: FC = ({ teamId, orgId, roleOptions, disabled }) => { +export const TeamRolePicker: FC = ({ + teamId, + roleOptions, + disabled, + onApplyRoles, + pendingRoles, + apply = false, +}) => { const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => { try { - return await fetchTeamRoles(teamId, orgId); + if (apply && Boolean(pendingRoles?.length)) { + return pendingRoles; + } + + if (contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList)) { + return await fetchTeamRoles(teamId); + } } catch (e) { - // TODO handle error - console.error('Error loading options'); + console.error('Error loading options', e); } return []; - }, [orgId, teamId]); + }, [teamId, pendingRoles]); useEffect(() => { getTeamRoles(); - }, [orgId, teamId, getTeamRoles]); + }, [teamId, getTeamRoles, pendingRoles]); const onRolesChange = async (roles: Role[]) => { - await updateTeamRoles(roles, teamId, orgId); - await getTeamRoles(); + if (!apply) { + await updateTeamRoles(roles, teamId); + await getTeamRoles(); + } else if (onApplyRoles) { + onApplyRoles(roles); + } }; const canUpdateRoles = @@ -41,11 +70,13 @@ export const TeamRolePicker: FC = ({ teamId, orgId, roleOptions, disabled return ( ); diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index b00e86d3936..11c5aa68e67 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -15,6 +15,17 @@ export interface Props { roleOptions: Role[]; disabled?: boolean; basicRoleDisabled?: boolean; + /** + * Set whether the component should send a request with the new roles to the + * backend in UserRolePicker.onRolesChange (apply=false), or call {@link onApplyRoles} + * with the updated list of roles (apply=true). + * + * Besides it sets the RolePickerMenu's Button title to + * * `Update` in case apply equals false + * * `Apply` in case apply equals true + * + * @default false + */ apply?: boolean; onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void; pendingRoles?: Role[]; @@ -34,11 +45,10 @@ export const UserRolePicker: FC = ({ }) => { const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => { try { - if (apply) { - if (pendingRoles?.length! > 0) { - return pendingRoles; - } + if (apply && Boolean(pendingRoles?.length)) { + return pendingRoles; } + if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesList)) { return await fetchUserRoles(userId, orgId); } diff --git a/public/app/core/components/RolePicker/hooks.ts b/public/app/core/components/RolePicker/hooks.ts new file mode 100644 index 00000000000..f2e46152326 --- /dev/null +++ b/public/app/core/components/RolePicker/hooks.ts @@ -0,0 +1,20 @@ +import { useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { contextSrv } from 'app/core/core'; +import { AccessControlAction } from 'app/types'; + +import { fetchRoleOptions } from './api'; + +export const useRoleOptions = (organizationId: number) => { + const [orgId, setOrgId] = useState(organizationId); + + const { value = [] } = useAsync(async () => { + if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { + return fetchRoleOptions(orgId); + } + return Promise.resolve([]); + }, [orgId]); + + return [{ roleOptions: value }, setOrgId] as const; +}; diff --git a/public/app/features/teams/CreateTeam.test.tsx b/public/app/features/teams/CreateTeam.test.tsx index 404dcc23dda..b1d320be4bf 100644 --- a/public/app/features/teams/CreateTeam.test.tsx +++ b/public/app/features/teams/CreateTeam.test.tsx @@ -10,6 +10,19 @@ beforeEach(() => { jest.clearAllMocks(); }); +jest.mock('app/core/core', () => ({ + contextSrv: { + licensedAccessControlEnabled: () => false, + hasPermission: () => true, + hasPermissionInMetadata: () => true, + user: { orgId: 1 }, + }, +})); + +jest.mock('app/core/components/RolePicker/hooks', () => ({ + useRoleOptions: jest.fn().mockReturnValue([{ roleOptions: [] }, jest.fn()]), +})); + const mockPost = jest.fn(() => { return Promise.resolve({}); }); diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index b0fabfcef5f..b713f3b8ade 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -1,51 +1,81 @@ -import React, { PureComponent } from 'react'; +import React, { useState } from 'react'; import { getBackendSrv, locationService } from '@grafana/runtime'; import { Button, Form, Field, Input, FieldSet } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; +import { updateTeamRoles } from 'app/core/components/RolePicker/api'; +import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; import { contextSrv } from 'app/core/core'; +import { AccessControlAction, Role } from 'app/types'; interface TeamDTO { - name: string; email: string; + name: string; } -export class CreateTeam extends PureComponent { - create = async (formModel: TeamDTO) => { - const result = await getBackendSrv().post('/api/teams', formModel); - if (result.teamId) { - await contextSrv.fetchUserPermissions(); - locationService.push(`/org/teams/edit/${result.teamId}`); +export const CreateTeam = (): JSX.Element => { + const currentOrgId = contextSrv.user.orgId; + const [pendingRoles, setPendingRoles] = useState([]); + const [{ roleOptions }] = useRoleOptions(currentOrgId); + + const canUpdateRoles = + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); + + const createTeam = async (formModel: TeamDTO) => { + const newTeam = await getBackendSrv().post('/api/teams', formModel); + if (newTeam.teamId) { + try { + await contextSrv.fetchUserPermissions(); + if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) { + await updateTeamRoles(pendingRoles, newTeam.teamId, newTeam.orgId); + } + } catch (e) { + console.error(e); + } + locationService.push(`/org/teams/edit/${newTeam.teamId}`); } }; - render() { - return ( - - -
- {({ register, errors }) => ( -
- - + + return ( + + + + {({ register, errors }) => ( +
+ + + + {contextSrv.licensedAccessControlEnabled() && ( + + - - - -
- -
-
- )} - -
-
- ); - } -} + )} + + + +
+ +
+
+ )} + +
+
+ ); +}; export default CreateTeam; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index 1092aefede7..08bc0ae26a4 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -7,7 +7,10 @@ import { getMockTeam } from './__mocks__/teamMocks'; jest.mock('app/core/services/context_srv', () => ({ contextSrv: { + licensedAccessControlEnabled: () => false, + hasPermission: () => true, hasPermissionInMetadata: () => true, + user: { orgId: 1 }, }, })); @@ -15,6 +18,10 @@ jest.mock('app/core/components/SharedPreferences/SharedPreferences', () => { return { SharedPreferences: () =>
}; }); +jest.mock('app/core/components/RolePicker/hooks', () => ({ + useRoleOptions: jest.fn().mockReturnValue([{ roleOptions: [] }, jest.fn()]), +})); + const setup = (propOverrides?: object) => { const props: Props = { team: getMockTeam(), @@ -30,7 +37,7 @@ describe('Team settings', () => { it('should render component', () => { setup(); - expect(screen.getByText('Team settings')).toBeInTheDocument(); + expect(screen.getByText('Team details')).toBeInTheDocument(); }); it('should validate required fields', async () => { diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 7a828f13064..6e75e6e7f20 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,10 +1,13 @@ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Input, Field, Form, Button, FieldSet, VerticalGroup } from '@grafana/ui'; +import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; +import { updateTeamRoles } from 'app/core/components/RolePicker/api'; +import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Team } from 'app/types'; +import { AccessControlAction, Role, Team } from 'app/types'; import { updateTeam } from './state/actions'; @@ -21,13 +24,24 @@ export type Props = ConnectedProps & OwnProps; export const TeamSettings: FC = ({ team, updateTeam }) => { const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); + const currentOrgId = contextSrv.user.orgId; + + const [{ roleOptions }] = useRoleOptions(currentOrgId); + const [pendingRoles, setPendingRoles] = useState([]); + + const canUpdateRoles = + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); return ( -
+
{ + onSubmit={async (formTeam: Team) => { + if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) { + await updateTeamRoles(pendingRoles, team.id); + } updateTeam(formTeam.name, formTeam.email); }} disabled={!canWriteTeamSettings} @@ -44,6 +58,19 @@ export const TeamSettings: FC = ({ team, updateTeam }) => { + {contextSrv.licensedAccessControlEnabled() && ( + + + + )} +