Commit 178d637b by Hugo Häggmark Committed by Leonard Gram

refactor: splitted TeamMembers to TeamMemberRow

parent 6a63725d
import React from 'react';
import { shallow } from 'enzyme';
import { TeamMember, TeamPermissionLevel } from '../../types';
import { getMockTeamMember } from './__mocks__/teamMocks';
import { TeamMemberRow, Props } from './TeamMemberRow';
import { SelectOptionItem } from '@grafana/ui';
const setup = (propOverrides?: object) => {
const props: Props = {
member: getMockTeamMember(),
syncEnabled: false,
editorsCanAdmin: false,
signedInUserIsTeamAdmin: false,
updateTeamMember: jest.fn(),
removeTeamMember: jest.fn(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<TeamMemberRow {...props} />);
const instance = wrapper.instance() as TeamMemberRow;
return {
wrapper,
instance,
};
};
describe('Render', () => {
describe('when feature toggle editorsCanAdmin is turned on', () => {
it('should render permissions select if user is team admin', () => {
const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: true });
expect(wrapper).toMatchSnapshot();
});
it('should render span and disable buttons if user is team member', () => {
const { wrapper } = setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: false });
expect(wrapper).toMatchSnapshot();
});
});
describe('when feature toggle editorsCanAdmin is turned off', () => {
it('should not render permissions', () => {
const { wrapper } = setup({ editorsCanAdmin: false, signedInUserIsTeamAdmin: true });
expect(wrapper).toMatchSnapshot();
});
});
});
describe('Functions', () => {
describe('on remove member', () => {
const member = getMockTeamMember();
const { instance } = setup({ member });
instance.onRemoveMember(member);
expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
});
describe('on update permision for user in team', () => {
const member: TeamMember = {
userId: 3,
teamId: 2,
avatarUrl: '',
email: 'user@user.org',
labels: [],
login: 'member',
permission: TeamPermissionLevel.Member,
};
const { instance } = setup({ member });
const permission = TeamPermissionLevel.Admin;
const item: SelectOptionItem = { value: permission };
const expectedTeamMemeber = { ...member, permission };
instance.onPermissionChange(item, member);
expect(instance.props.updateTeamMember).toHaveBeenCalledWith(expectedTeamMemeber);
});
});
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui';
import { TeamMember, teamsPermissionLevels } from 'app/types';
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
import { updateTeamMember, removeTeamMember } from './state/actions';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
export interface Props {
member: TeamMember;
syncEnabled: boolean;
editorsCanAdmin: boolean;
signedInUserIsTeamAdmin: boolean;
removeTeamMember?: typeof removeTeamMember;
updateTeamMember?: typeof updateTeamMember;
}
export class TeamMemberRow extends PureComponent<Props> {
constructor(props: Props) {
super(props);
this.renderLabels = this.renderLabels.bind(this);
this.renderPermissions = this.renderPermissions.bind(this);
}
onRemoveMember(member: TeamMember) {
this.props.removeTeamMember(member.userId);
}
onPermissionChange = (item: SelectOptionItem, member: TeamMember) => {
const permission = item.value;
const updatedTeamMember = { ...member, permission };
this.props.updateTeamMember(updatedTeamMember);
};
renderPermissions(member: TeamMember) {
const { editorsCanAdmin, signedInUserIsTeamAdmin } = this.props;
const value = teamsPermissionLevels.find(dp => dp.value === member.permission);
return (
<WithFeatureToggle featureToggle={editorsCanAdmin}>
<td>
<div className="gf-form">
{signedInUserIsTeamAdmin && (
<Select
isSearchable={false}
options={teamsPermissionLevels}
onChange={item => this.onPermissionChange(item, member)}
className="gf-form-select-box__control--menu-right"
value={value}
/>
)}
{!signedInUserIsTeamAdmin && <span>{value.label}</span>}
</div>
</td>
</WithFeatureToggle>
);
}
renderLabels(labels: string[]) {
if (!labels) {
return <td />;
}
return (
<td>
{labels.map(label => (
<TagBadge key={label} label={label} removeIcon={false} count={0} onClick={() => {}} />
))}
</td>
);
}
render() {
const { member, syncEnabled, signedInUserIsTeamAdmin } = this.props;
return (
<tr key={member.userId}>
<td className="width-4 text-center">
<img className="filter-table__avatar" src={member.avatarUrl} />
</td>
<td>{member.login}</td>
<td>{member.email}</td>
{this.renderPermissions(member)}
{syncEnabled && this.renderLabels(member.labels)}
<td className="text-right">
<DeleteButton onConfirm={() => this.onRemoveMember(member)} disabled={!signedInUserIsTeamAdmin} />
</td>
</tr>
);
}
}
function mapStateToProps(state) {
return {};
}
const mapDispatchToProps = {
removeTeamMember,
updateTeamMember,
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TeamMemberRow);
import React from 'react';
import { shallow } from 'enzyme';
import { TeamMembers, Props, State } from './TeamMembers';
import { TeamMember, TeamPermissionLevel } from '../../types';
import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
import { SelectOptionItem } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { TeamMember, OrgRole } from '../../types';
import { getMockTeamMembers } from './__mocks__/teamMocks';
import { User } from 'app/core/services/context_srv';
const signedInUserId = 1;
const originalContextSrv = contextSrv;
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
isGrafanaAdmin: false,
hasRole: role => false,
user: { id: signedInUserId },
},
}));
interface SetupProps {
propOverrides?: object;
isGrafanaAdmin?: boolean;
isOrgAdmin?: boolean;
}
const setup = (setupProps: SetupProps) => {
const setup = (propOverrides?: object) => {
const props: Props = {
members: [] as TeamMember[],
searchMemberQuery: '',
setSearchMemberQuery: jest.fn(),
loadTeamMembers: jest.fn(),
addTeamMember: jest.fn(),
removeTeamMember: jest.fn(),
updateTeamMember: jest.fn(),
syncEnabled: false,
editorsCanAdmin: false,
signedInUser: {
id: signedInUserId,
isGrafanaAdmin: false,
orgRole: OrgRole.Viewer,
} as User,
};
contextSrv.isGrafanaAdmin = setupProps.isGrafanaAdmin || false;
contextSrv.hasRole = role => setupProps.isOrgAdmin || false;
Object.assign(props, setupProps.propOverrides);
Object.assign(props, propOverrides);
const wrapper = shallow(<TeamMembers {...props} />);
const instance = wrapper.instance() as TeamMembers;
......@@ -51,11 +35,6 @@ const setup = (setupProps: SetupProps) => {
};
describe('Render', () => {
beforeEach(() => {
contextSrv.isGrafanaAdmin = originalContextSrv.isGrafanaAdmin;
contextSrv.hasRole = originalContextSrv.hasRole;
});
it('should render component', () => {
const { wrapper } = setup({});
......@@ -63,74 +42,16 @@ describe('Render', () => {
});
it('should render team members', () => {
const { wrapper } = setup({
propOverrides: {
members: getMockTeamMembers(5, 5),
},
});
const { wrapper } = setup({ members: getMockTeamMembers(5, 5) });
expect(wrapper).toMatchSnapshot();
});
it('should render team members when sync enabled', () => {
const { wrapper } = setup({
propOverrides: {
members: getMockTeamMembers(5, 5),
syncEnabled: true,
},
});
const { wrapper } = setup({ members: getMockTeamMembers(5, 5), syncEnabled: true });
expect(wrapper).toMatchSnapshot();
});
describe('when feature toggle editorsCanAdmin is turned on', () => {
it('should render permissions select if user is Grafana Admin', () => {
const members = getMockTeamMembers(5, 5);
const { wrapper } = setup({
propOverrides: { members, editorsCanAdmin: true },
isGrafanaAdmin: true,
isOrgAdmin: false,
});
expect(wrapper).toMatchSnapshot();
});
it('should render permissions select if user is Org Admin', () => {
const members = getMockTeamMembers(5, 5);
const { wrapper } = setup({
propOverrides: { members, editorsCanAdmin: true },
isGrafanaAdmin: false,
isOrgAdmin: true,
});
expect(wrapper).toMatchSnapshot();
});
it('should render permissions select if user is team admin', () => {
const members = getMockTeamMembers(5, signedInUserId);
const { wrapper } = setup({
propOverrides: { members, editorsCanAdmin: true },
isGrafanaAdmin: false,
isOrgAdmin: false,
});
expect(wrapper).toMatchSnapshot();
});
it('should render span and disable buttons if user is team member', () => {
const members = getMockTeamMembers(5, 5);
const { wrapper } = setup({
propOverrides: {
members,
editorsCanAdmin: true,
},
isGrafanaAdmin: false,
isOrgAdmin: false,
});
expect(wrapper).toMatchSnapshot();
});
});
});
describe('Functions', () => {
......@@ -144,15 +65,6 @@ describe('Functions', () => {
});
});
describe('on remove member', () => {
const { instance } = setup({});
const mockTeamMember = getMockTeamMember();
instance.onRemoveMember(mockTeamMember);
expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
});
describe('on add user to team', () => {
const { wrapper, instance } = setup({});
const state = wrapper.state() as State;
......@@ -169,23 +81,85 @@ describe('Functions', () => {
expect(instance.props.addTeamMember).toHaveBeenCalledWith(1);
});
describe('on update permision for user in team', () => {
const { instance } = setup({});
const permission = TeamPermissionLevel.Admin;
const item: SelectOptionItem = { value: permission };
const member: TeamMember = {
userId: 3,
teamId: 2,
avatarUrl: '',
email: 'user@user.org',
labels: [],
login: 'member',
permission: TeamPermissionLevel.Member,
};
const expectedTeamMemeber = { ...member, permission };
describe('isSignedInUserTeamAdmin', () => {
describe('when feature toggle editorsCanAdmin is turned off', () => {
it('should return true', () => {
const { instance } = setup({ editorsCanAdmin: false });
const result = instance.isSignedInUserTeamAdmin();
expect(result).toBe(true);
});
});
describe('when feature toggle editorsCanAdmin is turned on', () => {
it('should return true if signed in user is grafanaAdmin', () => {
const members = getMockTeamMembers(5, 5);
const { instance } = setup({
members,
editorsCanAdmin: true,
signedInUser: {
id: signedInUserId,
isGrafanaAdmin: true,
orgRole: OrgRole.Viewer,
},
});
const result = instance.isSignedInUserTeamAdmin();
expect(result).toBe(true);
});
it('should return true if signed in user is org admin', () => {
const members = getMockTeamMembers(5, 5);
const { instance } = setup({
members,
editorsCanAdmin: true,
signedInUser: {
id: signedInUserId,
isGrafanaAdmin: false,
orgRole: OrgRole.Admin,
},
});
const result = instance.isSignedInUserTeamAdmin();
expect(result).toBe(true);
});
it('should return true if signed in user is team admin', () => {
const members = getMockTeamMembers(5, signedInUserId);
const { instance } = setup({
members,
editorsCanAdmin: true,
signedInUser: {
id: signedInUserId,
isGrafanaAdmin: false,
orgRole: OrgRole.Viewer,
},
});
instance.onPermissionChange(item, member);
const result = instance.isSignedInUserTeamAdmin();
expect(instance.props.updateTeamMember).toHaveBeenCalledWith(expectedTeamMemeber);
expect(result).toBe(true);
});
it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => {
const members = getMockTeamMembers(5, 5);
const { instance } = setup({
members,
editorsCanAdmin: true,
signedInUser: {
id: signedInUserId,
isGrafanaAdmin: false,
orgRole: OrgRole.Viewer,
},
});
const result = instance.isSignedInUserTeamAdmin();
expect(result).toBe(false);
});
});
});
});
......@@ -2,32 +2,25 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User, teamsPermissionLevels, TeamPermissionLevel, OrgRole } from 'app/types';
import {
loadTeamMembers,
addTeamMember,
removeTeamMember,
setSearchMemberQuery,
updateTeamMember,
} from './state/actions';
import { TeamMember, User, TeamPermissionLevel, OrgRole } from 'app/types';
import { loadTeamMembers, addTeamMember, setSearchMemberQuery } from './state/actions';
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import { contextSrv, User as SignedInUser } from 'app/core/services/context_srv';
import TeamMemberRow from './TeamMemberRow';
export interface Props {
members: TeamMember[];
searchMemberQuery: string;
loadTeamMembers: typeof loadTeamMembers;
addTeamMember: typeof addTeamMember;
removeTeamMember: typeof removeTeamMember;
setSearchMemberQuery: typeof setSearchMemberQuery;
updateTeamMember: typeof updateTeamMember;
syncEnabled: boolean;
editorsCanAdmin?: boolean;
signedInUser?: SignedInUser;
}
export interface State {
......@@ -39,7 +32,6 @@ export class TeamMembers extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = { isAdding: false, newTeamMember: null };
this.renderPermissions = this.renderPermissions.bind(this);
}
componentDidMount() {
......@@ -50,10 +42,6 @@ export class TeamMembers extends PureComponent<Props, State> {
this.props.setSearchMemberQuery(value);
};
onRemoveMember(member: TeamMember) {
this.props.removeTeamMember(member.userId);
}
onToggleAdding = () => {
this.setState({ isAdding: !this.state.isAdding });
};
......@@ -81,65 +69,16 @@ export class TeamMembers extends PureComponent<Props, State> {
);
}
onPermissionChange = (item: SelectOptionItem, member: TeamMember) => {
const permission = item.value;
const updatedTeamMember = { ...member, permission };
this.props.updateTeamMember(updatedTeamMember);
};
private isSignedInUserTeamAdmin = () => {
const { members, editorsCanAdmin } = this.props;
const userInMembers = members.find(m => m.userId === contextSrv.user.id);
const isAdmin = contextSrv.isGrafanaAdmin || contextSrv.hasRole(OrgRole.Admin);
isSignedInUserTeamAdmin = (): boolean => {
const { members, editorsCanAdmin, signedInUser } = this.props;
const userInMembers = members.find(m => m.userId === signedInUser.id);
const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin;
const userIsTeamAdmin = userInMembers && userInMembers.permission === TeamPermissionLevel.Admin;
const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin;
return isSignedInUserTeamAdmin || !editorsCanAdmin;
};
renderPermissions(member: TeamMember) {
const { editorsCanAdmin } = this.props;
const isUserTeamAdmin = this.isSignedInUserTeamAdmin();
const value = teamsPermissionLevels.find(dp => dp.value === member.permission);
return (
<WithFeatureToggle featureToggle={editorsCanAdmin}>
<td>
<div className="gf-form">
{isUserTeamAdmin && (
<Select
isSearchable={false}
options={teamsPermissionLevels}
onChange={item => this.onPermissionChange(item, member)}
className="gf-form-select-box__control--menu-right"
value={value}
/>
)}
{!isUserTeamAdmin && <span>{value.label}</span>}
</div>
</td>
</WithFeatureToggle>
);
}
renderMember(member: TeamMember, syncEnabled: boolean) {
return (
<tr key={member.userId}>
<td className="width-4 text-center">
<img className="filter-table__avatar" src={member.avatarUrl} />
</td>
<td>{member.login}</td>
<td>{member.email}</td>
{this.renderPermissions(member)}
{syncEnabled && this.renderLabels(member.labels)}
<td className="text-right">
<DeleteButton onConfirm={() => this.onRemoveMember(member)} disabled={!this.isSignedInUserTeamAdmin()} />
</td>
</tr>
);
}
render() {
const { isAdding } = this.state;
const { searchMemberQuery, members, syncEnabled, editorsCanAdmin } = this.props;
......@@ -198,7 +137,18 @@ export class TeamMembers extends PureComponent<Props, State> {
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>{members && members.map(member => this.renderMember(member, syncEnabled))}</tbody>
<tbody>
{members &&
members.map(member => (
<TeamMemberRow
key={member.userId}
member={member}
syncEnabled={syncEnabled}
editorsCanAdmin={editorsCanAdmin}
signedInUserIsTeamAdmin={this.isSignedInUserTeamAdmin()}
/>
))}
</tbody>
</table>
</div>
</div>
......@@ -211,15 +161,14 @@ function mapStateToProps(state) {
members: getTeamMembers(state.team),
searchMemberQuery: getSearchMemberQuery(state.team),
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
};
}
const mapDispatchToProps = {
loadTeamMembers,
addTeamMember,
removeTeamMember,
setSearchMemberQuery,
updateTeamMember,
};
export default connect(
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render when feature toggle editorsCanAdmin is turned off should not render permissions 1`] = `
<tr
key="1"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser
</td>
<td>
test@test.com
</td>
<Component
featureToggle={false}
>
<td>
<div
className="gf-form"
>
<Select
autoFocus={false}
backspaceRemovesValue={true}
className="gf-form-select-box__control--menu-right"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={false}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"description": "Is team member",
"label": "Member",
"value": 0,
},
Object {
"description": "Can add/remove permissions, members and delete team.",
"label": "Admin",
"value": 4,
},
]
}
value={
Object {
"description": "Is team member",
"label": "Member",
"value": 0,
}
}
width={null}
/>
</div>
</td>
</Component>
<td
className="text-right"
>
<DeleteButton
disabled={false}
onConfirm={[Function]}
/>
</td>
</tr>
`;
exports[`Render when feature toggle editorsCanAdmin is turned on should render permissions select if user is team admin 1`] = `
<tr
key="1"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser
</td>
<td>
test@test.com
</td>
<Component
featureToggle={true}
>
<td>
<div
className="gf-form"
>
<Select
autoFocus={false}
backspaceRemovesValue={true}
className="gf-form-select-box__control--menu-right"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={false}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"description": "Is team member",
"label": "Member",
"value": 0,
},
Object {
"description": "Can add/remove permissions, members and delete team.",
"label": "Admin",
"value": 4,
},
]
}
value={
Object {
"description": "Is team member",
"label": "Member",
"value": 0,
}
}
width={null}
/>
</div>
</td>
</Component>
<td
className="text-right"
>
<DeleteButton
disabled={false}
onConfirm={[Function]}
/>
</td>
</tr>
`;
exports[`Render when feature toggle editorsCanAdmin is turned on should render span and disable buttons if user is team member 1`] = `
<tr
key="1"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser
</td>
<td>
test@test.com
</td>
<Component
featureToggle={true}
>
<td>
<div
className="gf-form"
>
<span>
Member
</span>
</div>
</td>
</Component>
<td
className="text-right"
>
<DeleteButton
disabled={true}
onConfirm={[Function]}
/>
</td>
</tr>
`;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment