Teams: Add TeamRolePicker to the Create and Edit Team pages (#53775)

* Add TeamRolePicker to CreateTeam and TeamSettings pages

* Align tests to the changes

* Change TeamRolePicker

* Add useRoleOptions hook

* Clean up

* Requested changes by reviewers

* Fixes

* Fixes
This commit is contained in:
Mihály Gyöngyösi
2022-08-18 13:21:06 +02:00
committed by GitHub
parent eedc7f1831
commit a915977002
8 changed files with 195 additions and 54 deletions
@@ -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;
}
@@ -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<Props> = ({ teamId, orgId, roleOptions, disabled }) => {
export const TeamRolePicker: FC<Props> = ({
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<Props> = ({ teamId, orgId, roleOptions, disabled
return (
<RolePicker
apply={apply}
onRolesChange={onRolesChange}
roleOptions={roleOptions}
appliedRoles={appliedRoles}
isLoading={loading}
disabled={disabled}
basicRoleDisabled={true}
canUpdateRoles={canUpdateRoles}
/>
);
@@ -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<Props> = ({
}) => {
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);
}
@@ -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;
};
@@ -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({});
});
+66 -36
View File
@@ -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<Role[]>([]);
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 (
<Page navId="teams">
<Page.Contents>
<Form onSubmit={this.create}>
{({ register, errors }) => (
<FieldSet label="New Team">
<Field label="Name" required invalid={!!errors.name} error="Team name is required">
<Input {...register('name', { required: true })} id="team-name" width={60} />
return (
<Page navId="teams">
<Page.Contents>
<Form onSubmit={createTeam}>
{({ register, errors }) => (
<FieldSet label="New Team">
<Field label="Name" required invalid={!!errors.name} error="Team name is required">
<Input {...register('name', { required: true })} id="team-name" width={60} />
</Field>
{contextSrv.licensedAccessControlEnabled() && (
<Field label="Role">
<TeamRolePicker
teamId={0}
roleOptions={roleOptions}
disabled={false}
apply={true}
onApplyRoles={setPendingRoles}
pendingRoles={pendingRoles}
/>
</Field>
<Field
label={'Email'}
description={'This is optional and is primarily used for allowing custom team avatars.'}
>
<Input {...register('email')} type="email" id="team-email" placeholder="email@test.com" width={60} />
</Field>
<div className="gf-form-button-row">
<Button type="submit" variant="primary">
Create
</Button>
</div>
</FieldSet>
)}
</Form>
</Page.Contents>
</Page>
);
}
}
)}
<Field
label={'Email'}
description={'This is optional and is primarily used for allowing custom team avatars.'}
>
<Input {...register('email')} type="email" id="team-email" placeholder="email@test.com" width={60} />
</Field>
<div className="gf-form-button-row">
<Button type="submit" variant="primary">
Create
</Button>
</div>
</FieldSet>
)}
</Form>
</Page.Contents>
</Page>
);
};
export default CreateTeam;
@@ -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: () => <div /> };
});
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 () => {
+31 -4
View File
@@ -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<typeof connector> & OwnProps;
export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team);
const currentOrgId = contextSrv.user.orgId;
const [{ roleOptions }] = useRoleOptions(currentOrgId);
const [pendingRoles, setPendingRoles] = useState<Role[]>([]);
const canUpdateRoles =
contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) &&
contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove);
return (
<VerticalGroup>
<FieldSet label="Team settings">
<FieldSet label="Team details">
<Form
defaultValues={{ ...team }}
onSubmit={(formTeam: Team) => {
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<Props> = ({ team, updateTeam }) => {
<Input {...register('name', { required: true })} id="name-input" />
</Field>
{contextSrv.licensedAccessControlEnabled() && (
<Field label="Role">
<TeamRolePicker
teamId={team.id}
roleOptions={roleOptions}
disabled={false}
apply={true}
onApplyRoles={setPendingRoles}
pendingRoles={pendingRoles}
/>
</Field>
)}
<Field
label="Email"
description="This is optional and is primarily used to set the team profile avatar (via gravatar service)."