mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: legacy access control cleanup for frontend team pages (#75005)
* clean up legacy access control code for teams * remove editorsCanAdmin config from the frontend * add editorsCanAdmin config option back for the frontend
This commit is contained in:
parent
5549cd7a76
commit
d3f69fd34a
@ -5179,12 +5179,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/features/teams/TeamMemberRow.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/teams/TeamMembers.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/teams/state/reducers.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
|
@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
import { OrgRole, Team } from '../../types';
|
||||
import { Team } from '../../types';
|
||||
|
||||
import { Props, TeamList } from './TeamList';
|
||||
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
||||
@ -30,18 +30,11 @@ const setup = (propOverrides?: object) => {
|
||||
totalPages: 0,
|
||||
page: 0,
|
||||
hasFetched: false,
|
||||
editorsCanAdmin: false,
|
||||
perPage: 10,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
contextSrv.user = props.signedInUser;
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TeamList {...props} />
|
||||
@ -62,11 +55,6 @@ describe('TeamList', () => {
|
||||
teams: getMultipleMockTeams(1),
|
||||
totalCount: 1,
|
||||
hasFetched: true,
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
orgRole: OrgRole.Editor,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none');
|
||||
@ -80,11 +68,6 @@ describe('TeamList', () => {
|
||||
teams: getMultipleMockTeams(1),
|
||||
totalCount: 1,
|
||||
hasFetched: true,
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none');
|
||||
@ -95,7 +78,7 @@ describe('TeamList', () => {
|
||||
it('should call delete team', async () => {
|
||||
const mockDelete = jest.fn();
|
||||
const mockTeam = getMockTeam();
|
||||
jest.spyOn(contextSrv, 'hasAccessInMetadata').mockReturnValue(true);
|
||||
jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockReturnValue(true);
|
||||
setup({ deleteTeam: mockDelete, teams: [mockTeam], totalCount: 1, hasFetched: true });
|
||||
await userEvent.click(screen.getByRole('button', { name: `Delete team ${mockTeam.name}` }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
@ -18,7 +18,6 @@ import {
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
|
||||
|
||||
@ -26,7 +25,6 @@ import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'
|
||||
import { Avatar } from '../admin/Users/Avatar';
|
||||
|
||||
import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions';
|
||||
import { isPermissionTeamAdmin } from './state/selectors';
|
||||
|
||||
type Cell<T extends keyof Team = keyof Team> = CellProps<Team, Team[T]>;
|
||||
export interface OwnProps {}
|
||||
@ -44,8 +42,6 @@ export const TeamList = ({
|
||||
deleteTeam,
|
||||
changeQuery,
|
||||
totalPages,
|
||||
signedInUser,
|
||||
editorsCanAdmin,
|
||||
page,
|
||||
changePage,
|
||||
changeSort,
|
||||
@ -110,16 +106,7 @@ export const TeamList = ({
|
||||
id: 'edit',
|
||||
header: '',
|
||||
cell: ({ row: { original } }: Cell) => {
|
||||
const isTeamAdmin = isPermissionTeamAdmin({
|
||||
permission: original.permission,
|
||||
editorsCanAdmin,
|
||||
signedInUser,
|
||||
});
|
||||
const canReadTeam = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsRead,
|
||||
original,
|
||||
isTeamAdmin
|
||||
);
|
||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
||||
return canReadTeam ? (
|
||||
<a href={`org/teams/edit/${original.id}`} aria-label={`Edit team ${original.name}`}>
|
||||
<Tooltip content={'Edit team'}>
|
||||
@ -133,16 +120,7 @@ export const TeamList = ({
|
||||
id: 'delete',
|
||||
header: '',
|
||||
cell: ({ row: { original } }: Cell) => {
|
||||
const isTeamAdmin = isPermissionTeamAdmin({
|
||||
permission: original.permission,
|
||||
editorsCanAdmin,
|
||||
signedInUser,
|
||||
});
|
||||
const canDelete = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsDelete,
|
||||
original,
|
||||
isTeamAdmin
|
||||
);
|
||||
const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original);
|
||||
|
||||
return (
|
||||
<DeleteButton
|
||||
@ -155,7 +133,7 @@ export const TeamList = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
[displayRolePicker, editorsCanAdmin, roleOptions, signedInUser, deleteTeam]
|
||||
[displayRolePicker, roleOptions, deleteTeam]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -219,8 +197,6 @@ function mapStateToProps(state: StoreState) {
|
||||
noTeams: state.teams.noTeams,
|
||||
totalPages: state.teams.totalPages,
|
||||
hasFetched: state.teams.hasFetched,
|
||||
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
|
||||
signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { TeamPermissionLevel } from '../../types';
|
||||
|
||||
import { TeamMemberRow, Props } from './TeamMemberRow';
|
||||
import { getMockTeamMember } from './__mocks__/teamMocks';
|
||||
|
||||
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);
|
||||
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TeamMemberRow {...props} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render team member labels when sync enabled', () => {
|
||||
const member = getMockTeamMember();
|
||||
member.labels = ['LDAP'];
|
||||
setup({ member, syncEnabled: true });
|
||||
expect(screen.getByText('LDAP')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||
it('should render permissions select if user is team admin', () => {
|
||||
const member = getMockTeamMember();
|
||||
setup({ editorsCanAdmin: true, signedInUserIsTeamAdmin: true, member });
|
||||
expect(screen.getByLabelText(`Select member's ${member.name} permission level`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when feature toggle editorsCanAdmin is turned off', () => {
|
||||
it('should not render permissions', () => {
|
||||
const member = getMockTeamMember();
|
||||
setup({ editorsCanAdmin: false, signedInUserIsTeamAdmin: true, member });
|
||||
expect(screen.queryByLabelText(`Select member's ${member.name} permission level`)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
it('should remove member on remove button click', async () => {
|
||||
const member = getMockTeamMember();
|
||||
const mockRemove = jest.fn();
|
||||
setup({ member, removeTeamMember: mockRemove, editorsCanAdmin: true, signedInUserIsTeamAdmin: true });
|
||||
await userEvent.click(screen.getByRole('button', { name: `Remove team member ${member.name}` }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
expect(mockRemove).toHaveBeenCalledWith(member.userId);
|
||||
});
|
||||
|
||||
it('should update permission for user in team', async () => {
|
||||
const member = getMockTeamMember();
|
||||
const mockUpdate = jest.fn();
|
||||
setup({ member, editorsCanAdmin: true, signedInUserIsTeamAdmin: true, updateTeamMember: mockUpdate });
|
||||
const permission = TeamPermissionLevel.Admin;
|
||||
const expectedTeamMember = { ...member, permission };
|
||||
await userEvent.click(screen.getByLabelText(`Select member's ${member.name} permission level`));
|
||||
await userEvent.click(screen.getByText('Admin'));
|
||||
|
||||
expect(mockUpdate).toHaveBeenCalledWith(expectedTeamMember);
|
||||
});
|
||||
});
|
@ -1,115 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select, DeleteButton } from '@grafana/ui';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
|
||||
import { TeamMember, teamsPermissionLevels, TeamPermissionLevel } from 'app/types';
|
||||
|
||||
import { updateTeamMember, removeTeamMember } from './state/actions';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
removeTeamMember,
|
||||
updateTeamMember,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {
|
||||
member: TeamMember;
|
||||
syncEnabled: boolean;
|
||||
editorsCanAdmin: boolean;
|
||||
signedInUserIsTeamAdmin: boolean;
|
||||
}
|
||||
export type Props = ConnectedProps<typeof connector> & OwnProps;
|
||||
|
||||
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: SelectableValue<TeamPermissionLevel>, member: TeamMember) => {
|
||||
const permission = item.value;
|
||||
const updatedTeamMember: TeamMember = {
|
||||
...member,
|
||||
permission: permission as number,
|
||||
};
|
||||
|
||||
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 className="width-5 team-permissions">
|
||||
{signedInUserIsTeamAdmin ? (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
options={teamsPermissionLevels}
|
||||
onChange={(item) => this.onPermissionChange(item, member)}
|
||||
value={value}
|
||||
width={32}
|
||||
aria-label={`Select member's ${member.name} permission level`}
|
||||
/>
|
||||
) : (
|
||||
<span>{value.label}</span>
|
||||
)}
|
||||
</td>
|
||||
</WithFeatureToggle>
|
||||
);
|
||||
}
|
||||
|
||||
renderLabels(labels: string[]) {
|
||||
if (!labels) {
|
||||
return <td />;
|
||||
}
|
||||
|
||||
return (
|
||||
<td>
|
||||
{labels.map((label) => (
|
||||
<TagBadge key={label} label={label} removeIcon={false} count={0} />
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { member, syncEnabled, signedInUserIsTeamAdmin } = this.props;
|
||||
return (
|
||||
<tr key={member.userId}>
|
||||
<td className="width-4 text-center">
|
||||
<img
|
||||
alt={`Avatar for team member "${member.name}"`}
|
||||
className="filter-table__avatar"
|
||||
src={member.avatarUrl}
|
||||
/>
|
||||
</td>
|
||||
<td>{member.login}</td>
|
||||
<td>{member.email}</td>
|
||||
<td>{member.name}</td>
|
||||
{this.renderPermissions(member)}
|
||||
{syncEnabled && this.renderLabels(member.labels)}
|
||||
<td className="text-right">
|
||||
<DeleteButton
|
||||
aria-label={`Remove team member ${member.name}`}
|
||||
size="sm"
|
||||
disabled={!signedInUserIsTeamAdmin}
|
||||
onConfirm={() => this.onRemoveMember(member)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connector(TeamMemberRow);
|
@ -1,68 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { OrgRole, TeamMember } from '../../types';
|
||||
|
||||
import { Props, TeamMembers } from './TeamMembers';
|
||||
import { getMockTeamMembers } from './__mocks__/teamMocks';
|
||||
import { setSearchMemberQuery } from './state/reducers';
|
||||
|
||||
const signedInUserId = 1;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => ({
|
||||
get: jest.fn().mockResolvedValue([{ userId: 1, login: 'Test' }]),
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
bootData: { navTree: [], user: {} },
|
||||
},
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const store = configureStore();
|
||||
|
||||
const props: Props = {
|
||||
members: [] as TeamMember[],
|
||||
searchMemberQuery: '',
|
||||
setSearchMemberQuery: mockToolkitActionCreator(setSearchMemberQuery),
|
||||
addTeamMember: jest.fn(),
|
||||
syncEnabled: false,
|
||||
editorsCanAdmin: false,
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<TeamMembers {...props} />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('TeamMembers', () => {
|
||||
it('should render team members', async () => {
|
||||
setup({ members: getMockTeamMembers(1, 1) });
|
||||
expect(await screen.findAllByRole('row')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should add user to a team', async () => {
|
||||
const mockAdd = jest.fn();
|
||||
setup({ addTeamMember: mockAdd });
|
||||
await userEvent.type(screen.getByLabelText('User picker'), 'Test{enter}');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Add to team' }));
|
||||
await waitFor(() => expect(mockAdd).toHaveBeenCalledWith(1));
|
||||
});
|
||||
});
|
@ -1,149 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, FilterInput, Label, InlineField } from '@grafana/ui';
|
||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { TeamMember, OrgUser } from 'app/types';
|
||||
|
||||
import TeamMemberRow from './TeamMemberRow';
|
||||
import { addTeamMember } from './state/actions';
|
||||
import { setSearchMemberQuery } from './state/reducers';
|
||||
import { getSearchMemberQuery, isSignedInUserTeamAdmin } from './state/selectors';
|
||||
|
||||
function mapStateToProps(state: any) {
|
||||
return {
|
||||
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 = {
|
||||
addTeamMember,
|
||||
setSearchMemberQuery,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {
|
||||
members: TeamMember[];
|
||||
syncEnabled: boolean;
|
||||
}
|
||||
|
||||
export type Props = ConnectedProps<typeof connector> & OwnProps;
|
||||
|
||||
export interface State {
|
||||
isAdding: boolean;
|
||||
newTeamMember?: SelectableValue<OrgUser['userId']> | null;
|
||||
}
|
||||
|
||||
export class TeamMembers extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newTeamMember: null };
|
||||
}
|
||||
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.props.setSearchMemberQuery(value);
|
||||
};
|
||||
|
||||
onToggleAdding = () => {
|
||||
this.setState({ isAdding: !this.state.isAdding });
|
||||
};
|
||||
|
||||
onUserSelected = (user: SelectableValue<OrgUser['userId']>) => {
|
||||
this.setState({ newTeamMember: user });
|
||||
};
|
||||
|
||||
onAddUserToTeam = async () => {
|
||||
this.props.addTeamMember(this.state.newTeamMember!.id);
|
||||
this.setState({ newTeamMember: null });
|
||||
};
|
||||
|
||||
renderLabels(labels: string[]) {
|
||||
if (!labels) {
|
||||
return <td />;
|
||||
}
|
||||
|
||||
return (
|
||||
<td>
|
||||
{labels.map((label) => (
|
||||
<TagBadge key={label} label={label} removeIcon={false} count={0} />
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isAdding } = this.state;
|
||||
const { searchMemberQuery, members, syncEnabled, editorsCanAdmin, signedInUser } = this.props;
|
||||
const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
<InlineField grow>
|
||||
<FilterInput placeholder="Search members" value={searchMemberQuery} onChange={this.onSearchQueryChange} />
|
||||
</InlineField>
|
||||
<Button className="pull-right" onClick={this.onToggleAdding} disabled={isAdding || !isTeamAdmin}>
|
||||
Add member
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SlideDown in={isAdding}>
|
||||
<div className="cta-form">
|
||||
<CloseButton aria-label="Close 'Add team member' dialogue" onClick={this.onToggleAdding} />
|
||||
<Label htmlFor="user-picker">Add team member</Label>
|
||||
<div className="gf-form-inline">
|
||||
<UserPicker inputId="user-picker" onSelected={this.onUserSelected} className="min-width-30" />
|
||||
{this.state.newTeamMember && (
|
||||
<Button type="submit" onClick={this.onAddUserToTeam}>
|
||||
Add to team
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SlideDown>
|
||||
|
||||
<div className="admin-list-table">
|
||||
<table className="filter-table filter-table--hover form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<WithFeatureToggle featureToggle={editorsCanAdmin}>
|
||||
<th>Permission</th>
|
||||
</WithFeatureToggle>
|
||||
{syncEnabled && <th />}
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{members &&
|
||||
members.map((member) => (
|
||||
<TeamMemberRow
|
||||
key={member.userId}
|
||||
member={member}
|
||||
syncEnabled={syncEnabled}
|
||||
editorsCanAdmin={editorsCanAdmin}
|
||||
signedInUserIsTeamAdmin={isTeamAdmin}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connector(TeamMembers);
|
@ -5,9 +5,8 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { createTheme } from '@grafana/data';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
|
||||
import { OrgRole, Team, TeamMember } from '../../types';
|
||||
import { Team } from '../../types';
|
||||
|
||||
import { Props, TeamPages } from './TeamPages';
|
||||
import { getMockTeam } from './__mocks__/teamMocks';
|
||||
@ -18,9 +17,8 @@ jest.mock('app/core/components/Select/UserPicker', () => {
|
||||
|
||||
jest.mock('app/core/services/context_srv', () => ({
|
||||
contextSrv: {
|
||||
accessControlEnabled: () => false,
|
||||
hasPermissionInMetadata: () => false,
|
||||
hasAccessInMetadata: () => true,
|
||||
accessControlEnabled: () => true,
|
||||
hasPermissionInMetadata: () => true,
|
||||
user: {},
|
||||
},
|
||||
}));
|
||||
@ -36,7 +34,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
stateInfo: '',
|
||||
licenseUrl: '',
|
||||
},
|
||||
featureToggles: { accesscontrol: false },
|
||||
featureToggles: { accesscontrol: true },
|
||||
bootData: { navTree: [], user: {} },
|
||||
buildInfo: {
|
||||
edition: 'Open Source',
|
||||
@ -76,17 +74,9 @@ const setup = (propOverrides?: object) => {
|
||||
pageNav: { text: 'Cool team ' },
|
||||
teamId: 1,
|
||||
loadTeam: jest.fn(),
|
||||
loadTeamMembers: jest.fn(),
|
||||
pageName: 'members',
|
||||
team: {} as Team,
|
||||
members: [] as TeamMember[],
|
||||
editorsCanAdmin: false,
|
||||
theme: createTheme(),
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@ -99,13 +89,6 @@ const setup = (propOverrides?: object) => {
|
||||
};
|
||||
|
||||
describe('TeamPages', () => {
|
||||
it('should render member page if team not empty', async () => {
|
||||
setup({
|
||||
team: getMockTeam(),
|
||||
});
|
||||
expect(await screen.findByRole('button', { name: 'Add member' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render settings and preferences page', async () => {
|
||||
setup({
|
||||
team: getMockTeam(),
|
||||
@ -128,26 +111,4 @@ describe('TeamPages', () => {
|
||||
|
||||
expect(await screen.findByText('Team group sync')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||
it('should render settings page if user is team admin', async () => {
|
||||
setup({
|
||||
team: getMockTeam(),
|
||||
pageName: 'settings',
|
||||
preferences: {
|
||||
homeDashboardUID: 'home-dashboard',
|
||||
theme: 'Default',
|
||||
timezone: 'Default',
|
||||
},
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Admin,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(await screen.findByText('Team settings')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,6 @@ import { includes } from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { featureEnabled } from '@grafana/runtime';
|
||||
import { Themeable2, withTheme2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
@ -14,12 +13,11 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction, StoreState } from 'app/types';
|
||||
|
||||
import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync';
|
||||
import TeamMembers from './TeamMembers';
|
||||
import TeamPermissions from './TeamPermissions';
|
||||
import TeamSettings from './TeamSettings';
|
||||
import { loadTeam, loadTeamMembers } from './state/actions';
|
||||
import { loadTeam } from './state/actions';
|
||||
import { getTeamLoadingNav } from './state/navModel';
|
||||
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
|
||||
import { getTeam } from './state/selectors';
|
||||
|
||||
interface TeamPageRouteParams {
|
||||
id: string;
|
||||
@ -52,22 +50,17 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
|
||||
const pageName = props.match.params.page ?? defaultPage;
|
||||
const teamLoadingNav = getTeamLoadingNav(pageName);
|
||||
const pageNav = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav).main;
|
||||
const members = getTeamMembers(state.team);
|
||||
|
||||
return {
|
||||
pageNav,
|
||||
teamId: teamId,
|
||||
pageName: pageName,
|
||||
team,
|
||||
members,
|
||||
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 = {
|
||||
loadTeam,
|
||||
loadTeamMembers,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
@ -92,10 +85,6 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
const { loadTeam, teamId } = this.props;
|
||||
this.setState({ isLoading: true });
|
||||
const team = await loadTeam(teamId);
|
||||
// With accesscontrol, the TeamPermissions will fetch team members
|
||||
if (!contextSrv.accessControlEnabled()) {
|
||||
await this.props.loadTeamMembers();
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
return team;
|
||||
}
|
||||
@ -118,49 +107,25 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
return text1.toLocaleLowerCase() === text2.toLocaleLowerCase();
|
||||
};
|
||||
|
||||
hideTabsFromNonTeamAdmin = (pageNav: NavModelItem, isSignedInUserTeamAdmin: boolean) => {
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
return pageNav;
|
||||
}
|
||||
|
||||
if (!isSignedInUserTeamAdmin && pageNav && pageNav.children) {
|
||||
pageNav.children
|
||||
.filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members))
|
||||
.map((navItem) => {
|
||||
navItem.hideFromTabs = true;
|
||||
});
|
||||
}
|
||||
|
||||
return pageNav;
|
||||
};
|
||||
|
||||
renderPage(isSignedInUserTeamAdmin: boolean): React.ReactNode {
|
||||
renderPage(): React.ReactNode {
|
||||
const { isSyncEnabled } = this.state;
|
||||
const { members, team } = this.props;
|
||||
const { team } = this.props;
|
||||
const currentPage = this.getCurrentPage();
|
||||
|
||||
const canReadTeam = contextSrv.hasAccessInMetadata(
|
||||
AccessControlAction.ActionTeamsRead,
|
||||
team!,
|
||||
isSignedInUserTeamAdmin
|
||||
);
|
||||
const canReadTeamPermissions = contextSrv.hasAccessInMetadata(
|
||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, team!);
|
||||
const canReadTeamPermissions = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsRead,
|
||||
team!,
|
||||
isSignedInUserTeamAdmin
|
||||
team!
|
||||
);
|
||||
const canWriteTeamPermissions = contextSrv.hasAccessInMetadata(
|
||||
const canWriteTeamPermissions = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsWrite,
|
||||
team!,
|
||||
isSignedInUserTeamAdmin
|
||||
team!
|
||||
);
|
||||
|
||||
switch (currentPage) {
|
||||
case PageTypes.Members:
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
if (canReadTeamPermissions) {
|
||||
return <TeamPermissions team={team!} />;
|
||||
} else {
|
||||
return <TeamMembers syncEnabled={isSyncEnabled} members={members} />;
|
||||
}
|
||||
case PageTypes.Settings:
|
||||
return canReadTeam && <TeamSettings team={team!} />;
|
||||
@ -183,13 +148,12 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { team, pageNav, members, editorsCanAdmin, signedInUser } = this.props;
|
||||
const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
|
||||
const { team, pageNav } = this.props;
|
||||
|
||||
return (
|
||||
<Page navId="teams" pageNav={this.hideTabsFromNonTeamAdmin(pageNav, isTeamAdmin)}>
|
||||
<Page navId="teams" pageNav={pageNav}>
|
||||
<Page.Contents isLoading={this.state.isLoading}>
|
||||
{team && Object.keys(team).length !== 0 && this.renderPage(isTeamAdmin)}
|
||||
{team && Object.keys(team).length !== 0 && this.renderPage()}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
|
@ -30,8 +30,12 @@ export const TeamSettings = ({ team, updateTeam }: Props) => {
|
||||
const [pendingRoles, setPendingRoles] = useState<Role[]>([]);
|
||||
|
||||
const canUpdateRoles =
|
||||
contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) &&
|
||||
contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove);
|
||||
contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesAdd) &&
|
||||
contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesRemove);
|
||||
|
||||
const canListRoles =
|
||||
contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRolesList, team) &&
|
||||
contextSrv.hasPermission(AccessControlAction.ActionRolesList);
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="lg">
|
||||
@ -57,12 +61,12 @@ export const TeamSettings = ({ team, updateTeam }: Props) => {
|
||||
<Input {...register('name', { required: true })} id="name-input" />
|
||||
</Field>
|
||||
|
||||
{contextSrv.licensedAccessControlEnabled() && (
|
||||
{contextSrv.licensedAccessControlEnabled() && canListRoles && (
|
||||
<Field label="Role">
|
||||
<TeamRolePicker
|
||||
teamId={team.id}
|
||||
roleOptions={roleOptions}
|
||||
disabled={false}
|
||||
disabled={!canUpdateRoles}
|
||||
apply={true}
|
||||
onApplyRoles={setPendingRoles}
|
||||
pendingRoles={pendingRoles}
|
||||
|
@ -23,25 +23,6 @@ export const getMockTeam = (i = 1, overrides = {}): Team => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockTeamMembers = (amount: number, teamAdminId: number): TeamMember[] => {
|
||||
const teamMembers: TeamMember[] = [];
|
||||
|
||||
for (let i = 1; i <= amount; i++) {
|
||||
teamMembers.push({
|
||||
userId: i,
|
||||
teamId: 1,
|
||||
avatarUrl: 'some/url/',
|
||||
email: 'test@test.com',
|
||||
name: 'testName',
|
||||
login: `testUser-${i}`,
|
||||
labels: ['label 1', 'label 2'],
|
||||
permission: i === teamAdminId ? TeamPermissionLevel.Admin : TeamPermissionLevel.Member,
|
||||
});
|
||||
}
|
||||
|
||||
return teamMembers;
|
||||
};
|
||||
|
||||
export const getMockTeamMember = (): TeamMember => {
|
||||
return {
|
||||
userId: 1,
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
import { TeamState } from '../../../types';
|
||||
import { getMockTeam } from '../__mocks__/teamMocks';
|
||||
|
||||
import { Team, TeamGroup, TeamState, OrgRole } from '../../../types';
|
||||
import { getMockTeam, getMockTeamMembers } from '../__mocks__/teamMocks';
|
||||
|
||||
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin, Config } from './selectors';
|
||||
import { getTeam } from './selectors';
|
||||
|
||||
describe('Team selectors', () => {
|
||||
describe('Get team', () => {
|
||||
@ -21,111 +19,4 @@ describe('Team selectors', () => {
|
||||
expect(team).toEqual(mockTeam);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get members', () => {
|
||||
const mockTeamMembers = getMockTeamMembers(5, 5);
|
||||
|
||||
it('should return team members', () => {
|
||||
const mockState: TeamState = {
|
||||
team: {} as Team,
|
||||
searchMemberQuery: '',
|
||||
members: mockTeamMembers,
|
||||
groups: [] as TeamGroup[],
|
||||
};
|
||||
|
||||
const members = getTeamMembers(mockState);
|
||||
expect(members).toEqual(mockTeamMembers);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const signedInUserId = 1;
|
||||
|
||||
const setup = (configOverrides?: Partial<Config>) => {
|
||||
const defaultConfig: Config = {
|
||||
editorsCanAdmin: false,
|
||||
members: getMockTeamMembers(5, 5),
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
return { ...defaultConfig, ...configOverrides };
|
||||
};
|
||||
|
||||
describe('isSignedInUserTeamAdmin', () => {
|
||||
describe('when feature toggle editorsCanAdmin is turned off', () => {
|
||||
it('should return true', () => {
|
||||
const config = setup();
|
||||
|
||||
const result = isSignedInUserTeamAdmin(config);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||
it('should return true if signed in user is grafanaAdmin', () => {
|
||||
const config = setup({
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: true,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
});
|
||||
|
||||
const result = isSignedInUserTeamAdmin(config);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if signed in user is org admin', () => {
|
||||
const config = setup({
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Admin,
|
||||
} as User,
|
||||
});
|
||||
|
||||
const result = isSignedInUserTeamAdmin(config);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if signed in user is team admin', () => {
|
||||
const config = setup({
|
||||
members: getMockTeamMembers(5, signedInUserId),
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
});
|
||||
|
||||
const result = isSignedInUserTeamAdmin(config);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if signed in user is not grafanaAdmin, org admin or team admin', () => {
|
||||
const config = setup({
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
});
|
||||
|
||||
const result = isSignedInUserTeamAdmin(config);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
import { Team, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types';
|
||||
import { Team, TeamState } from 'app/types';
|
||||
|
||||
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
||||
export const getTeamGroups = (state: TeamState) => state.groups;
|
||||
|
||||
export const getTeam = (state: TeamState, currentTeamId: any): Team | null => {
|
||||
@ -11,40 +9,3 @@ export const getTeam = (state: TeamState, currentTeamId: any): Team | null => {
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getTeamMembers = (state: TeamState) => {
|
||||
const regex = RegExp(state.searchMemberQuery, 'i');
|
||||
|
||||
return state.members.filter((member) => {
|
||||
return regex.test(member.login) || regex.test(member.email) || regex.test(member.name);
|
||||
});
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
members: TeamMember[];
|
||||
editorsCanAdmin: boolean;
|
||||
signedInUser: User;
|
||||
}
|
||||
|
||||
export const isSignedInUserTeamAdmin = (config: Config): boolean => {
|
||||
const { members, signedInUser, editorsCanAdmin } = config;
|
||||
const userInMembers = members.find((m) => m.userId === signedInUser.id);
|
||||
const permission = userInMembers ? userInMembers.permission : TeamPermissionLevel.Member;
|
||||
|
||||
return isPermissionTeamAdmin({ permission, signedInUser, editorsCanAdmin });
|
||||
};
|
||||
|
||||
export interface PermissionConfig {
|
||||
permission: TeamPermissionLevel;
|
||||
editorsCanAdmin: boolean;
|
||||
signedInUser: User;
|
||||
}
|
||||
|
||||
export const isPermissionTeamAdmin = (config: PermissionConfig): boolean => {
|
||||
const { permission, signedInUser, editorsCanAdmin } = config;
|
||||
const isAdmin = signedInUser.isGrafanaAdmin || signedInUser.orgRole === OrgRole.Admin;
|
||||
const userIsTeamAdmin = permission === TeamPermissionLevel.Admin;
|
||||
const isSignedInUserTeamAdmin = isAdmin || userIsTeamAdmin;
|
||||
|
||||
return isSignedInUserTeamAdmin || !editorsCanAdmin;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user