Commit 05bfc365 by Peter Holmberg

Teampages page

parent f68ac202
import { updateLocation } from './location';
import { updateNavIndex } from './navModel';
export { updateLocation };
export { updateLocation, updateNavIndex };
import { NavModelItem } from '../../types';
export enum ActionTypes {
UpdateNavIndex = 'UPDATE_NAV_INDEX',
}
export type Action = UpdateNavIndexAction;
// this action is not used yet
......@@ -5,9 +11,11 @@ export type Action = UpdateNavIndexAction;
// like datasource edit, teams edit page
export interface UpdateNavIndexAction {
type: 'UPDATE_NAV_INDEX';
type: ActionTypes.UpdateNavIndex;
payload: NavModelItem;
}
export const updateNavIndex = (): UpdateNavIndexAction => ({
type: 'UPDATE_NAV_INDEX',
export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({
type: ActionTypes.UpdateNavIndex,
payload: item,
});
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;
};
export const getRouteParamsId = state => state.routeParams.id;
export const getRouteParamsPage = state => state.routeParams.page;
import { NavModel, NavModelItem, NavIndex } from 'app/types';
function getNotFoundModel(): NavModel {
var node: NavModelItem = {
const node: NavModelItem = {
id: 'not-found',
text: 'Page not found',
icon: 'fa fa-fw fa-warning',
......
import React from 'react';
import { hot } from 'react-hot-loader';
import { observer } from 'mobx-react';
import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore';
import SlideDown from 'app/core/components/Animations/SlideDown';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
import { Team, TeamGroup } from '../../types';
interface Props {
team: Team;
......@@ -16,7 +15,6 @@ interface State {
const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`;
@observer
export class TeamGroupSync extends React.Component<Props, State> {
constructor(props) {
super(props);
......@@ -24,7 +22,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
}
componentDidMount() {
this.props.team.loadGroups();
// this.props.team.loadGroups();
}
renderGroup(group: TeamGroup) {
......@@ -49,12 +47,12 @@ export class TeamGroupSync extends React.Component<Props, State> {
};
onAddGroup = () => {
this.props.team.addGroup(this.state.newGroupId);
// this.props.team.addGroup(this.state.newGroupId);
this.setState({ isAdding: false, newGroupId: '' });
};
onRemoveGroup = (group: TeamGroup) => {
this.props.team.removeGroup(group.groupId);
// this.props.team.removeGroup(group.groupId);
};
isNewGroupValid() {
......@@ -63,7 +61,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
render() {
const { isAdding, newGroupId } = this.state;
const groups = this.props.team.groups.values();
const groups = this.props.team.groups;
return (
<div>
......
import React from 'react';
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { observer } from 'mobx-react';
import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore';
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 { Team, TeamMember } from '../../types';
interface Props {
team: Team;
......@@ -15,27 +14,26 @@ 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.team.loadMembers();
}
onSearchQueryChange = evt => {
this.props.team.setSearchQuery(evt.target.value);
// this.props.team.setSearchQuery(evt.target.value);
};
removeMember(member: TeamMember) {
this.props.team.removeMember(member);
// this.props.team.removeMember(member);
}
removeMemberConfirmed(member: TeamMember) {
this.props.team.removeMember(member);
// this.props.team.removeMember(member);
}
renderMember(member: TeamMember) {
......@@ -62,16 +60,15 @@ 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.setState({ newTeamMember: null });
// await this.props.team.addMember(this.state.newTeamMember.id);
// await this.props.team.loadMembers();
// this.setState({ newTeamMember: null });
};
render() {
const { newTeamMember, isAdding } = this.state;
const members = this.props.team.filteredMembers;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
const { team } = this.props;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
return (
<div>
......@@ -124,7 +121,7 @@ export class TeamMembers extends React.Component<Props, State> {
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>{members.map(member => this.renderMember(member))}</tbody>
<tbody>{team.members && team.members.map(member => this.renderMember(member))}</tbody>
</table>
</div>
</div>
......
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();
});
});
import React from 'react';
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
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';
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';
interface Props {
nav: typeof NavStore.Type;
teams: typeof TeamsStore.Type;
view: typeof ViewStore.Type;
export interface Props {
team: Team;
loadTeam: typeof loadTeam;
teamId: number;
pageName: string;
navModel: NavModel;
}
@inject('nav', 'teams', 'view')
@observer
export class TeamPages extends React.Component<Props, any> {
interface State {
isSyncEnabled: boolean;
currentPage: string;
}
enum PageTypes {
Members = 'members',
Settings = 'settings',
GroupSync = 'groupsync',
}
export class TeamPages extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.isSyncEnabled = config.buildInfo.isEnterprise;
this.currentPage = this.getCurrentPage();
this.state = {
isSyncEnabled: config.buildInfo.isEnterprise,
};
}
componentDidMount() {
this.loadTeam();
}
async loadTeam() {
const { teams, nav, view } = this.props;
await teams.loadById(view.routeParams.get('id'));
const { loadTeam, teamId } = this.props;
nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
}
getCurrentTeam(): Team {
const { teams, view } = this.props;
return teams.map.get(view.routeParams.get('id'));
await loadTeam(teamId);
}
getCurrentPage() {
const pages = ['members', 'settings', 'groupsync'];
const currentPage = this.props.view.routeParams.get('page');
const currentPage = this.props.pageName;
return _.includes(pages, currentPage) ? currentPage : pages[0];
}
render() {
const { nav } = this.props;
const currentTeam = this.getCurrentTeam();
renderPage() {
const { team } = this.props;
const { isSyncEnabled } = this.state;
const currentPage = this.getCurrentPage();
switch (currentPage) {
case PageTypes.Members:
return <TeamMembers team={team} />;
if (!nav.main) {
return null;
case PageTypes.Settings:
return <TeamSettings team={team} />;
case PageTypes.GroupSync:
return isSyncEnabled && <TeamGroupSync team={team} />;
}
return null;
}
render() {
const { team, navModel } = this.props;
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>
)}
<PageHeader model={navModel} />
{team && Object.keys(team).length !== 0 && <div className="page-container page-body">{this.renderPage()}</div>}
</div>
);
}
}
export default hot(module)(TeamPages);
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),
};
}
const mapDispatchToProps = {
loadTeam,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));
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';
import { Team } from '../../types';
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);
// this.props.team.setName(evt.target.value);
};
onChangeEmail = evt => {
this.props.team.setEmail(evt.target.value);
// this.props.team.setEmail(evt.target.value);
};
onUpdate = evt => {
evt.preventDefault();
this.props.team.update();
// this.props.team.update();
};
render() {
......
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',
},
],
},
};
};
// 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"
>
<TeamGroupSync
team={
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
</div>
</div>
`;
exports[`Render should render member page if team not empty 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
>
<TeamMembers
team={
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
</div>
</div>
`;
exports[`Render should render settings page 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
>
<TeamSettings
team={
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
</div>
</div>
`;
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { StoreState, Team } from '../../../types';
import { NavModelItem, StoreState, Team } from '../../../types';
import { updateNavIndex } from '../../../core/actions';
import { UpdateNavIndexAction } from '../../../core/actions/navModel';
export enum ActionTypes {
LoadTeams = 'LOAD_TEAMS',
LoadTeam = 'LOAD_TEAM',
SetSearchQuery = 'SET_SEARCH_QUERY',
}
......@@ -12,20 +15,30 @@ export interface LoadTeamsAction {
payload: Team[];
}
export interface LoadTeamAction {
type: ActionTypes.LoadTeam;
payload: Team;
}
export interface SetSearchQueryAction {
type: ActionTypes.SetSearchQuery;
payload: string;
}
export type Action = LoadTeamsAction | SetSearchQueryAction;
export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
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,
});
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
type: ActionTypes.SetSearchQuery,
payload: searchQuery,
......@@ -38,6 +51,44 @@ export function loadTeams(): ThunkResult<void> {
};
}
function buildNavModel(team: Team): NavModelItem {
return {
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`,
},
],
};
}
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 deleteTeam(id: number): ThunkResult<void> {
return async dispatch => {
await getBackendSrv()
......
import { Action, ActionTypes } from './actions';
import { initialState, teamsReducer } from './reducers';
import { initialTeamsState, teamsReducer } from './reducers';
describe('teams reducer', () => {
it('should set teams', () => {
......@@ -21,7 +21,7 @@ describe('teams reducer', () => {
payload,
};
const result = teamsReducer(initialState, action);
const result = teamsReducer(initialTeamsState, action);
expect(result.teams).toEqual(payload);
});
......@@ -34,7 +34,7 @@ describe('teams reducer', () => {
payload,
};
const result = teamsReducer(initialState, action);
const result = teamsReducer(initialTeamsState, action);
expect(result.searchQuery).toEqual('test');
});
......
import { TeamsState } from '../../../types';
import { Team, TeamsState, TeamState } from '../../../types';
import { Action, ActionTypes } from './actions';
export const initialState: TeamsState = { teams: [], searchQuery: '' };
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };
export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' };
export const teamsReducer = (state = initialState, action: Action): TeamsState => {
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
switch (action.type) {
case ActionTypes.LoadTeams:
return { ...state, teams: action.payload };
......@@ -14,6 +15,16 @@ export const teamsReducer = (state = initialState, action: Action): TeamsState =
return state;
};
export const teamReducer = (state = initialTeamState, action: Action): TeamState => {
switch (action.type) {
case ActionTypes.LoadTeam:
return { ...state, team: action.payload };
}
return state;
};
export default {
teams: teamsReducer,
team: teamReducer,
};
export const getSearchQuery = state => state.searchQuery;
export const getTeam = state => state.team;
export const getTeams = state => {
const regex = RegExp(state.searchQuery, 'i');
......
......@@ -96,7 +96,7 @@ export interface NavModelItem {
hideFromTabs?: boolean;
divider?: boolean;
children?: NavModelItem[];
breadcrumbs?: NavModelItem[];
breadcrumbs?: { title: string; url: string }[];
target?: string;
parentItem?: NavModelItem;
}
......@@ -122,6 +122,11 @@ export interface TeamsState {
searchQuery: string;
}
export interface TeamState {
team: Team;
searchQuery: string;
}
export interface StoreState {
navIndex: NavIndex;
location: LocationState;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment