mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge master
This commit is contained in:
@@ -4,13 +4,16 @@ jest.mock('app/core/store', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardExporter } from './DashboardExporter';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
describe('given dashboard with repeated panels', () => {
|
||||
let dash, exported;
|
||||
let dash: any, exported: any;
|
||||
|
||||
beforeEach(done => {
|
||||
dash = {
|
||||
@@ -89,25 +92,25 @@ describe('given dashboard with repeated panels', () => {
|
||||
config.buildInfo.version = '3.0.2';
|
||||
|
||||
//Stubs test function calls
|
||||
const datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };
|
||||
const datasourceSrvStub = ({ get: jest.fn(arg => getStub(arg)) } as any) as DatasourceSrv;
|
||||
|
||||
config.panels['graph'] = {
|
||||
id: 'graph',
|
||||
name: 'Graph',
|
||||
info: { version: '1.1.0' },
|
||||
};
|
||||
} as PanelPlugin;
|
||||
|
||||
config.panels['table'] = {
|
||||
id: 'table',
|
||||
name: 'Table',
|
||||
info: { version: '1.1.1' },
|
||||
};
|
||||
} as PanelPlugin;
|
||||
|
||||
config.panels['heatmap'] = {
|
||||
id: 'heatmap',
|
||||
name: 'Heatmap',
|
||||
info: { version: '1.1.2' },
|
||||
};
|
||||
} as PanelPlugin;
|
||||
|
||||
dash = new DashboardModel(dash, {});
|
||||
const exporter = new DashboardExporter(datasourceSrvStub);
|
||||
@@ -213,7 +216,7 @@ describe('given dashboard with repeated panels', () => {
|
||||
});
|
||||
|
||||
// Stub responses
|
||||
const stubs = [];
|
||||
const stubs: { [key: string]: {} } = {};
|
||||
stubs['gfdb'] = {
|
||||
name: 'gfdb',
|
||||
meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
|
||||
@@ -249,6 +252,6 @@ stubs['-- Grafana --'] = {
|
||||
},
|
||||
};
|
||||
|
||||
function getStub(arg) {
|
||||
function getStub(arg: string) {
|
||||
return Promise.resolve(stubs[arg || 'gfdb']);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,42 @@
|
||||
import config from 'app/core/config';
|
||||
// @ts-ignore
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import DatasourceSrv from 'app/features/plugins/datasource_srv';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
|
||||
interface Input {
|
||||
name: string;
|
||||
type: string;
|
||||
label: string;
|
||||
value: any;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Requires {
|
||||
[key: string]: {
|
||||
type: string;
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DataSources {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: string;
|
||||
pluginId: string;
|
||||
pluginName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class DashboardExporter {
|
||||
constructor(private datasourceSrv) {}
|
||||
constructor(private datasourceSrv: DatasourceSrv) {}
|
||||
|
||||
makeExportable(dashboard: DashboardModel) {
|
||||
// clean up repeated rows and panels,
|
||||
@@ -18,19 +51,19 @@ export class DashboardExporter {
|
||||
// undo repeat cleanup
|
||||
dashboard.processRepeats();
|
||||
|
||||
const inputs = [];
|
||||
const requires = {};
|
||||
const datasources = {};
|
||||
const promises = [];
|
||||
const variableLookup: any = {};
|
||||
const inputs: Input[] = [];
|
||||
const requires: Requires = {};
|
||||
const datasources: DataSources = {};
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const variableLookup: { [key: string]: any } = {};
|
||||
|
||||
for (const variable of saveModel.templating.list) {
|
||||
variableLookup[variable.name] = variable;
|
||||
}
|
||||
|
||||
const templateizeDatasourceUsage = obj => {
|
||||
let datasource = obj.datasource;
|
||||
let datasourceVariable = null;
|
||||
const templateizeDatasourceUsage = (obj: any) => {
|
||||
let datasource: string = obj.datasource;
|
||||
let datasourceVariable: any = null;
|
||||
|
||||
// ignore data source properties that contain a variable
|
||||
if (datasource && datasource.indexOf('$') === 0) {
|
||||
@@ -74,7 +107,7 @@ export class DashboardExporter {
|
||||
);
|
||||
};
|
||||
|
||||
const processPanel = panel => {
|
||||
const processPanel = (panel: PanelModel) => {
|
||||
if (panel.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(panel);
|
||||
}
|
||||
@@ -87,7 +120,7 @@ export class DashboardExporter {
|
||||
}
|
||||
}
|
||||
|
||||
const panelDef = config.panels[panel.type];
|
||||
const panelDef: PanelPlugin = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
@@ -135,7 +168,7 @@ export class DashboardExporter {
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
_.each(datasources, (value, key) => {
|
||||
_.each(datasources, (value: any) => {
|
||||
inputs.push(value);
|
||||
});
|
||||
|
||||
@@ -160,7 +193,7 @@ export class DashboardExporter {
|
||||
}
|
||||
|
||||
// make inputs and requires a top thing
|
||||
const newObj = {};
|
||||
const newObj: { [key: string]: {} } = {};
|
||||
newObj['__inputs'] = inputs;
|
||||
newObj['__requires'] = _.sortBy(requires, ['id']);
|
||||
|
||||
|
||||
@@ -5,17 +5,12 @@ import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
|
||||
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
|
||||
|
||||
// Components
|
||||
import { Switch } from '@grafana/ui';
|
||||
import { Input } from 'app/core/components/Form';
|
||||
import { EventsWithValidation } from 'app/core/components/Form/Input';
|
||||
import { InputStatus } from 'app/core/components/Form/Input';
|
||||
import { DataSourceSelectItem, EventsWithValidation, Input, InputStatus, Switch, ValidationEvents } from '@grafana/ui';
|
||||
import { DataSourceOption } from './DataSourceOption';
|
||||
import { FormLabel } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DataSourceSelectItem } from '@grafana/ui/src/types';
|
||||
import { ValidationEvents } from 'app/types';
|
||||
import { PanelModel } from '../state';
|
||||
|
||||
const timeRangeValidationEvents: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
|
||||
@@ -3,9 +3,10 @@ import { PanelModel } from './PanelModel';
|
||||
describe('PanelModel', () => {
|
||||
describe('when creating new panel model', () => {
|
||||
let model;
|
||||
let modelJson;
|
||||
|
||||
beforeEach(() => {
|
||||
model = new PanelModel({
|
||||
modelJson = {
|
||||
type: 'table',
|
||||
showColumns: true,
|
||||
targets: [{ refId: 'A' }, { noRefId: true }],
|
||||
@@ -23,7 +24,8 @@ describe('PanelModel', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
model = new PanelModel(modelJson);
|
||||
});
|
||||
|
||||
it('should apply defaults', () => {
|
||||
@@ -38,6 +40,15 @@ describe('PanelModel', () => {
|
||||
expect(model.targets[1].refId).toBe('B');
|
||||
});
|
||||
|
||||
it("shouldn't break panel with non-array targets", () => {
|
||||
modelJson.targets = {
|
||||
0: { refId: 'A' },
|
||||
foo: { bar: 'baz' },
|
||||
};
|
||||
model = new PanelModel(modelJson);
|
||||
expect(model.targets[0].refId).toBe('A');
|
||||
});
|
||||
|
||||
it('getSaveModel should remove defaults', () => {
|
||||
const saveModel = model.getSaveModel();
|
||||
expect(saveModel.gridPos).toBe(undefined);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Types
|
||||
// Utils
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
|
||||
// Types
|
||||
import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui';
|
||||
import { TableData } from '@grafana/ui/src';
|
||||
|
||||
@@ -125,10 +128,10 @@ export class PanelModel {
|
||||
}
|
||||
|
||||
ensureQueryIds() {
|
||||
if (this.targets) {
|
||||
if (this.targets && _.isArray(this.targets)) {
|
||||
for (const query of this.targets) {
|
||||
if (!query.refId) {
|
||||
query.refId = this.getNextQueryLetter();
|
||||
query.refId = getNextRefIdChar(this.targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,20 +269,10 @@ export class PanelModel {
|
||||
|
||||
addQuery(query?: Partial<DataQuery>) {
|
||||
query = query || { refId: 'A' };
|
||||
query.refId = this.getNextQueryLetter();
|
||||
query.refId = getNextRefIdChar(this.targets);
|
||||
this.targets.push(query as DataQuery);
|
||||
}
|
||||
|
||||
getNextQueryLetter(): string {
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return _.find(letters, refId => {
|
||||
return _.every(this.targets, other => {
|
||||
return other.refId !== refId;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
changeQuery(query: DataQuery, index: number) {
|
||||
// ensure refId is maintained
|
||||
query.refId = this.targets[index].refId;
|
||||
|
||||
@@ -60,7 +60,6 @@ import {
|
||||
splitCloseAction,
|
||||
splitOpenAction,
|
||||
addQueryRowAction,
|
||||
AddQueryRowPayload,
|
||||
toggleGraphAction,
|
||||
toggleLogsAction,
|
||||
toggleTableAction,
|
||||
@@ -87,9 +86,12 @@ const updateExploreUIState = (exploreId, uiStateFragment: Partial<ExploreUIState
|
||||
/**
|
||||
* Adds a query row after the row with the given index.
|
||||
*/
|
||||
export function addQueryRow(exploreId: ExploreId, index: number): ActionOf<AddQueryRowPayload> {
|
||||
const query = generateEmptyQuery(index + 1);
|
||||
return addQueryRowAction({ exploreId, index, query });
|
||||
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
|
||||
|
||||
dispatch(addQueryRowAction({ exploreId, index, query }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,10 +128,10 @@ export function changeQuery(
|
||||
index: number,
|
||||
override: boolean
|
||||
): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
return (dispatch, getState) => {
|
||||
// Null query means reset
|
||||
if (query === null) {
|
||||
query = { ...generateEmptyQuery(index) };
|
||||
query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
|
||||
}
|
||||
|
||||
dispatch(changeQueryAction({ exploreId, query, index, override }));
|
||||
@@ -287,7 +289,7 @@ export function importQueries(
|
||||
|
||||
const nextQueries = importedQueries.map((q, i) => ({
|
||||
...q,
|
||||
...generateEmptyQuery(i),
|
||||
...generateEmptyQuery(queries),
|
||||
}));
|
||||
|
||||
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
|
||||
@@ -629,9 +631,9 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes
|
||||
* Use this action for clicks on query examples. Triggers a query run.
|
||||
*/
|
||||
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
return (dispatch, getState) => {
|
||||
// Inject react keys into query objects
|
||||
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
|
||||
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
|
||||
dispatch(setQueriesAction({ exploreId, queries }));
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
|
||||
@@ -127,7 +127,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
const { query, index } = action.payload;
|
||||
|
||||
// Override path: queries are completely reset
|
||||
const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
|
||||
const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) };
|
||||
const nextQueries = [...queries];
|
||||
nextQueries[index] = nextQuery;
|
||||
|
||||
@@ -267,7 +267,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
// Modify all queries
|
||||
nextQueries = queries.map((query, i) => ({
|
||||
...modifier({ ...query }, modification),
|
||||
...generateEmptyQuery(i),
|
||||
...generateEmptyQuery(state.queries),
|
||||
}));
|
||||
// Discard all ongoing transactions
|
||||
nextQueryTransactions = [];
|
||||
@@ -276,7 +276,9 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
nextQueries = queries.map((query, i) => {
|
||||
// Synchronize all queries with local query cache to ensure consistency
|
||||
// TODO still needed?
|
||||
return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
|
||||
return i === index
|
||||
? { ...modifier({ ...query }, modification), ...generateEmptyQuery(state.queries) }
|
||||
: query;
|
||||
});
|
||||
nextQueryTransactions = queryTransactions
|
||||
// Consume the hint corresponding to the action
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Props, TeamList } from './TeamList';
|
||||
import { NavModel, Team } from '../../types';
|
||||
import { NavModel, Team, OrgRole } from '../../types';
|
||||
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@@ -21,6 +22,11 @@ const setup = (propOverrides?: object) => {
|
||||
searchQuery: '',
|
||||
teamsCount: 0,
|
||||
hasFetched: false,
|
||||
editorsCanAdmin: false,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@@ -49,6 +55,42 @@ describe('Render', () => {
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||
describe('and signedin user is not viewer', () => {
|
||||
it('should enable the new team button', () => {
|
||||
const { wrapper } = setup({
|
||||
teams: getMultipleMockTeams(1),
|
||||
teamsCount: 1,
|
||||
hasFetched: true,
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
orgRole: OrgRole.Editor,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and signedin user is a viewer', () => {
|
||||
it('should disable the new team button', () => {
|
||||
const { wrapper } = setup({
|
||||
teams: getMultipleMockTeams(1),
|
||||
teamsCount: 1,
|
||||
hasFetched: true,
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Life cycle', () => {
|
||||
|
||||
@@ -4,11 +4,13 @@ import { hot } from 'react-hot-loader';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { NavModel, Team } from 'app/types';
|
||||
import { NavModel, Team, OrgRole } from 'app/types';
|
||||
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
|
||||
import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
|
||||
import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@@ -19,6 +21,8 @@ export interface Props {
|
||||
loadTeams: typeof loadTeams;
|
||||
deleteTeam: typeof deleteTeam;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
editorsCanAdmin?: boolean;
|
||||
signedInUser?: User;
|
||||
}
|
||||
|
||||
export class TeamList extends PureComponent<Props, any> {
|
||||
@@ -39,7 +43,10 @@ export class TeamList extends PureComponent<Props, any> {
|
||||
};
|
||||
|
||||
renderTeam(team: Team) {
|
||||
const { editorsCanAdmin, signedInUser } = this.props;
|
||||
const permission = team.permission;
|
||||
const teamUrl = `org/teams/edit/${team.id}`;
|
||||
const canDelete = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser });
|
||||
|
||||
return (
|
||||
<tr key={team.id}>
|
||||
@@ -58,7 +65,7 @@ export class TeamList extends PureComponent<Props, any> {
|
||||
<a href={teamUrl}>{team.memberCount}</a>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirm={() => this.deleteTeam(team)} />
|
||||
<DeleteButton onConfirm={() => this.deleteTeam(team)} disabled={!canDelete} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -84,7 +91,10 @@ export class TeamList extends PureComponent<Props, any> {
|
||||
}
|
||||
|
||||
renderTeamList() {
|
||||
const { teams, searchQuery } = this.props;
|
||||
const { teams, searchQuery, editorsCanAdmin, signedInUser } = this.props;
|
||||
const isCanAdminAndViewer = editorsCanAdmin && signedInUser.orgRole === OrgRole.Viewer;
|
||||
const disabledClass = isCanAdminAndViewer ? ' disabled' : '';
|
||||
const newTeamHref = isCanAdminAndViewer ? '#' : 'org/teams/new';
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -101,7 +111,7 @@ export class TeamList extends PureComponent<Props, any> {
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
|
||||
<a className="btn btn-primary" href="org/teams/new">
|
||||
<a className={`btn btn-primary${disabledClass}`} href={newTeamHref}>
|
||||
New team
|
||||
</a>
|
||||
</div>
|
||||
@@ -152,6 +162,8 @@ function mapStateToProps(state) {
|
||||
searchQuery: getSearchQuery(state.teams),
|
||||
teamsCount: getTeamsCount(state.teams),
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
90
public/app/features/teams/TeamMemberRow.test.tsx
Normal file
90
public/app/features/teams/TeamMemberRow.test.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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', () => {
|
||||
it('should render team members when sync enabled', () => {
|
||||
const member = getMockTeamMember();
|
||||
member.labels = ['LDAP'];
|
||||
const { wrapper } = setup({ member, syncEnabled: true });
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
106
public/app/features/teams/TeamMemberRow.tsx
Normal file
106
public/app/features/teams/TeamMemberRow.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
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 className="width-5 team-permissions">
|
||||
<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);
|
||||
@@ -1,18 +1,25 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TeamMembers, Props, State } from './TeamMembers';
|
||||
import { TeamMember } from '../../types';
|
||||
import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
|
||||
import { TeamMember, OrgRole } from '../../types';
|
||||
import { getMockTeamMembers } from './__mocks__/teamMocks';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
|
||||
const signedInUserId = 1;
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
members: [] as TeamMember[],
|
||||
searchMemberQuery: '',
|
||||
setSearchMemberQuery: jest.fn(),
|
||||
loadTeamMembers: jest.fn(),
|
||||
addTeamMember: jest.fn(),
|
||||
removeTeamMember: jest.fn(),
|
||||
syncEnabled: false,
|
||||
editorsCanAdmin: false,
|
||||
signedInUser: {
|
||||
id: signedInUserId,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@@ -28,24 +35,13 @@ const setup = (propOverrides?: object) => {
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
const { wrapper } = setup({});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render team members', () => {
|
||||
const { wrapper } = setup({
|
||||
members: getMockTeamMembers(5),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render team members when sync enabled', () => {
|
||||
const { wrapper } = setup({
|
||||
members: getMockTeamMembers(5),
|
||||
syncEnabled: true,
|
||||
});
|
||||
const { wrapper } = setup({ members: getMockTeamMembers(5, 5) });
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -54,7 +50,7 @@ describe('Render', () => {
|
||||
describe('Functions', () => {
|
||||
describe('on search member query change', () => {
|
||||
it('it should call setSearchMemberQuery', () => {
|
||||
const { instance } = setup();
|
||||
const { instance } = setup({});
|
||||
|
||||
instance.onSearchQueryChange('member');
|
||||
|
||||
@@ -62,17 +58,8 @@ 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 { wrapper, instance } = setup({});
|
||||
const state = wrapper.state() as State;
|
||||
|
||||
state.newTeamMember = {
|
||||
|
||||
@@ -2,21 +2,24 @@ 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 } from '@grafana/ui';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { TeamMember, User } from 'app/types';
|
||||
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
|
||||
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
|
||||
import { addTeamMember, setSearchMemberQuery } from './state/actions';
|
||||
import { getSearchMemberQuery, isSignedInUserTeamAdmin } 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, 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;
|
||||
syncEnabled: boolean;
|
||||
editorsCanAdmin?: boolean;
|
||||
signedInUser?: SignedInUser;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -30,18 +33,10 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
this.state = { isAdding: false, newTeamMember: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadTeamMembers();
|
||||
}
|
||||
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.props.setSearchMemberQuery(value);
|
||||
};
|
||||
|
||||
onRemoveMember(member: TeamMember) {
|
||||
this.props.removeTeamMember(member.userId);
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
this.setState({ isAdding: !this.state.isAdding });
|
||||
};
|
||||
@@ -69,25 +64,11 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
{syncEnabled && this.renderLabels(member.labels)}
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirm={() => this.onRemoveMember(member)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isAdding } = this.state;
|
||||
const { searchMemberQuery, members, syncEnabled } = this.props;
|
||||
const { searchMemberQuery, members, syncEnabled, editorsCanAdmin, signedInUser } = this.props;
|
||||
const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-action-bar">
|
||||
@@ -103,7 +84,11 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
|
||||
<button className="btn btn-primary pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
||||
<button
|
||||
className="btn btn-primary pull-right"
|
||||
onClick={this.onToggleAdding}
|
||||
disabled={isAdding || !isTeamAdmin}
|
||||
>
|
||||
Add member
|
||||
</button>
|
||||
</div>
|
||||
@@ -132,11 +117,25 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
<th />
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<WithFeatureToggle featureToggle={editorsCanAdmin}>
|
||||
<th>Permission</th>
|
||||
</WithFeatureToggle>
|
||||
{syncEnabled && <th />}
|
||||
<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={isTeamAdmin}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,15 +145,14 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
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,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TeamPages, Props } from './TeamPages';
|
||||
import { NavModel, Team } from '../../types';
|
||||
import { NavModel, Team, TeamMember, OrgRole } from '../../types';
|
||||
import { getMockTeam } from './__mocks__/teamMocks';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
|
||||
jest.mock('app/core/config', () => ({
|
||||
buildInfo: { isEnterprise: true },
|
||||
@@ -13,8 +14,16 @@ const setup = (propOverrides?: object) => {
|
||||
navModel: {} as NavModel,
|
||||
teamId: 1,
|
||||
loadTeam: jest.fn(),
|
||||
loadTeamMembers: jest.fn(),
|
||||
pageName: 'members',
|
||||
team: {} as Team,
|
||||
members: [] as TeamMember[],
|
||||
editorsCanAdmin: false,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@@ -65,4 +74,46 @@ describe('Render', () => {
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('when feature toggle editorsCanAdmin is turned on', () => {
|
||||
it('should render settings page if user is team admin', () => {
|
||||
const { wrapper } = setup({
|
||||
team: getMockTeam(),
|
||||
pageName: 'settings',
|
||||
preferences: {
|
||||
homeDashboardId: 1,
|
||||
theme: 'Default',
|
||||
timezone: 'Default',
|
||||
},
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Admin,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not render settings page if user is team member', () => {
|
||||
const { wrapper } = setup({
|
||||
team: getMockTeam(),
|
||||
pageName: 'settings',
|
||||
preferences: {
|
||||
homeDashboardId: 1,
|
||||
theme: 'Default',
|
||||
timezone: 'Default',
|
||||
},
|
||||
editorsCanAdmin: true,
|
||||
signedInUser: {
|
||||
id: 1,
|
||||
isGrafanaAdmin: false,
|
||||
orgRole: OrgRole.Viewer,
|
||||
} as User,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,19 +7,24 @@ import Page from 'app/core/components/Page/Page';
|
||||
import TeamMembers from './TeamMembers';
|
||||
import TeamSettings from './TeamSettings';
|
||||
import TeamGroupSync from './TeamGroupSync';
|
||||
import { NavModel, Team } from 'app/types';
|
||||
import { loadTeam } from './state/actions';
|
||||
import { getTeam } from './state/selectors';
|
||||
import { NavModel, Team, TeamMember } from 'app/types';
|
||||
import { loadTeam, loadTeamMembers } from './state/actions';
|
||||
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
|
||||
import { getTeamLoadingNav } from './state/navModel';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
|
||||
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||
|
||||
export interface Props {
|
||||
team: Team;
|
||||
loadTeam: typeof loadTeam;
|
||||
loadTeamMembers: typeof loadTeamMembers;
|
||||
teamId: number;
|
||||
pageName: string;
|
||||
navModel: NavModel;
|
||||
members?: TeamMember[];
|
||||
editorsCanAdmin?: boolean;
|
||||
signedInUser?: User;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -51,6 +56,7 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
const { loadTeam, teamId } = this.props;
|
||||
this.setState({ isLoading: true });
|
||||
const team = await loadTeam(teamId);
|
||||
await this.props.loadTeamMembers();
|
||||
this.setState({ isLoading: false });
|
||||
return team;
|
||||
}
|
||||
@@ -61,30 +67,56 @@ export class TeamPages extends PureComponent<Props, State> {
|
||||
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
textsAreEqual = (text1: string, text2: string) => {
|
||||
if (!text1 && !text2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!text1 || !text2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return text1.toLocaleLowerCase() === text2.toLocaleLowerCase();
|
||||
};
|
||||
|
||||
hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => {
|
||||
if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) {
|
||||
navModel.main.children
|
||||
.filter(navItem => !this.textsAreEqual(navItem.text, PageTypes.Members))
|
||||
.map(navItem => {
|
||||
navItem.hideFromTabs = true;
|
||||
});
|
||||
}
|
||||
|
||||
return navModel;
|
||||
};
|
||||
|
||||
renderPage(isSignedInUserTeamAdmin: boolean) {
|
||||
const { isSyncEnabled } = this.state;
|
||||
const { members } = this.props;
|
||||
const currentPage = this.getCurrentPage();
|
||||
|
||||
switch (currentPage) {
|
||||
case PageTypes.Members:
|
||||
return <TeamMembers syncEnabled={isSyncEnabled} />;
|
||||
return <TeamMembers syncEnabled={isSyncEnabled} members={members} />;
|
||||
|
||||
case PageTypes.Settings:
|
||||
return <TeamSettings />;
|
||||
return isSignedInUserTeamAdmin && <TeamSettings />;
|
||||
case PageTypes.GroupSync:
|
||||
return isSyncEnabled && <TeamGroupSync />;
|
||||
return isSignedInUserTeamAdmin && isSyncEnabled && <TeamGroupSync />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { team, navModel } = this.props;
|
||||
const { team, navModel, members, editorsCanAdmin, signedInUser } = this.props;
|
||||
const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser });
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page navModel={this.hideTabsFromNonTeamAdmin(navModel, isTeamAdmin)}>
|
||||
<Page.Contents isLoading={this.state.isLoading}>
|
||||
{team && Object.keys(team).length !== 0 && this.renderPage()}
|
||||
{team && Object.keys(team).length !== 0 && this.renderPage(isTeamAdmin)}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
@@ -95,17 +127,24 @@ function mapStateToProps(state) {
|
||||
const teamId = getRouteParamsId(state.location);
|
||||
const pageName = getRouteParamsPage(state.location) || 'members';
|
||||
const teamLoadingNav = getTeamLoadingNav(pageName);
|
||||
const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav);
|
||||
const team = getTeam(state.team, teamId);
|
||||
const members = getTeamMembers(state.team);
|
||||
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav),
|
||||
navModel,
|
||||
teamId: teamId,
|
||||
pageName: pageName,
|
||||
team: getTeam(state.team, teamId),
|
||||
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,
|
||||
};
|
||||
|
||||
export default hot(module)(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Team, TeamGroup, TeamMember } from 'app/types';
|
||||
import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types';
|
||||
|
||||
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
||||
const teams: Team[] = [];
|
||||
@@ -9,6 +9,7 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
||||
avatarUrl: 'some/url/',
|
||||
email: `test-${i}@test.com`,
|
||||
memberCount: i,
|
||||
permission: TeamPermissionLevel.Member,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,10 +23,11 @@ export const getMockTeam = (): Team => {
|
||||
avatarUrl: 'some/url/',
|
||||
email: 'test@test.com',
|
||||
memberCount: 1,
|
||||
permission: TeamPermissionLevel.Member,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockTeamMembers = (amount: number): TeamMember[] => {
|
||||
export const getMockTeamMembers = (amount: number, teamAdminId: number): TeamMember[] => {
|
||||
const teamMembers: TeamMember[] = [];
|
||||
|
||||
for (let i = 1; i <= amount; i++) {
|
||||
@@ -36,6 +38,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => {
|
||||
email: 'test@test.com',
|
||||
login: `testUser-${i}`,
|
||||
labels: ['label 1', 'label 2'],
|
||||
permission: i === teamAdminId ? TeamPermissionLevel.Admin : TeamPermissionLevel.Member,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,6 +53,7 @@ export const getMockTeamMember = (): TeamMember => {
|
||||
email: 'test@test.com',
|
||||
login: 'testUser',
|
||||
labels: [],
|
||||
permission: TeamPermissionLevel.Member,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={false}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
@@ -183,6 +184,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={false}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
@@ -233,6 +235,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={false}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
@@ -283,6 +286,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={false}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
@@ -333,6 +337,259 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={false}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</PageContents>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is a viewer should disable the new team button 1`] = `
|
||||
<Page
|
||||
navModel={
|
||||
Object {
|
||||
"main": Object {
|
||||
"text": "Configuration",
|
||||
},
|
||||
"node": Object {
|
||||
"text": "Team List",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<PageContents
|
||||
isLoading={false}
|
||||
>
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search teams"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="btn btn-primary disabled"
|
||||
href="#"
|
||||
>
|
||||
New team
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="admin-list-table"
|
||||
>
|
||||
<table
|
||||
className="filter-table filter-table--hover form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Members
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "1%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
test-1
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
test-1@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
1
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={true}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</PageContents>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
exports[`Render when feature toggle editorsCanAdmin is turned on and signedin user is not viewer should enable the new team button 1`] = `
|
||||
<Page
|
||||
navModel={
|
||||
Object {
|
||||
"main": Object {
|
||||
"text": "Configuration",
|
||||
},
|
||||
"node": Object {
|
||||
"text": "Team List",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<PageContents
|
||||
isLoading={false}
|
||||
>
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search teams"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href="org/teams/new"
|
||||
>
|
||||
New team
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="admin-list-table"
|
||||
>
|
||||
<table
|
||||
className="filter-table filter-table--hover form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Members
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "1%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
test-1
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
test-1@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="link-td"
|
||||
>
|
||||
<a
|
||||
href="org/teams/edit/1"
|
||||
>
|
||||
1
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={true}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render team members when sync enabled 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
|
||||
className="width-5 team-permissions"
|
||||
>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<span>
|
||||
Member
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</Component>
|
||||
<td>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="LDAP"
|
||||
label="LDAP"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={true}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
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
|
||||
className="width-5 team-permissions"
|
||||
>
|
||||
<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
|
||||
className="width-5 team-permissions"
|
||||
>
|
||||
<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
|
||||
className="width-5 team-permissions"
|
||||
>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<span>
|
||||
Member
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</Component>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
disabled={true}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -69,6 +69,13 @@ exports[`Render should render component 1`] = `
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<Component
|
||||
featureToggle={false}
|
||||
>
|
||||
<th>
|
||||
Permission
|
||||
</th>
|
||||
</Component>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
@@ -153,6 +160,13 @@ exports[`Render should render team members 1`] = `
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<Component
|
||||
featureToggle={false}
|
||||
>
|
||||
<th>
|
||||
Permission
|
||||
</th>
|
||||
</Component>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
@@ -163,422 +177,106 @@ exports[`Render should render team members 1`] = `
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
<Connect(TeamMemberRow)
|
||||
editorsCanAdmin={false}
|
||||
key="1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-1
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="2"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-2
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="3"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-3
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="4"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-4
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="5"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-5
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render team members when sync enabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search members"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary pull-right"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Add member
|
||||
</button>
|
||||
</div>
|
||||
<Component
|
||||
in={false}
|
||||
>
|
||||
<div
|
||||
className="cta-form"
|
||||
>
|
||||
<button
|
||||
className="cta-form__close btn btn-transparent"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-close"
|
||||
/>
|
||||
</button>
|
||||
<h5>
|
||||
Add team member
|
||||
</h5>
|
||||
<div
|
||||
className="gf-form-inline"
|
||||
>
|
||||
<UserPicker
|
||||
className="min-width-30"
|
||||
onSelected={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
<div
|
||||
className="admin-list-table"
|
||||
>
|
||||
<table
|
||||
className="filter-table filter-table--hover form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th />
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "1%",
|
||||
}
|
||||
member={
|
||||
Object {
|
||||
"avatarUrl": "some/url/",
|
||||
"email": "test@test.com",
|
||||
"labels": Array [
|
||||
"label 1",
|
||||
"label 2",
|
||||
],
|
||||
"login": "testUser-1",
|
||||
"permission": 0,
|
||||
"teamId": 1,
|
||||
"userId": 1,
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-1
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 1"
|
||||
label="label 1"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 2"
|
||||
label="label 2"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
}
|
||||
signedInUserIsTeamAdmin={true}
|
||||
syncEnabled={false}
|
||||
/>
|
||||
<Connect(TeamMemberRow)
|
||||
editorsCanAdmin={false}
|
||||
key="2"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-2
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 1"
|
||||
label="label 1"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 2"
|
||||
label="label 2"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
member={
|
||||
Object {
|
||||
"avatarUrl": "some/url/",
|
||||
"email": "test@test.com",
|
||||
"labels": Array [
|
||||
"label 1",
|
||||
"label 2",
|
||||
],
|
||||
"login": "testUser-2",
|
||||
"permission": 0,
|
||||
"teamId": 1,
|
||||
"userId": 2,
|
||||
}
|
||||
}
|
||||
signedInUserIsTeamAdmin={true}
|
||||
syncEnabled={false}
|
||||
/>
|
||||
<Connect(TeamMemberRow)
|
||||
editorsCanAdmin={false}
|
||||
key="3"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-3
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 1"
|
||||
label="label 1"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 2"
|
||||
label="label 2"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
member={
|
||||
Object {
|
||||
"avatarUrl": "some/url/",
|
||||
"email": "test@test.com",
|
||||
"labels": Array [
|
||||
"label 1",
|
||||
"label 2",
|
||||
],
|
||||
"login": "testUser-3",
|
||||
"permission": 0,
|
||||
"teamId": 1,
|
||||
"userId": 3,
|
||||
}
|
||||
}
|
||||
signedInUserIsTeamAdmin={true}
|
||||
syncEnabled={false}
|
||||
/>
|
||||
<Connect(TeamMemberRow)
|
||||
editorsCanAdmin={false}
|
||||
key="4"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-4
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 1"
|
||||
label="label 1"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 2"
|
||||
label="label 2"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
member={
|
||||
Object {
|
||||
"avatarUrl": "some/url/",
|
||||
"email": "test@test.com",
|
||||
"labels": Array [
|
||||
"label 1",
|
||||
"label 2",
|
||||
],
|
||||
"login": "testUser-4",
|
||||
"permission": 0,
|
||||
"teamId": 1,
|
||||
"userId": 4,
|
||||
}
|
||||
}
|
||||
signedInUserIsTeamAdmin={true}
|
||||
syncEnabled={false}
|
||||
/>
|
||||
<Connect(TeamMemberRow)
|
||||
editorsCanAdmin={false}
|
||||
key="5"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="some/url/"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
testUser-5
|
||||
</td>
|
||||
<td>
|
||||
test@test.com
|
||||
</td>
|
||||
<td>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 1"
|
||||
label="label 1"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
<TagBadge
|
||||
count={0}
|
||||
key="label 2"
|
||||
label="label 2"
|
||||
onClick={[Function]}
|
||||
removeIcon={false}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
member={
|
||||
Object {
|
||||
"avatarUrl": "some/url/",
|
||||
"email": "test@test.com",
|
||||
"labels": Array [
|
||||
"label 1",
|
||||
"label 2",
|
||||
],
|
||||
"login": "testUser-5",
|
||||
"permission": 4,
|
||||
"teamId": 1,
|
||||
"userId": 5,
|
||||
}
|
||||
}
|
||||
signedInUserIsTeamAdmin={true}
|
||||
syncEnabled={false}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ exports[`Render should render member page if team not empty 1`] = `
|
||||
isLoading={true}
|
||||
>
|
||||
<Connect(TeamMembers)
|
||||
members={Array []}
|
||||
syncEnabled={true}
|
||||
/>
|
||||
</PageContents>
|
||||
@@ -47,3 +48,25 @@ exports[`Render should render settings and preferences page 1`] = `
|
||||
</PageContents>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
exports[`Render when feature toggle editorsCanAdmin is turned on should not render settings page if user is team member 1`] = `
|
||||
<Page
|
||||
navModel={Object {}}
|
||||
>
|
||||
<PageContents
|
||||
isLoading={true}
|
||||
/>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
exports[`Render when feature toggle editorsCanAdmin is turned on should render settings page if user is team admin 1`] = `
|
||||
<Page
|
||||
navModel={Object {}}
|
||||
>
|
||||
<PageContents
|
||||
isLoading={true}
|
||||
>
|
||||
<Connect(TeamSettings) />
|
||||
</PageContents>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
@@ -160,3 +160,12 @@ export function deleteTeam(id: number): ThunkResult<void> {
|
||||
dispatch(loadTeams());
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTeamMember(member: TeamMember): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().put(`/api/teams/${member.teamId}/members/${member.userId}`, {
|
||||
permission: member.permission,
|
||||
});
|
||||
dispatch(loadTeamMembers());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Team, NavModelItem, NavModel } from 'app/types';
|
||||
import { Team, NavModelItem, NavModel, TeamPermissionLevel } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export function buildNavModel(team: Team): NavModelItem {
|
||||
@@ -47,6 +47,7 @@ export function getTeamLoadingNav(pageName: string): NavModel {
|
||||
name: 'Loading',
|
||||
email: 'loading',
|
||||
memberCount: 0,
|
||||
permission: TeamPermissionLevel.Member,
|
||||
});
|
||||
|
||||
let node: NavModelItem;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTeam, getTeamMembers, getTeams } from './selectors';
|
||||
import { getTeam, getTeamMembers, getTeams, isSignedInUserTeamAdmin, Config } from './selectors';
|
||||
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
|
||||
import { Team, TeamGroup, TeamsState, TeamState } from '../../../types';
|
||||
import { Team, TeamGroup, TeamsState, TeamState, OrgRole } from '../../../types';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
|
||||
describe('Teams selectors', () => {
|
||||
describe('Get teams', () => {
|
||||
@@ -40,7 +41,7 @@ describe('Team selectors', () => {
|
||||
});
|
||||
|
||||
describe('Get members', () => {
|
||||
const mockTeamMembers = getMockTeamMembers(5);
|
||||
const mockTeamMembers = getMockTeamMembers(5, 5);
|
||||
|
||||
it('should return team members', () => {
|
||||
const mockState: TeamState = {
|
||||
@@ -55,3 +56,94 @@ describe('Team selectors', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,4 +1,5 @@
|
||||
import { Team, TeamsState, TeamState } from 'app/types';
|
||||
import { Team, TeamsState, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
|
||||
export const getSearchQuery = (state: TeamsState) => state.searchQuery;
|
||||
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
||||
@@ -28,3 +29,32 @@ export const getTeamMembers = (state: TeamState) => {
|
||||
return regex.test(member.login) || regex.test(member.email);
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user