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 { 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
|
||||
// kind of just a placeholder, will be need for dynamic pages
|
||||
// like datasource edit, teams edit page
|
||||
|
||||
export interface UpdateNavIndexAction {
|
||||
type: 'UPDATE_NAV_INDEX';
|
||||
export enum ActionTypes {
|
||||
UpdateNavIndex = 'UPDATE_NAV_INDEX',
|
||||
}
|
||||
|
||||
export const updateNavIndex = (): UpdateNavIndexAction => ({
|
||||
type: 'UPDATE_NAV_INDEX',
|
||||
export type Action = UpdateNavIndexAction;
|
||||
|
||||
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`
|
||||
*/
|
||||
class CustomScrollbar extends PureComponent<Props> {
|
||||
|
||||
static defaultProps: Partial<Props> = {
|
||||
customClassName: 'custom-scrollbars',
|
||||
autoHide: true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Action } from 'app/core/actions/navModel';
|
||||
import { NavModelItem, NavIndex } from 'app/types';
|
||||
import { Action, ActionTypes } from 'app/core/actions/navModel';
|
||||
import { NavIndex, NavModelItem } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export function buildInitialState(): NavIndex {
|
||||
@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?
|
||||
export const initialState: NavIndex = buildInitialState();
|
||||
|
||||
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;
|
||||
};
|
||||
|
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 { AlertRuleApi, StoreState } from 'app/types';
|
||||
import { AlertRuleDTO, StoreState } from 'app/types';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadAlertRules = 'LOAD_ALERT_RULES',
|
||||
SetSearchQuery = 'SET_SEARCH_QUERY',
|
||||
SetSearchQuery = 'SET_ALERT_SEARCH_QUERY',
|
||||
}
|
||||
|
||||
export interface LoadAlertRulesAction {
|
||||
type: ActionTypes.LoadAlertRules;
|
||||
payload: AlertRuleApi[];
|
||||
payload: AlertRuleDTO[];
|
||||
}
|
||||
|
||||
export interface SetSearchQueryAction {
|
||||
@ -17,7 +17,7 @@ export interface SetSearchQueryAction {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({
|
||||
export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({
|
||||
type: ActionTypes.LoadAlertRules,
|
||||
payload: rules,
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ActionTypes, Action } from './actions';
|
||||
import { alertRulesReducer, initialState } from './reducers';
|
||||
import { AlertRuleApi } from '../../../types';
|
||||
import { AlertRuleDTO } from 'app/types';
|
||||
|
||||
describe('Alert rules', () => {
|
||||
const payload: AlertRuleApi[] = [
|
||||
const payload: AlertRuleDTO[] = [
|
||||
{
|
||||
id: 2,
|
||||
dashboardId: 7,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moment from 'moment';
|
||||
import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types';
|
||||
import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import alertDef from './alertDef';
|
||||
|
||||
@ -29,7 +29,7 @@ function convertToAlertRule(rule, state): AlertRule {
|
||||
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadAlertRules: {
|
||||
const alertRules: AlertRuleApi[] = action.payload;
|
||||
const alertRules: AlertRuleDTO[] = action.payload;
|
||||
|
||||
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
|
||||
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 { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
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 {
|
||||
team: Team;
|
||||
export interface Props {
|
||||
groups: TeamGroup[];
|
||||
loadTeamGroups: typeof loadTeamGroups;
|
||||
addTeamGroup: typeof addTeamGroup;
|
||||
removeTeamGroup: typeof removeTeamGroup;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -16,15 +20,40 @@ interface State {
|
||||
|
||||
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
|
||||
|
||||
@observer
|
||||
export class TeamGroupSync extends React.Component<Props, State> {
|
||||
export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newGroupId: '' };
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -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() {
|
||||
const { isAdding, newGroupId } = this.state;
|
||||
const groups = this.props.team.groups.values();
|
||||
const groups = this.props.groups;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -86,7 +94,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
<h5>Add External Group</h5>
|
||||
<div className="gf-form-inline">
|
||||
<form className="gf-form-inline" onSubmit={this.onAddGroup}>
|
||||
<div className="gf-form">
|
||||
<input
|
||||
type="text"
|
||||
@ -98,16 +106,11 @@ export class TeamGroupSync extends React.Component<Props, State> {
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<button
|
||||
className="btn btn-success gf-form-btn"
|
||||
onClick={this.onAddGroup}
|
||||
type="submit"
|
||||
disabled={!this.isNewGroupValid()}
|
||||
>
|
||||
<button className="btn btn-success gf-form-btn" type="submit" disabled={!this.isNewGroupValid()}>
|
||||
Add group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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 { inject, observer } from 'mobx-react';
|
||||
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 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 {
|
||||
nav: typeof NavStore.Type;
|
||||
teams: typeof TeamsStore.Type;
|
||||
backendSrv: BackendSrv;
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
teams: Team[];
|
||||
searchQuery: string;
|
||||
teamsCount: number;
|
||||
loadTeams: typeof loadTeams;
|
||||
deleteTeam: typeof deleteTeam;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
}
|
||||
|
||||
@inject('nav', 'teams')
|
||||
@observer
|
||||
export class TeamList extends React.Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.props.nav.load('cfg', 'teams');
|
||||
export class TeamList extends PureComponent<Props, any> {
|
||||
componentDidMount() {
|
||||
this.fetchTeams();
|
||||
}
|
||||
|
||||
fetchTeams() {
|
||||
this.props.teams.loadTeams();
|
||||
async fetchTeams() {
|
||||
await this.props.loadTeams();
|
||||
}
|
||||
|
||||
deleteTeam(team: Team) {
|
||||
this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.teams.setSearchQuery(evt.target.value);
|
||||
deleteTeam = (team: Team) => {
|
||||
this.props.deleteTeam(team.id);
|
||||
};
|
||||
|
||||
renderTeamMember(team: Team): JSX.Element {
|
||||
onSearchQueryChange = event => {
|
||||
this.props.setSearchQuery(event.target.value);
|
||||
};
|
||||
|
||||
renderTeam(team: Team) {
|
||||
const teamUrl = `org/teams/edit/${team.id}`;
|
||||
|
||||
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 (
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
@ -72,7 +93,7 @@ export class TeamList extends React.Component<Props, any> {
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search teams"
|
||||
value={teams.search}
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<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%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
||||
<tbody>{teams.map(team => this.renderTeam(team))}</tbody>
|
||||
</table>
|
||||
</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() {
|
||||
const { nav, teams } = this.props;
|
||||
let view;
|
||||
|
||||
if (teams.filteredTeams.length > 0) {
|
||||
view = this.renderTeamList(teams);
|
||||
} else {
|
||||
view = this.renderEmptyList();
|
||||
}
|
||||
const { navModel, teamsCount } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={nav as any} />
|
||||
{view}
|
||||
<PageHeader model={navModel} />
|
||||
{teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
|
||||
</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 { hot } from 'react-hot-loader';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||
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 {
|
||||
team: Team;
|
||||
export interface Props {
|
||||
members: TeamMember[];
|
||||
searchMemberQuery: string;
|
||||
loadTeamMembers: typeof loadTeamMembers;
|
||||
addTeamMember: typeof addTeamMember;
|
||||
removeTeamMember: typeof removeTeamMember;
|
||||
setSearchMemberQuery: typeof setSearchMemberQuery;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -15,42 +21,22 @@ interface State {
|
||||
newTeamMember?: User;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TeamMembers extends React.Component<Props, State> {
|
||||
export class TeamMembers extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newTeamMember: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.team.loadMembers();
|
||||
this.props.loadTeamMembers();
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.team.setSearchQuery(evt.target.value);
|
||||
onSearchQueryChange = event => {
|
||||
this.props.setSearchMemberQuery(event.target.value);
|
||||
};
|
||||
|
||||
removeMember(member: TeamMember) {
|
||||
this.props.team.removeMember(member);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
onRemoveMember(member: TeamMember) {
|
||||
this.props.removeTeamMember(member.userId);
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
@ -62,16 +48,29 @@ export class TeamMembers extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
onAddUserToTeam = async () => {
|
||||
await this.props.team.addMember(this.state.newTeamMember.id);
|
||||
await this.props.team.loadMembers();
|
||||
this.props.addTeamMember(this.state.newTeamMember.id);
|
||||
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() {
|
||||
const { newTeamMember, isAdding } = this.state;
|
||||
const members = this.props.team.filteredMembers;
|
||||
const { searchMemberQuery, members } = this.props;
|
||||
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
|
||||
const { team } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -82,7 +81,7 @@ export class TeamMembers extends React.Component<Props, State> {
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search members"
|
||||
value={team.search}
|
||||
value={searchMemberQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<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%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{members.map(member => this.renderMember(member))}</tbody>
|
||||
<tbody>{members && members.map(member => this.renderMember(member))}</tbody>
|
||||
</table>
|
||||
</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 AlertRuleList from 'app/features/alerting/AlertRuleList';
|
||||
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 TeamPages from 'app/containers/Teams/TeamPages';
|
||||
import TeamList from 'app/containers/Teams/TeamList';
|
||||
|
||||
/** @ngInject */
|
||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { types, getEnv } from 'mobx-state-tree';
|
||||
import { NavItem } from './NavItem';
|
||||
import { Team } from '../TeamsStore/TeamsStore';
|
||||
|
||||
export const NavStore = types
|
||||
.model('NavStore', {
|
||||
@ -116,43 +115,4 @@ export const NavStore = types
|
||||
|
||||
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 { FolderStore } from './../FolderStore/FolderStore';
|
||||
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
||||
import { TeamsStore } from './../TeamsStore/TeamsStore';
|
||||
|
||||
export const RootStore = types.model({
|
||||
nav: types.optional(NavStore, {}),
|
||||
@ -17,9 +16,6 @@ export const RootStore = types.model({
|
||||
routeParams: {},
|
||||
}),
|
||||
folder: types.optional(FolderStore, {}),
|
||||
teams: types.optional(TeamsStore, {
|
||||
map: {},
|
||||
}),
|
||||
});
|
||||
|
||||
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 sharedReducers from 'app/core/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
import teamsReducers from 'app/features/teams/state/reducers';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
...alertingReducers,
|
||||
...teamsReducers,
|
||||
});
|
||||
|
||||
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 @@
|
||||
//
|
||||
// Location
|
||||
//
|
||||
import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams';
|
||||
import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
|
||||
import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
|
||||
import { NavModel, NavModelItem, NavIndex } from './navModel';
|
||||
|
||||
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 };
|
||||
|
||||
//
|
||||
// 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 {
|
||||
Team,
|
||||
TeamsState,
|
||||
TeamState,
|
||||
TeamGroup,
|
||||
TeamMember,
|
||||
AlertRuleDTO,
|
||||
AlertRule,
|
||||
AlertRulesState,
|
||||
LocationState,
|
||||
LocationUpdate,
|
||||
NavModel,
|
||||
NavModelItem,
|
||||
NavIndex,
|
||||
UrlQueryMap,
|
||||
UrlQueryValue,
|
||||
};
|
||||
|
||||
export interface StoreState {
|
||||
navIndex: NavIndex;
|
||||
location: LocationState;
|
||||
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