mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Teams page replace mobx (#13219)
* creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * team settings * actions for group sync * tests for team groups * removed comment * remove old stores * fix: formating of datasource.go * fix: minor changes to imports * adding debounce and fixing issue in teamlist * refactoring: moving types to their own files
This commit is contained in:
parent
1ce9001141
commit
9f73f13091
@ -1,77 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { hot } from 'react-hot-loader';
|
|
||||||
import { inject, observer } from 'mobx-react';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
|
||||||
import { NavStore } from 'app/stores/NavStore/NavStore';
|
|
||||||
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
|
|
||||||
import { ViewStore } from 'app/stores/ViewStore/ViewStore';
|
|
||||||
import TeamMembers from './TeamMembers';
|
|
||||||
import TeamSettings from './TeamSettings';
|
|
||||||
import TeamGroupSync from './TeamGroupSync';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
nav: typeof NavStore.Type;
|
|
||||||
teams: typeof TeamsStore.Type;
|
|
||||||
view: typeof ViewStore.Type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@inject('nav', 'teams', 'view')
|
|
||||||
@observer
|
|
||||||
export class TeamPages extends React.Component<Props, any> {
|
|
||||||
isSyncEnabled: boolean;
|
|
||||||
currentPage: string;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.isSyncEnabled = config.buildInfo.isEnterprise;
|
|
||||||
this.currentPage = this.getCurrentPage();
|
|
||||||
|
|
||||||
this.loadTeam();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadTeam() {
|
|
||||||
const { teams, nav, view } = this.props;
|
|
||||||
|
|
||||||
await teams.loadById(view.routeParams.get('id'));
|
|
||||||
|
|
||||||
nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentTeam(): Team {
|
|
||||||
const { teams, view } = this.props;
|
|
||||||
return teams.map.get(view.routeParams.get('id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPage() {
|
|
||||||
const pages = ['members', 'settings', 'groupsync'];
|
|
||||||
const currentPage = this.props.view.routeParams.get('page');
|
|
||||||
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { nav } = this.props;
|
|
||||||
const currentTeam = this.getCurrentTeam();
|
|
||||||
|
|
||||||
if (!nav.main) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageHeader model={nav as any} />
|
|
||||||
{currentTeam && (
|
|
||||||
<div className="page-container page-body">
|
|
||||||
{this.currentPage === 'members' && <TeamMembers team={currentTeam} />}
|
|
||||||
{this.currentPage === 'settings' && <TeamSettings team={currentTeam} />}
|
|
||||||
{this.currentPage === 'groupsync' && this.isSyncEnabled && <TeamGroupSync team={currentTeam} />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default hot(module)(TeamPages);
|
|
@ -1,69 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { hot } from 'react-hot-loader';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { Team } from 'app/stores/TeamsStore/TeamsStore';
|
|
||||||
import { Label } from 'app/core/components/Forms/Forms';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
team: Team;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class TeamSettings extends React.Component<Props, any> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeName = evt => {
|
|
||||||
this.props.team.setName(evt.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeEmail = evt => {
|
|
||||||
this.props.team.setEmail(evt.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdate = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.props.team.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3 className="page-sub-heading">Team Settings</h3>
|
|
||||||
<form name="teamDetailsForm" className="gf-form-group">
|
|
||||||
<div className="gf-form max-width-30">
|
|
||||||
<Label>Name</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={this.props.team.name}
|
|
||||||
className="gf-form-input max-width-22"
|
|
||||||
onChange={this.onChangeName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form max-width-30">
|
|
||||||
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
|
|
||||||
Email
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="gf-form-input max-width-22"
|
|
||||||
value={this.props.team.email}
|
|
||||||
placeholder="team@email.com"
|
|
||||||
onChange={this.onChangeEmail}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="gf-form-button-row">
|
|
||||||
<button type="submit" className="btn btn-success" onClick={this.onUpdate}>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default hot(module)(TeamSettings);
|
|
@ -1,3 +1,4 @@
|
|||||||
import { updateLocation } from './location';
|
import { updateLocation } from './location';
|
||||||
|
import { updateNavIndex, UpdateNavIndexAction } from './navModel';
|
||||||
|
|
||||||
export { updateLocation };
|
export { updateLocation, updateNavIndex, UpdateNavIndexAction };
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
export type Action = UpdateNavIndexAction;
|
import { NavModelItem } from '../../types';
|
||||||
|
|
||||||
// this action is not used yet
|
export enum ActionTypes {
|
||||||
// kind of just a placeholder, will be need for dynamic pages
|
UpdateNavIndex = 'UPDATE_NAV_INDEX',
|
||||||
// like datasource edit, teams edit page
|
|
||||||
|
|
||||||
export interface UpdateNavIndexAction {
|
|
||||||
type: 'UPDATE_NAV_INDEX';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateNavIndex = (): UpdateNavIndexAction => ({
|
export type Action = UpdateNavIndexAction;
|
||||||
type: 'UPDATE_NAV_INDEX',
|
|
||||||
|
export interface UpdateNavIndexAction {
|
||||||
|
type: ActionTypes.UpdateNavIndex;
|
||||||
|
payload: NavModelItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({
|
||||||
|
type: ActionTypes.UpdateNavIndex,
|
||||||
|
payload: item,
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,6 @@ interface Props {
|
|||||||
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
||||||
*/
|
*/
|
||||||
class CustomScrollbar extends PureComponent<Props> {
|
class CustomScrollbar extends PureComponent<Props> {
|
||||||
|
|
||||||
static defaultProps: Partial<Props> = {
|
static defaultProps: Partial<Props> = {
|
||||||
customClassName: 'custom-scrollbars',
|
customClassName: 'custom-scrollbars',
|
||||||
autoHide: true,
|
autoHide: true,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Action } from 'app/core/actions/navModel';
|
import { Action, ActionTypes } from 'app/core/actions/navModel';
|
||||||
import { NavModelItem, NavIndex } from 'app/types';
|
import { NavIndex, NavModelItem } from 'app/types';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export function buildInitialState(): NavIndex {
|
export function buildInitialState(): NavIndex {
|
||||||
@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?
|
|||||||
export const initialState: NavIndex = buildInitialState();
|
export const initialState: NavIndex = buildInitialState();
|
||||||
|
|
||||||
export const navIndexReducer = (state = initialState, action: Action): NavIndex => {
|
export const navIndexReducer = (state = initialState, action: Action): NavIndex => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.UpdateNavIndex:
|
||||||
|
const newPages = {};
|
||||||
|
const payload = action.payload;
|
||||||
|
|
||||||
|
for (const node of payload.children) {
|
||||||
|
newPages[node.id] = {
|
||||||
|
...node,
|
||||||
|
parentItem: payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, ...newPages };
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
3
public/app/core/selectors/location.ts
Normal file
3
public/app/core/selectors/location.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const getRouteParamsId = state => state.routeParams.id;
|
||||||
|
|
||||||
|
export const getRouteParamsPage = state => state.routeParams.page;
|
@ -1,15 +1,15 @@
|
|||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { AlertRuleApi, StoreState } from 'app/types';
|
import { AlertRuleDTO, StoreState } from 'app/types';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
LoadAlertRules = 'LOAD_ALERT_RULES',
|
LoadAlertRules = 'LOAD_ALERT_RULES',
|
||||||
SetSearchQuery = 'SET_SEARCH_QUERY',
|
SetSearchQuery = 'SET_ALERT_SEARCH_QUERY',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadAlertRulesAction {
|
export interface LoadAlertRulesAction {
|
||||||
type: ActionTypes.LoadAlertRules;
|
type: ActionTypes.LoadAlertRules;
|
||||||
payload: AlertRuleApi[];
|
payload: AlertRuleDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetSearchQueryAction {
|
export interface SetSearchQueryAction {
|
||||||
@ -17,7 +17,7 @@ export interface SetSearchQueryAction {
|
|||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({
|
export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({
|
||||||
type: ActionTypes.LoadAlertRules,
|
type: ActionTypes.LoadAlertRules,
|
||||||
payload: rules,
|
payload: rules,
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ActionTypes, Action } from './actions';
|
import { ActionTypes, Action } from './actions';
|
||||||
import { alertRulesReducer, initialState } from './reducers';
|
import { alertRulesReducer, initialState } from './reducers';
|
||||||
import { AlertRuleApi } from '../../../types';
|
import { AlertRuleDTO } from 'app/types';
|
||||||
|
|
||||||
describe('Alert rules', () => {
|
describe('Alert rules', () => {
|
||||||
const payload: AlertRuleApi[] = [
|
const payload: AlertRuleDTO[] = [
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
dashboardId: 7,
|
dashboardId: 7,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types';
|
import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types';
|
||||||
import { Action, ActionTypes } from './actions';
|
import { Action, ActionTypes } from './actions';
|
||||||
import alertDef from './alertDef';
|
import alertDef from './alertDef';
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ function convertToAlertRule(rule, state): AlertRule {
|
|||||||
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
|
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.LoadAlertRules: {
|
case ActionTypes.LoadAlertRules: {
|
||||||
const alertRules: AlertRuleApi[] = action.payload;
|
const alertRules: AlertRuleDTO[] = action.payload;
|
||||||
|
|
||||||
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
|
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
|
||||||
return convertToAlertRule(rule, rule.state);
|
return convertToAlertRule(rule, rule.state);
|
||||||
|
63
public/app/features/teams/TeamGroupSync.test.tsx
Normal file
63
public/app/features/teams/TeamGroupSync.test.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { Props, TeamGroupSync } from './TeamGroupSync';
|
||||||
|
import { TeamGroup } from '../../types';
|
||||||
|
import { getMockTeamGroups } from './__mocks__/teamMocks';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
groups: [] as TeamGroup[],
|
||||||
|
loadTeamGroups: jest.fn(),
|
||||||
|
addTeamGroup: jest.fn(),
|
||||||
|
removeTeamGroup: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<TeamGroupSync {...props} />);
|
||||||
|
const instance = wrapper.instance() as TeamGroupSync;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render groups table', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
groups: getMockTeamGroups(3),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
it('should call add group', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
|
||||||
|
instance.setState({ newGroupId: 'some/group' });
|
||||||
|
const mockEvent = { preventDefault: jest.fn() };
|
||||||
|
|
||||||
|
instance.onAddGroup(mockEvent);
|
||||||
|
|
||||||
|
expect(instance.props.addTeamGroup).toHaveBeenCalledWith('some/group');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call remove group', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
|
||||||
|
const mockGroup: TeamGroup = { teamId: 1, groupId: 'some/group' };
|
||||||
|
|
||||||
|
instance.onRemoveGroup(mockGroup);
|
||||||
|
|
||||||
|
expect(instance.props.removeTeamGroup).toHaveBeenCalledWith('some/group');
|
||||||
|
});
|
||||||
|
});
|
@ -1,12 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { connect } from 'react-redux';
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore';
|
|
||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||||
|
import { TeamGroup } from '../../types';
|
||||||
|
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
|
||||||
|
import { getTeamGroups } from './state/selectors';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
team: Team;
|
groups: TeamGroup[];
|
||||||
|
loadTeamGroups: typeof loadTeamGroups;
|
||||||
|
addTeamGroup: typeof addTeamGroup;
|
||||||
|
removeTeamGroup: typeof removeTeamGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -16,15 +20,40 @@ interface State {
|
|||||||
|
|
||||||
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
||||||
|
|
||||||
@observer
|
export class TeamGroupSync extends PureComponent<Props, State> {
|
||||||
export class TeamGroupSync extends React.Component<Props, State> {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { isAdding: false, newGroupId: '' };
|
this.state = { isAdding: false, newGroupId: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.team.loadGroups();
|
this.fetchTeamGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTeamGroups() {
|
||||||
|
await this.props.loadTeamGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleAdding = () => {
|
||||||
|
this.setState({ isAdding: !this.state.isAdding });
|
||||||
|
};
|
||||||
|
|
||||||
|
onNewGroupIdChanged = event => {
|
||||||
|
this.setState({ newGroupId: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddGroup = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.props.addTeamGroup(this.state.newGroupId);
|
||||||
|
this.setState({ isAdding: false, newGroupId: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveGroup = (group: TeamGroup) => {
|
||||||
|
this.props.removeTeamGroup(group.groupId);
|
||||||
|
};
|
||||||
|
|
||||||
|
isNewGroupValid() {
|
||||||
|
return this.state.newGroupId.length > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGroup(group: TeamGroup) {
|
renderGroup(group: TeamGroup) {
|
||||||
@ -40,30 +69,9 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleAdding = () => {
|
|
||||||
this.setState({ isAdding: !this.state.isAdding });
|
|
||||||
};
|
|
||||||
|
|
||||||
onNewGroupIdChanged = evt => {
|
|
||||||
this.setState({ newGroupId: evt.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddGroup = () => {
|
|
||||||
this.props.team.addGroup(this.state.newGroupId);
|
|
||||||
this.setState({ isAdding: false, newGroupId: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveGroup = (group: TeamGroup) => {
|
|
||||||
this.props.team.removeGroup(group.groupId);
|
|
||||||
};
|
|
||||||
|
|
||||||
isNewGroupValid() {
|
|
||||||
return this.state.newGroupId.length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isAdding, newGroupId } = this.state;
|
const { isAdding, newGroupId } = this.state;
|
||||||
const groups = this.props.team.groups.values();
|
const groups = this.props.groups;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -86,7 +94,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
|||||||
<i className="fa fa-close" />
|
<i className="fa fa-close" />
|
||||||
</button>
|
</button>
|
||||||
<h5>Add External Group</h5>
|
<h5>Add External Group</h5>
|
||||||
<div className="gf-form-inline">
|
<form className="gf-form-inline" onSubmit={this.onAddGroup}>
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -98,16 +106,11 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<button
|
<button className="btn btn-success gf-form-btn" type="submit" disabled={!this.isNewGroupValid()}>
|
||||||
className="btn btn-success gf-form-btn"
|
|
||||||
onClick={this.onAddGroup}
|
|
||||||
type="submit"
|
|
||||||
disabled={!this.isNewGroupValid()}
|
|
||||||
>
|
|
||||||
Add group
|
Add group
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</SlideDown>
|
</SlideDown>
|
||||||
|
|
||||||
@ -146,4 +149,16 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(module)(TeamGroupSync);
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
groups: getTeamGroups(state.team),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadTeamGroups,
|
||||||
|
addTeamGroup,
|
||||||
|
removeTeamGroup,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);
|
75
public/app/features/teams/TeamList.test.tsx
Normal file
75
public/app/features/teams/TeamList.test.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { Props, TeamList } from './TeamList';
|
||||||
|
import { NavModel, Team } from '../../types';
|
||||||
|
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
navModel: {} as NavModel,
|
||||||
|
teams: [] as Team[],
|
||||||
|
loadTeams: jest.fn(),
|
||||||
|
deleteTeam: jest.fn(),
|
||||||
|
setSearchQuery: jest.fn(),
|
||||||
|
searchQuery: '',
|
||||||
|
teamsCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<TeamList {...props} />);
|
||||||
|
const instance = wrapper.instance() as TeamList;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render teams table', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
teams: getMultipleMockTeams(5),
|
||||||
|
teamsCount: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Life cycle', () => {
|
||||||
|
it('should call loadTeams', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
|
||||||
|
instance.componentDidMount();
|
||||||
|
|
||||||
|
expect(instance.props.loadTeams).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
describe('Delete team', () => {
|
||||||
|
it('should call delete team', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
instance.deleteTeam(getMockTeam());
|
||||||
|
|
||||||
|
expect(instance.props.deleteTeam).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on search query change', () => {
|
||||||
|
it('should call setSearchQuery', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
const mockEvent = { target: { value: 'test' } };
|
||||||
|
|
||||||
|
instance.onSearchQueryChange(mockEvent);
|
||||||
|
|
||||||
|
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,42 +1,42 @@
|
|||||||
import React from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { inject, observer } from 'mobx-react';
|
|
||||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
import { NavStore } from 'app/stores/NavStore/NavStore';
|
|
||||||
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
|
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
|
import { NavModel, Team } from '../../types';
|
||||||
|
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
|
||||||
|
import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
|
||||||
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
nav: typeof NavStore.Type;
|
navModel: NavModel;
|
||||||
teams: typeof TeamsStore.Type;
|
teams: Team[];
|
||||||
backendSrv: BackendSrv;
|
searchQuery: string;
|
||||||
|
teamsCount: number;
|
||||||
|
loadTeams: typeof loadTeams;
|
||||||
|
deleteTeam: typeof deleteTeam;
|
||||||
|
setSearchQuery: typeof setSearchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject('nav', 'teams')
|
export class TeamList extends PureComponent<Props, any> {
|
||||||
@observer
|
componentDidMount() {
|
||||||
export class TeamList extends React.Component<Props, any> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.props.nav.load('cfg', 'teams');
|
|
||||||
this.fetchTeams();
|
this.fetchTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTeams() {
|
async fetchTeams() {
|
||||||
this.props.teams.loadTeams();
|
await this.props.loadTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTeam(team: Team) {
|
deleteTeam = (team: Team) => {
|
||||||
this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
|
this.props.deleteTeam(team.id);
|
||||||
}
|
|
||||||
|
|
||||||
onSearchQueryChange = evt => {
|
|
||||||
this.props.teams.setSearchQuery(evt.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderTeamMember(team: Team): JSX.Element {
|
onSearchQueryChange = event => {
|
||||||
|
this.props.setSearchQuery(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTeam(team: Team) {
|
||||||
const teamUrl = `org/teams/edit/${team.id}`;
|
const teamUrl = `org/teams/edit/${team.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -62,7 +62,28 @@ export class TeamList extends React.Component<Props, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTeamList(teams) {
|
renderEmptyList() {
|
||||||
|
return (
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<EmptyListCTA
|
||||||
|
model={{
|
||||||
|
title: "You haven't created any teams yet.",
|
||||||
|
buttonIcon: 'fa fa-plus',
|
||||||
|
buttonLink: 'org/teams/new',
|
||||||
|
buttonTitle: ' New team',
|
||||||
|
proTip: 'Assign folder and dashboard permissions to teams instead of users to ease administration.',
|
||||||
|
proTipLink: '',
|
||||||
|
proTipLinkTitle: '',
|
||||||
|
proTipTarget: '_blank',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTeamList() {
|
||||||
|
const { teams, searchQuery } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container page-body">
|
<div className="page-container page-body">
|
||||||
<div className="page-action-bar">
|
<div className="page-action-bar">
|
||||||
@ -72,7 +93,7 @@ export class TeamList extends React.Component<Props, any> {
|
|||||||
type="text"
|
type="text"
|
||||||
className="gf-form-input"
|
className="gf-form-input"
|
||||||
placeholder="Search teams"
|
placeholder="Search teams"
|
||||||
value={teams.search}
|
value={searchQuery}
|
||||||
onChange={this.onSearchQueryChange}
|
onChange={this.onSearchQueryChange}
|
||||||
/>
|
/>
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
@ -97,49 +118,38 @@ export class TeamList extends React.Component<Props, any> {
|
|||||||
<th style={{ width: '1%' }} />
|
<th style={{ width: '1%' }} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
<tbody>{teams.map(team => this.renderTeam(team))}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmptyList() {
|
|
||||||
return (
|
|
||||||
<div className="page-container page-body">
|
|
||||||
<EmptyListCTA
|
|
||||||
model={{
|
|
||||||
title: "You haven't created any teams yet.",
|
|
||||||
buttonIcon: 'fa fa-plus',
|
|
||||||
buttonLink: 'org/teams/new',
|
|
||||||
buttonTitle: ' New team',
|
|
||||||
proTip: 'Assign folder and dashboard permissions to teams instead of users to ease administration.',
|
|
||||||
proTipLink: '',
|
|
||||||
proTipLinkTitle: '',
|
|
||||||
proTipTarget: '_blank',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { nav, teams } = this.props;
|
const { navModel, teamsCount } = this.props;
|
||||||
let view;
|
|
||||||
|
|
||||||
if (teams.filteredTeams.length > 0) {
|
|
||||||
view = this.renderTeamList(teams);
|
|
||||||
} else {
|
|
||||||
view = this.renderEmptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader model={nav as any} />
|
<PageHeader model={navModel} />
|
||||||
{view}
|
{teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(module)(TeamList);
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
navModel: getNavModel(state.navIndex, 'teams'),
|
||||||
|
teams: getTeams(state.teams),
|
||||||
|
searchQuery: getSearchQuery(state.teams),
|
||||||
|
teamsCount: getTeamsCount(state.teams),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadTeams,
|
||||||
|
deleteTeam,
|
||||||
|
setSearchQuery,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList));
|
79
public/app/features/teams/TeamMembers.test.tsx
Normal file
79
public/app/features/teams/TeamMembers.test.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { TeamMembers, Props } from './TeamMembers';
|
||||||
|
import { TeamMember } from '../../types';
|
||||||
|
import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
members: [] as TeamMember[],
|
||||||
|
searchMemberQuery: '',
|
||||||
|
setSearchMemberQuery: jest.fn(),
|
||||||
|
loadTeamMembers: jest.fn(),
|
||||||
|
addTeamMember: jest.fn(),
|
||||||
|
removeTeamMember: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<TeamMembers {...props} />);
|
||||||
|
const instance = wrapper.instance() as TeamMembers;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render team members', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
members: getMockTeamMembers(5),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
describe('on search member query change', () => {
|
||||||
|
it('it should call setSearchMemberQuery', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
const mockEvent = { target: { value: 'member' } };
|
||||||
|
|
||||||
|
instance.onSearchQueryChange(mockEvent);
|
||||||
|
|
||||||
|
expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
wrapper.state().newTeamMember = {
|
||||||
|
id: 1,
|
||||||
|
label: '',
|
||||||
|
avatarUrl: '',
|
||||||
|
login: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.onAddUserToTeam();
|
||||||
|
|
||||||
|
expect(instance.props.addTeamMember).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
});
|
@ -1,13 +1,19 @@
|
|||||||
import React from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { connect } from 'react-redux';
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore';
|
|
||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||||
|
import { TeamMember } from '../../types';
|
||||||
|
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
|
||||||
|
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
team: Team;
|
members: TeamMember[];
|
||||||
|
searchMemberQuery: string;
|
||||||
|
loadTeamMembers: typeof loadTeamMembers;
|
||||||
|
addTeamMember: typeof addTeamMember;
|
||||||
|
removeTeamMember: typeof removeTeamMember;
|
||||||
|
setSearchMemberQuery: typeof setSearchMemberQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -15,42 +21,22 @@ interface State {
|
|||||||
newTeamMember?: User;
|
newTeamMember?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
export class TeamMembers extends PureComponent<Props, State> {
|
||||||
export class TeamMembers extends React.Component<Props, State> {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { isAdding: false, newTeamMember: null };
|
this.state = { isAdding: false, newTeamMember: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.team.loadMembers();
|
this.props.loadTeamMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchQueryChange = evt => {
|
onSearchQueryChange = event => {
|
||||||
this.props.team.setSearchQuery(evt.target.value);
|
this.props.setSearchMemberQuery(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
removeMember(member: TeamMember) {
|
onRemoveMember(member: TeamMember) {
|
||||||
this.props.team.removeMember(member);
|
this.props.removeTeamMember(member.userId);
|
||||||
}
|
|
||||||
|
|
||||||
removeMemberConfirmed(member: TeamMember) {
|
|
||||||
this.props.team.removeMember(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMember(member: TeamMember) {
|
|
||||||
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>
|
|
||||||
<td className="text-right">
|
|
||||||
<DeleteButton onConfirmDelete={() => this.removeMember(member)} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleAdding = () => {
|
onToggleAdding = () => {
|
||||||
@ -62,16 +48,29 @@ export class TeamMembers extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onAddUserToTeam = async () => {
|
onAddUserToTeam = async () => {
|
||||||
await this.props.team.addMember(this.state.newTeamMember.id);
|
this.props.addTeamMember(this.state.newTeamMember.id);
|
||||||
await this.props.team.loadMembers();
|
|
||||||
this.setState({ newTeamMember: null });
|
this.setState({ newTeamMember: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderMember(member: TeamMember) {
|
||||||
|
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>
|
||||||
|
<td className="text-right">
|
||||||
|
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { newTeamMember, isAdding } = this.state;
|
const { newTeamMember, isAdding } = this.state;
|
||||||
const members = this.props.team.filteredMembers;
|
const { searchMemberQuery, members } = this.props;
|
||||||
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
|
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
|
||||||
const { team } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -82,7 +81,7 @@ export class TeamMembers extends React.Component<Props, State> {
|
|||||||
type="text"
|
type="text"
|
||||||
className="gf-form-input"
|
className="gf-form-input"
|
||||||
placeholder="Search members"
|
placeholder="Search members"
|
||||||
value={team.search}
|
value={searchMemberQuery}
|
||||||
onChange={this.onSearchQueryChange}
|
onChange={this.onSearchQueryChange}
|
||||||
/>
|
/>
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
@ -124,7 +123,7 @@ export class TeamMembers extends React.Component<Props, State> {
|
|||||||
<th style={{ width: '1%' }} />
|
<th style={{ width: '1%' }} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{members.map(member => this.renderMember(member))}</tbody>
|
<tbody>{members && members.map(member => this.renderMember(member))}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -132,4 +131,18 @@ export class TeamMembers extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(module)(TeamMembers);
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
members: getTeamMembers(state.team),
|
||||||
|
searchMemberQuery: getSearchMemberQuery(state.team),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadTeamMembers,
|
||||||
|
addTeamMember,
|
||||||
|
removeTeamMember,
|
||||||
|
setSearchMemberQuery,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers);
|
63
public/app/features/teams/TeamPages.test.tsx
Normal file
63
public/app/features/teams/TeamPages.test.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { TeamPages, Props } from './TeamPages';
|
||||||
|
import { NavModel, Team } from '../../types';
|
||||||
|
import { getMockTeam } from './__mocks__/teamMocks';
|
||||||
|
|
||||||
|
jest.mock('app/core/config', () => ({
|
||||||
|
buildInfo: { isEnterprise: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
navModel: {} as NavModel,
|
||||||
|
teamId: 1,
|
||||||
|
loadTeam: jest.fn(),
|
||||||
|
pageName: 'members',
|
||||||
|
team: {} as Team,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<TeamPages {...props} />);
|
||||||
|
const instance = wrapper.instance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render member page if team not empty', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
team: getMockTeam(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render settings page', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
team: getMockTeam(),
|
||||||
|
pageName: 'settings',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render group sync page', () => {
|
||||||
|
const { wrapper } = setup({
|
||||||
|
team: getMockTeam(),
|
||||||
|
pageName: 'groupsync',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
105
public/app/features/teams/TeamPages.tsx
Normal file
105
public/app/features/teams/TeamPages.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
|
import TeamMembers from './TeamMembers';
|
||||||
|
import TeamSettings from './TeamSettings';
|
||||||
|
import TeamGroupSync from './TeamGroupSync';
|
||||||
|
import { NavModel, Team } from '../../types';
|
||||||
|
import { loadTeam } from './state/actions';
|
||||||
|
import { getTeam } from './state/selectors';
|
||||||
|
import { getNavModel } from '../../core/selectors/navModel';
|
||||||
|
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
team: Team;
|
||||||
|
loadTeam: typeof loadTeam;
|
||||||
|
teamId: number;
|
||||||
|
pageName: string;
|
||||||
|
navModel: NavModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isSyncEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PageTypes {
|
||||||
|
Members = 'members',
|
||||||
|
Settings = 'settings',
|
||||||
|
GroupSync = 'groupsync',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamPages extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isSyncEnabled: config.buildInfo.isEnterprise,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTeam() {
|
||||||
|
const { loadTeam, teamId } = this.props;
|
||||||
|
|
||||||
|
await loadTeam(teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPage() {
|
||||||
|
const pages = ['members', 'settings', 'groupsync'];
|
||||||
|
const currentPage = this.props.pageName;
|
||||||
|
return _.includes(pages, currentPage) ? currentPage : pages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPage() {
|
||||||
|
const { isSyncEnabled } = this.state;
|
||||||
|
const currentPage = this.getCurrentPage();
|
||||||
|
|
||||||
|
switch (currentPage) {
|
||||||
|
case PageTypes.Members:
|
||||||
|
return <TeamMembers />;
|
||||||
|
|
||||||
|
case PageTypes.Settings:
|
||||||
|
return <TeamSettings />;
|
||||||
|
|
||||||
|
case PageTypes.GroupSync:
|
||||||
|
return isSyncEnabled && <TeamGroupSync />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { team, navModel } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={navModel} />
|
||||||
|
{team && Object.keys(team).length !== 0 && <div className="page-container page-body">{this.renderPage()}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const teamId = getRouteParamsId(state.location);
|
||||||
|
const pageName = getRouteParamsPage(state.location) || 'members';
|
||||||
|
|
||||||
|
return {
|
||||||
|
navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`),
|
||||||
|
teamId: teamId,
|
||||||
|
pageName: pageName,
|
||||||
|
team: getTeam(state.team, teamId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadTeam,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));
|
44
public/app/features/teams/TeamSettings.test.tsx
Normal file
44
public/app/features/teams/TeamSettings.test.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { Props, TeamSettings } from './TeamSettings';
|
||||||
|
import { getMockTeam } from './__mocks__/teamMocks';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
team: getMockTeam(),
|
||||||
|
updateTeam: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<TeamSettings {...props} />);
|
||||||
|
const instance = wrapper.instance() as TeamSettings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
it('should update team', () => {
|
||||||
|
const { instance } = setup();
|
||||||
|
const mockEvent = { preventDefault: jest.fn() };
|
||||||
|
|
||||||
|
instance.setState({
|
||||||
|
name: 'test11',
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onUpdate(mockEvent);
|
||||||
|
|
||||||
|
expect(instance.props.updateTeam).toHaveBeenCalledWith('test11', 'test@test.com');
|
||||||
|
});
|
||||||
|
});
|
96
public/app/features/teams/TeamSettings.tsx
Normal file
96
public/app/features/teams/TeamSettings.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Label } from 'app/core/components/Forms/Forms';
|
||||||
|
import { Team } from '../../types';
|
||||||
|
import { updateTeam } from './state/actions';
|
||||||
|
import { getRouteParamsId } from '../../core/selectors/location';
|
||||||
|
import { getTeam } from './state/selectors';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
team: Team;
|
||||||
|
updateTeam: typeof updateTeam;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamSettings extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
name: props.team.name,
|
||||||
|
email: props.team.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeName = event => {
|
||||||
|
this.setState({ name: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeEmail = event => {
|
||||||
|
this.setState({ email: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdate = event => {
|
||||||
|
const { name, email } = this.state;
|
||||||
|
event.preventDefault();
|
||||||
|
this.props.updateTeam(name, email);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, email } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="page-sub-heading">Team Settings</h3>
|
||||||
|
<form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}>
|
||||||
|
<div className="gf-form max-width-30">
|
||||||
|
<Label>Name</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
className="gf-form-input max-width-22"
|
||||||
|
onChange={this.onChangeName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form max-width-30">
|
||||||
|
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="gf-form-input max-width-22"
|
||||||
|
value={email}
|
||||||
|
placeholder="team@email.com"
|
||||||
|
onChange={this.onChangeEmail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form-button-row">
|
||||||
|
<button type="submit" className="btn btn-success">
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const teamId = getRouteParamsId(state.location);
|
||||||
|
|
||||||
|
return {
|
||||||
|
team: getTeam(state.team, teamId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
updateTeam,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings);
|
59
public/app/features/teams/__mocks__/navModelMock.ts
Normal file
59
public/app/features/teams/__mocks__/navModelMock.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
export const getMockNavModel = (pageName: string) => {
|
||||||
|
return {
|
||||||
|
node: {
|
||||||
|
active: false,
|
||||||
|
icon: 'gicon gicon-team',
|
||||||
|
id: `team-${pageName}-2`,
|
||||||
|
text: `${pageName}`,
|
||||||
|
url: 'org/teams/edit/2/members',
|
||||||
|
parentItem: {
|
||||||
|
img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b',
|
||||||
|
id: 'team-2',
|
||||||
|
subTitle: 'Manage members & settings',
|
||||||
|
url: '',
|
||||||
|
text: 'test1',
|
||||||
|
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
icon: 'gicon gicon-team',
|
||||||
|
id: 'team-members-2',
|
||||||
|
text: 'Members',
|
||||||
|
url: 'org/teams/edit/2/members',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-sliders',
|
||||||
|
id: 'team-settings-2',
|
||||||
|
text: 'Settings',
|
||||||
|
url: 'org/teams/edit/2/settings',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b',
|
||||||
|
id: 'team-2',
|
||||||
|
subTitle: 'Manage members & settings',
|
||||||
|
url: '',
|
||||||
|
text: 'test1',
|
||||||
|
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
icon: 'gicon gicon-team',
|
||||||
|
id: 'team-members-2',
|
||||||
|
text: 'Members',
|
||||||
|
url: 'org/teams/edit/2/members',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-sliders',
|
||||||
|
id: 'team-settings-2',
|
||||||
|
text: 'Settings',
|
||||||
|
url: 'org/teams/edit/2/settings',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
65
public/app/features/teams/__mocks__/teamMocks.ts
Normal file
65
public/app/features/teams/__mocks__/teamMocks.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Team, TeamGroup, TeamMember } from '../../../types';
|
||||||
|
|
||||||
|
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
|
||||||
|
const teams: Team[] = [];
|
||||||
|
for (let i = 1; i <= numberOfTeams; i++) {
|
||||||
|
teams.push({
|
||||||
|
id: i,
|
||||||
|
name: `test-${i}`,
|
||||||
|
avatarUrl: 'some/url/',
|
||||||
|
email: `test-${i}@test.com`,
|
||||||
|
memberCount: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return teams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockTeam = (): Team => {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
avatarUrl: 'some/url/',
|
||||||
|
email: 'test@test.com',
|
||||||
|
memberCount: 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockTeamMembers = (amount: 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',
|
||||||
|
login: `testUser-${i}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return teamMembers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockTeamMember = (): TeamMember => {
|
||||||
|
return {
|
||||||
|
userId: 1,
|
||||||
|
teamId: 1,
|
||||||
|
avatarUrl: 'some/url/',
|
||||||
|
email: 'test@test.com',
|
||||||
|
login: 'testUser',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockTeamGroups = (amount: number): TeamGroup[] => {
|
||||||
|
const groups: TeamGroup[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= amount; i++) {
|
||||||
|
groups.push({
|
||||||
|
groupId: `group-${i}`,
|
||||||
|
teamId: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
@ -0,0 +1,281 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
className="page-sub-heading"
|
||||||
|
>
|
||||||
|
External group sync
|
||||||
|
</h3>
|
||||||
|
<class_1
|
||||||
|
className="page-sub-heading-icon"
|
||||||
|
content="Sync LDAP or OAuth groups with your Grafana teams."
|
||||||
|
placement="auto"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="gicon gicon-question gicon--has-hover"
|
||||||
|
/>
|
||||||
|
</class_1>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
</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 External Group
|
||||||
|
</h5>
|
||||||
|
<form
|
||||||
|
className="gf-form-inline"
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="gf-form-input width-30"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn btn-success gf-form-btn"
|
||||||
|
disabled={true}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Add group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Component>
|
||||||
|
<div
|
||||||
|
className="empty-list-cta"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="empty-list-cta__title"
|
||||||
|
>
|
||||||
|
There are no external groups to sync with
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="empty-list-cta__button btn btn-xlarge btn-success"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="gicon gicon-add-team"
|
||||||
|
/>
|
||||||
|
Add Group
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="empty-list-cta__pro-tip"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-rocket"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Sync LDAP or OAuth groups with your Grafana teams.
|
||||||
|
<a
|
||||||
|
className="text-link empty-list-cta__pro-tip-link"
|
||||||
|
href="asd"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render groups table 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
className="page-sub-heading"
|
||||||
|
>
|
||||||
|
External group sync
|
||||||
|
</h3>
|
||||||
|
<class_1
|
||||||
|
className="page-sub-heading-icon"
|
||||||
|
content="Sync LDAP or OAuth groups with your Grafana teams."
|
||||||
|
placement="auto"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="gicon gicon-question gicon--has-hover"
|
||||||
|
/>
|
||||||
|
</class_1>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-success pull-right"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-plus"
|
||||||
|
/>
|
||||||
|
Add group
|
||||||
|
</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 External Group
|
||||||
|
</h5>
|
||||||
|
<form
|
||||||
|
className="gf-form-inline"
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="gf-form-input width-30"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="cn=ops,ou=groups,dc=grafana,dc=org"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn btn-success gf-form-btn"
|
||||||
|
disabled={true}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Add group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Component>
|
||||||
|
<div
|
||||||
|
className="admin-list-table"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
className="filter-table filter-table--hover form-inline"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
External Group ID
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
key="group-1"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
group-1
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="btn btn-danger btn-mini"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-remove"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="group-2"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
group-2
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="btn btn-danger btn-mini"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-remove"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="group-3"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
group-3
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="btn btn-danger btn-mini"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-remove"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
354
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
Normal file
354
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<EmptyListCTA
|
||||||
|
model={
|
||||||
|
Object {
|
||||||
|
"buttonIcon": "fa fa-plus",
|
||||||
|
"buttonLink": "org/teams/new",
|
||||||
|
"buttonTitle": " New team",
|
||||||
|
"proTip": "Assign folder and dashboard permissions to teams instead of users to ease administration.",
|
||||||
|
"proTipLink": "",
|
||||||
|
"proTipLinkTitle": "",
|
||||||
|
"proTipTarget": "_blank",
|
||||||
|
"title": "You haven't created any teams yet.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render teams table 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form--has-input-icon gf-form--grow"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Search teams"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
className="gf-form-input-icon fa fa-search"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="btn btn-success"
|
||||||
|
href="org/teams/new"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-plus"
|
||||||
|
/>
|
||||||
|
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
|
||||||
|
onConfirmDelete={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="2"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/2"
|
||||||
|
>
|
||||||
|
test-2
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/2"
|
||||||
|
>
|
||||||
|
test-2@test.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/2"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
onConfirmDelete={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="3"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/3"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/3"
|
||||||
|
>
|
||||||
|
test-3
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/3"
|
||||||
|
>
|
||||||
|
test-3@test.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/3"
|
||||||
|
>
|
||||||
|
3
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
onConfirmDelete={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="4"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/4"
|
||||||
|
>
|
||||||
|
test-4
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/4"
|
||||||
|
>
|
||||||
|
test-4@test.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/4"
|
||||||
|
>
|
||||||
|
4
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
onConfirmDelete={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="5"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-4 text-center link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/5"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="filter-table__avatar"
|
||||||
|
src="some/url/"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/5"
|
||||||
|
>
|
||||||
|
test-5
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/5"
|
||||||
|
>
|
||||||
|
test-5@test.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="link-td"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="org/teams/edit/5"
|
||||||
|
>
|
||||||
|
5
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
onConfirmDelete={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,317 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form--has-input-icon gf-form--grow"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Search members"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
className="gf-form-input-icon fa fa-search"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-success pull-right"
|
||||||
|
disabled={false}
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-plus"
|
||||||
|
/>
|
||||||
|
Add a 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="width-30"
|
||||||
|
onSelected={[Function]}
|
||||||
|
value={null}
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "1%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render team members 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form--has-input-icon gf-form--grow"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Search members"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
className="gf-form-input-icon fa fa-search"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-success pull-right"
|
||||||
|
disabled={false}
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-plus"
|
||||||
|
/>
|
||||||
|
Add a 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="width-30"
|
||||||
|
onSelected={[Function]}
|
||||||
|
value={null}
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "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
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
onConfirmDelete={[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
|
||||||
|
onConfirmDelete={[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
|
||||||
|
onConfirmDelete={[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
|
||||||
|
onConfirmDelete={[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
|
||||||
|
onConfirmDelete={[Function]}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,48 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render group sync page 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<Connect(TeamGroupSync) />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render member page if team not empty 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<Connect(TeamMembers) />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render settings page 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<Connect(TeamSettings) />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,57 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
className="page-sub-heading"
|
||||||
|
>
|
||||||
|
Team Settings
|
||||||
|
</h3>
|
||||||
|
<form
|
||||||
|
className="gf-form-group"
|
||||||
|
name="teamDetailsForm"
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form max-width-30"
|
||||||
|
>
|
||||||
|
<Component>
|
||||||
|
Name
|
||||||
|
</Component>
|
||||||
|
<input
|
||||||
|
className="gf-form-input max-width-22"
|
||||||
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
|
type="text"
|
||||||
|
value="test"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form max-width-30"
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</Component>
|
||||||
|
<input
|
||||||
|
className="gf-form-input max-width-22"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="team@email.com"
|
||||||
|
type="email"
|
||||||
|
value="test@test.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="gf-form-button-row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn btn-success"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
237
public/app/features/teams/state/actions.ts
Normal file
237
public/app/features/teams/state/actions.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types';
|
||||||
|
import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
LoadTeams = 'LOAD_TEAMS',
|
||||||
|
LoadTeam = 'LOAD_TEAM',
|
||||||
|
SetSearchQuery = 'SET_TEAM_SEARCH_QUERY',
|
||||||
|
SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY',
|
||||||
|
LoadTeamMembers = 'TEAM_MEMBERS_LOADED',
|
||||||
|
LoadTeamGroups = 'TEAM_GROUPS_LOADED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadTeamsAction {
|
||||||
|
type: ActionTypes.LoadTeams;
|
||||||
|
payload: Team[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadTeamAction {
|
||||||
|
type: ActionTypes.LoadTeam;
|
||||||
|
payload: Team;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadTeamMembersAction {
|
||||||
|
type: ActionTypes.LoadTeamMembers;
|
||||||
|
payload: TeamMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadTeamGroupsAction {
|
||||||
|
type: ActionTypes.LoadTeamGroups;
|
||||||
|
payload: TeamGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetSearchQueryAction {
|
||||||
|
type: ActionTypes.SetSearchQuery;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetSearchMemberQueryAction {
|
||||||
|
type: ActionTypes.SetSearchMemberQuery;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| LoadTeamsAction
|
||||||
|
| SetSearchQueryAction
|
||||||
|
| LoadTeamAction
|
||||||
|
| LoadTeamMembersAction
|
||||||
|
| SetSearchMemberQueryAction
|
||||||
|
| LoadTeamGroupsAction;
|
||||||
|
|
||||||
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
|
||||||
|
|
||||||
|
const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({
|
||||||
|
type: ActionTypes.LoadTeams,
|
||||||
|
payload: teams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamLoaded = (team: Team): LoadTeamAction => ({
|
||||||
|
type: ActionTypes.LoadTeam,
|
||||||
|
payload: team,
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({
|
||||||
|
type: ActionTypes.LoadTeamMembers,
|
||||||
|
payload: teamMembers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({
|
||||||
|
type: ActionTypes.LoadTeamGroups,
|
||||||
|
payload: teamGroups,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({
|
||||||
|
type: ActionTypes.SetSearchMemberQuery,
|
||||||
|
payload: searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
|
||||||
|
type: ActionTypes.SetSearchQuery,
|
||||||
|
payload: searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function loadTeams(): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 });
|
||||||
|
dispatch(teamsLoaded(response.teams));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNavModel(team: Team): NavModelItem {
|
||||||
|
const navModel = {
|
||||||
|
img: team.avatarUrl,
|
||||||
|
id: 'team-' + team.id,
|
||||||
|
subTitle: 'Manage members & settings',
|
||||||
|
url: '',
|
||||||
|
text: team.name,
|
||||||
|
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
icon: 'gicon gicon-team',
|
||||||
|
id: `team-members-${team.id}`,
|
||||||
|
text: 'Members',
|
||||||
|
url: `org/teams/edit/${team.id}/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-sliders',
|
||||||
|
id: `team-settings-${team.id}`,
|
||||||
|
text: 'Settings',
|
||||||
|
url: `org/teams/edit/${team.id}/settings`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.buildInfo.isEnterprise) {
|
||||||
|
navModel.children.push({
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-refresh',
|
||||||
|
id: `team-groupsync-${team.id}`,
|
||||||
|
text: 'External group sync',
|
||||||
|
url: `org/teams/edit/${team.id}/groupsync`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return navModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTeam(id: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv()
|
||||||
|
.get(`/api/teams/${id}`)
|
||||||
|
.then(response => {
|
||||||
|
dispatch(teamLoaded(response));
|
||||||
|
dispatch(updateNavIndex(buildNavModel(response)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTeamMembers(): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
|
||||||
|
await getBackendSrv()
|
||||||
|
.get(`/api/teams/${team.id}/members`)
|
||||||
|
.then(response => {
|
||||||
|
dispatch(teamMembersLoaded(response));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTeamMember(id: number): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
|
||||||
|
await getBackendSrv()
|
||||||
|
.post(`/api/teams/${team.id}/members`, { userId: id })
|
||||||
|
.then(() => {
|
||||||
|
dispatch(loadTeamMembers());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTeamMember(id: number): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
|
||||||
|
await getBackendSrv()
|
||||||
|
.delete(`/api/teams/${team.id}/members/${id}`)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(loadTeamMembers());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTeam(name: string, email: string): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
await getBackendSrv()
|
||||||
|
.put(`/api/teams/${team.id}`, {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(loadTeam(team.id));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTeamGroups(): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
|
||||||
|
await getBackendSrv()
|
||||||
|
.get(`/api/teams/${team.id}/groups`)
|
||||||
|
.then(response => {
|
||||||
|
dispatch(teamGroupsLoaded(response));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTeamGroup(groupId: string): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
|
||||||
|
await getBackendSrv()
|
||||||
|
.post(`/api/teams/${team.id}/groups`, { groupId: groupId })
|
||||||
|
.then(() => {
|
||||||
|
dispatch(loadTeamGroups());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTeamGroup(groupId: string): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const team = getStore().team.team;
|
||||||
|
|
||||||
|
await getBackendSrv()
|
||||||
|
.delete(`/api/teams/${team.id}/groups/${groupId}`)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(loadTeamGroups());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTeam(id: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv()
|
||||||
|
.delete(`/api/teams/${id}`)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(loadTeams());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
72
public/app/features/teams/state/reducers.test.ts
Normal file
72
public/app/features/teams/state/reducers.test.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Action, ActionTypes } from './actions';
|
||||||
|
import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers';
|
||||||
|
import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks';
|
||||||
|
|
||||||
|
describe('teams reducer', () => {
|
||||||
|
it('should set teams', () => {
|
||||||
|
const payload = [getMockTeam()];
|
||||||
|
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.LoadTeams,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = teamsReducer(initialTeamsState, action);
|
||||||
|
|
||||||
|
expect(result.teams).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set search query', () => {
|
||||||
|
const payload = 'test';
|
||||||
|
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.SetSearchQuery,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = teamsReducer(initialTeamsState, action);
|
||||||
|
|
||||||
|
expect(result.searchQuery).toEqual('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('team reducer', () => {
|
||||||
|
it('should set team', () => {
|
||||||
|
const payload = getMockTeam();
|
||||||
|
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.LoadTeam,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = teamReducer(initialTeamState, action);
|
||||||
|
|
||||||
|
expect(result.team).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set team members', () => {
|
||||||
|
const mockTeamMember = getMockTeamMember();
|
||||||
|
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.LoadTeamMembers,
|
||||||
|
payload: [mockTeamMember],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = teamReducer(initialTeamState, action);
|
||||||
|
|
||||||
|
expect(result.members).toEqual([mockTeamMember]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set member search query', () => {
|
||||||
|
const payload = 'member';
|
||||||
|
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.SetSearchMemberQuery,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = teamReducer(initialTeamState, action);
|
||||||
|
|
||||||
|
expect(result.searchMemberQuery).toEqual('member');
|
||||||
|
});
|
||||||
|
});
|
44
public/app/features/teams/state/reducers.ts
Normal file
44
public/app/features/teams/state/reducers.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
|
||||||
|
import { Action, ActionTypes } from './actions';
|
||||||
|
|
||||||
|
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };
|
||||||
|
export const initialTeamState: TeamState = {
|
||||||
|
team: {} as Team,
|
||||||
|
members: [] as TeamMember[],
|
||||||
|
groups: [] as TeamGroup[],
|
||||||
|
searchMemberQuery: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.LoadTeams:
|
||||||
|
return { ...state, teams: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.SetSearchQuery:
|
||||||
|
return { ...state, searchQuery: action.payload };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const teamReducer = (state = initialTeamState, action: Action): TeamState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.LoadTeam:
|
||||||
|
return { ...state, team: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.LoadTeamMembers:
|
||||||
|
return { ...state, members: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.SetSearchMemberQuery:
|
||||||
|
return { ...state, searchMemberQuery: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.LoadTeamGroups:
|
||||||
|
return { ...state, groups: action.payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
teams: teamsReducer,
|
||||||
|
team: teamReducer,
|
||||||
|
};
|
56
public/app/features/teams/state/selectors.test.ts
Normal file
56
public/app/features/teams/state/selectors.test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { getTeam, getTeamMembers, getTeams } from './selectors';
|
||||||
|
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
|
||||||
|
import { Team, TeamGroup, TeamsState, TeamState } from '../../../types';
|
||||||
|
|
||||||
|
describe('Teams selectors', () => {
|
||||||
|
describe('Get teams', () => {
|
||||||
|
const mockTeams = getMultipleMockTeams(5);
|
||||||
|
|
||||||
|
it('should return teams if no search query', () => {
|
||||||
|
const mockState: TeamsState = { teams: mockTeams, searchQuery: '' };
|
||||||
|
|
||||||
|
const teams = getTeams(mockState);
|
||||||
|
|
||||||
|
expect(teams).toEqual(mockTeams);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should filter teams if search query', () => {
|
||||||
|
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' };
|
||||||
|
|
||||||
|
const teams = getTeams(mockState);
|
||||||
|
|
||||||
|
expect(teams.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Team selectors', () => {
|
||||||
|
describe('Get team', () => {
|
||||||
|
const mockTeam = getMockTeam();
|
||||||
|
|
||||||
|
it('should return team if matching with location team', () => {
|
||||||
|
const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] };
|
||||||
|
|
||||||
|
const team = getTeam(mockState, '1');
|
||||||
|
|
||||||
|
expect(team).toEqual(mockTeam);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Get members', () => {
|
||||||
|
const mockTeamMembers = getMockTeamMembers(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
30
public/app/features/teams/state/selectors.ts
Normal file
30
public/app/features/teams/state/selectors.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Team, TeamsState, TeamState } from 'app/types';
|
||||||
|
|
||||||
|
export const getSearchQuery = (state: TeamsState) => state.searchQuery;
|
||||||
|
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
|
||||||
|
export const getTeamGroups = (state: TeamState) => state.groups;
|
||||||
|
export const getTeamsCount = (state: TeamsState) => state.teams.length;
|
||||||
|
|
||||||
|
export const getTeam = (state: TeamState, currentTeamId): Team | null => {
|
||||||
|
if (state.team.id === parseInt(currentTeamId, 10)) {
|
||||||
|
return state.team;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTeams = (state: TeamsState) => {
|
||||||
|
const regex = RegExp(state.searchQuery, 'i');
|
||||||
|
|
||||||
|
return state.teams.filter(team => {
|
||||||
|
return regex.test(team.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
@ -4,9 +4,9 @@ import './ReactContainer';
|
|||||||
import ServerStats from 'app/features/admin/ServerStats';
|
import ServerStats from 'app/features/admin/ServerStats';
|
||||||
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
||||||
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
||||||
|
import TeamPages from 'app/features/teams/TeamPages';
|
||||||
|
import TeamList from 'app/features/teams/TeamList';
|
||||||
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
||||||
import TeamPages from 'app/containers/Teams/TeamPages';
|
|
||||||
import TeamList from 'app/containers/Teams/TeamList';
|
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { types, getEnv } from 'mobx-state-tree';
|
import { types, getEnv } from 'mobx-state-tree';
|
||||||
import { NavItem } from './NavItem';
|
import { NavItem } from './NavItem';
|
||||||
import { Team } from '../TeamsStore/TeamsStore';
|
|
||||||
|
|
||||||
export const NavStore = types
|
export const NavStore = types
|
||||||
.model('NavStore', {
|
.model('NavStore', {
|
||||||
@ -116,43 +115,4 @@ export const NavStore = types
|
|||||||
|
|
||||||
self.main = NavItem.create(main);
|
self.main = NavItem.create(main);
|
||||||
},
|
},
|
||||||
|
|
||||||
initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) {
|
|
||||||
const main = {
|
|
||||||
img: team.avatarUrl,
|
|
||||||
id: 'team-' + team.id,
|
|
||||||
subTitle: 'Manage members & settings',
|
|
||||||
url: '',
|
|
||||||
text: team.name,
|
|
||||||
breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
active: tab === 'members',
|
|
||||||
icon: 'gicon gicon-team',
|
|
||||||
id: 'team-members',
|
|
||||||
text: 'Members',
|
|
||||||
url: `org/teams/edit/${team.id}/members`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
active: tab === 'settings',
|
|
||||||
icon: 'fa fa-fw fa-sliders',
|
|
||||||
id: 'team-settings',
|
|
||||||
text: 'Settings',
|
|
||||||
url: `org/teams/edit/${team.id}/settings`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSyncEnabled) {
|
|
||||||
main.children.splice(1, 0, {
|
|
||||||
active: tab === 'groupsync',
|
|
||||||
icon: 'fa fa-fw fa-refresh',
|
|
||||||
id: 'team-settings',
|
|
||||||
text: 'External group sync',
|
|
||||||
url: `org/teams/edit/${team.id}/groupsync`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.main = NavItem.create(main);
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
@ -3,7 +3,6 @@ import { NavStore } from './../NavStore/NavStore';
|
|||||||
import { ViewStore } from './../ViewStore/ViewStore';
|
import { ViewStore } from './../ViewStore/ViewStore';
|
||||||
import { FolderStore } from './../FolderStore/FolderStore';
|
import { FolderStore } from './../FolderStore/FolderStore';
|
||||||
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
||||||
import { TeamsStore } from './../TeamsStore/TeamsStore';
|
|
||||||
|
|
||||||
export const RootStore = types.model({
|
export const RootStore = types.model({
|
||||||
nav: types.optional(NavStore, {}),
|
nav: types.optional(NavStore, {}),
|
||||||
@ -17,9 +16,6 @@ export const RootStore = types.model({
|
|||||||
routeParams: {},
|
routeParams: {},
|
||||||
}),
|
}),
|
||||||
folder: types.optional(FolderStore, {}),
|
folder: types.optional(FolderStore, {}),
|
||||||
teams: types.optional(TeamsStore, {
|
|
||||||
map: {},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type RootStoreType = typeof RootStore.Type;
|
type RootStoreType = typeof RootStore.Type;
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
import { types, getEnv, flow } from 'mobx-state-tree';
|
|
||||||
|
|
||||||
export const TeamMemberModel = types.model('TeamMember', {
|
|
||||||
userId: types.identifier(types.number),
|
|
||||||
teamId: types.number,
|
|
||||||
avatarUrl: types.string,
|
|
||||||
email: types.string,
|
|
||||||
login: types.string,
|
|
||||||
});
|
|
||||||
|
|
||||||
type TeamMemberType = typeof TeamMemberModel.Type;
|
|
||||||
export interface TeamMember extends TeamMemberType {}
|
|
||||||
|
|
||||||
export const TeamGroupModel = types.model('TeamGroup', {
|
|
||||||
groupId: types.identifier(types.string),
|
|
||||||
teamId: types.number,
|
|
||||||
});
|
|
||||||
|
|
||||||
type TeamGroupType = typeof TeamGroupModel.Type;
|
|
||||||
export interface TeamGroup extends TeamGroupType {}
|
|
||||||
|
|
||||||
export const TeamModel = types
|
|
||||||
.model('Team', {
|
|
||||||
id: types.identifier(types.number),
|
|
||||||
name: types.string,
|
|
||||||
avatarUrl: types.string,
|
|
||||||
email: types.string,
|
|
||||||
memberCount: types.number,
|
|
||||||
search: types.optional(types.string, ''),
|
|
||||||
members: types.optional(types.map(TeamMemberModel), {}),
|
|
||||||
groups: types.optional(types.map(TeamGroupModel), {}),
|
|
||||||
})
|
|
||||||
.views(self => ({
|
|
||||||
get filteredMembers(this: Team) {
|
|
||||||
const members = this.members.values();
|
|
||||||
const regex = new RegExp(self.search, 'i');
|
|
||||||
return members.filter(member => {
|
|
||||||
return regex.test(member.login) || regex.test(member.email);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.actions(self => ({
|
|
||||||
setName(name: string) {
|
|
||||||
self.name = name;
|
|
||||||
},
|
|
||||||
|
|
||||||
setEmail(email: string) {
|
|
||||||
self.email = email;
|
|
||||||
},
|
|
||||||
|
|
||||||
setSearchQuery(query: string) {
|
|
||||||
self.search = query;
|
|
||||||
},
|
|
||||||
|
|
||||||
update: flow(function* load() {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
|
|
||||||
yield backendSrv.put(`/api/teams/${self.id}`, {
|
|
||||||
name: self.name,
|
|
||||||
email: self.email,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
loadMembers: flow(function* load() {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`);
|
|
||||||
self.members.clear();
|
|
||||||
|
|
||||||
for (const member of rsp) {
|
|
||||||
self.members.set(member.userId.toString(), TeamMemberModel.create(member));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
removeMember: flow(function* load(member: TeamMember) {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`);
|
|
||||||
// remove from store map
|
|
||||||
self.members.delete(member.userId.toString());
|
|
||||||
}),
|
|
||||||
|
|
||||||
addMember: flow(function* load(userId: number) {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
yield backendSrv.post(`/api/teams/${self.id}/members`, { userId: userId });
|
|
||||||
}),
|
|
||||||
|
|
||||||
loadGroups: flow(function* load() {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`);
|
|
||||||
self.groups.clear();
|
|
||||||
|
|
||||||
for (const group of rsp) {
|
|
||||||
self.groups.set(group.groupId, TeamGroupModel.create(group));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
addGroup: flow(function* load(groupId: string) {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId });
|
|
||||||
self.groups.set(
|
|
||||||
groupId,
|
|
||||||
TeamGroupModel.create({
|
|
||||||
teamId: self.id,
|
|
||||||
groupId: groupId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
removeGroup: flow(function* load(groupId: string) {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
yield backendSrv.delete(`/api/teams/${self.id}/groups/${groupId}`);
|
|
||||||
self.groups.delete(groupId);
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
type TeamType = typeof TeamModel.Type;
|
|
||||||
export interface Team extends TeamType {}
|
|
||||||
|
|
||||||
export const TeamsStore = types
|
|
||||||
.model('TeamsStore', {
|
|
||||||
map: types.map(TeamModel),
|
|
||||||
search: types.optional(types.string, ''),
|
|
||||||
})
|
|
||||||
.views(self => ({
|
|
||||||
get filteredTeams(this: any) {
|
|
||||||
const teams = this.map.values();
|
|
||||||
const regex = new RegExp(self.search, 'i');
|
|
||||||
return teams.filter(team => {
|
|
||||||
return regex.test(team.name);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.actions(self => ({
|
|
||||||
loadTeams: flow(function* load() {
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 });
|
|
||||||
self.map.clear();
|
|
||||||
|
|
||||||
for (const team of rsp.teams) {
|
|
||||||
self.map.set(team.id.toString(), TeamModel.create(team));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
setSearchQuery(query: string) {
|
|
||||||
self.search = query;
|
|
||||||
},
|
|
||||||
|
|
||||||
loadById: flow(function* load(id: string) {
|
|
||||||
if (self.map.has(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const backendSrv = getEnv(self).backendSrv;
|
|
||||||
const team = yield backendSrv.get(`/api/teams/${id}`);
|
|
||||||
self.map.set(id, TeamModel.create(team));
|
|
||||||
}),
|
|
||||||
}));
|
|
@ -3,10 +3,12 @@ import thunk from 'redux-thunk';
|
|||||||
import { createLogger } from 'redux-logger';
|
import { createLogger } from 'redux-logger';
|
||||||
import sharedReducers from 'app/core/reducers';
|
import sharedReducers from 'app/core/reducers';
|
||||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||||
|
import teamsReducers from 'app/features/teams/state/reducers';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
...sharedReducers,
|
...sharedReducers,
|
||||||
...alertingReducers,
|
...alertingReducers,
|
||||||
|
...teamsReducers,
|
||||||
});
|
});
|
||||||
|
|
||||||
export let store;
|
export let store;
|
||||||
|
35
public/app/types/alerting.ts
Normal file
35
public/app/types/alerting.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export interface AlertRuleDTO {
|
||||||
|
id: number;
|
||||||
|
dashboardId: number;
|
||||||
|
dashboardUid: string;
|
||||||
|
dashboardSlug: string;
|
||||||
|
panelId: number;
|
||||||
|
name: string;
|
||||||
|
state: string;
|
||||||
|
newStateDate: string;
|
||||||
|
evalDate: string;
|
||||||
|
evalData?: object;
|
||||||
|
executionError: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertRule {
|
||||||
|
id: number;
|
||||||
|
dashboardId: number;
|
||||||
|
panelId: number;
|
||||||
|
name: string;
|
||||||
|
state: string;
|
||||||
|
stateText: string;
|
||||||
|
stateIcon: string;
|
||||||
|
stateClass: string;
|
||||||
|
stateAge: string;
|
||||||
|
url: string;
|
||||||
|
info?: string;
|
||||||
|
executionError?: string;
|
||||||
|
evalData?: { noData: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertRulesState {
|
||||||
|
items: AlertRule[];
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
@ -1,96 +1,30 @@
|
|||||||
//
|
import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams';
|
||||||
// Location
|
import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
|
||||||
//
|
import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
|
||||||
|
import { NavModel, NavModelItem, NavIndex } from './navModel';
|
||||||
|
|
||||||
export interface LocationUpdate {
|
export {
|
||||||
path?: string;
|
Team,
|
||||||
query?: UrlQueryMap;
|
TeamsState,
|
||||||
routeParams?: UrlQueryMap;
|
TeamState,
|
||||||
}
|
TeamGroup,
|
||||||
|
TeamMember,
|
||||||
export interface LocationState {
|
AlertRuleDTO,
|
||||||
url: string;
|
AlertRule,
|
||||||
path: string;
|
AlertRulesState,
|
||||||
query: UrlQueryMap;
|
LocationState,
|
||||||
routeParams: UrlQueryMap;
|
LocationUpdate,
|
||||||
}
|
NavModel,
|
||||||
|
NavModelItem,
|
||||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
NavIndex,
|
||||||
export type UrlQueryMap = { [s: string]: UrlQueryValue };
|
UrlQueryMap,
|
||||||
|
UrlQueryValue,
|
||||||
//
|
};
|
||||||
// Alerting
|
|
||||||
//
|
|
||||||
|
|
||||||
export interface AlertRuleApi {
|
|
||||||
id: number;
|
|
||||||
dashboardId: number;
|
|
||||||
dashboardUid: string;
|
|
||||||
dashboardSlug: string;
|
|
||||||
panelId: number;
|
|
||||||
name: string;
|
|
||||||
state: string;
|
|
||||||
newStateDate: string;
|
|
||||||
evalDate: string;
|
|
||||||
evalData?: object;
|
|
||||||
executionError: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlertRule {
|
|
||||||
id: number;
|
|
||||||
dashboardId: number;
|
|
||||||
panelId: number;
|
|
||||||
name: string;
|
|
||||||
state: string;
|
|
||||||
stateText: string;
|
|
||||||
stateIcon: string;
|
|
||||||
stateClass: string;
|
|
||||||
stateAge: string;
|
|
||||||
url: string;
|
|
||||||
info?: string;
|
|
||||||
executionError?: string;
|
|
||||||
evalData?: { noData: boolean };
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// NavModel
|
|
||||||
//
|
|
||||||
|
|
||||||
export interface NavModelItem {
|
|
||||||
text: string;
|
|
||||||
url: string;
|
|
||||||
subTitle?: string;
|
|
||||||
icon?: string;
|
|
||||||
img?: string;
|
|
||||||
id: string;
|
|
||||||
active?: boolean;
|
|
||||||
hideFromTabs?: boolean;
|
|
||||||
divider?: boolean;
|
|
||||||
children?: NavModelItem[];
|
|
||||||
breadcrumbs?: NavModelItem[];
|
|
||||||
target?: string;
|
|
||||||
parentItem?: NavModelItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NavModel {
|
|
||||||
main: NavModelItem;
|
|
||||||
node: NavModelItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NavIndex = { [s: string]: NavModelItem };
|
|
||||||
|
|
||||||
//
|
|
||||||
// Store
|
|
||||||
//
|
|
||||||
|
|
||||||
export interface AlertRulesState {
|
|
||||||
items: AlertRule[];
|
|
||||||
searchQuery: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
navIndex: NavIndex;
|
navIndex: NavIndex;
|
||||||
location: LocationState;
|
location: LocationState;
|
||||||
alertRules: AlertRulesState;
|
alertRules: AlertRulesState;
|
||||||
|
teams: TeamsState;
|
||||||
|
team: TeamState;
|
||||||
}
|
}
|
||||||
|
15
public/app/types/location.ts
Normal file
15
public/app/types/location.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface LocationUpdate {
|
||||||
|
path?: string;
|
||||||
|
query?: UrlQueryMap;
|
||||||
|
routeParams?: UrlQueryMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationState {
|
||||||
|
url: string;
|
||||||
|
path: string;
|
||||||
|
query: UrlQueryMap;
|
||||||
|
routeParams: UrlQueryMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
||||||
|
export type UrlQueryMap = { [s: string]: UrlQueryValue };
|
22
public/app/types/navModel.ts
Normal file
22
public/app/types/navModel.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export interface NavModelItem {
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
subTitle?: string;
|
||||||
|
icon?: string;
|
||||||
|
img?: string;
|
||||||
|
id: string;
|
||||||
|
active?: boolean;
|
||||||
|
hideFromTabs?: boolean;
|
||||||
|
divider?: boolean;
|
||||||
|
children?: NavModelItem[];
|
||||||
|
breadcrumbs?: Array<{ title: string; url: string }>;
|
||||||
|
target?: string;
|
||||||
|
parentItem?: NavModelItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavModel {
|
||||||
|
main: NavModelItem;
|
||||||
|
node: NavModelItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NavIndex = { [s: string]: NavModelItem };
|
32
public/app/types/teams.ts
Normal file
32
public/app/types/teams.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export interface Team {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
email: string;
|
||||||
|
memberCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
userId: number;
|
||||||
|
teamId: number;
|
||||||
|
avatarUrl: string;
|
||||||
|
email: string;
|
||||||
|
login: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamGroup {
|
||||||
|
groupId: string;
|
||||||
|
teamId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamsState {
|
||||||
|
teams: Team[];
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamState {
|
||||||
|
team: Team;
|
||||||
|
members: TeamMember[];
|
||||||
|
groups: TeamGroup[];
|
||||||
|
searchMemberQuery: string;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user