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:
@@ -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({});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)."
|
||||
|
||||
Reference in New Issue
Block a user